# Frontend Host Contract(TauriTavern) > 目的:把“宿主平台层(Host Kernel)对外承诺的行为”显式化,避免重构 `src/tauri/main/*` 时误伤上游 SillyTavern / 第三方扩展 / 重脚本 / 角色卡。 > 范围:仅覆盖前端宿主层(WebView 内运行的 Host Kernel)对外可观察的契约;不描述 Rust 后端内部实现。 > 参考:`docs/FrontendGuide.md`(集成架构与开发方式) --- ## 1. 稳定性分级(写清楚“哪些能改,哪些不能随便改”) 为了避免“什么都是 API”,本仓库把前端宿主行为按稳定性分为 3 类: 1. **Public Contract(对上游/插件/脚本/角色卡承诺)** - 一旦变更,必须在本文件记录,并在 smoke tests 里验证(见第 6 节)。 2. **Project Contract(项目内部约定)** - 例如 `init.js` 与 `bootstrap.js` 之间的协调信号;可以演进,但需要同步更新相关模块与文档。 3. **Internal(实现细节)** - 可自由重构,但不得改变 Public Contract 的外部可观察行为。 --- ## 2. 启动链路与就绪信号(Public + Project) ### 2.1 启动顺序(事实) 当前启动链路(见 `docs/FrontendGuide.md`): 开发态 HTTP 入口会先由 `scripts/tauri-dev-server.mjs` 注入 `src/dev-sw-bootstrap.js`,仅负责让持久 Service Worker registration 与当前 WebView 会话重新绑定,不参与应用模块初始化。 1. `src/init.js`:负责最早期的环境标记、可选 perf 开关与动态 import。 2. `src/tauri-main.js`:薄入口,仅调用 `bootstrapTauriMain()`。 3. `src/tauri/main/bootstrap.js`:composition root,创建 context、注册 routes、安装拦截器与补丁。 4. `src/script.js`:上游 SillyTavern 主应用入口(vendor)。 ### 2.2 就绪信号(Public/Project) - `window.__TAURITAVERN_MAIN_READY__ : Promise` - 由 `src/tauri/main/bootstrap.js` 写入,表示宿主层初始化已完成(或失败已被捕获并写入 console)。 - 在 Tauri runtime 下,resolve 前必须完成 Rust `BackendReadiness` 等待,确保首批依赖 `AppState` 的命令不靠 “state not managed” 文本重试作为正常控制流。 - `window.__TAURITAVERN_PERF_READY__ : Promise | undefined` - 仅在 perf-hud 启用时存在(见第 5 节)。 - `globalThis.__TAURITAVERN_PERF_ENABLED__ : boolean` - 由 `src/init.js` 在动态 import 前写入;`bootstrap` 会优先读取它(避免重复计算/时序差异)。 - `window.__TAURI_RUNNING__ : true` - 由 `src/init.js` 写入;用于桥接层尽早判断 Tauri 环境(避免移动端注入时序 race)。 --- ## 3. 全局 API(Public) > 这些符号被第三方脚本/扩展/角色卡直接调用,变更需极度谨慎。 ### 3.1 资源与缩略图(Public) 由 `createTauriMainContext()` 安装(实现:`src/tauri/main/context/index.js`,兼容入口:`src/tauri/main/context.js`): - `window.__TAURITAVERN_THUMBNAIL__(type, file, useTimestamp?) -> string` - 为 `bg` / `avatar` / `persona` 生成 `/thumbnail?...` Host Resource URL;未知 type 直接抛错。 - 正常第一方路径不使用 `useTimestamp`;该参数只保留为显式 force/debug cache bust。 - `window.__TAURITAVERN_BACKGROUND_PATH__(file) -> string` - 生成 `/backgrounds/` Host Resource URL。 这些 API 的**可观察行为**必须保持: - 对同一输入的 URL 形态(路径/查询参数意义)保持一致; - 失败时的返回值语义保持一致(例如 `null` vs 抛错 vs fallback string); - 不得引入同步阻塞(第三方会在渲染路径高频调用)。 ### 3.2 Android 导入/导出 Picker(Public) 由 `createTauriMainContext()` 安装(用于 Android Content URI 的回调接收): - `window.__TAURITAVERN_IMPORT_ARCHIVE_PICKER__`(对象:用于接收 Android 侧回调并 resolve/reject pending promise) - `window.__TAURITAVERN_EXPORT_ARCHIVE_PICKER__`(同上) > 这两者属于“跨语言桥接回调点”,命名与行为应视为 Public Contract。 ### 3.3 返回键处理(Public) 由 `src/tauri/main/back-navigation.js` 安装: - `window.__TAURITAVERN_HANDLE_BACK__() -> boolean` - 返回 `true` 表示已消费返回键(例如关闭对话框/浮层/抽屉/聊天等),否则返回 `false`。 ### 3.4 原生分享桥(Public) 由 `src/tauri/main/share-target-bridge.js` 安装: - `window.__TAURITAVERN_NATIVE_SHARE__ = { push(payload), subscribe(handler) }` - `push()`:注入分享 payload(url 或 png)。 - `subscribe()`:订阅消费;若早到则进入 backlog,首次订阅会 drain backlog。 ### 3.5 上游库兼容全局(Public) 由 `src/lib.js:initLibraryShims()` 安装: - `window._ : lodash` - SillyTavern 生态中的 third-party 扩展可能把 lodash external 为 `_`,并在 ESM 模块求值阶段直接访问。 - 该符号必须在 third-party 扩展模块加载前可用;不得依赖 webpack/Rspack 等打包器偶然泄漏全局。 该 ABI 属于 SillyTavern 兼容层,不放入 `window.__TAURITAVERN__.api`。新 TauriTavern 代码仍应从 `src/lib.js` 显式 import `lodash`。 ### 3.6 平台 ABI(Public,新) 为避免未来继续扩散 `window.__TAURITAVERN_*` 零散符号,宿主层额外提供一个**统一出口**: - `window.__TAURITAVERN__ : { abiVersion, traceHeader, ready, invoke, assets, api }` - `abiVersion: 1`:ABI 版本号(已发布扩展契约发生语义化破坏改动时递增)。 - `traceHeader: string`:请求追踪 header 名(见 4.4)。 - `ready: Promise | null`:与 `__TAURITAVERN_MAIN_READY__` 语义一致。 - `invoke.safeInvoke(...)` / `invoke.flushAll()`:对 `context` invoke 能力的稳定包装。 - `assets.thumbnailUrl` / `assets.backgroundPath`:对 3.1 中稳定 Host Resource URL helper 的统一引用。 - `api.layout`:布局契约 API(safe-area / viewport / Android IME),并配合 `data-tt-mobile-surface` taxonomy 让扩展以几行 opt-in 完成移动端适配。 - 详细签名与示例见:`docs/API/Layout.md`。 - `api.chat`:TauriTavern 独有的聊天/记忆类扩展 API(聊天摘要、元数据、历史分页、稳定存储、后端定位、纯文本检索)。 - 详细签名与示例见:`docs/API/Chat.md`。 - `api.characterCards`:角色卡文件选择宿主 API。用于在 Tauri desktop/iOS 上以 native picker 选择本地角色卡文件,并返回标准 `File[]` 给上游导入/替换流程继续处理。 - 当前已落地 Host ABI:`isNativePickerAvailable()`、`pickFiles(options?: { multiple?: boolean; title?: string }) -> Promise`。 - 当前 native picker 仅暴露 Rust 角色导入器真实支持的 `json/png` 角色卡格式;上游 `processDroppedFiles` 的格式判断保持不变。 - 语义:只补齐平台文件选择能力,不导入、不覆盖、不改变 `/api/characters/import`、`preserved_name`、角色 avatar identity 或上游 toast/tag/刷新收尾语义。 - 取消选择返回 `null`;picker/staging/read 失败必须抛错,不得静默回退到 WebView file input。 - `api.extension.store`:扩展级**全局持久化**(不绑定 chat),提供 KV JSON + Blob,支持多 table。 - 详细签名与示例见:`docs/API/Extension.md`。 - `api.db`:本地向量、JSON、文本索引、图和 TQL 数据库。`open` 异步等待可用;NodeId 为整数;签名、索引恢复边界与共享关闭语义见 [Database API](API/Database.md)。 - `api.dev`:TauriTavern 规范化的开发调试 API。内置 Settings 开发面板与第三方扩展都应消费这一层,而不是直接依赖 Tauri 事件名或 Rust 命令名。 - `api.dev.frontendLogs` - `list(options?: { limit?: number }) -> Promise` - `subscribe(handler) -> Promise` - `getConsoleCaptureEnabled() -> Promise` - `setConsoleCaptureEnabled(enabled: boolean) -> Promise` - 语义:宿主统一负责“运行时开关 + 持久化设置 + 本地 bootstrap flag”同步;调用方不应再自行读写 `localStorage`。 - `api.dev.backendLogs` - `tail(options?: { limit?: number }) -> Promise` - `subscribe(handler) -> Promise` - 语义:宿主负责共享后端日志流;多个订阅者并存时通过引用计数管理 `enable/disable stream`,不得彼此踩踏。 - `api.dev.llmApiLogs` - `index(options?: { limit?: number }) -> Promise` - `getPreview(id: number) -> Promise` - `getRaw(id: number) -> Promise` - `subscribeIndex(handler) -> Promise` - `getKeep() -> Promise` - `setKeep(value: number) -> Promise` - 语义:宿主统一负责历史索引、实时索引流与 keep 设置持久化;调用方不应直接操作 `devlog_*` 命令。 - `api.dev.exportBundle() -> Promise`:导出 debug bundle(zip)并返回保存路径,详见 `docs/API/Dev.md`。 `api.dev.*` 的长期契约要求: - DTO 字段保持 camelCase,新增字段只能做向后兼容扩展。 - `subscribe()` / `subscribeIndex()` 返回的 `unsubscribe` 必须幂等且可安全延迟调用。 - Tauri 事件名 `tauritavern-backend-log` / `tauritavern-llm-api-log` 与命令名 `devlog_*` 属于 Internal 实现细节,不是第三方 Public Contract。 - `api.worldInfo`:TauriTavern 规范化的 World Info / Lorebook 激活与导航 API。 - `getLastActivation() -> Promise` - 返回最近一次真实生成流程对应的最终激活结果。 - `null` 仅表示当前会话还没有捕获到任何一次最终激活结果。 - `subscribeActivations(handler) -> Promise` - 只推送最终激活结果,不暴露 `WORLDINFO_SCAN_DONE` 的中间循环状态。 - 不复播历史结果;若需要最近一次结果,应先调用 `getLastActivation()`。 - `openEntry(ref: { world: string; uid: string | number }) -> Promise<{ opened: boolean }>` - Best-effort 导航入口。 - `opened: true` 表示宿主已成功打开目标世界书并尝试定位到目标条目。 - `opened: false` 表示目标世界书或条目不存在;其他异常直接抛出,便于调试。 `api.worldInfo` 的 v1 收缩边界: - 只暴露“最终激活批次”,不直接暴露 `WORLD_INFO_ACTIVATED` / `WORLDINFO_SCAN_DONE` 原始载荷。 - 激活条目 DTO 仅承诺:`world`、`uid`、`displayName`、`constant`、可选 `position`。 - 不把扫描循环控制、预算内部状态、可变中间态对象直接升格为 Public Contract。 - `openEntry()` 必须复用上游 World Info 模块自身的导航能力;宿主 ABI 层不得直接依赖 `#WorldInfo`、`#world_editor_select`、`[uid=\"...\"]` 等 DOM 细节。 - `api.agent`:运行控制、历史、工作区详情与 Profile 管理,见 [Agent API](API/Agent.md)。 - 前端准备聊天输入,Rust 执行模型与工具循环;Agent Mode 关闭时沿用 Legacy Generate。 - Host API 解析稳定聊天身份,宿主提交桥复用聊天保存流程,持久版本发布后再关联到消息 metadata。分叉使用新身份并复制持久版本。 - 运行结束包含执行与宿主呈现的收尾;前端等待保存成功或明确失败后释放生成状态。续接保留原 Run 身份。 - Run 事件供历史与 Timeline 使用,实时参数预览供当前显示使用;它们与 SillyTavern 生成事件分别订阅。 - 运行控制属于 Public Contract;模型回合、任务详情、工具目录和 Timeline 关系是 Project Contract,由对应 API 提供展示 DTO。 - `api.llmConnections`:管理 Profile 引用的模型连接,见 [LLM Connection API](API/LlmConnections.md)。Profile 通过连接 ID 和模型 ID 绑定;Model Target 是界面的配置来源。 - `api.skill`:管理本地知识包的导入、编辑、作用域与导出,见 [Skill API](API/Skill.md)。模型在 Run 中通过 Skill 工具读取材料或执行脚本;安装与替换由管理界面处理。 - `api.mcp`:MCP registration、只读 tool discovery、model-facing description override 与第一方 Manager user test call 的独立平台 API。Agent 与 Legacy generation 已通过内部 application seam 消费 MCP,但 MCP 不依附 Agent Mode,公开 API 仍不提供 raw model-call executor。 - 当前为实验性的 Project Contract;详细签名见 `docs/API/MCP.md`。 - 当前暴露 `servers.list/create/update/setState/remove/discover/refresh`、`tools.setPermission`、`tools.setDescriptionOverride` 与 `tools.testCall({ registrationId, nativeName, argumentsJson }, { signal? })`;description override 只改变模型 descriptor 副本,不修改 discovery catalog、权限或执行身份;`update` 可修改名称、endpoint、custom headers 与协议版本;`discover` 读取 application persistent catalog,`refresh` 是唯一强制联网入口;不暴露 raw RPC 或 RMCP session。 - `testCall` 是第一方 Manager 的 Project Contract:Active registration 上的显式用户调用不受 Off/Ask/Allow 阻止且不修改 permission;typed outcome 区分 `known_response`、`not_sent` 与 `outcome_unknown`,AbortSignal 只停止本地等待,不承诺远端回滚。 - 同一 WebView 内的 vendor/extension scripts 仍按当前平台 trust model 视为用户授权代码;本 ABI 不声称验证物理点击或隔离 hostile extension。 - server 新建后总是 Paused;工具缺省 Off。discovery annotations 不构成 authority。 - Agent/Legacy model exposure 都只读取 application persistent snapshot,并在共享 resolver 应用 registration description override;Agent Profile `tools.toolDescriptions` 随后覆盖同一字段。发送前由 Rust 重查 permission;Legacy MCP 不进入全局 SillyTavern ToolManager、slash commands 或 extension enumeration。 - 第一方 MCP 管理 UI 是独立内置扩展;它只消费本 API,不把 React 状态升格为平台事实,也不在 TauriTavern Settings 中维护第二入口。 > 注意:`window.__TAURITAVERN__` 是“平台 ABI”,应保持**小而稳定**;不要把内部实现对象整个暴露出去。 ### 3.7 ChatSurface participant(Project Contract) - `window.__TAURITAVERN__.api.chatSurface` - 当前是实验性的 Project Contract,尚未作为 Public Contract 稳定发布。 - 暴露 `protocolVersion: 1`、`isManagedOwnershipRequired()`、`registerParticipant()` 与独立的 `registerContentProcessor()`;ownership query 返回本页已冻结的布尔决策,投影控制器、DOM adapter、内部 revision、admission 预算和虚拟滚动引擎均不外露。 - 内容处理器在首次投影前注册,异步返回显示 HTML;宿主保存结果供重挂载复用,`registration.refresh()` 显式刷新。participant v1 的同步契约保持不变。 - participant 必须显式声明协议版本;hook 返回同步 disposer,宿主用 `AbortSignal` 表达 mount/content/runtime 三种真实寿命。 - mount/remount/content lifecycle 不得伪装为 SillyTavern 消息业务事件。 - 完整协议与 raw API 接入示例见 `docs/API/ChatSurface.md`。 --- ## 4. 请求拦截与路由契约(Public) ### 4.1 拦截范围(事实) 由 `src/tauri/main/interceptors.js` 安装: - patch `window.fetch` - patch `jQuery.ajax`(兼容 jqXHR/Deferred 行为) 拦截生效条件(见 `src/tauri/main/bootstrap.js`): - 仅在 **Tauri 环境**启用(`bootstrapTauriMain()` 早退保护)。 - 仅拦截 **same-origin** 请求(包含被 patch 的同源 iframe/window)。 - 是否接管由 `router.canHandle(method, pathname)` 决定(仅看 `url.pathname`)。 ### 4.2 未命中行为(Public) - `fetch`:未命中路由直接透传原生 fetch。 - `ajax`:未命中路由直接透传原始 `$.ajax`。 - 命中但无 handler:返回 `404` JSON(`{ error: "Unsupported endpoint: ..." }`)。 > 这类行为会被上游与第三方依赖:不要改成 silent fail/空响应。 ### 4.3 路由表(Public) 路由定义集中在 `src/tauri/main/routes/*`,其路径本身属于 Public Contract(上游/插件会直接请求)。 第一方聊天完整加载与保存直接调用内部 payload transport;兼容 `/api/chats/get`、`/api/chats/group/get`、`/api/chats/save`、`/api/chats/group/save` 仍可由扩展主动调用。第一方操作不再产生这些 Fetch 请求,不能依赖 monkeypatch Fetch 观察它们;公开保存入口及既有业务事件不变。兼容保存成功仍为 `200 { ok: true }`,明确的 integrity 冲突仍为 `400 { error: 'integrity' }`,其他提交或清理失败不得仅因文案包含 integrity 而返回该冲突响应。 `saveMetadata()` / `getContext().saveMetadata()` 的持久化范围为 header 内的整个 `chat_metadata`,正文保持原字节,不再顺带保存消息。这是相对 SillyTavern 1.18.0 的明确语义变化;消息修改必须调用完整保存,不能依赖下一次 metadata 写入。metadata 的 debounce 保持 1000 ms,不取消待执行的完整保存;integrity 确认后仍强制完整保存,拒绝则 reload,普通失败不回退。该能力通过内部 transport 调用一个 metadata command,不新增兼容 HTTP route 或 Host ABI 别名。新群聊在首次问候事件前已有带 identity 的 header,事件写入不会被初始化覆盖。完整语义与成本见 `docs/CurrentState/ChatPayload.md` §3.1。 启用[历史滑动按需加载](CurrentState/ChatPayload.md#21-历史滑动按需加载)时,`getContext().chat` 的历史候选槽位允许为 null;兼容 get、导出与保存文件保持完整。 最关键的启动依赖: - `/csrf-token`:返回固定 token(用于兼容上游初始化对 CSRF 的假设) - `/version`:返回版本信息 高频与高风险路径(示例,不是完整列表): - `/api/*`:应用核心 API(settings/chats/characters/ai/worldinfo…) - 用户设定沿用上游 settings 数据形状;列表刷新不写入资料,单项保存失败不影响其他修改。 - `/api/backends/chat-completions/generate` 的流式响应由 Rust 进程内会话持有生成任务、移动端 best-effort 后台执行租约与未确认事件;前端通过单调递增的 `after_seq` 消费并确认,WebView 暂停后可在同一 Rust 进程内重放缺失事件。单次读取失败会使用相同 cursor 重试一次,第二次失败才关闭会话。后台租约失败或到期不拥有生成终止权;该保证不跨进程重启,也不伪装成上游 provider 的 HTTP 断点续传。 - `/css/user.css`:用户自定义 CSS 覆盖文件(数据目录 `_css/user.css`) - `/scripts/extensions/third-party/*`:third-party 扩展静态资源端点(ESM/CSS/url()/字体/图片) - `/thumbnail`:缩略图端点(与 `__TAURITAVERN_THUMBNAIL__` 强耦合) - 用户静态资源端点(通配符路由): - `/characters/*`、`/User Avatars/*` - `/backgrounds/*`、`/assets/*` - `/user/images/*`、`/user/files/*` ### 4.4 浏览器资源契约(Public) 这些路径必须能被浏览器**原生子资源加载**(`` / `` / `