Files
dsh_ai1net_server/交付物/共享层台账语义缺陷修法-20260925.md
T
admin 318430c9e9 chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)
范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

10 KiB
Raw Blame History

共享层台账两条语义缺陷修法(插件投放与分库线 · 第 21 棒)

日期:2026-09-25 11:35–12:2x|棒次:第 21 棒 · 执行棒|工作区:E:/ProgramData/AIProject/aliyun-dsh-server 代码基线:D:/github/dsh_shenxian(生产口径 dshs)|真机:47.77.182.89(Manager dshs + worker dshs-worker)+ 106.54.21.172(worker w-106) 本轮唯一动作:共享层台账两条语义缺陷(§5-10 ㈠㈡)——「清单与磁盘不同步」+「回滚素材名实不符」。


一、结论(三句话)

  1. 两条缺陷同源:该写进台账的值,此前来自"别处的副本",而不是盘上那份实体 —— ㈠ version 取自候选池行;㈡ "回滚素材"落的是新包自拷贝。
  2. 修法共用一条纪律:要写进台账的值,必须从盘上那份实体现读。落地 = 三个真值口径函数 + doShare 两处接线 + 一个显式自愈口(㈠ 的存量只能靠"拿盘上真值回写清单"收口)。
  3. 四条判据真机全绿:清单 ≡ 磁盘真值(含"篡改 → 自愈 → 回到真值"端到端)|节点连续两轮 current|回滚素材名实相符(且不含 node_modules)|单测 5 例钉住口径。47 已上线并重启;⛔ 未 commit/push。

二、判据①:清单字段与磁盘真值逐字相同

2.1 端到端(走平台新口,非直跑函数)

在 47 上制造一次真实的脱钩(㈠ 的现象),再看能不能修回来:

步 动作 真机读数
0 备份清单 /opt/dsh/backups/lib-p21/manifest-pre-p21.json
1 把 _dsh-local_storyforge 的 version 改成 0.2.0(盘上是 0.3.0) TAMPERED flat=_dsh-local_storyforge version 0.3.0 -> 0.2.0
2 只读干跑 reconcileSharedManifest() DRYRUN checked=4 fixed=1;version ["0.2.0","0.3.0"](后值 = 盘上真值)
3 建临时 admin 会话(R4) MKSESS_USER=cce6d1cd-… / admin
4 POST /api/plugins/shared/reconcile RECONCILE_HTTP=200;body fixed:[{flat:_dsh-local_storyforge, version:["0.2.0","0.3.0"]}]
5 GET /api/plugins/shared 复核 storyforge version=0.3.0 tree=1c45c8418875 files=117(回到真值)
6 与备份逐字 diff 唯一差异 = updatedAt(预期:自愈也是一次写入)
7 清理 SESSIONS_DELETED=1(临时会话用完即删)

⚠️ 诚实记录:第 6 步不是"零差异" —— 除 updatedAt 外逐字相同。updatedAt 的语义是"该条目最后一次写入",自愈改了三个描述字段 ⇒ 更新它是对的(且第二步起幂等:见 2.3)。

2.2 四个包的真值复核(管理面读数)

_dsh-local_storyforge           version=0.3.0  tree=1c45c8418875 files=117
dsh-plugin-mcn-suite            version=0.3.15 tree=939206c76abb files=657
_dsh-local_im-conversation-tabs version=0.1.0  tree=678e0f36e100 files=5
_softspark_dsh-file-preview     version=2.0.0  tree=5cc4bdf7d1a9 files=881

与盘上 package.json 逐个核对一致(_dsh-local_storyforge :: version=0.3.0 等,见 §四 取证命令)。

2.3 幂等(真机)

MANIFEST_MD5_BEFORE=73435d1f81052604528537fa8064074b
IDEMPOTENT_DRYRUN checked=4 fixed=0
MANIFEST_MD5_AFTER =73435d1f81052604528537fa8064074b

