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/lib/http/routes.js
CHANGED
|
@@ -12,7 +12,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
12
12
|
import { homedir, release } from 'node:os';
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
|
-
import { fetchViaCurl, probeUrl, systemProxy } from '../services/probe.js';
|
|
15
|
+
import { fetchViaCurl, gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
|
|
16
16
|
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install/install.js';
|
|
17
17
|
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.js';
|
|
18
18
|
import { resolveNpmPackage } from '../services/install/npm-resolve.js';
|
|
@@ -157,13 +157,12 @@ function hostEnv(profile) {
|
|
|
157
157
|
dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入
|
|
160
|
+
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
|
|
161
|
+
* npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
|
|
161
162
|
* 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
|
|
162
163
|
* 使安装通道与诊断使用相同的代理来源。 */
|
|
163
164
|
function mutationEnv(settings) {
|
|
164
165
|
const env = { ...process.env, CI: 'true' };
|
|
165
|
-
if (settings.npmRegistry !== '')
|
|
166
|
-
env.npm_config_registry = settings.npmRegistry;
|
|
167
166
|
const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '');
|
|
168
167
|
if (proxy !== '') {
|
|
169
168
|
env.HTTP_PROXY = proxy;
|
|
@@ -485,14 +484,25 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
485
484
|
// 空 body / 非 JSON:视为全量探测
|
|
486
485
|
}
|
|
487
486
|
const settings = loadSettings(profile);
|
|
487
|
+
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
488
|
+
// 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
|
|
488
489
|
const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org';
|
|
489
490
|
// 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
|
|
490
491
|
// 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
|
|
492
|
+
// npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
|
|
491
493
|
const allChecks = [
|
|
492
|
-
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ?
|
|
493
|
-
|
|
494
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
495
|
+
// GitHub 行用真实 git 克隆握手(git ls-remote)打 dshplugin/hello-dsh 小仓库:
|
|
496
|
+
// 「网页能打开」和「git 能克隆」是两码事,握手成功才算通道通;小仓库秒级完成,不打 17MB 的 dsh-plugin-hub。
|
|
497
|
+
{ 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 },
|
|
494
498
|
{ 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' },
|
|
495
499
|
];
|
|
500
|
+
// 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
|
|
501
|
+
// 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
|
|
502
|
+
// 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
|
|
503
|
+
if (settings.proxy !== '') {
|
|
504
|
+
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 });
|
|
505
|
+
}
|
|
496
506
|
const checks = onlyKey !== '' ? allChecks.filter((c) => c.key === onlyKey) : allChecks;
|
|
497
507
|
response.writeHead(200, {
|
|
498
508
|
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
@@ -502,17 +512,22 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
502
512
|
// 探测走代理的优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 环境变量 HTTPS_PROXY → 直连。
|
|
503
513
|
// 与安装通道一致(npm/git 靠 HTTP_PROXY 环境变量走代理),且自动跟随系统代理 ——
|
|
504
514
|
// 浏览器挂着代理能开、诊断就能测通,不用用户手动把代理填进设置。
|
|
515
|
+
// proxy 行例外:它验证的正是「设置里配的那个代理」,必须显式用它(而非 effectiveProxy)去探测。
|
|
505
516
|
const effectiveProxy = settings.proxy !== ''
|
|
506
517
|
? settings.proxy
|
|
507
518
|
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
508
|
-
const probe = (url) => probeUrl(url, effectiveProxy, 6000);
|
|
519
|
+
const probe = (url, proxy) => probeUrl(url, proxy ?? effectiveProxy, 6000);
|
|
509
520
|
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
510
521
|
const results = [];
|
|
511
522
|
for (const c of checks) {
|
|
512
523
|
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display });
|
|
513
|
-
|
|
524
|
+
// git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
|
|
525
|
+
const r = c.git
|
|
526
|
+
? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
|
|
527
|
+
: await probe(c.url, c.proxy);
|
|
514
528
|
results.push({ key: c.key, ok: r.ok });
|
|
515
|
-
|
|
529
|
+
// git 行的 status 是退出码,客户端 git 行只显示 OK / 不可达,不显示成 HTTP 码
|
|
530
|
+
send({ type: r.ok ? 'ok' : 'fail', key: c.key, display: c.display, ms: r.ms, status: c.git ? null : r.status });
|
|
516
531
|
}
|
|
517
532
|
send({ type: 'end', at: Date.now() });
|
|
518
533
|
response.end();
|
|
@@ -529,6 +544,39 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
529
544
|
});
|
|
530
545
|
},
|
|
531
546
|
}),
|
|
547
|
+
webServer.register({
|
|
548
|
+
kind: 'exact',
|
|
549
|
+
path: '/dsh-plugin-hub/proxy-check',
|
|
550
|
+
handler: async (request, response) => {
|
|
551
|
+
if (!requireTrustedPost(request, response))
|
|
552
|
+
return;
|
|
553
|
+
// 代理连通性校验:用待测代理打 dshplugin/hello-dsh 真实仓库页(权威可达目标),
|
|
554
|
+
// 供设置页输入代理时实时反馈「这个地址通不通」。不通过也允许保存 ——
|
|
555
|
+
// 用户可能是先填地址后开代理,因此这里只报告探测结果、不拦截保存。
|
|
556
|
+
let proxy = '';
|
|
557
|
+
let target = 'https://github.com/dshplugin/hello-dsh';
|
|
558
|
+
try {
|
|
559
|
+
const body = await readJsonBody(request);
|
|
560
|
+
if (body !== null && typeof body === 'object') {
|
|
561
|
+
const b = body;
|
|
562
|
+
if (typeof b.proxy === 'string')
|
|
563
|
+
proxy = b.proxy.trim();
|
|
564
|
+
if (typeof b.target === 'string' && b.target.trim() !== '')
|
|
565
|
+
target = b.target.trim();
|
|
566
|
+
}
|
|
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
|
+
}),
|
|
532
580
|
webServer.register({
|
|
533
581
|
kind: 'exact',
|
|
534
582
|
path: '/dsh-plugin-hub/catalog',
|
|
@@ -603,6 +651,10 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
603
651
|
? body.source
|
|
604
652
|
: '';
|
|
605
653
|
const settings = loadSettings(profile);
|
|
654
|
+
// 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
|
|
655
|
+
const lang = typeof body === 'object' && body !== null && body.lang === 'en'
|
|
656
|
+
? 'en'
|
|
657
|
+
: 'zh';
|
|
606
658
|
// 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令):
|
|
607
659
|
// 直接从命令框输入格式执行全局安装,不进任何 profile、无需宿主重启。
|
|
608
660
|
// 包列表优先取 body.globalNpm(客户端命令框解析后显式携带);兜底再从 rawRepo 解析,
|
|
@@ -702,6 +754,35 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
702
754
|
sendJson(response, 409, { error: `already queued: ${target}` });
|
|
703
755
|
return;
|
|
704
756
|
}
|
|
757
|
+
// 安装前网络连通性预检:npm 通道探 registry、git 通道探 github.com 连通性。
|
|
758
|
+
// 探测目标固定为通道的稳定入口(registry 根 / github.com 主页),不探具体包或仓库
|
|
759
|
+
// 页 —— 404 表示「目标不存在」而非网络不通,不该被当成网络故障拦截。
|
|
760
|
+
// 仅新安装预检(更新是已信任目标的覆盖重装,跳过);不通直接 400 拦下并打
|
|
761
|
+
// [network] 标记,客户端据此提示「你的网络不通」,而不是把网络失败当成
|
|
762
|
+
// 插件侧问题引导去作者仓库提 Issue(issue #10:git fetch 超时被误报为插件问题)。
|
|
763
|
+
// 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
|
|
764
|
+
if (already === null) {
|
|
765
|
+
const effectiveProxy = settings.proxy !== ''
|
|
766
|
+
? settings.proxy
|
|
767
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
768
|
+
const isGitChannel = repoTarget !== null && target === repoTarget;
|
|
769
|
+
const probeTarget = isGitChannel
|
|
770
|
+
? 'https://github.com/'
|
|
771
|
+
: `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`;
|
|
772
|
+
const net = await probeUrl(probeTarget, effectiveProxy, 6000);
|
|
773
|
+
if (!net.ok) {
|
|
774
|
+
// 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
|
|
775
|
+
// 不再写死英文(用户一眼看懂是网络问题,而不是插件问题)
|
|
776
|
+
const channelName = isGitChannel
|
|
777
|
+
? (lang === 'zh' ? 'GitHub' : 'github.com')
|
|
778
|
+
: (lang === 'zh' ? 'npm 源' : 'the npm registry');
|
|
779
|
+
const error = lang === 'zh'
|
|
780
|
+
? `[network] 安装已中止:安装前无法连接到 ${channelName}(${probeTarget})—— 您的网络似乎不通或被拦截(断网 / DNS 解析失败 / 防火墙 / 代理配置问题)。这不是插件本身的问题,请检查网络连接或代理设置,运行「系统诊断」检测各通道连通性后重试。`
|
|
781
|
+
: `[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.`;
|
|
782
|
+
sendJson(response, 400, { error, attempts });
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
705
786
|
// 安装前预检:仅新安装走预检(github 源分发改入口文件缺失时直接拦截)。
|
|
706
787
|
// 更新是已信任目标的覆盖重装,跳过预检避免重复下载 tarball。
|
|
707
788
|
// npm 包信任 registry 直接放行;github 源拦截时标记 [packaging] 前缀,
|
|
@@ -20,7 +20,9 @@ const REQUEST_TIMEOUT_MS = 8000;
|
|
|
20
20
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
21
21
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
22
22
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
23
|
-
*
|
|
23
|
+
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
24
|
+
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
25
|
+
* 慢网络下失败不阻塞安装。
|
|
24
26
|
*/
|
|
25
27
|
export function resolveNpmPackage(repo, registry = '') {
|
|
26
28
|
const key = repo.toLowerCase();
|
package/lib/services/probe.js
CHANGED
|
@@ -123,6 +123,49 @@ export function probeUrl(url, proxy, timeoutMs) {
|
|
|
123
123
|
}
|
|
124
124
|
return curlProbe(target.href, proxy, timeoutMs);
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* git 通道真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
128
|
+
* ref 握手一致),注入代理 env,GIT_TERMINAL_PROMPT=0 防凭据提示挂起。
|
|
129
|
+
*
|
|
130
|
+
* 为什么要真实 git 而非 curl 打网页:网页「能打开」和 git「能克隆」是两码事 ——
|
|
131
|
+
* 防火墙/代理常按端口与协议区分,HTTP 页可达不代表 git 传输可达。这里测的就是
|
|
132
|
+
* 克隆握手本身,回答「github:owner/repo 装不装得动」。
|
|
133
|
+
* 供系统诊断 GitHub 通道使用;探测目标用 dshplugin/hello-dsh 小仓库(秒级完成,
|
|
134
|
+
* 不打 17MB 的 dsh-plugin-hub 主页)。
|
|
135
|
+
*/
|
|
136
|
+
export function gitLsRemote(url, proxy, timeoutMs) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const started = Date.now();
|
|
139
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
|
|
140
|
+
if (proxy !== '') {
|
|
141
|
+
env.HTTP_PROXY = proxy;
|
|
142
|
+
env.HTTPS_PROXY = proxy;
|
|
143
|
+
env.http_proxy = proxy;
|
|
144
|
+
env.https_proxy = proxy;
|
|
145
|
+
}
|
|
146
|
+
const child = spawn('git', ['ls-remote', '--exit-code', url, 'HEAD'], { env });
|
|
147
|
+
let out = '';
|
|
148
|
+
child.stdout.on('data', (c) => { out += c.toString(); });
|
|
149
|
+
// git 的报错不透传给终端:结果以退出码 + HEAD ref 是否返回为准
|
|
150
|
+
child.stderr.on('data', () => { });
|
|
151
|
+
let done = false;
|
|
152
|
+
const finish = (r) => {
|
|
153
|
+
if (done)
|
|
154
|
+
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
|
+
}
|
|
126
169
|
/**
|
|
127
170
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入)。
|
|
128
171
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
package/lib/services/settings.js
CHANGED
|
@@ -12,8 +12,8 @@ import { dirname, join } from 'node:path';
|
|
|
12
12
|
import { profileDirectory } from './profile/profile.js';
|
|
13
13
|
export const DEFAULT_SETTINGS = {
|
|
14
14
|
checkUpdatesOnStart: true,
|
|
15
|
-
npmRegistry: '',
|
|
16
15
|
proxy: '',
|
|
16
|
+
npmRegistry: '',
|
|
17
17
|
enableNpmInstall: true,
|
|
18
18
|
enableGitInstall: true,
|
|
19
19
|
enableDshInstall: true,
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
3
3
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
4
4
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
5
|
-
*
|
|
5
|
+
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
6
|
+
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
7
|
+
* 慢网络下失败不阻塞安装。
|
|
6
8
|
*/
|
|
7
9
|
export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
|
|
@@ -18,6 +18,17 @@ export declare function systemProxy(): string | null;
|
|
|
18
18
|
* 供系统诊断 /diagnostics 使用;目标 URL 非法返回不可达。
|
|
19
19
|
*/
|
|
20
20
|
export declare function probeUrl(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult>;
|
|
21
|
+
/**
|
|
22
|
+
* git 通道真实克隆握手探测:spawn git ls-remote(https 传输,与 pnpm 克隆前的
|
|
23
|
+
* ref 握手一致),注入代理 env,GIT_TERMINAL_PROMPT=0 防凭据提示挂起。
|
|
24
|
+
*
|
|
25
|
+
* 为什么要真实 git 而非 curl 打网页:网页「能打开」和 git「能克隆」是两码事 ——
|
|
26
|
+
* 防火墙/代理常按端口与协议区分,HTTP 页可达不代表 git 传输可达。这里测的就是
|
|
27
|
+
* 克隆握手本身,回答「github:owner/repo 装不装得动」。
|
|
28
|
+
* 供系统诊断 GitHub 通道使用;探测目标用 dshplugin/hello-dsh 小仓库(秒级完成,
|
|
29
|
+
* 不打 17MB 的 dsh-plugin-hub 主页)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function gitLsRemote(url: string, proxy: string, timeoutMs: number): Promise<ProbeResult>;
|
|
21
32
|
/**
|
|
22
33
|
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入)。
|
|
23
34
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export interface HubSettings {
|
|
2
2
|
/** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
|
|
3
3
|
checkUpdatesOnStart: boolean;
|
|
4
|
-
/** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
|
|
5
|
-
npmRegistry: string;
|
|
6
4
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
7
5
|
proxy: string;
|
|
6
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置,本插件不注入任何 registry) */
|
|
7
|
+
npmRegistry: string;
|
|
8
8
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
9
9
|
enableNpmInstall: boolean;
|
|
10
10
|
/** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"build:server": "rm -rf lib && tsc -p tsconfig.server.json",
|
|
45
45
|
"build:client": "tsdown && node scripts/tools/normalize-client-banner.mjs",
|
|
46
46
|
"readme:stats": "node scripts/tools/sync-readme-stats.mjs",
|
|
47
|
-
"reload": "bash scripts/run/
|
|
48
|
-
"dev": "bash scripts/run/dev
|
|
47
|
+
"reload": "bash scripts/run/restart-dev.sh",
|
|
48
|
+
"dev": "bash scripts/run/restart-dev.sh",
|
|
49
49
|
"test": "node --test",
|
|
50
50
|
"test:watch": "node --test --watch",
|
|
51
51
|
"typecheck": "npm run typecheck:client && npm run typecheck:server && npm run typecheck:test",
|
|
@@ -35,6 +35,7 @@ import type { SectionView } from './layout/SectionTabs.tsx'
|
|
|
35
35
|
import { InstalledView } from './views/InstalledView.tsx'
|
|
36
36
|
import { CustomInstallView } from './views/CustomInstallView.tsx'
|
|
37
37
|
import { SettingsView } from './views/SettingsView.tsx'
|
|
38
|
+
import type { SettingsSection } from './views/SettingsView.tsx'
|
|
38
39
|
import { CloseIcon, ConfirmIcon } from './ui/icons.tsx'
|
|
39
40
|
import type { InstalledItem } from '../logic/installed.ts'
|
|
40
41
|
import { issueRepoOf, pluginOfItem } from '../logic/installed.ts'
|
|
@@ -47,6 +48,27 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
47
48
|
|
|
48
49
|
/** 一级导航:插件中心 / 已安装 / 自定义安装 / 设置 */
|
|
49
50
|
const [view, setView] = useState<SectionView>('market')
|
|
51
|
+
/** 外部跳转设置分组的一次性信号(错误弹窗「去系统诊断」→ 设置 → 系统诊断):
|
|
52
|
+
* 传给 SettingsView 消费后即清空,避免下次进设置被强制带跳 */
|
|
53
|
+
const [settingsSection, setSettingsSection] = useState<SettingsSection | null>(null)
|
|
54
|
+
/** 网络不通提示的「去系统诊断」直达:关掉所有弹窗(错误/通知/安装确认进度等,
|
|
55
|
+
* 否则 InstallModal 失败后仍停驻原地盖住设置页)→ 切到设置页 → 切到系统诊断分组(自动跑连通性检测) */
|
|
56
|
+
const openDiagnostics = () => {
|
|
57
|
+
setErrorMsg(null)
|
|
58
|
+
setShowNotifications(false)
|
|
59
|
+
setConfirmPlugin(null)
|
|
60
|
+
setConfirmCustomTarget(null)
|
|
61
|
+
setConfirmGlobalNpm(null)
|
|
62
|
+
setUninstallPlugin(null)
|
|
63
|
+
setUninstallItem(null)
|
|
64
|
+
setDetailItem(null)
|
|
65
|
+
setShowHubUpdate(false)
|
|
66
|
+
setShowAbout(false)
|
|
67
|
+
setIgnoreTarget(null)
|
|
68
|
+
setShowRestartConfirm(false)
|
|
69
|
+
setView('settings')
|
|
70
|
+
setSettingsSection('diagnostics')
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
/** 全局反馈 Toast:{id} 用于重复触发时重新走入场动画,kind 决定文案与配色 */
|
|
52
74
|
const [toast, setToast] = useState<ToastState | null>(null)
|
|
@@ -106,13 +128,16 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
106
128
|
|
|
107
129
|
const queue = useTaskQueue({
|
|
108
130
|
t,
|
|
131
|
+
// 界面语言带给服务端:网络预检等错误消息按用户语言提示(中文界面给中文,英文界面给英文)
|
|
132
|
+
langKey,
|
|
109
133
|
refreshInstalled: catalog.refreshInstalled,
|
|
110
|
-
onInstallDone: (viaModal, repo, needsRestart) => {
|
|
134
|
+
onInstallDone: (viaModal, repo, needsRestart, update) => {
|
|
111
135
|
setInstallNeedsRestart(needsRestart)
|
|
112
136
|
if (viaModal) setInstallDone(true)
|
|
113
137
|
else setToast({ id: Date.now(), kind: 'done' })
|
|
114
|
-
//
|
|
115
|
-
|
|
138
|
+
// 安装成功也写入通知记录:通知中心里成功与失败都能看到。
|
|
139
|
+
// action 区分「安装成功」与「更新成功」(更新 = 覆盖重装,同一执行通道)
|
|
140
|
+
setNotifications(addSuccess({ kind: 'install', action: update ? 'update' : 'install', repo: repo ?? '' }))
|
|
116
141
|
},
|
|
117
142
|
onUninstallDone: (viaModal, repo, needsRestart) => {
|
|
118
143
|
setUninstallNeedsRestart(needsRestart)
|
|
@@ -120,13 +145,16 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
120
145
|
else setToast({ id: Date.now(), kind: 'removed' })
|
|
121
146
|
setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
|
|
122
147
|
},
|
|
123
|
-
onError: (message, repo, kind, command, attempts) => {
|
|
148
|
+
onError: (message, repo, kind, command, attempts, update) => {
|
|
124
149
|
// 提 Issue 目标先反查仓库身份:npm 包名能在已安装表/目录里反查到 owner/repo 才保留,
|
|
125
|
-
// 反查不到置 null ——
|
|
150
|
+
// 反查不到置 null —— 错误弹窗据此不显示「一键提 Issue」按钮(防提到错误仓库)
|
|
126
151
|
const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
|
|
127
152
|
setErrorMsg({ message, repo: issueRepo, kind, command, attempts })
|
|
128
|
-
//
|
|
129
|
-
|
|
153
|
+
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
|
|
154
|
+
// 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
|
|
155
|
+
// 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
|
|
156
|
+
// 若这里就丢掉原始值,通知里会连名字都看不到;action 区分「安装失败/更新失败」
|
|
157
|
+
setNotifications(addFailure({ kind, action: update ? 'update' : 'install', repo: repo ?? '', message, command, attempts }))
|
|
130
158
|
},
|
|
131
159
|
installPlugin: confirmPlugin,
|
|
132
160
|
installCustomTarget: confirmCustomTarget,
|
|
@@ -458,6 +486,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
458
486
|
doCopy(text)
|
|
459
487
|
setToast({ id: Date.now(), kind: 'copied' })
|
|
460
488
|
},
|
|
489
|
+
// 网络不通提示「去系统诊断」→ 设置页并切到系统诊断分组(DiagnosticsView 挂载即自动跑探测)
|
|
490
|
+
openSection: settingsSection,
|
|
491
|
+
onConsumedOpenSection: () => setSettingsSection(null),
|
|
461
492
|
}),
|
|
462
493
|
// Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
|
|
463
494
|
// 无更新显示当前版本 + 记录 + 「已是最新」。Worker 拉取失败时兜底展示当前版本号。
|
|
@@ -517,6 +548,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
517
548
|
cancelTask: queue.cancelTask,
|
|
518
549
|
restarting,
|
|
519
550
|
onRestart: () => setShowRestartConfirm(true),
|
|
551
|
+
// 网络不通记录的「去系统诊断」直达:关通知中心 → 设置 → 系统诊断
|
|
552
|
+
onRunDiagnostics: openDiagnostics,
|
|
520
553
|
// 失败记录仓库反查:历史记录可能存的是裸 npm 包名,反查成 owner/repo 才显示仓库链接
|
|
521
554
|
// 与提 Issue 按钮;查不到则隐藏(防提到错误仓库)
|
|
522
555
|
resolveRepo: (repo: string) => issueRepoOf(repo, catalog.installedItems, catalog.plugins),
|
|
@@ -700,6 +733,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
700
733
|
doCopy(text)
|
|
701
734
|
setToast({ id: Date.now(), kind: 'errCopied' })
|
|
702
735
|
},
|
|
736
|
+
// 网络不通错误的「去系统诊断」直达:关错误弹窗 → 设置 → 系统诊断
|
|
737
|
+
onRunDiagnostics: openDiagnostics,
|
|
703
738
|
onClose: () => setErrorMsg(null),
|
|
704
739
|
}),
|
|
705
740
|
// 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出,稍后重启 / 立即重启
|
|
@@ -18,7 +18,7 @@ import type { MouseEvent } from 'react'
|
|
|
18
18
|
import styles from '../../styles/Modal.module.css'
|
|
19
19
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
20
20
|
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
21
|
-
import { classifyFailure, npmTooLowVersion } from '../../logic/failures.ts'
|
|
21
|
+
import { classifyFailure, npmTooLowVersion, unreachableTargetOf } from '../../logic/failures.ts'
|
|
22
22
|
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
23
23
|
import { pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
|
|
24
24
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
@@ -56,7 +56,7 @@ function BadgeGlyph({ ok }: { ok: boolean }) {
|
|
|
56
56
|
}))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, onUpdate, onIgnoreUpdate, cancelTask, restarting, onRestart, resolveRepo }: {
|
|
59
|
+
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, onUpdate, onIgnoreUpdate, cancelTask, restarting, onRestart, onRunDiagnostics, resolveRepo }: {
|
|
60
60
|
records: NotificationRecord[]
|
|
61
61
|
/** 进行中的安装/卸载任务(实时进度,与队列弹窗同一数据源) */
|
|
62
62
|
tasks: QueueTask[]
|
|
@@ -76,6 +76,8 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
76
76
|
cancelTask: (id: number) => void
|
|
77
77
|
restarting: boolean
|
|
78
78
|
onRestart: () => void
|
|
79
|
+
/** 网络不通时的「去系统诊断」直达按钮:跳到设置 → 系统诊断跑连通性检测(由宿主提供) */
|
|
80
|
+
onRunDiagnostics?: () => void
|
|
79
81
|
/** 失败记录 repo 反查:npm 包名(历史记录存的是裸包名)反查成 owner/repo 再显示/提 Issue;
|
|
80
82
|
* 查不到返回 null,按钮与仓库链接一并隐藏(防提到错误仓库)。不传则按记录原样展示。 */
|
|
81
83
|
resolveRepo?: (repo: string) => string | null
|
|
@@ -193,8 +195,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
193
195
|
h('span', {
|
|
194
196
|
className: r.ok ? styles.noticeTextOk : styles.noticeTextFail,
|
|
195
197
|
}, r.ok
|
|
196
|
-
? (isUpdate
|
|
197
|
-
|
|
198
|
+
? (isUpdate
|
|
199
|
+
? t('updateNoticeTitle')
|
|
200
|
+
: (r.action === 'update' ? t('updateDone') : (r.kind === 'install' ? t('installDone') : t('uninstallDone'))))
|
|
201
|
+
: (r.action === 'update'
|
|
202
|
+
? t('errorTitleUpdate')
|
|
203
|
+
: (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall')))),
|
|
198
204
|
// 更新提醒:新版本号紧随标题展示;repo 为纯文本(整行点击即去更新,不跳官网外链)
|
|
199
205
|
isUpdate && r.version
|
|
200
206
|
? h('span', { className: styles.noticeVersion, title: r.version }, `v${r.version}`)
|
|
@@ -211,7 +217,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
211
217
|
rel: 'noopener noreferrer',
|
|
212
218
|
title: shownRepo,
|
|
213
219
|
}, shownRepo)
|
|
214
|
-
|
|
220
|
+
// 反查不到仓库身份(如命令行安装的裸 npm 包名):回退显示记录里的原始值,
|
|
221
|
+
// 保证通知永不只剩一句「安装/更新成功」没有名字 —— 只是不给官网外链(查不到不敢乱跳)
|
|
222
|
+
: h('span', { className: styles.failRepo, title: r.repo }, r.repo))
|
|
215
223
|
: null,
|
|
216
224
|
!r.ok && h('button', {
|
|
217
225
|
className: styles.failCopy,
|
|
@@ -258,6 +266,20 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
258
266
|
return h('div', { className: styles.failPrepareHint },
|
|
259
267
|
t(v ? 'failNpmTooLowV' : 'failNpmTooLow', v ? { v } : undefined))
|
|
260
268
|
}
|
|
269
|
+
if (kind === 'network') {
|
|
270
|
+
// 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
271
|
+
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
272
|
+
// 不引导提 Issue
|
|
273
|
+
return h('div', null, [
|
|
274
|
+
// 「无法访问 …」醒目行:精准告诉用户具体哪个地址连不上(消息里取不到地址就跳过这行)
|
|
275
|
+
(() => { const target = unreachableTargetOf(r.message); return target ? h('div', { className: styles.failNetworkTarget }, t('failNetworkTarget', { url: target })) : null })(),
|
|
276
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
277
|
+
onRunDiagnostics ? h('button', {
|
|
278
|
+
className: styles.failDiagBtn,
|
|
279
|
+
onClick: onRunDiagnostics,
|
|
280
|
+
}, t('failNetworkRunDiag')) : null,
|
|
281
|
+
])
|
|
282
|
+
}
|
|
261
283
|
if (kind === 'pluginPrepare' || kind === 'pnpmIgnoredBuild') {
|
|
262
284
|
// [packaging](预检/装后校验拦截:git 分发缺产物)与
|
|
263
285
|
// prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
|
|
@@ -16,7 +16,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
|
|
|
16
16
|
import { ProgressView } from './ProgressView.tsx'
|
|
17
17
|
import { installCommandOf } from '../../logic/install-command.ts'
|
|
18
18
|
import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
|
|
19
|
-
import { classifyFailure, npmTooLowVersion } from '../../logic/failures.ts'
|
|
19
|
+
import { classifyFailure, npmTooLowVersion, unreachableTargetOf } from '../../logic/failures.ts'
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
22
22
|
* needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
|
|
@@ -320,7 +320,7 @@ export function UninstallModal(props: UninstallModalProps) {
|
|
|
320
320
|
|
|
321
321
|
/** 预填插件仓库的 GitHub Issue 链接:标题带插件名,正文附完整错误信息,方便用户一键反馈。 */
|
|
322
322
|
/** 安装/卸载失败弹窗:布局与失败记录一致(类型徽标 + 仓库超链接 + 隐蔽复制按钮),报错完整展示,底部一键提交 Issue。 */
|
|
323
|
-
export function ErrorModal({ message, repo, kind, command, attempts, t, env, onCopy, onClose }: {
|
|
323
|
+
export function ErrorModal({ message, repo, kind, command, attempts, t, env, onCopy, onClose, onRunDiagnostics }: {
|
|
324
324
|
message: string
|
|
325
325
|
repo: string | null
|
|
326
326
|
kind: 'install' | 'uninstall'
|
|
@@ -331,6 +331,8 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
331
331
|
env: EnvInfo | null
|
|
332
332
|
onCopy: (text: string) => void
|
|
333
333
|
onClose: () => void
|
|
334
|
+
/** 网络不通时的「去系统诊断」直达按钮:跳到设置 → 系统诊断跑连通性检测(由宿主提供) */
|
|
335
|
+
onRunDiagnostics?: () => void
|
|
334
336
|
}) {
|
|
335
337
|
// 失败归类:当前安装通道(npm/git)装不上 = 插件分发/依赖的问题,一律引导提 Issue
|
|
336
338
|
const failureKind = classifyFailure(message)
|
|
@@ -385,7 +387,20 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
385
387
|
failureKind === 'npmTooOld'
|
|
386
388
|
? h('div', { className: styles.failPrepareHint },
|
|
387
389
|
t(npmVersion ? 'failNpmTooLowV' : 'failNpmTooLow', npmVersion ? { v: npmVersion } : undefined))
|
|
388
|
-
: failureKind === '
|
|
390
|
+
: failureKind === 'network'
|
|
391
|
+
// 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
392
|
+
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
393
|
+
// 不引导提 Issue
|
|
394
|
+
? h('div', null, [
|
|
395
|
+
// 「无法访问 …」醒目行:精准告诉用户具体哪个地址连不上(消息里取不到地址就跳过这行)
|
|
396
|
+
(() => { const target = unreachableTargetOf(message); return target ? h('div', { className: styles.failNetworkTarget }, t('failNetworkTarget', { url: target })) : null })(),
|
|
397
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
398
|
+
onRunDiagnostics ? h('button', {
|
|
399
|
+
className: styles.failDiagBtn,
|
|
400
|
+
onClick: onRunDiagnostics,
|
|
401
|
+
}, t('failNetworkRunDiag')) : null,
|
|
402
|
+
])
|
|
403
|
+
: failureKind === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
|
|
389
404
|
? h('div', null, [
|
|
390
405
|
// [packaging](预检/装后校验拦截:git 分发缺产物)与
|
|
391
406
|
// prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
|
|
@@ -15,7 +15,7 @@ import styles from '../../styles/DiagnosticsView.module.css'
|
|
|
15
15
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
16
16
|
import { PLUGIN_VERSION } from '../../logic/constants.ts'
|
|
17
17
|
|
|
18
|
-
type ProbeKey = 'npm' | 'github' | 'catalog'
|
|
18
|
+
type ProbeKey = 'npm' | 'github' | 'catalog' | 'proxy'
|
|
19
19
|
|
|
20
20
|
/** 系统版本文本:提交 Issue 时粘贴到正文,方便作者复现。 */
|
|
21
21
|
function formatEnv(env: EnvInfo): string {
|
|
@@ -40,17 +40,28 @@ interface RowState {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const INITIAL_ROWS: RowState[] = [
|
|
43
|
-
|
|
44
|
-
{ key: '
|
|
43
|
+
// npm 行 display 初始置空:设置里未配置镜像时显示「未配置(跟随本机)」,探测结果回来再用服务端 display 覆盖
|
|
44
|
+
{ key: 'npm', display: '', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
|
|
45
|
+
{ key: 'github', display: 'github.com/dshplugin/hello-dsh', nameKey: 'diagGithub', status: 'idle', ms: null, statusCode: null },
|
|
45
46
|
{ key: 'catalog', display: 'api.dsh-plugin.org', nameKey: 'diagCatalog', status: 'idle', ms: null, statusCode: null },
|
|
46
47
|
]
|
|
47
48
|
|
|
48
|
-
export function DiagnosticsView({ t, env, onCopy }: {
|
|
49
|
+
export function DiagnosticsView({ t, env, proxy, onCopy }: {
|
|
49
50
|
t: Translate
|
|
50
51
|
env: EnvInfo | null
|
|
52
|
+
/** 设置里配置的 HTTP 代理地址;留空(未配置)不显示代理行 */
|
|
53
|
+
proxy: string
|
|
51
54
|
onCopy: (text: string) => void
|
|
52
55
|
}) {
|
|
53
|
-
|
|
56
|
+
// 代理行仅在配置了 HTTP 代理时出现:用该代理探测 github.com,验证「代理能不能把请求带出去」。
|
|
57
|
+
// 未配置(留空直连)没有可验证的代理,不渲染这一行,避免无意义的诊断项。
|
|
58
|
+
const [rows, setRows] = useState<RowState[]>(() => {
|
|
59
|
+
const base = INITIAL_ROWS.map((r) => ({ ...r }))
|
|
60
|
+
if (proxy.trim() !== '') {
|
|
61
|
+
base.push({ key: 'proxy', display: proxy.trim(), nameKey: 'diagProxy', status: 'idle', ms: null, statusCode: null })
|
|
62
|
+
}
|
|
63
|
+
return base
|
|
64
|
+
})
|
|
54
65
|
/** 当前探测请求:重测/卸载时 abort 旧请求,避免乱序结果覆盖 */
|
|
55
66
|
const runningRef = useRef<AbortController | null>(null)
|
|
56
67
|
|
|
@@ -87,12 +98,14 @@ export function DiagnosticsView({ t, env, onCopy }: {
|
|
|
87
98
|
const raw = buf.slice(0, nl).trim()
|
|
88
99
|
buf = buf.slice(nl + 1)
|
|
89
100
|
if (raw !== '') {
|
|
90
|
-
const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null }
|
|
101
|
+
const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null; display?: string }
|
|
91
102
|
if (ev.type === 'ok' || ev.type === 'fail') {
|
|
92
103
|
patchRows(ev.key as ProbeKey, {
|
|
93
104
|
status: ev.type === 'ok' ? 'ok' : 'fail',
|
|
94
105
|
ms: typeof ev.ms === 'number' ? ev.ms : null,
|
|
95
106
|
statusCode: typeof ev.status === 'number' ? ev.status : null,
|
|
107
|
+
// 服务端带出实际使用的源地址(配置镜像 → 镜像地址;未配置 → 空串)
|
|
108
|
+
display: typeof ev.display === 'string' ? ev.display : '',
|
|
96
109
|
})
|
|
97
110
|
}
|
|
98
111
|
}
|
|
@@ -159,7 +172,8 @@ export function DiagnosticsView({ t, env, onCopy }: {
|
|
|
159
172
|
title: t('diagRecheck'),
|
|
160
173
|
},
|
|
161
174
|
h('span', { className: styles.name }, t(r.nameKey)),
|
|
162
|
-
|
|
175
|
+
// npm 行未配置镜像:display 为空 → 显示「未配置(跟随本机)」,不误导成官方源
|
|
176
|
+
h('span', { className: styles.display }, r.display !== '' ? r.display : t('diagNpmUnset')),
|
|
163
177
|
h('span', { className: styles.meta }, r.status === 'ok' && r.ms !== null ? `${r.ms} ms` : ''),
|
|
164
178
|
badge,
|
|
165
179
|
)
|