起因:用户 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 代码面)。
19 KiB
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 上游同步机制
目标:官方更新能进来,我们的改动不丢、冲突可控。
做法(推荐:基线快照 + 补丁清单):
- 仓库内设
upstream-baseline/—— 存放 fork 时刻的官方源码快照,只读,带上游 commit hash 与版本号 - 仓库内设
patches/—— 存放我们对官方文件的每一处改动,一个改动一个 patch 文件 - 我们的新增代码(平台进程控制器等)放在
src/下独立文件,不修改官方文件 —— 从源头减少冲突 - 同步流程:
取官方新版 → 放到 upstream-baseline-new/ → diff 两个 baseline → 人工判断哪些官方改动要跟进 → 重放 patches/ → 冲突处处理 → 更新 baseline + 文档 - 判别原则:能在新增文件里做的事,绝不改官方文件;必须改的,一律进
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/** 中针对内置运行时的部分 |