Files

287 lines
15 KiB
Markdown
Raw Permalink Normal View History

# 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、只装插件)与"承载一个平台服务"方向相反,改造量大于重写。