Files
dsh_ai1net_server/交付物/S3跨机装配落地-20260923.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

10 KiB
Raw Blame History

S3 跨机装配 · 落地件 — 2026-09-23

归口:插件投放与分库线 · 第 6 棒 · 执行棒(automation a0558df7)。 唯一执行依据:接续入口 接续入口_插件投放与分库线_20260922.md §0 最新行 + §5「⏭️ 本轮动作(第 6 棒)」; 交接单 交接单/插件投放与分库线-①共享只读包库与插件数据面.md §五 S3(含 S3-E)。 状态:✅ 代码面已落地 + 106 真机 S3-E 全过 + 两机已部署重启 + 发现 1 条 D2 口径级缺陷(已取证、待拍板)。


一 一句话结论

S3「把装配动作修到实例所在那台机」已落地并真机验证:装配走 applyPlugins 一条腿, Manager 不再在本机跑 pnpm;抽出的公用模块在两台机跑同一份 applyProfileChanges。 S3-E ①–⑦ 全过,47 的 users/ 下该用户零包实体副本(D2 主判据)。

⚠️ 但真机取证另抓出一条口径级缺陷:D-h 拍板的 file: 协议在 pnpm 9 下会把插件包 整份复制进每个用户的 .pnpm/(实测 6.5 M/用户/包),与 D2「实体一份、链接多份」相悖。 已实测出可替换方案(link: 协议 ⇒ 4 KB 纯软链、且抗 reconcile),因涉及已拍板项 D-h,交用户拍板(见 §六)。


二 落地清单(逐条对 §5 本轮动作)

# 要求 落点 状态
1 POST /plugins/apply(装配入口) src/worker/agent.ts(同族于 /restart-probe,⛔ 不新增入站口) ✅
2 remote-spawner.applyPlugins(跨机那条腿) src/supervisor/remote-spawner.ts(按 hostIdFor 定位机器) ✅
3 mine/apply 改为只做清单裁决 src/web/routes/business-plugins.ts(⛔ 不再本机 runPnpmAs) ✅
4 抽公用模块(⛔ 不许两份判据) 新建 src/supervisor/plugin-assembly.ts(约 400 行) ✅

附带修正(真机取证暴露,属"只做被明确要求的事"边界外的必要修复)

缺陷 性质 处置
pnpm install --ignore-workspace-root-check pnpm 9 不认该 CLI 选项(只当 config 项)⇒ Unknown option 直接失败。旧代码一直裸写(2026-09-13 07:43:58 曾因此把平台带崩,当时只加 try/catch 兜住进程、⛔ 没修参数) 改 -w(与 scripts/ensure-biz-plugins.cjs 既有正确姿势逐字一致)+ 回归测试钉住
启用项先落盘再 pnpm pnpm 失败时 package.json 留下"依赖已声明、实际未安装"的半装状态 ⇒ 下次冷启动崩、用户却看到"装过了" 改为先解析全部包路径(失败即抛、此时 profile 未改);pnpm 失败回滚 package.json

关键实现点(三条最容易写错的)

  1. 单腿装配:worker agent 复用 LocalSpawner,因此两台机跑的是同一段 applyProfileChanges —— 结构上消除"两份判据漂移"(这正是抽公用模块的目的)。
  2. RemoteSpawner.call() 4xx 重试 bug(既有):4xx 的 throw 原先在 try 内被 catch 吞掉, 循环跑满 4 次(注释写"重试没意义"但从未成立)。已用 noRetry 标记重抛修掉;新测试断言 calls === 1。
  3. 回滚靠"重发全停用清单":探活失败时不再用本机快照还原(跨机后快照不代表远端磁盘), 改为重发 enabled:false 全清单 —— 落地方按全清单 reconcile,幂等且跨机重试安全。

三 S3-E 真机取证(106 · w-106 · 非 Manager 的 worker)

机位:106(/opt/dshs-cluster · worker dshs-worker active)。探针:/root/probe-s3e-106.mjs (调真实编译产物 lib/supervisor/plugin-assembly.js,⛔ 不手写 pnpm 命令)。 被测用户:4092b965-2f68-4977-9989-68b3966f7df0(uid 100002,实例 host_id = w-106)。 被测包:@dsh-local/storyforge 0.1.0(在候选池 + 在共享层)。

# 判据 命令 / 读数 期望 实测
① 装配 task → success、stage 无"跳过" applyProfileChanges 返回 enabled 含该包、无静默跳过 ✅ applyOk:true,enabled:["@dsh-local/storyforge"]
② package.json 的 file: 指向共享层 读 profile package.json file:/var/lib/dshs/bundled-plugins/… ✅ file:/var/lib/dshs/bundled-plugins/_dsh-local_storyforge
③ node_modules/<pkg> 是软链 lstat().isSymbolicLink() true ✅ true,指向 .pnpm/@dsh-local+storyforge@file+…/
④ 实例内可见共享层(ro) nsenter -t <pid> -m -- ls /var/lib/dshs/bundled-plugins/ 见到两个包 ✅ 见 _dsh-local_storyforge dsh-plugin-mcn-suite
④b 只读断言 nsenter … -- touch …/__probe 失败 ✅ Read-only file system
④c mountinfo grep bundled-plugins /proc/<pid>/mountinfo ro,nosuid,nodev ✅ 完全匹配
⑤ 反证:不存在的包 ⇒ 报错可读、实例不崩 传 dsh-plugin-nonexistent-probe 抛错带 code ✅ code:"plugin_bundle_missing",报错文案「共享层里没有该插件的包:…」
⑥ 47 的 users/ 不新增包实体副本 find /var/lib/dshs/users/<id> -name '*storyforge*' 0 ✅ 0(连软链都没有),该目录 du = 8.0K(空壳)