⇒ 真值已对齐后一处不动、字节不变(⛔ 不制造无谓写盘 ⇒ 不抖 mtime、不诱发节点重拉)。

2.4 判据①的字面动作未做(如实声明)

判据原文含「真机跑一次 share 后清单字段与磁盘真值逐字相同」。本棒没跑那次 share,原因:

  • 跑 share 只有两种走向 —— ① 幂等分支(池内 tgz 与清单 tgzSha256 相同)⇒ 一个字节都不动,验不到写入路径;② 替换分支 ⇒ 必须真发一个新版本,那会改线上共享层内容(影响面 = 全平台已可见该插件的用户)。
  • 等价替代(证明力不低于字面动作):篡改 → 自愈 → 与原值逐字相同(§2.1)+ 单测钉住写入侧口径(§五)。

三、判据②:节点对账连续两轮 current

在 106 上以节点身份触发两轮拉取(POST /shared-layer/pull,带该机自己的节点凭据;⛔ 凭据不出本机):

PULL=200
--- round1 --- {"ok":true,"missing":[],"stale":[],"extra":[],"pulled":[],"failed":[],"asOf":1790308081562}
--- round2 --- {"ok":true,"missing":[],"stale":[],"extra":[],"pulled":[],"failed":[],"asOf":1790308084957}

⇒ 连续两轮判 current,⛔ 不再 stale/反复重拉(两轮间隔 3.4 s)。Manager 侧对账面同步复核: NODE_REPORT={"hostId":"w-106","at":1790308084767,"authoritative":4,"missing":[],"stale":[],"extra":[],"failed":[]}


四、判据③:回滚素材名与内容自洽

检查 真机读数
盘上既有素材(mcn-suite/0.3.14.tgz)内容版本 ASSET_CONTENT_VERSION=0.3.14 ⇒ 文件名 = 内容版本 ✓
packSharedTree() 在真共享层包上跑(产物落 /tmp,⛔ 共享层一字未动) SRC_TRUTH_VERSION=0.3.15 → PACKED_CONTENT_VERSION=0.3.15 ⇒ 名实相符 ✓
素材是否夹带 node_modules ARCHIVE_NODE_MODULES_HITS=0 ✓(--exclude=node_modules 生效)
体积 PACK_SIZE=1996116(≈ 池内 tgz 1982036,同量级 ⇒ 未把依赖树打进去)

接线审计(部署后的 lib/ 里,证明新函数真的被 doShare 调用):

1787: const asset = rollbackAssetPath(app.config.backupDir, flat, oldVersion);
1790: packSharedTree(dir, asset);
1806: version = packageVersionAt(incoming) ?? version;
1815: backupTgz = rollbackAssetPath(app.config.backupDir, flat, version);

五、判据④:单测钉住两处口径

test/shared-layer-sync.test.mjs 新增 5 例(第 ③ 组「台账两条语义缺陷」):

用例 钉住什么
台账口径(㈠) version 取盘上实体现读:最浅 package.json 优先;读不到 / 无 version / 非法 JSON ⇒ null(⛔ 不编造、⛔ 不抛)
台账自愈(㈠) 脱钩 ⇒ 三件(version/treeSha256/fileCount)一起修;其余字段原样保留;再跑幂等
台账自愈: 双向只报告 清单有盘上无 ⇒ missingOnDisk(⛔ 不删条目);盘上有清单无 ⇒ extrasOnDisk(⛔ 不新增);旁挂文件不算 extra
回滚素材(㈡) 命名 = 上一版版本号;内容 = 旧实体;⛔ 不含 node_modules
反证:不许落进被归档目录 落点在被归档树内 ⇒ 抛 pack_into_self

六、门禁与部署

