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.
@@ -15,12 +15,13 @@
15
15
  * Diagnostics — live connectivity probe + environment snapshot
16
16
  * Logs — system log viewer + storage location
17
17
  */
18
- import { createElement as h, useState } from 'react'
18
+ import { createElement as h, useEffect, useRef, useState } from 'react'
19
19
  import type { ChangeEvent, MouseEvent, ReactNode } from 'react'
20
20
  import styles from '../../styles/SettingsView.module.css'
21
+ import dropdownStyles from '../../styles/Dropdown.module.css'
21
22
  import modalStyles from '../../styles/Modal.module.css'
22
- import { Dropdown } from '../ui/Dropdown.tsx'
23
23
  import { Toggle } from '../ui/Toggle.tsx'
24
+ import { Dropdown } from '../ui/Dropdown.tsx'
24
25
  import { DiagnosticsView } from './DiagnosticsView.tsx'
25
26
  import { LogsView } from './LogsView.tsx'
26
27
  import {
@@ -29,7 +30,18 @@ import {
29
30
  import type { EnvInfo, Translate } from '../../types.ts'
30
31
  import type { HubSettings } from '../../hooks/useSettings.ts'
31
32
 
32
- type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
33
+ /** 设置分组导航 key:顺序即展示顺序。导出供外部(如错误弹窗「去系统诊断」)跳转指定分组。 */
34
+ export type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
35
+
36
+ /** npm 镜像源预设:空串 = 未配置(跟随用户本机 npm 配置,本插件不注入任何 registry)。
37
+ * 只提供常见国内顶级镜像下拉,不做自定义输入 —— 想用其它镜像直接在 ~/.npmrc 配置即可。 */
38
+ const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
39
+ { value: '', labelKey: 'mirrorNone' },
40
+ { value: 'https://registry.npmjs.org', labelKey: 'mirrorOfficial' },
41
+ { value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
42
+ { value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
43
+ { value: 'https://mirrors.tuna.tsinghua.edu.cn/npm/', labelKey: 'mirrorTsinghua' },
44
+ ]
33
45
 
34
46
  /** 左侧导航:顺序即展示顺序;labelKey 是分组标题,icon 是行内小图标。 */
35
47
  const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnType<typeof h> }> = [
@@ -40,13 +52,6 @@ const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnTyp
40
52
  { key: 'reset', labelKey: 'settingsReset', icon: ResetIcon },
41
53
  ]
42
54
 
43
- /** npm 镜像源预设:空串 = 官方源;值相等即视为选中对应预设,其余进入「自定义」输入。 */
44
- const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
45
- { value: '', labelKey: 'mirrorOfficial' },
46
- { value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
47
- { value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
48
- ]
49
-
50
55
  /** 设置行:默认标签左控件右;stack=true 时控件独占一行(文本框整行显示),hairline 分隔。 */
51
56
  function SettingRow({ title, desc, children, stack = false }: {
52
57
  title: string
@@ -63,26 +68,71 @@ function SettingRow({ title, desc, children, stack = false }: {
63
68
  )
64
69
  }
65
70
 
66
- export function SettingsView({ t, settings, update, reset, env, onCopy }: {
71
+ export function SettingsView({ t, settings, update, reset, env, onCopy, openSection, onConsumedOpenSection }: {
67
72
  t: Translate
68
73
  settings: HubSettings
69
74
  update: (patch: Partial<HubSettings>) => void
70
75
  reset: () => void
71
76
  env: EnvInfo | null
72
77
  onCopy: (text: string) => void
78
+ /** 外部跳转信号(如错误弹窗「去系统诊断」):非空时切到对应分组,消费后通过 onConsumedOpenSection 清空 */
79
+ openSection?: SettingsSection | null
80
+ onConsumedOpenSection?: () => void
73
81
  }) {
74
82
  /** 当前激活的分组:左侧导航点击切换,右侧只渲染这一组 */
75
83
  const [section, setSection] = useState<SettingsSection>('updates')
76
84
  /** 恢复默认确认弹窗:点按钮先弹窗询问,确认后才真正 reset(不做两段式按钮) */
77
85
  const [resetConfirm, setResetConfirm] = useState(false)
78
86
 
79
- const mirrorCustom = !MIRROR_PRESETS.some((p) => p.value === settings.npmRegistry)
80
- const onMirrorChange = (value: string) => {
81
- if (value === 'custom') return // 进入自定义输入:保持当前值,下方出现输入框
82
- update({ npmRegistry: value })
87
+ // 外部跳转(错误弹窗「去系统诊断」→ 设置 → 系统诊断):切到对应分组并清空信号,避免下次进入设置被强制带跳
88
+ useEffect(() => {
89
+ if (!openSection) return
90
+ setSection(openSection)
91
+ onConsumedOpenSection?.()
92
+ }, [openSection, onConsumedOpenSection])
93
+
94
+ // —— HTTP 代理连通性:输入停笔后防抖实时探测(服务端 /proxy-check 用该代理打 github.com)。
95
+ // 结果只作提示、绝不阻断保存 —— 用户可能是先填地址后开代理,保存后再去开代理完全合法。
96
+ const [proxyProbe, setProxyProbe] = useState<'idle' | 'checking' | 'ok' | 'fail'>('idle')
97
+ const proxyProbeTimer = useRef<number | null>(null)
98
+ const proxyProbeSeq = useRef(0)
99
+ // 卸载时清掉未触发的防抖计时器,避免卸载后 setState
100
+ useEffect(() => () => {
101
+ if (proxyProbeTimer.current !== null) window.clearTimeout(proxyProbeTimer.current)
102
+ proxyProbeSeq.current += 1
103
+ }, [])
104
+ // 设置被外部重置(恢复默认/清空代理)时同步清掉探测状态,避免残留过期的「不通」提示
105
+ useEffect(() => {
106
+ if (settings.proxy === '') setProxyProbe('idle')
107
+ }, [settings.proxy])
108
+
109
+ const probeProxyNow = (value: string) => {
110
+ const v = value.trim()
111
+ if (v === '') { setProxyProbe('idle'); return }
112
+ const seq = ++proxyProbeSeq.current
113
+ setProxyProbe('checking')
114
+ void fetch('/dsh-plugin-hub/proxy-check', {
115
+ method: 'POST',
116
+ headers: { 'content-type': 'application/json' },
117
+ body: JSON.stringify({ proxy: v }),
118
+ cache: 'no-store',
119
+ }).then((res) => res.json())
120
+ .then((data: { ok?: boolean }) => {
121
+ if (seq !== proxyProbeSeq.current) return
122
+ setProxyProbe(data.ok ? 'ok' : 'fail')
123
+ })
124
+ .catch(() => { if (seq === proxyProbeSeq.current) setProxyProbe('fail') })
125
+ }
126
+
127
+ const onProxyChange = (e: ChangeEvent<HTMLInputElement>) => {
128
+ const value = e.target.value
129
+ // 保存照常:留空直连、填了就走该代理(先填后开代理也允许,仅提示不通)
130
+ update({ proxy: value })
131
+ if (proxyProbeTimer.current !== null) window.clearTimeout(proxyProbeTimer.current)
132
+ // 停笔 700ms 再探测,避免每敲一个字符都发请求
133
+ proxyProbeTimer.current = window.setTimeout(() => probeProxyNow(value), 700)
83
134
  }
84
- const onMirrorInput = (e: ChangeEvent<HTMLInputElement>) => update({ npmRegistry: e.target.value })
85
- const onProxyChange = (e: ChangeEvent<HTMLInputElement>) => update({ proxy: e.target.value })
135
+ const onMirrorChange = (value: string) => update({ npmRegistry: value })
86
136
 
87
137
  /** 每个分组的页面副标题(导航标题之上再补一句说明,保持桌面设置质感) */
88
138
  const pageDesc: Record<SettingsSection, string> = {
@@ -106,46 +156,44 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
106
156
  title: t('settingsCheckOnStart'),
107
157
  }),
108
158
  }),
159
+ // npm 镜像源:预设下拉(默认未配置跟随本机),选国内顶级镜像可加速安装;
160
+ // 不做自定义输入,其它镜像写在 ~/.npmrc 即可被天然尊重。
161
+ // 独占一行(同代理文本框):标签在上、下拉在下整行展开,长描述不会挤压控件
109
162
  h(SettingRow, {
110
163
  title: t('settingsMirror'),
111
164
  desc: t('settingsMirrorDesc'),
165
+ stack: true,
112
166
  children: h(Dropdown, {
113
- value: mirrorCustom ? 'custom' : settings.npmRegistry,
114
- options: [
115
- ...MIRROR_PRESETS.map((p) => ({ value: p.value, label: t(p.labelKey) })),
116
- { value: 'custom', label: t('mirrorCustom') },
117
- ],
167
+ value: settings.npmRegistry,
168
+ options: MIRROR_PRESETS.map((m) => ({ value: m.value, label: t(m.labelKey) })),
118
169
  onChange: onMirrorChange,
119
170
  title: t('settingsMirror'),
120
- className: styles.controlDropdown,
171
+ className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
121
172
  }),
122
173
  }),
123
- // 自定义镜像源:下拉选中「自定义」或已存非预设值时,追加一行输入框(文本框独占一行)
124
- mirrorCustom && h(SettingRow, {
125
- title: t('mirrorCustom'),
126
- stack: true,
127
- children: h('input', {
128
- className: styles.textInput,
129
- type: 'url',
130
- value: settings.npmRegistry,
131
- placeholder: 'https://registry.npmjs.org',
132
- spellCheck: false,
133
- onChange: onMirrorInput,
134
- }),
135
- }),
136
- // 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址
174
+ // 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址;停笔后实时探测连通性,
175
+ // 不通仅提示(可保存)—— 用户可能先填地址后开代理
137
176
  h(SettingRow, {
138
177
  title: t('settingsProxy'),
139
178
  desc: t('settingsProxyDesc'),
140
179
  stack: true,
141
- children: h('input', {
142
- className: styles.textInput,
143
- type: 'text',
144
- value: settings.proxy,
145
- placeholder: 'http://127.0.0.1:7890',
146
- spellCheck: false,
147
- onChange: onProxyChange,
148
- }),
180
+ children: h('div', { className: styles.proxyControl },
181
+ h('input', {
182
+ className: styles.textInput,
183
+ type: 'text',
184
+ value: settings.proxy,
185
+ placeholder: 'http://127.0.0.1:7890',
186
+ spellCheck: false,
187
+ onChange: onProxyChange,
188
+ }),
189
+ proxyProbe === 'checking'
190
+ ? h('div', { className: styles.proxyHint }, t('proxyCheckChecking'))
191
+ : proxyProbe === 'ok'
192
+ ? h('div', { className: `${styles.proxyHint} ${styles.proxyHintOk}` }, t('proxyCheckOk'))
193
+ : proxyProbe === 'fail'
194
+ ? h('div', { className: `${styles.proxyHint} ${styles.proxyHintFail}` }, t('proxyCheckFail'))
195
+ : null,
196
+ ),
149
197
  }),
150
198
  )
151
199
 
@@ -185,7 +233,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
185
233
 
186
234
  const diagnosticsCard = h('div', { className: styles.card },
187
235
  // 系统版本(最顶部)+ 通道状态列表:进入页面自动探测一轮,整行可点击重测单项,实时反馈
188
- h(DiagnosticsView, { t, env, onCopy }),
236
+ h(DiagnosticsView, { t, env, proxy: settings.proxy, onCopy }),
189
237
  )
190
238
 
191
239
  const logsCard = h('div', { className: styles.card },
@@ -14,10 +14,10 @@ import { useCallback, useEffect, useState } from 'react'
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 包(默认开) */
22
22
  enableNpmInstall: boolean
23
23
  /** 启用命令行安装 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,
@@ -10,7 +10,7 @@
10
10
  * uninstall, cancel) plus the modal task lookups used by the dialogs.
11
11
  */
12
12
  import { useEffect, useRef, useState } from 'react'
13
- import type { HubPlugin, Translate } from '../types.ts'
13
+ import type { HubPlugin, LocaleId, Translate } from '../types.ts'
14
14
  import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
15
  import type { InstalledItem } from '../logic/installed.ts'
16
16
 
@@ -28,6 +28,9 @@ export interface QueueTask {
28
28
  desc?: string
29
29
  /** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
30
30
  repo: string | null
31
+ /** 实际动作(add/update/remove):成功通知据此区分「安装成功」与「更新成功」。
32
+ * 服务端 /active 带回真实动作;本地乐观入队按 update 标记推断。 */
33
+ action?: 'add' | 'update' | 'remove'
31
34
  /** 安装时的目录信号快照(版本 + 仓库更新时间):任务成功后在本地记录,供「有更新」比对 */
32
35
  version?: string
33
36
  updatedAt?: string
@@ -61,13 +64,16 @@ export interface PendingRestart {
61
64
 
62
65
  export interface TaskQueueOptions {
63
66
  t: Translate
67
+ /** 界面语言(zh/en):随安装请求带给服务端,网络预检等错误消息按用户语言提示 */
68
+ langKey?: LocaleId
64
69
  refreshInstalled: () => void
65
70
  /** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
66
- * needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。 */
67
- onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
71
+ * needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。
72
+ * update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
73
+ onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean) => void
68
74
  onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
69
- /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)。 */
70
- onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[]) => void
75
+ /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)+ 是否更新。 */
76
+ onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
71
77
  /** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
72
78
  installPlugin: HubPlugin | null
73
79
  /** 当前打开的自定义安装(命令行)确认弹窗目标:与应用商店同一弹窗机制,匹配进行中的 custom 安装任务。 */
@@ -85,7 +91,7 @@ export interface TaskQueueOptions {
85
91
 
86
92
  export function useTaskQueue(opts: TaskQueueOptions) {
87
93
  const {
88
- t, refreshInstalled, onInstallDone, onUninstallDone, onError,
94
+ t, langKey, refreshInstalled, onInstallDone, onUninstallDone, onError,
89
95
  installPlugin, installCustomTarget, installGlobalTarget, uninstallPlugin, uninstallName, installedName, resolvePending,
90
96
  } = opts
91
97
  const [queue, setQueue] = useState<QueueTask[]>([])
@@ -195,14 +201,14 @@ export function useTaskQueue(opts: TaskQueueOptions) {
195
201
  // 同时缓存展示信息,待重启列表(服务端已登记)合并时直接补齐简介/版本
196
202
  void syncInstalledVersion(q.repo, q.version, q.updatedAt)
197
203
  pendingInfoRef.current.set(q.target, { desc: q.desc, version: q.version })
198
- onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart)
204
+ onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.action === 'update')
199
205
  }
200
206
  } else {
201
207
  // 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
202
208
  const detail = lines.length > 0
203
209
  ? [...lines].reverse().join('\n')
204
210
  : q.kind === 'uninstall' ? t('uninstallFail') : t('installFail')
205
- onError(detail, q.repo, q.kind, q.command, q.attempts)
211
+ onError(detail, q.repo, q.kind, q.command, q.attempts, q.action === 'update')
206
212
  }
207
213
  maybeStopPoll()
208
214
  }
@@ -367,6 +373,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
367
373
  kind: (isRemove ? 'uninstall' : 'install') as 'install' | 'uninstall',
368
374
  target: isRemove ? fallback : (display || fallback),
369
375
  repo: isRemove ? null : (display || fallback),
376
+ action: (a.action === 'add' || a.action === 'update' || a.action === 'remove') ? a.action : 'add',
370
377
  status: a.status === 'running' ? 'running' : 'pending',
371
378
  progress: typeof a.progress === 'number' ? a.progress : 0,
372
379
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
@@ -421,6 +428,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
421
428
  version: input.version, updatedAt: input.updatedAt,
422
429
  status: 'running', progress: 0, lines: [], optimistic: true,
423
430
  command: input.command,
431
+ action: input.update ? 'update' : 'add',
424
432
  // 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
425
433
  attempts: [], needsRestart: true,
426
434
  }])
