# 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: 就绪 ├─ 主窗口 → 加载 http://127.0.0.1: ├─ 失败恢复页(官方组件,改编) └─ 自动更新协调器(官方组件,换更新源) ``` --- ## 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/dsh-home-paths@0.0.1-rc.3`) | | `@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: 直到就绪(带超时) 6. 就绪 → 关闭启动页 → 打开主窗口加载平台地址 7. 失败/超时 → 显示失败页(含诊断信息与"重启"按钮) ``` ### 4.2 平台进程的生命周期 | 事件 | 处置 | |---|---| | 窗口全部关闭 | 默认行为待定(见 §9 未决项) | | 用户退出应用 | 先优雅终止平台进程,再退出 Electron | | 平台进程意外退出 | 尝试拉起一次;连续失败 → 显示失败页 | | 应用被强杀 | 下次启动时清理上一次残留的进程(记录 pid 文件) | | 端口被占 | 改为探测可用端口(**不要**写死 3080) | ### 4.3 就绪探测 - 轮询本机回环端口,**不要**用"端口能连上"当就绪判据(TCP 可连不代表服务可用) - 用一个轻量健康检查判断真正就绪 - 设总超时,超时给出可操作的失败页 ### 4.4 数据目录与配置 | 项 | 设计 | |---|---| | 平台数据目录 | 应用私有目录(Windows: `%APPDATA%\`),**不要**与用户手动安装的平台混用 | | 平台端口 | 每次启动探测可用端口,写进启动参数 | | 平台命令 | 打包进应用资源里,不依赖用户系统上的 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/**` 中针对内置运行时的部分 |