chore(工作区): 插件投放与分库线收口入库(第 35 棒 + 22:44 拍板落盘)

范围 = 本线(插件投放与分库线)产物 + 记忆类,共 27 件:
- 接续入口_插件投放与分库线(1 件):§0 新增 22:44 拍板行;§2 第 36 棒范围改 5 步、条件步转无条件
- 交付物(22 件):MCN 数据面接入阶段一/阶段二系列(含 B 案落地与 shadow 读数)、
  P0 修通与移动端真机验收、pnpm-EPERM、两机 lib 差异、共享层台账语义、
  基础插件身份与回滚、插件接入验收、插件数据面取数口、移动端迁包与字号扩面、跨机错误消息
- 记忆(4 件):2026-09-24 / 2026-09-25 日志、MEMORY.md、本棒 automation 执行记录

⛔ 未含他线在途改动(只报告、不代提交):机制层 CODEBUDDY.md / state.py /
.codebuddy/rules/server-ops.md、接续入口_IM线、接续入口_StoryForge验收线、
其余 29 个 automation 目录、docs/规则与载体/、归档/、接续包_*。
This commit is contained in:
admin committed 2026-09-25 23:06:29 +08:00
1 parent c1461fd958
commit 318430c9e9
27 files changed
+5649 -72

No files matched your search

