# 移动端修复「迁包 → 重投 47/106」+ 真机视觉验收(第 16 棒) > **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线|**日期**:2026-09-25 > **依据**:`接续入口_插件投放与分库线_20260922.md §2「🎯 本轮动作(第 16 棒)」` > ⛔ 本棒未 commit / push;⛔ 未改 `src/web/routes/im.ts`;⛔ 未做 MCN 数据面接入改造。 --- ## 一、结论(先说判定) | 件 | 判定 | 一句话 | |---|---|---| | 第 1 件 · 迁包 | ✅ **完成** | `mcn-mobile` 样式段已从**从未投放**的 `dsh-plugin-mcn` v0.1.0 整段平移进**线上真身** `dsh-plugin-mcn-suite`,38 个引用类名在目标包内**齐备 0 缺** | | 第 2 件 · 版本自增+重投 47/106 | ✅ **完成** | 0.3.13 → **0.3.14**;**47 + 106 共享层双双为 0.3.14**(106 由节点拉取机制一轮收敛);池内候选同步、回滚素材在位 | | 第 3 件 · 375×667 真机视觉验收 | ❌ **未通过(未取到真机读数)** | **不是代码问题,是链路上游断了**:已启用用户的实例仍指向**旧副本**,而平台的「重装配」操作**稳定失败**(两次复现)。⛔ 未宣称通过 | 🔴 **本轮最重要的产出 = 一条可复现的新缺陷**:**共享层更新对「已启用」的用户不生效,且平台没有可用的补救路径** —— 详见 §四。 --- ## 二、第 1 件 · 迁包(逐字平移,只改包名一处) | 项 | 值 | |---|---| | 源 | `D:/dshworkspace/plugin_package/dsh-plugin-mcn/lib/client.js` 第 216–268 行(`//#region 移动端 / 窄屏兼容`) | | 目标 | `D:/dshworkspace/plugin_package/dsh-plugin-mcn-suite/lib/client.js`(插在 `var McnNav_module_css_default` 之前) | | 改动 | 仅 1 处:`el3.dataset.plugin = "dsh-plugin-mcn"` → **`"dsh-plugin-mcn-suite"`** | | 体量 | 段 3642 B / 53 行 → 剥行尾后 **3595 B**;目标 401936 → **405531 B** | | 语法 | `node --check` **OK**(Node 22.22.2) | **三条自证**(脚本 `tmp/p16-port-mobile-20260925.py`,可重跑): 1. **类名齐备**:段内引用类名 **38 个**,目标包内缺失 **0 个** ⇒ 第 15 棒的"可移植性核对表"在**字节级**上被独立复核。 2. **行尾一致**:源段是 **CRLF**、目标包是**纯 LF** ⇒ 已归一(产物 CR = 0)。⚠️ 首版没做归一,产物混入 53 个 CR,**已回滚重做**。 3. **幂等**:目标已含 `data-plugin-css="mcn-mobile"` ⇒ 脚本直接 SKIP。 4. **可回滚**:改前副本 `tmp/p16-backup-20260925/dsh-plugin-mcn-suite-client.js.bak`(401936 B)。 --- ## 三、第 2 件 · 版本自增与重投(47 + 106) ### 3.1 重投前对账("别把别的漂移一起发出去") **改前副本 md5 = `0955f6b8b70f0c1357d29395643fdeb6` = 47 共享层线上 `lib/client.js` md5** ⇒ 本地 suite ≡ 线上真身,**逐字无漂移**。 全包逐文件比对(323 项,按路径建索引,排除 `node_modules`): | 差异 | 内容 | |---|---| | 内容不同 | **仅 `lib/client.js`**(就是本棒的改动) | | 仅本地 / 仅线上 | `tests/smoke.mjs`(本地有、不进包)/`pnpm-lock.yaml`(服务端安装时生成) | ⇒ **漂移面 = 1 个文件且是本棒的预期改动**。 ### 3.2 打包(以池内 tgz 为基底,只替换 3 个文件) | 项 | 值 | |---|---| | 基底 | `/var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz`(1989680 B,322 文件,顶层 `package/`) | | 新包 | `tmp/p16-pool/dsh-plugin-mcn-suite-0.3.14.tgz`(**1982036 B,322 文件**) | | sha256 | `3ca65153605b3c48f7de628d4754dc872b6f4fb7b617e9e57e704eb90bbab61c` | | 新旧差异 | **只差 3 个文件**:`package/lib/client.js`、`package/package.json`、`package/skills/.manifest.json`;**零增删** | 版本自增两处(该包「版本号唯一真源 = `package.json`」,`SUITE_VERSION` 由运行时读它 ⇒ 无字面量可改): `package.json` `version` → `0.3.14`(+ 变更史前置一条)|`skills/.manifest.json` → `suite: …@0.3.14`、技能 `version: 0.3.14`。 ⚠️ `files/bytes/sha256` 三项**未动**(技能载荷未变,只动版本字段)。 🔴 **行尾坑(本棒第二次踩)**:Windows 上用 Python 写文本默认把 `\n` 转成 `\r\n` ⇒ 首版把 CRLF 写进了 `package.json` 与清单,**已回滚重做**(写时显式 `newline="\n"`,产物 CR = 0)。 ### 3.3 上传递交(走平台正规口,非手改文件) `POST /api/plugins/business`(admin 会话)→ **http=200**:`version 0.3.14`、`replaced: true`、`trustedOverride: false`、 兼容性裁决 `level: ok`(`platformPkgs 240`、`sawPlatformRef true`);安全检测**只出 warning 未拦**(`sensitive-env@README.md` / `network-egress@lib/client.js` / …)。 ### 3.4 铺共享层(两段式:先预演、后确认) * 预演(`confirm` 缺省)→ `conflict: true`,并**顺带暴露一条台账缺陷**(见 §四 P2)。 * 确认 → `action: "replaced"`、`version 0.3.14`、`fileCount 657`、新 `treeSha256 f414df79…`、 回滚素材 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.14.tgz`。 **47 实况复核**:`lib/client.js` md5 = `d387089bd28c91cdbe46056e21288cbd`(= 本地新文件)、`mcn-mobile` 命中 **2 处**、`package.json` = 0.3.14、文件数 **657**(含 `pnpm install --prod` 重建的 `node_modules`)。 ### 3.5 106 同步(走节点拉取机制,非手工 scp) 106 上手动触发一轮:`POST http://127.0.0.1:19000/shared-layer/pull`(带 `x-dsh-agent-token`) → `{"stale":["dsh-plugin-mcn-suite"],"pulled":["dsh-plugin-mcn-suite"],"failed":[]}` → 106 盘上 **0.3.13 → 0.3.14**、`client.js` md5 = `d387089…`。**一轮收敛**。 ### 3.6 回滚 | 素材 | 位置 | 说明 | |---|---|---| | 上一版(内容 = 0.3.13) | 47 `/opt/dsh/backups/plugins/dsh-plugin-mcn-suite/0.3.9.tgz`(md5 `f47a07b9a2a889770e35315e22156e90`) | ⚠️ **文件名叫 0.3.9、内容是 0.3.13**(见 §四 P2) | | 本棒投出的版本 | 同上目录 `0.3.14.tgz` | 内容是**新**包(share 时补落的语义错位,见 §四 P2) | | 回滚动作 | 用上一版 tgz 重走上传递交 + `POST /api/plugins/business/share {confirm:true}` | 共享层目录被整体替换 ⇒ 指向它的用户下一次 reconcile 生效 | --- ## 四、抓出的缺陷(⛔ 均未擅自修,按纪律只报告) ### 🔴 P0 —— 已启用插件拿不到共享层新版本,且平台无可用补救路径 **判据链(全部可复现)**: 1. **实例依赖形态**:admin 实例 profile 的 `dsh-plugin-mcn-suite` 声明为 `file:/var/lib/dshs/users/cce6d1cd-…/home/.dsh-stage/dsh-plugin-mcn-suite.tgz` —— 是**一份 `.dsh-stage` 副本**(1989680 B = 旧版内容,09-20),**与共享层无关**。 ⇒ **共享层升到 0.3.14 对它零影响**,这与第 15 棒"修复落在没投放的包里"是**同一病根的下一层**。 2. **正规补救路径 = 关掉再开**(`apply` 是全量裁决:已启用的项再发 `enabled:true` 判 `noop`,不会重装配)。 3. **该路径稳定失败**:两次全量「关闭」均返回 `taskId`,各约 **52 s** 后 `{"status":"failed","stage":"失败","error":"安装失败,请重试或联系管理员"}`: * 任务 `0df1ea39ed3026c4`(08:32:38 → 08:33:30) * 任务 `3eb4b72b73373e07`(08:34:42 → 08:35:34) `/mine` 复核**仍为 `enabled: true`** ⇒ 平台**没有**把用户切到新版本的可用手段。 ⚠️ 已核实**不是**"暂存包缺失"(4 个 `.dsh-stage` 包全部在位)。 4. **失败原因不可观测(第二条缺陷)**:`journalctl -u dshs` 在失败时段**只打了 HTTP 请求行**,pnpm 的 stderr / 装配失败原因**未落任何日志** ⇒ 用户与运维都只能看到"请重试",**无法定位**。本棒的根因**因此未能定位**。 **影响面**:凡是**已经启用**过某插件的用户,都停留在启用当时那份副本上 ⇒ 平台的「插件更新」对存量用户**不成立**;共享层只对"新启用"的用户有效。这条直接命中本线目标 P2("插件更新的情形要有定义")。 **建议落点(下一棒)**:① 给 `applyPlugins` 的 pnpm 失败路径补**可观测性**(stderr 落日志 + 失败原因回传到任务对象);② 定位失败根因(候选:47 的 egress 护栏挡住了平台自身的 registry 解析 / pnpm store 锁竞争);③ 重装配要能把 `file:` 形态换成 `link:`。 ### 🟡 P1 —— 移动端真机视觉验收**仍未取得**(第 15 棒遗留,本棒被上面那条挡住) **"能不能真渲染"这件事本棒已做成判定,且结论是"能"**: | 探针 | 读数 | |---|---| | 47 侧浏览器 | ❌ 无(`chromium/chrome/firefox` 全无、无 playwright/puppeteer 缓存) | | **本机侧浏览器** | ✅ **有**:`C:/Program Files/Google/Chrome/Application/chrome.exe`(真 Chromium 内核) | | 平台门户 | ✅ `https://ai1net.com/portal.html` → 200 | | 实例 UI | ✅ `https://ai1net.com/u//dsh/` → **200**,且平台**自动下发实例凭据** `Set-Cookie: dsh-auth-…` | | 实例地址 | `https://admin.ai1net.com/?token=…`(`/api/dsh/status` 直出,可直接作为渲染入口) | ⇒ **路径已通,只差"实例上跑的是新版本"这一步**。修好 P0 即可直接补做五条读数(无需再做任何环境建设)。 ### 🟡 P2 —— 共享层清单与磁盘不同步 + 回滚素材命名语义错 * **清单 vs 盘**:本次预演读到的清单是 `version: "0.3.9"`、`tgzSha256 d14cd5fe…`、`treeSha256 cb1c73fb…`,而**盘上实际是 0.3.13 内容**(`package.json version = 0.3.13`、`client.js` md5 与池内 0.3.13 tgz 一致)。 ⇒ `authoritativeShared()` 拿这份 `treeSha256` 做节点对账 ⇒ **节点会永远判 `stale` / 反复重拉**。本棒 put 新版本后该条**自愈**(新 `treeSha256` 是现算的),但机制上的不同步来源未查明。 * **回滚素材命名**:`doShare` 里补落的备份路径用 **incoming 版本号** 命名、内容取 **incoming 的 tgz** (`backupTgz = /plugins//.tgz` 且 `copyFileSync(existing.tgzPath, …)`,而 `existing` = **上传后**的池条目) ⇒ 产出的 `0.3.14.tgz` **是"新包的自拷贝",不构成"回到上一版"的素材**;真正可回滚的那份躺在 `0.3.9.tgz` 里(名字与内容不符)。 ⇒ 建议:备份应记**被替换**的那一版,或名字带上"replaced"语义。 --- ## 五、复现命令(第三方可照抄) ```bash # ① 共享层版本与字节 ssh bt-server 'grep -m1 "\"version\"" /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/package.json' ssh bt-server 'md5sum /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # 期望 d387089bd28c91cdbe46056e21288cbd ssh test106 'md5sum /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # 应与 47 同值 # ② 池内候选 ssh bt-server 'ls -la /var/lib/dshs/business-plugins/dsh-plugin-mcn-suite.tgz' # 期望 1982036 B # ③ 实例依赖形态(P0 判据) ssh bt-server 'python3 -c "import json;print(json.load(open(\"/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde/home/profiles/web/package.json\"))[\"dependencies\"][\"dsh-plugin-mcn-suite\"])"' # 期望输出 file:…/.dsh-stage/dsh-plugin-mcn-suite.tgz ← 即「未指向共享层」 # ④ 重装配失败复现(需要 admin 会话):全量 apply 一条 enabled:false,随后轮询 # GET /api/plugins/mine/task/ → 约 52 s 后 status=failed / "安装失败,请重试或联系管理员" ``` **本棒用到的临时脚本**(均在 `tmp/`,可重跑):`p16-port-mobile-20260925.py`(迁包)· `p16-build-0314.py`(改版打包)· `p16-hash-list.py` / `p16-cmp.py`(逐文件对账)· `p16-pull-106.sh`(触发节点拉取)· `p16-cleanup.sh`(清临时会话与临时物)。**只读**工具:`p16-reach.sh`(连通性探路)。 --- ## 六、清理与合规 | 项 | 状态 | |---|---| | 临时 admin 会话 | ✅ **已删**(`DELETE FROM sessions WHERE user_agent='p16-mksess'` ⇒ rowCount 1,复核残留 **0**) | | 47 `/tmp` 临时物 | ✅ **零残留** | | 实例半状态 | ✅ **无**:实例 running、profile 依赖**未被改动**(两次失败均未落盘) | | 47/106 服务 | ✅ 正常(共享层 0.3.14、池 0.3.14) | | 装配协议 / `plugin-assembly.ts` | ✅ **零改动** | | 可见面开关 | ✅ 未开;⛔ 未开任何新口 | | `src/web/routes/im.ts` | ✅ **零改动**(IM 线 D 单判据 2 冻结遵守) | | commit / push | ✅ 未做 |