Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/128-dsh桌面客户端-开发方案.md
T
admin 04776af4b1 docs: 工作区根项目文档批量入仓(04-调整方案 113–128 / 交接单 T09–T21 / ops / archive)
起因:用户 2026-09-17 明确「所有文档都要同步,都放在开发仓库 docs 对应文件夹下」。
判据:工作区根 *.md 中在仓库(git ls-files --quotepath=false)搜不到的那些。

入仓 33 份(一律复制,工作区根原件保留不动,避免引用断链):
- 04-调整方案/113–128(16 份 · 原子占号后落盘):覆盖网络 传输方案取舍 / 应用场景与待完善清单 /
  插件化vs改内核 / 问题逐条推演 / 参数表与观测口径;会合中继拆分取证与改造方案;
  集群化改造方案 Manager-Worker;跨节点迁移与节点自举;项目代码分层范式与迭代风险评估;
  搬运与共享重建方案 guest w47→w106;方案规划方法提炼;文档无效信息审计报告;
  会话接续机制复盘与修复;会话接续规范;dsh 客户端化部署方案;dsh 桌面客户端开发方案
- 交接单/archive/交接单-已完成/T09–T21(13 份 · 覆盖网络线已完成单归档)
- ops/(2 份运行态指针:接续入口 / 接续包 · 覆盖网络线)
- archive/(2 份临时与内部简报)

已排除(无需重复入仓):覆盖网络线 10 份方案正文已入档案 103–112(文件名不同)。

登记:INDEX.md §二 新增 04-113–128 共 16 行 + §四 追加 T09–T21 说明 + 机器摘要行刷新
(⛔ 未跑 docs-index-stats.py --write:该脚本会按 \r\n 归一化全文件行尾,故改为字节级单行替换);
README.md 追加 1 条入仓指针;docs-manifest.json 复跑 scripts/docs-manifest.py 刷新。

验收:工作区根 47 份 .md —— 同名已入仓 8 / 本次内容一致 29 / 已知改名映射 10 / 未入仓 0;
git status 待提交清单只含本次新增与登记 3 件(未涉 src/ 与 relay 代码面)。
2026-09-17 18:24:19 +08:00

