- Home
- Plugin
- Sicurezza e permessi
- dsh-recency-guard
dsh-recency-guard
xuanyuanchumo/dsh-recency-guard
Crash-recovery guard for DeepSeek Harness plugins: records install/update timelines of third-party and experimental plugins with automatic config backups; after a crash it ranks suspects by recency and blocks them before the next boot, widens the quarantine per additional crash, and restores stable plugins afterwards. Confidence-graded recovery (single suspect acts immediately, multiple suspects need N consecutive confirmations) with two-phase commits and reboot reconciliation; a zero-dependency offline rescue CLI (CJK-width aligned output) reads the artifacts when the host cannot boot; the panel ships a lifecycle state machine, Markov transition matrix, Shewhart control band, CUSUM change-point metrics, an interactive capability graph, plus a plugin-info card with a GitHub feedback entry; per-project partitions keep records isolated.
Installazione
dsh plugin --profile web add github:xuanyuanchumo/dsh-recency-guardREADME
v0.10 新增(可观测性数学增强 + 洞察卡 + 插件信息/反馈入口)
- 转移矩阵卡(马尔可夫链):阶段行为的「去向分布」——monitoring 是健康值守 (80% 留在自己)还是高危巡逻(40% 掉进 crashed)一眼可见;幂法迭代求平稳分布 (ε=1e-9,L∞ 判敛),回答「长期来看守护停在哪个阶段」。样本不足时诚实显示 「不可判定」,白名单外迁移被丢弃且如实计数。
- 控制带(休哈特单值-MR):崩溃间隔的 x̄±3σ̂(σ̂=MR̄/1.128)控制带, 区分「偶然波动」与「特殊原因」——哪一次间隔越界(崩溃变密)被点名到序号; 与 CUSUM 变点、回归斜率、生存分析互为视角(趋势看方向,控制带看单点越界)。
- 数据流卡:journal 原文 → 事件投影 → 语义流动 → 面板渲染的四环链路白盒, 每环带说明与计数 —— 「画面上的事实从哪来、经过什么变换」不再靠猜。
- 系统诊断卡 + 插件信息面板 + 反馈入口(R18):插件版本 / 宿主版本 / 运行环境 /
分区键 / 数据面状态 / 快照耗时一卡尽览;仓库链接来自 package.json 的
repository/bugs,「问题反馈」按钮直达 GitHub Issues(未登记时隐藏按钮, 绝不放死链);工具status的命令窗口输出同步携带这两条。 - 命令窗口显示修复(R10):
rescue.mjs的 status/history 输出按 CJK 显示宽 (全角 2 列)对齐 —— 中文标签与英文值混排不再错位(rescue 零依赖纪律不变, 宽度判定内联实现)。
v0.9.1 新增(判定可视化)
- 恢复判定条:崩溃后的分级判定现在到达用户可见面 —— 面板生命周期卡片顶部
显示「观察中 / 已行动」标签 + 完整判定理由 + 进度(第 N/M 轮,act=false 虚线中性色,
「观察中」不是错误);工具
status的 facts 同步携带。undefined(从未崩溃过) 不渲染假判定。 - 事件流高亮两阶段:观察轮/恢复判定/两阶段对账的 journal note 在事件流卡片 用 warn/success 徽标区分(识别 detail 前缀派生语义,存储词汇不变)。
v0.9 新增(恢复置信分级 + 两阶段提交 + CLI 逃生通道 + 卡片限高)
- 恢复置信分级:唯一嫌疑是确定性证据(第 1 轮即关闭);多嫌疑是概率性证据,
同一头号嫌疑连续 N 轮(默认 2,可配
confirmRounds)复现才自动关闭。 偶发崩溃不再误伤无辜插件 —— 观察轮的崩溃证据链完整保留,供下次判定。 - 两阶段提交 + 重启对账:关闭动作分 prepared(记录待执行)→ quarantined (已生效)两阶段;崩溃打断时下次启动按磁盘现状对账(confirmed/reverted), 不留半成品状态。
- CLI 逃生通道:
node bin/rescue.mjs status|history|decision|archives --profile-dir <dir>—— 宿主起不来时离线读取守护产物(零依赖、只读、 语义化退出码 2 = 有未恢复崩溃,供脚本联动)。 - 面板备份策略卡片:时间线备份开关/保留份数/备份根/配置编辑服务可用性 一目了然(只读投影;改配置走宿主设置页)。
- 卡片固定高度 + 扩大:每张展开卡片内容区限高 240px 内部滚动(窄侧栏 180px),视口密度均匀;卡片头部 ⤢ 按钮临时解除限高摊开阅读,与折叠态独立。
v0.8 新增(备份增强 + 官方实验插件观测 + 事件驱动)
- 时间线备份:插件更新时自动把「更新前」的配置快照存为
<版本>@<时刻>.json, 每插件默认保留 5 份(可配),更新前后的配置可逐版对账 —— 回答「升级后行为变了, 是配置变了还是 bug」。工具action=backups同时报告最近备份与每插件时间线。 - 自定义备份目录:配置
backupRoot或环境变量RECENCY_GUARD_BACKUP_ROOT; 自定义根下仍按分区键分层,多项目隔离不因集中存放而失效。 - 官方实验插件观测:
@deepseek-ai/dsh-experimental-*只记录不关闭 (journalofficial-observe事件 + 整组状态的时间线备份)。它们是官方的 实验场,崩溃概率最高,但自动关闭官方组件的风险不可接受 —— 现在崩溃归因时 能回答「当时装了哪些实验组件、什么版本、什么状态」。 - 事件驱动变更检测:订阅宿主
plugin-manager/changed事件(500ms 防抖), GUI/工具路径的安装更新立即进时间线与嫌疑面,不用等重启。CLI/手工编辑 不发此事件,仍由每次启动的同步 diff 兑底 —— 事件是加速器,不是唯一真相。 - 嫌疑排名显示修复:数据层不再截断(曾 slice(0,10) 且渲染又 limit 5, 「报 N 只列 5」);命令窗口列 15 条带省略计数,面板默认 15 条 + 「展开其余 N 个」。
dsh-recency-guard
插件崩溃恢复守护 —— 解决"添加或更新第三方/实验性插件导致 DSH 崩溃、必须禁用所有插件再逐个添加"的问题。
当某个插件把 DSH 拖垮时,本插件会在崩溃的那一刻记录下"谁最可疑",并在下一次启动时、在崩溃插件的代码被加载之前把它挡在门外。反复崩溃时,关闭的插件会逐个增加(第 1 次关最近更新的 1 个,仍崩溃则关最近更新的 2 个)。稳定之后,除首要嫌疑外,其余被关闭的配置会自动恢复。
v0.12.1 修复(两条均由真机用户旅程发现)
- 崩溃信号补第二条通道(P0 检测缺口):崩溃信号此前只有哨兵文件,而哨兵写在
process.on('exit')里 —— SIGKILL / OOM killer / 断电 / 原生段错误根本不运行 exit 处理器。真机实测:强杀后bootMarker残留(pid 已死)而crashSeq仍是 0, 下一次启动报「启动时未发现上次崩溃」。 危险的不是少记一次,而是崩溃循环失去升级依据:插件以原生方式撞死宿主时, 每一轮都只看到残留标记、每一轮都被当成干净启动清零计数 ⇒ 永远停在「关 0 个」。 新增classifyAbnormalExit四种结局:clean(无残留 / 有哨兵)⇒ 归零;有哨兵时走既有通道、不重复计数;self-reload(残留 pid == 本进程,热重载/HMR)⇒ 归零 + 留痕;concurrent(残留 pid 仍活着且不是自己)⇒ 归零 + 留痕「另一个实例在运行」;abnormal(残留 pid 已死)⇒ 计数 +1,累计到confirmRounds才动手。 异常终止按概率性证据处理(残留标记也可由人为杀进程/关机产生):第 1 轮只观察、 第 2 轮才行动,达阈值后复用既有recover()通道关闭(不另写一套关闭逻辑, 避免两条通道的隔离语义各自漂移)。真机两次强杀验证:1/2 轮 act=false→2/2 轮 act=true 关闭 1 个。 顺带修了引出它的二阶缺陷:原recovering && !crashedLastRun把「没有哨兵」 当「活过了启动」的代理,新通道下异常终止同样没有哨兵 ⇒ 会把「关了嫌疑 → 又死一次」 读成「崩溃已过」而恢复全部插件,亲手拆掉刚建立的隔离。现改为要求exitClass === 'clean',并拆成纯函数shouldRestoreAfterBoot用 5 条单测钉住。
- 小地图点击无反应(真机 GUI 实测):
setPointerCapture?.(id)排在onNavigate(...)之前,而可选链只保护「方法不存在」、不保护方法抛异常。 浏览器在 pointerId 失效时按规范抛NotFoundError⇒ 整条 handler 中断 ⇒ 点小地图毫无反应(只有控制台有痕迹)。一个只为「拖拽连续性」存在的可选优化, 否决了「定位到那一块」这个主功能。 修法:捕获/释放各自try包裹并抽成capturePointer/releasePointer; 新增 3 条源码门(minimap-gesture.test.ts)钉住「不得出现裸调用」, 并经变异自证会响(写回裸调用 ⇒ 恰 1 条断言 FAIL)。真机复验: 导航生效且 0 未捕获异常。
v0.12 新增(命令窗口显示接线 · 乐观并发 · L4 审计线 · 依赖代际对齐)
- 命令窗口显示修复(根因级):
presentCall/presentResult/output.presentationMeta三个展示投影此前写好了却没接进defineTool—— 宿主因此落回通用回退卡片:标题是裸工具名recency_guard、正文是整坨参数 JSON。 接线后待执行卡片显示人话摘要(事件时间线 · 最多 40 条),完成卡片显示 带标题与可折叠分区。同时把本地视图类型换成宿主词汇(ToolCallView/ToolResultView,content是ContentBlock[]而非string)—— 类型漂移 正是「写了不消费」的恶果。反漂移双门:契约源码门 + 宿主行为门(走宿主同一条 数据流execute → presentationMeta → presentResult,含旧记录回放的诚实降级)。 - 乐观并发 revision(防「过期按钮」):面板快照带一枚从
state+decision+journal 尾派生的 12 hex 哈希;动作请求携带
expectedRevision,凭证过期 ⇒ 409 且动作不执行(不是执行了再报错), 并回传当前凭证供重读。用哈希而非计数器:计数器需要单调持久位点,崩溃时 会落后于状态(假冲突/假放行),哈希幂等 ⇒ 崩溃重启后同一事实必得同一枚。 缺参数 400 优先于 409;不带凭证放行(agent 主动修复路径);跨分区只读视图 恒null(只读无可改事实,带凭证必然假冲突)。面板动作已带凭证 + 冲突提示条。 同源性是结构保证:两处装配唯一收敛到Guard.revisionFactsFrom, 并由端到端门断言「面板凭证 == 动作路径凭证」,该门经变异实验自证会响。 - L4 审计线:写动作(recover/restore/reenable/archive)在执行之前
落一条审计行
{actor, action, justification}(崩溃发生在动作中间时理由已在盘上); 读动作零噪声(否则面板每 5s 轮询会把崩溃因果链冲掉)。复用 journal 的note通道而不是新开 kind —— kind 是跨层词汇表,漏改任一处审计行就在事件流里静默消失。 - 依赖代际对齐宿主(阻断级修复):
cordispeer 从~4.0.5改为>=4.0.5 <5.0.0。宿主实际携带 cordis 4.1.0 ⇒ 旧范围下 npm 安装会被 兼容闸门整体拒绝并回滚;桌面端用link:直连源码从不走闸门,所以这个洞 在本机完全不可见。devDependencies 同时从 0.2.1-rc.5 全量对齐装机代际 0.2.2-rc.1(此前测试跑在一个从未有人装过机器的代际上:绿灯不代表装机绿)。 新增「以宿主真实事实为见证」的契约门 11 条:逐 peer 用runtime/(从 app.asar 解出)的装机版本做semver.satisfies判定 + devDep == 装机版本断言。 - dsh.compatibility.dshReleases:逐发布线声明 compatible(借鉴 dsh-context), 市场索引与兼容闸门可逐版本判定,而不是只看 engines 区间。
- 图谱交互补齐:k 跳邻域聚焦(原「只看关联」是传递闭包 ⇒ 连通图上等于
不过滤,即「点了没反应」)、小地图 Overview(可点可拖的缩略导航,往返误差
< 1e-9)、边宽 = 连接度对数刻度、键盘方向键二维导航 +
aria-keyshortcuts。 - 技术债清理:数据层显示截断 4 处(「报 N 只列 5」同族,含搜索指示器
对 20 个命中说「第 1/8 个」的假话)改为数据给全量 + 渲染层显式省略计数;
i18n parity 测试补上(回退链会掩盖缺键:只在 zh 加键则英文露中文原文而 CI 全绿);
路由日志面桥回宿主 logger(console.error 不进宿主日志采集面);
防抖计时器持有 +
unref()+ dispose 清理(热重载不留幽灵定时器)。 - 复杂度治理:工具注册抽出
tool-registration.ts;小地图几何抽出minimap-model.ts(graph-model 117 → 101;且刻意从LAYOUT各自直读避免 graph-model ⇄ minimap-model 循环依赖 —— 循环里的常量 TDZ 会让 「小地图矩形尺寸随机为 0」只在一台机器上复现)。
v0.11.4 触屏双指缩放(图谱交互补齐)
- 双指捏合缩放:两指同时按下即进入捏合 —— 中点下的世界点在缩放中保持 不动(与单指缩放同一不变式),同时中点位移会带动画布平移;捏合期间取消 画布平移拖拽,抬指后清空取样。退化输入(两指重合/非有限)返回原视口, 绝不产生 Infinity。
- 回归:viewport 新增 4 条捏合断言(翻倍缩放/锚点不动/跟随平移/退化与上界)。
v0.11.3 图谱交互补齐(边可交互 + 布局过渡)
- 边可交互(审计 P0 收尾):此前边完全不可点击、
edge.label是死数据。 现在每条边叠加透明宽描边热区:悬停高亮 + 指针变化、点击显示边详情侧栏 (生产方 → 消费方 · 语义 · 边自带 label);选中边的语义标签与数据标签 分别绘制。选中态与节点详情互斥(单一真相),Esc / 空白处点击 / 「重置」都会清空。 - 布局切换过渡:节点坐标改用 CSS transform,布局切换与拖拽回弹有
过渡动画;拖动中的节点禁用过渡(否则「追不上指针」);
prefers-reduced-motion由全局规则降级。 - 回归:
edgeDetailOf纯函数新增 3 条断言(端点解析/字段带出/幽灵端点 null)。
v0.11.2 紧急修复(恢复诚实性 + 启动恢复时序)
- 恢复结果白盒化(P0 假绿修复):
reenable此前无条件返回reenabled: true并从autoDisabled移除 —— 实测管理器返回application: 'failed'(HMR 嵌套事务)时,state 仍记「已恢复」,面板/工具 显示成功而插件实际仍关闭。现在只有真的 applied 才算成功;失败者保留在 自动关闭名单待下轮重试,journal/note 如实列出「未启用(管理器拒绝)」。 - 启动恢复延后到 HMR 事务之外:boot 阶段 Loader 自身可能正处在事务中,
恢复调用会报
HMR transactions cannot be nested。自动恢复改为 boot 后 1.5s 执行(定时器 unref,不拖住宿主退出),失败由诚实结果兜底。 - 回归:host 工具测试新增「管理器拒绝 ⇒ reenable 报失败」断言;14/14 全绿。
v0.11.1 紧急修复(宿主兼容 + 启动崩溃 · 2026-11-08)
事故背景:桌面端 DSH 自动升级到 dsh 0.2.2-rc.1 后连续崩溃(5 次/3 分钟),
守护插件被兼容闸门移出 profile、不再保护;重新挂载后又因遗留写锁把宿主拖进
fatal load failure。本版本修复三处,全部是「守护不能拖垮宿主」的底线问题。
- 宿主预发布版本兼容(P0):
engines.dsh与全部@deepseek-ai/dsh-*peer 范围改为显式预发布分支>=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0。 原因:node-semver 只放行「与范围中某个比较符 major.minor.patch 元组相同 且自身带预发布标签」的预发布版本 —— 旧范围>=0.2.1-rc.5 <0.3.0-0会 静默排除0.2.2-rc.1,插件被兼容闸门判为不兼容并从 profile 移除。 已在dsh 0.2.2-rc.1干净沙箱(sb-rg-v0111)实测安装 + 启动 + 守护激活通过。 - 启动绝不抛出(P0):
boot()失败改为降级留痕(步骤记失败 + 警告 日志),不再 rethrow。此前一个可恢复的写锁超时会把宿主变成dsh: fatal load failure(退出码 1)—— 观测/恢复设施不能拖垮主流程。 - 失效写锁自愈(P0):新增
cleanStaleLocks(Store.init 启动时清理 +updateState超时后清理重试一次)。判据:锁内容为空/非正整数 PID(不完整 记录,atomic-write 永远无法接管)/ PID 对应进程已死,且文件年龄 > 5s (避开锁创建与写 PID 之间的零字节窗口,绝不抢活锁)。 真机事故:崩溃恰好发生在锁创建与写 PID 之间,遗留 0 字节state.json.lock,此后每次启动都等满超时并致命。 - 连续崩溃兜底升级(P0):
assessConfidence新增安全网 —— 累计崩溃crashSeq ≥ 3时即使概率计数未达confirmRounds也强制行动;同时readCrashHistory跳过启动时的「无嫌疑 crash-detected」记录。实测该记录 每轮打断连续计数,导致 crashSeq 涨到 3 仍停在「观察中 1/2 轮」,崩溃循环 永远无法自愈。 - 测试:新增
tests/unit/store-lock.test.ts(锁自愈 11 断言)与契约层 预发布范围守卫;14/14 全绿(Core 7 / Host 2 / Client 4 / Contract 1)。
v0.11 新增(数据面安全 + 图谱图例一致性 + 马尔可夫去向可见 + 市场准备)
- 动作路由方法守卫(P0 安全修复):
/action/archive|reenable|restore|recover此前不检查 HTTP 方法 ——GET /recency-guard/api/action/archive也会执行归档重置 (浏览器预取/爬虫/误点链接都可能触发)。现在非 POST 一律 405 +allow: POST。 ?partition=真正生效(P0):客户端一直拼分区查询参数,但数据面此前从不读它 —— 跨分区查看永远静默返回当前分区。现在快照路由在宿主侧实现分区感知: 请求其它分区返回只读磁盘投影(状态/日志/决策/备份),运行时字段如实标注 归属,未知分区返回available:false(绝不假绿)。- 面板动作面补齐:新增
/action/restore路由 + 面板「恢复非嫌疑插件」按钮 (工具早有此动作,路由此前缺失);暂停轮询时 footer 显示「数据已停留 N 分钟」。 - 图谱图例一致性(P0):图例颜色此前固定语义色调色板,切到「按类型」口径时 与节点实际颜色不一致;现在图例色跟随当前着色口径(legendColorOf),并直接 绘制节点形状(形状=第二编码通道,图例不展示它就只对知道映射的人可读)。
- 搜索循环跳转:多匹配时回车依次遍历(第 n/m 指示可见),不再永远停在第一个; 「重置」按钮现在同时清掉拖拽偏移(此前节点拖飞后只能刷新页面)。
- 马尔可夫跨阶段去向(P0):矩阵卡此前只渲染自环概率条 ——
monitoring → 崩溃 40%这类核心去向分布(README 宣称的)在界面上根本看不到。现在每行带 top-2 跨阶段去向徽标。 - 事件流展开:默认 12 条 + 「展开其余 N 条」显式计数(与嫌疑排名同款纪律, 数据层给全量、显示层管截断)。
- 测试盲区补齐:新增
tests/client/graph-model.test.ts(过滤共源/布局确定性/ 图例同源/平行边曲率)与tests/host/routes.test.ts(方法守卫/partition/参数 校验/请假语义)—— 文件头「已由测试钉死」的注释如今成真。13/13 全绿。 - 参考插件取长补短:系统研究 dsh-ops(输出 notice 三要素)、dsh-compat-guardian
(reconcile 自愈/原子写/乐观锁)、dsh-context(工作预算/资源自适应)、
dsh-cookie-guard(独立救援 CLI)后按审计清单采纳落地;研究报告见
.cycles/cycles/cycle-6/参考插件研究报告.md。 - 插件市场准备:仓库 topics(dsh-plugin 等)补齐、256px 图标、screenshots.json 声明、投稿 YAML 按 awesome-dsh-plugin contributing.md 形态定稿。
v0.7 新增(崩溃级修复 + 可交互图谱 + 可观测性白盒化)
先说最要紧的一条:v0.6 会把 profile 写崩。
- 崩溃级修复:守护行 YAML 注入。v0.6 写入守护行时
name:没有加引号, 而 YAML 把@列为保留指示符 —— 于是任意 scoped 包(@linxin666/...) 都会让整份cordis.patch.yml不可解析,DSH 起不来。真机桌面 profile 已中招(第 185/189/193 行)。v0.7 改为「永远引号 + 写前校验 + 原子回滚 + pristine 备份」三层防线,并提供npm run repair-patch修复既有污染。 - 静默失效修复:
apply里直接读未inject的ctx.configEditor会抛错, 表现为「端口在听、UI 能开,但插件根本没激活」(日志只有一行warning: 1 entry did not activate)。改走tryReadService后消失。 - 可交互图谱:hover 高亮 + tooltip、点击选中联动详情、搜索过滤、 布局切换、缩放平移钳制;fit 只做一次,数据轮询不重置用户缩放 (实测 7 秒轮询零变化)。滚轮改用原生非 passive 监听, 修掉「缩放时页面跟着滚」。
- 面板自己滚动(v0.7.1):宿主槽位是
overflow:hidden且整页不滚, 面板内容约 4000px 被裁到一屏 —— 下方指标/能力清单/归档根本看不到。 面板改为自带滚动容器(height:100%+min-height:0+overflow-y:autooverscroll-behavior:contain),实测可滚到scrollTop=3361并看到底部归档区。 滚轮分工同步调整:普通滚轮滚动面板,Ctrl/⌘ + 滚轮缩放图谱 (缩放入口仍在工具栏三个按钮上)。
- 可观测性白盒化:每个指标都带
formula/inputs/sampleSize/confidence;不可判定时defined=false且给原因("没有崩溃样本"), 绝不把无数据当 0。CUSUM 变点按相邻崩溃间隔回归(不是时间戳、不是累计计数)。 - 能力闭环:能力清单与注册处双向比对;
consumes/consumedBy方向修正; 21 项能力、对外暴露 12、连通率 100%、孤立 0(面板可见)。 - 8 条数据面路由全部可用(曾出现
isolation/lifecycle500, 根因是 RouteContext 缺 provider)。 - 模块拆分降复杂度:
patch.ts142 →patch-scalar.ts(87) +patch-yaml.tspatch.ts门面,band-detectorStatus: OK。
v0.6 新增
- 崩溃压力(EWMA):新增 KPI「崩溃压力」——EWMA 平滑的近期崩溃密度 (半衰期 5 次启动)。同样 1/3 的崩溃率,崩溃集中在最近时压力更高, 这是整体崩溃率给不出的时间局部性。无观测样本时诚实显示「不可判定」。
- 归档摘要:归档清单(工具
archives/ 面板归档卡片)现在带 manifest 摘要——那次归档时的崩溃次数、事件数、被关插件。「举他对照」时能看到 基线事实,而不只是「归过档」。 - 隔离可见性:工具
status输出新增「工作区 · 分区」行,命令窗口可 对账「这次守护是哪个项目的事实」;面板 header 显示可读项目名 + 完整工作区路径(悬停全文),哈希前缀退居兜底。 - 图谱邻居导航:节点详情侧栏新增上游(生产方)/下游(消费方)邻居 列表,点击即跳转定位——度数回答「有几条边」,邻居回答「连到谁」。
v0.5 新增
- 图谱交互二次增强:节点拖拽重排(布局偏移跨轮询保持)、搜索定位(id/标签/引用三级优先匹配,回车跳第一个)、节点详情侧栏(完整 id + 度数 + 定位按钮)、双击节点视口居中、贝塞尔曲线边(同一对端点的平行边奇偶交替弯曲,方向可辨)、高亮边带生产/消费语义标签。
- 面板现代化:KPI 摘要条置顶(稳定性 SVG 圆环、当前阶段、崩溃率、趋势判定、CUSUM 变点一览);崩溃间隔统计(中位/CV)与 CUSUM 变点接入指标卡片;轮询暂停/恢复开关(调试截图时画面不再跳动);卡片可折叠(折叠态记入 localStorage,跨会话保持)。
- 性能债清理:图谱 id→node 索引由「每条边重建一次」改为「每个节点数组建一次」(O(E·V) → O(V)),节点上百时轮询不再卡顿。
v0.3 新增
- 能力闭环可证:能力清单(注册/生产/消费)与实际注册面双向比对(契约测试 100% 层),三类缺口(消费断链/过度生产/孤立)可检出。
- 生命周期状态机:六阶段白名单迁移(idle/monitoring/crashed/deciding/stabilizing/recovered),非法迁移被拒且不改状态,迁移历史带原因与时刻。
- 可量化可观测:崩溃率/平均恢复耗时/检出率/隔离纯度/稳定性评分/风险评分/崩溃趋势七个读数,不可判定时为 null 而非 0。
- 管理面板 + 可交互图谱:
main与侧栏双席位,滚轮缩放以指针为锚、拖拽平移、点击高亮追溯(BFS)、Esc 一次退出;响应式(窄屏单列)、主题变量驱动。 - 多项目隔离:状态按项目目录分区(哈希键),A 项目崩溃不影响 B;守护行经指针文件搭桥。
- 归档重置 + 举他:归档 = 搬走 + 留档(manifest 先读后搬),重置后不继承旧结论、按最新 node_modules 事实重推嫌疑;另一工作区可只读清单作对照。
- 质量评审体系:Core/Host/Client/Contract 四层测试 + 类型/lint/构建/依赖审计全零门禁(npm audit high:0 critical:0)。
目录
它解决什么问题
在 DSH 里添加或更新一个第三方 / 实验性插件后,DSH 可能直接崩溃退出:
dsh: fatal uncaught exception: Error: ...
崩溃发生在插件启动阶段,所以此时插件本身已经无法运行,也就无法自己写任何恢复逻辑。传统做法是:手动禁用所有插件 → 重启 → 逐个加回 → 每次重启验证。这既慢又容易再次踩坑。
本插件把这件事自动化:
| 步骤 | 传统做法 | 本插件 |
|---|---|---|
| 记录安装/更新日期 | 靠记忆 | 自动写入日志(崩溃也不丢) |
| 备份插件配置 | 靠手动复制 | 自动备份到 .recency-guard/backups/ |
| 崩溃后定位嫌疑 | 靠猜 | 按"最近更新"排序 |
| 禁用插件 | 手动改配置 | 自动写入决策文件,重启即生效 |
| 反复崩溃 | 重复手工操作 | 第 2 次崩溃自动多关一个 |
| 恢复配置 | 手动逐个加回 | 非首要嫌疑自动恢复 |
工作原理
1. 崩溃是怎么发生的
DSH 的 Loader 会捕获插件 apply 中抛出的错误,让该插件失效但不影响其它部分。真正致命的是未被捕获的 rejection / 未捕获的异常:Harness 的 fail-loud 处理器会把它们变成一次 exit(1),整个 DSH 随之退出。
这意味着恢复必须发生在崩溃插件被加载之前,而不是之后。
2. 崩溃检测:进程级哨兵
Harness 在正常退出和崩溃时都会释放插件的 fiber,所以插件的 dispose 回调无法区分二者。能区分的是退出码:正常退出是 0,崩溃是 1。
因此本插件在进程级注册 process.on('exit')(不是 fiber 级 —— cordis 的 disposer 逆序执行,fiber 级的监听器会在进程真正退出前被移除):
- 退出码非 0 → 写入
.recency-guard/crashed哨兵文件 - 同时同步写出
.recency-guard/decision.json恢复决策
3. 恢复决策:在插件被加载之前生效
Loader 支持给 patch 行写一个 disabled: !!js "<表达式>"。关键在于:Loader 在导入该行的模块之前就会求值这个表达式。
本插件为每个第三方插件在 profile 的 cordis.patch.yml 中写入这样一行(安装时自动完成):
- id: some-third-party-plugin
name: dsh-some-third-party-plugin
disabled: !!js "(() => { try { const fs = process.getBuiltinModule('fs'); const profile = ctx.get('profileContext'); if (!profile || !profile.dir) return false; const file = profile.dir + '/.recency-guard/decision.json'; if (!fs.existsSync(file)) return false; const decision = JSON.parse(fs.readFileSync(file, 'utf8')); return Array.isArray(decision.disable) && decision.disable.includes('dsh-some-third-party-plugin'); } catch { return false } })()"
表达式读取上一轮崩溃时写下的决策文件,命中就把该插件挡在门外 —— 崩溃插件的代码根本不会被加载,崩溃自然不会重演。
表达式运行在 Loader 的
with (ctx) { eval(expr) }作用域里:有ctx和process,没有require。process.getBuiltinModule('fs')在该作用域可用,因此表达式能同步读到决策文件。
4. 为什么要"崩溃时"而不是"下次启动时"写决策
Loader 并发创建各个行。如果等到下次启动、守护插件自己加载时才写决策,决策可能晚于嫌疑插件那一行的求值,竞态会让嫌疑插件照常加载。
所以决策在崩溃退出的那一刻就写好:那时决策文件已经在磁盘上,下一次启动的第一个被求值的就是它。
5. 嫌疑排序
嫌疑 = profile 里声明的、非官方(@deepseek-ai/* 除外)、且不是本插件自己的依赖。排序依据:
- 日志中的安装/更新事件(权威)
node_modules的 mtime(首次运行时兜底)
排除官方包是为了保证恢复机制永远不会误伤 harness 自身;排除本插件自己,是因为关掉它就等于关掉恢复机制。
安装
在主力 profile 中安装
dsh plugin --profile <profile名> add dsh-recency-guard
安装后重启 DSH。本插件会在启动时自动:
- 在 profile 的
cordis.patch.yml中为每个第三方插件写入守护行 - 开始记录安装 / 更新日志
安装顺序:
dsh-recency-guard必须排在被守护插件之前,这样它才能在启动阶段完成守护行的安装。请在 profile 的package.json中确认:"dsh": { "profile": { "bundles": [ "@deepseek-ai/dsh-base", "dsh-recency-guard", "dsh-你的插件" ] } }
从本地源码安装(开发)
dsh plugin --profile <profile名> add file:/绝对路径/dsh-recency-guard
插件市场
已按 awesome-dsh-plugin 的 contributing.md
准备好投稿材料(docs/market-submission.md):一个文件
data/plugins/xuanyuanchumo__dsh-recency-guard.yml,category: security。
收录后即可在 dsh 的 Plugins → Discover 中搜索到,也可通过
dsh-market 一键安装。
投稿前置条件已逐条核实:
| 条件 | 状态 |
|---|---|
dsh.bundle.patch 声明(被拒首因:只声明 dsh.client 不可安装) | ✅ 两者都有 |
仓库根有 cordis.patch.yml | ✅ |
| 仓库满 1 天 / 真实可用代码 | ✅ |
dsh-plugin topic | ✅(GitHub API 复核) |
官方 @deepseek-ai/* 走 peer 且带显式预发布分支 | ✅ |
npm 已发布(latest = 0.12.1),repository 指回本仓库 | ✅ |
icon 为 GitHub 托管位图 | ✅ assets/icon-256.png |
为什么强调「预发布分支」:contributing.md 专门警告 —— 不带预发布分支的 peer 范围 会静默排除宿主的全部预发布版本,用户
npm install直接撞ERESOLVE。 本插件因此把范围写成显式||分支,并把cordis范围修为>=4.0.5 <5.0.0(宿主实际是 4.1.0,旧值~4.0.5会让安装被兼容闸门整体拒绝)。
配置
在 profile 的 cordis.patch.yml 中:
- id: recency-guard
name: dsh-recency-guard
config:
enabled: true # 总开关;false 时只记录,绝不自动关闭
includeExperimental: true # 是否把 experimental/beta 命名的包算作嫌疑
maxAutoDisable: 2 # 单次恢复最多关闭几个嫌疑插件
autoRestore: true # 稳定后自动恢复非首要嫌疑的配置
confirmRounds: 2 # 概率性证据的行动阈值(唯一嫌疑不受限)
versionedBackups: true # 插件更新时写带版本的时间线备份
backupRetain: 5 # 每插件保留几份时间线备份
backupRoot: '' # 备份自定义目录(留空=profile 内分区目录)
| 配置 | 默认 | 说明 |
|---|---|---|
enabled | true | 总开关。关闭后插件只记录日志,永不自动禁用任何插件 |
includeExperimental | true | 实验性/beta 命名的包是否算嫌疑 |
maxAutoDisable | 2 | 单次恢复最多关闭几个。崩溃次数递增,但封顶于此值 |
autoRestore | true | 稳定启动后自动重新启用非首要嫌疑 |
confirmRounds | 2 | 多嫌疑时同一头号嫌疑连续 N 轮崩溃才自动关闭(唯一嫌疑第 1 轮即行动) |
versionedBackups | true | 插件更新时写「带版本的时间线备份」,可逐版对账 |
backupRetain | 5 | 每个插件保留的备份份数(超出删最旧) |
backupRoot | '' | 备份自定义目录;环境变量 RECENCY_GUARD_BACKUP_ROOT 优先 |
工具:recency_guard
| action | 作用 |
|---|---|
status | 查看崩溃状态、嫌疑排名、被自动关闭的插件、备份列表;输出带「工作区 · 分区」行(多项目时对账归因) |
history | 查看安装 / 更新 / 崩溃 / 关闭 / 恢复的完整日志 |
recover | 立即按"最近更新"执行一次恢复关闭(无需等崩溃) |
restore | 重新启用被自动关闭的非首要嫌疑插件 |
reenable | 手动重新启用指定插件(bundle 参数) |
backups | 列出配置备份 |
archive | 归档当前产物并重置(按最新事实重新开始) |
archives | 列出历史归档,带 manifest 摘要(崩溃次数 / 事件数 / 曾关插件) |
示例:
{ "action": "status" }
数据文件
全部位于 profile 目录下:
<profile>/.recency-guard/
├── current.json # 指针文件:当前分区键(守护行的 disabled 表达式经它搭桥)
├── partitions.json # 分区索引:项目 → 分区键(多项目隔离的登记处)
├── projects/<分区键>/ # 每个项目的分区目录(状态/日志/决策完全独立)
│ ├── journal.jsonl # 只追加的事件日志:install / update / crash-detected / disable / restore …
│ ├── snapshot.json # 上次观测到的 { 插件名: 版本 },用于识别更新
│ ├── state.json # 崩溃计数、被自动关闭的插件、已恢复的插件
│ ├── decision.json # 本轮要拦截的插件(守护行的表达式读它)
│ ├── crashed # 崩溃哨兵:非零退出码时写下
│ ├── observation-*.json # 观测快照(启动事实,量化指标的数据源)
│ ├── archive/ # 本分区的历史归档(搬走+留档)
│ └── backups/ # 关闭前备份的配置(versionedBackups 时带版本)
└── …
恢复流程
第 1 次崩溃
└─ 退出码非 0 → 写哨兵 + 决策(关闭最近更新的 1 个嫌疑)
第 2 次启动
└─ 决策文件已就位 → 嫌疑插件的模块不被加载
├─ 仍然崩溃 → 关闭数 +1(最近更新的 2 个)
└─ 启动成功 → 视为恢复
第 3 次启动(稳定)
└─ 自动重新启用除首要嫌疑外的插件
└─ 首要嫌疑保持关闭,等你确认后用 reenable 手动开启
首要嫌疑(最后被关闭的那个)会一直保持关闭,直到你明确用 reenable 开启它 —— 避免刚恢复就再次崩溃。
安全边界
本插件在设计上刻意保守:
- 只碰第三方插件:
@deepseek-ai/*官方包永远不是嫌疑,harness 核心不会被关掉。 - 不碰自己:
dsh-recency-guard永远不在嫌疑名单里。 - 决策文件缺失 / 损坏时一律放行:表达式在任何异常下都返回
false,插件照常加载。守护机制永远不会把 profile 锁死在插件之外。 - 无法读取时不影响启动:所有文件操作都包在
try/catch中,失败只记录日志。 - 官方包与 harness 组件不受影响:崩溃检测只影响"用户自己加的插件"这一层。
开发与测试
npm install
npm run build # esbuild 双产物:dist/index.js(宿主)+ dist/client.js(客户端)
npm run typecheck # tsc --noEmit
npm test # 分层测试(Core/Host/Client/Contract)
工具行为测试需要 DSH 运行时包(harness 由应用提供,不在 profile 的 node_modules 中):
npm run extract-runtime # 从 DSH 安装目录解出运行时包到 .tmp/runtime
沙箱验证
开发时务必在独立沙箱 profile 中验证,不要动主力 profile:
dsh plugin --profile sb-recency-guard add file:/绝对路径/dsh-recency-guard
dsh --profile sb-recency-guard --help
许可证
MIT