@@ -436,6 +444,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
436
444
  display: input.repo,
437
445
  source: input.source,
438
446
  mode: input.update ? 'update' : undefined,
447
+ // 界面语言带给服务端:网络预检等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
448
+ ...(langKey ? { lang: langKey } : {}),
439
449
  ...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
440
450
  }),
441
451
  signal: controller.signal,
@@ -108,14 +108,19 @@ export const zh = {
108
108
  settingsUpdateDesc: '更新检查方式与安装走到的网络通道',
109
109
  settingsCheckOnStart: '启动时检查更新',
110
110
  settingsCheckOnStartDesc: '打开插件中心时检查已安装插件是否有新版,发现更新会提示',
111
+ settingsProxy: 'HTTP 代理',
112
+ settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
113
+ // 代理连通性实时探测:输入停笔后提示检测结果(不通仍可保存,先填地址后开代理也合法)
114
+ proxyCheckChecking: '正在测试该代理地址…',
115
+ proxyCheckOk: '测试可达,安装与目录请求将统一走该代理',
116
+ proxyCheckFail: '此地址测试不通,仍可保存;请确认代理已开启、地址与端口无误',
111
117
  settingsMirror: 'npm 镜像源',
112
- settingsMirrorDesc: 'npm 安装与版本反查走自定义镜像,网络不佳时更稳定;留空使用官方源',
118
+ settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
119
+ mirrorNone: '未配置(跟随本机 npm 配置)',
113
120
  mirrorOfficial: '官方源 registry.npmjs.org',
114
121
  mirrorNpmmirror: '阿里云 npmmirror',
115
122
  mirrorTencent: '腾讯云镜像',
116
- mirrorCustom: '自定义镜像源',
117
- settingsProxy: 'HTTP 代理',
118
- settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
123
+ mirrorTsinghua: '清华大学 TUNA 镜像',
119
124
  settingsSecurity: '安全信任',
120
125
  settingsSecurityDesc: '命令行安装的三个通道开关:NPM 包、GitHub 源码与 DSH 命令',
121
126
  settingsEnableNpm: '启用 NPM 安装',
@@ -181,8 +186,11 @@ export const zh = {
181
186
  settingsDiagFail: '不可达',
182
187
  // 系统诊断通道列表:一键全量诊断 + 一行一个,可点重测
183
188
  diagNpm: 'npm 源',
189
+ // 诊断页 npm 行:未配置镜像时的展示(探测目标仍为官方源,代表本机无配置时的回退)
190
+ diagNpmUnset: '未配置(跟随本机 npm 配置)',
184
191
  diagGithub: 'GitHub',
185
192
  diagCatalog: '目录站点',
193
+ diagProxy: 'HTTP 代理',
186
194
  diagOk: 'OK',
187
195
  diagChecking: '检测中…',
188
196
  diagIdle: '未检测',
@@ -231,6 +239,8 @@ export const zh = {
231
239
  installNow: '直接安装',
232
240
  installing: '安装中…',
233
241
  installDone: '安装成功',
242
+ // 更新成功(通知中心区分安装/更新):更新 = 已安装目标覆盖重装到最新,与首次安装不同语义
243
+ updateDone: '更新成功',
234
244
  installResultTitle: '插件安装成功',
235
245
  installResultDesc: '',
236
246
  // 更新语义:已安装插件点「更新」走同一条 add 命令原位覆盖重装,弹窗文案区分安装/更新
@@ -275,6 +285,8 @@ export const zh = {
275
285
  errorTitle: '操作失败',
276
286
  errorTitleInstall: '安装失败',
277
287
  errorTitleUninstall: '卸载失败',
288
+ // 更新失败(通知中心区分安装/更新):更新目标覆盖重装失败,与首次安装失败不同语义
289
+ errorTitleUpdate: '更新失败',
278
290
  errorPlugin: '插件',
279
291
  errorClose: '知道了',
280
292
  errorCopy: '复制错误信息',
@@ -287,10 +299,16 @@ export const zh = {
287
299
  // 安装前预检 / 装后校验拦截([packaging]):git 分发缺构建产物、入口文件不存在 = 插件未适配官方默认安装方式
288
300
  failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物或发布 npm 版。',
289
301
  // 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):只影响带原生模块的插件,属插件依赖打包问题,应提 Issue
290
- failIgnoredBuild: '该插件依赖了需要编译的原生模块(如 node-pty),pnpm 默认拦截这类构建脚本导致安装失败 —— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)。',
302
+ failIgnoredBuild: '该插件或其依赖需要执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截导致安装失败(如原生模块 node-pty、或 git 安装时插件的 prepare 脚本)—— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)或移除安装期构建。',
291
303
  // npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
292
304
  failNpmTooLow: '怀疑本机 npm 版本过低:npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
293
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: '去系统诊断检测网络',
294
312
  reportIssue: '去插件仓库提交 Issue',
295
313
  detail: '打开详情',
296
314
  noResult: '没有找到匹配的插件',
@@ -436,14 +454,20 @@ export const en = {
436
454
  settingsCheckOnStartDesc: 'Check installed plugins for new versions when the hub opens; you will be notified when updates are found',
437
455
  settingsAutoInstall: 'Auto-install updates',
438
456
  settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
439
- settingsMirror: 'npm registry mirror',
440
- settingsMirrorDesc: 'Route npm installs and lookups through a mirror for unstable networks; leave empty for the official registry',
457
+ settingsProxy: 'HTTP proxy',
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',
464
+ settingsMirror: 'npm mirror',
465
+ settingsMirrorDesc: 'Unset by default — follows local npm config; pick a domestic mirror if slow',
466
+ mirrorNone: 'Unset (follow local npm config)',
441
467
  mirrorOfficial: 'Official registry.npmjs.org',
442
468
  mirrorNpmmirror: 'Alibaba npmmirror',
443
469
  mirrorTencent: 'Tencent Cloud mirror',
444
- mirrorCustom: 'Custom mirror',
445
- settingsProxy: 'HTTP proxy',
446
- settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
470
+ mirrorTsinghua: 'Tsinghua TUNA mirror',
447
471
  settingsSecurity: 'Security & Trust',
448
472
  settingsSecurityDesc: 'Toggles for the three command-line install channels: npm packages, GitHub sources and DSH commands',
449
473
  settingsEnableNpm: 'Enable npm installs',
@@ -509,8 +533,11 @@ export const en = {
509
533
  settingsDiagFail: 'Unreachable',
510
534
  // System diagnostics channel list: one-click full check + one row per channel, click to re-probe
511
535
  diagNpm: 'npm registry',
536
+ // Diagnostics npm row: shown when no mirror is configured (probe still targets the official registry, the fallback)
537
+ diagNpmUnset: 'Unset (follow local npm config)',
512
538
  diagGithub: 'GitHub',
513
539
  diagCatalog: 'Catalog site',
540
+ diagProxy: 'HTTP proxy',
514
541
  diagOk: 'OK',
515
542
  diagChecking: 'Checking…',
516
543
  diagIdle: 'Not checked',
@@ -559,6 +586,8 @@ export const en = {
559
586
  installNow: 'Install',
560
587
  installing: 'Installing…',
561
588
  installDone: 'Installed',
589
+ // Updated (notification center distinguishes install/update): update = in-place reinstall to latest
590
+ updateDone: 'Updated',
562
591
  installResultTitle: 'Plugin installed successfully',
563
592
  installResultDesc: '',
564
593
  // Update semantics: the "update" action re-runs add in place — copy differentiates install vs update
@@ -602,6 +631,8 @@ export const en = {
602
631
  errorTitle: 'Operation failed',
603
632
  errorTitleInstall: 'Install failed',
604
633
  errorTitleUninstall: 'Removal failed',
634
+ // Update failed (notification center distinguishes install/update): update target reinstall failed
635
+ errorTitleUpdate: 'Update failed',
605
636
  errorPlugin: 'Plugin',
606
637
  errorClose: 'Got it',
607
638
  errorCopy: 'Copy error',
@@ -614,9 +645,15 @@ export const en = {
614
645
  // Pre-install / post-install guard hit ([packaging]): git distribution lacks build output, entry file missing = not adapted to the official default install method
615
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.',
616
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
617
- failIgnoredBuild: 'This plugin depends on a native module (e.g. node-pty) whose build script pnpm blocks by default, so the install fails — 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 switch to a prebuilt variant (e.g. node-pty-prebuilt-multiarch).',
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.',
618
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',
619
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',
620
657
  reportIssue: 'Report an Issue to the plugin repo',
621
658
  detail: 'Open details',
622
659
  noResult: 'No plugins match your search',
@@ -18,6 +18,9 @@ export interface NotificationRecord {
18
18
  ok: boolean
19
19
  /** owner/repo(可能为空:请求层失败但拿不到仓库时) */
20
20
  repo: string
21
+ /** 实际动作(install/update/uninstall):成功/失败通知据此区分「安装成功」与「更新成功」。
22
+ * 缺省按 kind 兜底(历史记录可能没有该字段)。更新提醒(kind='update')不用此字段。 */
23
+ action?: 'install' | 'update' | 'uninstall'
21
24
  /** 失败时的完整错误日志;成功记录为空串 */
22
25
  message: string
23
26
  /** 更新提醒:目录里的新版本号(展示用;其余通知省略) */
@@ -148,18 +151,22 @@ export function removeNotification(id: number): NotificationRecord[] {
148
151
  return next
149
152
  }
150
153
 
151
- export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
154
+ export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
152
155
 
153
156
  /**
154
- * 失败归类,四态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
157
+ * 失败归类,五态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
155
158
  * 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
156
- * 一律引导提 Issue;唯一例外是本机 npm 自身版本过低导致的内部崩溃(见 npmTooOld):
159
+ * 一律引导提 Issue;唯一的两个例外是本机环境问题(npm 版本过低 / 网络不通):
157
160
  * - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
158
161
  * 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
159
162
  * 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
160
- * - pnpmIgnoredBuild:插件依赖里的原生模块构建脚本被 pnpm 默认拦截(如 node-pty,
161
- * `ERR_PNPM_IGNORED_BUILDS`)。只影响带原生模块的插件,其他插件不受影响 —— 差异在
162
- * 插件的依赖选择,属插件依赖/打包问题 → 引导去仓库提 Issue(建议改用预编译版本)
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(建议改用预编译版本或关闭安装期构建)
163
170
  * - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
164
171
  * 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
165
172
  * - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
@@ -169,12 +176,22 @@ export function classifyFailure(message: string): FailureKind {
169
176
  // 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
170
177
  // 「插件打包分发问题」,误导用户去提 Issue
171
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'
172
192
  // 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
173
193
  // 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
174
194
  if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
175
- // 原生模块构建被忽略(IGNORED_BUILDS)要在 prepare 判定之前:dsh 尾部固定提示语带
176
- // allowBuilds/pnpm-workspace.yaml,不能据此把插件自身问题误判成通用失败
177
- if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return 'pnpmIgnoredBuild'
178
195
  // 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题
179
196
  if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return 'pluginPrepare'
180
197
  // 其余失败(含 git prepare 被 pnpm 白名单拦截):当前通道装不上 = 插件分发/依赖的问题,一律提 Issue
@@ -234,3 +251,18 @@ export function coreErrorCode(message: string): string | null {
234
251
  const m = message.match(/\[?ERR_[A-Z_]+\]?/)
235
252
  return m ? m[0].replace(/^\[|\]$/g, '') : null
236
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
  // 图片 ![alt](url):仅放行 http(s),用于展示反馈群二维码等;limited by CSS
36
- // 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定 250×250 不撑破弹窗。
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="250" height="250" loading="lazy">`
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
  })