Files
dsh_ai1net_server/dsh-server-docs/skills/dsh-plugin-diagnose/SKILL.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

121 lines
9.5 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.
---
name: dsh-plugin-diagnose
description: DSH 多租户平台(alotbuy.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。
version: 1.0.0
updated_at: 2026-09-13
last_change: 首版。由 2026-09-13 一整天 guest 实例 univer 插件四轮排查沉淀(worker socket 垫片被打包顺序废掉 / 宿主 glibc 2.32 < 绑定要求 2.35 / 网关提交 changeset 也建投影致其崩溃 / client 半边因一条不可满足的 inject 永久挂起),含 4 类真因、3 套取证配方与 8 条实测踩坑。
agent_created: true
---
# 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: <用户名>.alotbuy.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` 块。
**预期管理(必须一起告知用户)**:
- 插件激活**之前**产生的**旧回合不会回溯渲染**(预览卡片由 conversation turn 定义驱动)⇒ 要**新跑一轮**才看得见。
- 槽位探针里 `active:false` 常常**不是故障**:很多组件在「没内容可显示」时 `return null`
(如 univer dock:`if (operation.worktreeId === null) return null`),同槽里恒有内容的项才是 `true`。
- 某类槽(chain 类,如 `conversation.chat.turnTail`)的探针输出**不带名称字段** ⇒ 「按名字找不到」是**探针局限**;
改用**注册时写的常量**(如 `priority: -10`)去对号。
- **不是所有插件都注册 `tool.*.toolview`** ⇒ 「工具卡片没变样」≠ 插件坏了。
## 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 侧调用不受影响** —— 别把它当链路故障。
## 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 文件在浏览器里没有原生渲染。