Files
dsh_shenxian/dsh-server-docs/skills/dsh-plugin-diagnose/SKILL.md
T
admin 971ccc3703 feat(auth): 注册页人机验证 + 邮箱验证码;品牌标识去 DeepSeek(附域名迁移线 序㊿ 补提交)
三条线合并入库 —— 均已完成并上线(源码与生产一致,此前只部署未入仓)。
⚠️ 其中域名迁移线为**另一会话**产出,本会话只做入库、**未复验其正确性**(它自报零回归)。

【档案 134 · 注册页人机验证 + 邮箱验证码】
- DB 迁移 v10:users.email(唯一索引 LOWER(email))+ email_codes 事件表(2 索引)
- 新增模块 src/web/{register-guard,mail,turnstile,email-code}.ts
- routes/auth.ts:新增 GET /api/auth/register/config、POST /api/auth/register/email-code;
  注册接口加人机验证与验证码校验;config.ts 新增 12 项配置(默认空 ⇒ 不配 = 老行为)
- 邮件走**可插拔驱动**(brevo/http/log),发件人 [email protected](Brevo 域名已认证 + DKIM + SPF)
- 防爆破:三层配额(邮箱 6/h、8/天;IP 20/h;全局 200/h)+ 递增冷却阶梯
  (60→60→180→300→900→1800s)+ 试错 5 次作废 + 码只存哈希 + 单次使用 + 与用户名绑定
- Turnstile 服务端校 **success + action + hostname 三项**:sitekey 是公开的,
  只校 success 时"拿我们的 sitekey 在自己站点替真人取合法 token 再打我们接口"这条路是通的
- 新增 test/register-guard.test.mjs(19 用例)

【档案 137 · 品牌标识改造 — 去 DeepSeek 图形】
- login/register/admin 页头:删 DeepSeek 鲸鱼图标 + 「DeepSeek」文字图形
  → 平台标识(中文「能力枢纽」/英语及其他语言「CapabilityNet」,走 i18n 词条 brand.name)
- portal 顶栏换图标(页面名「管理门户」保留)
- 新建 web/favicon.svg(平台自有 hub 图标,避开 DeepSeek 蓝)+ 四页 favicon 指向它
- 新增 test/i18n-brand.test.mjs(node:vm 跑真实 i18n.js,六条语言路径断言渲染结果)
- scripts/verify-static.mjs 新增 SVG 段:XML 注释不得含 ASCII 双连字符(否则整份 SVG
  解析失败、图标静默不显示 —— 实际踩到过)
- 🔴 会话页面(实例内官方 dsh 界面)的标识**按用户要求未动**(也受 R2 约束)

【档案 135/136 · 域名迁移线(另一会话产出)】
- 域名收敛为 ai1net.com;旧域 alotbuy.com 降级为 301 过渡装置
- src/net/relay/{addr-override,directory,rendezvous,switcher}.ts 种子与候选链更新;
  src/web/server.ts、src/worker/relay-tunnel.ts、scripts/verify-cluster-domain.mjs
- 档案 136 = 控制面按两台中继取并集(**已立项、未落地**)

验证(本会话两条线):新增单测 21 条全通过|全量 221 pass / 0 fail / 1 skipped|
verify-static 全合格|其余 10 个 verify 脚本全 OK|线上实测:Turnstile 假 token 403、
发码 delivered、四页 deepseek 命中 0、favicon 200。
2026-09-19 09:11:24 +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 多租户平台(ai1net.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: <用户名>.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` 块。
**预期管理(必须一起告知用户)**:
- 插件激活**之前**产生的**旧回合不会回溯渲染**(预览卡片由 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 文件在浏览器里没有原生渲染。