dsh-plugin 1.6.5 → 1.6.7
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 +11 -11
- package/README.md +11 -11
- package/client/client.js +421 -390
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +132 -0
- package/lib/http/diagnostics.js +201 -0
- package/lib/http/install.js +399 -0
- package/lib/http/logs.js +164 -0
- package/lib/http/routes.js +19 -1263
- package/lib/http/settings.js +77 -0
- package/lib/http/shared.js +169 -0
- package/lib/http/task.js +208 -0
- package/lib/services/install/install.js +1 -1
- package/lib/services/install/package-entry.js +59 -8
- package/lib/services/install/preflight.js +47 -17
- package/lib/services/install/task-queue.js +75 -31
- package/lib/services/probe.js +1 -1
- package/lib/types/http/catalog.d.ts +10 -0
- package/lib/types/http/diagnostics.d.ts +4 -0
- package/lib/types/http/install.d.ts +11 -0
- package/lib/types/http/logs.d.ts +2 -0
- package/lib/types/http/routes.d.ts +15 -20
- package/lib/types/http/settings.d.ts +2 -0
- package/lib/types/http/shared.d.ts +44 -0
- package/lib/types/http/task.d.ts +3 -0
- package/lib/types/services/install/install.d.ts +1 -1
- package/lib/types/services/install/package-entry.d.ts +42 -10
- package/lib/types/services/install/preflight.d.ts +9 -1
- package/lib/types/services/install/task-queue.d.ts +6 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +3 -0
- package/src/client/components/modals/NotificationsModal.tsx +34 -18
- package/src/client/components/modals/modals.tsx +38 -28
- package/src/client/components/views/InstalledView.tsx +23 -11
- package/src/client/hooks/useCatalog.ts +1 -1
- package/src/client/locales.ts +28 -12
- package/src/client/logic/failures.ts +36 -157
- package/src/client/logic/urls.ts +75 -4
- package/src/client/types.ts +1 -1
- package/src/server/http/catalog.ts +143 -0
- package/src/server/http/diagnostics.ts +193 -0
- package/src/server/http/install.ts +391 -0
- package/src/server/http/logs.ts +149 -0
- package/src/server/http/routes.ts +22 -1222
- package/src/server/http/settings.ts +73 -0
- package/src/server/http/shared.ts +191 -0
- package/src/server/http/task.ts +194 -0
- package/src/server/services/install/install-types.ts +9 -1
- package/src/server/services/install/install.ts +11 -1
- package/src/server/services/install/package-entry.ts +72 -9
- package/src/server/services/install/preflight.ts +47 -18
- package/src/server/services/install/task-queue.ts +73 -34
- package/src/server/services/probe.ts +1 -1
|
@@ -151,7 +151,7 @@ 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' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | '
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hub'
|
|
155
155
|
|
|
156
156
|
/** Match the pnpm error code, not a generic mention of checksum or integrity. */
|
|
157
157
|
function missingTarballIntegrity(message: string): boolean {
|
|
@@ -174,7 +174,7 @@ export function releaseAgeRetryAfter(message: string): string | null {
|
|
|
174
174
|
*/
|
|
175
175
|
export function pnpmPolicyHintOf(message: string): 'failPnpmTarballIntegrityHint' | 'failPnpmReleaseAgeHint' | 'failPnpmPolicyHint' {
|
|
176
176
|
if (missingTarballIntegrity(message)) return 'failPnpmTarballIntegrityHint'
|
|
177
|
-
if (/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|Minimum release age/i.test(message)
|
|
177
|
+
if (/ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge/i.test(message)
|
|
178
178
|
&& releaseAgeRetryAfter(message) !== null) {
|
|
179
179
|
return 'failPnpmReleaseAgeHint'
|
|
180
180
|
}
|
|
@@ -187,180 +187,59 @@ export function pnpmPolicyHintParams(message: string): Record<string, string> {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
*
|
|
191
|
-
* 分界是「本机环境问题」(装任何插件都会同样失败 → 不引导提 Issue
|
|
192
|
-
*
|
|
193
|
-
* 原因见 classifyFailure 内注释。
|
|
194
|
-
*
|
|
195
|
-
* 本机环境问题:
|
|
196
|
-
* - npmTooOld:本机 npm 版本过低或自身缺陷 —— npm arborist 解 peer 依赖时抛 `edgesOut`
|
|
197
|
-
* (build-ideal-tree.js 内部报错),或服务端已核实版本低于阈值并打了 `[npm-too-low]` 标记
|
|
198
|
-
* → 引导升级 npm
|
|
199
|
-
* - dshMissing:安装器 spawn 的 `dsh` 命令找不到(Windows cmd「不是内部或外部命令」/ POSIX
|
|
200
|
-
* 「command not found」/ spawn ENOENT)→ 提示检查 PATH / 重装 DSH
|
|
201
|
-
* - gitMissing:安装器调用 `git` 时找不到可执行文件(Windows cmd「'git' is not recognized」/
|
|
202
|
-
* POSIX「git: command not found」/ spawn ENOENT)→ 提示安装 Git / 加入 PATH
|
|
203
|
-
* - pnpmMissing:dsh 存在但调用的 `pnpm` 找不到(dsh 报 `pnpm not found on PATH` / POSIX
|
|
204
|
-
* 「pnpm: command not found」/ spawn ENOENT)—— dsh 用 pnpm 管理 profile 插件
|
|
205
|
-
* → 提示安装 / 开启 pnpm
|
|
206
|
-
* - npmMissing:全局 npm 安装通道(`npm install -g …`)spawn 的 `npm` 命令找不到,形态同上
|
|
207
|
-
* → 提示安装 npm(Node.js 自带)/ 加入 PATH
|
|
208
|
-
* - pnpmStore:pnpm 报 store / virtual store 位置不匹配(`ERR_PNPM_UNEXPECTED_STORE` /
|
|
209
|
-
* `ERR_PNPM_UNEXPECTED_VIRTUAL_STORE` / `Unexpected store location`)—— profile 目录里的依赖
|
|
210
|
-
* 是另一个大版本的 pnpm 生成的(或 profile 目录被复制/移动、virtual-store-dir 配置变化),
|
|
211
|
-
* 当前 pnpm 出于安全不认 → 提示清理 profile 依赖目录后用当前 pnpm 重建
|
|
212
|
-
* - pnpmWorkspace:pnpm 拒绝在 workspace 根目录下安装(`ERR_PNPM_ADDING_TO_ROOT`)—— profile
|
|
213
|
-
* 目录含 pnpm-workspace.yaml 即被 pnpm 视为 workspace 根,宿主调 `pnpm add` 时未声明在根
|
|
214
|
-
* 操作(缺 `-w`/`--workspace-root`),整条命令被拒 → 提示在 profile 的 .npmrc 里加
|
|
215
|
-
* `ignore-workspace-root-check=true` 或升级宿主
|
|
216
|
-
* - originRejected:请求的来源没通过本地 hub 服务的校验(服务端 403,正文是裸的
|
|
217
|
-
* `untrusted origin`)—— 请求在进入安装流程前就被拒,与 pnpm 无关;常见于用非 localhost 的
|
|
218
|
-
* 地址(如局域网 IP)打开市场页面,或宿主页面的 origin 未被识别 → 提示从本机地址打开市场后重试
|
|
219
|
-
* - pnpmPolicy:pnpm 的供应链安全策略拒绝安装(缺少 tarball integrity、`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` /
|
|
220
|
-
* `Minimum release age` —— 锁文件里包的发布时间还不满 24 小时被拒;`untrusted origin` ——
|
|
221
|
-
* 依赖来源未被本机 pnpm 信任)→ 提示按子场景给解法(发布未满 24 小时 → `minimumReleaseAge: 0`
|
|
222
|
-
* 豁免或等满 24 小时;untrusted origin → 删除 profile 的 node_modules + pnpm-lock.yaml 清掉
|
|
223
|
-
* 不受信任来源后重装)
|
|
224
|
-
* - pnpmUnusedPatch:profile 里留着指向旧版本 dsh-plugin 的 patch 声明,本次安装解析到的版本
|
|
225
|
-
* 已经不是它(`ERR_PNPM_UNUSED_PATCH` / `The following patches were not used: dsh-plugin@1.4.2`)
|
|
226
|
-
* —— pnpm 发现补丁没被用上即中止整次安装 → 提示删掉该条目后重试
|
|
227
|
-
* - pnpmMissingDep:profile 里留着一条指向 registry 上不存在的包的依赖,pnpm 解析时收到 404
|
|
228
|
-
* (`ERR_PNPM_FETCH_404` / `Not Found - 404`)—— 该 profile 的任何安装都会先卡在这条依赖上
|
|
229
|
-
* → 提示按包名删掉该条目后重试(404 的包通常不是正在安装的插件,故不引导提 Issue)
|
|
230
|
-
* - fileLocked:pnpm 无法替换 profile 里被其他进程占用的文件(Windows `os error 32`
|
|
231
|
-
* 「另一个程序正在使用此文件」/ `EBUSY` / `resource busy or locked`)—— 通常是宿主进程或
|
|
232
|
-
* 杀毒软件实时扫描持有句柄 → 提示完全退出宿主后重试
|
|
233
|
-
* - accessDenied:pnpm 在 profile 里替换文件时被系统拒绝写入(Windows `os error 5`
|
|
234
|
-
* 「拒绝访问」= ERROR_ACCESS_DENIED / Node 的 `EPERM` `EACCES` / `Access is denied`)——
|
|
235
|
-
* 文件/目录被其他进程占用、只读属性、目录 ACL 受限,或杀毒软件实时防护拦截写入
|
|
236
|
-
* → 提示退出宿主 / 关杀软后重试,仍失败则检查只读属性并以管理员身份运行
|
|
237
|
-
* - fsUnavailable:本机文件系统层面根本写不进去 —— 磁盘/分区空间耗尽(`ENOSPC` /
|
|
238
|
-
* `no space left on device`,Windows 为 `os error 112`)、目标分卷或挂载点为只读
|
|
239
|
-
* (`EROFS` / `read-only file system`,Windows 写保护为 `os error 19`)、进程可用的文件句柄
|
|
240
|
-
* 被耗尽(`EMFILE` / `ENFILE` / `too many open files`),都发生在 pnpm 落盘阶段
|
|
241
|
-
* → 提示按报错代码对号入座(清空间 / 换可写目录 / 重启宿主释放句柄)
|
|
242
|
-
* - network:安装前连通性预检拦截(服务端 `[network]` 标记)、底层连接失败
|
|
243
|
-
* (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝),或
|
|
244
|
-
* registry tarball 拉取失败(`fetch failed` / `GET …/-/…tgz error (n)` —— 常见于本机
|
|
245
|
-
* npm/pnpm 的 registry 被指向内网/自定义源,该源取不到包),或界面请求压根没送达宿主服务
|
|
246
|
-
* (浏览器 fetch 的 `Failed to fetch` —— 宿主未就绪/正在重启,或本地代理拦了回环地址)
|
|
247
|
-
* → 提示检查网络,registry 指向自定义源时给出换源指引
|
|
248
|
-
*
|
|
249
|
-
* 插件分发/依赖问题(引导去仓库提 Issue):
|
|
250
|
-
* - pnpmIgnoredBuild:插件自身或依赖的构建脚本被 pnpm 安全白名单(allowBuilds)默认拦截
|
|
251
|
-
* (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`)—— 只影响带安装期
|
|
252
|
-
* 构建的插件 → 建议改用预编译版本或关闭安装期构建
|
|
253
|
-
* - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或构建产物
|
|
254
|
-
* 导致),或装后校验发现 package.json 声明的入口文件不在发布物里(服务端 `[packaging]` 标记)
|
|
255
|
-
* —— 属插件打包/分发问题
|
|
256
|
-
* - repo:其余失败,默认按插件仓库问题处理
|
|
190
|
+
* 失败归类:把安装输出归到具体成因,供弹窗文案与是否引导提 Issue 使用。
|
|
191
|
+
* 分界是「本机环境问题」(装任何插件都会同样失败 → 不引导提 Issue)与「插件分发问题」
|
|
192
|
+
* (只影响这个插件 → 才引导提 Issue)。各分支判定顺序有依赖,见函数内注释。
|
|
257
193
|
*/
|
|
258
194
|
export function classifyFailure(message: string): FailureKind {
|
|
259
|
-
// npm
|
|
260
|
-
//
|
|
261
|
-
// 「插件打包分发问题」,误导用户去提 Issue
|
|
195
|
+
// npm 自身崩溃(edgesOut)或服务端标记的本机 npm 版本过低。必须最先判,否则会被外层
|
|
196
|
+
// ERR_PNPM_PREPARE_PACKAGE 吞成「插件打包问题」
|
|
262
197
|
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
263
|
-
//
|
|
264
|
-
// POSIX「pnpm: command not found」/ spawn pnpm ENOENT / Windows cmd 中英文报错):
|
|
265
|
-
// 本机缺 pnpm。必须在 dshMissing 之前 —— dshMissing 正则含裸「command not found」,
|
|
266
|
-
// 会把 `pnpm: command not found` 吞成「dsh 缺失」,误引导用户去装 DSH
|
|
198
|
+
// 本机缺 pnpm。必须在 dshMissing 之前 —— 后者的裸「command not found」会吞掉 pnpm 缺失
|
|
267
199
|
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'
|
|
268
|
-
//
|
|
269
|
-
// POSIX「git: command not found」/ spawn git ENOENT):本机 Git 未安装或不在 PATH。
|
|
270
|
-
// 必须在 dshMissing 之前 —— dshMissing 正则含裸「is not recognized / command not found」,
|
|
271
|
-
// 会把 git 缺失('git' is not recognized as an internal or external command)吞成「dsh 缺失」,
|
|
272
|
-
// 误导用户去装 DSH
|
|
200
|
+
// 本机缺 git。必须在 dshMissing 之前,原因同上(裸「is not recognized」会吞掉 git 缺失)
|
|
273
201
|
if (/\[git-missing\]|spawn git ENOENT|'git' 不是内部或外部命令|"git" 不是内部或外部命令|git['"]?\s*is not recognized|git: command not found/i.test(message)) return 'gitMissing'
|
|
274
|
-
//
|
|
275
|
-
// 「'npm' is not recognized」/ POSIX「npm: command not found」/ spawn npm ENOENT):本机 npm 未安装
|
|
276
|
-
// 或不在 PATH,不是插件问题。必须在 dshMissing 之前 —— dshMissing 正则含裸「is not recognized /
|
|
277
|
-
// command not found」,会把 npm 缺失('npm' is not recognized)吞成「dsh 缺失」,误导用户去装 DSH
|
|
202
|
+
// 本机缺 npm。必须在 dshMissing 之前,原因同上
|
|
278
203
|
if (/\[npm-missing\]|spawn npm ENOENT|'npm' 不是内部或外部命令|"npm" 不是内部或外部命令|npm['"]?\s*is not recognized|npm: command not found/i.test(message)) return 'npmMissing'
|
|
279
|
-
//
|
|
280
|
-
// 经 UTF-8 解码成乱码无法匹配原文,故服务端在 spawn 前用 which/where 探测并打 ASCII 标记;
|
|
281
|
-
// 其余形态:Windows cmd「不是内部或外部命令」/ POSIX「command not found」/
|
|
282
|
-
// node spawn ENOENT):是本机 DSH 未正确安装或不在 PATH —— 必须先判,
|
|
283
|
-
// 否则会被外层 "Command failed" 吞成「插件打包问题」,误导用户去提 Issue
|
|
204
|
+
// 本机缺 dsh。服务端 [dsh-missing] 标记乱码免疫(Windows GBK 解码后无法匹配原文)
|
|
284
205
|
if (/\[dsh-missing\]|不是内部或外部命令|is not recognized as an internal or external command|command not found|spawn dsh ENOENT/i.test(message)) return 'dshMissing'
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
// 被复制/移动、virtual-store-dir 配置变化导致 virtual store 位置不匹配),当前 pnpm 出于安全
|
|
288
|
-
// 不认 —— 任何插件装进该 profile 都会同样失败 → 提示清理依赖目录用当前 pnpm 重建。
|
|
289
|
-
// 必须在 pnpmMissing 之后 —— pnpm 在(能跑起来报错),不是「找不到命令」。
|
|
206
|
+
// store 版本不匹配:profile 依赖由另一大版本 pnpm 生成,当前 pnpm 不认。
|
|
207
|
+
// 必须在 pnpmMissing 之后(pnpm 能跑起来报错,不是找不到命令)
|
|
290
208
|
if (/ERR_PNPM_UNEXPECTED_(VIRTUAL_)?STORE|Unexpected (virtual )?store location/i.test(message)) return 'pnpmStore'
|
|
291
|
-
// pnpm
|
|
292
|
-
// pnpm workspace 根,宿主调 pnpm add 时没声明在根操作(缺 -w/--workspace-root),
|
|
293
|
-
// pnpm 直接整条命令拒绝 —— 本机 profile/宿主调用方式问题,任何插件都装不上
|
|
209
|
+
// pnpm 拒绝往 workspace 根装依赖:profile 被视为 workspace 根但未声明根操作
|
|
294
210
|
if (/ERR_PNPM_ADDING_TO_ROOT|add the dependency to the workspace root/i.test(message)) return 'pnpmWorkspace'
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
// 必须放在 pnpmPolicy 之前 —— 后者正则含同名裸文本,会把这条吞成「pnpm 供应链策略」,
|
|
298
|
-
// 并建议用户删掉 profile 的 node_modules + pnpm-lock.yaml(无效且具破坏性)。
|
|
299
|
-
// 用「整行以它结尾」来区隔 pnpm 的真实报错:pnpm 的形态后面一定跟着 dsh 的上下文
|
|
300
|
-
// (如 `untrusted origin\ndsh: pnpm failed in profile directory …`),不会被本规则命中
|
|
211
|
+
// 本地服务来源校验拒绝(403 untrusted origin),请求没进安装流程。必须在 pnpmPolicy 之前
|
|
212
|
+
// —— 后者同名裸文本会把这条误判成供应链策略。用「整行以它结尾」区隔 pnpm 真实报错
|
|
301
213
|
if (/(^|\n)untrusted origin\s*$/.test(message)) return 'originRejected'
|
|
302
|
-
// pnpm
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
if (missingTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|Minimum release age|untrusted origin/i.test(message)) return 'pnpmPolicy'
|
|
307
|
-
// profile 里留着指向旧版本 dsh-plugin 的 patch 声明(ERR_PNPM_UNUSED_PATCH /
|
|
308
|
-
// `The following patches were not used: dsh-plugin@1.4.2`):本机 pnpm 配置与本次解析到的版本
|
|
309
|
-
// 对不上,pnpm 出于安全直接中止整次安装 —— 任何插件都装不进来 → 提示删条目后重试。
|
|
310
|
-
// 必须在兜底之前,否则被归成「插件侧失败」引导去提 Issue。
|
|
214
|
+
// pnpm 供应链策略拦截(缺 tarball integrity / minimumReleaseAge / untrusted origin)。
|
|
215
|
+
// 必须在 pnpmIgnoredBuild 之前
|
|
216
|
+
if (missingTarballIntegrity(message) || /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|ERR_PNPM_NO_MATURE_MATCHING_VERSION|Minimum release age|minimumReleaseAge|untrusted origin/i.test(message)) return 'pnpmPolicy'
|
|
217
|
+
// profile 残留旧版本 patch 声明,pnpm 中止安装。必须在兜底之前
|
|
311
218
|
if (/ERR_PNPM_UNUSED_PATCH|patches were not used/i.test(message)) return 'pnpmUnusedPatch'
|
|
312
|
-
// profile
|
|
313
|
-
// 「另一个程序正在使用此文件,进程无法访问」/ EBUSY / resource busy or locked)——
|
|
314
|
-
// 通常是宿主进程或杀毒软件实时扫描持有句柄;`os error 32` 用 ASCII 特征,中文原文乱码也能命中
|
|
315
|
-
// → 提示完全退出宿主后重试
|
|
219
|
+
// profile 文件被其他进程占用(os error 32 / EBUSY)。ASCII 特征免疫乱码
|
|
316
220
|
if (/os error 32|EBUSY|resource busy or locked|being used by another process/i.test(message)) return 'fileLocked'
|
|
317
|
-
// profile
|
|
318
|
-
// Node 的 EPERM、EACCES / 英文 `Access is denied`):占用、只读属性、目录 ACL 或杀软拦截,
|
|
319
|
-
// 任何插件都装不上。`os error 5` 只认 ASCII 特征 —— 中文原文经 GBK→UTF-8 解码会残缺,
|
|
320
|
-
// 不能依赖「拒绝访问」四个字
|
|
221
|
+
// 系统拒绝写 profile(os error 5 / EPERM / EACCES)。ASCII 特征免疫乱码
|
|
321
222
|
if (/os error 5\b|ERROR_ACCESS_DENIED|\bEPERM\b|\bEACCES\b|access is denied/i.test(message)) return 'accessDenied'
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// 文件句柄耗尽(EMFILE / ENFILE / too many open files)。都在 pnpm 落盘阶段发生,
|
|
325
|
-
// 空间不足、盘只读或句柄不够时任何插件都装不进来,不是插件问题 ——
|
|
326
|
-
// 必须在 network / prepare 之前判:磁盘满时 pnpm 会在日志尾部混着重试与超时特征,
|
|
327
|
-
// 先判网络会把「本机磁盘写不进去」误报成「你的网络不通」
|
|
223
|
+
// 文件系统写不进去(ENOSPC/EROFS/EMFILE/ENFILE)。必须在 network 之前判:
|
|
224
|
+
// 磁盘满时日志尾部混超时特征,先判网络会误报成「网络不通」
|
|
328
225
|
if (/os error 112\b|os error 19\b|ERROR_DISK_FULL|ERROR_WRITE_PROTECT|\bENOSPC\b|\bEROFS\b|\bEMFILE\b|\bENFILE\b|no space left on device|read-only file system|too many open files/i.test(message)) return 'fsUnavailable'
|
|
329
|
-
// 构建脚本被
|
|
330
|
-
// 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
|
|
331
|
-
// 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
|
|
332
|
-
// 被拦 —— 必须在 network 判定之前:日志尾部常混着重试残留的连接失败特征
|
|
333
|
-
// (ETIMEDOUT / Failed to connect 等),若先判网络会把「插件分发问题」误报成「你的网络不通」。
|
|
226
|
+
// 构建脚本被 allowBuilds 拦截。必须在 network 之前判:日志尾部混重试残留的连接失败特征
|
|
334
227
|
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/i.test(message)) return 'pnpmIgnoredBuild'
|
|
335
|
-
//
|
|
336
|
-
// 连接超时/拒绝/重置、DNS 解析失败、TLS/SSL 握手失败,或 registry tarball 拉取失败
|
|
337
|
-
// —— `fetch failed` / `GET https://…/-/…tgz error (n)` 是 pnpm 下载包文件时的网络层失败,
|
|
338
|
-
// 常见于本机 npm/pnpm registry 被指向内网/自定义源而取不到包)—— 是本机网络/代理/源
|
|
339
|
-
// 配置问题,不是插件问题。必须在 prepare/Command failed 判定之前:git fetch 失败常被
|
|
340
|
-
// 外层包成 "Command failed: git fetch ...",先按网络特征归类才不会误判成插件问题。
|
|
341
|
-
// 404 类「目标不存在」不含这些特征,仍归 repo(那是仓库/包的问题)。
|
|
342
|
-
// 注意:fetch failed 只在 pnpm 拉取阶段出现;prepare/构建已跑起来(tarball 到手)的
|
|
343
|
-
// 插件问题不带此特征,不会误伤(allowBuilds 拦截在上一分支先判)。
|
|
344
|
-
// `Failed to fetch` 是浏览器 fetch 的 TypeError(词序与 Node 的 fetch failed 相反):界面
|
|
345
|
-
// 请求根本没送达宿主服务(宿主未就绪/正在重启、本地代理拦了回环地址)。
|
|
228
|
+
// 网络/代理/源配置问题。必须在 prepare 之前判:git fetch 失败常被外层包成 "Command failed"
|
|
346
229
|
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'
|
|
347
|
-
//
|
|
348
|
-
// 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
|
|
230
|
+
// 装后校验发现入口文件缺失([packaging])= git 分发缺构建产物
|
|
349
231
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
350
|
-
//
|
|
351
|
-
// 是被明确告知「这个包不存在」—— 属于 profile 的本地依赖问题,不是插件问题。profile 里留着
|
|
352
|
-
// 一条指向未发布包的依赖条目时,该 profile 的**任何**安装都会先卡在这条依赖上。
|
|
353
|
-
// 必须在 pluginPrepare 之前判定:此时宿主尾部只有通用的 `dsh: plugin command failed`,
|
|
354
|
-
// 落到下面会被当成构建失败。
|
|
355
|
-
// (两个形态:pnpm 的 `[ERR_PNPM_FETCH_404] GET <url>: Not Found - 404` 与 npm 的
|
|
356
|
-
// `npm error 404 Not Found - GET <url> - Not found`)
|
|
232
|
+
// registry 上不存在的依赖(404),profile 任何安装都会卡在这条。必须在 pluginPrepare 之前判
|
|
357
233
|
if (/ERR_PNPM_FETCH_404|404 Not Found|Not Found - 404/i.test(message)) return 'pnpmMissingDep'
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
//
|
|
234
|
+
// 要装的版本在实际安装源里没有(镜像滞后或版本写错)。必须在 pluginPrepare 之前判
|
|
235
|
+
if (/\bERR_PNPM_NO_MATCHING_VERSION\b|\bETARGET\b|No matching version found/i.test(message)) return 'registryVersionStale'
|
|
236
|
+
// profile 残留指向不存在目录的本地依赖(link:/file:)。必须在 pluginPrepare 之前判
|
|
237
|
+
if (/\bERR_PNPM_LINKED_PKG_DIR_NOT_FOUND\b|Could not install from .+ as it does not exist/i.test(message)) return 'pnpmLinkedDirMissing'
|
|
238
|
+
// 构建脚本真的跑挂了才算插件问题。Command failed 区分大小写:只匹配 pnpm 的包装,
|
|
239
|
+
// 不匹配宿主那句小写 dsh: plugin command failed
|
|
361
240
|
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|prepare-guard/i.test(message) || /Command failed/.test(message)) return 'pluginPrepare'
|
|
362
|
-
//
|
|
363
|
-
return '
|
|
241
|
+
// 其余未知失败:引导到插件中心仓库排查
|
|
242
|
+
return 'hub'
|
|
364
243
|
}
|
|
365
244
|
|
|
366
245
|
/** 从服务端 `[npm-too-low]` 标记行提取本机 npm 版本(如 `[npm-too-low] npm@11.3.0` → "11.3.0");
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* URL stays within GitHub's 8192-byte request limit.
|
|
9
9
|
*/
|
|
10
10
|
import type { EnvInfo, HubPlugin, LocaleId } from '../types.ts'
|
|
11
|
-
import { GITHUB_URL, PLUGIN_VERSION, SITE_URL } from './constants.ts'
|
|
11
|
+
import { GITHUB_URL, HUB_REPO, PLUGIN_VERSION, SITE_URL } from './constants.ts'
|
|
12
12
|
import { MAX_CORE_CHARS, classifyFailure, coreErrorCode, pnpmPolicyHintOf, summarizeError } from './failures.ts'
|
|
13
13
|
import { executedCommandOf } from './install-command.ts'
|
|
14
14
|
import type { FailureKind } from './failures.ts'
|
|
@@ -55,12 +55,15 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
55
55
|
case 'pnpmPolicy': return 'pnpm supply-chain policy blocked the install on the user machine'
|
|
56
56
|
case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
|
|
57
57
|
case 'pnpmMissingDep': return 'dependency not found on the npm registry on the user machine'
|
|
58
|
+
case 'registryVersionStale': return 'requested version not available on the npm registry in use on the user machine'
|
|
59
|
+
case 'pnpmLinkedDirMissing': return 'dangling local (link:) dependency in the user profile'
|
|
58
60
|
case 'fileLocked': return 'file locked by another process on the user machine'
|
|
59
61
|
case 'accessDenied': return 'file write access denied on the user machine'
|
|
60
62
|
case 'fsUnavailable': return 'local file system unavailable on the user machine'
|
|
61
63
|
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
62
64
|
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
63
65
|
case 'network': return 'network failure on the user side'
|
|
66
|
+
case 'hub': return 'install failed (cause unclassified)'
|
|
64
67
|
default: return 'plugin install failed'
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -77,13 +80,13 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
77
80
|
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
78
81
|
// 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
|
|
79
82
|
// [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
|
|
80
|
-
// (package.json
|
|
83
|
+
// (package.json 声明的文件在发布物里缺失),作者照此补齐即可。
|
|
81
84
|
const kind = classifyFailure(message)
|
|
82
85
|
const executed = executedCommandOf(command, attempts)
|
|
83
86
|
// 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
|
|
84
87
|
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
85
88
|
const reason = /\[packaging\]/i.test(message)
|
|
86
|
-
? 'plugin distribution is incomplete — the entry
|
|
89
|
+
? 'plugin distribution is incomplete — a file the package needs (its entry from main / exports, an entry a bundled patch mounts, or the patch declared by dsh.bundle.patch) is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
|
|
87
90
|
: kind === 'network'
|
|
88
91
|
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
89
92
|
: kind === 'pluginPrepare'
|
|
@@ -129,7 +132,7 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
129
132
|
? [
|
|
130
133
|
'',
|
|
131
134
|
'## 安装方式说明',
|
|
132
|
-
'DSH 插件支持三种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)、`dsh plugin add git+https://github.com/owner/repo.git`(Git 直装,仓库需提交构建产物),以及预构建 release 包 `dsh plugin add https://github.com/owner/repo/releases/download/<tag>/<asset>.tgz`(仓库不便提交构建产物时的推荐方式)。注意:pnpm ≥10 默认拦截依赖构建脚本,Git 直装不会执行包内 `prepare
|
|
135
|
+
'DSH 插件支持三种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)、`dsh plugin add git+https://github.com/owner/repo.git`(Git 直装,仓库需提交构建产物),以及预构建 release 包 `dsh plugin add https://github.com/owner/repo/releases/download/<tag>/<asset>.tgz`(仓库不便提交构建产物时的推荐方式)。注意:pnpm ≥10 默认拦截依赖构建脚本,Git 直装不会执行包内 `prepare`,请勿依赖它生成产物。当前插件的分发物缺少它需要的文件(`main` / `exports` 声明的入口、补丁按本包名 mount 的入口,或 `dsh.bundle.patch` 声明的补丁),请提交构建产物、改用 release `.tgz` 直链,或发布完整的 npm 包。',
|
|
133
136
|
]
|
|
134
137
|
: []),
|
|
135
138
|
'',
|
|
@@ -180,3 +183,71 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
180
183
|
}
|
|
181
184
|
return url
|
|
182
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 插件中心自身仓库的预填 Issue 链接:用于「非插件侧」失败(无法识别的未知原因、
|
|
189
|
+
* 以及 pnpm allowBuilds 拦截这类不再归到插件作者的情形)。正文带上目标插件与完整
|
|
190
|
+
* 错误摘要,供插件中心维护者分诊 —— 不再把环境问题/插件中心自身问题误投给插件作者。
|
|
191
|
+
*/
|
|
192
|
+
export function hubIssueUrl(pluginRepo: string | null, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
193
|
+
const kind = classifyFailure(message)
|
|
194
|
+
const executed = executedCommandOf(command, attempts)
|
|
195
|
+
const repo = pluginRepo && pluginRepo.trim() !== '' ? pluginRepo.trim() : 'unknown'
|
|
196
|
+
const title = `[dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
197
|
+
const code = coreErrorCode(message)
|
|
198
|
+
const build = (coreChars: number): string => {
|
|
199
|
+
const body = [
|
|
200
|
+
'## Summary',
|
|
201
|
+
`- Auto-classified cause: ${reasonTitleOf(kind)}`,
|
|
202
|
+
`- Target plugin: \`${repo}\``,
|
|
203
|
+
...(code ? [`- Key error: \`${code}\``] : []),
|
|
204
|
+
// 为什么投递到这里而非插件仓库:只有确有证据是插件自身构建失败(pluginPrepare)才投插件仓库
|
|
205
|
+
'- Why filed here: the installer only routes a failure to the plugin repo when there is positive evidence the plugin build itself failed (pluginPrepare). This failure does not match that, so it is routed here for triage.',
|
|
206
|
+
'',
|
|
207
|
+
`## [DSH Plugin 插件市场](${SITE_URL}) · 安装 Plugin 失败错误信息`,
|
|
208
|
+
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次安装失败一并提交。`,
|
|
209
|
+
`- 实际执行的安装命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
|
|
210
|
+
`- 执行结果:安装失败,未能安装该插件。`,
|
|
211
|
+
...(attempts && attempts.length > 0
|
|
212
|
+
? ['', '## Attempted install channels(已尝试的安装方式)', ...attempts.map((a) => `- ${a}`)]
|
|
213
|
+
: []),
|
|
214
|
+
'',
|
|
215
|
+
'## Environment',
|
|
216
|
+
`- Plugin: \`${repo}\``,
|
|
217
|
+
...(env
|
|
218
|
+
? [
|
|
219
|
+
`- DSH: ${env.dshVersion ? `v${env.dshVersion}` : 'unknown'}`,
|
|
220
|
+
`- Plugin Hub: v${PLUGIN_VERSION}`,
|
|
221
|
+
`- Node: ${env.nodeVersion}`,
|
|
222
|
+
`- pnpm: ${env.pnpmVersion ? `v${env.pnpmVersion}` : 'unknown'}`,
|
|
223
|
+
`- npm: ${env.npmVersion ? `v${env.npmVersion}` : 'unknown'}`,
|
|
224
|
+
`- git: ${env.gitVersion ? `v${env.gitVersion}` : 'unknown'}`,
|
|
225
|
+
`- OS: ${env.platform} ${env.arch} (${env.release})`,
|
|
226
|
+
`- Profile: \`${env.profile}\``,
|
|
227
|
+
`- DSH Home: \`${env.dshHome}\``,
|
|
228
|
+
`- Full log: \`~/.dsh/profiles/${env.profile}/hub.log\` (paste or attach for the full output)`,
|
|
229
|
+
]
|
|
230
|
+
: []),
|
|
231
|
+
...(repo !== 'unknown' ? [`- Catalog: [${pluginSiteUrl(repo)}](${pluginSiteUrl(repo)})`] : []),
|
|
232
|
+
'',
|
|
233
|
+
'## Error (core)',
|
|
234
|
+
'',
|
|
235
|
+
'```',
|
|
236
|
+
summarizeError(message, coreChars),
|
|
237
|
+
'```',
|
|
238
|
+
'',
|
|
239
|
+
'---',
|
|
240
|
+
`This error was produced by the [dsh-plugin-hub](${GITHUB_URL}) installer bundled with DeepSeek Harness.`,
|
|
241
|
+
].join('\n')
|
|
242
|
+
return `https://github.com/${HUB_REPO}/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`
|
|
243
|
+
}
|
|
244
|
+
const MAX_URL_CHARS = 7600
|
|
245
|
+
let url = build(MAX_CORE_CHARS)
|
|
246
|
+
if (url.length > MAX_URL_CHARS) {
|
|
247
|
+
for (const budget of [4000, 2500, 1400, 800, 400]) {
|
|
248
|
+
url = build(budget)
|
|
249
|
+
if (url.length <= MAX_URL_CHARS) break
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return url
|
|
253
|
+
}
|
package/src/client/types.ts
CHANGED
|
@@ -116,7 +116,7 @@ export interface HubUpdateInfo {
|
|
|
116
116
|
* - `pending` 远端有更新但发布未满门槛(见 useCatalog)→ 暂缓提示并说明原因
|
|
117
117
|
* - `latest` 与远端最新版一致 → 确已最新
|
|
118
118
|
* 「有新版但暂缓提示」必须与「确已最新」分开:两者若共用文案,弹窗会在本机版本落后时
|
|
119
|
-
* 谎称「你正在使用 v{远端最新版}
|
|
119
|
+
* 谎称「你正在使用 v{远端最新版},已是最新」。
|
|
120
120
|
*/
|
|
121
121
|
export type HubUpdateState = 'none' | 'available' | 'pending' | 'latest'
|
|
122
122
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* Catalog proxy routes: server-side caching of the directory/stats payloads
|
|
7
|
+
* and profile-level installed-package reads shared with the install/task modules.
|
|
8
|
+
*/
|
|
9
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import { dirname, join } from 'node:path'
|
|
12
|
+
import { githubRepoOf } from '../services/install/install.ts'
|
|
13
|
+
import { loadSettings } from '../services/settings.ts'
|
|
14
|
+
import { fetchViaCurl, systemProxy } from '../services/probe.ts'
|
|
15
|
+
import { CATALOG_CACHE_TTL_MS, requireMethod, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
16
|
+
|
|
17
|
+
/** 磁盘缓存内容:原始响应字符串 + 写入时间戳(TTL 判定用)。 */
|
|
18
|
+
interface CatalogCacheEntry {
|
|
19
|
+
at: number
|
|
20
|
+
body: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function catalogCacheFile(profile: string, key: string): string {
|
|
24
|
+
return join(profileDirectory(profile), 'cache', `catalog-${key}.json`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
|
|
28
|
+
* maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
|
|
29
|
+
function readCatalogCache(file: string, maxAgeMs: number): string | null {
|
|
30
|
+
try {
|
|
31
|
+
const entry = JSON.parse(readFileSync(file, 'utf8')) as CatalogCacheEntry
|
|
32
|
+
if (entry && typeof entry.at === 'number' && typeof entry.body === 'string'
|
|
33
|
+
&& Date.now() - entry.at < maxAgeMs) {
|
|
34
|
+
return entry.body
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// 文件缺失 / 解析失败 / 字段不完整 → miss,重新拉取覆盖写盘
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 写缓存:目录不存在则创建;写失败静默忽略(缓存只是加速,不影响功能)。 */
|
|
43
|
+
function writeCatalogCache(file: string, body: string): void {
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
46
|
+
writeFileSync(file, JSON.stringify({ at: Date.now(), body } satisfies CatalogCacheEntry))
|
|
47
|
+
} catch {
|
|
48
|
+
// 磁盘只读/无权限等极端情况:忽略,继续走直连
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function profileDirectory(profile: string): string {
|
|
53
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Read non-official dependencies installed into one profile. */
|
|
57
|
+
export function readInstalled(profile: string): Record<string, string> {
|
|
58
|
+
try {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8')) as {
|
|
60
|
+
dependencies?: Record<string, string>
|
|
61
|
+
}
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(manifest.dependencies ?? {}).filter(([name]) => !name.startsWith('@deepseek-ai/')),
|
|
64
|
+
)
|
|
65
|
+
} catch {
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 读已装依赖实际落在磁盘上的仓库身份(`owner/repo`):解析 node_modules/<name>/package.json
|
|
71
|
+
* 的 repository 字段。已装 spec 可能是纯版本号(丢了仓库身份),但真实包元数据仍指向来源仓库,
|
|
72
|
+
* 据此可区分「同名依赖其实是同一仓库的残留(应转 update)」还是「跨仓库包名冲突(应报错)」。
|
|
73
|
+
* 读不到 / 解析不出返回 null(交给调用方按冲突处理)。 */
|
|
74
|
+
export function installedRepoOf(profile: string, name: string): string | null {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'node_modules', name, 'package.json'), 'utf8')) as {
|
|
77
|
+
repository?: string | { url?: string }
|
|
78
|
+
}
|
|
79
|
+
const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url
|
|
80
|
+
return repo ? githubRepoOf(repo) : null
|
|
81
|
+
} catch {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function registerCatalogRoutes(webServer: WebServerService, profile: string): WebRoute[] {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
kind: 'exact',
|
|
90
|
+
path: '/dsh-plugin-hub/catalog',
|
|
91
|
+
handler: async (request, response) => {
|
|
92
|
+
if (!requireMethod(request, response, 'GET')) return
|
|
93
|
+
// 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
|
|
94
|
+
// 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
|
|
95
|
+
// 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
|
|
96
|
+
const url = new URL(request.url ?? '/', 'http://localhost')
|
|
97
|
+
const settings = loadSettings(profile)
|
|
98
|
+
// 代理优先级与安装/诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连
|
|
99
|
+
const proxy = settings.proxy !== ''
|
|
100
|
+
? settings.proxy
|
|
101
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
102
|
+
const isStats = url.searchParams.get('stats') === '1'
|
|
103
|
+
const lang = url.searchParams.get('lang') === 'en' ? 'en' : 'zh'
|
|
104
|
+
const target = isStats
|
|
105
|
+
? 'https://api.dsh-plugin.org/stats.json'
|
|
106
|
+
: `https://api.dsh-plugin.org/plugins.${lang}.json`
|
|
107
|
+
// 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl 拉远程
|
|
108
|
+
const cacheFile = catalogCacheFile(profile, isStats ? 'stats' : `plugins-${lang}`)
|
|
109
|
+
const cached = readCatalogCache(cacheFile, CATALOG_CACHE_TTL_MS)
|
|
110
|
+
if (cached !== null) {
|
|
111
|
+
try {
|
|
112
|
+
sendJson(response, 200, JSON.parse(cached))
|
|
113
|
+
return
|
|
114
|
+
} catch {
|
|
115
|
+
// 缓存正文损坏:忽略,走重新拉取并覆盖写盘
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const r = await fetchViaCurl(target, proxy, 20000)
|
|
119
|
+
if (!r.ok || r.body === '') {
|
|
120
|
+
// 实时拉取失败:退回任意年龄的本地缓存(上次成功数据),
|
|
121
|
+
// 避免证书吊销受阻 / 网络抖动时整个插件市场打不开 —— 数据可能过期,但好过空白。
|
|
122
|
+
const stale = readCatalogCache(cacheFile, Number.POSITIVE_INFINITY)
|
|
123
|
+
if (stale !== null) {
|
|
124
|
+
try {
|
|
125
|
+
sendJson(response, 200, JSON.parse(stale))
|
|
126
|
+
return
|
|
127
|
+
} catch {
|
|
128
|
+
// 兜底缓存也损坏:落到下面的 502
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
sendJson(response, 502, { error: 'catalog fetch failed' })
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
sendJson(response, 200, JSON.parse(r.body))
|
|
136
|
+
writeCatalogCache(cacheFile, r.body)
|
|
137
|
+
} catch {
|
|
138
|
+
sendJson(response, 502, { error: 'catalog fetch returned invalid JSON' })
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
]
|
|
143
|
+
}
|