插件开发

自 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.png
cd my-plugin
zip -r ../my-plugin.tpg manifest.json entry.js locales ui cover.png

Tavo 会拒绝缺失 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:状态栏、悬浮面板、全局控制条、样式注入、读取当前聊天或输入框后展示的页面级信息。显示在聊天页面中,没有当前消息上下文。按用途选择沉浸式全屏或局部辅助界面,并遵守下文的布局边界。
contributes.htmlFragments + /messages单条消息附近的 UI:消息装饰、状态标签、每条消息按钮、只挂最后一条角色消息的状态块。每条匹配消息都会渲染一次,可用 role / position 过滤;拥有 tavo.message.current()。状态请使用 chat 作用域,可保存紧凑的当前状态,或以稳定消息 id 为 key 的小型映射。同时也需要控制 DOM 和脚本开销。

避免插件变量值过大

插件变量可保存任意受支持的可序列化值,但不宜存放过大的数据,尤其是图片、音频的 Base64 或 data URL,否则可能拖慢加载甚至导致内存溢出。大内容请使用 tavo.file.*,变量只保存虚拟路径。

默认显示位置

用户可以在插件设置页的「显示位置」中选择插件出现的位置。这里只列出插件实际声明的位置,同一位置下的所有动作或片段一起控制。

作者可在 manifest.json 的 contributes.visible 中设置各位置的默认显示状态,值为布尔值。未设置的位置默认为 true:

键显示位置
inputActions输入框菜单
sidebar侧边栏
lastMessageActions最后一条消息
chat聊天页,对应挂载在 /chat 下的 HTML 片段
messages消息,对应挂载在 /messages 下的 HTML 片段

例如,在已有的 contributes 中添加以下默认值,可让侧边栏和消息位置默认隐藏,其他位置默认显示:

{
  "contributes": {
    "visible": {
      "sidebar": false,
      "messages": false
    }
  }
}

用户对某个位置的选择优先于作者默认值,未修改的位置继续跟随作者默认值。在选择弹窗中点击「恢复默认」并确认后,会清除该插件的位置偏好,重新使用作者默认值,未声明的默认值仍为 true。所有位置都可以取消选择。

隐藏动作位置会移除该位置的插件入口。关闭 chat 或 messages 会停止挂载对应位置的 HTML 片段,片段内的 JavaScript 也不会随片段加载执行,另一个位置不受影响。位置开关不会关闭插件的 entry、生命周期或 hooks,插件总开关仍控制整个插件的启停。

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。
authorv2 是作者名,会显示在插件详情中。必须是 1–32 个用户可见字符的单行文本,不可国际化。
descriptionv2 是插件的详细介绍,会显示在列表和详情中;可国际化并支持 Markdown。
releaseNotes否同级 version 对应的更新说明。v2 可国际化并支持 Markdown。每个插件包只描述当前版本,不在字段里重复版本号,也不嵌入完整历史。
localizationv2 是必须有 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 label1–48 个字符,单行
settings 字段和结构化 select option label1–80 个字符,单行
settings description / info.text1–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、description 和 info 元素 text;
  • list 条目编辑字段 items.fields[].label;
  • 结构化 select option 的 label;
  • text 和 textarea setting 的 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(),确保切换语言时完整重渲染用户可见内容。

把插件国际化拆成三个必须分别检查的表面:

  1. 宿主原生 UI:manifest、动作标签和 settings 使用 $t。
  2. 插件运行时 UI:HTML、Toast、错误和动态文案使用 tavo.plugin.i18n.t(),HTML 订阅 onChange() 后完整重渲染。
  3. 插件生成或插入的可见内容:明确选择跟随当前对话语言、跟随 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 的一部分。 主题及其资源等通用接口见 TavoJS 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 和 /messages HTML 片段中使用,并且只会读取当前插件的配置。
  • 该 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.fileProcessors 中声明文件处理器:

字段必需说明
id是插件内唯一的处理器 ID,与注册时一致。
extensions与 mimeTypes 至少提供一项不含点的扩展名,例如 ["txt"]。
mimeTypes与 extensions 至少提供一项完整 MIME 类型,例如 ["text/plain"]。
entry否包内相对脚本路径或有序路径数组,仅在处理文件时加载,每次处理后不保留状态。省略时在插件顶层入口注册。
icon否包内 PNG、WebP 或静态 SVG 路径,单个不超过 256 KiB。

扩展名和 MIME 匹配不区分大小写,任一匹配即可成为候选。多个入口脚本按顺序 执行并可共享变量,不支持 ES 模块语法。第三方依赖请打包成包内脚本。

最小 manifest.json:

{
  "id": "example.text-reader",
  "name": "Text Reader",
  "version": "1.0.0",
  "permissions": ["file"],
  "contributes": {
    "fileProcessors": [
      {"id": "text", "extensions": ["txt"], "entry": "text.js"}
    ]
  }
}

