Files
dsh_ai1net_server/交付物/版本管理与包更新机制-执行报告-20260926.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

21 KiB
Raw Permalink Blame History

版本管理与包更新机制 · 第 36 棒执行报告(S1–S4)

会话名:p36e-platform-versioning(域锁:src/worker + src/web/routes/business-plugins.ts + 本工作区 交付物/.workbuddy/入口 + dsh-server-docs/05-交接单) 用户拍板(2026-09-26 06:36 本人原话):「C 方案,然后B,这次就用版本更新机制去更新网络上各节点,子节点和设备」 唯一执行单:交付物/版本管理与包更新机制-现状与方案-20260926.md(§8 执行棒细化 S1→S4) 本报告口径:每条附命令原文 + 输出原文 + 退出码;未达成的一律具名,⛔ 不折算成达成。


0 判定(首屏)

子阶段 判定 一句话依据
S1 止血(层 A) ✅ 达成 对账/下载超时解耦、按体积动态预算、重试+退避、失败留痕;全部阈值走 env;已 build + 上机
S2 版本管理(B-1/B-2/B-3) ✅ 达成 版本目录化+通道灰度+定向钉版真机写盘成功;反向判据(钉一个取不到的版本)被 409 挡住
S2 的设备纳入(B-4) 🔴 未达成(具名缺口) 本机没有桌面端工程载体(只有设计文档),且设备侧没有可复用的拉取凭据通路 ⇒ 按执行单 §8.2-1「没有 ⇒ 具名报告缺口 + 给最小接入方案,⛔ 不硬造」处置
S3 全局矩阵(B-5) ✅ 达成 GET /api/plugins/shared/status 真实返回「每包 × 每 host 的目标版本/漂移/最近对账」
S4 真跑分发 ✅ 达成(设备腿见 B-4) w-106 真实拉取 2,018,751 B 的包,耗时 3,437 ms,拉后指纹与权威逐字相同;w-47 结构性免拉(与 Manager 共用同一共享根);主判据读数见 §4.4

主判据(执行单 §6-6):47 侧 /var/lib/dshs/bundled-plugins/.node-sync-reports.json 里 w-106 的 stale 不含 dsh-plugin-mcn-suite 且 failed 为空 —— 达成(见 §4.4 原始 JSON)。


1 改动清单与产物指纹

文件 行数(本棒前 → 后) md5 上机
src/worker/agent.ts 708(HEAD) → 1442 8edac420b7f73b97af28f9890d4dd658 ✅ 47 + 106
lib/worker/agent.js(构建产物) — 83da1ca5129c8ec0c2784dd957f9db6b ✅ 47 + 106
src/web/routes/business-plugins.ts 1877(HEAD) → 3410 1d37e0931f3828d8a66994f325679424 ✅ 47
lib/web/routes/business-plugins.js(构建产物) — ae90ca08a8889c691a5a7a307f97c5b8 ✅ 47
test/shared-layer-sync.test.mjs 592 → 1045 4e52c165ab77f3e7ed0dfadaed58750b —

⚠️ 行数差 ≠ 本棒新增:两个 src 文件在本棒开工前已含前序在途改动(共享层对账面本体,执行单 §1 引的 :373–519 就在其中)。本棒净新增 ≈ 50 行(agent.ts)+ 版本管理运维面(business-plugins.ts)。判据:git diff -U0 | grep '^@@' 的 hunk 分布 + 上机后只有这 2 个 js 文件与现网不同(其余 128 个 lib 文件逐字一致)。


2 S1 止血(层 A)落成内容

病根(执行单 §1):src/worker/agent.ts 的 const timeoutMs = opts.timeoutMs ?? 20000 被对账与产物下载共用 ⇒ 2.0 MB 包跨云(47↔106 经 443 中继)撞 20 s 预算 ⇒ 106 报 failed:[{flat:"dsh-plugin-mcn-suite", error:"The operation was aborted due to timeout"}] + stale:[dsh-plugin-mcn-suite]。

