# 平台线 · 版本管理与包更新机制 —— 现状取证 + 缺陷 + 方案(规划棒产出) > **会话**:`p36c-platform-versioning`(2026-09-26 06:29–)|**工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` > **上游**:用户 2026-09-26 06:2x 原话「**你就是平台线,看看如何修复(还有 worker 如何更新包,是不是应该有个机制,现在的网络结构各种节点和设备,如何进行版本管理和包更新机制)**」⇒ 本棒**授权扩到平台线**(`src/**` 可改),但本轮**只读取证 + 出方案**(规划与执行分离)。 > **上下文**:承第 36 棒续作(交付件 `MCN数据面接入-阶段二-切档与收口-20260925.md §9`)—— 106 上报 `stale:[dsh-plugin-mcn-suite]` + `failed:[timeout]`。 > **本文件同时是下一棒的「唯一执行单」**(§6 为 8 段交接要素)。 --- ## 1 现状取证(代码级 · 全部带 `文件:行号`,⛔ 未改一行) ### 1.1 已有机制:**节点主动拉(pull)+ 主节点对账告知** | 环节 | 实现 | 位置 | |---|---|---| | worker 定时拉 | 启动即拉一次 + `setInterval(…, pullIntervalMs)` | `src/worker/agent.ts:816-819` | | worker 手动拉 | `POST /shared-layer/pull`(带 `AGENT_TOKEN_HEADER`,⛔ 不进免凭据白名单) | `src/worker/agent.ts:812` | | 拉取主流程 | `syncSharedLayerOnce(opts)` | `src/worker/agent.ts:373` | | 第 1 步:对账 | `POST /api/plugins/shared/node/sync`,body = 本机实况 `inventory[]` | `src/worker/agent.ts:422-427` | | 第 2 步:按差异拉产物 | 下载 + 校验 + 解包 + 覆盖 | `src/worker/agent.ts:~455-520` | | Manager 侧对账面 | 差异告知(`missing`/`stale`/`extra`)+ 落对账快照 | `src/web/routes/business-plugins.ts:373-428` | | 权威清单 | `/.manifest.json`(`version`/`tgzSha256`/`treeSha256`/`fileCount`/`backupTgz`/`rollbackTgz`) | 真机读 | | 对账快照 | `/.node-sync-reports.json`(**每 host 只留最新一条**,共 20 条) | `business-plugins.ts:386,416-428` | | 共享层落点 | `config.bundledPluginDir` = `/var/lib/dshs/bundled-plugins`(root 所有、实例只读挂 `--ro-bind-try`) | `src/config.ts` + `orchestrator.ts` | | 装配 | `app.supervisor.applyPlugins(userId, items)`;集群形态 → worker agent `POST /plugins/apply` | `business-plugins.ts:640-641, 2261` | **结论**:机制**骨架齐全**(拉取 / 对账 / 指纹 / 回滚素材 / 装配 / 定时),⛔ **不是"没有机制"**。 ### 1.2 缺陷清单(按影响排序) | # | 缺陷 | 证据 | 影响 | |---|---|---|---| | **D1** | **下载超时硬编码 20 s,且与"对账"共用同一常量** | `src/worker/agent.ts:376` `const timeoutMs = opts.timeoutMs ?? 20000`;对账面 `:426`、下载面 `:508` 都用它 | 2.0 MB 包跨云(47↔106 经 relay)拉不完 ⇒ `AbortError: The operation was aborted due to timeout`(与 106 上报串**逐字吻合**)⇒ **106 永远停在旧版** | | **D2** | **无显式重试/退避** | 失败只写进 `failed[]`,靠下一个定时轮再试 | 偶发网络抖动 = 白等一整轮 | | **D3** | **失败历史不可见** | `.node-sync-reports.json` 每 host 只留最新一条(`business-plugins.ts:417`) | 无法回答"这个节点连续失败几次了" | | **D4** | **版本管理只有"最新一份"** | manifest 每 flat 只有一组 `version`/指纹;`rollbackTgz` 只保**上一版** | 无法:多版本并存 · 灰度 · 按节点/设备定向 · 降级到任意历史版 | | **D5** | **无"目标版本"声明面** | 节点拉取目标是"Manager 当前共享层",无 channel/版本钉选 | 不能"先让测试节点升,再全量" | | **D6** | **设备(桌面端)不在分发链上** | 分发链只覆盖 worker;桌面端节点/设备无包更新通路(本轮未取证到任何设备侧拉取实现) | 设备侧版本无从管理 | | **D7** | **平台侧无全局版本矩阵视图** | 只能逐 host 看 `reports`(且只留最新) | 运维要"一屏看全节点版本与漂移",现在做不到 | | **D8** | **台账状态与实时视图不一致**(关联发现) | `dsh_instances.status=stopped` vs 实时 `running@20001` | **影响一切"实例是否在跑"的判断**(第 36 棒已被误导过一次) | --- ## 2 方案(分层:先堵血、再补机制、后做拓扑) ### 2.1 层 A —— **立即修复**(小改、可独立验证、⛔ 不动数据模型) | 项 | 做法 | 判据 | |---|---|---| | **A-1 超时分离** | 新增 `syncTimeoutMs ?? 20000`(对账)与 `downloadTimeoutMs ?? 300000`(产物下载)两个参数,`:426` 用前者、`:508` 用后者 | 106 拉 2 MB 包成功;`failed` 清空 | | **A-2 按大小动态超时** | 若有 `content-length`,`effective = max(60s, size / 已知最低速率 + 余量)`,并设上限 | 大包不再靠猜 | | **A-3 重试 + 退避** | 单轮内失败重试 2 次(指数退避 2s/8s),仍失败才写 `failed` | 抖动可自愈 | | **A-4 失败留痕** | `failed` 记 `{flat, attempts, lastError, firstFailedAt}`;对账快照保留改为「每 host 每条 flat 一条历史」或另落 append-only 日志 | 可回答"连续失败几次" | | **A-5 台账纠偏** | 单独立项核查 `dsh_instances.status` 写回路径(属 D8,⛔ 不在本棒范围) | 台账 == 实时视图 | > ⚠️ **A-1/A-3 是"纯插入"型改动**(不破回归)⇒ 符合本平台既有「新增能力 ⇒ 纯插入;修 bug ⇒ 可改既有逻辑但须回归用例 + 新行为可观测 + 阈值可注入」纪律。 ### 2.2 层 B —— **版本管理与更新机制**(机制补全) **B-1 版本目录化**:共享层从「一份包」改为「**一份包 + 版本目录**」—— ``` // # 现状:只有当前版 /.versions/// # 建议:历史版本留档(tgz + treeSha256) /.manifest.json # 每 flat 增:current / channel / history[] ``` ⇒ 支持**任意版本回滚**(不再只有 `rollbackTgz` 一档)。 **B-2 目标版本声明(channel)**:manifest 每 flat 增 `channel: { stable: , canary: }` + 节点侧 `pullChannel`(env)。 ⇒ 节点按 channel 拉 ⇒ 天然支持**灰度和定向发布**(测试节点走 canary)。 **B-3 节点/设备下发列表(可选)**:`channel` 之外再支持 `pinnedHosts: { : }` ⇒ 单节点钉版/降级。 **B-4 设备(桌面端)纳入**:设备侧实现同一个 `syncSharedLayerOnce` 语义(或复用打包好的 SDK/客户端),凭据走**设备 grant**(序46/47 已有设备凭据体系)。 ⇒ 设备与 worker 用**同一套对账协议**,只是 `hostId` 换成设备 id。 **B-5 全局版本矩阵**:新增 Manager 侧 `GET /api/plugins/shared/status` —— 返回 `{ flat → { current, 各 host 的 version/指纹/drift/最近成功时间 } }`。 ⇒ **一屏看全节点与设备**(用户问的"如何进行版本管理"的运维面)。 ### 2.3 层 C —— **拓扑与跨区联邦**(架构级,承 `架构设计/覆盖网络-顶层架构全貌.md`) - **内容源归属**:每个 **Manager 各持有自己的共享层**(= 各区 Manager 自带内容源),节点/设备只向**自己所属区的 Manager** 拉 ⇒ ⛔ 不做"全网单点源"(否则跨区带宽与单点故障)。 - **跨区一致性**:包由**版本号 + treeSha256** 标识 ⇒ 各区可独立发包,**同一版本号必须字节一致**(发布时校验 `tgzSha256`);不一致 = 发布失败。 - **离线/弱网**:拉取失败**不影响实例运行**(已有语义:本轮不动本地文件)⇒ 版本更新是**最终一致**,不是强一致。 - **安全**:包校验用 `tgzSha256` + 解包后 `treeSha256`(已有);🔴 建议补**发布者签名校验**(承覆盖网络线"根密钥只管授权撤回签名者"的口径)—— ⚠️ 属跨线议题,**本棒只登记不设计**。 --- ## 3 本轮**立即能定**与**需用户拍板**的分界 - **立即能做(无需拍板)**:层 A 全部(病因明确、纯技术);层 B-5(只读视图,不加新概念)。 - **需拍板**:**层 B 的深度** —— 做到"单版本 + 任意回滚"(B-1)就够,还是直接做到"channel 灰度 + 定向钉版"(B-1~B-3)?(见 §5 待拍板) - **跨线登记**:B-4(设备纳入)依赖序46/47 的设备凭据体系;层 C 属覆盖网络线架构层 ⇒ **只登记,不本线实施**。 --- ## 4 只读前置(下一棒开工先跑,⛔ 不许跳) 1. `"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "$WS/state.py" --online` 2. `bash $DOC/07-scripts/preflight-lock.sh "<会话名>" src/worker/agent.ts src/web/routes/business-plugins.ts` 3. 🔴 **`src/worker` 属机制层** ⇒ 按域锁规则「机制层必须独占」;**开工前必须确认无其他会话在跑**(`state.py` 看持有者活性)。 4. 真机只读:`cat /var/lib/dshs/bundled-plugins/.node-sync-reports.json`(47)+ `systemctl cat dshs-worker.service`(看 pull 相关 env)。 ## 5 决策点(**待拍板 = 1 项**) **🔴 已拍板(2026-09-26 06:36 · 用户本人原话)**:「**C 方案,然后B,这次就用版本更新机制去更新网络上各节点,子节点和设备**」。 **口径对齐(⛔ 执行棒务必按本行理解,勿被 §2 的层名混淆)**: - 用户说的 **C 案** = 本文 **层 A(止血)+ 层 B-1~B-4**(版本目录化/channel 灰度/定向钉版/**设备纳入**)——「完整版本管理」。 - 用户说的 **B 案(随后做)** = 本文 **层 B-5**(全局版本矩阵视图)。 - **层 C(跨区拓扑)本次 ⛔ 不实施**(属覆盖网络线架构层);但实现须与其口径兼容:每区 Manager 各持内容源、节点只向本区拉、同版本号须字节一致。 - **收尾必须用这套机制真跑一次分发**:`dsh-plugin-mcn-suite@0.5.1`(含以后版本)→ **47 本机 worker + 106 子节点 + 设备(桌面端)**,并读数验证。 - ✅ **执行棒已登记** = automation `da73ab87-fb2f-47f0-a8d8-8ea3ad35eb19`(一次性 · 2026-09-26T06:52)。 **原三选项(存档)**: - **A 案(只修超时,层 A)**:优点 = 改动小(两处常量 + 一个重试)、当天可验、106 立刻能拿到修复;缺点 = 版本管理仍是"最新一份",下次要灰度/回滚还得再做一轮。 - **B 案(层 A + 层 B)**:优点 = 一次把"节点与设备的版本管理与更新机制"补齐(含全局矩阵视图);缺点 = 要动 manifest 结构与装配口径,回归面更大,且要新增概念(channel),周期长。 - **C 案(层 A + 只加全局矩阵视图 B-5)**:优点 = 先解决"看不见"的问题(D7),成本低,为后续灰度留观测底座;缺点 = 仍无灰度/多版本能力。 - **我的倾向**:**A 先落、C 紧随**(可推翻)—— 106 上有真实用户在等修复,A 是止血;B-5 是纯读视图、不加概念,风险低且立刻让版本漂移可见;**层 B 的通道化等有过一次真实灰度需求再做**。 ## 6 八段交接要素(下一棒 = 执行棒) 1. **目标**:修 D1(106 拉包超时)并按用户拍板落地层 A(/+B-5/+B)。 2. **只读前置**:同 §4。 3. **范围**:`src/worker/agent.ts`(`syncSharedLayerOnce` 超时/重试)+(按拍板)`src/web/routes/business-plugins.ts`(对账面)+ 新增只读状态路由;⛔ 不改 `manifest` 结构之外的既有语义;⛔ 不动 `07-scripts/`。 4. **决策点**:§5 那一项(须先拿到回话再开工)。 5. **步骤**:① 只读复核 §1 全部行号仍在 ② 落 A-1/A-2/A-3/A-4 ③ 单测(若有)+ `npm test` ④ `build` ⑤ 47 `restart dshs-worker`⑥ 读 106 对账快照确认 `stale`/`failed` 清空 ⑦(拍板含 C/B 时)落状态路由 ⑧ 收口。 6. **验收**:**主判据 = `.node-sync-reports.json` 里 `w-106` 的 `stale` **不含** `dsh-plugin-mcn-suite` 且 `failed` 为空**;副判据 = 34/362/5 影子读不变、红线零新增、`npm test` 不劣化。 7. **回滚**:`git revert` 本棒提交 + `restart dshs-worker`;⚠️ 对账/下载失败**本就不动本地文件** ⇒ 无数据风险。 8. **回报格式**:逐条附命令原文 + 输出原文 + 退出码;未达成具名。 ## 7 本轮已改文件(⛔ 仅本工作区产物,未碰 `src/**`) - 本文件(新建)|入口 §0/§2 + `$DOC/05-交接单/README.md §一` + 当日日志(收口行) --- ## 8 执行棒细化(拍板后新增 · 2026-09-26 06:36) ### 8.1 子阶段顺序(⛔ 不许跳、不许换序) | 子阶段 | 内容 | 完成判据 | |---|---|---| | **S1 · 止血** | 层 A-1~A-4(拆超时 20 s↔300 s/按大小动态/重试退避 2s·8s/失败留痕) | 106 对账快照 `stale` 不含 `dsh-plugin-mcn-suite`、`failed` 为空 | | **S2 · 完整版本管理** | 层 B-1 版本目录化 + B-2 channel(stable/canary)+ B-3 定向钉版 + **B-4 设备纳入** | 能"只让某节点/设备升到指定版本"、可回滚到任意历史版;**设备侧真跑通一次拉取** | | **S3 · 全局矩阵视图** | 层 B-5 `GET /api/plugins/shared/status` | 一屏返回「每 flat × 每 host/设备」的版本、指纹、漂移、最近成功时间 | | **S4 · 真跑分发**(收尾必做) | 用 S1–S3 落成的机制把 `0.5.1` 分发到 **w-47 + w-106 + 设备** | 三处读数一致;`w-106` 快照 `stale/failed` 清空 | ### 8.2 未知项与处置(⛔ 不许"假装知道") 1. **设备(桌面端)侧载体未取证**(承 D6)⇒ **开工第一件事就是取证**:桌面端有没有「包/插件目录 + 拉取入口」、凭据走哪条(序46/47 的设备 grant?)。**有 ⇒ 按同一对账协议接入;没有 ⇒ 具名报告缺口 + 给最小接入方案,⛔ 不硬造。** 2. **`manifest` 是共享层旁的旁挂状态文件** ⇒ 改其结构须**向后兼容**(老节点读新 manifest 不能崩)。 3. **`src/worker` 属机制层** ⇒ 域锁必须**独占**(开工前确认无其他会话在跑)。 ### 8.3 必须遵守的既有纪律 - 新增能力 ⇒ **纯插入**;修 bug ⇒ 可改既有逻辑但**须回归用例 + 新行为可观测 + 阈值可注入**(超时/重试次数一律走 env,⛔ 不写死)。 - 测试跑 `lib/` ⇒ **改完必 `build`**(否则新旧产物混跑,症状像"注入无效")。 - ⛔ 不改 `07-scripts/`;⛔ 不动 `config`/`crypto`/`isolation`/`index` 等机制层其他文件(只碰必需者且先报告)。 - 生产变更(`restart dshs-worker` / 47)可直接做,**动手前一句话说明**;不可逆操作先报清单。 --- ## 9 收口记录(2026-09-26 06:36–06:5x · 会话 `p36d-platform-versioning`) > 口径:每条附**命令原文**+**输出原文**+**退出码**。⛔ 未 ssh 真机、⛔ 未改 `src/**`、⛔ 未碰他线在途改动。 ### 9.1 落盘四处(命令 + 输出 + 退出码) | # | 落点 | 命令(节选) | 输出原文 | rc | |---|---|---|---|---| | 1 | 本件 §5 + §8 | `patch-c3.py`(步 1) | `DOC_S5_OK` / `DOC_S8_OK` | 0 | | 2 | 入口 §0 拍板行 | Edit 精确片段替换(锚 = `- 🔴 **2026-09-26 06:29–06:5x|用户授权`) | `Successfully edited file` | 0 | | 3 | 入口 §2 续作二/三注 | Edit(锚 = `⛔ 平台侧问题(拉取超时/台账滞后)**只报告不改码**。详解 ⇒ 交付件 **§9**。`) | `Successfully edited file` | 0 | | 4 | `$DOC/05-交接单/README.md §一` 第 44 行 | Edit(锚 = `🔴 **2026-09-26 06:29 用户授权「你就是平台线」⇒ 已出《版本管理与包更新机制-现状与方案-20260926》**`) | `Successfully edited file` | 0 | | 5 | `.workbuddy/memory/2026-09-26.md` | Edit(追加「第 36 棒续作三 · 拍板落地棒」节) | `Successfully edited file` | 0 | **校验读数**(`wc -c` / `grep -c`): ``` 入口 344849 B / 986 行 grep -c "2026-09-26 06:36" = 2 grep -c "续作三" = 1 本件 14xxx B grep -c "已拍板(2026-09-26 06:36" = 1 grep -c "^## 8 执行棒细化" = 1 README 62575 B grep -c "06:36 用户拍板" = 1 grep -c "da73ab87" = 1 日志 20091 B grep -c "p36d-platform-versioning" 节 = 1 ``` ### 9.2 🔴 事故:入口被清 0 字节(已自愈 · 无损)+ 新增铁律 - **触发**:`tmp/p36/patch-c3.py` 第 70 行 `open(P, "wb").write(b"\n".join(parts))` —— `NEW0` 漏 `.encode("utf-8")`。 - **输出原文**:`Traceback … line 70, in \ TypeError: sequence item 11: expected a bytes-like object, str found`,`RC=1`。 - **真实伤害**(比 traceback 严重得多):复合式求值顺序 = **先 `open(P,"wb")`(此刻文件已被截断为 0)→ 再求值 `join()`** ⇒ join 抛错时**入口已经没了**。若只看 traceback 会误判成"没写进去"。 - **发现**:`wc -c 接续入口_插件投放与分库线_20260922.md` ⇒ `0`。 - **复原**(无损): ``` git checkout -- "接续入口_插件投放与分库线_20260922.md" # rc=0 wc -c → 341330 ; wc -l → 968 ;grep -c "2026-09-26 06:29" → 1 git hash-object → 1d44073bdd40b4a3becfd45f2cd26f4cb78796ad git rev-parse HEAD: → 1d44073bdd40b4a3becfd45f2cd26f4cb78796ad (逐字节一致) git status --porcelain → (空 = clean) ``` 前提核对:HEAD 版**含** 06:29 规划棒行、`git fetch` 后 `git rev-list --left-right --count origin/master...HEAD` = `0 0` ⇒ 复原不丢任何已落内容。 - 🔴 **新增铁律(已写入入口 §0 事故留档)**:改共享文件**只准** ① Edit 精确片段替换,或 ② 「**先算全量 bytes 再一次性写**」;⛔ **禁用 `open(...,"wb").write(表达式)` 复合式**(表达式求值前文件已被清空)。同源风险 = **任何"先 open 再算内容"的写法**。 - **补救方式**:改用 Edit 工具做四处插入,不再走整文件覆盖。 ### 9.3 提交 / 推送 / 对账(命令 + 输出 + 退出码) ``` $ git add -- "交付物/版本管理与包更新机制-现状与方案-20260926.md" \ "接续入口_插件投放与分库线_20260922.md" ".workbuddy/memory/2026-09-26.md" $ git -c core.quotepath=false diff --cached --name-only .workbuddy/memory/2026-09-26.md 交付物/版本管理与包更新机制-现状与方案-20260926.md 接续入口_插件投放与分库线_20260922.md ← 仅 3 件,⛔ 无 tmp/、⛔ 无中间产物、⛔ 无 05-交接单 $ git diff --cached --stat 3 files changed, 94 insertions(+), 1 deletion(-) $ git -c core.autocrlf=false commit -F - → RC=0 ;HEAD = 2a1226c $ git push origin master → RC=0 To work.alotbuy.com:maogeigei/dsh_shenxian_workspace.git a38d8b6..2a1226c master -> master $ git fetch -q origin ; git rev-list --left-right --count origin/master...HEAD 0 0 ← ✅ 对账归零 $ test "$(git rev-parse HEAD)" = "$(git rev-parse origin/master)" → ✅ 一致 ``` ⛔ **未 `git add -A`**;⛔ **未代提交他线在途改动**(`CODEBUDDY.md` / `state.py` / `.codebuddy/rules/server-ops.md` / `.workbuddy/memory/MEMORY.md` / 他线 `接续入口_*` 均**未**纳入)。 ### 9.4 文档库侧 - 本线本轮在文档库**唯一**改动 = `$DOC/05-交接单/README.md §一` 第 44 行。 - `git check-ignore -v` ⇒ `.gitignore:45:dsh-server-docs/05-交接单/` ⇒ **整目录被忽略、未被跟踪** ⇒ **按既有口径不入库**(⛔ 未 `git add -f`)。 - 推送前对账 `07-scripts/docs-sync-check.sh` ⇒ **`结果: 存在差异 ❌`**(一致 268 / 内容不一致 **21** / 仅本地 **4** / 仅服务器 0)。🔴 **差异面全为既有在途**(`07-scripts/*`、`08-skills/*`、`CODEBUDDY.md`、`01-规范/09`、`02-架构设计/*`、`04-调整方案/*`、`05-交接单/*` 等**他线在改**)⇒ **⛔ 非本线引入、⛔ 本线不动不属于自己的文件**;结论 = **如实记录、交文档库同步线处置**。 ### 9.5 automation 状态 | id | 名称 | 期望 | 实测 | |---|---|---|---| | `35e1bfbc-d247-4db2-9d90-b0580793736f` | 插件投放与分库线-第36棒-执行棒 | `PAUSED` | ✅ `PAUSED` | | `da73ab87-fb2f-47f0-a8d8-8ea3ad35eb19` | 版本管理与包更新机制-执行棒 | `ACTIVE` · `2026-09-26T06:52` | ✅ `ACTIVE`(排期 = 收口 06:44 + 8 min,落在 5~8 min 窗口内) | ### 9.6 锁 - 域锁 `p36d-platform-versioning`(域 = `交付物` / `.workbuddy` / 本线入口 / `05-交接单`)= **持有中**,反序释放:先 `--release-publish`,**最后** `ME="p36d-platform-versioning" --release-exec`(⛔ 不带会话名 ⇒ fail-closed 拒)。 - 复用既有会话名 `p36d-platform-versioning` 领发布锁(`--claim-publish "p36d-platform-versioning"` → `✓ 已持发布锁`,rc=0)。