text.js:

tavo.plugin.registerFileProcessor('text', {
  async extract(file, {signal}) {
    const text = await tavo.file.load(file.path, {encoding: 'utf8'});
    if (signal.aborted) throw new Error('Cancelled');
    return {content: text};
  }
});

处理器是否启用由插件代码决定。需要关联设置页开关时,读取设置后再注册:

if (tavo.plugin.config.get('enableText')) {
  tavo.plugin.registerFileProcessor('text', processor);
}

未注册的处理器会被跳过,没有其他可用处理器时发送原文件。 设置变更后会重新执行入口,后续文件处理使用新的设置。

输入为 {path, name, mimeType, size},路径是虚拟路径,大小单位为字节。 使用处理器独立 entry 时,可调用 tavo.file.load、tavo.plugin.config.get、 tavo.plugin.registerFileProcessor,本地化插件还可使用 tavo.plugin.i18n.t(key, params) 和语言字段。配置和语言以开始处理时的值为准, 不支持变化订阅或其他聊天 API。读取需要 file 权限且仅限当前输入文件, 支持 utf8(字符串)、base64(字符串)、bytes(Uint8Array)。

返回 {content},content 是提供给模型的完整字符串。Tavo 原样插入, 不额外添加提示词或格式。是否包含文件名、说明、XML 或 Markdown,由插件决定。 成功结果可在重启后复用,文件、插件包、设置或语言变化后会重新解析。

异常应在 extract() 中抛出 Error 或让 Promise 拒绝,不要返回 {error}。 unsupported_format 会尝试下一个匹配处理器,大小超限使用 too_large,解析超时使用 timeout,后两者应提供本地化文案。其他失败使用 throw new Error(message)。例如:

throw Object.assign(new Error('不支持此文件格式'), {
  code: 'unsupported_format',
});

文件和结果大小限制由插件决定,Tavo 不截断结果,也不限制累计文件内容长度。 解析超时也由插件控制,可通过插件设置提供给用户调整。处理传入的 signal 以支持取消。

没有匹配的启用处理器或未开启高级渲染时,Tavo 尝试发送原文件。解析失败会停止发送并保留草稿, 所有候选均抛出 unsupported_format 时也一样。新消息与纳入上下文的历史附件遵循相同规则。

文件图标来自已启用的插件。 是否注册处理器不影响图标可用性,缺失或不可用时使用通用文件图标。 SVG 支持基本图形、路径、渐变和裁剪,不支持脚本、事件、外部资源、内嵌 HTML、 样式表或文字,文字请转为路径。

输入动作

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:changedchat: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.addContext({ text: 'Model-only context' });
});

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 按注册顺序运行,宿主不设执行超时,会等待每个 handler 完成。handler 报错或写入无效文本时,Tavo 会忽略该 handler 的修改并继续后续 handler。建议插件根据自身任务定义超时时间,并在插件设置页面提供可供用户调整超时时间的控件。超时后应取消自身请求,并选择保留原文继续生成或提供明确的错误提示,避免无限等待。

用户停止生成会立即结束宿主等待并忽略迟到结果,但不会强制取消插件已经发出的网络请求;需要取消自身 I/O 的插件应使用 AbortController。generation:success 仍是每个 handler 最多等待 5 秒。这两个 Hooks 不能通过 event.cancel() 取消生成。每次生成只会触发 generation:success、generation:error 或 generation:cancelled 中的一个。

发送阶段如何选择 Hook

需求使用的 Hook
快速校验、取消发送、修改聊天中实际保存的用户文本input:beforeSend
向量化、向量召回、外部 API、只对模型可见的上下文注入generation:prepare
快速整理模型最终正文generation:success

不要用 input:beforeSend → event.cancel() → 等待外部 API → tavo.input.send() 实现记忆召回。这种拦截-重发模式会让用户消息在计算完成前无法显示,并有递归发送或重复副作用风险。

在 generation:prepare 中读取聊天消息并执行召回,优先用 event.addContext() 把召回结果作为独立上下文注入。用 tavo.message.find(range, filter) 可以按范围、角色或隐藏状态 选择已保存的消息。这个示例从最近四条可见消息构造查询,并在已用 upsertText() 保存笔记的当前聊天集合中搜索。

tavo.plugin.on('generation:prepare', async (event) => {
  const recent = await tavo.message.find([-4, -1], { hidden: false });
  const query = recent.map(({ content }) => content).join('\n').trim();
  if (!query) return;

  const collection = tavo.vector.collection('my_plugin_notes');
  const hits = await collection.searchText(query, { limit: 1, timeout: 5000 });
  const recalled = hits?.[0]?.text;
  if (recalled) event.addContext({ text: recalled });
});

