dsh-plugin 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +43 -18
- package/README.md +43 -18
- package/client/client.js +486 -334
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +84 -5
- package/lib/services/probe.js +43 -0
- package/lib/types/services/probe.d.ts +11 -0
- package/package.json +3 -3
- package/src/client/components/PluginHubSection.tsx +35 -0
- package/src/client/components/catalog/CatalogList.tsx +11 -3
- package/src/client/components/modals/NotificationsModal.tsx +18 -2
- package/src/client/components/modals/modals.tsx +18 -3
- package/src/client/components/views/DiagnosticsView.tsx +14 -4
- package/src/client/components/views/MarketView.tsx +5 -1
- package/src/client/components/views/SettingsView.tsx +75 -14
- package/src/client/hooks/useTaskQueue.ts +6 -2
- package/src/client/locales.ts +31 -4
- package/src/client/logic/failures.ts +38 -9
- package/src/client/logic/renderMarkdown.ts +2 -2
- package/src/client/logic/urls.ts +24 -17
- package/src/client/styles/List.module.css +24 -1
- package/src/client/styles/Modal.module.css +48 -7
- package/src/client/styles/SettingsView.module.css +24 -0
- package/src/server/http/routes.ts +81 -6
- package/src/server/services/probe.ts +43 -0
|
@@ -15,7 +15,7 @@
|
|
|
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
21
|
import dropdownStyles from '../../styles/Dropdown.module.css'
|
|
@@ -30,7 +30,8 @@ import {
|
|
|
30
30
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
31
31
|
import type { HubSettings } from '../../hooks/useSettings.ts'
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/** 设置分组导航 key:顺序即展示顺序。导出供外部(如错误弹窗「去系统诊断」)跳转指定分组。 */
|
|
34
|
+
export type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
|
|
34
35
|
|
|
35
36
|
/** npm 镜像源预设:空串 = 未配置(跟随用户本机 npm 配置,本插件不注入任何 registry)。
|
|
36
37
|
* 只提供常见国内顶级镜像下拉,不做自定义输入 —— 想用其它镜像直接在 ~/.npmrc 配置即可。 */
|
|
@@ -67,20 +68,70 @@ function SettingRow({ title, desc, children, stack = false }: {
|
|
|
67
68
|
)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export function SettingsView({ t, settings, update, reset, env, onCopy }: {
|
|
71
|
+
export function SettingsView({ t, settings, update, reset, env, onCopy, openSection, onConsumedOpenSection }: {
|
|
71
72
|
t: Translate
|
|
72
73
|
settings: HubSettings
|
|
73
74
|
update: (patch: Partial<HubSettings>) => void
|
|
74
75
|
reset: () => void
|
|
75
76
|
env: EnvInfo | null
|
|
76
77
|
onCopy: (text: string) => void
|
|
78
|
+
/** 外部跳转信号(如错误弹窗「去系统诊断」):非空时切到对应分组,消费后通过 onConsumedOpenSection 清空 */
|
|
79
|
+
openSection?: SettingsSection | null
|
|
80
|
+
onConsumedOpenSection?: () => void
|
|
77
81
|
}) {
|
|
78
82
|
/** 当前激活的分组:左侧导航点击切换,右侧只渲染这一组 */
|
|
79
83
|
const [section, setSection] = useState<SettingsSection>('updates')
|
|
80
84
|
/** 恢复默认确认弹窗:点按钮先弹窗询问,确认后才真正 reset(不做两段式按钮) */
|
|
81
85
|
const [resetConfirm, setResetConfirm] = useState(false)
|
|
82
86
|
|
|
83
|
-
|
|
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)
|
|
134
|
+
}
|
|
84
135
|
const onMirrorChange = (value: string) => update({ npmRegistry: value })
|
|
85
136
|
|
|
86
137
|
/** 每个分组的页面副标题(导航标题之上再补一句说明,保持桌面设置质感) */
|
|
@@ -120,19 +171,29 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
|
|
|
120
171
|
className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
|
|
121
172
|
}),
|
|
122
173
|
}),
|
|
123
|
-
//
|
|
174
|
+
// 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址;停笔后实时探测连通性,
|
|
175
|
+
// 不通仅提示(可保存)—— 用户可能先填地址后开代理
|
|
124
176
|
h(SettingRow, {
|
|
125
177
|
title: t('settingsProxy'),
|
|
126
178
|
desc: t('settingsProxyDesc'),
|
|
127
179
|
stack: true,
|
|
128
|
-
children: h('
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
),
|
|
136
197
|
}),
|
|
137
198
|
)
|
|
138
199
|
|
|
@@ -172,7 +233,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
|
|
|
172
233
|
|
|
173
234
|
const diagnosticsCard = h('div', { className: styles.card },
|
|
174
235
|
// 系统版本(最顶部)+ 通道状态列表:进入页面自动探测一轮,整行可点击重测单项,实时反馈
|
|
175
|
-
h(DiagnosticsView, { t, env, onCopy }),
|
|
236
|
+
h(DiagnosticsView, { t, env, proxy: settings.proxy, onCopy }),
|
|
176
237
|
)
|
|
177
238
|
|
|
178
239
|
const logsCard = h('div', { className: styles.card },
|
|
@@ -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
|
|
|
@@ -64,6 +64,8 @@ export interface PendingRestart {
|
|
|
64
64
|
|
|
65
65
|
export interface TaskQueueOptions {
|
|
66
66
|
t: Translate
|
|
67
|
+
/** 界面语言(zh/en):随安装请求带给服务端,网络预检等错误消息按用户语言提示 */
|
|
68
|
+
langKey?: LocaleId
|
|
67
69
|
refreshInstalled: () => void
|
|
68
70
|
/** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
|
|
69
71
|
* needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。
|
|
@@ -89,7 +91,7 @@ export interface TaskQueueOptions {
|
|
|
89
91
|
|
|
90
92
|
export function useTaskQueue(opts: TaskQueueOptions) {
|
|
91
93
|
const {
|
|
92
|
-
t, refreshInstalled, onInstallDone, onUninstallDone, onError,
|
|
94
|
+
t, langKey, refreshInstalled, onInstallDone, onUninstallDone, onError,
|
|
93
95
|
installPlugin, installCustomTarget, installGlobalTarget, uninstallPlugin, uninstallName, installedName, resolvePending,
|
|
94
96
|
} = opts
|
|
95
97
|
const [queue, setQueue] = useState<QueueTask[]>([])
|
|
@@ -442,6 +444,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
442
444
|
display: input.repo,
|
|
443
445
|
source: input.source,
|
|
444
446
|
mode: input.update ? 'update' : undefined,
|
|
447
|
+
// 界面语言带给服务端:网络预检等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
|
|
448
|
+
...(langKey ? { lang: langKey } : {}),
|
|
445
449
|
...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
|
|
446
450
|
}),
|
|
447
451
|
signal: controller.signal,
|
package/src/client/locales.ts
CHANGED
|
@@ -25,7 +25,9 @@ export const zh = {
|
|
|
25
25
|
fork: 'Fork',
|
|
26
26
|
loading: '正在加载插件数据…',
|
|
27
27
|
failed: '插件数据加载失败',
|
|
28
|
-
|
|
28
|
+
// 目录拉不出来时的精准提示:按「是否配置了代理」区分,引导排查网络 / 代理
|
|
29
|
+
failedDescNoProxy: '你的网络似乎不通,无法获取插件目录数据。请检查网络连接后重试;或点击右上角按钮在浏览器中打开插件中心。',
|
|
30
|
+
failedDescProxy: '你的网络似乎不通,或设置里的 HTTP 代理地址不可达,无法获取插件目录数据。请检查代理设置与网络连接后重试;或点击右上角按钮在浏览器中打开插件中心。',
|
|
29
31
|
retry: '重试',
|
|
30
32
|
install: '安装',
|
|
31
33
|
// AI 识别可能需要命令行辅助的插件(webInstallable=false):仍可尝试一键安装,失败时复制命令去 dsh 终端
|
|
@@ -110,6 +112,10 @@ export const zh = {
|
|
|
110
112
|
settingsCheckOnStartDesc: '打开插件中心时检查已安装插件是否有新版,发现更新会提示',
|
|
111
113
|
settingsProxy: 'HTTP 代理',
|
|
112
114
|
settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
|
|
115
|
+
// 代理连通性实时探测:输入停笔后提示检测结果(不通仍可保存,先填地址后开代理也合法)
|
|
116
|
+
proxyCheckChecking: '正在测试该代理地址…',
|
|
117
|
+
proxyCheckOk: '测试可达,安装与目录请求将统一走该代理',
|
|
118
|
+
proxyCheckFail: '此地址测试不通,仍可保存;请确认代理已开启、地址与端口无误',
|
|
113
119
|
settingsMirror: 'npm 镜像源',
|
|
114
120
|
settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
|
|
115
121
|
mirrorNone: '未配置(跟随本机 npm 配置)',
|
|
@@ -186,6 +192,7 @@ export const zh = {
|
|
|
186
192
|
diagNpmUnset: '未配置(跟随本机 npm 配置)',
|
|
187
193
|
diagGithub: 'GitHub',
|
|
188
194
|
diagCatalog: '目录站点',
|
|
195
|
+
diagProxy: 'HTTP 代理',
|
|
189
196
|
diagOk: 'OK',
|
|
190
197
|
diagChecking: '检测中…',
|
|
191
198
|
diagIdle: '未检测',
|
|
@@ -294,10 +301,16 @@ export const zh = {
|
|
|
294
301
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺构建产物、入口文件不存在 = 插件未适配官方默认安装方式
|
|
295
302
|
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少构建产物(package.json 声明的入口文件在仓库中不存在),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物或发布 npm 版。',
|
|
296
303
|
// 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):只影响带原生模块的插件,属插件依赖打包问题,应提 Issue
|
|
297
|
-
failIgnoredBuild: '
|
|
304
|
+
failIgnoredBuild: '该插件或其依赖需要执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截导致安装失败(如原生模块 node-pty、或 git 安装时插件的 prepare 脚本)—— 其他插件不受影响,属该插件的依赖/打包问题。建议向作者仓库一键反馈,请作者改用预编译版本(如 node-pty-prebuilt-multiarch)或移除安装期构建。',
|
|
298
305
|
// npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
|
|
299
306
|
failNpmTooLow: '怀疑本机 npm 版本过低:npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
300
307
|
failNpmTooLowV: '本机 npm 版本过低(当前 v{v}):npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
308
|
+
// 安装前连通性预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):本机网络或代理问题,不是插件问题,不引导提 Issue
|
|
309
|
+
failNetworkHint: '安装前检测到网络不通:无法连接到安装源(可能是断网、DNS 解析失败、被防火墙拦截,或代理配置有问题)。这不是插件本身的问题 —— 请检查网络连接,或到「设置」里确认 HTTP 代理配置,再到「系统诊断」运行一次连通性检测,然后重试。',
|
|
310
|
+
// 网络失败弹窗里的「无法访问 …」醒目行:直接告诉用户具体哪个地址连不上({url} 为提取的不可达地址)
|
|
311
|
+
failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
|
|
312
|
+
// 「去系统诊断」直达按钮:网络不通提示下方的品牌实底按钮,跳到设置 → 系统诊断跑连通性检测
|
|
313
|
+
failNetworkRunDiag: '去系统诊断检测网络',
|
|
301
314
|
reportIssue: '去插件仓库提交 Issue',
|
|
302
315
|
detail: '打开详情',
|
|
303
316
|
noResult: '没有找到匹配的插件',
|
|
@@ -357,7 +370,9 @@ export const en = {
|
|
|
357
370
|
fork: 'Fork',
|
|
358
371
|
loading: 'Loading plugin data…',
|
|
359
372
|
failed: 'Failed to load plugin data',
|
|
360
|
-
|
|
373
|
+
// Precise hints when the catalog cannot load: distinguished by whether a proxy is configured
|
|
374
|
+
failedDescNoProxy: 'Your network seems unreachable, so the plugin catalog could not be loaded. Please check your connection and retry; or open the hub in your browser with the button above.',
|
|
375
|
+
failedDescProxy: 'Your network seems unreachable, or the HTTP proxy in Settings is unreachable, so the plugin catalog could not be loaded. Please check the proxy address and your connection, then retry; or open the hub in your browser with the button above.',
|
|
361
376
|
retry: 'Retry',
|
|
362
377
|
install: 'Install',
|
|
363
378
|
// AI-flagged plugins that may need command-line help (webInstallable=false): one-click install still allowed,
|
|
@@ -445,6 +460,11 @@ export const en = {
|
|
|
445
460
|
settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
|
|
446
461
|
settingsProxy: 'HTTP proxy',
|
|
447
462
|
settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
|
|
463
|
+
// Live proxy reachability probe: reported after the user stops typing (an unreachable
|
|
464
|
+
// address can still be saved — users may fill it in before actually starting the proxy)
|
|
465
|
+
proxyCheckChecking: 'Testing this proxy address…',
|
|
466
|
+
proxyCheckOk: 'Reachable — installs and catalog requests will route through it',
|
|
467
|
+
proxyCheckFail: 'Unreachable, but you can still save it; make sure the proxy is running and the address and port are correct',
|
|
448
468
|
settingsMirror: 'npm mirror',
|
|
449
469
|
settingsMirrorDesc: 'Unset by default — follows local npm config; pick a domestic mirror if slow',
|
|
450
470
|
mirrorNone: 'Unset (follow local npm config)',
|
|
@@ -521,6 +541,7 @@ export const en = {
|
|
|
521
541
|
diagNpmUnset: 'Unset (follow local npm config)',
|
|
522
542
|
diagGithub: 'GitHub',
|
|
523
543
|
diagCatalog: 'Catalog site',
|
|
544
|
+
diagProxy: 'HTTP proxy',
|
|
524
545
|
diagOk: 'OK',
|
|
525
546
|
diagChecking: 'Checking…',
|
|
526
547
|
diagIdle: 'Not checked',
|
|
@@ -628,9 +649,15 @@ export const en = {
|
|
|
628
649
|
// Pre-install / post-install guard hit ([packaging]): git distribution lacks build output, entry file missing = not adapted to the official default install method
|
|
629
650
|
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks the build output (the entry file declared in package.json is not in the repository), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output or an npm release.',
|
|
630
651
|
// native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): only affects plugins with native deps — a dependency/packaging issue of the plugin itself, file an issue
|
|
631
|
-
failIgnoredBuild: 'This plugin
|
|
652
|
+
failIgnoredBuild: 'This plugin (or its dependencies) needs to run build scripts, which pnpm blocks by default via its allowBuilds allowlist, so the install fails (e.g. native modules like node-pty, or the plugin\'s own prepare script on git installs) — other plugins are unaffected. This is a dependency/packaging issue of the plugin itself. Please report it to the author repository and ask the author to ship a prebuilt variant (e.g. node-pty-prebuilt-multiarch) or drop install-time builds.',
|
|
632
653
|
failNpmTooLow: 'Your local npm may be too old: npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
633
654
|
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',
|
|
655
|
+
// 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
|
|
656
|
+
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.',
|
|
657
|
+
// Prominent "cannot reach …" line in the network-failure dialog: shows the exact unreachable address ({url})
|
|
658
|
+
failNetworkTarget: 'Cannot reach {url} — your network connection is down',
|
|
659
|
+
// Diagnostics shortcut button under the network-down hint: jumps to Settings → System Diagnostics
|
|
660
|
+
failNetworkRunDiag: 'Run a network diagnostic',
|
|
634
661
|
reportIssue: 'Report an Issue to the plugin repo',
|
|
635
662
|
detail: 'Open details',
|
|
636
663
|
noResult: 'No plugins match your search',
|
|
@@ -151,18 +151,22 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'repo'
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* 失败归类,五态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
|
|
158
158
|
* 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
|
|
159
|
-
* 一律引导提 Issue
|
|
159
|
+
* 一律引导提 Issue;唯一的两个例外是本机环境问题(npm 版本过低 / 网络不通):
|
|
160
160
|
* - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
|
|
161
161
|
* 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
|
|
162
162
|
* 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
|
|
163
|
-
* -
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* - network:安装前连通性预检拦截(服务端 `[network]` 标记)或底层连接失败
|
|
164
|
+
* (ERR_PNPM_GIT_FETCH_FAILED / ETIMEDOUT / DNS 解析 / TLS 握手 / 代理拒绝)——
|
|
165
|
+
* 是本机网络不通/被墙/代理有问题,不是插件问题 → 提示检查网络,不引导提 Issue
|
|
166
|
+
* - pnpmIgnoredBuild:插件自身或依赖的构建脚本被 pnpm 安全白名单(allowBuilds)默认拦截
|
|
167
|
+
* (`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED` / `ERR_PNPM_IGNORED_BUILDS`)。只影响带安装期
|
|
168
|
+
* 构建的插件,其他插件不受影响 —— 差异在插件的依赖/打包方式,属插件依赖/打包问题
|
|
169
|
+
* → 引导去仓库提 Issue(建议改用预编译版本或关闭安装期构建)
|
|
166
170
|
* - pluginPrepare:插件的 prepare/构建脚本实际执行失败(git tarball 常因缺失子模块或
|
|
167
171
|
* 构建产物导致)—— 属插件打包/分发问题,应引导去仓库提 Issue
|
|
168
172
|
* - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
|
|
@@ -172,12 +176,22 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
172
176
|
// 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
|
|
173
177
|
// 「插件打包分发问题」,误导用户去提 Issue
|
|
174
178
|
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
179
|
+
// 构建脚本被 pnpm 白名单(allowBuilds)拦截:插件的 prepare 脚本或依赖里的原生模块构建
|
|
180
|
+
// 被默认拒绝(ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED / ERR_PNPM_IGNORED_BUILDS)。
|
|
181
|
+
// 这类错误出现即说明 pnpm 已成功 fetch 到 tarball(网络是通的),主因是插件构建脚本
|
|
182
|
+
// 被拦 —— 必须在 network 判定之前:日志尾部常混着重试残留的连接失败特征
|
|
183
|
+
// (ETIMEDOUT / Failed to connect 等),若先判网络会把「插件分发问题」误报成
|
|
184
|
+
// 「你的网络不通」(graph-memory issues #82-#84:PREPARE_NOT_ALLOWED + 尾随超时)。
|
|
185
|
+
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:|ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED/i.test(message)) return 'pnpmIgnoredBuild'
|
|
186
|
+
// 网络问题(服务端 [network] 标记,或安装日志里的连接失败特征:git fetch 失败、
|
|
187
|
+
// 连接超时/拒绝/重置、DNS 解析失败、TLS/SSL 握手失败)—— 是本机网络/代理问题,
|
|
188
|
+
// 不是插件问题。必须在 prepare/Command failed 判定之前:git fetch 失败常被
|
|
189
|
+
// 外层包成 "Command failed: git fetch ...",先按网络特征归类才不会误判成插件问题。
|
|
190
|
+
// 404 类「目标不存在」不含这些特征,仍归 repo(那是仓库/包的问题)。
|
|
191
|
+
if (/\[network\]|ERR_PNPM_GIT_FETCH_FAILED|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH|getaddrinfo|Could not connect|Could not resolve host|Network unreachable|Failed to connect|socket hang up|CERT_HAS_EXPIRED|SSL certificate problem|\bTLS\b|\bSSL\b/i.test(message)) return 'network'
|
|
175
192
|
// 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
|
|
176
193
|
// 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
|
|
177
194
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
178
|
-
// 原生模块构建被忽略(IGNORED_BUILDS)要在 prepare 判定之前:dsh 尾部固定提示语带
|
|
179
|
-
// allowBuilds/pnpm-workspace.yaml,不能据此把插件自身问题误判成通用失败
|
|
180
|
-
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return 'pnpmIgnoredBuild'
|
|
181
195
|
// 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题
|
|
182
196
|
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return 'pluginPrepare'
|
|
183
197
|
// 其余失败(含 git prepare 被 pnpm 白名单拦截):当前通道装不上 = 插件分发/依赖的问题,一律提 Issue
|
|
@@ -237,3 +251,18 @@ export function coreErrorCode(message: string): string | null {
|
|
|
237
251
|
const m = message.match(/\[?ERR_[A-Z_]+\]?/)
|
|
238
252
|
return m ? m[0].replace(/^\[|\]$/g, '') : null
|
|
239
253
|
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 从网络类失败消息里提取「具体连不上的地址」,供弹窗精准提示(你的网络无法访问什么)。
|
|
257
|
+
* 优先取服务端 [network] 预检消息括号里的探测地址(中文「(…)」/ 英文「(…)」),
|
|
258
|
+
* 否则取消息里出现的第一个 URL(git fetch 的仓库地址、registry 域名等)。
|
|
259
|
+
* 提取不到返回 null —— 调用方据此跳过地址行。
|
|
260
|
+
*/
|
|
261
|
+
export function unreachableTargetOf(message: string): string | null {
|
|
262
|
+
// [network] 预检消息:`无法连接到 GitHub(https://github.com/)` / `cannot reach the npm registry (https://registry.npmjs.org/)`
|
|
263
|
+
const paren = message.match(/[((](https?:\/\/[^\s'"`<>()+]+)[))]/)
|
|
264
|
+
if (paren) return paren[1]
|
|
265
|
+
// 其余网络失败(git fetch / npm 连接失败):取第一个 URL,去掉行尾标点
|
|
266
|
+
const url = message.match(/https?:\/\/[^\s'"`<>()()]+/)
|
|
267
|
+
return url ? url[0].replace(/[.,;:))\]]+$/, '') : null
|
|
268
|
+
}
|
|
@@ -33,12 +33,12 @@ function inline(s: string): string {
|
|
|
33
33
|
return `\u0000${codeSpans.length - 1}\u0000`
|
|
34
34
|
})
|
|
35
35
|
// 图片 :仅放行 http(s),用于展示反馈群二维码等;limited by CSS
|
|
36
|
-
// 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定
|
|
36
|
+
// 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定 100×100 不撑破弹窗。
|
|
37
37
|
out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) => {
|
|
38
38
|
const src = String(url)
|
|
39
39
|
if (!/^https?:\/\//i.test(src) || /[\u0000]/.test(src)) return m
|
|
40
40
|
if (/\/dsh-plugin-user-group-qr-[^/]*$/i.test(src)) {
|
|
41
|
-
return `<img src="${src}" alt="${String(alt)}" width="
|
|
41
|
+
return `<img src="${src}" alt="${String(alt)}" width="100" height="100" loading="lazy">`
|
|
42
42
|
}
|
|
43
43
|
return `<img src="${src}" alt="${String(alt)}" loading="lazy">`
|
|
44
44
|
})
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { EnvInfo, HubPlugin, LocaleId } from '../types.ts'
|
|
11
11
|
import { GITHUB_URL, PLUGIN_VERSION, SITE_URL } from './constants.ts'
|
|
12
12
|
import { MAX_CORE_CHARS, classifyFailure, coreErrorCode, summarizeError } from './failures.ts'
|
|
13
|
+
import type { FailureKind } from './failures.ts'
|
|
13
14
|
|
|
14
15
|
/** dsh-plugin.org 中文页挂在 /zh/ 前缀下,英文在根路径。 */
|
|
15
16
|
export function langPathOf(lang: LocaleId): string {
|
|
@@ -33,18 +34,22 @@ export function pluginSiteUrl(repo: string): string {
|
|
|
33
34
|
return `${SITE_URL}plugins/${repo}`
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
/** 按失败类型给出简洁的错误原因标题(对外用英文):
|
|
38
|
+
* 标题直接点明问题出在哪一侧(构建白名单/分发物/本机 npm/网络/插件侧),
|
|
39
|
+
* 作者与用户扫一眼列表就能分流 —— 不再用含糊的 Install/Remove 动作词。 */
|
|
40
|
+
function reasonTitleOf(kind: FailureKind): string {
|
|
41
|
+
switch (kind) {
|
|
42
|
+
case 'npmTooOld': return 'npm too old to install'
|
|
43
|
+
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
44
|
+
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
45
|
+
case 'network': return 'network failure on the user side'
|
|
46
|
+
default: return 'plugin install failed'
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
|
-
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org
|
|
47
|
-
*
|
|
51
|
+
* 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识 + 错误原因,
|
|
52
|
+
* 正文只带核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
|
|
48
53
|
* 错误核心摘要(完整日志太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,
|
|
49
54
|
* 故只收集重点)。错误弹窗、失败记录共用此逻辑。
|
|
50
55
|
* env 取不到时为 null,链接照常生成、只是少环境段。
|
|
@@ -52,20 +57,21 @@ function actionLabelOf(command: string | undefined): string {
|
|
|
52
57
|
* 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
|
|
53
58
|
*/
|
|
54
59
|
export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
|
|
55
|
-
// 标题按实际执行的动作区分(安装/更新/卸载),避免作者误读 —— 旧版固定写死
|
|
56
|
-
// 「Install/Remove」会让更新失败被当成删除失败。
|
|
57
|
-
const title = `[dsh-plugin.org | dsh-plugin-hub] ${actionLabelOf(command)} failed: ${repo}`
|
|
58
60
|
// 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
|
|
59
61
|
// [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
|
|
60
62
|
// (package.json 声明的入口文件在发布物里缺失),作者照此补齐即可。
|
|
61
63
|
const kind = classifyFailure(message)
|
|
64
|
+
// 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
|
|
65
|
+
const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
|
|
62
66
|
const reason = /\[packaging\]/i.test(message)
|
|
63
67
|
? 'plugin distribution is incomplete — the entry file declared in package.json is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
|
|
64
|
-
: kind === '
|
|
65
|
-
? '
|
|
66
|
-
: kind === '
|
|
67
|
-
? 'plugin
|
|
68
|
-
:
|
|
68
|
+
: kind === 'network'
|
|
69
|
+
? 'network connectivity issue on the user side (DNS, proxy, firewall, or a blocked connection)'
|
|
70
|
+
: kind === 'pluginPrepare'
|
|
71
|
+
? 'plugin prepare/build script failed during install (packaging/distribution issue)'
|
|
72
|
+
: kind === 'pnpmIgnoredBuild'
|
|
73
|
+
? 'plugin depends on a native module whose build script pnpm blocks by default (use a prebuilt variant)'
|
|
74
|
+
: 'plugin-side install failure'
|
|
69
75
|
const code = coreErrorCode(message)
|
|
70
76
|
// 按给定核心摘要预算构建完整预填 URL(含 URL 编码);预算可调,供超长时逐档缩小
|
|
71
77
|
const build = (coreChars: number): string => {
|
|
@@ -134,3 +140,4 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
134
140
|
}
|
|
135
141
|
return url
|
|
136
142
|
}
|
|
143
|
+
|
|
@@ -301,7 +301,6 @@ body[data-ds-dark-theme] .uninstallBtn:hover {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
.retryBtn {
|
|
304
|
-
margin-top: 4px;
|
|
305
304
|
font-size: 12px;
|
|
306
305
|
line-height: 18px;
|
|
307
306
|
color: var(--hub-text-secondary);
|
|
@@ -318,6 +317,30 @@ body[data-ds-dark-theme] .uninstallBtn:hover {
|
|
|
318
317
|
background: var(--hub-bg-hover);
|
|
319
318
|
}
|
|
320
319
|
|
|
320
|
+
/* 失败态按钮行:重试 + 「去系统诊断检测网络」并排 */
|
|
321
|
+
.stateActions {
|
|
322
|
+
margin-top: 4px;
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
gap: 8px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.diagBtn {
|
|
329
|
+
font-size: 12px;
|
|
330
|
+
line-height: 18px;
|
|
331
|
+
color: #fff;
|
|
332
|
+
border: none;
|
|
333
|
+
border-radius: 6px;
|
|
334
|
+
padding: 4px 12px;
|
|
335
|
+
background: var(--hub-brand);
|
|
336
|
+
cursor: pointer;
|
|
337
|
+
user-select: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.diagBtn:hover {
|
|
341
|
+
background: var(--hub-brand-hover);
|
|
342
|
+
}
|
|
343
|
+
|
|
321
344
|
/* —— footer —— */
|
|
322
345
|
|
|
323
346
|
.footer {
|
|
@@ -1239,6 +1239,47 @@
|
|
|
1239
1239
|
border-radius: 6px;
|
|
1240
1240
|
}
|
|
1241
1241
|
|
|
1242
|
+
/* 网络不通提示下的「去系统诊断」操作按钮:品牌实底小按钮(区别于红色提 Issue 大按钮) */
|
|
1243
|
+
.failDiagBtn {
|
|
1244
|
+
display: block;
|
|
1245
|
+
width: 100%;
|
|
1246
|
+
box-sizing: border-box;
|
|
1247
|
+
margin-top: 8px;
|
|
1248
|
+
padding: 5px 16px;
|
|
1249
|
+
font-size: 13px;
|
|
1250
|
+
line-height: 18px;
|
|
1251
|
+
font-weight: 600;
|
|
1252
|
+
text-align: center;
|
|
1253
|
+
color: var(--hub-text-on-fill);
|
|
1254
|
+
background: var(--hub-btn-fill);
|
|
1255
|
+
border: none;
|
|
1256
|
+
border-radius: 8px;
|
|
1257
|
+
cursor: pointer;
|
|
1258
|
+
user-select: none;
|
|
1259
|
+
transition: background 120ms ease, box-shadow 120ms ease;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.failDiagBtn:hover {
|
|
1263
|
+
background: var(--hub-btn-hover);
|
|
1264
|
+
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/* 网络类失败弹窗的「无法访问 …」醒目行:红色加粗,直接告诉用户具体哪个地址连不上
|
|
1268
|
+
(URL 较长允许换行断词),与下方琥珀色说明区分主次 */
|
|
1269
|
+
.failNetworkTarget {
|
|
1270
|
+
margin-top: 6px;
|
|
1271
|
+
padding: 8px 12px;
|
|
1272
|
+
font-size: 13px;
|
|
1273
|
+
line-height: 20px;
|
|
1274
|
+
font-weight: 700;
|
|
1275
|
+
word-break: break-all;
|
|
1276
|
+
color: var(--hub-danger);
|
|
1277
|
+
background: var(--hub-danger-tint);
|
|
1278
|
+
border: 1px solid var(--hub-danger-border);
|
|
1279
|
+
border-radius: 6px;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
|
|
1242
1283
|
/* —— 「关注我们」弹窗 —— */
|
|
1243
1284
|
|
|
1244
1285
|
/* 关注弹窗:比普通弹窗略宽,给 Markdown 介绍 + 反馈群二维码留出阅读空间;
|
|
@@ -1356,13 +1397,13 @@
|
|
|
1356
1397
|
}
|
|
1357
1398
|
|
|
1358
1399
|
/* 用户反馈群二维码(renderMarkdown 对 dsh-plugin-user-group-qr- 前缀图片加了
|
|
1359
|
-
width/height="
|
|
1360
|
-
CSS max-height 对 height
|
|
1361
|
-
|
|
1362
|
-
.aboutContent img[width="
|
|
1363
|
-
width:
|
|
1364
|
-
height:
|
|
1365
|
-
max-height:
|
|
1400
|
+
width/height="100"):固定 100×100 正方形。必须显式覆盖通用 max-height 上限 ——
|
|
1401
|
+
CSS max-height 对 height 有最终裁决权,不覆盖的话高度被 max-height 规则压掉,
|
|
1402
|
+
图片就会变成非正方形(看起来像「尺寸没生效」) */
|
|
1403
|
+
.aboutContent img[width="100"] {
|
|
1404
|
+
width: 100px;
|
|
1405
|
+
height: 100px;
|
|
1406
|
+
max-height: 100px;
|
|
1366
1407
|
object-fit: contain;
|
|
1367
1408
|
}
|
|
1368
1409
|
|
|
@@ -178,6 +178,30 @@
|
|
|
178
178
|
width: 100%;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/* 代理输入框列布局:输入框 + 连通性探测提示上下排列,整行展开 */
|
|
182
|
+
.proxyControl {
|
|
183
|
+
width: 100%;
|
|
184
|
+
display: flex;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
align-items: stretch;
|
|
187
|
+
gap: 5px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* 代理连通性提示:检测中灰字 / 可达绿字 / 不通琥珀字(仍可保存,不阻断输入) */
|
|
191
|
+
.proxyHint {
|
|
192
|
+
font-size: 11px;
|
|
193
|
+
line-height: 1.4;
|
|
194
|
+
color: var(--hub-text-disabled);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.proxyHintOk {
|
|
198
|
+
color: var(--hub-success);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.proxyHintFail {
|
|
202
|
+
color: var(--hub-warning);
|
|
203
|
+
}
|
|
204
|
+
|
|
181
205
|
/* 下拉控件:行内固定宽度,与输入框等高 */
|
|
182
206
|
.controlDropdown {
|
|
183
207
|
min-width: 180px;
|