@sidady/dsh-steward 0.9.14 → 0.9.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ACCEPTANCE.md CHANGED
@@ -37,38 +37,94 @@
37
37
  > 还有一条**静态判据**的用法:像「渲染处不许把 ISO 的 T 换成空格」这类规约(v0.9.11),
38
38
  > 与其等它在界面上出错,不如直接扫源码——但静态判据必须**验证过会红**,否则它只是装饰。
39
39
 
40
- > **2026-11-10 五次修订(v0.9.12):基线升到 v0.9.12 / 281 断言,新增 A14 一行。**
41
- > 这轮换的是配色,看似「只是好看」,实际改的是**信息语义**:原来选中态靠变蓝表示,
42
- > 而蓝在同一块面板里还表示 info 级发现——颜色同时承担「选中」和「严重度」两件事,
43
- > 看的人就没法只看颜色判断状态。改成 baseline 的反相选中后,颜色只剩「数据」这一个含义。
44
- > A14 要抓的是回流:`check.mjs` 只抓「硬编码的灰」,抓不到「自造一套蓝」——
45
- > 那只是另一种饱和度,checker 无从判断谁对,所以真正的判据落在了 smoke 的静态扫描里。
40
+ > **2026-11-11 六次修订(v0.9.14):基线升到 v0.9.14 / 292 断言,新增 A15 与 A16 两行。**
41
+ > v0.9.13 与 v0.9.14 修的是同一个真机故障的两层:面板画得出来、每个请求都 404。
42
+ > 第一层(v0.9.13)是**说法**问题——把 404 翻译成「接口没挂上」,但它把原因只归给了热重载。
43
+ > 第二层(v0.9.14)才是**病因**:冷启动时 `webServer` 与 `tools` 的 apply 排在本插件之后,
44
+ > 旧的 `getService(ctx,'webServer')` 一次性探测拿到 null,路由与两个工具都没注册,
45
+ > 而三处注册外面都包着 fail-soft `try/catch`,于是**失败完全无声**。
46
+ > 这两行分别钉住「说得准」和「挂得上」:
47
+ > - **A15**「路由/工具是否注册成功,能不能被看见」——错把「面板画出来了」当成「插件在工作」。
48
+ > - **A16**「冷启动与热重载是不是被当成同一个病因」——症状相同、修法不同,说错会让人白等一次重启。
46
49
  >
47
- ## 基线快照(2026-11-10 15:40 采集(v0.9.12 重采),回看时用来对照)
50
+ > 还有一条更一般的教训值得记在这里:**fail-soft 是正确设计,但它的代价必须是可观测**。
51
+ > 这次不是注册逻辑写错了,是「注册失败」这个事实没有任何出口——面板照常渲染、`ping` 照常回
52
+ > `{"ok":true}`、日志里只有一行 warn。**一个不会喊的守卫,和没有守卫是一样的。**
53
+
54
+ > **2026-11-11 七次修订(v0.9.15):基线升到 v0.9.15 / 313 断言,新增 A17–A21 五行。**
55
+ > 这一轮不是修 bug,是把职能设定里「本该有、却一直缺」的几件补齐:
56
+ > - **P0**:`setSaved` 把 UTC 切片当本地时间贴给东八区(差 8 小时),而静态判据只认单引号写法、
57
+ > 双引号绕过去了;`ui.autoScanMinutes` 是个死设置——界面显示「已保存」、行为永远按硬编码的 10 分钟。
58
+ > - **P1**:只有「静音」没有「这条判错了」入口,两周验收只能人肉回看,管家学不会;
59
+ > 现在 `feedback.json` + plan→confirm→audit 同一条链,判错后降 40 分、不再计直达处置、可撤销、不过期。
60
+ > - **P2**:趋势页只是流水行、只报坏不报好、15 个 finding 里 10 个没有直达(其中一条还指向不存在的动作卡)、
61
+ > 自检结果只在 `ping` 里。现在服务端算好 `trendVerdict` 的人话、好转/连续恶化会主动弹、
62
+ > 面板显示 `wiring`、删掉那条假直达并补上 `backup-freshness → backup`。
63
+ >
64
+ > 五行的共同点:**它们都是「界面上看着有、其实没接上」**——设置一个值不生效、一个按钮点不到东西、
65
+ > 一个时间差 8 小时、一条结论没人说。这类缺陷不会让体检报错,只会让人对面板失去信任。
66
+ > 所以 A17–A21 一律判「接没接上 / 说没说清」,不判「代码写得对不对」。
67
+ >
68
+ > **2026-11-12 八次修订(v0.9.16):基线升到 v0.9.16 / 337 断言,新增 A22 一行。**
69
+ > **2026-11-13 补记(并入未发布的 0.9.16,不升号)**:修掉 Finding 卡「去处置」按钮白底白字(用户 m13209 截图报的纯白矩形)——`Btn` 里 primary 反相前景色被调用方 `tone` 顶掉,而 `C.text` 与 `C.solid` 同源于 `--dsw-alias-label-primary`。改法:删掉误传的 `tone`、`Btn` 在 primary 时强制 `C.on`,并补 4 条防回归静态判据(4/4 反向验证会红)。基线 337 → **341 断言**。这是显示 bug,不碰授权面、不碰数据层,随 0.9.16 一起发。
70
+ > **2026-11-14 补记(并入未发布的 0.9.16,不升号)**:两条用户现场反馈,全只读、不碰授权面——
71
+ > ①(m13463)插件新版本:`analyzeUpgrades()` 从市场缓存派生三档风险 + 利弊/建议中文人话 +
72
+ > `fingerprint` 去重专用提醒;**边界**:无 changelog 不编造具体修复内容、DSH 宿主自身新版本无
73
+ > 可读信号源,只分析市场缓存里的插件新版本。②(m13398)`collectFreeLaneHealth()` 只读
74
+ > `our-free-model/availability.json`,抓「标着可用却探测失败」与全 stale,出 info 级
75
+ > `free-lane-health`;**边界**:单一第三方插件 best-effort,没装属正常,不加 coverage 键
76
+ > (`sourcesTotal` 仍 7)/不 push error,返回体 `freeLane` 让跳过可观测,中期应等宿主把插件级
77
+ > 上游失败汇进 `diagnostics.jsonl`。基线 341 → **370 断言**(本轮 +29,新增 A23/A24 两行与一组断言;7 条关键判据反向验证过会红)。
78
+ > **口径变更,必须如实记**:11-12 曾按覆盖矩阵收口、约定「11-23 回看前不再加功能代码」;
79
+ > 但用户随后重新定义了核心定位(m12723「管家不只是听命令的工具,而是在一定授权内可以安全自主完成职责任务的智能帮手」),
80
+ > 并授权「现在做」(m12744)——那把尺子从「覆盖度」换成了「自主度」,原封板结论因此失效,这次不是违背约定而是**换了一把尺子重排权重**:
81
+ > 不会自己醒来当班的管家不是「差一点贴心」,是不成其为管家。为避免把「扩自主权」与「11-23 干净观察期」混在一起,
82
+ > 这一版**只做 L0 观察档**(周期体检 + 当班台账 + 预备建议,全只读、绝不碰授权面),L1/L2 受托执行仍不实现。
83
+ > - **A22** 专盯这次新增的自主度带来的新误报面:管家自己醒来后有没有制造噪音 / 有没有偷偷动手 / 定时器有没有泄漏。
84
+ >
85
+ > 设计文档另存 Mnemon `42042339-cc9d-4ecb-8d71-dc3ade7dffdd`(与定稿 f7e5a482 并列保留,不覆盖)。
86
+ > **2026-11-14 第二补记(并入未发布的 0.9.16,不升号)**:修右侧栏「智能管家」tab 点开后只剩标题、正文落到宿主兜底「这类内容还没有可用的查看方式。」(用户 m13878 截图)。根因:ui.entry 为 right 时 apply() 先挂 both、异步读到设置再挂 right,第二次 disposeAllNow() 把刚注册的 sidebar.right.pane.tab 正文槽位拆掉;且正文注册嵌在 sidebarRightTabs 的 inject 回调里。改法:右侧整套按 rightMounted 幂等挂卸、正文槽位顶层独立注册、ToastHost 独立 ctx.effect 常驻(旧版被 disposeAllNow 误清,提醒一直没真正生效)。新增 A25 一行与 5 静态 + 4 运行期 harness 断言(运行期把真正 apply() body 抠出来跑一遍 both→right 重挂,数正文槽位存活次数)。基线 370 → **379 断言**。显示 / 生命周期 bug,不碰授权面、不碰数据层。
87
+
88
+ ## 基线快照(2026-11-12 22:33 采集(v0.9.16 重采),回看时用来对照)
48
89
 
