dsh-my-guardian 0.4.2 → 0.4.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.3] - 2026-09-25
9
+
10
+ ### 变更
11
+
12
+ - fix: 适配宿主 0.1.7-rc.2 的事件名与席位契约,修正失效文档断言
13
+
8
14
  ## [0.4.2] - 2026-09-17
9
15
 
10
16
  ### 变更
package/lib/client.js CHANGED
@@ -1210,7 +1210,9 @@ exports.apply = function apply(ctx) {
1210
1210
  id: TAB_ID_PKG,
1211
1211
  kind: TAB_ID,
1212
1212
  title: () => strings.title(),
1213
- guide: [{ order: TAB_ORDER, title: () => strings.title() }],
1213
+ // guide 条目 id 必填(宿主 SidebarRightGuideEntry):缺了它注册不报错,但宿主
1214
+ // 会把 entryId: undefined 传给 sidebar.right.tab.guide.entry 席位(静默降级)。
1215
+ guide: [{ id: TAB_ID_PKG, order: TAB_ORDER, title: () => strings.title() }],
1214
1216
  }),
1215
1217
  'dsh-my-guardian: tab type',
1216
1218
  )
package/lib/events.js CHANGED
@@ -5,16 +5,155 @@
5
5
  import { ERROR_SNIP, EVENT_LIMIT } from './state.js';
6
6
  /** Append a diagnostic event to the shared state's ring buffer. */
7
7
  export function logEvent(shared, type, message) {
8
- shared.state.events.push({
8
+ const record = {
9
9
  time: Date.now(),
10
10
  type,
11
11
  message: String(message).slice(0, ERROR_SNIP),
12
- });
12
+ };
13
+ shared.state.events.push(record);
13
14
  if (shared.state.events.length > EVENT_LIMIT)
14
15
  shared.state.events.splice(0, shared.state.events.length - EVENT_LIMIT);
16
+ return record;
15
17
  }