332 lines
19 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.
# DSH 桌面客户端开发方案 —— 基于官方 Electron 壳迭代
- 版本:**v1 规划稿**(2026-09-16)
- 状态:⏳ **待评审**(本文只做规划,不含代码改动)
- **分层指针(2026-09-16 收口)**:本文管**交付载体层**(壳 / 打包 / 签名 / 自动更新);**平台侧**(Windows 子进程启动适配、形态开关)以 `dsh客户端化部署方案_20260916.md` 为准
- 上游:`deepseek-ai/deepseek-harness` 的 `apps/desktop`(Electron 壳)+ `apps/desktop-host`(Node 宿主)
- 上游基线:`master` 分支,`apps/desktop` 版本 `0.1.6-alpha.1`(11239 个文件的全仓快照)
- 上游定位提醒:官方自称 **developer preview**,明写「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」⇒ 同步机制必须按"会变"来设计
---
## 0. 一句话方案
**以官方 Electron 壳为骨架,换掉它的内核** —— 保留启动生命周期、单实例锁、失败恢复页、自动更新、Windows 打包链;
把"启动内置 dsh 运行时 + 插件管理"替换为"**启动我们自己的平台进程,窗口加载它**"。
产出一个**独立仓库**的桌面客户端,用户双击打开即用。
---
## 1. 目标形态
| 项 | 目标 |
|---|---|
| 交付物 | Windows 安装包(`Setup.exe`),双击安装,桌面快捷方式启动 |
| 运行态 | Electron 主进程 → 拉起平台子进程(本机回环)→ 窗口加载平台页面 |
| 对外暴露 | **仅本机回环**(沿用 v3 单机自用形态) |
| 数据 | 平台数据目录(默认用户目录下),与官方 CLI/桌面版**互不干扰** |
| 与平台的关系 | 客户端只是平台的**启动器 + 窗口**;平台机制一行不改(除 §5 那一处) |
---
## 2. 官方壳解剖:取什么、换什么、丢什么
### 2.1 官方壳的组成
| 部分 | 内容 |
|---|---|
| `apps/desktop` | 100 个文件:`src/`(主进程逻辑)、`renderer/`(启动页 + 插件管理 UI)、`scripts/`(打包链)、`tests/`、`electron-builder.config.mjs` |
| `apps/desktop-host` | **仅 6 个文件**:`src/index.ts`、`src/wire.ts`、`config/desktop.cordis.patch.yml` + 3 个配置 |
| 运行时 | `resources/dsh` 内置一份完整的 dsh 与依赖树(打包时生成) |
| 通信 | **不开监听端口**,用"分帧字节管道"承载 Fetch 与流式响应;`dsh-app://` 服务客户端资源 |
> 关键认识:官方壳的**复杂度几乎全部服务于"内置 dsh 运行时"**(包管理、profile 独占、管道传输、插件生命周期)。
> 我们只要"拉起一个本机 HTTP 服务 + 开个窗口",**这些复杂度大多可以直接不背**。
### 2.2 逐模块处置
| 模块 | 官方作用 | 处置 |
|---|---|---|
| `src/main.ts` | Electron 生命周期、窗口、自定义协议、应急页 | **保留骨架**,改"加载目标"与"启动什么" |
| `src/backend-controller.ts` | 后端状态机(启动/停止/恢复) | **换内核** → 管理平台进程 |
| `src/single-instance.ts` | 进程级单实例锁 | ✅ **原样保留** |
| `src/startup-document.ts` / `startup-error.ts` | 启动失败页(自带诊断与恢复动作) | ✅ **保留并改编**(文案换成我们的) |
| `src/locale.ts` / `src/ipc.ts` / `src/paths.ts` / `src/preload*.ts` | 本地化 / IPC / 路径 | ✅ **保留** |
| `src/update-coordinator.ts` | 自动更新 | ✅ **保留**,更新源指向我们自己的通道 |
| `src/host-process.ts` / `host-protocol.ts` | 启动 Node 宿主 + 管道协议 | ❌ **删**(我们走 HTTP,不需要管道) |
| `src/project-manager.ts` / `profile-packages.ts` / `runtime-tree.ts` / `core-package-set.ts` / `owned-directory.ts` | profile 与运行时包管理 | ❌ **删或大幅简化**(运行时由平台负责) |
| `renderer/plugin-manager.*` | 插件管理器 UI | ❌ **删**(插件由平台的「功能管理」管) |
| `renderer/startup.*` | 启动加载页 | ✅ **保留并改编** |
| `scripts/package-target.ts` / `windows-sign.mjs` / `installer.nsh` / `smoke-windows.ps1` / `desktop-build-paths.mjs` / `desktop-release-environment.mjs` | **Windows 打包链**(electron-builder + NSIS + 签名 + 冒烟) | ✅ **保留 —— 这是最值得复用的部分** |
| `scripts/prepare-dsh.ts` / `prepare-runtime.ts` / `prepare-package-set.ts` / `runtime-file-policy.ts` / `macos-runtime.ts` | 为内置运行时准备文件 | ❌ **删**(改为准备我们的平台产物) |
| `electron-builder.config.mjs` | 打包配置(appId、签名、自动更新、NSIS) | ✅ **保留**,改 appId / productName / 更新源 |
| `tests/**` | 官方自己的测试(大量针对内置运行时) | 🟡 **按新内核重写**;与打包链相关的保留 |
### 2.3 改造后的架构
```
Electron 主进程
├─ 单实例锁(官方组件,原样)
├─ 启动页窗口(官方组件,改编)
├─ 平台进程控制器(替换 backend-controller)
│ └─ spawn 平台 → 轮询 127.0.0.1:<port> 就绪
├─ 主窗口 → 加载 http://127.0.0.1:<port>
├─ 失败恢复页(官方组件,改编)
└─ 自动更新协调器(官方组件,换更新源)
```
---
## 3. 代码仓库方案(**本节回答"要不要单独一个仓库"**)
### 3.1 判定
✅ **需要独立仓库。** 命名建议 `dsh-desktop`(或 `dsh-client`)。
### 3.2 五条理由
| # | 理由 |
|---|---|
| 1 | **发布物与节奏不同**:桌面客户端有自己的版本号、安装包、签名、自动更新通道;与平台服务的发版完全不同步 |
| 2 | **上游要持续同步**:fork 官方代码后必须能跟官方更新(官方明说会破坏性变更)—— 独立仓库才能把"官方代码"与"我们的改动"分开管理 |
| 3 | **红线 R2 的边界要清晰**:桌面壳是**官方源码的衍生品**。放进平台仓库,会让"我们自研的平台"与"官方衍生代码"混在一棵树里,合规边界与代码归属都变模糊 |
| 4 | **依赖形态差异大**:桌面端重 devDeps(Electron 44 / electron-builder / AWS SDK / TS 6),平台是轻量 Node 服务 —— 混在一起会拖慢平台 CI、放大依赖面 |
| 5 | **安全与信任模型不同**:桌面端跑在**用户电脑**上,平台跑在**服务器**上;两者的权限、密钥、发布审批要求不一样 |
### 3.3 可行性的关键证据(依赖能否在独立仓库解析)
这是"能不能独立"的硬前提,已核实:
| 项 | 事实 |
|---|---|
| `apps/desktop` 的 `dependencies` | **只有 2 个,且都是公开包**:`electron-updater`、`semver` |
| `apps/desktop` 的 `devDependencies` | 16 个,其中 **只有 1 个** 是 monorepo 内部引用(`workspace:^`):`@deepseek-ai/dsh-home-paths` |
| 上述那个包是否已发 npm | ✅ 已发布(`@deepseek-ai/[email protected]`) |
| `@deepseek-ai/dsh-desktop` / `dsh-desktop-host` 本身 | `private: true`,**未发布到 npm** ⇒ **必须 fork 源码,不能直接依赖** |
> **结论**:独立仓库里把唯一那个 `workspace:^` 换成 npm 版本即可,其余全是公开包。
> **依赖层面无障碍** —— 这是本方案成立的关键证据。
### 3.4 上游同步机制
**目标**:官方更新能进来,我们的改动不丢、冲突可控。
**做法(推荐:基线快照 + 补丁清单)**:
1. 仓库内设 `upstream-baseline/` —— 存放 fork 时刻的官方源码快照,**只读**,带上游 commit hash 与版本号
2. 仓库内设 `patches/` —— 存放**我们对官方文件的每一处改动**,一个改动一个 patch 文件
3. 我们的新增代码(平台进程控制器等)放在 `src/` 下**独立文件**,不修改官方文件 —— 从源头减少冲突
4. 同步流程:
```
取官方新版 → 放到 upstream-baseline-new/ → diff 两个 baseline
→ 人工判断哪些官方改动要跟进 → 重放 patches/ → 冲突处处理 → 更新 baseline + 文档
```
5. **判别原则**:能在**新增文件**里做的事,绝不改官方文件;必须改的,一律进 `patches/` 并写明理由
> 备选做法(若改动最终很少):只维护一份 **`DEVIATIONS.md` 改动清单** + 手工同步,省掉 patch 的机械开销。
> 采用哪一档,等第一轮改造完、看到实际改动面再定。
---
## 4. 关键技术设计
### 4.1 启动流程
```
1. 取得单实例锁(官方组件)—— 已有实例则聚焦它的窗口并退出
2. 显示启动页(官方组件,改编文案)
3. 解析平台启动参数(端口、数据目录、命令路径)
4. spawn 平台进程,捕获 stdout/stderr
5. 轮询 127.0.0.1:<port> 直到就绪(带超时)
6. 就绪 → 关闭启动页 → 打开主窗口加载平台地址
7. 失败/超时 → 显示失败页(含诊断信息与"重启"按钮)
```
### 4.2 平台进程的生命周期
| 事件 | 处置 |
|---|---|
| 窗口全部关闭 | 默认行为待定(见 §9 未决项) |
| 用户退出应用 | 先优雅终止平台进程,再退出 Electron |
| 平台进程意外退出 | 尝试拉起一次;连续失败 → 显示失败页 |
| 应用被强杀 | 下次启动时清理上一次残留的进程(记录 pid 文件) |
| 端口被占 | 改为探测可用端口(**不要**写死 3080) |
### 4.3 就绪探测
- 轮询本机回环端口,**不要**用"端口能连上"当就绪判据(TCP 可连不代表服务可用)
- 用一个轻量健康检查判断真正就绪
- 设总超时,超时给出可操作的失败页
### 4.4 数据目录与配置
| 项 | 设计 |
|---|---|
| 平台数据目录 | 应用私有目录(Windows: `%APPDATA%\<appname>`),**不要**与用户手动安装的平台混用 |
| 平台端口 | 每次启动探测可用端口,写进启动参数 |
| 平台命令 | 打包进应用资源里,不依赖用户系统上的 Node |
| 与官方桌面版/CLI 的关系 | **互不干扰**:官方桌面版独占它自己的 profile;我们的客户端用独立数据目录 |
### 4.5 失败恢复
沿用官方壳的三档恢复动作骨架:
| 动作 | 我们的含义 |
|---|---|
| 重启 | 杀掉平台进程后重新拉起 |
| 重置 | 清掉平台数据目录(**需二次确认**,会丢会话) |
| 查看日志 | 打开日志目录(官方没有这一档,建议新增 —— 用户遇到问题第一件事就是找日志) |
### 4.6 自动更新
- 官方用 `update-coordinator.ts` + `electron-updater`,配置由 `resolveDesktopAutoUpdateConfig` 从环境解析
- 我们要做的:把更新源指向自己的通道;更新前**先停平台进程**
- ⚠️ 官方 README 提到签名/公证/更新托管"需要生产发布环境" ⇒ 自动更新是**发布期**才需要的能力,开发期可以先关掉
---
### 4.7 形态开关:从单机自用到多人访问(**零改动保留**)
客户端**默认**按单机自用启动(不配域名、平台监听回环)。要把它当服务器给多人用,**不需要改客户端代码** —— 只需让它把域名配置**透传**给平台:
| 档 | 做法 | 改动量 |
|---|---|---|
| **最小** | 用户在系统里设环境变量(平台读 `DSHS_BASE_DOMAIN` / `DSHS_COOKIE_DOMAIN`),客户端 spawn 平台时**原样继承** | **0** |
| 更好(该做项) | 客户端提供一个"服务设置"入口:填域名 → 写进平台配置 → 重启生效 | 小 |
| 配套(用户侧) | 内网 DNS 泛解析 `*.<域名>` → 本机地址 | 用户操作 |
- ⚠️ **改为"白名单透传"(2026-09-16 修正)**:原表述「不要清理环境变量、也不要白名单化」会把**平台级凭据**(共享模型密钥等)一并继承给一个装到用户机器上的进程。**正确做法 = 只放形态开关三项**(`DSHS_BASE_DOMAIN` / `DSHS_COOKIE_DOMAIN` / `DSHS_SECURE_COOKIES`),其余不放行 —— 既保住"零改动切多人",又不把平台凭据发出去
- ⚠️ 多人形态下隔离档位仍是默认 `soft`(用户之间无隔离),与部署方案 §1 的取舍一致
- ⚠️ 不需要 HTTPS 也能跑(同 site 下的 `SameSite=Lax` 足够),但**有 HTTPS 更规范**
---
## 5. 平台侧配套改动
桌面客户端要能跑起来,平台侧**必须**先解决一处(详见客户端化部署方案 §4.1,已实测):
| # | 事项 | 说明 |
|---|---|---|
| P1 | **Windows 子进程启动适配** | 平台现在用不带 shell 的方式启动实例,Windows 上会 `ENOENT` / `EINVAL`;必须走 `cmd.exe /c` 或 `shell: true` |
| P2 | 安装包运行方式确认 | 平台以什么形态打包进客户端资源(源码 + node_modules / 预编译产物),需与实际打包方式对齐 |
| P3 | 端口可配置 | 客户端要探测可用端口 ⇒ 平台需支持任意端口启动(当前已支持 `--port`) |
> 这三条都在**平台仓库**里改,与桌面客户端仓库分开 —— 这也是独立仓库的好处之一。
---
## 6. 打包与发布
### 6.1 可复用的官方打包链
| 文件 | 作用 |
|---|---|
| `electron-builder.config.mjs` | 打包配置工厂(appId / 签名 / 自动更新 / NSIS) |
| `scripts/package-target.ts` | 打包入口(按 target 打包) |
| `scripts/desktop-build-paths.mjs` | 各 target 的输出路径 |
| `scripts/desktop-release-environment.mjs` | 从环境变量解析发布参数 |
| `scripts/windows-sign.mjs` + `installer.nsh` | Windows 签名与 NSIS 安装器定制 |
| `scripts/smoke-windows.ps1` | Windows 原生冒烟(需要 Electron / Makensis / 7-zip 路径) |
### 6.2 Windows 签名需要准备的东西
官方配置从环境变量读取,意味着**签名是外部依赖,需要提前准备**:
| 环境变量 | 含义 |
|---|---|
| `DSH_DESKTOP_WINDOWS_CER_FILE` | 证书文件 |
| `DSH_DESKTOP_WINDOWS_SIGNTOOL` | 签名工具路径 |
| `DSH_DESKTOP_WINDOWS_TOKEN_PIN` | 令牌 PIN |
| `DSH_DESKTOP_WINDOWS_KEY_CONTAINER` | 密钥容器 |
| `DSH_DESKTOP_UNSIGNED` | 置 1 出**未签名**包(仅 Windows,**用于内部测试**) |
> ✅ 好消息:官方支持 `package:win:x64:unsigned` ⇒ **开发与内测完全不需要证书**,出正式包才需要。
### 6.3 应用标识
`appId` 与 `productName` 由 `resolveDesktopAppId(env)` 从环境解析 ⇒ 属于**配置项**,不需要改代码。
---
## 7. 开发里程碑
| 阶段 | 目标 | 验收 |
|---|---|---|
| **M0 打通** | 最薄的路:Electron 壳 + spawn 平台 + 窗口加载 | 本机双击启动,能看到平台页面 |
| **M1 生命周期** | 进程管理、就绪探测、退出收尾、崩溃恢复 | 反复启停 20 次无残留进程;拔掉平台进程能自动恢复或给失败页 |
| **M2 体验** | 启动页、失败页含日志入口、单实例、数据目录 | 二次启动聚焦已有窗口;失败页能一键看日志 |
| **M3 打包** | 出未签名 Windows 安装包 | 干净机器上安装 → 启动 → 可用 |
| **M4 发布** | 签名包 + 自动更新通道 | 安装 → 自动更新到新版本成功 |
> **M0 是关键**:它同时验证桌面壳改造与平台侧 P1(Windows 子进程适配)两件事。
---
## 8. 验收标准
| # | 验收项 | 判据 |
|---|---|---|
| V1 | 安装即用 | 干净 Windows 机器上装包后,双击可用,无需预先装 Node |
| V2 | 无残留进程 | 退出应用后,平台进程不残留 |
| V3 | 异常可恢复 | 平台进程被杀 → 应用能恢复或给出可操作的失败页 |
| V4 | 单实例 | 重复启动只聚焦已有窗口,不起第二个平台 |
| V5 | 数据隔离 | 与官方桌面版/CLI 互不干扰,各自数据目录独立 |
| V6 | 卸载干净 | 卸载后程序目录清空(用户数据是否保留需明确策略) |
| V7 | 打包链可复现 | 在干净构建机上能按文档打出安装包 |
---
## 9. 风险与未决项
### 9.1 风险
| # | 风险 | 等级 | 应对 |
|---|---|---|---|
| R1 | 上游快速迭代导致同步成本高 | 🔴 高 | 改动集中在新增文件 + `patches/` 留痕;官方破坏性变更时评估"跟 or 不跟" |
| R2 | Electron 体积大(安装包通常 80–150 MB) | 🟡 中 | 若体积敏感,评估 Tauri 等替代;但会失去官方打包链的复用价值 |
| R3 | 无签名证书时用户会看到安全警告 | 🟡 中 | 内测用未签名包;正式交付需采购代码签名证书 |
| R4 | 平台进程的跨平台启动方式还需实测 | 🟡 中 | M0 阶段同时验证 §5 的 P1 |
| R5 | 官方桌面版若正式发布,我们的定位会变 | 🟡 中 | 定期评估"是否可以直接用官方版 + 我们的插件" |
### 9.2 未决项(**开发前需要定**)
**D1 · 关闭窗口时的行为**
甲:退出应用并停掉平台进程 —— 优点:干净、不留后台;缺点:再次使用要重新启动。
乙:最小化到系统托盘,平台继续跑 —— 优点:随时可用,AI 长任务不被中断;缺点:常驻后台占内存。
**D2 · 客户端与官方桌面版的关系**
甲:完全独立(独立数据目录、独立产品名)—— 优点:互不干扰;缺点:用户装了官方版会有两套。
乙:共享数据目录 —— 优点:会话/设置互通;缺点:要处理与官方版的 profile 冲突(官方版独占它自己的 profile)。
**D3 · 是否保留"插件管理"界面**
甲:删掉,插件全部由平台的「功能管理」管 —— 优点:单一入口、与平台机制一致;缺点:客户端内少一个入口。
乙:保留官方那套插件管理器 —— 优点:复用现成 UI;缺点:与平台的插件机制并存会产生两条路径,容易互相打架。
---
## 附录 A · 证据与复核方式
| 结论 | 来源 |
|---|---|
| 桌面壳 = Electron + 内置运行时 + 私有宿主进程 | `apps/desktop/package.json`(description)+ `apps/desktop/README.md` |
| 不开监听端口、用字节管道 | 官方 README 首段 |
| `apps/desktop` 100 个文件 / `apps/desktop-host` 6 个文件 | GitHub 全仓 tree 快照 |
| `dependencies` 仅 2 个公开包 | `apps/desktop/package.json` |
| `devDependencies` 仅 1 个 `workspace:^` | 同上 |
| 该包已发 npm | npm registry `@deepseek-ai/dsh-home-paths` |
| `@deepseek-ai/dsh-desktop` 为 `private` | 同上 `package.json` |
| 打包链文件清单 | `apps/desktop/scripts/**` |
| Windows 签名靠环境变量 | `electron-builder.config.mjs` |
| 支持未签名包 | `package.json` 的 `package:win:x64:unsigned` |
| 官方宣称会破坏性变更 | 官方 README「Developer preview」段 |
## 附录 B · 需要 fork 的官方文件清单(按处置分组)
| 分组 | 文件 |
|---|---|
| **保留**(含打包链) | `electron-builder.config.mjs`、`scripts/package-target.ts`、`scripts/desktop-build-paths.mjs`、`scripts/desktop-release-environment.mjs`、`scripts/desktop-auto-update-environment.mjs`、`scripts/windows-sign.*`、`scripts/installer.nsh`、`scripts/smoke-windows.ps1`、`scripts/macos-runtime.ts`、`scripts/package-macos.ts`、`scripts/verify-macos-signature.*`、`scripts/notarize-macos-disk-images.*`、`src/single-instance.ts`、`src/locale.ts`、`src/ipc.ts`、`src/paths.ts`、`src/preload*.ts`、`src/update-coordinator.ts`、`renderer/startup.*` |
| **改**(换内核/改编) | `src/main.ts`、`src/startup-document.ts`、`src/startup-error.ts`、`src/backend-controller.ts` |
| **删**(为内置运行时服务) | `src/host-process.ts`、`src/host-protocol.ts`、`src/project-manager.ts`、`src/profile-packages.ts`、`src/runtime-tree.ts`、`src/core-package-set.ts`、`src/owned-directory.ts`、`src/release.ts`、`renderer/plugin-manager.*`、`scripts/prepare-dsh.ts`、`scripts/prepare-runtime.ts`、`scripts/prepare-package-set.ts`、`scripts/runtime-file-policy.ts`、`apps/desktop-host/**` |
| **重写** | `tests/**` 中针对内置运行时的部分 |