@noob-stupid/dsh-plugin-console 0.5.32 → 0.5.34

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/README.md CHANGED
@@ -4,12 +4,44 @@
4
4
 
5
5
  ---
6
6
 
7
- <img width="1170" height="609" alt="image" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
8
-
9
7
  # DSH Plugin Gating Hub (dsh-plugin-gating-hub)
10
8
 
11
9
  *Renamed from `dsh-plugin-hub` — old URLs redirect.*
12
10
 
11
+ **Framework-upgrade safety and plugin-version gating for DSH.** A failed upgrade rolls back.
12
+ A framework changed by **any** update channel is still guarded. A version the host cannot
13
+ take is refused, not installed.
14
+
15
+ **Install (npm, one line)**
16
+
17
+ ```sh
18
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
19
+ ```
20
+
21
+ Then restart the dsh service → refresh the page → **Settings → Plugins → Plugin Console**.
22
+
23
+ ![Upgrade safety panel, expanded in place](docs/images/upgrade-safety-panel.png)
24
+
25
+ **Three hard guarantees**
26
+
27
+ - 🛡️ **Contract pre-check → rollback point → auto-rollback.** A data-driven **contract pre-check**
28
+ runs first (message contract / dependency API / config schema / removed API / loader contract);
29
+ then config backup + a full-tree **rollback point**. A failed install or relaunch **auto-rolls
30
+ the tree back**, a 「roll back to previous」 button restores the last version, and plugins the
31
+ new framework cannot load are **auto-disabled** — version check catches fake success, 15-min
32
+ timeout + stall detection.
33
+ - 👁️ **Even when another update channel changes the framework.** An **environment fingerprint**
34
+ catches changes made by the official desktop updater or a manual `pnpm`: it auto-runs a
35
+ **read-only pre-flight**, and disables a row **only with hard evidence** (package unresolvable /
36
+ `file://` target gone / named in a boot-failure log). `@deepseek-ai/*` and **protected / core
37
+ rows are never auto-disabled**; each auto-disable writes a one-command recovery record.
38
+ - 🔒 **Plugin install & upgrade gating.** Version/declaration gate (`dsh.engines.framework` /
39
+ `engines.dsh` / `@deepseek-ai/*` ranges, built-in zero-dependency semver), a **repo-size gate**
40
+ on source channels, and explicit handling of **non-registry sources** — a bare version is
41
+ written back only once the registry really resolves it, otherwise the package is materialized
42
+ as `link:`. Install scripts are never auto-authorized, and pnpm's supply-chain gate is never
43
+ bypassed.
44
+
13
45
  [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
14
46
  [![Awesome DSH Plugin](https://awesome-dsh-plugin.com/badge.svg)](https://awesome-dsh-plugin.com)
15
47
  [![GitHub stars](https://img.shields.io/github/stars/Noob-stupid/dsh-plugin-gating-hub?style=flat-square&logo=github)](https://github.com/Noob-stupid/dsh-plugin-gating-hub/stargazers)
@@ -22,13 +54,11 @@
22
54
  [![GitHub Release](https://img.shields.io/github/v/release/Noob-stupid/dsh-plugin-gating-hub?style=flat-square)](https://github.com/Noob-stupid/dsh-plugin-gating-hub/releases)[![dsh.so security](https://www.dsh.so/badge/dsh-plugin-hub.svg)](https://www.dsh.so/artifact/dsh-plugin-hub)
23
55
  [![dsh.so install](https://www.dsh.so/badge/install/dsh-plugin-hub.svg)](https://www.dsh.so/artifact/dsh-plugin-hub)
24
56
 
25
- > **Framework upgrade safety & plugin version gating for DeepSeek Harness (DSH)**: one-click
26
- > framework upgrade with **auto-rollback on failure** → **one-click rollback to the previous
27
- > version** after an upgrade → plugins the new framework cannot load are **auto-disabled** →
28
- > the **plugin upgrade gate** refuses a version the host can't take.
29
- > A **built-in multi-source plugin market & index** (500+ plugins / 300+ skills, zero GitHub
30
- > API calls) rides on top as the **discovery layer** — and **every source is swappable**: install
31
- > source (incl. a private intranet registry), search source (URL template + headers), index source
57
+ <img width="1170" height="609" alt="DSH Plugin Gating Hub" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
58
+
59
+ > **Discovery layer on top**: a **built-in multi-source plugin market & index** (500+ plugins /
60
+ > 300+ skills, zero GitHub API calls) — and **every source is swappable**: install source
61
+ > (incl. a private intranet registry), search source (URL template + headers), index source
32
62
  > (self-hosted intranet index), Git source (incl. a local `file://` bare repo), so plugins can be
33
63
  > installed on an **intranet-only or fully offline** machine.
34
64
 
@@ -385,7 +415,7 @@ Verification matrix, raw logs and the one framework-side caveat (`0.1.2-rc.1` sh
385
415
 
386
416
  ---
387
417
 
388
- ##帮助
418
+ ## Help & Ecosystem
389
419
 
390
420
  - **Panel missing**: restart dsh → refresh → Settings → Plugins → Plugin Console.
391
421
  - **Toggle does nothing**: infrastructure rows are "Protected" (by design); normal toggles
package/README.zh.md CHANGED
@@ -4,15 +4,41 @@
4
4
 
5
5
  ---
6
6
 
7
- <img width="1170" height="609" alt="image" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
8
-
9
7
  # DSH 插件门控中心(dsh-plugin-gating-hub)
10
8
 
11
9
  *原名 `dsh-plugin-hub`,已改名;旧链接 301 跳转有效。*
12
10
 
13
- > **DSH 框架升级安全与插件升级门控**:一键框架升级(**失败自动回滚**)→ 升级后**一键回滚到
14
- > 上一版** → 新框架带不动的插件**自动禁用** → **插件升级门控**按版本/声明拒绝装不上的插件版本。
15
- > **内置多源插件市场与自定义索引**(500+ 插件 / 300+ 技能,零 GitHub API 调用)作为**发现层**——
11
+ **DSH 的框架升级安全 + 插件升级门控。** 升级失败自动回滚。**别的通道**改了框架也照样守门。
12
+ 宿主带不动的插件版本直接拒绝,不会装上。
13
+
14
+ **安装(npm,一行)**
15
+
16
+ ```sh
17
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
18
+ ```
19
+
20
+ 然后重启 dsh 服务 → 刷新页面 → **设置 → 插件 → 插件控制台**。
21
+
22
+ ![「升级安全」面板(就地展开)](docs/images/upgrade-safety-panel.png)
23
+
24
+ **三条最硬的能力**
25
+
26
+ - 🛡️ **升级前契约预检 → 回滚点 → 失败自动回滚。** 先跑数据化的**契约预检**(消息契约 /
27
+ 依赖 API / 配置 schema / 被删 API / 装载器契约),再做配置备份 + 全树**回滚点**;
28
+ 安装或拉起失败即**全树自动回滚**,升级成功后还有「回滚到上一版」按钮,
29
+ 新框架带不动的插件**自动禁用** —— 版本校验防假成功,15 分钟硬超时 + 卡死检测。
30
+ - 👁️ **别的更新通道改了框架,也守门。** **环境指纹**能发现官方桌面端更新器或手动 `pnpm`
31
+ 造成的框架变更:变化即自动跑一次**只读预检**,且**有确证证据才**禁用某行
32
+ (包确证解析不到 / `file://` 目标确实不存在 / 启动失败日志点名);`@deepseek-ai/*`(框架自带)
33
+ 与**受保护 / 核心行永不禁用**,每次自动禁用都留一条可读的恢复命令。
34
+ - 🔒 **插件安装与升级门控。** 版本/声明门控(`dsh.engines.framework` / `engines.dsh` /
35
+ `@deepseek-ai/*` 范围,内置零依赖 semver)+ 源码通道的**体积门禁** + **非 registry 来源**的
36
+ 显式处理(只有在 registry 真能解析到该版本时才写回裸版本号,否则物化成 `link:`);
37
+ 安装脚本**绝不自动放行**,pnpm 的供应链闸**绝不绕过**。
38
+
39
+ <img width="1170" height="609" alt="DSH 插件门控中心" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
40
+
41
+ > **上层还带一个发现层**:**内置多源插件市场与自定义索引**(500+ 插件 / 300+ 技能,零 GitHub API 调用)——
16
42
  > 而且**每一条源都能换成你自己的**:安装源(含内网私有 registry)、搜索源(URL 模板 + 请求头)、
17
43
  > 索引源(内网自建索引)、Git 源(含 `file://` 本地裸仓库),**纯内网 / 断网也能装插件**。
18
44
 
package/lib/client.js CHANGED
@@ -208,6 +208,9 @@ window.__ModuleLoader__.load({
208
208
  riskyEnableConfirm: "确认强行启用",
209
209
  riskyEnableCancel: "取消",
210
210
  adoptableDetected: "检测到已适配:当前版本 {version} 已通过源码扫描,点「已适配,立即解锁」即可启用(不会自动开启)",
211
+ adoptableRescinded: "判据已更新:旧版扫描给出的「不兼容」结论已撤回(当前判据下源码扫描干净,版本不变也算)。点「已适配,立即解锁」即可启用(不会自动开启)",
212
+ compatRescindedTag: "判据已撤回·可解锁",
213
+ compatRescindedHint: "这条禁用是旧判据的误报:当前判据重算后不再判「不兼容」。点上方「已适配,立即解锁」即可启用;补丁由你确认后才改。",
211
214
  compatGateLabel: "兼容门",
212
215
  compatGateAutoDisable: "升级时自动禁用不适配插件",
213
216
  compatGateAutoDetect: "打开时自动检测已适配(仅提示)",
@@ -256,7 +259,6 @@ window.__ModuleLoader__.load({
256
259
  unloading: "卸载中",
257
260
  toggledOn: "已请求启用",
258
261
  toggledOff: "已请求停用",
259
- failed: "操作失败",
260
262
  view: "查看",
261
263
  install: "添加并启用",
262
264
  installing: "正在安装…",
@@ -527,7 +529,6 @@ window.__ModuleLoader__.load({
527
529
  sourcesUpdated: "软件源已更新",
528
530
  sourceOf: "源:",
529
531
  searchSourceTitle: "点击切换搜索源(GitHub / Gitee / 自定义)",
530
- recentFailures: "最近安装失败",
531
532
  installedAt: "已安装",
532
533
  checkUpdate: "检测更新",
533
534
  checkingUpdate: "检测中…",
@@ -762,6 +763,9 @@ window.__ModuleLoader__.load({
762
763
  riskyEnableConfirm: "Force enable",
763
764
  riskyEnableCancel: "Cancel",
764
765
  adoptableDetected: "Now compatible: version {version} passed the source scan — click \"Adapted - unlock now\" to enable it (never auto-enabled)",
766
+ adoptableRescinded: "Criteria updated: the older scan's \"incompatible\" verdict has been withdrawn (the current criteria find the source clean even without a version change). Click \"Adapted - unlock now\" to enable it (never auto-enabled)",
767
+ compatRescindedTag: "Verdict withdrawn - unlockable",
768
+ compatRescindedHint: "This disable came from a false positive of the older criteria: recomputing with the current criteria no longer says \"incompatible\". Click \"Adapted - unlock now\" above; your patch is only changed after you confirm.",
765
769
  compatGateLabel: "Compat gate",
766
770
  compatGateAutoDisable: "Auto-disable incompatible plugins on upgrade",
767
771
  compatGateAutoDetect: "Detect adapted plugins on open (hint only)",
@@ -810,7 +814,6 @@ window.__ModuleLoader__.load({
810
814
  unloading: "Unloading",
811
815
  toggledOn: "Enable requested",
812
816
  toggledOff: "Disable requested",
813
- failed: "Operation failed",
814
817
  view: "Inspect",
815
818
  install: "Add & enable",
816
819
  installing: "Installing…",
@@ -1077,7 +1080,6 @@ window.__ModuleLoader__.load({
1077
1080
  sourcesUpdated: "Sources updated",
1078
1081
  sourceOf: "Source: ",
1079
1082
  searchSourceTitle: "Click to switch search source (GitHub / Gitee / custom)",
1080
- recentFailures: "Recent install failures",
1081
1083
  installedAt: "Installed",
1082
1084
  checkUpdate: "Check update",
1083
1085
  checkingUpdate: "Checking…",
@@ -1229,6 +1231,21 @@ window.__ModuleLoader__.load({
1229
1231
  function phaseLabel(phase, t) {
1230
1232
  return phase === null ? t("unobserved") : t(PHASE_KEYS[phase]);
1231
1233
  }
1234
+ // 0.5.33 改错 ①-2:「已适配,立即解锁」按钮的门控判据 —— 必须与同一卡片正文用的是**同一个**判据。
1235
+ // 现症:按钮只判 pendingCompat === true,不判 entry.adoptable → 正文按 adoptable 说"不适配",
1236
+ // 按钮按 pendingCompat 说"已适配",自相矛盾;点下去 POST /adapt-unlock 必得 409(适配校验 fail),
1237
+ // 补丁与行状态零变化 ⇒ 用户观感"点了没反应"。
1238
+ // 现在只有**真的检测到已适配**(adoptable 是服务端 detectAdoptablePending 给出的对象)才出按钮;
1239
+ // 其余情况不给这个承诺,原因照旧由正文那行(pendingCompatHint + checkNote)如实展示。
1240
+ // 0.5.34 加法:`compatRescinded`(记录里的 fail 是旧判据的误报、已被当前判据撤回)同样给一键解锁入口 ——
1241
+ // 这类行**没有版本变化**,旧判据要求"版本变化"会让用户被永久钉在「待适配」(真机 dshmarket 就是这样)。
1242
+ // 判据本身没放宽:adoptable 仍是服务端算出来的对象(真 fail 的行根本拿不到它)。
1243
+ function canAdaptUnlock(entry) {
1244
+ return entry !== null && entry !== undefined
1245
+ && (entry.pendingCompat === true || entry.compatRescinded === true)
1246
+ && entry.adoptable !== null && entry.adoptable !== undefined
1247
+ && typeof entry.adoptable === "object";
1248
+ }
1232
1249
  async function call(path, body) {
1233
1250
  const response = await fetch(path, body === undefined
1234
1251
  ? {}
@@ -1273,7 +1290,7 @@ window.__ModuleLoader__.load({
1273
1290
  if (reload) setMessage(t("restarting"));
1274
1291
  call("/plugin-console/restart", {}).then(
1275
1292
  () => { if (reload) window.setTimeout(() => window.location.reload(), 12000); },
1276
- (error) => { setMessage(error !== null && typeof error.message === "string" && error.message !== "" ? error.message : t("failed")); },
1293
+ (error) => { setMessage(error !== null && typeof error.message === "string" && error.message !== "" ? error.message : t("safetySwitchFailed")); },
1277
1294
  );
1278
1295
  }
1279
1296
  /** 超时类错误附上可操作提示(网络黑洞期的最常见表现)。 */
@@ -1718,7 +1735,7 @@ window.__ModuleLoader__.load({
1718
1735
  setFwClearing(true);
1719
1736
  call("/plugin-console/framework-status-clear", {}).then(
1720
1737
  () => { setFrameworkStatus({ status: "idle", message: null }); setFwStatusDismissed(true); setMessage(t("fwRecordCleared")); },
1721
- (error) => { setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1738
+ (error) => { setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
1722
1739
  ).finally(() => setFwClearing(false));
1723
1740
  };
1724
1741
  const fwStatusRefresh = () => {
@@ -1736,7 +1753,7 @@ window.__ModuleLoader__.load({
1736
1753
  setFwCheckBusy(true);
1737
1754
  call("/plugin-console/framework-check", refresh === true ? { refresh: true } : {}).then(
1738
1755
  (data) => { setFwCheckBusy(false); if (data && data.ok === true) setFwCheck(data); },
1739
- (error) => { setFwCheckBusy(false); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1756
+ (error) => { setFwCheckBusy(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
1740
1757
  );
1741
1758
  };
1742
1759
  // 升级前契约预检:先只读预检(抓目标版本会话格式契约 + 扫本地生产方),
@@ -1753,7 +1770,7 @@ window.__ModuleLoader__.load({
1753
1770
  setFwPreflightBusy(false);
1754
1771
  setFwPreflight((prev) => (apply === true ? { report: (prev && prev.report) ?? null, patch: data } : { report: data, patch: null }));
1755
1772
  },
1756
- (error) => { setFwPreflightBusy(false); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1773
+ (error) => { setFwPreflightBusy(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
1757
1774
  );
1758
1775
  };
1759
1776
  const fwShortPath = (p) => String(p ?? "").replace(/\\/gu, "/").split("/").slice(-3).join("/");
@@ -1766,7 +1783,7 @@ window.__ModuleLoader__.load({
1766
1783
  setFrameworkStatus({ status: "rollback", message: t("frameworkRollbackDone").replace("{from}", String(data?.from ?? "?")) });
1767
1784
  pollFrameworkStatus();
1768
1785
  },
1769
- (error) => { setFrameworkUpgrading(false); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1786
+ (error) => { setFrameworkUpgrading(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
1770
1787
  );
1771
1788
  };
1772
1789
  // 回滚按钮可用性:已经处在「回滚目标版本」时不再提供回滚(点了等于恢复现状)。
@@ -2013,10 +2030,10 @@ window.__ModuleLoader__.load({
2013
2030
  setAiJob({ jobId: data.jobId, source, status: "running", stage: "planning" });
2014
2031
  aiPoll(data.jobId);
2015
2032
  }
2016
- else { setAiRunning(false); setMessage(t("failed") + ":" + (data?.error ?? "未知")); }
2033
+ else { setAiRunning(false); setMessage(t("safetySwitchFailed") + ":" + (data?.error ?? "未知")); }
2017
2034
  } catch (error) {
2018
2035
  setAiRunning(false);
2019
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2036
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2020
2037
  }
2021
2038
  }
2022
2039
  async function aiRun() {
@@ -2035,7 +2052,7 @@ window.__ModuleLoader__.load({
2035
2052
  aiPoll(aiJob.jobId);
2036
2053
  } catch (error) {
2037
2054
  setAiRunning(false);
2038
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2055
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2039
2056
  }
2040
2057
  }
2041
2058
  async function aiCancel() {
@@ -2056,7 +2073,7 @@ window.__ModuleLoader__.load({
2056
2073
  const data = await call("/plugin-console/component/" + action, { id });
2057
2074
  setMessage(t("compActionDone").replace("{action}", t(action === "start" ? "compStart" : "compStop")).replace("{name}", data?.name ?? id));
2058
2075
  } catch (error) {
2059
- setMessage(t("failed") + ":" + error.message);
2076
+ setMessage(t("safetySwitchFailed") + ":" + error.message);
2060
2077
  }
2061
2078
  setCompBusy(null);
2062
2079
  refreshComps();
@@ -2069,7 +2086,7 @@ window.__ModuleLoader__.load({
2069
2086
  setMessage(data.running ? (data.healthy ? t("compHealthy") : t("compUnhealthy")) + ":" + (data.name ?? id) : t("compStopped") + ":" + (data.name ?? id));
2070
2087
  }
2071
2088
  } catch (error) {
2072
- setMessage(t("failed") + ":" + error.message);
2089
+ setMessage(t("safetySwitchFailed") + ":" + error.message);
2073
2090
  }
2074
2091
  setCompBusy(null);
2075
2092
  refreshComps();
@@ -2088,7 +2105,7 @@ window.__ModuleLoader__.load({
2088
2105
  await call("/plugin-console/component/autostart", { id, enabled: on });
2089
2106
  setMessage((on ? "✔ 已开启自启动:" : "已关闭自启动:") + id);
2090
2107
  } catch (error) {
2091
- setMessage(t("failed") + ":" + error.message);
2108
+ setMessage(t("safetySwitchFailed") + ":" + error.message);
2092
2109
  }
2093
2110
  setCompBusy(null);
2094
2111
  refreshComps();
@@ -2500,7 +2517,7 @@ window.__ModuleLoader__.load({
2500
2517
  setRiskyConfirm({ entryId: entry.entryId, moduleName: details.moduleName ?? entry.moduleName ?? null, frameworkVersion: details.frameworkVersion ?? null, checkNote: details.checkNote ?? null });
2501
2518
  return;
2502
2519
  }
2503
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2520
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2504
2521
  },
2505
2522
  ).finally(() => setBusy(null));
2506
2523
  };
@@ -2513,7 +2530,7 @@ window.__ModuleLoader__.load({
2513
2530
  : prev));
2514
2531
  setMessage(t("compatGateSaved"));
2515
2532
  },
2516
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
2533
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
2517
2534
  );
2518
2535
  };
2519
2536
  const levenshtein = (a, b) => {
@@ -2704,7 +2721,7 @@ window.__ModuleLoader__.load({
2704
2721
  },
2705
2722
  (error) => {
2706
2723
  setLoadingMore(false);
2707
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2724
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2708
2725
  },
2709
2726
  );
2710
2727
  return;
@@ -2728,7 +2745,7 @@ window.__ModuleLoader__.load({
2728
2745
  },
2729
2746
  (error) => {
2730
2747
  setLoadingMore(false);
2731
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2748
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2732
2749
  },
2733
2750
  );
2734
2751
  return;
@@ -2746,7 +2763,7 @@ window.__ModuleLoader__.load({
2746
2763
  },
2747
2764
  (error) => {
2748
2765
  setLoadingMore(false);
2749
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2766
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2750
2767
  },
2751
2768
  );
2752
2769
  return;
@@ -2784,7 +2801,7 @@ window.__ModuleLoader__.load({
2784
2801
  },
2785
2802
  (error) => {
2786
2803
  setLoadingMore(false);
2787
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
2804
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
2788
2805
  },
2789
2806
  ),
2790
2807
  );
@@ -2953,7 +2970,7 @@ window.__ModuleLoader__.load({
2953
2970
  window.setTimeout(() => window.location.reload(), 2500);
2954
2971
  }
2955
2972
  } else {
2956
- setMessage(t("failed") + ":" + (data.error ?? "未知错误") + diagMessage(data));
2973
+ setMessage(t("safetySwitchFailed") + ":" + (data.error ?? "未知错误") + diagMessage(data));
2957
2974
  }
2958
2975
  setInstalling(null);
2959
2976
  setTimeout(refresh, 1500);
@@ -2966,8 +2983,8 @@ window.__ModuleLoader__.load({
2966
2983
  /** 本地 AI 兜底授权:调用模型 API 产生费用,必须用户明确同意。 */
2967
2984
  const aiConsent = (jobId, approved) => {
2968
2985
  call("/plugin-console/ai-consent", { jobId, approved }).then(
2969
- () => { if (!approved) setMessage(t("failed") + ":已取消本地 AI 兜底(不会调用模型 API)"); },
2970
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
2986
+ () => { if (!approved) setMessage(t("safetySwitchFailed") + ":已取消本地 AI 兜底(不会调用模型 API)"); },
2987
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
2971
2988
  );
2972
2989
  };
2973
2990
  /** 安装进度文案:服务端 progress{channel,phase,index,total,name,done} → 一行人话。
@@ -3127,7 +3144,7 @@ window.__ModuleLoader__.load({
3127
3144
  // clientId 打码回显(如 abc12345…):用户未修改时原样传回服务端,服务端识别打码标记保留原配置
3128
3145
  const idValue = giteeClientId.trim();
3129
3146
  if (!idValue || !giteeClientSecret.trim()) {
3130
- setMessage(t("failed") + ":client_id / client_secret 不能为空");
3147
+ setMessage(t("safetySwitchFailed") + ":client_id / client_secret 不能为空");
3131
3148
  return;
3132
3149
  }
3133
3150
  if (idValue.endsWith("…")) {
@@ -3139,7 +3156,7 @@ window.__ModuleLoader__.load({
3139
3156
  const startGiteeOauth = () => {
3140
3157
  call("/plugin-console/gitee-oauth-url").then(
3141
3158
  (data) => { if (data && data.url) window.open(data.url, "_blank", "noopener"); },
3142
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
3159
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
3143
3160
  );
3144
3161
  };
3145
3162
  const sourcesAction = (payload, done) => {
@@ -3151,12 +3168,12 @@ window.__ModuleLoader__.load({
3151
3168
  setMessage(t("sourcesUpdated"));
3152
3169
  if (done) done();
3153
3170
  },
3154
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
3171
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
3155
3172
  ).finally(() => setSourcesBusy(false));
3156
3173
  };
3157
3174
  const addSource = () => {
3158
3175
  if (!isAllowedSourceUrl(sourceUrl.trim())) {
3159
- setMessage(t("failed") + ":" + t("invalidSourceUrl"));
3176
+ setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
3160
3177
  return;
3161
3178
  }
3162
3179
  sourcesAction({ action: "add", name: sourceName.trim(), url: sourceUrl.trim() }, () => { setSourceName(""); setSourceUrl(""); });
@@ -3166,7 +3183,7 @@ window.__ModuleLoader__.load({
3166
3183
  setRegScan({ status: "scanning" });
3167
3184
  call("/plugin-console/registry-scan", {}).then(
3168
3185
  (data) => setRegScan({ status: "ready", results: Array.isArray(data.results) ? data.results : [], scannedAt: typeof data.scannedAt === "number" ? data.scannedAt : null }),
3169
- (error) => { setRegScan(null); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
3186
+ (error) => { setRegScan(null); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
3170
3187
  );
3171
3188
  };
3172
3189
  // 扫描结果派生视图:每条源的命中结果 + 可达计数 + 最新版本来自哪个源
@@ -3190,7 +3207,7 @@ window.__ModuleLoader__.load({
3190
3207
  };
3191
3208
  const addIndexSource = () => {
3192
3209
  if (!isAllowedSourceUrl(indexSourceUrl.trim())) {
3193
- setMessage(t("failed") + ":" + t("invalidSourceUrl"));
3210
+ setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
3194
3211
  return;
3195
3212
  }
3196
3213
  sourcesAction({ action: "add-index", name: indexSourceName.trim(), url: indexSourceUrl.trim() }, () => { setIndexSourceName(""); setIndexSourceUrl(""); });
@@ -3213,11 +3230,11 @@ window.__ModuleLoader__.load({
3213
3230
  const addGitSource = () => {
3214
3231
  const tpl = gitSourceUrl.trim();
3215
3232
  if (!tpl.includes("{owner}") || !tpl.includes("{repo}")) {
3216
- setMessage(t("failed") + ":" + t("gitUrlPlaceholder"));
3233
+ setMessage(t("safetySwitchFailed") + ":" + t("gitUrlPlaceholder"));
3217
3234
  return;
3218
3235
  }
3219
3236
  if (!isAllowedSourceUrl(tpl)) {
3220
- setMessage(t("failed") + ":" + t("invalidSourceUrl"));
3237
+ setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
3221
3238
  return;
3222
3239
  }
3223
3240
  sourcesAction({ action: "add-git", name: gitSourceName.trim(), urlTemplate: tpl }, () => { setGitSourceName(""); setGitSourceUrl(""); });
@@ -3241,7 +3258,7 @@ window.__ModuleLoader__.load({
3241
3258
  };
3242
3259
  const addSearchSource = () => {
3243
3260
  if (!isAllowedSourceUrl(searchSourceUrl.trim()) || !searchSourceUrl.includes("{q}")) {
3244
- setMessage(t("failed") + ":" + t("invalidSearchUrl"));
3261
+ setMessage(t("safetySwitchFailed") + ":" + t("invalidSearchUrl"));
3245
3262
  return;
3246
3263
  }
3247
3264
  // 解析请求头:每行 "名称: 值"
@@ -3281,7 +3298,7 @@ window.__ModuleLoader__.load({
3281
3298
  }
3282
3299
  },
3283
3300
  (error) => {
3284
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
3301
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
3285
3302
  setInstalling(null);
3286
3303
  },
3287
3304
  );
@@ -3353,7 +3370,7 @@ window.__ModuleLoader__.load({
3353
3370
  },
3354
3371
  (error) => {
3355
3372
  setTogglingSkill(null);
3356
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
3373
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
3357
3374
  },
3358
3375
  );
3359
3376
  };
@@ -3404,7 +3421,7 @@ window.__ModuleLoader__.load({
3404
3421
  (error) => {
3405
3422
  setFrameworkUpgrading(false);
3406
3423
  setConfirmFrameworkUpgrade(false);
3407
- setMessage(t("failed") + ":" + friendlyGithubError(error).message);
3424
+ setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
3408
3425
  },
3409
3426
  );
3410
3427
  };
@@ -3416,7 +3433,7 @@ window.__ModuleLoader__.load({
3416
3433
  setConfirmSkillDelete(null);
3417
3434
  setInstalledSkills((prev) => prev.filter((s) => s.name !== name));
3418
3435
  },
3419
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
3436
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
3420
3437
  );
3421
3438
  };
3422
3439
  const installedQueryNorm = installedQuery.trim().toLowerCase();
@@ -3470,7 +3487,7 @@ window.__ModuleLoader__.load({
3470
3487
  el("p", { className: styles.status }, t("loadingDetails")));
3471
3488
  } else if (details.status === "error") {
3472
3489
  detailPanel = el("div", { className: styles.detail },
3473
- el("p", { className: styles.status, "data-error": "true" }, t("failed") + ":" + details.error.message));
3490
+ el("p", { className: styles.status, "data-error": "true" }, t("safetySwitchFailed") + ":" + details.error.message));
3474
3491
  } else {
3475
3492
  const meta = details.data.meta;
3476
3493
  const readme = details.data.readme;
@@ -3482,7 +3499,11 @@ window.__ModuleLoader__.load({
3482
3499
  entry.pendingCompat === true
3483
3500
  ? el("p", { className: styles.status, "data-pending": entry.adoptable ? "false" : "true" },
3484
3501
  entry.adoptable
3485
- ? t("adoptableDetected").replace("{version}", String(entry.adoptable.version ?? "?")).replace("{prev}", String(compatInfo?.version ?? "?"))
3502
+ ? (entry.adoptable.basis === "verdict-rescinded"
3503
+ // 0.5.34:旧判据的误报已被当前判据撤回(真机 dshmarket 现场)——如实说是"判据更新",
3504
+ // 不能说成"插件已适配"(那会让人以为作者发过新版)
3505
+ ? t("adoptableRescinded").replace("{version}", String(entry.adoptable.version ?? "?"))
3506
+ : t("adoptableDetected").replace("{version}", String(entry.adoptable.version ?? "?")).replace("{prev}", String(compatInfo?.version ?? "?")))
3486
3507
  : t("pendingCompatHint") + (compatInfo && compatInfo.checkNote ? ";已知校验:" + compatInfo.checkNote : ""))
3487
3508
  : null,
3488
3509
  meta !== null && meta.version ? el("div", { className: styles.rowTop },
@@ -3499,7 +3520,7 @@ window.__ModuleLoader__.load({
3499
3520
  title: t("aiEmpowerTitle"),
3500
3521
  onClick: () => openAiFor(String(entry.moduleName ?? entry.repo ?? "")),
3501
3522
  }, t("aiEmpowerLabel")),
3502
- entry.pendingCompat === true
3523
+ canAdaptUnlock(entry)
3503
3524
  ? el("button", {
3504
3525
  type: "button",
3505
3526
  className: styles.toggle,
@@ -3514,7 +3535,7 @@ window.__ModuleLoader__.load({
3514
3535
  setMessage(t("adaptUnlockDone"));
3515
3536
  window.setTimeout(() => window.location.reload(), 1200);
3516
3537
  },
3517
- (error) => { setAdaptUnlocking(null); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
3538
+ (error) => { setAdaptUnlocking(null); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
3518
3539
  );
3519
3540
  },
3520
3541
  }, adaptUnlocking === entry.entryId ? t("loadingPhase") : t("adaptUnlockBtn"))
@@ -3586,15 +3607,23 @@ window.__ModuleLoader__.load({
3586
3607
  phaseLabel(entry.fiberPhase, t)),
3587
3608
  el("code", null, entry.entryId),
3588
3609
  ),
3610
+ // 0.5.33 加法 ①-4:框架 peer 预检**否决**的本地复算结果(服务端 entries[].veto)。
3611
+ // 框架只在内存里禁用、原因只打 stderr —— 这里把它补出来。复用**既有行**内的
3612
+ // status 短句位(不新增常驻面板行),出路放悬浮提示(复用既有 title 位置)。
3613
+ entry.veto && entry.veto.reason
3614
+ ? el("p", { className: styles.status, "data-pending": "true", title: entry.veto.hint ?? undefined },
3615
+ entry.veto.reason)
3616
+ : null,
3589
3617
  el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
3590
3618
  el("div", { className: styles.rowTop },
3591
3619
  el("button", {
3592
3620
  type: "button",
3593
3621
  className: styles.toggle,
3594
3622
  disabled: !entry.toggleable || busy === entry.entryId || entry.pendingCompat === true,
3595
- title: entry.pendingCompat === true ? t("pendingCompatHint") : undefined,
3623
+ // 0.5.34:结论已被当前判据撤回的行不再显示「待适配」(旧判据的误报不许当事实展示)
3624
+ title: entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedHint") : t("pendingCompatHint")) : undefined,
3596
3625
  onClick: () => toggle(entry, !entry.enabled),
3597
- }, busy === entry.entryId ? t("loadingPhase") : (entry.pendingCompat === true ? t("pendingCompatTag") : t(entry.enabled ? "off" : "on"))),
3626
+ }, busy === entry.entryId ? t("loadingPhase") : (entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedTag") : t("pendingCompatTag")) : t(entry.enabled ? "off" : "on"))),
3598
3627
  el("button", {
3599
3628
  type: "button",
3600
3629
  className: styles.toggle,
@@ -3623,7 +3652,7 @@ window.__ModuleLoader__.load({
3623
3652
  setTimeout(refresh, 1500);
3624
3653
  }
3625
3654
  },
3626
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
3655
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
3627
3656
  ).finally(() => setDeleteBusy(null));
3628
3657
  } else {
3629
3658
  setConfirmDeleteRowId(entry.entryId);
@@ -3679,7 +3708,7 @@ window.__ModuleLoader__.load({
3679
3708
  setFamilyCheck((prev) => ({ ...prev, [root]: { latest: d?.latest ?? null, installed } }));
3680
3709
  setMessage(t("familyUpdate") + ":" + root + " " + (d?.latest ?? "?") + (d?.migrate && d.migrate.compatible ? " → " + d.migrate.to + " v" + d.migrate.latest : "") + (d?.error ? "(" + d.error + ")" : ""));
3681
3710
  },
3682
- (e) => setMessage(t("failed") + ":" + friendlyGithubError(e).message),
3711
+ (e) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(e).message),
3683
3712
  );
3684
3713
  },
3685
3714
  }, t("familyCheckUpdate")),
@@ -3706,7 +3735,7 @@ window.__ModuleLoader__.load({
3706
3735
  setMessage(t("familyUnlockAllDone") + ":" + unlockedCount + " 行" + (Array.isArray(d?.kept) && d.kept.length > 0 ? ";保留 " + d.kept.length + " 行(未通过校验)" : ""));
3707
3736
  window.setTimeout(() => window.location.reload(), 1500);
3708
3737
  },
3709
- (e) => setMessage(t("failed") + ":" + friendlyGithubError(e).message),
3738
+ (e) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(e).message),
3710
3739
  );
3711
3740
  },
3712
3741
  }, t("familyUnlockAll")))
@@ -3765,7 +3794,7 @@ window.__ModuleLoader__.load({
3765
3794
  : t("pendingRestartNote") + ":" + (data?.packageName ?? "") + (data?.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
3766
3795
  refresh();
3767
3796
  },
3768
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
3797
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
3769
3798
  ).finally(() => setDeleteBusy(null));
3770
3799
  } else {
3771
3800
  setConfirmDeleteRowId(job.jobId);
@@ -4388,7 +4417,7 @@ window.__ModuleLoader__.load({
4388
4417
  ? el("div", { className: styles.rowTop },
4389
4418
  el("p", { className: styles.message, style: { flex: 1, minWidth: 0, wordBreak: "break-all" } }, t("repoLandDone") + repoLandResult.path),
4390
4419
  el("button", { type: "button", className: styles.toggle, onClick: repoLandOpenFolder }, t("repoLandOpenFolder")))
4391
- : el("p", { className: styles.message, "data-error": "true" }, t("failed") + ":" + (repoLandResult.error ?? "?")))
4420
+ : el("p", { className: styles.message, "data-error": "true" }, t("safetySwitchFailed") + ":" + (repoLandResult.error ?? "?")))
4392
4421
  : null,
4393
4422
  el("strong", { className: styles.name }, t("repoLandList") + "(" + repoLanded.length + ")"),
4394
4423
  repoLanded.length === 0
@@ -4415,7 +4444,7 @@ window.__ModuleLoader__.load({
4415
4444
  onClick: () => {
4416
4445
  call("/plugin-console/framework-relaunch", {}).then(
4417
4446
  (data) => { if (data && data.ok === true) setMessage(t("relaunchDone")); },
4418
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
4447
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
4419
4448
  );
4420
4449
  },
4421
4450
  }, t("relaunchBtn"))
@@ -4432,7 +4461,7 @@ window.__ModuleLoader__.load({
4432
4461
  const keptNote = keptCount > 0 ? ";保留在用插件 " + keptCount + " 个" : "";
4433
4462
  if (data && data.ok === true) setMessage(t("cleanResiduals") + ":" + (data.count === 0 ? "无残余" : "已清理 " + data.count + " 项") + keptNote);
4434
4463
  },
4435
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
4464
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
4436
4465
  );
4437
4466
  },
4438
4467
  }, "🧹 " + t("cleanResiduals")),
@@ -4472,10 +4501,10 @@ window.__ModuleLoader__.load({
4472
4501
  setMessage(t("cfDone").replace("{renamed}", String(renamed)).replace("{linked}", String(relinked)).replace("{removed}", String(removed)).replace("{mb}", String(freed))
4473
4502
  + (failed > 0 ? ";" + t("cfFailed").replace("{n}", String(failed)) : ""));
4474
4503
  },
4475
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
4504
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
4476
4505
  );
4477
4506
  },
4478
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
4507
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
4479
4508
  );
4480
4509
  },
4481
4510
  }, "🧰 " + t("cleanFrameworkResiduals")),
@@ -4559,10 +4588,10 @@ const lockWarn = data.lockUpdated === false && typeof data.lockNote === "string"
4559
4588
  setMessage((data.note ?? "已更新") + (lockWarn === "" ? "" : "。" + lockWarn) + ",正在重启生效…");
4560
4589
  restartServiceViaServer(setMessage, { reload: false });
4561
4590
  } else {
4562
- setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
4591
+ setMessage(t("safetySwitchFailed") + ":" + (data?.reason ?? "未知"));
4563
4592
  }
4564
4593
  },
4565
- (error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
4594
+ (error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
4566
4595
  );
4567
4596
  }
4568
4597
  }, "⬇ " + t("hubUpdateAvailable") + " v" + selfUpdate.latest) : null,
@@ -5146,7 +5175,7 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
5146
5175
  ? el("p", { className: styles.status }, t("loading"))
5147
5176
  : sourcesData.error !== undefined
5148
5177
  ? el("div", null,
5149
- el("p", { className: styles.status }, t("failed") + ":" + sourcesData.error),
5178
+ el("p", { className: styles.status }, t("safetySwitchFailed") + ":" + sourcesData.error),
5150
5179
  el("button", { type: "button", className: styles.toggle, onClick: () => { setSourcesData(null); openSources(); } }, t("retry")))
5151
5180
  : el("div", { className: styles.accWrap },
5152
5181
  accSection("registries", t("sourcesBtn"), [
@@ -5704,6 +5733,9 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
5704
5733
  // 仅测试用导出(tests/test-plugin-actions.mjs 的离线渲染断言:用假 React 真渲染这个组件,
5705
5734
  // 断言"有/无 suggestedAction、执行中、成功、失败、中英切换"五种情形的 DOM 结构)。
5706
5735
  exports.SuggestedActionBox = SuggestedActionBox;
5736
+ // 仅测试用导出(tests/test-adapt-unlock-gate.mjs ①-2:断言「已适配,立即解锁」按钮的门控判据,
5737
+ // 以及它必须与卡片正文用同一个判据)。
5738
+ exports.canAdaptUnlock = canAdaptUnlock;
5707
5739
  return module.exports;
5708
5740
  }
5709
5741
  });