发送顺序为 input:beforeSend → 保存并显示用户消息 → generation:prepare → 构建请求并开始生成。

event.addContext({ text, role?, position? }) 只在插件的 generation:prepare 中可用。 它把非空纯文本按原样追加到本轮请求,保留原有聊天消息。启用上下文日志时,实际注入的内容记为「插件」。 role 可用 user、assistant 或 system,默认 user。模型协议可以合并同角色的消息。 兼容旧版 Tavo 时,可用 typeof event.addContext === 'function' 检查当前版本是否支持。

  • 省略 position 时默认 { type: 'afterMainPrompt' }。相对位置支持 beforeMainPrompt、 afterMainPrompt、beforeChatHistory 和 afterChatHistory。停用或未加入预设顺序的标记视为缺失。 缺少主提示词标记时回退到聊天历史前,缺少所需的聊天历史标记时跳过。
  • 使用 { type: 'atDepth', depth: 1 } 可按聊天历史倒数插入。depth 是非负整数,省略时默认为 1, 即最后一条历史消息之前。0 表示历史末尾。超过历史长度时放在历史开头。 当预设不输出聊天历史时,深度注入也会跳过。
  • addContext() 同步返回,参数无效时抛错。handler 抛错或写入无效的 event.text 时, 本 handler 追加的上下文也会回滚。

输入发送 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...。

包内 JS、CSS 和图片

HTML 可以使用 /plugins/<插件 ID>/<包内路径> 引用已启用插件包里的文件,不需要额外的 manifest 声明或固定的 assets/ 目录:

<link rel="stylesheet" href="/plugins/com.example.plugin/ui/panel.css">
<img src="/plugins/com.example.plugin/images/logo.png">
<script src="/plugins/com.example.plugin/js/panel.js"></script>

包内普通 JS 按 HTML 顺序执行,与内联脚本一样获得 scoped tavo,各脚本的局部变量不自动共享。async、defer 不改变顺序,也不会自动等待脚本启动的异步任务。缺失或无法读取脚本时,该片段不会加载。包内模块脚本暂不支持。

HTML 中的相对路径不会自动以插件目录为基准。外部 CSS 中的相对 url()、@import 则以 CSS 自身地址为基准。ESM、Worker 和动态脚本加载不属于目前的兼容保证范围。

这些地址只读,不提供目录列表,禁止路径或符号链接越出插件目录。静态包资源允许跨插件读取,请勿把密钥放入插件包。配置和用户数据不通过此路径提供,禁用或卸载插件后,其资源地址不可用。

聊天页悬浮控件

/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。

布局边界与键盘避让

游戏等需要沉浸感的插件可以采用全屏布局,让背景和装饰延伸到整个聊天视口,但按钮、输入框等交互内容必须保持在可操作范围内。工具、状态面板等辅助类插件通常不需要占满屏幕,建议将界面限制在可操作范围内,按需显示,减少对聊天内容的遮挡。

可操作范围应避开应用顶部导航栏和系统安全区,交互内容应放在 topBar 边界下方。需要保留聊天输入框时,底部使用 bottomInput 边界,否则使用 bottomSafe。全屏布局不会让顶部导航栏区域变得可操作,提高 z-index 也无法改变这一点。

聊天页插件可以使用以下四个布局边界。它们以聊天页面视口中的 CSS 像素计量,分别从顶部或底部向内计算距离。

CSS 变量JS 字段含义
--tavo-inset-top-safetopSafe顶部到系统状态栏下方安全边界的距离
--tavo-inset-top-bartopBar顶部到当前聊天页顶部导航栏下方的距离,已包含顶部安全区
--tavo-inset-bottom-inputbottomInput底部到键盘收起时聊天输入区域上沿的距离,已包含底部安全区
--tavo-inset-bottom-safebottomSafe底部系统安全区的距离

同一侧的两个值是可选边界,不应相加。面板需要避开聊天输入框时选择 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 查询。

类型必填字段可选字段说明
switchkey, labeldefault, description布尔开关。
selectkey, label, optionsdefault, descriptionoptions 可包含兼容字符串或结构化 { value, label } object。
sliderkey, label, min, maxstep, default, descriptionmax 必须大于 min;step 必须大于 0。
textkey, labeldefault, description单行文本输入。
textareakey, labeldefault, description多行文本输入。
listkey, label, itemsdefault, description, fixed, min, max, sortable可编辑的对象列表。
infotexticonicon 可为 info 或 warning。
divider无无整宽分割线。
break无无开始新的设置分组,不渲染可见控件。

字段类元素用 key 作为存储配置的键。发布后尽量不要改 key,除非你有意重置用户已有配置。

字符串 select option 的存储值和字面 label 相同。结构化 option 用稳定、 不可国际化的 value 作为存储值,label 可写字面文本或 $t object。 语言切换只更新 label,不会改变用户已保存的 value。

