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 一律写「远程服务器」。
9.5 KiB
9.5 KiB
name, description, version, updated_at, last_change, agent_created
| name | description | version | updated_at | last_change | agent_created |
|---|---|---|---|---|---|
| dsh-plugin-diagnose | DSH 多租户平台(alotbuy.com / 47.77.182.89)**业务插件**的故障诊断技能 —— 专治「工具能用但浏览器里什么都没有」「原生绑定装不上」「明明改了却没生效」这类**静默失败**。当出现「插件没 UI / 预览不出现 / 卡片不渲染」「导入导出报 GLIBC 版本」「网关崩了但数据其实写进去了」「改了包上传了还是老行为」时触发。核心:先按 **host 半边 / client 半边 / 网关·原生绑定** 三层归属定位,再用「**inject 差集**」「**glibc 直测**」「**产物插探针**」三把尺子取证——**每一次都要有可复现的命令**。 | 1.0.0 | 2026-09-13 | 首版。由 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 差集(一把尺子,通用):
- 取实例页面(不碰用户浏览器,用临时会话走平台代理):
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 - 从页面里取实际下发的客户端插件集合(权威清单,别用 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(',')} - 解析
__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 永久挂起 - 差集非空 = 确诊。两个常见根因:
- 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 <名字>核一下。
- inject 了被平台角色补丁禁用的官方包。平台对普通用户禁:
- 修法:从插件
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 门禁
第一步永远先直测(别读代码猜):
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. 「改了却没生效」的三类(按可能性排序)
- 打包顺序把垫片废掉:模块级
const被降级为var整体提升 ⇒ 垫片跑到时是undefined, 比较恒 false ⇒ 静默回退原实现。判据:在产物里插一行探针打印该常量。 修法:垫片内用函数内字面量,不依赖任何模块级常量。 - 插件没真正换上新包:
mine/apply对已启用插件是 noop ⇒ 必须停用 → 启用两步; 任务终态是success(不是done,轮询别只判 done,否则空转)。改完核 md5(实装产物 == 你构建的)。 - 客户端包按
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 文件在浏览器里没有原生渲染。