@noob-stupid/dsh-plugin-console 0.3.25 → 0.3.26

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,13 +20,16 @@
20
20
 
21
21
 
22
22
 
23
- > ## 🎉 v0.3.24 — AI Empower released (2026-08-31)
23
+ > ## 🎉 v0.3.25 released — framework-upgrade safety & plugin adapt gate (2026-09-04)
24
24
  >
25
- > Give the Plugin Console a package name or GitHub repo: the local AI reads its docs,
26
- > drafts a **deployment plan** (install / write config / start service / health check),
27
- > and — after your confirmation — executes it safely. Server-type components get an
28
- > automatic control card (start/stop/status/open + dropdown). An OpenViking template
29
- > deploys a local memory server in one flow.
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.
30
33
  >
31
34
  > **Install:**
32
35
  > ```bash
@@ -82,6 +85,7 @@ auto-rollback).
82
85
  | | Benefit | Detail |
83
86
  |---|---|---|
84
87
  | 🤖 | **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
+ | 🛡️ | **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 |
85
89
  | 🚀 | **Server component cards** | Left-side floating card auto-aligned to the main panel: start / stop / status / **open Web UI** buttons, multi-server dropdown, collapsible |
86
90
  | 🧩 | **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 |
87
91
  | 🤖 | **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 |
@@ -246,6 +250,47 @@ Requires: DSH ≥ 0.1.0-rc.6 (web profile, with `dsh-client-modules` / `dsh-host
246
250
  - **Card dismiss semantics**: terminal states (done/failed) are permanently dismissed on ✕
247
251
  (persisted); in-progress dismissal is session-only and the card returns after a refresh.
248
252
 
253
+ ### Framework upgrade safety & plugin adapt gate (v0.3.25)
254
+
255
+ > Origin: the 2026-09-04 DSH `0.1.1-rc.2 → 0.1.2-rc.1` incident — the upgrade script ran in
256
+ > the wrong working directory, overwrote the new CLI into the old `.pnpm` entry, left a
257
+ > mixed-version tree, and the service could not start. This version makes upgrade safety
258
+ > and plugin compatibility detection first-class console mechanisms.
259
+
260
+ **Upgrade safety trio**
261
+
262
+ 1. **Framework root detection**: locates the real install root (the top-level `node_modules`
263
+ containing `.pnpm`), fixing the `require.resolve`-returns-`.pnpm`-realpath root cause
264
+ (relink no-op / new package overwriting the old tree); refuses to upgrade when the root
265
+ cannot be identified (service stays running);
266
+ 2. **Full-tree checkpoint before upgrade**: mirrors every `@deepseek-ai` version package in
267
+ `.pnpm` + the top-level scope + `lock.yaml`
268
+ (under `~/.dsh/plugin-console/framework-backups/<version>/fw-tree/`);
269
+ 3. **Automatic full-tree rollback + retry**: install failure or relaunch failure restores
270
+ the pre-upgrade tree and relaunches automatically — no more "please run it manually"
271
+ dead-ends; after rollback, relink/repair steps are skipped to avoid damaging the
272
+ restored tree a second time.
273
+
274
+ **One-click rollback**: after an upgrade the framework card shows a **「回滚到上一版」** button
275
+ (`/plugin-console/framework-rollback`): stop service → restore full tree → relaunch →
276
+ health check, state visible throughout.
277
+
278
+ **Plugin adapt gate (compatibility detection)**
279
+
280
+ - After an upgrade, plugin entries recorded in `~/.dsh/plugin-console/compat-pending.json`
281
+ are **force-disabled**; the enable button is locked and the server rejects `/toggle` with
282
+ 409 (unbypassable). The enabled-button label itself shows「待适配」as the state hint;
283
+ - The **details** panel explains the flow: **Check update → Update & adapt**;
284
+ - **Post-update auto-verification**: scans the latest `package.json` for an explicit
285
+ `dsh.engines.framework` / `engines.dsh` declaration plus `@deepseek-ai/*` dependency
286
+ ranges (built-in zero-dependency semver engine — strict npm prerelease rules for
287
+ dependencies, loose for explicit declarations); if the version changed and no
288
+ incompatibility was found, the `disabled` block is **removed automatically** and the row
289
+ unlocks; aggregator updates validate all synced subpackages too;
290
+ - **AI Empower pre-check**: during planning the server pre-checks the registry's latest
291
+ declaration and adapt-gate hits, injects them into the subagent prompt (the plan must
292
+ explain the conclusion to the user) and marks it on the plan panel (red for incompatible).
293
+
249
294
  ### Marketplace (multi-source)
