Files
dsh_ai1net_server/.workbuddy/memory/2026-09-下19.md
T
admin ce8e6ceed9 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)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

147 lines
34 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 工作日志 · 2026-09(第 20 片)
> ⚠️ **本目录日志已按【月】分片**(2026-09-15 用户定,单文件 ≤50 KB):同月续片 `2026-09.md` / `2026-09-下.md` / `2026-09-下2.md` / `2026-09-下3.md` / `2026-09-下4.md` / `2026-09-下5.md` / `2026-09-下6.md` / `2026-09-下7.md` / `2026-09-下8.md` / `2026-09-下9.md` / `2026-09-下10.md` / `2026-09-下11.md` / `2026-09-下12.md` / `2026-09-下13.md` / `2026-09-下14.md` / `2026-09-下15.md` / `2026-09-下16.md` / `2026-09-下17.md` / `2026-09-下18.md` / `2026-09-下19.md` / `2026-09-下20.md` / `2026-09-下21.md` / `2026-09-下22.md` / `2026-09-下23.md`
> **写入约定**:一律 append 到**当月最后一片**;该片超 50 KB ⇒ 新建 `2026-09-下N.md`;⛔ **不要再按日新建 `2026-09-DD.md`**。
> **覆盖来源**:2026-09-14.md
> **上一片**:`2026-09-下18.md` **下一片**:`2026-09-下20.md`
---
## 13:2x–13:5x · 「重不重」→ 官方推荐库里有现成的:**已装上轻量文件预览插件**(不重新开发)
**用户原话**:「univer 太重了,有没有别的方式做个插件让用户点击对话中的文件,在会话栏旁边的窗口中打开展示」→ 追加:「**需要查官方推荐库是否有类似插件,是重新开发还是改造**」。
**侦察(只读,全部实测)**:
- 「官方推荐库」入口 = `Awesome-DeepSeek-Harness-Plugins`(数据源 cordis.run 插件索引,331 个)+ npm `keywords:dsh-plugin`。
- dsh 客户端**没有官方 viewer 注册点**;但布局里有 `details` 列(会话栏旁、可拖宽)与 `shell.overlay` 浮层;官方 `dsh-client-ui-deliverables` 已把「本轮产出文件」渲染成**可点击 chips**(点击走 chat 的 `openFile`)。
- 平台已有 `GET /api/fs/download?path=`(requireAuth + 限定调用者根内)且**跨域放行**(`access-control-allow-origin: https://<user>.alotbuy.com` + `allow-credentials: true`,实测 200/536 KB)⇒ 纯客户端方案可行。
**候选体检(判据 = 依赖的官方包**是否存在/是否被角色补丁禁** + 版本范围 + 体积)**:
| 候选 | 结论 |
|---|---|
| ✅ **`@softspark/dsh-file-preview` v2.0.0(**65 KB tgz**)** | peer **精确锁 `0.1.2-rc.1`(=我们的 dsh 版本)**;inject 4 个(locale/api-remotes/api-session-controller/ui-renderer)**本平台全有且未被禁**;`apply`+`inject` 合规;读文件走**官方 host Remote seam**(不碰平台 API);patch 自述「**包住 `remote.session.openWorkspacePath` 接管对话里的文件打开手势**,卸载即恢复原生」 |
| ❌ `dsh-file-viewer` v0.3.5 | inject 含 **`@deepseek-ai/dsh-client-runtime`(本平台不存在)** ⇒ 会**静默挂死** |
| ❌ `@deepseek-ai/dsh-client-ui-sidebar-documentpreview` | 面向 **0.1.5** 系(依赖 `dsh-client-ui-sidebar-right`,我们只有 `-ui-sidebar`)⇒ 版本不匹配(R1 不升 dsh) |
| ❌ `dsh-file-review` v0.8.1 | inject 含 **`ui-settings-plugins`(被平台角色补丁禁)** ⇒ 静默挂死 |
| ❌ `dsh-at-file` / `@linxin666/...aionui-panel` | 同样注入不存在的 `dsh-client-runtime` |
**落地**:admin `POST /api/plugins/business` 导入候选池 → **`compat.level: ok`、`trustedOverride: false`**(预检全过,无需声明信任)→ `mine/apply` 启用 → 实例重启探活 `success`。
**验证(L2 机制级)**:实装 `node_modules/@softspark/dsh-file-preview` ✓;**页面客户端插件 46 → 47**,`@softspark/dsh-file-preview` 行 inject **4 个全部可满足(缺 = 无)** ✓。
**待 L5**:用户硬刷新后点对话里的文件,应弹只读预览弹窗(我这边渲染不了,需你实测)。
**体积对比**:65 KB vs univer 42 MB ⇒ **约 1/650**;且**未动 univer 任何一处**(两者可并存)。
### 14:0x–14:2x · 「按你的建议处理」→ 逐条处置(含**否决我自己的一条**)
- **新插件能力边界(读包内 client.js 实测)**:✅ 文本类(md/txt/json/yaml/csv/js/ts/py/sh/sql/html/css/xml)、图片(png/jpg/jpeg/gif/webp/svg)、PDF(kinds: text/html/svg/image/pdf);❌ xlsx/docx/pptx/.univer(命中 `unsupported`,另有 too large 上限)。host 半边 = 会话授权 + 只读工作区(workspace/path.resolve/base64)⇒ 权限收窄 ✓。
- **三条建议处置**:① L5 归用户;② **不装 36 MB 的 Office 组合** —— 改走已有能力(`univer_import` 自 **0.2.28** 起 glibc 已通 ⇒ xlsx 导进 `.univer` 即在线看),"xlsx 能不能在会话里看"的**答案已从"不能"变成"可以"**;③ **⛔ 我自己否决了"停用 univer"** —— 它承载用户明说的核心用法(AI 生成 md/doc/excel + Univer 在线看),且 univer 网关**按需启动 + 空闲自停**(常驻成本主要是 42 MB 磁盘)⇒ 要停的前置 = 先把 `office-file-generation` 技能迁出 univer,等用户明确说停再做。
- 落档:档案 89 追加「能力边界 + 处置表」;四件套 rc=0、双端一致(仍只剩别人那个 `dsh-opensource-release/SKILL.md`)。
### 17:0x–17:2x · 只读调研:K8S 部署能力 + 上千人能否集群(**未改任何文件**)
- **结论:k8s 是「代码内置的模式 B」,不是待开发项**。开关 = `DSHS_DEPLOY_MODE=local|k8s`(`src/config.ts:18/244`)+ `DSHS_DB_URL` 非空即切 Postgres(`src/db/index.ts:19`)。三处可切换点:`server.ts:260`(Spawner)/ `fs/provider.ts:32`(UserFs)/ `db/index.ts`(DB)。
- **模块清单**(新增于本调研):`supervisor/k8s-spawner.ts`(每用户 Pod/Svc/NP/Secret/ConfigMap + watchdog Job + files Pod)、`supervisor/leader.ts`(Lease 抢主 + fencing)、`supervisor/reconcile.ts`(期望态收敛 + informer 崩溃观察)、`fs/k8s-user-fs.ts`、`db/pg.ts`、`tcp-bridge.ts`、`web/file-service.ts`、`deploy/*.yaml`(**11 个**,ACK 全套)、上游 `docs/k8s.md`(设计稿,**本机未 check out**)、`docs/k8s-deploy.md`(ACK/k3s 实跑踩坑,含 CNPG 3 实例 + Phase 3 联调)。
- ⚠️ **本项目从未在生产用它**:档案 19 §C8 已标注「上游 k8s/PG/leader 代码在本部署未使用、未验证」;`03-路线图` 决策 3 = 1.8G/2 核 → **1–3 人**。
- **local 模式不能集群(判据)**:实例态在 `LocalSpawner` 的内存 Map(mains/children/lastActive/熔断)、DB 是单文件 SQLite、每用户随机回环端口 + iptables portGuard(主机本地)、`ensure-*.cjs` 直接改主机上 profile 目录、**无实例归属记录** ⇒ 起两个 dshs 会重复拉起同一用户(无互斥/心跳)⇒ 多台只能是「独立孤岛」,不是集群。
- **切到 k8s ≠ 改配置**(这是我们平台自研能力的真实缺口,逐条读码确认):① `landModels`(档案 87)用 `writeHomeFile` **直写宿主机 `$DSH_HOME`** + chown ⇒ 控制面在 k8s 下**没有用户卷**(`spawner.ts` 头注释明说)⇒ 模型设置层失效;② `ensurePickerProfile` / `ensure-role-profile-patch.cjs` 同因失效;③ `restartAndProbe`(档案 34 插件探活)在 K8sSpawner 里是 `ok:true` **空实现**;④ `breakerInfo`/`quotaInfo`(档案 78/84 观测面)只有 LocalSpawner 有 ⇒ 实例内配额显示/熔断态无数据源;⑤ `touch`/`launchToken` no-op;⑥ 备份/清理(ws-cleanup·session-gc·purge-trash·backup.sh)全是主机侧脚本。
- **上千人容量算式**(未写进文档,供后续立项):每活跃用户 request ≈ 0.55 vCPU / 1.15 GiB(DSH Pod 500m/1Gi + files Pod 50m/128Mi)⇒ **1000 并发 ≈ 550 vCPU / 1.15 TiB ⇒ 8C16G 节点约 11–12 个用户/节点 ≈ 85–90 节点**;30% 并发(300 活跃)≈ 25 节点。对象数每用户最多 8 个 ⇒ 1000 用户 ~8000 对象,**现 `deploy/06-quota.yaml` 的 `count/pods: 60` 必须换成分片命名空间**。
- **k8s 路径上 1000 人级待补洞**:reconcile 每 10s **全量 list pods + 逐用户串行 ensure/reap**(O(N) API 调用);`endpointFor` **每请求 readNamespacedPod**(无 informer 缓存);leader 单点(需按 userId hash 分片);files Pod **创建后无回收**(只在 DSH Pod 侧有 idle reap);NAS RWX 单文件系统 shard;控制面副本数需按长连接数扩;Prometheus/Loki 仅方案未接入。
- 参考落点:本机代码仓 `D:\github\dsh_shenxian`(注意其 `docs/` = 上游 7 篇,与本项目文档库是两回事,见档案 19 §C10)。
#### ⚠️ 17:1x 自我更正(上一条里我把 DB 错列为限制,用户当场纠正)
- **用户原话口径**:「可以改为连接数据库集群,**数据库不是限制**」⇒ 成立。`DSHS_DB_URL` 非空即切 PG(`db/index.ts:19`),`db/adapter.ts` 头注释已声明「routes depend only on this — never on better-sqlite3 or pg directly」⇒ **DB 层早有抽象,是配置项不是天花板**。
- **更关键的一处实测**:`dsh_instances`(实例归属/期望态表)**只有 k8s 路径在写** —— 全仓 grep:`db.upsertInstance`/`deleteInstance`/`findUserInstance`/`listInstancesByRole` 的调用方**只有** `k8s-spawner.ts`(168/176/183/285) 与 `reconcile.ts`;**`orchestrator.ts` 对 `this.db.` 零引用**(`LocalSpawner` 构造函数根本不收 db)⇒ **换库单独做 = 零收益**(库共享了,但没人往里写归属)。
- **修正后的真限制(4 条)**:① 实例状态在进程内存(`mains`/`children`/`lastActive`/熔断态)② 回环端口 + iptables `portGuard`(owner-match 主机本地)③ bwrap + uid + systemd scope(主机本地)④ `ensure-*.cjs` 直改**主机上**的 profile 目录;**加**⑤ 归属表不写库。
- **绿框(反而不是障碍的两项)**:DB 可切 PG 集群;uid = `baseUid + row_id` **由 DB 决定 ⇒ 跨机天然一致**(这是以后真要做分片的手里已有的好牌)。
- **若硬要走「local + PG + 自制集群」**,需新建三层:归属/心跳(≈ k8s 的 dsh_instances + Lease,用主机语义重写 reconcile)、host→owner 路由层(现在是本机代理 + 单机 nginx)、共享存储(NAS/NFS,否则只能"分片固定"不能转移)⇒ 结论不变:**做出来是「分片集群」(固定归属 + 有界转移),拿不到 k8s 的任意调度/秒级重建,工作量却已接近重写一遍 reconcile+leader**。
#### 17:2x 用户提「manager/worker(1 台门户+状态,N 台承载实例)」→ **可行,且跨机面比预想的通**(实测核对,纠正上面"要新建三层"的过重表述)
- ✅ **`Spawner` 接口就是为多后端留的口子**(`spawner.ts` 头注释原话:route layer depends only on this interface, so both backends coexist behind the same API)⇒ 加第 3 种 `RemoteSpawner` **不用动路由层**。
- ✅ **代理层 TCP 目标与 Host 头是分开的**:`proxy.ts:299` `host: endpoint.host`(连接目标)vs `proxy.ts:108` `out.host = \`127.0.0.1:${port}\``(信任栅栏)⇒ **跨机代理不需要改信任逻辑**。
- ✅ **"Host 头端口必须等于实例端口"这个坑不存在**(读官方源码定论):`deepseek-harness/packages/client/connection/src/api-request-trust.ts:103` = `isLoopbackHostname(hostUrl.hostname)` —— **loopback 判定只看主机名、不看端口**;Origin/Sec-Fetch 已被 `proxy.ts:30 STRIP_HEADERS` 剥掉(含 origin/referer/sec-fetch-*/x-forwarded-*)⇒ 转发端口 ≠ 实例端口照样过。**⇒ 跨机代理在协议层已经通了。**
- ✅ **worker 侧暴露实例的原语已存在**:`dshs tcp-bridge <listen> <target>`(`src/tcp-bridge.ts` + `cli.ts:241`),就是 k8s sidecar 用的那条 ⇒ worker 上一条命令即可把 127.0.0.1:随机端口 暴露到内网。
- ⚠️ **真障碍只剩三条(与"写不写库"无关)**:
1. **worker 上"那只手"**(唯一结构性新增)—— (i) worker 也跑 dshs 连同一 PG(零 agent,但用户永绑一台、无跨机拉起)|(ii) 写 worker agent + `RemoteSpawner`(最贴 manager/worker,但新程序+新部署形态)|(iii) worker 跑单节点 k3s(= 退化成模式 B)。
2. **权限扩大(触 R5)**:dsh **硬禁** `--host 0.0.0.0`(官方 `packages/bundle/web-app/src/startup.ts:74-76` 原文 "it would expose remote code execution to the network")⇒ 必须靠 tcp-bridge/nginx 暴露到内网 ⇒ **同 VPC 任意主机可直连实例端口**(只剩 dsh 自己的 launch-token/cookie 一层)⇒ 要 nft 只放行 manager IP + 桥只绑内网网卡,**必须出权限影响评估**。
3. **文件面是第二个跨机面(易漏)**:`userFs` 现在直读本机 `dataRoot/users/<id>/ws` ⇒ manager 要读任意用户工作区 ⇒ 复用 `dshs file-service`(`fs/k8s-user-fs.ts` + `web/file-service.ts`,**k8s 版已有现成实现**)或工作区放共享存储。
- ⚠️ **自动转移的前提 = 共享 home,且这是最贵一步**:DSH_HOME 含 sessions/skills/credentials/profile/node_modules ⇒ 必须共享存储;而 ① dsh **chokidar watch 整个 home**(已踩过的坑)→ NFS inotify 不可靠 ② 每用户 profile 里自己的 `node_modules`(数千小文件)→ NFS 冷启动退化。
- **分层建议**:先「**静态分片、不做转移**」(**不需要归属表、不需要心跳** —— 写库只在要动态调度/转移时才有意义)→ 再「共享 home + 心跳租约」→ 要弹性/秒级重建直接用模式 B。
- **自制 manager/worker 的真实优势(相对模式 B,值得记住)**:bwrap/uid/scope 隔离 **+ 我们全部自研能力(模型落地/角色补丁/picker/插件探活/配额观测/熔断)原样保留**,不用像模式 B 那样逐条重写。**判据**:能接受"worker 挂了其上用户短暂不可用"⇒ 自制更划算;必须自动转移+弹性 ⇒ 模式 B。
- **待办(若用户要推进)**:可在 47.77.182.89 做 5 分钟实证 —— 起一个实例 + `dshs tcp-bridge` 暴露到内网 IP,再用带 `Host: 127.0.0.1:<非实例端口>` 的 curl 打 `/api/*`:**403 = 栅栏拒(理论被推翻)/401 = 栅栏已放行**(同时验证"栅栏不校验对端 IP"这一前提)。
#### 17:2x 用户明确意图 = **跨服务器迁移** + 「实例信息可放用户专属云存储」⇒ 结论修正:**NFS 顾虑大幅减弱**(读官方源码实证)
- **⚠️ 更正我们长期的错误说法**:MEMORY.md 写的「dsh 用 chokidar **watch 整个 home**」**不准确**。实测全仓只有 **3 个 watcher,且无一个递归整 home**:
1. `packages/credentials/credentials-local/src/index.ts:585` —— 监视**单个文件** `$DSH_HOME/.credentials.yaml`(`awaitWriteFinish` + debounceMs);
2. `packages/settings/settings-file/src/index.ts:239` —— 同款,监视 settings **文件**;
3. `packages/skill/skill-filesystem/src/index.ts:488` —— 监视 **skill 根**,`depth: 1`。
⇒ 当年那次 root 600 备份文件引发的崩溃循环,**成因不能用"整 home 被 watch"解释**(结论待查,别再当已知事实引用)。**运维规则本身不变**:平台备份一律 `/opt/dsh/backups/`,绝不落用户 home。
- ✅ **`usePolling` 官方逃生门已找到**:`skill-filesystem` 有显式配置 `watchUsePolling ?? false` + `watchPollIntervalMs`(`index.ts:610-619`)⇒ skill 层在 NFS 上**可开轮询**。credentials/settings 两个只暴露 `watch:boolean` + `debounceMs`(**无 usePolling**),但两者都在启动时 `loadInitial()` 必读(`credentials-local:580`)⇒ NFS 上最坏只损失**热重载**,重启即恢复(且我们的 `landModels` 是 **spawn 前写盘**,冷启动路径不受影响)。
- ⇒ **「home 放共享存储」从"很危险"降级为"代价可控"**:真正剩下的未知项只有「NFS 上同机写、inotify 是否触发热重载」(**不再是阻断项**)+ 「每用户 profile 的 `node_modules`(数千小文件)冷启动慢」(可用分层规避:profile 由镜像提供 + 只把 sessions/skills/credentials/ws 放共享存储)。
- **迁移协议四件必需(缺一不可,照抄 k8s 语义即可)**:① 租约 CAS(`UPDATE … SET holder=$me, epoch=epoch+1 WHERE user_id=? AND (holder IS NULL OR heartbeat_at < now()-ttl)`,affected=1 才算抢到 → 参照 `leader.ts:200-216` 的 replace+RV 乐观并发)② fencing(老 holder 可能只是网络慢,每次操作带 epoch;`k8s-spawner.ts:112 stampFencing` 可照抄)③ 优雅让出 drain(实例在写 sessions,必须 SIGTERM→等落盘→再迁;现为 SIGTERM→grace→SIGKILL,`docs/blueprint.md:40`)④ **单写者保证**(缺它则脑裂双写同一 home → 会话日志损坏,宁可不做自动迁移)。
- **代理层在迁移上是优势**:`endpointFor` **每请求解析**(无状态)⇒ 归属切换**即时生效**,不需要任何粘性会话配置。
- **建议的"用户专属云存储"正确用法**:**权威归属放 DB**(对象存储无原子 CAS、无跨用户扫描、时间戳判活不可靠);用户专属目录里可放一份**归属/实例清单快照**(如 `<user>/.platform/instance.json`)作为 **DB 的容灾重建源**,而**不是**权威源。
### 17:3x · 产出《集群化改造方案 Manager/Worker》草案(含连接保障与故障恢复)
- **产物**:`E:\ProgramData\AI技能\aliyun-dsh-server\集群化改造方案_Manager-Worker_20260914.md`(11 节:架构 / 网络拓扑 / 数据模型 / 关键流程 / 实现清单 / P0-P2 问题点 / 备份 / 成本 / 待拍板 / 实测清单 / **连接保障与故障恢复**)。
- **本机新查到的两条关键事实(写进方案)**:
1. **`/etc/passwd` 是 bwrap 白名单里的必挂项**(`orchestrator.ts:640`,为 `os.userInfo()` / uid→name)⇒ 多机后每台 Worker 的 `/etc/passwd` 必须有该 uid 条目(或 `os.userInfo()` 不再被调用)⇒ **P2-1 待 5 分钟实测**(`setpriv --reuid <数字>` 是否无需建号)。
2. **现状备份实测值**(`02-运维手册 §C.8`):`/opt/dsh/backup.sh` 每周日 05:00 全量 tar.gz + 60 天保留 + `.backup` 一致性快照;首跑 **8.7 MB / 6919 条目** ⇒ 现在全量无压力,**用户数上去后瓶颈在 `profiles/web/node_modules` 的数千小文件**。
- **备份结论(回答用户"有没有更高效方式")**:**单靠文件备份不是最高效**。推荐**四层**:① 共享存储卷快照(NAS/ESSD,近零 CPU)② restic 增量去重 → 对象存储(异地)③ PG PITR(WAL 归档)④ 平台配置 tar(体积小,保持现状);**L2 只备「不可重建」子集** = `sessions/` + `credentials` + 用户 `skills/` + `ws/`,**排除 `profiles/web/node_modules`**(可由 `dsh.profile.bundles` 清单 + 镜像版本重建)⇒ 单用户体积从数百 MB 降到数 MB。
- **连接保障设计(新增 §11)核心 6 条**:① 设计原则「**连接不是关键路径**」(权威在 PG + 数据在共享存储 ⇒ 断连不坏数据,最坏只是不可访问)② 单向拨入(Worker **不反向连 Manager、不连 DB**,控制口是唯一被拨入口)③ **半开连接防护**(复用 `proxy.ts:450-455` 的 `agent.destroy()` + 新 Agent + 短连接重试;跨机后 NAT 静默丢空闲连接会让 2026-09-09 那类挂起更严重)④ **幂等键**(`operation_id` = epoch + 序号;Worker 按 userId 幂等,与 `AlreadyRunningError` 对齐)⑤ agent **只接受白名单动作**(否则 Worker 沦陷 = 全集群沦陷)⑥ **心跳兼任 fencing 广播**(Manager 在心跳响应下发 `expected_epoch`,Worker 发现自己过期就**自杀 self-fencing** ⇒ 防双写最后一道防线)。
- **恢复矩阵**:10 个场景(控制超时 / 心跳失败未过 TTL(只告警不接管)/ 超 TTL 接管(用户中断 **30–60 s**)/ 计划内先迁后停 / agent 崩后对账 / Manager 挂 1 台(无感)/ **Manager 全挂(门户中断但实例仍在跑)** / 实例崩(Worker 自愈,Manager 不参与)/ **PG 不可用(拒新冷启动 + 归属缓存 60 s 保在线用户)** / 脑裂(fencing + 自愈))。
- **对账(reconcile)改进点**:控制主 30 s 一次,**`GET /instances` 一次拿回整机列表**,而非 k8s 版 `reconcile.ts` 逐用户 O(N) 调用;**一期自动接管默认关闭**(告警 + 人工确认),与 R9 教训一致。
- **护栏复用**:`src/supervisor/firewall.ts` 的 `createPortGuard()`(iptables OUTPUT owner-match)**在 Worker 上语义不变、零改动复用**;控制口(9000)与代理口(9001-9999)分离。
- ⚠️ 方案是**草案放项目根**(未入文档库):`档案只增不改` ⇒ 定稿前不宜冻结为正式档案;定稿时需抢全局执行锁 + 原子占号。
### 17:3x · 三条决策已定 + 补 §12(存储选型)/ §13(PG 承载)详解
- **已定(用户拍板)**:① Manager **门户双活 + 控制主备**,**且必须支持单活部署**(同一套代码 1..N 台;单活只需注意:会话必须落 PG(已满足)+ 长连接断开后前端能重连(**待实测**,可用档案 50 的注入位兜底);**禁止任何"必须 2 台"的硬假设**)② 存储**做成可插拔后端**(本地盘 / 自建 NFS / 云 NAS / 云块存储都能接)③ PG 一期**自建**(独立机 + 四层备份),300 人后再评估迁 RDS。
- **存储四形态辨析(写进 §12,易混点)**:① 本地盘(最快、**不能多机共享**)② 块存储 ESSD/CBS(快、**一块盘只挂一台**、秒级快照、**必须与实例同可用区** ⇒ 会锁死 Worker 调度)③ **文件存储 NAS/CFS(RWX 多机同挂 = "位置无关"的实现方式**、小文件性能一般)④ 对象存储 OSS/COS/S3(HTTP API,**不是文件系统**)。⇒ 我原来那两个选项 = ③ vs ② 之争。
- 🔴 **关键约束:对象存储绝不能挂成 `dataRoot`** —— s3fs/ossfs 缺 POSIX 语义(rename/append/锁不一致),对我们是致命的(chokidar watch + 原子写 + append-only 会话日志)⇒ **只能做备份层**。
- ✅ **"两种都支持"成本极低**:所有用户路径从 `config.dataRoot` 派生 ⇒ **指向挂载点即切换后端,业务代码零改动**;要新增的只有**能力探测 + 可观测降级**(原子 rename/fsync、`statfs` 识别 NFS ⇒ 自动开 `skill-filesystem.watchUsePolling`、启动时小文件延迟采样预警、inode 余量)。做法照档案 88 的「按序探测 + 可观测降级」。
- ✅ **推荐"按目录分层挂载"**(比整卷选一种更优):`profiles/**/node_modules` **不入共享层**(镜像+清单可重建,也是备份 L2 排除项)|`sessions`/`credentials`/`skills` → 共享层|`ws/` → 共享层或块存储加速。⚠️ NAS 通用型若小文件起不来,**第一选择是换**极速型/CFS Turbo(不是放弃共享存储)。
- **PG 三个坑(写进 §13)**:① **跨云绝对不要**(入口在腾讯云,若 DB 在另一朵云 ⇒ 每条 SQL 10–30 ms 抖动被放大到每个请求;**必须与 Manager 同 VPC**)② **连接数**(RDS 小规格常限 100;复核是否全走池)③ **`db/sqlite.ts`(+`repo.ts`) 与 `db/pg.ts` 是两套独立实现** ⇒ PG 回归必须**同一组 smoke 跑两边对比**,比"改个 URL"重得多。迁移路径:自建→RDS 用**逻辑复制**在线迁(停机秒级)。
- 定项以外的技术细节(端口段、TTL 数值、对账周期、备份频率)**由实现侧自决,不再上抛**。
### 17:4x · 用户要求「一/二期方案必须互相兼容」→ 新增 §14(阶段兼容性设计)
- **总原则(写进文档)**:**分期只分「自动化程度与规模」,不分「机制、数据结构、协议」** —— 机制与结构一期定死,二期只加机器/加开关/加运维。
- **兼容矩阵 7 维度**:Manager 数(1..N 同代码)|Worker 数(**一期就走 RemoteSpawner+agent,哪怕 Worker 在本机**)|存储(一期就写能力探测 + 按目录分层)|DB(**一期必须 PG,不能先用 SQLite 顶** —— 租约依赖 PG 原子 `UPDATE…WHERE`;两期都是 PG ⇒ RDS 迁移只需改 URL + 逻辑复制)|自动接管(开关默认关,但 **lease+fencing+self-fencing 一期全实现**)|备份(一期就用同一套工具/格式,只调频率)|代理路由(零改动)。
- 🔴 **实测到的阶段兼容风险(已写进 §14.2,全是真实存在的)**:
1. **平台状态目录散落 8 处硬编码**(`/opt/dsh/state/{runtime-baseline,capabilities}.json`、模型托管清单、`/opt/dsh/backups`、`/var/run/dsh-storage-report.json`、`/var/log/dsh-crash-breaker.log`、`/opt/dshs/scripts/ensure-biz-plugins.cjs`、`/usr/local/dsh-runtime`×3)⇒ 二期换存储会「一半在 NAS 一半在本地」隐性分裂;
2. 🔴 **模型托管清单是"每机本地文件"**(`server.ts:106` `/opt/dsh/state/model-landing/<userId>.json`)⇒ 多 Manager 后 A 机写过的 ref、B 机不知道 ⇒ **违反档案 87「只碰自己写过的」⇒ 重复写/漏删用户凭据**(这条最危险,必须挪 PG 或共享目录);
3. **`process.cwd()` 定位脚本**(`orchestrator.ts:251` 找 `ensure-workspace-picker.cjs`)⇒ 多机 cwd 由 systemd WorkingDirectory 决定、不稳定,改基于安装根的绝对路径(复用档案 88 的按序探测);
4. 两套 DB 实现(sqlite+repo / pg)⇒ schema 变更必须两份同改;
5. `dsh_hosts`+归属列一期就建(二期零 schema 变更);
6. uid 是否需建号(§10 第 2 项)一期测掉。
- ✅ **好的一面**:`dataRoot` 派生点 **80 处已全走 config** ⇒ "挂载点一改就切存储"成立的前提已具备。
- **新增"状态三分类"表**(用户数据 / 平台状态 / 机器基线)—— **这是"存储可插拔"能否成立的前提**:分类做对二期只改挂载表,不做就要翻代码。⚠️ 机器基线(`/usr/local/dsh-runtime`、镜像、bwrap 白名单)**不能跟着用户迁移**。
- **新增反模式清单 5 条** + **一期 DoD 8 项**(含:单活实测、远端 Worker 走完整协议、advisory lock 单机自动退化为主、PG 全套 smoke + 回滚演练、能力探测可观测、四层备份同一套工具、自动接管开关默认关)。
### 21:0x · 用户要求「还要考虑方便部署和管理」→ 新增 §15(部署与运维)
- **判据一句话**:**凡是"要登录某台机器手工做"的事,都必须能用一条命令替代**(系统级故障除外)。
- 🔴 **部署单元硬结论:Worker 必须整机镜像,不能容器化** —— 实例隔离依赖 `systemd-run --scope` + `bwrap` + 每用户 `setpriv` 改 uid ⇒ 需真实 systemd 与特权;容器化 = 放弃现有隔离语义(换实现)。**这是与 k8s 模式最本质差别,别照搬容器路线**。Manager 无状态无特权 ⇒ 容器化收益大风险低。建议整机镜像(而非手工装)以保证 §14.3 的"机器基线"一致。
- ✅ **重要修正:共享存储不是开通集群的前置 —— 可以后置**(此前我把它写进了必备项)。拆两步:
- **1a**:Manager-01 + Worker-01 **同机**(Manager 组里一台兼任 Worker),`dataRoot` 原地不动 ⇒ **现有用户零迁移** ⇒ 拿到"归属表 + 租约 + 双活门户 + 跨机协议已验证",**不需要共享存储**(本地盘即可);
- **1b**:加 Worker-02 + 挂 NAS,需要时把用户 rsync 搬一次 ⇒ 拿到"可迁移"。
- 好处:① 第一步不动数据 ⇒ 上线风险极低、随时退回单机;② NAS 小文件性能风险推迟到 1b 再评估;③ `RemoteSpawner`+agent 协议在 1a 就被真实流量验证(正好满足 §14「哪怕同机也走远程协议」)。
- ⚠️ 1a 唯一注意:**Manager 默认 `capacity=0`**(只做门户/控制不接实例),避免门户与实例抢内存。
- **一条命令加机器(`join.sh`)自检项**:cgroup v2/swap/nft、bwrap+setpriv 可用(**顺带验 §14.2 #6 是否需建号**)、`/usr/local/dsh-runtime` 版本匹配、存储后端探测 + 小文件延迟采样、出网 443 与内网可达 Manager、**版本自报**;注册幂等。
- **管理面**:门户新增「集群」页(仅 admin)—— 主机列表(含**版本**/水位/心跳)、实例归属视图(可单用户迁移/批量迁移整机)、存储探测结果、备份状态、告警(含**版本漂移**)、审计。红线不变:破坏性动作仍「先清单 + 二次确认」。
- **一键自检与可观测**:`dshs doctor`(把 §10 的 5 项实测固化成命令)、`dshs cluster status`、`/metrics`+Prometheus、集中日志(Loki/SLS,单机 journald 到集群失效)、**统一 trace id**(🔴 跨机排查最容易后悔的地方)、告警规则。
- **灰度升级 = 集群化最大运维红利**:drain → 等回收 → 换版本 → 自检 → 一次只动一台;Manager 先升非控制主那台;**协议须向后兼容(Manager 兼容 N-1 Worker)**;回滚 = 切符号链接。⚠️ **上集群前必须让 `scripts/ci.sh` 恢复绿**(现状 `test/db.test.mjs` 长期红,属别人 lane)—— 否则"发布可信"这个前提不成立。
- **运维任务表**(日/周/月/变更/应急)+ **资产衔接**:`DEPLOY-本部署.md` → 派生 `DEPLOY-集群.md`;`02-运维手册` 加「集群运维」章;`ops/` 加 join.sh 与集群巡检;`backup-platform.sh` 升级为四层编排。⚠️ 这些都在受保护根内 ⇒ **动它们必须先抢全局执行锁**。
### 21:1x · 用户问「所有服务器都要配域名吗 / 不在同一 IP 下能否组网」→ 新增 §16
- ✅ **只有入口需要域名**(`domain` + `*.domain` + 通配证书)。Manager / Worker / PG / NAS **全部不需要公网域名**;Worker 上**不装 nginx、不配证书**。三条易误解点:① **多 Manager 不需要多域名、不需要粘性会话**(登录靠 PG 会话表 + 同一 `cookieDomain`,给 Manager 各配子域**不需要也不建议**)② **Manager 之间不需要网络直连** —— 互斥靠 **PG advisory lock**,只要"都能连同一个 PG"(结构性优点)③ **备案是域名的属性不是服务器的属性**(阿里云公网 SLB 才校验,现状 CF→ECS 直连不受影响)。
- ✅ **不在同一 IP 下能组网,但分三档**:① 同 VPC(延迟 0.1–1 ms,**可任意迁移**,推荐)② 跨可用区同 VPC(0.5–2 ms,仍可迁移,跨 AZ 流量可能计费)③ **跨云/跨机房**(10–30 ms,必须建 overlay:**WireGuard 首选** / Tailscale·ZeroTier / 专线·CEN;SSH 隧道与纯公网直连不推荐)。
- 🔴 **跨云三代价(重要架构推论,写进 §16.3)**:① **PG 必须与 Manager 同侧**(跨云 SQL 10–30 ms 不可接受)② **NAS 不能跨云挂载** ⇒ 每云一个存储域 ⇒ **用户不能跨云迁移**(所以"任意 Worker 可接手"隐含前提 = 同地域 + 同一存储域;跨云会降级成"每云一个独立集群域")③ **代理流量全走 Manager** ⇒ 跨云带宽成本 + 延迟叠加。⇒ **建议先在一个云内横向扩,不过早跨云**。
- **LB/VIP 前提**:`keepalived` VRRP **需同一二层网络**(跨 AZ 通常不成立,别默认选它)⇒ 用云 HAVIP 或 **SLB(后端注册用 IP,不需要域名)**;⚠️ 阿里云公网 SLB 有 ICP 校验。
- **运维要点**:**防火墙白名单写网段(如 `10.99.0.0/24`)而不是单个 IP** ⇒ 加机器/换 IP 零改防火墙,与 §15「一条命令加机器」目标一致;可选配内网 DNS 名(用 IP 完全等价)。
### 21:1x–21:2x · 现网实测(用户给定 manager=47.77.182.89 / worker-01=47.77.182.89 / worker-02=106.54.21.172)→ 方案新增 §17
- ✅ **全都可达**:47 SSH **端口 32022**(别名 `bt-server`)|106 SSH 22(密钥 `id_ed25519_test106`)|**双向 TCP 22/80/443/8888 等全开**。
- 🔴 **ICMP 双向被安全组拒绝** ⇒ **存活检测绝不能用 ping**(方案 §11 心跳用 HTTP `/healthz` 恰好正确,保持)。
- 🔴 **实测 RTT:47→106 connect 150 ms、106→47 183 ms**(本机回环 0.1 ms;HTTP 首页 total 300 ms)⇒ **比方案原估的 10–30 ms 高 5–10 倍**。⇒ **"不要跨云"的结论不变,但理由从"贵"升级为"实测不可接受"**;两者分属**阿里云 vs 腾讯云**(hostname `iZrj99…` vs `VM-0-8-opencloudos`;内网 172.18.16.212 vs 10.0.0.8)。
- **两台基线差异(§14.3"机器基线不能跟着迁移"的实证)**:47 = Alibaba Cloud Linux 3.2104 / 内核 5.10.134 / **cgroup v1(tmpfs)** / **glibc 2.32(需挂 glibc 2.35 运行时,档案 76)** / **2 核 1870 MB(可用仅 773 MB)**;106 = OpenCloudOS 9.6 / 内核 6.6.119 / **cgroup v2** / **glibc 2.38(不需要该 hack)** / **4 核 3655 MB(可用 2655 MB)** / systemd 255。两台 **bwrap/setpriv/systemd-run/nft/node/dsh 全齐**;两台**都有 swap**。
- 🔴 **发现 1(原"待实测"项闭环):uid 必须建号** —— `setpriv --reuid 100001` 成功(`id -u`=100001),但同 uid 下 node **`os.userInfo()` 直接 FAIL `ERR_SYSTEM_ERROR`** ⇒ **每台 Worker 都必须为要跑的 uid 建号/写 passwd 条目**;**加机器必须同步账号**(漏了 = 实例启动即崩)。这也印证书里为何把 `/etc/passwd` 列为 bwrap 白名单必挂项(`orchestrator.ts:640`)。⇒ 对应 §14.2 #6 = **需要建号**。
- 🟠 **发现 2(新隐患,值得单独立项):`systemd-run` 未设 `MemorySwapMax`**(`orchestrator.ts:749-751` 只有 `MemoryMax` + `TasksMax`),而两台机器都有 swap ⇒ 实例超限**先被换出(变慢)而不是被 OOM kill** ⇒ **"OOM 杀→熔断自愈"(档案 20/78)的语义在有 swap 的机器上不成立**,且不同机器 swap 大小不同 ⇒ 同一用户跨 Worker 表现不一致。建议 Worker 统一 `-p MemorySwapMax=0` 或 join 自检告警。⚠️ **现状单机就有此隐患**(47 也有 swap),不是集群引入。
- 🟡 **106 上已有一套独立平台在跑**(不是裸 Worker):`/root/dsh-users-platform/` + systemd `dsh-users-platform.service`(active 16h+,`EnvironmentFile=/etc/dsh-users-platform.env`,User=root,Restart=always,监听 127.0.0.1:3080);数据根**不在** `/var/lib/dshs`;另有宝塔面板 :8888 与 nginx。⇒ 要用它当 Worker-02 需先定:**(a) 保留独立测试环境(推荐) / (b) 停掉改造成纯 Worker / (c) 就地变成 1a 集群**。
- **部署建议(基于实测)**:🔴 **这两台不能组成"一个"集群**(跨云 150–183 ms)⇒ 要么**选一个云内扩容**,要么**各自独立**(= "每云一个集群域");⚠️ **47 不适合做 Manager**(可用内存 773 MB);✅ **若要试 1a,优先放 106**(可用 2655 MB 且隔离组件全就绪)。
- 📌 **跨日续**(09-15 06:3x 的 K8S 对比 / 验证策略 / 访问模式兼容)已记入 `2026-09-15.md`。