# S4「接进既有的分发与更新机制」· 落地与回执(2026-09-26 · 第 42 棒) > **线 / 棒次**:插件投放与分库线 · 第 42 棒(执行棒)|会话 `插件投放与分库线-42` > **执行依据**:规格书 `D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md`(§〇 / §五 A / §五 C) > + 方案 `交付物/功能打包到基础插件-分步实施方案-20260926.md` **§3.10.6**(S4 逐步照做唯一权威)+ 方案 S4 段 + §4 C 类坑 C1–C6 > **前置**:第 41 棒 S3「数据面」已落地(建库 `dshs_pl_ai1net` → 迁移 M1→M5 → 对账差集双空,三轮全绿) > **产物**:拉取器 `tmp/p42/puller.mjs`(照抄版)+ 沙箱 Manager `tmp/p42/mgr-mock.mjs` + 编排 `tmp/p42/run-s4.mjs` + 真机锚点 `tmp/p42/anchor-real.mjs` > **读数**:`tmp/p42/读数-20260926.txt`(S4 编排,53 ✅ / 0 ❌)|`tmp/p42/尺读数-S4-20260926.txt`(尺子)|`tmp/p42/real-anchor/`(真机包) > **包内改动**:**零**。S4 全部落在「分发侧 + profile 装配侧」,⛔ 未改 `E:/ProgramData/AIProject/dsh-plugin-ai1net/` 任何文件。 --- ## 一、一句话结论 **包能被平台「对账 + 增量拉取 + 原子换上」,落地后两端指纹逐字一致;三条反例判据全过(清单拿不到 ⇒ 本地零改动/单包失败 ⇒ 其余照常更新/`extra` ⇒ 只告知不删)。** 判据表(**退出码即结论**): | # | 判据 | 期望 | 实测 | 判 | |---|---|---|---|---| | S4-1 | 与权威实现**逐段对得上**(⛔ 不自创第二套) | 逐字 | 11 段逐字照抄(见 §二) | ✅ | | S4-2 | 四步 happy path 跑通 | rc=0 | 对账→取包→原子换→回报 全通 | ✅ | | S4-3 | 依赖写 `link:<绝对路径>`,两端**逐字同形** | 逐字相同 | 两端字符串 100% 相同 | ✅ | | S4-4 | 补 `@deepseek-ai/*` 软链 | 指向本机 scope | linked=1 / kept=3 / relinked=1(负控) | ✅ | | S4-5 | **三条反例判据** | 全过 | 全过(见 §五) | ✅ | | S4-6 | 装配后实例页「能力管理」栏出现 | 需真壳 | ⚠️ **未取到(rc=3)** | ⚠️ | | — | 尺子:装配端A / 端B + 取数 + 回调 + v-baseline | 全绿 | 5/5 `rc=0` + baseline `rc=0` | ✅ | | — | 包内门禁 `build.mjs --check-only` | rc=0 | `rc=0` | ✅ | | — | **真机锚点**:本份指纹实现 vs 真机权威 `treeSha256` | 逐字相同 | **两包都相等**(storyforge 117 / mcn-suite 659,§六) | ✅ | --- ## 二、S4-1「照抄」逐段对照表(⛔ 判据 = 逐段对得上) 权威 = `D:/github/dsh_shenxian/src/worker/agent.ts`(节点侧)+ `src/supervisor/plugin-assembly.ts`(软链自足)+ `src/web/routes/business-plugins.ts`(Manager 侧)。 | # | 段 | 权威位置 | 落到本棒哪里 | 改了什么 | |---|---|---|---|---| | 1 | `hashTreeAt()` | `agent.ts:241-260` | `puller.mjs` | 逐字(仅去 TS 类型) | | 2 | `envNumOr()` + `SHARED_PULL_*` 7 个常量 | `agent.ts:194-227` | `puller.mjs` | 逐字(默认值一字未改:20 000 / 300 000 / 900 000 / 64 KiB/s / 2 / 2 000 / `stable`) | | 3 | `NODE_PULL_STATE_NAME` / `MAX_PACK_BYTES` / 拉取记录读写 | `agent.ts:262-298` | `puller.mjs` | 逐字 | | 4 | `errText` / `PackError` / `isRetryable` / `defaultSleep` / `backoffOf` | `agent.ts:365-393` | `puller.mjs` | 逐字 | | 5 | `effectiveDownloadTimeout()` | `agent.ts:412-421` | `puller.mjs` | 逐字 | | 6 | **`pullOnePack()`**(原子换六动作) | `agent.ts:423-525` | `puller.mjs` | 逐字(含 `tar` 用 cwd+相对名的纪律、gzip 魔数、成员白名单、`chown` 仅 Linux、graveyard 回滚) | | 7 | **`syncSharedLayerOnce()`** | `agent.ts:539-733` | `puller.mjs` | 逐字 + **新增 1 个测试缝** `__dumpPullStateTo`(只多写一份记录,⛔ 不改算法/默认值) | | 8 | `resolveDshPackageRoot()` / `resolveHostDshScopeDir()` | `plugin-assembly.ts:1185-1233` | `puller.mjs` | 逐字同源副本(权威文件是 `.ts`,Node 跑不了;两文件均纯 Node 零框架依赖) | | 9 | `ensureSharedScopeLink()` | `plugin-assembly.ts:1265-1311` | `puller.mjs` | 逐字 | | 10 | `healSharedLayerSelfSufficiency()` | `plugin-assembly.ts:1330-1365` | `puller.mjs` | 逐字 | | 11 | `sharedDirNameOf()`(flat 名口径) | `plugin-assembly.ts:871-873` | `puller.mjs` | 逐字 | **沙箱 Manager 侧**(`mgr-mock.mjs`):`hashTree()` ← `business-plugins.ts:242-259` 逐字|`sharedLayerDiff()` ← 同 :789-798 逐字|产物口打包口径 ← 同 :2528 起(`tar -czf - --owner=0 --group=0 --numeric-owner `,`cwd=root`)|对账端点口径 ← 同 :2432 起(inventory 指纹**只认 64 hex**,其余 `null`="不知道";`pull = missing + stale`)。 ✅ **自查**:本棒产出**没有第二套机制** —— 全部是照抄 + 一个测试缝。 --- ## 三、S4-2 四步实现(happy path) **对账 → 取包 → 落地(原子换六动作)→ 回报**,全部走既有链路口径: 1. **对账**:`POST /api/plugins/shared/node/sync`(头部带 `x-dsh-agent-token` / `x-dsh-host-id` / `x-dsh-pull-channel`;body 带 inventory)。清单拿不到 ⇒ 早退,**⛔ 什么都不动**。 2. **取包**:`GET /api/plugins/shared/node/pack/?version=`;**对账 20 s 与下载 300 s 两套超时分开**(`syncTimeoutMs` / `downloadTimeoutMs`),并带 `packBytes` 量级提示动态放宽、封顶 900 s、最低速率 64 KiB/s(§4-C2)。 3. **落地(原子换六动作)**:① 落 `.pull-stage-` ② `tar -xzf` ③ 成员白名单(必须全在 `/` 下,挡 `../` 与绝对路径)④ **自算 `hashTreeAt` 与平台告知的 `treeSha256` 逐字比** ⑤ `chown root:root`(仅 Linux)⑥ `rename` 原子换(旧目录挪 graveyard → 新的就位 → 成了再删旧的;**换失败把旧的挪回来**);`finally` 只清 stage ⇒ 最坏结果 = "没更新",⛔ 绝不留下半包。 4. **回报**:有失败才 `POST /api/plugins/shared/node/result`(⛔ 不参与任何判定)。 ### happy path 原始输出(节选自 `tmp/p42/读数-20260926.txt`) ``` ════ B · happy path(四步:对账 → 取包 → 落地原子换 → 回报) ════ ✅ ok=true —— reason=null ✅ missing = 两个包(本地全空) —— ["_dsh-local_ai1net","_dsh-local_probe2"] ✅ stale 全空(本地无记录 ⇒ 只能是 missing,⛔ 不会被折算成"一致") —— [] ✅ extra 如实告知本地自有目录(⛔ 平台不据此删 —— 见 C-③) —— ["_dsh-local_leftover"] ✅ pulled = 两个包 —— ["_dsh-local_ai1net","_dsh-local_probe2"] ✅ failed 空 —— [] ✅ channel = stable —— stable ✅ 回报端点:无失败 ⇒ 平台侧零回报 —— resultCalls=0 ✅ 产物口调用 2 次(每包一次) —— ["_dsh-local_ai1net","_dsh-local_probe2"] ✅ 落地后指纹逐字相同 · _dsh-local_ai1net —— node=6f9e6fb121433adc2b29d6f58c2ff9b130b796f3fa70810fd99d4a384ca13d09 mgr=6f9e6fb121433adc2b29d6f58c2ff9b130b796f3fa70810fd99d4a384ca13d09 ✅ 落地后指纹逐字相同 · _dsh-local_probe2 —— node=cd3bd919577b85e09c810cb3ff2ef511eda45504136eadb518c8c2bec19445d5 mgr=cd3bd919577b85e09c810cb3ff2ef511eda45504136eadb518c8c2bec19445d5 ✅ .node-pull.json 记下了两包的 treeSha256(形状同真机 NodePullEntry) —— {"_dsh-local_ai1net":"6f9e6fb12143","_dsh-local_probe2":"cd3bd919577b"} ✅ 第二轮:ok=true 且 pull 清单为空 —— pulled=[] missing=[] stale=[] ✅ 第二轮:产物口零调用(不重拉 = C1 判据的正面) —— packCalls=2 ``` 🔴 **「指纹逐字相同」是本节的核心判据**:节点侧解包后**自算**的 hex 与沙箱 Manager **独立实现**算出的 hex **逐字相等**(36 字节全串,不是前 12 位摘要)⇒ 这同时是 §4-C1 的正面证明。 ### §4-C1 的机验(两侧算法必须同规则)+ 三条负控 ``` ════ A · 指纹算法两侧逐字一致(§4-C1) ════ ✅ 两侧 hex 逐字相同 · _dsh-local_ai1net —— fileCount 8=8 ✅ 两侧 hex 逐字相同 · _dsh-local_probe2 —— fileCount 5=5 ✅ 负控①:不归一化 `\` ⇒ 与权威 hex 不等(判据能变红) ✅ 负控②:"只哈希文件名清单" ⇒ 与权威 hex 不等 —— C1 点名的近似物之一 ✅ 负控③:改一个字节内容 ⇒ 权威 hex 变(指纹对内容敏感) —— 并已还原 ⇒ 指纹可复现 ``` ⚠️ 首版负控写的是"**去掉排序** ⇒ 应不等",**实测失败**(该 fixture 只有 5 个文件,`readdirSync` 顺序恰好等于排序序)⇒ 已换成 C1 点名的两类真近似物(**不归一化 `\`** / **只哈希文件名清单**)⇒ 均能变红。**教训:负控要挑"真会翻车的写法",不是随便改一刀。** --- ## 四、S4-3 profile 装配(`link:` 两端逐字同形) - 端A = **桌面 profile 实件副本**(`E:/github/dsh-desktop-0.1.7rc2/.../home-adapt/profiles/desktop/package.json`,**只读引用,未改真件**)+ 追加本包; - 端B = **平台侧形态**(`profiles/web` 键位:`bundles` + `dependencies` 的 `link:`,`patchReload: live`); - 🔴 两端 `dependencies["@dsh-local/ai1net"]` **逐字相同**,且指向 **拉下来的那一份**(不是源包目录)⇒ 闭环成立: ``` ✅ 两端 `link:` 值**逐字相同**(§4-C3) A=link:E:/ProgramData/AIProject/aliyun-dsh-server/tmp/p42/node-shared/_dsh-local_ai1net B=link:E:/ProgramData/AIProject/aliyun-dsh-server/tmp/p42/node-shared/_dsh-local_ai1net ✅ 端A / 端B 链接均解析到落下来的那一份 ``` - **flat 名口径实测**:`sharedDirNameOf('@dsh-local/ai1net')` = `_dsh-local_ai1net`;与**真机既有包实读**同形(`_dsh-local_storyforge` / `_dsh-local_im-conversation-tabs` / `_softspark_dsh-file-preview`)⇒ ✅ 口径一致。 - ⚠️ **真机平台侧的真值形态** = `link:/var/lib/dshs/bundled-plugins/_dsh-local_ai1net`(`/var/lib/dshs/bundled-plugins` 为真读值)。本沙箱用**本地等价绝对路径**代替 —— **这是建模**,⛔ 不等于"真机装配已通"。 --- ## 五、S4-5 三条反例判据(⛔ 不验不算完成) ### ① 清单拿不到(黑洞)⇒ 本地**零改动** ``` ════ C-① 反例:清单拿不到(黑洞)⇒ 本地文件**零改动** ════ ✅ ok=false 且 reason 指明"取不到" —— reason=sync_unreachable: The operation was aborted due to timeout ✅ pulled 空(一个包都没拉) —— [] ✅ 本地文件零改动(全树 sha256 前后逐字一致) —— 23b0ab26f2788194 → 23b0ab26f2788194(31 项) 留痕条数 31 ⇒ 31;含软链快照,⛔ 不是只看文件 ``` **做法**:黑洞服务收请求**永不响应**(比"连接被拒"更狠,连 HTTP 层都不给);实测走的是 `AbortSignal.timeout(syncTimeoutMs)` 那条真实路径。 ⚠️ **"零改动"口径**:用**自写强留痕**(每个文件的 `size + mtimeMs + 内容 sha256` + 每条软链的目标)算全树单值 —— ⛔ 不能只用 `hashTreeAt`,因为它**刻意不计软链**,会漏掉"软链被动了"这一类改动。 ### ② 单个包失败 ⇒ 其余包**照常更新** ``` ════ C-② 反例:单个包失败 ⇒ 其余包**照常更新**(不是整轮放弃) ════ ✅ stale 含两包(都对不上权威) —— ["_dsh-local_ai1net","_dsh-local_probe2"] ✅ 主包**照常更新**(pulled 含它) —— pulled=["_dsh-local_ai1net"] ✅ 主包新指纹=权威新指纹(逐字) ✅ 失败只记备包一项(不是整轮放弃) —— ["_dsh-local_probe2"] ✅ 失败原因是**永久性**(错误页打进产物口 ⇒ pack_not_gzip) —— pack_not_gzip ✅ 永久性错误**不重试**(attempts=1;§A-3 语义) —— attempts=1 ✅ ok=false 且 reason = partial:…(部分失败语义) —— reason=partial: _dsh-local_probe2=pack_not_gzip ✅ 备包**旧版仍在、零改动**(半包绝不落地) —— 5356194d200f01fb → 5356194d200f01fb ✅ 失败已回报平台侧(/node/result 1 次) —— resultCalls=1 failed=["_dsh-local_probe2"] ``` **加演(同族、语义相反)**:把坏体换成**截断 gzip** ⇒ `tar` 抛裸异常 ⇒ 判为**瞬态** ⇒ **会重试**: ``` ✅ 加演:截断 gzip ⇒ 判为**瞬态**并重试(attempts=3) —— attempts=3 err=Command failed: tar -tzf pack.tgz ``` ⇒ **永久性 vs 瞬态的分流是真的在起作用**(同一个 flat,两种坏体,两套行为),⛔ 不是"一律重试"也不是"一律放弃"。 ### ③ `extra`(本地多出来的包)⇒ 平台只告知,本地**一个文件没被动过** ``` ════ C-③ 反例:`extra`(本地多出来的包)⇒ 平台只告知,本地**一个文件没被动过** ════ ✅ 对账告知了 extra 含它 —— ["_dsh-local_leftover"] ✅ 该目录**仍存在**(⛔ 没被删) ✅ 该目录内容与 mtime **零改动** —— 92cc4a9d4bb63109 → 92cc4a9d4bb63109 ✅ extra ⛔ 不进 pull 清单(不会去拉它) ``` --- ## 六、🔴 真机锚点:本份指纹实现 **=** 真机权威指纹(⛔ 不是近似物) | flat | 真机台账 `treeSha256`(读 `/var/lib/dshs/bundled-plugins/.manifest.json`) | 本份 `hashTreeAt` 实算 | 文件数 | tgz 字节 | 判 | |---|---|---|---|---|---| | `_dsh-local_storyforge` | `1c45c8418875df50a687359806b5b44e5125ffaf7f54c405a566a17d90b1ac3c` | **同串** | 117 | 1,766,902 | ✅ | | `dsh-plugin-mcn-suite` | `27f6c6d7ee84f14666ff966750a7d7d41379d280aef72544ce154bc9f49fac9f` | **同串** | 659 | 7,689,968 | ✅ | ``` ════ 真机锚点 · _dsh-local_storyforge ════ 取包命令:ssh root@47.77.182.89 'tar -czf - -C /var/lib/dshs/bundled-plugins _dsh-local_storyforge' {"flat":"_dsh-local_storyforge","tgzBytes":1766902,"fileCount":117, "got":"1c45c8418875df50a687359806b5b44e5125ffaf7f54c405a566a17d90b1ac3c", "want":"1c45c8418875df50a687359806b5b44e5125ffaf7f54c405a566a17d90b1ac3c","equal":true} rc=0 ════ 真机锚点 · dsh-plugin-mcn-suite ════ {"flat":"dsh-plugin-mcn-suite","tgzBytes":7689968,"fileCount":659, "got":"27f6c6d7ee84f14666ff966750a7d7d41379d280aef72544ce154bc9f49fac9f", "want":"27f6c6d7ee84f14666ff966750a7d7d41379d280aef72544ce154bc9f49fac9f","equal":true} rc=0 ``` (原始读数:`tmp/p42/真机锚点-20260926.txt`) **做法(全程只读)**:`ssh 47 'tar -czf - -C /var/lib/dshs/bundled-plugins '`(**与产物口逐字同口径**的打包)→ 本机解开 → 本份 `hashTreeAt()` 算 → 与真机台账逐字比。⇒ **这一步把"照抄"从"看起来一样"变成"算出来一样"**,是本节最强的一条证据。两个包(117 / 659 文件)**都逐字相同**。 ⚠️ **两处如实**(都不影响上述判据): 1. **首版清场把判据的退出码污染了**:解包暂存目录带 659 个文件,本机的**安全删除护栏**会拦"一次删 >50 个文件"(`SAFE_DELETE_BULK_CONFIRM_REQUIRED`)⇒ `rmSync` 抛错 ⇒ 脚本在 `equal:true` 之后仍以 `rc=1` 退出。 ⇒ 已把清场改成 **best-effort**(清场失败只打日志,**退出码只由 `equal` 决定**)。**教训:辅助动作(清场)不许影响判据的退出码。** 2. **中途一次误读已被纠正**:护栏那次"拦截"实际**已部分删除**了暂存树 ⇒ 事后想复用那份解包物复算,算出 415 文件 / 指纹不符。那不是指纹问题(当时比对是 659 文件 / `equal:true`),而是**素材已被动过** ⇒ 已改为**重新解包**再算,结论以上表为准。**教训:"复用旧素材复算"前先确认素材没被动过。** --- ## 七、尺子回归(⛔ 未改尺子一行) | 面 | 靶子 / 入口 | rc | 说明 | |---|---|---|---| | 装配 · 端A 客户端形态 | `--profile-json tmp/p42/端A-客户端形态/package.json` | **0** | 解析到 `tmp/p42/node-shared/_dsh-local_ai1net/lib/index.js` 8405 B | | 装配 · 端B 平台形态 | `--profile-json tmp/p42/端B-平台形态/package.json` | **0** | 同上(**两端解析到同一份落下来的产物**) | | 取数(via 端B) | 同上 | **0** | 真实 import + 真实模块求值;`inject=['connection']` ✅ | | 取数(via 端A) | 同上 | **0** | 两端加载同一份 ⇒ 一致 | | 回调(via 端B) | 同上 | **0** | 真实 `apply(ctx)`,`routeCalls` 逐字记录 `{"path":"/api/ai1net/ping","methods":["GET"]}` | | `v-baseline.sh` | 47 共享层 + 本机 desktop profile | **0** | 平台侧 4 包;客户端侧 4 bundle | | 包内门禁 | `node scripts/build.mjs --check-only`(包目录) | **0** | 「✅ 门禁通过」(含数据面 ①d 段,`DECL_DIGEST=88c752a3e12597b3…`) | ⇒ **S2 / S3 的绿没被本棒弄红**(尺子一行未改,用 `P39_OUT` 把读数引到 `tmp/p42/ruler/`,⛔ 未覆盖 p40/p41 读数)。 ### ⚠️ 未取到 / 不成立(如实) | 项 | 结果 | 原因 | |---|---|---| | **S4-6 实例页「能力管理(插件/技能)」栏开始出现** | ⚠️ **未取到** | 需**真壳在跑**;启动器属**客户端线**(`dsh-ai1net-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`),本线 ⛔ 不代启 | | `v-ui.sh`(浏览器级) | ⚠️ **rc=3 未取到** | CDP `127.0.0.1:9330` 不可达 ⇒ 本机无运行中的真壳(与前两棒同因) | | `v-plugin.sh 运行` 面 | ⚠️ **不成立(空真)** | 可用启动日志 mtime = **2026-09-25T09:19:43**,**早于本包存在** ⇒ "无失败条目"不构成证据 | | **真机投放**(把包真投到 47/106) | ⛔ **刻意不做** | 属硬边界「⛔ 不代用户起实例 / 不碰把包装进去」;且包尚在沙箱验证阶段 | --- ## 八、🔴 平台既有链路的真实行为 vs 沙箱建模(逐条标注 · ⛔ 不许把建模当"已通") | 项 | 平台既有链路的**真实行为**(读码 + 真机只读取证) | 本棒做法 | 是否 = 真实 | |---|---|---|---| | 对账/产物/回报三个端点 | 真端点存在且**在跑**:真机 `.node-sync-reports.json` 有 `w-106` 的汇报行(`authoritative:4 missing:[] stale:[] extra:[]`),时间 09-26 | 沙箱同口径建模(本机 http) | ⚠️ **建模** | | 指纹算法 | `hashTree`(Manager)与 `hashTreeAt`(节点)**两份实现、同算法**,靠 `test/shared-layer-sync.test.mjs` 钉住 | 两份都照抄,互相对照 + **真机锚点**验证 | ✅ **真实**(见 §六) | | flat 名/`link:` 目标 | `/var/lib/dshs/bundled-plugins/`,真机实读 4 个 flat | 口径照抄,路径本地等价 | ⚠️ **口径真实、路径建模** | | `@deepseek-ai` 软链 | 真机 4 个包**全部**已有:`→ /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai` | 本机真 scope 目录(见 §九) | ✅ **形态与目标形状一致** | | 鉴权 | 真平台查 `dsh_hosts.agent_token` 表(`nodeIdentity`) | 沙箱只比固定令牌 | ⚠️ **建模** | | 版本 / 通道 / 钉版 | 真平台有 `.versions/` 归档 + `channel{stable,canary}` + `pinnedHosts`(真机台账实读 `dsh-plugin-mcn-suite` 两个键都在) + `resolveSharedTarget`(钉版 > 通道 > 现役) | 沙箱**只做"现役一版"** | ⚠️ **未覆盖 B-1/B-2/B-3 三条路** | | 审计落库 | 真平台写 `app.db.audit`(`shared_layer_report`) | 沙箱只写文件 | ⚠️ **建模** | | 传输 | 真平台走公网 / relay(47↔106) | 回环 | ⚠️ **建模** | 🔴 **结论纪律**:本棒能断言的只有 —— 「**照抄的那段拉取逻辑,在既有口径下四步跑通、三条反例成立、指纹与真机权威同源**」。⛔ **不能**据此断言「真机投放已通」「门户链路已通」「跨机对账已通」。真机投放属 S9/部署面。 --- ## 九、S4-4 `@deepseek-ai/*` 软链(缺链现象 = "包在但加载不了") ``` ════ S4-4 ════ ✅ 解析到**本机真实** dsh scope 目录(⛔ 不是造一个) scope=E:\github\dsh-desktop-0.1.7rc2\apps\desktop\.desktop-build\development\project-adapt\node_modules\@deepseek-ai\dsh\node_modules\@deepseek-ai ✅ scope 的父链确是 `/node_modules/@deepseek-ai`(用权威 isDshPackageDir 的同一判据) dsh 包根 package.json name=@deepseek-ai/dsh ✅ scope 目录**名字层**含 dsh 宿主子包(dsh-base / dsh-agent) —— 共 119 项 ✅ (预置后)三包链都在 ⇒ 本轮全 kept,一个字节没动 ✅ 软链指向本机 scope · _dsh-local_ai1net / _dsh-local_probe2 / _dsh-local_leftover ✅ 缺链 ⇒ 补齐(linked=1),即"包在但加载不了"会被自愈 ✅ 幂等:第二轮全 kept,一个字节没动 ✅ 负控:指歪的链被纠正(relinked=1) ✅ 软链不计入 treeSha256(补链不改变指纹 ⇒ ⛔ 不会因此永远 stale) ``` 🔴 **为什么"必须每轮全量补"**(照抄段注释):软链**不进指纹** ⇒ 缺链的老包**永远不会**被判 `stale` ⇒ 只在铺装那一步补的话,老包会**永远**缺这一条。 ⚠️ **一处如实**:本机这份 dev 副本的 scope 里,这些条目是 **pnpm 软链指向 monorepo `packages/**`**,而该路径在**本机不存在**(悬空)—— 那是**客户端线 dev 构建的现状**,与本棒无关。本棒验证的不变量是"**链指向本机解析出的 scope**",⛔ 不验证 scope 内部是否可用。 --- ## 十、§4-C1–C6 坑对表(逐条) | # | 现象 | 本棒怎么处理 | 证据 | |---|---|---|---| | **C1** | 永远 stale / 每轮全量重拉(指纹算法两侧不一致) | 两份实现都照抄;**跨实现互算** + **真机锚点**;3 条负控证明判据能变红 | §二 / §三 / §六;第二轮 `packCalls` 停在 2(**不重拉**) | | **C2** | 对账与下载共用一个超时 | `syncTimeoutMs` / `downloadTimeoutMs` **分开**,`packBytes` 提示动态放宽、封顶、最低速率 —— 全部可被环境变量覆盖、⛔ 不写死 | 逐字照抄;黑洞反例走的是同步超时那条路径 | | **C3** | `link:` 目标两机不同 ⇒ 装配失配 | 两端 `link:` 字符串**逐字相同**;flat 名与权威 `sharedDirNameOf` 同口径 | §四 | | **C4** | `ERR_MODULE_NOT_FOUND`(包在但加载不了) | 补链 + **缺链补齐 + 指歪纠正 + 幂等** 四条判据 | §九 | | **C5** | 「不知道」被折算成「一致」 | 节点侧无记录 ⇒ 报 `null`(⛔ 不报假 hex);平台侧指纹**只认 64 hex**,其余当 `null` ⇒ 判 `stale` 重拉一次自愈 | 逐字照抄;实测第二轮 `stale=[]` 且 `packCalls` 未增 | | **C6** | 失败一轮全放弃 / 删了不该删的 | 三条语义纪律**逐条实测**:① 取不到 ⇒ 零改动 ② 单包失败 ⇒ 只记 `failed`,其余照常 ③ `extra` ⇒ 只告知、⛔ 不删 | §五 | --- ## 十一、硬边界自查 / 遗留 / 回滚 - ⛔ **未改官方 dsh 主程序与缓存**(R2):本棒只**读** `D:/github/dsh_shenxian/src/**`(照抄来源)与本机 desktop 构建的 dev 副本,**零写**。 - ⛔ **未重造分发与更新机制**:全部照抄(§二 对照表 11 段 + Manager 侧 4 段)。 - ⛔ **未代用户起实例**;⛔ **未把包装进任何 profile**(装配用的是 **沙箱 profile**,真件 profile 只读引用)。 - ⛔ **未改客户端工作区** `dsh-ai1net-desktop` / `dsh-desktop-0.1.7rc2` 任何文件(只读引用;`端A` 是 profile **副本**)。 - ⛔ **包内零改动**:`E:/ProgramData/AIProject/dsh-plugin-ai1net/` 一字未动。 - ⛔ **未 commit / 未 push**;真机侧**零写**(只读 `tar` 取包 + 读 `.manifest.json` / `.node-sync-reports.json` / `ls -l` 软链)。 - ⚠️ **红线门禁**:本棒**未触发**任何一项 —— 未重启控制面、未中断在线用户、改动文件数 < 10、**无不可逆操作**(沙箱目录可整体 `mv` 归档)。 - 🔴 **停点**:**S4 的 P1 已解除**(09-26 13:30 用户拍板 = A「放开桌面只读读包权」);**S9 的 P2 仍待拍板,⛔ 本棒未跨过**。 - **遗留(如实)**:① **S4-6 / `v-ui.sh` 仍欠一笔真壳取证**(客户端线窗口)② 沙箱**未覆盖**版本/通道/钉版三条路(真平台有,属 S9/发布面)③ `tmp/p42/real-anchor/_x-*` 是**真机锚点的解包暂存**(含 mcn-suite 659 文件树),因**安全删除护栏**(一次删 >50 文件需确认)**保留未删** ⇒ 随 `tmp/` 7 天口径处理,⛔ 未绕过护栏。 - **回滚**:本棒 **无生产副作用** ⇒ 回滚 = `mv tmp/p42/ 归档/`(按 §七 规格书回滚口径,⛔ 不用 `rm`)。沙箱外部唯一变化 = 沙箱共享层里的软链与 profile 副本,全在 `tmp/p42/` 内。 --- ## 十二、出处 - 规格书:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/交接单_包规格书_20260926.md` - 方案:`交付物/功能打包到基础插件-分步实施方案-20260926.md §3.10.6 / S4 段 / §4 C 类` - 前置回执:`交付物/数据面-S3落地与三轮回执-20260926.md` - 权威实现(只读):`D:/github/dsh_shenxian/src/worker/agent.ts`|`src/supervisor/plugin-assembly.ts`|`src/web/routes/business-plugins.ts` - 本棒产物(⛔ 不入库):`tmp/p42/{puller,mgr-mock,run-s4,anchor-real}.mjs`|`tmp/p42/读数-20260926.txt`|`tmp/p42/尺读数-S4-20260926.txt`|`tmp/p42/real-anchor/`