@@ -0,0 +1,154 @@
# P0 修通(装配链)+ MCN 移动端 375×667 真机验收(第 17 棒)
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`|**线**:插件投放与分库线|**日期**:2026-09-25
> **依据**:`接续入口_插件投放与分库线_20260922.md §2「🎯 本轮动作(第 17 棒)」`
> ⛔ 未 commit / push;⛔ 未改 `src/web/routes/im.ts`;⛔ 未做 MCN 数据面接入改造。
---
## 一、结论(先说判定)
| 件 | 判定 | 一句话 |
|---|---|---|
| 第 1 件 · 补可观测性 | ✅ **完成并真机见效** | 装配失败从"零日志"变成"命令 + 退出码 + pnpm 原文 + 任务对象 `detail`";**本轮三条根因全部是它直接指出的** |
| 第 2 件 · 定位并修通重装配 | ✅ **通过(判据逐字满足)** | 实例依赖已是 `link:/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite`;实例加载 **`[mcn-suite] loaded v0.3.14`(自 0.3.13 升上来)** ⇒ 共享层更新对**已启用**用户生效 |
| 第 3 件 · 375×667 真机五条视觉验收 | 🟡 **4/5 通过,第 ④ 条未通过(已定位到具体控件)** | 独占 ✅/可退出 ✅(等价判据)/无横向溢出 ✅/横屏 ✅;**聚焦不缩放 ❌**:工作台内可见表单控件 `select.v1rksel`(「全部赛道」)= **13px** |
🔴 **核心产出 = 三条此前不可观测的 P0 被定位并修掉两条+探针验证一条**(详见 §三)。第 16 棒那句"整条链路断 52 s 后失败、根因不明"现在是**完全可解释**的。
---
## 二、第 1 件 · 可观测性(逐条落点)
| 落点 | 改法 |
|---|---|
| `src/supervisor/plugin-assembly.ts` | 新增 `pnpmExecError()`:把 `execFileSync` 抛出的 `stdout/stderr` 尾巴(≤1200 字符)拼进 `message`,并挂结构化字段 `pnpmArgs/pnpmCwd/pnpmStderr/pnpmStdout/pnpmExit`,同时 `console.error` 落 journald |
| `src/worker/agent.ts` | `/plugins/apply` 失败时落一条带 `userId + items + msg` 的 `console.error`(**worker 是独立 systemd 单元,日志与 Manager 不通**),回包 message 上限 400 → **2000** |
| `src/web/routes/business-plugins.ts` | 任务对象新增 **`detail`**(原始原因,≤2000);`recordFailure()` 同时落 journald;`friendlyError` 认 `pnpmStderr`/`pnpm_timeout` |
**真机对比(47)**:改前 `journalctl -u dshs` 在失败时段**只有 HTTP 请求行**;改后一条命令即可读到根因:
```
[business-plugins] 装配失败 @apply:dsh-plugin-mcn-suite=on: The operation was aborted due to timeout
[worker/plugins/apply] 失败 code=pnpm_failed items=dsh-plugin-mcn-suite:off msg=pnpm remove dsh-plugin-mcn-suite --reporter silent 失败:… (exit=1)
```
⚠️ 已上线:`lib/{supervisor/plugin-assembly,worker/agent,web/routes/business-plugins}.js` 三件与本地 **md5 逐字一致**(`cdfe3a2b…` / `d54bfde0…` / `5eaec1c6…`);改前副本 `/opt/dsh/backups/lib-p17/47-pre-p17-20260925-085630.tgz`。
---
## 三、第 2 件 · 三条 P0(两条已修 + 一条探针验证)
### P0-① 无条件 `-w` ⇒ pnpm 直接报错退出(已修)
**判据链**:`plugin-assembly.ts` 里 `PNPM_INSTALL_ARGS` 把 `-w` 写成**常量**(`['install','-w','--reporter','silent']`),而平台自己铺出来的 profile(`scripts/ensure-biz-plugins.cjs` 那条链)**没有** `pnpm-workspace.yaml`(47 · admin 实例 `home/profiles/web/` 实测只有 `package.json` / `pnpm-lock.yaml` / `cordis.*` / `node_modules`)。
**探针(47,1 秒可复现)**:`pnpm remove foo -w --reporter silent` 在无 workspace 文件的目录 ⇒
`ERROR --workspace-root may only be used inside a workspace`(RC=1)。
⇒ 门户「启停插件」**整条链断在 pnpm 这一步**,症状只有"安装失败,请重试或联系管理员"。
**修法(最小)**:改成 `workspaceRootFlag(dir)` / `pnpmInstallArgs(dir)` —— **只在真有 `pnpm-workspace.yaml` 时才加 `-w`**,与仓内既有三处脚本(`ensure-biz-plugins.cjs:219`、`ensure-portal-entry.cjs:215`、`install-plugin-for-user.cjs:134`)**逐字同一姿势**。
**真机前后**:同一动作(全量关闭 2 个已启用插件)改前 **52 s → failed**(第 16 棒两次复现),改后 **≈5 s → success**。
### P0-② `/restart-probe` 吃 10 s 默认预算(已修)
**判据链**:`RemoteSpawner.call()` 对**所有**口用 `AbortSignal.timeout(this.timeoutMs)`(默认 **10 s**),而 agent 侧 `/restart-probe` 要**真重启实例**(冷启动实测 10–14 s)+ 2.5 s 稳定窗、且它自己的预算是 **60 s** ⇒ **必然超时**;更坏的是 `call()` 超时后**再重试 3 次**(200ms/1s/3s),**每次重试都等于又重启一次实例**。
**真机读数**:一条任务连打 **8 次** `/restart-probe`(08:59:32/:42/:53、09:00:06/:26/:37/:48、09:01:01),约 100 s 后以 `The operation was aborted due to timeout` 收场 ⇒ 插件被误判"与实例不兼容"自动禁用。
**修法**:`call()` 增可选 `opts.timeoutMs`;`/restart-probe` 走 `PROBE_TIMEOUT_MS = 90_000`(>对端 60 s 预算)。
**真机前后**:改前永远 `aborted due to timeout`;改后探活能拿到**对端真实判定**(`实例未能启动` / `ok:true`)。
### P0-③ 共享层里的插件**加载不起来**(已探针验证,正式修法待转正)
**判据链**:`link:` 建立后实例冷启动即崩——`crash-restart` 的 `lastError` 原文:
```
Error: dsh: plugin tree failed to load: … failed to import loader entry mcn-suite (dsh-plugin-mcn-suite):
Cannot find package '@deepseek-ai/dsh-llm' imported from
/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/host/mcn/index.js [ERR_MODULE_NOT_FOUND]
```
**机理**:旧 `file:` 形态把实体放进**实例自己的 profile 树内**(`<profile>/node_modules/.pnpm/…`),Node 向上解析时能命中 pnpm 的提升目录 ⇒ 平台包(`@deepseek-ai/*`)解析得到;`link:` 把实体放到**树外**(共享层)⇒ 解析链断在共享层自己的 `node_modules`(里面只有 `react/react-dom/xlsx`,**零个 `@deepseek-ai`**)。插件 `peerDependencies` 里也没声明 `dsh-llm`(它靠运行环境提供)。
**已做的探针(**待转正**)**:给共享层目录补一条解析链接——
`<shared>/node_modules/@deepseek-ai → /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai`(240 个平台包)
⇒ 重启用**一次通过**,实例 `[mcn-suite] loaded v0.3.14`。⛔ **这是探针,不是最终修法**:正式落点应在**共享层铺装流程**(`doShare`)里自动完成,否则下次 share 覆盖目录就丢。
---
## 四、第 2 件 · 判据读数(第三方可复现)
```bash
U=/var/lib/dshs/users/cce6d1cd-b376-4304-80f0-0e1c58c9ffde
ssh bt-server 'python3 -c "import json;print(json.load(open(\"'"$U"'/home/profiles/web/package.json\"))[\"dependencies\"][\"dsh-plugin-mcn-suite\"])"'
# 期望输出:link:/var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite ✅ 实测一致
ssh bt-server 'journalctl -u dshs-worker --since "09:09" | grep "mcn-suite] loaded"'
# ✅ [mcn-suite] loaded v0.3.14(host 面 2 个:mcn, voxemw-cloud)
ssh bt-server 'grep -c mcn-mobile /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/lib/client.js' # ✅ 2
```
**任务读数**:`taskId=41e6fdc740915ca3` → `status:success / stage:完成 / rejected:[]`,最终 `bundles` 含 `dsh-plugin-mcn-suite`。
---
## 五、第 3 件 · 375×667 真机五条(本机真 Chrome + CDP)
**渲染环境**:本机 `C:/Program Files/Google/Chrome/Application/chrome.exe`(真 Chromium)+ CDP(`Emulation.setDeviceMetricsOverride` 375×667 / 667×375,DSF 2,`mobile:true`);入口 = `/api/dsh/status` 直出的实例地址 `https://admin.ai1net.com/?token=…`(平台自动下发实例凭据)。取证脚本 ⇒ `tmp/p17-visual.mjs`(零依赖,Node 22 自带 WebSocket)。
| # | 判据 | 读数 | 判定 |
|---|---|---|---|
| ① | 工作台**独占** | 开启后 `[data-slot="main.conversation"]` computed **display:none**;`.mcnNav_split` display=block、box `x=56 w=319 h=667`(56px = 插件自己的图标 rail)⇒ 375px 视口**全被插件占满** | ✅ |
| ② | **能退出** | 单面板形态**不渲染** `.mcnNav_close`(计数 0)⇒ 该判据在此形态无对应元素。**等价判据**:再点一次 `.mcnNav_trigger` ⇒ URL `#/mcn` → `/`、`.mcnNav_split` 1→**0**、会话区 display none→**contents** | ✅(等价判据,如实标注) |
| ③ | **无横向溢出** | 竖屏 `scrollWidth 375 = innerWidth 375`(溢出 **0**);横屏 `667 = 667`(溢出 **0**) | ✅ |
| ④ | **输入聚焦不缩放** | 工作台内**唯一可见表单控件** = `SELECT.v1rksel`(「全部赛道」赛道筛选)字号 **13px < 16px** ⇒ 聚焦会触发 iOS 缩放。移动端样式段只覆盖 `.mcnNav_input/.mcnNav_select/.mcnNav_textarea{font-size:16px}` ⇒ 该控件**在覆盖之外** | ❌ **未通过**(已定位到控件与缺口) |
| ⑤ | **横屏仍可用** | 667×375 下工作台仍开启(`.mcnNav_split` 计数 1)、无横向溢出、卡片按宽屏重排 | ✅ |
| 附 | 0.3.14 真机生效 | `style[data-plugin-css="mcn-mobile"]` 计数 **1**(该样式段只存在于 0.3.14) | ✅ |
**截图**(`tmp/`):`p17-shot-375x667-open.png`(竖屏开启,工作台占满)|`p17-shot-667x375-open.png`(横屏开启)|`p17-shot-375-exit.png`(二次点击后已回到会话态)。
**用户报障("一半会话一半插件")**:直观看已修复 —— 开启后会话区 computed `display:none`。
---
## 六、本轮新抓出(⛔ 未擅自修 · 按纪律报告)
| 级别 | 缺陷 | 说明与建议落点 |
|---|---|---|
| 🔴 P0 | **共享层不自足**(§三 P0-③) | 探针已临时补链;**正式落点 = `doShare` 铺装流程**(把插件需要的平台包解析链写进共享层目录),否则下次 share 覆盖即丢 |
| 🟡 P1 | **移动端规则覆盖缺口** | `.mcnNav_input/select/textarea` 之外的插件原生控件(如 `v1rksel`)仍是 13px ⇒ 第 ④ 条未通过。建议:把规则扩到工作台全部表单控件(或按 `input,select,textarea` 通配限定在插件容器内) |
| 🟡 P1 | **回滚不清理 `node_modules`** | `restoreProfile()` 只还原 `package.json`/`pnpm-lock.yaml`/`cordis.patch.yml` ⇒ 留下"依赖声明已撤、软链仍在"的残影(本轮实测该残影让实例崩过一次,随后自愈) |
| 🟡 P2 | **错误消息在跨机处被截 200 字符** | `remote-spawner.ts` 的 `Error(... text.slice(0,200))` ⇒ Manager 侧 `detail` 只能看到半截(本轮靠 worker 侧 journald 补齐)。建议放宽到 ~1200 |
| 🟡 P2 | **`preflight-lock.sh` 对工作区根文件判【D】** | 本棒目标里的 `接续入口_*.md` 被判"未归类 ⇒ 拒开工"(脚本只认 `^(src\|poc\|test\|web\|docs\|scripts\|交接单\|skills)/`)。域本身可算(guard 的锚点算法给出 `aliyun-dsh-server/<文件名>`)⇒ 属**门禁脚本盲区**,本棒按该域锁了、未改脚本(跨库文件,只报告) |
| 🟠 待拍板 | **`@softspark/dsh-file-preview` 被我这一棒关掉且现在不可见** | 复现第 2 件要求"全量 apply 一条 `enabled:false`",把它一起关了;它**不在共享层**,而可见性判据 = 共享层已发布 ∪ 我已启用 ⇒ 它从用户面清单消失,**平台内已无法自助重开**。恢复 = 先 `POST /api/plugins/business/share`(发布到共享层)再启用;但发布会让**所有登录用户**在插件市场看见并可自助安装 ⇒ 属可见面变化,**本棒未擅自做** |
---
## 七、复现命令
```bash
# ① pnpm 参数坑(1 秒,无需真机状态)
ssh bt-server 'rm -rf /tmp/probe && mkdir /tmp/probe && printf "{}" > /tmp/probe/package.json && cd /tmp/probe && pnpm remove foo -w --reporter silent; echo RC=$?; rm -rf /tmp/probe'
# 期望:ERROR --workspace-root may only be used inside a workspace / RC=1
# ② 装配链(需要 admin 会话):全量 apply 一条 enabled:false 后轮询 task
# 期望:≈5 s success(改前:≈52 s failed)
# ③ 共享层解析链
ssh bt-server 'ls /var/lib/dshs/bundled-plugins/dsh-plugin-mcn-suite/node_modules/@deepseek-ai | wc -l' # 期望 240
# ④ 移动端五条(本机)
P17_SID=<sid> P17_URL='https://admin.ai1net.com/?token=<token>' P17_CLICK='.mcnNav_trigger' \
node tmp/p17-visual.mjs
```
**本轮临时脚本**(均在 `tmp/`):`p17-visual.mjs`(CDP 真机取证,通用:可换视口/点击目标)· `p17-mksess.sh`(临时 admin 会话)· `p17-apply.sh`(发起 apply 并轮询终态)· `p17-repro.sh`(复现全量关闭)。
---
## 八、清理与合规
| 项 | 状态 |
|---|---|
| 临时 admin 会话 | ✅ 已删(`DELETE FROM sessions WHERE user_agent='p17-mksess'`,见入口 §0) |
| 47 临时物 | ✅ `/tmp/p17-*.tgz` 已删;`/opt/dsh/backups/lib-p17/` 保留(回滚素材) |
| 实例状态 | ✅ running(pid 稳定、`/api/dsh/status` 200、无 crash-restart) |
| 装配协议 | ✅ 未改(`link:` 仍是唯一协议) |
| `src/web/routes/im.ts` | ✅ **零改动** |
| 可见面 | ⚠️ 唯一变化 = mcn-suite 从"旧副本"变成"指向共享层的最新版"(本线目标本身);⛔ 未新开任何口 |
| commit / push | ✅ 未做 |