chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)

回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
This commit is contained in:
admin committed 2026-09-24 07:51:03 +08:00
commit ce8e6ceed9
396 files changed
+66045

No files matched your search

@@ -0,0 +1,287 @@
# DSH 平台客户端化部署方案 —— 单机自用
- 版本:**v3**(2026-09-16;v1 规划稿 → v2 多用户 → **v3 范围收窄为单机自用**)
- 性质:**本文只做规划,不含任何代码改动**
- 定位:与 `集群化改造方案_Manager-Worker_20260914.md` 同级,是「客户端化」这件事**平台侧**的单一来源
- **分层指针(2026-09-16 收口)**:本文管**平台侧**(能不能在 Windows 跑起来、形态怎么切);**交付载体层**(Electron 壳 / 打包 / 签名 / 自动更新)以 `dsh桌面客户端_开发方案_20260916.md` 为准 —— 两份合起来才是完整的「客户端化」,⛔ 不各写一份
---
## 0. 一句话方案
把平台装到用户**自己的 Windows 电脑**上,本机浏览器打开 `http://127.0.0.1:3080`,**一个人用**。
不需要局域网、不需要域名、不需要证书、不需要开机自启(可选)。
平台**默认就监听回环地址** —— 这一项连配置都不用改。
---
## 1. 本次范围收窄的两条原则
### 1.1 使用者只有他自己
**不考虑当作服务器、不考虑别人访问。** 由此一次性消掉的东西:
| 消掉的 | 原因 |
|---|---|
| 监听 `0.0.0.0` + 防火墙放行 | 只有本机访问,回环地址即可 |
| 多账号 / 逐用户开通 | 只有一个人 |
| 子域或子路径分流 | 直接访问门户首页即可 |
| 租户隔离 | 没有"别人" |
| 开机自启(服务化) | 双击启动即可,非必需 |
### 1.2 平台机制**全部保留**,只是在客户端部署时**不启用**
**这是一条硬约束:不为客户端化去删改多租户相关的代码。**
客户端形态下这些机制**照旧存在、照旧可用**,只是没有使用场景。
> 用户的判断依据:**多租户需要域名才能跑起来,而客户端没有域名** ⇒ 客户端就用单用户形态。
因此本方案对代码的要求只有一条:**让平台能在 Windows 上把实例启起来**(见 §4)。
多租户、门户、账号体系、隔离档位、配额、集群 —— **一行都不动**。
---
### 1.3 多人形态**没有被去掉** —— "配域名即启用"
**形态开关就是一个配置项**,平台本来就支持,⛔ **不需要改任何代码**:
| 配置 | 形态 | 用户地址 |
|---|---|---|
| 域名**留空**(默认) | 单机自用 | `http://127.0.0.1:3080` |
| 配了**域名** | 多人访问 | `http://<用户名>.<域名>` |
- 开关 = `DSHS_BASE_DOMAIN`(配 `DSHS_COOKIE_DOMAIN`;有 HTTPS 时再加 `DSHS_SECURE_COOKIES`)—— 三个都是**环境变量**,见 `src/config.ts:320-321`
- 配套(用户侧):内网 DNS 泛解析 `*.<域名>` → 本机地址
- ✅ **不需要 HTTPS 也能跑**:门户域与用户子域属于**同一 site**,Cookie 的 `SameSite=Lax` 足以支撑跨子域跳转(`src/web/auth.ts:55-57` 即按"有没有 HTTPS"区分这两档)
- ⚠️ 隔离档位保持默认 `soft` ⇒ 多人访问时**用户之间无隔离**,与 §1.1 的取舍一致(且 `account` 档位在 Windows 上本来就不可用)
> **本节结论**:客户端版本**天然保留"可以当服务器"的能力** —— 启用它不需要写一行代码,只需要用户填一个域名。
> 🔒 **切多人形态的前置检查(2026-09-16 加 —— 未过则不得开这个开关)**
> 这个开关零代码,但它会**同时打开**三个被"单机自用"关掉的风险面:
> 1. **隔离档仍是 `soft` ⇒ 用户之间没有隔离**(且 `account` 档在 Windows 上本来不可用)⇒ 多人形态的**前置条件 = 先有隔离方案**;在拿到之前,只能明确限定为"**仅互信小圈子使用**",并且页面要写明这一点。
> 2. **平台级凭据不得随客户端分发**:域名配置要透传给平台,但**透传必须是白名单** —— 只放 `DSHS_BASE_DOMAIN` / `DSHS_COOKIE_DOMAIN` / `DSHS_SECURE_COOKIES` 三项,⛔ 不得把平台共享模型密钥一并继承下去;模型密钥只走"**用户自己的密钥**"那一层。
> 3. **实例必须属于机器主人**:一台机器上的实例只能归该机器使用者 —— 多人形态下需**显式**保证,不能靠"默认只有一个用户"蒙过去。
---
## 2. 范围收窄带来的变化(相对 v2)
| 维度 | v2(内网多用户) | **v3(单机自用)** |
|---|---|---|
| 监听地址 | 改 `0.0.0.0` + 防火墙 | **默认 `127.0.0.1`,零配置** |
| 账号 | 多账号 + 逐个开通 | 一个本地账号即可 |
| 入口 | 纯 IP + 子路径分流 | 直接 `http://127.0.0.1:3080` |
| 隔离 | 不需要 | **不需要**(没有"别人") |
| 服务化 | 开机自启(必需) | 可选 |
| 风险「用户互读文件」 | 🔴 须书面告知客户 | **不存在** |
| 待改代码 | 配置 + 打包 + spawn 适配 | **只剩 spawn 适配 + 打包** |
---
## 3. 现成能力盘点(**不需要动的东西**)
| 能力 | 现状 | 依据 |
|---|---|---|
| 监听回环 | **默认就是 `127.0.0.1`** | `config.ts:162` / `cli.ts:32` |
| 数据根 | 默认 `~/.dshs`(**非硬编码**) | `config.ts:257` |
| 数据库 | 默认 SQLite 单文件 | `config.ts:271` |
| 免 HTTPS | `--secure-cookies` 默认关 | `config.ts:281` |
| 文件属主处理 | `chown/chmod` 块条件是 `process.getuid() === 0` | **实测 Windows 上 `typeof process.getuid === 'undefined'`** ⇒ 整块自动跳过(`local-user-fs.ts:40`) |
| 端口守卫 | 默认 false | `config.ts:323` |
| 单机自检 | `dshs doctor` | 装机与排障直接用 |
| 版本巡检 | `runtime-baseline.cjs`(Node 写) | 跨平台 |
---
## 4. 硬阻塞点(**已在本机 Windows 实测**)
### 4.1 唯一的硬阻塞:平台启动实例的方式在 Windows 上不成立
平台用 `spawn(command, args)` 启动实例,**不带 shell**(`orchestrator.ts:663-665`)。
Windows 上 npm 全局包是 `.cmd` 垫片,于是:
| 尝试 | 本机实测结果 | 判定 |
|---|---|---|
| `spawn('npm', ['--version'])`(裸名,=平台当前做法) | `ERROR(event): ENOENT` | ❌ 起不来 |
| `spawn('…\\npm.cmd', ['--version'])`(显式 .cmd) | `THROW(sync): EINVAL` | ❌ Node 安全限制 |
| `spawn('cmd.exe', ['/c', 'npm', '--version'])` | `OK exit=0`,输出 `10.9.7` | ✅ 可用 |
| `spawn(p, { shell: true })` | `OK exit=0` | ✅ 可用 |
> 复现(Windows + Node 22):
> `node -e "require('child_process').spawn('npm',['--version'],{stdio:'inherit'}).on('error',e=>console.log(e.code))"` → 打印 `ENOENT`
**结论**:**必须**让平台在 Windows 上以 `cmd.exe /c` 或 `shell: true` 启动实例。这是全案唯一必须改代码的地方。
### 4.2 附带的小点
| # | 点 | 判定 |
|---|---|---|
| A | 播种工作区时无条件 `chownSync`(`orchestrator.ts:503`) | 🟢 非阻塞:有 `try/catch` 兜底,只会刷一行失败日志 |
| B | `dshs doctor` 会探测 cgroup 等 Linux 项 | 🟢 非阻塞:只是自检输出不准 |
---
## 5. 改造清单
### 5.1 必做
| # | 事项 | 量级 | 说明 |
|---|---|---|---|
| C1 | **Windows 子进程启动适配** | 小 | 实例启动处对 `win32` 走 `cmd.exe /c`;建议做成可配置 |
| C2 | 安装包 / 一键安装脚本 | 中 | 装 Node → 装平台与 dsh → 建目录 → 初始化本地账号 → 建启动快捷方式 → 自检 |
### 5.2 该做
| # | 事项 | 说明 |
|---|---|---|
| C3 | 启动器 | 双击启动 + 自动打开浏览器;退出时干净收尾(别留孤儿进程) |
| C4 | 容量参数按单机重算 | 现有默认按 2C2G 多用户宿主定的;Windows 无 cgroup,需靠并发上限约束 |
| C5 | 备份与清理的 Windows 实现 | 现有 `.sh` 那批要换;Node 写的那批可直接用 |
### 5.3 可选
| # | 事项 |
|---|---|
| C6 | 开机自启(单机自用非必需) |
| C7 | 顺手修 §4.2 的两处小点 |
| C8 | 免登录形态(本机单人,可考虑省掉登录步骤) |
> ⛔ **不在清单里 = 不做**:多租户、门户、账号体系、隔离档位、配额、集群 —— **一律不改**(§1.2)。
---
## 6. 落地步骤
| 步 | 动作 | 验证方式 |
|---|---|---|
| S1 | 装 Node.js(22+) | `node -v` 有输出 |
| S2 | 装平台 + 官方 dsh | `dshs --help` 与 `dsh --version` 都有输出 |
| S3 | 初始化本地账号 | 能登录门户 |
| S4 | 第一次启动 | 浏览器打开 `http://127.0.0.1:3080` 出现门户 |
| S5 | 打开实例(**这一步验证 C1**) | 实例能起来并进入对话界面 |
| S6 | 建启动快捷方式 | 双击即可启动并自动开浏览器 |
| S7 | 断网验证 | 断开公网(保留模型 API 通路)后一切正常 |
---
## 7. 验收标准
| # | 验收项 | 判据 |
|---|---|---|
| V1 | 本机可用 | `http://127.0.0.1:3080` 门户正常 |
| V2 | 实例可用 | 能打开实例、能正常对话、能改工作区文件 |
| V3 | 断网可用 | 断开公网(保留模型 API 通路)后,登录/实例/改文件全部正常 |
| V4 | 一键启动 | 双击快捷方式即可启动,无需命令行 |
| V5 | 数据可恢复 | 按备份流程恢复后,历史会话与工作区文件完整 |
| V6 | 无残留外部依赖 | 全程不装 Linux 环境、不装容器运行时 |
| V7 | 不对局域网暴露 | 从另一台机器访问本机端口**不通**(默认回环即满足) |
---
## 8. 风险
| # | 风险 | 等级 | 说明与应对 |
|---|---|---|---|
| R1 | 单实例无内存上限 | 🟡 中 | Windows 无 cgroup,失控实例可能拖慢整机;靠并发上限与人工干预约束 |
| R2 | 与现网形态不同 | 🟡 中 | 出问题不能直接对照现网排查,需另建排障知识 |
| R3 | 模型 API 需出网 | 🟡 中 | 需确认用户网络出网策略;必要时配代理 |
| R4 | Windows 上实例的沙箱强度低于 Linux | 🟡 中 | dsh 本体的 Windows 沙箱官方标注为"部分强制";单机自用可接受,需知悉 |
> ✅ 相对 v2 **消失**的风险:用户之间互读文件、无 HTTPS、局域网暴露。
---
## 9. 回滚
| 层 | 回滚 |
|---|---|
| C1(代码) | 按平台分支判断,Linux 侧行为不变 ⇒ **现网零影响** |
| 安装 | 卸载脚本 + 删除数据目录(安装时记录路径) |
| 整体 | 客户端化**完全不接触现网**(47/106 形态不动) |
---
## 10. 附录 A · 证据与复核命令
| 结论 | 复核命令 / 来源 |
|---|---|
| 默认监听回环 | `grep -n "DEFAULT_HOST" src/config.ts` |
| 启动实例不带 shell | `sed -n '663,665p' src/supervisor/orchestrator.ts` |
| chown 块在 Windows 自动跳过 | `sed -n '40,47p' src/fs/local-user-fs.ts` |
| seed 的 chown 无条件调用 | `sed -n '502,505p' src/supervisor/orchestrator.ts` |
| portGuard 默认关 | `grep -n "DSHS_PORT_GUARD" src/config.ts` |
| Windows spawn 行为 | §4.1 四行实测(本机 Windows + Node 22.22.2) |
| 无域名时有降级路径 | `sed -n '58,62p' src/web/routes/dsh.ts` |
| dsh 本体支持 Windows | 官方 `.agents/notes/**` 三篇 |
## 11. 附录 B · 保留但不启用(**给以后接手的人**)
这些机制在客户端形态下**存在但不使用**,不属于缺陷、不需要清理:
| 机制 | 客户端下的状态 | 事实备注 |
|---|---|---|
| 多租户 / 多账号 | 存在,只建一个账号 | — |
| 子域分流 | 不启用(`baseDomain` 留空) | 平台**内置了降级路径**:`baseDomain` 为空时入口自动回落到子路径 `/u/<id>/dsh/`(`src/web/routes/dsh.ts:58-62`)。即"没有域名跑不起来"在代码层面已有兜底;客户端单人场景**不需要依赖它** |
| 租户隔离(`account` 档位) | 不启用(默认 `soft`) | — |
| 端口守卫(nft) | 不启用(默认关) | 仅在 Linux+root 下可用 |
| 集群(Manager/Worker) | 不启用(`local` 模式) | — |
| 开机自启 | 可选 | — |
## 12. 附录 C · 被放弃的选项(记录决策依据)
| 曾考虑 | 放弃原因 |
|---|---|
| **内网多用户形态**(v2) | 用户明确「考虑用户自己使用就可以了,不用考虑当作服务器 其他人访问」 |
| 官方桌面版(Electron)作为载体 | ~~与"机制都保留"冲突 —— 换载体会丢掉我们的门户能力~~ ⚠️ **该判定已于 2026-09-16 失效**:`dsh桌面客户端_开发方案_20260916.md` 用「**保留官方壳、换内核为我们的平台进程**」规避了这条 —— 门户能力不丢。本行仅作历史记录 |
| Linux 宿主 / WSL2 / 容器 | 需额外环境;单机自用无必要 |
| 域名 + 证书形态 | 客户端无域名,且单人自用不需要 |
> 若范围再次变化(例如要回到内网多人),v2 的对比表与结论仍然有效,可在本文基础上恢复。
---
## 13. 附录 D · 官方桌面客户端:能否沿用(调研结论)
**问题**:能否沿用官方 Electron 桌面版,把这个项目装进去?
**结论**:❌ **直接装不进去**(三条硬理由);✅ 但它的**形态与打包链可以沿用**。
### 13.1 官方桌面版是什么
| 项 | 事实 |
|---|---|
| 组成 | `apps/desktop`(Electron 壳)+ `apps/desktop-host`(**私有 Node host 进程**)+ `resources/dsh`(内置运行时) |
| 网络 | **不开监听端口**;用分帧字节管道承载 Fetch 请求与流式响应 |
| profile | Electron **独占** `$DSH_HOME/profiles/desktop`;CLI 不能启动或修改它 |
| 插件 | profile 的 `dependencies` **只放外部插件**;有独立插件管理窗口(增删改查) |
| 数据 | 与 CLI **共享** `$DSH_HOME` 下的 会话 / 设置 / 凭据 / 工作区 / 存储 |
### 13.2 装不进去的三条硬理由
1. **载体只接受 dsh 插件,我们的平台不是插件。** 平台是独立的 HTTP 服务(路由 + 数据库 + 子进程编排),没有"作为 profile 插件被加载"的形态。
2. **桌面版没有 web server,而我们的插件是平台的前端。** 官方原文:「Desktop does not provide a `webServer`」,官方自己的「Open In...」插件就因此被禁用。我们的 `business-plugins` 数据全部来自平台 API(`/api/plugins/mine`、`/api/dsh/status`、`/api/skills/mine`、`/api/me/keys`…,见 `poc/business-plugins/lib/client.js`)—— 装进去就是空壳。
3. **两套编排者会争同一份 `$DSH_HOME`。** 桌面版自己就在跑 dsh;平台还要为每个用户再起 dsh 实例。
### 13.3 可以沿用的部分
| 可沿用 | 说明 |
|---|---|
| **形态** | Electron 桌面应用:双击打开、不暴露端口 |
| **打包链** | 官方 `apps/desktop/scripts/` 的 electron-builder + NSIS + Windows 签名 + 冒烟脚本,可作参照 |
| **数据互通** | 官方桌面版与 CLI 共享 `$DSH_HOME` 的会话/设置/凭据 —— 我们的壳若指向同一 `$DSH_HOME`,用户数据可互通 |
**做法**:自建一个**薄 Electron 壳**(不复用官方壳代码)——
主进程启动平台 → 等 `127.0.0.1:3080` 就绪 → 窗口加载它 → 退出时收干净子进程。
用户看到的是桌面应用,里面是**机制完整保留**的平台。
### 13.4 三档形态与投入
| 档 | 形态 | 投入 | 体验 |
|---|---|---|---|
| 1 | 快捷方式 + 浏览器(v3 现有 C3) | 最小 | 中 |
| 2 | 薄 Electron 壳 + 借用官方打包链 | 中(壳小,难在跑通打包链) | 好 |
| 3 | Fork 官方桌面版源码改造 | 大 | 最好但最脆 |
> ⛔ **不建议第 3 档**:官方桌面版的整个设计(不开端口、独占 profile、只装插件)与"承载一个平台服务"方向相反,改造量大于重写。
@@ -0,0 +1,331 @@
# 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/**` 中针对内置运行时的部分 |
@@ -0,0 +1,156 @@
# 可行性评估:服务部署 + 客户端安装(客户端壳套服务器项目)+ 覆盖网络互联
> 日期:2026-09-16 | 状态:**调研稿**(只读取证完成,未改任何代码/服务器)
> 触发:用户问「当前项目是否可以改造为支持服务部署以及客户端安装(客户端壳套服务器项目),让他们通过覆盖网络互联」
> ⚠️ **本稿落点说明**:文档库(`dsh-server-docs/`)当时被**全局执行锁**占用(持有者 `guest迁移-2307`,09-15 23:08 起)⇒ 按纪律**未写入文档库**,暂落工作区根。锁释放后应转为 `04-调整方案/<NN>-*.md` 正式档案并登记。
---
## TL;DR
**可行,而且现有架构已经给出约 80% 的形状** —— 因为「一台机器 = 一个可被平台拨入的执行体」这件事,T08 集群化已经定义完了,并且**已经有两条现成能力**正好解决"没有公网 IP":
① `soft` 隔离模式下实例**只是一个普通子进程**(不需要 root / bwrap / systemd);
② Worker 用**拨出式 SSH 反向隧道**接入 Manager —— **节点本来就不需要公网 IP**。
真正的缺口是 4 条:**客户端运行时落点**、**节点身份(现为共享令牌)**、**信任模型反转**、**分发与版本矩阵**。
---
## 一、现成资产(可直接复用 · 均带代码坐标,已核实)
| # | 资产 | 证据 | 为什么关键 |
|---|---|---|---|
| 1 | **`soft` 隔离模式**:实例 = 裸子进程 | `src/supervisor/orchestrator.ts:663` —— `isolationMode !== 'account'` 时直接 `spawn(command, args)`,**无 bwrap / 无 setpriv / 无 systemd-run / 不需要 root**;`src/config.ts:14,180`(`'soft'|'account'`,**默认 `soft`**) | 客户端侧不需要任何特权即可承载实例;Linux 隔离层(bwrap·scope·uid)是**可选增强**,不是前置依赖 |
| 2 | **Worker agent 协议**(≈ 现成的"节点契约") | `src/worker/agent.ts` 头部 4 条纪律:**单向拨入**(不反向连 Manager、不写控制面)· **幂等键 `operationId`** · **最小接口白名单** · **self-fencing(epoch 落后即自停)**;端点:`/healthz` `/launch` `/stop` `/status` `/endpoint` `/fence` `/watchdog` `/fs/*` | 「个人电脑作为节点」所需的核心语义**已经定义并跑通过**(S0–S7 全绿 + 真跨机演练) |
| 3 | **拨出式反向隧道**(解决无公网 IP) | `src/worker/tunnel.ts:152` —— `ssh -O forward -R <port>:127.0.0.1:<port>`;含 20 s 本地定时器自愈(T08 §16.5 ②) | 节点**主动拨出**即可被平台访问,**无需公网 IP、无需端口映射** —— 这正是覆盖网络"中继支"的等价物 |
| 4 | **归属/租约分层判据** | 集群化设计 §1.3:**归属与租约只有 Manager 能写**;Worker 自有本地库 | 客户端天生**只能是数据面**,不会出现"客户端写权威状态"的脑裂 —— 与"个人电脑不可信"天然相容 |
| 5 | **Spawner 接口已抽象** | `src/supervisor/spawner.ts`(`launch / stop / list`)+ `local-*` / `remote-*` 两个实现 | 新增一种"客户端节点"= **多一个 Spawner 实现**,不是改内核 |
| 6 | **平台无跨平台分支** | 全仓仅 `src/supervisor/firewall.ts:42` 一处 `process.platform === 'linux'`(且仅用于 nft 联网护栏) | 除隔离与防火墙外,**代码本身跨平台** |
**结论**:能移植的不是"平台",而是**实例运行时 + 节点代理**这两块;管理面(门户/DB/归属/租约)留在服务器不动。
---
## 二、缺口(4 条,按必须先解决排序)
### 缺口 1 · 客户端运行时落点(技术,自决范围)
- Linux 侧原语分布(已清点):`bwrap`(`orchestrator.ts` / `spawner.ts` / `agent.ts` / `cli.ts` / `routes/admin.ts`)· `systemd-run`(`cli.ts` / `orchestrator.ts`)· `setpriv`(`cli.ts` / `config.ts` / `orchestrator.ts` / `routes/business-plugins.ts`)· `nft`(`cli.ts` / `config.ts` / `orchestrator.ts` / `agent.ts`)· 绝对路径 `/var/lib/dshs` `/opt/dshs` `/usr/local/dsh-runtime`。
- ⇒ 客户端侧需要一套**平台无关的落点**:Node + `dsh` 可执行 + profile + 工作区目录(Windows 用 `%LOCALAPPDATA%`、macOS 用 `~/Library/Application Support`)。
- ⚠️ **未验证(须实测)**:`dsh` 自身在 Windows / macOS 上的**沙箱后端行为**。Linux 侧注释显示 dsh 有"沙箱后端探针",探针失败会 `refusing to run the command unconfined`(实例内 bash 被整体拒绝)—— Windows / macOS 上走哪条后端、是否可用,**尚无证据**。
(验收口径 L2 起:客户端上起一个实例 → `curl` 实例页 200 + 实例内跑一次 bash 工具。)
### 缺口 2 · 节点身份(现在是共享令牌,公网上不成立)
- 现状自陈(`agent.ts` 头部):token 是**共享密钥 + bearer 式比较**,"仅内网 + nft 白名单";HMAC / 防重放**留待后续**。
- 个人电脑在公网 ⇒ 共享令牌**无法吊销单个节点**、无法抗重放、一台泄露等于放行全部。
- ⇒ 必须升级为**每节点独立身份**(客户端证书 / 独立密钥对)+ 准入与吊销,且**通道本身要端到端加密**。
### 缺口 3 · 信任模型反转(最重要的一条,不是技术问题而是边界问题)
- 现平台假设:**宿主机由我们掌控**(所以用 uid 隔离"用户 A 防用户 B")。
- 客户端形态下:**宿主机由用户掌控** ⇒ 原有隔离语义在"防机器主人"这一维上**直接失效**(机器主人本来就能读自己机器上的一切)。
- ⇒ 必须新增硬约束:**一台客户端上的实例只能属于该客户端的主人**;平台**不得**把别人的实例、别人的数据、或平台级凭据下发到客户端。
- ⚠️ 现状反例:平台会把**共享模型密钥**注入实例(档案 85/87 的两层密钥中的"共享"那一层)—— 下发到用户掌控的机器等于**把平台密钥交给用户**。
- ⇒ 客户端形态**必须只用"用户自己的密钥"这一层**;共享层只在服务端实例上使用。
### 缺口 4 · 分发与版本矩阵(运维)
- 客户端安装包 + 自动升级 + **客户端版本 × 平台版本兼容矩阵**(现有 `plugin-compat` / `dsh-install.ts` 是同类问题的服务端解,需要客户端版对应物)。
- 客户端一多,**故障面从"我们的机器"扩到"用户的机器"**(系统时间、磁盘、杀软、代理),排障成本上升。
---
## 三、形态三档(**真取舍,需用户拍板**)
> 判据:三档的**最终效果**对用户完全不同(数据在哪、算力归谁、谁能调度),属"业务目标与优先级",不能由技术标准裁决。
**A 档 · 客户端只做"壳"(远程访问)**
- 优点:改动最小;安全模型**零变化**;不引入任何跨租户风险;沿用现有实例与全部隔离。
- 缺点:个人电脑**不成为节点**,算力仍在服务器;离线不可用;覆盖网络在这一档**没有实际用途**。
**B 档 · 客户端自带"自己的实例"(平台退化为控制面 + 中继 + 目录)**
- 优点:数据与算力都在用户自己机器上,**服务器成本大降**;离线可用;能用到本机文件与本地软件;不引入跨租户风险(每位用户只碰自己的机器)。
- 缺点:`soft` 模式隔离强度**低于**现在的 `account` 模式(本机场景可接受,但需明确边界);Windows / macOS 运行时**未验证**;平台对实例的可控性下降(升级、清理、取证都要客户端配合)。
**C 档 · 客户端作为集群 Worker(平台可把实例调度到用户机器)**
- 优点:算力可横向扩张且复用**已跑通的集群协议**(agent 四纪律 + 归属/租约分层已就位);对平台运营方收益最直接。
- 缺点:**信任模型反转**(别人的代码、别人的数据、平台凭据落到用户掌控的机器上);必须有节点准入 + 吊销 + 强身份;**合规与责任边界要重新界定**;Windows / macOS 的隔离层基本**要从零做**(无 bwrap / 无 systemd / 无 uid 隔离)。
**我的倾向:先做 B**(唯一一档能"增量落地 + 不引入跨租户风险",且正好吃满资产 1/3/4 的现成能力)。A 档价值有限(覆盖网络用不上);C 档应等服务端能力与合规边界都清楚后再谈。
---
## 四、若走 B 档:落地顺序(每步可单独回滚)
1. **验证 dsh 在 Windows / macOS 的可运行性**(只读 + 本地装,不动服务器)——产出:能否起实例 + 实例内 bash 工具是否可用(L2/L4)。
2. **客户端节点代理**(复用 agent 协议,落 `soft` 模式):先只做 `/healthz` + `/launch` + `/stop` + `/fence`,台账式幂等。
3. **通道**:先用现有**拨出式 SSH 反向隧道**打通(零新依赖,当天可验),再评估换"打洞优先 + 中继兜底"。
4. **身份**:每节点独立凭据 + 吊销(替换共享 bearer)。
5. **强约束落码**:实例所属 == 客户端主人;共享密钥不下发客户端(**红线级**,须有断言/测试钉住)。
6. 分发与自动升级。
**回滚**:全程只新增(新客户端形态 + 新 Spawner 实现),**服务端现有路径不动** ⇒ 删客户端即回到现状。
---
## 五、未验证 / 待确认(诚实清单,勿当已知)
| # | 项 | 状态 |
|---|---|---|
| 1 | `dsh` 在 Windows / macOS 起实例 + 实例内 bash 是否可用 | ⚠️ **未验证**(本轮无对应环境) |
| 2 | `soft` 模式下实例的实际隔离强度(能读到宿主什么) | ⚠️ **未验证**(本轮只读了代码分支,未实测) |
| 3 | 客户端形态下"共享密钥不下发"是否已有现成开关 | ⚠️ **待查**(档案 85/87 是两层密钥,需确认能否按形态裁剪) |
| 4 | 打洞优先方案与现有隧道的替换代价 | 📋 方案调研(见同日讨论结论:自托管 NetBird / headscale + 自建中继) |
| 5 | 客户端 × 平台版本兼容矩阵的判据 | 📋 待设计(可仿 `plugin-compat`) |
---
## 附:本轮取证命令(可复跑)
```bash
cd /d/github/dsh_shenxian
sed -n '660,665p' src/supervisor/orchestrator.ts # soft 模式 = 裸 spawn
grep -n "isolationMode" src/config.ts # 'soft'|'account',默认 soft
sed -n '1,30p' src/worker/agent.ts # 节点四条协议纪律
sed -n '147,155p' src/worker/tunnel.ts # -R 反向转发(拨出式)
grep -rn "process.platform" src # 全仓仅 1 处(firewall.ts)
```
---
## 六、与另两份客户端文档对账(2026-09-16 08:5x 追加)
> 对账对象(同为大工作区根,**均晚于本稿 8 小时**,08:06 产出):
> `dsh客户端化部署方案_20260916.md`(v3 · 单机自用 · 自称「客户端化」的单一来源)
> `dsh桌面客户端_开发方案_20260916.md`(v1 · 基于官方 Electron 壳 · 独立仓库)
### 6.1 结论:**不冲突,是互补;但两处需要收口**
| 维度 | 本稿 | 部署方案 v3 | 桌面客户端 v1 | 判定 |
|---|---|---|---|---|
| 形态选择 | 三档 A/B/C,倾向 **B** | **单机自用**(用户原话已定,§12) | 单机自用为默认,域名可切多人 | ✅ **同向**(本稿的 B 即其单机自用;三档问题已被用户拍板,见 6.2) |
| 地基 | 资产 1:`soft` 默认、无需 root | 明确"隔离档保持默认 soft" | 同 | ✅ 互相印证 |
| 平台改动面 | 资产 6:仅 firewall 一处平台分支 | "唯一硬阻塞 = spawn 方式" | P1 同一条 | ✅ 不同维度,可并存 |
| 分发与版本 | 缺口 4 | — | §3.4 上游同步 / §4.6 自动更新 / §6 打包 | ✅ 已被其覆盖 |
### 6.2 本稿需修正的两点(**如实记录,勿当原判**)
1. **缺口 1 的位置标错了**:本稿把"客户端运行时"列为缺口、并怀疑 `dsh` 在 Windows 的**沙箱后端**不可用。部署方案 v3 **已在本机 Windows 实测**:真正的硬阻塞是**平台自己的启动方式** —— 裸名 `spawn('npm')` 在 Windows 报 `ENOENT`(全局包是 `.cmd`),显式 `.cmd` 被 Node 拒(`EINVAL`),改用 `cmd.exe /c` 或 `shell: true` 即通。⇒ **阻塞点在平台调用层,不在 dsh 运行时**,且**代价很小**(一处调用方式)。
2. **三档"需用户拍板"已不再是开放问题**:用户此前已定「考虑用户自己使用就可以了,不用考虑当作服务器 其他人访问」⇒ **A/C 两档的多人维度已被放弃**。本稿第五节的三档保留为**历史决策依据**,不再是待拍板项。
### 6.3 仅本稿覆盖、另两份未涉的:**覆盖网络**(用户最初问题里的那一项)
- 两份文档的"多人形态"路线 = **内网 DNS 泛解析 + 域名**(局域网内多机访问),与"覆盖网络"是**同一需求的两种替代实现**:
- 局域网 + 域名:只能在同一内网;跨公网不行;需要域名与 DNS 泛解析。
- 覆盖网络:跨公网可直连(打洞优先、中继兜底);不需要域名;但需要客户端常驻 + 节点身份。
- ⇒ **本稿与另两份文档在"能力面"上不冲突,但在"要不要跨公网互连"上是分叉**;两份文档默认不跨公网。若确定不做跨公网,本稿的缺口 2/3 可整体降级为"不适用"。
- 📄 **百台规模的逻辑预演见 `覆盖网络_百台规模推演_20260916.md`**(2026-09-16):结论 = 100 台规模下**控制面不是瓶颈,中继带宽与"大流量过网"才是**(实例首屏含 ≈10.8 MB 客户端脚本);而"单机自用"天然把大流量留在回环上 ⇒ 该形态在 100 台规模下是**正确版而非妥协版**。
### 6.4 两处需收口的隐患(**建议进文档库时一并处理**)
1. **双源声明**:部署方案 v3 自称「'客户端化'的**单一来源**」,而桌面客户端 v1 与其**同日并存**且内容不同层(平台侧 vs 交付载体层)⇒ 属项目明令禁止的"第二个漂移源"风险。建议:**v3 定平台侧、v1 定交付层,v3 里补一行指针指向 v1**,或合并。
2. **判定句互相矛盾**:v3 §12 附录 C 把「官方桌面版(Electron)作为载体」列为**放弃项**(理由:换载体会丢掉门户能力),而 v1 **恰恰选了这条路**。v1 的"保留壳、换内核为我们的平台"已实际**消解**该理由(门户能力并未丢),但 **v3 的判定句未更新** ⇒ 并列阅读时会读成"同一件事既放弃又采纳"。建议在 v3 §12 该行加注"已被 v1 以'换内核'方式规避,判定失效"。
### 6.5 本稿的**缺口 3 仍然有效,且是"切多人形态时的前置门禁"**
- 部署方案 v3 §1.3 明确写:切多人形态**零代码**(只配域名),且**多人时隔离档仍是 `soft` ⇒ 用户之间无隔离**;桌面客户端 v1 §4.7 进一步要求"**客户端 spawn 平台时不要清理环境变量、不要白名单化**"(为透传域名)。
- 这两条与本稿缺口 3 的硬约束**正面相交**:
- ① **实例只能属于机器主人** —— 单机自用下天然成立;一旦配域名多人用,**立刻不成立**(soft 档无隔离)。
- ② **平台级共享模型密钥不得随客户端分发** —— 与"环境变量原样继承"相冲。**建议改为白名单透传**,只放形态开关三项(`DSHS_BASE_DOMAIN` / `DSHS_COOKIE_DOMAIN` / `DSHS_SECURE_COOKIES`),模型密钥只走"用户自己的密钥"那一层。
- ⇒ 结论:**不是"不兼容",而是"范围收窄把风险面暂时关掉了、但开关就在配置里"**。建议把上述两条写成**切多人形态的前置检查项**(而非留在散文里)。