@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 CHANGED
@@ -20,23 +20,25 @@
20
20
 
21
21
 
22
22
 
23
- > ## 🎉 v0.3.25 released — framework-upgrade safety & plugin adapt gate (2026-09-04)
23
+ > ## 🎉 v0.3.27 released — family-bundle cards, never-crash safety & adapt-gate hardening (2026-09-04)
24
24
  >
25
- > **Upgrade safety trio**: framework root detection (fixes the `.pnpm` realpath
26
- > mixed-version root cause), a **full-tree checkpoint** before upgrade, and
27
- > **automatic full-tree rollback** on install/relaunch failure — plus a
28
- > **one-click rollback** button on the framework card.
29
- > **Plugin adapt gate**: plugins incompatible with the new framework are
30
- > force-disabled (enable locked); 「Check update → Update & adapt」auto-verifies
31
- > declarations/dependencies and unlocks on success; AI Empower pre-checks
32
- > framework compatibility during planning.
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 → **AI 赋能** button → enter e.g. `OpenViking` → confirm the plan.
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.25 正式发布 — 框架升级安全与插件适配门(2026-09-04)
9
+ > ## 🎉 v0.3.27 正式发布 — 全家桶分组卡片、永不崩安全与适配门强化(2026-09-04)
10
10
  >
11
- > **升级安全三件套**:框架安装根识别(修复 `.pnpm` realpath 混版本根因)、升级前**全树 checkpoint**、
12
- > 拉起失败**自动全树回滚**;升级后框架卡片**一键回滚到上一版**。
13
- > **插件适配门**:升级后不兼容插件强制禁用(启用锁定),「检测更新 → 更新并适配」→
14
- > 自动校验声明/依赖,通过即解锁;AI 赋能规划时自动预检框架适配并给出说明。
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 中更新:dsh plugin --profile web add @noob-stupid/dsh-plugin-console
23
+ > # 或在 DSH 中:dsh plugin --profile web add @noob-stupid/dsh-plugin-console
20
24
  > ```
21
- > **体验入口:** 插件控制台 → 右上角 **AI 赋能** 按钮 → 输入如 `OpenViking` → 确认计划 → 自动部署。
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 rows = state.status === "ready"
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
- }).map((entry) => {
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"))) : null,
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 }, moduleShortName(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 require = createRequire(join(profileDir, 'package.json'))
136
- const pkgPath = require.resolve(`${packageName}/package.json`)
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 require = createRequire(baseUrl)
171
- const pkgPath = require.resolve(`${moduleName}/package.json`)
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 require = createRequire(join(profileDir, 'package.json'))
378
- const dir = dirname(require.resolve(`${pkg}/package.json`))
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 require = createRequire(join(profileDir, 'package.json'))
467
- const dir = dirname(require.resolve(`${pkg}/package.json`))
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 require = createRequire(baseUrl)
1048
- const pkgPath = require.resolve(`${moduleName}/package.json`)
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: pkg.name ?? moduleName,
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 require = createRequire(join(profileDir, 'package.json'))
3508
- const pkgPath = require.resolve(`${p.moduleName}/package.json`)
3509
- pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
3510
- pkgDir = dirname(pkgPath)
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 require = createRequire(join(profileDir, 'package.json'))
3545
- const pkgPath = require.resolve(`${norm}/package.json`)
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
- await removeBundleFromManifest(profileDir, ownerBundle)
4222
- let uninstallError = null
4223
- try {
4224
- await pnpmRemove(profileDir, ownerBundle)
4225
- } catch (error) {
4226
- uninstallError = error instanceof Error ? error.message : String(error)
4227
- }
4228
- sendJson(res, 200, { ok: true, removed: 'bundle', packageName: ownerBundle, restart: true, uninstallError })
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
- sendJson(res, 200, { ok: true, packageName, latest, next, beta, depsOutdated, error, source })
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.26",
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",