dsh-plugin 1.4.9 → 1.4.11

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.
Files changed (33) hide show
  1. package/client/client.js +355 -365
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +45 -19
  4. package/lib/services/install/npm-check.js +4 -5
  5. package/lib/services/install/task-queue.js +51 -13
  6. package/lib/services/probe.js +20 -7
  7. package/lib/services/profile/profile.js +1 -1
  8. package/lib/types/http/routes.d.ts +3 -0
  9. package/lib/types/services/install/install-types.d.ts +2 -0
  10. package/lib/types/services/install/npm-check.d.ts +1 -1
  11. package/lib/types/services/install/task-queue.d.ts +7 -0
  12. package/lib/types/services/probe.d.ts +2 -2
  13. package/lib/types/services/profile/profile.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/client/components/PluginHubSection.tsx +1 -1
  16. package/src/client/components/catalog/PluginCard.tsx +2 -2
  17. package/src/client/components/modals/NotificationsModal.tsx +13 -9
  18. package/src/client/components/modals/modals.tsx +10 -6
  19. package/src/client/data/catalog.ts +1 -1
  20. package/src/client/hooks/useCatalog.ts +2 -2
  21. package/src/client/locales.ts +7 -0
  22. package/src/client/logic/constants.ts +1 -1
  23. package/src/client/logic/failures.ts +76 -95
  24. package/src/client/logic/urls.ts +5 -2
  25. package/src/client/styles/InstalledView.module.css +1 -1
  26. package/src/client/styles/Modal.module.css +2 -2
  27. package/src/client/types.ts +1 -1
  28. package/src/server/http/routes.ts +41 -19
  29. package/src/server/services/install/install-types.ts +2 -0
  30. package/src/server/services/install/npm-check.ts +4 -5
  31. package/src/server/services/install/task-queue.ts +49 -12
  32. package/src/server/services/probe.ts +20 -6
  33. package/src/server/services/profile/profile.ts +1 -1
@@ -20,7 +20,7 @@ export const HUB_REPO = 'dshplugin/dsh-plugin-hub'
20
20
  /**
21
21
  * Hub 自我更新版本控制(接口中心 Pages:api.dsh-plugin.org,静态 JSON 发布)。
22
22
  * 发新版 = 在 api-center 的 releases/ 写发版记录 + 重新部署,hub.json 随之更新,
23
- * 所有已装用户的「可更新」徽标即可见,不再依赖主站目录数据管道。
23
+ * 所有已装用户的「可更新」徽标即可见,不经过主站目录数据管道。
24
24
  * 响应:{ version: string | null, publishedAt: string | null, notes: string | {zh,en} | null }
25
25
  */
26
26
  export const HUB_UPDATE_URL = 'https://api.dsh-plugin.org/hub.json'
@@ -151,86 +151,73 @@ export function removeNotification(id: number): NotificationRecord[] {
151
151
  return next
152
152
  }
153
153
 
154
- export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
154
+ export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
155
155
 
