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.
@@ -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 的子进程环境:透传宿主环境,并按设置注入 npm 镜像源与 HTTP(S) 代理。
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 !== '' ? settings.npmRegistry : 'registry.npmjs.org', cmd: `npm view dsh-plugin version --registry ${registry}` },
493
- { key: 'github', url: 'https://github.com/dshplugin/dsh-plugin-hub', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/dsh-plugin-hub' },
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
- const r = await probe(c.url);
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
- send({ type: r.ok ? 'ok' : 'fail', key: c.key, display: c.display, ms: r.ms, status: r.status });
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
- * @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
23
+ * registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
24
+ * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
25
+ * 慢网络下失败不阻塞安装。
24
26
  */
25
27
  export function resolveNpmPackage(repo, registry = '') {
26
28
  const key = repo.toLowerCase();
@@ -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 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
@@ -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
- * @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
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.5",
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/reload-dsh.sh",
48
- "dev": "bash scripts/run/dev-dsh.sh",
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
- setNotifications(addSuccess({ kind: 'install', repo: repo ?? '' }))
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 —— 错误弹窗/通知中心据此不显示「一键提 Issue」按钮(防提到错误仓库)
150
+ // 反查不到置 null —— 错误弹窗据此不显示「一键提 Issue」按钮(防提到错误仓库)
126
151
  const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
127
152
  setErrorMsg({ message, repo: issueRepo, kind, command, attempts })
128
- // 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕
129
- setNotifications(addFailure({ kind, repo: issueRepo ?? '', message, command, attempts }))
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 ? t('updateNoticeTitle') : (r.kind === 'install' ? t('installDone') : t('uninstallDone')))
197
- : (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall'))),
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
- : null)
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 === 'pluginPrepare' || failureKind === 'pnpmIgnoredBuild'
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
- { key: 'npm', display: 'registry.npmjs.org', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
44
- { key: 'github', display: 'github.com', nameKey: 'diagGithub', status: 'idle', ms: null, statusCode: null },
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
- const [rows, setRows] = useState<RowState[]>(INITIAL_ROWS)
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
- h('span', { className: styles.display }, r.display),
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
  )