49
90
  | 项 | 基线值 |
50
91
  |---|---|
51
- | bundle 版本 | `@sidady/dsh-steward` v0.9.12,installed + enabled(ping 与 package.json 一致) |
52
- | 本地验证 | 9 个 JS `node --check` 全过,`smoke.mjs` **281 passed / 0 failed** |
53
- | 体检结论 | `level=warning`,`counts={critical:0, warning:2, info:10, muted:0}`,`errors=[]`,共 12 条 findings |
54
- | crash 日志 | **4 份**:`crash-2026-11-07T02-53-24-868Z-web-boot.log`(10199 B) / `crash-2026-11-08T09-10-33-857Z-web-boot.log`(61087 B) / `crash-2026-11-09T23-27-20-521Z-web-boot.log`(31821 B) / `crash-2026-11-09T23-28-38-158Z-web-boot.log`(31821 B) |
55
- | audit.jsonl | 40 行(v0.9.8 起有 2000 行上限;`ok:false` 行仍在,说明拒绝路径活着) |
56
- | trend.json | **4 个点**,最新 `2026-11-10T03:50:33.499Z`;`cFreeBytes` 87.3 GB(更早一点 89.4 GB,不是恒定值) |
57
- | acked.json | 空数组 `[]`(v0.7.7 起 ack 带 `expiresAt`) |
58
- | 工作区 | 7 个 / 合计约 **192 MB**(采集时 201341401 B;这个数每次都在变,回看时只看「7 个」与增长判定,不要咬死体积);判据是**增长**不是绝对体积,本轮「与上次相比无显著增长」 |
59
- | 插件更新 | 32 个已装包比对,13 个有新版本 + 3 个**已装比缓存新**(缓存偏旧,不算升级),0 个无法比较 |
92
+ | bundle 版本 | `@sidady/dsh-steward` v0.9.16,installed + enabled;ping 回报 `version:"0.9.16"` 且 `wiring.route:"registered"`(三条 via 全 inject) |
93
+ | npm 上的版本 | **`0.9.15`**(`dist-tags.latest`)——**本机 0.9.16 尚未 publish**:m12744 只授权「现在做」L0 当班环,沿用「发布需用户逐次说要发」的口径,不主动推远端。回看时若仍不等,先确认是「故意未发」还是漏发 |
94
+ | GitHub | `stallenlu/dsh-steward` public,远端 `main` 最新提交 `3eca47b`(docs 同步到 0.9.15);本地 v0.9.16 系列已多次 commit(最新 HEAD 用 `git log -1` 查,回看当天以实测为准)、**均未 push / publish**(沿用「发布需用户逐次说要发」授权口径) |
95
+ | 目录条目 | awesome-dsh-plugin PR **#14042 已 MERGED**(2026-11-11T17:47:32Z,mergeCommit `07de6ca0…`,`Submission gate` + `check` 全 pass);main 分支已确认有 `data/plugins/stallenlu__dsh-steward.yml`(category dev,中英 description 齐全)。注意:`curl plugins.json` 曾被截断成假阴性,核对以 `gh api` 查 main 分支为准 |
96
+ | 本地验证 | 9 个 JS `node --check` 全过,`smoke.mjs` **379 passed / 0 failed**。演进:v0.9.15 基线 313 → L0 当班环 +24(含 1 条运行期求值判据、定时器生命周期判据)→ 337 → 白色按钮回归 +4 → 341 → 本轮(升级利弊分析 + 免费通道运行期体检)**+29 → 370** → 右侧栏 tab 正文修复 **+9 → 379**。关键判据反向验证过会红:L0 定时器 5 条、白按钮 4 条、升级利弊/免费通道 7 条、右侧栏 tab 正文 9 条 |
97
+ | 体检结论 | `level=warning`,`counts={critical:0, warning:2, info:11, muted:0}`,`errors=[]`,共 **13** 条 findings(比 v0.9.15 多一条 `config-drift` info) |
98
+ | `trendVerdict` | `{state:"stable", worseningStreak:0, text:"与上一次体检相比没有明显变化。"}` |
99
+ | **`duty`(v0.9.16 新增)** | report / ping / audit 三处返回体都带:`mode:"L0"`、`observeOnly:true`、`intervalMinutes:10`、`running:false`、`log`(当班台账)、`suggestions`(预备建议) |
100
+ | `duty.jsonl` | **1 行**(boot 那班):`trigger:"boot"`、`state:"observe-only"`、`executed:0`、seen「看了 13 条发现…其中 13 条仍在报」、6 条预备建议。L0 恒不调 plan/execute |
101
+ | `autoScanMinutes` / `wiring` | `autoScanMinutes: 10`、`wiring.route:"registered"`;面板生成时间行下显示「接口已挂上」+「当班 · … L0 观察档」 |
102
+ | 定时器生命周期 | L0 `setInterval` 挂在 `ctx.effect(..., 'dsh-steward: L0 duty')` 上,dispose 里 `clearInterval(dutyTimer)`——防 HMR 热重载后旧 fiber 的定时器不停 |
103
+ | crash 日志 | **仍 4 份**,最晚 `crash-2026-11-09T23-28-38-158Z-web-boot.log`;v0.9.13 起**零新增** |
104
+ | audit.jsonl | **42 行**(基线 40 → 42,新增 2 行是 11-11 20:26 用户手动 `clean` + `knowledge`;**本次 L0 当班后 0 行新增**,实测 `14:30Z` 之后无 plan/execute) |
105
+ | trend.json | **5 个点** |
106
+ | acked.json / feedback.json | `[]` / **尚不存在**(没有用户点过「这条判错了」就不会建文件) |
107
+ | 工作区 | 7 个(回看只比「7 个」与增长判定,不咬死体积) |
108
+ | 插件更新 | 比对 11 个已装包(回看以当天为准) |
60
109
  | 数据源覆盖 | `coverage` **7/7** = `data`,`unavailable=[]`,`errors=[]` |