156
156
  /**
157
- * 失败归类,七态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
158
- * 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
159
- * 一律引导提 Issue;唯一的例外是本机环境问题(npm 版本过低 / 找不到 dsh / 找不到 pnpm / 网络不通):
160
- * - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
161
- * 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
162
- * 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
157
+ * 失败归类:把安装输出归到具体成因,供弹窗文案、是否引导提 Issue 与 issue 预填原因使用。
158
+ * 分界是「本机环境问题」(装任何插件都会同样失败 → 不引导提 Issue)与
159
+ * 「插件分发/依赖问题」(只影响这个插件 → 引导去仓库提 Issue);各分支的判定顺序有依赖,
160
+ * 原因见 classifyFailure 内注释。
161
+ *
162
+ * 本机环境问题:
163
+ * - npmTooOld:本机 npm 版本过低或自身缺陷 —— npm arborist 解 peer 依赖时抛 `edgesOut`
164
+ * (build-ideal-tree.js 内部报错),或服务端已核实版本低于阈值并打了 `[npm-too-low]` 标记
165
+ * → 引导升级 npm
163
166
  * - dshMissing:安装器 spawn 的 `dsh` 命令找不到(Windows cmd「不是内部或外部命令」/ POSIX
164
- * 「command not found」/ spawn ENOENT)—— 是本机 DSH 未正确安装或不在 PATH,不是插件问题
165
- * → 提示检查 PATH/重装 DSH,不引导提 Issue
167
+ * 「command not found」/ spawn ENOENT)→ 提示检查 PATH / 重装 DSH
166
168
  * - gitMissing:安装器调用 `git` 时找不到可执行文件(Windows cmd「'git' is not recognized」/
167
- * POSIX「git: command not found」/ spawn ENOENT)—— 是本机 Git 未安装或不在 PATH,不是插件
168
- * 问题。pnpm 会把缺失 git 报成 `ERR_PNPM_GIT_RESOLVE_FAILED`(git ls-remote failed),若只看
169
- * 错误码会误归插件侧失败;且 dshMissing 的通用「not recognized」模式会先把它吞成 dsh 缺失,
170
- * 所以必须在 dshMissing 之前判断(dsh-plugin-hub#21:Win 下装 git 源插件,
171
- * `'git' is not recognized` 被误归仓库问题引导去提 Issue)→ 提示安装 Git / 加入 PATH,不引导提 Issue
172
- * - pnpmMissing:dsh 存在但调用的 `pnpm` 找不到(dsh 报 `pnpm not found on PATH`/POSIX
173
- * 「pnpm: command not found」/ spawn ENOENT)—— 本机缺 pnpm(dsh 用 pnpm 管理 profile 插件),
174
- * 不是插件问题(dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
175
- * → 提示安装/开启 pnpm,不引导提 Issue
176
- * - npmMissing:全局 npm 安装通道(`npm install -g ...`)spawn 的 `npm` 命令找不到
177
- * (Windows cmd「'npm' is not recognized」/ POSIX「npm: command not found」/ spawn ENOENT)——
178
- * 本机 npm 未安装或不在 PATH,不是插件问题。dshMissing 的通用「not recognized」模式会把
179
- * `'npm' is not recognized` 吞成 dsh 缺失,所以必须在 dshMissing 之前判断
180
- * → 提示安装 npm(Node.js 自带)/加入 PATH,不引导提 Issue
181
- * - pnpmStore:pnpm 存在但报 store / virtual store 位置不匹配(`ERR_PNPM_UNEXPECTED_STORE` /
182
- * `ERR_PNPM_UNEXPECTED_VIRTUAL_STORE` / `Unexpected store location`)—— profile 目录的 node_modules
183
- * 是用不同大版本的 pnpm 生成的(或 profile 目录被复制/移动、virtual-store-dir 配置变化),
184
- * 当前 pnpm 不认,任何插件装进该 profile 都会失败;不是插件问题(dsh-plugin-hub#14:macOS 下
185
- * `ERR_PNPM_UNEXPECTED_STORE` 被误归插件侧失败;dsh-plugin-hub#30:`ERR_PNPM_UNEXPECTED_VIRTUAL_STORE`
186
- * 漏判被误归插件侧失败)→ 提示清理 profile 依赖目录重建,不引导提 Issue
187
- * - pnpmWorkspace:pnpm 在 workspace 根目录下拒绝安装(`ERR_PNPM_ADDING_TO_ROOT` —— 宿主在
188
- * profile 目录里调用 `pnpm add` 时未声明在 workspace 根操作)。profile 目录含
189
- * pnpm-workspace.yaml 即被 pnpm 视为 workspace 根,缺 `-w`/`--workspace-root` 就整条命令
190
- * 被拒,任何插件都装不上,不是插件问题(dsh-plugin-hub#40)→ 提示在 profile 的 .npmrc 里
191
- * 加 `ignore-workspace-root-check=true` 或升级宿主,不引导提 Issue
169
+ * POSIX「git: command not found」/ spawn ENOENT)→ 提示安装 Git / 加入 PATH
170
+ * - pnpmMissing:dsh 存在但调用的 `pnpm` 找不到(dsh 报 `pnpm not found on PATH` / POSIX
171
+ * 「pnpm: command not found」/ spawn ENOENT)—— dsh 用 pnpm 管理 profile 插件
172
+ * → 提示安装 / 开启 pnpm
173
+ * - npmMissing:全局 npm 安装通道(`npm install -g …`)spawn 的 `npm` 命令找不到,形态同上
174
+ * → 提示安装 npm(Node.js 自带)/ 加入 PATH
175
+ * - pnpmStore:pnpm 报 store / virtual store 位置不匹配(`ERR_PNPM_UNEXPECTED_STORE` /
176
+ * `ERR_PNPM_UNEXPECTED_VIRTUAL_STORE` / `Unexpected store location`)—— profile 目录里的依赖
177
+ * 是另一个大版本的 pnpm 生成的(或 profile 目录被复制/移动、virtual-store-dir 配置变化),
178
+ * 当前 pnpm 出于安全不认 → 提示清理 profile 依赖目录后用当前 pnpm 重建
179
+ * - pnpmWorkspace:pnpm 拒绝在 workspace 根目录下安装(`ERR_PNPM_ADDING_TO_ROOT`)—— profile
180
+ * 目录含 pnpm-workspace.yaml 即被 pnpm 视为 workspace 根,宿主调 `pnpm add` 时未声明在根
181
+ * 操作(缺 `-w`/`--workspace-root`),整条命令被拒 → 提示在 profile 的 .npmrc 里加
182
+ * `ignore-workspace-root-check=true` 或升级宿主
183
+ * - originRejected:请求的来源没通过本地 hub 服务的校验(服务端 403,正文是裸的
184
+ * `untrusted origin`)—— 请求在进入安装流程前就被拒,与 pnpm 无关;常见于用非 localhost 的
185
+ * 地址(如局域网 IP)打开市场页面,或宿主页面的 origin 未被识别 → 提示从本机地址打开市场后重试
192
186
  * - pnpmPolicy:pnpm 11 的供应链安全策略拒绝安装(`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` /
193
- * `Minimum release age` —— 锁文件里包的发布时间还不满 24 小时被拒;`untrusted origin` —— 依赖来源未被
194
- * 本机 pnpm 信任)。拦的是「刚发布的新包」与「未被信任的来源」,装任何新插件都会撞墙,不是
195
- * 插件问题(dsh-plugin-hub#15/#16:用户装官方 dsh-plugin 也被这两类策略拦下并误归插件侧失败)
196
- * → 提示按子场景给解法(发布未满 24 小时 → `minimumReleaseAge: 0` 豁免或等满 24 小时;untrusted
197
- * origin → 删除 profile 的 node_modules + pnpm-lock.yaml 清掉不受信任来源后重装),不引导提 Issue
187
+ * `Minimum release age` —— 锁文件里包的发布时间还不满 24 小时被拒;`untrusted origin` ——
188
+ * 依赖来源未被本机 pnpm 信任)→ 提示按子场景给解法(发布未满 24 小时 → `minimumReleaseAge: 0`
189
+ * 豁免或等满 24 小时;untrusted origin → 删除 profile 的 node_modules + pnpm-lock.yaml 清掉
190
+ * 不受信任来源后重装)
198
191
  * - pnpmUnusedPatch:profile 里留着指向旧版本 dsh-plugin 的 patch 声明,本次安装解析到的版本
199
192
  * 已经不是它(`ERR_PNPM_UNUSED_PATCH` / `The following patches were not used: dsh-plugin@1.4.2`)
200
- * —— pnpm 发现补丁没被用上即中止整次安装,任何插件都装不进来,不是插件问题
201
- * (dsh-plugin-hub#48:用户升级 dsh-plugin 后旧 patch 条目失配)→ 提示删掉该条目后重试,不引导提 Issue
193
+ * —— pnpm 发现补丁没被用上即中止整次安装 → 提示删掉该条目后重试
202
194
  * - fileLocked:pnpm 无法替换 profile 里被其他进程占用的文件(Windows `os error 32`
203
- * 「另一个程序正在使用此文件」/ `EBUSY` / `resource busy or locked`)—— 通常是宿主进程或杀毒软件
204
- * 实时扫描持有句柄,不是插件问题(dsh-plugin-hub#47)→ 提示完全退出宿主后重试,不引导提 Issue
195
+ * 「另一个程序正在使用此文件」/ `EBUSY` / `resource busy or locked`)—— 通常是宿主进程或
196
+ * 杀毒软件实时扫描持有句柄 → 提示完全退出宿主后重试
205
197
  * - accessDenied:pnpm 在 profile 里替换文件时被系统拒绝写入(Windows `os error 5`
206
198
  * 「拒绝访问」= ERROR_ACCESS_DENIED / Node 的 `EPERM` `EACCES` / `Access is denied`)——
207
- * 同属本机文件系统层面的写入受阻:文件/目录被其他进程占用、只读属性、目录 ACL 受限,
208
- * 或杀毒软件实时防护拦截写入(`os error 5` 与 `os error 32` 语义不同:前者是「拒绝访问」,
209
- * 后者是「文件正被占用」,但用户侧处置一致)。任何插件装进该目录都会同样失败,不是插件问题
210
- * (dsh-plugin-hub#50:Win 下 `swap: 拒绝访问。 (os error 5)` 漏判,被误报成插件侧安装失败并
211
- * 自动提了 issue)→ 提示退出宿主/关杀软后重试,仍失败则检查只读属性并以管理员身份运行,
212
- * 不引导提 Issue
199
+ * 文件/目录被其他进程占用、只读属性、目录 ACL 受限,或杀毒软件实时防护拦截写入
200
+ * → 提示退出宿主 / 关杀软后重试,仍失败则检查只读属性并以管理员身份运行
213
201
  * - fsUnavailable:本机文件系统层面根本写不进去 —— 磁盘/分区空间耗尽(`ENOSPC` /
214
202
  * `no space left on device`,Windows 为 `os error 112`)、目标分卷或挂载点为只读
215
203
  * (`EROFS` / `read-only file system`,Windows 写保护为 `os error 19`)、进程可用的文件句柄
216
- * 被耗尽(`EMFILE` / `ENFILE` / `too many open files`)。这三类都在 pnpm 落盘阶段发生,
217
- * 与具体装哪个插件无关:空间不足、盘只读、句柄不够时,任何插件都装不进来,不是插件问题
218
- * → 提示按报错代码对号入座(清空间 / 换可写目录 / 重启宿主释放句柄),不引导提 Issue
204
+ * 被耗尽(`EMFILE` / `ENFILE` / `too many open files`),都发生在 pnpm 落盘阶段
205
+ * → 提示按报错代码对号入座(清空间 / 换可写目录 / 重启宿主释放句柄)
219
206
  * - network:安装前连通性预检拦截(服务端 `[network]` 标记)、底层连接失败
220
207
  * (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝),或
221
208
  * registry tarball 拉取失败(`fetch failed` / `GET …/-/…tgz error (n)` —— 常见于本机
222
209
  * npm/pnpm 的 registry 被指向内网/自定义源,该源取不到包),或界面请求压根没送达宿主服务
223
- * (浏览器 fetch 的 `Failed to fetch` —— 宿主未就绪/正在重启,或本地代理拦了回环地址;
224
- * dsh-plugin-hub#45:该消息此前落进 repo 兜底,被误报成「插件侧安装失败」)—— 是本机网络
225
- * 不通/被墙/代理有问题/源配置异常,不是插件问题 → 提示检查网络,registry 指向自定义源时
226
- * 给出换源指引,不引导提 Issue
210
+ * (浏览器 fetch 的 `Failed to fetch` —— 宿主未就绪/正在重启,或本地代理拦了回环地址)
211
+ * → 提示检查网络,registry 指向自定义源时给出换源指引
212
+ *
213
+ * 插件分发/依赖问题(引导去仓库提 Issue):
227
214
  * - pnpmIgnoredBuild:插件自身或依赖的构建脚本被 pnpm 安全白名单(allowBuilds)默认拦截
228
- * (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`)。只影响带安装期
229
- * 构建的插件,其他插件不受影响 —— 差异在插件的依赖/打包方式,属插件依赖/打包问题
230
- * → 引导去仓库提 Issue(建议改用预编译版本或关闭安装期构建)
231
- * - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
232
- * 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
233
- * - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
215
+ * (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`)—— 只影响带安装期
216
+ * 构建的插件 → 建议改用预编译版本或关闭安装期构建
217
+ * - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或构建产物
218
+ * 导致),或装后校验发现 package.json 声明的入口文件不在发布物里(服务端 `[packaging]` 标记)
219
+ * —— 属插件打包/分发问题
220
+ * - repo:其余失败,默认按插件仓库问题处理
234
221
  */
