dsh-plugin 1.3.5 → 1.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +72 -18
- package/README.md +72 -18
- package/client/client.js +582 -425
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +90 -9
- package/lib/services/install/npm-resolve.js +3 -1
- package/lib/services/probe.js +43 -0
- package/lib/services/settings.js +1 -1
- package/lib/types/services/install/npm-resolve.d.ts +3 -1
- package/lib/types/services/probe.d.ts +11 -0
- package/lib/types/services/settings.d.ts +2 -2
- package/package.json +3 -3
- package/src/client/components/PluginHubSection.tsx +42 -7
- package/src/client/components/modals/NotificationsModal.tsx +27 -5
- package/src/client/components/modals/modals.tsx +18 -3
- package/src/client/components/views/DiagnosticsView.tsx +21 -7
- package/src/client/components/views/SettingsView.tsx +94 -46
- package/src/client/hooks/useSettings.ts +3 -3
- package/src/client/hooks/useTaskQueue.ts +18 -8
- package/src/client/locales.ts +48 -11
- package/src/client/logic/failures.ts +41 -9
- package/src/client/logic/renderMarkdown.ts +2 -2
- package/src/client/logic/urls.ts +26 -8
- package/src/client/styles/Dropdown.module.css +10 -0
- package/src/client/styles/Modal.module.css +52 -0
- package/src/client/styles/SettingsView.module.css +24 -0
- package/src/server/http/routes.ts +87 -9
- package/src/server/services/install/npm-resolve.ts +3 -1
- package/src/server/services/probe.ts +43 -0
- package/src/server/services/settings.ts +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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('
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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,
|
package/src/client/locales.ts
CHANGED
|
@@ -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
|
-
|
|
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: '
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
157
|
+
* 失败归类,五态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
|
|
155
158
|
* 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
|
|
156
|
-
* 一律引导提 Issue
|
|
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
|
-
* -
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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
|
// 图片 :仅放行 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
|
})
|