61
- | 配置指纹 | `cordis.patch.yml` `1b85f7154b1261fd`(60159 B / 1994 行) / `package.json` `dc6c2f83d584c1a0`(2784 B / 87 行)。version 升会改 `package.json` 指纹,属正常;**块数变少**才报警 |
62
- | 设置 | `settings.json` 存在,`savedAt=2026-11-10T03:58:45.090Z`;`ttl.plugin-updates=14`(已回默认),`ui.entry=right`,`ui.toast=critical`,`ui.toastSeconds=12`,`model.probeOnReport=cached`,`perProvider=2`,`maxTotal=12`,`growthWarnMb=20` |
63
- | 客户端包 | 38 个声明了 `dsh.client`,checked 38 / unreadable 0 / withoutId 0;`inject` 只声明 `@deepseek-ai/dsh-client-ui-slots`(locale 已于 v0.9.10 去掉虚挂) |
64
- | 默认模型 | 12 个哨兵探针:8 ok / 0 dead / 4 busy(busy = 「没有返回内容」,不算坏);默认模型可用 4504 ms |
65
- | 加密备份 | 2 份,最新 `dsh-config-2026-11-09-1749ba.zip`(95.7 MB)1 天前;另一份 `dsh-config-20261108-180014-ab10fd.zip`(65.6 MB) |
110
+ | 配置指纹 | 回看时重取;`patch` 体量会变,只比 `- id:` 块数有没有减少,不要比字节 |
111
+ | 设置 | `savedAt 2026-11-11T14:00:45.505Z`;`ui.autoScanMinutes=10` 现在**一处管两处**(面板缓存刷新 + 后台当班周期),`model.probeOnReport:"cached"` |
112
+ | 客户端包 | 38 个声明 `dsh.client`,checked 38 / unreadable 0 / withoutId 0 |
113
+ | 默认模型 | `model-health` 可用(`probeOnReport:cached`);注:一次 report 曾 30 s 超时,是撞上模型探活,重试 0.22 s / 200,不是路由故障 |
114
+ | 兼容 / 加密备份 | 声明门槛的包适配宿主 `0.2.2-rc.1`;加密备份 2 份,最新 `dsh-config-2026-11-09-1749ba.zip` |
66
115
 
67
116
  > **版本号与基线数字的关系**:本表的数字是「当天真实跑出来的」,不是「必须永远等于这个」;
68
117
  > 回看时若断言数已经涨了(比如又加了几条),那是覆盖变多,记一笔新数字即可,不算退化。
69
- > 真正要盯的是:crash 日志有没有新增、`errors` 是否仍为空、`coverage` 是否仍 7/7。
70
- > **回看前先做一件事**:把 `package.json` 的 version 与 ping 的 version 对一下。两者不等说明
71
- > 磁盘改了但宿主没重载——此时面板看到的是旧代码,任何 A 项的结论都不作数。
118
+ > 真正要盯的是:crash 日志有没有新增、`errors` 是否仍为空、`coverage` 是否仍 7/7、
119
+ > `ping.wiring.route` 是否仍是 `registered`、`trendVerdict` / `autoScanMinutes` / `wiring` / `duty` 四个返回体字段是否还在(v0.9.16 起 `duty` 是第四个)。
120
+ > **回看前先做一件事**:把 `package.json` 的 version、npm 上的 latest、ping 的 version 三个对一下。
121
+ > 三者不等**通常**说明「磁盘改了 / 发了 / 宿主没重载」里有一环断了——此时面板看到的是旧代码,任何 A 项的结论都不作数;
122
+ > 但也有例外(见下:本次是「故意未发」),先分清性质再下结论。
123
+ > **本次重采时三方不等,是已知且故意的状态**:本地 0.9.16 / ping 0.9.16 / npm `dist-tags.latest` **0.9.15** / GitHub `3eca47b`。
124
+ > 原因不是「磁盘改了 / 宿主没重载」里哪一环断了,而是 **m12744 只授权写代码、没授权发版**(沿用逐次授权)。
125
+ > 回看时若 11-23 当天已经是 0.9.16 且三方一致,记一笔新数字即可;若仍不等,先问「是故意未发还是漏发」,别直接判 ❌。
126
+ > v0.9.15 发布那次从 registry 反装 `npm pack @sidady/dsh-steward@0.9.15` 解包核对:17 文件、`lib/` 8 个 JS、8 个相对 import 全命中、
127
+ > 在 tarball 里跑 `node smoke.mjs` 仍 **313 passed / 0 failed**(那一版的基线)。
72
128
 
73
129
  ## 检查表
74
130
 
@@ -100,6 +156,17 @@
100
156
  | A12 | 模型健康是否把「忙」当「坏」 | 报告 `model-health` 的 `data.state` + `deadModels` | 只把 404/422(上游明确拒绝路由)与「连续两轮拿不到内容」算坏;429/5xx/超时/空回复一律记 busy 且不升 warning、不进 `deadModels`。若某个免费模型只是一时冷却却被写成「已不存在」→ ❌(这是最容易被免费额度误伤的一项) |
101
157
  | A13 | 「多久以前」与「比启动早多久」有没有被混成一个数 | 报告 `boot-crash-latest` 的 `data.ageHours` / `bootLagHours` / `ageText` / `bootLagText` | 四个数各司其职:`ageHours` 是**距现在**多久、`bootLagHours` 是**距本次启动**多久,两者可以相差很大(真机 7.3 小时 vs 0.01 小时)。标题/正文里出现「约 0 小时前」而事件实际已过去数小时 → ❌(v0.9.10 前就是这样)。同类要一并查:列表截断有没有说出「另有 N 个」(工作区 / 备份 / 采集告警,v0.9.10) |
102
158
  | A14 | 配色是不是又从 baseline 溜回自造色 | `node check.mjs ../dsh-steward/lib/client.js` + `smoke.mjs` 的配色静态判据 | 两个都必过:checker 报 `no hardcoded neutral` 不给 error 只说明「没有硬编码的灰」,**抓不到「自造一套蓝」**(那是另一种饱和度,不是灰);所以真正的判据是 smoke 那 7 条 —— baseline 块外不许出现非中性的 `rgba()/rgb()`、不许出现 `var(--dsw` 之外的裸 hex、选中态必须用 `C.solid`/`C.on` 反相。若面板上又出现 `rgba(90,159,212,…)` 这类蓝、或选中态靠变蓝表示 → ❌(v0.9.12 前就是这样,12 处) |
