@noob-stupid/dsh-plugin-console 0.3.47 → 0.3.48

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
@@ -308,7 +308,7 @@ MIT
308
308
 
309
309
  The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
310
310
 
311
- - **What it is**: the monolithic `lib/index.js` (7637 lines) split into layers — `lib/index.js` **142 lines** + `lib/server/**` **36 modules**; **feature-equivalent to `0.3.46`**, maintainability only;
312
- - **Verified**: 17 test suites green · 8 architecture-guard assertions · **47/47 routes identical** (`status` + response fields) against the stable build;
311
+ - **What it is**: the monolithic `lib/index.js` (7637 lines) split into layers — `lib/index.js` **142 lines** + `lib/server/**` **36 modules**; **feature-equivalent to `0.3.48`**, maintainability only;
312
+ - **Verified**: 18 test suites green · 8 architecture-guard assertions · **47/47 routes identical** (`status` + response fields) against the stable build;
313
313
  - **Not yet exercised**: real framework upgrade / rollback, restart guardian, real install/uninstall, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
314
- - **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.46`, or `main` of this repo.
314
+ - **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.48`, or `main` of this repo.
package/README.zh.md CHANGED
@@ -1,281 +1,282 @@
1
- > **English**: [README.md](README.md) | **中文**: [README.zh.md](README.zh.md)
2
-
3
- ---
4
-
5
- <img width="1170" height="609" alt="image" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
6
-
7
- # DSH 插件中心(dsh-plugin-hub)
8
-
9
- > 一站式管理你的 DeepSeek Harness 插件:一键启用/停用、500+ 插件与技能市场一键安装、
10
- > 框架一键升级(失败自动回滚)。
11
-
12
- ## 为什么是 DSH 插件中心
13
-
14
- - 🧩 **插件 + 技能双市场** — 自动收录 `dsh-plugin` 仓库(按 star **500+**)外加技能 tab;
15
- 浏览、搜索、一键安装,**零 GitHub API 调用**(CDN 分发,秒开零限流)。
16
- - 🚀 **框架一键升级** — 备份 → 在线安装(服务不中断)→ 校验 → **失败自动回滚**,端到端实测。
17
- - 🤖 **AI 赋能** — 输入 npm 包名或 GitHub 仓库,本地 AI 读文档生成**部署计划**
18
- (安装/写配置/启动服务/健康检查),你确认后安全执行;服务器组件自动生成控制卡片。
19
- → [详见](docs/ai-empower.zh.md)
20
-
21
- ## 一键部署
22
-
23
- ```bash
24
- # npm 发布版(推荐:预构建产物,无需 git / 构建授权)
25
- dsh plugin --profile web add @noob-stupid/dsh-plugin-console
26
-
27
- # 或直接装 GitHub 源码(需 git,首次需 allowBuilds 授权)
28
- dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
29
- ```
30
-
31
- 然后重启 dsh 服务 → 刷新页面 → **设置 → 插件 → 插件控制台**。
32
-
33
- <details><summary><b>更多部署方式</b>(部署脚本 / 一句话交给 AI)</summary>
34
-
35
- ### 方式二:部署脚本(网络受限时的兜底)
36
-
37
- Windows(PowerShell):
38
-
39
- ```powershell
40
- git clone https://github.com/Noob-stupid/dsh-plugin-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
41
- ```
42
-
43
- Linux / macOS:
44
-
45
- ```bash
46
- git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
47
- ```
48
-
49
- 脚本会做两件事:把插件包拷进 `$DSH_HOME/profiles/<profile>/node_modules/`,
50
- 并在 `cordis.patch.yml` 幂等追加启用条目。完成后:
51
-
52
- 1. 重启 dsh 服务(宿主代码变更需要重启进程;命令行方式重启进程,桌面客户端退出重开);
53
- 2. 刷新页面 → 设置 → 插件 → **插件管理**。
54
-
55
- ### 方式三:一句话交给 AI
56
-
57
- > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add @noob-stupid/dsh-plugin-console`(npm 发布版);若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @noob-stupid/dsh-plugin-console)。完成后重启 dsh web。
58
-
59
- 要求:DSH ≥ 0.1.0-rc.6(web profile,含 `dsh-client-modules` / `dsh-host-plugin-inventory`)。
60
-
61
- </details>
62
-
63
- ---
64
-
65
- ## 核心优势
66
-
67
- | | 优势 | 说明 |
68
- |---|---|---|
69
- | 🤖 | **AI 赋能** | 输入 npm 包名 / GitHub 仓库,本地 AI 读文档生成**部署计划**(安装/写配置/启动服务/健康检查),你确认后安全执行;服务器类组件自动生成控制卡片 |
70
- | 🛡️ | **框架升级适配门与安全** | 升级后不兼容插件强制禁用(适配门锁定,「检测更新 → 更新并适配 → 自动校验解锁」);升级前全树 checkpoint、拉起失败自动全树回滚、框架卡片一键回滚到上一版 |
71
- | 🏠 | **全家桶分组卡片与安全** | 同根包子路径导出自动聚合为一张全家桶卡片(收起/展开、批量检测更新、一键启用已适配、已知校验预览);永不崩安全(启用前导入探针 + 补丁自愈 + exports 回退);适配门源码扫描硬判据与迁移检测;删除聚合包子行仅停用该行 |
72
- | 🚀 | **服务器组件卡片** | 左侧浮卡与主面板顶边对齐:启动 / 停止 / 状态 / 【打开】直达 Web UI,多服务器下拉、可折叠(状态记忆) |
73
- | 🧩 | **插件 + 技能双市场** | 自动收录 `dsh-plugin` topic 仓库(按 star **500+**),另有**技能 tab**(`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`,最多 300)——浏览、搜索、一键安装,零 GitHub API 调用;插件自带技能(如 openviking-memory)只读展示 |
74
- | 🤖 | **自动收录 CI** | GitHub Actions 每 6 小时自动重跑 `build-index`(也支持手动触发);作者只需给自己的仓库打 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签,**无需申请、无需审核** |
75
- | ⚡ | **秒开、零限流** | 索引以静态 `marketplace/index.json` 提交仓库,经 jsDelivr CDN 分发(宿主 10 分钟缓存)——终端用户**零 GitHub API 调用、零限流** |
76
- | 🔄 | **版本检测与一键更新** | semver 比较 + beta/next 识别(本地已是测试版最新时不误报);已安装条目自动比对 npm `dist-tags`,聚合包子包版本不配套时给出 depsOutdated 提示 |
77
- | 🔀 | **多源检索** | GitHub / Gitee(仓库直装模式)/ 自定义搜索源(URL 模板 + 请求头认证 + 私网 http);「⊞」并行合并 GitHub + 全部自定义源 |
78
- | 🔒 | **安全默认** | 全部路由仅环回;AI 兜底置于明确费用授权弹窗之后;AI 赋能执行器命令/路径白名单;基础设施行禁止开关(受保护) |
79
-
80
- <details>
81
- <summary><b>📑 目录</b></summary>
82
-
83
- - [核心优势](#核心优势)
84
- - [AI 赋能与服务器组件控制](#ai-赋能与服务器组件控制)
85
- - [一键部署](#一键部署)
86
- - [使用方法](#使用方法)
87
- - [功能](#功能)
88
- - [原理](#原理)
89
- - [兼容性策略](#兼容性策略)
90
- - [项目结构](#项目结构)
91
- - [HTTP 接口](#http-接口)
92
- - [安全说明](#安全说明)
93
- - [已知限制](#已知限制)
94
- - [帮助 / Help](#帮助--help)
95
- - [生态与收录](#生态与收录)
96
- - [参与贡献](#参与贡献)
97
- - [许可证](#许可证)
98
- </details>
99
-
100
- ---
101
-
102
- ## 使用方法
103
-
104
- 1. 重启 DSH → 打开 Web GUI → **设置 → 插件 → 插件管理**。
105
- 2. **已安装列表**:一键开关(HMR 约 1 秒生效)、按名称/id 搜索、展开详情(版本、仓库、README 摘要)。
106
- 3. **插件市场**:GitHub 源空查询直接打开静态索引(秒开);输入关键词实时搜索。顶部登录标切换
107
- 搜索源(GitHub / Gitee / 自定义);「⊞」多源合并;「★」只看可 `dsh plugin add` 直装。
108
- 4. **技能 tab**:搜索框旁切换「插件 / 技能」,浏览并一键安装技能(克隆到 `~/.dsh/skills/<名称>/`)。
109
- 5. **安装**:点「添加到本地」→ 后台安装链自动执行(可放心离开页面);已安装条目自动出现
110
- 「检测更新」/「更新 → vX」。
111
-
112
- ---
113
-
114
- ## 功能
115
-
116
- ### 已安装插件(一键开关 + 详情)
117
-
118
- - **默认只显示第三方插件**(后装/非 dsh 自带),带「第三方」标签与删除入口;点「全部」切换查看完整列表(1.5s 亮框反馈);
119
- - 列出全部插件条目(名称、加载状态、启用状态);支持按名称/id 搜索;
120
- - 点「停用」= 在用户补丁层写入 `- id: X` + `disabled: true`,HMR 立即生效;
121
- - 点「启用」= 移除该停用条目;bundle 层本就停用的行用 `disabled: false` 覆盖;
122
- - 打标「补丁停用 / 补丁强制启用」区分用户补丁状态;
123
- - **基础设施保护**:host 传输/热加载/存储/设置链上的插件(timer、hmr、webserver 等
124
- 70+ 行)标记「受保护」,禁止开关——误停用会破坏热加载本身;
125
- - **详情面板**:每个插件可点「详情」,展开简介、版本、仓库/主页链接与 README 摘要;
126
- - **版本检测**:「检测更新」走 curl 读 npm `dist-tags.latest`(node 网络黑洞时也可用),
127
- 并提示需要同步的子包版本(depsOutdated),避免半更新混搭导致启动冲突。
128
-
129
- ### 框架一键升级(deepseek-harness 卡片)
130
-
131
- - **deepseek-harness 卡片**在检测到框架新版本时显示「框架升级 → vX」(稳定版 latest 优先;
132
- latest 与当前相同时取 next 预发布渠道);点击一键完成:备份配置与框架本体(回滚点)→
133
- **在线安装**(服务保持运行、页面不断开)→ 版本校验 → **自动重启生效**;
134
- - **实时进度**:升级时弹出 `DSH-Upgrade` 窗口,实时显示 pnpm 下载进度;面板内进度卡片
135
- 同步显示等待时长;
136
- - **升级保护**:失败**自动回滚**(robocopy,升级前校验回滚点有效)、版本校验防假成功、
137
- 15 分钟硬超时、卡死检测(debug 日志无更新自动切换 registry)、全局异常兜底、
138
- 15 分钟残留状态清理——框架绝不处于损坏状态;
139
- - **pnpm 通道**:npm-cli.js 在 schtasks 任务环境启动即卡死(debug 日志 0 字节、网络请求
140
- 都发不出);升级改用 `corepack pnpm`(实测 0.4s 秒启动、11.5s 装完 rc.8)+ **国内源
141
- npmmirror**,并带 `dangerouslyAllowAllBuilds` 让 node-pty/koffi 等原生模块正常编译;
142
- - **运行时解析 bin.js**:pnpm 布局下 `@deepseek-ai/dsh` 是 Junction——拉起服务在运行时
143
- 解析(跟随 Junction 到当前版本),而非使用脚本生成时固化的路径;
144
- - **卡片关闭语义**:终态(成功/失败)点叉号永久关闭(持久化);进行中点叉号仅本次会话
145
- 隐藏,刷新后恢复显示。
146
-
147
- ### 插件市场(多搜索源)
148
-
149
- - **搜索源切换**:点击顶部登录态标识弹出菜单,在 **GitHub / Gitee / 自定义源** 间切换(选择持久化);
150
- 市场标题、加载提示、搜索框占位符、说明行全部随源切换;
151
- - **GitHub 源**:默认搜索 `dsh-plugin`,浏览器直连(失败自动回退服务端通道);
152
- - **Gitee 源**:Gitee 官方搜索 API 已停用,采用**仓库直装模式**——输入 `owner/repo`
153
- (支持中文路径与完整 URL)精确查找仓库并安装;
154
- - **自定义搜索源**:软件源管理中添加(URL 模板含 `{q}`/`{page}` 占位符),支持
155
- **请求头认证**(如 `Authorization: Bearer ...`)与**本机/私网 http 地址**;
156
- - **多源汇总**:搜索框旁「⊞」开启——GitHub + 全部自定义源并行检索,结果合并并标注来源;
157
- - **★ 官方筛选**:只筛**可 `dsh plugin add` 直装**的插件——根包带 `dsh.bundle` 清单(官方)
158
- 或聚合仓库中**子包带 `dsh.bundle`**(子包可直装);标记由服务端 curl 双通道 + 客户端兜底补全,
159
- 搜索结果与筛选即时可用;
160
- - **类型徽标**:官方 / 聚合 / 技能(仓库含 SKILL.md)自动识别;
161
- - 「添加到本地」= 按当前源安装(registry 失败自动回退 git 通道)+ 写入启用条目,HMR 生效。
162
-
163
- ### 静态索引市场(插件 / 技能双 tab)
164
-
165
- > **混合架构**:浏览走静态索引(秒开、零 API 调用),搜索走实时通道(GitHub 搜索 API / 多源并行)——
166
- > 两者互补:新仓库即使还没进索引,也能被实时搜索找到。
167
-
168
- - GitHub 源空查询展示**静态索引**(`marketplace/index.json`,jsDelivr CDN + 宿主 10 分钟缓存):
169
- 按 star 排序 500+ 插件,秒开、**零 GitHub API 调用**;
170
- - 搜索框旁**「插件 / 技能」tab**:技能 tab 列出自动收录的 `agent-skills` ∪ `claude-skills` ∪
171
- `dsh-skill` 仓库(最多 300);
172
- - **自动版本比对**:市场中已安装的条目后台自动查 npm `dist-tags.latest`,卡片变「更新 → vX」;
173
- - **技能安装**:技能条目一键安装 = git clone → 复制 SKILL.md 及同目录资源到
174
- `~/.dsh/skills/<名称>/`(frontmatter 的 name 优先于仓库名;SKILL.md 位于仓库根或第一层
175
- 子目录均可识别);已装技能显示灰色「已装」徽标。
176
-
177
- ### 软件源管理
178
-
179
- 市场标题行右侧悬浮「软件源」按钮(半透明、颜色加深),弹出管理模态框:
180
-
181
- ![软件源管理](https://github.com/user-attachments/assets/ef712900-65ae-4f6f-9584-bacdd8d34ea1)
182
-
183
- - **安装源(registry)**:添加 / 行内编辑 / 设为主源 / 恢复默认;支持私有源与内网地址;
184
- **删除已保护**(插件安装依赖的 npm 源,避免误删);
185
- - **搜索源**:内置 GitHub、Gitee + 自定义搜索源(增删、🔒 显示请求头数量);
186
- - **Gitee 登录(可选)**:直装模式无需登录;登录仅提高接口限额——创建第三方应用
187
- (gitee.com → 数据管理 → 第三方应用,权限勾选 user_info、projects)后填入
188
- client_id / client_secret → 保存 → 授权登录。
189
-
190
- ---
191
-
192
- ## 文档
193
-
194
- - [AI 赋能与服务器组件控制](docs/ai-empower.zh.md)
195
- - [框架升级适配门与升级安全](docs/upgrade-safety-adapt-gate.zh.md)
196
- - [原理](docs/how-it-works.zh.md) · [兼容性策略](docs/compatibility.zh.md) · [项目结构](docs/project-layout.zh.md)
197
- - [HTTP 接口](docs/http-endpoints.zh.md) · [AI 兜底安装与授权弹窗](docs/ai-fallback.zh.md)
198
- - [安全说明与免责声明](docs/security-disclaimer.zh.md)
199
-
200
- ---
201
-
202
- ## 已知限制
203
-
204
- - 宿主代码变更需要**重启服务**(面板的重启按钮自带守护、安全);客户端变更只需刷新页面;
205
- - 实时 GitHub 搜索依赖 GitHub 可达性(浏览器直连 + 服务端兜底;网络黑洞期请稍后重试);
206
- - 版本检测只对已发布到 npm 的包生效;技能类仓库没有版本概念;
207
- - 静态索引有上限(每次构建 500 插件 / 300 技能);新仓库靠 star 爬升或等下一轮 6 小时 CI 进入索引;
208
- - 技能由 `dsh-skill-filesystem` 扫描发现——若当前 profile 未启用该插件,装好的技能会休眠,启用并重启后生效。
209
-
210
- ---
211
-
212
- ## 帮助 / Help
213
-
214
- 遇到问题先看这里;仍有疑问请到 [Issues](https://github.com/Noob-stupid/dsh-plugin-hub/issues) 提问。
215
-
216
- - **面板没出现**:重启 dsh 服务 → 刷新页面 → 设置 → 插件 → 插件管理。
217
- - **点开关没反应**:基础设施行带"受保护"标签(禁止开关,这是保护机制);普通插件开关经
218
- HMR 生效,约 1-3 秒,可点刷新查看。
219
- - **顶部出现兼容性警告**:官方发布了破坏性更新,请到本仓库获取适配版本(见兼容性策略)。
220
- - **市场搜索没结果/报错**:GitHub 源走浏览器直连(与浏览器可用性一致),失败自动回退
221
- 服务端通道;Gitee 源为仓库直装模式(输入 `owner/repo`);自定义源检查地址与请求头配置;
222
- 网络黑洞期请稍后重试。
223
- - **★ 筛选为空**:★ 只筛可 `dsh plugin add` 直装(官方 + 子包带 bundle 的聚合);
224
- 标记 1-3 秒后台补全后自动出现,不会误报"没有"。
225
- - **安装失败**:确认仓库有 package.json 且包名已发布到 npm;npm 装不了的会回退
226
- `github:owner/repo` 安装(需要 git);可在软件源管理中换主源(如 npmmirror 网络波动时)。
227
- - **装好的技能 DSH 不识别**:在 profile 的 `cordis.yml` 启用 `@deepseek-ai/dsh-skill-filesystem`
228
- 并重启;技能位于 `~/.dsh/skills/<名称>/`。
229
-
230
- ---
231
-
232
- ## 生态与收录
233
-
234
- - 已被 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)(社区主精选列表)与 [DSH Directory](https://dsh.directory) 收录;
235
- - 本中心的自动收录索引(500+ 插件 / 300 技能,CI 每 6 小时刷新)会收录**任何**打了 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签的仓库——作者打上标签即可自动出现在市场,无需申请;
236
- - 如果你在开发 DSH 插件,本面板就是你的分发渠道:所有使用本面板的用户都能一键安装你的插件。
237
-
238
- ## 支持
239
-
240
- 如果这个面板帮你省了时间、让 DSH 更好用:
241
-
242
- - ⭐ **给本仓库点个 Star**——直接帮助更多 DSH 用户发现它;
243
- - 分享给身边的 DSH 用户或社区;
244
- - 提交你自己的插件(打上 `dsh-plugin` 标签)一起壮大生态;
245
- - 发现 bug 或有新需求?[开一个 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues)。
246
-
247
- ---
248
-
249
- ## 参与贡献
250
-
251
- 欢迎各种形式的贡献——Issue、PR、文档、翻译:
252
-
253
- - **贡献指南**:[CONTRIBUTING.md](CONTRIBUTING.md)
254
- - **行为准则**:[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
255
- - **安全政策**:[SECURITY.md](SECURITY.md)(漏洞私下报告)
256
- - **Issue 模板**:从 [新建 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues/new/choose) 页面选择 Bug / 功能建议
257
-
258
- ---
259
-
260
- ## 更新日志
261
-
262
- 版本记录见 [CHANGELOG.md](CHANGELOG.md)。
263
-
264
- ---
265
-
266
- ## License
267
-
268
- MIT
269
-
270
- ---
271
-
272
- ---
273
-
274
- ## 预览版(实验性 · 请勿用于生产)
275
-
276
- 分层重构预览线在**独立仓库**:[**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
277
-
278
- - **它是什么**:把单体 `lib/index.js`(7637 行)拆成分层结构 —— `lib/index.js` **142 行** + `lib/server/**` **36 个模块**,**功能与 `0.3.46` 等价**,只为可维护性;
279
- - **已验证**:17 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打 **47 条路由,`status` + 响应字段 47/47 一致**;
280
- - **尚未实测**:真框架升级 / 真回滚、重启守护链路、真装真卸、组件进程启停、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
281
- - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.46`,或本仓库 `main`。
1
+ > **English**: [README.md](README.md) | **中文**: [README.zh.md](README.zh.md)
2
+
3
+ ---
4
+
5
+ <img width="1170" height="609" alt="image" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
6
+
7
+ # DSH 插件中心(dsh-plugin-hub)
8
+
9
+ > 一站式管理你的 DeepSeek Harness 插件:一键启用/停用、500+ 插件与技能市场一键安装、
10
+ > 框架一键升级(失败自动回滚)。
11
+
12
+ ## 为什么是 DSH 插件中心
13
+
14
+ - 🧩 **插件 + 技能双市场** — 自动收录 `dsh-plugin` 仓库(按 star **500+**)外加技能 tab;
15
+ 浏览、搜索、一键安装,**零 GitHub API 调用**(CDN 分发,秒开零限流)。
16
+ - 🚀 **框架一键升级** — 备份 → 在线安装(服务不中断)→ 校验 → **失败自动回滚**,端到端实测。
17
+ - 🤖 **AI 赋能** — 输入 npm 包名或 GitHub 仓库,本地 AI 读文档生成**部署计划**
18
+ (安装/写配置/启动服务/健康检查),你确认后安全执行;服务器组件自动生成控制卡片。
19
+ → [详见](docs/ai-empower.zh.md)
20
+
21
+ ## 一键部署
22
+
23
+ ```bash
24
+ # npm 发布版(推荐:预构建产物,无需 git / 构建授权)
25
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
26
+
27
+ # 或直接装 GitHub 源码(需 git,首次需 allowBuilds 授权)
28
+ dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
29
+ ```
30
+
31
+ 然后重启 dsh 服务 → 刷新页面 → **设置 → 插件 → 插件控制台**。
32
+
33
+ <details><summary><b>更多部署方式</b>(部署脚本 / 一句话交给 AI)</summary>
34
+
35
+ ### 方式二:部署脚本(网络受限时的兜底)
36
+
37
+ Windows(PowerShell):
38
+
39
+ ```powershell
40
+ git clone https://github.com/Noob-stupid/dsh-plugin-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
41
+ ```
42
+
43
+ Linux / macOS:
44
+
45
+ ```bash
46
+ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
47
+ ```
48
+
49
+ 脚本会做两件事:把插件包拷进 `$DSH_HOME/profiles/<profile>/node_modules/`,
50
+ 并在 `cordis.patch.yml` 幂等追加启用条目。完成后:
51
+
52
+ 1. 重启 dsh 服务(宿主代码变更需要重启进程;命令行方式重启进程,桌面客户端退出重开);
53
+ 2. 刷新页面 → 设置 → 插件 → **插件管理**。
54
+
55
+ ### 方式三:一句话交给 AI
56
+
57
+ > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add @noob-stupid/dsh-plugin-console`(npm 发布版);若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @noob-stupid/dsh-plugin-console)。完成后重启 dsh web。
58
+
59
+ 要求:DSH ≥ 0.1.0-rc.6(web profile,含 `dsh-client-modules` / `dsh-host-plugin-inventory`)。
60
+
61
+ </details>
62
+
63
+ ---
64
+
65
+ ## 核心优势
66
+
67
+ | | 优势 | 说明 |
68
+ |---|---|---|
69
+ | 🤖 | **AI 赋能** | 输入 npm 包名 / GitHub 仓库,本地 AI 读文档生成**部署计划**(安装/写配置/启动服务/健康检查),你确认后安全执行;服务器类组件自动生成控制卡片 |
70
+ | 🛡️ | **框架升级适配门与安全** | 升级后不兼容插件强制禁用(适配门锁定,「检测更新 → 更新并适配 → 自动校验解锁」);升级前全树 checkpoint、拉起失败自动全树回滚、框架卡片一键回滚到上一版 |
71
+ | 🏠 | **全家桶分组卡片与安全** | 同根包子路径导出自动聚合为一张全家桶卡片(收起/展开、批量检测更新、一键启用已适配、已知校验预览);永不崩安全(启用前导入探针 + 补丁自愈 + exports 回退);适配门源码扫描硬判据与迁移检测;删除聚合包子行仅停用该行 |
72
+ | 🚀 | **服务器组件卡片** | 左侧浮卡与主面板顶边对齐:启动 / 停止 / 状态 / 【打开】直达 Web UI,多服务器下拉、可折叠(状态记忆) |
73
+ | 🧩 | **插件 + 技能双市场** | 自动收录 `dsh-plugin` topic 仓库(按 star **500+**),另有**技能 tab**(`agent-skills` ∪ `claude-skills` ∪ `dsh-skill`,最多 300)——浏览、搜索、一键安装,零 GitHub API 调用;插件自带技能(如 openviking-memory)只读展示 |
74
+ | 🤖 | **自动收录 CI** | GitHub Actions 每 6 小时自动重跑 `build-index`(也支持手动触发);作者只需给自己的仓库打 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签,**无需申请、无需审核** |
75
+ | ⚡ | **秒开、零限流** | 索引以静态 `marketplace/index.json` 提交仓库,经 jsDelivr CDN 分发(宿主 10 分钟缓存)——终端用户**零 GitHub API 调用、零限流** |
76
+ | 🔄 | **版本检测与一键更新** | semver 比较 + beta/next 识别(本地已是测试版最新时不误报);已安装条目自动比对 npm `dist-tags`,聚合包子包版本不配套时给出 depsOutdated 提示 |
77
+ | 🔀 | **多源检索** | GitHub / Gitee(仓库直装模式)/ 自定义搜索源(URL 模板 + 请求头认证 + 私网 http);「⊞」并行合并 GitHub + 全部自定义源 |
78
+ | 🔒 | **安全默认** | 全部路由仅环回;AI 兜底置于明确费用授权弹窗之后;AI 赋能执行器命令/路径白名单;基础设施行禁止开关(受保护) |
79
+
80
+ <details>
81
+ <summary><b>📑 目录</b></summary>
82
+
83
+ - [核心优势](#核心优势)
84
+ - [AI 赋能与服务器组件控制](#ai-赋能与服务器组件控制)
85
+ - [一键部署](#一键部署)
86
+ - [使用方法](#使用方法)
87
+ - [功能](#功能)
88
+ - [原理](#原理)
89
+ - [兼容性策略](#兼容性策略)
90
+ - [项目结构](#项目结构)
91
+ - [HTTP 接口](#http-接口)
92
+ - [安全说明](#安全说明)
93
+ - [已知限制](#已知限制)
94
+ - [帮助 / Help](#帮助--help)
95
+ - [生态与收录](#生态与收录)
96
+ - [参与贡献](#参与贡献)
97
+ - [许可证](#许可证)
98
+ </details>
99
+
100
+ ---
101
+
102
+ ## 使用方法
103
+
104
+ 1. 重启 DSH → 打开 Web GUI → **设置 → 插件 → 插件管理**。
105
+ 2. **已安装列表**:一键开关(HMR 约 1 秒生效)、按名称/id 搜索、展开详情(版本、仓库、README 摘要)。
106
+ 3. **插件市场**:GitHub 源空查询直接打开静态索引(秒开);输入关键词实时搜索。顶部登录标切换
107
+ 搜索源(GitHub / Gitee / 自定义);「⊞」多源合并;「★」只看可 `dsh plugin add` 直装。
108
+ 4. **技能 tab**:搜索框旁切换「插件 / 技能」,浏览并一键安装技能(克隆到 `~/.dsh/skills/<名称>/`)。
109
+ 5. **安装**:点「添加到本地」→ 后台安装链自动执行(可放心离开页面);已安装条目自动出现
110
+ 「检测更新」/「更新 → vX」。
111
+
112
+ ---
113
+
114
+ ## 功能
115
+
116
+ ### 已安装插件(一键开关 + 详情)
117
+
118
+ - **默认只显示第三方插件**(后装/非 dsh 自带),带「第三方」标签与删除入口;点「全部」切换查看完整列表(1.5s 亮框反馈);
119
+ - 列出全部插件条目(名称、加载状态、启用状态);支持按名称/id 搜索;
120
+ - 点「停用」= 在用户补丁层写入 `- id: X` + `disabled: true`,HMR 立即生效;
121
+ - 点「启用」= 移除该停用条目;bundle 层本就停用的行用 `disabled: false` 覆盖;
122
+ - 打标「补丁停用 / 补丁强制启用」区分用户补丁状态;
123
+ - **基础设施保护**:host 传输/热加载/存储/设置链上的插件(timer、hmr、webserver 等
124
+ 70+ 行)标记「受保护」,禁止开关——误停用会破坏热加载本身;
125
+ - **详情面板**:每个插件可点「详情」,展开简介、版本、仓库/主页链接与 README 摘要;
126
+ - **版本检测**:「检测更新」走 curl 读 npm `dist-tags.latest`(node 网络黑洞时也可用),
127
+ 并提示需要同步的子包版本(depsOutdated),避免半更新混搭导致启动冲突。
128
+
129
+ ### 框架一键升级(deepseek-harness 卡片)
130
+
131
+ - **deepseek-harness 卡片**在检测到框架新版本时显示「框架升级 → vX」(稳定版 latest 优先;
132
+ latest 与当前相同时取 next 预发布渠道);点击一键完成:备份配置与框架本体(回滚点)→
133
+ **在线安装**(服务保持运行、页面不断开)→ 版本校验 → **自动重启生效**;
134
+ - **实时进度**:升级时弹出 `DSH-Upgrade` 窗口,实时显示 pnpm 下载进度;面板内进度卡片
135
+ 同步显示等待时长;
136
+ - **升级保护**:失败**自动回滚**(robocopy,升级前校验回滚点有效)、版本校验防假成功、
137
+ 15 分钟硬超时、卡死检测(debug 日志无更新自动切换 registry)、全局异常兜底、
138
+ 15 分钟残留状态清理——框架绝不处于损坏状态;
139
+ - **pnpm 通道**:npm-cli.js 在 schtasks 任务环境启动即卡死(debug 日志 0 字节、网络请求
140
+ 都发不出);升级改用 `corepack pnpm`(实测 0.4s 秒启动、11.5s 装完 rc.8)+ **国内源
141
+ npmmirror**,并带 `dangerouslyAllowAllBuilds` 让 node-pty/koffi 等原生模块正常编译;
142
+ - **运行时解析 bin.js**:pnpm 布局下 `@deepseek-ai/dsh` 是 Junction——拉起服务在运行时
143
+ 解析(跟随 Junction 到当前版本),而非使用脚本生成时固化的路径;
144
+ - **卡片关闭语义**:终态(成功/失败)点叉号永久关闭(持久化);进行中点叉号仅本次会话
145
+ 隐藏,刷新后恢复显示。
146
+
147
+ ### 插件市场(多搜索源)
148
+
149
+ - **搜索源切换**:点击顶部登录态标识弹出菜单,在 **GitHub / Gitee / 自定义源** 间切换(选择持久化);
150
+ 市场标题、加载提示、搜索框占位符、说明行全部随源切换;
151
+ - **GitHub 源**:默认搜索 `dsh-plugin`,浏览器直连(失败自动回退服务端通道);
152
+ - **Gitee 源**:Gitee 官方搜索 API 已停用,采用**仓库直装模式**——输入 `owner/repo`
153
+ (支持中文路径与完整 URL)精确查找仓库并安装;
154
+ - **自定义搜索源**:软件源管理中添加(URL 模板含 `{q}`/`{page}` 占位符),支持
155
+ **请求头认证**(如 `Authorization: Bearer ...`)与**本机/私网 http 地址**;
156
+ - **多源汇总**:搜索框旁「⊞」开启——GitHub + 全部自定义源并行检索,结果合并并标注来源;
157
+ - **★ 官方筛选**:只筛**可 `dsh plugin add` 直装**的插件——根包带 `dsh.bundle` 清单(官方)
158
+ 或聚合仓库中**子包带 `dsh.bundle`**(子包可直装);标记由服务端 curl 双通道 + 客户端兜底补全,
159
+ 搜索结果与筛选即时可用;
160
+ - **类型徽标**:官方 / 聚合 / 技能(仓库含 SKILL.md)自动识别;
161
+ - 「添加到本地」= 按当前源安装(registry 失败自动回退 git 通道)+ 写入启用条目,HMR 生效。
162
+
163
+ ### 静态索引市场(插件 / 技能双 tab)
164
+
165
+ > **混合架构**:浏览走静态索引(秒开、零 API 调用),搜索走实时通道(GitHub 搜索 API / 多源并行)——
166
+ > 两者互补:新仓库即使还没进索引,也能被实时搜索找到。
167
+
168
+ - GitHub 源空查询展示**静态索引**(`marketplace/index.json`,jsDelivr CDN + 宿主 10 分钟缓存):
169
+ 按 star 排序 500+ 插件,秒开、**零 GitHub API 调用**;
170
+ - 搜索框旁**「插件 / 技能」tab**:技能 tab 列出自动收录的 `agent-skills` ∪ `claude-skills` ∪
171
+ `dsh-skill` 仓库(最多 300);
172
+ - **自动版本比对**:市场中已安装的条目后台自动查 npm `dist-tags.latest`,卡片变「更新 → vX」;
173
+ - **技能安装**:技能条目一键安装 = git clone → 复制 SKILL.md 及同目录资源到
174
+ `~/.dsh/skills/<名称>/`(frontmatter 的 name 优先于仓库名;SKILL.md 位于仓库根或第一层
175
+ 子目录均可识别);已装技能显示灰色「已装」徽标。
176
+
177
+ ### 软件源管理
178
+
179
+ 市场标题行右侧悬浮「软件源」按钮(半透明、颜色加深),弹出管理模态框:
180
+
181
+ ![软件源管理](https://github.com/user-attachments/assets/ef712900-65ae-4f6f-9584-bacdd8d34ea1)
182
+
183
+ - **安装源(registry)**:添加 / 行内编辑 / 设为主源 / 恢复默认;支持私有源与内网地址;
184
+ **删除已保护**(插件安装依赖的 npm 源,避免误删);
185
+ - **搜索源**:内置 GitHub、Gitee + 自定义搜索源(增删、🔒 显示请求头数量);
186
+ - **Gitee 登录(可选)**:直装模式无需登录;登录仅提高接口限额——创建第三方应用
187
+ (gitee.com → 数据管理 → 第三方应用,权限勾选 user_info、projects)后填入
188
+ client_id / client_secret → 保存 → 授权登录。
189
+
190
+ ---
191
+
192
+ ## 文档
193
+
194
+ - [AI 赋能与服务器组件控制](docs/ai-empower.zh.md)
195
+ - [框架升级适配门与升级安全](docs/upgrade-safety-adapt-gate.zh.md)
196
+ - [原理](docs/how-it-works.zh.md) · [兼容性策略](docs/compatibility.zh.md) · [项目结构](docs/project-layout.zh.md)
197
+ - [HTTP 接口](docs/http-endpoints.zh.md) · [AI 兜底安装与授权弹窗](docs/ai-fallback.zh.md)
198
+ - [安全说明与免责声明](docs/security-disclaimer.zh.md)
199
+
200
+ ---
201
+
202
+ ## 已知限制
203
+
204
+ - 宿主代码变更需要**重启服务**(面板的重启按钮自带守护、安全);客户端变更只需刷新页面;
205
+ - 实时 GitHub 搜索依赖 GitHub 可达性(浏览器直连 + 服务端兜底;网络黑洞期请稍后重试);
206
+ - 版本检测只对已发布到 npm 的包生效;技能类仓库没有版本概念;
207
+ - 静态索引有上限(每次构建 500 插件 / 300 技能);新仓库靠 star 爬升或等下一轮 6 小时 CI 进入索引;
208
+ - 技能由 `dsh-skill-filesystem` 扫描发现——若当前 profile 未启用该插件,装好的技能会休眠,启用并重启后生效。
209
+
210
+ ---
211
+
212
+ ## 帮助 / Help
213
+
214
+ 遇到问题先看这里;仍有疑问请到 [议题](https://github.com/Noob-stupid/dsh-plugin-hub/issues) 提问。
215
+
216
+ - **面板没出现**:重启 dsh 服务 → 刷新页面 → 设置 → 插件 → 插件管理。
217
+ - **点开关没反应**:基础设施行带"受保护"标签(禁止开关,这是保护机制);普通插件开关经
218
+ HMR 生效,约 1-3 秒,可点刷新查看。
219
+ - **顶部出现兼容性警告**:官方发布了破坏性更新,请到本仓库获取适配版本(见兼容性策略)。
220
+ - **市场搜索没结果/报错**:GitHub 源走浏览器直连(与浏览器可用性一致),失败自动回退
221
+ 服务端通道;Gitee 源为仓库直装模式(输入 `owner/repo`);自定义源检查地址与请求头配置;
222
+ 网络黑洞期请稍后重试。
223
+ - **★ 筛选为空**:★ 只筛可 `dsh plugin add` 直装(官方 + 子包带 bundle 的聚合);
224
+ 标记 1-3 秒后台补全后自动出现,不会误报"没有"。
225
+ - **安装失败**:确认仓库有 package.json 且包名已发布到 npm;npm 装不了的会回退
226
+ `github:owner/repo` 安装(需要 git);可在软件源管理中换主源(如 npmmirror 网络波动时)。
227
+ - **装好的技能 DSH 不识别**:在 profile 的 `cordis.yml` 启用 `@deepseek-ai/dsh-skill-filesystem`
228
+ 并重启;技能位于 `~/.dsh/skills/<名称>/`。
229
+
230
+ ---
231
+
232
+ ## 生态与收录
233
+
234
+ - 已被 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)(社区主精选列表)与 [DSH Directory](https://dsh.directory) 收录;
235
+ - 本中心的自动收录索引(500+ 插件 / 300 技能,CI 每 6 小时刷新)会收录**任何**打了 `dsh-plugin` / `agent-skills` / `claude-skills` / `dsh-skill` 标签的仓库——作者打上标签即可自动出现在市场,无需申请;
236
+ - 如果你在开发 DSH 插件,本面板就是你的分发渠道:所有使用本面板的用户都能一键安装你的插件。
237
+
238
+ ## 支持
239
+
240
+ 如果这个面板帮你省了时间、让 DSH 更好用:
241
+
242
+ - ⭐ **给本仓库点个 Star**——直接帮助更多 DSH 用户发现它;
243
+ - 分享给身边的 DSH 用户或社区;
244
+ - 提交你自己的插件(打上 `dsh-plugin` 标签)一起壮大生态;
245
+ - 发现 bug 或有新需求?[开一个 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues)。
246
+
247
+ ---
248
+
249
+ ## 参与贡献
250
+
251
+ 欢迎各种形式的贡献——Issue、PR、文档、翻译:
252
+
253
+ - **贡献指南**:[CONTRIBUTING.md](CONTRIBUTING.md)
254
+ - **行为准则**:[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
255
+ - **安全政策**:[SECURITY.md](SECURITY.md)(漏洞私下报告)
256
+ - **Issue 模板**:从 [新建 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues/new/choose) 页面选择 Bug / 功能建议
257
+
258
+ ---
259
+
260
+ ## 更新日志
261
+
262
+ 版本记录见 [CHANGELOG.md](CHANGELOG.md)。
263
+
264
+ ---
265
+
266
+ ## License
267
+
268
+ MIT
269
+
270
+ ---
271
+
272
+ ---
273
+
274
+ ## 预览版(实验性 · 请勿用于生产)
275
+
276
+ 分层重构预览线在**独立仓库**:[**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
277
+
278
+ - **它是什么**:把单体 `lib/index.js`(7637 行)拆成分层结构 —— `lib/index.js` **142 行** + `lib/server/**` **36 个模块**,**功能与 `0.3.48` 等价**,只为可维护性;
279
+ - **已验证**:18 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打 **47 条路由,`status` + 响应字段 47/47 一致**;
280
+ - **尚未实测**:真框架升级 / 真回滚、重启守护链路、真装真卸、组件进程启停、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
281
+ - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.48`,或本仓库 `main`。
282
+ - 请多反馈问题
package/lib/index.js CHANGED
@@ -594,7 +594,7 @@ async function removeBundleFromManifest(profileDir, bundlePkg) {
594
594
  /** 用 corepack pnpm 移除包(与安装同一管理器与凭据抑制环境)。
595
595
  * 注意:pnpm remove 不支持 --registry 选项,去掉避免静默失败。 */