250
295
 
251
296
  - **Source switcher**: click the login pill to switch between **GitHub / Gitee / custom
@@ -349,6 +394,10 @@ GitHub Actions (every 6h, repo token)
349
394
  - Supports the **DSH 0.1.0 series** (`0.1.0-rc.6` and siblings).
350
395
  - The panel reads the running `@deepseek-ai/dsh-web-app` version: after a breaking upgrade
351
396
  (0.2 / 1.0) a compatibility warning appears instead of silent failure.
397
+ - **Plugin adapt gate (v0.3.25)**: third-party plugins incompatible with the current
398
+ framework are force-disabled after an upgrade (enable locked); run 「Check update」to
399
+ upgrade the plugin first — it unlocks automatically after the auto-verification passes.
400
+ See 「Framework upgrade safety & plugin adapt gate」.
352
401
  - Likely breaking seams: patch semantics, `webServer.register`, loader entry shape,
353
402
  `dsh.client` bundle format, `settings.plugins.tab` slot.
354
403
  - Deploy scripts do not check versions; the in-panel warning is authoritative.
@@ -390,6 +439,11 @@ test-harness.mjs Logic self-test (state/toggle/validation/loopback; search SKI
390
439
  | `/plugin-console/gitee-oauth-url` / `gitee-oauth-callback` | GET | Gitee OAuth flow |
391
440
  | `/plugin-console/ai-consent` | POST | Approve/cancel the AI-fallback step |
392
441
  | `/plugin-console/restart` | POST | Watchdog-safe self-restart (panel button equivalent) |
442
+ | `/plugin-console/framework-upgrade` | POST | One-click framework upgrade (full-tree checkpoint → online install → rollback protection) |
443
+ | `/plugin-console/framework-rollback` | POST | One-click rollback to the previous version (stop → full-tree restore → auto relaunch → health check) |
444
+ | `/plugin-console/self-update` | POST | Update the console itself (latest npm tarball into the profile) |
445
+ | `/plugin-console/ai-empower/plan` / `status` / `run` / `cancel` | POST | AI Empower: plan (with framework compat pre-check) → status polling → confirmed run → cancel |
446
+ | `/plugin-console/components` / `component/start` / `stop` / `status` | GET/POST | Server component registry and start/stop/status |
393
447
 
394
448
  ---
395
449
 
package/README.zh.md CHANGED
@@ -6,11 +6,12 @@
6
6
 
7
7
  # DSH 插件中心(dsh-plugin-hub)
8
8
 
9
- > ## 🎉 v0.3.24 正式发布 — AI 赋能一键部署(2026-08-31)
9
+ > ## 🎉 v0.3.25 正式发布 — 框架升级安全与插件适配门(2026-09-04)
10
10
  >
11
- > 在插件控制台输入 npm 包名或 GitHub 仓库,本地 AI 读取文档自动生成**部署计划**,
12
- > 你确认后安全执行;服务器类组件自动生成控制卡片(启动/停止/状态/打开 + 下拉)。
13
- > 内置 OpenViking 模板,一条指令部署本地记忆服务器。
11
+ > **升级安全三件套**:框架安装根识别(修复 `.pnpm` realpath 混版本根因)、升级前**全树 checkpoint**、
12
+ > 拉起失败**自动全树回滚**;升级后框架卡片**一键回滚到上一版**。
13
+ > **插件适配门**:升级后不兼容插件强制禁用(启用锁定),「检测更新 → 更新并适配」→
14
+ > 自动校验声明/依赖,通过即解锁;AI 赋能规划时自动预检框架适配并给出说明。
14
15
  >
15
16
  > **安装:**
16
17
  > ```bash
@@ -75,6 +76,7 @@
75
76
  | | 优势 | 说明 |
76
77
  |---|---|---|
77
78
  | 🤖 | **AI 赋能(v0.3.24)** | 输入 npm 包名 / GitHub 仓库,本地 AI 读文档生成**部署计划**(安装/写配置/启动服务/健康检查),你确认后安全执行;服务器类组件自动生成控制卡片 |
79
+ | 🛡️ | **框架升级适配门与安全(v0.3.25)** | 升级后不兼容插件强制禁用(适配门锁定,「检测更新 → 更新并适配 → 自动校验解锁」);升级前全树 checkpoint、拉起失败自动全树回滚、框架卡片一键回滚到上一版 |
78
80
  | 🚀 | **服务器组件卡片** | 左侧浮卡与主面板顶边对齐:启动 / 停止 / 状态 / 【打开】直达 Web UI,多服务器下拉、可折叠(状态记忆) |
79
81
  | 🧩 | **插件 + 技能双市场** | 自动收录 `dsh-plugin` topic 仓库(按 star **500+**),另有**技能 tab**(`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`,最多 300)——浏览、搜索、一键安装,零 GitHub API 调用;插件自带技能(如 openviking-memory)只读展示 |
80
82
  | 🤖 | **自动收录 CI** | GitHub Actions 每 6 小时自动重跑 `build-index`(也支持手动触发);作者只需给自己的仓库打 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签,**无需申请、无需审核** |
@@ -227,6 +229,37 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
227
229
  - **卡片关闭语义**:终态(成功/失败)点叉号永久关闭(持久化);进行中点叉号仅本次会话
228
230
  隐藏,刷新后恢复显示。
229
231
 
232
+ ### 框架升级适配门与升级安全(v0.3.25)
233
+
234
+ > 缘起:2026-09-04 DSH `0.1.1-rc.2 → 0.1.2-rc.1` 升级事故——升级脚本在错误的工作目录执行,
235
+ > 把新 CLI 原位覆盖进旧 `.pnpm` 目录、依赖树混版本,服务无法拉起。本版本把「升级安全」与
236
+ > 「插件兼容检测」固化为控制台机制。
237
+
238
+ **升级安全三件套**
239
+
240
+ 1. **框架安装根识别**:定位真正的框架安装根(含 `.pnpm` 的顶层 `node_modules`),修复
241
+ `require.resolve` 返回 `.pnpm` 内部 realpath 导致的「重链空转 / 新包覆盖旧目录」根因;
242
+ 定位不到时**拒绝升级**(不 kill 服务、服务保持运行);
243
+ 2. **升级前全树 checkpoint**:镜像 `.pnpm` 全部 `@deepseek-ai` 版本包 + 顶层 scope +
244
+ `lock.yaml`(写入 `~/.dsh/plugin-console/framework-backups/<版本>/fw-tree/`);
245
+ 3. **拉起失败自动全树回滚并重试**:安装失败或服务拉起失败都会自动恢复升级前全树并再次拉起,
246
+ 不再只留「请手动运行」提示;回滚后跳过重链/修复(避免对已恢复旧树二次破坏)。
247
+
248
+ **一键回滚**:升级后框架卡片出现「回滚到上一版」按钮(`/plugin-console/framework-rollback`):
249
+ 停服 → 全树恢复 → 自动拉起 → 健康检测,全程状态可见。
250
+
251
+ **插件适配门(插件检测)**
252
+
253
+ - 升级后写入 `~/.dsh/plugin-console/compat-pending.json` 的插件行被**强制禁用**,启用按钮
254
+ 锁定(服务端 `/toggle` 直接 409,无法绕过);行状态以按钮文案「待适配」提示;
255
+ - 「详情」面板给出说明与入口:**检测更新 → 更新并适配**;
256
+ - **更新后自动校验**:扫描最新版 `package.json` 的 `dsh.engines.framework` / `engines.dsh`
257
+ 显式声明 + `@deepseek-ai/*` 依赖范围(内置零依赖 semver 判定器:依赖判定遵循 npm
258
+ prerelease 严格规则、声明判定宽松);版本已变化且未判定不兼容 → **自动移除禁用块解锁**;
259
+ 聚合包更新连带校验同步的子包;
260
+ - **AI 赋能适配预检**:规划时服务端预检 registry 最新版声明与适配门命中情况,注入子代理
261
+ 提示词(要求计划向用户解释适配结论),并在计划面板标注(不兼容标红)。
262
+
230
263
  ### 插件市场(多搜索源)
231
264
 
232
265
  - **搜索源切换**:点击顶部登录态标识弹出菜单,在 **GitHub / Gitee / 自定义源** 间切换(选择持久化);
@@ -323,6 +356,9 @@ GitHub Actions(每 6 小时,仓库自带 token)
323
356
  - 当前支持 **DSH 0.1.0 系列**(`0.1.0-rc.6` 及同系列版本)。
324
357
  - 面板会读取运行中的 `@deepseek-ai/dsh-web-app` 版本:官方发布破坏性升级
325
358
  (0.2 / 1.0 等)后,面板顶部会显示兼容性警告并给出本仓库地址,而不是默默失效。
359
+ - **插件适配门(v0.3.25)**:框架升级后不兼容的第三方插件会被**强制禁用**(启用按钮锁定);
360
+ 请先「检测更新」升级该插件,更新并通过自动校验后才会解锁——详见
361
+ 「框架升级适配门与升级安全」。
326
362
  - 官方破坏性更新可能改动的接口:补丁层语义、`webServer.register`、
327
363
  加载器条目结构、`dsh.client` bundle 格式、`settings.plugins.tab` 插槽。
328
364
  届时随官方版本更新本仓库即可(依赖面已收窄到上述几个点)。
@@ -341,6 +377,7 @@ scripts/apply-framework-patch.cjs 框架层补丁(issue #5,幂等,可重
341
377
  marketplace/index.json 生成的静态索引(jsDelivr CDN 分发)
342
378
  deploy.ps1 / deploy.sh 一键部署脚本(Windows / Linux·macOS)
343
379
  test-harness.mjs 逻辑自检(state/toggle/校验/环回保护;搜索视网络环境 SKIP)
380
+ test-compat-gate.mjs 适配门单元测试(内置 semver 判定器:声明/依赖兼容规则)
344
381
  ```
345
382
  <img width="1878" height="945" alt="image" src="https://github.com/user-attachments/assets/b26f2f19-0ba4-4be7-9ca1-b3fd4c51a7a8" />
346
383
 
@@ -365,6 +402,11 @@ test-harness.mjs 逻辑自检(state/toggle/校验/环回保护;搜索视
365
402
  | `/plugin-console/gitee-oauth-url` / `gitee-oauth-callback` | GET | Gitee OAuth 流程 |
366
403
  | `/plugin-console/ai-consent` | POST | 同意/取消 AI 兜底步骤 |
367
404
  | `/plugin-console/restart` | POST | 自带守护的安全自重启(等同面板按钮) |
405
+ | `/plugin-console/framework-upgrade` | POST | 框架一键升级(全树 checkpoint → 在线安装 → 全树回滚保护) |
406
+ | `/plugin-console/framework-rollback` | POST | 一键回滚到升级前版本(停服 → 全树恢复 → 自动拉起 → 健康检测) |
407
+ | `/plugin-console/self-update` | POST | 控制台自身一键更新(npm 最新 tarball 到当前 profile) |
408
+ | `/plugin-console/ai-empower/plan` / `status` / `run` / `cancel` | POST | AI 赋能:规划(含框架适配预检)→ 状态轮询 → 确认执行 → 取消 |
409
+ | `/plugin-console/components` / `component/start` / `stop` / `status` | GET/POST | 服务器组件清单与启动/停止/状态 |
368
410
 
369
411
  ---
370
412
 
package/lib/client.js CHANGED
@@ -1396,7 +1396,29 @@ window.__ModuleLoader__.load({
1396
1396
  // 挂载时加载静态插件索引(GitHub 源空查询时秒开浏览,零 API 调用)
1397
1397
  react.useEffect(() => {
1398
1398
  call("/plugin-console/market-index").then(
1399
- (data) => { if (data && Array.isArray(data.items)) setIndexData({ items: data.items, skills: Array.isArray(data.skills) ? data.skills : [], loaded: true }); },
1399
+ (data) => {
1400
+ if (data && Array.isArray(data.items)) {
1401
+ setIndexData({ items: data.items, skills: Array.isArray(data.skills) ? data.skills : [], loaded: true });
1402
+ // ★「只看官方」需要官方标记:索引条目补标(网络/服务端此前判定失败时不动已有标记,避免 null 覆盖)
1403
+ // 服务端 /enrich 已带 24h 结果缓存时,此处仅兜底浏览器直连失败部分。
1404
+ const base = data.items;
1405
+ enrichOfficialBundle(base).then((enriched) => {
1406
+ if (!Array.isArray(enriched)) return;
1407
+ const merged = enriched.map((e, i) => {
1408
+ const b = base[i] ?? {};
1409
+ return {
1410
+ ...e,
1411
+ official: e.official !== null ? e.official : (b.official ?? null),
1412
+ aggregate: e.aggregate === true || b.aggregate === true,
1413
+ aggregateInstallable: b.aggregateInstallable === true,
1414
+ hasSkill: e.hasSkill === true || b.hasSkill === true,
1415
+ hasSuite: e.hasSuite === true || b.hasSuite === true,
1416
+ };
1417
+ });
1418
+ setIndexData((prev) => (prev !== null && prev.loaded === true ? { ...prev, items: merged } : prev));
1419
+ });
1420
+ }
1421
+ },
1400
1422
  () => {},
1401
1423
  );
1402
1424
  // 市场状态记忆:关闭前搜索过 → 重开时自动恢复该搜索(query 已从 localStorage 初始化)
package/lib/index.js CHANGED
@@ -293,19 +293,67 @@ function semverRangeMatchLoose(versionText, rangeText) {
293
293
  return semverRangeMatch(`${v.major}.${v.minor}.${v.patch}`, rangeText)
294
294
  }
295
295
 
296
+ /**
297
+ * 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用
298
+ * (2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
299
+ * 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
300
+ * = 整个服务启动崩溃)。返回命中的符号列表,空数组 = 干净。
301
+ */
302
+ function scanSettingsApiUsage(pkgDir) {
303
+ const found = []
304
+ const seen = new Set()
305
+ const walk = (dir, depth) => {
306
+ if (depth > 3) return
307
+ let entries = []
308
+ try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
309
+ for (const entry of entries) {
310
+ const full = join(dir, entry.name)
311
+ if (entry.isDirectory()) {
312
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
313
+ walk(full, depth + 1)
314
+ continue
315
+ }
316
+ if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
317
+ if (seen.has(full)) continue
318
+ seen.add(full)
319
+ if (seen.size > 120) return
320
+ try {
321
+ const text = readFileSync(full, 'utf8').slice(0, 400000)
322
+ for (const sym of ['settingsNamespace', 'installSettingsSection']) {
323
+ if (text.includes(sym) && !found.includes(sym)) found.push(sym)
324
+ }
325
+ } catch {}
326
+ }
327
+ }
328
+ walk(pkgDir, 0)
329
+ return found
330
+ }
331
+
296
332
  /**
297
333
  * 插件-框架兼容校验(框架升级适配门):
298
334
  * 1) 显式兼容声明(dsh.engines.framework / dsh.compat.framework / engines.dsh):以声明为准;
299
- * 2) 否则扫描依赖/peerDeps 的 @deepseek-ai/*:任一范围明确不含框架版本 → fail;
300
- * 3) 无声明且依赖全部满足 → unknown(新版变动不大;由调用方在「版本已变化」前提下降级为放行)。
335
+ * 2) 扫描依赖/peerDeps 的 @deepseek-ai/*:任一范围明确不含框架版本 → fail;
336
+ * 3) 【硬判据】提供 pkgDir 时扫描包源码:命中已删除的 dsh-settings API
337
+ * (settingsNamespace / installSettingsSection)→ 直接 fail(2026-09-04 教训:
338
+ * 0.3.6 全家仍引用旧 API,静态声明/依赖检查判 pass 是假通过,loader 单行失败 = 服务崩溃);
339
+ * 4) 无声明且依赖全部满足 → unknown(新版变动不大;由调用方在「版本已变化」前提下放行)。
301
340
  * 返回 { decision: 'pass'|'fail'|'unknown', reason }。
302
341
  */
303
- function checkPluginFrameworkCompat(pkg, frameworkVersion) {
342
+ function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
304
343
  const declared = pkg?.dsh?.engines?.framework ?? pkg?.dsh?.compat?.framework ?? pkg?.engines?.dsh ?? null
305
344
  if (typeof declared === 'string' && declared !== '') {
306
- return semverRangeMatchLoose(frameworkVersion, declared)
307
- ? { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
308
- : { decision: 'fail', reason: `声明兼容范围 ${declared} 不满足当前框架 ${frameworkVersion}` }
345
+ const ok = semverRangeMatchLoose(frameworkVersion, declared)
346
+ if (!ok) return { decision: 'fail', reason: `声明兼容范围 ${declared} 不满足当前框架 ${frameworkVersion}` }
347
+ }
348
+ // 硬判据:实际源码扫描(比声明/依赖更接近真实兼容性)
349
+ if (pkgDir !== null && typeof pkgDir === 'string' && semverRangeMatchLoose(frameworkVersion, '>=0.1.2')) {
350
+ const broken = scanSettingsApiUsage(pkgDir)
351
+ if (broken.length > 0) {
352
+ return { decision: 'fail', reason: `源码仍引用 0.1.2 起已删除的 dsh-settings API(${broken.join('、')})——实际不兼容,启用会让整个服务启动崩溃` }
353
+ }
354
+ if (typeof declared === 'string' && declared !== '') return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion},且源码无已删除 API 引用` }
355
+ } else if (typeof declared === 'string' && declared !== '') {
356
+ return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
309
357
  }
310
358
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.peerDependencies ?? {}), ...(pkg?.optionalDependencies ?? {}) }
311
359
  const hits = Object.entries(deps).filter(([name]) => /^@deepseek-ai\//u.test(name))
@@ -1485,56 +1533,114 @@ async function fetchSkillMeta(repo, branch, skillDir) {
1485
1533
  * 全部条目并发 + raw 主站与镜像双通道竞速,单条 4 秒封顶。
1486
1534
  * 聚合仓库(根包 private+workspaces)额外检查子包是否有 dsh.bundle 清单:
1487
1535
  * 任一子包可 `dsh plugin add` 直装 → aggregateInstallable = true(★ 筛选会包含它)。 */
1488
- async function enrichItems(items) {
1489
- return Promise.all(items.map(async (item) => {
1490
- let official = null
1491
- let aggregate = false
1492
- let aggregateInstallable = false
1493
- let hasSkill = false
1494
- let hasSuite = false
1495
- try {
1496
- const branch = item.defaultBranch ?? 'main'
1497
- const base = `https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/package.json`
1498
- const [pkgResult, skillResult, suiteResult] = await Promise.allSettled([
1499
- Promise.any([
1500
- curlText(base, 4000),
1501
- curlText(`https://ghproxy.net/${base}`, 4000),
1502
- ]),
1503
- detectSkillRepo(item.fullName, branch),
1504
- rawTextWithFallback(item.fullName, branch, '.gitmodules'),
1505
- ])
1506
- if (suiteResult.status === 'fulfilled' && suiteResult.value !== null) {
1507
- hasSuite = true
1508
- }
1509
- if (pkgResult.status === 'fulfilled') {
1510
- const pkg = JSON.parse(pkgResult.value)
1511
- if (typeof pkg.dsh?.bundle?.patch === 'string') {
1512
- official = true
1513
- } else if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? '')))) {
1514
- aggregate = true
1515
- try {
1516
- const tree = await curlJson(`https://api.github.com/repos/${item.fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`, 6000)
1517
- const pkgPaths = (tree.tree ?? [])
1518
- .filter((n) => n.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(n.path))
1519
- .map((n) => n.path)
1520
- .slice(0, 12)
1521
- if (pkgPaths.length > 0) {
1522
- const subs = await Promise.all(pkgPaths.map((p) => curlText(`https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/${p}`, 4000)
1523
- .then((t) => { try { return JSON.parse(t) } catch { return null } })
1524
- .catch(() => null)))
1525
- if (subs.some((sp) => sp && typeof sp.dsh?.bundle?.patch === 'string')) {
1526
- aggregateInstallable = true
1527
- }
1536
+ const ENRICH_CACHE_FILE = () => join(dshHome(), 'plugin-console', 'enrich-cache.json')
1537
+ const ENRICH_CACHE_TTL = 24 * 60 * 60 * 1000
1538
+
1539
+ function readEnrichCache() {
1540
+ try {
1541
+ const j = JSON.parse(readFileSync(ENRICH_CACHE_FILE(), 'utf8'))
1542
+ return j !== null && typeof j === 'object' ? j : {}
1543
+ } catch { return {} }
1544
+ }
1545
+ function writeEnrichCache(cache) {
1546
+ try {
1547
+ mkdirSync(dirname(ENRICH_CACHE_FILE()), { recursive: true })
1548
+ const entries = Object.entries(cache).sort((a, b) => (b[1]?.at ?? 0) - (a[1]?.at ?? 0)).slice(0, 2000)
1549
+ writeFileSync(ENRICH_CACHE_FILE(), JSON.stringify(Object.fromEntries(entries), null, 2), 'utf8')
1550
+ } catch {}
1551
+ }
1552
+
1553
+ /** 单项识别:官方通道 / 聚合仓库 / 技能 / 套装(失败返回 official=null)。 */
1554
+ async function enrichItemOne(item) {
1555
+ let official = /^deepseek-ai\//u.test(item.fullName ?? '') ? true : null
1556
+ let aggregate = false
1557
+ let aggregateInstallable = false
1558
+ let hasSkill = false
1559
+ let hasSuite = false
1560
+ try {
1561
+ const branch = item.defaultBranch ?? 'main'
1562
+ const base = `https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/package.json`
1563
+ const [pkgResult, skillResult, suiteResult] = await Promise.allSettled([
1564
+ Promise.any([
1565
+ curlText(base, 4000),
1566
+ curlText(`https://ghproxy.net/${base}`, 4000),
1567
+ ]),
1568
+ detectSkillRepo(item.fullName, branch),
1569
+ rawTextWithFallback(item.fullName, branch, '.gitmodules'),
1570
+ ])
1571
+ if (suiteResult.status === 'fulfilled' && suiteResult.value !== null) {
1572
+ hasSuite = true
1573
+ }
1574
+ if (pkgResult.status === 'fulfilled') {
1575
+ const pkg = JSON.parse(pkgResult.value)
1576
+ if (typeof pkg.dsh?.bundle?.patch === 'string') {
1577
+ official = true
1578
+ } else if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? '')))) {
1579
+ aggregate = true
1580
+ try {
1581
+ const tree = await curlJson(`https://api.github.com/repos/${item.fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`, 6000)
1582
+ const pkgPaths = (tree.tree ?? [])
1583
+ .filter((n) => n.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(n.path))
1584
+ .map((n) => n.path)
1585
+ .slice(0, 12)
1586
+ if (pkgPaths.length > 0) {
1587
+ const subs = await Promise.all(pkgPaths.map((p) => curlText(`https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/${p}`, 4000)
1588
+ .then((t) => { try { return JSON.parse(t) } catch { return null } })
1589
+ .catch(() => null)))
1590
+ if (subs.some((sp) => sp && typeof sp.dsh?.bundle?.patch === 'string')) {
1591
+ aggregateInstallable = true
1528
1592
  }
1529
- } catch {}
1530
- }
1593
+ }
1594
+ } catch {}
1531
1595
  }
