dsh-plugin 1.3.6 → 1.3.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 +43 -18
- package/README.md +43 -18
- package/client/client.js +465 -324
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +84 -5
- package/lib/services/probe.js +43 -0
- package/lib/types/services/probe.d.ts +11 -0
- package/package.json +3 -3
- package/src/client/components/PluginHubSection.tsx +31 -0
- package/src/client/components/modals/NotificationsModal.tsx +18 -2
- package/src/client/components/modals/modals.tsx +18 -3
- package/src/client/components/views/DiagnosticsView.tsx +14 -4
- package/src/client/components/views/SettingsView.tsx +75 -14
- package/src/client/hooks/useTaskQueue.ts +6 -2
- package/src/client/locales.ts +25 -2
- package/src/client/logic/failures.ts +38 -9
- package/src/client/logic/renderMarkdown.ts +2 -2
- package/src/client/logic/urls.ts +24 -17
- package/src/client/styles/Modal.module.css +48 -7
- package/src/client/styles/SettingsView.module.css +24 -0
- package/src/server/http/routes.ts +81 -6
- package/src/server/services/probe.ts +43 -0
package/src/client/locales.ts
CHANGED
|
@@ -110,6 +110,10 @@ export const zh = {
|
|
|
110
110
|
settingsCheckOnStartDesc: '打开插件中心时检查已安装插件是否有新版,发现更新会提示',
|
|
111
111
|
settingsProxy: 'HTTP 代理',
|
|
112
112
|
settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
|
|
113
|
+
// 代理连通性实时探测:输入停笔后提示检测结果(不通仍可保存,先填地址后开代理也合法)
|
|
114
|
+
proxyCheckChecking: '正在测试该代理地址…',
|
|
115
|
+
proxyCheckOk: '测试可达,安装与目录请求将统一走该代理',
|
|
116
|
+
proxyCheckFail: '此地址测试不通,仍可保存;请确认代理已开启、地址与端口无误',
|
|
113
117
|
settingsMirror: 'npm 镜像源',
|
|
114
118
|
settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
|
|
115
119
|
mirrorNone: '未配置(跟随本机 npm 配置)',
|
|
@@ -186,6 +190,7 @@ export const zh = {
|
|
|
186
190
|
diagNpmUnset: '未配置(跟随本机 npm 配置)',
|
|
187
191
|
diagGithub: 'GitHub',
|
|
188
192
|
diagCatalog: '目录站点',
|
|
193
|
+
diagProxy: 'HTTP 代理',
|
|
189
194
|
diagOk: 'OK',
|
|
190
195
|
diagChecking: '检测中…',
|
|
191
196
|
diagIdle: '未检测',
|
|
@@ -294,10 +299,16 @@ export const zh = {
|
|
|
294
299
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺构建产物、入口文件不存在 = 插件未适配官方默认安装方式
|
|
295
300
|
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物或发布 npm 版。',
|
|
296
301
|
// 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):只影响带原生模块的插件,属插件依赖打包问题,应提 Issue
|
|
297
|
-
failIgnoredBuild: '
|
|
302
|
+
failIgnoredBuild: '该插件或其依赖需要执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截导致安装失败(如原生模块 node-pty、或 git 安装时插件的 prepare 脚本)—— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)或移除安装期构建。',
|
|
298
303
|
// npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
|
|
299
304
|
failNpmTooLow: '怀疑本机 npm 版本过低:npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
300
305
|
failNpmTooLowV: '本机 npm 版本过低(当前 v{v}):npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
306
|
+
// 安装前连通性预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):本机网络或代理问题,不是插件问题,不引导提 Issue
|
|
307
|
+
failNetworkHint: '安装前检测到网络不通:无法连接到安装源(可能是断网、DNS 解析失败、被防火墙拦截,或代理配置有问题)。这不是插件本身的问题 —— 请检查网络连接,或到「设置」里确认 HTTP 代理配置,再到「系统诊断」运行一次连通性检测,然后重试。',
|
|
308
|
+
// 网络失败弹窗里的「无法访问 …」醒目行:直接告诉用户具体哪个地址连不上({url} 为提取的不可达地址)
|
|
309
|
+
failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
|
|
310
|
+
// 「去系统诊断」直达按钮:网络不通提示下方的品牌实底按钮,跳到设置 → 系统诊断跑连通性检测
|
|
311
|
+
failNetworkRunDiag: '去系统诊断检测网络',
|
|
301
312
|
reportIssue: '去插件仓库提交 Issue',
|
|
302
313
|
detail: '打开详情',
|
|
303
314
|
noResult: '没有找到匹配的插件',
|
|
@@ -445,6 +456,11 @@ export const en = {
|
|
|
445
456
|
settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
|
|
446
457
|
settingsProxy: 'HTTP proxy',
|
|
447
458
|
settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
|
|
459
|
+
// Live proxy reachability probe: reported after the user stops typing (an unreachable
|
|
460
|
+
// address can still be saved — users may fill it in before actually starting the proxy)
|
|
461
|
+
proxyCheckChecking: 'Testing this proxy address…',
|
|
462
|
+
proxyCheckOk: 'Reachable — installs and catalog requests will route through it',
|
|
463
|
+
proxyCheckFail: 'Unreachable, but you can still save it; make sure the proxy is running and the address and port are correct',
|
|
448
464
|
settingsMirror: 'npm mirror',
|
|
449
465
|
settingsMirrorDesc: 'Unset by default — follows local npm config; pick a domestic mirror if slow',
|
|
450
466
|
mirrorNone: 'Unset (follow local npm config)',
|
|
@@ -521,6 +537,7 @@ export const en = {
|
|
|
521
537
|
diagNpmUnset: 'Unset (follow local npm config)',
|
|
522
538
|
diagGithub: 'GitHub',
|
|
523
539
|
diagCatalog: 'Catalog site',
|
|
540
|
+
diagProxy: 'HTTP proxy',
|
|
524
541
|
diagOk: 'OK',
|
|
525
542
|
diagChecking: 'Checking…',
|
|
526
543
|
diagIdle: 'Not checked',
|
|
@@ -628,9 +645,15 @@ export const en = {
|
|
|
628
645
|
// Pre-install / post-install guard hit ([packaging]): git distribution lacks build output, entry file missing = not adapted to the official default install method
|
|
629
646
|
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output or an npm release.',
|
|
630
647
|
// native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): only affects plugins with native deps — a dependency/packaging issue of the plugin itself, file an issue
|
|
631
|
-
failIgnoredBuild: 'This plugin
|
|
648
|
+
failIgnoredBuild: 'This plugin (or its dependencies) needs to run build scripts, which pnpm blocks by default via its allowBuilds allowlist, so the install fails (e.g. native modules like node-pty, or the plugin\'s own prepare script on git installs) — other plugins are unaffected. This is a dependency/packaging issue of the plugin itself. Please report it to the author repository and ask the author to ship a prebuilt variant (e.g. node-pty-prebuilt-multiarch) or drop install-time builds.',
|
|
632
649
|
failNpmTooLow: 'Your local npm may be too old: npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
633
650
|
failNpmTooLowV: 'Your local npm is too old (v{v}): npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
651
|
+
// Pre-install connectivity probe / connection failure in the install log ([network] / git fetch timeout / DNS / TLS): a local network or proxy issue, not a plugin issue — no issue link
|
|
652
|
+
failNetworkHint: 'The network could not be reached before install: the install source is unreachable (offline, DNS failure, firewall block, or a misconfigured proxy). This is not a problem with the plugin itself — please check your connection, confirm the HTTP proxy in Settings, run a connectivity check under System Diagnostics, then retry.',
|
|
653
|
+
// Prominent "cannot reach …" line in the network-failure dialog: shows the exact unreachable address ({url})
|
|
654
|
+
failNetworkTarget: 'Cannot reach {url} — your network connection is down',
|
|
655
|
+
// Diagnostics shortcut button under the network-down hint: jumps to Settings → System Diagnostics
|
|
656
|
+
failNetworkRunDiag: 'Run a network diagnostic',
|
|
634
657
|
reportIssue: 'Report an Issue to the plugin repo',
|
|
635
658
|
detail: 'Open details',
|
|
636
659
|
noResult: 'No plugins match your search',
|
|
@@ -151,18 +151,22 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 失败归类,五态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
|
|
158
158
|
* 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
|
|
159
|
-
* 一律引导提 Issue
|
|
159
|
+
* 一律引导提 Issue;唯一的两个例外是本机环境问题(npm 版本过低 / 网络不通):
|
|
160
160
|
* - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
|
|
161
161
|
* 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
|
|
162
162
|
* 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
|
|
163
|
-
* -
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* - network:安装前连通性预检拦截(服务端 `[network]` 标记)或底层连接失败
|
|
164
|
+
* (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝)——
|
|
165
|
+
* 是本机网络不通/被墙/代理有问题,不是插件问题 → 提示检查网络,不引导提 Issue
|
|
166
|
+
* - pnpmIgnoredBuild:插件自身或依赖的构建脚本被 pnpm 安全白名单(allowBuilds)默认拦截
|
|
167
|
+
* (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`)。只影响带安装期
|
|
168
|
+
* 构建的插件,其他插件不受影响 —— 差异在插件的依赖/打包方式,属插件依赖/打包问题
|
|
169
|
+
* → 引导去仓库提 Issue(建议改用预编译版本或关闭安装期构建)
|
|
166
170
|
* - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
|
|
167
171
|
* 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
|
|
168
172
|
* - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
|
|
@@ -172,12 +176,22 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
172
176
|
// 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
|
|
173
177
|
// 「插件打包分发问题」,误导用户去提 Issue
|
|
174
178
|
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
179
|
+
// 构建脚本被 pnpm 白名单(allowBuilds)拦截:插件的 prepare 脚本或依赖里的原生模块构建
|
|
180
|
+
// 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
|
|
181
|
+
// 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
|
|
182
|
+
// 被拦 —— 必须在 network 判定之前:日志尾部常混着重试残留的连接失败特征
|
|
183
|
+
// (ETIMEDOUT / Failed to connect 等),若先判网络会把「插件分发问题」误报成
|
|
184
|
+
// 「你的网络不通」(graph-memory issues #82-#84:PREPARE_NOT_ALLOWED + 尾随超时)。
|
|
185
|
+
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/i.test(message)) return 'pnpmIgnoredBuild'
|
|
186
|
+
// 网络问题(服务端 [network] 标记,或安装日志里的连接失败特征:git fetch 失败、
|
|
187
|
+
// 连接超时/拒绝/重置、DNS 解析失败、TLS/SSL 握手失败)—— 是本机网络/代理问题,
|
|
188
|
+
// 不是插件问题。必须在 prepare/Command failed 判定之前:git fetch 失败常被
|
|
189
|
+
// 外层包成 "Command failed: git fetch ...",先按网络特征归类才不会误判成插件问题。
|
|
190
|
+
// 404 类「目标不存在」不含这些特征,仍归 repo(那是仓库/包的问题)。
|
|
191
|
+
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/i.test(message)) return 'network'
|
|
175
192
|
// 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
|
|
176
193
|
// 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
|
|
177
194
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
178
|
-
// 原生模块构建被忽略(IGNORED_BUILDS)要在 prepare 判定之前:dsh 尾部固定提示语带
|
|
179
|
-
// allowBuilds/pnpm-workspace.yaml,不能据此把插件自身问题误判成通用失败
|
|
180
|
-
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return 'pnpmIgnoredBuild'
|
|
181
195
|
// 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题
|
|
182
196
|
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return 'pluginPrepare'
|
|
183
197
|
// 其余失败(含 git prepare 被 pnpm 白名单拦截):当前通道装不上 = 插件分发/依赖的问题,一律提 Issue
|
|
@@ -237,3 +251,18 @@ export function coreErrorCode(message: string): string | null {
|
|
|
237
251
|
const m = message.match(/\[?ERR_[A-Z_]+\]?/)
|
|
238
252
|
return m ? m[0].replace(/^\[|\]$/g, '') : null
|
|
239
253
|
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 从网络类失败消息里提取「具体连不上的地址」,供弹窗精准提示(你的网络无法访问什么)。
|
|
257
|
+
* 优先取服务端 [network] 预检消息括号里的探测地址(中文「(…)」/ 英文「(…)」),
|
|
258
|
+
* 否则取消息里出现的第一个 URL(git fetch 的仓库地址、registry 域名等)。
|
|
259
|
+
* 提取不到返回 null —— 调用方据此跳过地址行。
|
|
260
|
+
*/
|
|
261
|
+
export function unreachableTargetOf(message: string): string | null {
|
|
262
|
+
// [network] 预检消息:`无法连接到 GitHub(https://github.com/)` / `cannot reach the npm registry (https://registry.npmjs.org/)`
|
|
263
|
+
const paren = message.match(/[((](https?:\/\/[^\s'"`<>()+]+)[))]/)
|
|
264
|
+
if (paren) return paren[1]
|
|
265
|
+
// 其余网络失败(git fetch / npm 连接失败):取第一个 URL,去掉行尾标点
|
|
266
|
+
const url = message.match(/https?:\/\/[^\s'"`<>()()]+/)
|
|
267
|
+
return url ? url[0].replace(/[.,;:))\]]+$/, '') : null
|
|
268
|
+
}
|
|
@@ -33,12 +33,12 @@ function inline(s: string): string {
|
|
|
33
33
|
return `\u0000${codeSpans.length - 1}\u0000`
|
|
34
34
|
})
|
|
35
35
|
// 图片 :仅放行 http(s),用于展示反馈群二维码等;limited by CSS
|
|
36
|
-
// 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定
|
|
36
|
+
// 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定 100×100 不撑破弹窗。
|
|
37
37
|
out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) => {
|
|
38
38
|
const src = String(url)
|
|
39
39
|
if (!/^https?:\/\//i.test(src) || /[\u0000]/.test(src)) return m
|
|
40
40
|
if (/\/dsh-plugin-user-group-qr-[^/]*$/i.test(src)) {
|
|
41
|
-
return `<img src="${src}" alt="${String(alt)}" width="
|
|
41
|
+
return `<img src="${src}" alt="${String(alt)}" width="100" height="100" loading="lazy">`
|
|
42
42
|
}
|
|
43
43
|
return `<img src="${src}" alt="${String(alt)}" loading="lazy">`
|
|
44
44
|
})
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { EnvInfo, HubPlugin, LocaleId } from '../types.ts'
|
|
11
11
|
import { GITHUB_URL, PLUGIN_VERSION, SITE_URL } from './constants.ts'
|
|
12
12
|
import { MAX_CORE_CHARS, classifyFailure, coreErrorCode, summarizeError } from './failures.ts'
|
|
13
|
+
import type { FailureKind } from './failures.ts'
|
|
13
14
|
|
|
14
15
|
/** dsh-plugin.org 中文页挂在 /zh/ 前缀下,英文在根路径。 */
|
|
15
16
|
export function langPathOf(lang: LocaleId): string {
|
|
@@ -33,18 +34,22 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
33
34
|
return `${SITE_URL}plugins/${repo}`
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
/** 按失败类型给出简洁的错误原因标题(对外用英文):
|
|
38
|
+
* 标题直接点明问题出在哪一侧(构建白名单/分发物/本机 npm/网络/插件侧),
|
|
39
|
+
* 作者与用户扫一眼列表就能分流 —— 不再用含糊的 Install/Remove 动作词。 */
|
|
40
|
+
function reasonTitleOf(kind: FailureKind): string {
|
|
41
|
+
switch (kind) {
|
|
42
|
+
case 'npmTooOld': return 'npm too old to install'
|
|
43
|
+
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
44
|
+
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
45
|
+
case 'network': return 'network failure on the user side'
|
|
46
|
+
default: return 'plugin install failed'
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
|
-
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org
|
|
47
|
-
*
|
|
51
|
+
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识 + 错误原因,
|
|
52
|
+
* 正文只带核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
|
|
48
53
|
* 错误核心摘要(完整日志太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,
|
|
49
54
|
* 故只收集重点)。错误弹窗、失败记录共用此逻辑。
|
|
50
55
|
* env 取不到时为 null,链接照常生成、只是少环境段。
|
|
@@ -52,20 +57,21 @@ function actionLabelOf(command: string | undefined): string {
|
|
|
52
57
|
* 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
|
|
53
58
|
*/
|
|
54
59
|
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
55
|
-
// 标题按实际执行的动作区分(安装/更新/卸载),避免作者误读 —— 旧版固定写死
|
|
56
|
-
// 「Install/Remove」会让更新失败被当成删除失败。
|
|
57
|
-
const title = `[dsh-plugin.org | dsh-plugin-hub] ${actionLabelOf(command)} failed: ${repo}`
|
|
58
60
|
// 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
|
|
59
61
|
// [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
|
|
60
62
|
// (package.json 声明的入口文件在发布物里缺失),作者照此补齐即可。
|
|
61
63
|
const kind = classifyFailure(message)
|
|
64
|
+
// 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
|
|
65
|
+
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
62
66
|
const reason = /\[packaging\]/i.test(message)
|
|
63
67
|
? 'plugin distribution is incomplete — the entry file declared in package.json is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
|
|
64
|
-
: kind === '
|
|
65
|
-
? '
|
|
66
|
-
: kind === '
|
|
67
|
-
? 'plugin
|
|
68
|
-
:
|
|
68
|
+
: kind === 'network'
|
|
69
|
+
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
70
|
+
: kind === 'pluginPrepare'
|
|
71
|
+
? 'plugin prepare/build script failed during install (packaging/distribution issue)'
|
|
72
|
+
: kind === 'pnpmIgnoredBuild'
|
|
73
|
+
? 'plugin depends on a native module whose build script pnpm blocks by default (use a prebuilt variant)'
|
|
74
|
+
: 'plugin-side install failure'
|
|
69
75
|
const code = coreErrorCode(message)
|
|
70
76
|
// 按给定核心摘要预算构建完整预填 URL(含 URL 编码);预算可调,供超长时逐档缩小
|
|
71
77
|
const build = (coreChars: number): string => {
|
|
@@ -134,3 +140,4 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
134
140
|
}
|
|
135
141
|
return url
|
|
136
142
|
}
|
|
143
|
+
|
|
@@ -1239,6 +1239,47 @@
|
|
|
1239
1239
|
border-radius: 6px;
|
|
1240
1240
|
}
|
|
1241
1241
|
|
|
1242
|
+
/* 网络不通提示下的「去系统诊断」操作按钮:品牌实底小按钮(区别于红色提 Issue 大按钮) */
|
|
1243
|
+
.failDiagBtn {
|
|
1244
|
+
display: block;
|
|
1245
|
+
width: 100%;
|
|
1246
|
+
box-sizing: border-box;
|
|
1247
|
+
margin-top: 8px;
|
|
1248
|
+
padding: 5px 16px;
|
|
1249
|
+
font-size: 13px;
|
|
1250
|
+
line-height: 18px;
|
|
1251
|
+
font-weight: 600;
|
|
1252
|
+
text-align: center;
|
|
1253
|
+
color: var(--hub-text-on-fill);
|
|
1254
|
+
background: var(--hub-btn-fill);
|
|
1255
|
+
border: none;
|
|
1256
|
+
border-radius: 8px;
|
|
1257
|
+
cursor: pointer;
|
|
1258
|
+
user-select: none;
|
|
1259
|
+
transition: background 120ms ease, box-shadow 120ms ease;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.failDiagBtn:hover {
|
|
1263
|
+
background: var(--hub-btn-hover);
|
|
1264
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/* 网络类失败弹窗的「无法访问 …」醒目行:红色加粗,直接告诉用户具体哪个地址连不上
|
|
1268
|
+
(URL 较长允许换行断词),与下方琥珀色说明区分主次 */
|
|
1269
|
+
.failNetworkTarget {
|
|
1270
|
+
margin-top: 6px;
|
|
1271
|
+
padding: 8px 12px;
|
|
1272
|
+
font-size: 13px;
|
|
1273
|
+
line-height: 20px;
|
|
1274
|
+
font-weight: 700;
|
|
1275
|
+
word-break: break-all;
|
|
1276
|
+
color: var(--hub-danger);
|
|
1277
|
+
background: var(--hub-danger-tint);
|
|
1278
|
+
border: 1px solid var(--hub-danger-border);
|
|
1279
|
+
border-radius: 6px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1242
1283
|
/* —— 「关注我们」弹窗 —— */
|
|
1243
1284
|
|
|
1244
1285
|
/* 关注弹窗:比普通弹窗略宽,给 Markdown 介绍 + 反馈群二维码留出阅读空间;
|
|
@@ -1356,13 +1397,13 @@
|
|
|
1356
1397
|
}
|
|
1357
1398
|
|
|
1358
1399
|
/* 用户反馈群二维码(renderMarkdown 对 dsh-plugin-user-group-qr- 前缀图片加了
|
|
1359
|
-
width/height="
|
|
1360
|
-
CSS max-height 对 height
|
|
1361
|
-
|
|
1362
|
-
.aboutContent img[width="
|
|
1363
|
-
width:
|
|
1364
|
-
height:
|
|
1365
|
-
max-height:
|
|
1400
|
+
width/height="100"):固定 100×100 正方形。必须显式覆盖通用 max-height 上限 ——
|
|
1401
|
+
CSS max-height 对 height 有最终裁决权,不覆盖的话高度被 max-height 规则压掉,
|
|
1402
|
+
图片就会变成非正方形(看起来像「尺寸没生效」) */
|
|
1403
|
+
.aboutContent img[width="100"] {
|
|
1404
|
+
width: 100px;
|
|
1405
|
+
height: 100px;
|
|
1406
|
+
max-height: 100px;
|
|
1366
1407
|
object-fit: contain;
|
|
1367
1408
|
}
|
|
1368
1409
|
|
|
@@ -178,6 +178,30 @@
|
|
|
178
178
|
width: 100%;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/* 代理输入框列布局:输入框 + 连通性探测提示上下排列,整行展开 */
|
|
182
|
+
.proxyControl {
|
|
183
|
+
width: 100%;
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
align-items: stretch;
|
|
187
|
+
gap: 5px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* 代理连通性提示:检测中灰字 / 可达绿字 / 不通琥珀字(仍可保存,不阻断输入) */
|
|
191
|
+
.proxyHint {
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
line-height: 1.4;
|
|
194
|
+
color: var(--hub-text-disabled);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.proxyHintOk {
|
|
198
|
+
color: var(--hub-success);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.proxyHintFail {
|
|
202
|
+
color: var(--hub-warning);
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
/* 下拉控件:行内固定宽度,与输入框等高 */
|
|
182
206
|
.controlDropdown {
|
|
183
207
|
min-width: 180px;
|
|
@@ -13,7 +13,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
13
13
|
import { homedir, release } from 'node:os'
|
|
14
14
|
import { dirname, join, resolve } from 'node:path'
|
|
15
15
|
import { spawn } from 'node:child_process'
|
|
16
|
-
import { fetchViaCurl, probeUrl, systemProxy } from '../services/probe.ts'
|
|
16
|
+
import { fetchViaCurl, gitLsRemote, probeUrl, systemProxy } from '../services/probe.ts'
|
|
17
17
|
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName, type LoaderHandle } from '../services/install/install.ts'
|
|
18
18
|
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.ts'
|
|
19
19
|
import { resolveNpmPackage } from '../services/install/npm-resolve.ts'
|
|
@@ -494,11 +494,19 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
494
494
|
// 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
|
|
495
495
|
// 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
|
|
496
496
|
// npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
|
|
497
|
-
const allChecks: Array<{ key: string; url: string; display: string; cmd: string }> = [
|
|
497
|
+
const allChecks: Array<{ key: string; url: string; display: string; cmd: string; proxy?: string; git?: boolean }> = [
|
|
498
498
|
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
499
|
-
|
|
499
|
+
// GitHub 行用真实 git 克隆握手(git ls-remote)打 dshplugin/hello-dsh 小仓库:
|
|
500
|
+
// 「网页能打开」和「git 能克隆」是两码事,握手成功才算通道通;小仓库秒级完成,不打 17MB 的 dsh-plugin-hub。
|
|
501
|
+
{ key: 'github', url: 'https://github.com/dshplugin/hello-dsh', display: 'github.com/dshplugin/hello-dsh', cmd: 'git ls-remote https://github.com/dshplugin/hello-dsh', git: true },
|
|
500
502
|
{ key: 'catalog', url: 'https://api.dsh-plugin.org/stats.json', display: 'api.dsh-plugin.org', cmd: 'curl -s https://api.dsh-plugin.org/stats.json' },
|
|
501
503
|
]
|
|
504
|
+
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
505
|
+
// 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
|
|
506
|
+
// 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
|
|
507
|
+
if (settings.proxy !== '') {
|
|
508
|
+
allChecks.push({ key: 'proxy', url: 'https://github.com', display: settings.proxy, cmd: `curl -s -o /dev/null -x ${settings.proxy} https://github.com`, proxy: settings.proxy })
|
|
509
|
+
}
|
|
502
510
|
const checks = onlyKey !== '' ? allChecks.filter((c) => c.key === onlyKey) : allChecks
|
|
503
511
|
response.writeHead(200, {
|
|
504
512
|
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
@@ -508,17 +516,22 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
508
516
|
// 探测走代理的优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 环境变量 HTTPS_PROXY → 直连。
|
|
509
517
|
// 与安装通道一致(npm/git 靠 HTTP_PROXY 环境变量走代理),且自动跟随系统代理 ——
|
|
510
518
|
// 浏览器挂着代理能开、诊断就能测通,不用用户手动把代理填进设置。
|
|
519
|
+
// proxy 行例外:它验证的正是「设置里配的那个代理」,必须显式用它(而非 effectiveProxy)去探测。
|
|
511
520
|
const effectiveProxy = settings.proxy !== ''
|
|
512
521
|
? settings.proxy
|
|
513
522
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
514
|
-
const probe = (url: string) => probeUrl(url, effectiveProxy, 6000)
|
|
523
|
+
const probe = (url: string, proxy?: string) => probeUrl(url, proxy ?? effectiveProxy, 6000)
|
|
515
524
|
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
516
525
|
const results: Array<{ key: string; ok: boolean }> = []
|
|
517
526
|
for (const c of checks) {
|
|
518
527
|
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display })
|
|
519
|
-
|
|
528
|
+
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
|
|
529
|
+
const r = c.git
|
|
530
|
+
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
531
|
+
: await probe(c.url, c.proxy)
|
|
520
532
|
results.push({ key: c.key, ok: r.ok })
|
|
521
|
-
|
|
533
|
+
// git 行的 status 是退出码,客户端 git 行只显示 OK / 不可达,不显示成 HTTP 码
|
|
534
|
+
send({ type: r.ok ? 'ok' : 'fail', key: c.key, display: c.display, ms: r.ms, status: c.git ? null : r.status })
|
|
522
535
|
}
|
|
523
536
|
send({ type: 'end', at: Date.now() })
|
|
524
537
|
response.end()
|
|
@@ -535,6 +548,35 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
535
548
|
})
|
|
536
549
|
},
|
|
537
550
|
}),
|
|
551
|
+
webServer.register({
|
|
552
|
+
kind: 'exact',
|
|
553
|
+
path: '/dsh-plugin-hub/proxy-check',
|
|
554
|
+
handler: async (request, response) => {
|
|
555
|
+
if (!requireTrustedPost(request, response)) return
|
|
556
|
+
// 代理连通性校验:用待测代理打 dshplugin/hello-dsh 真实仓库页(权威可达目标),
|
|
557
|
+
// 供设置页输入代理时实时反馈「这个地址通不通」。不通过也允许保存 ——
|
|
558
|
+
// 用户可能是先填地址后开代理,因此这里只报告探测结果、不拦截保存。
|
|
559
|
+
let proxy = ''
|
|
560
|
+
let target = 'https://github.com/dshplugin/hello-dsh'
|
|
561
|
+
try {
|
|
562
|
+
const body = await readJsonBody(request)
|
|
563
|
+
if (body !== null && typeof body === 'object') {
|
|
564
|
+
const b = body as { proxy?: unknown; target?: unknown }
|
|
565
|
+
if (typeof b.proxy === 'string') proxy = b.proxy.trim()
|
|
566
|
+
if (typeof b.target === 'string' && b.target.trim() !== '') target = b.target.trim()
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
// 空 body / 非 JSON:按默认目标探测
|
|
570
|
+
}
|
|
571
|
+
if (proxy === '') {
|
|
572
|
+
sendJson(response, 200, { ok: false, ms: null, status: null, target, reason: 'empty' })
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
// 探测走该代理本身(不是 effectiveProxy):验证的就是用户填的这个地址
|
|
576
|
+
const r = await probeUrl(target, proxy, 6000)
|
|
577
|
+
sendJson(response, 200, { ok: r.ok, ms: r.ms, status: r.status, target })
|
|
578
|
+
},
|
|
579
|
+
}),
|
|
538
580
|
webServer.register({
|
|
539
581
|
kind: 'exact',
|
|
540
582
|
path: '/dsh-plugin-hub/catalog',
|
|
@@ -605,6 +647,10 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
605
647
|
? (body as { source: string }).source
|
|
606
648
|
: ''
|
|
607
649
|
const settings = loadSettings(profile)
|
|
650
|
+
// 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
|
|
651
|
+
const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
|
|
652
|
+
? 'en'
|
|
653
|
+
: 'zh'
|
|
608
654
|
// 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令):
|
|
609
655
|
// 直接从命令框输入格式执行全局安装,不进任何 profile、无需宿主重启。
|
|
610
656
|
// 包列表优先取 body.globalNpm(客户端命令框解析后显式携带);兜底再从 rawRepo 解析,
|
|
@@ -703,6 +749,35 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
703
749
|
sendJson(response, 409, { error: `already queued: ${target}` })
|
|
704
750
|
return
|
|
705
751
|
}
|
|
752
|
+
// 安装前网络连通性预检:npm 通道探 registry、git 通道探 github.com 连通性。
|
|
753
|
+
// 探测目标固定为通道的稳定入口(registry 根 / github.com 主页),不探具体包或仓库
|
|
754
|
+
// 页 —— 404 表示「目标不存在」而非网络不通,不该被当成网络故障拦截。
|
|
755
|
+
// 仅新安装预检(更新是已信任目标的覆盖重装,跳过);不通直接 400 拦下并打
|
|
756
|
+
// [network] 标记,客户端据此提示「你的网络不通」,而不是把网络失败当成
|
|
757
|
+
// 插件侧问题引导去作者仓库提 Issue(issue #10:git fetch 超时被误报为插件问题)。
|
|
758
|
+
// 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
759
|
+
if (already === null) {
|
|
760
|
+
const effectiveProxy = settings.proxy !== ''
|
|
761
|
+
? settings.proxy
|
|
762
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
763
|
+
const isGitChannel = repoTarget !== null && target === repoTarget
|
|
764
|
+
const probeTarget = isGitChannel
|
|
765
|
+
? 'https://github.com/'
|
|
766
|
+
: `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`
|
|
767
|
+
const net = await probeUrl(probeTarget, effectiveProxy, 6000)
|
|
768
|
+
if (!net.ok) {
|
|
769
|
+
// 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
|
|
770
|
+
// 不再写死英文(用户一眼看懂是网络问题,而不是插件问题)
|
|
771
|
+
const channelName = isGitChannel
|
|
772
|
+
? (lang === 'zh' ? 'GitHub' : 'github.com')
|
|
773
|
+
: (lang === 'zh' ? 'npm 源' : 'the npm registry')
|
|
774
|
+
const error = lang === 'zh'
|
|
775
|
+
? `[network] 安装已中止:安装前无法连接到 ${channelName}(${probeTarget})—— 您的网络似乎不通或被拦截(断网 / DNS 解析失败 / 防火墙 / 代理配置问题)。这不是插件本身的问题,请检查网络连接或代理设置,运行「系统诊断」检测各通道连通性后重试。`
|
|
776
|
+
: `[network] install aborted: cannot reach ${channelName} (${probeTarget}) before install — your network connection appears to be down or blocked (DNS / proxy / firewall). This is not a problem with the plugin itself. Check your connection or proxy settings, run the connectivity diagnostic, then retry.`
|
|
777
|
+
sendJson(response, 400, { error, attempts })
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
}
|
|
706
781
|
// 安装前预检:仅新安装走预检(github 源分发改入口文件缺失时直接拦截)。
|
|
707
782
|
// 更新是已信任目标的覆盖重装,跳过预检避免重复下载 tarball。
|
|
708
783
|
// npm 包信任 registry 直接放行;github 源拦截时标记 [packaging] 前缀,
|
|
@@ -124,6 +124,49 @@ export function probeUrl(url: string, proxy: string, timeoutMs: number): Promise
|
|
|
124
124
|
return curlProbe(target.href, proxy, timeoutMs)
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* git 通道真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
129
|
+
* ref 握手一致),注入代理 env,GIT_TERMINAL_PROMPT=0 防凭据提示挂起。
|
|
130
|
+
*
|
|
131
|
+
* 为什么要真实 git 而非 curl 打网页:网页「能打开」和 git「能克隆」是两码事 ——
|
|
132
|
+
* 防火墙/代理常按端口与协议区分,HTTP 页可达不代表 git 传输可达。这里测的就是
|
|
133
|
+
* 克隆握手本身,回答「github:owner/repo 装不装得动」。
|
|
134
|
+
* 供系统诊断 GitHub 通道使用;探测目标用 dshplugin/hello-dsh 小仓库(秒级完成,
|
|
135
|
+
* 不打 17MB 的 dsh-plugin-hub 主页)。
|
|
136
|
+
*/
|
|
137
|
+
export function gitLsRemote(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult> {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const started = Date.now()
|
|
140
|
+
const env: NodeJS.ProcessEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }
|
|
141
|
+
if (proxy !== '') {
|
|
142
|
+
env.HTTP_PROXY = proxy
|
|
143
|
+
env.HTTPS_PROXY = proxy
|
|
144
|
+
env.http_proxy = proxy
|
|
145
|
+
env.https_proxy = proxy
|
|
146
|
+
}
|
|
147
|
+
const child = spawn('git', ['ls-remote', '--exit-code', url, 'HEAD'], { env })
|
|
148
|
+
let out = ''
|
|
149
|
+
child.stdout.on('data', (c: Buffer) => { out += c.toString() })
|
|
150
|
+
// git 的报错不透传给终端:结果以退出码 + HEAD ref 是否返回为准
|
|
151
|
+
child.stderr.on('data', () => {})
|
|
152
|
+
let done = false
|
|
153
|
+
const finish = (r: ProbeResult) => {
|
|
154
|
+
if (done) return
|
|
155
|
+
done = true
|
|
156
|
+
clearTimeout(timer)
|
|
157
|
+
resolve(r)
|
|
158
|
+
}
|
|
159
|
+
child.on('error', () => finish(probeFail()))
|
|
160
|
+
child.on('close', (code) => {
|
|
161
|
+
const ok = code === 0 && out.trim() !== ''
|
|
162
|
+
// status 复用为 git 退出码(0 = 克隆握手成功);客户端 git 行不把它显示成 HTTP 码
|
|
163
|
+
finish({ ok, ms: Date.now() - started, status: code })
|
|
164
|
+
})
|
|
165
|
+
// 超时兜底:与 curlProbe 同口径,防止 git 挂起
|
|
166
|
+
const timer = setTimeout(() => { child.kill(); finish(probeFail()) }, timeoutMs + 1000)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
127
170
|
/**
|
|
128
171
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入)。
|
|
129
172
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|