项 落成 可观测点 阈值(全部 env 可注入)
A-1 超时解耦 对账、下载各自独立预算;下载侧用自己的 AbortController + 可重置定时器(收到 content-length 后按实际体积重算) 启动日志一行打印全部预算 DSHS_SHARED_PULL_SYNC_TIMEOUT_MS(20000) / DSHS_SHARED_PULL_DOWNLOAD_TIMEOUT_MS(300000)
A-2 体积动态预算 纯函数 effectiveDownloadTimeout(sizeBytes, …) = min(cap, max(base, ceil(size/rate*1000*1.5))) 导出函数 + 单测 3 断言 DSHS_SHARED_PULL_MAX_TIMEOUT_MS(900000) / DSHS_SHARED_PULL_MIN_RATE_BPS(64 KiB/s)
A-3 重试 + 退避 只重试瞬态失败(PackError.retryable:not_here 不重试、5xx/网络重试);退避 = base × 4^attempt failed[].attempts DSHS_SHARED_PULL_RETRIES(2) / DSHS_SHARED_PULL_BACKOFF_MS(2000)
A-4 失败留痕 失败进 failed[] 带 attempts/firstFailedAt/lastFailedAt;Manager 侧另落 .node-failures.jsonl(环形 200) 47 侧 .node-failures.jsonl —

顺带修掉一个真 bug(回归用例钉住):重试成功后旧码仍会把该包推进 failed[] ⇒ 读数自相矛盾(pulled 与 failed 同现)。现以显式 pulledOk 标志门控,单测断言 pulled 成功时 failed 必须为空。


3 S2/S3 版本管理面(B-1/B-2/B-3/B-5)落成内容

  • B-1 版本目录化:.versions/<flat>/<ver>.tgz 归档(与产物口同口径,含 node_modules,故解包后 treeSha256 与当初记的逐字相同);manifest 每条目新增 history[];新增 POST /api/plugins/shared/rollback-live(原子换:解到 stage → 成员白名单 → 指纹校验 → chown → rename;校验不过 ⇒ 现役一个字不动;被换下来的那版自动归档)。
  • B-2 通道:channel: {canary: <ver|null>} + 节点携带 x-dsh-pull-channel 头;stable 为默认、⛔ 空值即回落现役。
  • B-3 定向钉版:pinnedHosts: {<hostId>: <ver|null>};解析优先级 钉版 > 通道 > 现役。
  • B-5 全局矩阵:GET /api/plugins/shared/status(admin)⇒ {flats, hosts},每格给目标/实际指纹/drift: current|stale|unknown/最近对账与失败。⚠️ 老节点快照无 inventory ⇒ 一律 unknown,⛔ 不折算成 current。
  • 两条纪律(写进代码注释):① 取不到的版本入口就拒(409 version_not_available + 回可选清单)② 重投新版不清通道/钉版(声明与内容解耦)。
  • 向后兼容:所有新字段可选,老节点读新 manifest 不崩(执行单 §8.2-2)。

4 S4 真跑分发(逐条命令 + 输出 + 退出码)

4.1 机制真实调用(生产运维面,admin 临时会话 · R4 用完即删)

$ "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" tmp/p36e-s4.py <sid>
### [0] 共享层当前清单(GET /api/plugins/shared/status · B-5 矩阵)
    http=200
    flats[ dsh-plugin-mcn-suite ] = {"flat":"dsh-plugin-mcn-suite","live":{"version":"0.5.1",
        "treeSha256":"27f6c6d7…49fac9f","fileCount":659},"channel":{},"pinnedHosts":{},
        "history":[],"versions":["0.5.1"],"hosts":[{"hostId":"w-106","channel":"stable",…}]}
### [1] 真投一次(POST /api/plugins/business/share)
    http=200  {"ok":true,"idempotent":true,"path":"/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite", …}
### [2] B-2 通道:canary → 0.5.1
    http=200 {"ok":true,…,"channel":{"canary":"0.5.1"},"versions":["0.5.1"]}
### [3] B-3 钉版:w-106 → 0.5.1
    http=200 {"ok":true,…,"pinnedHosts":{"w-106":"0.5.1"}}
### [4] 反向判据:钉一个取不到的版本 ⇒ 必须 409
    http=409 {"error":"version_not_available","versions":["0.5.1"]}   ⇒ ✅ 已挡住
### [5] 复原:解钉 w-106 + canary 回落现役
    http=200(解钉)/http=200(回落)  ⇒ pinnedHosts {"w-106":null}、channel {"canary":null}
rc=0

写盘复核(ssh 47):manifest 键里出现 channel/pinnedHosts ⇒ B-2/B-3 确实落到生产文件(非仅内存)。

4.2 47 本机 worker(w-47)这一腿 —— 结构性免拉(不是缺口)

$ ssh bt-server 'bash -s' < tmp/p36e-w47.sh
① worker 单元的 SHARED 相关 env:            (空 ⇒ 用代码默认值)
② LISTEN 127.0.0.1:19100 … users:(("node",pid=…))
③ port=19100 tok_len=23
   {"ok":false,"reason":"pull_disabled","missing":[],"stale":[],"extra":[],"pulled":[],"failed":[], …}