159
+ | A15 | 路由与工具是不是「注册了但没说」 | `ping` 的 `wiring.route` / `wiring.via` + 面板能不能出报告 | 必须 `wiring.route === "registered"`,且 `wiring.via` 里 `systemPrompt` / `webServer` / `tools` 三条都是 `inject`。若面板画得出来但每个请求 404、`dsh_steward_*` 工具又「未知工具」→ ❌(v0.9.14 前就是这样:冷启动竞态下三处注册全被静默跳过,而 `ping` 还回 `{"ok":true}`) |
160
+ | A16 | 冷启动与热重载有没有被当成同一个病因 | 面板 404 时的提示文案 + 复现条件 | 两种情境都要能讲对:**热重载**=改插件文件后旧路由被销毁;**冷启动**=`webServer` 比插件晚 apply。文案里只写其中一种 → ❌(v0.9.13 只写热重载,于是用户重启后看到同一句提示,等于被指错了方向)。判据不看措辞好不好听,看它有没有把「我刚重启过」这个人排除掉 |
161
+ | A17 | 本地时间有没有被 UTC 冒充(两种引号都查) | `smoke.mjs` 的 v0.9.11 / v0.9.15 静态判据 + 面板「设置」的保存时间 | 判据必须同时认单引号与双引号的 `.replace("T"," ")` 写法,且 `setSaved` 走 `localStamp()`。若把保存时间显示成 UTC(比实际差 8 小时)、或改回 `.replace("T"," ")` 后静态判据仍全绿 → ❌(v0.9.15 前双引号绕过判据) |
162
+ | A18 | 设置项是不是真的接上了(**抓死设置**) | report 返回体 `autoScanMinutes` + 客户端 `useEffect` 阈值 | 改 `ui.autoScanMinutes` 后:①report 返回体里的值跟着变;②切回面板时的刷新阈值跟着变(不许再出现硬编码 `10 * 60 * 1000`)。若界面显示「已保存」而行为不变 → ❌(v0.9.15 前就是这样) |
163
+ | A19 | 误报反馈闭环是否成立 | `feedback.json` + 面板「这条判错了 / 撤销判错」 + `audit.jsonl` | 四件都要真:①点「判错」走 plan→confirm→audit 同一条链、有 hash;②该 finding 打上 `falsePositive`、score 降 40、不再计直达处置、证据仍保留;③「撤销」能恢复;④条目**不过期**(静音会过期,判错不该悄悄重新武装)。任何一件没接上 → ❌ |
164
+ | A20 | 趋势结论与好转通知是不是服务端算的人话 | report `trendVerdict` + 面板「审计趋势」结论行 + ToastHost | `trendVerdict` 必须有 `state`(worsening / recovered / stable)、`worseningStreak`、中文 `text`,由服务端 `summarizeTrend()` 算好、客户端只渲染;好转或连续 ≥2 次变差要主动弹一条(用 `generatedAt` 去重,不许反复弹)。若趋势页仍只是流水行、或只报坏不报好 → ❌ |
165
+ | A21 | 直达按钮不造假 + 自检结果用户可见 | `ACTION_FOR` + 动作卡清单 + 面板生成时间行 | ①`ACTION_FOR` 里每一条映射都要有对应的动作卡(v0.9.15 删掉了指向不存在卡片的 `model-health → model-default`,补上 `backup-freshness → backup`);②`wiring.route` 要显示在面板上,没挂上时给红字与恢复办法,不能只藏在 `ping` 里。若点了「去看看」跳到一个不存在的动作、或路由挂不上却面板无提示 → ❌ |
166
+ | A22 | **L0 当班环有没有守住「只观察、不动作」**(v0.9.16 新增自主度带来的新误报面) | `duty.jsonl` + `duty` 返回体 + `audit.jsonl` 增量 + 定时器生命周期 | 五件都满足才 ✅:①每班 `state:"observe-only"`、`executed:0`,`audit.jsonl` 在当班后**零新增** plan/execute;②`duty.jsonl` 行数随时间单调增长、每班有中文 `seen`/`action`/`suggestions`(预备建议 ≠ 已执行);③周期跟随 `ui.autoScanMinutes`,改设置后下一班按新周期重排;④`ping`/`report`/`audit` 三处都能查到 `duty`;⑤定时器挂在 `ctx.effect` 上且 dispose 清掉——**热重载/停用插件后旧 fiber 不许留下一个永不停止的当班环**。任一不满足 → ❌(尤其①,管家自己醒来后偷偷动手是比误报严重得多的越权) |
167
+ | A23 | 插件新版本的利弊/风险/建议是不是服务端算的人话、且不编造 | report `updates.advice` + `plugin-updates` finding + ToastHost 专用 version toast | 四件都满足才 ✅:①`advice` 有 `outdatedCount`/`rows`/`fingerprint`/`text`/`recommendation`/`toastTitle`/`toastBody`,三档风险(宿主门槛不符 high / 预览版或跨 ≥5 版本 medium / 常规小步 low)各自能触发;②`text` 明写「优点 / 风险 / 不编造具体修复内容」,不谎称读了 changelog;③`recommendation` 明写「管家只读分析、不代升级」;④toast 用 `fingerprint` 去重、不改 `toastWorthy`(info 级 finding 仍不全局弹)。若把「有新版」直接当成「该升」、或编造修复内容、或每次体检反复弹同一条 → ❌ |
168
+ | A24 | 免费通道运行期故障是不是被「只读、不越界」地接住 | `$DSH_HOME/our-free-model/availability.json` + report `freeLane` + `free-lane-health` finding | 四件都满足才 ✅:①只读**一家**第三方插件私有台账,抓「标着 available 却记着上游失败 detail」与全 stale;②缺台账/解析失败时**静默跳过**——不加 coverage 键(`sourcesTotal` 仍 7)、不 push 采集 error、不加 finding,但返回体 `freeLane` 让跳过可观测;③finding 是 info 级、不进 `ACTIONABLE_IDS`、不动别的插件文件;④明写边界(中期应等宿主把插件级上游失败汇进 `diagnostics.jsonl`)。若把「没装该插件」报成体检退化、或据此自动改配置/停用插件 → ❌ |
169
+ | A25 | 右侧栏「智能管家」tab 的正文有没有在设置切换后仍挂得住 | 右侧栏 tab 正文 + settings `ui.entry` + ToastHost + `smoke.mjs` 运行期 harness | 五件都满足才 ✅:①`ui.entry=right` 时初次挂 both、异步读到设置再挂 right,**正文槽位不许被二次重挂 dispose**——点开 tab 显示面板本体而不是宿主兜底「这类内容还没有可用的查看方式。」;②正文槽位 `sidebar.right.pane.tab`(key=PANEL_ID)在 apply() 顶层独立注册,不嵌在 `sidebarRightTabs` 的 inject 回调里;③右侧整套用 `rightMounted` 幂等门,wantRight 不变就不拆不挂;④ToastHost 用独立 `ctx.effect` 常驻、不进任何拆装列表(新版提醒会真正弹出);⑤`ui.entry` 三值(both/left/right)语义不变,right 时左侧入口与 main 全拆。运行期 harness 断言「正文恰好注册 1 次且两次重挂后仍存活、ToastHost 仍存活、right 时 leftLive=0」。若切到 right 后面板变空、或静态判据全绿但运行期正文已丢 → ❌ |
103
170
 
