@noob-stupid/dsh-plugin-console 0.5.32 → 0.5.33
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 +40 -10
- package/README.zh.md +31 -5
- package/lib/client.js +70 -52
- package/lib/server/domain/compat.js +4 -58
- package/lib/server/domain/peer-veto.js +103 -0
- package/lib/server/domain/settings-api-scan.js +251 -0
- package/lib/server/routes/plugins.js +3 -1
- package/lib/server/routes/state.js +17 -3
- package/package.json +1 -1
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
|
+

|
|
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://github.com/deepseek-ai/deepseek-harness)
|
|
14
46
|
[](https://awesome-dsh-plugin.com)
|
|
15
47
|
[](https://github.com/Noob-stupid/dsh-plugin-gating-hub/stargazers)
|
|
@@ -22,13 +54,11 @@
|
|
|
22
54
|
[](https://github.com/Noob-stupid/dsh-plugin-gating-hub/releases)[](https://www.dsh.so/artifact/dsh-plugin-hub)
|
|
23
55
|
[](https://www.dsh.so/artifact/dsh-plugin-hub)
|
|
24
56
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
+

|
|
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
|
@@ -256,7 +256,6 @@ window.__ModuleLoader__.load({
|
|
|
256
256
|
unloading: "卸载中",
|
|
257
257
|
toggledOn: "已请求启用",
|
|
258
258
|
toggledOff: "已请求停用",
|
|
259
|
-
failed: "操作失败",
|
|
260
259
|
view: "查看",
|
|
261
260
|
install: "添加并启用",
|
|
262
261
|
installing: "正在安装…",
|
|
@@ -527,7 +526,6 @@ window.__ModuleLoader__.load({
|
|
|
527
526
|
sourcesUpdated: "软件源已更新",
|
|
528
527
|
sourceOf: "源:",
|
|
529
528
|
searchSourceTitle: "点击切换搜索源(GitHub / Gitee / 自定义)",
|
|
530
|
-
recentFailures: "最近安装失败",
|
|
531
529
|
installedAt: "已安装",
|
|
532
530
|
checkUpdate: "检测更新",
|
|
533
531
|
checkingUpdate: "检测中…",
|
|
@@ -810,7 +808,6 @@ window.__ModuleLoader__.load({
|
|
|
810
808
|
unloading: "Unloading",
|
|
811
809
|
toggledOn: "Enable requested",
|
|
812
810
|
toggledOff: "Disable requested",
|
|
813
|
-
failed: "Operation failed",
|
|
814
811
|
view: "Inspect",
|
|
815
812
|
install: "Add & enable",
|
|
816
813
|
installing: "Installing…",
|
|
@@ -1077,7 +1074,6 @@ window.__ModuleLoader__.load({
|
|
|
1077
1074
|
sourcesUpdated: "Sources updated",
|
|
1078
1075
|
sourceOf: "Source: ",
|
|
1079
1076
|
searchSourceTitle: "Click to switch search source (GitHub / Gitee / custom)",
|
|
1080
|
-
recentFailures: "Recent install failures",
|
|
1081
1077
|
installedAt: "Installed",
|
|
1082
1078
|
checkUpdate: "Check update",
|
|
1083
1079
|
checkingUpdate: "Checking…",
|
|
@@ -1229,6 +1225,18 @@ window.__ModuleLoader__.load({
|
|
|
1229
1225
|
function phaseLabel(phase, t) {
|
|
1230
1226
|
return phase === null ? t("unobserved") : t(PHASE_KEYS[phase]);
|
|
1231
1227
|
}
|
|
1228
|
+
// 0.5.33 改错 ①-2:「已适配,立即解锁」按钮的门控判据 —— 必须与同一卡片正文用的是**同一个**判据。
|
|
1229
|
+
// 现症:按钮只判 pendingCompat === true,不判 entry.adoptable → 正文按 adoptable 说"不适配",
|
|
1230
|
+
// 按钮按 pendingCompat 说"已适配",自相矛盾;点下去 POST /adapt-unlock 必得 409(适配校验 fail),
|
|
1231
|
+
// 补丁与行状态零变化 ⇒ 用户观感"点了没反应"。
|
|
1232
|
+
// 现在只有**真的检测到已适配**(adoptable 是服务端 detectAdoptablePending 给出的对象)才出按钮;
|
|
1233
|
+
// 其余情况不给这个承诺,原因照旧由正文那行(pendingCompatHint + checkNote)如实展示。
|
|
1234
|
+
function canAdaptUnlock(entry) {
|
|
1235
|
+
return entry !== null && entry !== undefined
|
|
1236
|
+
&& entry.pendingCompat === true
|
|
1237
|
+
&& entry.adoptable !== null && entry.adoptable !== undefined
|
|
1238
|
+
&& typeof entry.adoptable === "object";
|
|
1239
|
+
}
|
|
1232
1240
|
async function call(path, body) {
|
|
1233
1241
|
const response = await fetch(path, body === undefined
|
|
1234
1242
|
? {}
|
|
@@ -1273,7 +1281,7 @@ window.__ModuleLoader__.load({
|
|
|
1273
1281
|
if (reload) setMessage(t("restarting"));
|
|
1274
1282
|
call("/plugin-console/restart", {}).then(
|
|
1275
1283
|
() => { if (reload) window.setTimeout(() => window.location.reload(), 12000); },
|
|
1276
|
-
(error) => { setMessage(error !== null && typeof error.message === "string" && error.message !== "" ? error.message : t("
|
|
1284
|
+
(error) => { setMessage(error !== null && typeof error.message === "string" && error.message !== "" ? error.message : t("safetySwitchFailed")); },
|
|
1277
1285
|
);
|
|
1278
1286
|
}
|
|
1279
1287
|
/** 超时类错误附上可操作提示(网络黑洞期的最常见表现)。 */
|
|
@@ -1718,7 +1726,7 @@ window.__ModuleLoader__.load({
|
|
|
1718
1726
|
setFwClearing(true);
|
|
1719
1727
|
call("/plugin-console/framework-status-clear", {}).then(
|
|
1720
1728
|
() => { setFrameworkStatus({ status: "idle", message: null }); setFwStatusDismissed(true); setMessage(t("fwRecordCleared")); },
|
|
1721
|
-
(error) => { setMessage(t("
|
|
1729
|
+
(error) => { setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
1722
1730
|
).finally(() => setFwClearing(false));
|
|
1723
1731
|
};
|
|
1724
1732
|
const fwStatusRefresh = () => {
|
|
@@ -1736,7 +1744,7 @@ window.__ModuleLoader__.load({
|
|
|
1736
1744
|
setFwCheckBusy(true);
|
|
1737
1745
|
call("/plugin-console/framework-check", refresh === true ? { refresh: true } : {}).then(
|
|
1738
1746
|
(data) => { setFwCheckBusy(false); if (data && data.ok === true) setFwCheck(data); },
|
|
1739
|
-
(error) => { setFwCheckBusy(false); setMessage(t("
|
|
1747
|
+
(error) => { setFwCheckBusy(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
1740
1748
|
);
|
|
1741
1749
|
};
|
|
1742
1750
|
// 升级前契约预检:先只读预检(抓目标版本会话格式契约 + 扫本地生产方),
|
|
@@ -1753,7 +1761,7 @@ window.__ModuleLoader__.load({
|
|
|
1753
1761
|
setFwPreflightBusy(false);
|
|
1754
1762
|
setFwPreflight((prev) => (apply === true ? { report: (prev && prev.report) ?? null, patch: data } : { report: data, patch: null }));
|
|
1755
1763
|
},
|
|
1756
|
-
(error) => { setFwPreflightBusy(false); setMessage(t("
|
|
1764
|
+
(error) => { setFwPreflightBusy(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
1757
1765
|
);
|
|
1758
1766
|
};
|
|
1759
1767
|
const fwShortPath = (p) => String(p ?? "").replace(/\\/gu, "/").split("/").slice(-3).join("/");
|
|
@@ -1766,7 +1774,7 @@ window.__ModuleLoader__.load({
|
|
|
1766
1774
|
setFrameworkStatus({ status: "rollback", message: t("frameworkRollbackDone").replace("{from}", String(data?.from ?? "?")) });
|
|
1767
1775
|
pollFrameworkStatus();
|
|
1768
1776
|
},
|
|
1769
|
-
(error) => { setFrameworkUpgrading(false); setMessage(t("
|
|
1777
|
+
(error) => { setFrameworkUpgrading(false); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
1770
1778
|
);
|
|
1771
1779
|
};
|
|
1772
1780
|
// 回滚按钮可用性:已经处在「回滚目标版本」时不再提供回滚(点了等于恢复现状)。
|
|
@@ -2013,10 +2021,10 @@ window.__ModuleLoader__.load({
|
|
|
2013
2021
|
setAiJob({ jobId: data.jobId, source, status: "running", stage: "planning" });
|
|
2014
2022
|
aiPoll(data.jobId);
|
|
2015
2023
|
}
|
|
2016
|
-
else { setAiRunning(false); setMessage(t("
|
|
2024
|
+
else { setAiRunning(false); setMessage(t("safetySwitchFailed") + ":" + (data?.error ?? "未知")); }
|
|
2017
2025
|
} catch (error) {
|
|
2018
2026
|
setAiRunning(false);
|
|
2019
|
-
setMessage(t("
|
|
2027
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2020
2028
|
}
|
|
2021
2029
|
}
|
|
2022
2030
|
async function aiRun() {
|
|
@@ -2035,7 +2043,7 @@ window.__ModuleLoader__.load({
|
|
|
2035
2043
|
aiPoll(aiJob.jobId);
|
|
2036
2044
|
} catch (error) {
|
|
2037
2045
|
setAiRunning(false);
|
|
2038
|
-
setMessage(t("
|
|
2046
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2039
2047
|
}
|
|
2040
2048
|
}
|
|
2041
2049
|
async function aiCancel() {
|
|
@@ -2056,7 +2064,7 @@ window.__ModuleLoader__.load({
|
|
|
2056
2064
|
const data = await call("/plugin-console/component/" + action, { id });
|
|
2057
2065
|
setMessage(t("compActionDone").replace("{action}", t(action === "start" ? "compStart" : "compStop")).replace("{name}", data?.name ?? id));
|
|
2058
2066
|
} catch (error) {
|
|
2059
|
-
setMessage(t("
|
|
2067
|
+
setMessage(t("safetySwitchFailed") + ":" + error.message);
|
|
2060
2068
|
}
|
|
2061
2069
|
setCompBusy(null);
|
|
2062
2070
|
refreshComps();
|
|
@@ -2069,7 +2077,7 @@ window.__ModuleLoader__.load({
|
|
|
2069
2077
|
setMessage(data.running ? (data.healthy ? t("compHealthy") : t("compUnhealthy")) + ":" + (data.name ?? id) : t("compStopped") + ":" + (data.name ?? id));
|
|
2070
2078
|
}
|
|
2071
2079
|
} catch (error) {
|
|
2072
|
-
setMessage(t("
|
|
2080
|
+
setMessage(t("safetySwitchFailed") + ":" + error.message);
|
|
2073
2081
|
}
|
|
2074
2082
|
setCompBusy(null);
|
|
2075
2083
|
refreshComps();
|
|
@@ -2088,7 +2096,7 @@ window.__ModuleLoader__.load({
|
|
|
2088
2096
|
await call("/plugin-console/component/autostart", { id, enabled: on });
|
|
2089
2097
|
setMessage((on ? "✔ 已开启自启动:" : "已关闭自启动:") + id);
|
|
2090
2098
|
} catch (error) {
|
|
2091
|
-
setMessage(t("
|
|
2099
|
+
setMessage(t("safetySwitchFailed") + ":" + error.message);
|
|
2092
2100
|
}
|
|
2093
2101
|
setCompBusy(null);
|
|
2094
2102
|
refreshComps();
|
|
@@ -2500,7 +2508,7 @@ window.__ModuleLoader__.load({
|
|
|
2500
2508
|
setRiskyConfirm({ entryId: entry.entryId, moduleName: details.moduleName ?? entry.moduleName ?? null, frameworkVersion: details.frameworkVersion ?? null, checkNote: details.checkNote ?? null });
|
|
2501
2509
|
return;
|
|
2502
2510
|
}
|
|
2503
|
-
setMessage(t("
|
|
2511
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2504
2512
|
},
|
|
2505
2513
|
).finally(() => setBusy(null));
|
|
2506
2514
|
};
|
|
@@ -2513,7 +2521,7 @@ window.__ModuleLoader__.load({
|
|
|
2513
2521
|
: prev));
|
|
2514
2522
|
setMessage(t("compatGateSaved"));
|
|
2515
2523
|
},
|
|
2516
|
-
(error) => setMessage(t("
|
|
2524
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
2517
2525
|
);
|
|
2518
2526
|
};
|
|
2519
2527
|
const levenshtein = (a, b) => {
|
|
@@ -2704,7 +2712,7 @@ window.__ModuleLoader__.load({
|
|
|
2704
2712
|
},
|
|
2705
2713
|
(error) => {
|
|
2706
2714
|
setLoadingMore(false);
|
|
2707
|
-
setMessage(t("
|
|
2715
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2708
2716
|
},
|
|
2709
2717
|
);
|
|
2710
2718
|
return;
|
|
@@ -2728,7 +2736,7 @@ window.__ModuleLoader__.load({
|
|
|
2728
2736
|
},
|
|
2729
2737
|
(error) => {
|
|
2730
2738
|
setLoadingMore(false);
|
|
2731
|
-
setMessage(t("
|
|
2739
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2732
2740
|
},
|
|
2733
2741
|
);
|
|
2734
2742
|
return;
|
|
@@ -2746,7 +2754,7 @@ window.__ModuleLoader__.load({
|
|
|
2746
2754
|
},
|
|
2747
2755
|
(error) => {
|
|
2748
2756
|
setLoadingMore(false);
|
|
2749
|
-
setMessage(t("
|
|
2757
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2750
2758
|
},
|
|
2751
2759
|
);
|
|
2752
2760
|
return;
|
|
@@ -2784,7 +2792,7 @@ window.__ModuleLoader__.load({
|
|
|
2784
2792
|
},
|
|
2785
2793
|
(error) => {
|
|
2786
2794
|
setLoadingMore(false);
|
|
2787
|
-
setMessage(t("
|
|
2795
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
2788
2796
|
},
|
|
2789
2797
|
),
|
|
2790
2798
|
);
|
|
@@ -2953,7 +2961,7 @@ window.__ModuleLoader__.load({
|
|
|
2953
2961
|
window.setTimeout(() => window.location.reload(), 2500);
|
|
2954
2962
|
}
|
|
2955
2963
|
} else {
|
|
2956
|
-
setMessage(t("
|
|
2964
|
+
setMessage(t("safetySwitchFailed") + ":" + (data.error ?? "未知错误") + diagMessage(data));
|
|
2957
2965
|
}
|
|
2958
2966
|
setInstalling(null);
|
|
2959
2967
|
setTimeout(refresh, 1500);
|
|
@@ -2966,8 +2974,8 @@ window.__ModuleLoader__.load({
|
|
|
2966
2974
|
/** 本地 AI 兜底授权:调用模型 API 产生费用,必须用户明确同意。 */
|
|
2967
2975
|
const aiConsent = (jobId, approved) => {
|
|
2968
2976
|
call("/plugin-console/ai-consent", { jobId, approved }).then(
|
|
2969
|
-
() => { if (!approved) setMessage(t("
|
|
2970
|
-
(error) => setMessage(t("
|
|
2977
|
+
() => { if (!approved) setMessage(t("safetySwitchFailed") + ":已取消本地 AI 兜底(不会调用模型 API)"); },
|
|
2978
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
2971
2979
|
);
|
|
2972
2980
|
};
|
|
2973
2981
|
/** 安装进度文案:服务端 progress{channel,phase,index,total,name,done} → 一行人话。
|
|
@@ -3127,7 +3135,7 @@ window.__ModuleLoader__.load({
|
|
|
3127
3135
|
// clientId 打码回显(如 abc12345…):用户未修改时原样传回服务端,服务端识别打码标记保留原配置
|
|
3128
3136
|
const idValue = giteeClientId.trim();
|
|
3129
3137
|
if (!idValue || !giteeClientSecret.trim()) {
|
|
3130
|
-
setMessage(t("
|
|
3138
|
+
setMessage(t("safetySwitchFailed") + ":client_id / client_secret 不能为空");
|
|
3131
3139
|
return;
|
|
3132
3140
|
}
|
|
3133
3141
|
if (idValue.endsWith("…")) {
|
|
@@ -3139,7 +3147,7 @@ window.__ModuleLoader__.load({
|
|
|
3139
3147
|
const startGiteeOauth = () => {
|
|
3140
3148
|
call("/plugin-console/gitee-oauth-url").then(
|
|
3141
3149
|
(data) => { if (data && data.url) window.open(data.url, "_blank", "noopener"); },
|
|
3142
|
-
(error) => setMessage(t("
|
|
3150
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
3143
3151
|
);
|
|
3144
3152
|
};
|
|
3145
3153
|
const sourcesAction = (payload, done) => {
|
|
@@ -3151,12 +3159,12 @@ window.__ModuleLoader__.load({
|
|
|
3151
3159
|
setMessage(t("sourcesUpdated"));
|
|
3152
3160
|
if (done) done();
|
|
3153
3161
|
},
|
|
3154
|
-
(error) => setMessage(t("
|
|
3162
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
3155
3163
|
).finally(() => setSourcesBusy(false));
|
|
3156
3164
|
};
|
|
3157
3165
|
const addSource = () => {
|
|
3158
3166
|
if (!isAllowedSourceUrl(sourceUrl.trim())) {
|
|
3159
|
-
setMessage(t("
|
|
3167
|
+
setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
|
|
3160
3168
|
return;
|
|
3161
3169
|
}
|
|
3162
3170
|
sourcesAction({ action: "add", name: sourceName.trim(), url: sourceUrl.trim() }, () => { setSourceName(""); setSourceUrl(""); });
|
|
@@ -3166,7 +3174,7 @@ window.__ModuleLoader__.load({
|
|
|
3166
3174
|
setRegScan({ status: "scanning" });
|
|
3167
3175
|
call("/plugin-console/registry-scan", {}).then(
|
|
3168
3176
|
(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("
|
|
3177
|
+
(error) => { setRegScan(null); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
3170
3178
|
);
|
|
3171
3179
|
};
|
|
3172
3180
|
// 扫描结果派生视图:每条源的命中结果 + 可达计数 + 最新版本来自哪个源
|
|
@@ -3190,7 +3198,7 @@ window.__ModuleLoader__.load({
|
|
|
3190
3198
|
};
|
|
3191
3199
|
const addIndexSource = () => {
|
|
3192
3200
|
if (!isAllowedSourceUrl(indexSourceUrl.trim())) {
|
|
3193
|
-
setMessage(t("
|
|
3201
|
+
setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
|
|
3194
3202
|
return;
|
|
3195
3203
|
}
|
|
3196
3204
|
sourcesAction({ action: "add-index", name: indexSourceName.trim(), url: indexSourceUrl.trim() }, () => { setIndexSourceName(""); setIndexSourceUrl(""); });
|
|
@@ -3213,11 +3221,11 @@ window.__ModuleLoader__.load({
|
|
|
3213
3221
|
const addGitSource = () => {
|
|
3214
3222
|
const tpl = gitSourceUrl.trim();
|
|
3215
3223
|
if (!tpl.includes("{owner}") || !tpl.includes("{repo}")) {
|
|
3216
|
-
setMessage(t("
|
|
3224
|
+
setMessage(t("safetySwitchFailed") + ":" + t("gitUrlPlaceholder"));
|
|
3217
3225
|
return;
|
|
3218
3226
|
}
|
|
3219
3227
|
if (!isAllowedSourceUrl(tpl)) {
|
|
3220
|
-
setMessage(t("
|
|
3228
|
+
setMessage(t("safetySwitchFailed") + ":" + t("invalidSourceUrl"));
|
|
3221
3229
|
return;
|
|
3222
3230
|
}
|
|
3223
3231
|
sourcesAction({ action: "add-git", name: gitSourceName.trim(), urlTemplate: tpl }, () => { setGitSourceName(""); setGitSourceUrl(""); });
|
|
@@ -3241,7 +3249,7 @@ window.__ModuleLoader__.load({
|
|
|
3241
3249
|
};
|
|
3242
3250
|
const addSearchSource = () => {
|
|
3243
3251
|
if (!isAllowedSourceUrl(searchSourceUrl.trim()) || !searchSourceUrl.includes("{q}")) {
|
|
3244
|
-
setMessage(t("
|
|
3252
|
+
setMessage(t("safetySwitchFailed") + ":" + t("invalidSearchUrl"));
|
|
3245
3253
|
return;
|
|
3246
3254
|
}
|
|
3247
3255
|
// 解析请求头:每行 "名称: 值"
|
|
@@ -3281,7 +3289,7 @@ window.__ModuleLoader__.load({
|
|
|
3281
3289
|
}
|
|
3282
3290
|
},
|
|
3283
3291
|
(error) => {
|
|
3284
|
-
setMessage(t("
|
|
3292
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
3285
3293
|
setInstalling(null);
|
|
3286
3294
|
},
|
|
3287
3295
|
);
|
|
@@ -3353,7 +3361,7 @@ window.__ModuleLoader__.load({
|
|
|
3353
3361
|
},
|
|
3354
3362
|
(error) => {
|
|
3355
3363
|
setTogglingSkill(null);
|
|
3356
|
-
setMessage(t("
|
|
3364
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
3357
3365
|
},
|
|
3358
3366
|
);
|
|
3359
3367
|
};
|
|
@@ -3404,7 +3412,7 @@ window.__ModuleLoader__.load({
|
|
|
3404
3412
|
(error) => {
|
|
3405
3413
|
setFrameworkUpgrading(false);
|
|
3406
3414
|
setConfirmFrameworkUpgrade(false);
|
|
3407
|
-
setMessage(t("
|
|
3415
|
+
setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message);
|
|
3408
3416
|
},
|
|
3409
3417
|
);
|
|
3410
3418
|
};
|
|
@@ -3416,7 +3424,7 @@ window.__ModuleLoader__.load({
|
|
|
3416
3424
|
setConfirmSkillDelete(null);
|
|
3417
3425
|
setInstalledSkills((prev) => prev.filter((s) => s.name !== name));
|
|
3418
3426
|
},
|
|
3419
|
-
(error) => setMessage(t("
|
|
3427
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
3420
3428
|
);
|
|
3421
3429
|
};
|
|
3422
3430
|
const installedQueryNorm = installedQuery.trim().toLowerCase();
|
|
@@ -3470,7 +3478,7 @@ window.__ModuleLoader__.load({
|
|
|
3470
3478
|
el("p", { className: styles.status }, t("loadingDetails")));
|
|
3471
3479
|
} else if (details.status === "error") {
|
|
3472
3480
|
detailPanel = el("div", { className: styles.detail },
|
|
3473
|
-
el("p", { className: styles.status, "data-error": "true" }, t("
|
|
3481
|
+
el("p", { className: styles.status, "data-error": "true" }, t("safetySwitchFailed") + ":" + details.error.message));
|
|
3474
3482
|
} else {
|
|
3475
3483
|
const meta = details.data.meta;
|
|
3476
3484
|
const readme = details.data.readme;
|
|
@@ -3499,7 +3507,7 @@ window.__ModuleLoader__.load({
|
|
|
3499
3507
|
title: t("aiEmpowerTitle"),
|
|
3500
3508
|
onClick: () => openAiFor(String(entry.moduleName ?? entry.repo ?? "")),
|
|
3501
3509
|
}, t("aiEmpowerLabel")),
|
|
3502
|
-
entry
|
|
3510
|
+
canAdaptUnlock(entry)
|
|
3503
3511
|
? el("button", {
|
|
3504
3512
|
type: "button",
|
|
3505
3513
|
className: styles.toggle,
|
|
@@ -3514,7 +3522,7 @@ window.__ModuleLoader__.load({
|
|
|
3514
3522
|
setMessage(t("adaptUnlockDone"));
|
|
3515
3523
|
window.setTimeout(() => window.location.reload(), 1200);
|
|
3516
3524
|
},
|
|
3517
|
-
(error) => { setAdaptUnlocking(null); setMessage(t("
|
|
3525
|
+
(error) => { setAdaptUnlocking(null); setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message); },
|
|
3518
3526
|
);
|
|
3519
3527
|
},
|
|
3520
3528
|
}, adaptUnlocking === entry.entryId ? t("loadingPhase") : t("adaptUnlockBtn"))
|
|
@@ -3586,6 +3594,13 @@ window.__ModuleLoader__.load({
|
|
|
3586
3594
|
phaseLabel(entry.fiberPhase, t)),
|
|
3587
3595
|
el("code", null, entry.entryId),
|
|
3588
3596
|
),
|
|
3597
|
+
// 0.5.33 加法 ①-4:框架 peer 预检**否决**的本地复算结果(服务端 entries[].veto)。
|
|
3598
|
+
// 框架只在内存里禁用、原因只打 stderr —— 这里把它补出来。复用**既有行**内的
|
|
3599
|
+
// status 短句位(不新增常驻面板行),出路放悬浮提示(复用既有 title 位置)。
|
|
3600
|
+
entry.veto && entry.veto.reason
|
|
3601
|
+
? el("p", { className: styles.status, "data-pending": "true", title: entry.veto.hint ?? undefined },
|
|
3602
|
+
entry.veto.reason)
|
|
3603
|
+
: null,
|
|
3589
3604
|
el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
|
|
3590
3605
|
el("div", { className: styles.rowTop },
|
|
3591
3606
|
el("button", {
|
|
@@ -3623,7 +3638,7 @@ window.__ModuleLoader__.load({
|
|
|
3623
3638
|
setTimeout(refresh, 1500);
|
|
3624
3639
|
}
|
|
3625
3640
|
},
|
|
3626
|
-
(error) => setMessage(t("
|
|
3641
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
3627
3642
|
).finally(() => setDeleteBusy(null));
|
|
3628
3643
|
} else {
|
|
3629
3644
|
setConfirmDeleteRowId(entry.entryId);
|
|
@@ -3679,7 +3694,7 @@ window.__ModuleLoader__.load({
|
|
|
3679
3694
|
setFamilyCheck((prev) => ({ ...prev, [root]: { latest: d?.latest ?? null, installed } }));
|
|
3680
3695
|
setMessage(t("familyUpdate") + ":" + root + " " + (d?.latest ?? "?") + (d?.migrate && d.migrate.compatible ? " → " + d.migrate.to + " v" + d.migrate.latest : "") + (d?.error ? "(" + d.error + ")" : ""));
|
|
3681
3696
|
},
|
|
3682
|
-
(e) => setMessage(t("
|
|
3697
|
+
(e) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(e).message),
|
|
3683
3698
|
);
|
|
3684
3699
|
},
|
|
3685
3700
|
}, t("familyCheckUpdate")),
|
|
@@ -3706,7 +3721,7 @@ window.__ModuleLoader__.load({
|
|
|
3706
3721
|
setMessage(t("familyUnlockAllDone") + ":" + unlockedCount + " 行" + (Array.isArray(d?.kept) && d.kept.length > 0 ? ";保留 " + d.kept.length + " 行(未通过校验)" : ""));
|
|
3707
3722
|
window.setTimeout(() => window.location.reload(), 1500);
|
|
3708
3723
|
},
|
|
3709
|
-
(e) => setMessage(t("
|
|
3724
|
+
(e) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(e).message),
|
|
3710
3725
|
);
|
|
3711
3726
|
},
|
|
3712
3727
|
}, t("familyUnlockAll")))
|
|
@@ -3765,7 +3780,7 @@ window.__ModuleLoader__.load({
|
|
|
3765
3780
|
: t("pendingRestartNote") + ":" + (data?.packageName ?? "") + (data?.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
3766
3781
|
refresh();
|
|
3767
3782
|
},
|
|
3768
|
-
(error) => setMessage(t("
|
|
3783
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
3769
3784
|
).finally(() => setDeleteBusy(null));
|
|
3770
3785
|
} else {
|
|
3771
3786
|
setConfirmDeleteRowId(job.jobId);
|
|
@@ -4388,7 +4403,7 @@ window.__ModuleLoader__.load({
|
|
|
4388
4403
|
? el("div", { className: styles.rowTop },
|
|
4389
4404
|
el("p", { className: styles.message, style: { flex: 1, minWidth: 0, wordBreak: "break-all" } }, t("repoLandDone") + repoLandResult.path),
|
|
4390
4405
|
el("button", { type: "button", className: styles.toggle, onClick: repoLandOpenFolder }, t("repoLandOpenFolder")))
|
|
4391
|
-
: el("p", { className: styles.message, "data-error": "true" }, t("
|
|
4406
|
+
: el("p", { className: styles.message, "data-error": "true" }, t("safetySwitchFailed") + ":" + (repoLandResult.error ?? "?")))
|
|
4392
4407
|
: null,
|
|
4393
4408
|
el("strong", { className: styles.name }, t("repoLandList") + "(" + repoLanded.length + ")"),
|
|
4394
4409
|
repoLanded.length === 0
|
|
@@ -4415,7 +4430,7 @@ window.__ModuleLoader__.load({
|
|
|
4415
4430
|
onClick: () => {
|
|
4416
4431
|
call("/plugin-console/framework-relaunch", {}).then(
|
|
4417
4432
|
(data) => { if (data && data.ok === true) setMessage(t("relaunchDone")); },
|
|
4418
|
-
(error) => setMessage(t("
|
|
4433
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
4419
4434
|
);
|
|
4420
4435
|
},
|
|
4421
4436
|
}, t("relaunchBtn"))
|
|
@@ -4432,7 +4447,7 @@ window.__ModuleLoader__.load({
|
|
|
4432
4447
|
const keptNote = keptCount > 0 ? ";保留在用插件 " + keptCount + " 个" : "";
|
|
4433
4448
|
if (data && data.ok === true) setMessage(t("cleanResiduals") + ":" + (data.count === 0 ? "无残余" : "已清理 " + data.count + " 项") + keptNote);
|
|
4434
4449
|
},
|
|
4435
|
-
(error) => setMessage(t("
|
|
4450
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
4436
4451
|
);
|
|
4437
4452
|
},
|
|
4438
4453
|
}, "🧹 " + t("cleanResiduals")),
|
|
@@ -4472,10 +4487,10 @@ window.__ModuleLoader__.load({
|
|
|
4472
4487
|
setMessage(t("cfDone").replace("{renamed}", String(renamed)).replace("{linked}", String(relinked)).replace("{removed}", String(removed)).replace("{mb}", String(freed))
|
|
4473
4488
|
+ (failed > 0 ? ";" + t("cfFailed").replace("{n}", String(failed)) : ""));
|
|
4474
4489
|
},
|
|
4475
|
-
(error) => setMessage(t("
|
|
4490
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
4476
4491
|
);
|
|
4477
4492
|
},
|
|
4478
|
-
(error) => setMessage(t("
|
|
4493
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
4479
4494
|
);
|
|
4480
4495
|
},
|
|
4481
4496
|
}, "🧰 " + t("cleanFrameworkResiduals")),
|
|
@@ -4559,10 +4574,10 @@ const lockWarn = data.lockUpdated === false && typeof data.lockNote === "string"
|
|
|
4559
4574
|
setMessage((data.note ?? "已更新") + (lockWarn === "" ? "" : "。" + lockWarn) + ",正在重启生效…");
|
|
4560
4575
|
restartServiceViaServer(setMessage, { reload: false });
|
|
4561
4576
|
} else {
|
|
4562
|
-
setMessage(t("
|
|
4577
|
+
setMessage(t("safetySwitchFailed") + ":" + (data?.reason ?? "未知"));
|
|
4563
4578
|
}
|
|
4564
4579
|
},
|
|
4565
|
-
(error) => setMessage(t("
|
|
4580
|
+
(error) => setMessage(t("safetySwitchFailed") + ":" + friendlyGithubError(error).message),
|
|
4566
4581
|
);
|
|
4567
4582
|
}
|
|
4568
4583
|
}, "⬇ " + t("hubUpdateAvailable") + " v" + selfUpdate.latest) : null,
|
|
@@ -5146,7 +5161,7 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
5146
5161
|
? el("p", { className: styles.status }, t("loading"))
|
|
5147
5162
|
: sourcesData.error !== undefined
|
|
5148
5163
|
? el("div", null,
|
|
5149
|
-
el("p", { className: styles.status }, t("
|
|
5164
|
+
el("p", { className: styles.status }, t("safetySwitchFailed") + ":" + sourcesData.error),
|
|
5150
5165
|
el("button", { type: "button", className: styles.toggle, onClick: () => { setSourcesData(null); openSources(); } }, t("retry")))
|
|
5151
5166
|
: el("div", { className: styles.accWrap },
|
|
5152
5167
|
accSection("registries", t("sourcesBtn"), [
|
|
@@ -5704,6 +5719,9 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
5704
5719
|
// 仅测试用导出(tests/test-plugin-actions.mjs 的离线渲染断言:用假 React 真渲染这个组件,
|
|
5705
5720
|
// 断言"有/无 suggestedAction、执行中、成功、失败、中英切换"五种情形的 DOM 结构)。
|
|
5706
5721
|
exports.SuggestedActionBox = SuggestedActionBox;
|
|
5722
|
+
// 仅测试用导出(tests/test-adapt-unlock-gate.mjs ①-2:断言「已适配,立即解锁」按钮的门控判据,
|
|
5723
|
+
// 以及它必须与卡片正文用同一个判据)。
|
|
5724
|
+
exports.canAdaptUnlock = canAdaptUnlock;
|
|
5707
5725
|
return module.exports;
|
|
5708
5726
|
}
|
|
5709
5727
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// L1 · domain —— compat.js(框架兼容判定与适配门:设置 API 扫描 / 兼容门读写 / 待适配记录 / 隔离记录合并的前置部分 / 启动失败分析;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectCompat 等吃运行上下文的留到 Step 8)
|
|
2
2
|
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
|
-
import { readFileSync, writeFileSync,
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs'
|
|
5
5
|
import { readFile } from 'node:fs/promises'
|
|
6
6
|
import { spawn } from 'node:child_process'
|
|
7
7
|
import { dirname, join, sep, resolve } from 'node:path'
|
|
@@ -13,6 +13,9 @@ import { listEntries } from './runtime.js'
|
|
|
13
13
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
14
14
|
import { dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
15
15
|
import { semverRangeMatch, semverRangeMatchLoose } from '../infra/semver.js'
|
|
16
|
+
// 0.5.33 改错:已删除 API 的源码扫描搬进独立 domain 模块(判据收紧为"真的从 dsh-settings 绑定进来")。
|
|
17
|
+
// 这三个名字继续由本模块导出,对外契约不变。
|
|
18
|
+
import { REMOVED_SETTINGS_SYMBOLS, referencesRemovedSymbol, scanSettingsApiUsage } from './settings-api-scan.js'
|
|
16
19
|
|
|
17
20
|
/** Cordis Fiber 状态映射(与 dsh-host-plugin-inventory 一致)。 */
|
|
18
21
|
const FIBER_STATE = { PENDING: 0, LOADING: 1, ACTIVE: 2, FAILED: 3, DISPOSED: 4, UNLOADING: 5 }
|
|
@@ -26,63 +29,6 @@ const FIBER_PHASE = {
|
|
|
26
29
|
[FIBER_STATE.UNLOADING]: 'unloading',
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
/**
|
|
30
|
-
* 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用
|
|
31
|
-
* (2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
|
|
32
|
-
* 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
|
|
33
|
-
* = 整个服务启动崩溃)。返回命中的符号列表,空数组 = 干净。
|
|
34
|
-
*/
|
|
35
|
-
const REMOVED_SETTINGS_SYMBOLS = ['settingsNamespace', 'installSettingsSection']
|
|
36
|
-
|
|
37
|
-
/** 源码是否**引用**了已删除符号——而不是「恰好包含同名前缀的其它标识符」或「自己定义的同名局部函数」。
|
|
38
|
-
* v0.3.35 修复(真机演练抓到的误伤):原先用 `text.includes('settingsNamespace')` 子串匹配,
|
|
39
|
-
* 而框架自带的 @deepseek-ai/dsh-api-settings-controller 里有个 `settingsNamespaceRequestSchema`,
|
|
40
|
-
* 于是框架自己的 settings 控制器被判「引用已删除 API」→ 升级预扫会把它自动禁用(砍掉框架功能)。
|
|
41
|
-
* 现在要求标识符边界,且排除「本地 const/let/var/function/class 定义且同行没有 dsh-settings 引用」。 */
|
|
42
|
-
function referencesRemovedSymbol(text, sym) {
|
|
43
|
-
const re = new RegExp(`(?<![A-Za-z0-9_$])${sym}(?![A-Za-z0-9_$])`, 'u')
|
|
44
|
-
for (const line of text.split(/\r?\n/u)) {
|
|
45
|
-
const m = re.exec(line)
|
|
46
|
-
if (m === null) continue
|
|
47
|
-
const before = line.slice(0, m.index)
|
|
48
|
-
const localDefinition = /(?:^|[^\w$])(?:const|let|var|function|class|async\s+function)\s*$/u.test(before)
|
|
49
|
-
if (localDefinition && !/dsh-settings/u.test(line)) continue
|
|
50
|
-
return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function scanSettingsApiUsage(pkgDir) {
|
|
56
|
-
const found = []
|
|
57
|
-
const seen = new Set()
|
|
58
|
-
const walk = (dir, depth) => {
|
|
59
|
-
if (depth > 3) return
|
|
60
|
-
let entries = []
|
|
61
|
-
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
const full = join(dir, entry.name)
|
|
64
|
-
if (entry.isDirectory()) {
|
|
65
|
-
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
66
|
-
walk(full, depth + 1)
|
|
67
|
-
continue
|
|
68
|
-
}
|
|
69
|
-
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
|
|
70
|
-
if (seen.has(full)) continue
|
|
71
|
-
seen.add(full)
|
|
72
|
-
if (seen.size > 120) return
|
|
73
|
-
try {
|
|
74
|
-
const text = readFileSync(full, 'utf8').slice(0, 400000)
|
|
75
|
-
for (const sym of REMOVED_SETTINGS_SYMBOLS) {
|
|
76
|
-
if (found.includes(sym)) continue
|
|
77
|
-
if (referencesRemovedSymbol(text, sym)) found.push(sym)
|
|
78
|
-
}
|
|
79
|
-
} catch {}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
walk(pkgDir, 0)
|
|
83
|
-
return found
|
|
84
|
-
}
|
|
85
|
-
|
|
86
32
|
/** 包是否由框架自带(解析到 profile 目录之外,如 npx/pnpm 缓存里的 @deepseek-ai/*)。
|
|
87
33
|
* v0.3.35:这类行随框架一起发布,禁用**不是**正确处置(正确处置是回滚框架),
|
|
88
34
|
* 而且一旦判定有误就会把框架功能砍掉——所以升级预扫永不自动禁用它们。 */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// L1 · domain —— peer-veto.js(框架 peer 预检「否决」的**本地复算**;0.5.33 加法,诊断缺口 D-⑧)
|
|
2
|
+
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
|
+
//
|
|
4
|
+
// ── 要解决什么 ────────────────────────────────────────────────────────────────────
|
|
5
|
+
// 真机症状:「补丁里明明写着启用(或压根没写 disabled)、运行时却没挂载(fiberPhase === null),
|
|
6
|
+
// 面板上一个字都不说为什么」。真实机制(框架侧,不是我们的 bug):
|
|
7
|
+
// `@deepseek-ai/dsh-app-boot/lib/index.js`
|
|
8
|
+
// · `evaluatePluginCompatibility(manifest, exemptions, runtimeVersion)`(:286-313)
|
|
9
|
+
// —— 只看 **peerDependencies**,只看 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*`,
|
|
10
|
+
// `workspace:^|~|*` 视为「等于当前运行时」,其余按 semver(includePrerelease)判;
|
|
11
|
+
// · `preflight()`(:2063-2109)—— 判不满足就在**内存里** `row.disabled = true`(**不写用户补丁**),
|
|
12
|
+
// 原因只 `process.stderr.write` 一行。
|
|
13
|
+
// 本机典型现场:profile 顶层压着 `@deepseek-ai/dsh-schedule@0.0.1-rc.3` 残影,它的 peer 写
|
|
14
|
+
// `@deepseek-ai/dsh-agent@^0.0.1-rc.3` 等 7 项 → 对运行中的 0.2.0-rc.2 **必然不满足** → 启动即被否决,
|
|
15
|
+
// 而补丁尾部还是 `- id: schedule / disabled: false`,面板只显示「未挂载」。
|
|
16
|
+
//
|
|
17
|
+
// ── 本模块做什么(**加法**:不改任何既有判据与开关,只补一句原因 + 一条出路)────────────
|
|
18
|
+
// 在同一判据下本地复算一遍:读该行**实际解析到的包** manifest 的 peerDependencies × 当前框架版本。
|
|
19
|
+
// 判定不满足 → 给短句原因 + 出路;包解析不到 → **另给**「包不存在」原因(两者绝不混为一谈)。
|
|
20
|
+
//
|
|
21
|
+
// ── 保守性(宁可不说,不可错说)────────────────────────────────────────────────
|
|
22
|
+
// 框架用的是 node-semver 的 `{ includePrerelease: true }`;仓库既有的 semver 工具里,
|
|
23
|
+
// `semverRangeMatchLoose` 与它最接近但**偏宽松**(把它当"满足"的少数情形,框架可能判不满足)。
|
|
24
|
+
// 这里**故意用宽松的那一支**:只有连宽松读法都不满足时才说话 —— 与 ①-1「不误报」同一纪律。
|
|
25
|
+
// 代价是可能漏报少数否决(表现回退成"什么都不显示",不会错说原因)。
|
|
26
|
+
|
|
27
|
+
import { readFileSync } from 'node:fs'
|
|
28
|
+
import { resolvePackageJson } from '../infra/paths.js'
|
|
29
|
+
import { semverRangeMatchLoose } from '../infra/semver.js'
|
|
30
|
+
|
|
31
|
+
/** 框架预检只看这个前缀的 peer(与 dsh-app-boot 的 `name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')` 对齐)。 */
|
|
32
|
+
const PEER_PREFIX = '@deepseek-ai/dsh'
|
|
33
|
+
|
|
34
|
+
/** `workspace:^` / `workspace:~` / `workspace:*` 在框架里等价于「当前运行时版本」→ 永远满足。 */
|
|
35
|
+
const WORKSPACE_RANGES = new Set(['workspace:^', 'workspace:~', 'workspace:*'])
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 纯判据:返回**不满足**的 peer 列表(空数组 = 没有相关 peer 声明,或全部满足)。
|
|
39
|
+
* 与 `dsh-app-boot` 的 `evaluatePluginCompatibility` 逐条对齐(见文件头)。
|
|
40
|
+
*/
|
|
41
|
+
function unsatisfiedPeers(manifest, runtimeVersion) {
|
|
42
|
+
if (manifest === null || typeof manifest !== 'object') return []
|
|
43
|
+
if (!Object.hasOwn(manifest, 'peerDependencies')) return []
|
|
44
|
+
const deps = manifest.peerDependencies
|
|
45
|
+
if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) return []
|
|
46
|
+
if (typeof runtimeVersion !== 'string' || runtimeVersion === '') return []
|
|
47
|
+
const out = []
|
|
48
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
49
|
+
if (name !== PEER_PREFIX && !name.startsWith(`${PEER_PREFIX}-`)) continue
|
|
50
|
+
if (typeof range !== 'string') { out.push({ name, range: String(range) }); continue }
|
|
51
|
+
const requirement = WORKSPACE_RANGES.has(range) ? runtimeVersion : range
|
|
52
|
+
if (requirement.trim() === '' || !semverRangeMatchLoose(runtimeVersion, requirement)) out.push({ name, range })
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 短句化的「需要什么」:一项给全名,多项给首项 + 总数(面板一行放得下)。 */
|
|
58
|
+
function describePeers(peers) {
|
|
59
|
+
const first = `${peers[0].name}@${peers[0].range}`
|
|
60
|
+
return peers.length === 1 ? first : `${first} 等 ${peers.length} 项`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 行级复算(IO 可注入,便于离线断言)。
|
|
65
|
+
*
|
|
66
|
+
* 入参:`{ moduleName, profileDir, frameworkVersion }`
|
|
67
|
+
* 返回:`null`(无话可说)|`{ kind:'peers'|'missing', reason, hint }`
|
|
68
|
+
* · `kind:'peers'` —— 本地复算判定 peer 不满足(大概率就是框架否决它的原因)
|
|
69
|
+
* · `kind:'missing'` —— 包解析不到 / 清单读不出来(**另一种原因**,措辞与出路都不同)
|
|
70
|
+
*/
|
|
71
|
+
function peerVetoFor({ moduleName, profileDir, frameworkVersion, deps = {} } = {}) {
|
|
72
|
+
const { resolvePkg = resolvePackageJson, readText = (p) => readFileSync(p, 'utf8') } = deps
|
|
73
|
+
if (typeof moduleName !== 'string' || moduleName === '' || moduleName.startsWith('cordis:')) return null
|
|
74
|
+
if (typeof profileDir !== 'string' || profileDir === '') return null
|
|
75
|
+
if (typeof frameworkVersion !== 'string' || frameworkVersion === '') return null
|
|
76
|
+
let pkgPath = null
|
|
77
|
+
try { pkgPath = resolvePkg(moduleName, profileDir) } catch { pkgPath = null }
|
|
78
|
+
if (pkgPath === null) {
|
|
79
|
+
return {
|
|
80
|
+
kind: 'missing',
|
|
81
|
+
reason: `被框架否决:包不存在(${moduleName} 在本 profile 解析不到)`,
|
|
82
|
+
hint: `出路:这一行指向的包已经不在本 profile 里(升级/卸载后的残留行)——重装或更新该插件即可;确实不需要它时,在补丁里删掉或禁用它。`,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let manifest = null
|
|
86
|
+
try { manifest = JSON.parse(readText(pkgPath)) } catch { manifest = null }
|
|
87
|
+
if (manifest === null || typeof manifest !== 'object') {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'missing',
|
|
90
|
+
reason: `被框架否决:包清单读不出来(${moduleName})`,
|
|
91
|
+
hint: '出路:重装该插件——package.json 读不出来时框架自己也无法校验它,只能按最坏情况跳过挂载。',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const peers = unsatisfiedPeers(manifest, frameworkVersion)
|
|
95
|
+
if (peers.length === 0) return null
|
|
96
|
+
return {
|
|
97
|
+
kind: 'peers',
|
|
98
|
+
reason: `被框架否决:peer 不满足(需要 ${describePeers(peers)},当前 ${frameworkVersion})`,
|
|
99
|
+
hint: `出路:把 ${moduleName} 更新到兼容当前框架的版本(首选);若它是升级后残留的旧包,从 profile 里清掉;要保留这个精确版本,就得用框架的精确版本豁免(dsh plugin allow-version / 插件管理器 → profile 的 compatibility.json)。`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { PEER_PREFIX, WORKSPACE_RANGES, unsatisfiedPeers, peerVetoFor }
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// L1 · domain —— settings-api-scan.js(「已删除 dsh-settings API」源码扫描;0.5.33 改错时从 compat.js 搬出)
|
|
2
|
+
// 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
|
+
//
|
|
4
|
+
// 为什么单独成模块:判据从"源码里出现过这个名字"改成"这个名字**真的从
|
|
5
|
+
// `@deepseek-ai/dsh-settings` 绑定进来**"——要先剥注释/字符串字面量,再解析 import/require 的
|
|
6
|
+
// 绑定子句,逻辑比原先的按行子串匹配长得多,而 compat.js 已经 544 行(守卫上限 600)。
|
|
7
|
+
// 搬移保持对外契约不变:`REMOVED_SETTINGS_SYMBOLS` / `referencesRemovedSymbol` /
|
|
8
|
+
// `scanSettingsApiUsage` 三个名字照旧由 compat.js 导出。
|
|
9
|
+
//
|
|
10
|
+
// ── 0.5.33 改错(真机误报,证据充分)────────────────────────────────────────────────
|
|
11
|
+
// 现症:`dshmarket` 被判 `check:'fail'`(= 「源码仍引用 0.1.2 起已删除的 dsh-settings API」),
|
|
12
|
+
// 于是**框架升级预扫会把这个好插件自动禁用**。逐条核对它的三处命中:
|
|
13
|
+
// · dshmarket/lib/settings.js:45 / :53 / :73 —— 全在**注释**里;
|
|
14
|
+
// · dshmarket/lib/routes.js:3136 `settingsNamespace: settingsNamespaceState(),` —— 一个
|
|
15
|
+
// **HTTP 载荷对象的属性名**(值是自己的局部函数 `settingsNamespaceState`,与本包无关);
|
|
16
|
+
// · 该包对 `@deepseek-ai/dsh-settings` 的 **import 零命中**(它早已把那两个 helper 内联)。
|
|
17
|
+
// 旧判据(`text.includes(sym)` → v0.3.35 换成标识符边界 + 排除局部定义)挡不住这两类:
|
|
18
|
+
// 注释里的名字与对象键上的名字都不是"引用",但它们都在**代码行**上、也不是本地定义。
|
|
19
|
+
//
|
|
20
|
+
// 新判据(更严格,不放宽任何真引用):
|
|
21
|
+
// ① 先掩掉注释、字符串/模板字面量、正则字面量(等长空格替换,保留换行与列偏移);
|
|
22
|
+
// 唯一保留的是**模块说明符字符串**(`from 'x'` / `require('x')` / `import('x')`),
|
|
23
|
+
// 否则没法知道某个绑定来自哪个包。
|
|
24
|
+
// ② 只在**真的对这个包**的 import/require 绑定子句里取名字:
|
|
25
|
+
// `import { settingsNamespace } from '@deepseek-ai/dsh-settings'`、
|
|
26
|
+
// `import settingsNamespace from …`、`import * as s from …` 后的 `s.settingsNamespace`、
|
|
27
|
+
// `const { settingsNamespace } = require('@deepseek-ai/dsh-settings')`、
|
|
28
|
+
// `const { installSettingsSection } = await import('@deepseek-ai/dsh-settings')`。
|
|
29
|
+
// ③ 对象键、局部同名函数、别的模块的同名导入、注释与字符串里的名字 —— 一律不算。
|
|
30
|
+
|
|
31
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
32
|
+
import { join } from 'node:path'
|
|
33
|
+
|
|
34
|
+
/** 0.1.2-rc.1 起被官方删除的 dsh-settings API(命中即"实际不兼容")。 */
|
|
35
|
+
const REMOVED_SETTINGS_SYMBOLS = ['settingsNamespace', 'installSettingsSection']
|
|
36
|
+
|
|
37
|
+
/** 这些 API 原本来自哪个包(只有真的从它绑定进来的名字才算引用)。 */
|
|
38
|
+
const SETTINGS_MODULE = '@deepseek-ai/dsh-settings'
|
|
39
|
+
|
|
40
|
+
const SPECIFIER_RE = `['"]${SETTINGS_MODULE.replace(/[/\\^$*+?.()|[\]{}]/gu, '\\$&')}['"]`
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 把注释、字符串/模板字面量、正则字面量掩成**等长空格**(换行/回车保留,行号与列偏移不变)。
|
|
44
|
+
* `keepSpecifier` 为真时保留"模块说明符"字符串的原样内容(`from 'x'` / `require('x')` / `import('x')`)。
|
|
45
|
+
*
|
|
46
|
+
* 这是**掩码**而不是删除:后续判据仍然按整份源码做正则,位置对得上才好排查。
|
|
47
|
+
*/
|
|
48
|
+
function maskLiterals(text, keepSpecifier) {
|
|
49
|
+
const src = String(text ?? '')
|
|
50
|
+
const out = src.split('')
|
|
51
|
+
const blank = (from, to) => {
|
|
52
|
+
for (let k = from; k < to; k += 1) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' '
|
|
53
|
+
}
|
|
54
|
+
const isWordChar = (ch) => ch !== undefined && /[A-Za-z0-9_$]/u.test(ch)
|
|
55
|
+
const wordBefore = (index) => {
|
|
56
|
+
let j = index
|
|
57
|
+
while (j > 0 && (src[j - 1] === ' ' || src[j - 1] === '\t')) j -= 1
|
|
58
|
+
let k = j
|
|
59
|
+
while (k > 0 && isWordChar(src[k - 1])) k -= 1
|
|
60
|
+
return src.slice(k, j)
|
|
61
|
+
}
|
|
62
|
+
const prevMeaningful = (index) => {
|
|
63
|
+
for (let k = index - 1; k >= 0; k -= 1) {
|
|
64
|
+
const ch = src[k]
|
|
65
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') continue
|
|
66
|
+
return ch
|
|
67
|
+
}
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
/** 当前位置若是 `word(` 的第一个实参,返回 `word`(用于 require(...) / import(...) 的说明符判定)。 */
|
|
71
|
+
const callWordBefore = (index) => {
|
|
72
|
+
let k = index - 1
|
|
73
|
+
while (k >= 0 && (src[k] === ' ' || src[k] === '\t')) k -= 1
|
|
74
|
+
if (k < 0 || src[k] !== '(') return ''
|
|
75
|
+
k -= 1
|
|
76
|
+
while (k >= 0 && (src[k] === ' ' || src[k] === '\t')) k -= 1
|
|
77
|
+
const end = k + 1
|
|
78
|
+
while (k >= 0 && isWordChar(src[k])) k -= 1
|
|
79
|
+
return src.slice(k + 1, end)
|
|
80
|
+
}
|
|
81
|
+
// 正则字面量的判定沿用仓库既有启发式(tests/test-architecture-guard.mjs 的 stripCode 同款):
|
|
82
|
+
// 前一个有效字符说明这里不可能是除法,或者前一个词是 return/typeof 这类关键字。
|
|
83
|
+
const REGEX_PREV = new Set(['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '\n', ''])
|
|
84
|
+
const REGEX_KEYWORDS = new Set(['return', 'typeof', 'case', 'in', 'of', 'do', 'else', 'void', 'delete', 'new', 'instanceof', 'yield', 'await'])
|
|
85
|
+
let i = 0
|
|
86
|
+
while (i < src.length) {
|
|
87
|
+
const c = src[i]
|
|
88
|
+
const n = src[i + 1]
|
|
89
|
+
if (c === '/' && n === '/') {
|
|
90
|
+
const end = src.indexOf('\n', i)
|
|
91
|
+
const to = end < 0 ? src.length : end
|
|
92
|
+
blank(i, to)
|
|
93
|
+
i = to
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
if (c === '/' && n === '*') {
|
|
97
|
+
const end = src.indexOf('*/', i + 2)
|
|
98
|
+
const to = end < 0 ? src.length : end + 2
|
|
99
|
+
blank(i, to)
|
|
100
|
+
i = to
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (c === "'" || c === '"' || c === '`') {
|
|
104
|
+
let j = i + 1
|
|
105
|
+
while (j < src.length && src[j] !== c) {
|
|
106
|
+
if (src[j] === '\\') j += 1
|
|
107
|
+
j += 1
|
|
108
|
+
}
|
|
109
|
+
const end = j < src.length ? j + 1 : src.length
|
|
110
|
+
// 说明符字符串保留:紧跟在 `from` 之后,或作为 require(...) / import(...) 的第一个实参。
|
|
111
|
+
const asCallArgument = /^(?:require|import)$/u.test(callWordBefore(i))
|
|
112
|
+
const isSpecifier = keepSpecifier === true && c !== '`' && (wordBefore(i) === 'from' || asCallArgument)
|
|
113
|
+
if (!isSpecifier) blank(i + 1, end - 1)
|
|
114
|
+
i = end
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (c === '/') {
|
|
118
|
+
const pm = prevMeaningful(i)
|
|
119
|
+
if (REGEX_PREV.has(pm) || REGEX_KEYWORDS.has(wordBefore(i))) {
|
|
120
|
+
let j = i + 1
|
|
121
|
+
let inClass = false
|
|
122
|
+
while (j < src.length) {
|
|
123
|
+
const ch = src[j]
|
|
124
|
+
if (ch === '\\') { j += 2; continue }
|
|
125
|
+
if (ch === '[') inClass = true
|
|
126
|
+
else if (ch === ']') inClass = false
|
|
127
|
+
else if (ch === '/' && !inClass) { j += 1; break }
|
|
128
|
+
else if (ch === '\n') break
|
|
129
|
+
j += 1
|
|
130
|
+
}
|
|
131
|
+
blank(i, j)
|
|
132
|
+
i = j
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
i += 1
|
|
137
|
+
}
|
|
138
|
+
return out.join('')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 从一个绑定子句里取出**被导入的名字**(`a as b` 取 `a`;默认导入取它自己;`*` 交给命名空间那条路)。 */
|
|
142
|
+
function importedNamesOf(clause) {
|
|
143
|
+
const names = new Set()
|
|
144
|
+
const text = String(clause ?? '')
|
|
145
|
+
const brace = text.match(/\{([^}]*)\}/u)
|
|
146
|
+
if (brace !== null) {
|
|
147
|
+
for (const part of brace[1].split(',')) {
|
|
148
|
+
const seg = part.trim()
|
|
149
|
+
if (seg === '') continue
|
|
150
|
+
const imported = seg.split(/\s+as\s+/u)[0].trim()
|
|
151
|
+
if (/^[A-Za-z_$][\w$]*$/u.test(imported)) names.add(imported)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const rest = brace === null ? text : text.replace(brace[0], ' ')
|
|
155
|
+
for (const seg of rest.split(',')) {
|
|
156
|
+
const t = seg.trim()
|
|
157
|
+
if (t === '' || t === '*' || /\s/u.test(t)) continue
|
|
158
|
+
if (/^[A-Za-z_$][\w$]*$/u.test(t)) names.add(t)
|
|
159
|
+
}
|
|
160
|
+
return names
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 从源码里取出「真的从 `@deepseek-ai/dsh-settings` 绑定进来的名字」。
|
|
165
|
+
* 返回 `{ named, namespaces }`:`named` 是直接绑定的符号,`namespaces` 是包级命名空间
|
|
166
|
+
* (`import * as s` / `const s = require(…)`),它们的成员访问 `s.settingsNamespace` 也算引用。
|
|
167
|
+
*/
|
|
168
|
+
function settingsApiBindings(text) {
|
|
169
|
+
// 掩码只做一次:注释/字符串/正则变空格,模块说明符字符串保留 —— 判据据此认包。
|
|
170
|
+
const code = maskLiterals(text, true)
|
|
171
|
+
const named = new Set()
|
|
172
|
+
const namespaces = new Set()
|
|
173
|
+
// ① ESM 命名空间导入:`import * as s from '<mod>'`
|
|
174
|
+
for (const m of code.matchAll(new RegExp(`\\bimport\\s*\\*\\s*as\\s+([A-Za-z_$][\\w$]*)\\s+from\\s*${SPECIFIER_RE}`, 'gu'))) {
|
|
175
|
+
namespaces.add(m[1])
|
|
176
|
+
}
|
|
177
|
+
// ② ESM 具名/默认导入:`import { a, b as c } from '<mod>'` / `import d from '<mod>'`
|
|
178
|
+
// 子句里不允许再出现 import/from,长度封顶 2000 —— 挡住"跨到上一条 import 语句"的假匹配。
|
|
179
|
+
for (const m of code.matchAll(new RegExp(`\\bimport\\s+(?!\\*)((?:(?!\\bimport\\b|\\bfrom\\b)[\\s\\S]){0,2000}?)\\s+from\\s*${SPECIFIER_RE}`, 'gu'))) {
|
|
180
|
+
for (const name of importedNamesOf(m[1])) named.add(name)
|
|
181
|
+
}
|
|
182
|
+
// ③ CJS / 动态导入的解构绑定:`const { a } = require('<mod>')` / `= await import('<mod>')`
|
|
183
|
+
const callRe = `(?:require|import)\\s*\\(\\s*${SPECIFIER_RE}\\s*\\)`
|
|
184
|
+
for (const m of code.matchAll(new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*(?:await\\s+)?${callRe}`, 'gu'))) {
|
|
185
|
+
for (const name of importedNamesOf(`{${m[1]}}`)) named.add(name)
|
|
186
|
+
}
|
|
187
|
+
// ④ CJS / 动态导入的命名空间绑定:`const s = require('<mod>')`
|
|
188
|
+
for (const m of code.matchAll(new RegExp(`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*(?:await\\s+)?${callRe}`, 'gu'))) {
|
|
189
|
+
namespaces.add(m[1])
|
|
190
|
+
}
|
|
191
|
+
return { named, namespaces }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 命中的已删除符号列表(空数组 = 干净)。判据见文件头:只有真绑定/真成员访问才算。 */
|
|
195
|
+
function settingsApiReferences(text) {
|
|
196
|
+
const { named, namespaces } = settingsApiBindings(text)
|
|
197
|
+
const found = []
|
|
198
|
+
for (const sym of REMOVED_SETTINGS_SYMBOLS) {
|
|
199
|
+
if (named.has(sym)) { found.push(sym); continue }
|
|
200
|
+
for (const ns of namespaces) {
|
|
201
|
+
if (new RegExp(`(?<![\\w$.])${ns}\\s*\\.\\s*${sym}(?![\\w$])`, 'u').test(maskLiterals(text, true))) { found.push(sym); break }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return found
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 源码是否**引用**了某个已删除符号(对外契约不变;判据已收紧到"真绑定/真成员访问")。 */
|
|
208
|
+
function referencesRemovedSymbol(text, sym) {
|
|
209
|
+
return settingsApiReferences(text).includes(sym)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用。
|
|
214
|
+
*
|
|
215
|
+
* 2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
|
|
216
|
+
* 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
|
|
217
|
+
* = 整个服务启动崩溃。
|
|
218
|
+
*
|
|
219
|
+
* 扫描预算(未改):限深 3 层、上限 120 个文件、单文件读前 400KB。
|
|
220
|
+
*/
|
|
221
|
+
function scanSettingsApiUsage(pkgDir) {
|
|
222
|
+
const found = []
|
|
223
|
+
const seen = new Set()
|
|
224
|
+
const walk = (dir, depth) => {
|
|
225
|
+
if (depth > 3) return
|
|
226
|
+
let entries = []
|
|
227
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const full = join(dir, entry.name)
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
232
|
+
walk(full, depth + 1)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
|
|
236
|
+
if (seen.has(full)) continue
|
|
237
|
+
seen.add(full)
|
|
238
|
+
if (seen.size > 120) return
|
|
239
|
+
try {
|
|
240
|
+
const text = readFileSync(full, 'utf8').slice(0, 400000)
|
|
241
|
+
for (const sym of settingsApiReferences(text)) {
|
|
242
|
+
if (!found.includes(sym)) found.push(sym)
|
|
243
|
+
}
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
walk(pkgDir, 0)
|
|
248
|
+
return found
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { REMOVED_SETTINGS_SYMBOLS, SETTINGS_MODULE, maskLiterals, settingsApiBindings, settingsApiReferences, referencesRemovedSymbol, scanSettingsApiUsage }
|
|
@@ -327,7 +327,9 @@ async function routeAdaptUnlock(req, res, rc) {
|
|
|
327
327
|
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
328
328
|
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
329
329
|
if (check.decision === 'fail') {
|
|
330
|
-
|
|
330
|
+
// 0.5.33 改错 ①-2:文案必须写清「为什么不行 + 出路」——旧文案只有一句"适配校验未通过",
|
|
331
|
+
// 用户在界面上根本不知道下一步该做什么(而按钮此前又只在 adoptable 时出现,容易变成对不上的承诺)。
|
|
332
|
+
sendError(res, 409, `适配校验未通过:${String(check.reason ?? '适配校验未通过')}。出路:先点「检测更新」把它更新到兼容当前框架 ${fwVer} 的版本(更新完成会自动重跑源码扫描,通过即解锁);若它已不再需要,可在补丁里删掉/禁用这一行。本次未改动补丁与适配门清单。`)
|
|
331
333
|
return
|
|
332
334
|
}
|
|
333
335
|
await removeDisableBlock(patchPath, rowId)
|
|
@@ -5,7 +5,8 @@ import { readFileSync, existsSync } from 'node:fs'
|
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { detectAdoptablePending, detectCompat, gatingSummary, readCompatGate, readCompatPending } from '../domain/compat.js'
|
|
7
7
|
import { compUiUrl, findComponents } from '../domain/components.js'
|
|
8
|
-
import { detectFrameworkUpgrade } from '../domain/framework.js'
|
|
8
|
+
import { currentFrameworkVersion, detectFrameworkUpgrade } from '../domain/framework.js'
|
|
9
|
+
import { peerVetoFor } from '../domain/peer-veto.js'
|
|
9
10
|
import { readExtraBundleRows } from '../domain/install-job.js'
|
|
10
11
|
import { installJobView, readGithubAuth } from '../domain/install.js'
|
|
11
12
|
import { readPluginDetails } from '../domain/market.js'
|
|
@@ -46,6 +47,7 @@ async function routeStateGet(req, res, rc) {
|
|
|
46
47
|
setPatchHealReport(patchHeal)
|
|
47
48
|
}
|
|
48
49
|
const extraRows = await readExtraBundleRows(dirname(patchPath))
|
|
50
|
+
const profileDir = dirname(patchPath)
|
|
49
51
|
const compatPending = readCompatPending()
|
|
50
52
|
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
51
53
|
// 兼容门总开关 + 自动检测(只提示不自动开)
|
|
@@ -58,11 +60,21 @@ async function routeStateGet(req, res, rc) {
|
|
|
58
60
|
rollbackRec = { from: rr.from ?? null, to: rr.to ?? null, at: rr.at ?? null, applicable: true }
|
|
59
61
|
}
|
|
60
62
|
} catch {}
|
|
63
|
+
// 0.5.33 加法 D-⑧(诊断缺口):**补丁要求启用、运行时却没挂载**(fiberPhase === null)的行,
|
|
64
|
+
// 本地复算框架启动时的 peer 预检,补出短句原因 + 出路。框架那一侧只在**内存里**把
|
|
65
|
+
// row.disabled 置 true(不写用户补丁)、原因只 process.stderr.write —— 面板此前一个字都看不出。
|
|
66
|
+
// 判据与 dsh-app-boot 的 evaluatePluginCompatibility / preflight 对齐(见 domain/peer-veto.js)。
|
|
67
|
+
const vetoFrameworkVersion = currentFrameworkVersion(ctx)
|
|
61
68
|
const entries = listEntries(ctx).map((entry) => {
|
|
62
69
|
const meta = entryPkgMeta(entry.moduleName, ctx.baseUrl ?? 'file:///', profileDirOf(ctx))
|
|
70
|
+
const disabledByPatch = patch.disables.includes(entry.rowId)
|
|
71
|
+
let veto = null
|
|
72
|
+
if (!disabledByPatch && entry.fiberPhase === null && vetoFrameworkVersion !== null) {
|
|
73
|
+
try { veto = peerVetoFor({ moduleName: entry.moduleName, profileDir, frameworkVersion: vetoFrameworkVersion }) } catch { veto = null }
|
|
74
|
+
}
|
|
63
75
|
return {
|
|
64
76
|
...entry,
|
|
65
|
-
userDisabled:
|
|
77
|
+
userDisabled: disabledByPatch,
|
|
66
78
|
userForced: patch.forced.includes(entry.rowId),
|
|
67
79
|
extra: patch.inserts.includes(entry.rowId) || extraRows.has(entry.moduleName) || extraRows.has(entry.rowId),
|
|
68
80
|
installDate: meta?.installDate ?? null,
|
|
@@ -70,9 +82,11 @@ async function routeStateGet(req, res, rc) {
|
|
|
70
82
|
repository: meta?.repository ?? null,
|
|
71
83
|
// v0.3.45:只有"补丁里此刻确实还禁着它"的行才显示【待适配】——
|
|
72
84
|
// 记录与开关脱节时(用户手动启用过 / 补丁块被清掉)不能继续挂着「待适配」误导人
|
|
73
|
-
pendingCompat: compatPendingRows.has(entry.rowId) &&
|
|
85
|
+
pendingCompat: compatPendingRows.has(entry.rowId) && disabledByPatch,
|
|
74
86
|
// 自动检测结果(只提示,不自动开):该待适配行现在是否已适配(插件已更新 + 扫描通过)
|
|
75
87
|
adoptable: adoptable.get(entry.rowId) ?? null,
|
|
88
|
+
// 框架 peer 预检的本地复算结果(null = 无话可说;只在这类行上下发,不新增常驻面板行)
|
|
89
|
+
veto,
|
|
76
90
|
}
|
|
77
91
|
})
|
|
78
92
|
const compat = await detectCompat(ctx.baseUrl ?? 'file:///')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.33",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|