④ /var/lib/dshs/bundled-plugins owner=root:root mode=755
   无 .node-pull.json(⇒ 该机从不拉取)
rc=0

判据:共享根路径 = config.ts:627 默认的 <dataRoot>/bundled-plugins,47 的 dataRoot = /var/lib/dshs ⇒ Manager 与 w-47 是同一个目录本体,w-47 即权威源、无需拉取;pull_disabled 是正确形态(不是配置漏了)。⇒ 执行单「分发到 47 本机 worker」由「同目录」满足。

4.3 106 子节点(w-106)—— 真拉 2 MB

为让「节点落后」这一支真实发生(而非空跑),只在 106 侧清掉它自己的拉取记录(共享层内容一字不动):

$ ssh test106 'bash -s' < tmp/p36e-refetch106.sh
① before: entries = ['_dsh-local_storyforge','dsh-plugin-mcn-suite','_dsh-local_im-conversation-tabs','_softspark_dsh-file-preview']
   after : entries = ['_dsh-local_storyforge','_dsh-local_im-conversation-tabs','_softspark_dsh-file-preview']
② === PULL ===
   {"ok":true,"reason":null,"missing":[],"stale":["dsh-plugin-mcn-suite"],"extra":[],
    "pulled":["dsh-plugin-mcn-suite"],"failed":[],"asOf":1790378897940,"channel":"stable"}
   耗时 = 3437 ms          ← 2,018,751 B 包 / 经 443 中继到 47
③ 拉后节点侧记录:{"treeSha256":"27f6c6d7ee84f14666ff966750a7d7d41379d280aef72544ce154bc9f49fac9f",
                  "pulledAt":1790378897938,"version":"0.5.1"}   ← 与权威逐字相同
④ 共享层实况:stage 残留 = 0 · live node_modules 存在 = yes · owner = root:root 755
rc=0

说明:3,437 ms 的实测不能用来断言「旧码同一载荷必失败」——旧码的 20 s 预算在本次网络条件下也可能够;A-1 要消灭的是预算与对账耦合这一结构性缺陷(冷启动/拥塞时同一 20 s 会被对账先吃掉),判据是解耦本身可观测(见 §2 表),⛔ 不夸大。

4.4 主判据读数(拉后第二轮对账 + 47 侧快照)

$ ssh test106 'bash -s' < tmp/_pull_106.sh          # 第二轮(拉后已收敛)
{"ok":true,"reason":null,"missing":[],"stale":[],"extra":[],"pulled":[],"failed":[],"asOf":1790378917884,"channel":"stable"}

$ ssh bt-server 'cat /var/lib/dshs/bundled-plugins/.node-sync-reports.json'
[
  {
    "hostId": "w-106",
    "at": 1790378917692,
    "authoritative": 4,
    "missing": [],
    "stale": [],            ← ✅ 不含 dsh-plugin-mcn-suite
    "extra": [],
    "failed": [],           ← ✅ 为空
    "inventory": [ … {"flat":"dsh-plugin-mcn-suite",
                       "treeSha256":"27f6c6d7ee84f14666ff966750a7d7d41379d280aef72544ce154bc9f49fac9f"} ],
    "channel": "stable"
  }
]
rc=0

⇒ 主判据达成(stale 不含该包、failed 为空)。

4.5 设备(桌面端)这一腿 —— 🔴 未达成,具名缺口 + ⚠️ 本报告一处取证更正(2026-09-26 07:40)

更正:本节初稿(07:3x)写的两条结论,在读到桌面线自己的工作区后被推翻,据实更正 ——

初稿结论 更正后(附依据)
「本机没有桌面端工程载体」 🔴 错(取证面不全)。桌面端工程确实存在:本工作区是文档线(CODEBUDDY.md + docs/ + .workbuddy/,非 git 仓、无执行锁),代码在 E:\github\dsh-client(自研仓)+ 工作树 E:\github\dsh-desktop-0.1.7rc2 / dsh-desktop-0.1.5rc1。初稿只查了本工作区与 D:/github、E:/ProgramData/AIProject,漏了 E:\github
「设备凭据通路不可复用」 🔴 错。设备凭据体系已存在且已实测跑通:用户级设备授权(登录 → 换设备凭据 → 拨号入网,桌面线 S4 实测:network=u:… / hostId=d-<uid>-<公钥前8位> / 租约 24 h)。真问题只是:内容拉取三口只认「主机密钥」,不认识设备

