dsh-plugin 1.3.5 → 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 +72 -18
- package/README.md +72 -18
- package/client/client.js +582 -425
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +90 -9
- package/lib/services/install/npm-resolve.js +3 -1
- package/lib/services/probe.js +43 -0
- package/lib/services/settings.js +1 -1
- package/lib/types/services/install/npm-resolve.d.ts +3 -1
- package/lib/types/services/probe.d.ts +11 -0
- package/lib/types/services/settings.d.ts +2 -2
- package/package.json +3 -3
- package/src/client/components/PluginHubSection.tsx +42 -7
- package/src/client/components/modals/NotificationsModal.tsx +27 -5
- package/src/client/components/modals/modals.tsx +18 -3
- package/src/client/components/views/DiagnosticsView.tsx +21 -7
- package/src/client/components/views/SettingsView.tsx +94 -46
- package/src/client/hooks/useSettings.ts +3 -3
- package/src/client/hooks/useTaskQueue.ts +18 -8
- package/src/client/locales.ts +48 -11
- package/src/client/logic/failures.ts +41 -9
- package/src/client/logic/renderMarkdown.ts +2 -2
- package/src/client/logic/urls.ts +26 -8
- package/src/client/styles/Dropdown.module.css +10 -0
- package/src/client/styles/Modal.module.css +52 -0
- package/src/client/styles/SettingsView.module.css +24 -0
- package/src/server/http/routes.ts +87 -9
- package/src/server/services/install/npm-resolve.ts +3 -1
- package/src/server/services/probe.ts +43 -0
- package/src/server/services/settings.ts +3 -3
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,9 +34,22 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
33
34
|
return `${SITE_URL}plugins/${repo}`
|
|
34
35
|
}
|
|
35
36
|
|
|
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
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
|
-
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org
|
|
38
|
-
*
|
|
51
|
+
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识 + 错误原因,
|
|
52
|
+
* 正文只带核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
|
|
39
53
|
* 错误核心摘要(完整日志太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,
|
|
40
54
|
* 故只收集重点)。错误弹窗、失败记录共用此逻辑。
|
|
41
55
|
* env 取不到时为 null,链接照常生成、只是少环境段。
|
|
@@ -43,18 +57,21 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
43
57
|
* 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
|
|
44
58
|
*/
|
|
45
59
|
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
46
|
-
const title = `[dsh-plugin.org | dsh-plugin-hub] Install/Remove failed: ${repo}`
|
|
47
60
|
// 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
|
|
48
61
|
// [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
|
|
49
62
|
// (package.json 声明的入口文件在发布物里缺失),作者照此补齐即可。
|
|
50
63
|
const kind = classifyFailure(message)
|
|
64
|
+
// 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
|
|
65
|
+
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
51
66
|
const reason = /\[packaging\]/i.test(message)
|
|
52
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'
|
|
53
|
-
: kind === '
|
|
54
|
-
? '
|
|
55
|
-
: kind === '
|
|
56
|
-
? 'plugin
|
|
57
|
-
:
|
|
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'
|
|
58
75
|
const code = coreErrorCode(message)
|
|
59
76
|
// 按给定核心摘要预算构建完整预填 URL(含 URL 编码);预算可调,供超长时逐档缩小
|
|
60
77
|
const build = (coreChars: number): string => {
|
|
@@ -123,3 +140,4 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
123
140
|
}
|
|
124
141
|
return url
|
|
125
142
|
}
|
|
143
|
+
|
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
flex-shrink: 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/* 全宽变体:设置页「独占一行」的下拉与文本框同宽,按钮铺满整行 */
|
|
16
|
+
.dropdownFill {
|
|
17
|
+
width: 100%;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dropdownFill > .dropdownBtn {
|
|
21
|
+
width: 100%;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
/* 触发器:工具栏按钮风格 —— 细边框 + 次级灰底,与通知入口按钮同一视觉语言,
|
|
16
26
|
只占一个按钮位,给工具栏后续新增按钮留空间 */
|
|
17
27
|
.dropdownBtn {
|
|
@@ -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 介绍 + 反馈群二维码留出阅读空间;
|
|
@@ -1355,6 +1396,17 @@
|
|
|
1355
1396
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
1356
1397
|
}
|
|
1357
1398
|
|
|
1399
|
+
/* 用户反馈群二维码(renderMarkdown 对 dsh-plugin-user-group-qr- 前缀图片加了
|
|
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;
|
|
1407
|
+
object-fit: contain;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1358
1410
|
/* 更新时间小字:弱化展示在内容区下方 */
|
|
1359
1411
|
.aboutMeta {
|
|
1360
1412
|
font-size: 11px;
|
|
@@ -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'
|
|
@@ -184,12 +184,12 @@ function hostEnv(profile: string): Record<string, string | null> {
|
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入
|
|
187
|
+
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
|
|
188
|
+
* npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
|
|
188
189
|
* 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
|
|
189
190
|
* 使安装通道与诊断使用相同的代理来源。 */
|
|
190
191
|
function mutationEnv(settings: HubSettings): NodeJS.ProcessEnv {
|
|
191
192
|
const env: NodeJS.ProcessEnv = { ...process.env, CI: 'true' }
|
|
192
|
-
if (settings.npmRegistry !== '') env.npm_config_registry = settings.npmRegistry
|
|
193
193
|
const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '')
|
|
194
194
|
if (proxy !== '') {
|
|
195
195
|
env.HTTP_PROXY = proxy
|
|
@@ -488,14 +488,25 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
488
488
|
// 空 body / 非 JSON:视为全量探测
|
|
489
489
|
}
|
|
490
490
|
const settings = loadSettings(profile)
|
|
491
|
+
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
492
|
+
// 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
|
|
491
493
|
const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org'
|
|
492
494
|
// 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
|
|
493
495
|
// 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
{ key: '
|
|
496
|
+
// npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
|
|
497
|
+
const allChecks: Array<{ key: string; url: string; display: string; cmd: string; proxy?: string; git?: boolean }> = [
|
|
498
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
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 },
|
|
497
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' },
|
|
498
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
|
+
}
|
|
499
510
|
const checks = onlyKey !== '' ? allChecks.filter((c) => c.key === onlyKey) : allChecks
|
|
500
511
|
response.writeHead(200, {
|
|
501
512
|
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
@@ -505,17 +516,22 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
505
516
|
// 探测走代理的优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 环境变量 HTTPS_PROXY → 直连。
|
|
506
517
|
// 与安装通道一致(npm/git 靠 HTTP_PROXY 环境变量走代理),且自动跟随系统代理 ——
|
|
507
518
|
// 浏览器挂着代理能开、诊断就能测通,不用用户手动把代理填进设置。
|
|
519
|
+
// proxy 行例外:它验证的正是「设置里配的那个代理」,必须显式用它(而非 effectiveProxy)去探测。
|
|
508
520
|
const effectiveProxy = settings.proxy !== ''
|
|
509
521
|
? settings.proxy
|
|
510
522
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '')
|
|
511
|
-
const probe = (url: string) => probeUrl(url, effectiveProxy, 6000)
|
|
523
|
+
const probe = (url: string, proxy?: string) => probeUrl(url, proxy ?? effectiveProxy, 6000)
|
|
512
524
|
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
513
525
|
const results: Array<{ key: string; ok: boolean }> = []
|
|
514
526
|
for (const c of checks) {
|
|
515
527
|
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display })
|
|
516
|
-
|
|
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)
|
|
517
532
|
results.push({ key: c.key, ok: r.ok })
|
|
518
|
-
|
|
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 })
|
|
519
535
|
}
|
|
520
536
|
send({ type: 'end', at: Date.now() })
|
|
521
537
|
response.end()
|
|
@@ -532,6 +548,35 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
532
548
|
})
|
|
533
549
|
},
|
|
534
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
|
+
}),
|
|
535
580
|
webServer.register({
|
|
536
581
|
kind: 'exact',
|
|
537
582
|
path: '/dsh-plugin-hub/catalog',
|
|
@@ -602,6 +647,10 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
602
647
|
? (body as { source: string }).source
|
|
603
648
|
: ''
|
|
604
649
|
const settings = loadSettings(profile)
|
|
650
|
+
// 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
|
|
651
|
+
const lang = typeof body === 'object' && body !== null && (body as { lang?: unknown }).lang === 'en'
|
|
652
|
+
? 'en'
|
|
653
|
+
: 'zh'
|
|
605
654
|
// 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令):
|
|
606
655
|
// 直接从命令框输入格式执行全局安装,不进任何 profile、无需宿主重启。
|
|
607
656
|
// 包列表优先取 body.globalNpm(客户端命令框解析后显式携带);兜底再从 rawRepo 解析,
|
|
@@ -700,6 +749,35 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
700
749
|
sendJson(response, 409, { error: `already queued: ${target}` })
|
|
701
750
|
return
|
|
702
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
|
+
}
|
|
703
781
|
// 安装前预检:仅新安装走预检(github 源分发改入口文件缺失时直接拦截)。
|
|
704
782
|
// 更新是已信任目标的覆盖重装,跳过预检避免重复下载 tarball。
|
|
705
783
|
// npm 包信任 registry 直接放行;github 源拦截时标记 [packaging] 前缀,
|
|
@@ -23,7 +23,9 @@ const REQUEST_TIMEOUT_MS = 8000
|
|
|
23
23
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
24
24
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
25
25
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
26
|
-
*
|
|
26
|
+
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
27
|
+
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
28
|
+
* 慢网络下失败不阻塞安装。
|
|
27
29
|
*/
|
|
28
30
|
export function resolveNpmPackage(repo: string, registry = ''): Promise<string | null> {
|
|
29
31
|
const key = repo.toLowerCase()
|
|
@@ -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 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
|
@@ -14,10 +14,10 @@ import { profileDirectory } from './profile/profile.ts'
|
|
|
14
14
|
export interface HubSettings {
|
|
15
15
|
/** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
|
|
16
16
|
checkUpdatesOnStart: boolean
|
|
17
|
-
/** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
|
|
18
|
-
npmRegistry: string
|
|
19
17
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
20
18
|
proxy: string
|
|
19
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置,本插件不注入任何 registry) */
|
|
20
|
+
npmRegistry: string
|
|
21
21
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
22
22
|
enableNpmInstall: boolean
|
|
23
23
|
/** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
|
|
@@ -30,8 +30,8 @@ export interface HubSettings {
|
|
|
30
30
|
|
|
31
31
|
export const DEFAULT_SETTINGS: HubSettings = {
|
|
32
32
|
checkUpdatesOnStart: true,
|
|
33
|
-
npmRegistry: '',
|
|
34
33
|
proxy: '',
|
|
34
|
+
npmRegistry: '',
|
|
35
35
|
enableNpmInstall: true,
|
|
36
36
|
enableGitInstall: true,
|
|
37
37
|
enableDshInstall: true,
|