TauriTavern/docs/CurrentState/StartupOptimization.md
Darkatse 2eecdefe51 feat(startup): show a dialog when startup fails
A failed startup removed the status hint and left a frozen window, with the error only in the log. Module loading in init.js and firstLoadInit now show one modal dialog with the error, stage and stack, a native clipboard copy and reload, and still throw the original error. In init.js the dialog module loads off the error path, since a module-loading failure can take the dialog's own imports down with it.

The dialog renders in a shadow root, and its host resets inherited styles inline because page rules outrank :host. It reopens when closed instead of cancelling the close request, because Chromium ignores preventDefault() on a close request without user activation.

Refs #332
2026-10-07 18:29:30 -04:00

196 lines
12 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.

# 开屏启动优化(Stage-based Boot)现状
本文档描述 **当前已经落地** 的开屏启动优化实现:它解决什么问题、端到端链路如何工作、阶段语义与契约是什么、以及后续开发最容易误改的边界。
> 规划与设计背景见:`docs/StartupOptimizationPlan.md`(本文只写“现在怎么跑”)。
---
## 1. 范围与结论
当前开屏优化的核心落点是把启动从“一个巨大的串行 init”拆成 **可见可点(Shell)→ 核心可用(Core)→ 主应用可交互(Full / APP_READY)**,并同时降低首屏 JS/库解析负担。
已落地的关键点:
- **Shell 先到**:`#preloader` 在 `firstLoadInit()` 的 Shell 阶段就移除,主页可以尽早可见可点。
- **Host Ready 显式等待**:在首次 `/api/*` 访问前等待 `__TAURITAVERN_MAIN_READY__`,确保拦截器/路由已安装,且 Tauri 后端已进入可接收 `AppState` 命令的 readiness 状态。
- **启动并发读取**:设置与启动元数据并行读取、按顺序应用,并复用本次结果。
- **扩展启动分层**:扩展发现可提前后台启动;系统扩展仍在 Full 阶段完成,local/global third-party 扩展延后到 `APP_READY` 后串行激活,从启动关键路径移出。
- **lib.bundle 拆分为 core/optional**:`highlight.js` 通过 `lib.js` 的 async helper 按需加载;上游同步 `/lib.js` ABI 保留在 core bundle。
- **重任务后移**:`initTokenizers()`、`initScrapers()` 在 `APP_READY` 后、两次 paint 之后后台启动。
---
## 2. 端到端链路(从 index.html 到 APP_READY)
### 2.1 HTML 入口与预加载遮罩
- `src/index.html`
- 首屏遮罩:`<div id="preloader"></div>`
- 模块入口:`<script type="module" src="init.js"></script>`
- 约束:不使用 modulepreload(移动端 WebView 上曾有缓存/预加载失败问题)。
### 2.2 Bootloader:分层 import + Android import 重试
- `src/init.js`
- 设置:`window.__TAURI_RUNNING__ = true`,并计算 `globalThis.__TAURITAVERN_PERF_ENABLED__`
- 通过 `importWithRetry()` 依次加载:
1) `./lib.js`(静态依赖 `src/dist/lib.core.bundle.js`)
2) `./tauri-main.js`(安装 Host Kernel:路由/拦截器/ABI)
3) `./script.js`(SillyTavern 主前端)
- 目的:在 Android WebView 首启 I/O 抖动场景下提高模块加载确定性。
### 2.3 Host Kernel:把同源 /api 变成可路由端点
- `src/tauri/main/bootstrap.js`
- 安装 `window.__TAURITAVERN__`(稳定 Host ABI):invoke broker / 资源路径 helpers 等
- Patch:
- `fetch` 拦截(同源、命中路由表则走 `router.handle(...)`)
- `jQuery.ajax` 拦截(同源、命中路由表则转发)
- same-origin iframe/window 的补丁(保证扩展/脚本在 iframe 内也能命中路由/下载桥)
- 设置 readiness:
- `window.__TAURITAVERN_MAIN_READY__ = readyPromise`
- 前端用 `waitForTauriMainReady()` 等它,确保首次 `/api/*` 调用前 Host 已就绪,且 Rust `BackendReadiness` 已完成。
- Host Ready 后只加载不会传递导入主应用的模块。设置入口先注册同步/配对监听,事件呈现与设置 UI 共用一个 `APP_READY` Promise;请求参数面板本身依赖主应用,直接在 `APP_READY` 后导入。
### 2.4 前端启动编排:Shell → Core → Full(保持 APP_READY 语义)
- `src/script.js:firstLoadInit()`
- **Shell 阶段**
- `initLocales()` 先于任何可见界面:语言数据是静态资源,不依赖 Host 就绪;首帧界面与启动状态文案都已是用户语言
- `removePreloader()`(`src/scripts/loader.js`):移除 `#preloader`
- 初始化纯前端 UI/DOM handler、基础 patch(不会做 `/api/*`)
- **Core 阶段**
- `await waitForTauriMainReady({ failFast: true })`:保证 Host 拦截器与 Rust backend readiness 就绪
- `/csrf-token` + 并发启动 `/api/bootstrap` 与 `/api/settings/get`
- `initSecrets()` + `primeSecretStateSnapshot(...)` + `readSecretState()`
- 默认 slash commands、模型/设置等核心模块初始化
- **Full 阶段**
- 按设置、角色、群组、头像的顺序应用读取结果。
- 扩展(如果启用):
- 后台:`startOfflineExtensionsDiscovery()`(可在 Core 阶段提前启动)
- Full:`activateStartupSystemExtensions({ parallelism })` 只激活系统扩展,并在需要时提前完成 Extras auto-connect
- 对外事件保持原语义:
- `event_types.APP_INITIALIZED` → `event_types.APP_READY`
- **Post-ready(后台)**
- `initTokenizers()`、`initScrapers()`:在 `APP_READY` 后异步执行(避免阻塞首屏)。
- 若存在启用中的 third-party 扩展:`activateDeferredThirdPartyExtensions({ parallelism: 1 })` 在首屏完成后后台激活,并在完成后 emit `EXTENSION_SETTINGS_LOADED`。
---
## 3. 阶段语义与全局信号(契约)
### 3.1 内部阶段(UI 体验)
- `Shell`:允许 UI 可见/可点;**不允许依赖扩展已激活**。
- `Core`:允许进行首次 `/api/*` 与核心数据加载;扩展仍可能未激活。
- `Full`:完成主应用可交互所需初始化与系统扩展激活;third-party 扩展可能仍在后台补全。
当前暴露的信号:
- `globalThis.__TAURITAVERN_STARTUP_STAGE__`:由 `firstLoadInit()` 写入(shell/core/full)
- `event_types.APP_READY`:作为“主应用可交互”的稳定语义;晚加载的扩展依赖其 auto-fire 行为完成 ready 钩子
### 3.2 Host 就绪(拦截器、路由与 BackendReadiness)
- `window.__TAURITAVERN_MAIN_READY__`:在 `src/tauri/main/bootstrap.js` 设置
- `waitForTauriMainReady()`:前端轮询等待该 promise 注册并 resolve(`src/scripts/extensions/runtime/tauri-ready.js`)
- Tauri runtime 下,Host 初始化链会在任何需要 `AppState` 的首批工作前调用 `wait_for_backend_ready`,避免依赖 Tauri 的 “state not managed” 错误文本重试。
---
## 4. 设置与 bootstrap 元数据读取
`/api/settings/get` 独立读取设置;`/api/bootstrap` 提供角色、群组、头像和密钥状态等启动元数据。前端并发获取,再按既有顺序应用;本次启动复用读取结果,避免重复请求。
设置与角色是启动必需数据,失败会阻止启动;群组、头像或密钥状态失败时保留可见错误并仅禁用对应能力,不阻止 `APP_READY`。
启动失败只有两个出口:`src/init.js` 加载应用模块失败,或 `firstLoadInit()` 抛错。两处都调用 `showStartupFailure(error)` 显示不可关闭的模态失败对话框(错误、阶段与调用栈,可复制或重新加载),并继续抛出原错误,日志照常记录。展示不能替换原错误:`init.js` 的失败可能来自模块加载本身,所以对话框模块在错误路径之外加载,加载失败时作为第二个错误上报。对话框在 Shadow DOM 中渲染,不受主题与自定义 CSS 影响。
后端采样入口见 [bootstrap_commands.rs](../../src-tauri/crates/tauritavern/src/presentation/commands/bootstrap_commands.rs)。
---
## 5. 扩展加载(离线发现 + 激活批处理)现状
> 详细扩展兼容链路见:`docs/CurrentState/ThirdPartyExtensions.md`
当前策略要点:
- 发现:`startOfflineExtensionsDiscovery()` 等待 Host Ready 后调用 `/api/extensions/discover`,并加载各扩展 `manifest.json`。
- 系统扩展激活:`activateStartupSystemExtensions({ parallelism })`
- 同 `loading_order` 的扩展分组;组内按 `parallelism` 分块 `Promise.all` 并在 chunk 间 `delay(0)` 主动让出事件循环。
- Android 设备默认 `parallelism = 1`;其他默认 `parallelism = 2`(见 `src/script.js`)。
- third-party 激活:`activateDeferredThirdPartyExtensions({ parallelism: 1 })`
- 仅处理 `local/global` 扩展,默认在 `APP_READY` 后执行,避免把大体量 third-party 模块求值放进启动关键路径。
- third-party 兼容(hljs):
- third-party 扩展激活前会 `await getHljs()`,确保 `window.hljs` 存在(`src/scripts/extensions.js`)。
---
## 6. lib.core / lib.optional(按需加载)现状
### 6.1 产物与入口
- Rspack 入口:`rspack.config.js`
- `lib.core` → `src/lib-bundle-core.js` → `src/dist/lib.core.bundle.js`
- `lib.optional` → `src/lib-bundle-optional.js` → `src/dist/lib.optional.bundle.js`
### 6.2 前端门面(lib.js)
- `src/lib.js`
- 静态 import core bundle,并 re-export 上游常用库
- 可选库按需加载:
- `getHljs()`:动态 import optional bundle;并注册 `stscript` 语言(`src/scripts/slash-commands/stscript-hljs-language.js`)
- `initLibraryShims()`:将少量库挂到 `window`(用于第三方扩展兼容);其中 `window._ = lodash` 是正式 ABI,需先于 third-party 扩展模块求值完成
### 6.3 代码高亮(延迟执行)
- `src/scripts/tauri/perf/code-highlight-coordinator.js`
- 通过 `IntersectionObserver` + `requestIdleCallback` 做“近视区”高亮
- 内部通过 `getHljs()` 拉起可选 bundle(不进入 Shell/Core 静态依赖图)
---
## 7. Panel Runtime 的扩展 DOM “停车”与兼容白名单
Panel Runtime 会在 `APP_READY` 后安装,用于在抽屉关闭时把部分面板 DOM 子树 park 到 `DocumentFragment`,减少低端设备的 DOM/observer 压力。
已知兼容点:
- `src/tauri/main/adapters/panel-runtime/extensions-subtree-gates.js`
- 当前对扩展设置容器启用“子树 gate + park/hydrate”
- **白名单永远保持连接**:`regex_container`、`qr_container`
- 目的:避免 SPresets 等脚本在抽屉关闭时找不到 `#saved_regex_scripts` 触发 `MutationObserver.observe(target not Node)`。
- `src/tauri/main/adapters/panel-runtime/top-settings-panel-parking.js`
- 左栏 pinned 锚点分两类,判据不同:
- `LEFT_NAV_REQUIRED_ANCHORS`(所有档位,正确性):面板外代码读取的节点保持在线,包括 `onModelChange` 读取边界值的 `#range_block_openai`,以及助手读取支持来源的 `#openai_reasoning_effort_block`。
- `LEFT_NAV_COMPAT_ANCHORS`(仅 `compat`,兼容让步):`#openai_api-presets`、`#completion_prompt_manager`,让第三方脚本在抽屉关闭时仍能选中。
- 世界书抽屉:`compat` 只 park 条目列表 `#world_popup_entries_list`(世界书 DOM 的主体),世界书下拉框、分页和编辑按钮保持在线;`aggressive` 仍 park 整个 `#wi-holder`。世界书模块持有列表节点,正常渲染与停放期间的更新使用同一个目标;重新打开只挂回原节点,不额外刷新。离屏更新仍有渲染计算,但停放期间的重型条目不连接 document,保留 DOM 裁剪收益;列表的分页后测量与条目定位跳过离屏节点。
---
## 8. 可观测性(perf + 状态)
- Perf 开关:`localStorage tt:perf = '1'` 或 URL `?ttPerf=1`
- 标记(部分):
- `src/init.js`:`tt:init:*`
- `src/tauri/main/bootstrap.js`:`tt:tauri:*`
- `src/script.js`:`tt:startup:shell/core/full` + `tt:startup:ready`
- 运行时提示:
- `src/scripts/tauri/startup/startup-status-overlay.js`:右下角非阻塞启动状态 overlay(`APP_READY` 后移除)与启动失败对话框(见 §4)
- `pnpm run test:browser` 构建 vendor bundle 并运行 `tests/browser/*.test.mjs`。其中 `startup-order.test.mjs` 验证 Host-ready 模块先加载时主应用仍能正常初始化、早到的事件等待应用就绪后呈现,以及启动失败时对话框显示错误且错误继续上报。该测试组包含在 `pnpm test` 和 `pnpm run check` 中。
---
## 9. 明确支持 / 不支持边界
支持:
- 主页尽早可见可点(Shell 先到)。
- `APP_READY` 表示主应用已可交互;late-loaded third-party 扩展依赖其 auto-fire 语义继续完成初始化。
- third-party 扩展资源同源加载,并提供 `window._`(lodash)与 `window.hljs` 兼容(见上)。
不保证(需要按需扩展白名单/契约):
- 任何第三方脚本在 **扩展抽屉关闭** 时都能访问到其期望的“扩展设置 DOM”(Panel Runtime 可能 park 子树;目前只对白名单容器强保证)。