插件开发
自 v0.91.0 起
Tavo 插件是 zip 格式的 .tpg 包。
插件能做什么
插件可以把可复用的玩法和工具加到 Tavo 中:
- 添加自己的设置页,让用户调整开关、选项、文本、滑杆等配置。
- 在聊天输入框
+菜单、右侧边栏或最后一条稳定消息的 action bar 加入动作。 - 感知 chat/message 变化,并改写或取消输入框发送。
- 在聊天页挂载 HTML 片段,例如状态栏、悬浮面板或消息装饰。
- 通过 TavoJS API 操作输入框、消息、变量、生成等能力。
如果一个扩展需要跨角色、用户身份或聊天复用,就适合做成插件;如果行为只属于某一张角色卡,通常直接写在角色卡的 TavoJS 里更合适。
使用前提
插件需要在聊天中开启高级渲染才能生效。插件代码独立于角色卡和消息内容中的 JavaScript;聊天内容的 JavaScript 设置不会关闭已启用插件的代码。
包结构
先创建一个文件夹,再把文件夹内容压缩为 .tpg 文件。manifest.json 必须位于插件包根目录。如果插件声明了输入动作、右侧边栏动作或最后一条消息动作,entry 指向的文件也必须存在于插件包内。
my-plugin/
├── manifest.json
├── entry.js
├── locales/
│ ├── en.json
│ └── zh-CN.json
├── ui/
│ └── panel.html
└── cover.pngcd my-plugin
zip -r ../my-plugin.tpg manifest.json entry.js locales ui cover.pngTavo 会拒绝缺失 manifest、路径不安全、specVersion 不支持或入口脚本文件不存在的插件包。如果插件使用的 specVersion 高于已安装 Tavo 支持的版本,应升级 Tavo,而不是把 manifest 改成字段语义可能不同的旧版本。插件路径是插件包内的虚拟相对路径,所有平台都必须使用正斜杠 /,包括 Windows。不要把 OS 路径分隔符或原始 path.join() 输出写进 manifest 路径;不要在包里放绝对路径、Windows 反斜杠路径,也不要依赖 ../ 路径跳出插件目录。
选择插件入口
先按用户要完成的动作选择插件入口,再写 manifest.json。一个插件可以同时声明多个入口;常见做法是用 settings.schema 保存配置,再用一个或多个入口触发或展示能力。
| 入口 | 适合场景 | 注意 |
|---|---|---|
contributes.inputActions | 用户正在编辑输入框、需要主动点一下完成的即时动作:生成回答草稿、改写当前输入、插入模板、追加提示词、发送前辅助。 | 菜单文案单行显示;handler 当前不接收参数;没有当前消息上下文,tavo.message.current() 返回 null。 |
contributes.sidebar | 聊天级、低频但明确的工具动作:总结聊天、导出或保存、批处理消息、刷新插件状态、打开一次性管理流程。 | 这是右侧边栏里的原生动作,不是内嵌面板;第一版不支持图标、描述、动态状态或聊天类型过滤。 |
contributes.lastMessageActions | 针对当前聊天绝对最后一条稳定消息的快捷动作,例如检查回复、保存要点或运行后处理。 | 只在高级渲染的消息 action bar 中显示。role 默认 character,也可为 character、user 或 any。点击时 handler 收到 { chatId, messageId, role },tavo.message.current() 返回 null。action bar overflow 不是消息 context menu。 |
contributes.htmlFragments + /chat | 聊天页级常驻 UI:状态栏、悬浮面板、全局控制条、样式注入、读取当前聊天或输入框后展示的页面级信息。 | 显示在聊天页面中,没有当前消息上下文。保持轻量,避免遮挡聊天 UI。 |
contributes.htmlFragments + /messages | 单条消息附近的 UI:消息装饰、状态标签、每条消息按钮、只挂最后一条角色消息的状态块。 | 每条匹配消息都会渲染一次,可用 role / position 过滤;拥有 tavo.message.current()。状态请使用 chat 作用域,可保存紧凑的当前状态,或以稳定消息 id 为 key 的小型映射。同时也需要控制 DOM 和脚本开销。 |
避免插件变量值过大
插件变量可保存任意受支持的可序列化值,但不宜存放过大的数据,尤其是图片、音频的 Base64 或 data URL,否则可能拖慢加载甚至导致内存溢出。大内容请使用 tavo.file.*,变量只保存虚拟路径。
manifest.json
manifest.json 描述插件身份、入口脚本、权限声明、设置表单、输入动作、右侧边栏动作、最后一条消息动作和 HTML 片段。
{
"specVersion": 2,
"id": "com.example.quick-note",
"name": { "$t": "plugin.name" },
"version": "1.0.0",
"entry": "entry.js",
"author": "Example Author",
"description": { "$t": "plugin.description" },
"releaseNotes": { "$t": "releaseNotes.1_0_0" },
"localization": {
"defaultLocale": "en",
"resources": {
"en": "locales/en.json",
"zh-CN": "locales/zh-CN.json"
}
},
"cover": "cover.png",
"permissions": ["input", "message", "tts"],
"contributes": {
"inputActions": [
{ "id": "insert-note", "label": { "$t": "actions.insertNote" } }
],
"sidebar": [
{ "id": "append-note", "label": { "$t": "actions.appendNote" } }
],
"lastMessageActions": [
{
"id": "inspect",
"label": { "$t": "actions.inspect" },
"icon": "icons/inspect.png",
"role": "character"
}
],
"htmlFragments": [
{ "id": "chat-note-panel", "src": "ui/panel.html", "mount": "/chat/body/end" }
],
"settings": {
"schema": [
{ "type": "info", "text": { "$t": "settings.info" }, "icon": "info" },
{ "key": "enabled", "type": "switch", "label": { "$t": "settings.enabled" }, "default": true },
{
"key": "mode",
"type": "select",
"label": { "$t": "settings.mode.label" },
"default": "short",
"options": [
{ "value": "short", "label": { "$t": "settings.mode.short" } },
{ "value": "detailed", "label": { "$t": "settings.mode.detailed" } }
]
},
{ "key": "strength", "type": "slider", "label": { "$t": "settings.strength" }, "min": 0, "max": 1, "step": 0.1, "default": 0.5 },
{ "type": "break" },
{ "key": "template", "type": "textarea", "label": { "$t": "settings.template.label" }, "default": { "$t": "settings.template.default" } }
]
}
}
}根字段
Manifest v2 与 minAppVersion:自 v0.93.0 起
| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 小写插件 id。可使用字母、数字、.、_、-,例如 com.author.my-plugin。Tavo 会将它归一为小写。 |
name | 是 | 显示名称。v2 可写字面字符串或 { "$t": "key" }。 |
version | 是 | v2 必须是合法 SemVer,如 1.0.0、1.0.0-beta.1;v1 仍兼容任意非空版本字符串。 |
specVersion | 新插件是 | 使用 2。缺省或显式 1 仍可本地安装,但没有插件包国际化 API。 |
entry | 条件必填 | 当插件声明 contributes.inputActions、contributes.sidebar 或 contributes.lastMessageActions 时必填。它指向插件入口脚本,通常是 entry.js。路径必须是相对路径,不能跳出插件包,并使用 / 分隔。旧版 scripts.actions manifest 仍会作为兼容别名生效;两者同时存在时优先使用 entry。 |
author | v2 是 | 作者名,会显示在插件详情中。必须是 1–32 个用户可见字符的单行文本,不可国际化。 |
description | v2 是 | 插件的详细介绍,会显示在列表和详情中;可国际化并支持 Markdown。 |
releaseNotes | 否 | 同级 version 对应的更新说明。v2 可国际化并支持 Markdown。每个插件包只描述当前版本,不在字段里重复版本号,也不嵌入完整历史。 |
localization | v2 是 | 必须有 defaultLocale;可选 resources 把 locale 映射到插件包内 JSON catalog。 |
cover | 否 | 插件封面图片的相对路径。 |
minAppVersion | 否 | v2 可选;声明时必须是合法 SemVer,安装前会与当前 App 版本比较。v1 不执行最低版本拦截。 |
permissions | 否 | 字符串数组,用于说明插件需要使用的能力,例如 input、message、generate、variable、file、network、tts。 |
contributes | 否 | 声明式插件贡献。 |
用户可见文本限制
Manifest v2 对字面声明和 $t 解析后的当前语言文本使用同一组限制。字符数按
用户感知字符(Unicode grapheme cluster)计算,而不是 UTF-16 code unit。
| 位置 | v2 限制 |
|---|---|
manifest name | 必填,1–64 个字符,单行 |
manifest author | 必填,1–32 个字符,单行 |
manifest description | 必填,1–10,000 个字符,且 UTF-8 不超过 64 KiB;允许多行 |
manifest releaseNotes | 可选,1–5,000 个字符,且 UTF-8 不超过 32 KiB;允许多行 |
input/sidebar/last-message action label | 1–48 个字符,单行 |
settings 字段和结构化 select option label | 1–80 个字符,单行 |
settings info.text | 1–500 个字符,允许多行纯文本 |
所有值都不能只包含空白,也不能包含不安全控制字符;单行字段不能包含换行。
Catalog 中某条翻译违反对应限制时,Tavo 会忽略该条翻译并继续沿 $t 回退。
description 应完整说明插件用途、使用方式、能力与注意事项。建议至少写
200 个字符,同时遵守硬上限。它支持并推荐使用 GitHub Flavored Markdown
的标题、列表、强调和链接;原始 HTML 不会执行。插件列表不新增 summary
字段,而是把当前语言的 description 转成纯文本、折叠空白并取前 120 个
字符显示。
releaseNotes 只说明同级 version 的本次变化。不要在字段中重复版本号,也
不要把完整发布历史复制到每个插件包。它可以使用 GitHub Flavored Markdown;
省略不会阻止校验、安装或运行。
发布新版时
准备发布新版本时,应检查 releaseNotes,尽量改写为本版的实际变化,避免
无意中原样沿用上一版说明。使用 $t 时,推荐使用与当前版本对应的 key,方便
维护各版本文案。releaseNotes 仍是可选字段,不会阻止校验、打包或发布。
版本与 App 兼容性
specVersion: 2 的 version 和可选 minAppVersion 使用 SemVer。版本必须
包含 major.minor.patch,例如 1.2.0;v1.2.0、1.2 和含前导零的版本
都无效。预发布版本按 SemVer 排序,例如 1.0.0-rc.1 < 1.0.0。
缺省 specVersion 和显式 specVersion: 1 保持兼容:version 与
minAppVersion 继续接受原有非空字符串,且不会执行最低 App 版本拦截。
国际化
自 v0.93.0 起
Manifest v2 使用同一套插件包内 catalog,同时服务 Tavo 原生插件 UI
和插件 HTML/JavaScript。localization.defaultLocale 必填;可选 resources
使用 en、en-US、zh-Hans、zh-CN 这样的连字符 locale。
zh_CN 这样的下划线标签无效。Catalog 路径必须遵守和 entry、
HTML fragment 相同的插件包内相对路径规则。
强烈建议新插件至少支持 en,再根据目标用户增加至少一种常用语言。如果
维护能力允许,也可以继续支持更多语言。额外语言应根据真实用户群选择,
不需要把某一种固定语言当成所有插件的第二语言。
{
"localization": {
"defaultLocale": "en",
"resources": {
"en": "locales/en.json",
"zh-CN": "locales/zh-CN.json"
}
}
}每个 catalog 都是扁平的 UTF-8 JSON object,key 必须是非空字符串,value 必须是字符串:
{
"plugin.name": "快速备注",
"plugin.description": "在聊天中添加快速备注面板。",
"releaseNotes.1_0_0": "## 首次发布\n\n- 新增快速备注面板。",
"actions.insertNote": "插入备注",
"actions.appendNote": "写入备注",
"actions.inspect": "检查",
"settings.info": "设置备注面板。",
"settings.enabled": "启用",
"settings.mode.label": "模式",
"settings.mode.short": "简短",
"settings.mode.detailed": "详细",
"settings.strength": "强度",
"settings.template.label": "模板",
"settings.template.default": "记住:",
"runtime.panel.title": "快速备注",
"runtime.greeting": "你好,{name}!",
"runtime.input.rememberPrefix": "记住:",
"runtime.input.sidebarNote": "来自侧栏插件的备注",
"runtime.input.draft": "草稿:\n{input}"
}需要始终原样显示的文案直接写普通字符串。只有严格符合
{ "$t": "key" } 的 object 才会查询国际化;以 $ 开头的字符串也仍是
字面文本。Key 可以是 plugin.name 这样的语义名,也可以是 快速备注
这样的原文。推荐使用语义 key,但不强制。Tavo 不自动翻译,也不要求
所有 catalog 拥有相同 key。整条回退链都没有找到 $t 时,会显示 key
本身。普通安装与运行时不会因此阻止插件;下文的 tavo_plugin_audit 会比较
catalog key 并给出非阻塞告警。
支持国际化的位置有:
- manifest
name、description和releaseNotes; - input action、sidebar action 和 last-message action 的
label; - settings 字段
label和 info 元素text; - 结构化 select option 的
label; text和textareasetting 的default。
每个 key 会先查找请求 locale 和兼容 locale,再查找 English,然后是插件
defaultLocale,最后显示 key 本身。App 跟随系统语言时,插件使用设备原始
locale,即使 Tavo 自身界面不支持该语言。
插件 HTML 与 JavaScript 国际化
tavo.plugin.i18n:自 v0.93.0 起
本地化 v2 插件的 entry、action handler、/chat fragment 和 /messages
fragment 中都会提供同步 tavo.plugin.i18n API:
const i18n = tavo.plugin.i18n;
function render() {
document.querySelector('#title').textContent =
i18n.t('runtime.panel.title');
const greeting = i18n.t('runtime.greeting', { name: 'Colin' });
document.querySelector('#greeting').textContent = greeting;
}
render();
const unsubscribe = i18n.onChange((event) => {
console.log(event.requestedLocale, event.locale);
render();
});requestedLocale、locale、defaultLocale都是 live getter。supportedLocales是防修改的数组拷贝。t(key, params?)同步返回字符串;缺 key 时原样返回 key,并支持{name}这样的简单占位符,参数可为字符串、有限数字和布尔值。例如 catalog 中的你好,{name}!配合{ name: 'Colin' }会返回你好,Colin!。onChange(handler)返回 unsubscribe function。handler 运行前,getter 和t()已经使用新语言。
切换语言不会重新执行插件 entry。Tavo 也不会自动翻译或修改已有
插件 DOM;请在 onChange handler 中自行 rerender。该 namespace 只属于插件,
普通角色卡或消息 TavoJS 无法使用。
国际化应作为插件完成标准,而不只是 manifest 配置。所有用户可见的
manifest、settings、HTML 和 JavaScript 文案都应进入 catalog,并按所在位置
通过 { "$t": "key" } 或 tavo.plugin.i18n.t() 读取。这包括 HTML 文本节点、
按钮、placeholder、title、aria-label、加载和空状态、错误、确认提示及
Toast 文案。品牌名、协议 token、标识符等明确不随语言变化的内容可以保留
为字面值。每个国际化 HTML fragment 都应先按当前语言完整渲染一次,再订阅
tavo.plugin.i18n.onChange(),确保切换语言时完整重渲染用户可见内容。
把插件国际化拆成三个必须分别检查的表面:
- 宿主原生 UI:manifest、动作标签和 settings 使用
$t。 - 插件运行时 UI:HTML、Toast、错误和动态文案使用
tavo.plugin.i18n.t(),HTML 订阅onChange()后完整重渲染。 - 插件生成或插入的可见内容:明确选择跟随当前对话语言、跟随 App / 插件 locale,或使用一个稳定的用户设置。隐藏提示词本身可以保持作者使用的语言, 但必须明确约束用户最终看到的输出语言。
不要用 navigator.language、私有 localStorage 或插件自己的一套语言开关
替代 Tavo 提供的 locale。它们会让插件 UI 与 App 语言产生漂移。
Entry 脚本
自 v0.92.0 起
entry 是插件的主脚本,用于注册已声明的输入 / 侧边栏 / 最后一条消息动作 handler 和插件 Hooks。旧版 scripts.actions 仍会作为兼容别名生效。
Action handler 使用 TavoJS API 与 Tavo 交互。下面只展示插件事件注册,更多输入框、消息、变量和生成接口请参考完整 API 文档。
tavo.plugin.onInputAction('insert-note', async () => {
await tavo.input.append(tavo.plugin.i18n.t('runtime.input.rememberPrefix'));
});
tavo.plugin.onSidebarAction('append-note', async () => {
const note = tavo.plugin.i18n.t('runtime.input.sidebarNote');
await tavo.input.append(`\n\n${note}`);
});
tavo.plugin.onLastMessageAction(
'inspect',
async ({ chatId, messageId, role }) => {
const message = await tavo.message.get(messageId);
await tavo.utils.toast(`${role}:${chatId}:${message.id}`);
},
);如果插件只提供设置、HTML 片段或其它声明式贡献,可以省略 entry,除非它还需要执行 JavaScript。
使用 tavo
在 entry、/chat HTML 片段和 /messages HTML 片段中直接使用 tavo,例如 await tavo.input.get()。Tavo 会自动让它对应当前插件。不要使用 window.tavo 或 globalThis.tavo,它们不是插件 API 的一部分。
/messages HTML 片段可以通过 tavo.message.current() 读取当前消息;在 entry、输入框动作、侧边栏动作、最后一条消息 action handler 和 /chat HTML 片段中,该方法返回 null。
// 推荐:在所有插件入口中都使用未限定的 tavo。
tavo.plugin.onInputAction('guide', async () => {
const input = await tavo.input.get();
await tavo.input.set(tavo.plugin.i18n.t('runtime.input.draft', { input }));
});
// 不建议:插件代码不要使用 window/globalThis。
// const input = await window.tavo.input.get();
// const cfg = globalThis.tavo.plugin.config.get('basePrompt');读取插件设置
contributes.settings.schema 声明设置页的表单结构和默认值;插件代码通过 tavo.plugin.config 读取该插件的有效配置值。这两个方法是同步、只读 API,不需要 await:
const enabled = tavo.plugin.config.get('enabled');
const config = tavo.plugin.config.all();get(key)返回用户已保存的值;没有用户值时回退到 schema 中该字段的default。key 不存在且没有默认值时返回null。all()返回该插件全部有效配置值的浅拷贝,包括 schema 默认值和用户覆盖值。修改返回对象不会保存或改变插件设置。tavo.plugin.config可在entry、输入框 / 侧边栏 / 最后一条消息 action handler、/chat和/messagesHTML 片段中使用,并且只会读取当前插件的配置。- 该 API 不返回原始
contributes.settings.schema定义,也不提供写入方法。schema 仍由插件自己的manifest.json声明,用户通过 Tavo 的插件设置页修改配置。
动作图标资源
contributes.inputActions[].icon 和 contributes.lastMessageActions[].icon 都使用插件包内的 PNG 或 WebP 相对路径。推荐使用透明背景的正方形图标,尺寸为 64 × 64 或 128 × 128。建议不要超过 512 × 512 或约 256 KB。
Tavo 会将图片等比例缩放到对应的菜单或消息 action bar 图标槽位,源图片尺寸不会改变菜单或 action bar 的布局。这些数值是性能建议,不是安装时的硬性限制;过大的图片会增加资源加载和处理开销,影响运行性能。
输入动作
contributes.inputActions 用来声明聊天输入框 + 菜单里的动作。
{
"contributes": {
"inputActions": [
{ "id": "insert-note", "label": { "$t": "actions.insertNote" } }
]
}
}| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 稳定的动作 id。 |
label | 是 | 菜单里显示的文案。 |
icon | 否 | 可选的插件包内 PNG/WebP 相对路径。规格建议见动作图标资源。 |
输入动作的 handler 写在 entry 指向的文件里。推荐使用 onInputAction;它会注册底层事件 inputActions:<action-id>:
tavo.plugin.onInputAction('insert-note', async () => {
await tavo.input.append(tavo.plugin.i18n.t('runtime.input.rememberPrefix'));
});也可以使用底层事件形式:tavo.plugin.on('inputActions:<id>', handler)。新插件推荐优先使用 onInputAction(id, handler);它会校验 id,并注册到底层 inputActions:<id> 事件。
侧边栏动作
contributes.sidebar 用来声明聊天右侧边栏里的动作。
{
"contributes": {
"sidebar": [
{ "id": "append-note", "label": { "$t": "actions.appendNote" } }
]
}
}| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 稳定的动作 id。 |
label | 是 | 右侧边栏行里显示的文案。 |
侧边栏动作的 handler 写在 entry 指向的文件里。推荐使用 onSidebarAction;它会注册底层事件 sidebar:<action-id>:
tavo.plugin.onSidebarAction('append-note', async () => {
const note = tavo.plugin.i18n.t('runtime.input.sidebarNote');
await tavo.input.append(`\n\n${note}`);
});也可以使用底层事件形式:tavo.plugin.on('sidebar:<id>', handler)。新插件推荐优先使用 onSidebarAction(id, handler);它会校验 id,并注册到底层 sidebar:<id> 事件。
原生输入框、侧边栏和最后一条消息派发不区分 handler 是通过 helper 还是 plugin.on 注册的;它只查找最终事件名。使用 plugin.on 时,<id> 必须和对应 contribution 的 id 完全一致,包括大小写、连字符等字符。
同一个插件声明多个侧边栏动作时,Tavo 会把它们显示在同一个分组里,标题为 插件 · 插件名。
最后一条消息动作
contributes.lastMessageActions 为当前聊天的绝对最后一条稳定消息的 action bar 声明动作。流式、加载或临时消息不会成为目标,之后又追加消息时旧目标不再显示这些动作。此入口只在高级渲染时存在,基础渲染没有对应入口。
{
"contributes": {
"lastMessageActions": [
{
"id": "inspect",
"label": { "$t": "actions.inspect" },
"icon": "icons/inspect.png",
"role": "character"
}
]
}
}| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 稳定动作 id,必须与注册的 handler id 完全一致。 |
label | 是 | action bar 文案,遵守原生 action 单行限制。 |
icon | 是 | 必需的安全插件包内 PNG/WebP 图片相对路径。规格建议见动作图标资源。 |
role | 否 | 目标角色,默认 character,只允许 character、user 和 any。 |
在 entry 中用 tavo.plugin.onLastMessageAction(id, handler) 注册,也可用底层 tavo.plugin.on('lastMessageActions:<id>', handler)。handler 在点击时只收到 { chatId, messageId, role },绝不包含消息内容。tavo.message.current() 在这个 entry handler 中仍为 null,请用 tavo.message.get(messageId) 读取目标。
原生控件始终优先,插件动作随后按稳定插件和 manifest 顺序排列。宽度不足时,只有插件动作进入 action bar overflow,绝不挤走原生控件。这个 overflow 不是消息 context menu。等待 handler 时整个 bar 都会禁用,任何成功、失败、取消或异常退出后都会恢复。没有 handler 或 handler 抛错会报告插件动作失败。MCP 可以检查、校验和审计这些贡献,不能执行。
插件 Hooks
entry 脚本通过 tavo.plugin.on(type, handler) 注册 Hooks。
Chat 与 Message 通知
自 v0.92.0 起
这些 Hooks 用来感知聊天和消息变化,不能修改或阻止聊天与生成流程。单个 handler 报错不会影响聊天或其它插件。
| 事件 | 触发时机 |
|---|---|
chat:opened | 当前聊天打开时。 |
chat:closed | 离开当前聊天或切换到其它聊天时。 |
chat:updated | 当前 chat 的元数据更新,例如标题、角色、persona、preset、lorebooks、memory 或背景变化。 |
chat:changed | chat:updated 的兼容别名;handler 收到的 event.type 仍为 chat:updated。 |
message:added | 一条消息添加并保存到当前 chat 后;流式生成过程中不会重复触发。 |
message:updated | 当前 chat 中已保存消息的内容或元数据发生变化时。 |
message:deleted | 一条消息从当前 chat 中删除时。 |
message:changed | 在 message:added、message:updated 或 message:deleted 之后触发的 umbrella 事件。 |
具体的 message 事件会先触发,随后触发 message:changed。如果插件启用时用户已经打开聊天,也会收到一次 chat:opened。
所有事件对象都包含 type、pluginId 和 ISO 时间字符串 at。chat 事件还包含 chatId 和 chat;message 事件还包含 chatId、change 和 message。
tavo.plugin.on('chat:opened', async (event) => {
console.log('打开聊天', event.chat?.name || event.chatId);
});
tavo.plugin.on('message:changed', async (event) => {
console.log(event.type, event.change, event.message?.id, event.at);
});生成生命周期 Hooks
自 v0.92.0 起
这些 Hooks 只能在已安装插件的 entry 脚本中通过 tavo.plugin.on(...) 注册;HTML 片段、角色卡和消息内容中的 TavoJS 不能注册或接收这些事件。manifest 应声明 "permissions": ["generate"]。
tavo.plugin.on('generation:prepare', async (event) => {
event.text = '[Model-only context]\n' + event.text;
});
tavo.plugin.on('generation:success', async (event) => {
event.text = event.text.trim();
});
tavo.plugin.on('generation:error', async (event) => {
console.error(event.error.code, event.error.message);
});
tavo.plugin.on('generation:cancelled', async (event) => {
console.log('stopped', event.partial);
});每个事件都有只读的 generationId、chatId、source、at、type 和 pluginId。目前会响应 reply、groupReply、continuation、othersContinuation 和 regeneration;不会响应图片、语音、总结、独立生成或纯 TavoJS/JSAPI 发起的生成。
generation:prepare在用户消息已经保存并显示后、模型请求开始前运行。handler 可以是同步函数,也可以返回 Promise;Tavo 会在开始模型请求前等待 Promise。event.text是本次请求发送给模型的最后一条用户消息;修改只影响本次模型请求,不会修改聊天中已保存的消息,并且可以设为空。generation:success在生成和 extension 处理完成后、角色消息保存前运行;可改写的最终正文必须 非空,空改写会被丢弃。generation:error在生成失败时通知插件,event.error提供code和message。generation:cancelled是含布尔partial的非阻塞终止通知。partial: true仍会保存半截响应, 随后触发现有的message:added;partial: false不会保存消息。
generation:prepare 的 handler 按注册顺序运行,整条 pipeline 共享 55 秒预算,而不是每个 handler 各有 55 秒。handler 报错、超时或写入无效文本时,Tavo 会忽略该 handler 的修改并继续生成;预算耗尽时会跳过尚未开始的 handler。用户停止生成会立即结束宿主等待并忽略迟到结果,但不会强制取消插件已经发出的网络请求;需要取消自身 I/O 的插件应使用 AbortController。generation:success 仍是每个 handler 最多等待 5 秒。这两个 Hooks 不能通过 event.cancel() 取消生成。每次生成只会触发 generation:success、generation:error 或 generation:cancelled 中的一个。
发送阶段如何选择 Hook
| 需求 | 使用的 Hook |
|---|---|
| 快速校验、取消发送、修改聊天中实际保存的用户文本 | input:beforeSend |
| embedding、向量召回、外部 API、只对模型可见的上下文注入 | generation:prepare |
| 快速整理模型最终正文 | generation:success |
不要用 input:beforeSend → event.cancel() → 等待外部 API → tavo.input.send() 实现记忆召回。这种拦截-重发模式会让用户消息在计算完成前无法显示,并有递归发送或重复副作用风险。请迁移到 generation:prepare:
tavo.plugin.on('generation:prepare', async (event) => {
const original = event.text;
const recalled = await recallMemory(original); // 插件自己的召回实现
if (!recalled) return;
event.text = ['<memory>', recalled, '</memory>', original].join('\n');
});发送顺序是:input:beforeSend → 用户消息保存并上屏 → generation:prepare → 构建模型请求 → 开始生成。prepare 只能改写本次请求中的最后一条用户文本,不能新增独立 system/context 消息,也不会修改已经显示的用户消息。
输入发送 Hooks
自 v0.92.0 起
input:beforeSend 拦截发送按钮 / 回车、tavo.input.send() 和 MCP tavo_input_send;input:afterSend 在 Tavo 接受输入后通知插件。manifest 应声明 "permissions": ["input"]。
tavo.plugin.on('input:beforeSend', async (event) => {
event.text = event.text.trim();
if (!event.text.includes(':')) event.cancel('请补充角色名');
});
tavo.plugin.on('input:afterSend', async (event) => {
console.log('输入已接受:', event.text);
});before-send 在 macros 展开和 slash command 解析前运行。type、pluginId、chatId、source、at 只读;text 是唯一可修改字段且必须保持字符串。source 为 ui、tavojs 或 mcp。handler 返回值会被忽略;请调用 event.cancel(reason?) 显式取消。
handler 按插件和注册顺序运行,每个最多等待 5 秒。handler 报错、超时或把 text 改成非字符串时,Tavo 会忽略该 handler 的修改并继续发送。显式取消会停止后续 handler,并保留之前 handler 已完成的文本修改和附件。input:afterSend 不等待模型回复或生图完成。
HTML 片段
contributes.htmlFragments 用来声明显示在聊天页或消息附近的本地 HTML 文件。
HTML 片段中的脚本属于已安装插件,不受聊天内容的 JavaScript 设置影响;该设置只控制角色卡、模型输出和其它消息气泡内容中的脚本。
{
"contributes": {
"htmlFragments": [
{ "id": "chat-panel", "src": "ui/panel.html", "mount": "/chat/body/end" },
{ "id": "message-tail", "src": "ui/tail.html", "mount": "/messages/end?role=character&position=last" }
]
}
}| 字段 | 必填 | 说明 |
|---|---|---|
id | 是 | 稳定的片段 id。 |
src | 是 | 插件包内的相对文件路径。不能是绝对路径,不能包含 \,不能是 URL,也不能用 ../ 跳出目录。 |
mount | 是 | 挂载位置。当前支持 /chat/... 和 /messages...。 |
聊天页悬浮控件
/chat HTML 片段如果使用 position: fixed、position: absolute 或类似定位在聊天页上放置悬浮按钮,该按钮必须允许用户拖动,避免多个插件的控件相互重叠或遮挡 Tavo 的聊天界面。
- 同时支持鼠标和触摸拖动;有条件时也应支持触控笔。
- 区分点击和拖动,避免用户尝试移动按钮时误触原操作。
- 建议按稳定的插件 id 和片段 id 记住上一次拖动位置,并在窗口尺寸、屏幕方向或安全区变化后将恢复位置限制在当前可视区域内。
- 初始位置和恢复位置不得遮挡输入、发送、返回等宿主关键控件,并应在常见手机、平板和桌面尺寸下验证。
- 应提供可发现的重置位置方式,作为持久化数据无效或用户无法继续拖动时的恢复手段。
支持的聊天挂载点:
/chat/chat/head/start/chat/head/end/chat/body/start/chat/body/end
支持的消息挂载点:
/messages/start/messages/end/messages/start?role=user/messages/end?role=character/messages/end?position=last/messages/end?role=character&position=last
消息挂载点中,role 可为 user 或 character;position 可为 first 或 last。
布局边界与键盘避让
聊天页插件可以使用以下四个布局边界。它们以聊天页面视口中的 CSS 像素计量,分别从顶部或底部向内计算距离。
| CSS 变量 | JS 字段 | 含义 |
|---|---|---|
--tavo-inset-top-safe | topSafe | 顶部到系统状态栏下方安全边界的距离 |
--tavo-inset-top-bar | topBar | 顶部到当前聊天页顶部导航栏下方的距离,已包含顶部安全区 |
--tavo-inset-bottom-input | bottomInput | 底部到键盘收起时聊天输入区域上沿的距离,已包含底部安全区 |
--tavo-inset-bottom-safe | bottomSafe | 底部系统安全区的距离 |
同一侧的两个值是可选边界,不应相加。面板需要避开聊天输入框时选择 bottomInput,只需避开底部系统安全区时选择 bottomSafe。标题、大小、圆角、关闭按钮和动画由插件决定,Tavo 不提供或强制面板容器。
下面的面板上下留出 8px,并让内容在高度不足时滚动:
.my-plugin-panel {
position: fixed;
top: calc(var(--tavo-inset-top-bar, 0px) + 8px);
bottom: calc(var(--tavo-inset-bottom-input, 0px) + 8px);
left: 12px;
right: 12px;
overflow-y: auto;
}悬浮入口的初始位置也可使用这些变量。拖动和恢复位置时,使用 JS 边界将控件限制在可用区域内,并扣除控件自身尺寸:
if (tavo.ui && typeof tavo.ui.getInsets === 'function') {
const applyInsets = (insets) => {
// 使用 insets.topBar、insets.bottomInput 等更新自己的布局或拖动范围。
console.log(insets);
};
applyInsets(tavo.ui.getInsets());
const unsubscribe = tavo.ui.onInsetsChanged(applyInsets);
// 片段销毁或重新初始化前调用 unsubscribe()。
}getInsets() 同步返回只读快照。onInsetsChanged(handler) 只在值发生变化后通知,不会立即回调,返回取消订阅函数。首次宿主布局到达前可能为 0。尺寸、顶栏显示状态或输入区域内容高度变化会更新边界,键盘本身的高度和动画不计入这四个值,无需自行叠加键盘高度。
Tavo 内置键盘避让,插件无需额外引入脚本或监听键盘。 输入框获得焦点后,Tavo 会尝试滚动内容,必要时调整固定面板,让输入框露出。插件只需保证内容超出面板时可以正常滚动,无需让整个插件跟随键盘移动。该机制适用于常见布局,复杂的自定义布局仍需在设备上验证输入框能否正常露出。
适用范围是高级渲染中的聊天页插件:/chat HTML 片段可使用 CSS 变量,聊天页运行的插件 entry/action 可通过 tavo.ui 读取边界。消息内嵌内容(/messages)暂不支持这组接口和 CSS 变量。这不是模型 Tool Calling 或 MCP 工具接口。旧版 Tavo 需要检测 tavo.ui 是否存在,并提供保守的布局降级,CSS 示例中的 0px 仅用于变量缺失时的语法回退,不保证旧版安全距离。
插件遮罩仅覆盖聊天内容区域,不影响应用顶部导航栏。顶部导航栏占用的区域对插件来说是不可操作区域,放在其中的插件按钮、输入框等控件无法正常响应用户操作,提高 z-index 也无法解决。请将需要用户操作的插件内容放在 topBar 边界下方。宿主只发布边界,不扫描插件元素或强制它们遵守边界。更新按帧合并,只有变化的值才写入 CSS 和通知订阅者,插件回调应避免无条件重绘整个面板。
设置 Schema
contributes.settings.schema 是一个平铺数组,Tavo 会按顺序渲染到插件设置页。
这里声明的是设置表单 schema。插件代码应使用 tavo.plugin.config.get(key) / all() 读取合并 schema 默认值与用户保存值后的有效配置;插件 API 不提供原始 schema 查询。
| 类型 | 必填字段 | 可选字段 | 说明 |
|---|---|---|---|
switch | key, label | default | 布尔开关。 |
select | key, label, options | default | options 可包含兼容字符串或结构化 { value, label } object。 |
slider | key, label, min, max | step, default | max 必须大于 min;step 必须大于 0。 |
text | key, label | default | 单行文本输入。 |
textarea | key, label | default | 多行文本输入。 |
info | text | icon | icon 可为 info 或 warning。 |
divider | 无 | 无 | 整宽分割线。 |
break | 无 | 无 | 开始新的设置分组,不渲染可见控件。 |
字段类元素用 key 作为存储配置的键。发布后尽量不要改 key,除非你有意重置用户已有配置。
字符串 select option 的存储值和字面 label 相同。结构化 option 用稳定、
不可国际化的 value 作为存储值,label 可写字面文本或 $t object。
语言切换只更新 label,不会改变用户已保存的 value。
text 和 textarea 的 default 也可写 $t object。只有用户没有保存
覆盖值时,国际化默认值才跟随语言变化;切换语言不会覆盖用户值。
重置字段会删除覆盖值,重新使用当前语言的默认值。
使用 AI Agent 开发
AI Agent 指能直接帮你操作项目文件和工具的编码助手,例如 Codex、Claude Code、Trae、CodeBuddy 等。连接到 Tavo 的 MCP Server 后,它可以读取当前 App 暴露的资源文档和工具,并按你的需求创建插件。
- 在 Tavo 中打开
设置->MCP Server,启用 MCP Server。 - 按照 MCP Server 中的说明连接 AI Agent。
- 让 AI Agent 先读取 MCP Server 内部的插件资源文档,例如
tavo://docs/plugins。 - 直接提出插件需求,并要求它依次调用
tavo_plugin_validate_manifest、tavo_plugin_audit和tavo_plugin_package。 - 审阅审计结果,在 Tavo 中启用插件后先在备份聊天中切换支持的语言测试。
- 如果想要发布插件,可以让 AI Agent 打包出
.tpg文件。
可以从这样的提示词开始:
先读取 Tavo MCP Server 内部的插件资源文档,例如 tavo://docs/plugins。
帮我做一个名为 Quick Note 的 Tavo 插件。
manifest 使用 specVersion 2,entry.js 放在插件包根目录。
localization.defaultLocale 设为 en,至少支持 en,再根据目标用户增加一种常用语言,
也可以继续增加更多语言。所有用户可见的 manifest、settings、HTML 和 JavaScript
文案都要使用 catalog,HTML 需要首次渲染并在语言变化时完整重渲染。
为生成或插入的用户可见内容明确语言策略,不要读取 navigator.language 或保存
另一套插件语言。完成源文件后依次调用 tavo_plugin_validate_manifest、
tavo_plugin_audit 和 tavo_plugin_package,并逐条审阅 localizationAudit;不要在仍有
未审阅问题时宣称国际化已经完成。
插件需要一个名为 insert-note 的输入动作、一个名为 enabled 的 switch 设置,
并提供挂载到 /chat/body/end 的 HTML 片段。
完成后先在 Tavo 里安装、启用并测试;如果我要发布,请帮我打包成 .tpg 文件。可以直接从这组通用、可复制的双语模板开始: manifest.json、 entry.js、 HTML fragment、 English catalog 和 简体中文 catalog。
Agent 的工具调用顺序应为:
tavo_plugin_validate_manifest -> tavo_plugin_audit -> tavo_plugin_package -> tavo_plugin_install使用对话式 AI 辅助开发
对话式 AI 如 ChatGPT、Claude、豆包、DeepSeek 等,是主要通过聊天窗口协作的 AI。它们适合帮你设计插件思路、生成 manifest.json、编写 entry.js 和 HTML 片段,再由你复制到本地打包测试。
文档每一页都有 复制本页 按钮,可以直接复制当前页面的 Markdown 内容,粘贴给对话式 AI 作为上下文。建议把你的插件目标、目标 Tavo 版本、需要的入口和设置项一起发给它。
- 点击本页的
复制本页。 - 把复制出的 Markdown 和你的插件需求一起发给对话式 AI。
- 要求它输出文件树、
manifest.json、entry.js和需要的 HTML 片段。 - 把生成的文件复制到本地插件文件夹,打包为
.tpg(zip 格式)后在备份聊天中测试。 - 如果安装或运行失败,把 Tavo 的报错和当前文件内容贴回去,让它继续修改。
发布前检查
分享插件前:
- 在干净的 Tavo 配置或备份聊天中安装一次。
- 确认插件包根目录包含
manifest.json;如果插件声明了输入动作、侧边栏动作或最后一条消息动作,也要确认entry指向的文件存在。 - 确认所有 HTML fragment 的
src文件都存在。 - 依次运行
tavo_plugin_validate_manifest、tavo_plugin_audit和tavo_plugin_package,逐条处理或审阅localizationAudit中的问题。 - 确认所有用户可见的 manifest、settings、HTML 和 JavaScript 文案都已进入 catalog, 没有意外硬编码。
- 确认每个国际化 HTML fragment 都有首次渲染和
tavo.plugin.i18n.onChange()重渲染。 - 在 Tavo 中切换插件支持的语言,确认原生 UI 和全部 HTML surface 都已更新, 没有原始 key、上一种语言残留或意外回退语言。
- 确认生成或插入的用户可见内容有明确语言策略,并在目标对话语言下测试。
- 确认每个设置字段都有稳定的
key。 - 确认权限列表没有超出实际需要。
- 保留生成
.tpg的源文件夹,方便之后维护。