596
596
  async function pnpmRemove(profileDir, packageName) {
597
- const args = ['pnpm', 'remove', packageName]
597
+ const args = ['remove', packageName]
598
598
  const opts = {
599
599
  cwd: profileDir,
600
600
  timeout: 120000,
@@ -602,17 +602,8 @@ async function pnpmRemove(profileDir, packageName) {
602
602
  maxBuffer: 4 * 1024 * 1024,
603
603
  env: { ...process.env, COREPACK_NPM_REGISTRY: 'https://registry.npmmirror.com', GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' },
604
604
  }
605
- if (process.platform === 'win32') {
606
- const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
607
- if (existsSync(corepackJs)) {
608
- await execFileAsync(process.execPath, [corepackJs, ...args], opts)
609
- return
610
- }
611
- const cmd = `${JSON.stringify('corepack')} ${args.map((arg) => JSON.stringify(arg)).join(' ')}`
612
- await execFileAsync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', cmd], opts)
613
- return
614
- }
615
- await execFileAsync('corepack', args, opts)
605
+ // 跨平台定位 corepack/pnpm(Linux 的 corepack 不在 <node bin>/node_modules 下,2026-09-20 事故)
606
+ await runPnpmWithFallback(args, { execOpts: opts })
616
607
  }
617
608
 
618
609
  /** 清理陈旧包目录与 pnpm _tmp_ 残留(Windows 原子替换 EPERM 的根因),返回清理数量。 */
@@ -1705,11 +1696,12 @@ const API_MIRROR_PREFIXES = [
1705
1696
  'https://ghfast.top/https://api.github.com',
1706
1697
  ]
1707
1698
 
1708
- /** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。 */
1699
+ /** raw.githubusercontent.com 的镜像竞速前缀(与浏览器端四通道一致)。
1700
+ * 2026-09-19 移除 mirror.ghproxy.com:该域名早已失效(实测连接超时;失效域名被停放页接管时
1701
+ * 会回 2xx HTML —— 正是"普通插件被误判成 submodule 套装"的假阳性来源之一)。 */
1709
1702
  const RAW_MIRROR_PREFIXES = [
1710
1703
  'https://ghproxy.net/https://raw.githubusercontent.com',
1711
1704
  'https://ghfast.top/https://raw.githubusercontent.com',
1712
- 'https://mirror.ghproxy.com/https://raw.githubusercontent.com',
1713
1705
  ]
1714
1706
 
1715
1707
  /** 并行竞速:任一分支拿到 2xx 即胜出;非 2xx 与错误都视为失败,全部失败则拒绝。 */
@@ -1868,9 +1860,12 @@ async function githubText(url, signal, token = null) {
1868
1860
  }
1869
1861
 
1870
1862
  /** 用 curl 子进程请求 JSON(绕过本机中间设备对 node:https TLS 指纹的拦截;curl 走系统网络栈)。 */
1871
- async function curlJson(url, timeout = 15000, headers = {}) {
1863
+ async function curlJson(url, timeout = 15000, headers = {}, { ipv4 = false } = {}) {
1872
1864
  const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
1873
1865
  const args = ['-s', '-m', String(Math.max(5, Math.ceil(timeout / 1000))), '-H', 'accept: application/json', '-w', '\n__HTTP__%{http_code}']
1866
+ // ipv4:只给 GitHub 域名用——「解析出 IPv6 但没有 IPv6 路由」的环境里 curl 默认要先空等 ~5.2s 才回退
1867
+ // IPv4(2026-09-20 另一位用户实测:默认 5473ms vs `-4` 的 461ms);双栈正常的环境无副作用。
1868
+ if (ipv4) args.push('-4')
1874
1869
  for (const [key, value] of Object.entries(headers)) args.push('-H', `${key}: ${value}`)
1875
1870
  args.push(url)
1876
1871
  const { stdout } = await execFileAsync(bin, args, { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 })
@@ -1881,12 +1876,17 @@ async function curlJson(url, timeout = 15000, headers = {}) {
1881
1876
  return JSON.parse(body)
1882
1877
  }
1883
1878
 
1884
- /** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。 */
1885
- async function curlText(url, timeout = 10000) {
1879
+ /** 用 curl 拉取文本(raw 文件用:本机 curl 直连 raw.githubusercontent.com 秒回,绕开 node:https 黑洞)。
1880
+ * 导出供 test-suite-detect.mjs 断言传输层口径(200 空 body → 空串,404 → 抛 HTTP 404)。 */
1881
+ export async function curlText(url, timeout = 10000, { ipv4 = false } = {}) {
1886
1882
  const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
1883
+ const args = ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}']
1884
+ // 见 curlJson 注释:GitHub 域名走 IPv4 快通道(IPv6 无路由的环境下默认要白等 ~5.2s)
1885
+ if (ipv4) args.push('-4')
1886
+ args.push(url)
1887
1887
  const { stdout } = await execFileAsync(
1888
1888
  bin,
1889
- ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}', url],
1889
+ args,
1890
1890
  { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
1891
1891
  )
1892
1892
  const m = String(stdout).match(/\n__HTTP__(\d+)\s*$/u)
@@ -2049,7 +2049,9 @@ async function enrichItemOne(item) {
2049
2049
  detectSkillRepo(item.fullName, branch),
2050
2050
  rawTextWithFallback(item.fullName, branch, '.gitmodules'),
2051
2051
  ])
2052
- if (suiteResult.status === 'fulfilled' && suiteResult.value !== null) {
2052
+ // 套装判定必须过内容校验:代理/CDN 对不存在的 .gitmodules 也可能回 2xx 空 body,
2053
+ // 只判"探测非 null"会把普通插件标成套装置仓库(2026-09-19 事故)。
2054
+ if (suiteResult.status === 'fulfilled' && looksLikeGitmodules(suiteResult.value)) {
2053
2055
  hasSuite = true
2054
2056
  }
2055
2057
  if (pkgResult.status === 'fulfilled') {
@@ -2149,11 +2151,53 @@ function githubRepoInfo(repo) {
2149
2151
  return `${match[1]}/${match[2]}`
2150
2152
  }
2151
2153
 
2154
+ /** 「真的读到了文件内容」判定:null / 空串 / 纯空白都不算。导出供 test-suite-detect.mjs 断言。
2155
+ * 真实事故(2026-09-19,用户反馈装 dsh-whale-widget 报「未找到 .gitmodules」):
2156
+ * 本机加速器/代理(Watt Toolkit 之类,劫持 raw.githubusercontent.com 到 127.0.0.1)
2157
+ * 会对**不存在的文件**回 2xx + 空 body;旧代码只判 `body === null`,
2158
+ * 于是空串被当成"文件存在" → 普通插件被判定成 submodule 套装置仓库 → 安装必然失败。 */
2159
+ export function readBodyOrNull(body) {
2160
+ return typeof body === 'string' && body.trim() !== '' ? body : null
2161
+ }
2162
+
2163
+ /** .gitmodules 内容校验:必须是真 gitmodules(含 [submodule "x"] 段)才算套装仓库。导出供测试。
2164
+ * 只判"探测非 null"会把代理/CDN 的垃圾响应也算成套装;判定权交给内容,不交给状态码。 */
2165
+ export function looksLikeGitmodules(text) {
2166
+ return typeof text === 'string' && /^\s*\[submodule\s+"/mu.test(text)
2167
+ }
2168
+
2169
+ /** raw 抓取外层预算(毫秒)。事件(2026-09-20,另一位用户:Android + proot Ubuntu 容器,域名解析出
2170
+ * IPv6 但容器无 IPv6 路由):4 条通道最快也要 5.4s(curl 直连被 -m 6 掐死、node:https 5435ms、
2171
+ * jsDelivr 5564ms),旧的 5000ms 预算**必然先超时** → 抓取失败被当成"文件不存在",
2172
+ * 报成「仓库没有 package.json」。放宽到 10s:真 404 仍是毫秒级返回,只有全网慢时才多等。 */
2173
+ export const FETCH_BUDGET_MS = 10000
2174
+ /** 仓库元数据(默认分支)探测预算:旧值 3s 在同一环境下同样必输 → branch 恒为 'main',
2175
+ * 默认分支为 dev 的仓库会取错分支。 */
2176
+ export const META_BUDGET_MS = 8000
2177
+ export const FETCH_OK = 'ok'
2178
+ export const FETCH_NOT_FOUND = 'not-found'
2179
+ export const FETCH_UNREACHABLE = 'unreachable'
2180
+
2181
+ /** 竞速组合(纯逻辑,导出供单测):通道结果 + 预算 → { state, body }。
2182
+ * · anyPromise 以 null 成功 = 某通道确定性 404 → not-found
2183
+ * · anyPromise 以字符串成功 = 读到内容 → ok
2184
+ * · anyPromise 全部失败(AggregateError)或超出预算 → unreachable(**不再与 404 混为一谈**) */
2185
+ export function raceFetchOutcome(anyPromise, budgetMs) {
2186
+ const settled = Promise.resolve(anyPromise)
2187
+ .then((body) => (body === null ? { state: FETCH_NOT_FOUND, body: null } : { state: FETCH_OK, body }))
2188
+ .catch(() => ({ state: FETCH_UNREACHABLE, body: null }))
2189
+ return Promise.race([
2190
+ settled,
2191
+ new Promise((resolve) => setTimeout(() => resolve({ state: FETCH_UNREACHABLE, body: null }), budgetMs)),
2192
+ ])
2193
+ }
2194
+
2152
2195
  /** raw 文件读取:https 主通道(含镜像)、gh CLI、curl 直连、curl jsDelivr CDN **四通道并行竞速**。
2153
2196
  * 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。
2154
2197
  * 关键语义:**404 是确定性结果**(文件不存在,探测 .gitmodules/SKILL.md 等时的正常答案),
2155
- * 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。 */
2156
- async function rawTextWithFallback(repo, branch, path) {
2198
+ * 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。
2199
+ * 返回值区分「确定性 404」与「超时/通道全灭」——二者都以"没内容"结束,但用户文案必须不同。 */
2200
+ async function rawTextFetch(repo, branch, path, budgetMs = FETCH_BUDGET_MS) {
2157
2201
  const url = `${GITHUB_RAW}/${repo}/${encodeURIComponent(branch)}/${path}`
2158
2202
  // 404/4xx:确定性"不存在",立即以 null 胜出(不等慢分支)
2159
2203
  const notFound = (promise) => promise
@@ -2161,48 +2205,60 @@ async function rawTextWithFallback(repo, branch, path) {
2161
2205
  if (error instanceof Error && /HTTP 404|HTTP 400|Not Found|not found|非 2xx|HTTP 非 2xx/u.test(error.message)) return null
2162
2206
  throw error
2163
2207
  })
2164
- const https = notFound(githubText(url)
2165
- .then((body) => {
2166
- if (body === null) throw new Error('HTTP 非 2xx')
2167
- return body
2168
- }))
2208
+ // 各通道统一口径:状态 2xx **且** body 非空白,才算读到文件(否则抛"非 2xx" → 归一为 null)
2209
+ const real = (promise) => promise.then((body) => {
2210
+ const text = readBodyOrNull(body)
2211
+ if (text === null) throw new Error('HTTP 非 2xx')
2212
+ return text
2213
+ })
2214
+ const https = notFound(real(githubText(url)))
2169
2215
  const gh = notFound(githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
2170
2216
  .then((data) => {
2171
2217
  if (data && typeof data.content === 'string') return Buffer.from(data.content, 'base64').toString('utf8')
2172
2218
  throw new Error('contents 无内容')
2173
- }))
2174
- const curl = notFound(curlText(url, 6000)
2175
- .then((body) => {
2176
- if (body === null) throw new Error('HTTP 非 2xx')
2177
- return body
2178
- }))
2179
- const cdn = notFound(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 6000)
2180
- .then((body) => {
2181
- if (body === null) throw new Error('HTTP 非 2xx')
2219
+ })
2220
+ .then((text) => {
2221
+ const body = readBodyOrNull(text)
2222
+ if (body === null) throw new Error('contents 无内容')
2182
2223
  return body
2183
2224
  }))
2184
- try {
2185
- // 任一通道成功(含 404→null)立即返回;整体 5s 封顶(jsDelivr 正常 0.7s,
2186
- // 黑洞期快速失败优于等 12s),超时视为不可达(null)。
2187
- return await Promise.race([
2188
- Promise.any([https, gh, curl, cdn]),
2189
- new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
2190
- ])
2191
- } catch {
2192
- return null
2193
- }
2225
+ const curl = notFound(real(curlText(url, 9000, { ipv4: true })))
2226
+ const cdn = notFound(real(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 9000, { ipv4: true })))
2227
+ return await raceFetchOutcome(Promise.any([https, gh, curl, cdn]), budgetMs)
2194
2228
  }
2195
2229
 
2196
- async function fetchRepoPackage(repo, branch) {
2230
+ /** 兼容入口:调用方只关心"有没有内容"(null = 没有)。需要区分超时/404 的场景用 rawTextFetch。 */
2231
+ async function rawTextWithFallback(repo, branch, path) {
2232
+ const { state, body } = await rawTextFetch(repo, branch, path)
2233
+ return state === FETCH_OK ? body : null
2234
+ }
2235
+
2236
+ /** 包探测(带失败原因):reason ∈ ok / not-found / unreachable / invalid。导出供测试。
2237
+ * 事故(2026-09-20,另一位用户:Android + proot Ubuntu,容器无 IPv6 路由):
2238
+ * 抓取超时与真 404 都让上层拿到同一个 null,于是「网络太慢」被报成「仓库没有 package.json」。 */
2239
+ async function fetchRepoPackageEx(repo, branch) {
2240
+ const { state, body } = await rawTextFetch(repo, branch, 'package.json')
2241
+ if (state !== FETCH_OK) return { pkg: null, reason: state }
2197
2242
  try {
2198
- const body = await rawTextWithFallback(repo, branch, 'package.json')
2199
- if (body === null) return null
2200
2243
  const pkg = JSON.parse(body)
2201
- if (pkg == null || typeof pkg !== 'object' || typeof pkg.name !== 'string') return null
2202
- return pkg
2203
- } catch {
2204
- return null
2244
+ if (pkg !== null && typeof pkg === 'object' && typeof pkg.name === 'string') return { pkg, reason: 'ok' }
2245
+ } catch {}
2246
+ return { pkg: null, reason: 'invalid' }
2247
+ }
2248
+
2249
+ async function fetchRepoPackage(repo, branch) {
2250
+ return (await fetchRepoPackageEx(repo, branch)).pkg
2251
+ }
2252
+
2253
+ /** 探测失败时的用户可读文案:「抓取超时/不可达」与「真的没有」必须区分开。导出供测试。 */
2254
+ export function packageProbeErrorText(repo, branch, reason) {
2255
+ if (reason === FETCH_UNREACHABLE) {
2256
+ return `抓取超时/网络不可达:没能读到 ${repo}(${branch} 分支)的 package.json —— 通常是网络到 GitHub 太慢(例如解析出 IPv6 却无 IPv6 路由)。请重试;若持续失败,可先用「仓库落地」克隆到本地目录。`
2257
+ }
2258
+ if (reason === 'invalid') {
2259
+ return `仓库 ${repo} 的 package.json 不是合法的包描述(缺少 name 字段),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
2205
2260
  }
2261
+ return `仓库 ${repo} 没有 package.json(也不是技能仓库),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
2206
2262
  }
2207
2263
 
2208
2264
  /**
@@ -2211,7 +2267,7 @@ async function fetchRepoPackage(repo, branch) {
2211
2267
  * (曾导致入口链接丢失、DSH 启动崩溃)。registry 走国内镜像。
2212
2268
  */
2213
2269
  async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmirror.com', timeout = 90000, signal = null) {
2214
- const args = ['pnpm', 'add', spec, '--registry', registry]
2270
+ const args = ['add', spec, '--registry', registry]
2215
2271
  const opts = {
2216
2272
  cwd: profileDir,
2217
2273
  timeout,
@@ -2225,19 +2281,9 @@ async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmir
2225
2281
  GCM_INTERACTIVE: 'never',
2226
2282
  },
2227
2283
  }
2228
- if (process.platform === 'win32') {
2229
- // .cmd 批处理不能直接 execFile(EINVAL):优先 node 直跑 corepack.js
2230
- const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
2231
- if (existsSync(corepackJs)) {
2232
- await execFileAsync(process.execPath, [corepackJs, ...args], opts)
2233
- return
2234
- }
2235
- // 兜底:经 cmd.exe 运行 corepack
2236
- const cmd = `${JSON.stringify('corepack')} ${args.map((arg) => JSON.stringify(arg)).join(' ')}`
2237
- await execFileAsync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', cmd], opts)
2238
- return
2239
- }
2240
- await execFileAsync('corepack', args, opts)
2284
+ // 跨平台定位 corepack/pnpm(Windows 布局 / Linux npm 全局布局 / PATH 兜底);
2285
+ // 旧代码只认 Windows 布局,Linux 上会生成 MODULE_NOT_FOUND 的命令(2026-09-20 事故)
2286
+ await runPnpmWithFallback(args, { execOpts: opts })
2241
2287
  }
2242
2288
 
2243
2289
  /**
@@ -2783,6 +2829,64 @@ function packageEntryExists(dir, pkg) {
2783
2829
  return candidates.some((c) => existsSync(join(dir, c)))
2784
2830
  }
2785
2831
 
2832
+ /** git 可执行名(跨平台)。事故(2026-09-20,另一位用户:Android + proot Ubuntu):
2833
+ * 「仓库落地」硬编码 `git.exe` → 非 Windows 环境 spawn git.exe ENOENT,克隆必然失败。导出供测试。 */
2834
+ export function gitBin() {
2835
+ return process.platform === 'win32' ? 'git.exe' : 'git'
2836
+ }
2837
+
2838
+ /** pnpm 执行方式定位(跨平台,纯函数便于单测 → 按优先级返回列表,逐个尝试)。导出供测试。
2839
+ * 事故(2026-09-20,同一位用户,node v24 + Linux):只按 Windows 布局找 corepack.js
2840
+ * (`<node bin>/node_modules/corepack/dist/corepack.js`),而 Linux 的 npm 全局布局在
2841
+ * `<prefix>/lib/node_modules/corepack/...` → AI 赋能的 install-npm 生成
2842
+ * `node /usr/local/bin/node_modules/corepack/dist/corepack.js pnpm add …` → MODULE_NOT_FOUND。 */
2843
+ export function resolvePnpmRunners({ platform = process.platform, execPath = process.execPath, comspec = process.env.ComSpec ?? 'cmd.exe', exists = existsSync } = {}) {
2844
+ const binDir = dirname(execPath)
2845
+ const corepackCandidates = [
2846
+ join(binDir, 'node_modules', 'corepack', 'dist', 'corepack.js'), // Windows 官方安装器 / nvm-windows
2847
+ join(binDir, '..', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // Linux/macOS npm 全局
2848
+ join(binDir, '..', 'libexec', 'lib', 'node_modules', 'corepack', 'dist', 'corepack.js'), // brew / 自编译布局
2849
+ ]
2850
+ const runners = []
2851
+ for (const js of corepackCandidates) {
2852
+ if (exists(js)) {
2853
+ runners.push({ kind: 'node-corepack', note: `node ${js} pnpm`, run: (args) => ({ bin: execPath, argv: [js, 'pnpm', ...args] }) })
2854
+ }
2855
+ }
2856
+ if (platform === 'win32') {
2857
+ // .cmd 批处理不能直接 execFile(EINVAL)→ 经 cmd.exe 调用(整条命令作为一个参数)
2858
+ runners.push({
2859
+ kind: 'cmd-corepack',
2860
+ note: 'cmd /c corepack pnpm',
2861
+ run: (args) => ({ bin: comspec, argv: ['/d', '/s', '/c', ['corepack', 'pnpm', ...args].map((a) => JSON.stringify(a)).join(' ')] }),
2862
+ })
2863
+ } else {
2864
+ runners.push({ kind: 'corepack', note: 'corepack pnpm', run: (args) => ({ bin: 'corepack', argv: ['pnpm', ...args] }) })
2865
+ runners.push({ kind: 'pnpm', note: 'pnpm', run: (args) => ({ bin: 'pnpm', argv: args }) })
2866
+ }
2867
+ return runners
2868
+ }
2869
+
2870
+ /** 依次尝试各执行方式;只有"执行方式本身不可用"(ENOENT / MODULE_NOT_FOUND)才换下一个,
2871
+ * 真正的安装失败(网络、依赖冲突等)立即抛出,并附上已尝试的清单便于排查。 */
2872
+ async function runPnpmWithFallback(args, { execOpts = {}, runners = resolvePnpmRunners() } = {}) {
2873
+ let lastError = null
2874
+ for (const runner of runners) {
2875
+ const { bin, argv } = runner.run(args)
2876
+ try {
2877
+ // eslint-disable-next-line no-await-in-loop
2878
+ await execFileAsync(bin, argv, execOpts)
2879
+ return { runner }
2880
+ } catch (error) {
2881
+ lastError = error
2882
+ const message = String(error?.message ?? '')
2883
+ if (!/ENOENT|Cannot find module/u.test(message)) throw error
2884
+ }
2885
+ }
2886
+ const tried = runners.map((r) => r.note).join(' → ')
2887
+ throw new Error(`${lastError?.message ?? 'pnpm 执行失败'}(已尝试:${tried})`)
2888
+ }
2889
+
2786
2890
  /** git 非交互环境:禁止任何登录/凭据窗口弹出(私有仓库或不可达源直接失败,不做交互式重试)。 */
2787
2891
  function gitEnv() {
2788
2892
  return {
@@ -2865,6 +2969,24 @@ async function installBundleFromRelease(subRepo, pkgName, target) {
2865
2969
  }
2866
2970
  }
2867
2971
 
2972
+ /** 套装探测(唯一入口):main → master,且**必须内容像 .gitmodules**(含 [submodule "x"] 段)才算套装。
2973
+ * 判据是内容、不是"探测非 null":2026-09-19 用户反馈装 dsh-whale-widget 被判成 submodule 套装置仓库、
2974
+ * clone 后报「未找到 .gitmodules」——根因就是代理/CDN 对**不存在的文件**回 2xx(空 body 也算"读到")。 */
2975
+ async function probeGitmodules(repo) {
2976
+ const main = await rawTextWithFallback(repo, 'main', '.gitmodules')
2977
+ if (looksLikeGitmodules(main)) return main
2978
+ const master = await rawTextWithFallback(repo, 'master', '.gitmodules')
2979
+ return looksLikeGitmodules(master) ? master : null
2980
+ }
2981
+
2982
+ /** 安装类型决策(纯函数,导出供 test-suite-detect.mjs 断言):`.gitmodules` 的**内容**说了算——
2983
+ * 显式 suite 请求若探测内容不像 .gitmodules 也回落普通插件安装(前端标记可能来自 24h 缓存的误判,
2984
+ * 不能当判据);技能请求不受影响。 */
2985
+ export function resolveInstallKind(requestKind, probeText) {
2986
+ if (requestKind === 'skill') return 'skill'
2987
+ return looksLikeGitmodules(probeText) ? 'suite' : 'plugin'
2988
+ }
2989
+
2868
2990
  /** 套装安装(submodule 聚合仓库):照仓库 install.ps1/README 语义——
2869
2991
  * clone 套装 → 手动镜像拉取子模块 → 按类型装配(bundle 插件含 Release tgz 兜底 / 普通插件 / 技能 / agent 预设)。
2870
2992
  * 不执行第三方脚本本体(安全护栏:脚本型只读语义不运行)。 */
@@ -2876,7 +2998,12 @@ async function runSuiteInstallJob(job, ctx) {
2876
2998
  mkdirSync(tmpDir, { recursive: true })
2877
2999
  await gitCloneRepo(job.repo, tmpDir, job.source)
2878
3000
  const subs = readGitmodules(tmpDir)
2879
- if (subs.length === 0) throw new Error('未找到 .gitmodules(不是 submodule 套装仓库)')
3001
+ if (subs.length === 0) {
3002
+ // 探测与仓库实际内容不符(假阳性 / 仓库已重构):**不报失败**,把决定权交回调用方
3003
+ // 回落普通插件安装(npm → Release → git 规格),用户不该因为一次误判装不上插件。
3004
+ job.stage = 'detecting'
3005
+ return { notASuite: true }
3006
+ }
2880
3007
  job.stage = 'detecting'
2881
3008
  const patchPath = findPatchPath(ctx)
2882
3009
  const profileDir = dirname(patchPath)
@@ -3185,7 +3312,9 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
3185
3312
  job.error = `本地 AI 修复通道没有可用的子代理提供方:${String(error)};请手动执行:dsh plugin --profile web add <包名>`
3186
3313
  return
3187
3314
  }
3188
- const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
3315
+ // 跨平台提示:corepack 的位置随安装方式/系统而异(Windows 官方安装器在 <node bin>/node_modules,
3316
+ // Linux 的 npm 全局在 <prefix>/lib/node_modules),所以给子代理的提示不再写死路径
3317
+ const corepackRunner = resolvePnpmRunners()[0]
3189
3318
  const prompt = [
3190
3319
  '你是 DeepSeek Harness 的插件安装专家。用户通过插件管理面板安装插件失败,现在由你主动接管安装(不是被动"修复",而是像人工一样把它装好)。',
3191
3320
  `目标仓库:${job.repo}`,
@@ -3194,7 +3323,7 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
3194
3323
  `此前确定性通道的错误:${lastError ?? '未知'}`,
3195
3324
  '接管步骤:',
3196
3325
  `1) 先弄清楚装什么:若根包是 private 的聚合仓库(monorepo),用 gh api repos/${job.repo}/git/trees/main?recursive=1 或读 packages 目录下的 package.json 找出子包名;优先选聚合包(名字含 all,如 dsh-web-ui-all,一个包装全部);`,
3197
- `2) 用 corepack pnpm 安装(Windows 执行 node ${JSON.stringify(corepackJs)} pnpm add <包名> --registry https://registry.npmmirror.com,工作目录 ${profileDir});镜像 404 时改 --registry https://registry.npmjs.org;都不行再用 git 通道;`,
3326
+ `2) 用 corepack/pnpm 安装(本机可用的执行方式:${corepackRunner?.note ?? 'corepack pnpm 或 pnpm'},即在该方式后接 add <包名> --registry https://registry.npmmirror.com,工作目录 ${profileDir};Windows 上是 node <corepack.js> pnpm,Linux 上通常是 corepack pnpm 或直接 pnpm);镜像 404 时改 --registry https://registry.npmjs.org;都不行再用 git 通道;`,
3198
3327
  '3) **node 网络黑洞判定**:pnpm 长时间无下载进度(Progress 停在 downloaded 0)、或报 socket hang up / EPERM / TIMEOUT、或 node 脚本连 127.0.0.1 都超时,说明本机拦截 node 进程网络——此时 curl 与系统 git 通常仍可用,立即改用 curl 手动安装:先 `curl -s https://registry.npmmirror.com/<包名>` 取最新版本号,再 `curl -sL -o <临时目录>/pkg.tgz https://registry.npmmirror.com/<包名>/-/<包名>-<版本>.tgz`,解压 tar -xzf 后把 package 内容放入 profile/node_modules/<包名>(先备份旧目录);包有 dependencies 时用同样方式逐个 curl 拉取补齐到 node_modules;零依赖包(如 dsh-skin)一条龙即可完成;',
3199
3328
  `4) 安装成功后按官方 dsh plugin add 规则落配置:包声明 dsh.bundle 时把包名追加进 profile 目录 package.json 的 dsh.profile.bundles 数组;普通插件在 cordis.patch.yml 追加 insert 行(id 由包名去 scope、非字母数字转连字符生成,name 填包名);`,
3200
3329
  '5) 装完自查:确认包已出现在 node_modules、配置已写入;必要时用 gh api 核对仓库信息;',
@@ -3773,10 +3902,13 @@ async function runAiStep(job, step, ctx, profileDir, signal) {
3773
3902
  const pkg = String(step.package ?? '')
3774
3903
  if (!/^(@[A-Za-z0-9_\-\.]+\/)?[A-Za-z0-9_\-\.]+(@[A-Za-z0-9_\-\.]+)?$/u.test(pkg)) throw new Error(`包名不合法:${pkg}`)
3775
3904
  const dir = resolvePlaceholders(String(step.directory ?? profile), { profileDir: profile })
3776
- const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
3777
- const params = [corepackJs, 'pnpm', 'add', pkg, '--dir', dir, '--registry', 'https://registry.npmmirror.com']
3778
- jobLog(job, ` 运行: node ${params.join(' ')}`)
3779
- await execFileAsync(process.execPath, params, { timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, signal })
3905
+ // 跨平台:不再拼 `node <node bin>/node_modules/corepack/dist/corepack.js`(Linux 上必 MODULE_NOT_FOUND,
3906
+ // 2026-09-20 另一位用户的 install-npm 失败现场就是这一行),改为按优先级尝试可用的执行方式
3907
+ const args = ['add', pkg, '--dir', dir, '--registry', 'https://registry.npmmirror.com']
3908
+ const { runner } = await runPnpmWithFallback(args, {
3909
+ execOpts: { timeout: 900000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, signal },
3910
+ })
3911
+ jobLog(job, ` 运行: ${runner.note} ${args.join(' ')}`)
3780
3912
  return
3781
3913
  }
3782
3914
  case 'write-file': {
@@ -4499,25 +4631,30 @@ async function runInstallJob(job, ctx) {
4499
4631
  let candidates = [job.packageName].filter((name) => typeof name === 'string' && name !== '')
4500
4632
  let subpackageMode = false
4501
4633
  if (candidates.length === 0) {
4502
- // 套装兜底:submodule 聚合仓库(根 .gitmodules 存在)→ 自动转套装安装,
4503
- // 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich)
4504
- let suiteProbe = await rawTextWithFallback(job.repo, 'main', '.gitmodules')
4505
- if (suiteProbe === null) suiteProbe = await rawTextWithFallback(job.repo, 'master', '.gitmodules')
4506
- if (suiteProbe !== null) {
4634
+ // 套装兜底:submodule 聚合仓库(根 .gitmodules 内容校验通过)→ 自动转套装安装,
4635
+ // 不依赖前端标记(搜索结果 enrich 是异步的、索引浏览条目无 enrich)。
4636
+ // 判据是**内容**(resolveInstallKind),不是"探测非 null":后者会把代理/CDN 对不存在文件回的
4637
+ // 2xx 空 body、垃圾页当成套装(2026-09-19 用户反馈的「未找到 .gitmodules」事故根因)。
4638
+ if (resolveInstallKind(job.kind, await probeGitmodules(job.repo)) === 'suite') {
4507
4639
  job.kind = 'suite'
4508
- await runSuiteInstallJob(job, ctx)
4509
- return
4510
- }
4511
- // 兜底:宿主端自行拉取仓库元数据(githubJson 与 curl 竞速 + 3s 超时降级,黑洞期不卡 40s)
4640
+ const suiteResult = await runSuiteInstallJob(job, ctx)
4641
+ // clone 后才发现没有 .gitmodules(探测假阳性 / 仓库已重构)→ 回落普通安装,不给用户一个失败
4642
+ if (suiteResult?.notASuite !== true) return
4643
+ job.kind = 'plugin'
4644
+ job.suiteNote = '探测到的 .gitmodules 与仓库实际内容不符(不是 submodule 套装仓库),已自动回落普通插件安装'
4645
+ }
4646
+ // 兜底:宿主端自行拉取仓库元数据(githubJson 与 curl 竞速 + 8s 超时降级,黑洞期不卡 40s)。
4647
+ // 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s,3s 预算必输 → branch 恒为 main,
4648
+ // 默认分支为 dev 的仓库会取错分支(2026-09-20 另一位用户实测)。
4512
4649
  const meta = await Promise.race([
4513
4650
  Promise.any([
4514
4651
  githubJson(`${GITHUB_API}/repos/${job.repo}`),
4515
- curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000),
4652
+ curlJson(`${GITHUB_API}/repos/${job.repo}`, 12000, {}, { ipv4: true }),
4516
4653
  ]),
4517
- new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
4654
+ new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
4518
4655
  ]).catch(() => null)
4519
4656
  const branch = meta?.default_branch ?? 'main'
4520
- const pkg = await fetchRepoPackage(job.repo, branch)
4657
+ const { pkg, reason } = await fetchRepoPackageEx(job.repo, branch)
4521
4658
  if (pkg === null) {
4522
4659
  // 无 package.json:先探测是否技能仓库(含 SKILL.md)→ 自动转技能安装;
4523
4660
  // 否则标记 hint=repo-land,前端给出「仓库落地」一键入口(克隆到本地目录)。
@@ -4529,7 +4666,9 @@ async function runInstallJob(job, ctx) {
4529
4666
  }
4530
4667
  job.status = 'failed'
4531
4668
  job.hint = 'repo-land'
4532
- job.error = `仓库 ${job.repo} 没有 package.json(也不是技能仓库),无法作为插件安装——可改用「仓库落地」克隆到本地目录。`
4669
+ // 文案区分「抓取超时/不可达」与「真的没有」——旧代码两者共用一个出口,报错永远说"文件不存在"
4670
+ job.error = packageProbeErrorText(job.repo, branch, reason)
4671
+ job.probeReason = reason
4533
4672
  return
4534
4673
  }
4535
4674
  if (pkg.private === true) {
@@ -5492,24 +5631,26 @@ async function handle(ctx, req, res) {
5492
5631
  if (pathname === `${ROUTE_PREFIX}/repo`) {
5493
5632
  const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
5494
5633
  const auth = readGithubAuth()
5495
- // meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,3 秒超时即降级——
5634
+ // meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,8 秒超时即降级——
5496
5635
  // Promise.any 全失败时要等最慢分支(黑洞期 https 41.5s),加 race 超时避免拖累整体。
5636
+ // 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s(2026-09-20 实测),3s 必输 → branch 取错。
5497
5637
  let meta = null
5498
5638
  try {
5499
5639
  meta = await Promise.race([
5500
5640
  Promise.any([
5501
5641
  githubJson(`${GITHUB_API}/repos/${repo}`, req.signal, auth.token),
5502
- curlJson(`${GITHUB_API}/repos/${repo}`, 12000),
5642
+ curlJson(`${GITHUB_API}/repos/${repo}`, 12000, {}, { ipv4: true }),
5503
5643
  ]),
5504
- new Promise((resolve) => setTimeout(() => resolve(null), 3000)),
5644
+ new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
5505
5645
  ])
5506
5646
  } catch {}
5507
5647
  const branch = meta?.default_branch ?? 'main'
5508
5648
  const pkg = await fetchRepoPackage(repo, branch)
5509
5649
  const skill = await detectSkillRepo(repo, branch)
5510
5650
  const skillMeta = skill.hasSkill ? await fetchSkillMeta(repo, branch, skill.skillDir) : null
5511
- // 套装识别:根 .gitmodules 存在(submodule 聚合仓库)
5512
- const hasSuite = (await rawTextWithFallback(repo, branch, '.gitmodules')) !== null
5651
+ // 套装识别:根 .gitmodules 存在**且内容真的是 gitmodules**(只判非 null 会被代理/CDN 对不存在
5652
+ // 文件回的 2xx 空 body 骗到,把普通插件标成套装——2026-09-19 用户反馈事故)
5653
+ const hasSuite = looksLikeGitmodules(await rawTextWithFallback(repo, branch, '.gitmodules'))
5513
5654
  // 官方安装方式(详情面板展示 + 一键复制,供用户手动安装):
5514
5655
  // 套装 → 仓库 install.ps1/README 的官方步骤;普通/聚合 → dsh plugin add 官方命令
5515
5656
  let installCommand = null
@@ -7094,9 +7235,9 @@ async function handle(ctx, req, res) {
7094
7235
  sendError(res, 400, 'deepseek-harness 是 DSH 框架本体,不是插件——无需安装(升级请用官方 dsh 升级方式)')
7095
7236
  return
7096
7237
  }
7097
- const kind = body.kind === 'skill' ? 'skill' : body.kind === 'suite' ? 'suite' : 'plugin'
7098
- if ((kind === 'skill' || kind === 'suite') && repo === '') {
7099
- sendError(res, 400, `${kind === 'skill' ? '技能' : '套装'}安装必须提供仓库(owner/name)`)
7238
+ const requestedKind = body.kind === 'skill' ? 'skill' : body.kind === 'suite' ? 'suite' : 'plugin'
7239
+ if ((requestedKind === 'skill' || requestedKind === 'suite') && repo === '') {
7240
+ sendError(res, 400, `${requestedKind === 'skill' ? '技能' : '套装'}安装必须提供仓库(owner/name)`)
7100
7241
  return
7101
7242
  }
7102
7243
  const source = body.source === 'gitee' ? body.source : 'github'
@@ -7113,6 +7254,14 @@ async function handle(ctx, req, res) {
7113
7254
  sendError(res, 409, '该插件正在安装/更新中,请等待当前任务完成后再试')
7114
7255
  return
7115
7256
  }
7257
+ // 套装请求必须复检内容(2026-09-19 事故):前端标记可能来自 24h enrich 缓存的误判,
7258
+ // 探测内容不像 .gitmodules 就按普通插件安装——别把用户送进注定失败的套装通道。
7259
+ let kind = requestedKind
7260
+ let suiteNote = null
7261
+ if (requestedKind === 'suite') {
7262
+ kind = resolveInstallKind('suite', await probeGitmodules(repo))
7263
+ if (kind !== 'suite') suiteNote = '探测未发现有效的 .gitmodules(不是 submodule 套装仓库),已按普通插件安装'
7264
+ }
7116
7265
  const job = {
7117
7266
  id: `job-${installJobSeq += 1}`,
7118
7267
  repo,
@@ -7132,10 +7281,20 @@ async function handle(ctx, req, res) {
7132
7281
  update: body.update === true,
7133
7282
  kind,
7134
7283
  }
7284
+ if (suiteNote !== null) job.suiteNote = suiteNote
7135
7285
  installJobs.set(job.id, job)
7286
+ // 套装通道兜底:探测说有、clone 下来却没有 .gitmodules(假阳性 / 仓库已重构)时
7287
+ // 回落普通插件安装,而不是给用户一个「未找到 .gitmodules」的失败。
7288
+ const runSuiteThenFallback = async () => {
7289
+ const result = await runSuiteInstallJob(job, ctx)
7290
+ if (result?.notASuite !== true) return
7291
+ job.kind = 'plugin'
7292
+ job.suiteNote = '仓库实际内容与探测不符(没有 .gitmodules),已自动回落普通插件安装'
7293
+ await runInstallJob(job, ctx)
7294
+ }
7136
7295
  // 后台执行:请求立即返回,安装不受客户端断开/离开面板影响
7137
- void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ? runSuiteInstallJob(job, ctx) : runInstallJob(job, ctx))
7138
- sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind })
7296
+ void (kind === 'skill' ? runSkillInstallJob(job) : kind === 'suite' ? runSuiteThenFallback() : runInstallJob(job, ctx))
7297
+ sendJson(res, 200, { ok: true, jobId: job.id, status: 'installing', kind, ...(suiteNote !== null ? { suiteNote } : {}) })
7139
7298
  return
7140
7299
  }
7141
7300
 
@@ -7383,7 +7542,7 @@ async function handle(ctx, req, res) {
7383
7542
  let usedUrl = ''
7384
7543
  for (const url of urls) {
7385
7544
  try {
7386
- await execFileAsync('git.exe', ['clone', '--depth', '1', url, target], { cwd: root, timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, env: gitEnv() })
7545
+ await execFileAsync(gitBin(), ['clone', '--depth', '1', url, target], { cwd: root, timeout: 600000, windowsHide: true, maxBuffer: 4 * 1024 * 1024, env: gitEnv() })
7387
7546
  cloned = true
7388
7547
  usedUrl = url
7389
7548
  break
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.3.47",
3
+ "version": "0.3.48",
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",