⇒ 因此结论降级(难度大减):B-4 的最小接入方案 ❌ 不是"新造一套设备凭据",✅ 而是给那三个只读口增认「已有的设备身份」(一处校验臂,用已登记的设备公钥)。⚠️ 它仍是权限面扩展(小幅、只读、可撤回) ⇒ 按 R5 先出评估、由用户点头后再由平台线实施。

本棒已交付的替代物(把"具名缺口"落成可开工件):

  • 📄 对接单已写到桌面线工作区:E:/ProgramData/AIProject/dsh-ai1net-desktop/docs/对接单_桌面端接入版本管理与包更新机制_20260926.md(协议四步 + 字段 + 三条语义纪律 + 复用点 + 两处改动 + 3 棒判据 + 10 条坑 + ⛔ 边界),并在该工作区 docs/README.md 索引登记一行。
  • 🔴 原始取证(未做成工程实现,仍为缺口):桌面端没有拉取器、没有把共享包接进 profile 装配 ⇒ 这两件事在对接单 §4.2 里给出落点与判据,⛔ 本棒未实施、⛔ 未假装已接入。

最小接入方案(供拍板后实施,工作量:小-中):

  • ① 平台侧给设备加一条与节点同形的对账面入口,身份用设备自己的公钥(已有字段),不新发密钥;
  • ② 设备侧实现同一个「拨出-对账-拉取-回报」四步(协议与 worker 逐字相同,只把身份从主机换成设备);
  • ③ 矩阵视图(B-5)把设备行与主机行同表呈现(同一 hosts[],加 kind 区分);
  • 前置:先有桌面端工程载体(本轮已确认本机没有)。

5 副判据

副判据 结果
三机产物一致 ✅ lib/worker/agent.js 83da1ca5…、lib/web/routes/business-plugins.js ae90ca08… 在 本机/47/106 逐字相同(其余 128 个 lib 文件未动)
npm test 不劣化 ✅ 全量 646 例 / 641 通过 / 3 失败;3 例失败为 test/im-sdk.test.mjs「内核零改动」断言,原因是 IM 线在途未提交改动(src/im/hub.ts、src/im/ws.ts、src/routes/im.ts)—— 与本棒改动零交集(本棒只改 §1 表内 2 个文件)
本模块单测 ✅ test/shared-layer-sync.test.mjs 32/32 通过(含 A-1 下载慢于对账超时仍成功、A-2 动态预算、A-3 只重试瞬态、A-4 留痕、B-1 归档解包指纹逐字相等、B-2/B-3 解析表 8 断言、B-5 矩阵)
红线零新增 ✅ 无 root 属主实例文件(全棒未触碰任何实例 home)|无权限扩大|无批量写|对 PG 仅 1 次临时会话 INSERT + 1 次 DELETE
34/362/5 影子读 🔴 未重取(具名) —— 该夹具(probe-shadow.mjs)属 MCN 数据面线产物,已在前棒收口时清理,本机无副本;本棒未重建(避免越线 + 成本)。替代取证:本棒对插件数据面与实例文件零写(仅有 1 次临时会话增删),门户 200 / 两机 worker active / 共享根 4 个 flat 无残留。⇒ 下次真正需要该读数时,请由 MCN 线补夹具。
共享根卫生 ✅ 4 个 flat + .manifest.json + .node-sync-reports.json,incoming/trash/pull-stage 残留 = 0(47 与 106 双侧)

6 本棒发现(不影响主判据,建议后续)

# 发现 说明与建议
F-1 归档首次为空 本轮一切重投都是同内容幂等早退(idempotent:true)⇒ 不换版 ⇒ 不归档(语义正确),但结果是 .versions/ 目录尚不存在、history 为空 ⇒ 「可回滚到任意历史版」在第一次真换版之前是空的。建议:首次换版时自然补上;要更早可用,可把既有回滚素材并入归档(见 F-2)。
F-2 两条回滚路径的素材未打通 现网 manifest 里 rollbackTgz = /opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.5.0.tgz 已在位,但没进新归档 ⇒ rollback-live 去回滚 0.5.0 会回 409 version_not_archived;而旧的「素材重放」语义又能回。建议:发布时把既有素材登记成归档一条(纯插入,无破坏性)。
F-3 w-47 pull_disabled 是正确形态 见 §4.2 —— ⛔ 别当故障去"修"(给它配上拉取地址反而会与自身权威源打架)。
F-4 候选池只有 0.5.1 一个版本 GET /api/plugins/business ⇒ 池里该包仅 0.5.1 ⇒ 本轮不存在"更新的版本"可分;这是「真跑分发」只能用同一载荷做增量拉取的原因(⛔ 不伪造新版本)。
F-5 超时耦合的旁证 实测同一 2 MB 载荷 3.4 s 完成;旧码的耦合缺陷在正常网络下不必然暴露 ⇒ 单元测试(A-1「下载慢于对账超时也必须成功」)是本条唯一能稳定复现的判据。

