用户令逐字:「E:/ProgramData/.workbuddy/skills 提交仓库是指的这里」—— 即本目录就是仓库(2026-10-07 已在本目录建仓,见当日日志 §22),本轮把余下未纳管的 9 个技能一并提交。 本次入库(9 个技能,46 个文件): 1、`AI HOT` 2、`draw-ui` 3、`dsh-diagnose` 4、`dsh-knowledge` 5、`dsh-local-env` 6、`dsh-opensource-release` 7、`dsh-workflow` 8、`oil-motion` 9、`skills-security-check` 提交前核对: · **凭据类扫描**(`*.env` / `*token*` / `*.key` / `*secret*` / `*.pem`)⇒ **零命中** ✓; · 体积合计约 20 MB(`draw-ui` 12M + `oil-motion` 6.5M 是大头,形态为配图/素材 —— 仓库 `.gitignore` 里明写「`assets/*.png` 是内容不是产物」⇒ 属刻意入库); · 运行产物仍按既定规则排除(`__pycache__` / `logs/` / `tmp/` / `.venv/` / `*.egg-info` / `uv.lock` / `.workbuddy/`)。
105 lines
13 KiB
Markdown
105 lines
13 KiB
Markdown
---
|
||
name: dsh-diagnose
|
||
description: DSH 多租户平台(ai1net.com / 47.77.182.89)**故障诊断总入口** —— 覆盖三层:① **服务器上单个用户实例**(内存 / OOM / 崩溃重启 / 打不开 / 起不来 / 挂了 / 一直重启 / 503 / 502 / 会话突然中断 / 响应慢 / 怀疑内存不够)② **业务插件**(插件没 UI / 预览不出现 / 卡片不渲染 / 装了插件但界面没变化·跟没启用一样 / 导入导出报 GLIBC 版本 / 网关崩了但数据其实写进去了 / 改了包上传了还是老行为)③ **跨机状态回流**(某字段跨机恒为 false / 永远是旧值 / admin 看到的启停状态与用户实际不一致 / 命令下去了但状态回不来 / 刷新后状态回跳 / 要不要让 worker 主动上报)。⚠️ **边界**:本技能管**服务器上的实例与插件**;「**本机** Windows 上把官方 dsh 跑起来(web 实例 / 桌面壳)及其取证」属 `dsh-local-env`。核心 = **先按症状定层**(⛔ 别先猜代码)+ 三层各自的第一原则(实例=先读 journal 再分服务/实例/会话三级;插件=**静默失败当默认假设**;跨机=**先判"缺失能力 vs 代码缺陷"**)。
|
||
version: 1.0.0
|
||
updated_at: 2026-09-28
|
||
last_change: 【2026-09-28】由三个同域技能**合并**而成:`dsh-instance-diagnose`(v1.0.0)+ `dsh-plugin-diagnose`(v1.0.0)+ `dsh-distributed-state-readback`(首版)。按 `dsh-knowledge-upkeep §10`「主干 + 详情档」形态:判据实体留在本主干,全文下沉到 `references/`(**内容守恒,逐行未改**)。合并理由:三者都在回答同一问题「**坏了 / 值不对,怎么定位**」,且前两者正文**本就互相声明边界**(同一领域的分层)⇒ 合并后由 §0 分诊表统一入口。⛔ 未删任何判据、命令、事故事实。
|
||
agent_created: true
|
||
---
|
||
|
||
# dsh-diagnose — DSH 故障诊断(三层入口)
|
||
|
||
> ## 🔴 第 0 步:按**症状**定层(⛔ 别先读代码猜)
|
||
>
|
||
> | 用户说的 | 层 | 去哪 |
|
||
> |---|---|---|
|
||
> | 实例打不开 / 起不来 / 挂了 / 一直重启 / **503** / **502** / 会话突然中断 / 很慢 / 内存不够 | **① 服务器实例** | §1 |
|
||
> | 插件没 UI / 预览不出现 / 卡片不渲染 / **装了插件但界面没变化** / GLIBC / **改了包还是老行为** / 网关崩了但数据进去了 | **② 业务插件** | §2 |
|
||
> | **某字段跨机恒为 `false`** / 永远是旧值 / admin 看到的状态与实际不一致 / 命令下去了状态回不来 / 刷新后状态回跳 | **③ 跨机状态回流** | §3 |
|
||
> | 「**本机** Windows 上把官方 dsh 跑起来」/ 桌面壳 / 本机取证 | ⛔ **不属本技能** | 技能 `dsh-local-env` |
|
||
>
|
||
> ⚠️ **②③ 最容易走错方向**:两者的症状都**静默**(不报错、不崩,只是"什么都没有"或"值不对")⇒ ⛔ 别当代码缺陷去 debug,先按各自的**第一原则**判性质。
|
||
|
||
---
|
||
|
||
## 1. 服务器实例(①)
|
||
|
||
**第 0 步永远是「先读 journal 拿真实失败请求」,不要先猜**(报障第一步)。
|
||
|
||
**定位顺序(每层可单独结案)**:
|
||
- **第 0 层 · 跨机日志取证(先做)** —— `dshlog` 把 47/106 的 journald 拉回本机再判(跨机、跨单元、带毫秒时间线)。🔴 两条硬前提:`ssh -C` **必加**(不加约 20 KB/s,**看起来像卡死**);**实例层(`dsh --profile`)的 stdout 不进 journald** ⇒ `dshlog` 拿不到实例自己的日志,实例层证据仍须上机取 cgroup / proc。
|
||
- **第 1 层 · 服务级** —— `systemctl status dshs` + journal 里 `crash-restart|instance-restart`。🔴 `exitCode 137` = 内核 SIGKILL(128+9),**九成是 OOM**。
|
||
- **第 2 层 · 实例级(内存三件套)** —— 先现查 scope 名(**每次重启 hash 会变**)⇒ `memory.usage_in_bytes` / `max_usage_in_bytes`(**字节,不是 KB**)/ `memory.stat`(**分 rss 与 cache**,rss 占绝对多数 ⇒ 内核 OOM killer 无路可退)+ `/proc/<pid>/smaps_rollup`。⚠️ **本机是 cgroup v1**(`/sys/fs/cgroup/memory/system.slice/…`),写 v2 路径**静默读到空**。
|
||
- **第 3 层 · 会话级(谁在吃内存)** —— 按 `session.jsonl.zstd` 的 mtime/size 排序;⚠️ 会话目录中间还有一层 workspace 转义目录,⛔ 别按 `home/sessions/<id>` 找。
|
||
|
||
**四条死亡路径(症状不同,别混)**:A · V8 堆限(`Reached heap limit` / `status=ABRT`)|B · cgroup 限(`oom-kill:constraint=CONSTRAINT_MEMCG` / `status=9` → 平台 137)|C · bwrap 挂载点被遮蔽(`Can't chdir to <正常路径>` —— **路径看起来完全正常**,极易误判成"目录没建")|D · cwd 为空(`Can't chdir to :` —— **路径是空的**,一眼可辨)。
|
||
⇒ **见到 `Can't chdir` 直接分诊**:路径空 ⇒ D(查 `folder` 是不是 NULL);路径正常却报不存在 ⇒ C(看 bwrap 的**中间目录是不是"就近创建"的**;✅ 正解=所有中间目录**统一前置 + 去重 + 由外到内**)。
|
||
|
||
**快速分诊(用户看到的状态码决定查哪层)**:**502** ⇒ 先看平台 journal 那条请求有没有 `request completed`(没有 ⇒ **平台接了却没回响应**,⛔ 别先查 nginx/域名/实例死活)|**431** ⇒ Cookie 超限(陈旧 `dsh-auth-*` 堆积;⚠️ 平台 journal 查不到该请求)|白屏 / `Failed to load plugins` ⇒ 先问"**无痕窗口是否同样报错**"。
|
||
|
||
**两条铁律**:🔴 **压测走隔离 cgroup**(`systemd-run … -p MemoryHigh/`MemoryMax`),⛔ 不在生产实例上压满配额(会触发 crash-loop / 熔断);⚠️ 复现路径 B 必须用**纯堆外分配**(`Buffer.allocUnsafe(...).fill(1)`),用 JS 对象数组会**先撞 V8 堆限(路径 A)**、永远复现不出 137。
|
||
|
||
**账实判据**:成本由「**装了什么**」决定,不是「装了几个」—— 一个重型插件(如 `dsh-univer-office` **+65 MiB**)≈ 无穷多个轻量插件;实测**整份会话数据只 0.9 MB** ⇒ 「减会话长度 / 少用 web_fetch」对内存**几乎无用**。
|
||
|
||
📂 **全文 ⇒ `references/00-服务器实例故障.md`**(含平台事实表、内存去向 smaps 拆解、插件内存逐个实测、注入层真机验证配方、9 条踩坑清单)
|
||
|
||
---
|
||
|
||
## 2. 业务插件(②)
|
||
|
||
> **第一原则:静默失败要当默认假设。** 排查顺序永远是「**先证明某一层到底有没有活着**」,而不是先读代码猜。
|
||
|
||
**三层归属(先定层)**:**host 半边**(工具能调通?死了=报"未知工具")|**client 半边**(页面 `__DSH_BOOT__` 里**有没有**该插件行?死了=**有无都没痕迹**:无卡片/无 dock、无报错)|**网关 / 原生绑定**(进程在、socket 能连、健康路径 200?死了=进程崩 / reset / `.node` 加载报错)。
|
||
|
||
**最隐蔽的一类 · client 半边静默挂死**:机理 = 加载器解析 `package.json` 的 `dsh.client.inject`(**包名**列表),**只要有一条 inject 永远不可满足,`apply()` 永不执行**(连注册都没有,静默)。
|
||
🔑 **通用尺子 = `inject` 差集**:取实例页面实际下发的客户端插件集合(**权威清单,⛔ 别用 manifest 正则——会漏行**)→ 取目标插件的 `inject` → **求差集**。**差集非空 = 确诊**。
|
||
⚠️ **别混两个 inject**:`package.json` 的 `dsh.client.inject` 是**包名**(不可满足 ⇒ **整体不 apply**);源码里的 `export const inject = [...]` 是**服务名**(只影响个别 `ctx.inject` 块)。
|
||
|
||
**第二类 · 组件挂上了、点击却没反应(外部契约失效)**:dsh 升级会改**外部契约**(槽位名 `conversation` → `main.conversation`;`ctx.locale.getLocale()` 由返回 id 变成返回**快照对象**)⇒ `querySelector` / 取值**静默失效**。
|
||
|
||
**桥接类(host 半边连不上 / 静默无限重连)**:⚠️ **先破一个假象** —— 内核 cordis 的默认 exporter **只写内存环形缓冲、不落 stdout** ⇒ 内核插件报错在实例日志里**看不见**,必须先挂 `ctx.logger.exporter(...)`。四层诊断配方(上游直连 → 实例内 fetch → **出站抓包**(能看出"从哪一代请求开始丢凭据")→ 硬编码对照)**逐层排除,⛔ 别跳步**。
|
||
|
||
**「注册成功」≠「模型可见」**:`register()` 返回 disposer 只说明"调用没抛错",能不能被模型看见要**另用 `tools.schemas()` 对账**。⚠️ 读法陷阱:⛔ 别用 `getOwnPropertyNames(...tools.data)` 数工具 —— `Map` 属性名是**空数组**,会把"有 N 个工具"误读成 `size=0`;**必须先判 `instanceof Map`**。
|
||
|
||
**原生绑定 / glibc**:第一步永远**先直测**(`ldd --version` + `node -e "require('<.node>')"`),⛔ 别读代码猜。两类对策不同:**有 JS 回退开关** ⇒ 资产侧垫片(可用则原样交上游 ⇒ 宿主升级后自动恢复);**无回退** ⇒ 只能宿主/镜像层解决(换 glibc ≥ 2.35)⇒ **上报用户决策,别硬凑**。
|
||
|
||
**「改了却没生效」四类**(按可能性):打包顺序把垫片废掉(模块级 `const` 被降级为 `var` ⇒ 静默回退原实现)|插件没真正换上新包(`mine/apply` 对**已启用**插件是 **noop** ⇒ 必须**停用→启用**两步;终态是 `success` 不是 `done`)|客户端包按 `rev` 缓存(**必须硬刷新**)|🔴 **同 `version` 号不会被重装** ⇒ **改代码必升 version** + 装完做**内容门禁**(`grep -q "<新符号>"`)。
|
||
|
||
**验证纪律**:每条结论都要有**可复现的命令**;`grep` 会骗人(esbuild 的 CJS 导出用 getter ⇒ 假阴性);业务插件 stdout **不落 journald** ⇒ 找不到"日志"时**别下结论**。
|
||
|
||
📂 **全文 ⇒ `references/01-业务插件故障.md`**(含注入差集完整命令、桥接类四层配方、可见性对账脚本、实测真因、平台侧事实)
|
||
|
||
---
|
||
|
||
## 3. 跨机状态回流(③)
|
||
|
||
> **第一原则:先判「缺失能力」还是「代码缺陷」** —— 判错方向会白烧一整轮。
|
||
|
||
**第一步 · 三条排除**(在**实例所在那台机**上取证,⛔ 不是 Manager):① 操作本身失败了吗(看**目标机文件系统**:软链/声明实际存在吗)② 执行环境没配好吗(看**目标机进程命令行** `/proc/<pid>/cmdline`)③ 调用链断了吗(**下发是否真走到远端**)。
|
||
🔑 **判据一句话:原机上成了 + Manager 侧读不到 = 缺失能力(拓扑必然),⛔ 不是缺陷。**
|
||
|
||
**第二步 · 分开看读/写两条路径**(⚠️ 最省时间):写路径(Manager→worker 下发)多**已有**|响应路径(任务返回值)多**已有**|**读路径(`GET` 那种"随时查状态")常缺的就是这条**。
|
||
⇒ 判据:**"操作后立刻返回的值是对的、刷新后变错" ⇒ 缺的只有读路径**;设计目标应写成「让读路径也拿到真值」,⛔ **不是**"加个缓存"(缓存=第二个真相)。
|
||
|
||
**第三步 · 三候选用「物理可行性」筛**(⛔ 不是用"哪个更好"筛),照这个顺序问:
|
||
1. **worker 有入站口吗?** DSH 硬口径是「worker 只拨出、无入站口」⇒ **候选 C(worker 主动上报)物理上走不通**,要新开出站通道/入站端点 ⇒ 撞「扩大可见面」红线门禁。
|
||
2. **reconcile 谁驱动?** DSH 由 **worker 本机定时器**驱动、**Manager 不在场** ⇒ **候选 B(共用台账)不能单独成立**,只能当缓存。
|
||
3. **已有一条 Manager→worker 的周期调用吗?** 有(`reportHost()` 每次心跳 `GET /healthz`)⇒ **候选 A 的增量 = 加宽返回值**,成本最低。
|
||
⇒ 结论通常是 **A 为骨架 + B 降级为可选缓存**;C 需先授权。
|
||
|
||
**语义细节(⛔ 不写这三条,回流做出来照样是错的)**:`asOf` 必带且 `stale` **现算不落库**(落库的 `stale` 自己也会陈旧);阈值 = **3 × 心跳周期**且**与心跳周期同源计算**(⛔ 不写两个独立常量);**三层语义别混**(意图=控制面审计/**事实=对端 profile 的 bundles**/不可用=数据面台账)⇒ **`GET` 取「事实」不取「意图」**;🔴 降级时 **⛔ 不许把"未知"显示成"未启用"**(这正是原缺陷的成因)。
|
||
|
||
📂 **全文 ⇒ `references/02-跨机状态回流.md`**(含本判据的 DSH 实证、三候选完整优缺点、失败降级场景表、与既有台账的粒度对比、交付件 10 节骨架、9 条反模式、关键文件地图)
|
||
|
||
---
|
||
|
||
## 4. 详情档索引(跨档引用按此表定位)
|
||
|
||
| 档 | 覆盖的原技能 | 原章节 |
|
||
|---|---|---|
|
||
| `references/00-服务器实例故障.md` | `dsh-instance-diagnose`(v1.0.0,全文) | 何时用 / 平台事实 / 三层定位 / 快速分诊 / 四条死亡路径 / 隔离复现 / 踩坑清单 / 定量归因 / 内存去向 / 插件内存成本 / 不兼容插件 / 注入层验证配方 / 相关 |
|
||
| `references/01-业务插件故障.md` | `dsh-plugin-diagnose`(v1.0.0,全文) | §0 第一原则 / §1 三层归属 / §2 静默挂死 / §2.5 桥接类 / §3 glibc / §4 改了没生效 / §5 验证纪律 / §6 平台侧事实 |
|
||
| `references/02-跨机状态回流.md` | `dsh-distributed-state-readback`(首版,全文) | §0 治什么 / §1 三条排除 / §2 读写路径 / §3 三候选 / §4 语义细节 / §5 与台账关系 / §6 交付件骨架 / §7 反模式 / §8 文件地图 |
|
||
|
||
🔴 **合并前的三个技能名已退役**(`dsh-instance-diagnose` / `dsh-plugin-diagnose` / `dsh-distributed-state-readback`)⇒ 正文或别处若出现这三个名字,**按本技能对应章节读**。
|