范围 = 本线(插件投放与分库线)产物 + 记忆类,共 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/规则与载体/、归档/、接续包_*。
13 KiB
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 件 · 判据读数(第三方可复现)
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(发布到共享层)再启用;但发布会让所有登录用户在插件市场看见并可自助安装 ⇒ 属可见面变化,本棒未擅自做 |
七、复现命令
# ① 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 | ✅ 未做 |