NovelAI 脚本编写与 TypeScript 技能指南
本技能为开发 NovelAI 用户脚本 以及与 NovelAI 文本生成引擎交互提供全面的说明、API 参考、类型规则和架构指南。
1. 快速参考与文档文件
所有深入文档和类型定义均位于 references/ 目录中:
- TypeScript 指南 — 避免在 Web Worker 解释器中发生运行时崩溃的关键规则、类型模式、事件处理和类型转换。
- 脚本 API 完整文档 — 所有
api.v1命名空间(Storage、UI、Document、Hooks、Generation、Lorebook、TTS 等)的完整参考。 - 文本生成文档 — NovelAI 编辑器与网站功能(模型、上下文设置、采样器、Lorebook UI、ATTG、文字冒险)的背景信息。脚本代码很少需要;当脚本需要推理编辑器如何构建上下文或某个设置的含义时查阅。
- 脚本类型定义 (
.d.ts) — 官方 TypeScript 定义(来自https://novelai.net/scripting/types/script-types.d.ts)。这是属性名称和允许值的真实来源。 当它与文档冲突时,以类型为准。 - JSX 指南 + JSX 类型 — 基于 Preact 的
jsxUI 组件,用于自定义视觉元素(表格、SVG/Canvas 图表、实时小部件):全局变量(h、render、钩子、createStore)、元素/属性白名单、事件、Canvas 以及如何类型检查。仅可选:仅在用户明确要求时使用 JSX。 否则,即使对于复杂布局,也应使用标准组件构建 UI,最多可以提及 JSX 可能有所帮助。 - 实验性类型 —
api.v1.editor.block()和api.v1.document.transaction(),受用户experimentalScriptingApi设置控制(参见 API 文档第 18 节)。除非用户表示已启用,否则不要使用这些。 - UI 组件系统 — 如何构建可重用、可扩展的 UI:令牌、样式预设、类型化组件工厂、与存储绑定的字段、渲染/刷新策略、可重用对话框。将此作为任何带 UI 的脚本的默认结构。
- 脚本头部与配置示例 — 包含每种配置选项类型(字符串、多行字符串、数字、布尔值、选择器)的
/*--- ... ---*/脚本头部。 - 官方示例:https://github.com/NovelAI/novelai-script-examples。*脚本演示* 场景以及类似 查找与替换、笔记 和 智能摘要 的脚本展示了实际模式。
2. 核心架构与运行时环境
- 隔离的 Web Worker 执行:
- 脚本在 Web Worker 内部的隔离 JavaScript 解释器中运行。
- 脚本不能访问任意 DOM 元素或发起原始网络请求。
- 所有与 NovelAI 的交互均通过带版本的
api.v1异步接口进行。 - 全局
setTimeout/clearTimeout不可用。请使用await api.v1.timers.setTimeout(...)、api.v1.timers.clearTimeout(...)和api.v1.timers.sleep(ms)。
- 脚本是暂时的:
- 当故事切换、用户脚本模态框关闭或脚本被禁用时,脚本可能会被重新加载或卸载。
- 切勿将持久状态存储在全局内存变量中。 始终使用
api.v1.storage持久化状态。
- 静默的 Promise 拒绝:
- Worker 中未处理的拒绝 Promise 不会在控制台或 UI 中打印。
- 始终使用
try/catch包装异步调用,或附加.catch((err) => ...)处理程序。
- 紧急恢复:
- 如果脚本导致不稳定,请在 NovelAI URL 后追加
?noscript=true以在加载时禁用脚本执行。
- 如果脚本导致不稳定,请在 NovelAI URL 后追加
3. NovelAI 脚本的关键 TypeScript 指南
Web Worker 解释器需要干净、严格注释的 TypeScript 以避免编译/运行时错误:
A. 显式参数注解
始终为标准函数和箭头回调中的所有参数指定类型:
// ❌ 错误
function processEntry(id, data) { ... }
items.filter(item => item.active);
// ✅ 正确
function processEntry(id: string, data: any): void { ... }
items.filter((item: any) => item.active);
B. 对象迭代转换
Object.entries() 和 Object.values() 必须显式转换:
// ✅ 正确
for (const [key, value] of Object.entries(myObj) as [string, any][]) { ... }
for (const val of Object.values(myObj) as any[]) { ... }
C. 数组 shift 的 undefined 检查
Array.prototype.shift() 返回 T | undefined:
// ✅ 正确
const item = queue.shift();
if (!item) return;
D. 仅在异步上下文中使用 Await
调用异步 API 方法的 UI 构建器和辅助函数必须是 async:
// ✅ 正确
async function loadUserData(): Promise<void> {
const hasData = await api.v1.storage.has('userData');
if (hasData) {
const data = await api.v1.storage.get('userData');
}
}
4. 关键 API 命名空间 (api.v1)
| 命名空间 / 方法 | 用途 |
|---|---|
api.v1.storage | 持久化键值存储:get、getOrDefault、set、setIfAbsent、has、remove、list(也有 storyStorage、historyStorage、tempStorage)。 |
api.v1.hooks | 注册事件生命周期钩子(onGenerationRequested、onBeforeContextBuild、onContextBuilt、onResponse、onGenerationEnd、onScriptsLoaded、onLorebookEntrySelected、onTextAdventureInput、onHistoryNavigated、onDocumentConvertedToText)。每个脚本每个钩子只能注册一个回调;再次注册将替换之前的。 |
api.v1.ui | 创建 UI 扩展(scriptPanel、sidebarPanel、lorebookPanel、toolbarButton、contextMenuButton、toolboxOption)、模态框、窗口、提示;用 updateParts() 通过 id 更改单个组件。 |
api.v1.document | 按段落 ID 读写故事文档:scan()、sectionIds()、textFromSelection()、append()、appendParagraph(s)()、insertParagraph(s)After()、updateParagraph(s)()、removeParagraph(s)(),以及 document.history。 |
api.v1.editor | generate()、isBlocked()、selection.get() 和 editor.decorations(高亮、标记、小部件)。 |
api.v1.memory / systemPrompt / prefill | 获取或设置故事的 Memory、System Prompt 和 Prefill(设置需要 storyEdit)。 |
api.v1.generate() | 使用自定义参数和上下文直接执行文本生成。 |
api.v1.generateWithStory() | 使用当前故事上下文和设置运行生成。 |
api.v1.lorebook | 查询、更新、创建或删除 Lorebook 条目和类别。 |
api.v1.an | 获取或设置作者注释文本。 |
api.v1.buildContext() | 在上下文发送到 AI 之前访问和检查上下文组装。 |
api.v1.config | await api.v1.config.get('name') 读取脚本头部中声明的用户可配置值(见下文)。 |
api.v1.permissions | has() / request() / list()。在使用受限制的 API 之前请求 documentEdit、storyEdit、lorebookEdit、editorDecorations、clipboardWrite、fileInput、fileDownload(以及 presetEdit)。 |
脚本头部与配置
每个 .naiscript 都以 /*--- ... ---*/ 注释中的 YAML 头部开头(compatibilityVersion: naiscript-1.0、id、name、version、author、description、memoryLimit、可选的 config)。config 条目(name、prettyName、type、default、description)会显示在用户脚本模态框的“配置”选项卡中,因此用户无需编辑代码即可更改它们。类型包括 string(可选 multiline: true)、number(可选 min/max/decimal)、boolean 和 select(options: [{ label, value }];默认选择第一个选项)。参见 script_config_examples.ts。运行时读取值,并提供回退:
const openOnCreate: boolean = (await api.v1.config.get('open_on_create')) ?? true;
5. 标准脚本模板
创建新的 NovelAI 脚本时,请遵循以下结构:
(async () => {
try {
const STORAGE_KEY = "my_custom_script_data";
// 1. 初始化持久状态
const state = await api.v1.storage.getOrDefault(STORAGE_KEY, { enabled: true, count: 0 });
await api.v1.storage.setIfAbsent(STORAGE_KEY, state);
// 2. 注册生命周期钩子
// onGenerationRequested 在构建上下文之前运行;它只能停止生成或准备状态。
api.v1.hooks.register('onGenerationRequested', async (params: any) => {
if (!state.enabled) return;
// 在此为后续钩子准备状态,或返回 { stopGeneration: true } 来阻止生成
});
// 要修改模型看到的内容,请使用 onBeforeContextBuild(memory、lorebook、AN...)
// 或 onContextBuilt(返回 { messages },包含编辑后的消息数组)。
api.v1.hooks.register('onContextBuilt', async (params: any) => {
if (!state.enabled || params.dryRun) return;
return { messages: params.messages };
});
// 3. 注册 UI — 始终使用 update(),而不是 register()
api.v1.hooks.register('onScriptsLoaded', async () => {
await api.v1.ui.update([{
type: 'sidebarPanel' as const,
id: 'my-script-panel',
name: '自定义工具',
content: [
{ type: 'text' as const, text: `状态: ${state.enabled ? '已启用' : '已禁用'}` } as any
]
}]);
});
} catch (err: any) {
api.v1.error("脚本初始化错误:", err);
}
})();
6. 样式、主题令牌与工厂函数
NovelAI 脚本在隔离的 Web Worker 中运行,无法访问 DOM。切勿使用 document.createElement、document.head、cloneNode 或任何直接操作页面的浏览器 API。所有 UI 均通过传递给 api.v1.ui.update() 的声明式 UI Part 对象 构建。
任何包含 UI 的脚本的默认架构:遵循 NovelAI_UI_Component_System.md。 它提供了一个可重用、可扩展的分层结构,可对照官方类型进行类型检查:
- 命名空间和令牌:
NS+id()、COLOR主题令牌映射、SPACE/RADIUS常量- 使用
sx()组合的样式预设- 基于
api.v1.ui.part.*构建的组件工厂,带有选项对象和变体- 保持
storageKey前缀和存储 API 同步的field()绑定- 一个
render(),使用updateParts/{{templates}}进行小的刷新- 可重用的
confirm()/notify()对话框下面的代码片段是相同概念的简化版本。如果用户要求 JSX,请参见 NovelAI_JSX_Guide.md。JSX 样式使用普通 CSS 和
var(--theme-*),而不是令牌名称。不要自行切换到 JSX。
样式对象预设
在脚本顶部定义带类型的样式对象,并将其展开到 UI 组件上:
const CARD_STYLE: Record<string, string> = {
backgroundColor: 'bg1', // 主题令牌
border: '1px solid bg2', // 主题令牌
borderRadius: '6px',
padding: '10px',
margin: '6px 0',
};
const DANGER_STYLE: Record<string, string> = {
...CARD_STYLE,
borderColor: 'warning',
};
// 在 UI Part 中通过展开应用:
{ type: 'container' as const, style: CARD_STYLE, content: [...] } as any
主题令牌
不要硬编码十六进制颜色,而是直接在 style 对象的值中使用 NovelAI 主题令牌字符串。在渲染时,每个令牌将被替换为用户活动主题的值作为字面内联值(例如 'bg1' 变为 rgb(...) 代码)。这些不是 CSS 变量:没有可以配合 var() 使用的 --theme-* 自定义属性,并且网站其他位置的自定义 CSS 也无法引用它们。要在脚本中获取原始主题值,请使用 api.v1.theme.get()。(例外:根据官方类型,JSX UI 组件在其自己的影子 DOM 中获取 --theme-* 自定义属性。)
| 令牌 | 作用 |
|---|---|
'bg0' | 最深/最外层背景 |
'bg1' | 默认面板背景 |
'bg2' | 凸起表面/边框/按钮背景 |
'bg3' | 高亮/选中表面 |
'textHeadings' | 高对比度标题 |
'textMain' | 默认正文文本 |
'textDisabled' | 禁用/低对比度文本 |
'textPlaceholder' | 输入占位符文本 |
'textHighlight' | 搜索/高亮颜色 |
'warning' | 警告/警报状态 |
'textPrompt'、'textUser'、'textAI'、'textEdit' | 故事文本颜色 |
'lowIntensityColor'、'midIntensityColor'、'highIntensityColor' | 低/中/高概率颜色(如令牌概率中使用) |
'default' | 正文字体家族(在 fontFamily 中使用) |
'headings' | 标题字体家族(在 fontFamily 中使用) |
'code' | 等宽代码字体家族 |
'field' | 表单/输入字段字体家族 |
⚠️ 关于
'primary'的说明:NovelAI 没有'primary'或'error'主题令牌。使用color: 'primary'会产生浏览器会忽略的无效 CSS。对于活动/高亮表面,请使用'bg3';对于高对比度强调,请使用'textHeadings',或提供明确的十六进制/ rgba 颜色字符串(例如'#90caf9')。
工厂函数(替代 cloneNode / 模板)
在脚本顶部编写返回预先设置样式和配置的 UI Part 对象的工厂函数。这可以避免在整个脚本中重复内联样式对象。
// 可重复使用的带样式按钮工厂
function makeButton(
text: string,
onClick: () => Promise<void>,
options?: { disabled?: boolean; variant?: 'default' | 'danger' }
): any {
const baseStyle: Record<string, string> = {
backgroundColor: options?.variant === 'danger' ? 'warning' : 'bg2',
color: options?.variant === 'danger' ? 'textHeadings' : 'textMain',
border: '1px solid bg2',
borderRadius: '4px',
padding: '6px 12px',
cursor: options?.disabled ? 'not-allowed' : 'pointer',
};
return {
type: 'button' as const,
style: baseStyle,
label: text,
onClick: async () => {
if (!options?.disabled) await onClick();
},
disabled: options?.disabled ?? false,
};
}
// 用法:
const myButton = makeButton('点击我', async () => {
await api.v1.storage.set('clicked', true);
});
7. 错误处理与调试
- 静默的 Promise 拒绝:始终捕获 Promise 拒绝。
- 日志记录:使用
api.v1.log(msg)进行常规日志记录,api.v1.warn(msg)用于警告,api.v1.error(msg, error)用于错误。这些会出现在浏览器的开发者工具控制台中。 - 脚本恢复:如果脚本导致问题,使用
?noscript=trueURL 参数禁用脚本。
8. 补充说明
- 始终优先使用
api.v1.storage进行持久化,而不是全局变量。 - 当两个文档冲突时,
.d.ts类型定义是真实来源。 - 除非用户明确要求,否则不要使用 JSX。
- 在调用受限制的 API 之前请求必要的权限。
- 有关 UI 组件、JSX 或实验性 API 的更多详细信息,请参阅
references/目录中的文档。