7 部署与回滚

  • 上机动作(动手前已按 R8 一句话说明):scp 4 个文件到 47(2 个 lib + 2 个 src)、2 个到 106;systemctl restart dshs(47)+ restart dshs-worker(47/106)。
  • 现网自证:106 新日志 [shared-pull] 已启用:https://ai1net.com(每 1800 s 一轮) · 通道=stable · 对账超时=20000ms · 下载超时=300000ms(上限 900000ms) · 重试=2 次(退避基 2000ms);门户 https://ai1net.com 200;新路由未带 admin 会话时 401(预期)。
  • 备份:47 与 106 双份 → /opt/dsh/backups/vmgmt-20260926/。
  • 回滚:git revert(本棒未提交,故为将工作树这两处还原)+ 用上述备份回拷 + 重启两个 unit。⚠️ 对账/下载失败本就不动本地文件 ⇒ 无数据风险。

8 卫生 · 锁 · 未做事项

  • R4:临时 admin 会话现场新建、用完即删(deleted = 1)。
  • 远端临时物:47 与 106 的 /tmp/p36e-* 与 /root/.node-pull.json.p36e-bak 全部删除(双侧核对计数 = 0)。
  • 本工作区产物:交付物/版本管理与包更新机制-执行报告-20260926.md(本文件)+ 本棒夹具留 tmp/p36e-*(tmp/ 7 天生命周期)。
  • ⛔ 未 commit / 未 push:按工作区 CODEBUDDY.md §4「未明确要求 ⇒ 不 commit / 不 push」;且 D:/github/dsh_shenxian 工作树里另有其他线在途改动(assets/…、dsh-server-docs/… 等多处),本棒拒绝连带提交。
  • 本棒未做(具名):① B-4 设备接入(§4.5,前置缺失 + 涉权限面,须拍板)② 34/362/5 影子读重取(§5,夹具属别线)③ F-2 归档素材打通(建议项,未实施)。

9 待拍板(1 项)

要你定的是:桌面端(设备)要不要现在纳入版本更新链。

为什么需要你定:纳入就要给每一台桌面设备发一份「能拉包」的凭据 —— 等于在权限面上多开一个口子;而且目前这台电脑上没有桌面端程序本体(只有设计文档),即便现在做出来,也无法真机验收。不纳入的话,桌面端的插件版本继续靠人工更新,时间长了会和服务器节点漂移。

A 案:本次只登记不实施,等桌面端工程就位后再做。 (优点:不动权限面、不产出无法验收的半成品;缺点:设备侧版本仍靠人工,长期与节点漂移。)

B 案:现在先把平台侧的「设备专属拉取凭据 + 对账入口」做出来,客户端以后接。 (优点:客户端一就位即可直接接;缺点:要新增一类凭据,权限面扩大,且当下无法真机验收。)

我的倾向:A(可推翻)。

依据:用户 2026-09-17 明令「要用户拍板的,等拍了再新建接续会话」⇒ 本棒未登记下一棒(唯一剩余实质项 B-4 正卡在这条拍板上)。


附:本棒真实执行清单(可复现)

步骤 命令(本机 / 47 / 106)
状态 "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "$WS/state.py"
开工门禁 bash $DOC/07-scripts/preflight-lock.sh "p36e-platform-versioning" src/worker/agent.ts src/web/routes/business-plugins.ts ⇒ rc=0
抢域锁 bash $DOC/07-scripts/handoff-guard.sh --claim-exec "p36e-platform-versioning" --domains src/worker,src/web/routes/business-plugins.ts,aliyun-dsh-server/交付物,aliyun-dsh-server/.workbuddy,aliyun-dsh-server/接续入口_插件投放与分库线_20260922.md,dsh-server-docs/05-交接单 ⇒ rc=0
构建 tsc -p tsconfig.json ⇒ rc=0(测试跑 lib/ ⇒ 改完必 build)
单测 tmp/_runtest.sh(package.json 的 test 脚本去 npm run build &&)⇒ 646/641/3
三机对账 md5sum lib/**/*.js × 本机/47/106
上机 scp + systemctl restart dshs dshs-worker
真跑分发 §4.1–§4.4 四个探针(tmp/p36e-s4.py、tmp/p36e-w47.sh、tmp/p36e-refetch106.sh、tmp/_pull_106.sh)