DSH × Tauri 桌面客户端

August 15, 2026 · View on GitHub

上游:dsh-tauri-plan.md §1(总体架构)、§4(Tauri 骨架);计划:docs/00-project-plan.md。 本文定义 v1 的组件边界、进程模型、目录结构、关键接口与安全模型,是各模块详细设计(02~04)的总纲。

1. 系统上下文

┌────────────────────────────────────────────────────────────┐
│ macOS 桌面客户端(dsh-desktop)                               │
│                                                            │
│  ┌──────────┐   ┌──────────┐   ┌───────────────────────┐  │
│  │ 主窗口    │   │ 设置窗口  │   │ Server Manager        │  │
│  │ (远程 dsh │   │ (本地壳   │   │ spawn/解析/健康检查/   │  │
│  │  UI,     │   │  UI,     │   │ 进程组清理/崩溃重启     │  │
│  │  无 IPC) │   │  有 IPC) │   └──────────┬────────────┘  │
│  └────┬─────┘   └────┬─────┘              │ spawn         │
│       │ navigate     │ invoke commands    ▼               │
│  ┌────▼──────────────────────────────────────────────┐    │
│  │ Tauri 核心(事件总线:server-status / server-error)   │    │
│  └───────────────────────────────────────────────────┘    │
└────────────────────────────────────────────────────────────┘
        │ spawn: node <dsh>/lib/bin.js web --port 0

┌────────────────────────────────────────────────────────────┐
│ dsh web 子进程(Node,Cordis 插件树)                          │
│  · webserver 127.0.0.1:<port>  → / (SPA) + /api (网关)      │
│  · 读写 $DSH_HOME/profiles/web(cordis.yml 重写、符号链接自愈)│
│  · stdout: "dsh web: http://127.0.0.1:<port>"               │
└────────────────────────────────────────────────────────────┘

核心不变式:客户端绝不修改 dsh 代码、dsh 前端、用户 profile;它只负责"起进程、读一行 URL、开窗口、关进程"。

2. 进程模型与生命周期

2.1 启动序列

App 启动
  ├─ 单实例检查(已有实例 → 聚焦主窗口,退出本进程)
  ├─ 解析配置(DSH_HOME、端口策略、是否已托管运行中服务)
  ├─ resolve_runtime() → (node, dsh_dir)(探测链见 docs/03)
  ├─ ServerManager::start()
  │    ├─ spawn(node dsh/bin.js web --port 0,进程组,管道)
  │    ├─ 读线程:逐行扫 stdout → 正则提取 URL → 通知主线程
  │    └─ 健康检查:TCP 连 127.0.0.1:port(重试)→ HTTP GET /(200)
  ├─ 主窗口创建(先显示本地加载页)
  ├─ 收到 URL → webview.navigate(url)
  └─ 广播 server-status: running / error

2.2 退出序列

主窗口关闭 / 托盘"退出" / Cmd+Q
  ├─ 标记 user_intent = true(阻止"崩溃重启"逻辑误触发)
  ├─ ServerManager::stop()
  │    ├─ 发 SIGTERM(仅子进程)
  │    ├─ 等 5s(宽限期,让 dsh 自己 closeAllConnections)
  │    ├─ 仍存活 → 按进程组 SIGKILL(连带孙进程)
  ├─ 关闭设置窗口 / 托盘图标
  └─ 退出 App(ExitCode 0)

2.3 崩溃重启序列

子进程退出且 user_intent == false
  ├─ 记录退出码与 stderr 尾部
  ├─ 等退避(1s,2s,4s,…,上限 30s)
  ├─ 连续崩溃 < 5 次 → 重新走 2.1(主窗口切回加载页)
  └─ 连续崩溃 ≥ 5 次 → 进入 Error 态,主窗口导航到错误页

3. 模块划分与职责

模块文件职责依赖
生命周期编排src/main.rs初始化配置、装配各模块、注册 Tauri commands、退出编排全部
Server Managersrc/server.rs子进程 spawn / stdout 解析 / 健康检查 / 停止 / 重启;状态机与日志环形缓冲settings
运行时探测src/runtime.rsresolve_runtime():bundled → 配置 → PATH;校验可执行
窗口管理src/window.rs主窗口导航、加载页/错误页切换、on_navigation 外链拦截、设置窗口管理server, settings
设置持久化src/settings.rsDSH_HOME、端口策略、开机自启、日志行数等读写(JSON 文件于 ~/Library/Application Support/<app>/settings.json)
托盘src/tray.rs状态菜单、打开主界面、重启服务、退出server
壳 UI(前端)src/(React)加载页、错误页、设置页;仅本地页面持有 Tauri IPC

4. 目录结构(落地目标态)

dsh-desktop/
├─ package.json                  # 前端脚本;devDeps: @tauri-apps/cli, vite, react
├─ vite.config.ts                # 壳 UI 构建;base: './'
├─ index.html
├─ src/                          # 壳 UI(React + TS)
│  ├─ main.tsx
│  ├─ pages/
│  │  ├─ Loading.tsx             # 启动加载页(显示阶段文案:spawning/health-check)
│  │  ├─ Error.tsx               # 错误页(错误码 + stderr 尾部 + 重试按钮)
│  │  └─ Settings.tsx            # 设置页(DSH_HOME/端口策略/日志/自启)
│  └─ lib/ipc.ts                 # invoke 封装(类型化)
├─ scripts/
│  ├─ probe-dsh.mjs              # M0.2 原型:spawn + 解析 URL(可演进为测试工具)
│  ├─ vendor-node.mjs            # M2.1:按平台下载 Node 二进制到 resources/node
│  ├─ vendor-dsh.mjs             # M2.1:npm i --omit=dev 到 resources/dsh
│  └─ prune-dsh.mjs              # M2.4:web profile bundle 闭包裁剪
├─ src-tauri/
│  ├─ Cargo.toml
│  ├─ build.rs                   # 可选:构建期触发 vendor 脚本/资源校验
│  ├─ tauri.conf.json
│  ├─ capabilities/
│  │  └─ shell.json              # 仅本地壳窗口的权限(remote 不授任何权限)
│  ├─ resources/                 # 构建期生成(M2.1),不提交 git
│  │  ├─ dsh/                    # dsh 安装树(--omit=dev)
│  │  └─ node/<arch>/node        # Node 二进制
│  └─ src/
│     ├─ main.rs
│     ├─ server.rs
│     ├─ runtime.rs
│     ├─ window.rs
│     ├─ settings.rs
│     └─ tray.rs
└─ dist/                         # 壳 UI 产物(tauri 打包时内嵌)

