@noob-stupid/dsh-plugin-console 0.3.25 → 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 +64 -7
- package/README.zh.md +53 -6
- package/lib/client.js +185 -9
- package/lib/index.js +482 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,20 +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
|
-
>
|
|
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.
|
|
30
35
|
>
|
|
31
36
|
> **Install:**
|
|
32
37
|
> ```bash
|
|
33
38
|
> npm i -g @noob-stupid/dsh-plugin-console
|
|
34
39
|
> # or in DSH: dsh plugin --profile web add @noob-stupid/dsh-plugin-console
|
|
35
40
|
> ```
|
|
36
|
-
> **Try it:** Plugin Console →
|
|
41
|
+
> **Try it:** Plugin Console → family card at the bottom → 「批量检测更新 / 一键启用已适配」.
|
|
37
42
|
>
|
|
38
43
|
> Feedback: [GitHub Issues](https://github.com/Noob-stupid/dsh-plugin-hub/issues).
|
|
39
44
|
A **plugin management panel** for the DeepSeek Harness (DSH) Web GUI: one-click
|
|
@@ -82,6 +87,8 @@ auto-rollback).
|
|
|
82
87
|
| | Benefit | Detail |
|
|
83
88
|
|---|---|---|
|
|
84
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 |
|
|
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 |
|
|
85
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 |
|
|
86
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 |
|
|
87
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 |
|
|
@@ -246,6 +253,47 @@ Requires: DSH ≥ 0.1.0-rc.6 (web profile, with `dsh-client-modules` / `dsh-host
|
|
|
246
253
|
- **Card dismiss semantics**: terminal states (done/failed) are permanently dismissed on ✕
|
|
247
254
|
(persisted); in-progress dismissal is session-only and the card returns after a refresh.
|
|
248
255
|
|
|
256
|
+
### Framework upgrade safety & plugin adapt gate (v0.3.25)
|
|
257
|
+
|
|
258
|
+
> Origin: the 2026-09-04 DSH `0.1.1-rc.2 → 0.1.2-rc.1` incident — the upgrade script ran in
|
|
259
|
+
> the wrong working directory, overwrote the new CLI into the old `.pnpm` entry, left a
|
|
260
|
+
> mixed-version tree, and the service could not start. This version makes upgrade safety
|
|
261
|
+
> and plugin compatibility detection first-class console mechanisms.
|
|
262
|
+
|
|
263
|
+
**Upgrade safety trio**
|
|
264
|
+
|
|
265
|
+
1. **Framework root detection**: locates the real install root (the top-level `node_modules`
|
|
266
|
+
containing `.pnpm`), fixing the `require.resolve`-returns-`.pnpm`-realpath root cause
|
|
267
|
+
(relink no-op / new package overwriting the old tree); refuses to upgrade when the root
|
|
268
|
+
cannot be identified (service stays running);
|
|
269
|
+
2. **Full-tree checkpoint before upgrade**: mirrors every `@deepseek-ai` version package in
|
|
270
|
+
`.pnpm` + the top-level scope + `lock.yaml`
|
|
271
|
+
(under `~/.dsh/plugin-console/framework-backups/<version>/fw-tree/`);
|
|
272
|
+
3. **Automatic full-tree rollback + retry**: install failure or relaunch failure restores
|
|
273
|
+
the pre-upgrade tree and relaunches automatically — no more "please run it manually"
|
|
274
|
+
dead-ends; after rollback, relink/repair steps are skipped to avoid damaging the
|
|
275
|
+
restored tree a second time.
|
|
276
|
+
|
|
277
|
+
**One-click rollback**: after an upgrade the framework card shows a **「回滚到上一版」** button
|
|
278
|
+
(`/plugin-console/framework-rollback`): stop service → restore full tree → relaunch →
|
|
279
|
+
health check, state visible throughout.
|
|
280
|
+
|
|
281
|
+
**Plugin adapt gate (compatibility detection)**
|
|
282
|
+
|
|
283
|
+
- After an upgrade, plugin entries recorded in `~/.dsh/plugin-console/compat-pending.json`
|
|
284
|
+
are **force-disabled**; the enable button is locked and the server rejects `/toggle` with
|
|
285
|
+
409 (unbypassable). The enabled-button label itself shows「待适配」as the state hint;
|
|
286
|
+
- The **details** panel explains the flow: **Check update → Update & adapt**;
|
|
287
|
+
- **Post-update auto-verification**: scans the latest `package.json` for an explicit
|
|
288
|
+
`dsh.engines.framework` / `engines.dsh` declaration plus `@deepseek-ai/*` dependency
|
|
289
|
+
ranges (built-in zero-dependency semver engine — strict npm prerelease rules for
|
|
290
|
+
dependencies, loose for explicit declarations); if the version changed and no
|
|
291
|
+
incompatibility was found, the `disabled` block is **removed automatically** and the row
|
|
292
|
+
unlocks; aggregator updates validate all synced subpackages too;
|
|
293
|
+
- **AI Empower pre-check**: during planning the server pre-checks the registry's latest
|
|
294
|
+
declaration and adapt-gate hits, injects them into the subagent prompt (the plan must
|
|
295
|
+
explain the conclusion to the user) and marks it on the plan panel (red for incompatible).
|
|
296
|
+
|
|
249
297
|
### Marketplace (multi-source)
|
|
250
298
|
|
|
251
299
|
- **Source switcher**: click the login pill to switch between **GitHub / Gitee / custom
|
|
@@ -349,6 +397,10 @@ GitHub Actions (every 6h, repo token)
|
|
|
349
397
|
- Supports the **DSH 0.1.0 series** (`0.1.0-rc.6` and siblings).
|
|
350
398
|
- The panel reads the running `@deepseek-ai/dsh-web-app` version: after a breaking upgrade
|
|
351
399
|
(0.2 / 1.0) a compatibility warning appears instead of silent failure.
|
|
400
|
+
- **Plugin adapt gate (v0.3.25)**: third-party plugins incompatible with the current
|
|
401
|
+
framework are force-disabled after an upgrade (enable locked); run 「Check update」to
|
|
402
|
+
upgrade the plugin first — it unlocks automatically after the auto-verification passes.
|
|
403
|
+
See 「Framework upgrade safety & plugin adapt gate」.
|
|
352
404
|
- Likely breaking seams: patch semantics, `webServer.register`, loader entry shape,
|
|
353
405
|
`dsh.client` bundle format, `settings.plugins.tab` slot.
|
|
354
406
|
- Deploy scripts do not check versions; the in-panel warning is authoritative.
|
|
@@ -390,6 +442,11 @@ test-harness.mjs Logic self-test (state/toggle/validation/loopback; search SKI
|
|
|
390
442
|
| `/plugin-console/gitee-oauth-url` / `gitee-oauth-callback` | GET | Gitee OAuth flow |
|
|
391
443
|
| `/plugin-console/ai-consent` | POST | Approve/cancel the AI-fallback step |
|
|
392
444
|
| `/plugin-console/restart` | POST | Watchdog-safe self-restart (panel button equivalent) |
|
|
445
|
+
| `/plugin-console/framework-upgrade` | POST | One-click framework upgrade (full-tree checkpoint → online install → rollback protection) |
|
|
446
|
+
| `/plugin-console/framework-rollback` | POST | One-click rollback to the previous version (stop → full-tree restore → auto relaunch → health check) |
|
|
447
|
+
| `/plugin-console/self-update` | POST | Update the console itself (latest npm tarball into the profile) |
|
|
448
|
+
| `/plugin-console/ai-empower/plan` / `status` / `run` / `cancel` | POST | AI Empower: plan (with framework compat pre-check) → status polling → confirmed run → cancel |
|
|
449
|
+
| `/plugin-console/components` / `component/start` / `stop` / `status` | GET/POST | Server component registry and start/stop/status |
|
|
393
450
|
|
|
394
451
|
---
|
|
395
452
|
|
package/README.zh.md
CHANGED
|
@@ -6,18 +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
|
-
>
|
|
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——修复"删一个子包导致整个全家桶消失"的事故。
|
|
14
19
|
>
|
|
15
20
|
> **安装:**
|
|
16
21
|
> ```bash
|
|
17
22
|
> npm i -g @noob-stupid/dsh-plugin-console
|
|
18
|
-
> # 或在 DSH
|
|
23
|
+
> # 或在 DSH 中:dsh plugin --profile web add @noob-stupid/dsh-plugin-console
|
|
19
24
|
> ```
|
|
20
|
-
> **体验入口:** 插件控制台 →
|
|
25
|
+
> **体验入口:** 插件控制台 → 列表底部全家桶卡片 → 批量检测更新 / 一键启用已适配。
|
|
21
26
|
>
|
|
22
27
|
> 反馈:[GitHub Issues](https://github.com/Noob-stupid/dsh-plugin-hub/issues)。
|
|
23
28
|
|
|
@@ -75,6 +80,8 @@
|
|
|
75
80
|
| | 优势 | 说明 |
|
|
76
81
|
|---|---|---|
|
|
77
82
|
| 🤖 | **AI 赋能(v0.3.24)** | 输入 npm 包名 / GitHub 仓库,本地 AI 读文档生成**部署计划**(安装/写配置/启动服务/健康检查),你确认后安全执行;服务器类组件自动生成控制卡片 |
|
|
83
|
+
| 🛡️ | **框架升级适配门与安全(v0.3.25)** | 升级后不兼容插件强制禁用(适配门锁定,「检测更新 → 更新并适配 → 自动校验解锁」);升级前全树 checkpoint、拉起失败自动全树回滚、框架卡片一键回滚到上一版 |
|
|
84
|
+
| 🏠 | **全家桶分组卡片与安全(v0.3.27)** | 同根包子路径导出自动聚合为一张全家桶卡片(收起/展开、批量检测更新、一键启用已适配、已知校验预览);永不崩安全(启用前导入探针 + 补丁自愈 + exports 回退);适配门源码扫描硬判据与迁移检测;删除聚合包子行仅停用该行 |
|
|
78
85
|
| 🚀 | **服务器组件卡片** | 左侧浮卡与主面板顶边对齐:启动 / 停止 / 状态 / 【打开】直达 Web UI,多服务器下拉、可折叠(状态记忆) |
|
|
79
86
|
| 🧩 | **插件 + 技能双市场** | 自动收录 `dsh-plugin` topic 仓库(按 star **500+**),另有**技能 tab**(`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`,最多 300)——浏览、搜索、一键安装,零 GitHub API 调用;插件自带技能(如 openviking-memory)只读展示 |
|
|
80
87
|
| 🤖 | **自动收录 CI** | GitHub Actions 每 6 小时自动重跑 `build-index`(也支持手动触发);作者只需给自己的仓库打 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签,**无需申请、无需审核** |
|
|
@@ -227,6 +234,37 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
|
|
|
227
234
|
- **卡片关闭语义**:终态(成功/失败)点叉号永久关闭(持久化);进行中点叉号仅本次会话
|
|
228
235
|
隐藏,刷新后恢复显示。
|
|
229
236
|
|
|
237
|
+
### 框架升级适配门与升级安全(v0.3.25)
|
|
238
|
+
|
|
239
|
+
> 缘起:2026-09-04 DSH `0.1.1-rc.2 → 0.1.2-rc.1` 升级事故——升级脚本在错误的工作目录执行,
|
|
240
|
+
> 把新 CLI 原位覆盖进旧 `.pnpm` 目录、依赖树混版本,服务无法拉起。本版本把「升级安全」与
|
|
241
|
+
> 「插件兼容检测」固化为控制台机制。
|
|
242
|
+
|
|
243
|
+
**升级安全三件套**
|
|
244
|
+
|
|
245
|
+
1. **框架安装根识别**:定位真正的框架安装根(含 `.pnpm` 的顶层 `node_modules`),修复
|
|
246
|
+
`require.resolve` 返回 `.pnpm` 内部 realpath 导致的「重链空转 / 新包覆盖旧目录」根因;
|
|
247
|
+
定位不到时**拒绝升级**(不 kill 服务、服务保持运行);
|
|
248
|
+
2. **升级前全树 checkpoint**:镜像 `.pnpm` 全部 `@deepseek-ai` 版本包 + 顶层 scope +
|
|
249
|
+
`lock.yaml`(写入 `~/.dsh/plugin-console/framework-backups/<版本>/fw-tree/`);
|
|
250
|
+
3. **拉起失败自动全树回滚并重试**:安装失败或服务拉起失败都会自动恢复升级前全树并再次拉起,
|
|
251
|
+
不再只留「请手动运行」提示;回滚后跳过重链/修复(避免对已恢复旧树二次破坏)。
|
|
252
|
+
|
|
253
|
+
**一键回滚**:升级后框架卡片出现「回滚到上一版」按钮(`/plugin-console/framework-rollback`):
|
|
254
|
+
停服 → 全树恢复 → 自动拉起 → 健康检测,全程状态可见。
|
|
255
|
+
|
|
256
|
+
**插件适配门(插件检测)**
|
|
257
|
+
|
|
258
|
+
- 升级后写入 `~/.dsh/plugin-console/compat-pending.json` 的插件行被**强制禁用**,启用按钮
|
|
259
|
+
锁定(服务端 `/toggle` 直接 409,无法绕过);行状态以按钮文案「待适配」提示;
|
|
260
|
+
- 「详情」面板给出说明与入口:**检测更新 → 更新并适配**;
|
|
261
|
+
- **更新后自动校验**:扫描最新版 `package.json` 的 `dsh.engines.framework` / `engines.dsh`
|
|
262
|
+
显式声明 + `@deepseek-ai/*` 依赖范围(内置零依赖 semver 判定器:依赖判定遵循 npm
|
|
263
|
+
prerelease 严格规则、声明判定宽松);版本已变化且未判定不兼容 → **自动移除禁用块解锁**;
|
|
264
|
+
聚合包更新连带校验同步的子包;
|
|
265
|
+
- **AI 赋能适配预检**:规划时服务端预检 registry 最新版声明与适配门命中情况,注入子代理
|
|
266
|
+
提示词(要求计划向用户解释适配结论),并在计划面板标注(不兼容标红)。
|
|
267
|
+
|
|
230
268
|
### 插件市场(多搜索源)
|
|
231
269
|
|
|
232
270
|
- **搜索源切换**:点击顶部登录态标识弹出菜单,在 **GitHub / Gitee / 自定义源** 间切换(选择持久化);
|
|
@@ -323,6 +361,9 @@ GitHub Actions(每 6 小时,仓库自带 token)
|
|
|
323
361
|
- 当前支持 **DSH 0.1.0 系列**(`0.1.0-rc.6` 及同系列版本)。
|
|
324
362
|
- 面板会读取运行中的 `@deepseek-ai/dsh-web-app` 版本:官方发布破坏性升级
|
|
325
363
|
(0.2 / 1.0 等)后,面板顶部会显示兼容性警告并给出本仓库地址,而不是默默失效。
|
|
364
|
+
- **插件适配门(v0.3.25)**:框架升级后不兼容的第三方插件会被**强制禁用**(启用按钮锁定);
|
|
365
|
+
请先「检测更新」升级该插件,更新并通过自动校验后才会解锁——详见
|
|
366
|
+
「框架升级适配门与升级安全」。
|
|
326
367
|
- 官方破坏性更新可能改动的接口:补丁层语义、`webServer.register`、
|
|
327
368
|
加载器条目结构、`dsh.client` bundle 格式、`settings.plugins.tab` 插槽。
|
|
328
369
|
届时随官方版本更新本仓库即可(依赖面已收窄到上述几个点)。
|
|
@@ -341,6 +382,7 @@ scripts/apply-framework-patch.cjs 框架层补丁(issue #5,幂等,可重
|
|
|
341
382
|
marketplace/index.json 生成的静态索引(jsDelivr CDN 分发)
|
|
342
383
|
deploy.ps1 / deploy.sh 一键部署脚本(Windows / Linux·macOS)
|
|
343
384
|
test-harness.mjs 逻辑自检(state/toggle/校验/环回保护;搜索视网络环境 SKIP)
|
|
385
|
+
test-compat-gate.mjs 适配门单元测试(内置 semver 判定器:声明/依赖兼容规则)
|
|
344
386
|
```
|
|
345
387
|
<img width="1878" height="945" alt="image" src="https://github.com/user-attachments/assets/b26f2f19-0ba4-4be7-9ca1-b3fd4c51a7a8" />
|
|
346
388
|
|
|
@@ -365,6 +407,11 @@ test-harness.mjs 逻辑自检(state/toggle/校验/环回保护;搜索视
|
|
|
365
407
|
| `/plugin-console/gitee-oauth-url` / `gitee-oauth-callback` | GET | Gitee OAuth 流程 |
|
|
366
408
|
| `/plugin-console/ai-consent` | POST | 同意/取消 AI 兜底步骤 |
|
|
367
409
|
| `/plugin-console/restart` | POST | 自带守护的安全自重启(等同面板按钮) |
|
|
410
|
+
| `/plugin-console/framework-upgrade` | POST | 框架一键升级(全树 checkpoint → 在线安装 → 全树回滚保护) |
|
|
411
|
+
| `/plugin-console/framework-rollback` | POST | 一键回滚到升级前版本(停服 → 全树恢复 → 自动拉起 → 健康检测) |
|
|
412
|
+
| `/plugin-console/self-update` | POST | 控制台自身一键更新(npm 最新 tarball 到当前 profile) |
|
|
413
|
+
| `/plugin-console/ai-empower/plan` / `status` / `run` / `cancel` | POST | AI 赋能:规划(含框架适配预检)→ 状态轮询 → 确认执行 → 取消 |
|
|
414
|
+
| `/plugin-console/components` / `component/start` / `stop` / `status` | GET/POST | 服务器组件清单与启动/停止/状态 |
|
|
368
415
|
|
|
369
416
|
---
|
|
370
417
|
|
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
|
});
|
|
@@ -1396,7 +1428,29 @@ window.__ModuleLoader__.load({
|
|
|
1396
1428
|
// 挂载时加载静态插件索引(GitHub 源空查询时秒开浏览,零 API 调用)
|
|
1397
1429
|
react.useEffect(() => {
|
|
1398
1430
|
call("/plugin-console/market-index").then(
|
|
1399
|
-
(data) => {
|
|
1431
|
+
(data) => {
|
|
1432
|
+
if (data && Array.isArray(data.items)) {
|
|
1433
|
+
setIndexData({ items: data.items, skills: Array.isArray(data.skills) ? data.skills : [], loaded: true });
|
|
1434
|
+
// ★「只看官方」需要官方标记:索引条目补标(网络/服务端此前判定失败时不动已有标记,避免 null 覆盖)
|
|
1435
|
+
// 服务端 /enrich 已带 24h 结果缓存时,此处仅兜底浏览器直连失败部分。
|
|
1436
|
+
const base = data.items;
|
|
1437
|
+
enrichOfficialBundle(base).then((enriched) => {
|
|
1438
|
+
if (!Array.isArray(enriched)) return;
|
|
1439
|
+
const merged = enriched.map((e, i) => {
|
|
1440
|
+
const b = base[i] ?? {};
|
|
1441
|
+
return {
|
|
1442
|
+
...e,
|
|
1443
|
+
official: e.official !== null ? e.official : (b.official ?? null),
|
|
1444
|
+
aggregate: e.aggregate === true || b.aggregate === true,
|
|
1445
|
+
aggregateInstallable: b.aggregateInstallable === true,
|
|
1446
|
+
hasSkill: e.hasSkill === true || b.hasSkill === true,
|
|
1447
|
+
hasSuite: e.hasSuite === true || b.hasSuite === true,
|
|
1448
|
+
};
|
|
1449
|
+
});
|
|
1450
|
+
setIndexData((prev) => (prev !== null && prev.loaded === true ? { ...prev, items: merged } : prev));
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
},
|
|
1400
1454
|
() => {},
|
|
1401
1455
|
);
|
|
1402
1456
|
// 市场状态记忆:关闭前搜索过 → 重开时自动恢复该搜索(query 已从 localStorage 初始化)
|
|
@@ -2217,13 +2271,31 @@ window.__ModuleLoader__.load({
|
|
|
2217
2271
|
: ((searchSourcesList !== null ? searchSourcesList.find((s) => s.id === source)?.name : undefined) ?? source);
|
|
2218
2272
|
const extraIds = new Set((state.status === "ready" ? state.data.patch?.inserts ?? [] : []));
|
|
2219
2273
|
const extraCount = state.status === "ready" ? state.data.entries.filter((entry) => entry.extra === true || extraIds.has(entry.rowId)).length : 0;
|
|
2220
|
-
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"
|
|
2221
2276
|
? state.data.entries.filter((entry) => {
|
|
2222
2277
|
if (extraOnly && !(entry.extra === true || extraIds.has(entry.rowId))) return false;
|
|
2223
2278
|
if (installedQueryNorm === "") return true;
|
|
2224
2279
|
return [entry.moduleName, entry.entryId, entry.rowId]
|
|
2225
2280
|
.some((value) => String(value ?? "").toLowerCase().includes(installedQueryNorm));
|
|
2226
|
-
})
|
|
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;
|
|
2227
2299
|
const open = details !== null && details.entryId === entry.entryId;
|
|
2228
2300
|
let detailPanel = null;
|
|
2229
2301
|
if (open) {
|
|
@@ -2236,10 +2308,13 @@ window.__ModuleLoader__.load({
|
|
|
2236
2308
|
} else {
|
|
2237
2309
|
const meta = details.data.meta;
|
|
2238
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;
|
|
2239
2314
|
detailPanel = el("div", { className: styles.detail },
|
|
2240
2315
|
meta !== null ? el("p", { className: styles.desc }, meta.description ?? t("noDetail")) : null,
|
|
2241
2316
|
entry.pendingCompat === true
|
|
2242
|
-
? 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 : ""))
|
|
2243
2318
|
: null,
|
|
2244
2319
|
meta !== null && meta.version ? el("div", { className: styles.rowTop },
|
|
2245
2320
|
el("p", { className: styles.status }, t("versionLabel") + ":" + meta.version),
|
|
@@ -2254,7 +2329,38 @@ window.__ModuleLoader__.load({
|
|
|
2254
2329
|
className: styles.toggle,
|
|
2255
2330
|
title: t("aiEmpowerTitle"),
|
|
2256
2331
|
onClick: () => openAiFor(String(entry.moduleName ?? entry.repo ?? "")),
|
|
2257
|
-
}, 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,
|
|
2258
2364
|
updateCheck !== null && updateCheck.status === "ready"
|
|
2259
2365
|
? (updateCheck.error
|
|
2260
2366
|
? el("p", { className: styles.status, "data-error": "true" }, t("updateCheckFailed") + ":" + updateCheck.error)
|
|
@@ -2292,7 +2398,13 @@ window.__ModuleLoader__.load({
|
|
|
2292
2398
|
}
|
|
2293
2399
|
return el("li", { key: entry.entryId, className: styles.row },
|
|
2294
2400
|
el("div", { className: styles.rowTop },
|
|
2295
|
-
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
|
+
})()),
|
|
2296
2408
|
el("span", { className: styles.tag, "data-enabled": entry.enabled ? "true" : "false" },
|
|
2297
2409
|
t(entry.enabled ? "enabledTag" : "disabledTag")),
|
|
2298
2410
|
entry.extra === true ? el("span", { className: styles.tag, "data-user": "true" }, t("extraTag")) : null,
|
|
@@ -2338,7 +2450,7 @@ window.__ModuleLoader__.load({
|
|
|
2338
2450
|
setMessage(t("deleteBundleNote") + ":" + (data.packageName ?? "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2339
2451
|
setRestartHint(true);
|
|
2340
2452
|
} else {
|
|
2341
|
-
setMessage(t("deleteNote") + ":" + (data.packageName ?? "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2453
|
+
setMessage(t("deleteNote") + ":" + (data.packageName ?? "") + (data.note ? ";" + data.note : "") + (data.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
2342
2454
|
setTimeout(refresh, 1500);
|
|
2343
2455
|
}
|
|
2344
2456
|
},
|
|
@@ -2366,8 +2478,72 @@ window.__ModuleLoader__.load({
|
|
|
2366
2478
|
),
|
|
2367
2479
|
detailPanel,
|
|
2368
2480
|
);
|
|
2369
|
-
})
|
|
2370
|
-
|
|
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] : [];
|
|
2371
2547
|
const marketShown = market === null || market.status !== "ready"
|
|
2372
2548
|
? []
|
|
2373
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
|
|
@@ -293,19 +324,69 @@ function semverRangeMatchLoose(versionText, rangeText) {
|
|
|
293
324
|
return semverRangeMatch(`${v.major}.${v.minor}.${v.patch}`, rangeText)
|
|
294
325
|
}
|
|
295
326
|
|
|
327
|
+
/**
|
|
328
|
+
* 扫描已安装包源码,检测对已删除的 dsh-settings API 的引用
|
|
329
|
+
* (2026-09-04 教训:0.1.2-rc.1 起 settingsNamespace / installSettingsSection 已删除,
|
|
330
|
+
* 静态声明检查判 pass 是假通过;真实兼容性只有模块 import 时见分晓,而 loader 单行失败
|
|
331
|
+
* = 整个服务启动崩溃)。返回命中的符号列表,空数组 = 干净。
|
|
332
|
+
*/
|
|
333
|
+
function scanSettingsApiUsage(pkgDir) {
|
|
334
|
+
const found = []
|
|
335
|
+
const seen = new Set()
|
|
336
|
+
const walk = (dir, depth) => {
|
|
337
|
+
if (depth > 3) return
|
|
338
|
+
let entries = []
|
|
339
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
const full = join(dir, entry.name)
|
|
342
|
+
if (entry.isDirectory()) {
|
|
343
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
344
|
+
walk(full, depth + 1)
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
if (!entry.isFile() || !/\.(?:js|mjs|cjs)$/u.test(entry.name)) continue
|
|
348
|
+
if (seen.has(full)) continue
|
|
349
|
+
seen.add(full)
|
|
350
|
+
if (seen.size > 120) return
|
|
351
|
+
try {
|
|
352
|
+
const text = readFileSync(full, 'utf8').slice(0, 400000)
|
|
353
|
+
for (const sym of ['settingsNamespace', 'installSettingsSection']) {
|
|
354
|
+
if (text.includes(sym) && !found.includes(sym)) found.push(sym)
|
|
355
|
+
}
|
|
356
|
+
} catch {}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
walk(pkgDir, 0)
|
|
360
|
+
return found
|
|
361
|
+
}
|
|
362
|
+
|
|
296
363
|
/**
|
|
297
364
|
* 插件-框架兼容校验(框架升级适配门):
|
|
298
365
|
* 1) 显式兼容声明(dsh.engines.framework / dsh.compat.framework / engines.dsh):以声明为准;
|
|
299
|
-
* 2)
|
|
300
|
-
* 3)
|
|
366
|
+
* 2) 扫描依赖/peerDeps 的 @deepseek-ai/*:任一范围明确不含框架版本 → fail;
|
|
367
|
+
* 3) 【硬判据】提供 pkgDir 时扫描包源码:命中已删除的 dsh-settings API
|
|
368
|
+
* (settingsNamespace / installSettingsSection)→ 直接 fail(2026-09-04 教训:
|
|
369
|
+
* 0.3.6 全家仍引用旧 API,静态声明/依赖检查判 pass 是假通过,loader 单行失败 = 服务崩溃);
|
|
370
|
+
* 4) 无声明且依赖全部满足 → unknown(新版变动不大;由调用方在「版本已变化」前提下放行)。
|
|
301
371
|
* 返回 { decision: 'pass'|'fail'|'unknown', reason }。
|
|
302
372
|
*/
|
|
303
|
-
function checkPluginFrameworkCompat(pkg, frameworkVersion) {
|
|
373
|
+
function checkPluginFrameworkCompat(pkg, frameworkVersion, pkgDir = null) {
|
|
304
374
|
const declared = pkg?.dsh?.engines?.framework ?? pkg?.dsh?.compat?.framework ?? pkg?.engines?.dsh ?? null
|
|
305
375
|
if (typeof declared === 'string' && declared !== '') {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
376
|
+
const ok = semverRangeMatchLoose(frameworkVersion, declared)
|
|
377
|
+
if (!ok) return { decision: 'fail', reason: `声明兼容范围 ${declared} 不满足当前框架 ${frameworkVersion}` }
|
|
378
|
+
}
|
|
379
|
+
// 硬判据:实际源码扫描(比声明/依赖更接近真实兼容性)
|
|
380
|
+
if (pkgDir !== null && typeof pkgDir === 'string' && semverRangeMatchLoose(frameworkVersion, '>=0.1.2')) {
|
|
381
|
+
const broken = scanSettingsApiUsage(pkgDir)
|
|
382
|
+
if (broken.length > 0) {
|
|
383
|
+
return { decision: 'fail', reason: `源码仍引用 0.1.2 起已删除的 dsh-settings API(${broken.join('、')})——实际不兼容,启用会让整个服务启动崩溃` }
|
|
384
|
+
}
|
|
385
|
+
if (typeof declared === 'string' && declared !== '') return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion},且源码无已删除 API 引用` }
|
|
386
|
+
// 源码扫描干净 = 权威判据:依赖范围可能滞后旧版("假拒绝"),不作为 fail。
|
|
387
|
+
return { decision: 'unknown', reason: '源码扫描无已删除 API 引用(声明/依赖范围为参考,不作为不兼容判据)' }
|
|
388
|
+
} else if (typeof declared === 'string' && declared !== '') {
|
|
389
|
+
return { decision: 'pass', reason: `声明兼容范围 ${declared} 满足框架 ${frameworkVersion}` }
|
|
309
390
|
}
|
|
310
391
|
const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.peerDependencies ?? {}), ...(pkg?.optionalDependencies ?? {}) }
|
|
311
392
|
const hits = Object.entries(deps).filter(([name]) => /^@deepseek-ai\//u.test(name))
|
|
@@ -326,8 +407,8 @@ async function readExtraBundleRows(profileDir) {
|
|
|
326
407
|
for (const pkg of bundles) {
|
|
327
408
|
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
328
409
|
try {
|
|
329
|
-
const
|
|
330
|
-
const dir = dirname(
|
|
410
|
+
const pk = resolvePackageJson(pkg, profileDir)
|
|
411
|
+
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
331
412
|
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
332
413
|
const lines = text.split(/\r?\n/u)
|
|
333
414
|
let inInsert = false
|
|
@@ -415,8 +496,8 @@ async function readExtraBundleOwners(profileDir) {
|
|
|
415
496
|
for (const pkg of bundles) {
|
|
416
497
|
if (DEFAULT_BUNDLES.includes(pkg)) continue
|
|
417
498
|
try {
|
|
418
|
-
const
|
|
419
|
-
const dir = dirname(
|
|
499
|
+
const pk = resolvePackageJson(pkg, profileDir)
|
|
500
|
+
const dir = dirname(pk ?? join(profileDir, 'node_modules', ...String(pkg).split('/'), 'package.json'))
|
|
420
501
|
const text = await readFile(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
421
502
|
const lines = text.split(/\r?\n/u)
|
|
422
503
|
let inInsert = false
|
|
@@ -802,6 +883,119 @@ const FRAMEWORK_STATE_FILE = () => join(dshHome(), 'plugin-console', 'framework-
|
|
|
802
883
|
const FRAMEWORK_BACKUP_ROOT = () => join(dshHome(), 'plugin-console', 'framework-backups')
|
|
803
884
|
const COMPAT_PENDING_FILE = () => join(dshHome(), 'plugin-console', 'compat-pending.json')
|
|
804
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
|
+
|
|
805
999
|
/** 读取框架升级适配门清单(compat-pending.json);损坏/缺失返回 null。 */
|
|
806
1000
|
function readCompatPending() {
|
|
807
1001
|
try { return JSON.parse(readFileSync(COMPAT_PENDING_FILE(), 'utf8')) } catch { return null }
|
|
@@ -996,11 +1190,11 @@ function summarizeReadme(text) {
|
|
|
996
1190
|
async function readPluginDetails(moduleName, baseUrl) {
|
|
997
1191
|
if (typeof moduleName !== 'string' || moduleName.startsWith('cordis:')) return { meta: null, readme: null }
|
|
998
1192
|
try {
|
|
999
|
-
const
|
|
1000
|
-
|
|
1193
|
+
const pkgPath = resolvePackageJson(moduleName, baseDirOf(baseUrl))
|
|
1194
|
+
if (pkgPath === null) throw new Error('not found')
|
|
1001
1195
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
1002
1196
|
const meta = {
|
|
1003
|
-
name:
|
|
1197
|
+
name: moduleName,
|
|
1004
1198
|
version: pkg.version ?? null,
|
|
1005
1199
|
description: pkg.description ?? null,
|
|
1006
1200
|
homepage: pkg.homepage ?? null,
|
|
@@ -1485,56 +1679,114 @@ async function fetchSkillMeta(repo, branch, skillDir) {
|
|
|
1485
1679
|
* 全部条目并发 + raw 主站与镜像双通道竞速,单条 4 秒封顶。
|
|
1486
1680
|
* 聚合仓库(根包 private+workspaces)额外检查子包是否有 dsh.bundle 清单:
|
|
1487
1681
|
* 任一子包可 `dsh plugin add` 直装 → aggregateInstallable = true(★ 筛选会包含它)。 */
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1682
|
+
const ENRICH_CACHE_FILE = () => join(dshHome(), 'plugin-console', 'enrich-cache.json')
|
|
1683
|
+
const ENRICH_CACHE_TTL = 24 * 60 * 60 * 1000
|
|
1684
|
+
|
|
1685
|
+
function readEnrichCache() {
|
|
1686
|
+
try {
|
|
1687
|
+
const j = JSON.parse(readFileSync(ENRICH_CACHE_FILE(), 'utf8'))
|
|
1688
|
+
return j !== null && typeof j === 'object' ? j : {}
|
|
1689
|
+
} catch { return {} }
|
|
1690
|
+
}
|
|
1691
|
+
function writeEnrichCache(cache) {
|
|
1692
|
+
try {
|
|
1693
|
+
mkdirSync(dirname(ENRICH_CACHE_FILE()), { recursive: true })
|
|
1694
|
+
const entries = Object.entries(cache).sort((a, b) => (b[1]?.at ?? 0) - (a[1]?.at ?? 0)).slice(0, 2000)
|
|
1695
|
+
writeFileSync(ENRICH_CACHE_FILE(), JSON.stringify(Object.fromEntries(entries), null, 2), 'utf8')
|
|
1696
|
+
} catch {}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/** 单项识别:官方通道 / 聚合仓库 / 技能 / 套装(失败返回 official=null)。 */
|
|
1700
|
+
async function enrichItemOne(item) {
|
|
1701
|
+
let official = /^deepseek-ai\//u.test(item.fullName ?? '') ? true : null
|
|
1702
|
+
let aggregate = false
|
|
1703
|
+
let aggregateInstallable = false
|
|
1704
|
+
let hasSkill = false
|
|
1705
|
+
let hasSuite = false
|
|
1706
|
+
try {
|
|
1707
|
+
const branch = item.defaultBranch ?? 'main'
|
|
1708
|
+
const base = `https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/package.json`
|
|
1709
|
+
const [pkgResult, skillResult, suiteResult] = await Promise.allSettled([
|
|
1710
|
+
Promise.any([
|
|
1711
|
+
curlText(base, 4000),
|
|
1712
|
+
curlText(`https://ghproxy.net/${base}`, 4000),
|
|
1713
|
+
]),
|
|
1714
|
+
detectSkillRepo(item.fullName, branch),
|
|
1715
|
+
rawTextWithFallback(item.fullName, branch, '.gitmodules'),
|
|
1716
|
+
])
|
|
1717
|
+
if (suiteResult.status === 'fulfilled' && suiteResult.value !== null) {
|
|
1718
|
+
hasSuite = true
|
|
1719
|
+
}
|
|
1720
|
+
if (pkgResult.status === 'fulfilled') {
|
|
1721
|
+
const pkg = JSON.parse(pkgResult.value)
|
|
1722
|
+
if (typeof pkg.dsh?.bundle?.patch === 'string') {
|
|
1723
|
+
official = true
|
|
1724
|
+
} else if (pkg.private === true && (Array.isArray(pkg.workspaces) || /(^|-)dsh[-/]/u.test(String(pkg.name ?? '')))) {
|
|
1725
|
+
aggregate = true
|
|
1726
|
+
try {
|
|
1727
|
+
const tree = await curlJson(`https://api.github.com/repos/${item.fullName}/git/trees/${encodeURIComponent(branch)}?recursive=1`, 6000)
|
|
1728
|
+
const pkgPaths = (tree.tree ?? [])
|
|
1729
|
+
.filter((n) => n.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(n.path))
|
|
1730
|
+
.map((n) => n.path)
|
|
1731
|
+
.slice(0, 12)
|
|
1732
|
+
if (pkgPaths.length > 0) {
|
|
1733
|
+
const subs = await Promise.all(pkgPaths.map((p) => curlText(`https://raw.githubusercontent.com/${item.fullName}/${encodeURIComponent(branch)}/${p}`, 4000)
|
|
1734
|
+
.then((t) => { try { return JSON.parse(t) } catch { return null } })
|
|
1735
|
+
.catch(() => null)))
|
|
1736
|
+
if (subs.some((sp) => sp && typeof sp.dsh?.bundle?.patch === 'string')) {
|
|
1737
|
+
aggregateInstallable = true
|
|
1528
1738
|
}
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1739
|
+
}
|
|
1740
|
+
} catch {}
|
|
1531
1741
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1742
|
+
}
|
|
1743
|
+
if (skillResult.status === 'fulfilled' && skillResult.value?.hasSkill === true) {
|
|
1744
|
+
hasSkill = true
|
|
1745
|
+
}
|
|
1746
|
+
} catch {}
|
|
1747
|
+
return { ...item, official, aggregate, aggregateInstallable, hasSkill, hasSuite }
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* 批量识别(★ 筛选数据源):并发限流 + 24h 结果缓存。
|
|
1752
|
+
* 网络黑洞期(raw.githubusercontent 大部分拉取失败)自动回退缓存中的上次判定,
|
|
1753
|
+
* 保证「只看官方」不因瞬时网络而坍缩成 0/1 条。
|
|
1754
|
+
*/
|
|
1755
|
+
async function enrichItems(items) {
|
|
1756
|
+
const cache = readEnrichCache()
|
|
1757
|
+
const out = new Array(items.length)
|
|
1758
|
+
let next = 0
|
|
1759
|
+
let cacheDirty = false
|
|
1760
|
+
async function worker() {
|
|
1761
|
+
while (true) {
|
|
1762
|
+
const i = next
|
|
1763
|
+
next += 1
|
|
1764
|
+
if (i >= items.length) return
|
|
1765
|
+
const item = items[i]
|
|
1766
|
+
const key = `${item.fullName}@${item.defaultBranch ?? 'main'}`
|
|
1767
|
+
const cached = cache[key]
|
|
1768
|
+
try {
|
|
1769
|
+
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/'))) {
|
|
1770
|
+
out[i] = { ...item, ...cached.data }
|
|
1771
|
+
continue
|
|
1772
|
+
}
|
|
1773
|
+
let result = await enrichItemOne(item)
|
|
1774
|
+
// 本次失败 → 回退缓存(哪怕已过期),避免「看天吃饭」
|
|
1775
|
+
if ((result.official === null && !/^deepseek-ai\//u.test(item.fullName ?? '')) && cached?.data) {
|
|
1776
|
+
result = { ...item, ...cached.data }
|
|
1777
|
+
} else {
|
|
1778
|
+
cache[key] = { at: Date.now(), data: { official: result.official, aggregate: result.aggregate, aggregateInstallable: result.aggregateInstallable, hasSkill: result.hasSkill, hasSuite: result.hasSuite } }
|
|
1779
|
+
cacheDirty = true
|
|
1780
|
+
}
|
|
1781
|
+
out[i] = result
|
|
1782
|
+
} catch {
|
|
1783
|
+
out[i] = cached?.data ? { ...item, ...cached.data } : { ...item, official: null, aggregate: false, aggregateInstallable: false, hasSkill: false, hasSuite: false }
|
|
1534
1784
|
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
}))
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
await Promise.all(Array.from({ length: Math.min(12, items.length) }, worker))
|
|
1788
|
+
if (cacheDirty) writeEnrichCache(cache)
|
|
1789
|
+
return out
|
|
1538
1790
|
}
|
|
1539
1791
|
|
|
1540
1792
|
function githubRepoInfo(repo) {
|
|
@@ -3396,14 +3648,17 @@ async function maybeAutoAdaptCompat({ profileDir, packageName, syncedNames, patc
|
|
|
3396
3648
|
const kept = []
|
|
3397
3649
|
for (const p of targets) {
|
|
3398
3650
|
let pkg = null
|
|
3651
|
+
let pkgDir = null
|
|
3399
3652
|
try {
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
|
|
3653
|
+
const pkgPath = resolvePackageJson(p.moduleName, profileDir)
|
|
3654
|
+
if (pkgPath !== null) {
|
|
3655
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
3656
|
+
pkgDir = dirname(pkgPath)
|
|
3657
|
+
}
|
|
3403
3658
|
} catch {}
|
|
3404
3659
|
const version = typeof pkg?.version === 'string' ? pkg.version : null
|
|
3405
3660
|
const changed = version !== null && version !== p.version
|
|
3406
|
-
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
3661
|
+
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
3407
3662
|
if (changed && check.decision !== 'fail') {
|
|
3408
3663
|
try { await removeDisableBlock(patchPath, p.rowId) } catch {}
|
|
3409
3664
|
p.status = 'adopted'
|
|
@@ -3433,8 +3688,8 @@ async function frameworkCompatReportFor(source, ctx, profileDir) {
|
|
|
3433
3688
|
const isNpm = norm.startsWith('@') || !norm.includes('/')
|
|
3434
3689
|
const installed = (() => {
|
|
3435
3690
|
try {
|
|
3436
|
-
const
|
|
3437
|
-
|
|
3691
|
+
const pkgPath = resolvePackageJson(norm, profileDir)
|
|
3692
|
+
if (pkgPath === null) return null
|
|
3438
3693
|
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? null
|
|
3439
3694
|
} catch { return null }
|
|
3440
3695
|
})()
|
|
@@ -3833,6 +4088,17 @@ async function handle(ctx, req, res) {
|
|
|
3833
4088
|
if (method === 'GET' && pathname === `${ROUTE_PREFIX}/state`) {
|
|
3834
4089
|
const patchPath = findPatchPath(ctx)
|
|
3835
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 {}
|
|
3836
4102
|
const extraRows = await readExtraBundleRows(dirname(patchPath))
|
|
3837
4103
|
const compatPending = readCompatPending()
|
|
3838
4104
|
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
@@ -3868,7 +4134,7 @@ async function handle(ctx, req, res) {
|
|
|
3868
4134
|
const selfPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
3869
4135
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
3870
4136
|
} catch {}
|
|
3871
|
-
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 })) })
|
|
3872
4138
|
return
|
|
3873
4139
|
}
|
|
3874
4140
|
|
|
@@ -4070,11 +4336,25 @@ async function handle(ctx, req, res) {
|
|
|
4070
4336
|
const compatPending = readCompatPending()
|
|
4071
4337
|
const pend = (compatPending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
4072
4338
|
if (pend) {
|
|
4073
|
-
sendError(res, 409, `「${rowId}」仍在框架升级适配门中(${compatPending.frameworkVersion ?? '?'}
|
|
4339
|
+
sendError(res, 409, `「${rowId}」仍在框架升级适配门中(${compatPending.frameworkVersion ?? '?'})被强制禁用:请先点击「更新并适配 / 已适配,立即解锁」,通过兼容校验后系统自动解锁`)
|
|
4074
4340
|
return
|
|
4075
4341
|
}
|
|
4076
4342
|
}
|
|
4077
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
|
+
}
|
|
4078
4358
|
const result = enabled
|
|
4079
4359
|
? await enableEntry(patchPath, rowId)
|
|
4080
4360
|
: await disableEntry(patchPath, rowId)
|
|
@@ -4110,14 +4390,18 @@ async function handle(ctx, req, res) {
|
|
|
4110
4390
|
const owners = await readExtraBundleOwners(profileDir)
|
|
4111
4391
|
const ownerBundle = owners.get(rowId) ?? owners.get(moduleName)
|
|
4112
4392
|
if (ownerBundle !== undefined) {
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
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
|
+
})
|
|
4121
4405
|
return
|
|
4122
4406
|
}
|
|
4123
4407
|
if (!patch.inserts.includes(rowId)) {
|
|
@@ -4556,11 +4840,105 @@ async function handle(ctx, req, res) {
|
|
|
4556
4840
|
return
|
|
4557
4841
|
}
|
|
4558
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
|
+
|
|
4559
4937
|
if (pathname === `${ROUTE_PREFIX}/check-update`) {
|
|
4560
4938
|
// 检测已安装插件是否有新版本:curl registry 元数据取 dist-tags.latest(node 网络黑洞时 curl 可用)。
|
|
4561
4939
|
// 聚合包(有 dependencies)额外对比子包版本:声明版本 vs 本地 node_modules 实际版本,
|
|
4562
4940
|
// 返回 depsOutdated 提示"更新本包需同步子包",避免半更新混搭导致启动冲突。
|
|
4563
|
-
const packageName = typeof body.packageName === 'string' ? body.packageName.trim() : ''
|
|
4941
|
+
const packageName = packageNameOf(typeof body.packageName === 'string' ? body.packageName.trim() : '')
|
|
4564
4942
|
if (!packageName) {
|
|
4565
4943
|
sendError(res, 400, 'packageName 不能为空')
|
|
4566
4944
|
return
|
|
@@ -4656,7 +5034,29 @@ async function handle(ctx, req, res) {
|
|
|
4656
5034
|
error = `npm registry 查询失败且 GitHub 回退不可用(${fallbackError};${fbErr instanceof Error ? fbErr.message : String(fbErr)})`
|
|
4657
5035
|
}
|
|
4658
5036
|
}
|
|
4659
|
-
|
|
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 })
|
|
4660
5060
|
return
|
|
4661
5061
|
}
|
|
4662
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",
|