1532
- if (skillResult.status === 'fulfilled' && skillResult.value?.hasSkill === true) {
1533
- hasSkill = true
1596
+ }
1597
+ if (skillResult.status === 'fulfilled' && skillResult.value?.hasSkill === true) {
1598
+ hasSkill = true
1599
+ }
1600
+ } catch {}
1601
+ return { ...item, official, aggregate, aggregateInstallable, hasSkill, hasSuite }
1602
+ }
1603
+
1604
+ /**
1605
+ * 批量识别(★ 筛选数据源):并发限流 + 24h 结果缓存。
1606
+ * 网络黑洞期(raw.githubusercontent 大部分拉取失败)自动回退缓存中的上次判定,
1607
+ * 保证「只看官方」不因瞬时网络而坍缩成 0/1 条。
1608
+ */
1609
+ async function enrichItems(items) {
1610
+ const cache = readEnrichCache()
1611
+ const out = new Array(items.length)
1612
+ let next = 0
1613
+ let cacheDirty = false
1614
+ async function worker() {
1615
+ while (true) {
1616
+ const i = next
1617
+ next += 1
1618
+ if (i >= items.length) return
1619
+ const item = items[i]
1620
+ const key = `${item.fullName}@${item.defaultBranch ?? 'main'}`
1621
+ const cached = cache[key]
1622
+ try {
1623
+ if (cached !== undefined && typeof cached?.at === 'number' && Date.now() - cached.at < ENRICH_CACHE_TTL && cached.data && (cached.data.official !== null || item.fullName.startsWith('deepseek-ai/'))) {
1624
+ out[i] = { ...item, ...cached.data }
1625
+ continue
1626
+ }
1627
+ let result = await enrichItemOne(item)
1628
+ // 本次失败 → 回退缓存(哪怕已过期),避免「看天吃饭」
1629
+ if ((result.official === null && !/^deepseek-ai\//u.test(item.fullName ?? '')) && cached?.data) {
1630
+ result = { ...item, ...cached.data }
1631
+ } else {
1632
+ cache[key] = { at: Date.now(), data: { official: result.official, aggregate: result.aggregate, aggregateInstallable: result.aggregateInstallable, hasSkill: result.hasSkill, hasSuite: result.hasSuite } }
1633
+ cacheDirty = true
1634
+ }
1635
+ out[i] = result
1636
+ } catch {
1637
+ out[i] = cached?.data ? { ...item, ...cached.data } : { ...item, official: null, aggregate: false, aggregateInstallable: false, hasSkill: false, hasSuite: false }
1534
1638
  }
1535
- } catch {}
1536
- return { ...item, official, aggregate, aggregateInstallable, hasSkill, hasSuite }
1537
- }))
1639
+ }
1640
+ }
1641
+ await Promise.all(Array.from({ length: Math.min(12, items.length) }, worker))
1642
+ if (cacheDirty) writeEnrichCache(cache)
1643
+ return out
1538
1644
  }
1539
1645
 
1540
1646
  function githubRepoInfo(repo) {
@@ -3396,14 +3502,16 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
3396
3502
  const kept = []
3397
3503
  for (const p of targets) {
3398
3504
  let pkg = null
3505
+ let pkgDir = null
3399
3506
  try {
3400
3507
  const require = createRequire(join(profileDir, 'package.json'))
3401
3508
  const pkgPath = require.resolve(`${p.moduleName}/package.json`)
3402
3509
  pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
3510
+ pkgDir = dirname(pkgPath)
3403
3511
  } catch {}
3404
3512
  const version = typeof pkg?.version === 'string' ? pkg.version : null
3405
3513
  const changed = version !== null && version !== p.version
3406
- const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer) : { decision: 'unknown', reason: '无法读取包信息' }
3514
+ const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
3407
3515
  if (changed && check.decision !== 'fail') {
3408
3516
  try { await removeDisableBlock(patchPath, p.rowId) } catch {}
3409
3517
  p.status = 'adopted'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.3.25",
3
+ "version": "0.3.26",
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",