5.2 KiB
从 SillyTavern 扩展适配到 TauriTavern
本指南帮你把现有 SillyTavern 扩展快速适配到 TauriTavern,解锁 SillyTavern 没有的强大能力。
好消息:大部分适配工作就是把你以前手写的"找消息、搜消息、存数据"逻辑,替换成一行 API 调用。代码通常会变得更短、更清晰。
快速对照表
先看你的代码里有没有这些模式——如果有,TauriTavern 提供了更好的原生替代:
| 你以前的做法 | 问题 | TauriTavern 替代 |
|---|---|---|
chat.slice().reverse().find(...) |
手动复制并遍历长数组 | ✅ handle.locate.findLastMessage() |
手写关键词 filter/includes |
CJK 不友好,无评分排序 | ✅ handle.searchMessages() |
数据塞进消息体 + saveChat() |
膨胀 payload,数据与消息耦合 | ✅ handle.store.setJson() |
hack 写入 chat_metadata |
容量有限,语义不清 | ✅ handle.metadata.setExtension() |
反复读取 context.chat.length |
已准确,但需要稳定 handle/summary 时可走后端 | windowInfo().totalCount 或 handle.summary() |
为后台任务复制 context.chat.slice(-N) |
会额外复制消息对象引用 | ✅ handle.history.tail({ limit: N }) |
背景:完整历史与增强查询
TauriTavern 与 SillyTavern 1.18.0 一样,保证 getContext().chat 包含当前聊天的完整、有序消息历史。因此,依赖完整数组与绝对索引的上游扩展无需为了数据窗口做兼容分支。
增强 API(findLastMessage、searchMessages、history.*)不是修复不完整数组的旁路,而是可选的高效后端能力:它们适合有界扫描、CJK 检索、分页任务或避免在扩展中建立第二份大数组。
另一个容易踩坑的点:
- 如果你的扩展会修改聊天消息本身并希望落盘(例如清理/脱敏/批量替换),请调用
await getContext().saveChat()(它会走宿主的统一保存队列)。 - 不要从
script.js里直接 import/调用saveChat():这属于内部实现细节,也会绕过公开 context 边界与调用语义。
第 1 步:初始化 API
await (window.__TAURITAVERN__?.ready ?? window.__TAURITAVERN_MAIN_READY__);
const api = window.__TAURITAVERN__.api.chat;
const handle = api.current.handle();
💡 两端兼容:用
if (window.__TAURITAVERN__)判断运行环境,让同一个扩展在 SillyTavern 和 TauriTavern 上都能工作。
第 2 步:替换"找最后一条消息"
这是最常见的适配场景——几乎所有记忆/数据库类扩展都在做这件事。
之前:
const hit = context.chat.slice().reverse().find(
msg => msg.extra?.MyExtData
);
之后:
const hit = await handle.locate.findLastMessage({
role: 'assistant',
hasExtraKeys: ['MyExtData'],
scanLimit: 2000,
});
// hit = { index, message } 或 null
Rust 后端处理,即使万条消息也毫秒响应。
第 3 步:替换关键词搜索
之前:
const results = context.chat.filter(msg =>
msg.mes.includes('关键词')
);
之后:
const hits = await handle.searchMessages({
query: '关键词 或 中文短语',
limit: 20,
filters: { role: 'assistant', scanLimit: 5000 },
});
内置 CJK 优化,自动分词,带匹配评分和摘要片段。
第 4 步:迁移数据持久化
这是最令人兴奋的改进——SillyTavern 从未提供 标准的扩展数据持久化机制。
大数据 → store.*
之前(常见 hack):
// 把数据塞进最后一条消息...
lastMsg.extra.myData = hugeJsonObject;
saveChat(); // 膨胀聊天文件
之后:
await handle.store.setJson({
namespace: 'my-ext',
key: 'index',
value: hugeJsonObject,
});
独立存储,不影响聊天文件大小。
小配置 → metadata.*
await handle.metadata.setExtension({
namespace: 'my-ext',
value: { lastProcessedFloor: 42, enabled: true },
});
存储在 chat_metadata.extensions[namespace],语义清晰。
第 5 步:索引语义
如果你的扩展会持久化"已处理到第几楼"之类的进度,注意使用绝对索引:
const info = await api.current.windowInfo();
const total = info.totalCount; // 全量消息数
const absIndex = info.windowStartIndex + chatIndex; // 当前固定等于 chatIndex
⚠️ 持久化时使用 0-based 绝对消息索引;JSONL header 不计入索引。
第 6 步(可选):历史遍历
如果你确实需要遍历历史消息,使用分页读取:
let page = await handle.history.tail({ limit: 100 });
while (page.hasMoreBefore) {
for (const msg of page.messages) { /* 处理消息 */ }
page = await handle.history.before(page, { limit: 100 });
}
移动端推荐批量拉取减少 IPC 调用:
const pages = await handle.history.beforePages(page, { limit: 200, pages: 5 });
移动端性能建议
TauriTavern 运行在桌面和移动端,几条简单规则让你的扩展在低端设备上也流畅:
- ✅ 用
findLastMessage/searchMessages替代 JS 侧遍历 - ✅ 用
store.*存大数据,不塞消息体 - ✅ 总是设置
scanLimit,避免全量扫描 - ✅ 用
beforePages()批量分页,减少 IPC 往返