104
171
  ### B. 可回溯(每次动作都能查账)
105
172
 
@@ -114,7 +181,7 @@
114
181
  | # | 检查项 | 数据源 | 打勾条件 |
115
182
  |---|---|---|---|
116
183
  | C1 | 没有代跑提权命令 | 会话记录 + `audit.jsonl` | 全期零次 `DISM` / `diskpart compact` / 卸载程序被 Agent 执行(只呈现过建议)→ ✅ |
117
- | C2 | 没有「未经授权就动手」 | `audit.jsonl` 每条 `kind` ∈ {clean,backup,disable,enable,knowledge,ack,unack,model-default} 且都有 `hash` | 不存在 hash 为空却 `ok:true` 的执行行 → ✅ |
184
+ | C2 | 没有「未经授权就动手」 | `audit.jsonl` 每条 `kind` ∈ {clean,backup,disable,enable,knowledge,ack,unack,model-default,feedback,unfeedback} 且都有 `hash` | 不存在 hash 为空却 `ok:true` 的执行行 → ✅ |
118
185
  | C3 | 口令红线:11-16 周提醒只提醒不代做 | 会话记录(2026-11-16 10:00 那次触发) | Agent 没有索要口令、没有代跑、没有提议代跑 → ✅;任何一项违反 → ❌ 并回写 USER.md |
119
186
  | C4 | 坏请求不会触发写动作 | 对路由发畸形 body | `{oops` / `{}` / 超 16 KB / JSON 数组 必须全部 400,且**不得**因此跑一次体检(v0.9.8 前会把空 action 当 report)→ ✅ |
120
187
 
@@ -122,7 +189,7 @@
122
189
 
123
190
  | # | 检查项 | 数据源 | 打勾条件 |
124
191
  |---|---|---|---|
125
- | D1 | 依赖缺失只降级不崩溃 | `node smoke.mjs` 的 fail-soft 断言 | 回看当天跑一次 smoke,仍全绿(基线 281 passed / 0 failed)→ ✅(若期间又加断言,以当天全绿为准并记下新数字) |
192
+ | D1 | 依赖缺失只降级不崩溃 | `node smoke.mjs` 的 fail-soft 断言 | 回看当天跑一次 smoke,仍全绿(基线 379 passed / 0 failed)→ ✅(若期间又加断言,以当天全绿为准并记下新数字) |
126
193
  | D2 | 热挂载后落库一致 | profile `package.json` + `/api/dsh-steward/` 200 + ping 版本 | grep `dsh-steward`=2、路由 200、ping 的 `version` = `package.json` 的 `version` 三者一致 → ✅。**注意**:`remove_bundle` 会因 `bundle-in-use` 失败,实际路径是 `set_bundle enabled:false → true`;ping 版本正确也**不**等于新代码已加载(读的是磁盘 package.json),要看新文案是否出现 |
127
194
  | D3 | 可选数据源缺失不拖垮报告 | 报告 `status` 与 `coverage` | 缺一个可选源时 `status` 仍是 `ok`、只在 `coverage` 里记 `unavailable` → ✅ |
128
195
  | D4 | 慢请求不会无限等待 | 面板发一次 `model-scan` | 服务端 3 路并发(12 探针最坏 ≈180 s),前端 `report`/`model-scan` 预算 240 s、普通动作 30 s。若面板转圈超过 5 分钟仍无任何提示 → ❌ |
@@ -158,6 +225,27 @@ curl -s -X POST http://127.0.0.1:<当前端口>/api/dsh-steward/ -H 'content-typ
158
225
 
159
226
  # 5) 坏请求不该跑体检(C4)
160
227
  curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:<当前端口>/api/dsh-steward/ -H 'content-type: application/json' -d '{oops'
