dsh-plugin-compat-check 0.0.0-stage → 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,562 @@
1
+ // Client half of dsh-plugin-compat-check — the overview table (Plan.md §5 C2).
2
+ //
3
+ // This file is a PREBUILT artifact: the host does not build plugin client code.
4
+ // If it moves to src/ the entry fails to activate with "run pnpm build before
5
+ // launch", so it stays here as plain, dependency-free JS.
6
+ //
7
+ // Contract (Plan.md §7.3, verified on 0.2.0-rc.2):
8
+ // window.__ModuleLoader__.load({ id: "<package name>", factory })
9
+ // factory(require) -> { name, inject: ["slots"], apply(ctx) }
10
+ //
11
+ // `inject` here lists OTHER CLIENT PLUGIN PACKAGES, not service names.
12
+ //
13
+ // The slot this fills is `settings.plugins.tab`, whose host page is
14
+ // Settings → 内置插件 (served by @deepseek-ai/dsh-client-ui-settings-plugins).
15
+ // That page already has a 插件列表 tab, so this one must answer a DIFFERENT
16
+ // question: it is a **verdict** table ("is what I have installed actually
17
+ // compatible"), not an inventory.
18
+ //
19
+ // Data comes from the host half over HTTP: GET /plugin-compat-check/report.
20
+ // The browser cannot read the filesystem, and the verdict must not be computed
21
+ // twice — one kernel, one answer.
22
+
23
+ window.__ModuleLoader__.load({
24
+ id: "dsh-plugin-compat-check",
25
+ factory: (require) => {
26
+ var module = { exports: {} };
27
+ var exports = module.exports;
28
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
29
+
30
+ var React = require("react");
31
+
32
+ var SLOT = "settings.plugins.tab";
33
+ var LABEL = "兼容性";
34
+ var ROUTE = "/plugin-compat-check/report";
35
+ // C3:单插件视图的两个槽位。实测(Plan.md §5 C3):
36
+ // - 详情页不是「抽屉」,是侧栏插件面板**换了 React 视图状态**:
37
+ // 侧栏轨道按钮 [class*="panelRow"][aria-label="插件"] → 卡片标题
38
+ // button[class*="cardOpen"][aria-label="查看 <名字>"] → 返回用 button[class*="crumb"]。
39
+ // 没有 URL/hash 路由,所以只能靠点击驱动。
40
+ // - `plugins.item` 是**列表卡片**的槽:角标挂在这里,用户不用点进去就能扫一眼。
41
+ // `plugins.item` **不注册**(原因见 apply 里的注释):它拿不到插件身份。
42
+ // 我们自己那个配置页条目的 id,仅用于台账兜底。
43
+ var SELF_ITEM_ID = "dsh-plugin-compat-check";
44
+ var SLOT_DETAIL = "plugins.detail.section";
45
+ var SLOT_DETAIL_BADGE = "plugins.detail.badge";
46
+
47
+ var VERDICT = {
48
+ broken: { text: "不兼容", color: "#dc2626" },
49
+ partial: { text: "部分可用", color: "#d97706" },
50
+ unknown: { text: "未知", color: "#6b7280" },
51
+ ok: { text: "可用", color: "#16a34a" },
52
+ };
53
+ var SEVERITY = { fatal: "致命", "feature-dead": "功能失效", degraded: "降级" };
54
+ var CATEGORY = {
55
+ "host-service-missing": "服务缺失",
56
+ "host-method-missing": "方法缺失",
57
+ "unprovided-dependency": "无人提供的依赖",
58
+ "declaration-mismatch": "声明与实测不符",
59
+ };
60
+
61
+ // 渲染侧标记:只在我们的组件体里设置。页面上出现同名文字**不能**证明
62
+ // 是我们渲染的,这个标记能 —— 验收要经得起这个追问(Plan.md §7.2.1 ③)。
63
+ function mark(extra) {
64
+ try {
65
+ var prev = window.__COMPAT_CHECK__ || {};
66
+ var next = { slot: SLOT, label: LABEL };
67
+ for (var k in prev) if (Object.prototype.hasOwnProperty.call(prev, k) && prev[k] !== null && prev[k] !== undefined) next[k] = prev[k];
68
+ for (var j in extra) {
69
+ if (!Object.prototype.hasOwnProperty.call(extra, j)) continue;
70
+ // ⚠️ `null` **不覆盖**已有值。早先这里用 Object.assign,于是
71
+ // 一次 `detailFor: null` 的渲染会把前一次好好的诊断信息抹掉 ——
72
+ // 而"诊断信息自己把证据抹掉"恰恰是最难查的那种。
73
+ if (extra[j] === null || extra[j] === undefined) continue;
74
+ next[j] = extra[j];
75
+ }
76
+ next.at = Date.now();
77
+ window.__COMPAT_CHECK__ = next;
78
+ } catch (error) {
79
+ /* 诊断绝不影响渲染 */
80
+ }
81
+ }
82
+
83
+ function mergeStyle() {
84
+ var out = {};
85
+ for (var i = 0; i < arguments.length; i++) {
86
+ var a = arguments[i];
87
+ if (!a) continue;
88
+ for (var k in a) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k];
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /** C3 · 详情页 section:这个插件的完整结论 + 每条发现的证据 */
94
+ function useCompatibility() {
95
+ var state = React.useState({ phase: "loading", data: null, error: null });
96
+ var value = state[0];
97
+ var set = state[1];
98
+ var alive = React.useRef(true);
99
+
100
+ var load = React.useCallback(function (refresh) {
101
+ set({ phase: "loading", data: null, error: null });
102
+ try {
103
+ fetch(refresh ? ROUTE + "?refresh=1" : ROUTE, {
104
+ headers: { accept: "application/json" },
105
+ credentials: "same-origin",
106
+ })
107
+ .then(function (res) {
108
+ if (!res.ok) throw new Error("HTTP " + res.status);
109
+ return res.json();
110
+ })
111
+ .then(function (data) {
112
+ if (!alive.current) return;
113
+ set({ phase: "done", data: data, error: null });
114
+ })
115
+ .catch(function (error) {
116
+ if (!alive.current) return;
117
+ // 取不到数据**也要说清楚是什么问题**。一个永远转圈的加载状态
118
+ // 与一个永远空白的表格,用户无法区分"工具坏了"与"没问题"。
119
+ set({ phase: "error", data: null, error: String((error && error.message) || error) });
120
+ });
121
+ } catch (error) {
122
+ set({ phase: "error", data: null, error: String((error && error.message) || error) });
123
+ }
124
+ }, []);
125
+
126
+ React.useEffect(function () {
127
+ alive.current = true;
128
+ load(false);
129
+ return function () { alive.current = false; };
130
+ }, [load]);
131
+
132
+ return { value: value, reload: load };
133
+ }
134
+
135
+ var styles = {
136
+ root: { padding: "12px 4px", font: "13px/1.5 system-ui, -apple-system, sans-serif", color: "inherit" },
137
+ head: { display: "flex", alignItems: "baseline", gap: "10px", flexWrap: "wrap", marginBottom: "10px" },
138
+ title: { font: "600 14px system-ui, -apple-system, sans-serif" },
139
+ meta: { opacity: 0.65, fontSize: "12px" },
140
+ btn: {
141
+ padding: "3px 10px", borderRadius: "6px",
142
+ border: "1px solid rgba(127,127,127,.45)", background: "transparent",
143
+ color: "inherit", fontSize: "12px", cursor: "pointer",
144
+ },
145
+ table: { width: "100%", borderCollapse: "collapse", fontSize: "12.5px" },
146
+ th: {
147
+ textAlign: "left", fontWeight: 600, padding: "6px 8px",
148
+ borderBottom: "1px solid rgba(127,127,127,.35)", whiteSpace: "nowrap",
149
+ },
150
+ td: { padding: "6px 8px", borderBottom: "1px solid rgba(127,127,127,.16)", verticalAlign: "top" },
151
+ mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", fontSize: "11.5px", opacity: 0.85 },
152
+ pill: {
153
+ display: "inline-block", padding: "1px 7px", borderRadius: "999px",
154
+ fontSize: "11.5px", fontWeight: 600, color: "#fff", whiteSpace: "nowrap",
155
+ },
156
+ note: { opacity: 0.7, fontSize: "12px", marginTop: "10px" },
157
+ err: { padding: "10px", borderRadius: "8px", background: "rgba(220,38,38,.08)", fontSize: "12.5px" },
158
+ hint: { opacity: 0.6, fontSize: "12px", padding: "10px 0" },
159
+ finding: { marginTop: "3px" },
160
+ suspect: { opacity: 0.75, fontStyle: "italic" },
161
+ dim: { opacity: 0.6, fontSize: "11.5px", marginTop: "4px" },
162
+ };
163
+
164
+ function Sev(p) {
165
+ var s = p.severity;
166
+ var color = s === "fatal" ? "#dc2626" : s === "feature-dead" ? "#d97706" : "#6b7280";
167
+ return React.createElement("span", { style: { color: color, fontWeight: 600 } }, SEVERITY[s] || s);
168
+ }
169
+
170
+ function Findings(p) {
171
+ var pl = p.plugin;
172
+ if (!pl.findings || pl.findings.length === 0) {
173
+ var parts = [];
174
+ if (pl.vacuous) {
175
+ parts.push(React.createElement("div", { key: "v", style: styles.suspect },
176
+ "这个插件没有用过任何 DSH 服务,因此没有可检查的东西 —— 「可用」在这里是空洞的。"));
177
+ }
178
+ var noCalls = Object.keys(pl.resolved || {})
179
+ .map(function (k) { return pl.resolved[k]; })
180
+ .filter(function (c) { return c.disposition === "no-calls"; });
181
+ if (noCalls.length) {
182
+ parts.push(React.createElement("div", { key: "n", style: styles.dim },
183
+ "无证据可判(只出现名字,没有调用点):" + noCalls.map(function (c) { return c.service; }).join("、")));
184
+ }
185
+ if (pl.clientSide && pl.clientSide.length) {
186
+ parts.push(React.createElement("div", { key: "c", style: styles.dim },
187
+ "客户端半边(命令行测不到,需界面验证):" + pl.clientSide.map(function (c) { return c.service; }).join("、")));
188
+ }
189
+ return parts.length ? React.createElement("div", null, parts) : null;
190
+ }
191
+ return React.createElement("div", null, pl.findings.map(function (f, i) {
192
+ var api = f.service ? (f.method ? f.service + "." + f.method : f.service) : "(包声明)";
193
+ var loc = f.location ? f.location.file + ":" + f.location.line : "—";
194
+ return React.createElement("div", { key: i, style: styles.finding },
195
+ React.createElement(Sev, { severity: f.severity }),
196
+ React.createElement("span", null, " " + (CATEGORY[f.category] || f.category) + " "),
197
+ React.createElement("code", { style: styles.mono }, api),
198
+ React.createElement("span", { style: styles.mono }, " @ " + loc),
199
+ f.certainty !== "certain"
200
+ ? React.createElement("span", { style: styles.suspect, title: "没有运行时面,只能靠官方目录推断" }, " [疑似]")
201
+ : null,
202
+ f.impact ? React.createElement("div", { style: styles.dim }, f.impact) : null);
203
+ }));
204
+ }
205
+
206
+ /**
207
+ * 从槽位给的 props 里解析出「这是哪个插件」。
208
+ *
209
+ * ## 契约(读自宿主源码 + 实测印证;三种形态都见过)
210
+ *
211
+ * 详情页的 `subject` 按**条目种类**有两个分支:
212
+ *
213
+ * | 条目种类 | subject 形态 | 对应谁 |
214
+ * |---|---|---|
215
+ * | 安装的包 | `{ kind: "package", pkg: { name, version, … } }` | **社区插件卡**(最该有角标的) |
216
+ * | 官方 bundle | `{ kind: "bundle", pkg: { name, version, rows: […] } }` | 官方 bundle 卡 |
217
+ * | 面板条目 | `{ kind: "item", id }` | 第三方配置页注册的条目(id 不是包名) |
218
+ *
219
+ * 这两种形态是**踩出来的**,不是猜的:
220
+ * - 第一版按 `props.name` / `props.entry.name` 猜 → 一条都没匹配上,
221
+ * 角标全部**静默不渲染**(页面看起来正常,只是少了个东西);
222
+ * - 第二版按 `subject.id` → 打开官方 bundle 卡时拿到
223
+ * `{kind:"bundle", pkg:{...}}`,`subject.id` 是 undefined,于是又一次静默失败。
224
+ *
225
+ * 解析顺序:`subject.pkg.name` → `subject.id`(是包名)→ 台账(id → 包名)
226
+ * → `props.id` 同上 → 自家条目 id。都不中返回 null,**角标不渲染**:
227
+ * 渲染一个猜的结论比不渲染更糟 —— 用户会当真。
228
+ */
229
+ function pickPlugin(data, props) {
230
+ var report = data && data.report;
231
+ if (!report || !report.plugins) return null;
232
+ var plugins = report.plugins;
233
+
234
+ var byPkg = function (name, how) {
235
+ if (typeof name !== 'string' || name === '') return null;
236
+ return plugins[name] ? { name: name, plugin: plugins[name], how: how } : null;
237
+ };
238
+
239
+ var subj = props && props.subject;
240
+ if (subj && typeof subj === 'object') {
241
+ var pkg = subj.pkg;
242
+ var direct = typeof pkg === 'string' ? pkg : (pkg && typeof pkg === 'object' ? pkg.name : null);
243
+ var hit = byPkg(direct, 'subject.pkg.name');
244
+ if (hit) return hit;
245
+ hit = byPkg(subj.id, 'subject.id=包名');
246
+ if (hit) return hit;
247
+ hit = lookupLedger(report, subj.id);
248
+ if (hit) return hit;
249
+ }
250
+ if (props && typeof props === 'object') {
251
+ var hit2 = byPkg(props.id, 'props.id=包名');
252
+ if (hit2) return hit2;
253
+ hit2 = lookupLedger(report, props.id);
254
+ if (hit2) return hit2;
255
+ if (props.view) {
256
+ hit2 = byPkg(SELF_ITEM_ID, '自家条目 id');
257
+ if (hit2) return hit2;
258
+ }
259
+ }
260
+ return null;
261
+ }
262
+
263
+ /** 条目 id → 关联包名(台账由宿主半边的 computeReport 采集) */
264
+ function lookupLedger(report, id) {
265
+ if (typeof id !== 'string' || id === '') return null;
266
+ var items = report.items || [];
267
+ for (var i = 0; i < items.length; i++) {
268
+ var it = items[i];
269
+ if (!it || it.id !== id) continue;
270
+ var pkg = it.packageName || it.package || it.name;
271
+ if (typeof pkg === 'string' && report.plugins[pkg]) {
272
+ return { name: pkg, plugin: report.plugins[pkg], how: '台账条目 ' + id + ' → ' + pkg };
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
278
+ /** 结论档位 → 角标的颜色与文字(多处共用,避免文案分叉) */
279
+ function verdictOf(plugin) {
280
+ if (!plugin) return null;
281
+ return VERDICT[plugin.verdict] || VERDICT.unknown;
282
+ }
283
+
284
+ /** 详情页要的是**单个插件**的结论;数据源与总览表同一个路由,所以不可能两边不一致。 */
285
+ function usePluginState() {
286
+ return useCompatibility();
287
+ }
288
+
289
+ /**
290
+ * C4 · 基线角标:回答「**和上次比变了没有**」,并给出方向。
291
+ *
292
+ * 规则是 Plan.md §2 第 5 题的落地:**任何变化都打**,不只是变差。
293
+ * 只有变差才亮会漏掉"某个插件被修好了、你可以放心留着了" —— 那同样是用户要知道的事。
294
+ * 没有基线时**不打**角标,但要说清是「还没比过」而不是「没问题」。
295
+ */
296
+ function Badge(p) {
297
+ var b = p.baseline;
298
+ if (!b) return null;
299
+ if (b.note) {
300
+ return React.createElement("div", { style: mergeStyle(styles.dim, { marginTop: 0, marginBottom: "8px" }) },
301
+ b.note);
302
+ }
303
+ if (b.error) {
304
+ return React.createElement("div", { style: styles.dim }, "基线差分没算成:" + b.error);
305
+ }
306
+ var badge = b.badge || {};
307
+ if (!badge.show) {
308
+ return React.createElement("div", { style: mergeStyle(styles.dim, { marginTop: 0, marginBottom: "8px" }) },
309
+ badge.detail || "与上次相比没有任何变化");
310
+ }
311
+ var color = badge.direction === "worse" ? "#dc2626"
312
+ : badge.direction === "better" ? "#16a34a" : "#6b7280";
313
+ var diff = b.diff || {};
314
+ var lines = [];
315
+ (diff.worsened || []).forEach(function (e) {
316
+ lines.push("↓ " + e.name + "@" + (e.versionFrom || "?") + "→" + (e.versionTo || "?") + ":" + e.reason);
317
+ (e.newFindings || []).forEach(function (f) {
318
+ lines.push(" 新增:" + f.key + " @ " + (f.location || "—"));
319
+ });
320
+ });
321
+ (diff.improved || []).forEach(function (e) { lines.push("↑ " + e.name + ":" + e.reason); });
322
+ (diff.added || []).forEach(function (e) { lines.push("+ " + e.name + "@" + e.version); });
323
+ (diff.removed || []).forEach(function (e) { lines.push("− " + e.name + "@" + e.version); });
324
+ return React.createElement("div", { style: { marginBottom: "10px" } },
325
+ React.createElement("span", {
326
+ style: mergeStyle(styles.pill, { background: color }),
327
+ }, badge.text),
328
+ diff.dshVersionChanged
329
+ ? React.createElement("span", { style: mergeStyle(styles.dim, { marginLeft: "8px" }) },
330
+ "DSH " + (diff.dshVersionFrom || "?") + " → " + (diff.dshVersionTo || "?"))
331
+ : null,
332
+ React.createElement("div", { style: mergeStyle(styles.mono, { marginTop: "4px", whiteSpace: "pre-wrap" }) },
333
+ lines.join("\n")));
334
+ }
335
+
336
+ function DetailSection(props) {
337
+ try {
338
+ return DetailSectionBody(props);
339
+ } catch (error) {
340
+ // 组件体抛异常会被 React 静默丢弃(整棵子树消失),
341
+ // 而"跑了但什么都没渲染"是最难查的形态。所以自己抓、自己报。
342
+ mark({ detailSectionError: String((error && error.message) || error) });
343
+ return React.createElement("div", { style: styles.err },
344
+ "dsh-plugin-compat-check: 渲染时出错 —— " + String((error && error.message) || error));
345
+ }
346
+ }
347
+
348
+ function DetailSectionBody(props) {
349
+ var st = usePluginState();
350
+ var data = st.value.data;
351
+ var hit = pickPlugin(data, props);
352
+ // 解析失败时把**看到的形状**记下来。角标静默不渲染是最难查的形态:
353
+ // 页面看起来正常,只是少了个东西。所以把"为什么没匹配上"留成可查的证据。
354
+ if (!hit) {
355
+ try {
356
+ var subj = props && props.subject;
357
+ var pkg = subj && subj.pkg;
358
+ mark({
359
+ detailUnmatched: {
360
+ hasProps: Boolean(props),
361
+ hasSubject: Boolean(subj),
362
+ subjectKind: subj && subj.kind || null,
363
+ subjectId: subj && subj.id || null,
364
+ pkgType: typeof pkg,
365
+ pkgName: typeof pkg === 'string' ? pkg : (pkg && pkg.name) || null,
366
+ inReport: Boolean(data && data.report && data.report.plugins),
367
+ pluginCount: data && data.report && data.report.plugins ? Object.keys(data.report.plugins).length : 0,
368
+ },
369
+ });
370
+ } catch (e) { /* 诊断 */ }
371
+ }
372
+ mark({ detailSectionRendered: true, detailFor: hit ? hit.name : null });
373
+
374
+ if (st.value.phase === "loading") {
375
+ return React.createElement("div", { style: styles.hint }, "正在体检…");
376
+ }
377
+ if (st.value.phase === "error" || !data || data.ok === false) {
378
+ return React.createElement("div", { style: styles.err },
379
+ "拿不到体检结果(这是工具的问题,不是这个插件的结论):"
380
+ + (st.value.error || (data && data.error) || "未知原因"));
381
+ }
382
+ if (!hit) {
383
+ // 找不到对应项**也要说明**,而不是渲染一个空白 ——
384
+ // 空白会被读成"这个插件没问题",而它其实是"我们没算出来"。
385
+ return React.createElement("div", { style: styles.dim },
386
+ "这个插件不在本次体检范围内(可能它不是带 dsh 清单的社区插件,或它的条目用了别的键名)。"
387
+ + "请以「兼容性」总览页为准。");
388
+ }
389
+ var pl = hit.plugin;
390
+ var v = verdictOf(pl);
391
+ return React.createElement("div", { style: styles.root, "data-compat-detail": hit.name },
392
+ React.createElement("div", { style: styles.head },
393
+ React.createElement("span", { style: mergeStyle(styles.pill, { background: v.color }) }, v.text),
394
+ React.createElement("span", { style: styles.meta },
395
+ "dsh-plugin-compat-check · " + (pl.findings || []).length + " 条发现"),
396
+ React.createElement("button", {
397
+ style: styles.btn, type: "button",
398
+ onClick: function () { st.reload(true); },
399
+ }, "重新检查")),
400
+ React.createElement(Findings, { plugin: pl }),
401
+ pl.vacuous ? React.createElement("div", { style: styles.dim },
402
+ "这个插件的结论是空洞的:它没有用过任何 DSH 服务,所以没有可检查的东西。") : null);
403
+ }
404
+
405
+ /** C3 · 详情页角标:一行字,放在标题行 */
406
+ function DetailBadge(props) {
407
+ var st = usePluginState();
408
+ var data = st.value.data;
409
+ var hit = pickPlugin(data, props);
410
+ mark({ detailBadgeRendered: true, detailBadgeFor: hit ? hit.name : null });
411
+ if (!hit) return null;
412
+ var v = verdictOf(hit.plugin);
413
+ if (!v) return null;
414
+ return React.createElement("span", {
415
+ "data-compat-detail-badge": hit.name,
416
+ style: mergeStyle(styles.pill, { background: v.color }),
417
+ title: "dsh-plugin-compat-check",
418
+ }, v.text);
419
+ }
420
+
421
+ function Table(p) {
422
+ var data = p.data;
423
+ var report = data && data.report;
424
+ if (!report) return null;
425
+ var names = Object.keys(report.plugins);
426
+ names.sort(function (a, b) {
427
+ var rank = { broken: 0, partial: 1, unknown: 2, ok: 3 };
428
+ var d = rank[report.plugins[a].verdict] - rank[report.plugins[b].verdict];
429
+ return d !== 0 ? d : a.localeCompare(b);
430
+ });
431
+ var s = report.summary || {};
432
+ return React.createElement("div", { style: styles.root },
433
+ React.createElement("div", { style: styles.head },
434
+ React.createElement("span", { style: styles.title }, "兼容性"),
435
+ React.createElement("span", { style: styles.meta },
436
+ names.length + " 个插件 · 可用 " + (s.ok || 0) + " · 部分可用 " + (s.partial || 0)
437
+ + " · 不兼容 " + (s.broken || 0)
438
+ + (s.vacuousOk ? " · 其中 " + s.vacuousOk + " 个「可用」无可检查对象" : "")),
439
+ React.createElement("button", {
440
+ style: styles.btn, type: "button",
441
+ onClick: function () { p.onReload(); },
442
+ }, "重新检查")),
443
+ React.createElement(Badge, { baseline: data.baseline }),
444
+ React.createElement("table", { style: styles.table },
445
+ React.createElement("thead", null, React.createElement("tr", null,
446
+ React.createElement("th", { style: styles.th }, "插件"),
447
+ React.createElement("th", { style: styles.th }, "结论"),
448
+ React.createElement("th", { style: styles.th }, "发现"))),
449
+ React.createElement("tbody", null, names.map(function (n) {
450
+ var pl = report.plugins[n];
451
+ var v = VERDICT[pl.verdict] || VERDICT.unknown;
452
+ return React.createElement("tr", { key: n },
453
+ React.createElement("td", { style: styles.td },
454
+ React.createElement("div", null, n),
455
+ React.createElement("div", { style: styles.mono },
456
+ "@" + (pl.version || "?") + (pl.vacuous ? " · 无可检查对象" : ""))),
457
+ React.createElement("td", { style: styles.td },
458
+ React.createElement("span", { style: Object.assign({}, styles.pill, { background: v.color }) }, v.text)),
459
+ React.createElement("td", { style: styles.td }, React.createElement(Findings, { plugin: pl })));
460
+ }))),
461
+ React.createElement("div", { style: styles.note },
462
+ report.hasRuntimeSurface
463
+ ? "方法级判定依据:宿主运行时面(确定)。"
464
+ : "方法级判定依据:官方 API 目录(契约目录,不是运行时面)—— 因此方法级结论标为「疑似」。"
465
+ + "「疑似」不是缺陷,是诚实:判不准时降级标注,而不是升格成确定。"),
466
+ React.createElement("div", { style: styles.note },
467
+ "判定内核:" + (data.kernelDir || "?")
468
+ + " · 体检对象:" + (data.nodeModules || "?") + "(" + (data.nodeModulesWhy || "?") + ")"
469
+ + (data.elapsedMs !== undefined ? " · 耗时 " + data.elapsedMs + "ms" : "")));
470
+ }
471
+
472
+ function Panel() {
473
+ var st = useCompatibility();
474
+ var value = st.value;
475
+ mark({
476
+ state: value.phase,
477
+ renders: ((window.__COMPAT_CHECK__ || {}).renders || 0) + 1,
478
+ // 验收要能断言"角标真的按差分管了",所以把它的状态一起标出来
479
+ badgeShow: Boolean(value.data && value.data.baseline && value.data.baseline.badge
480
+ && value.data.baseline.badge.show),
481
+ badgeText: (value.data && value.data.baseline && value.data.baseline.badge
482
+ && value.data.baseline.badge.text) || null,
483
+ });
484
+
485
+ if (value.phase === "loading") {
486
+ return React.createElement("div", { style: styles.root },
487
+ React.createElement("div", { style: styles.hint }, "正在体检已装插件……"));
488
+ }
489
+ if (value.phase === "error") {
490
+ return React.createElement("div", { style: styles.root },
491
+ React.createElement("div", { style: styles.err },
492
+ React.createElement("div", { style: { fontWeight: 600, marginBottom: "4px" } }, "拿不到体检结果"),
493
+ React.createElement("div", null, value.error),
494
+ React.createElement("div", { style: { marginTop: "6px", opacity: 0.75 } },
495
+ "这一条是工具本身的问题,不是插件的结论 —— 请不要据此判断兼容性。")),
496
+ React.createElement("button", {
497
+ style: styles.btn, type: "button",
498
+ onClick: function () { st.reload(true); },
499
+ }, "重试"));
500
+ }
501
+ var data = value.data;
502
+ if (!data || data.ok === false) {
503
+ return React.createElement("div", { style: styles.root },
504
+ React.createElement("div", { style: styles.err },
505
+ React.createElement("div", { style: { fontWeight: 600, marginBottom: "4px" } }, "体检没跑成"),
506
+ React.createElement("div", null, (data && data.error) || "未知原因")));
507
+ }
508
+ return React.createElement(Table, { data: data, onReload: function () { st.reload(true); } });
509
+ }
510
+
511
+ function apply(ctx) {
512
+ console.log("[dsh-plugin-compat-check] CLIENT-HALF-APPLIED");
513
+ // 最外层 try:注册失败也**绝不能**把客户端启动带崩 ——
514
+ // 那正好是本工具要检测的那类故障(健康检查插件自己制造故障)。
515
+ try {
516
+ registerSlot(ctx, SLOT, { name: SLOT, id: "dsh-plugin-compat-check", order: 1, label: LABEL }, Panel, "tab");
517
+ // C3:**详情页**的两个槽位。各自独立注册 —— 一个槽位在某个版本上不存在时,
518
+ // 另一个照常工作,而不是一起静默消失。
519
+ //
520
+ // ⚠️ 为什么不注册 `plugins.item`(曾经注册过):
521
+ // 实测它的 props 是 `{view:"summary"}`,**没有插件身份**(宿主用
522
+ // `renderSlot("plugins.item", {view}, {only: entryId})` 限定,只对匹配的
523
+ // 条目渲染,而那个 entryId 是**配置页注册时给的 id**,不是包名)。
524
+ // 也就是说它加不到"社区插件卡"上 —— 想给某插件的卡片加角标,需要知道那个
525
+ // 卡片的 entry id,而我们无从得知。硬撑只会得到一个永不出现的组件,
526
+ // 或者一个挂在自己配置页上的角标。**概览本来就有地方放**:
527
+ // `settings.plugins.tab`(设置 → 内置插件 → 兼容性)。
528
+ registerSlot(ctx, SLOT_DETAIL, { name: SLOT_DETAIL, id: "dsh-plugin-compat-check/detail", order: 1 }, DetailSection, "detail");
529
+ registerSlot(ctx, SLOT_DETAIL_BADGE, { name: SLOT_DETAIL_BADGE, id: "dsh-plugin-compat-check/detail-badge", order: 1 }, DetailBadge, "detail-badge");
530
+ } catch (error) {
531
+ console.warn("[dsh-plugin-compat-check] REGISTER-THREW (outer) " + String(error));
532
+ }
533
+ }
534
+
535
+ var registered = [];
536
+
537
+ /**
538
+ * 注册一个槽位,并把结果**记在标记里**。
539
+ *
540
+ * 为什么要记账:注册失败必须是**响的**。静默失败会让人以为"这个槽位不渲染",
541
+ * 而真相是"我们没注册上" —— 两者要查的方向完全不同。实测(Plan.md §5 C3)
542
+ * 三个 detail 槽位都真的渲染,所以一旦它们不出现,第一个该怀疑的是注册。
543
+ */
544
+ function registerSlot(ctx, slot, spec, Comp, tag) {
545
+ try {
546
+ ctx.slots.inject(slot, function () {
547
+ return ctx.slots.register(spec, Comp);
548
+ });
549
+ registered.push(slot);
550
+ console.log("[dsh-plugin-compat-check] REGISTERED " + slot);
551
+ } catch (error) {
552
+ console.warn("[dsh-plugin-compat-check] REGISTER-THREW " + slot + " " + String(error));
553
+ }
554
+ mark({ registered: registered.slice() });
555
+ }
556
+
557
+ exports.name = "dsh-plugin-compat-check";
558
+ exports.apply = apply;
559
+ exports.inject = ["slots"];
560
+ return module.exports;
561
+ },
562
+ });
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "dsh-plugin-compat-check",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Reports which installed DSH plugins are compatible with the current DSH version, with file:line and reason. The judgement kernel is staged into kernel/ at pack time, so the published tarball is self-contained.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": {
11
+ "default": "./lib/client.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "platform": "web"
22
+ }
23
+ },
24
+ "files": [
25
+ "index.js",
26
+ "README.md",
27
+ "skill/",
28
+ "lib/",
29
+ "cordis.patch.yml",
30
+ "kernel/",
31
+ "scripts/"
32
+ ],
33
+ "engines": {
34
+ "node": "^22.19.0 || >=24"
35
+ },
36
+ "scripts": {
37
+ "prepack": "node scripts/stage-skill.mjs && node scripts/stage-kernel.mjs",
38
+ "postpack": "node scripts/unstage-kernel.mjs",
39
+ "prepublishOnly": "node scripts/prepublish-check.mjs",
40
+ "stage-skill": "node scripts/stage-skill.mjs"
41
+ },
42
+ "keywords": [
43
+ "dsh",
44
+ "deepseek-harness",
45
+ "plugin",
46
+ "compatibility",
47
+ "health-check"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/lcy0121/dsh-plugin-compat-check.git"
52
+ },
53
+ "homepage": "https://github.com/lcy0121/dsh-plugin-compat-check#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/lcy0121/dsh-plugin-compat-check/issues"
56
+ }
57
+ }