16
- /** Register the loader/HMR diagnostic listeners (R9/R10). */
18
+ /**
19
+ * 本插件监听的宿主事件名(登记表)。
20
+ *
21
+ * 判据在 test/host-event-contract.mjs:这些名字必须命中**目标宿主真实事件表**
22
+ * (test/fixtures/host-events.json,由 scripts/host-events.mjs 从宿主源码取证:
23
+ * `interface Events` 声明 ∪ `ctx.<emit|parallel|...>` 派发),否则必须有
24
+ * HOST_EVENT_FALLBACKS 降级信号。新增/删除 ctx.on 时必须同步本表。
25
+ */
26
+ export const LISTENED_HOST_EVENTS = [
27
+ 'loader/entry-init',
28
+ 'loader/partial-dispose',
29
+ 'hmr/config-update-failed',
30
+ ];
31
+ /** 宿主在配置热更新失败点打的结构化 warn 首参(逐字取自宿主源码,测试会校验)。 */
32
+ const CONFIG_FAILURE_MARKER = 'config reload at %C failed';
33
+ /** cordis exporter 的 verbosity 阈值:2 = 收 warn 及更严重的消息(error/warn)。 */
34
+ const WARN_LEVEL = 2;
35
+ /** marker 之后紧跟的 `ctx.logger.warn(error)` 配对窗口(ms)。 */
36
+ const PAIR_WINDOW_MS = 50;
37
+ /** 双版本去重窗口(ms):0.1.5-rc.1 上同一失败既打日志又发事件。 */
38
+ const DEDUP_WINDOW_MS = 1000;
39
+ /** 去重表上限(长跑内存护栏)。 */
40
+ const DEDUP_LIMIT = 50;
41
+ /** marker 先到时错误文本未知的占位(紧随的 Error warn 会补齐)。 */
42
+ const PENDING_DETAIL = '(error detail logged by host)';
43
+ /**
44
+ * 降级信号登记:宿主 0.1.7-rc.2 把 HMR 换成 @deepseek-ai/dsh-hmr,事件表只剩
45
+ * `hmr/change` / `hmr/reload` —— `hmr/config-update-failed` 被删除,`ctx.on` 该事件
46
+ * 在新宿主上**静默失效**(不报错、不告警),"配置热更新失败"诊断整体消失。
47
+ *
48
+ * 替代可观测量(逐字取证,非猜测):0.1.5-rc.1 的 cordis-plugin-hmr 与 0.1.7-rc.2 的
49
+ * dsh-hmr **在同一个 catch 里先打同一对 warn、然后旧版才发事件**:
50
+ * ctx.logger.warn('config reload at %C failed', filename); ctx.logger.warn(error)
51
+ * (旧:cordis-plugin-hmr/lib/index.js;新:dsh-hmr/src/watch-config.ts)
52
+ * 所以结构化日志是两个宿主共有的可观测面,用它接手被删事件的诊断职责。
53
+ */
54
+ export const HOST_EVENT_FALLBACKS = [
55
+ {
56
+ event: 'hmr/config-update-failed',
57
+ kind: 'logger-warn',
58
+ marker: CONFIG_FAILURE_MARKER,
59
+ targetSource: 'packages/boot/hmr/src/watch-config.ts',
60
+ legacySource: 'node_modules/@deepseek-ai/cordis-plugin-hmr/lib/index.js',
61
+ },
62
+ ];
63
+ /** 失败原因文本:Error 取 message、其他字符串化、未配对到时给占位。 */
64
+ function failureText(error) {
65
+ if (error === undefined)
66
+ return PENDING_DETAIL;
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+ /**
70
+ * 创建"配置热更新失败"记录器(两条通道共用一张最近失败表)。
71
+ *
72
+ * 去重依据同上:旧宿主在同一个 catch 里先 warn 再发事件,同一失败会经两条通道
73
+ * 到达(日志通道在前),只记一条;不同文件名照常各记一条。
74
+ * @param ctx DSH server 端 Context。
75
+ * @param shared 插件共享状态(环形缓冲 + 落盘)。
76
+ * @param now 注入时钟(默认 Date.now;测试用于验证窗口过期分支)。
77
+ */
78
+ export function createConfigFailureTracker(ctx, shared, now = Date.now) {
79
+ const recent = new Map();
80
+ let pending;
81
+ /** 该文件名在去重窗口内是否首次失败。 */
82
+ const firstTime = (key) => {
83
+ const time = now();
84
+ const last = recent.get(key);
85
+ if (last !== undefined && time - last < DEDUP_WINDOW_MS)
86
+ return false;
87
+ recent.set(key, time);
88
+ if (recent.size > DEDUP_LIMIT)
89
+ recent.delete(String(recent.keys().next().value));
90
+ return true;
91
+ };
92
+ const write = (filename, error) => {
93
+ const key = String(filename);
94
+ if (!firstTime(key))
95
+ return undefined;
96
+ const record = logEvent(shared, 'update-failed', `${key}: ${failureText(error)}`);
97
+ ctx.logger?.warn(`[dsh-my-guardian] config update failed (rolled back): ${key}`);
98
+ shared.persistSoon();
99
+ return record;
100
+ };
101
+ return {
102
+ fromEvent(filename, error) {
103
+ write(filename, error);
104
+ },
105
+ fromLogMarker(filename, loggerName) {
106
+ const record = write(filename, undefined);
107
+ if (record !== undefined)
108
+ pending = { record, key: String(filename), name: loggerName, at: now() };
109
+ },
110
+ fromLogDetail(error, loggerName) {
111
+ const state = pending;
112
+ pending = undefined;
113
+ if (state === undefined)
114
+ return;
115
+ if (now() - state.at > PAIR_WINDOW_MS || loggerName !== state.name)
116
+ return;
117
+ state.record.message = `${state.key}: ${failureText(error)}`.slice(0, ERROR_SNIP);
118
+ },
119
+ };
120
+ }
121
+ /**
122
+ * 注册结构化日志降级通道(0.1.7-rc.2 起被删事件的替代可观测量)。
123
+ *
124
+ * `ctx.logger.exporter()` 是 cordis 日志服务的公开扩展点:注册的 exporter 随当前
125
+ * fiber 释放(实测插件卸载后 exporter 一并移除,不泄漏),收到的是**未格式化的**
126
+ * 结构化消息(`args` 里的 marker 字面量可直接比对,无需解析格式串)。
127
+ * logger 缺失或没有 exporter(老/最小 ctx)时静默降级,不抛异常。
128
+ */
129
+ function attachConfigFailureLogFallback(ctx, failures) {
130
+ const logger = ctx.logger;
131
+ if (logger === undefined || typeof logger.exporter !== 'function')
132
+ return;
133
+ logger.exporter({
134
+ levels: { default: WARN_LEVEL },
135
+ export(message) {
136
+ if (message?.type !== 'warn')
137
+ return;
138
+ const args = Array.isArray(message.args) ? message.args : [];
139
+ if (args[0] === CONFIG_FAILURE_MARKER) {
140
+ failures.fromLogMarker(args[1], message.name);
141
+ }
142
+ else if (args.length === 1 && args[0] instanceof Error) {
143
+ failures.fromLogDetail(args[0], message.name);
144
+ }
145
+ },
146
+ });
147
+ }
148
+ /**
149
+ * Register the loader/HMR diagnostic listeners (R9/R10)。
150
+ *
151
+ * 两条通道在**两个宿主上都注册**:`hmr/config-update-failed` 在 0.1.5-rc.1 上有效、
152
+ * 在 0.1.7-rc.2 上永不触发(注册未知事件不报错),日志通道则两个宿主都有——
153
+ * 靠 createConfigFailureTracker 去重合并成一条诊断。
154
+ */
17
155
  export function attachEventListeners(ctx, shared) {
156
+ const failures = createConfigFailureTracker(ctx, shared);
18
157
  ctx.on('loader/entry-init', (entry) => {
19
158
  logEvent(shared, 'entry-init', `entry ${entryLabelOf(entry)} initialized`);
20
159
  });
@@ -22,10 +161,9 @@ export function attachEventListeners(ctx, shared) {
22
161
  logEvent(shared, 'entry-dispose', `entry ${entryLabelOf(entry)} disposed`);
23
162
  });
24
163
  ctx.on('hmr/config-update-failed', (filename, error) => {
25
- logEvent(shared, 'update-failed', `${String(filename)}: ${error instanceof Error ? error.message : String(error)}`);
26
- ctx.logger?.warn(`[dsh-my-guardian] config update failed (rolled back): ${String(filename)}`);
27
- shared.persistSoon();
164
+ failures.fromEvent(filename, error);
28
165
  });
166
+ attachConfigFailureLogFallback(ctx, failures);
29
167
  }
30
168
  /**
31
169
  * entry 可读标识——⚠️ 只读 options 字段,绝不访问 `entry.id` getter:
@@ -33,7 +33,9 @@ exports.apply = function apply(ctx) {
33
33
  id: TAB_ID_PKG,
34
34
  kind: TAB_ID,
35
35
  title: () => strings.title(),
36
- guide: [{ order: TAB_ORDER, title: () => strings.title() }],
36
+ // guide 条目 id 必填(宿主 SidebarRightGuideEntry):缺了它注册不报错,但宿主
37
+ // 会把 entryId: undefined 传给 sidebar.right.tab.guide.entry 席位(静默降级)。
38
+ guide: [{ id: TAB_ID_PKG, order: TAB_ORDER, title: () => strings.title() }],
37
39
  }),
38
40
  'dsh-my-guardian: tab type',
39
41
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-my-guardian",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "DSH 插件治理守护:新装/更新插件先进候选区,启动后热挂载——成功转正、失败自动禁用、连续失败冻结,一键安全模式,侧边栏诊断面板。DSH web plugin: staged plugin loading with auto-disable on failure, freeze, safe mode and a sidebar panel.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",