项 读数
npm run build rc=0
npm test 589 tests / 587 过 / 0 败 / 2 跳过(第 20 棒 584 ⇒ +5 用例)
npm run check:layering ✅ 无新增违规(现存 5 条全在基线内)
部署 47 一件(lib/web/routes/business-plugins.js,md5 3a752e01e4f16b4e6d6c47c305362a51 与本地逐字一致)
重启 systemctl restart dshs ⇒ active · NRestarts=0 · 门户 PORTAL_HTTP=200
新口注册 POST /api/plugins/shared/reconcile 无凭据 ⇒ 401(⇒ 路由已注册且门禁生效;404 才是没注册)
备份 /opt/dsh/backups/lib-p21/47-pre-p21-20260925-114554.tgz + manifest-pre-p21.json

⛔ 未做:commit / push|改 src/web/routes/im.ts(零改动)|手改用户 home 下的 profile / .pnpm|清理 .dsh-module-fallback 人工软链(§5-18 待拍板)。


七、未闭合 / 本棒新发现

  1. 🟡 106 侧的 lib/web/routes/business-plugins.js 与 47 不一致(本棒顺带读数):47 投前 4109ba34…(124222 B)/ 投后 3a752e01…(新),而 106 = 70d5c0f2…(114041 B,mtime 09-25 00:28) ⇒ 106 上该文件落后好几个版本。本棒只投 47(改动是 Manager 侧台账逻辑,106 是 worker;⛔ 不擅自改别处的部署面)。⇒ 建议下一棒做一次两机 lib 差异面核对。
  2. 🟡 106 的新代码仍未生效(§5-19 延续):dshs-worker 未重启 ⇒ 重启前的 106 用户仍会撞第 20 棒修掉的 EPERM。⛔ 重启会中断 106 在线实例 ⇒ 待用户给窗口。
  3. 🔴 ㈡ 的"完整替换路径"未在真机跑通:47 现有包在备份面都已有同名素材(上传钩子时代落的)⇒ 新逻辑走"已存在则跳过"⇒ 真机上打包分支未被触发(其正确性由单测 + §四 的等价复现保证)。要真机跑通需真发一个新版本 ⇒ 会改线上共享层内容 ⇒ 本棒未做。
  4. 🔴 待拍板(延续):§5-18 历史人工兜底软链是否清理|§5-14 @softspark/dsh-file-preview 可见性(用户 11:0x 口径已收口标记问题,但它已在共享层 ⇒ 全员可见)。
  5. ⚠️ 已知边界(写进代码注释):新旧同版本号重投时,"上一版素材"与"当前版本快照"同名 ⇒ backupTgz 会指向旧内容;该场景"回到上一版"本就无意义,故不造名区分。

八、清理与卫生

  • 47 /tmp:p21-e2e.sh / p21-e2e.ok / p21-verify.sh / p21-mksess* ⇒ 全部已删(TMP_P21_CLEAN)。
  • 临时 admin 会话:SESSIONS_DELETED=1 ⇒ 残留 0(R4)。
  • 备份面保留(回滚素材):/opt/dsh/backups/lib-p21/(2 件)。
  • 服务状态:dshs=active · dshs-worker=active。
  • 本工作区中间产物 ⇒ tmp/p21-*.sh(收口清理,见 §0 收口行)。

九、复现命令(第三方可照跑)

# ① 清单 ↔ 磁盘真值(只读)
ssh bt-server 'cd /opt/dshs && node --input-type=module -e "
const m=await import(\"/opt/dshs/lib/web/routes/business-plugins.js\");
const fs=await import(\"node:fs\");const root=\"/var/lib/dshs/bundled-plugins\";
const {report}=m.reconcileSharedManifest(root, JSON.parse(fs.readFileSync(root+\"/.manifest.json\",\"utf8\")));
console.log(\"checked=\"+report.checked+\" fixed=\"+report.fixed.length, JSON.stringify(report.fixed));"'

# ② 节点两轮对账(在 106 上,用本机凭据)
ssh test106 'set -a; . /etc/dshs-worker.env; set +a; for i in 1 2; do
  curl -s -X POST -H "x-dsh-agent-token: $DSHS_CLUSTER_AGENT_TOKEN" \
    http://127.0.0.1:19000/shared-layer/pull -w " HTTP=%{http_code}\n"; done'