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

34 KiB
Raw Blame History

工作日志 · 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。