228
+
229
+ # 6) 路由到底挂上没有(A15)——比「面板能不能画出来」可靠得多
230
+ # 期望:version 三方一致,且 wiring.route === registered
231
+ # wiring.via 里 systemPrompt / webServer / tools 三条都是 inject
232
+ curl -s -o "$TMP/ping.json" -X POST http://127.0.0.1:<当前端口>/api/dsh-steward/ -H 'content-type: application/json' -d '{"action":"ping"}'
233
+ cat "$TMP/ping.json"
234
+
235
+ # 7) v0.9.15 新增字段(A18–A21)——确认「接上了」而不只是「写好了」
236
+ curl -s -o "$TMP/rep.json" -X POST http://127.0.0.1:<当前端口>/api/dsh-steward/ -H 'content-type: application/json' -d '{"action":"report"}'
237
+ node -e "const r=require(process.argv[1]); console.log('autoScanMinutes:',r.autoScanMinutes,'wiring:',r.wiring.route,'trendVerdict:',r.report.trendVerdict.state,r.report.trendVerdict.text)" "$TMP/rep.json"
238
+ # 误报反馈:点一次「这条判错了」后应出现 feedback.json,且该 finding 带 falsePositive、score 降 40
239
+ cat ~/.dsh/dsh-steward/feedback.json 2>/dev/null || echo '(还没有判错记录)'
240
+
241
+ # 8) v0.9.16 L0 当班台账(A22)——确认「只观察不动作」
242
+ wc -l ~/.dsh/dsh-steward/duty.jsonl
243
+ node -e "const fs=require('fs');const L=fs.readFileSync(process.argv[1],'utf8').split('\\n').filter(Boolean).map(JSON.parse);console.log('班次',L.length,'最新',JSON.stringify(L[L.length-1]))" ~/.dsh/dsh-steward/duty.jsonl
244
+ # 关键:把 duty.jsonl 里最新一班的 at 与 audit.jsonl 最后几行比对,
245
+ # 当班之后若冒出新的 execute 行 = L0 越权,直接判 ❌
246
+ tail -5 ~/.dsh/dsh-steward/audit.jsonl
247
+ # 定时器生命周期(静态):smoke 里那条 ctx.effect/clearInterval 判据必须全绿且反向验证过会红
248
+ grep -n "dsh-steward: L0 duty" dsh-steward/lib/index.js
161
249
  ```
162
250
 
163
251
  > 端口每轮启动都会变,用运行时上下文里的 GUI URL,不要沿用旧端口。
@@ -194,8 +282,30 @@ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:<当前端口>
194
282
  - **依赖声明必须与代码一致**:`dsh.client.inject` 里声明了却零调用的包就是虚挂,它今天不报错、将来宿主改注入方式时会以「找不到依赖」的形式炸掉(v0.9.10 去掉了 locale)。判定方式是 grep 实际调用,不是看别的插件怎么写。
195
283
  - **静态判据也必须验证会红**:像「渲染处不许把 ISO 的 T 换成空格」这种扫源码的断言,写完要故意改回旧写法确认它 FAIL,否则它只是装饰(v0.9.11 做了这次反向验证)。这条对所有 `smoke.mjs` 里的规约型断言都适用。
196
284
  - **测试全绿不等于包能装**:`files` 白名单漏一个文件时,从源码树跑的断言全绿、装包的人一启动就 `ERR_MODULE_NOT_FOUND` 并拖垮整个 Web UI(v0.9.9 的 `lib/models.js`)。发布前必须跑一次 `npm pack` 并解包核对相对 import(D5)。
285
+ - **fail-soft 的代价必须是可观测**:
286
+ 注册失败、依赖缺失这类「不该拖垮启动」的降级是正确的,但如果它连一条能查到的状态都不留,
287
+ 就等于没有守卫。v0.9.14 之前三处注册全包在 `try/catch` 里只 warn 一行,
288
+ 结果面板照常渲染、`ping` 照常回 `{ok:true}`、路由却从未注册——
289
+ **一个不会喊的守卫和没有守卫是一样的**。
290
+ 判据:任何 fail-soft 分支都要能把「我降级了」暴露到 ping / 报告 / 日志三处里至少一处(A15)。
197
291
  - **info 不算打扰,warning 才算**:判误报时先看 severity。info 天天在、你从不理它,是噪音的候选而非误报;warning 天天在、你无事可做,就是误报。
198
292
  - 出现 ❌ 时不改判定标准,只记原因;判定标准本身要改,必须写下「为什么原标准不可执行」,不许事后放宽。
293
+ - **「界面上有」不等于「接上了」**:一个设置项显示「已保存」但行为按硬编码走、一个按钮指向不存在的动作卡、
294
+ 一个时间差 8 小时、一条趋势结论没人说——这些都不让体检报错,只让人对面板失去信任。
295
+ A17–A21 一律判「接没接上 / 说没说清」,不判「代码写得对不对」。
296
+ - **静态判据本身也要防绕过**:v0.9.11 那条「不许把 ISO 的 T 换成空格」只认单引号,v0.9.15 前双引号写法
297
+ 就绕过去了。规约型断言写完要问一句「换一种引号 / 换一种写法还能不能抓到」,并反向验证会红。
298
+ - **判错不是静音**:静音是「我知道了先不修」(会过期、会回来),判错是「这条结论本身错了」
299
+ (不过期、降权、但证据保留)。两者混成一个入口,两周验收就学不到东西(A19)。
300
+ - **好转也要说**:只报坏不报好,用户永远不知道管家在变好还是在变差;但好转通知必须去重,
301
+ 否则每次体检都弹一次,本身就成了新的噪音(A20)。
302
+ - **自主度必须可撤销、可回溯、可观测**:管家自己醒来当班,带来的是一整类**新的**误报面——
303
+ 不是「报得不准」而是「它会不会偷偷动手 / 会不会每热重载一次就多一个永不停止的定时器」。
304
+ 所以 L0 的边界要写成可核对的事实:`executed:0`、`audit.jsonl` 零增量、`duty.jsonl` 有中文台账、
305
+ 定时器归 `ctx.effect` 管(A22)。**「没写 execute」不算证据,「当班后审计零新增」才算**。
306
+ - **换尺子要记口径变更**:11-12 的封板约定(不再加功能代码)被用户重新定义核心定位后推翻,
307
+ 这不是违背约定,是评判轴从「覆盖度」换成「自主度」。回看时若见到「说好不加却加了」,
308
+ 先查有没有对应的用户授权与本文档的修订记录,别直接当成退化。
199
309
 
200
310
  ## 回看结论栏(11-23 填)
201
311
 
package/README.md CHANGED
@@ -1,6 +1,118 @@
1
1
  # dsh-steward — 智能管家(体检 + 授权执行)插件
2
2
 
3
- **状态:v0.9.14 已安装并热挂载(体检 + 授权执行 + 对话入口 + 设置项)**
3
+ **状态:v0.9.16 已安装并热挂载(体检 + 授权执行 + 对话入口 + 设置项 + L0 当班)**
4
+
5
+ ## v0.9.16(L0 当班环:管家会自己醒来,但只观察、只把建议备好)
6
+
7
+ 「核心定位再思考」之后落地的第一档,也是**只做 L0** 的一版。此前管家只在开机 45 秒后
8
+ 体检一次、或你打开面板时才动——它不像「管家」,更像一件等你用的工具。v0.9.16 补的是
9
+ 「当班」这件事本身:不开面板,它也按设置周期自己醒来体检一遍。
10
+
11
+ - **周期当班**:`ui.autoScanMinutes`(原来只管面板缓存刷新,现在「一个数字管两处」)
12
+ 同时驱动后台 `setInterval`。改设置后下一班自动按新周期重排,不用重启。带重入保护
13
+ (上一班没结束不开新班)与 1 秒去抖,不会因事件抖动连开好几班。
14
+ - **当班台账 `duty.jsonl`**:每班一条,写清「看了什么 / 判断 / 做了没做 / 为什么没做」。
15
+ 人话由服务端 `dutyBriefing()` 算好,客户端只渲染(沿用 USER.md 的口径)。
16
+ - **预备建议**:把仍在报、且没被静音/判错的发现的建议预先整理成清单,等你来动作中心
17
+ 确认;**预备 ≠ 执行**。
18
+ - **硬边界(L0 观察档)**:`runDuty()` 全程只读,一次都不调用 `plan`/`execute`;
19
+ 报告 / ping / audit 返回体带 `duty`,面板「体检总览」显示最近一班状态,「审计趋势」
20
+ 显示当班台账。加密备份口令、启动/boot/`cordis.patch.yml`、停用启用插件、改默认模型
21
+ 等一律不进任何自动层(见「明确不做」)。
22
+ - **档位路线**:L0 观察(本版,默认开)→ L1 预备(主动把计划推到面前,一次确认,默认关)
23
+ → L2 受托(白名单内可逆动作按策略自动执行并回报,默认关、须逐项开)。L1/L2 **未实现**,
24
+ 设计见 Mnemon 文档 `42042339-cc9d-4ecb-8d71-dc3ade7dffdd`。扩大自主权要等 11-23 两周
25
+ 无误报回看之后,本版不碰授权面。
26
+
27
+ 静态判据(smoke)钉住:周期定时器存在且带重入/去抖、当班全程不触碰 plan/execute、
28
+ `dutyBriefing` 是纯函数且排除静音/判错项、三处返回体都带 duty、面板显示当班状态与台账。
29
+
30
+ ## v0.9.16 补记(并入未发布版,不升号):升级利弊分析 + 免费通道运行期体检
31
+
32
+ 两条都来自用户现场反馈,**全只读、不碰授权面**:
33
+
34
+ - **插件新版本,管家会主动提醒并给利弊与建议**(m13463)。`collectUpdates` 现在多收一个
35
+ `hostVersion`(宿主版本取自崩溃日志的 `meta.app`),`analyzeUpgrades()` 纯函数从市场缓存的
36
+ `versions[]` / `engines.dsh` 派生三档风险:宿主门槛不符(high,升了可能直接起不来)、预览版 /
37
+ 跨 ≥5 个版本(medium)、常规小步(low);把「优点 / 风险 / 分级 / 处理建议」写成中文人话塞进
38
+ `plugin-updates` 的 detail 与 recommendation,并在面板用 `updates.advice.fingerprint` 弹一条
39
+ **去重**的专用提醒(不改 `toastWorthy`,info 级 finding 仍不全局放开)。
40
+ **边界**:市场缓存里没有 changelog,优点只写「跟随上游 / 依赖对齐 / 减少碎片」这类可证的,
41
+ 不编造具体修复内容;DSH 宿主自身没有可读的新版本信号源,本版只分析市场缓存里的**插件**
42
+ 新版本,不假装能测宿主自更新。
43
+ - **our-free-model 的运行期故障,管家能看见了**(m13398)。`collectFreeLaneHealth()` 只读
44
+ `$DSH_HOME/our-free-model/availability.json`,专抓「标着 available、最近一次探测却失败」的
45
+ 自相矛盾与「探测结果全部 stale」,出一条 info 级 `free-lane-health`。
46
+ **边界**:这是**单一第三方插件**的 best-effort 加分项,没装它属正常,因此**不加 coverage 键、
47
+ 不 push 采集错误**(不给所有没装它的用户造永久误报),返回体用 `freeLane` 让「跳过」在 data
48
+ 层可观测;不读别家私有台账、不碰别的插件代码。中期更稳的做法是等宿主把插件级上游失败汇进
49
+ `bot/logs/diagnostics.jsonl`,管家接一层而非接 N 家。
50
+
51
+ 静态判据(smoke)钉住:三档风险各自触发、`fingerprint` 确定性且顺序无关、利弊文案不编造
52
+ changelog、`collectUpdates` 收 `hostVersion` 且返回 `advice`、免费通道缺台账时静默跳过且
53
+ `sourcesTotal` 仍 7、客户端 version toast 有去重指纹。
54
+
55
+ ## v0.9.16 补记(并入未发布版,不升号):右侧栏 tab 正文「这类内容还没有可用的查看方式」修复
56
+
57
+ 用户 (m13878) 截图报:右侧栏「智能管家」标签点开后,正文区显示宿主的蓝色兜底文字
58
+ 「这类内容还没有可用的查看方式。」——芯片标题还在、面板本体没了。
59
+
60
+ - **根因不是插件没挂载,是二次重挂把正文拆了**。旧 `apply()` 把右侧 tab 的「类型注册 +
61
+ 正文槽位 + 浮层提醒」全塞进 `mountRight()`,而 `mountFor(entry)` 每次都先 `disposeAllNow()`
62
+ 全拆一遍。`ui.entry='right'` 时:先挂 `both`(正文注册好)→ 异步读到设置再挂 `right`
63
+ → 这第二次重挂把刚注册好的 `sidebar.right.pane.tab` 正文 dispose 掉;更糟的是正文还嵌在
64
+ `ctx.inject(['sidebarRightTabs'])` 的回调里,服务重载时机一错也丢。宿主 `TabSlot` 用
65
+ `entryKey = definition?.id ?? tab.kind` 找 keyed 槽位,找不到就渲染 `tab.unavailable`
66
+ 兜底;芯片标题是打开时捕获的,所以「标题在、正文空」。
67
+ - **修法三件**:①右侧整套按 `wantRight` **幂等**挂/卸(`rightMounted` 门,wantRight 不变就不拆
68
+ 不挂),正文槽位 `ctx.slots.inject('sidebar.right.pane.tab', …)` 提到 `apply()` 顶层直接注册,
69
+ 不再依赖 `sidebarRightTabs` 服务的回调——对照 dsh-config-manager / dsh-better-sidebar /
70
+ dsh-univer-office 都是这么做的;②浮层 ToastHost 用独立 `ctx.effect` 常驻,不进任何拆装列表
71
+ (旧版被 `disposeAllNow()` 误清,提醒一直没真正生效);③left/main 单独幂等挂/卸,`ui.entry`
72
+ 三值语义不变。
73
+ - **验证**:smoke 新增 5 条静态判据 + 4 条运行期 harness 断言(把真正的 `apply()` body 抠出来,
74
+ 用假 Cordis ctx 跑一遍「both → settings=right」重挂,数正文槽位到底被注册/拆了几次)。
75
+ 静态判据看不出的二次重挂竞态,运行期 harness 能看出来。
76
+
77
+ 这是显示 / 生命周期 bug,不碰授权面、不碰数据层、不新增检测项,随 0.9.16 一起发。
78
+
79
+ ## v0.9.15(把「该有的职能」补齐:时间、设置、反馈、趋势、直达)
80
+
81
+ 这一轮按职能设定逐条对照代码,补上八处「功能上没考虑到、但角色上应该有」的缺口。
82
+ 顺序按影响排:先修真 bug 与死设置,再补反馈闭环,最后是趋势与直达。
83
+
84
+ ### P0:两个「说了但没做」
85
+
86
+ - **设置页的「上次保存」差 8 小时**:`savedAt` 是 UTC ISO,旧代码用 `.replace("T"," ")` 切片
87
+ 当成当地时间贴出来。静态判据只认单引号写法,双引号那次悄悄绕过去了——现在两种引号都查,
88
+ 并且这一行改走 `localStamp()`。
89
+ - **`ui.autoScanMinutes` 是个死设置**:设置里能存 1–240 分钟、界面也回「已保存」,但客户端
90
+ 把刷新阈值写死成 10 分钟。现在服务端把夹取后的值随报告下发,客户端按它算阈值;
91
+ 「设置改了却没反应」正是验收表 A9 要抓的静默失效,不该长在自己家。
92
+
93
+ ### P1:误报要能被纠正,管家才学得会
94
+
95
+ 只有「已知晓(静音)」时,用户看到一条判错的结论,除了不理它没有别的办法。
96
+ 新增 `feedback.json` 与面板按钮「这条判错了 / 撤销判错」,走与静音完全相同的
97
+ plan → 一次性指纹 → confirm → audit 链。标记后:证据仍然保留、不再计「有直达处置」、
98
+ 重要性分数降 40 并写进 `scoreReasons`,所以它会排到后面而不是消失。
99
+ **这是可逆的判定,不是删除**。刻意不进语言层(brain.js / intent.js 不加这个 kind):
100
+ 「把某条具体发现判错」用自然语言说不清是哪一条,按钮更不易误触。
101
+
102
+ ### P2:趋势要会说话,直达不许造假
103
+
104
+ - 服务端新增 `summarizeTrend()`,用上一次报告算出 `trendVerdict`(好转 / 连续变差 / 平稳)
105
+ **和一句人话**,客户端只渲染——符合「服务端算好人话、客户端只渲染」。
106
+ 审计趋势顶部显示结论,恢复或连续两次变差时右下角主动弹一条(不再只报坏不报好)。
107
+ - `ACTION_FOR` 里 `model-health → model-default` 指向一张并不存在的动作卡,是假直达:
108
+ 删掉,并把 `backup-freshness → backup` 补上(备份新鲜度该能一步跳到备份卡)。
109
+ 其余只有文字建议的 finding 不给按钮——宁可少给,也不给一个点了没反应的按钮。
110
+ - `wiring`(路由 / 工具到底注册上没有,v0.9.14 起只在 ping 里)随报告下发,
111
+ 面板在生成时间旁显示「接口已挂上」;没挂上时给出可操作的提示。
112
+
113
+ ### 边界
114
+
115
+ `feedback` 与 `ack` 都只写管家自己的数据目录,不动任何系统状态;口令仍绝不由 Agent 生成或代输。
4
116
 
5
117
  ## v0.9.14(冷启动竞态:路由与工具都注册了,只是没在最合适的那一刻)
6
118
 
@@ -319,7 +431,7 @@ v0.7 给这个输入框加了「模型理解」:可选的第二通话方式,
319
431
  - `lib/brain.js`:v0.7 模型理解层。把一句话交给宿主 `llm` 服务,把模型输出归一成与 `intent.js` **完全同形状**的意图;JSON 抽取容错(容忍围栏与前后废话),各字段走白名单校验,任何不合规都降级为 clarify。**只翻译,不执行。**
320
432
  - `lib/index.js`:host(路由 + `dsh_steward_report` / `dsh_steward_act` 两个工具 + 事件订阅 + boot 后自动体检 + systemPrompt 注入)。
321
433
  - `lib/client.js`:client(`__ModuleLoader__.load` + 三 Tab 面板 + 动作卡 + 侧栏/右侧栏入口,全内联样式)。
322
- - `smoke.mjs`:本地验证(不启动宿主),281 项断言(含打包自检、时间戳与配色静态规约判据)。
434
+ - `smoke.mjs`:本地验证(不启动宿主),379 项断言(另含右侧栏 tab 正文修复的静态与运行期 harness 判据)(含打包自检、时间戳、配色、「延迟注册」、L0 当班环、白色按钮回归、升级利弊分析与免费通道运行期体检等静态与运行期判据)。
323
435
 
324
436
  ## 本地验证(不装、不重启)
325
437
  ```
