dsh-plugin 2.3.2 → 2.3.3
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 +3 -9
- package/README.md +3 -9
- package/client/client.js +367 -362
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +4 -4
- package/lib/http/diagnostics.js +4 -4
- package/lib/http/install.js +13 -13
- package/lib/http/npm-release-age.js +3 -3
- package/lib/http/shared.js +1 -1
- package/lib/services/host-version.js +2 -2
- package/lib/services/install/dsh-compat.js +1 -1
- package/lib/services/install/npm-resolve.js +1 -1
- package/lib/services/install/operation-log.js +3 -3
- package/lib/services/install/preflight.js +1 -1
- package/lib/services/install/registry-fallback.js +3 -3
- package/lib/services/install/release-target.js +2 -2
- package/lib/services/install/task-queue.js +35 -20
- package/lib/services/loader.js +8 -8
- package/lib/services/npm-release-age.js +8 -8
- package/lib/services/probe.js +12 -12
- package/lib/services/profile/installed-versions.js +2 -2
- package/lib/services/profile/profile.js +2 -2
- package/lib/types/services/host-version.d.ts +1 -1
- package/lib/types/services/install/install-types.d.ts +5 -5
- package/lib/types/services/install/operation-log.d.ts +1 -1
- package/lib/types/services/install/preflight.d.ts +1 -1
- package/lib/types/services/install/registry-fallback.d.ts +1 -1
- package/lib/types/services/install/release-target.d.ts +1 -1
- package/lib/types/services/install/task-queue.d.ts +10 -7
- package/lib/types/services/loader.d.ts +5 -5
- package/lib/types/services/npm-release-age.d.ts +5 -5
- package/lib/types/services/probe.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +29 -29
- package/src/client/components/catalog/CatalogList.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +3 -3
- package/src/client/components/modals/AboutModal.tsx +2 -2
- package/src/client/components/modals/ConfirmDialog.tsx +2 -2
- package/src/client/components/modals/HubUpdateModal.tsx +3 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
- package/src/client/components/modals/LogsPathModal.tsx +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +2 -2
- package/src/client/components/modals/modals.tsx +10 -10
- package/src/client/components/views/CustomInstallView.tsx +2 -2
- package/src/client/components/views/InstalledView.tsx +2 -2
- package/src/client/components/views/SettingsView.tsx +3 -3
- package/src/client/data/catalog.ts +1 -1
- package/src/client/data/host.ts +2 -2
- package/src/client/data/hub.ts +3 -3
- package/src/client/hooks/useCatalog.ts +14 -14
- package/src/client/hooks/useSettings.ts +2 -2
- package/src/client/hooks/useTaskQueue.ts +30 -23
- package/src/client/locales.ts +15 -15
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/ensure-plugin-css.ts +2 -2
- package/src/client/logic/failures.ts +9 -9
- package/src/client/logic/install-command.ts +1 -2
- package/src/client/logic/installed.ts +1 -1
- package/src/client/logic/release-age.ts +10 -10
- package/src/client/logic/urls.ts +3 -3
- package/src/client/styles/Header.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/styles/SectionTabs.module.css +1 -1
- package/src/client/styles/tokens.module.css +1 -1
- package/src/client/types.ts +4 -4
- package/src/server/http/catalog.ts +4 -4
- package/src/server/http/diagnostics.ts +4 -4
- package/src/server/http/install.ts +13 -13
- package/src/server/http/npm-release-age.ts +3 -3
- package/src/server/http/shared.ts +1 -1
- package/src/server/services/host-version.ts +2 -2
- package/src/server/services/install/dsh-compat.ts +1 -1
- package/src/server/services/install/install-types.ts +5 -5
- package/src/server/services/install/npm-resolve.ts +1 -1
- package/src/server/services/install/operation-log.ts +3 -3
- package/src/server/services/install/preflight.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +3 -3
- package/src/server/services/install/release-target.ts +2 -2
- package/src/server/services/install/task-queue.ts +40 -26
- package/src/server/services/loader.ts +10 -10
- package/src/server/services/npm-release-age.ts +10 -10
- package/src/server/services/probe.ts +12 -12
- package/src/server/services/profile/installed-versions.ts +2 -2
- package/src/server/services/profile/profile.ts +2 -2
|
@@ -35,7 +35,7 @@ export function AboutModal({ info, lang, t, onClose }: {
|
|
|
35
35
|
t: Translate
|
|
36
36
|
onClose: () => void
|
|
37
37
|
}) {
|
|
38
|
-
// 内容:字符串直接使用;{zh,en}
|
|
38
|
+
// 内容:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则展示备用文案。
|
|
39
39
|
// Worker 未推送(info 为 null / content 为空)时弹窗仍能打开,显示「暂未发布」而非报错。
|
|
40
40
|
const contentRaw = info
|
|
41
41
|
? typeof info.content === 'string'
|
|
@@ -50,7 +50,7 @@ export function AboutModal({ info, lang, t, onClose }: {
|
|
|
50
50
|
// 同一次打开内保持稳定,避免父组件重渲染导致图片被反复重载。
|
|
51
51
|
const qrTs = useMemo(() => Date.now(), [])
|
|
52
52
|
const contentHtml = contentRaw.trim() ? renderMarkdown(bustQrCache(contentRaw, qrTs)) : null
|
|
53
|
-
// 更新时间:ISO
|
|
53
|
+
// 更新时间:ISO 字符串转本地可读格式;非法值直接隐藏
|
|
54
54
|
let updated: string | null = null
|
|
55
55
|
if (info?.updatedAt) {
|
|
56
56
|
const d = new Date(info.updatedAt)
|
|
@@ -23,9 +23,9 @@ export function ConfirmDialog({ type, title, desc, confirmLabel, cancelLabel, bu
|
|
|
23
23
|
desc?: string
|
|
24
24
|
confirmLabel: string
|
|
25
25
|
cancelLabel?: string
|
|
26
|
-
/**
|
|
26
|
+
/** 确认动作进行中:禁用按钮防止二次点击 */
|
|
27
27
|
busy?: boolean
|
|
28
|
-
/**
|
|
28
|
+
/** 进行中时确认按钮文案(如「清空中…」);缺省沿用 confirmLabel */
|
|
29
29
|
busyLabel?: string
|
|
30
30
|
t: Translate
|
|
31
31
|
onConfirm: () => void
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* - 有更新(state='available'):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
|
|
8
8
|
* Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
|
|
9
9
|
* 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
|
|
10
|
-
* - 暂缓提示(state='pending'
|
|
11
|
-
*
|
|
10
|
+
* - 暂缓提示(state='pending'):远端确有新版本,但发布还不满冷却期,pnpm 此时会
|
|
11
|
+
* 不提示地回退到旧版本 —— 仍按「有新版本」展示,只给「稍后再说」,并说明稍后会自动提示。
|
|
12
12
|
* - 无更新 / 信息缺失(state='latest' / 'none'):标题「当前版本」,版本号与本机版本取自
|
|
13
13
|
* currentVersion(构建注入的 PLUGIN_VERSION),**不得**用远端最新版号冒充本机版本。
|
|
14
14
|
* 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
|
|
@@ -59,7 +59,7 @@ export function HubUpdateModal({ info, state, currentVersion, lang, t, onProceed
|
|
|
59
59
|
const notesHtml = notesBody.trim() ? renderMarkdown(notesBody) : null
|
|
60
60
|
// 说明只在需要动作或需要解释时出现:已是最新时胶囊已说明状态,不再重复一句话
|
|
61
61
|
const desc = hasUpdate ? t('hubUpdateDesc') : pending ? t('hubPendingDesc') : null
|
|
62
|
-
// 发布时间:ISO
|
|
62
|
+
// 发布时间:ISO 字符串转本地可读格式;非法值直接隐藏
|
|
63
63
|
let published: string | null = null
|
|
64
64
|
if (info.publishedAt) {
|
|
65
65
|
const d = new Date(info.publishedAt)
|
|
@@ -70,7 +70,7 @@ export function InstalledDetailModal({ item, t, lang, langPath, onClose, onUpdat
|
|
|
70
70
|
? t('versionUpHint', { from: item.installedVersion, to: item.catalogVersion })
|
|
71
71
|
: t('updateAvailableHint'))
|
|
72
72
|
: item.pendingUntil != null
|
|
73
|
-
//
|
|
73
|
+
// 冷却期未到:给确切可重试时刻,而不是让「有更新」悄悄消失
|
|
74
74
|
? h('div', { className: styles.detailUpdateHint }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
75
75
|
: null,
|
|
76
76
|
// 详情信息网格:分类 / 状态 / 版本 / 收录入口 / 包名 / 统计 / 安装路径 / 安装时间 / 最后更新
|
|
@@ -70,7 +70,7 @@ export function LogsPathModal({ t, defaultPath, currentPath, onSaved, onClose }:
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/** 选择目录:系统对话框从默认位置打开,选中路径填入输入框(仍可手改)。 */
|
|
74
74
|
const browse = async () => {
|
|
75
75
|
if (browsing) return
|
|
76
76
|
setBrowsing(true)
|
|
@@ -118,7 +118,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
118
118
|
h('span', { className: styles.pendingRowStatus },
|
|
119
119
|
p.kind === 'uninstall' ? t('restartPendingHintUninstall') : t('restartPendingHint')),
|
|
120
120
|
h('span', { className: styles.pendingRowActions },
|
|
121
|
-
//
|
|
121
|
+
// 例外项:刷新解决不了(热加载失败 / 非 dsh 插件 / 更新)才登记待重启;
|
|
122
122
|
// 关闭本弹窗即视为「稍后」,只在真正需要时给单个「立即重启」
|
|
123
123
|
h('button', {
|
|
124
124
|
className: styles.restartNow,
|
|
@@ -376,7 +376,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
376
376
|
])
|
|
377
377
|
}
|
|
378
378
|
if (kind === 'hostIncompatible') {
|
|
379
|
-
//
|
|
379
|
+
// 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,安装前就被拒绝。
|
|
380
380
|
// 作者能修(把该版本加进兼容清单),故引导到作者仓库;同时说明本地可临时授权
|
|
381
381
|
const version = /incompatible with dsh\s+([^\s:;,]+)/i.exec(r.message ?? '')?.[1] ?? ''
|
|
382
382
|
return h('div', null, [
|
|
@@ -20,9 +20,9 @@ import type { FailureAction } from '../../logic/failures.ts'
|
|
|
20
20
|
|
|
21
21
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
22
22
|
* 终态只有两种:
|
|
23
|
-
* - needsRestart=true
|
|
23
|
+
* - needsRestart=true(刷新解决不了:热加载失败 / 非 dsh 插件 / 更新)→ 仅一个「立即重启」
|
|
24
24
|
* 按钮(重启会中断进行中的任务,故仍需用户确认),关闭即放弃;
|
|
25
|
-
* - 其余(含 needsReload=true
|
|
25
|
+
* - 其余(含 needsReload=true 的「已热加载、仅剩客户端 UI」)→ 仅「完成」按钮:关闭结果弹窗时
|
|
26
26
|
* 上层自动刷新页面让插件生效,不再给「稍后/立即刷新」手动选择。 */
|
|
27
27
|
function ResultView({
|
|
28
28
|
title, desc, t, restarting, needsRestart, needsReload, bundleDeselected, onRestart, onClose,
|
|
@@ -32,7 +32,7 @@ function ResultView({
|
|
|
32
32
|
t: Translate
|
|
33
33
|
restarting: boolean
|
|
34
34
|
needsRestart: boolean
|
|
35
|
-
/**
|
|
35
|
+
/** 仅刷新页面即生效(插件已热加载):给一句自动刷新说明,按钮仍是「完成」 */
|
|
36
36
|
needsReload?: boolean
|
|
37
37
|
/** 市场自更新成功、但 bundle 被取消勾选:重启后市场不会出现,提示重新勾选 */
|
|
38
38
|
bundleDeselected?: boolean
|
|
@@ -90,15 +90,15 @@ export interface InstallModalProps {
|
|
|
90
90
|
restarting: boolean
|
|
91
91
|
/** 已安装插件的覆盖更新(走同一条 add 命令原位重装,文案区分安装/更新) */
|
|
92
92
|
update: boolean
|
|
93
|
-
/**
|
|
93
|
+
/** 已过发布冷却期的安装版本:命令条带上它,手抄执行与一键安装结果一致(未查到时不传) */
|
|
94
94
|
pinVersion?: string
|
|
95
95
|
/** 仅命令行插件(webInstallable=false):不提供一键安装,只展示命令供复制到 dsh 终端 */
|
|
96
96
|
cliOnly: boolean
|
|
97
|
-
/**
|
|
97
|
+
/** 安装请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
|
|
98
98
|
submitting: boolean
|
|
99
99
|
/** 完成结果是否需重启才生效:true → 仅给「立即重启」;false → 仅「完成」(关闭即自动刷新) */
|
|
100
100
|
needsRestart: boolean
|
|
101
|
-
/**
|
|
101
|
+
/** 完成结果是否只需刷新页面即生效(插件已热加载):true → 结果视图显示自动刷新说明 */
|
|
102
102
|
needsReload: boolean
|
|
103
103
|
/** 市场自更新成功、但 profile 的 bundle 列表已不含 dsh-plugin:结果页提示重新勾选 */
|
|
104
104
|
bundleDeselected?: boolean
|
|
@@ -238,7 +238,7 @@ export interface UninstallModalProps {
|
|
|
238
238
|
t: Translate
|
|
239
239
|
langPath: string
|
|
240
240
|
restarting: boolean
|
|
241
|
-
/**
|
|
241
|
+
/** 卸载请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
|
|
242
242
|
submitting: boolean
|
|
243
243
|
/** 完成结果是否需重启才生效:true(停用失败等刷新解决不了的场景)→ 仅给「立即重启」;
|
|
244
244
|
* false(loader 已即时停用)→ 仅「完成」,关闭即自动刷新页面摘除插件面板 */
|
|
@@ -272,7 +272,7 @@ export function UninstallModal(props: UninstallModalProps) {
|
|
|
272
272
|
h('div', { className: styles.modalHead },
|
|
273
273
|
h('div', {
|
|
274
274
|
className: busy
|
|
275
|
-
// 注意 task 判空:submitting
|
|
275
|
+
// 注意 task 判空:submitting(请求进行中)时任务尚未入队,task 为 null,不能直接解引用
|
|
276
276
|
? `${styles.modalTitle} ${task !== null && task.status === 'pending' ? styles.modalTitleQueued : styles.modalTitleBusy}`
|
|
277
277
|
: styles.modalTitle,
|
|
278
278
|
}, title),
|
|
@@ -367,7 +367,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
367
367
|
// 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
|
|
368
368
|
// 只复制 message 会丢上下文 —— 命令行安装输入的目标、执行过的命令都在 command/attempts 里,
|
|
369
369
|
// 光拷一句「unsupported install target」出去没人看得懂
|
|
370
|
-
//
|
|
370
|
+
// 展示与复制都用展开后的文本:宿主兼容性提示里的 peerDependencies 是压缩 JSON,
|
|
371
371
|
// 原样展示会挤成一整行;格式化只影响可读性,不改变归类与上报逻辑
|
|
372
372
|
const displayMessage = formatFailureMessage(message)
|
|
373
373
|
const copyText = [
|
|
@@ -511,7 +511,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
|
|
|
511
511
|
}, t('failIssueBig')) : null,
|
|
512
512
|
])
|
|
513
513
|
: failureKind === 'hostIncompatible'
|
|
514
|
-
//
|
|
514
|
+
// 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,安装前就被宿主拒绝。
|
|
515
515
|
// 这是作者能修的(把该版本加进兼容清单),故直接引导到作者仓库;同时说明本地可临时授权
|
|
516
516
|
? h('div', null, [
|
|
517
517
|
(() => {
|
|
@@ -26,7 +26,7 @@ import { CloseIcon, HelpIcon } from '../ui/icons.tsx'
|
|
|
26
26
|
const NPM_PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/
|
|
27
27
|
|
|
28
28
|
/** npm 全局安装命令(官方 README 格式,如 `npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui`)——
|
|
29
|
-
* 提取 -g 之后的包列表;任一包名非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf
|
|
29
|
+
* 提取 -g 之后的包列表;任一包名非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf 规则一致。 */
|
|
30
30
|
const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
31
31
|
|
|
32
32
|
/** 常规 npm/pnpm 安装命令(装进当前 profile,如 `npm install lodash`、`pnpm add @scope/pkg`)——
|
|
@@ -34,7 +34,7 @@ const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
|
34
34
|
const NPM_INSTALL_CMD_RE = /^(?:npm\s+(?:install|i)|pnpm\s+(?:add|i|install))\s+(.+)$/i
|
|
35
35
|
|
|
36
36
|
/** 空格分隔的包列表:逐个按 npm 包名语法校验 + 拒绝以 - 开头的 token(npm 会把 --xxx 当参数而非包名),
|
|
37
|
-
* 任一非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf
|
|
37
|
+
* 任一非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf 同一套规则。 */
|
|
38
38
|
function splitPackages(raw: string): string[] | null {
|
|
39
39
|
const packages = raw.trim().split(/\s+/).filter((p) => p !== '')
|
|
40
40
|
if (packages.length === 0 || packages.some((p) => !NPM_PACKAGE_RE.test(p) || p.startsWith('-'))) return null
|
|
@@ -122,11 +122,11 @@ function InstalledRow({ item, t, langKey, canReveal, revealLabel, pendingRestart
|
|
|
122
122
|
item.hasUpdate
|
|
123
123
|
? h('span', { className: styles.updateBadge, title: t('updateAvailableHint') }, t('updateAvailable'))
|
|
124
124
|
: item.pendingUntil != null
|
|
125
|
-
//
|
|
125
|
+
// 冷却期未到:更新存在但还装不上,给确切时刻,避免看起来「已是最新」
|
|
126
126
|
? h('span', { className: styles.updateBadge, title: t('updatePendingHint') }, t('updatePending', { time: clockTime(item.pendingUntil) }))
|
|
127
127
|
: null,
|
|
128
128
|
),
|
|
129
|
-
// 右上角 Star / Fork
|
|
129
|
+
// 右上角 Star / Fork:与插件市场列表同一规则,装之前看过的热度在已安装里也能对照。
|
|
130
130
|
// 自定义安装没有目录元数据(plugin === null),没有可展示的统计
|
|
131
131
|
item.plugin
|
|
132
132
|
? h('div', { className: styles.rowStats },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 各分组对应本地服务端实际强制的行为:
|
|
10
10
|
* 更新与源 —— 启动检查更新 / npm 镜像 / HTTP 代理(都接进安装命令链路)
|
|
11
|
-
* 安全与信任 —— 受限的自定义安装(服务端 403
|
|
11
|
+
* 安全与信任 —— 受限的自定义安装(服务端 403 拦截)
|
|
12
12
|
* 诊断 —— 实时连通性探测 + 环境快照
|
|
13
13
|
* 日志 —— 系统日志查看器 + 存放位置
|
|
14
14
|
*/
|
|
@@ -176,7 +176,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy, openSect
|
|
|
176
176
|
className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
|
|
177
177
|
}),
|
|
178
178
|
}),
|
|
179
|
-
//
|
|
179
|
+
// 自动换源:只在公共源之间切换;自定义/内网源不换源,避免私有包名泄露
|
|
180
180
|
h(SettingRow, {
|
|
181
181
|
title: t('settingsRegistryFallback'),
|
|
182
182
|
desc: t('settingsRegistryFallbackDesc'),
|
|
@@ -214,7 +214,7 @@ export function SettingsView({ t, settings, update, reset, env, onCopy, openSect
|
|
|
214
214
|
)
|
|
215
215
|
|
|
216
216
|
const securityCard = h('div', { className: styles.card },
|
|
217
|
-
// 安全与信任:命令行安装的开关(仅 NPM 包)——
|
|
217
|
+
// 安全与信任:命令行安装的开关(仅 NPM 包)—— 服务端按通道校验 403 拦截
|
|
218
218
|
//(npm 目标吃 enableNpmInstall);目录内收录的插件安装不受影响(默认开启)
|
|
219
219
|
h(SettingRow, {
|
|
220
220
|
title: t('settingsEnableNpm'),
|
|
@@ -10,7 +10,7 @@ import type { HubPlugin, LocaleId } from '../types.ts'
|
|
|
10
10
|
import { normalize } from '../logic/normalize.ts'
|
|
11
11
|
|
|
12
12
|
// 目录/统计数据经服务端 /catalog 代理路由获取(curl 子进程注入设置里的代理),
|
|
13
|
-
// 与 npm / git
|
|
13
|
+
// 与 npm / git 安装通道同一代理规则 —— 浏览器不直连 api.dsh-plugin.org。
|
|
14
14
|
const PROXY_BASE = '/dsh-plugin-hub/catalog'
|
|
15
15
|
// refresh=1:跳过服务端 1 小时缓存直接回源(失败仍回退旧缓存),供「强制刷新」使用
|
|
16
16
|
const PLUGINS_URL = (lang: LocaleId, refresh: boolean) => `${PROXY_BASE}?lang=${lang}${refresh ? '&refresh=1' : ''}`
|
package/src/client/data/host.ts
CHANGED
|
@@ -48,11 +48,11 @@ export async function fetchInstalled(): Promise<HostInstalled | null> {
|
|
|
48
48
|
export interface NpmReleaseInfo {
|
|
49
49
|
version: string
|
|
50
50
|
publishedAt: string
|
|
51
|
-
/** 已过 pnpm
|
|
51
|
+
/** 已过 pnpm 发布冷却期的最高稳定版:安装/展示命令锁定它;无可锁定版本时为 null。 */
|
|
52
52
|
installable?: NpmReleaseInfo | null
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/** 批量查询已安装插件 npm
|
|
55
|
+
/** 批量查询已安装插件 npm 包的最新版本发布时间(发布冷却期用);失败返回 null(调用方按原逻辑照常提示)。 */
|
|
56
56
|
export async function fetchNpmReleaseAge(packages: string[]): Promise<Record<string, NpmReleaseInfo | null> | null> {
|
|
57
57
|
if (packages.length === 0) return {}
|
|
58
58
|
try {
|
package/src/client/data/hub.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
6
|
* Hub 元数据仓库:自我更新检查与「关注我们」内容背后的 Cloudflare Worker 接口。
|
|
7
|
-
* 失败降级为 null ——
|
|
7
|
+
* 失败降级为 null —— 由调用方决定失败时如何显示(不显示徽标、占位文案),绝不抛错。
|
|
8
8
|
*/
|
|
9
9
|
import type { HubAboutInfo, HubUpdateInfo, HubUpdateVersion } from '../types.ts'
|
|
10
10
|
import { HUB_ABOUT_URL, HUB_UPDATE_URL } from '../logic/constants.ts'
|
|
@@ -19,7 +19,7 @@ function busted(url: string): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** 归一化接口下发的 recentVersions:只保留带版本号的条目,其余字段按类型补。
|
|
22
|
-
* 这条数据是「最新版还在 24.5h
|
|
22
|
+
* 这条数据是「最新版还在 24.5h 冷却期内时回退到更早的已过冷却期版本」的唯一依据;
|
|
23
23
|
* 入口处漏传会让候选只剩最新版,整体变成 pending —— 用户就永远看不到更新入口。 */
|
|
24
24
|
function recentVersionsOf(value: unknown): HubUpdateVersion[] {
|
|
25
25
|
if (!Array.isArray(value)) return []
|
|
@@ -37,7 +37,7 @@ function recentVersionsOf(value: unknown): HubUpdateVersion[] {
|
|
|
37
37
|
return list
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** 版本控制中心:最新版本 + Markdown 变更记录 +
|
|
40
|
+
/** 版本控制中心:最新版本 + Markdown 变更记录 + 最近发版(冷却期回退用);不可用返回 null。 */
|
|
41
41
|
export async function fetchHubUpdate(): Promise<HubUpdateInfo | null> {
|
|
42
42
|
try {
|
|
43
43
|
const res = await fetch(busted(HUB_UPDATE_URL), { cache: 'no-store' })
|
|
@@ -52,7 +52,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
52
52
|
* 界面停在「正在加载插件数据…」假死 —— 超时自动重拉,最多自动重试 2 次后转失败态;
|
|
53
53
|
* 用户手动刷新(reload)时清零重新计数。 */
|
|
54
54
|
const loadRetriesRef = useRef(0)
|
|
55
|
-
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 60s,客户端 30s
|
|
55
|
+
/** 目录加载超时阈值:超过即判定挂起,强制刷新一次(服务端代理 curl 上限 60s,客户端 30s 先超时) */
|
|
56
56
|
const LOAD_TIMEOUT_MS = 30000
|
|
57
57
|
/** 挂起后最多自动重试次数:避免代理持续不通时无限循环刷新(每次重试仍 30s,2 次后转失败界面可手动重试) */
|
|
58
58
|
const MAX_LOAD_RETRIES = 2
|
|
@@ -68,7 +68,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
68
68
|
* 强制刷新 Hub 版本与「关注我们」信息:点击头部版本号/徽标时触发。
|
|
69
69
|
* 挂载期的 useEffect 只在进入/reload 时拉一次,发版后已打开的界面点版本号
|
|
70
70
|
* 仍显示旧数据;此处重新拉接口中心,最新版本与变更记录点击即见。
|
|
71
|
-
*
|
|
71
|
+
* 失败时返回 null(弹窗退回展示当前版本号),与挂载期拉取规则一致。
|
|
72
72
|
*/
|
|
73
73
|
const refreshHub = async () => {
|
|
74
74
|
// 版本接口已恒带时间戳绕过 CDN,手动/自动拉取都能拿到最新公告
|
|
@@ -115,7 +115,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
115
115
|
settled = true
|
|
116
116
|
// 插件市场不显示自己:DSH Plugin Hub 从目录里排除自身条目,
|
|
117
117
|
// 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
|
|
118
|
-
//
|
|
118
|
+
// 目录数据已排除自身,此处再过滤一次,防旧快照仍含自身条目。
|
|
119
119
|
const hadSelf = list.some((p) => p.source?.repo === SELF_REPO)
|
|
120
120
|
// 过滤前单独保留 hub 自身条目:它不进目录列表,但「可更新」徽标的直接更新需要它作为重装目标
|
|
121
121
|
setHubPlugin(list.find((p) => p.source?.repo === SELF_REPO) ?? null)
|
|
@@ -131,7 +131,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
131
131
|
settled = true
|
|
132
132
|
setFailed(true)
|
|
133
133
|
})
|
|
134
|
-
//
|
|
134
|
+
// 加载超时保护:宿主刚重启/代理未就绪时目录请求可能长时间挂起,界面停在「正在加载插件数据…」
|
|
135
135
|
// 像假死。超过阈值未收尾 → 强制刷新列表(重拉一次,最多 MAX_LOAD_RETRIES 次自动重试),
|
|
136
136
|
// 重试仍失败 → 转失败界面(显示「重试」按钮,手动刷新时计数清零重新开始)。
|
|
137
137
|
const timer = window.setTimeout(() => {
|
|
@@ -152,7 +152,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
152
152
|
// 否则英文模式仍停留在 plugins.zh.json,描述/名称全是中文
|
|
153
153
|
}, [reloadKey, lang])
|
|
154
154
|
|
|
155
|
-
/** 刷新当前 profile
|
|
155
|
+
/** 刷新当前 profile 已安装插件表;宿主未挂本地路由时降级为空表。 */
|
|
156
156
|
const refreshInstalled = async () => {
|
|
157
157
|
const data = await fetchInstalled()
|
|
158
158
|
if (data === null) return
|
|
@@ -168,7 +168,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
168
168
|
|
|
169
169
|
// Hub 自身版本检查走独立 Worker(版本控制中心):发新版后写一次 Worker KV,
|
|
170
170
|
// 所有已装用户立即看到「可更新」+ Markdown 变更记录,不经过主站目录数据管道。
|
|
171
|
-
//
|
|
171
|
+
// 失败时降级为 null(徽标不出现,不影响目录本身加载)。
|
|
172
172
|
useEffect(() => {
|
|
173
173
|
let cancelled = false
|
|
174
174
|
void fetchHubUpdate().then((info) => { if (!cancelled) setHubUpdateInfo(info) })
|
|
@@ -176,7 +176,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
176
176
|
}, [reloadKey])
|
|
177
177
|
|
|
178
178
|
// 「关注我们」内容走同一套 Worker:作者写好 Markdown(平台介绍 + 反馈群二维码)推送 /admin/about,
|
|
179
|
-
// 客户端下次进入拉 /about
|
|
179
|
+
// 客户端下次进入拉 /about 即可看到最新,非写死。失败时返回 null(弹窗展示备用文案,不阻塞)。
|
|
180
180
|
useEffect(() => {
|
|
181
181
|
let cancelled = false
|
|
182
182
|
void fetchHubAbout().then((info) => { if (!cancelled) setHubAboutInfo(info) })
|
|
@@ -231,7 +231,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
231
231
|
return
|
|
232
232
|
}
|
|
233
233
|
let cancelled = false
|
|
234
|
-
// 批量一次查询;失败/返回 null
|
|
234
|
+
// 批量一次查询;失败/返回 null 时按原逻辑照常提示,网络问题不会漏掉真实更新
|
|
235
235
|
void fetchNpmReleaseAge(releaseAgeCandidates).then((result) => {
|
|
236
236
|
if (cancelled) return
|
|
237
237
|
setReleaseAge({ key: releaseAgeKey, releases: result ?? EMPTY_RELEASES })
|
|
@@ -244,7 +244,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* 不能提示更新的包:
|
|
247
|
-
* - npm
|
|
247
|
+
* - npm 上最新版还没过发布冷却期(发布不满 24h,pnpm 会不提示地回退,点了也装不上);
|
|
248
248
|
* - npm 上最新版并不高于已装版本 —— 更新走的是 `pkg@latest`,此时无论目录标到哪个 tag,
|
|
249
249
|
* 都不可能装上(作者可能只打了 GitHub release 没发 npm),提示只会变成永远点不掉的循环。
|
|
250
250
|
*/
|
|
@@ -280,7 +280,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
280
280
|
return releaseAgeReady && !updateBlocked.has(pkg)
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
/**
|
|
283
|
+
/** 更新被冷却期未到时的可重试时刻(毫秒);未安装/无更新/非冷却期原因 → null。 */
|
|
284
284
|
const updatePendingUntil = (p: HubPlugin): number | null => {
|
|
285
285
|
if (!hasUpdateOf(p, installed, versions)) return null
|
|
286
286
|
const pkg = npmPackageOf(p, versions)
|
|
@@ -289,7 +289,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
/** 已安装项统一列表(目录元数据 + 运行时信息合并):校验未就绪/被拦下的去掉更新标记、
|
|
292
|
-
*
|
|
292
|
+
* 冷却期未到的带上可重试时刻,驱动「已安装」tab。 */
|
|
293
293
|
const installedItems = useMemo<InstalledItem[]>(() => {
|
|
294
294
|
return rawInstalledItems.map((item) => {
|
|
295
295
|
if (!item.hasUpdate || !item.plugin) return item
|
|
@@ -309,13 +309,13 @@ export function useCatalog(lang: LocaleId) {
|
|
|
309
309
|
* 构建时注入的 PLUGIN_VERSION 是运行 bundle 的真实版本;测试/异常场景缺失时
|
|
310
310
|
* 回退到安装时记录的版本。版本号不等即新版(版本号只在发版时变更,绝无降级场景)。
|
|
311
311
|
*
|
|
312
|
-
* 另加 pnpm
|
|
312
|
+
* 另加 pnpm 发布冷却期:新版本发布还不满 24 小时(+30 分钟缓冲)时,pnpm 会不提示地
|
|
313
313
|
* 回退到旧版本,此时若提示「可更新」用户点了也更新不到。publishedAt 由接口中心按 npm
|
|
314
|
-
* registry
|
|
314
|
+
* registry 真实发布时间下发(同源),据此判断最准。冷却期命中要单列成 `pending` 而不是
|
|
315
315
|
* 落回「无更新」——回落会让弹窗在本机版本落后时谎称「已是最新」。
|
|
316
316
|
*/
|
|
317
317
|
const hubCurrentVersion = PLUGIN_VERSION || versions[HUB_REPO]?.version || ''
|
|
318
|
-
// 候选含 recentVersions
|
|
318
|
+
// 候选含 recentVersions:最新版还在冷却期内时回退到「更早、但已过冷却期」的版本,
|
|
319
319
|
// 避免连续快速发版把所有提示窗口都吞掉(见 pickHubUpdate)。
|
|
320
320
|
const hubUpdateResult = hubUpdateInfo === null
|
|
321
321
|
? { state: 'none' as HubUpdateState, target: null }
|
|
@@ -60,7 +60,7 @@ export function useSettings() {
|
|
|
60
60
|
return () => { alive = false }
|
|
61
61
|
}, [])
|
|
62
62
|
|
|
63
|
-
/** 部分更新:本地立即生效(乐观),同时 POST
|
|
63
|
+
/** 部分更新:本地立即生效(乐观),同时 POST 持久化;持久化失败不影响本次会话。 */
|
|
64
64
|
const update = useCallback((patch: Partial<HubSettings>) => {
|
|
65
65
|
setSettings((prev) => ({ ...prev, ...patch }))
|
|
66
66
|
void fetch('/dsh-plugin-hub/settings', {
|
|
@@ -71,7 +71,7 @@ export function useSettings() {
|
|
|
71
71
|
}).catch(() => { /* 宿主未挂路由:仅本次会话生效 */ })
|
|
72
72
|
}, [])
|
|
73
73
|
|
|
74
|
-
/**
|
|
74
|
+
/** 重置为默认设置:本地立即回默认,同时通知服务端删除设置文件;失败不影响本地已重置的设置。 */
|
|
75
75
|
const reset = useCallback(() => {
|
|
76
76
|
setSettings({ ...DEFAULT_SETTINGS })
|
|
77
77
|
void fetch('/dsh-plugin-hub/settings/reset', {
|
|
@@ -49,11 +49,11 @@ export interface QueueTask {
|
|
|
49
49
|
command?: string
|
|
50
50
|
/** 实际执行/重试过的安装命令(按先后顺序):失败时 issue 预填一并贴给作者 */
|
|
51
51
|
attempts?: string[]
|
|
52
|
-
/** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
/** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true:数据缺失时不视为已生效。
|
|
53
|
+
* 仅刷新解决不了的例外场景(热加载失败 / 非 dsh 插件 / 更新)为 true,结果视图给「立即重启」;
|
|
54
|
+
* 其余(热加载成功、卸载已即时停用、全局安装)为 false,结果视图仅「完成」。 */
|
|
55
55
|
needsRestart: boolean
|
|
56
|
-
/**
|
|
56
|
+
/** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,或卸载后插件面板仍残留在当前页面):
|
|
57
57
|
* true 时关闭结果弹窗 / Toast 消失后由上层自动刷新页面,不给手动刷新按钮。 */
|
|
58
58
|
needsReload?: boolean
|
|
59
59
|
/** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:重启后市场不会出现 */
|
|
@@ -78,11 +78,11 @@ export interface TaskQueueOptions {
|
|
|
78
78
|
refreshInstalled: () => void
|
|
79
79
|
/** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
|
|
80
80
|
* needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」);
|
|
81
|
-
* needsReload
|
|
81
|
+
* needsReload 表示是否只需刷新页面即生效(热加载成功、或卸载后插件面板仍残留)。
|
|
82
82
|
* update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
|
|
83
83
|
onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean, needsReload?: boolean, bundleDeselected?: boolean) => void
|
|
84
84
|
onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, needsReload?: boolean) => void
|
|
85
|
-
/**
|
|
85
|
+
/** 任务失败:完整输出或备用文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 实际执行/重试过的安装命令(可缺省)+ 是否更新。 */
|
|
86
86
|
onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
|
|
87
87
|
/** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
|
|
88
88
|
installPlugin: HubPlugin | null
|
|
@@ -114,7 +114,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
114
114
|
/** 待重启列表(服务端内存态 + 本地展示信息补齐);与 queue 一样以 ref 为唯一事实来源。 */
|
|
115
115
|
const [pendingRestarts, setPendingRestarts] = useState<PendingRestart[]>([])
|
|
116
116
|
const pendingRef = useRef<PendingRestart[]>([])
|
|
117
|
-
/** 本次会话内安装成功插件的展示信息(目标 → 简介/版本):轮询合并待重启列表时补齐,刷新后由 resolvePending
|
|
117
|
+
/** 本次会话内安装成功插件的展示信息(目标 → 简介/版本):轮询合并待重启列表时补齐,刷新后由 resolvePending 补上。 */
|
|
118
118
|
const pendingInfoRef = useRef<Map<string, { desc?: string; version?: string }>>(new Map())
|
|
119
119
|
/** 保持最新的 resolvePending:轮询闭包捕获的是调用那一刻的目录状态,目录晚加载时也能补齐。 */
|
|
120
120
|
const resolvePendingRef = useRef(resolvePending)
|
|
@@ -125,12 +125,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
125
125
|
const settlingRef = useRef<Set<number>>(new Set())
|
|
126
126
|
/** 轮询定时器句柄(队列清空/组件卸载时清理) */
|
|
127
127
|
const pollRef = useRef<number | null>(null)
|
|
128
|
-
/**
|
|
128
|
+
/** 请求还没返回的目标集合(同步防重):双击安装/卸载时第二击直接忽略。
|
|
129
129
|
* 本地队列任务要等 fetch 返回后才入队,仅靠 queueRef 检查拦不住请求窗口内的重复点击。 */
|
|
130
130
|
const submittingRef = useRef<Set<string>>(new Set())
|
|
131
131
|
/** 乐观入队临时 id 计数器:递减产生唯一负数,与服务端自增正数 id 永不冲突。 */
|
|
132
132
|
const tempIdRef = useRef(0)
|
|
133
|
-
/**
|
|
133
|
+
/** 请求进行中标记:弹窗据此禁用确认按钮,避免等待响应期间被再次点击。 */
|
|
134
134
|
const [submitting, setSubmitting] = useState(false)
|
|
135
135
|
|
|
136
136
|
/** 停止后台任务轮询(任务结束或组件卸载时清理)。 */
|
|
@@ -146,7 +146,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
146
146
|
if (queueRef.current.length === 0 && pendingRef.current.length === 0) stopPoll()
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
//
|
|
149
|
+
// 组件卸载时清理轮询定时器
|
|
150
150
|
useEffect(() => stopPoll, [])
|
|
151
151
|
|
|
152
152
|
/** 更新队列(同步镜像到 ref:queueRef 是唯一事实来源,state 只负责触发渲染,
|
|
@@ -200,7 +200,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
200
200
|
body: JSON.stringify({ repo, version, updatedAt }),
|
|
201
201
|
})
|
|
202
202
|
} catch {
|
|
203
|
-
//
|
|
203
|
+
// 宿主未挂本地路由:直接忽略,仅本机展示受影响
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
|
|
@@ -274,7 +274,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
274
274
|
}, stepMs)
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
/** 任务在 /active 中消失 = 已结束:查 /status 拿终态并收尾(cancelled
|
|
277
|
+
/** 任务在 /active 中消失 = 已结束:查 /status 拿终态并收尾(cancelled 直接移除)。 */
|
|
278
278
|
const settleTask = async (q: QueueTask) => {
|
|
279
279
|
let status = 'failed'
|
|
280
280
|
let lines: string[] = []
|
|
@@ -292,7 +292,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
292
292
|
attempts = Array.isArray(task.attempts) ? task.attempts : undefined
|
|
293
293
|
bundleDeselected = task.bundleDeselected === true ? true : undefined
|
|
294
294
|
installedVersion = typeof task.installedVersion === 'string' && task.installedVersion !== '' ? task.installedVersion : undefined
|
|
295
|
-
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader
|
|
295
|
+
// 终态带出的「是否需要重启 / 是否只需刷新」覆盖本地(服务端 done 时按 loader 热加载/即时移除情况设置)
|
|
296
296
|
const needsRestart = typeof task.needsRestart === 'boolean' ? task.needsRestart : q.needsRestart
|
|
297
297
|
const needsReload = typeof task.needsReload === 'boolean' ? task.needsReload : (q.needsReload ?? false)
|
|
298
298
|
if (status === 'done') {
|
|
@@ -383,7 +383,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
383
383
|
try { await settleTask(q) } finally { settlingRef.current.delete(q.id) }
|
|
384
384
|
}
|
|
385
385
|
} catch {
|
|
386
|
-
//
|
|
386
|
+
// 服务端暂不可达:等下一轮再试
|
|
387
387
|
}
|
|
388
388
|
}, 600)
|
|
389
389
|
}
|
|
@@ -455,8 +455,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
455
455
|
source: 'catalog' | 'custom'
|
|
456
456
|
/** 更新已安装目标(放行 add 覆盖重装) */
|
|
457
457
|
update?: boolean
|
|
458
|
-
/**
|
|
459
|
-
*
|
|
458
|
+
/** 锁定精确版本(Hub 自我更新用):最新版可能仍在 pnpm 发布冷却期内,
|
|
459
|
+
* 必须安装「已过冷却期」的那个版本,不能再用 @latest。 */
|
|
460
460
|
pinVersion?: string
|
|
461
461
|
/** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
|
|
462
462
|
globalNpm?: string[]
|
|
@@ -466,7 +466,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
466
466
|
}) => {
|
|
467
467
|
// 防重复入队:同一目标已在排队/执行中则忽略
|
|
468
468
|
if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
|
|
469
|
-
//
|
|
469
|
+
// 请求进行中防重:任务要等响应回来才进本地队列,这之前再次点击(双击)直接忽略
|
|
470
470
|
if (submittingRef.current.has(input.repo)) return
|
|
471
471
|
submittingRef.current.add(input.repo)
|
|
472
472
|
// 乐观入队:点击安装立即显示「正在安装中」,不等 POST 响应。
|
|
@@ -498,7 +498,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
498
498
|
display: input.repo,
|
|
499
499
|
source: input.source,
|
|
500
500
|
mode: input.update ? 'update' : undefined,
|
|
501
|
-
//
|
|
501
|
+
// 锁定版本(Hub 自我更新):服务端据此执行 `add <pkg>@<version>` 而不是 @latest
|
|
502
502
|
...(input.pinVersion ? { version: input.pinVersion } : {}),
|
|
503
503
|
// 界面语言带给服务端:安装前网络探测等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
|
|
504
504
|
...(langKey ? { lang: langKey } : {}),
|
|
@@ -531,7 +531,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
531
531
|
// 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
|
|
532
532
|
pollQueue()
|
|
533
533
|
} catch (err) {
|
|
534
|
-
//
|
|
534
|
+
// 网络异常:移除乐观条目,并以错误弹窗提示(附上底层原因,避免只剩一句笼统的「安装失败」)
|
|
535
535
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
536
536
|
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
537
537
|
// [request-timeout]:给分类器一个与界面语言无关的超时标记
|
|
@@ -572,8 +572,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
572
572
|
/** 命令行安装:用户手输 npm 包名,走 custom 源(受安全开关控制)。
|
|
573
573
|
* update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
|
|
574
574
|
* channel=入口渠道(NPM 包卡片):上报服务端落日志,失败弹窗/通知据此精准溯源。
|
|
575
|
-
* 队列展示/防重一律用归一化身份:与服务端 /active
|
|
576
|
-
* npm
|
|
575
|
+
* 队列展示/防重一律用归一化身份:与服务端 /active 合并、弹窗目标匹配同一套规则,
|
|
576
|
+
* npm 包名原样传递不受影响。 */
|
|
577
577
|
const installCustom = async (raw: string, opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
578
578
|
const target = raw.trim()
|
|
579
579
|
if (!target) return
|
|
@@ -589,7 +589,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
/** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
592
|
-
* 队列展示/防重用包列表空格拼接的身份(与服务端 target
|
|
592
|
+
* 队列展示/防重用包列表空格拼接的身份(与服务端 target 同一套规则);入口渠道固定为 npm(NPM 包卡片)。 */
|
|
593
593
|
const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
594
594
|
const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
|
|
595
595
|
if (list.length === 0) return
|
|
@@ -609,7 +609,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
609
609
|
const enqueueUninstall = async (name: string, repo: string | null, desc?: string) => {
|
|
610
610
|
if (!name) return
|
|
611
611
|
if (queueRef.current.some((q) => q.kind === 'uninstall' && q.target === name)) return
|
|
612
|
-
//
|
|
612
|
+
// 请求进行中防重:与安装一致,响应回来前再次点击直接忽略
|
|
613
613
|
if (submittingRef.current.has(name)) return
|
|
614
614
|
submittingRef.current.add(name)
|
|
615
615
|
setSubmitting(true)
|
|
@@ -624,6 +624,13 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
624
624
|
})
|
|
625
625
|
const data = await res.json() as { ok?: boolean; task?: number; error?: string }
|
|
626
626
|
if (!data.ok || typeof data.task !== 'number') {
|
|
627
|
+
// 依赖已经不在了(列表还没刷新时重复点击):按已卸载收尾,刷新列表,不弹失败
|
|
628
|
+
if (res.status === 400 && data.error === 'plugin is not installed') {
|
|
629
|
+
refreshInstalled()
|
|
630
|
+
void clearInstalledVersion(repo)
|
|
631
|
+
onUninstallDone(false, repo, false, false)
|
|
632
|
+
return
|
|
633
|
+
}
|
|
627
634
|
onError(data.error || `HTTP ${res.status}`, repo, 'uninstall')
|
|
628
635
|
return
|
|
629
636
|
}
|