--- 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 --- # 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]"` ⇒ 受控 `