- 变更规模:新增 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/ 知识文件,按口径入库)
21 KiB
版本管理与包更新机制 · 第 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 一句话说明):
scp4 个文件到 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.com200;新路由未带 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) |