# 第 18 棒落地件 · 共享层自足性转正 + 回滚残留 + 移动端第 ④ 条(2026-09-25) > **线**:插件投放与分库线(入口 `接续入口_插件投放与分库线_20260922.md`) > **棒次**:第 18 棒 · 执行棒(automation `ed2b5e61-25b9-4b97-849c-1e0b204b669e`) > **代码基线**:`D:\github\dsh_shenxian`(生产口径 `dshs`)|⛔ 未 commit / push > **结论**:第 1 件 ✅(判据 ①②③ 全绿)|第 2 件 ✅(判据成立,另发现"缺失侧"未覆盖 ⇒ 已上报)|第 3 件 ✅ **5/5**(含本次修的第 ④ 条) --- ## 一、第 1 件 · 共享层自足性从探针转为正式修法 **病**:共享只读包库 `/node_modules` 里只有插件自己的依赖、**零个 `@deepseek-ai`** ⇒ `link:` 形态下实例从共享层加载宿主代码必报 `Cannot find package '@deepseek-ai/dsh-llm'` ⇒ 实例 crash-restart ⇒ 探活判"实例未能启动"⇒ 插件被自动禁用。 **修法(唯一一份实现落在 ③ 能力层,① 入口层复用)** | 落点 | 内容 | |---|---| | `src/supervisor/plugin-assembly.ts` | 新增 `resolveDshPackageRoot()`(解析顺序:env 显式覆盖 → 平台配置的 dsh bin 解软链 → `npm root -g`)、`resolveHostDshScopeDir()`、`ensureSharedScopeLink()`、`healSharedLayerSelfSufficiency()` | | `src/web/routes/business-plugins.ts` | `doShare` 目录就位后调用 `healSharedLayerSelfSufficiency(root)`(**刻意放在 `hashTree` 之后** ⇒ 指纹不含软链,不引起重拉)+ 审计与响应体带 `sharedSelfSufficiency` 摘要 | | `src/worker/agent.ts` | `syncSharedLayerOnce` **每轮**幂等自愈(worker 是独立单元,Manager 与 worker 各自把关) | | `src/web/dsh-install.ts` | 该文件原有一份 dsh 根解析副本 ⇒ **下沉到 ③ 层复用**,删本地副本(代价:不再是"零内部 import",验证改走 `scripts/verify-dsh-install.mjs`) | **软链落点 = 包目录内 `/node_modules/@deepseek-ai`**,⛔ 不落共享层根 —— 否则 `syncSharedLayerOnce` 的 `onDisk` 对账面会把 `node_modules` 当"多出来的产物"上报 `extra`。 **判据与读数(真机,`47 = bt-server` / `106 = test106`)** | # | 判据 | 读数 | |---|---|---| | ① | **删掉人工探针链后重新 share ⇒ 共享层自带解析链** | 响应 `sharedSelfSufficiency.scope = /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai`,`linked = ['_dsh-local_im-conversation-tabs','_dsh-local_storyforge','dsh-plugin-mcn-suite']` | | ② | **47 + 106 两端都成立** | 47 三包 scope 均 LINK 可解(240 项);**106 三包 scope 均 LINK → `/usr/lib/node_modules/...`**(两机 `npm root -g` 路径不同 ⇒ 必须各机本地解析,⛔ 不能由 Manager 算好随包分发) | | ③ | **重新启用 mcn-suite ⇒ 实例加载 0.3.15、任务 success、`rejected:[]`** | `off` 任务 `a524537251ff570a` = `success`;`on` 任务 `28dd88e4c391cad6` = `success`、`rejected:[]`、实例探活通过;journald:**`[mcn-suite] loaded v0.3.15(host 面 2 个:mcn, voxemw-cloud)`** + `skills ensured: 装 mcn-short-video@0.3.15(升级自 0.3.14)`;`/api/dsh/status` = `running:true port:20000 restarts:0` | **⛔ 未污染对账面**:共享层根**没有**多出 `node_modules`;`hashTree`/`hashTreeAt` 本就不计符号链接 ⇒ `treeSha256` 不变 ⇒ 不触发节点重拉。 --- ## 二、第 2 件 · 回滚残留 **病**:`restoreProfile()` 只还原 `package.json`/`pnpm-lock.yaml`/`cordis.patch.yml` ⇒ 回滚后 `node_modules` 与声明不一致。 **修法**:新增 `declaredNamesIn(snap)`(`dependencies` ∪ `dsh.profile.bundles`,快照解析失败 ⇒ 取空集**宁可不删**)+ `pruneOrphanDepLinks(dir, snap)`(**只删软链**、**只删未声明**、跳过 `.` 开头与真目录、scoped 目录下钻),在 `restoreProfile()` 末尾调用。单测钉住四条不变量:声明内不删 / `bundles` 内不删 / 真目录不碰 / `.pnpm` 不碰。 **判据**:回滚后 `node_modules/` 与 `dependencies` 一致(至少不残留无主软链)⇒ ✅ 成立。 > 🔴 **本棒实测到的另一半(⛔ 未修,已上报)**:残留有**两个方向**。本次 `off` 动作在真机上撞 `pnpm remove` 失败(见 §四-1),结果是**声明还在、`node_modules/dsh-plugin-mcn-suite` 软链被删掉了** —— 实例启动直接崩: > `Error: dsh: cannot resolve profile bundle "dsh-plugin-mcn-suite" from the dsh installation or ` ×5 ⇒ `crash-loop-circuit-open`(冷却 600 s)。 > `pruneOrphanDepLinks` 只治"多",不治"缺"。**建议修法(下一棒,小改)**:`restoreProfile()` 里对**声明中的 `link:` 依赖**做一次确定性重建(`link:` 目标就在快照里,纯 `symlinkSync`,**不需要跑 pnpm**、不碰 `.pnpm` 实体)⇒ 两个方向都收敛。 > > **恢复过程(走平台正规口,未手改 profile)**:`off`→`on` 循环重跑,两次任务均 `success` ⇒ `pnpm install` 自动重建软链 ⇒ 实例重启探活通过。⛔ 全程未手工 `ln -s`、未手改 `package.json`/`.pnpm`。 --- ## 三、第 3 件 · 移动端第 ④ 条(聚焦不缩放)+ 五条重跑 **改了什么**(插件真源 `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/lib/client.js`,`mcn-mobile` 段第 ③ 条) ```css .mcnNav_input,.mcnNav_select,.mcnNav_textarea,.v1rksel,.dvAcc_input,.dvAcc_select,.dvAcc_textarea{font-size:16px!important} .mcnNav_panel input:not([type=checkbox]):not([type=radio]),…,.mcnNav_page select,…,.dvAcc_page textarea{font-size:16px!important} ``` 理由:`.dvAcc_*` 基础样式用 `font:var(--dsw-font-s-14)` **简写**且**按需注入**(可能晚于本段)⇒ 必须 `!important`;`SELECT.v1rksel` 此前**不在任何移动端规则内** ⇒ 13.33px。 **投放链**:版本 `0.3.14 → 0.3.15`(`package.json` + `skills/.manifest.json` 两处真源)⇒ 以池内 0.3.14 tgz 为基底**确定性重打**(322 文件,变化面**恰 3 个文件**,自证基底 md5 ≡ 线上 `d387089b…`)⇒ `POST /api/plugins/business` ⇒ 两段式 share(`action:"replaced"`、`treeSha256 939206c76abb…`、`fileCount 657`)⇒ 47/106 共享层双双 0.3.15(106 首轮 `stale→pulled`)⇒ 47 四件、106 两件 md5 逐字一致 ⇒ restart ⇒ 门户 200 / worker 401。 **五条真机读数(`tmp/p17-visual.mjs`,零依赖 CDP 驱动本机真 Chrome,视口 375×667 + 横屏 667×375)** | # | 判据 | 读数 | 判定 | |---|---|---|---| | ① | 工作台独占 | 开面板后 `navCount=1`,面板盒 `{x:56,y:0,w:319,h:667}`;会话区 `display:none` | ✅ | | ② | 能退出 | 二次点 `.mcnNav_trigger` ⇒ URL `#/mcn`→`/`、`navCount` 1→0、会话区复原(现版 UI **不渲染** `.mcnNav_close` ⇒ 用等价判据) | ✅ | | ③ | 无横向溢出 | `scrollWidth==innerWidth`(竖 375=375、横 667=667,溢出 **0**) | ✅ | | ④ | **聚焦不缩放** | 工作台内唯一可见表单控件 `SELECT.v1rksel` 计算字号 = **16px**(修前 13.33px);注入样式段文本级取证:`style[data-plugin-css="mcn-mobile"]` 命中新规则 | ✅ | | ⑤ | 横屏可用 | 667×375:面板 `{x:56,y:0,w:611,h:375}`,溢出 0 | ✅ | **零点**:`style[data-plugin-css="mcn-mobile"]` 计数 = 1 ⇒ 新段确在浏览器里生效(不是"以为改了")。 --- ## 四、两条新发现(⛔ 未擅自修,已写进 §2 后续) 1. 🔴 **`pnpm remove/install` 在真 profile 里会**偶发**失败(exit 255,`--reporter silent` 把原因全吞掉)** —— 第 16/17/18 棒累计三次(09:01 apply timeout、09:05 isolate install 失败、09:56 remove 失败)。**本棒已排除的假设(全部实测反证,别再重复走)**:㈠ 共享包目录内多出的 `node_modules/@deepseek-ai` 软链(A/B 对照**双双 rc=0**)㈡ 真实路径条件(真实路径副本 rc=0)㈢ worker 的最小 spawn 环境(`env -i PATH=… HOME=…` rc=0)㈣ 祖先目录不可 `scandir`(`/var/lib/dshs` 与 `/var/lib/dshs/users` 是 **711 ⇒ 该 uid `r` 位确实没有**,但实测不影响 pnpm 运行)㈤ 磁盘 / inode(23 GB 可用)。⇒ 高度指向**并发/时序**(同一 store 下并发 pnpm、或与共享层拉取重叠)。**下一次复现时的正解动作**:给失败路径补一次 `--reporter default` 重跑(现在 `--reporter silent` 结构上让 `pnpmExecError` 拿不到任何正文)—— 属 `plugin-assembly.ts` 的小改。 2. 🟡 **`restoreProfile()` 不重建缺失的 `link:` 软链**(见 §二 的红框)⇒ 今日真实致实例崩溃。 --- ## 五、门禁与卫生 - `npm run build` rc=0|`npm test` = **575 tests / 573 过 / 0 败 / 2 跳过**(基线 449/447/2;新增 6 个用例:软链补建与幂等 / 悬空与指错 / 真目录不碰 / `no_scope` 零改动 / 全量补含老包且不落共享层根 / 回滚清无主软链)|`check-layering` **✅ 无新增违规**(现存 5 条全在基线内)。 - 部署备份:`/opt/dsh/backups/lib-p18/{47,106}-pre-p18`;插件回滚素材 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.15.tgz`。 - 清理:临时 admin 会话**残留 0**(两机 `/tmp/p18*` 残留 0)|⛔ 未 commit/push、⛔ `src/web/routes/im.ts` 零改动、⛔ 未手改用户 `home` 下的 profile / `.pnpm`。