Add a disabled-by-default setting with reload semantics and localized help. Document the cold representation, ownership, and save compatibility contracts.
30 KiB
TauriTavern 前端指南
本文档描述 TauriTavern 当前前端(基于 SillyTavern 1.18.0)在 Tauri 环境下的集成架构与开发方式。
宿主层对外契约清单见:docs/FrontendHostContract.md(重构时优先保障其不回归)。
1. 目标与原则
- 最小侵入:尽量保持上游 SillyTavern 前端行为不变。
- 模块化:将 Tauri 注入逻辑拆分为独立模块,避免单文件膨胀。
- 低耦合:路由注册、请求拦截、业务上下文分离。
- 入口收敛:统一走
init.js -> tauri-main.js -> tauri/main/*,减少重复入口。
2. 启动链路
当前前端启动顺序如下:
src/init.js动态导入:lib.js->tauri-main.js->script.jssrc/lib.js静态导入src/dist/lib.core.bundle.js,统一提供 ESM 导出;highlight.js通过getHljs()按需加载src/tauri-main.js仅调用bootstrapTauriMain()(薄入口)src/tauri/main/bootstrap.js负责:- 创建运行上下文(
context) - 注册前端路由(
router + routes/*) - 安装请求拦截器(
fetch与jQuery.ajax) - 安装平台 ABI:
window.__TAURITAVERN__(小而稳定的宿主对外接口) - 安装同源窗口下载桥(移动端浏览器式导出 -> 原生落盘)
- 安装 Tauri mobile 兼容层(runtime polyfills + geometry firewall + surface classifier,仅移动端)
- 为宿主接管的路由响应注入追踪 header:
x-tauritavern-trace-id - 初始化 bridge 与目录信息
- 创建运行上下文(
3. 目录结构(前端集成相关)
src/
├── tauri-bridge.js # 低层 bridge:invoke/listen/convertFileSrc
├── tauri-main.js # 新入口:只做 bootstrap
├── tauri/
│ └── main/
│ ├── bootstrap.js # 组合根(composition root)
│ ├── context.js # 兼容 shim(re-export `context/index`)
│ ├── context/ # Host Kernel facade + types(对外契约保持稳定)
│ ├── kernel/ # 纯逻辑(策略/计算/键生成/追踪等)
│ ├── services/ # 有状态能力(assets/thumbnails/characters/android…)
│ ├── adapters/ # 触碰 window/DOM/上游 ST 的适配层
│ ├── download-bridge.js # 同源窗口下载桥接
│ ├── http-utils.js # URL/Body/Response 工具
│ ├── interceptors.js # fetch/jQuery 注入
│ ├── router.js # 轻量路由注册与分发
│ └── routes/
│ ├── system-routes.js
│ ├── settings-routes.js
│ ├── extensions-routes.js
│ ├── resource-routes.js
│ ├── character-routes.js
│ ├── chat-routes.js
│ └── ai-routes.js
└── scripts/
├── extensions/runtime/ # 第三方插件运行时(资源解析/模块重写/加载器)
└── ... # 上游 SillyTavern 功能模块
4. 核心模块职责
4.1 bootstrap.js
- 组装模块依赖并执行初始化。
- 确保只 bootstrap 一次。
- 在 bridge 初始化后再次尝试 patch 运行时补丁(处理加载时序问题)。
- 维护对第三方可见的宿主 ABI(
window.__TAURITAVERN__)与请求追踪 header。
4.2 context.js
context.js仅作为兼容入口(避免外部 import 路径变化)。- 真实实现位于
src/tauri/main/context/index.js:作为 Host Kernel facade 组装kernel + services + adapters。 safeInvoke具备可配置的 invoke 策略(dedupe / write-behind / TTL cache),集中在src/tauri/main/kernel/invokes/invoke-policies.js。- Host 侧已知的 Rust 命令名收敛为类型:
src/tauri/main/kernel/invokes/tauri-commands.js(TauriInvokeCommand)。 - 与第三方直接交互的全局符号(如缩略图 helpers)属于 Public Contract(见
docs/FrontendHostContract.md)。
4.2.1 window.__TAURITAVERN__.api.chat(扩展/记忆类插件 API)
这是 TauriTavern 独有 API 的唯一入口(刻意不做 alias),为扩展提供有界历史读取、后端定位、检索与持久化能力;它增强上游完整
chat[]契约,不替代该契约。
- 安装位置:
src/tauri/main/api/chat.js(在src/tauri/main/bootstrap.js中安装到window.__TAURITAVERN__.api.chat)。 - 类型声明:
src/types.d.ts(便于扩展作者用 TS/JSDoc 一键上手)。 - 详细 API 文档与适配指南:
docs/API/。
4.3 interceptors.js
- 代理
window.fetch。 - 代理
$.ajax并保持 Deferred/jqXHR 行为兼容。 - 只拦截本地 API 请求,其余请求透传原生实现。
4.4 download-bridge.js
- 只处理移动端同源窗口中的浏览器式下载(如
blob:/data:/ 同源 URL +a[download])。 - 将命中的导出转接到现有原生文件导出链路。
- 不参与 API 路由判断,避免与请求拦截职责混合。
4.5 router.js + routes/*
router.js提供简洁注册接口:get/post/all。routes/*按业务域组织,降低文件复杂度与改动冲突。
5. 请求注入流程
- 前端发起
fetch('/api/...')或$.ajax('/api/...') - 拦截器通过
router.canHandle(method, path)判断是否由本地路由接管 - 命中后交给路由分发到
routes/* - 路由通过
context.safeInvoke(...)调用 Rust 命令 - 返回标准
Response给前端调用方
补充:
/csrf-token在system-routes.js中返回固定 token,用于通过前端初始化流程中的 CSRF 依赖检查。- 所有宿主接管的路由响应都会附带
x-tauritavern-trace-id,用于将 DevTools Network 与 console/perf-hud 关联定位问题(header 名也可从window.__TAURITAVERN__?.traceHeader获取)。
6. 路由分域说明
| 文件 | 负责范围 |
|---|---|
system-routes.js |
ping/version/csrf 等系统基础接口 |
settings-routes.js |
设置、快照、密钥、预设 |
extensions-routes.js |
扩展发现、安装、更新、删除等 |
resource-routes.js |
头像、背景、主题、群组等资源接口 |
character-routes.js |
角色列表、创建、编辑、导入导出、重命名 |
chat-routes.js |
聊天读写、搜索、最近记录、导出 |
ai-routes.js |
Chat Completion(OpenAI / Claude / Gemini / OpenCode) |
tokenizer-routes.js |
tokenizer count/encode/decode/bias 与上游本地 tokenizer 兼容路由 |
6.1 聊天 payload 与 DOM 边界
- 上游接管点:
src/script.js(character chat)与src/scripts/group-chats.js(group chat)。 - 统一入口:上游只 import
src/scripts/chat-payload-transport.js,不要直接依赖src/scripts/tauri/chat/*。 - 第一方当前聊天通过
src/scripts/chat-payload-transport.js加载全部楼层,可选历史滑动按需加载;/api/chats/get与/api/chats/group/get保留为扩展和脚本的兼容路由。header 与完整、有序的消息数组分离后,generation、扩展和保存共享同一个 canonicalchat[]。 chat_truncation只限制初始 DOM。Show More 从完整chat[]补挂楼层,不发起分页 I/O,不改变消息绝对索引。- 第一方完整保存通过统一 transport 直连;
/api/chats/save与/api/chats/group/save保留为扩展兼容路由。当前聊天业务入口仍经enqueueChatSave(),transport 自身不重复入队。落盘的 header metadata 统一取自persistedChatMetadata(),它去掉chat_metadata.lastInContextMessageId;不要另行拼装。 - commit 在首次异步让出前捕获逐记录 JSON 文本快照,再经 target-local commit session 编码、分帧和原子发布;保存期间的消息修改不混入本次提交。integrity 错误按明确的
code处理,不按错误文案猜测。 saveMetadata()只保存 header 的chat_metadata,通过同一 transport facade 发送独立 JSON 快照,不遍历或传输消息。它仍经当前聊天保存队列,但不取消挂起的完整保存、不保存消息派生缓存。角色与群聊、完整与 metadata 四种当前聊天写入共用runChatSave()的失败策略:integrity 弹窗与强制完整保存恢复都留在同一次队列任务中,其他失败提示并抛出。- 新群聊在问候扩展事件前绑定 metadata 并落盘初始 header,保证 metadata 保存的文件存在前提;后续初始化不得覆盖事件修改。修改消息的扩展需显式调用完整保存。
- tail/before/beforePages 只属于
api.chat.history与 Agent 的显式只读查询,不参与前端当前聊天状态。 - 合法 JSONL 任一记录解析失败时整体加载失败;不得提交部分历史或静默降级。
完整现状见 docs/CurrentState/ChatPayload.md。
6.2 ChatSurface 所有权与 participant
chat[]仍是完整、唯一的数据事实源;ChatSurface 只拥有#chat > .mes的当前视图投影。installChatSurfaceRuntime()是script.js唯一的 concrete composition seam;结构/投影位于 kernel,生命周期协调位于 services,真实 DOM/scroll 写入位于 adapters。- 外部 renderer 通过
window.__TAURITAVERN__.api.chatSurface.registerParticipant()接入,不直接控制投影,也不依赖伪造消息事件。 - 异步模板通过独立的
registerContentProcessor()准备显示 HTML;content-preparation.js保存消息内容结果,滚动重挂载不重复求值。manifesthooks.chatSurface在首次投影前完成注册,内容结果通过既有同步事务交给 participant。 - 最终内容统一通过
getMessageTextHTML()格式化;核心调用finalizeMessageContent(messageId, event?, ...args)完成内容提交后再发送相应消息事件。刷新只替换内容,控制器的commitContent()只提交已准备内容,不回到预处理入口。 - renderer 在入口只调用一次
isManagedOwnershipRequired():true时只启用 participant owner,false时只启用原 static owner;API 是否存在和当前 DOM 形状都不能替代该决策。 - 结构 reconcile 与纯 range projection 分离:前者只在 canonical
chat[]结构改变时 O(N) 执行,后者携带 epoch/revision token 并保持 O(M)。 - mount lease 与 content lease 分离;streaming 中间帧只提交内容,最终帧才恢复 decorator/runtime。
chat_virtualization_enabled=false是默认值并保持上游 Show More 语义;TauriTavern Settings 的“聊天 DOM 虚拟化”开关直接绑定这个布尔值并在保存后 reload。用户显式开启后,experimental bounded 使用 TanStack 几何提交V ∪ T,true tail 常驻且.mes上界为32 + 1。- TanStack symbols 由
lib.jsvendor facade 在 composition root 注入单一 adapter;原生 ESM 源码不直接解析 bare package。virtualizer 的scrollToFn也注入既有 ChatScrollAdapter,因此#chat的物理滚动写入仍只有一个 owner。 - bounded 的 runtime candidate 由独立 grant lease 管理;viewport 滚动可以 revoke/re-grant,而不会伪造 content commit 或消息业务事件。
- runtime demand 只按 visible 与 overscan 排序;常驻 true tail 不额外预留 runtime 名额。真实全局 reflow 会保持独立 follow-tail 意图,或以稳定消息 key + clamp 后的楼内 offset 恢复锚点;普通 settings save 与 IME height-only resize 不再清空全部历史测量。
- 可由
chat[mesid]推导的楼层状态由 materializer 负责;bounded DOM commit 后,私有syncMountedChatViewState()在最终测量前幂等重放 swipe/delete 等临时核心 UI。该 seam 不发送 SillyTavern 业务事件。 - 旧扩展直接删除消息 root 时,兼容桥只负责释放相应 root 的 lease;它不会接管第三方的内容写入或重编号。bounded policy 必须先通过 participant capability gate。
- Project raw API 见
docs/API/ChatSurface.md。
6.3 Theme 作用域与所有权
power_user.theme始终表示当前实际生效的命名 Theme;主题 CRUD、斜杠命令与扩展上下文继续遵循这一上游语义。power_user.theme_fallback保存无局部绑定时的全局 Theme;系统动态主题只更新该 fallback,不覆盖当前聊天、角色或群组绑定。- Theme 解析顺序固定为
chat_metadata.theme -> character/group binding -> theme_fallback。角色绑定以未经转换的 avatar filename 为键,群组绑定以 group id 为键。 - Theme 文件只保存可移植的外观快照,不包含本机角色或聊天身份。聊天绑定随 JSONL header metadata 持久化;角色与群组绑定随 settings 持久化。
- 所有生效切换必须经
src/scripts/power-user.js的统一入口;Tauri appearance adapter 不模拟#themes的 DOM change 事件。
6.4 Chat Completion 参数管理
实现位于 src/scripts/tauri/generation-params/,在 APP_READY 后导入并挂载。参数发现、渠道支持与值的读写沿用上游设置和控件。
- 移除请求参数表示本次生成不启用该参数,保留原值;移除开关表示关闭;隐藏本地区块不改变其内容或行为。旧预设缺少 Fast Mode 字段时明确关闭。
- 请求参数的移除状态保存在预设
extensions.tauritavern.omit_params,本地区块显隐仅保存在设备上。旧预设保持原行为,移除范围限于可选参数,不能删除messages、model等结构字段。 - Prompt 组装和生成判断使用本次生效设置,
createGenerationParameters()出口统一省略字段;用户显式配置的 Additional Parameters 仍拥有最终覆盖权。 - JSON 视图只编辑当前格式可用的参数,非法输入整体不应用;它不是最终请求预览,后续仍遵循渠道与模型的转换规则。
7. 插件系统前端适配
7.1 设计目标
- 保持上游
scripts/extensions.js的调用语义不变(manifest 结构、启用逻辑、依赖检查)。 - 将 Tauri 专属逻辑限制在独立 runtime 子模块,减少与上游同步冲突。
- 支持第三方插件从用户数据目录加载 JS/CSS/静态资源,不依赖 Node.js 后端。
7.2 模块分层
src/scripts/extensions.js:插件激活编排层(发现、排序、依赖/版本检查、触发加载)。src/scripts/browser-fixes.js:上游浏览器补丁(保持与 SillyTavern 同步)。src/tauri/main/compat/mobile/mobile-runtime-compat.js:Tauri mobile 运行时 polyfills(补齐旧 WebView 缺失 JS API)。src/tauri/main/compat/mobile/mobile-overlay-surface-admission.js:Tauri mobile 第三方 fixed overlay admission(分类 + 契约输出)。src/tauri/main/compat/mobile/mobile-overlay-compat-controller.js:Tauri mobile overlay compat controller(观察 + 有界 settle window,暴露window.__TAURITAVERN_MOBILE_OVERLAY_COMPAT__)。src/tauri/main/compat/mobile/mobile-iframe-viewport-contract-bridge.js:Same-origin iframe 的 viewport/inset contract bridge(viewport-host边界变量同步)。src/scripts/extensions/runtime/resource-paths.js:扩展资源路径规范化与 third-party 判定。src/scripts/extensions/runtime/tauri-ready.js:等待__TAURITAVERN_MAIN_READY__,避免 bridge 未就绪时提前加载。src/scripts/extensions/runtime/third-party-runtime.js:第三方扩展样式兼容层(legacy WebView 下为样式 URL 附加ttCompat=layer,触发 Rust 端点做@layer展平;不再走前端预取/Blob 注入)。src/scripts/extensions/runtime/asset-loader.js:脚本与样式注入、超时保护、重复注入幂等控制。
7.3 端到端加载链路
loadExtensionSettings()先等待waitForTauriMainReady()。- 前端通过
/api/extensions/discover获取扩展列表与类型,读取 manifest 并进入activateExtensions()。 - 对每个扩展执行
addExtensionLocale()+addExtensionScript()+addExtensionStyle()。 - 当扩展为
third-party/*时:- JS 入口脚本直接从
/scripts/extensions/third-party/*加载(真实同源静态资源端点)。 - CSS 仅在旧 WebView 不支持
@layer时由 runtime 附加ttCompat=layerquery;由 Rust 端点返回展平后的 CSS bytes(否则仍走原始 URL)。
- JS 入口脚本直接从
/scripts/extensions/third-party/*由 Rust 协议层端点提供(WebViewon_web_resource_requesthook),统一返回 bytes +Content-Type+ 404 语义。
7.3.1 当前实现结论
- 当前实现已经从“前端模拟静态文件服务”收敛为“前端只负责编排,Rust 负责 third-party 资源端点”。
src/scripts/extensions/runtime/third-party-runtime.js不再承担 JS 源码重写或伪服务器职责,主要只保留第三方样式兼容修复。- 面向持续开发的现状说明见
docs/CurrentState/ThirdPartyExtensions.md;涉及实现边界或改动前,先读该文档,再决定是改前端 runtime 还是改后端资源端点。
7.3.2 First-party React extension
TauriTavern 自有的状态型 UI 作为 SillyTavern first-party extension 挂载 React client island。当前 Agent、MCP 与 Settings owned scope 均遵循这一基线:
- manifest、locale 和 SmartTheme CSS 仍遵循现有扩展资源契约;不引入 Next.js、独立页面 shell 或第二套路由。
src/index.tsx只负责等待 Host ABI、解析 concrete actions、创建 extension container 与createRoot()。- React presentation 只接收 strict typed initial state、actions 与 translator;不得直接
invoke()、操作 jQuery 或读取 Rust command 名。 - 跨窗口和长期数据继续由 Rust/Host service 拥有;局部 React state 只表示当前视图,不能成为平台事实源或持久 cache。
- 样式必须使用
--SmartTheme*、字体、动画与边框变量,保留 SillyTavern vanilla 视觉和用户主题覆盖能力。 tsconfig.ui.json、React Hooks/TypeScript lint 与 Rstest/Testing Library 显式覆盖 Agent、MCP 和src/scripts/tauri/setting三个 owned scope;pnpm check是统一验收入口。- production 与 development 共用
createRspackConfigs(mode)。标准 Tauri dev server 在首次 development 编译成功后才监听,并只在成功重编译后 reload。 scripts/check-first-party-ui-guardrails.mjs为自有扩展架构限定为 React + Strict TypeScript。 当前工程基线、冻结 handle 与 bundle 数据见docs/CurrentState/FirstPartyUI.md。
7.4 契约与约束
- third-party 扩展命名约定为
third-party/<folder>,前后端均按该约定解析。 - 扩展命令参数统一使用 camelCase(如
extensionName),避免 invoke 参数缺失。 - 客户端版本检查仍遵循上游格式:
SillyTavern:<version>:TauriTavern,用于minimum_client_version判断。 - 拦截器是否接管请求由
router.canHandle(method, path)决定,不再维护分散的路径白名单。 /api/extensions/branches返回远端 branch 的{ name, commit, current, label };label当前有意为空,前端仅在非空时显示。/api/extensions/switch接受 branch 短名并在成功时返回空204;现有 reload/settings 时序保持不变,不新增 switch hook或借机触发 update hook。
7.5 常见问题定位
Extension module is not JavaScript:- 通常表示拿到了 HTML 回包而非模块文件。
- 优先检查
/scripts/extensions/third-party/*是否被协议层端点正确响应(应返回 404 或 JS bytes,而不是index.html)。
missing required key extensionName:- 表示 invoke 参数命名不匹配,检查路由 body -> 命令参数映射。
- legacy WebView 样式
@layer仍不生效:- 检查样式请求是否带
?ttCompat=layer;并验证/scripts/extensions/third-party/*端点返回的是text/cssbytes(非 404/HTML)。
- 检查样式请求是否带
7.6 后续开发规则
- 新增插件加载能力时,优先扩展
src/scripts/extensions/runtime/*,不要把 Tauri 细节回灌到extensions.js。 - 新增插件 API 时,优先在
src/tauri/main/routes/extensions-routes.js封装,再通过context.safeInvoke()调 Rust 命令。 - 若调整 third-party 静态资源路径约定,必须同时更新
resource-paths.js与 Rust 协议层端点的前缀解析逻辑。
7.7 移动端插件兼容(新增)
7.7.1 JS 运行时兼容(Android 旧 WebView)
- 实现位置:
src/tauri/main/compat/mobile/mobile-runtime-compat.js。 - 入口:
src/tauri/main/bootstrap.js中安装(仅 Tauri mobile)。 - 行为:基础 API 仅在缺失时补齐;Android 的 Web Clipboard 写入统一映射到原生写入器;整体只执行一次。
- 当前按需补齐:
Array.prototype.atString.prototype.atArray.prototype.findLastArray.prototype.findLastIndexArray.prototype.toSortedArray.prototype.toReversedObject.hasOwnnavigator.clipboard.writeText(仅 Android;保留 Clipboard 对象上的其他方法)
该策略用于修复移动端第三方插件在初始化阶段出现的 TypeError: *.at is not a function,以及 Android WebView 拒绝 Web Clipboard 写入的问题。TauriTavern 第一方代码在所有平台直接使用 src/tauri-bridge.js 的原生写入器;宿主只授予 clipboard-manager:allow-write-text,不授予剪贴板读取能力,也不需要申请操作系统运行时权限。
7.7.2 CSS @layer 降级(Android 旧 WebView)
- 实现位置:
src/scripts/extensions/runtime/third-party-runtime.js(样式加载链路)。 - 触发条件:
- 样式内容包含
@layer; - 当前 WebView 不支持 CSS Cascade Layers。
- 样式内容包含
- 处理方式:
- runtime 将样式 URL 改写为
...?ttCompat=layer; - Rust 协议层端点识别该 query 并移除
@layer包裹(展平层级),返回可被旧 WebView 直接应用的 CSS。
- runtime 将样式 URL 改写为
该策略用于修复移动端插件面板(如 TH-custom-tailwind)样式大面积失效导致的布局错乱。
7.7.3 浮层 safe-area 修正(移动端)
- 实现位置:
- 分类/契约输出:
src/tauri/main/compat/mobile/mobile-overlay-surface-admission.js - 观察与有界 settle window:
src/tauri/main/compat/mobile/mobile-overlay-compat-controller.js - 同源 iframe bridge:
src/tauri/main/compat/mobile/mobile-iframe-viewport-contract-bridge.js
- 分类/契约输出:
- 入口:
src/tauri/main/bootstrap.js中安装(仅 Tauri mobile)。 - 触发条件:只处理“第三方顶层 surface”候选(通常为
position: fixed且顶边贴近 0 的窗口/遮罩)。 - 处理策略(两段式):
- JS classifier:观察
document.body直系子节点增删,并对script_idportal root 扫描其子树;对已跟踪候选仅监听自身生命周期属性(class/style/hidden/open/aria-hidden)以撤销/恢复 host-admitted contract,属性重算按 animation frame 合并;稳定的free-window只响应 inline lifecycle style(display/visibility/position/pointer-events/cursor/touch-action)变化,几何类 style 写入保持在拖动热路径之外;对命中元素分类并输出:data-tt-mobile-surface="backdrop|viewport-host|fullscreen-window|free-window|edge-window"data-tt-mobile-surface-admitted="1"(host-private sentinel)--tt-original-top=<px>(仅 edge-window)
- CSS contract:由
mobile-geometry-firewall.js提供[data-tt-mobile-surface="..."]的几何规则,统一执行 safe-area 约束(backdrop 保持 full-bleed)。
- JS classifier:观察
- 显式 opt-in:若节点已带
data-tt-mobile-surface,classifier 将尊重并不再改写(便于第三方脚本作者自我修复)。 - Android 变量语义:
--tt-inset-top表示当前布局应避开的有效 inset;非沉浸模式下反映顶部 safe area,沉浸模式下回落为0,因此对应的 contract 会自然退化为 full-bleed。
该策略用于修复 JS-Slash-Runner 等脚本在运行时注入固定定位弹窗样式时,关闭按钮落入状态栏导致不可点击的问题。
7.7.4 调试建议
- 若看到
*.at is not a function:- 检查是否为 Tauri mobile 会话,并确认
window.__TAURITAVERN_MOBILE_RUNTIME_COMPAT__ === true。
- 检查是否为 Tauri mobile 会话,并确认
- 若 Android 复制失败:
- 检查
plugin:clipboard-manager|write_textinvoke 的拒绝原因;该链路不会静默回退到 Web Clipboard。
- 检查
- 若插件样式错乱但 CSS 已成功请求:
- 优先检查是否命中
@layer降级分支; - 关注
resolveStylesheetUrl()是否返回带ttCompat=layer的 URL。
- 优先检查是否命中
- 若脚本弹窗贴顶到状态栏:
- 检查脚本是否通过
<style>或行内style设置了固定定位顶边; - 检查
window.__TAURITAVERN_MOBILE_OVERLAY_COMPAT__是否已安装。
- 检查脚本是否通过
7.8 嵌入式运行时(Embedded Runtime,消息内 iframe)
架构状态:仅保留为 static compatibility。它不是 ChatSurface bounded 路线的 runtime owner;新虚拟化开发不得继续扩展 parking/ghost/slot 体系。
该旧机制把消息内 iframe 包装成带预算、park/hydrate 与自愈的 slot,并在 static 消息重渲染时尽量避免 iframe teardown。bounded 启动会跳过该 chat owner;renderer 必须通过唯一 ChatSurface participant 接入,两套 owner 不得并存。
当前落地点(代码):
- 安装入口:
src/tauri/main/services/embedded-runtime/install.jsbootstrap.js在 main ready 后按 bootstrap mirror 决定是否加载;在APP_READY后安装 chat adapters。
- Manager 与 profiles:
src/tauri/main/services/embedded-runtime/*- 全局调试入口:
globalThis.__TAURITAVERN_EMBEDDED_RUNTIME__ - 配置来源:
tauritavern-settings.embedded_runtime_profile - bootstrap mirror:
localStorage tt:embeddedRuntimeProfile = 'off' | 'auto' | 'compat' | 'mobile-safe' - 旧版
localStorage tt:runtimeProfile仅用于迁移
- 全局调试入口:
- DOM detectors:
src/tauri/main/adapters/embedded-runtime/*-runtime-adapter.js- 已支持:JS-Slash-Runner(
.TH-render)与 LittleWhiteBox(.xiaobaix-iframe-wrapper)。
- 已支持:JS-Slash-Runner(
- 消息写入 facade:
src/scripts/tauri/message/mes-text-write.js- 上游调用点统一依赖 facade;
off时直接恢复普通.mes_textHTML 写入语义。
- 上游调用点统一依赖 facade;
- 渲染事务:
src/tauri/main/adapters/embedded-runtime/message-render-transaction.js- 作为 facade 在 ER 开启时的底层实现,避免把 iframe runtime 当成普通 DOM 反复销毁重建。
当前边界说明:
- 已纳入管控:消息内 iframe runtime(JSR/LWB)。
- 暂不纳入:面板类 runtime 的 park(目前依赖浏览器本身回收即可)。
- 上述边界只描述 legacy profile 的当前事实,不是后续 bounded 架构的扩展清单。
更多“当前如何工作/哪些契约不能破坏/回归点”见:docs/CurrentState/EmbeddedRuntime.md。
8. 兼容层策略
src/tauri-main.js:新主入口(推荐)。- 新开发统一集中在
src/tauri/main/*,避免重复实现与多处注入链路并存。
9. 如何新增一个 Tauri 注入接口
- 在 Rust 后端新增/确认命令(
src-tauri/crates/tauritavern/src/presentation/commands/*)。 - 若宿主层会调用该命令,将命令名加入
src/tauri/main/kernel/invokes/tauri-commands.js(TauriInvokeCommand,避免字符串拼写漂移)。 - 若该命令为高频/可合并写入的调用,按需在
src/tauri/main/kernel/invokes/invoke-policies.js增加/调整策略(dedupe / write-behind)。 - 在
src/tauri/main/routes/对应业务域中新增路由:路由层禁止直接引用window,需要浏览器能力时下沉到adapters/或services/。 - 路由内只做参数校验、DTO 组装、
context.safeInvoke调用;错误直接暴露(避免 silent fallback)。 - 保持返回结构稳定(状态码 + JSON 结构),避免破坏上游前端调用假设。
- 跑
pnpm run check,确保完整工程门禁通过。
10. 调试与验证
建议最小验证流程:
pnpm run checkpnpm run dev- 启动后确认:
- 首屏加载正常
- 不再出现 CSRF 初始化错误
- 角色/聊天/设置等核心接口可用
如需快速定位问题:
- 查看 DevTools 中请求是否命中本地注入路径。
- 查看控制台
invoke报错信息与路由返回状态码。 - 检查对应
routes/*是否遗漏请求字段映射。
10.1 轻量性能仪表(Perf HUD)
用于快速定位移动端/低端机型的主线程卡顿、DOM 膨胀、以及 invoke 热点。
- 默认关闭:未启用时不会加载 HUD 模块,也不会包裹
context.safeInvoke(prod 默认近似零成本)。 - 启用(需 reload 才能抓启动打点):
- 控制台:
localStorage.setItem('tt:perf','1'); location.reload(); - 或 URL:
?ttPerf=1
- 控制台:
- 启用后等待就绪:
await window.__TAURITAVERN_PERF_READY__ - 常用导出命令:
window.__TAURITAVERN_PERF__.downloadReport()下载 JSON(便于交给 AI 分析)window.__TAURITAVERN_PERF__.exportJson({ includeResources: true })直接拿到 JSON 字符串await window.__TAURITAVERN_PERF__.copyReport()通过原生写入器复制到剪贴板
- HUD 操作:拖动标题栏移动(位置持久化),点击标题栏展开/收起;桌面端可用
Ctrl+Alt+P切换开关。
11. 工程守护(Guardrails + 类型检查)
目标:把宿主层(src/tauri/main/*)限制在可长期维护的规模与依赖形态,避免再次回到单文件膨胀与隐式耦合。
- 一键检查:
pnpm run check(宿主边界、类型、架构边界、全部测试与 Clippy)。 - Guardrails(
scripts/check-frontend-guardrails.mjs):- 行数预算:默认单文件
<= 500行;关键聚合文件受scripts/guardrails/frontend-lines-baseline.json的基线约束。 - 依赖边界:
kernel/ports不得 importservices/routes/adapters;services不得 importroutes。 - 路由契约:
src/tauri/main/routes/*禁止直接引用window(需要触碰浏览器/DOM/上游 ST 时,新增adapters/*)。
- 行数预算:默认单文件
- 类型检查(
tsc -p tsconfig.host.json):strict+exactOptionalPropertyTypes+noUncheckedIndexedAccess等强约束。- JS 文件默认不强制检查;需要在文件头加
// @ts-check并配合 JSDoc(Host Kernel 目录已按此标准化)。