TauriTavern/docs/API/Chat.md
Darkatse c9b380aa70 refactor(chat): unify payload and metadata commit sessions
Route full saves and metadata updates through one bounded commit protocol,
with session-owned staging and atomic publication that preserves message bytes.
Simplify behavioral coverage and keep documentation focused on core contracts.
2026-10-01 00:53:02 -04:00

204 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `window.__TAURITAVERN__.api.chat` — API 参考
TauriTavern 为“记忆/数据库/检索类扩展”扩展开发者提供的增强 API。
> **核心理念**:把 SillyTavern 扩展开发中最常见的脏活累活——找消息、搜内容、存数据——变成一行 API 调用。Rust 后端处理重活,JS 侧只管调接口。
## 0. 快速上手
```js
await (window.__TAURITAVERN__?.ready ?? window.__TAURITAVERN_MAIN_READY__);
const api = window.__TAURITAVERN__.api.chat;
const handle = api.current.handle();
```
> TauriTavern 遵循 SillyTavern 的完整历史契约:`getContext().chat` 是当前聊天的完整、有序消息数组。下面的增强 API 适合在 Rust 后端执行有界读取、定位、检索与独立状态持久化,避免扩展重复实现重扫描或复制大数据。
## 1. 核心入口
启用[历史滑动按需加载](../CurrentState/ChatPayload.md#21-历史滑动按需加载)时,`getContext().chat` 的历史候选槽位可能为 null;本页后端读取 API 仍返回完整内容。
| 方法 | 返回值 | 说明 |
| --- | --- | --- |
| `api.chat.open(ref)` | `ChatHandle` | 打开指定聊天 |
| `api.chat.current.ref()` | `ChatRef` | 当前聊天的引用 |
| `api.chat.current.handle()` | `ChatHandle` | 当前聊天的操作句柄 |
| `api.chat.current.windowInfo()` | `Promise<WindowInfo>` | 当前完整历史的兼容状态信息 |
**`ChatRef` 类型**:
- 角色聊天:`{ kind: 'character', characterId, fileName }`
- 群聊:`{ kind: 'group', chatId }`
**`WindowInfo` 类型**:
| 字段 | 说明 |
| --- | --- |
| `mode` | 固定为 `'off'` |
| `chatKind` | `'character' \| 'group'` |
| `chatRef` | 当前 `ChatRef` |
| `totalCount` | 聊天总消息数(不含 header) |
| `windowStartIndex` | 固定为 `0` |
| `windowLength` | 完整消息数,与 `totalCount` 相同 |
`windowInfo()` 保留 Promise 与字段形状是为了稳定公开 ABI;它不触发后端 summary 查询,也不对应可配置加载模式。消息数组索引就是绝对索引:
```js
const info = await api.current.windowInfo();
const absIndex = info.windowStartIndex + chatIndex; // 等同于 chatIndex
```
## 2. ChatHandle 能力
通过 `api.open(ref)` 或 `api.current.handle()` 获取 `ChatHandle`。
---
### `locate.findLastMessage()` — 精准定位,不再手动遍历
> 💡 **解决的痛点**:定位最后一条包含特定数据的消息通常需要 `chat.slice().reverse().find(...)` 手动遍历;后端定位可以限定扫描范围,避免扩展重复复制和扫描长数组。
```js
const hit = await handle.locate.findLastMessage({
role: 'assistant', // 可选:限定角色
hasExtraKeys: ['TavernDB_ACU_IsolatedData'], // 可选:消息必须包含的 extra 键
scanLimit: 2000, // 可选:最多扫描多少条
});
```
**返回值**:
- 命中:`{ index, message }` — 绝对索引 + 原始消息对象
- 未命中:`null`
全部由 Rust 后端高效执行——即使 10000 条消息也毫无压力。
---
### `searchMessages()` — 内置全文检索
> 💡 **解决的痛点**:SillyTavern 没有内置的消息搜索 API,扩展开发者只能手写 `filter/includes` 扫描,CJK 内容更是无法正常匹配。
```js
const hits = await handle.searchMessages({
query: '是啊,吃什么',
limit: 20,
filters: {
role: 'assistant', // 可选:限定角色
startIndex: 100, // 可选:起始索引
endIndex: 5000, // 可选:结束索引
scanLimit: 5000, // 可选:从尾部向前最多扫描条数
},
});
```
**返回 `SearchHit[]`**:
| 字段 | 说明 |
| --- | --- |
| `index` | 绝对索引(0-based) |
| `score` | 匹配评分(0~1,越大越匹配) |
| `snippet` | 可直接用于 UI 展示的短片段 |
| `role` | `'user' \| 'assistant' \| 'system' \| 'tool'` |
| `text` | 命中消息的完整 `mes` 文本 |
**技术特点**:
- 基于片段命中评分 + TopK 召回(非向量检索,轻量高效)
- CJK / 无空格文本自动 bigram 分词,大幅提升中日韩文匹配率
- 纯标点或符号 query 中的连续片段按原始字符序列做字面量子串匹配;混合 query 继续使用既有分词评分语义
- `scanLimit` 控制性能上限,移动端友好
---
### `store.*` — 扩展数据持久化
> 💡 **解决的痛点**:SillyTavern **从未提供** 扩展数据持久化方案。开发者只能把数据塞进消息体(放大 payload、数据耦合),或 hack 写入 `chat_metadata`(容量有限、语义不清)。
TauriTavern 提供每个聊天独立的 KV JSON 存储:
```js
// 写入
await handle.store.setJson({ namespace: 'my-ext', key: 'index', value: largeData });
// 读取
const data = await handle.store.getJson({ namespace: 'my-ext', key: 'index' });
// 更新(对象会深度合并;非对象会直接替换)
await handle.store.updateJson({
namespace: 'my-ext',
key: 'index',
value: { lastFloor: 42, updatedAt: Date.now() },
});
// 重命名 key
await handle.store.renameKey({
namespace: 'my-ext',
key: 'index',
newKey: 'index-v2',
});
// 列出所有键
const keys = await handle.store.listKeys({ namespace: 'my-ext' });
// 删除
await handle.store.deleteJson({ namespace: 'my-ext', key: 'old-key' });
```
兼容:`updateJSON()` 是 `updateJson()` 的别名;`updateKey()` 是 `renameKey()` 的别名。
**适用场景**:表格、索引、数据库快照等大 JSON 数据。数据与消息彻底解耦,不会膨胀聊天文件。
---
### `metadata.*` — 轻量配置存储
适合存储进度、配置项、短摘要等小状态:
```js
// 读取 chat 元数据
const meta = await handle.metadata.get();
// 写入扩展配置
await handle.metadata.setExtension({ namespace: 'my-ext', value: { lastFloor: 42 } });
// 删除(value 设为 null)
await handle.metadata.setExtension({ namespace: 'my-ext', value: null });
```
数据存储在 `chat_metadata.extensions[namespace]` 中,与聊天文件一同迁移。每次调用只修改指定 namespace,不自动合并到当前页面的活 metadata。
---
### `summary()` / `stableId()`
```js
// 获取聊天摘要(不需要加载完整消息)
const summary = await handle.summary({ includeMetadata: true });
// 获取可持久化的稳定 ID
const id = await handle.stableId();
```
`summary()` 默认只返回轻量摘要。`includeMetadata: true` 返回完整 metadata,读取成本随返回数据大小增长。
---
### `history.*` — 按需分页读取
扩展希望以有界页面处理历史、避免再创建一份全量数组时使用:
```js
let page = await handle.history.tail({ limit: 100 });
while (page.hasMoreBefore) {
// page.messages: ChatMessage[]
// page.startIndex: 本页首条消息的绝对索引
page = await handle.history.before(page, { limit: 100 });
}
```
| 方法 | 说明 |
| --- | --- |
| `history.tail({ limit })` | 获取最新的 N 条消息 |
| `history.before(page, { limit })` | 向前翻一页 |
| `history.beforePages(page, { limit, pages })` | 一次拉多页(减少 IPC 往返,移动端推荐) |
返回值包含 `startIndex`(本页首条消息的绝对索引,0-based)和 `hasMoreBefore`(是否还有更早的消息)。