@noob-stupid/dsh-plugin-console 0.3.26 → 0.3.27
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 +13 -10
- package/README.zh.md +12 -7
- package/lib/client.js +162 -8
- package/lib/index.js +321 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,23 +20,25 @@
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
> ## 🎉 v0.3.
|
|
23
|
+
> ## 🎉 v0.3.27 released — family-bundle cards, never-crash safety & adapt-gate hardening (2026-09-04)
|
|
24
24
|
>
|
|
25
|
-
> **
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
> **
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
25
|
+
> **Family-bundle cards**: same-root subpath exports (e.g. `@linxin666/dsh-web-all/settings`) auto-group
|
|
26
|
+
> into one family card at the bottom — collapse/expand, batch check-updates, **one-click unlock-adapted**,
|
|
27
|
+
> known-check preview, `web-all/`-prefix-free sub-cards.
|
|
28
|
+
> **Never-crash safety**: pre-enable import probe (child-process) + patch auto-heal (core-row restore &
|
|
29
|
+
> missing-module auto-disable) + exports-fallback `package.json` resolution — the memory-plugin
|
|
30
|
+
> false-disable incident is fixed.
|
|
31
|
+
> **Adapt-gate hardening**: source-scan hard criterion (post-0.3.6 false-pass), migrate detection
|
|
32
|
+
> (`dsh.migrate.to`), softened dependency-strict judgment, pending hints moved into details.
|
|
33
|
+
> **Safe sub-package delete**: deleting a bundle sub-row disables only that row and keeps the bundle —
|
|
34
|
+
> fixing the incident where deleting one sub-package removed the whole family bundle.
|
|
33
35
|
>
|
|
34
36
|
> **Install:**
|
|
35
37
|
> ```bash
|
|
36
38
|
> npm i -g @noob-stupid/dsh-plugin-console
|
|
37
39
|
> # or in DSH: dsh plugin --profile web add @noob-stupid/dsh-plugin-console
|
|
38
40
|
> ```
|
|
39
|
-
> **Try it:** Plugin Console →
|
|
41
|
+
> **Try it:** Plugin Console → family card at the bottom → 「批量检测更新 / 一键启用已适配」.
|
|
40
42
|
>
|
|
41
43
|
> Feedback: [GitHub Issues](https://github.com/Noob-stupid/dsh-plugin-hub/issues).
|
|
42
44
|
A **plugin management panel** for the DeepSeek Harness (DSH) Web GUI: one-click
|
|
@@ -86,6 +88,7 @@ auto-rollback).
|
|
|
86
88
|
|---|---|---|
|
|
87
89
|
| 🤖 | **AI Empower (v0.3.24)** | Give the console a package name or GitHub repo — the local AI reads docs, drafts a **deployment plan** (install / write config / start service / health check) and executes it safely after your confirmation; server-type components get an automatic control card |
|
|
88
90
|
| 🛡️ | **Framework upgrade safety & adapt gate (v0.3.25)** | Force-disables plugins incompatible with the new framework (enable locked; 「Check update → Update & adapt」auto-verifies and unlocks); full-tree checkpoint before upgrade, automatic rollback on relaunch failure, one-click rollback to the previous version |
|
|
91
|
+
| 🏠 | **Family-bundle cards & safety (v0.3.27)** | Same-root subpath exports group into one family card (collapse/expand, batch check-update, one-click unlock-adapted, known-check preview); never-crash safety (pre-enable import probe + patch auto-heal + exports fallback); adapt-gate source-scan hard criterion & migrate detection; deleting a bundle sub-row disables only that row |
|
|
89
92
|
| 🚀 | **Server component cards** | Left-side floating card auto-aligned to the main panel: start / stop / status / **open Web UI** buttons, multi-server dropdown, collapsible |
|
|
90
93
|
| 🧩 | **Plugin & skill hub** | Auto-collected index of `dsh-plugin` topic repos (**500+** by stars) plus a **Skills tab** (`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`, up to 300) — browse, search, one-click install, no GitHub API calls |
|
|
91
94
|
| 🤖 | **Auto-collection CI** | GitHub Actions reruns `build-index` every 6 hours (manual trigger available); authors just add the `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` topic — no application needed |
|
package/README.zh.md
CHANGED
|
@@ -6,19 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
# DSH 插件中心(dsh-plugin-hub)
|
|
8
8
|
|
|
9
|
-
> ## 🎉 v0.3.
|
|
9
|
+
> ## 🎉 v0.3.27 正式发布 — 全家桶分组卡片、永不崩安全与适配门强化(2026-09-04)
|
|
10
10
|
>
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
11
|
+
> **全家桶分组卡片**:同根包子路径导出(如 `@linxin666/dsh-web-all/settings`)自动聚合为一张
|
|
12
|
+
> 全家桶卡片(列表底部)——收起/展开、批量检测更新、**一键启用已适配**、已知校验预览,子卡剥离
|
|
13
|
+
> `web-all/` 前缀。
|
|
14
|
+
> **永不崩安全**:启用前子进程导入探针 + 补丁自动自愈(核心行恢复/缺失模块自动禁用)+
|
|
15
|
+
> exports 回退 `package.json` 解析——记忆插件被误禁用的事故已修复。
|
|
16
|
+
> **适配门强化**:源码扫描硬判据(0.3.6 假通过教训)、迁移检测(`dsh.migrate.to`)、
|
|
17
|
+
> 依赖严格判定软化、待适配提示移入详情。
|
|
18
|
+
> **子包删除安全**:删除聚合包子行仅停用该行、不动 bundle——修复"删一个子包导致整个全家桶消失"的事故。
|
|
15
19
|
>
|
|
16
20
|
> **安装:**
|
|
17
21
|
> ```bash
|
|
18
22
|
> npm i -g @noob-stupid/dsh-plugin-console
|
|
19
|
-
> # 或在 DSH
|
|
23
|
+
> # 或在 DSH 中:dsh plugin --profile web add @noob-stupid/dsh-plugin-console
|
|
20
24
|
> ```
|
|
21
|
-
> **体验入口:** 插件控制台 →
|
|
25
|
+
> **体验入口:** 插件控制台 → 列表底部全家桶卡片 → 批量检测更新 / 一键启用已适配。
|
|
22
26
|
>
|
|
23
27
|
> 反馈:[GitHub Issues](https://github.com/Noob-stupid/dsh-plugin-hub/issues)。
|
|
24
28
|
|
|
@@ -77,6 +81,7 @@
|
|
|
77
81
|
|---|---|---|
|
|
78
82
|
| 🤖 | **AI 赋能(v0.3.24)** | 输入 npm 包名 / GitHub 仓库,本地 AI 读文档生成**部署计划**(安装/写配置/启动服务/健康检查),你确认后安全执行;服务器类组件自动生成控制卡片 |
|
|
79
83
|
| 🛡️ | **框架升级适配门与安全(v0.3.25)** | 升级后不兼容插件强制禁用(适配门锁定,「检测更新 → 更新并适配 → 自动校验解锁」);升级前全树 checkpoint、拉起失败自动全树回滚、框架卡片一键回滚到上一版 |
|
|
84
|
+
| 🏠 | **全家桶分组卡片与安全(v0.3.27)** | 同根包子路径导出自动聚合为一张全家桶卡片(收起/展开、批量检测更新、一键启用已适配、已知校验预览);永不崩安全(启用前导入探针 + 补丁自愈 + exports 回退);适配门源码扫描硬判据与迁移检测;删除聚合包子行仅停用该行 |
|
|
80
85
|
| 🚀 | **服务器组件卡片** | 左侧浮卡与主面板顶边对齐:启动 / 停止 / 状态 / 【打开】直达 Web UI,多服务器下拉、可折叠(状态记忆) |
|
|
81
86
|
| 🧩 | **插件 + 技能双市场** | 自动收录 `dsh-plugin` topic 仓库(按 star **500+**),另有**技能 tab**(`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`,最多 300)——浏览、搜索、一键安装,零 GitHub API 调用;插件自带技能(如 openviking-memory)只读展示 |
|
|
82
87
|
| 🤖 | **自动收录 CI** | GitHub Actions 每 6 小时自动重跑 `build-index`(也支持手动触发);作者只需给自己的仓库打 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签,**无需申请、无需审核** |
|
package/lib/client.js
CHANGED
|
@@ -104,6 +104,21 @@ window.__ModuleLoader__.load({
|
|
|
104
104
|
pendingCompatTag: "待适配",
|
|
105
105
|
pendingCompatHint: "框架升级后被强制禁用:先点「检测更新」,如有新版按钮会变为「更新并适配」,点击后更新完成自动校验通过即解锁启用",
|
|
106
106
|
updateAndAdapt: "更新并适配",
|
|
107
|
+
familyTag: "全家桶",
|
|
108
|
+
familySubs: "子包",
|
|
109
|
+
familyUnlockAll: "一键启用已适配",
|
|
110
|
+
familyUnlockAllTip: "对全家桶内所有源码扫描通过(已适配)的子包批量解锁启用;未通过的保持禁用",
|
|
111
|
+
familyUnlockAllConfirm: "将对全家桶内所有已适配(源码扫描通过)的子包一键解锁启用?未通过的将保持禁用。",
|
|
112
|
+
familyUnlockAllDone: "已批量适配解锁",
|
|
113
|
+
familyExpand: "展开",
|
|
114
|
+
familyCollapse: "收起",
|
|
115
|
+
familyUpdate: "全家桶版本检测",
|
|
116
|
+
familyCheckUpdate: "批量检测更新",
|
|
117
|
+
adaptUnlockBtn: "已适配,立即解锁",
|
|
118
|
+
adaptUnlockHint: "已安装版本经源码扫描确认适配当前框架:立即移除禁用块并解锁启用",
|
|
119
|
+
adaptUnlockDone: "已适配解锁:刷新后该行可正常启用",
|
|
120
|
+
migrateTip: "项目已迁移(适配当前框架)",
|
|
121
|
+
migrateBtn: "迁移并适配",
|
|
107
122
|
aiEmpowerFrameworkCheck: "框架适配检测",
|
|
108
123
|
userForcedTag: "补丁强制启用",
|
|
109
124
|
protectedTag: "受保护",
|
|
@@ -404,6 +419,21 @@ window.__ModuleLoader__.load({
|
|
|
404
419
|
pendingCompatTag: "Pending adapt",
|
|
405
420
|
pendingCompatHint: "Force-disabled after framework upgrade: click Check update first; if a newer version exists the button becomes Update & adapt - click it and it unlocks automatically after the compat check",
|
|
406
421
|
updateAndAdapt: "Update & adapt",
|
|
422
|
+
familyTag: "Family bundle",
|
|
423
|
+
familySubs: "sub packages",
|
|
424
|
+
familyUnlockAll: "Unlock all adapted",
|
|
425
|
+
familyUnlockAllTip: "Batch-unlock every sub package in this family whose source scan passed; failed ones stay disabled",
|
|
426
|
+
familyUnlockAllConfirm: "Unlock all adapted (source-scan passed) sub packages in this family? Failed ones stay disabled.",
|
|
427
|
+
familyUnlockAllDone: "Family batch unlock done",
|
|
428
|
+
familyExpand: "Expand",
|
|
429
|
+
familyCollapse: "Collapse",
|
|
430
|
+
familyUpdate: "Family version check",
|
|
431
|
+
familyCheckUpdate: "Check all",
|
|
432
|
+
adaptUnlockBtn: "Adapted - unlock now",
|
|
433
|
+
adaptUnlockHint: "Installed version passed the source scan for the current framework: unlock immediately",
|
|
434
|
+
adaptUnlockDone: "Unlocked: refresh and the row is enabled",
|
|
435
|
+
migrateTip: "Project migrated (adapted to the current framework)",
|
|
436
|
+
migrateBtn: "Migrate & adapt",
|
|
407
437
|
aiEmpowerFrameworkCheck: "Framework compat check",
|
|
408
438
|
userForcedTag: "Forced on by patch",
|
|
409
439
|
protectedTag: "Protected",
|
|
@@ -971,10 +1001,12 @@ window.__ModuleLoader__.load({
|
|
|
971
1001
|
function PluginConsoleTab({ t }) {
|
|
972
1002
|
const [state, setState] = react.useState({ status: "loading" });
|
|
973
1003
|
const [busy, setBusy] = react.useState(null);
|
|
1004
|
+
const [adaptUnlocking, setAdaptUnlocking] = react.useState(null);
|
|
974
1005
|
const [message, setMessage] = react.useState(null);
|
|
975
1006
|
const [reloadHint, setReloadHint] = react.useState(false);
|
|
976
1007
|
const [restartHint, setRestartHint] = react.useState(false);
|
|
977
1008
|
const [jobs, setJobs] = react.useState({});
|
|
1009
|
+
const [familyCollapsed, setFamilyCollapsed] = react.useState({});
|
|
978
1010
|
const [query, setQuery] = react.useState(() => {
|
|
979
1011
|
try { return localStorage.getItem("pc-market-query") ?? ""; } catch { return ""; }
|
|
980
1012
|
});
|
|
@@ -2239,13 +2271,31 @@ window.__ModuleLoader__.load({
|
|
|
2239
2271
|
: ((searchSourcesList !== null ? searchSourcesList.find((s) => s.id === source)?.name : undefined) ?? source);
|
|
2240
2272
|
const extraIds = new Set((state.status === "ready" ? state.data.patch?.inserts ?? [] : []));
|
|
2241
2273
|
const extraCount = state.status === "ready" ? state.data.entries.filter((entry) => entry.extra === true || extraIds.has(entry.rowId)).length : 0;
|
|
2242
|
-
const
|
|
2274
|
+
const pkgRootOf = (m) => { const mm = String(m ?? ""); const x = mm.match(/^(@[^/]+\/[^/]+)(?:\/.*)?$/u); return x ? x[1] : mm; };
|
|
2275
|
+
const filteredEntries = state.status === "ready"
|
|
2243
2276
|
? state.data.entries.filter((entry) => {
|
|
2244
2277
|
if (extraOnly && !(entry.extra === true || extraIds.has(entry.rowId))) return false;
|
|
2245
2278
|
if (installedQueryNorm === "") return true;
|
|
2246
2279
|
return [entry.moduleName, entry.entryId, entry.rowId]
|
|
2247
2280
|
.some((value) => String(value ?? "").toLowerCase().includes(installedQueryNorm));
|
|
2248
|
-
})
|
|
2281
|
+
})
|
|
2282
|
+
: [];
|
|
2283
|
+
// 聚合包分组:仅「同根包 + 子路径导出」(web-all 0.3.14 式,moduleName=pkg/sub)→ 全家桶组;
|
|
2284
|
+
// 经典多子包聚合(每子包独立名)不归组。组头与子包卡片一律置于列表【最底部】。
|
|
2285
|
+
const famRootOf = (entry) => {
|
|
2286
|
+
const m = String(entry.moduleName ?? "");
|
|
2287
|
+
if (m.startsWith("@deepseek-ai/")) return null;
|
|
2288
|
+
return pkgRootOf(m);
|
|
2289
|
+
};
|
|
2290
|
+
const familyCounts = new Map();
|
|
2291
|
+
for (const entry of (state.status === "ready" ? state.data.entries : [])) {
|
|
2292
|
+
const root = famRootOf(entry);
|
|
2293
|
+
if (root === null) continue;
|
|
2294
|
+
familyCounts.set(root, (familyCounts.get(root) ?? 0) + 1);
|
|
2295
|
+
}
|
|
2296
|
+
const familyCards = filteredEntries.map((entry) => {
|
|
2297
|
+
const familyRoot = String(entry.moduleName ?? "").startsWith("@deepseek-ai/") ? null : pkgRootOf(entry.moduleName);
|
|
2298
|
+
void familyRoot;
|
|
2249
2299
|
const open = details !== null && details.entryId === entry.entryId;
|
|
2250
2300
|
let detailPanel = null;
|
|
2251
2301
|
if (open) {
|
|
@@ -2258,10 +2308,13 @@ window.__ModuleLoader__.load({
|
|
|
2258
2308
|
} else {
|
|
2259
2309
|
const meta = details.data.meta;
|
|
2260
2310
|
const readme = details.data.readme;
|
|
2311
|
+
const compatInfo = state.status === "ready" && state.data.compatPending
|
|
2312
|
+
? (state.data.compatPending.pending ?? []).find((p) => p.rowId === entry.rowId) ?? null
|
|
2313
|
+
: null;
|
|
2261
2314
|
detailPanel = el("div", { className: styles.detail },
|
|
2262
2315
|
meta !== null ? el("p", { className: styles.desc }, meta.description ?? t("noDetail")) : null,
|
|
2263
2316
|
entry.pendingCompat === true
|
|
2264
|
-
? el("p", { className: styles.status, "data-pending": "true" }, t("pendingCompatHint"))
|
|
2317
|
+
? el("p", { className: styles.status, "data-pending": "true" }, t("pendingCompatHint") + (compatInfo && compatInfo.checkNote ? ";已知校验:" + compatInfo.checkNote : ""))
|
|
2265
2318
|
: null,
|
|
2266
2319
|
meta !== null && meta.version ? el("div", { className: styles.rowTop },
|
|
2267
2320
|
el("p", { className: styles.status }, t("versionLabel") + ":" + meta.version),
|
|
@@ -2276,7 +2329,38 @@ window.__ModuleLoader__.load({
|
|
|
2276
2329
|
className: styles.toggle,
|
|
2277
2330
|
title: t("aiEmpowerTitle"),
|
|
2278
2331
|
onClick: () => openAiFor(String(entry.moduleName ?? entry.repo ?? "")),
|
|
2279
|
-
}, t("aiEmpowerLabel"))
|
|
2332
|
+
}, t("aiEmpowerLabel")),
|
|
2333
|
+
entry.pendingCompat === true
|
|
2334
|
+
? el("button", {
|
|
2335
|
+
type: "button",
|
|
2336
|
+
className: styles.toggle,
|
|
2337
|
+
"data-pending": "true",
|
|
2338
|
+
title: t("adaptUnlockHint"),
|
|
2339
|
+
disabled: installing === entry.moduleName || adaptUnlocking === entry.entryId,
|
|
2340
|
+
onClick: () => {
|
|
2341
|
+
setAdaptUnlocking(entry.entryId);
|
|
2342
|
+
call("/plugin-console/adapt-unlock", { rowId: entry.rowId }).then(
|
|
2343
|
+
() => {
|
|
2344
|
+
setAdaptUnlocking(null);
|
|
2345
|
+
setMessage(t("adaptUnlockDone"));
|
|
2346
|
+
window.setTimeout(() => window.location.reload(), 1200);
|
|
2347
|
+
},
|
|
2348
|
+
(error) => { setAdaptUnlocking(null); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
|
|
2349
|
+
);
|
|
2350
|
+
},
|
|
2351
|
+
}, adaptUnlocking === entry.entryId ? t("loadingPhase") : t("adaptUnlockBtn"))
|
|
2352
|
+
: null) : null,
|
|
2353
|
+
updateCheck !== null && updateCheck.status === "ready" && updateCheck.migrate && updateCheck.migrate.compatible
|
|
2354
|
+
? el("div", null,
|
|
2355
|
+
el("div", { className: styles.rowTop },
|
|
2356
|
+
el("p", { className: styles.status }, t("migrateTip") + ":" + String(updateCheck.migrate.to ?? "") + (updateCheck.migrate.latest ? " v" + updateCheck.migrate.latest : "")),
|
|
2357
|
+
el("button", {
|
|
2358
|
+
type: "button",
|
|
2359
|
+
className: styles.toggle,
|
|
2360
|
+
disabled: installing === entry.moduleName,
|
|
2361
|
+
onClick: () => startJob("", updateCheck.migrate.to, "github", "plugin", false),
|
|
2362
|
+
}, installing === entry.moduleName ? t("installing") : t("migrateBtn"))))
|
|
2363
|
+
: null,
|
|
2280
2364
|
updateCheck !== null && updateCheck.status === "ready"
|
|
2281
2365
|
? (updateCheck.error
|
|
2282
2366
|
? el("p", { className: styles.status, "data-error": "true" }, t("updateCheckFailed") + ":" + updateCheck.error)
|
|
@@ -2314,7 +2398,13 @@ window.__ModuleLoader__.load({
|
|
|
2314
2398
|
}
|
|
2315
2399
|
return el("li", { key: entry.entryId, className: styles.row },
|
|
2316
2400
|
el("div", { className: styles.rowTop },
|
|
2317
|
-
el("strong", { className: styles.name, title: entry.moduleName },
|
|
2401
|
+
el("strong", { className: styles.name, title: entry.moduleName }, (() => {
|
|
2402
|
+
const famRoot = famRootOf(entry);
|
|
2403
|
+
const mn = String(entry.moduleName ?? "");
|
|
2404
|
+
return famRoot !== null && (familyCounts.get(famRoot) ?? 0) > 1 && mn.startsWith(famRoot + "/")
|
|
2405
|
+
? mn.slice(famRoot.length + 1)
|
|
2406
|
+
: moduleShortName(entry.moduleName);
|
|
2407
|
+
})()),
|
|
2318
2408
|
el("span", { className: styles.tag, "data-enabled": entry.enabled ? "true" : "false" },
|
|
2319
2409
|
t(entry.enabled ? "enabledTag" : "disabledTag")),
|
|
2320
2410
|
entry.extra === true ? el("span", { className: styles.tag, "data-user": "true" }, t("extraTag")) : null,
|
|
@@ -2360,7 +2450,7 @@ window.__ModuleLoader__.load({
|
|
|
2360
2450
|
setMessage(t("deleteBundleNote") + ":" + (data.packageName ?? "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2361
2451
|
setRestartHint(true);
|
|
2362
2452
|
} else {
|
|
2363
|
-
setMessage(t("deleteNote") + ":" + (data.packageName ?? "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2453
|
+
setMessage(t("deleteNote") + ":" + (data.packageName ?? "") + (data.note ? ";" + data.note : "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2364
2454
|
setTimeout(refresh, 1500);
|
|
2365
2455
|
}
|
|
2366
2456
|
},
|
|
@@ -2388,8 +2478,72 @@ window.__ModuleLoader__.load({
|
|
|
2388
2478
|
),
|
|
2389
2479
|
detailPanel,
|
|
2390
2480
|
);
|
|
2391
|
-
})
|
|
2392
|
-
|
|
2481
|
+
});
|
|
2482
|
+
// 全家桶组头:聚合包(同根包 ≥2 行)合并展示为一个「web 包」卡片
|
|
2483
|
+
const familyHeaders = [];
|
|
2484
|
+
const familySeen = new Set();
|
|
2485
|
+
for (const entry of filteredEntries) {
|
|
2486
|
+
const root = famRootOf(entry);
|
|
2487
|
+
if (root === null || (familyCounts.get(root) ?? 0) <= 1 || familySeen.has(root)) continue;
|
|
2488
|
+
familySeen.add(root);
|
|
2489
|
+
familyHeaders.push(el("div", { key: "family-" + root, className: styles.detail, style: { border: "1px solid var(--dsw-alias-state-business-primary)", borderRadius: 10, padding: "8px 12px", gridColumn: "1 / -1" } },
|
|
2490
|
+
el("div", { className: styles.rowTop, style: { alignItems: "center" } },
|
|
2491
|
+
el("strong", { className: styles.name, style: { flex: "0 1 auto", minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, title: root }, root.replace(/^@[^/]+\//u, "").replace(/^dsh-/u, "")),
|
|
2492
|
+
el("span", { className: styles.tag, "data-user": "true" }, t("familyTag") + " · " + (familyCounts.get(root) ?? 0) + " " + t("familySubs")),
|
|
2493
|
+
el("button", {
|
|
2494
|
+
type: "button",
|
|
2495
|
+
className: styles.toggle,
|
|
2496
|
+
style: { whiteSpace: "nowrap", flexShrink: 0 },
|
|
2497
|
+
onClick: () => setFamilyCollapsed((prev) => ({ ...prev, [root]: prev[root] !== true })),
|
|
2498
|
+
}, familyCollapsed[root] === true ? "▸ " + t("familyExpand") : "▾ " + t("familyCollapse")),
|
|
2499
|
+
el("button", {
|
|
2500
|
+
type: "button",
|
|
2501
|
+
className: styles.toggle,
|
|
2502
|
+
style: { whiteSpace: "nowrap", flexShrink: 0 },
|
|
2503
|
+
onClick: () => {
|
|
2504
|
+
setMessage(t("checkingUpdate"));
|
|
2505
|
+
call("/plugin-console/check-update", { packageName: root }).then(
|
|
2506
|
+
(d) => setMessage(t("familyUpdate") + ":" + root + " " + (d?.latest ?? "?") + (d?.migrate && d.migrate.compatible ? " → " + d.migrate.to + " v" + d.migrate.latest : "") + (d?.error ? "(" + d.error + ")" : "")),
|
|
2507
|
+
(e) => setMessage(t("failed") + ":" + friendlyGithubError(e).message),
|
|
2508
|
+
);
|
|
2509
|
+
},
|
|
2510
|
+
}, t("familyCheckUpdate")),
|
|
2511
|
+
el("button", {
|
|
2512
|
+
type: "button",
|
|
2513
|
+
className: styles.toggle,
|
|
2514
|
+
style: { whiteSpace: "nowrap", flexShrink: 0 },
|
|
2515
|
+
title: t("familyUnlockAllTip"),
|
|
2516
|
+
onClick: () => {
|
|
2517
|
+
if (!window.confirm(t("familyUnlockAllConfirm"))) return;
|
|
2518
|
+
call("/plugin-console/adapt-unlock-all", { root }).then(
|
|
2519
|
+
(d) => {
|
|
2520
|
+
setMessage(t("familyUnlockAllDone") + ":" + (d.unlocked ?? []).length + " 行" + (Array.isArray(d.kept) && d.kept.length > 0 ? ";保留 " + d.kept.length + " 行(未通过校验)" : ""));
|
|
2521
|
+
window.setTimeout(() => window.location.reload(), 1500);
|
|
2522
|
+
},
|
|
2523
|
+
(e) => setMessage(t("failed") + ":" + friendlyGithubError(e).message),
|
|
2524
|
+
);
|
|
2525
|
+
},
|
|
2526
|
+
}, t("familyUnlockAll")))
|
|
2527
|
+
));
|
|
2528
|
+
}
|
|
2529
|
+
// 组头放到最底部:普通条目在前,全家桶(组头+子包卡片)整体在后
|
|
2530
|
+
const nonFamilyCards = [];
|
|
2531
|
+
const familySections = new Map();
|
|
2532
|
+
familyCards.forEach((card, i) => {
|
|
2533
|
+
const e = filteredEntries[i];
|
|
2534
|
+
if (e === undefined) { nonFamilyCards.push(card); return; }
|
|
2535
|
+
const root = famRootOf(e);
|
|
2536
|
+
if (root === null || (familyCounts.get(root) ?? 0) <= 1) { nonFamilyCards.push(card); return; }
|
|
2537
|
+
if (!familySections.has(root)) familySections.set(root, { head: familyHeaders.find((h) => h && h.key === "family-" + root) ?? null, cards: [] });
|
|
2538
|
+
familySections.get(root).cards.push(card);
|
|
2539
|
+
});
|
|
2540
|
+
const familySectionEls = [];
|
|
2541
|
+
for (const [, section] of familySections) {
|
|
2542
|
+
if (section.head !== null) familySectionEls.push(section.head);
|
|
2543
|
+
const root = section.head !== null ? section.head.key.slice("family-".length) : null;
|
|
2544
|
+
if (root === null || familyCollapsed[root] !== true) familySectionEls.push(...section.cards);
|
|
2545
|
+
}
|
|
2546
|
+
const rows = state.status === "ready" ? [...nonFamilyCards, ...familySectionEls] : [];
|
|
2393
2547
|
const marketShown = market === null || market.status !== "ready"
|
|
2394
2548
|
? []
|
|
2395
2549
|
: officialOnly
|
package/lib/index.js
CHANGED
|
@@ -132,8 +132,8 @@ const FIBER_PHASE = {
|
|
|
132
132
|
* (有入口)都是这样安装的,当作插件条目 insert 会漏掉它们的 bundle 补丁。 */
|
|
133
133
|
async function detectBundleOnly(profileDir, packageName) {
|
|
134
134
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
135
|
+
const pkgPath = resolvePackageJson(packageName, profileDir)
|
|
136
|
+
if (pkgPath === null) throw new Error('not found')
|
|
137
137
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
138
138
|
return typeof pkg.dsh?.bundle?.patch === 'string'
|
|
139
139
|
} catch {
|
|
@@ -158,6 +158,37 @@ async function addBundleToManifest(profileDir, packageName) {
|
|
|
158
158
|
/** 官方 profile 模板自带的 bundle(其余 bundle 视为用户额外添加)。 */
|
|
159
159
|
const DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']
|
|
160
160
|
|
|
161
|
+
/** 包名归一:支持子路径导出形式(@linxin666/dsh-web-all/settings → @linxin666/dsh-web-all)。 */
|
|
162
|
+
function packageNameOf(moduleName) {
|
|
163
|
+
if (typeof moduleName !== 'string') return moduleName
|
|
164
|
+
const m = moduleName.match(/^(@[^/]+\/[^/]+)(?:\/.*)?$/u)
|
|
165
|
+
return m ? m[1] : moduleName
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 把 file:// URL(如 ctx.baseUrl)转成目录路径(resolve 用)。 */
|
|
169
|
+
function baseDirOf(baseUrl) {
|
|
170
|
+
try {
|
|
171
|
+
if (typeof baseUrl === 'string' && baseUrl.startsWith('file:')) return dirname(fileURLToPath(baseUrl))
|
|
172
|
+
} catch {}
|
|
173
|
+
return typeof baseUrl === 'string' && baseUrl !== '' ? baseUrl : '.'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 解析包 package.json 的绝对路径(exports 限制包 fallback):
|
|
178
|
+
* require.resolve('pkg/package.json') 对声明了 exports 且不含 './package.json' 的包会抛错
|
|
179
|
+
* (2026-09-04 教训:@openviking/dsh-memory-plugin 因此被 heal 误判模块缺失并自动禁用)——
|
|
180
|
+
* fallback 直接查 node_modules 物理路径。
|
|
181
|
+
*/
|
|
182
|
+
function resolvePackageJson(pkgName, baseDir) {
|
|
183
|
+
const name = packageNameOf(pkgName)
|
|
184
|
+
try {
|
|
185
|
+
return createRequire(join(baseDir, 'package.json')).resolve(`${name}/package.json`)
|
|
186
|
+
} catch {}
|
|
187
|
+
const parts = String(name).split('/')
|
|
188
|
+
const candidate = join(baseDir, 'node_modules', ...parts, 'package.json')
|
|
189
|
+
return existsSync(candidate) ? candidate : null
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
/** 已加载插件的包元信息缓存(安装日期/版本/仓库),60 秒 TTL。 */
|
|
162
193
|
const pkgMetaCache = new Map()
|
|
163
194
|
const PKG_META_TTL = 60000
|
|
@@ -167,8 +198,8 @@ function entryPkgMeta(moduleName, baseUrl) {
|
|
|
167
198
|
if (hit !== undefined && Date.now() - hit.at < PKG_META_TTL) return hit
|
|
168
199
|
const meta = { at: Date.now(), installDate: null, version: null, repository: null }
|
|
169
200
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
201
|
+
const pkgPath = resolvePackageJson(moduleName, baseDirOf(baseUrl))
|
|
202
|
+
if (pkgPath === null) throw new Error('not found')
|
|
172
203
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
173
204
|
// 安装日期:优先 curl 通道写入的 .dsh-installed-at 标记(真实安装时刻);
|
|
174
205
|
// 否则用 package.json 的 mtime,但 npm tarball 会把文件时间固定为 1985-10-26
|
|
@@ -352,6 +383,8 @@ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
|
|
|
352
383
|
return { decision: 'fail', reason: `源码仍引用 0.1.2 起已删除的 dsh-settings API(${broken.join('、')})——实际不兼容,启用会让整个服务启动崩溃` }
|
|
353
384
|
}
|
|
354
385
|
if (typeof declared === 'string' && declared !== '') return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion},且源码无已删除 API 引用` }
|
|
386
|
+
// 源码扫描干净 = 权威判据:依赖范围可能滞后旧版("假拒绝"),不作为 fail。
|
|
387
|
+
return { decision: 'unknown', reason: '源码扫描无已删除 API 引用(声明/依赖范围为参考,不作为不兼容判据)' }
|
|
355
388
|
} else if (typeof declared === 'string' && declared !== '') {
|
|
356
389
|
return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
|
|
357
390
|
}
|
|
@@ -374,8 +407,8 @@ async function readExtraBundleRows(profileDir) {
|
|
|
374
407
|
for (const pkg of bundles) {
|
|
375
408
|
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
376
409
|
try {
|
|
377
|
-
const
|
|
378
|
-
const dir = dirname(
|
|
410
|
+
const pk = resolvePackageJson(pkg, profileDir)
|
|
411
|
+
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
379
412
|
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
380
413
|
const lines = text.split(/\r?\n/u)
|
|
381
414
|
let inInsert = false
|
|
@@ -463,8 +496,8 @@ async function readExtraBundleOwners(profileDir) {
|
|
|
463
496
|
for (const pkg of bundles) {
|
|
464
497
|
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
465
498
|
try {
|
|
466
|
-
const
|
|
467
|
-
const dir = dirname(
|
|
499
|
+
const pk = resolvePackageJson(pkg, profileDir)
|
|
500
|
+
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
468
501
|
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
469
502
|
const lines = text.split(/\r?\n/u)
|
|
470
503
|
let inInsert = false
|
|
@@ -850,6 +883,119 @@ const FRAMEWORK_STATE_FILE = () => join(dshHome(), 'plugin-console', 'framework-
|
|
|
850
883
|
const FRAMEWORK_BACKUP_ROOT = () => join(dshHome(), 'plugin-console', 'framework-backups')
|
|
851
884
|
const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
852
885
|
|
|
886
|
+
/** 框架核心行:误禁用会导致启动失败(2026-09-04 session-persistence-jsonl 事故:6 行 pending、
|
|
887
|
+
* 启动断言失败)。任何补丁写入(适配门/脚本/工具)都禁止禁用这些行;自愈机制自动恢复误禁。 */
|
|
888
|
+
const CORE_PATCH_ROW_IDS = new Set([
|
|
889
|
+
'session-persistence-jsonl', 'webserver', 'timer', 'hmr', 'session', 'session-checkpoint-policy',
|
|
890
|
+
'message-feedback', 'workspace', 'storage', 'storage-json', 'storage-domain', 'api-gateway',
|
|
891
|
+
'api-session-controller', 'api-workspace-controller', 'credentials', 'settings', 'attachment-local',
|
|
892
|
+
'subprocess', 'sandbox', 'sandbox-policy', 'shell-env', 'agent', 'agent-loop', 'llm', 'web-runtime', 'web-startup',
|
|
893
|
+
])
|
|
894
|
+
|
|
895
|
+
let patchHealAt = null
|
|
896
|
+
let patchHealReport = null
|
|
897
|
+
|
|
898
|
+
/** 解析用户补丁中 insert 块的 id → moduleName(name 字段)。 */
|
|
899
|
+
function parseInsertNames(text) {
|
|
900
|
+
const map = new Map()
|
|
901
|
+
const lines = text.split(/\r?\n/u)
|
|
902
|
+
let inInsert = false
|
|
903
|
+
let curId = null
|
|
904
|
+
for (const line of lines) {
|
|
905
|
+
if (/^- insert:\s*$/u.test(line)) { inInsert = true; curId = null; continue }
|
|
906
|
+
if (inInsert && /^- /u.test(line)) inInsert = false
|
|
907
|
+
if (inInsert) {
|
|
908
|
+
const idMatch = line.match(/^\s+- id: ([A-Za-z0-9_.-]+)/u)
|
|
909
|
+
if (idMatch) { curId = idMatch[1]; continue }
|
|
910
|
+
if (curId !== null) {
|
|
911
|
+
const nameMatch = line.match(/^\s+name: ['"]([^'"]+)['"]/u)
|
|
912
|
+
if (nameMatch) { map.set(curId, nameMatch[1]); curId = null }
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return map
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* 补丁安全自愈(服务永不崩机制):
|
|
921
|
+
* ① 核心行被误禁用 → 自动移除禁用块恢复;
|
|
922
|
+
* ② 启用态用户 insert 行的模块缺失(如引用未安装包的行)→ 自动禁用(loader 对缺失模块会致命崩溃)。
|
|
923
|
+
* return { healed:[], autoDisabled:[], healedAt } —— healedAt=0 表示本次无修改。
|
|
924
|
+
*/
|
|
925
|
+
async function healPatchSafety(patchPath) {
|
|
926
|
+
const profileDir = dirname(patchPath)
|
|
927
|
+
const { text } = await readPatchState(patchPath)
|
|
928
|
+
let next = text
|
|
929
|
+
const healed = []
|
|
930
|
+
const autoDisabled = []
|
|
931
|
+
for (const id of CORE_PATCH_ROW_IDS) {
|
|
932
|
+
const re = new RegExp(`^- id: ${escapeRegExp(id)}\\s*\\r?\\n {2}disabled: true\\s*\\r?\\n`, 'mu')
|
|
933
|
+
if (re.test(next)) {
|
|
934
|
+
next = next.replace(re, '')
|
|
935
|
+
healed.push(id)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
if (healed.length === 0) {
|
|
939
|
+
const insertNames = parseInsertNames(next)
|
|
940
|
+
let require = null
|
|
941
|
+
for (const [id, moduleName] of insertNames) {
|
|
942
|
+
if (!moduleName || moduleName.startsWith('cordis:')) continue
|
|
943
|
+
let ok = true
|
|
944
|
+
try { ok = resolvePackageJson(moduleName, profileDir) !== null } catch { ok = false }
|
|
945
|
+
if (!ok && !new RegExp(`^- id: ${escapeRegExp(id)}\\s*\\r?\\n {2}disabled: true\\s*\\r?\\n`, 'mu').test(next)) {
|
|
946
|
+
next = `${next.trimEnd()}\n- id: ${id}\n disabled: true\n`
|
|
947
|
+
autoDisabled.push(id)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (next !== text) {
|
|
952
|
+
await writeFile(patchPath, sanitizePatchText(next), 'utf8')
|
|
953
|
+
}
|
|
954
|
+
return { healed, autoDisabled, healedAt: next !== text ? Date.now() : 0 }
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 启用前冒烟检查(服务永不崩机制):在独立子进程中动态 import 插件主模块,
|
|
959
|
+
* 捕获 loader 会遇到的 import/resolution 错误(SyntaxError、缺失导出、模块缺失)。
|
|
960
|
+
* 子进程失败不影响控制台;探测失败/超时按"不通过"处理(宁可拒绝,不冒险拖崩服务)。
|
|
961
|
+
*/
|
|
962
|
+
function probePluginImport(moduleName, profileDir) {
|
|
963
|
+
return new Promise((resolve) => {
|
|
964
|
+
const script = [
|
|
965
|
+
'import { createRequire } from "node:module";',
|
|
966
|
+
'import path from "node:path";',
|
|
967
|
+
'try {',
|
|
968
|
+
' const req = createRequire(path.join(process.argv[1], "package.json"));',
|
|
969
|
+
' const mainPath = req.resolve(process.argv[2]);',
|
|
970
|
+
' const { pathToFileURL } = await import("node:url");',
|
|
971
|
+
' await import(pathToFileURL(mainPath).href);',
|
|
972
|
+
' console.log("PROBE_OK");',
|
|
973
|
+
'} catch (e) { console.log("PROBE_ERR:" + String(e && e.message ? e.message : e).slice(0, 400)); process.exitCode = 1; }',
|
|
974
|
+
].join('\n')
|
|
975
|
+
let child = null
|
|
976
|
+
try {
|
|
977
|
+
child = spawn(process.execPath, ['--input-type=module', '-e', script, profileDir, moduleName], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
978
|
+
} catch {
|
|
979
|
+
resolve({ ok: true, detail: 'probe 无法启动(放行)' })
|
|
980
|
+
return
|
|
981
|
+
}
|
|
982
|
+
let out = ''
|
|
983
|
+
let timer = null
|
|
984
|
+
const finish = (ok, detail) => {
|
|
985
|
+
if (timer !== null) clearTimeout(timer)
|
|
986
|
+
resolve({ ok, detail })
|
|
987
|
+
}
|
|
988
|
+
timer = setTimeout(() => { try { child.kill() } catch {}; finish(false, '探测超时') }, 8000)
|
|
989
|
+
child.stdout.on('data', (c) => { out += c })
|
|
990
|
+
child.stderr.on('data', (c) => { out += c })
|
|
991
|
+
child.on('error', () => finish(true, 'probe 进程错误(放行)'))
|
|
992
|
+
child.on('close', (code) => {
|
|
993
|
+
if (out.includes('PROBE_OK')) finish(true, 'import OK')
|
|
994
|
+
else finish(false, (out.match(/PROBE_ERR:([^\n]*)/u)?.[1] ?? out.slice(0, 300)).trim())
|
|
995
|
+
})
|
|
996
|
+
})
|
|
997
|
+
}
|
|
998
|
+
|
|
853
999
|
/** 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 null。 */
|
|
854
1000
|
function readCompatPending() {
|
|
855
1001
|
try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
|
|
@@ -1044,11 +1190,11 @@ function summarizeReadme(text) {
|
|
|
1044
1190
|
async function readPluginDetails(moduleName, baseUrl) {
|
|
1045
1191
|
if (typeof moduleName !== 'string' || moduleName.startsWith('cordis:')) return { meta: null, readme: null }
|
|
1046
1192
|
try {
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1193
|
+
const pkgPath = resolvePackageJson(moduleName, baseDirOf(baseUrl))
|
|
1194
|
+
if (pkgPath === null) throw new Error('not found')
|
|
1049
1195
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
1050
1196
|
const meta = {
|
|
1051
|
-
name:
|
|
1197
|
+
name: moduleName,
|
|
1052
1198
|
version: pkg.version ?? null,
|
|
1053
1199
|
description: pkg.description ?? null,
|
|
1054
1200
|
homepage: pkg.homepage ?? null,
|
|
@@ -3504,10 +3650,11 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
|
|
|
3504
3650
|
let pkg = null
|
|
3505
3651
|
let pkgDir = null
|
|
3506
3652
|
try {
|
|
3507
|
-
const
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3653
|
+
const pkgPath = resolvePackageJson(p.moduleName, profileDir)
|
|
3654
|
+
if (pkgPath !== null) {
|
|
3655
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
3656
|
+
pkgDir = dirname(pkgPath)
|
|
3657
|
+
}
|
|
3511
3658
|
} catch {}
|
|
3512
3659
|
const version = typeof pkg?.version === 'string' ? pkg.version : null
|
|
3513
3660
|
const changed = version !== null && version !== p.version
|
|
@@ -3541,8 +3688,8 @@ async function frameworkCompatReportFor(source, ctx, profileDir) {
|
|
|
3541
3688
|
const isNpm = norm.startsWith('@') || !norm.includes('/')
|
|
3542
3689
|
const installed = (() => {
|
|
3543
3690
|
try {
|
|
3544
|
-
const
|
|
3545
|
-
|
|
3691
|
+
const pkgPath = resolvePackageJson(norm, profileDir)
|
|
3692
|
+
if (pkgPath === null) return null
|
|
3546
3693
|
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
|
|
3547
3694
|
} catch { return null }
|
|
3548
3695
|
})()
|
|
@@ -3941,6 +4088,17 @@ async function handle(ctx, req, res) {
|
|
|
3941
4088
|
if (method === 'GET' && pathname === `${ROUTE_PREFIX}/state`) {
|
|
3942
4089
|
const patchPath = findPatchPath(ctx)
|
|
3943
4090
|
const patch = await readPatchState(patchPath)
|
|
4091
|
+
// 补丁安全自愈(核心行误禁用恢复 / 缺失模块行自动禁用)——每 2 分钟最多跑一次
|
|
4092
|
+
let patchHeal = null
|
|
4093
|
+
try {
|
|
4094
|
+
if (patchHealAt === null || Date.now() - patchHealAt > 120000) {
|
|
4095
|
+
patchHealAt = Date.now()
|
|
4096
|
+
patchHeal = await healPatchSafety(patchPath)
|
|
4097
|
+
} else {
|
|
4098
|
+
patchHeal = patchHealReport
|
|
4099
|
+
}
|
|
4100
|
+
if (patchHeal !== null) patchHealReport = patchHeal
|
|
4101
|
+
} catch {}
|
|
3944
4102
|
const extraRows = await readExtraBundleRows(dirname(patchPath))
|
|
3945
4103
|
const compatPending = readCompatPending()
|
|
3946
4104
|
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
@@ -3976,7 +4134,7 @@ async function handle(ctx, req, res) {
|
|
|
3976
4134
|
const selfPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
3977
4135
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
3978
4136
|
} catch {}
|
|
3979
|
-
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null })) }, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
4137
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null })) }, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
3980
4138
|
return
|
|
3981
4139
|
}
|
|
3982
4140
|
|
|
@@ -4178,11 +4336,25 @@ async function handle(ctx, req, res) {
|
|
|
4178
4336
|
const compatPending = readCompatPending()
|
|
4179
4337
|
const pend = (compatPending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
4180
4338
|
if (pend) {
|
|
4181
|
-
sendError(res, 409, `「${rowId}」仍在框架升级适配门中(${compatPending.frameworkVersion ?? '?'}
|
|
4339
|
+
sendError(res, 409, `「${rowId}」仍在框架升级适配门中(${compatPending.frameworkVersion ?? '?'})被强制禁用:请先点击「更新并适配 / 已适配,立即解锁」,通过兼容校验后系统自动解锁`)
|
|
4182
4340
|
return
|
|
4183
4341
|
}
|
|
4184
4342
|
}
|
|
4185
4343
|
const patchPath = findPatchPath(ctx)
|
|
4344
|
+
// 启用前冒烟检查(服务永不崩机制):第三方模块在独立子进程中试 import,
|
|
4345
|
+
// 失败(SyntaxError/缺失导出/模块缺失)即拒绝启用,杜绝「单行 import 失败→整个服务启动崩溃」
|
|
4346
|
+
if (enabled) {
|
|
4347
|
+
const moduleName = target?.options?.name
|
|
4348
|
+
if (typeof moduleName === 'string' && !moduleName.startsWith('cordis:') && !moduleName.startsWith('@deepseek-ai/')) {
|
|
4349
|
+
try {
|
|
4350
|
+
const probe = await probePluginImport(moduleName, dirname(patchPath))
|
|
4351
|
+
if (probe.ok !== true) {
|
|
4352
|
+
sendError(res, 409, `启用前冒烟检查未通过:模块加载失败(${probe.detail ?? '未知'})——该插件与当前框架不兼容或依赖缺失,已阻止启用(服务不会再被拖崩);请先「检测更新/更新并适配」其适配版`)
|
|
4353
|
+
return
|
|
4354
|
+
}
|
|
4355
|
+
} catch {}
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4186
4358
|
const result = enabled
|
|
4187
4359
|
? await enableEntry(patchPath, rowId)
|
|
4188
4360
|
: await disableEntry(patchPath, rowId)
|
|
@@ -4218,14 +4390,18 @@ async function handle(ctx, req, res) {
|
|
|
4218
4390
|
const owners = await readExtraBundleOwners(profileDir)
|
|
4219
4391
|
const ownerBundle = owners.get(rowId) ?? owners.get(moduleName)
|
|
4220
4392
|
if (ownerBundle !== undefined) {
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4393
|
+
// 2026-09-04 事故教训:删除聚合包子路径行(如 @linxin666/dsh-web-all/plugin-manager)时
|
|
4394
|
+
// 曾把整个 bundle 从清单移除,pnpm 卸载失败(corepack 报错)后重启,全家桶整体消失。
|
|
4395
|
+
// 现改为:任何 bundle 行的「删除」= 仅停用该行(patch disabled:true),bundle 清单不动,
|
|
4396
|
+
// 之后随时可「启用」恢复;整体卸载请走包管理器。
|
|
4397
|
+
await disableEntry(patchPath, rowId)
|
|
4398
|
+
sendJson(res, 200, {
|
|
4399
|
+
ok: true,
|
|
4400
|
+
removed: 'row',
|
|
4401
|
+
packageName: moduleName,
|
|
4402
|
+
restart: false,
|
|
4403
|
+
note: '该行来自聚合包 ' + ownerBundle + ',已仅停用本行(bundle 保留,可随时启用恢复);整体卸载请用包管理器执行 pnpm remove ' + ownerBundle,
|
|
4404
|
+
})
|
|
4229
4405
|
return
|
|
4230
4406
|
}
|
|
4231
4407
|
if (!patch.inserts.includes(rowId)) {
|
|
@@ -4664,11 +4840,105 @@ async function handle(ctx, req, res) {
|
|
|
4664
4840
|
return
|
|
4665
4841
|
}
|
|
4666
4842
|
|
|
4843
|
+
if (pathname === `${ROUTE_PREFIX}/adapt-unlock`) {
|
|
4844
|
+
// 待适配行「已适配,立即解锁」:无需等新版本——对已装模块重跑源码扫描,通过即移除禁用块。
|
|
4845
|
+
const rowId = typeof body.rowId === 'string' ? body.rowId : ''
|
|
4846
|
+
if (!/^[A-Za-z0-9_:.-]{1,80}$/u.test(rowId)) {
|
|
4847
|
+
sendError(res, 400, 'rowId 无效')
|
|
4848
|
+
return
|
|
4849
|
+
}
|
|
4850
|
+
const pending = readCompatPending()
|
|
4851
|
+
const entry = (pending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
4852
|
+
if (entry === undefined) {
|
|
4853
|
+
sendError(res, 404, '该行不在适配门待适配清单中(或已解锁)')
|
|
4854
|
+
return
|
|
4855
|
+
}
|
|
4856
|
+
const patchPath = findPatchPath(ctx)
|
|
4857
|
+
const profileDir = dirname(patchPath)
|
|
4858
|
+
let pkg = null
|
|
4859
|
+
let pkgDir = null
|
|
4860
|
+
let version = null
|
|
4861
|
+
try {
|
|
4862
|
+
const require = createRequire(join(profileDir, 'package.json'))
|
|
4863
|
+
const pkgPath = resolvePackageJson(entry.moduleName, profileDir)
|
|
4864
|
+
if (pkgPath !== null) {
|
|
4865
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
4866
|
+
version = typeof pkg?.version === 'string' ? pkg.version : null
|
|
4867
|
+
pkgDir = dirname(pkgPath)
|
|
4868
|
+
}
|
|
4869
|
+
} catch {}
|
|
4870
|
+
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
4871
|
+
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
4872
|
+
if (check.decision === 'fail') {
|
|
4873
|
+
sendError(res, 409, `适配校验未通过:${check.reason}`)
|
|
4874
|
+
return
|
|
4875
|
+
}
|
|
4876
|
+
await removeDisableBlock(patchPath, rowId)
|
|
4877
|
+
entry.status = 'adopted'
|
|
4878
|
+
entry.adoptedAt = Date.now()
|
|
4879
|
+
entry.adoptedVersion = version
|
|
4880
|
+
entry.adoptedFramework = fwVer
|
|
4881
|
+
entry.check = check.decision
|
|
4882
|
+
entry.checkNote = check.reason ?? null
|
|
4883
|
+
writeCompatPending(pending)
|
|
4884
|
+
sendJson(res, 200, { ok: true, rowId, adopted: true, check: check.decision, note: check.reason ?? null })
|
|
4885
|
+
return
|
|
4886
|
+
}
|
|
4887
|
+
|
|
4888
|
+
if (pathname === `${ROUTE_PREFIX}/adapt-unlock-all`) {
|
|
4889
|
+
// 全家桶「一键启用已适配」:对家庭内所有待适配行批量重跑源码扫描,通过的全部解锁,未通过保留禁用。
|
|
4890
|
+
const root = typeof body.root === 'string' ? body.root : ''
|
|
4891
|
+
if (!/^(@[a-z0-9-][a-z0-9-._~]*\/)?[a-z0-9-][a-z0-9-._~]*$/u.test(root) || root.length > 214) {
|
|
4892
|
+
sendError(res, 400, 'root 无效')
|
|
4893
|
+
return
|
|
4894
|
+
}
|
|
4895
|
+
const pending = readCompatPending()
|
|
4896
|
+
const patchPath = findPatchPath(ctx)
|
|
4897
|
+
const profileDir = dirname(patchPath)
|
|
4898
|
+
const targets = (pending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending' && (p.moduleName === root || String(p.moduleName).startsWith(root + '/')))
|
|
4899
|
+
if (targets.length === 0) {
|
|
4900
|
+
sendError(res, 404, '该全家桶没有待适配行(或已全部解锁)')
|
|
4901
|
+
return
|
|
4902
|
+
}
|
|
4903
|
+
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
4904
|
+
const unlocked = []
|
|
4905
|
+
const kept = []
|
|
4906
|
+
for (const p of targets) {
|
|
4907
|
+
let pkg = null
|
|
4908
|
+
let pkgDir = null
|
|
4909
|
+
let version = null
|
|
4910
|
+
try {
|
|
4911
|
+
const pkgPath = resolvePackageJson(p.moduleName, profileDir)
|
|
4912
|
+
if (pkgPath !== null) {
|
|
4913
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
4914
|
+
version = typeof pkg?.version === 'string' ? pkg.version : null
|
|
4915
|
+
pkgDir = dirname(pkgPath)
|
|
4916
|
+
}
|
|
4917
|
+
} catch {}
|
|
4918
|
+
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'fail', reason: '无法读取包信息' }
|
|
4919
|
+
if (check.decision === 'fail') {
|
|
4920
|
+
kept.push({ rowId: p.rowId, reason: check.reason })
|
|
4921
|
+
continue
|
|
4922
|
+
}
|
|
4923
|
+
await removeDisableBlock(patchPath, p.rowId)
|
|
4924
|
+
p.status = 'adopted'
|
|
4925
|
+
p.adoptedAt = Date.now()
|
|
4926
|
+
p.adoptedVersion = version
|
|
4927
|
+
p.adoptedFramework = fwVer
|
|
4928
|
+
p.check = check.decision
|
|
4929
|
+
p.checkNote = check.reason ?? null
|
|
4930
|
+
unlocked.push(p.rowId)
|
|
4931
|
+
}
|
|
4932
|
+
writeCompatPending(pending)
|
|
4933
|
+
sendJson(res, 200, { ok: true, root, unlocked, kept })
|
|
4934
|
+
return
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4667
4937
|
if (pathname === `${ROUTE_PREFIX}/check-update`) {
|
|
4668
4938
|
// 检测已安装插件是否有新版本:curl registry 元数据取 dist-tags.latest(node 网络黑洞时 curl 可用)。
|
|
4669
4939
|
// 聚合包(有 dependencies)额外对比子包版本:声明版本 vs 本地 node_modules 实际版本,
|
|
4670
4940
|
// 返回 depsOutdated 提示"更新本包需同步子包",避免半更新混搭导致启动冲突。
|
|
4671
|
-
const packageName = typeof body.packageName === 'string' ? body.packageName.trim() : ''
|
|
4941
|
+
const packageName = packageNameOf(typeof body.packageName === 'string' ? body.packageName.trim() : '')
|
|
4672
4942
|
if (!packageName) {
|
|
4673
4943
|
sendError(res, 400, 'packageName 不能为空')
|
|
4674
4944
|
return
|
|
@@ -4764,7 +5034,29 @@ async function handle(ctx, req, res) {
|
|
|
4764
5034
|
error = `npm registry 查询失败且 GitHub 回退不可用(${fallbackError};${fbErr instanceof Error ? fbErr.message : String(fbErr)})`
|
|
4765
5035
|
}
|
|
4766
5036
|
}
|
|
4767
|
-
|
|
5037
|
+
// migrate 换名检测(2026-09-04 缺陷修复):本地包声明 dsh.migrate.to 时查目标包最新版/引擎声明,
|
|
5038
|
+
// 识别「项目已改名/迁移发布」的更新(如 @linxin666/dsh-web-ui-all → @linxin666/dsh-web-all 0.3.14)
|
|
5039
|
+
let migrate = null
|
|
5040
|
+
try {
|
|
5041
|
+
const localPath = resolvePackageJson(packageName, profileDir)
|
|
5042
|
+
if (localPath !== null) {
|
|
5043
|
+
const localPkg = JSON.parse(readFileSync(localPath, 'utf8'))
|
|
5044
|
+
const to = typeof localPkg.dsh?.migrate?.to === 'string' ? localPkg.dsh.migrate.to : null
|
|
5045
|
+
if (to !== null && to !== '' && to !== packageName) {
|
|
5046
|
+
const encTo = to.startsWith('@')
|
|
5047
|
+
? `@${encodeURIComponent(to.slice(1).split('/')[0])}%2f${encodeURIComponent(to.split('/').slice(1).join('/'))}`
|
|
5048
|
+
: encodeURIComponent(to)
|
|
5049
|
+
const meta = await fetchJsonUrl(`https://registry.npmmirror.com/${encTo}`)
|
|
5050
|
+
const toLatest = meta?.['dist-tags']?.latest ?? meta?.['dist-tags']?.next ?? null
|
|
5051
|
+
const toPkg = toLatest !== null ? (meta?.versions?.[toLatest] ?? null) : null
|
|
5052
|
+
const engine = toPkg?.dsh?.engines?.dsh ?? toPkg?.engines?.dsh ?? null
|
|
5053
|
+
const fwVer = currentFrameworkVersion(ctx)
|
|
5054
|
+
const compatible = fwVer !== null && (engine === null || semverRangeMatchLoose(fwVer, engine))
|
|
5055
|
+
migrate = { to, latest: toLatest, engine, compatible }
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
} catch {}
|
|
5059
|
+
sendJson(res, 200, { ok: true, packageName, latest, next, beta, depsOutdated, error, source, migrate })
|
|
4768
5060
|
return
|
|
4769
5061
|
}
|
|
4770
5062
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.27",
|
|
4
4
|
"description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|