resources/ 由构建脚本生成,加入 .gitignore;probe-dsh.mjs 保留为回归测试工具。

5. 关键接口(草案)

5.1 Server Manager(Rust)

pub enum ServerState { Starting, Running { url: String }, Stopping, Stopped, Error(ServerError) }

pub struct ServerManager {
    state: Arc<Mutex<ServerState>>,
    log: Arc<Mutex<RingBuffer<String>>>,   // stdout+stderr 合并,默认 2000 行
    tx: mpsc::Sender<ServerEvent>,         // 事件:UrlReady / StateChanged / Crashed
}

impl ServerManager {
    pub fn start(&mut self, rt: &Runtime, cfg: &AppSettings) -> Result<(), ServerError>;
    pub fn stop(&mut self);                 // SIGTERM → 宽限 → 进程组 SIGKILL
    pub fn restart(&mut self) -> Result<(), ServerError>;
    pub fn state(&self) -> ServerState;
    pub fn logs(&self, tail: usize) -> Vec<String>;
}

事件通过 Tauri emit 广播:server-status { state, url?, port?, error? }

5.2 Tauri commands(仅壳 UI 可调)

command入参返回说明
get_server_status-{state, url?, port?}轮询/初始化用
get_logstail?String[]环形缓冲尾部
restart_server-()托盘/设置页调用
get_settings / set_settingkey, valueAppSettingsDSH_HOME、端口策略、自启等
toggle_autostartenabled: boolboolLoginItems
open_settings / show_main-()窗口管理
quit_app-()走完整退出序列

5.3 配置模型(AppSettings,JSON 持久化)

{
  "dsh_home": null,            // null = 跟随环境变量/默认 ~/.dsh
  "port_policy": "auto",       // "auto" | { "fixed": 3080 }
  "cwd": null,                 // 子进程工作目录,null = 用户主目录
  "autostart": false,
  "log_lines": 2000
}

环境优先级:命令行/环境变量(DSH_HOME) > 设置文件 > 默认值。设置页修改后立即持久化,端口策略在下次启动服务时生效。

6. 错误处理与错误页

6.1 错误码表

含义触发错误页动作
E_RUNTIME_NOT_FOUND探测链无可用 node/dshresolve_runtime 全失败展示探测详情 + 设置页入口
E_SPAWN_FAILEDspawn 失败(权限/路径)start()展示错误 + 重试
E_START_TIMEOUT60s 未就绪超时展示 stderr 尾部
E_CHILD_EXITED启动阶段子进程退出提前退出展示退出码 + stderr
E_CRASH_LIMIT连续崩溃 ≥5 次重启策略展示最近 5 次退出码 + 日志
E_HEALTH_FAIL端口有 URL 但健康检查不过健康检查展示日志 + 重试
E_NAV_FAILWebView 加载失败navigate 失败展示 URL + 重试

错误页为本地页面(有 IPC),按钮:重试查看日志打开设置

6.2 日志规范

  • 子进程 stdout 与 stderr 合并写入环形缓冲(行尾带 [out] / [err] 前缀)。
  • 每条日志带时间戳 HH:MM:SS.mmm
  • 客户端自身事件(启动/停止/重启/错误)以 [app] 前缀同缓冲记录。
  • 缓冲上限默认 2000 行,超限丢弃最旧;设置页可调。

7. 安全模型

威胁对策
dsh 前端被 XSS 后提权到 Rust主窗口加载的是远程内容,capabilities 不授予任何 IPC;withGlobalTauri: false;主窗口无 invoke 能力
DNS rebindingdsh 服务端只信任 IP 字面量 Host(127.0.0.1);客户端导航仅允许 loopback URL(见 8)
外链跳转on_navigation 拦截:非 loopback 交给系统浏览器(tauri-plugin-opener)
本地端口被恶意页面占用端口策略默认 --port 0 随机分配;固定端口仅用户显式开启
设置文件篡改设置 JSON 只含非敏感项(不含凭据);凭据始终留在 ~/.dsh

8. 导航白名单(主窗口)

允许导航的目标 URL(正则):

^http://127\.0\.0\.1:\d+(/.*)?$
^http://localhost:\d+(/.*)?$      # 仅当 dsh 配置了 localhost 别名时,默认只认 127.0.0.1

其余一律拦截 → tauri-plugin-opener 打开系统浏览器;about:blank / 本地 tauri:// 页面(加载页/错误页/设置页)放行。

9. 与上游方案的对应关系

方案章节落地位置
§1 总体架构本文 §1~§2
§2 Server Managerdocs/02-server-manager.md
§3 运行时与打包docs/03-runtime-bundling.md
§4 Tauri 骨架 / IPC 安全 / 外链docs/04-tauri-shell.md + 本文 §7~§8
§5~§7 里程碑/风险/决策docs/00-project-plan.md