# TauriTavern 前端指南 本文档描述 TauriTavern 当前前端(基于 SillyTavern 1.18.0)在 Tauri 环境下的集成架构与开发方式。 宿主层对外契约清单见:`docs/FrontendHostContract.md`(重构时优先保障其不回归)。 ## 1. 目标与原则 - **最小侵入**:尽量保持上游 SillyTavern 前端行为不变。 - **模块化**:将 Tauri 注入逻辑拆分为独立模块,避免单文件膨胀。 - **低耦合**:路由注册、请求拦截、业务上下文分离。 - **入口收敛**:统一走 `init.js -> tauri-main.js -> tauri/main/*`,减少重复入口。 ## 2. 启动链路 当前前端启动顺序如下: 1. `src/init.js` 动态导入:`lib.js` -> `tauri-main.js` -> `script.js` 2. `src/lib.js` 静态导入 `src/dist/lib.core.bundle.js`,统一提供 ESM 导出;`highlight.js` 通过 `getHljs()` 按需加载 3. `src/tauri-main.js` 仅调用 `bootstrapTauriMain()`(薄入口) 4. `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. 目录结构(前端集成相关) ```text 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. 请求注入流程 1. 前端发起 `fetch('/api/...')` 或 `$.ajax('/api/...')` 2. 拦截器通过 `router.canHandle(method, path)` 判断是否由本地路由接管 3. 命中后交给路由分发到 `routes/*` 4. 路由通过 `context.safeInvoke(...)` 调用 Rust 命令 5. 返回标准 `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` 加载全部楼层,可选[历史滑动按需加载](CurrentState/ChatPayload.md#21-历史滑动按需加载);`/api/chats/get` 与 `/api/chats/group/get` 保留为扩展和脚本的兼容路由。header 与完整、有序的消息数组分离后,generation、扩展和保存共享同一个 canonical `chat[]`。 - `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` 保存消息内容结果,滚动重挂载不重复求值。manifest `hooks.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.js` vendor 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 端到端加载链路 1. `loadExtensionSettings()` 先等待 `waitForTauriMainReady()`。 2. 前端通过 `/api/extensions/discover` 获取扩展列表与类型,读取 manifest 并进入 `activateExtensions()`。 3. 对每个扩展执行 `addExtensionLocale()` + `addExtensionScript()` + `addExtensionStyle()`。 4. 当扩展为 `third-party/*` 时: - JS 入口脚本直接从 `/scripts/extensions/third-party/*` 加载(真实同源静态资源端点)。 - CSS 仅在旧 WebView 不支持 `@layer` 时由 runtime 附加 `ttCompat=layer` query;由 Rust 端点返回展平后的 CSS bytes(否则仍走原始 URL)。 5. `/scripts/extensions/third-party/*` 由 Rust 协议层端点提供(WebView `on_web_resource_request` hook),统一返回 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/`,前后端均按该约定解析。 - 扩展命令参数统一使用 camelCase(如 `extensionName`),避免 invoke 参数缺失。 - 客户端版本检查仍遵循上游格式:`SillyTavern::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/css` bytes(非 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.at` - `String.prototype.at` - `Array.prototype.findLast` - `Array.prototype.findLastIndex` - `Array.prototype.toSorted` - `Array.prototype.toReversed` - `Object.hasOwn` - `navigator.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。 该策略用于修复移动端插件面板(如 `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_id` portal 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=`(仅 edge-window) - CSS contract:由 `mobile-geometry-firewall.js` 提供 `[data-tt-mobile-surface="..."]` 的几何规则,统一执行 safe-area 约束(backdrop 保持 full-bleed)。 - 显式 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`。 - 若 Android 复制失败: - 检查 `plugin:clipboard-manager|write_text` invoke 的拒绝原因;该链路不会静默回退到 Web Clipboard。 - 若插件样式错乱但 CSS 已成功请求: - 优先检查是否命中 `@layer` 降级分支; - 关注 `resolveStylesheetUrl()` 是否返回带 `ttCompat=layer` 的 URL。 - 若脚本弹窗贴顶到状态栏: - 检查脚本是否通过 `