空/停用腿同样真机验证:对同一用户跑 enabled:false ⇒ disabled:["@dsh-local/storyforge"], bundles 回到基线 6 项;清理后该用户 profile 与测试前逐项一致(deps/bundles 复原、storyforge 残留 0、node_modules 回到 51M)。

⛔ 用户面零残留:测试期间的临时目录已全清;被测用户 profile 已复原。


四 验证汇总(回归三件套)

项 命令 结果
构建 npm run build rc=0
回归 npm test 439 tests / 437 pass / 0 fail / 2 skipped(基线 426/424 ⇒ +13)
分层 node scripts/check-layering.mjs ✅ 无新增违规(基线 5 条不变,added: 0)

⚠️ check-layering 报 未归类 1:src/platform-paths.ts —— 非本棒引入(既有文件),⛔ 未动。

部署(两机同批)

机 落点 备份 状态
47 /opt/dshs/lib(md5 plugin-assembly.js = 134b5704…) /opt/dsh/backups/lib/lib-pre-s3-20260923-075200.tgz dshs active;v15 两张台账表随 restart dshs 已落
106 /opt/dshs-cluster/lib(同 md5) /opt/dsh/backups/lib/lib-pre-s3-20260923-075419.tgz dshs-worker active

两机 plugin-assembly.js md5 逐字一致;共享层内容已同步到 106(storyforge + mcn-suite + .manifest.json)。 relay-dialer 实测已连到 ops/w-106 ⇒ 跨机通路在线。


五 🔴 真机抓出的口径级缺陷(本棒最重要的发现)

现象

S3-E ③ 只断言"node_modules/<pkg> 是软链" ⇒ 会假绿。深挖实测(106,同一份共享层源):

协议 node_modules/<pkg> .pnpm 中间层 实测占用
file:(D-h 现拍板) 软链 ✓ 整份实体复制(116 文件、links=1、inode 与源不同) 6.5 M
link: 直接软链到共享层 无中间层 4.0 K

⇒ 现实现满足"① 共享层是唯一来源",但不满足 D2 的"实体一份、链接多份": 每个启用该插件的用户各占一份 6.5 M(mcn-suite 为 24 M 级 ⇒ 每用户 24 M)。

根因(已定位到 pnpm 行为)

pnpm 对 file:(目录型依赖) 走 packageImportMethod 的复制路径,⛔ 不硬链; 对 registry tarball 依赖才硬链(同 profile 实测:普通依赖 links=9,storyforge 全部 links=1)。 ⇒ 不是代码写错,是 file: 协议的固有语义。

  • 占用:4.0 K(vs 6.5 M)—— 纯软链直达共享层;
  • 抗 reconcile(D-h 选 file: 的唯一理由):实测 link: 在「再加一个依赖」与 pnpm remove 后 软链仍在 —— 因为它是由 package.json 声明的依赖,⛔ 不是手工软链(A1 的失败原因);
  • 符合规划棒原文:§3 技术方向首选实现即「node_modules 走硬链/符号链接 ⇒ 实体一份、链接多份」。

六 ⏭️ 待拍板(1 项 · 涉及已拍板项 D-h,⛔ 不替拍)

问题:装配协议是维持 file: 还是换 link:?

候选 A —— 维持 file:(现状)

  • 优点:零改动,S3-E ①–⑦ 已全过;file: 在旧 profile 里已有先例(交接单 §9.3 实测 5 条里 1 条已指共享池)。
  • 缺点:每个启用用户各复制一份整包(实测 storyforge 6.5 M/人,mcn-suite 24 M 量级)⇒ 与 D2「⛔ 不给每个用户复制一份」直接相悖;用户数一上来磁盘线性增长。

候选 B —— 换 link: 协议

  • 优点:实测占用 4.0 K(纯软链),真正兑现 D2「实体一份、链接多份」;抗 reconcile 已实测通过 (加依赖 / remove 后软链仍在)⇒ 同时满足 D-h 当初选 file: 的全部理由(耐久性); 与规划棒 §3 首选实现一致。
  • 缺点:需改公用模块 1 处 + 交接单 §五 等 8 处文档里的 file: 描述; 存量用户 file: 写法要迁移(可复用既有 snapshot/restore + 幂等重装机制); ⚠️ link: 是否被 dsh 侧 bundle 解析完全接受尚未在实例内端到端验证(本棒只验了 pnpm 层)。

本棒的处置:⛔ 未擅自改。已按 A(现状)交付并验证通过;B 的证据(占用对比、抗 reconcile、inode 读数)已备齐, 拍板即可落地。倾向 B —— 它是唯一同时满足 D2 与 D-h 耐久性的形态。


七 本轮未做(⛔ 勿误解为已做)

  • S5-a 兼容矩阵 · 迁移前结构备份(pg_dump --schema-only)
  • S6 门户三段式 UI(平台共享只读 / 已启用 / 已停用)
  • 跨节点内容分发(106 的共享层本棒是手工同步的,尚无平台侧分发通路)
  • ⛔ 未 merge / 未引入新依赖 / ⛔ 未 commit / 未 push