Files
dsh_ai1net_server/交付物/版本管理与包更新机制-执行报告-20260926.md
T
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

244 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 版本管理与包更新机制 · 第 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`) |