Files
dsh_ai1net_server/dsh-server-docs/08-skills/dsh-plugin-diagnose/SKILL.md
T
admin e6207aa691
build / build-and-scan (push) Canceled after 0s
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

17 KiB
Raw Blame History

name, description, version, updated_at, last_change, agent_created
name description version updated_at last_change agent_created
dsh-plugin-diagnose DSH 多租户平台(ai1net.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「**装了插件但界面没变化 / 界面跟没启用一样**」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。⚠️ **与 `dsh-desktop-dev-shell` 的边界**:本技能管**服务器实例里的插件**(投放后没生效 / 启用了没 UI);「**本机**源码构建的壳里挂插件、界面不出现」属对方。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。 1.0.0 2026-09-22 【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 条实测踩坑。 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. 取实例页面(不碰用户浏览器,用临时会话走平台代理):
    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 正则——会漏行):
    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 → 求差集:
    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):

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、失败响亮报出)。

可见性判据(「注册成功」≠「模型可见」,必须分开对账)

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 门禁

第一步永远先直测(别读代码猜):

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 文件在浏览器里没有原生渲染。