235
222
  export function classifyFailure(message: string): FailureKind {
236
223
  // npm 内部崩溃(edgesOut)或服务端 [npm-too-low] 标记:是本机 npm 版本过低/自身缺陷,
@@ -239,16 +226,14 @@ export function classifyFailure(message: string): FailureKind {
239
226
  if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
240
227
  // 找不到 pnpm 命令(dsh 报 `pnpm not found on PATH` —— dsh 用它管理 profile 插件 /
241
228
  // POSIX「pnpm: command not found」/ spawn pnpm ENOENT / Windows cmd 中英文报错):
242
- // 本机缺 pnpm,不是插件问题。必须在 dshMissing 之前 —— dshMissing 正则含裸「command not
243
- // found」,会把 `pnpm: command not found` 吞成「dsh 缺失」,误引导用户去装 DSH
244
- // (dsh-plugin-hub#13:Linux 下 `dsh: pnpm not found on PATH` 被误归插件侧失败)
229
+ // 本机缺 pnpm。必须在 dshMissing 之前 —— dshMissing 正则含裸「command not found」,
230
+ // 会把 `pnpm: command not found` 吞成「dsh 缺失」,误引导用户去装 DSH
245
231
  if (/\[pnpm-missing\]|pnpm not found|pnpm: command not found|spawn pnpm ENOENT|'pnpm' 不是内部或外部命令|"pnpm" 不是内部或外部命令|pnpm['"]?\s*is not recognized/i.test(message)) return 'pnpmMissing'
246
232
  // 找不到 git 命令(服务端 [git-missing] 标记 / Windows cmd 中英文「'git' is not recognized」/
247
- // POSIX「git: command not found」/ spawn git ENOENT):本机 Git 未安装或不在 PATH,不是插件问题。
233
+ // POSIX「git: command not found」/ spawn git ENOENT):本机 Git 未安装或不在 PATH。
248
234
  // 必须在 dshMissing 之前 —— dshMissing 正则含裸「is not recognized / command not found」,
249
235
  // 会把 git 缺失('git' is not recognized as an internal or external command)吞成「dsh 缺失」,
250
- // 误导用户去装 DSH;且 pnpm 报 `ERR_PNPM_GIT_RESOLVE_FAILED`(git ls-remote failed)时
251
- // 若只看错误码会落到插件侧失败、引导去提 Issue(dsh-plugin-hub#21)
236
+ // 误导用户去装 DSH
252
237
  if (/\[git-missing\]|spawn git ENOENT|'git' 不是内部或外部命令|"git" 不是内部或外部命令|git['"]?\s*is not recognized|git: command not found/i.test(message)) return 'gitMissing'
253
238
  // 找不到 npm 命令(全局 npm 安装通道 `npm install -g` spawn 的 npm 缺失 / Windows cmd 中英文
254
239
  // 「'npm' is not recognized」/ POSIX「npm: command not found」/ spawn npm ENOENT):本机 npm 未安装
@@ -258,45 +243,45 @@ export function classifyFailure(message: string): FailureKind {
258
243
  // 找不到 dsh 命令(服务端 [dsh-missing] 标记 —— 乱码免疫:Windows cmd 中文版输出 GBK,
259
244
  // 经 UTF-8 解码成乱码无法匹配原文,故服务端在 spawn 前用 which/where 探测并打 ASCII 标记;
260
245
  // 其余形态:Windows cmd「不是内部或外部命令」/ POSIX「command not found」/
261
- // node spawn ENOENT):是本机 DSH 未正确安装或不在 PATH,不是插件问题 —— 必须先判,
246
+ // node spawn ENOENT):是本机 DSH 未正确安装或不在 PATH —— 必须先判,
262
247
  // 否则会被外层 "Command failed" 吞成「插件打包问题」,误导用户去提 Issue
263
- // (dsh-plugin-hub#12:Win 下 'dsh' 不在 PATH,cmd 报「不是内部或外部命令」被误归插件侧失败)
264
248
  if (/\[dsh-missing\]|不是内部或外部命令|is not recognized as an internal or external command|command not found|spawn dsh ENOENT/i.test(message)) return 'dshMissing'
265
249
  // pnpm 大版本/虚拟 store 位置不一致(ERR_PNPM_UNEXPECTED_STORE / ERR_PNPM_UNEXPECTED_VIRTUAL_STORE /
266
- // Unexpected store location):profile 目录里旧依赖是另一个大版本 pnpm 生成的(或 profile 目录
250
+ // Unexpected store location):profile 目录里的依赖是另一个大版本 pnpm 生成的(或 profile 目录
267
251
  // 被复制/移动、virtual-store-dir 配置变化导致 virtual store 位置不匹配),当前 pnpm 出于安全
268
- // 不认旧 store —— 本机环境问题,任何插件装进该 profile 都会同样失败,不是插件问题
269
- // (dsh-plugin-hub#14:macOS 下 ERR_PNPM_UNEXPECTED_STORE 被误归插件侧失败;
270
- // dsh-plugin-hub#30:ERR_PNPM_UNEXPECTED_VIRTUAL_STORE 漏判被误归插件侧失败);
271
- // 提示清理依赖目录用当前 pnpm 重建,不引导提 Issue。必须在 pnpmMissing 之后 ——
272
- // pnpm 在(能跑起来报错),不是「找不到命令」。
252
+ // 不认 —— 任何插件装进该 profile 都会同样失败 → 提示清理依赖目录用当前 pnpm 重建。
253
+ // 必须在 pnpmMissing 之后 —— pnpm 在(能跑起来报错),不是「找不到命令」。
273
254
  if (/ERR_PNPM_UNEXPECTED_(VIRTUAL_)?STORE|Unexpected (virtual )?store location/i.test(message)) return 'pnpmStore'
274
- // pnpm 拒绝在 workspace 根目录下安装(ERR_PNPM_ADDING_TO_ROOT):profile 目录被视为
255
+ // pnpm 拒绝在 workspace 根目录下安装(ERR_PNPM_ADDING_TO_ROOT):profile 目录被视为
275
256
  // pnpm workspace 根,宿主调 pnpm add 时没声明在根操作(缺 -w/--workspace-root),
276
- // pnpm 直接整条命令拒绝 —— 本机 profile/宿主调用方式问题,任何插件都装不上,不是插件问题
277
- // (dsh-plugin-hub#40:Win 下装 hub 本体,ADDING_TO_ROOT 被误归插件侧失败)
257
+ // pnpm 直接整条命令拒绝 —— 本机 profile/宿主调用方式问题,任何插件都装不上
278
258
  if (/ERR_PNPM_ADDING_TO_ROOT|add the dependency to the workspace root/i.test(message)) return 'pnpmWorkspace'
259
+ // 本地 hub 服务的来源校验拒绝了请求(服务端 403,正文恰好是 `untrusted origin`):
260
+ // 请求在 requireTrustedPost 就被拦下,压根没进入安装流程,与 pnpm 无关。
261
+ // 必须放在 pnpmPolicy 之前 —— 后者正则含同名裸文本,会把这条吞成「pnpm 供应链策略」,
262
+ // 并建议用户删掉 profile 的 node_modules + pnpm-lock.yaml(无效且具破坏性)。
263
+ // 用「整行以它结尾」来区隔 pnpm 的真实报错:pnpm 的形态后面一定跟着 dsh 的上下文
264
+ // (如 `untrusted origin\ndsh: pnpm failed in profile directory …`),不会被本规则命中
265
+ if (/(^|\n)untrusted origin\s*$/.test(message)) return 'originRejected'
279
266
  // pnpm 供应链安全策略拦截(pnpm 11:minimumReleaseAge 拒收「刚发布」的包 /
280
267
  // untrusted origin 来源不受信任):pnpm 在、也连得上,纯粹是本机策略不放行 ——
281
- // 装任何「新发布/非信任来源」的插件都会同样失败,不是插件问题(dsh-plugin-hub#15/#16)。
268
+ // 装任何「新发布/非信任来源」的插件都会同样失败。
282
269
  // 必须在 pnpmIgnoredBuild 之前 —— 该策略优先于「构建脚本被白名单拦截」,且两者都不引导提 Issue。
283
270
  if (/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|Minimum release age|untrusted origin/i.test(message)) return 'pnpmPolicy'
284
271
  // profile 里留着指向旧版本 dsh-plugin 的 patch 声明(ERR_PNPM_UNUSED_PATCH /
285
272
  // `The following patches were not used: dsh-plugin@1.4.2`):本机 pnpm 配置与本次解析到的版本
286
- // 对不上,pnpm 出于安全直接中止整次安装 —— 任何插件都装不进来,不是插件问题
287
- // (dsh-plugin-hub#48:用户升级 dsh-plugin 后旧 patch 条目失配)→ 提示删条目后重试,不引导提 Issue。
273
+ // 对不上,pnpm 出于安全直接中止整次安装 —— 任何插件都装不进来 → 提示删条目后重试。
288
274
  // 必须在兜底之前,否则被归成「插件侧失败」引导去提 Issue。
289
275
  if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
290
276
  // profile 里的文件被其他进程占用,pnpm 无法替换(Windows `os error 32`
291
277
  // 「另一个程序正在使用此文件,进程无法访问」/ EBUSY / resource busy or locked)——
292
278
  // 通常是宿主进程或杀毒软件实时扫描持有句柄;`os error 32` 用 ASCII 特征,中文原文乱码也能命中
293
- // (dsh-plugin-hub#47)→ 提示完全退出宿主后重试,不引导提 Issue
279
+ // → 提示完全退出宿主后重试
294
280
  if (/os error 32|EBUSY|resource busy or locked|being used by another process/i.test(message)) return 'fileLocked'
295
281
  // profile 里的文件/目录被系统拒绝写入(Windows `os error 5`「拒绝访问」= ERROR_ACCESS_DENIED /
296
282
  // Node 的 EPERM、EACCES / 英文 `Access is denied`):占用、只读属性、目录 ACL 或杀软拦截,
297
- // 同属本机文件系统问题,任何插件都装不上,不是插件问题。`os error 5` 只认 ASCII 特征 ——
298
- // 中文原文经 GBK→UTF-8 解码会残缺,不能依赖「拒绝访问」四个字
299
- // (dsh-plugin-hub#50:swap 阶段 `os error 5` 漏判,落 repo 兜底被误报成插件侧失败)
283
+ // 任何插件都装不上。`os error 5` 只认 ASCII 特征 —— 中文原文经 GBK→UTF-8 解码会残缺,
284
+ // 不能依赖「拒绝访问」四个字
300
285
  if (/os error 5\b|ERROR_ACCESS_DENIED|\bEPERM\b|\bEACCES\b|access is denied/i.test(message)) return 'accessDenied'
301
286
  // 本机文件系统层面根本写不进去:空间耗尽(ENOSPC / no space left on device / Windows os error 112)、
302
287
  // 目标盘或挂载点只读(EROFS / read-only file system / Windows 写保护 os error 19)、
@@ -309,8 +294,7 @@ export function classifyFailure(message: string): FailureKind {
309
294
  // 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
310
295
  // 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
311
296
  // 被拦 —— 必须在 network 判定之前:日志尾部常混着重试残留的连接失败特征
312
- // (ETIMEDOUT / Failed to connect 等),若先判网络会把「插件分发问题」误报成
313
- // 「你的网络不通」(graph-memory issues #82-#84:PREPARE_NOT_ALLOWED + 尾随超时)。
297
+ // (ETIMEDOUT / Failed to connect 等),若先判网络会把「插件分发问题」误报成「你的网络不通」。
314
298
  if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/i.test(message)) return 'pnpmIgnoredBuild'
315
299
  // 网络问题(服务端 [network] 标记,或安装日志里的连接失败特征:git fetch 失败、
316
300
  // 连接超时/拒绝/重置、DNS 解析失败、TLS/SSL 握手失败,或 registry tarball 拉取失败
@@ -320,10 +304,9 @@ export function classifyFailure(message: string): FailureKind {
320
304
  // 外层包成 "Command failed: git fetch ...",先按网络特征归类才不会误判成插件问题。
321
305
  // 404 类「目标不存在」不含这些特征,仍归 repo(那是仓库/包的问题)。
322
306
  // 注意:fetch failed 只在 pnpm 拉取阶段出现;prepare/构建已跑起来(tarball 到手)的
323
- // 插件问题不带此特征,不会误伤(graph-memory #82-#84 的 allowBuilds 拦截在上一分支先判)。
307
+ // 插件问题不带此特征,不会误伤(allowBuilds 拦截在上一分支先判)。
324
308
  // `Failed to fetch` 是浏览器 fetch 的 TypeError(词序与 Node 的 fetch failed 相反):界面
325
- // 请求根本没送达宿主服务(宿主未就绪/正在重启、本地代理拦了回环地址)—— 同样不是插件问题
326
- // (dsh-plugin-hub#45:此前不被识别,落进 repo 兜底并自动提了「插件侧安装失败」)。
309
+ // 请求根本没送达宿主服务(宿主未就绪/正在重启、本地代理拦了回环地址)。
327
310
  if (/\[network\]|ERR_PNPM_GIT_FETCH_FAILED|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH|getaddrinfo|Could not connect|Could not resolve host|Network unreachable|Failed to connect|socket hang up|CERT_HAS_EXPIRED|SSL certificate problem|\bTLS\b|\bSSL\b|fetch failed|failed to fetch|\bGET https?:\/\/\S+\.tgz\s+error \(\d+\)/i.test(message)) return 'network'
328
311
  // 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
329
312
  // 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
@@ -347,9 +330,7 @@ const CORE_LINE_RE = /ERR_[A-Z_]+|ELIFECYCLE|Command failed|prepare-guard|Failed
347
330
  * `Peer dependencies that should be installed`):宿主提供的 peer(@deepseek-ai/*、react、
348
331
  * dsh-client-* 等,DSH profile 用 autoInstallPeers:false 不自动装)缺失是无害噪音,与插件本身
349
332
  * 无关 —— 抓核心错误时必须跳过,否则会被 CORE_LINE_RE 的 `missing` 分支误抓、淹没真正的
350
- * 错误码(如 git 源的 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED),让 issue 正文只剩一堆 peer WARN
351
- * (dsh-plugin-hub#28:anime-find 的 auto-issue 核心错误全是 missing-peer WARN 树,真正的
352
- * prepare 错误被盖住)。 */
333
+ * 错误码(如 git 源的 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED),让 issue 正文只剩一堆 peer WARN。 */
353
334
  const PEER_WARN_RE = /missing peer|issues with peer dependencies|peer dependencies that should be installed/i
354
335
  /** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
355
336
  * 固定模板与 URL 编码开销约 1~2K,核心错误(以 ASCII 日志为主)可安全带到 ~5K;
@@ -425,7 +406,7 @@ const PUBLIC_REGISTRY_HOSTS = new Set([
425
406
  * 网络类失败消息里,判断是否「npm/pnpm 的 registry 被指向了内网/自定义源导致拉包失败」。
426
407
  * pnpm 下载包文件的 URL 形如 `<registry>/<pkg>/-/<pkg>-<ver>.tgz`(路径含 `/-/`);
427
408
  * 若该 tarball 的主机不属于官方/常见公开镜像,说明本机 registry 被配成了私有/内网源
428
- * (如公司 Artifactory)—— 常因源未同步该包、需内网认证或网络策略拦截而失败(dsh-plugin-hub#32)。
409
+ * (如公司 Artifactory)—— 常因源未同步该包、需内网认证或网络策略拦截而失败。
429
410
  * 返回该主机名供前端给出「检查 registry 配置」的精准提示;官方/公开源或提取不到返回 null,
430
411
  * 调用方按通用网络问题提示即可。
431
412
  */
@@ -42,7 +42,7 @@ export function pluginSiteUrl(repo: string): string {
42
42
 
43
43
  /** 按失败类型给出简洁的错误原因标题(对外用英文):
44
44
  * 标题直接点明问题出在哪一侧(构建白名单/分发物/本机 npm/网络/插件侧),
45
- * 作者与用户扫一眼列表就能分流 —— 不再用含糊的 Install/Remove 动作词。 */
45
+ * 让作者与用户扫一眼列表就能分流,而不是笼统的 Install/Remove 动作词。 */
46
46
  function reasonTitleOf(kind: FailureKind): string {
47
47
  switch (kind) {
48
48
  case 'npmTooOld': return 'npm too old to install'
@@ -50,6 +50,7 @@ function reasonTitleOf(kind: FailureKind): string {
50
50
  case 'pnpmMissing': return 'pnpm command not found on the user machine'
51
51
  case 'pnpmStore': return 'pnpm store version mismatch on the user machine'
52
52
  case 'pnpmWorkspace': return 'pnpm workspace-root check blocked the install on the user machine'
53
+ case 'originRejected': return 'request origin rejected by the local service on the user machine'
53
54
  case 'pnpmPolicy': return 'pnpm supply-chain policy blocked the install on the user machine'
54
55
  case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
55
56
  case 'fileLocked': return 'file locked by another process on the user machine'
@@ -86,7 +87,9 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
86
87
  ? 'plugin prepare/build script failed during install (packaging/distribution issue)'
87
88
  : kind === 'pnpmIgnoredBuild'
88
89
  ? 'plugin depends on a native module whose build script pnpm blocks by default (use a prebuilt variant)'
89
- : kind === 'pnpmPolicy'
90
+ : kind === 'originRejected'
91
+ ? 'the request never reached the install flow — the local service rejected its origin (local environment issue)'
92
+ : kind === 'pnpmPolicy'
90
93
  ? 'the pnpm supply-chain policy on the user machine blocked the install (minimum release age for freshly published packages / untrusted origin)'
91
94
  : kind === 'pnpmUnusedPatch'
92
95
  ? 'the profile keeps a pnpm patch entry for an older dsh-plugin version, so pnpm aborted the install (local config issue)'
@@ -322,7 +322,7 @@
322
322
  min-width: 0;
323
323
  }
324
324
 
325
- /* 第二行元信息:仓库 · 分类 —— 独立成行,左缩进与名称对齐,不再挤压名称 */
325
+ /* 第二行元信息:仓库 · 分类 —— 独立成行,左缩进与名称对齐 */
326
326
  .rowMeta {
327
327
  display: flex;
328
328
  align-items: center;
@@ -1026,9 +1026,9 @@
1026
1026
  background: var(--hub-danger-tint);
1027
1027
  }
1028
1028
 
1029
- /* 成功通知卡片(时间戳已并入头部行):删除按钮不再用 auto margin 顶到最右,
1029
+ /* 成功通知卡片(时间戳并入头部行):删除按钮不占 auto margin,
1030
1030
  由时间戳的 margin-left:auto 把「时间 + 删除」整体推到行尾 —— 图标文字与
1031
- 时间垂直居中,卡片只有一行内容,整体居中不顶头 */
1031
+ 时间垂直居中,卡片只有一行内容 */
1032
1032
  .noticeRowOk .noticeRemove {
1033
1033
  margin-left: 0;
1034
1034
  }
@@ -23,7 +23,7 @@ export interface LocaleService {
23
23
  getSnapshot(): { active: LocaleId }
24
24
  subscribe(fn: () => void): () => void
25
25
  /** 切换宿主(系统)语言偏好:Hub 语言跟随宿主,右上角语言按钮直接写入宿主偏好,
26
- * 宿主左侧菜单/设置弹窗与 Hub 面板一起切换,不再与宿主语言脱节 */
26
+ * 宿主左侧菜单/设置弹窗与 Hub 面板一起切换,与宿主语言保持一致 */
27
27
  setLocale(id: string): void
28
28
  }
29
29
 
@@ -24,7 +24,7 @@ import { appendLog, clearLog, readLog, logFilePath, defaultLogFilePath, customLo
24
24
 
25
25
  /**
26
26
  * 跨平台宿主重启脚本(以 `node -e` 运行,独立于宿主进程)。
27
- * 取代原先的 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
27
+ * 不用 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
28
28
  * 由 `spawn(process.execPath, ['-e', SCRIPT, port], { detached, stdio:'ignore' })` 孵化,
29
29
  * 宿主进程被 kill 后仍能完成「停旧 → 等端口释放 → 拉起新 dsh web」。
30
30
  */
@@ -167,8 +167,7 @@ function installedRepoOf(profile: string, name: string): string | null {
167
167
  }
168
168
 
169
169
  /** 宿主 dsh CLI 版本:优先直接跑 `dsh --version`(与 pnpm/npm/git 同款同步探测,1500ms 超时兜底,
170
- * 绝不挂起)—— 原先从 process.argv[1] 向上找 package.json 猜版本在部分宿主加载方式下取不到,
171
- * 导致 issue 环境快照 DSH: unknown;拿不到再退回入口 package.json 查找(dsh 不在 PATH 时兜底)。 */
170
+ * 绝不挂起);拿不到再退回从入口 `process.argv[1]` 向上找 package.json(dsh 不在 PATH 时兜底)。 */
172
171
  function hostDshVersion(): string | null {
173
172
  const viaCli = toolVersion('dsh', ['--version'])
174
173
  if (viaCli) return viaCli
@@ -223,7 +222,7 @@ function hostEnv(profile: string): Record<string, string | null> {
223
222
  }
224
223
 
225
224
  /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
226
- * npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
225
+ * 不注入 npm registry —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
227
226
  * 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
228
227
  * 使安装通道与诊断使用相同的代理来源。 */
229
228
  function mutationEnv(settings: HubSettings): NodeJS.ProcessEnv {
@@ -257,13 +256,21 @@ function sendJson(response: ServerResponse, status: number, value: unknown): voi
257
256
  response.end(JSON.stringify(value))
258
257
  }
259
258
 
260
- /** POST mutations are only accepted from the local web server origin. */
261
- function isSameOrigin(request: IncomingMessage): boolean {
259
+ /** POST mutations are only accepted from the local web server origin or the desktop host page.
260
+ * Exported for tests (tests/routes.test.ts) — the origin check is the whole CSRF defence. */
261
+ export function isSameOrigin(request: IncomingMessage): boolean {
262
262
  const origin = request.headers.origin
263
- const host = request.headers.host
264
- if (origin === undefined || host === undefined) return false
263
+ if (origin === undefined) return false
265
264
  try {
266
265
  const url = new URL(origin)
266
+ // 桌面端(Electron 宿主)的页面跑在宿主自定义协议下,origin 形如 `dsh-app://app`。
267
+ // 客户端一律用相对路径请求,宿主把 `/dsh-plugin-hub/*` 转发给本地 http server 时
268
+ // Origin 头原样保留,与 Host(如 localhost:3081)不等,因此必须显式放行该协议,
269
+ // 否则桌面端全部 POST 都会被判成 untrusted origin。该协议只有宿主自身能产生,
270
+ // 外部网页无法把 Origin 伪造成 `dsh-app://…`,放行不扩大 CSRF 面。
271
+ if (url.protocol === 'dsh-app:') return true
272
+ const host = request.headers.host
273
+ if (host === undefined) return false
267
274
  const localHostnames = new Set(['localhost', '127.0.0.1', '[::1]'])
268
275
  return url.host === host && localHostnames.has(url.hostname)
269
276
  } catch {
@@ -634,7 +641,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
634
641
  path: '/dsh-plugin-hub/catalog',
635
642
  handler: async (request, response) => {
636
643
  if (!requireMethod(request, response, 'GET')) return
637
- // 目录/统计数据服务端代理:浏览器不再直连 dsh-plugin.org,改经此路由
644
+ // 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
638
645
  // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
639
646
  // 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
640
647
  const url = new URL(request.url ?? '/', 'http://localhost')
@@ -688,7 +695,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
688
695
  path: '/dsh-plugin-hub/install',
689
696
  handler: async (request, response) => {
690
697
  if (!requireTrustedPost(request, response)) return
691
- // 任务自动入队:即使已有插件操作在跑也接受请求(FIFO 串行执行),不再 409 拒绝
698
+ // 任务自动入队:即使已有插件操作在跑也接受请求(FIFO 串行执行),不做 409 拒绝
692
699
  try {
693
700
  const body = await readJsonBody(request)
694
701
  // mode: 'update' = 已安装目标的覆盖更新(放行 add,pnpm 对已存在依赖原位覆盖重装)。
@@ -826,20 +833,35 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
826
833
  // 页 —— 404 表示「目标不存在」而非网络不通,不该被当成网络故障拦截。
827
834
  // 仅新安装预检(更新是已信任目标的覆盖重装,跳过);不通直接 400 拦下并打
828
835
  // [network] 标记,客户端据此提示「你的网络不通」,而不是把网络失败当成
829
- // 插件侧问题引导去作者仓库提 Issue(issue #10:git fetch 超时被误报为插件问题)。
836
+ // 插件侧问题引导去作者仓库提 Issue。
830
837
  // 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
831
838
  if (already === null) {
832
839
  const effectiveProxy = settings.proxy !== ''
833
840
  ? settings.proxy
834
841
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
835
842
  const isGitChannel = repoTarget !== null && target === repoTarget
836
- const probeTarget = isGitChannel
837
- ? 'https://github.com/'
838
- : `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`
839
- const net = await probeUrl(probeTarget, effectiveProxy, 6000)
843
+ const npmProbeTarget = `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`
844
+ // git 通道的探针换成克隆握手本身:git ls-remote 走的就是 pnpm 克隆前的同一套
845
+ // https 传输。curl 打 github.com 主页会被按协议/端口区分的防火墙与 TLS 复检
846
+ // 拦截(能 git 克隆、打不开网页),把可装的仓库误判成 [network] 而中止
847
+ // (见 issue #71)。握手失败时再用 curl 复核一次主页连通性:只有两者都不通
848
+ // 才判「网络不通」;仅握手失败(仓库不存在、私有仓库、需要凭据)放行,
849
+ // 交给后续预检与真实安装给出准确原因。
850
+ const gitProbeTarget = repoTarget === null ? null : repoTarget.replace(/^git\+/, '')
851
+ let net: Awaited<ReturnType<typeof probeUrl>>
852
+ if (gitProbeTarget !== null) {
853
+ net = await gitLsRemote(gitProbeTarget, effectiveProxy, 6000)
854
+ if (!net.ok) {
855
+ const reachable = await probeUrl('https://github.com/', effectiveProxy, 6000)
856
+ if (reachable.ok) net = reachable
857
+ }
858
+ } else {
859
+ net = await probeUrl(npmProbeTarget, effectiveProxy, 6000)
860
+ }
861
+ const probeTarget = gitProbeTarget ?? npmProbeTarget
840
862
  if (!net.ok) {
841
863
  // 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
842
- // 不再写死英文(用户一眼看懂是网络问题,而不是插件问题)
864
+ // 让用户一眼看懂是网络问题而非插件问题
843
865
  const channelName = isGitChannel
844
866
  ? (lang === 'zh' ? 'GitHub' : 'github.com')
845
867
  : (lang === 'zh' ? 'npm 源' : 'the npm registry')
@@ -866,9 +888,9 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
866
888
  return
867
889
  }
868
890
  // 包名冲突检测(仅 git 通道):git 目标包内声明的 name 若与 profile 已装依赖同名,
869
- // pnpm 会以该 name 做依赖键撞车,抛出的 CLI 报错晦涩难懂(issue #25:sandbase-harness
870
- // 声明 managed-agents,但该包名在 registry 上被另一仓库占用,npm 反查搜不到 → git 直装
871
- // 撞已装同名依赖)。到这里已装依赖要么来源不同仓库、要么 spec 是版本号解析不出仓库身份。
891
+ // pnpm 会以该 name 做依赖键撞车,抛出的 CLI 报错晦涩难懂 —— 且该包名在 registry 上
892
+ // 可能被别的仓库占用,npm 反查搜不到,git 直装就会撞上已装的同名依赖。
893
+ // 到这里已装依赖要么来源不同仓库、要么 spec 是版本号解析不出仓库身份。
872
894
  // 读 node_modules 里已装包的真实 repository 区分两种情况:
873
895
  // - 指向同一仓库 → 是「同仓库残留」(spec 丢了仓库身份),就地转 update 覆盖重装(幂等);
874
896
  // - 指向别的仓库 / 读不到 → 跨仓库包名冲突,入队前转成明确 409,指导用户先卸载或改装。
@@ -49,6 +49,8 @@ export interface Invocation {
49
49
  prefixArgs: string[]
50
50
  cwd: string
51
51
  useShell: boolean
52
+ /** 必须叠加到子进程环境上的变量(打包桌面端要带 ELECTRON_RUN_AS_NODE) */
53
+ env?: NodeJS.ProcessEnv
52
54
  }
53
55
 
54
56
  /** 排队中的变更任务及其启动参数。 */
@@ -5,10 +5,9 @@
5
5
  *
6
6
  * 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
7
7
  *
8
- * 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时
9
- * 有已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`(build-ideal-tree.js
10
- * 的 #loadPeerSet),官方标记 Cannot Reproduce(npm/cli#8261、#9787),npm 11.6.0 起不再复现。
11
- * 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,若按插件打包问题引导提 Issue 就误导了用户。
8
+ * 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时有
9
+ * 已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`。这是 npm 自身缺陷,
10
+ * 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,不能按插件打包问题引导用户提 Issue。
12
11
  *
13
12
  * 这里的 `npmTooLowMarker` 是纯函数(输出文本 + 环境 → 标记行或 null),不触碰任务/队列,
14
13
  * 由调用方(task-queue.ts)负责把标记追加到任务输出;spawn 读 npm 版本是唯一副作用,独立可测。
@@ -17,7 +16,7 @@ import { spawnSync } from 'node:child_process'
17
16
 
18
17
  /** npm arborist 内部崩溃特征:`Cannot read properties of null (reading 'edgesOut')`。 */
19
18
  export const NPM_CRASH_EDGES_OUT_RE = /Cannot read properties of null \(reading 'edgesOut'\)|edgesOut/i
20
- /** 该缺陷在 npm 11.6.0 起不再复现;低于此版本遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
19
+ /** 低于该版本(该缺陷在 npm 11.6.0 修复)遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
21
20
  export const NPM_MIN_VERSION = [11, 6, 0]
22
21
 
23
22
  /** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */