Files
dsh_ai1net_server/交付物/P0修通与移动端真机验收-20260925.md
admin 318430c9e9 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/规则与载体/、归档/、接续包_*。
2026-09-25 23:06:29 +08:00

155 lines
13 KiB
Markdown
Raw Permalink 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.
# 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 | ✅ 未做 |