docs/design/2026-07-31-desktop-web-shell-release.md
当前桌面 PoC 已证明 Tauri 可以复用 daemon 提供的 Web Shell,而不需要维护第二套 UI。但 PoC 仍缺少公开发布所需的用户流程、故障恢复、签名更新、安全边界和三平台安装产物。
本设计把 packages/desktop-shell 完善为薄桌面壳:桌面壳只负责生命周期与平台集成,产品功能继续由 qwen serve 和 @qwen-code/web-shell 提供。
flowchart LR
A[Tauri bootstrap] -->|选择并持久化 workspace| B[Desktop runtime manager]
B -->|spawn process group| C[Bundled Node + qwen serve]
C -->|authenticated loopback URL| D[Existing Web Shell]
A -->|retry / choose workspace / logs| B
B -->|exit event| A
E[GitHub latest.json + installers] -->|signed updater| B
| 组件 | 职责 |
|---|---|
| bootstrap 页面 | 启动状态、工作区选择、失败恢复、版本与日志入口 |
| Rust 桌面状态 | 设置持久化、窗口状态、runtime 生命周期、单实例、更新状态 |
| bundled runtime | 当前平台 Node.js、Qwen Code bundle、Web Shell 静态资源 |
| 发布 CI | 三平台构建、签名、公证、smoke、校验和、latest.json、GitHub Release |
| 状态 | 用户看到的内容 | 可用操作 |
|---|---|---|
starting | Qwen Code 品牌启动页和当前工作区 | 等待 |
needs_workspace | 首次启动工作区选择 | 选择目录 |
ready | daemon-served Web Shell | 正常使用 |
failed | 精简错误摘要 | 重试、选择其他目录、打开日志 |
stopped | daemon 意外退出提示 | 重启 daemon、选择目录、打开日志 |
应用先创建 bootstrap 窗口,再异步启动 daemon。daemon 深度健康检查(/health?deep=true)通过后,同一个窗口导航到 http://127.0.0.1:<port>/#token=<token>。token 只存在于 URL fragment 中,永远不会随请求发往服务端,因此不需要 cookie 握手,也不会进入 access log 或 Referer。这样慢启动和失败路径都有可见 UI。
必须使用深度健康检查:serve fast path 在真正的 runtime(含 Web Shell)挂载之前,就会用 bootstrap app 应答浅层 /health。此时 /health?deep=true 仍返回 503 {"reason": "bootstrap"},因此只有它变为 200 才代表 Web Shell 可用;若用浅层健康检查判定就绪,导航会撞进 deferred runtime 窗口。
设置文件存储于 Tauri app_config_dir 下的 desktop-state.json:
{
"workspace": "/absolute/path",
"window": {
"width": 1280,
"height": 820,
"x": 120,
"y": 80,
"maximized": false
}
}
启动优先级:
QWEN_DESKTOP_WORKSPACE,用于开发和自动化测试。只有已存在且为目录的绝对规范路径会传给 daemon。选择新的工作区时先停止当前 process group,再用新目录重新启动。
QWEN_SERVER_TOKEN)下发给 daemon,并通过 URL fragment(/#token=<token>)交给 Web Shell 前端;前端读取后从 URL 中清除,并以 Authorization: Bearer 头调用 API。fragment 不会发送到服务端,因此不需要 cookie。127.0.0.1 随机端口并启用 --require-auth。runtime-stopped 事件并返回 bootstrap 故障页。default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'。http、https、mailto 外链交给系统浏览器;file、javascript、自定义协议拒绝。invoke command。asInvoker、Common Controls v6 和 long-path awareness。prepare-runtime.js 生成:
manifest.json:桌面版本、Qwen Code 版本、Qwen Code commit、Node 版本、target、构建时间。checksums.json:所有 bundled runtime 文件的 SHA-256。LICENSE 和桌面 NOTICE。LICENSE。打包前 smoke 会校验 manifest、关键文件和 checksum。GitHub Release 同时发布每个安装产物的 SHA256SUMS.txt。
Tauri updater 使用签名更新产物和固定公开 key。应用启动后后台检查一次更新:
发布 CI 使用 TAURI_SIGNING_PRIVATE_KEY 与 TAURI_SIGNING_PRIVATE_KEY_PASSWORD 生成 updater signatures。latest.json 指向同一 GitHub Release 的平台更新包。只有非 draft、非 prerelease 发布会更新固定的 desktop-latest feed release。
| 平台 | 架构 | 安装包 | 签名要求 |
|---|---|---|---|
| macOS | arm64、x64 | .dmg、.app.tar.gz updater | Developer ID Application + notarization |
| Windows | x64 | NSIS .exe updater/installer | Authenticode SHA-256 + timestamp |
| Linux | x64 | .AppImage updater/installer、.deb | updater minisign;无 OS code-signing |
Windows WebView2 使用 download bootstrapper;系统离线且缺失 WebView2 时安装失败会明确提示依赖。Linux CI 安装 Tauri WebKit/GTK、AppImage 和 deb 构建依赖。
main 分支有意保持开发占位版本(0.0.1),已发布版本以 git tag 为准。latest.json 和 SHA256SUMS.txt。desktop-latest feed。缺失签名密钥时只允许 dry_run=true,公开发布必须 fail closed。
/health、未认证的 Web Shell root 导航返回 200(且不下发任何 cookie)、未携带 token 的 /capabilities 返回 401。