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