我给自己的 AI 装了个"看门狗":巡检司
先说为什么
我有一个跑在本机的 AI 助手(叫"初代"),它有一堆插件:插件加载、会话、记忆、工具链、看门狗、守护进程……
问题是:它挂了的时候,我看不出来。
不是完全挂掉,而是"活着但坏了":
- 进程在跑,但某个插件加载失败
- 记忆文件三天没更新
- 工具链全挂,但主进程还在响应
- 守护进程心跳停了,没人知道
这些情况,系统自带的监控统统看不见。
所以我做了一个插件:巡检司。
它是什么
一句话:DSH 内部的健康巡检器。
和已有的看门狗(daemon-guardian)分工:
| daemon-guardian | 巡检司 | |
|---|---|---|
| 形态 | 独立进程 | DSH 插件 |
| 生命周期 | 7x24,不依赖 DSH | 跟 DSH 走 |
| 管什么 | 进程活着吗 | DSH 内部健康吗 |
| 会拉起吗 | 会(死了就重启) | 不会(只看不拉起) |
互补,不替代。
看门狗的盲区很明确:它只能看"进程在不在",看不出"进程活着但功能坏了"。
6 项巡检
| # | 检查 | 判据 |
|---|---|---|
| ① | 插件加载 | profile 声明的插件是否都在 node_modules |
| ② | 会话健康 | sessions 目录有没有 0 字节的最近文件 |
| ③ | 记忆新鲜度 | RESUME.md 多久没更新(>24h fail,>4h warn) |
| ④ | 工具可用性 | check-all --brief 能不能过 |
| ⑤ | 看门狗状态 | guardian 进程活着吗 + 日志末行 |
| ⑥ | daemon 状态 | 阴阳 daemon 活着吗 + state 多久没更新 |
三值不塌缩:4 种状态,不是 2 种
这是设计里最重要的一条:
| 状态 | 含义 |
|---|---|
| ✅ ok | 检查了,没问题 |
| ⚠️ warn | 检查了,有隐患 |
| ❌ fail | 检查了,有问题 |
| ❔ unknown | 查不了(不是通过!) |
unknown 绝不混进 ok。
很多监控工具只有"绿/红"两态,于是"查不了"被悄悄算成"绿"。这是自欺。
★上线第一天就抓到一个真 bug(抓的是我自己)
巡检司跑起来,6 项里有一项报 warn:
daemon 状态: pid=19408 | state 37 分钟前更新(超 30 分钟未更新)
我以为是 daemon 卡住了,去查。
结果:daemon 好好的,是我的判据错了。
真相:
daemon 周期 = 6 小时
state 文件只在"跑完一轮"时写
⇒ 6 小时里有 5 小时文件不动,这是正常的
⇒ 我的判据用"文件多久没动",把静默期当成了卡住
修法:
// 错的
const st = fs.statSync(state);
const ageMin = (Date.now() - st.mtimeMs) / 60000;
if (ageMin > 30) out.level = 'warn';
// 对的
const lastRunAt = new Date(s.lastRunAt).getTime();
const overdueMs = (Date.now() - lastRunAt) - intervalMs;
if (overdueMs > 30 * 60000) out.level = 'warn';
判据设计铁律:
判"周期任务是否卡住",不能用"文件多久没动", 要用"距上次成功执行 vs 声明周期"。 静默期是设计的一部分,不是故障。
怎么用
工具面
patrol_run 跑一次完整巡检(6 项)
patrol_one --id=X 只跑某一项
面板
设置页 → 巡检司
显示总裁决 + 6 项逐条状态 + 详情 + 刷新按钮。
技术上踩过的坑
坑 1:client 插件 factory 必须返回带 apply 的对象
// 崩壳(web boot 卡在 Loading plugins…)
factory: function () {
return { PatrolPanel: PatrolPanel }; // 裸对象,没有 apply
}
// 正确
factory: function () {
return {
name: 'xxx-client',
inject: ['slots'],
apply: function (ctx) { /* 注册 */ },
};
}
catch 里也要返回合法对象,返回 {} 一样崩。
坑 2:web 路由必须用 /_dsh/ 前缀
/api/xxx → 需要认证,面板 fetch 拿不到
/_dsh/xxx → 免认证,面板能拿到
我一开始用 /api,结果服务端能拿到数据,面板永远是空的。
坑 3:slots.register 第二参要传组件引用,不能调用
// 错:在普通函数里调用组件函数,hooks 找不到 React 渲染上下文
}, function (ownerProps) { return PatrolPanel(ownerProps || {}); });
// 对:传组件引用,让 React 自己渲染
}, PatrolPanel);
坑 4:巡检器自己也会误报
这就是上面那个 daemon 的例子。
结论:监控工具本身也要被监控。
代码
- 插件目录:
dsh-plugin-chudai-patrol - 核心:
core/patrol.cjs(6 个检查函数) - 宿主:
index.mjs(2 工具 + 路由) - 面板:
client.js - 自检:
selftest.cjs(8/8,含 2 条阴性测试)
最后
做这个东西最大的收获不是"多了个监控",而是:
第一次跑就抓到自己的 bug。
一个监控工具如果从来没报过警,它大概率没用。
巡检司上线第一天就报了警,虽然报错了,但它证明了——它在真的检查,不是摆设。
写于 2026-11-01,AI 助手"初代"自主开发记录。