text 和 textarea 的 default 也可写 $t object。只有用户没有保存 覆盖值时,国际化默认值才跟随语言变化;切换语言不会覆盖用户值。 重置字段会删除覆盖值,重新使用当前语言的默认值。

对象列表

用 list 声明提示词快捷方式等由多个字段组成的设置。用户可以添加、删除条目,也可以点击条目进入编辑页。列表标题下方的 description 可以省略。fixed: true 锁定当前条数,条目仍可编辑。sortable: true 允许长按拖动条目并保存顺序。两者默认均为 false,也可以同时开启。

{
  "type": "list",
  "key": "shortcuts",
  "label": "提示词快捷方式",
  "description": "点击快捷方式可编辑名称和提示词。",
  "items": {
    "type": "object",
    "titleKey": "name",
    "fields": [
      { "key": "name", "type": "text", "label": "名称", "required": true },
      { "key": "prompt", "type": "textarea", "label": "提示词", "required": true }
    ]
  },
  "default": [
    { "name": "续写场景", "prompt": "从上一句话继续写,保持角色语气。" }
  ]
}

items.fields 至少包含一个 text 或 textarea 字段,每个字段都有唯一的 key 和一个 label。required: true 要求字符串包含非空白字符。items.titleKey 引用其中一个字段,取其值作为条目的单行标题。标题值缺失或为空白时,显示列表的 label。

default 是按顺序排列的对象数组,省略时为 [],条数须满足 min 和 max。两者是非负整数,max 不小于 min。下限默认为 0,省略 max 表示无上限。对象只包含已声明的字段,字段值为字符串。列表标题、说明和编辑字段的 label 可以用 $t,条目值使用普通字符串。

tavo.plugin.config.get('shortcuts') 返回当前有效的对象数组。用户删空列表后,读到的是 []。恢复默认会重新使用 manifest 中的默认列表。使用 list 的 v2 manifest 将 minAppVersion 设为 1.10.0 或更高。

条件显示

所有设置元素都可以加可选的 when 字符串表达式。条件为 true 时,Tavo 才显示该元素或应用其布局作用。对 info 和 divider,条件控制提示或分割线是否出现。对 break,条件为真时才开始新分组。

{ "type": "divider", "when": "config.intensity >= 70" }
表达式示例显示条件
config.useCustomPrompt开关打开。
config.promptMode == 'custom'选中的选项值是 custom。
config.intensity >= 70滑块值达到 70。
contains(config.prompt, 'spoiler')文本包含子串 spoiler。
config.intensity >= 70 && contains(config.prompt, 'spoiler')两个条件都成立。

用 config.key 或 config['key.with.dots'] 引用同一份 schema 中排在前面的配置字段。支持 ==、!=、数字比较(>、>=、<、<=)、!、&&、||、括号和区分大小写的 contains(text, search)。比较 select 时使用选项的存储值。校验 manifest 时会检查字段引用和类型,并要求表达式返回布尔值。

条件随表单变化重新求值。字段隐藏时会保留已保存的值,其他显隐表达式在此期间无法读取该字段。使用 when 的 v2 manifest 应将 minAppVersion 设为 1.10.0 或更高。

list 可以由 when 控制显隐。表达式的配置输入支持 switch、select、slider、text 和 textarea。

使用 AI Agent 开发

AI Agent 指能直接帮你操作项目文件和工具的编码助手,例如 Codex、Claude Code、Trae、CodeBuddy 等。连接到 Tavo MCP 服务器后,它可以读取当前 App 暴露的资源文档和工具,并按你的需求创建插件。

  1. 在 Tavo 中打开 更多 → 设置 → 创作者 → Tavo MCP 服务器,启用服务器。
  2. 按照 Tavo MCP 服务器 中的说明连接 AI Agent。
  3. 让 AI Agent 先读取服务器提供的插件资源文档,例如 tavo://docs/plugins。
  4. 直接提出插件需求,并要求它依次调用 tavo_plugin_validate_manifest、 tavo_plugin_audit 和 tavo_plugin_package。
  5. 审阅审计结果,在 Tavo 中启用插件后先在备份聊天中切换支持的语言测试。
  6. 如果想要发布插件,可以让 AI Agent 打包出 .tpg 文件。

可以从这样的提示词开始:

先读取 Tavo MCP 服务器提供的插件资源文档,例如 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 版本、需要的入口和设置项一起发给它。

  1. 点击本页的 复制本页。
  2. 把复制出的 Markdown 和你的插件需求一起发给对话式 AI。
  3. 要求它输出文件树、manifest.json、entry.js 和需要的 HTML 片段。
  4. 把生成的文件复制到本地插件文件夹,打包为 .tpg(zip 格式)后在备份聊天中测试。
  5. 如果安装或运行失败,把 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 的源文件夹,方便之后维护。

目录