UI 扩展
UI 扩展通过挂钩到 SillyTavern 的事件和 API 来扩展其功能。它们运行在浏览器上下文中,几乎可以不受限制地访问 DOM、JavaScript API 和 SillyTavern 上下文。扩展可以修改 UI、调用内部 API 并与聊天数据交互。本指南介绍如何创建你自己的扩展(需要具备 JavaScript 知识)。
只想安装扩展?
请前往:扩展。
若要扩展 Node.js 服务器的功能,请参阅服务器插件页面。
不会写 JavaScript?
扩展提交
想把你的扩展贡献到官方内容仓库?欢迎联系我们!
为确保所有扩展都安全且易用,我们有以下几项要求:
- 你的扩展必须是开源的,并使用自由许可证(参见 Choose a License)。如果不确定,AGPLv3 是个不错的选择。
- 扩展必须兼容 SillyTavern 的最新发布版本。如果核心代码发生变更,请准备好更新你的扩展。
- 扩展必须有完善的文档,包括一个 README 文件,其中应包含安装说明、使用示例和功能列表。
- 依赖服务器插件才能运行的扩展将不予接受。
示例
查看简单 SillyTavern 扩展的实际示例:
- https://github.com/city-unit/st-extension-example - 基础扩展模板。演示了清单创建、本地脚本导入、添加设置 UI 面板以及持久化扩展设置的使用。
- https://github.com/search?q=topic%3Aextension+org%3ASillyTavern&type=Repositories - GitHub 上所有官方 SillyTavern 扩展的列表。
打包
扩展也可以利用打包来与其它模块隔离,并使用 NPM 中的任意依赖项,包括 Vue、React 等 UI 框架。
- https://github.com/SillyTavern/Extension-WebpackTemplate - 使用 TypeScript 和 Webpack 的扩展模板仓库(不含 React)。
- https://github.com/SillyTavern/Extension-ReactTemplate - 使用 React 和 Webpack 的极简扩展模板仓库。
要从打包产物中使用相对导入,你可能需要创建一个导入包装器。下面是 Webpack 的一个示例:
/**
* Import a member from a module by URL, bypassing webpack.
* @param {string} url URL to import from
* @param {string} what Name of the member to import
* @param {any} defaultValue Fallback value
* @returns {Promise<any>} Imported member
*/
export async function importFromUrl(url, what, defaultValue = null) {
try {
const module = await import(/* webpackIgnore: true */ url);
if (!Object.hasOwn(module, what)) {
throw new Error(`No ${what} in module`);
}
return module[what];
} catch (error) {
console.error(`Failed to import ${what} from ${url}: ${error}`);
return defaultValue;
}
}
// Import a function from 'script.js' module
const generateRaw = await importFromUrl('/script.js', 'generateRaw');
manifest.json
每个扩展必须在 data/<user-handle>/extensions 中拥有一个文件夹,并包含一个 manifest.json 文件,其中包含扩展的元数据以及指向扩展入口点的 JS 脚本文件路径。
可下载的扩展在通过 HTTP 提供服务时会挂载到 /scripts/extensions/third-party 文件夹中,因此应基于此路径使用相对导入。为便于本地开发,建议将你的扩展仓库放在 /scripts/extensions/third-party 文件夹中(即“为所有用户安装”选项)。
{
"display_name": "The name of the extension",
"loading_order": 1,
"requires": [],
"optional": [],
"dependencies": [],
"js": "index.js",
"css": "style.css",
"author": "Your name",
"version": "1.0.0",
"homePage": "https://github.com/your/extension",
"auto_update": true,
"minimum_client_version": "1.0.0",
"i18n": {
"de-de": "i18n/de-de.json"
},
"hooks": {
"install": "onInstall",
"update": "onUpdate",
"delete": "onDelete",
"enable": "onEnable",
"disable": "onDisable",
"activate": "onActivate"
}
}
清单字段
display_name是必填项,显示在“管理扩展”菜单中。loading_order是可选项,数值越大加载越晚。js是主 JS 文件引用,为必填项。css是可选的样式文件引用。author是必填项,应包含作者的名称或联系方式。auto_update设为true时,扩展会在 ST 包版本变更时自动更新。i18n是一个可选对象,用于指定支持的区域设置及其对应的 JSON 文件(见下文)。dependencies是一个可选的字符串数组,指定本扩展所依赖的其它扩展。generate_interceptor是一个可选字符串,用于指定在文本生成请求时调用的全局函数名称。minimum_client_version是一个可选字符串,用于指定本扩展正常工作所需的最低 SillyTavern 版本。hooks是一个可选对象,用于指定从 JS 入口点模块导出的生命周期钩子 函数名称。
依赖项
扩展还可以依赖于其它 SillyTavern 扩展。如果这些依赖缺失或被禁用,扩展将不会加载。
依赖项通过其在 public/extensions 目录中显示的文件夹名来指定。
示例:
- 内置扩展:
"vectors"、"caption" - 第三方扩展:
"third-party/Extension-WebLLM"、"third-party/Extension-Mermaid"
已弃用字段
requires是一个可选的字符串数组,用于指定所需的 Extras 模块。如果所连接的 Extras API 未提供所有列出的模块,扩展将不会被加载。optional是一个可选的字符串数组,用于指定可选的 Extras 模块。即使这些模块缺失,扩展仍会加载,扩展应优雅地处理它们缺失的情况。
要检查当前连接的 Extras API 提供了哪些模块,请从 scripts/extensions.js 导入 modules 数组。
脚本编写
扩展初始化的最佳实践
- 对于需要在 SillyTavern 加载阶段(阻塞式加载器处于活动状态时)运行的同步设置,请使用
activate钩子。 - 对于应在所有扩展和 UI 元素加载并设置完成、但加载器仍在阻塞时运行的设置,请使用
APP_INITIALIZED事件。 - 对于不需要阻塞 SillyTavern 进入可用状态的异步设置,请使用
APP_READY事件。它应使用定时器或类似机制来推迟处理,因为事件处理函数会被 await。
使用 getContext
SillyTavern 全局对象中的 getContext() 函数可让你访问 SillyTavern 上下文,它是所有主要应用状态对象、实用函数和工具的集合。
const context = SillyTavern.getContext();
context.chat; // Chat log - MUTABLE
context.characters; // Character list
context.characterId; // Index of the current character
context.groups; // Group list
context.groupId; // ID of the current group
// And many more...
你可以在 SillyTavern 源代码中找到完整的可用属性和函数列表。
如果在 getContext 中缺少你需要的函数/属性,请联系开发者或向我们提交一个 Pull Request!
共享库
SillyTavern 前端内部使用的大多数 npm 库都共享在 SillyTavern 全局对象的 libs 属性中。
lodash- 实用工具库。文档。Fuse- 模糊搜索库。文档。DOMPurify- HTML 净化库。文档。hljs- 语法高亮库。文档。localforage- 浏览器存储库(IndexedDB/localStorage 抽象)。文档。Handlebars- 模板库。文档。css- CSS 解析/字符串化工具。文档。Bowser- 浏览器/平台检测库。文档。DiffMatchPatch- 文本差异、匹配与补丁库。文档。Readability/isProbablyReaderable- Mozilla 的文章提取库。文档。SVGInject- 内联 SVG 注入库。文档。showdown- Markdown 转换库。文档。moment- 日期/时间操作库。文档。seedrandom- 带种子的随机数生成器。文档。Popper- 工具提示/弹出层定位引擎。文档。droll- 掷骰子库。文档。morphdom- 快速 DOM 差异比对/补丁库。文档。slideToggle- 原生 JS 滑动切换动画。文档。chalk- 终端字符串样式库(在浏览器中用途有限)。文档。yaml- YAML 解析器与字符串化器。文档。chevrotain- 解析器构建工具包。文档。gzipSync/gzip- 来自 fflate 的快速压缩工具。文档。
你可以在 SillyTavern 源代码中找到完整的导出库列表。
示例: 使用 DOMPurify 库。
TypeScript 说明
如果你想为 SillyTavern 全局对象中的所有方法(你大概会需要)获得自动补全,包括 getContext() 和 libs,你应当添加一个 TypeScript .d.ts 模块声明。该声明应从 SillyTavern 的源代码导入全局类型,具体路径取决于你的扩展所在位置。下面是一个同时适用于“所有用户”和“当前用户”两种安装方式的示例。
global.d.ts - 将此文件放在扩展目录的根目录中(与 manifest.json 同级):
export {};
// 1. Import for user-scoped extensions
import '../../../../public/global';
// 2. Import for server-scoped extensions
import '../../../../global';
// Define additional types if needed...
declare global {
// Add global type declarations here
}
HTML 模板
扩展可以使用 Handlebars HTML 模板来构建其 UI。将 .html 模板文件放在扩展目录中,并使用 getContext() 中的 renderExtensionTemplateAsync() 函数来渲染它们。
该函数接收扩展的文件夹名称、模板文件名(不含 .html)以及一个用于 Handlebars 模板变量的可选数据对象。返回的 HTML 会自动经过 DOMPurify 净化,并通过 data-i18n 属性进行本地化。
const { renderExtensionTemplateAsync } = SillyTavern.getContext();
// Renders 'third-party/my-extension/settings.html' with the given data
const settingsHtml = await renderExtensionTemplateAsync(
'third-party/my-extension',
'settings',
{ title: 'My Extension', version: '1.0', defaultValue: 'test' }
);
// Append to the extensions settings panel
$('#extensions_settings2').append(settingsHtml);
模板文件示例(settings.html):
<div class="my-extension-settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="{{title}}">{{title}}</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="my_ext_option">
<span data-i18n="Option">Option</span>
</label>
<input id="my_ext_option" type="text" value="{{defaultValue}}" />
</div>
</div>
</div>
renderExtensionTemplate()(同步版本)已弃用。请始终改用 renderExtensionTemplateAsync()。
从其它文件导入
从 SillyTavern 代码中导入是不可靠的,如果 ST 模块的内部结构发生变化,随时可能失效。getContext 提供了更稳定的 API。
除非你在构建打包扩展,否则可以从其它 JS 文件导入变量和函数。
例如,以下代码片段会在后台使用当前选定的 API 生成一条回复:
import { generateQuietPrompt } from "../../../../script.js";
async function handleMessage(data) {
const text = data.message;
const translated = await generateQuietPrompt({ quietPrompt: text });
// ...
}
状态管理
持久化设置
当扩展需要持久化其状态时,可以使用 getContext() 函数中的 extensionSettings 对象来存储和读取数据。扩展可以在设置对象中存储任何可 JSON 序列化的数据,并且必须使用唯一的键以避免与其它扩展冲突。
要持久化设置,请使用 saveSettingsDebounced() 函数,它会将设置保存到服务器。
const { extensionSettings, saveSettingsDebounced } = SillyTavern.getContext();
// Define a unique identifier for your extension
const MODULE_NAME = 'my_extension';
// Define default settings
const defaultSettings = Object.freeze({
enabled: false,
option1: 'default',
option2: 5
});
// Define a function to get or initialize settings
function getSettings() {
// Initialize settings if they don't exist
if (!extensionSettings[MODULE_NAME]) {
extensionSettings[MODULE_NAME] = structuredClone(defaultSettings);
}
// Ensure all default keys exist (helpful after updates)
for (const key of Object.keys(defaultSettings)) {
if (!Object.hasOwn(extensionSettings[MODULE_NAME], key)) {
extensionSettings[MODULE_NAME][key] = defaultSettings[key];
}
}
return extensionSettings[MODULE_NAME];
}
// Use the settings
const settings = getSettings();
settings.option1 = 'new value';
// Save the settings
saveSettingsDebounced();
聊天元数据
要将某些数据绑定到特定对话,可以使用 getContext() 函数中的 chatMetadata 对象。该对象允许你存储与对话关联的任意数据,可用于存储扩展特定的状态。
要持久化元数据,请使用 saveMetadata() 函数,它会将元数据保存到服务器。
不要将 chatMetadata 的引用保存到长生命周期的变量中,因为切换对话时该引用会发生变化。请始终使用 SillyTavern.getContext().chatMetadata 来访问当前对话的元数据。
const { chatMetadata, saveMetadata } = SillyTavern.getContext();
// Set some metadata for the current chat
chatMetadata['my_key'] = 'my_value';
// Get the metadata for the current chat
const value = chatMetadata['my_key'];
// Save the metadata to the server
await saveMetadata();
当对话切换时会触发 CHAT_CHANGED 事件,因此你可以监听该事件来相应地更新扩展状态。详见
角色卡
SillyTavern 完全支持角色卡 V2 规范,该规范允许在角色卡 JSON 数据中存储任意数据。
这对于需要存储与角色关联的额外数据,并在导出角色卡时使其可共享的扩展非常有用。
要向角色卡的 extensions 数据字段写入数据,请使用 getContext() 函数中的 writeExtensionField 函数。该函数接收一个角色 ID、一个字符串键以及一个要写入的值。该值必须是可 JSON 序列化的。
注意事项
尽管名为 characterId,但它并非“真正的”唯一标识符,而是该角色在 characters 数组中的索引。
当前角色的索引由上下文中的 characterId 属性提供。如果想向当前选定的角色写入数据,请使用 SillyTavern.getContext().characterId。如果需要为其它角色存储数据,请在 characters 数组中搜索该角色以找到其索引。
注意:在群聊中或未选择任何角色时,characterId 为 undefined!
const { writeExtensionField, characterId } = SillyTavern.getContext();
// Write some data to the character card
await writeExtensionField(characterId, 'my_extension_key', {
someData: 'value',
anotherData: 42
});
// Read the data back from the character card
const character = SillyTavern.getContext().characters[characterId];
// The data is stored in the `extensions` object of the character's data
const myData = character.data?.extensions?.my_extension_key;
设置预设
任意 JSON 数据可以存储在主 API 类型的设置预设中。它会随预设 JSON 一起导出和导入,因此你可以用它来存储预设的扩展特定设置。以下 API 类型支持设置预设中的数据扩展:
- 聊天补全
- 文本补全
- NovelAI
- KoboldAI / AI Horde
要读取或写入数据,首先需要从上下文获取 PresetManager 实例:
const { getPresetManager } = SillyTavern.getContext();
// Get the preset manager for the current API type
const pm = getPresetManager();
// Write data to the preset extension field:
// - path: the path to the field in the preset data
// - value: the value to write
// - name (optional): the name of the preset to write to, defaults to the currently selected preset
await pm.writePresetExtensionField({ path: 'hello', value: 'world' });
// Read data from the preset extension field:
// - path: the path to the field in the preset data
// - name (optional): the name of the preset to read from, defaults to the currently selected preset
const value = pm.readPresetExtensionField({ path: 'hello' });
当预设被更改或主 API 被切换时会触发 PRESET_CHANGED 和 MAIN_API_CHANGED 事件,因此你可以监听这些事件来相应地更新扩展状态。详见
国际化
有关提供翻译的一般信息,请参阅国际化页面。
扩展可以提供额外的本地化字符串,供 t、translate 函数以及 HTML 模板中的 data-i18n 属性使用。
请在此处查看支持的区域设置列表(lang 键):https://github.com/SillyTavern/SillyTavern/blob/release/public/locales/lang.json
直接调用 addLocaleData
将一个区域代码和一个包含翻译的对象传递给 addLocaleData 函数。不允许覆盖已有的键。如果传入的区域代码并非当前选定的区域,该数据将被静默忽略。
SillyTavern.getContext().addLocaleData('fr-fr', { 'Hello': 'Bonjour' });
SillyTavern.getContext().addLocaleData('de-de', { 'Hello': 'Hallo' });
通过扩展清单
在清单中添加一个 i18n 对象,其中包含支持的区域设置列表及其对应的 JSON 文件路径(相对于扩展目录)。
{
"display_name": "Foobar",
"js": "index.js",
// rest of the fields
"i18n": {
"fr-fr": "i18n/french.json",
"de-de": "i18n/german.json"
}
}
注册斜杠命令(新方式)
虽然 registerSlashCommand 仍为向后兼容而保留,但新的斜杠命令现在应通过 SlashCommandParser.addCommandObject() 注册,以便向解析器(进而向自动补全和命令帮助)提供关于命令及其参数的扩展详情。
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'repeat',
callback: (namedArgs, unnamedArgs) => {
return Array(namedArgs.times ?? 5)
.fill(unnamedArgs.toString())
.join(isTrueBoolean(namedArgs.space.toString()) ? ' ' : '')
;
},
aliases: ['example-command'],
returns: 'the repeated text',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'times',
description: 'number of times to repeat the text',
typeList: ARGUMENT_TYPE.NUMBER,
defaultValue: '5',
}),
SlashCommandNamedArgument.fromProps({ name: 'space',
description: 'whether to separate the texts with a space',
typeList: ARGUMENT_TYPE.BOOLEAN,
defaultValue: 'off',
enumList: ['on', 'off'],
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({ description: 'the text to repeat',
typeList: ARGUMENT_TYPE.STRING,
isRequired: true,
}),
],
helpString: `
<div>
Repeats the provided text a number of times.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/repeat foo</code></pre>
returns "foofoofoofoofoo"
</li>
<li>
<pre><code class="language-stscript">/repeat times=3 space=on bar</code></pre>
returns "bar bar bar"
</li>
</ul>
</div>
`,
}));
所有已注册的命令都可以在 STscript 中以任意方式使用。
事件
监听事件
使用 eventSource.on(eventType, eventHandler) 来监听事件:
const { eventSource, event_types } = SillyTavern.getContext();
eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage);
function handleIncomingMessage(data) {
// Handle message
}
主要的事件类型如下:
应用生命周期:
APP_INITIALIZED:应用已初始化,接近就绪,但加载器仍可见。可以在此处进行 UI 修改。当应用初始化完成后再附加新的监听器,该事件会自动触发。APP_READY:应用已完全加载并可用。当应用就绪后再附加新的监听器,该事件会自动触发。
消息:
MESSAGE_SENT:用户发送了消息并记录到chat对象中,但尚未在 UI 中渲染。MESSAGE_RECEIVED:LLM 消息已生成并记录到chat对象中,但尚未在 UI 中渲染。USER_MESSAGE_RENDERED:用户发送的消息已在 UI 中渲染。CHARACTER_MESSAGE_RENDERED:生成的 LLM 消息已在 UI 中渲染。MESSAGE_EDITED:一条消息已被用户编辑。MESSAGE_DELETED:一条消息已被删除。MESSAGE_SWIPED:一条消息的滑动(swipe)已被触发。STREAM_TOKEN_RECEIVED:在流式生成过程中收到了一个新的 token。
生成:
GENERATION_AFTER_COMMANDS:在处理完斜杠命令后,生成即将开始。GENERATION_STARTED:生成已开始。GENERATION_STOPPED:生成被用户停止。GENERATION_ENDED:生成已完成或出错。
对话:
CHAT_CHANGED:对话已切换(例如切换到另一个角色,或加载了另一个对话)。CHAT_CREATED:创建了一个新对话。CHAT_DELETED:一个对话已被删除。
角色:
CHARACTER_EDITED:角色的数据已变更。CHARACTER_DELETED:一个角色已被删除。CHARACTER_DUPLICATED:一个角色已被复制。
人格:
PERSONA_CHANGED:活动人格已变更。PERSONA_CREATED:创建了一个新人格。PERSONA_UPDATED:一个人格已更新。PERSONA_RENAMED:一个人格已被重命名。PERSONA_DELETED:一个人格已被删除。
设置与预设:
SETTINGS_UPDATED:应用设置已更新。PRESET_CHANGED:活动预设已变更。MAIN_API_CHANGED:主 API 类型已切换。CHATCOMPLETION_SOURCE_CHANGED:聊天补全来源已变更。CHATCOMPLETION_MODEL_CHANGED:聊天补全模型已变更。CONNECTION_PROFILE_LOADED:已加载一个连接配置。
世界书:
WORLDINFO_UPDATED:世界书数据已更新。WORLDINFO_SETTINGS_UPDATED:世界书设置已变更。
工具调用:
TOOL_CALLS_PERFORMED:工具调用已执行。TOOL_CALLS_RENDERED:工具调用结果已在聊天中渲染。
文本转语音:
TTS_JOB_STARTED:一个 TTS 任务已开始。TTS_AUDIO_READY:TTS 音频数据已准备好可播放。TTS_JOB_COMPLETE:一个 TTS 任务已完成。
完整的事件类型列表可在源代码中找到。
事件数据
每个事件向监听器传递数据的方式并不统一。有些事件不发出任何数据;有些会传递一个对象或原始值。请参阅发出事件的源代码以了解其传递的数据,或通过调试器进行检查。
发出事件
你可以通过调用 eventSource.emit(eventType, ...eventData) 从扩展中产生任何应用事件,包括自定义事件:
const { eventSource } = SillyTavern.getContext();
// Can be a built-in event_types field or any string.
const eventType = 'myCustomEvent';
// Use `await` to ensure all event handlers complete before continuing execution.
await eventSource.emit(eventType, { data: 'custom event data' });
提示词拦截器
提示词拦截器提供了一种方式,让扩展可以在发起文本生成请求之前执行任意活动,例如修改聊天数据、添加注入或中止生成。
来自不同扩展的拦截器会按顺序运行。顺序由各自 manifest.json 文件中的 loading_order 字段决定。loading_order 值较小的扩展运行得更早。如果未指定 loading_order,则以 display_name 作为回退。如果两者都未指定,则顺序不确定。
注册拦截器
要定义一个提示词拦截器,请在扩展的 manifest.json 文件中添加一个 generate_interceptor 字段。其值应为一个全局函数的名称,SillyTavern 将调用该函数。
{
"display_name": "My Interceptor Extension",
"loading_order": 10, // Affects execution order
"generate_interceptor": "myCustomInterceptorFunction",
// ... other manifest properties
}
拦截器函数
generate_interceptor 函数是一个全局函数,会在非试运行的生成请求时被调用。它必须定义在全局作用域中(例如 globalThis.myCustomInterceptorFunction = async function(...) { ... }),如果需要执行任何异步操作,则可以返回一个 Promise。
拦截器函数接收以下参数:
chat:表示将用于提示词构建的聊天历史的消息对象数组。你可以直接修改此数组(例如添加、删除或更改消息)。请注意,消息是可变的,因此你对数组所做的任何更改都会反映到实际的聊天历史中。如果你希望更改是临时的,请使用structuredClone创建消息对象的深拷贝。contextSize:一个数字,表示为即将到来的生成所计算的当前上下文大小(以 token 为单位)。abort:一个函数,调用时会发出信号阻止文本生成继续进行。它接受一个布尔参数,如果为true,则阻止任何后续拦截器运行。type:一个字符串,表示生成的类型或触发方式(例如'quiet'、'regenerate'、'impersonate'、'swipe'等)。这有助于拦截器根据生成的发起方式有条件地应用逻辑。
实现示例:
globalThis.myCustomInterceptorFunction = async function(chat, contextSize, abort, type) {
// Example: Add a system note before the last user message
const systemNote = {
is_user: false,
name: "System Note",
send_date: Date.now(),
mes: "This was added by my extension!"
};
// Insert before the last message
chat.splice(chat.length - 1, 0, systemNote);
}
生命周期钩子
扩展可以在 manifest.json 中定义生命周期钩子,这些钩子会在扩展生命周期的特定时刻被调用。每个钩子都映射到扩展 JS 入口点模块(即 js 字段指定的文件)中的一个导出函数。
所有钩子都是可选的。钩子函数可以返回一个会被 await 的 Promise(带有 5 秒超时)。如果钩子超过超时时间,会记录一条警告并继续执行。钩子中的错误会被捕获并记录,而不会阻塞操作。
可用钩子
清单配置
在你的 manifest.json 中添加一个 hooks 对象,将钩子名称映射到导出的函数名称:
{
"display_name": "My Extension",
"js": "index.js",
// Other fields here...
"hooks": {
"install": "onInstall",
"update": "onUpdate",
"delete": "onDelete",
"enable": "onEnable",
"disable": "onDisable",
"activate": "onActivate",
"clean": "onClean"
}
}
名称可以自由选择,只要是合法的 JS 函数名即可。 你可以配置任意数量的钩子,无需全部填写和实现。
实现
从你的主 JS 入口点导出钩子函数。每个函数不接收参数,并可选择返回一个 Promise:
// index.js - your extension's entry point
export async function onInstall() {
console.log('Extension installed! Performing first-time setup...');
// e.g., initialize default data, create storage entries
}
export async function onActivate() {
console.log('Extension activated during page load');
}
export async function onUpdate() {
console.log('Extension updated! Running migrations...');
// e.g., migrate data from old format to new format
}
export async function onDelete() {
console.log('Extension about to be deleted. Cleaning up...');
// e.g., remove stored data, clean up localStorage
const { localforage } = SillyTavern.libs;
await localforage.removeItem('my_extension_data');
}
export function onEnable() {
console.log('Extension enabled');
}
export function onDisable() {
console.log('Extension disabled');
}
export async function onClean() {
console.log('Extension data cleaned');
// e.g., cleanup of the extension's data here
}
钩子函数有 5 秒超时。如果你的钩子耗时更长,执行将继续并记录一条警告。请保持钩子逻辑快速且轻量。
生成文本
SillyTavern 提供了多个函数,可使用当前选定的 LLM API 在不同上下文中生成文本。这些函数允许你在对话上下文中、在没有任何上下文的原始生成中、或使用结构化输出来生成文本。
在对话上下文中
generateQuietPrompt() 函数用于在带有附加“静默”提示词(历史后指令)的对话上下文中于后台生成文本(输出不会在 UI 中渲染)。这对于在不打断用户体验、同时保持相关聊天和角色数据不变的情况下生成文本非常有用,例如生成摘要或图像提示词。
const { generateQuietPrompt } = SillyTavern.getContext();
const quietPrompt = 'Generate a summary of the chat history.';
const result = await generateQuietPrompt({
quietPrompt,
});
原始生成
generateRaw() 函数用于在没有任何对话上下文的情况下生成文本。当你想完全控制提示词构建过程时,它非常有用。
它接受一个作为文本补全字符串或聊天补全对象数组的 prompt,并根据选定的 API 类型以适当的格式构造请求,例如在聊天/文本模式之间转换、应用指令格式化等。你还可以向该函数传递一个额外的 systemPrompt 和一个 prefill,以进一步控制生成过程。
const { generateRaw } = SillyTavern.getContext();
const systemPrompt = 'You are a helpful assistant.';
const prompt = 'Generate a story about a brave knight.';
const prefill = 'Once upon a time,';
/*
In Chat Completion mode, will produce a prompt like this:
[
{role: 'system', content: 'You are a helpful assistant.'},
{role: 'user', content: 'Generate a story about a brave knight.'},
{role: 'assistant', content: 'Once upon a time,'}
]
*/
/*
In Text Completion mode (no instruct), will produce a prompt like this:
"You are a helpful assistant.\nGenerate a story about a brave knight.\nOnce upon a time,"
*/
const result = await generateRaw({
systemPrompt,
prompt,
prefill,
});
结构化输出
目前仅聊天补全 API 支持。可用性因所选来源和模型而异。如果所选模型不支持结构化输出,生成要么会失败,要么会返回一个空对象('{}')。请查阅你所使用的特定 API 的文档,以了解是否支持结构化输出。
你可以使用结构化输出功能来确保模型生成一个符合所提供的 JSON Schema 的有效 JSON 对象。这对于需要结构化数据的扩展非常有用,例如状态跟踪、数据分类等。
要使用结构化输出,你必须向 generateRaw() 或 generateQuietPrompt() 传递一个 JSON schema 对象。模型随后会生成一个符合该 schema 的响应,并以字符串化的 JSON 对象形式返回。
输出不会针对 schema 进行验证,你必须自行处理生成输出的解析与验证。如果模型未能生成有效的 JSON 对象,该函数将返回一个空对象('{}')。
Zod 是一个用于生成和验证 JSON schema 的流行库。这里不会介绍其用法。
const { generateRaw, generateQuietPrompt } = SillyTavern.getContext();
// Define a JSON schema for the expected output
const jsonSchema = {
// Required: a name for the schema
name: 'StoryStateModel',
// Optional: a description of the schema
description: 'A schema for a story state with location, plans, and memories.',
// Optional: the schema will be used in strict mode, meaning that only the fields defined in the schema will be allowed
strict: true,
// Required: a definition of the schema
value: {
'$schema': 'http://json-schema.org/draft-04/schema#',
'type': 'object',
'properties': {
'location': {
'type': 'string'
},
'plans': {
'type': 'string'
},
'memories': {
'type': 'string'
}
},
'required': [
'location',
'plans',
'memories'
],
},
};
const prompt = 'Generate a story state with location, plans, and memories. Output as a JSON object.';
const rawResult = await generateRaw({
prompt,
jsonSchema,
});
const quietResult = await generateQuietPrompt({
quietPrompt: prompt,
jsonSchema,
});
注册自定义宏
你可以注册自定义宏,这些宏可在任何支持宏替换的地方使用,例如角色卡字段、STscript 命令、提示词模板等。
新宏系统
注册宏的推荐方式是通过 SillyTavern.getContext() 提供的 macros.register() 函数。该系统支持参数、类别、描述和丰富的文档元数据。
const { macros } = SillyTavern.getContext();
// Simple macro with a handler function
macros.register('tomorrow', {
description: 'Returns tomorrow\'s date',
handler: () => {
return new Date(Date.now() + 24 * 60 * 60 * 1000).toLocaleDateString();
},
});
// Macro with unnamed arguments and a category
macros.register('greet', {
description: 'Generates a greeting for the given name',
category: macros.category.UTILITY,
unnamedArgs: [
{ name: 'name', description: 'The name to greet' },
],
handler: ({ unnamedArgs }) => {
const [name] = unnamedArgs;
return `Hello, ${name}!`;
},
});
handler 函数接收一个 MacroExecutionContext 对象,其中包含
args- 传递给宏的所有无名参数。unnamedArgs- 与所定义参数列表匹配的位置参数。list- 列表参数(在无名参数之后),如果未启用列表则为null。env- 宏环境,可访问角色数据、聊天状态等。resolve(text)- 用于解析文本中嵌套宏的函数(当delayArgResolution为true时)。
以及更多。
处理函数将同步运行,因此它们永远不能返回 Promise 或同步调用异步操作。
要注销一个宏:
const { macros } = SillyTavern.getContext();
macros.registry.unregisterMacro('greet');
你还可以为已有的宏注册别名:
const { macros } = SillyTavern.getContext();
macros.registerAlias('greet', 'hello', { visible: true });
旧版宏系统(已弃用)
getContext() 中的 registerMacro() 和 unregisterMacro() 已弃用。请改用 macros.register() 和 macros.registry.unregisterMacro()。
旧版 API 仍可用于向后兼容,但将在未来版本中移除:
const { registerMacro, unregisterMacro } = SillyTavern.getContext();
// Simple string macro
registerMacro('fizz', 'buzz');
// Function macro (must be synchronous)
registerMacro('tomorrow', () => {
return new Date(Date.now() + 24 * 60 * 60 * 1000).toLocaleDateString();
});
// Unregister
unregisterMacro('fizz');
消息格式化钩子
预发布特性
此功能目前仅在 SillyTavern 的 staging 分支上可用,尚未包含在最新发布版本中。
扩展可以挂入消息格式化流水线,在消息文本到达 DOM 之前对其进行转换。这对于添加标注(ruby 标签、工具提示)、高亮或自定义文本转换非常有用。
钩子同步运行,并且必须返回一个字符串。异步函数和非字符串返回值在注册时会抛出 TypeError,或在运行时被静默忽略并伴随一条控制台警告。不要在这些钩子中执行昂贵的操作——它们会在每条消息渲染时运行。
流水线阶段
钩子可以注册到三个流水线阶段。所有阶段都运行在 DOMPurify 净化之前,因此输出始终是安全的:
afterMarkdown 阶段是默认值,也是想要标注已渲染 HTML 的扩展最常用的插入点。
注册钩子
通过 getContext() 访问 messageFormatter:
const { messageFormatter } = SillyTavern.getContext();
// Simple hook - transforms message text
messageFormatter.addHook((mes, ctx) => {
// Skip user messages
if (ctx.isUser) return mes;
// Add furigana to Japanese text
return addFurigana(mes);
});
// Hook with explicit stage and order
messageFormatter.addHook((mes, ctx) => {
// Transform after Markdown conversion but before sanitization
return mes.replace(/\*\*(.+?)\*\*/g, '<mark>$1</mark>');
}, {
stage: messageFormatter.stage.AFTER_MARKDOWN,
order: messageFormatter.order.EARLY,
});
钩子上下文
钩子接收一个不可变的上下文对象,其中包含消息元数据:
该上下文对象通过 Object.freeze() 冻结——尝试修改它不会产生任何效果。
钩子排序
同一阶段内的钩子按升序运行。使用 order 选项来控制执行顺序:
const { hook_order } = messageFormatter;
// Predefined constants
hook_order.EARLIEST; // 0
hook_order.EARLY; // 10
hook_order.NORMAL; // 50 (default)
hook_order.LATE; // 90
hook_order.LATEST; // 100
// Custom numeric value
messageFormatter.addHook(myHook, { order: 25 });
数值越小越先运行。当多个扩展转换同一段文本时,这非常有用——例如,一个扩展可能较早地提取数据,而另一个扩展可能稍后对其进行格式化。
错误处理
钩子的执行被包裹在 try/catch 中。如果某个钩子抛出异常,它会被跳过并记录一条控制台错误——流水线会继续处理剩余的钩子。
如果某个钩子返回非字符串值(包括 undefined 或 Promise),会发出一条控制台警告并忽略该返回值。流水线将以未更改的先前文本继续。
完整流水线顺序
供参考,完整的消息格式化流水线如下:
- 提示词偏置剥离(仅消息 0)
- 注释/隐藏消息规范化
beforeRegex扩展钩子- 自定义正则规则(
getRegexedString) afterRegex扩展钩子- Markdown 自动修复(
fixMarkdown) - HTML 标签编码(
encode_tags) - Showdown Markdown → HTML 转换
afterMarkdown扩展钩子- 名称前缀剥离(
allow_name2_display) - DOMPurify 净化
所有扩展钩子(第 3、5、9 步)都运行在 DOMPurify 之前,因此它们的输出始终会被净化。
函数工具调用
扩展可以注册自定义函数工具,供 LLM 在聊天补全期间调用。这使你的扩展能够对来自模型的结构化数据做出反应——例如查询 API、执行计算或触发扩展功能。
有关包含前置条件、受支持的 API、注册字段和提示的完整指南,请参阅专门的函数调用页面。
快速示例:
const { registerFunctionTool } = SillyTavern.getContext();
registerFunctionTool({
name: 'get_weather',
displayName: 'Get Weather',
description: 'Get the current weather for a given location',
parameters: {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
location: { type: 'string', description: 'City name' },
},
required: ['location'],
},
action: async ({ location }) => {
const data = await fetchWeatherData(location);
return JSON.stringify(data);
},
});
动作加载器
动作加载器为长时间运行的操作提供了加载遮罩和 toast 通知系统。它取代了已弃用的 showLoader() / hideLoader() 函数。
通过 getContext() 中的 loader 访问它:
const { loader } = SillyTavern.getContext();
// Basic blocking loader with a stoppable toast
const handle = loader.show({ message: 'Processing data...' });
try {
const result = await someExpensiveOperation();
} finally {
await handle.hide();
}
选项
叠加加载器
可以同时激活多个加载器。只要至少有一个阻塞式加载器处于活动状态,遮罩就会保持可见:
const { loader } = SillyTavern.getContext();
const loader1 = loader.show({ message: 'Task 1...' });
const loader2 = loader.show({ message: 'Task 2...' });
await loader1.hide(); // Overlay stays — loader2 is still active
await loader2.hide(); // Now overlay hides
非阻塞加载器
对于不应阻塞 UI 的后台任务:
const { loader } = SillyTavern.getContext();
const handle = loader.show({
blocking: false,
message: 'Downloading in background...',
onStop: () => abortDownload(),
});
弹窗与用户反馈
弹窗辅助函数
SillyTavern 通过 getContext() 中的 Popup.show 提供便捷的弹窗辅助函数:
const { Popup } = SillyTavern.getContext();
// Confirmation dialog — returns POPUP_RESULT.AFFIRMATIVE or POPUP_RESULT.NEGATIVE
const confirmed = await Popup.show.confirm('Confirm Action', 'Are you sure you want to proceed?');
// Text input dialog — returns the entered string, or null if cancelled
const userInput = await Popup.show.input('Enter Name', 'Please provide a name:', 'default value');
// Information display — returns the clicked button result
await Popup.show.text('Info', 'Operation completed successfully.');
自定义弹窗
对于更复杂的弹窗,可直接实例化 Popup 并使用完整选项:
const { Popup, POPUP_TYPE, POPUP_RESULT } = SillyTavern.getContext();
const popup = new Popup(
'<div>Custom HTML content here</div>',
POPUP_TYPE.TEXT,
'',
{
wide: true, // Wide display mode
okButton: 'Save', // Custom OK button text
cancelButton: 'Discard', // Custom Cancel button text
customButtons: [
{
text: 'Export',
icon: 'fa-download',
result: POPUP_RESULT.CUSTOM1,
},
],
customInputs: [
{
id: 'my_checkbox',
label: 'Enable feature',
type: 'checkbox',
defaultState: false,
},
],
allowVerticalScrolling: true,
}
);
const result = await popup.show();
if (result === POPUP_RESULT.AFFIRMATIVE) {
// OK was clicked
} else if (result === POPUP_RESULT.CUSTOM1) {
// Export button was clicked
}
// Read custom input values
const checkboxValue = popup.inputResults?.get('my_checkbox');
弹窗类型
Toast 通知
对于轻量级反馈,请使用 toastr(全局可用):
toastr.success('Data saved successfully');
toastr.error('Failed to connect to API');
toastr.warning('This feature is experimental');
toastr.info('Processing...');
数据银行抓取器
扩展可以为数据银行功能注册自定义数据抓取器。抓取器提供了一种从自定义来源(例如网页、API、文件格式)导入数据的方式:
const { registerDataBankScraper } = SillyTavern.getContext();
await registerDataBankScraper({
id: 'my_scraper',
name: 'My Data Source',
description: 'Import data from My Data Source',
iconClass: 'fa-solid fa-database',
iconAvailable: true,
isAvailable: async () => true,
scrape: async () => {
// Return an array of File objects
const content = await fetchDataFromSource();
return [new File([content], 'data.txt', { type: 'text/plain' })];
},
});
调试函数
扩展可以注册自定义调试函数,它们会出现在调试菜单中(可通过高级用户设置访问)。这对于在开发期间暴露诊断工具、缓存/清理功能或手动触发器非常有用:
const { registerDebugFunction } = SillyTavern.getContext();
registerDebugFunction(
'my_ext_clear_cache', // Unique function ID
'Clear My Extension Cache', // Display name
'Clears all cached data for My Extension', // Description
async () => {
const { localforage } = SillyTavern.libs;
await localforage.removeItem('my_extension_cache');
toastr.success('Cache cleared');
}
);
发起 Extras 请求
Extras API 已弃用。不建议在新扩展中使用它。
doExtrasFetch() 函数允许你向 SillyTavern Extras API 服务器发起请求。
例如,要调用 /api/summarize 端点:
import { getApiUrl, doExtrasFetch } from "../../extensions.js";
const url = new URL(getApiUrl());
url.pathname = '/api/summarize';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({
// Request body
})
});
getApiUrl() 返回 Extras 服务器的基础 URL。
doExtrasFetch() 函数:
- 添加
Authorization和Bypass-Tunnel-Reminder头 - 处理获取结果
- 返回结果(响应对象)
这让你可以轻松地从扩展中调用 Extras API。
你可以指定:
- 请求方法:GET、POST 等
- 额外的请求头
- POST 请求的请求体
- 任何其它 fetch 选项
最佳实践
安全
切勿在 extensionSettings 中存储 API 密钥或机密
扩展设置对所有其它扩展可见,并以明文形式存储。不要在客户端存储敏感数据:
// BAD - Don't do this!
extensionSettings[MODULE_NAME].apiKey = 'secret_key_123';
// NOTE: There is no secure way to store secrets in client-side extensions.
// If you need to handle sensitive data, use server plugins instead.
// See: https://docs.sillytavern.app/for-contributors/server-plugins/
净化用户输入
在将用户输入的数据用于命令、API 调用或 DOM 操作之前,始终进行验证和净化:
// Validate input type first
if (typeof userInput !== 'string') {
toastr.error('Invalid input type');
return;
}
// Use DOMPurify to sanitize HTML input
const { DOMPurify } = SillyTavern.libs;
const cleanInput = DOMPurify.sanitize(userInput);
避免使用 eval() 或 Function() 构造函数
它们可以执行任意代码并带来安全风险。如果需要动态求值,请使用更安全的替代方案,或严格限制输入。
性能
不要在 extensionSettings 中存储大量数据
扩展设置会被加载到内存中并频繁保存。大量数据可能导致性能问题:
// BAD - Don't store large data
extensionSettings[MODULE_NAME].largeDataset = { /* megabytes of data */ };
// GOOD - Use localforage (abstraction over IndexedDB/localStorage)
const { localforage } = SillyTavern.libs;
await localforage.setItem(`${MODULE_NAME}_data`, largeData);
// Or use localStorage for smaller data
localStorage.setItem(`${MODULE_NAME}_data`, JSON.stringify(smallData));
清理事件监听器
当不再需要事件监听器时将其移除,以防止内存泄漏:
function cleanup() {
eventSource.removeListener(event_types.MESSAGE_RECEIVED, handleMessage);
document.getElementById('myElement').removeEventListener('click', handleClick);
}
不要阻塞 UI 线程
对于繁重的操作,请使用 async/await 或 Web Worker:
// Use async for I/O operations
async function processData() {
const result = await fetch('/api/process');
return result.json();
}
// Break up heavy computations
async function heavyComputation(data) {
for (let i = 0; i < data.length; i++) {
// Process chunk
if (i % 1000 === 0) {
await new Promise(resolve => setTimeout(resolve, 0)); // Yield to UI
}
}
}
兼容性
优先使用 getContext() 而非直接导入
上下文 API 更稳定,不太可能在 SillyTavern 更新时失效:
// GOOD - Stable API
const { chat, characters, saveSettingsDebounced } = SillyTavern.getContext();
// AVOID - May break with internal changes
import { chat, characters } from '../../../../script.js';
使用唯一的模块名称
使用具有描述性且唯一的模块名称,以避免与其它扩展冲突:
// GOOD - Specific and unique
const MODULE_NAME = 'my_extension_name';
// BAD - Too generic, likely to conflict
const MODULE_NAME = 'settings';
用户体验
提供清晰的反馈
使用 toastr 进行轻量级通知,使用 Popup 进行重要的用户交互。完整详情请参阅
对于长时间运行的操作,请使用
提供有用的控制台消息
为控制台日志使用一致的前缀。但不要在生产环境中用过多的日志刷屏:
const MODULE_NAME = 'MyExtension';
console.log(`[${MODULE_NAME}] Extension loaded`);
console.debug(`[${MODULE_NAME}] Processing data:`, data);
console.error(`[${MODULE_NAME}] Error occurred:`, error);
代码质量
使用 lib.js 中已打包的库
在添加新的依赖项之前,请先查看SillyTavern.libs 使用。
正确初始化设置
始终提供默认值并处理缺失的键:
function loadSettings() {
// Merge with defaults to handle new keys after updates and initialize if it doesn't exist.
extensionSettings[MODULE_NAME] = SillyTavern.libs.lodash.merge(
structuredClone(defaultSettings),
extensionSettings[MODULE_NAME]
);
}