Files
workbuddy_skills/dsh-diagnose/references/01-业务插件故障.md
T
admin e03465c398 按用户令提交:把此前未纳管的 9 个技能目录一并入库
用户令逐字:「E:/ProgramData/.workbuddy/skills  提交仓库是指的这里」——
即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。

本次入库(9 个技能,46 个文件):
1、`AI HOT`
2、`draw-ui`
3、`dsh-diagnose`
4、`dsh-knowledge`
5、`dsh-local-env`
6、`dsh-opensource-release`
7、`dsh-workflow`
8、`oil-motion`
9、`skills-security-check`

提交前核对:
· **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓;
· 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 ——
  仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库);
· 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
2026-10-08 22:29:08 +08:00

202 lines
17 KiB
Markdown
Raw 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.
# 业务插件故障(②)
> **归属**:技能 `dsh-diagnose` · 详情档(主干 `../SKILL.md` §2)
> **本档覆盖**:原技能 `dsh-plugin-diagnose` **全文**
> **原行段**:原 `SKILL.md` 全文 186 行,其中 frontmatter 占前 8 行已剥离 ⇒ 本档承载第 9–186 行
> **搬运方式**:**逐行未改**(⛔ 未删任何判据 / 命令 / 事故事实)
> ⚠️ 本技能已由 `dsh-diagnose` **合并**,原名 `dsh-plugin-diagnose` 退役 ⇒ 见到该名按本档读。
---
# DSH 业务插件故障诊断
> 适用对象:**候选池里投放的业务插件**(`dsh-univer-office`、`dsh-plugin-mcn-suite`、`@dsh-local/*` 等)。
> 实例本身的故障(打不开 / OOM / 崩溃重启)用 `dsh-instance-diagnose`,不是本技能。
## 0. 第一原则:**静默失败要当默认假设**
业务插件最容易「坏得没声音」:host 工具照跑、日志干净、界面就是没有东西。
所以排查顺序永远是「**先证明某一层到底有没有活着**」,而不是先读代码猜。
## 1. 三层归属(先定层,再动手)
| 层 | 怎么判「它活着」 | 死了什么样 |
|---|---|---|
| **host 半边**(`lib/index.js`) | 插件的 DSH 工具能调通(`univer_*` / 业务工具返回 `ok:true`) | 工具直接报「未知工具」 |
| **client 半边**(`lib/client.js`) | 页面 `__DSH_BOOT__` 里**有**该插件行,且槽位探针能看到它的占位者 | **有无都没痕迹**:无卡片/无 dock/无服务、无报错 |
| **网关 / 原生绑定** | 网关进程在、socket 能连、`/[健康路径]` 200 | 进程崩、连接被 reset、`.node` 加载报错 |
## 2. 客户端半边**静默挂死**(最隐蔽的一类,2026-09-13 实测)
**症状**:host 工具全好,浏览器里**一个 UI 元素都没有**,Console 无报错。
**机理**:dsh 客户端加载器(`@deepseek-ai/dsh-client-modules`)对每个插件行解析 `package.json` 的
`dsh.client` → `inject`(**包名**列表);**cordis 的 `inject waiting`** 决定 fiber 何时 `apply()`。
⇒ **只要有一条 inject 永远不可满足,`apply()` 永不执行**(连注册都没有,静默)。
**判据:`inject` 差集**(一把尺子,通用):
1. 取实例页面(**不碰用户浏览器**,用临时会话走平台代理):
```bash
SID=$(node /opt/dshs/mksess-guest.cjs) # 需对应用户;用完必须删会话
curl -s -H "Host: <用户名>.ai1net.com" -H "Cookie: sid=$SID" http://127.0.0.1:3080/ -o page.html
```
2. 从页面里取**实际下发的客户端插件集合**(权威清单,别用 manifest 正则——会漏行):
```python
import re; s=open('page.html',encoding='utf-8',errors='ignore').read()
served={p.split('/client.js')[0] for u in re.findall(r'/plugins/\?\?[^"\'&]+',s)
for p in u.split('??',1)[1].split(',')}
```
3. 解析 `__DSH_BOOT__` 里目标插件的行 → 取它的 `inject` → **求差集**:
```python
rows=re.findall(r'\{"id":"([^"]+)","url":"[^"]*","rev":"[^"]*","inject":\[(.*?)\]\}', s)
need=re.findall(r'"([^"]+)"', dict(rows)['<插件包名>'])
print([x for x in need if x not in served]) # 非空 = 该客户端 fiber 永久挂起
```
4. **差集非空 = 确诊**。两个常见根因:
- **inject 了被平台角色补丁禁用的官方包**。平台对普通用户禁:
`@deepseek-ai/dsh-client-ui-settings-models` / `-settings-plugins` / `-settings-plugin-inventory` /
`@deepseek-ai/dsh-client-ui-cordis` / `dsh-client-hmr` / `dsh-host-directory-picker-auto`
(`ensure-role-profile-patch.cjs`,档案 15;**admin profile 不禁**——A/B 对照一眼能看出来)。
- **inject 了根本不存在的包名**(如 `@deepseek-ai/dsh-client-runtime`)。
⇒ 直接 `ls /usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/ | grep <名字>` 核一下。
5. **修法**:从插件 `package.json` 的 `dsh.client.inject` **移除**该包(或改成真实存在的)。
⚠️ **别混两个 inject**:`package.json` 的 `dsh.client.inject` 是**包名**(模块级依赖);
客户端源码里的 `export const inject = ['slots','locale','conversation']` 是**服务名**。前者不可满足会**整体不 apply**,后者只影响个别 `ctx.inject` 块。
**第二类(2026-09-20 实测):组件挂上了、点击却「没反应」—— 外部契约失效**(与上面同族,但更难看出)
- 症状:侧栏入口**看得见**、Console 无报错,点下去 **UI 毫无变化**(不是"没渲染",是"面板宿主找不到")。
- 真因:客户端靠 **DOM 契约(槽位名)** 找宿主 ⇒ dsh 升级**改了槽位名**后 `querySelector` 恒 null。
实测(0.1.5-rc.1):`conversation` → **`main.conversation`**;`document.querySelector('[data-slot="conversation"]')` 恒 null
⇒ 判据 = `!!document.querySelector('[data-mcn-panel]')` 为 **false**(同时 `#/mcn` 的 hash 已变、入口高亮已生效)。
- 同族还有 **官方 API 形态变了**:`ctx.locale.getLocale()` 由「返回 id 字符串」变成「返回**快照对象** `{active,…}`」
⇒ `String(obj)` 恒为 `"[object Object]"` ⇒ 受控 `<select>` 只显示第一项 ⇒ 看着像"切语言没反应"(其实界面语言可能是对的)。
判据:`document.documentElement.lang` 与控件 `value` **不一致**。
- 取证姿势(浏览器实测 · 平台实例):`mksess.cjs` 造临时会话 → `agent-browser`。⚠️ **`set headers` 注 Cookie 无效**,
改用 `open https://ai1net.com/login.html` + `eval document.cookie='sid=…; domain=.ai1net.com; path=/'` 再 `open` 实例子域;
⚠️ 前台会被沙箱 SIGTERM ⇒ 先 `run_in_background` 起一次守护,后续命令加 `> 文件 2>&1`(**不走管道**)即可前台跑。用完**删会话**。
- 详版 ⇒ **`PLAYBOOK-实例与插件坑 §20`** / 档案 **`04-调整方案/143-MCN工作台入口失效与语言切换显示修复.md`**。
**预期管理(必须一起告知用户)**:
- 插件激活**之前**产生的**旧回合不会回溯渲染**(预览卡片由 conversation turn 定义驱动)⇒ 要**新跑一轮**才看得见。
- 槽位探针里 `active:false` 常常**不是故障**:很多组件在「没内容可显示」时 `return null`
(如 univer dock:`if (operation.worktreeId === null) return null`),同槽里恒有内容的项才是 `true`。
- 某类槽(chain 类,如 `conversation.chat.turnTail`)的探针输出**不带名称字段** ⇒ 「按名字找不到」是**探针局限**;
改用**注册时写的常量**(如 `priority: -10`)去对号。
- **不是所有插件都注册 `tool.*.toolview`** ⇒ 「工具卡片没变样」≠ 插件坏了。
## 2.5 host 半边「桥接类」插件:**连不上 / 静默无限重连**(2026-09-21 实测 · carbon 插件线)
**症状**:实例起得来、`dsh web:` 正常、日志干净,但**会话里一个桥接工具都没有**(如 `mcp__*`)。
这类插件(MCP 桥 / 外部服务代理 / 远程 API 插件)的 host 半边**只在连上之后才注册工具**
⇒ 「连不上」的表现与「插件压根没装」**完全一样**。
⚠️ **先破一个假象**:内核 cordis 的默认日志 exporter **只写内存环形缓冲、不落 stdout**
⇒ 内核插件(含 `@deepseek-ai/dsh-mcp-client`)的报错在实例日志里**看不见**。
第一件事就是把日志挂一份出来(公开 API):
```js
ctx.logger.exporter({ colors: 0, levels: { default: 3 }, export: (m) =>
process.stdout.write(`[log] ${m.type} ${m.name}: ${(m.args || []).map(String).join(" ")}\n`) });
```
挂上后立刻能看到 `warn mcp-client: connection attempt failed: …`(不挂就永远在猜)。
**诊断配方(四层,逐层排除,⛔ 别跳步)**
| # | 尺子 | 判读 |
|---|---|---|
| 1 | **上游直连**:官方 SDK / curl 直打端点 | 通 ⇒ 上游没问题,问题在 dsh 侧 |
| 2 | **实例进程内出站**:插件里 `fetch` 打同一 URL | 通 ⇒ 不是沙箱 / nft / 代理拦;不通 ⇒ 查实例 egress 规则 |
| 3 | **出站抓包**:包一层 `globalThis.fetch`,打 `method / url / rpc / status`,失败时打**实发 header**(秘密只留尾 5 位) | 🔴 关键:能看出「**从哪一代请求开始丢凭据**」 |
| 4 | **硬编码对照**:把秘密**直接写进配置**(仅测试用) | 立刻全绿 ⇒ **病根 = 凭据交付通道**,与上游 / 协议 / 网络无关 |
**本线实测真因(值得背下来)**:内核 `@deepseek-ai/dsh-mcp-client` 建传输时用
`{ requestInit: { headers: config.headers } }` —— **按引用捕获 headers**;且每代重连都从
**同一个 config 对象**重建 ⇒ **第 1 代能带上运行期注入的凭据,第 2 代起每次 `initialize` 都 401**
(抓包实发 header 只剩 `accept` / `content-type`)⇒ 无限退避重试、工具永远挂不上。
「原地改内存 header」与「每周期重新注入」两种修补**都不收敛**
⇒ 结论:**插件自持连接**(自己用 `fetch` 实现 streamable-HTTP:凭据只留内存、每请求自带 header、失败响亮报出)。
**可见性判据(「注册成功」≠「模型可见」,必须分开对账)**
```js
const list = tools.schemas(); // 省略 scope ⇒ 全局视图
const set = new Set(list.map((s) => s.name));
const missing = myNames.filter((n) => !set.has(n));
console.log(`schemas()=${list.length} 缺=${missing.length} get(首个)=${!!tools.get(myNames[0])}`);
```
- ⛔ **读法陷阱**:⛔ 别用 `Object.getOwnPropertyNames(tools.layers.global.tools.data)` 数工具 ——
`new Map()` 的属性名是**空数组**,会把「有 N 个工具」误读成 `size=0`(本线因此误判过一轮)。
必须**先判 `instanceof Map`** 再读 `size`。
- `register()` 返回 disposer 只说明「调用没抛错」;**能不能被模型看见**要用上面这把尺子另证。
## 3. 原生绑定 / glibc 门禁
**第一步永远先直测**(别读代码猜):
```bash
ldd --version | head -1 # 宿主 glibc(本平台 = 2.32,Alibaba Cloud Linux 3)
node -e "try{require('<那个 .node>');console.log('OK')}catch(e){console.log(e.message.split('\n')[0])}"
```
**两类绑定,对策不同**:
| 类型 | 例子 | 对策 |
|---|---|---|
| **有 JS/上游回退开关** | `engine-formula-rust-binding`(`useRustEngine:false` 走 JS 引擎) | **资产侧垫片**:包一层同名导出,**绑定装不上才**强制关掉(可用则原样交上游 ⇒ 宿主升级 glibc 后自动恢复) |
| **无回退** | `exchange-node-binding`(Office 导入导出) | **只能在宿主/镜像层解决**(换 glibc ≥ 2.35 的基底)⇒ 上报用户决策,别硬凑 |
**关键警示(2026-09-13 踩过)**:
- 同一条 glibc 门禁可能被**多个进程**撞到 —— 同一插件在 **worker** 与 **gateway** 里可能**都要**建投影;
**只给 worker 打垫片不够**:网关崩掉的表现是「**写入报错但数据已落盘**」(提交时崩 → 客户端读不到响应 → 连接 reset ⇒ 假错误 ⇒ 有重复写入风险)。
- 打垫片要**同时**处理 ESM(`import.meta.url` 可用)与 **CJS**(esbuild 把 `import.meta` 降级成 `{}` ⇒
`createRequire(undefined)` **启动即崩**);CJS 产物里让垫片显式 import 该包的 **CJS 入口**。
## 4. 「改了却没生效」的四类(按可能性排序)
1. **打包顺序把垫片废掉**:模块级 `const` 被降级为 `var` 整体提升 ⇒ 垫片跑到时是 `undefined`,
比较恒 false ⇒ **静默回退原实现**。判据:在**产物**里插一行探针打印该常量。
**修法**:垫片内用**函数内字面量**,不依赖任何模块级常量。
2. **插件没真正换上新包**:`mine/apply` 对**已启用**插件是 **noop** ⇒ 必须**停用 → 启用**两步;
任务终态是 **`success`**(不是 `done`,轮询别只判 done,否则空转)。改完**核 md5**(实装产物 == 你构建的)。
3. **客户端包按 `rev` 缓存**:客户端半边改动后**必须硬刷新**页面;顺带记住**硬刷新会取消在途的
`platform: client` 工具查询**(由页面回答),而 **host 侧调用不受影响** —— 别把它当链路故障。
4. **同 `version` 号不会被重装**:改完 `lib/` 却忘了升 `package.json` 的 `version` ⇒
`dsh plugin add file:<tgz>` 之后跑的**还是旧代码**(本线因此被误导数轮排查)。
判据 = 装完立刻做**内容门禁**:`grep -q "<新符号>" <profile>/node_modules/<pkg>/lib/index.js`,
未命中就先 `rm -rf` 该包目录再装。⇒ **改代码必升 version**(0.1.0→0.1.1→0.1.2 …)。
## 5. 验证纪律(每条结论都要有可复现的命令)
- **结构自检 ≠ 能打开**:`zipfile.testzip()` 只证明「能解压」。文档类产物要上**严格解析器**
(`python-docx` / `openpyxl` / `python-pptx`;装进隔离 venv)。本次正是靠它抓到 `w:tbl` 缺必需子元素 `w:tblGrid`。
- **改完必须在真机跑一次**,并尽量用**租户 uid**(`setpriv --reuid=<uid>`)+ 真实 socket/真实数据。
- 找不到「日志」时别下结论:业务插件的 stdout **不落 journald**,实例 journal 只有 systemd 启停两行。
- **`grep` 会骗人**:esbuild 的 CJS 导出用 getter(`__toCommonJS` / `apply: () => apply`),
`grep "exports.apply"` 会给你**假阴性** —— 要按打包器形态去找。
## 6. 平台侧事实(省得反复查)
- 插件投放:admin `POST /api/plugins/business`(`{filename, file:<base64>, trust?}`)→ 池 `/var/lib/dshs/business-plugins/`;
再 `mine/apply` 启用。临时会话:`node /opt/dshs/mksess{,-guest}.cjs`,**用完必须**
`DELETE FROM sessions WHERE user_agent='poc-curl2'`。
- 技能是**显式清单**注册(`src/host/skills/plugin.ts` 的 `DEFINITIONS`),**不是扫目录** ⇒ 加技能要**同时**改清单并重建 `lib`。
- 起自带网关做复现时:`NODE_PATH` 要带 `…/node_modules/.pnpm/node_modules`,否则 `libsql`/`ws` 等外部依赖解析不到,会被误判成插件坏了。
- 平台「我的文件」面板 = **浏览 + 下载,没有预览**;**只有 `.univer` 能在线预览**,Office 文件在浏览器里没有原生渲染。
---
## 变更历史(原 frontmatter · 逐字保留)
```text
name: dsh-plugin-diagnose
description: DSH 多租户平台(ai1net.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「**装了插件但界面没变化 / 界面跟没启用一样**」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。⚠️ **与 `dsh-desktop-dev-shell` 的边界**:本技能管**服务器实例里的插件**(投放后没生效 / 启用了没 UI);「**本机**源码构建的壳里挂插件、界面不出现」属对方。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。
version: 1.0.0
updated_at: 2026-09-22
last_change: 【2026-09-22 按要求统一版本号】frontmatter `version` → `1.0.0`(原 v1.3.0);正文与历史中的版本号为当时记录,未改动。此前 v1.3.0(2026-09-22):补触发词「装了插件但界面没变化 / 界面跟没启用一样」并写明与 `dsh-desktop-dev-shell` 的边界 —— 实测:用户说这句时原描述命不中本技能(只有对方声明了同一说法),服务器实例语境下会加载错。此前 v1.2.0(2026-09-21):新增 **§2.5 host 半边「桥接类」插件:连不上 / 静默无限重连** —— 含「cordis 默认 exporter 不落 stdout,必须先挂 logger.exporter 才能看见内核插件报错」这条前提,四层诊断配方(上游直连 → 实例内 fetch → 出站抓包 → 硬编码对照),以及本线实测真因(内核 mcp-client `requestInit.headers` 按引用捕获 ⇒ 第 1 代带凭据、第 2 代起 401);并补「注册成功 ≠ 模型可见」的公开面回读判据与 `Map` 属性名空数组的读法陷阱;§4 从三类扩为四类(新增「同 version 号不会被重装 ⇒ 改代码必升 version + 内容门禁」)。v1.1.0(2026-09-20):§2 新增「第二类:组件挂上了、点击却没反应」—— dsh 升级改了**外部契约**(槽位名 `conversation`→`main.conversation`;官方 `ctx.locale.getLocale()` 由返回 id 变成返回**快照对象**)导致 `querySelector`/取值静默失效;补浏览器实测姿势(`agent-browser` 的 `set headers` 注 Cookie 无效 + 前台会被沙箱 SIGTERM 两个坑)与判据。v1.0.0:首版。由 2026-09-13 一整天 guest 实例 univer 插件四轮排查沉淀(worker socket 垫片被打包顺序废掉 / 宿主 glibc 2.32 < 绑定要求 2.35 / 网关提交 changeset 也建投影致其崩溃 / client 半边因一条不可满足的 inject 永久挂起),含 4 类真因、3 套取证配方与 8 条实测踩坑。
agent_created: true
```