dsh-plugin 1.4.8 → 1.4.10
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.en.md +9 -9
- package/README.md +9 -9
- package/client/client.js +357 -367
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +24 -15
- package/lib/services/install/npm-check.js +4 -5
- package/lib/services/install/task-queue.js +1 -1
- package/lib/services/probe.js +16 -5
- package/lib/services/profile/profile.js +33 -4
- package/lib/types/http/routes.d.ts +3 -0
- package/lib/types/services/install/npm-check.d.ts +1 -1
- package/lib/types/services/probe.d.ts +10 -1
- package/lib/types/services/profile/profile.d.ts +10 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +2 -2
- package/src/client/components/modals/NotificationsModal.tsx +13 -9
- package/src/client/components/modals/modals.tsx +10 -6
- package/src/client/data/catalog.ts +1 -1
- package/src/client/hooks/useCatalog.ts +2 -2
- package/src/client/locales.ts +7 -0
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/failures.ts +76 -95
- package/src/client/logic/urls.ts +5 -2
- package/src/client/styles/InstalledView.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/types.ts +1 -1
- package/src/server/http/routes.ts +22 -15
- package/src/server/services/install/npm-check.ts +4 -5
- package/src/server/services/install/task-queue.ts +1 -1
- package/src/server/services/probe.ts +16 -4
- package/src/server/services/profile/profile.ts +29 -4
|
@@ -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
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
|
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
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* -
|
|
182
|
-
* `
|
|
183
|
-
*
|
|
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
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
208
|
-
*
|
|
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
|
|
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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* - repo
|
|
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
|
|
243
|
-
//
|
|
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
|
|
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
|
|
250
|
+
// Unexpected store location):profile 目录里的依赖是另一个大版本 pnpm 生成的(或 profile 目录
|
|
267
251
|
// 被复制/移动、virtual-store-dir 配置变化导致 virtual store 位置不匹配),当前 pnpm 出于安全
|
|
268
|
-
//
|
|
269
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
298
|
-
//
|
|
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
|
-
// 插件问题不带此特征,不会误伤(
|
|
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)——
|
|
409
|
+
* (如公司 Artifactory)—— 常因源未同步该包、需内网认证或网络策略拦截而失败。
|
|
429
410
|
* 返回该主机名供前端给出「检查 registry 配置」的精准提示;官方/公开源或提取不到返回 null,
|
|
430
411
|
* 调用方按通用网络问题提示即可。
|
|
431
412
|
*/
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
42
42
|
|
|
43
43
|
/** 按失败类型给出简洁的错误原因标题(对外用英文):
|
|
44
44
|
* 标题直接点明问题出在哪一侧(构建白名单/分发物/本机 npm/网络/插件侧),
|
|
45
|
-
*
|
|
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 === '
|
|
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)'
|
|
@@ -1026,9 +1026,9 @@
|
|
|
1026
1026
|
background: var(--hub-danger-tint);
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
-
/*
|
|
1029
|
+
/* 成功通知卡片(时间戳并入头部行):删除按钮不占 auto margin,
|
|
1030
1030
|
由时间戳的 margin-left:auto 把「时间 + 删除」整体推到行尾 —— 图标文字与
|
|
1031
|
-
|
|
1031
|
+
时间垂直居中,卡片只有一行内容 */
|
|
1032
1032
|
.noticeRowOk .noticeRemove {
|
|
1033
1033
|
margin-left: 0;
|
|
1034
1034
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
698
|
+
// 任务自动入队:即使已有插件操作在跑也接受请求(FIFO 串行执行),不做 409 拒绝
|
|
692
699
|
try {
|
|
693
700
|
const body = await readJsonBody(request)
|
|
694
701
|
// mode: 'update' = 已安装目标的覆盖更新(放行 add,pnpm 对已存在依赖原位覆盖重装)。
|
|
@@ -826,7 +833,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
826
833
|
// 页 —— 404 表示「目标不存在」而非网络不通,不该被当成网络故障拦截。
|
|
827
834
|
// 仅新安装预检(更新是已信任目标的覆盖重装,跳过);不通直接 400 拦下并打
|
|
828
835
|
// [network] 标记,客户端据此提示「你的网络不通」,而不是把网络失败当成
|
|
829
|
-
// 插件侧问题引导去作者仓库提 Issue
|
|
836
|
+
// 插件侧问题引导去作者仓库提 Issue。
|
|
830
837
|
// 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
831
838
|
if (already === null) {
|
|
832
839
|
const effectiveProxy = settings.proxy !== ''
|
|
@@ -839,7 +846,7 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
839
846
|
const net = await probeUrl(probeTarget, effectiveProxy, 6000)
|
|
840
847
|
if (!net.ok) {
|
|
841
848
|
// 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
|
|
842
|
-
//
|
|
849
|
+
// 让用户一眼看懂是网络问题而非插件问题
|
|
843
850
|
const channelName = isGitChannel
|
|
844
851
|
? (lang === 'zh' ? 'GitHub' : 'github.com')
|
|
845
852
|
: (lang === 'zh' ? 'npm 源' : 'the npm registry')
|
|
@@ -866,9 +873,9 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
866
873
|
return
|
|
867
874
|
}
|
|
868
875
|
// 包名冲突检测(仅 git 通道):git 目标包内声明的 name 若与 profile 已装依赖同名,
|
|
869
|
-
// pnpm 会以该 name 做依赖键撞车,抛出的 CLI
|
|
870
|
-
//
|
|
871
|
-
//
|
|
876
|
+
// pnpm 会以该 name 做依赖键撞车,抛出的 CLI 报错晦涩难懂 —— 且该包名在 registry 上
|
|
877
|
+
// 可能被别的仓库占用,npm 反查搜不到,git 直装就会撞上已装的同名依赖。
|
|
878
|
+
// 到这里已装依赖要么来源不同仓库、要么 spec 是版本号解析不出仓库身份。
|
|
872
879
|
// 读 node_modules 里已装包的真实 repository 区分两种情况:
|
|
873
880
|
// - 指向同一仓库 → 是「同仓库残留」(spec 丢了仓库身份),就地转 update 覆盖重装(幂等);
|
|
874
881
|
// - 指向别的仓库 / 读不到 → 跨仓库包名冲突,入队前转成明确 409,指导用户先卸载或改装。
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
|
|
7
7
|
*
|
|
8
|
-
* 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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(不妄下结论)。 */
|
|
@@ -485,7 +485,7 @@ function spawnMutation(options: {
|
|
|
485
485
|
|
|
486
486
|
return (async () => {
|
|
487
487
|
let result = await spawnOnce(args)
|
|
488
|
-
// npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')
|
|
488
|
+
// npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`):
|
|
489
489
|
// 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
|
|
490
490
|
// 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
|
|
491
491
|
// 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
|
|
@@ -38,7 +38,7 @@ const probeFail = (): ProbeResult => ({ ok: false, ms: null, status: null })
|
|
|
38
38
|
* 代理受限或吊销服务不可达时整个握手直接失败(CRYPT_E_NO_REVOCATION_CHECK),
|
|
39
39
|
* 表现为 `curl: (35) schannel: next InitializeSecurityContext failed` —— 而 Node/npm/
|
|
40
40
|
* 浏览器各有自己的 TLS 实现,照常可用,用户只会看到「插件市场打不开」。
|
|
41
|
-
* --ssl-revoke-best-effort(curl ≥ 7.70
|
|
41
|
+
* --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,避免它拦死握手;
|
|
42
42
|
* 其它平台后端(SecureTransport / OpenSSL)没有这个强制行为,不加参数。
|
|
43
43
|
* 运行时读 process.platform(而非模块级常量),便于测试覆盖两条分支。
|
|
44
44
|
*/
|
|
@@ -90,6 +90,20 @@ function macSystemProxy(): string | null {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* 从 `reg query … /v ProxyServer` 的输出里解析出 `http://host:port`;解析不出返回 null。
|
|
95
|
+
* 注册表里的值常见三种写法:`http://127.0.0.1:10793`(带 scheme)、`127.0.0.1:10793`、
|
|
96
|
+
* `http=host:port;https=host:port`(按协议分别配置)。
|
|
97
|
+
* 字符类必须同时排除 `/` 与 `=`:只排除 `=` 时,带 scheme 的值会让「主机名」吞掉 `//`,
|
|
98
|
+
* 拼出 `http:////127.0.0.1:10793` 这种立刻失败的非法地址 —— 代理本来是通的,却把诊断、
|
|
99
|
+
* 目录拉取、npm/git 预检全判成「不可达」。
|
|
100
|
+
*/
|
|
101
|
+
export function parseWinProxyServer(stdout: string): string | null {
|
|
102
|
+
const m = /([^:\s=/]+):(\d+)/.exec(stdout)
|
|
103
|
+
if (m === null) return null
|
|
104
|
+
return `http://${m[1]}:${m[2]}`
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
/** Windows 系统代理:WinINET Internet 设置注册表;未开启返回 null。 */
|
|
94
108
|
function winSystemProxy(): string | null {
|
|
95
109
|
try {
|
|
@@ -97,9 +111,7 @@ function winSystemProxy(): string | null {
|
|
|
97
111
|
const enable = spawnSync('reg', ['query', key, '/v', 'ProxyEnable'], { encoding: 'utf8', timeout: 1500 })
|
|
98
112
|
const server = spawnSync('reg', ['query', key, '/v', 'ProxyServer'], { encoding: 'utf8', timeout: 1500 })
|
|
99
113
|
if (!/0x1/i.test(enable.stdout ?? '')) return null
|
|
100
|
-
|
|
101
|
-
if (m === null) return null
|
|
102
|
-
return `http://${m[1]}:${m[2]}`
|
|
114
|
+
return parseWinProxyServer(server.stdout ?? '')
|
|
103
115
|
} catch {
|
|
104
116
|
return null
|
|
105
117
|
}
|
|
@@ -8,14 +8,39 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
10
10
|
import { homedir } from 'node:os'
|
|
11
|
-
import { join } from 'node:path'
|
|
11
|
+
import { basename, dirname, join } from 'node:path'
|
|
12
12
|
import { PACKAGE_RE, REPO_RE } from '../install/install-types.ts'
|
|
13
13
|
|
|
14
14
|
/** Resolve the active profile from the booted CLI args, falling back to `web`. */
|
|
15
15
|
export function readProfileArg(fallback = 'web'): string {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
return profileFromArgv(process.argv, fallback)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 从启动参数解析当前 profile,两种宿主形态都认:
|
|
21
|
+
* - 官方 CLI:`--profile <name>`(也兼容 `--profile=<name>` 等号形式);
|
|
22
|
+
* - Electron 桌面宿主:不传 `--profile`,而是把 profile 目录作为位置参数传入
|
|
23
|
+
* (`… <dshRoot> <home>\profiles\desktop …`)。
|
|
24
|
+
* 桌面端只认 `--profile` 会永远落回 fallback,把路由/日志/安装全挂到错误的 profile 上
|
|
25
|
+
* (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」)。
|
|
26
|
+
* 位置参数按「父目录名为 profiles」精确识别 profile 目录,避免误取其它位置参数。
|
|
27
|
+
*/
|
|
28
|
+
export function profileFromArgv(argv: readonly string[], fallback = 'web'): string {
|
|
29
|
+
const index = argv.indexOf('--profile')
|
|
30
|
+
const candidate = index >= 0 ? argv[index + 1] : undefined
|
|
31
|
+
if (candidate !== undefined && !candidate.startsWith('-')) return candidate
|
|
32
|
+
const equals = argv.find((arg) => arg.startsWith('--profile='))
|
|
33
|
+
if (equals !== undefined) {
|
|
34
|
+
const name = equals.slice('--profile='.length)
|
|
35
|
+
if (name !== '') return name
|
|
36
|
+
}
|
|
37
|
+
for (const arg of argv.slice(2)) {
|
|
38
|
+
if (arg === '' || arg.startsWith('-')) continue
|
|
39
|
+
if (basename(dirname(arg)) !== 'profiles') continue
|
|
40
|
+
const name = basename(arg)
|
|
41
|
+
if (name !== '') return name
|
|
42
|
+
}
|
|
43
|
+
return fallback
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
/** Resolve a profile directory (`DSH_HOME` or `~/.dsh`). */
|