package/lib/actions.js CHANGED
@@ -104,6 +104,16 @@ async function readAcked(deps) {
104
104
  });
105
105
  }
106
106
 
107
+ /** v0.9.15: 'this judgement was wrong' is distinct from 'I know, be quiet'.
108
+ * Entries are permanent until explicitly undone; expiry would silently
109
+ * re-arm a finding the user already told us was a false positive. */
110
+ async function readFeedback(deps) {
111
+ let list = [];
112
+ try { list = (await deps.io.readJson(deps.feedbackFile)) || []; } catch { list = []; }
113
+ if (!Array.isArray(list)) return [];
114
+ return list.filter((a) => a && typeof a === 'object' && typeof a.id === 'string');
115
+ }
116
+
107
117
  /** Default mute duration per finding id — a mutable condition gets a shorter
108
118
  * leash than a structural one. These are FALLBACKS only: v0.8.0 起真正的值来自
109
119
  * 用户设置(settings.ttl.<id> → settings.ttl.default),这里只作为 settings.js
@@ -135,7 +145,7 @@ function currentSettings(deps) {
135
145
  * - readBytes(file): Buffer|string — used ONLY to verify a freshly written
136
146
  * DCA1 container in-request; never persisted, never logged.
137
147
  * - now(): () => Date
138
- * - auditFile / trendFile / knowledgeFile / ackedFile: paths under plugin data dir.
148
+ * - auditFile / trendFile / knowledgeFile / ackedFile / feedbackFile: paths under plugin data dir.
139
149
  */
140
150
  export function createActionHub(deps) {
141
151
  const pending = new Map(); // hash -> { kind, plan, createdAt }
@@ -335,6 +345,29 @@ export function createActionHub(deps) {
335
345
  remember(hash, { kind, plan: p, createdAt: deps.now().toISOString() });
336
346
  return { ok: true, planHash: hash, plan: p };
337
347
  }
348
+ if (kind === 'feedback' || kind === 'unfeedback') {
349
+ const report = typeof deps.getReport === 'function' ? deps.getReport() : null;
350
+ const id = typeof params.id === 'string' ? params.id.trim() : '';
351
+ if (!report || !Array.isArray(report.findings)) return { ok: false, error: '还没有体检报告——先做一次体检。' };
352
+ if (!id) return { ok: false, error: '需要 finding id(报告条目的编号)。' };
353
+ const f = report.findings.find((x) => x.id === id);
354
+ if (!f) return { ok: false, error: `报告里没有编号 "${id}" 的条目。` };
355
+ const list = await readFeedback(deps);
356
+ const marked = list.some((a) => a.id === id);
357
+ if (kind === 'feedback' && marked) return { ok: false, error: `「${f.title}」已经标记为判错了。` };
358
+ if (kind === 'unfeedback' && !marked) return { ok: false, error: `「${f.title}」没有被标记为判错。` };
359
+ const p = {
360
+ kind, findingId: id, title: f.title,
361
+ human: kind === 'feedback'
362
+ ? [`把「${f.title}」标记为这条判错了。`,
363
+ '效果:从下次体检起这条不再计直达处置、重要性分数降 40,但证据仍然保留,随时可撤销。',
364
+ '这只是告诉管家「这次的判断不准」,不会改动任何系统状态。']
365
+ : [`撤销「${f.title}」的判错标记,恢复它的正常评分。`],
366
+ };
367
+ const hash = hashPlan(kind, p);
368
+ remember(hash, { kind, plan: p, createdAt: deps.now().toISOString() });
369
+ return { ok: true, planHash: hash, plan: p };
370
+ }
338
371
  if (kind === 'model-default') {
339
372
  // 只改「默认模型是哪一个」这一项选择,不碰任何插件的配置。
340
373
  // 这是三个模型类动作里唯一会写宿主状态的,所以它必须有 plan 与指纹。
@@ -487,6 +520,21 @@ export function createActionHub(deps) {
487
520
  '恢复:该条目上的「取消知晓」,或 Agent 工具 kind=unack。']
488
521
  : [`「${entry.plan.title}」已恢复计入风险计数。`],
489
522
  };
523
+ } else if (entry.kind === 'feedback' || entry.kind === 'unfeedback') {
524
+ let list = await readFeedback(deps);
525
+ const id = entry.plan.findingId;
526
+ if (entry.kind === 'feedback') {
527
+ if (!list.some((a) => a.id === id)) list.push({ id, title: entry.plan.title, at: deps.now().toISOString() });
528
+ } else {
529
+ list = list.filter((a) => a.id !== id);
530
+ }
531
+ await deps.io.writeJson(deps.feedbackFile, list);
532
+ result = {
533
+ ok: true, findingId: id,
534
+ human: entry.kind === 'feedback'
535
+ ? [`已把「${entry.plan.title}」标记为判错——下次体检起它会排在后面,但证据仍在。`, '撤销:该条目上的「撤销判错」。']
536
+ : [`「${entry.plan.title}」的判错标记已撤销,恢复正常评分。`],
537
+ };
490
538
  } else if (entry.kind === 'model-default') {
491
539
  const sel = typeof deps.getDefaultModel === 'function' ? deps.getDefaultModel() : null;
492
540
  if (!sel || typeof sel.saveSelection !== 'function') {