Files
dsh_ai1net_server/dsh-server-docs/04-调整方案/127-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

288 lines
15 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 平台客户端化部署方案 —— 单机自用
- 版本:**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、只装插件)与"承载一个平台服务"方向相反,改造量大于重写。