dsh-plugin 2.3.2 → 2.3.4

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.
Files changed (84) hide show
  1. package/README.en.md +3 -9
  2. package/README.md +3 -9
  3. package/client/client.js +371 -364
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +4 -4
  6. package/lib/http/diagnostics.js +4 -4
  7. package/lib/http/install.js +13 -13
  8. package/lib/http/npm-release-age.js +3 -3
  9. package/lib/http/shared.js +1 -1
  10. package/lib/services/host-version.js +2 -2
  11. package/lib/services/install/dsh-compat.js +1 -1
  12. package/lib/services/install/npm-resolve.js +1 -1
  13. package/lib/services/install/operation-log.js +3 -3
  14. package/lib/services/install/preflight.js +1 -1
  15. package/lib/services/install/registry-fallback.js +3 -3
  16. package/lib/services/install/release-target.js +2 -2
  17. package/lib/services/install/task-queue.js +35 -20
  18. package/lib/services/loader.js +8 -8
  19. package/lib/services/npm-release-age.js +8 -8
  20. package/lib/services/probe.js +12 -12
  21. package/lib/services/profile/installed-versions.js +2 -2
  22. package/lib/services/profile/profile.js +2 -2
  23. package/lib/types/services/host-version.d.ts +1 -1
  24. package/lib/types/services/install/install-types.d.ts +5 -5
  25. package/lib/types/services/install/operation-log.d.ts +1 -1
  26. package/lib/types/services/install/preflight.d.ts +1 -1
  27. package/lib/types/services/install/registry-fallback.d.ts +1 -1
  28. package/lib/types/services/install/release-target.d.ts +1 -1
  29. package/lib/types/services/install/task-queue.d.ts +10 -7
  30. package/lib/types/services/loader.d.ts +5 -5
  31. package/lib/types/services/npm-release-age.d.ts +5 -5
  32. package/lib/types/services/probe.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/src/client/components/PluginHubSection.tsx +29 -29
  35. package/src/client/components/catalog/CatalogList.tsx +1 -1
  36. package/src/client/components/catalog/PluginCard.tsx +3 -3
  37. package/src/client/components/modals/AboutModal.tsx +2 -2
  38. package/src/client/components/modals/ConfirmDialog.tsx +2 -2
  39. package/src/client/components/modals/HubUpdateModal.tsx +3 -3
  40. package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
  41. package/src/client/components/modals/LogsPathModal.tsx +1 -1
  42. package/src/client/components/modals/NotificationsModal.tsx +2 -2
  43. package/src/client/components/modals/modals.tsx +10 -10
  44. package/src/client/components/views/CustomInstallView.tsx +2 -2
  45. package/src/client/components/views/InstalledView.tsx +2 -2
  46. package/src/client/components/views/SettingsView.tsx +3 -3
  47. package/src/client/data/catalog.ts +1 -1
  48. package/src/client/data/host.ts +2 -2
  49. package/src/client/data/hub.ts +3 -3
  50. package/src/client/hooks/useCatalog.ts +14 -14
  51. package/src/client/hooks/useSettings.ts +2 -2
  52. package/src/client/hooks/useTaskQueue.ts +30 -23
  53. package/src/client/locales.ts +15 -15
  54. package/src/client/logic/constants.ts +1 -1
  55. package/src/client/logic/ensure-plugin-css.ts +2 -2
  56. package/src/client/logic/failures.ts +9 -9
  57. package/src/client/logic/install-command.ts +1 -2
  58. package/src/client/logic/installed.ts +1 -1
  59. package/src/client/logic/release-age.ts +10 -10
  60. package/src/client/logic/urls.ts +6 -4
  61. package/src/client/styles/Header.module.css +1 -1
  62. package/src/client/styles/Modal.module.css +2 -2
  63. package/src/client/styles/SectionTabs.module.css +1 -1
  64. package/src/client/styles/tokens.module.css +1 -1
  65. package/src/client/types.ts +4 -4
  66. package/src/server/http/catalog.ts +4 -4
  67. package/src/server/http/diagnostics.ts +4 -4
  68. package/src/server/http/install.ts +13 -13
  69. package/src/server/http/npm-release-age.ts +3 -3
  70. package/src/server/http/shared.ts +1 -1
  71. package/src/server/services/host-version.ts +2 -2
  72. package/src/server/services/install/dsh-compat.ts +1 -1
  73. package/src/server/services/install/install-types.ts +5 -5
  74. package/src/server/services/install/npm-resolve.ts +1 -1
  75. package/src/server/services/install/operation-log.ts +3 -3
  76. package/src/server/services/install/preflight.ts +1 -1
  77. package/src/server/services/install/registry-fallback.ts +3 -3
  78. package/src/server/services/install/release-target.ts +2 -2
  79. package/src/server/services/install/task-queue.ts +40 -26
  80. package/src/server/services/loader.ts +10 -10
  81. package/src/server/services/npm-release-age.ts +10 -10
  82. package/src/server/services/probe.ts +12 -12
  83. package/src/server/services/profile/installed-versions.ts +2 -2
  84. 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
- /** 在途时确认按钮文案(如「清空中…」);缺省沿用 confirmLabel */
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'):远端确有新版本,但发布未满供应链安全门槛,pnpm 此时会
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
- // 兜底项:刷新解决不了(热挂载失败 / 非 dsh 插件 / 更新)才登记待重启;
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
- // 宿主版本门禁:插件声明的兼容范围不含当前 dsh 版本,安装前就被拒绝。
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(刷新解决不了:热挂载失败 / 非 dsh 插件 / 更新)→ 仅一个「立即重启」
23
+ * - needsRestart=true(刷新解决不了:热加载失败 / 非 dsh 插件 / 更新)→ 仅一个「立即重启」
24
24
  * 按钮(重启会中断进行中的任务,故仍需用户确认),关闭即放弃;
25
- * - 其余(含 needsReload=true 的「已热挂载、仅剩客户端 UI」)→ 仅「完成」按钮:关闭结果弹窗时
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
- /** 安装请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
97
+ /** 安装请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
98
98
  submitting: boolean
99
99
  /** 完成结果是否需重启才生效:true → 仅给「立即重启」;false → 仅「完成」(关闭即自动刷新) */
100
100
  needsRestart: boolean
101
- /** 完成结果是否只需刷新页面即生效(插件已热挂载):true → 结果视图显示自动刷新说明 */
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
- /** 卸载请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
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(请求在途)时任务尚未入队,task 为 null,不能直接解引用
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
- // 展示与复制都用展开后的文本:宿主门禁消息里的 peerDependencies 是压缩 JSON,
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
- // 宿主版本门禁:插件声明的兼容范围不含当前 dsh 版本,安装前就被宿主拒绝。
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 包)—— 服务端按通道门禁 403 拦截
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 安装通道同一代理口径 —— 浏览器不直连 api.dsh-plugin.org。
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' : ''}`
@@ -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 供应链门禁的最高稳定版:安装/展示命令钉住它;无可钉版本时为 null。 */
51
+ /** 已过 pnpm 发布冷却期的最高稳定版:安装/展示命令锁定它;无可锁定版本时为 null。 */
52
52
  installable?: NpmReleaseInfo | null
53
53
  }
54
54
 
55
- /** 批量查询已安装插件 npm 包的最新版本发布时间(供应链门禁用);失败返回 null(调用方按原逻辑照常提示)。 */
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 {
@@ -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 变更记录 + 最近发版(门禁回退用);不可用返回 null。 */
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
- * 失败静默为 null(弹窗兜底展示当前版本号),与挂载期拉取口径一致。
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
- // 失败静默降级为 null(徽标不出现,不影响目录本身加载)。
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 即可看到最新,非写死。失败静默为 null(弹窗展示兜底文案,不阻塞)。
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 上最新版还没过供应链门禁(发布不满 24h,pnpm 会静默回退,点了也装不上);
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
- /** 更新被门禁暂缓时的可重试时刻(毫秒);未安装/无更新/非门禁原因 → null。 */
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
- * 门禁暂缓的带上可重试时刻,驱动「已安装」tab。 */
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 供应链安全门槛:新版本发布还不满 24 小时(+30 分钟缓冲)时,pnpm 会静默
312
+ * 另加 pnpm 发布冷却期:新版本发布还不满 24 小时(+30 分钟缓冲)时,pnpm 会不提示地
313
313
  * 回退到旧版本,此时若提示「可更新」用户点了也更新不到。publishedAt 由接口中心按 npm
314
- * registry 真实发布时间下发(同源),据此判断最准。门槛命中要单列成 `pending` 而不是
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
- * 仅刷新解决不了的兜底场景(热挂载失败 / 非 dsh 插件 / 更新)为 true,结果视图给「立即重启」;
54
- * 其余(热挂载成功、卸载已即时停用、全局安装)为 false,结果视图仅「完成」。 */
52
+ /** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true:数据缺失时不视为已生效。
53
+ * 仅刷新解决不了的例外场景(热加载失败 / 非 dsh 插件 / 更新)为 true,结果视图给「立即重启」;
54
+ * 其余(热加载成功、卸载已即时停用、全局安装)为 false,结果视图仅「完成」。 */
55
55
  needsRestart: boolean
56
- /** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,或卸载后插件面板仍残留在当前页面):
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
- /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 实际执行/重试过的安装命令(可缺省)+ 是否更新。 */
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
- /** 钉住精确版本(Hub 自我更新用):最新版可能仍在 pnpm 供应链门禁内,
459
- * 必须安装「已过门禁」的那个版本,不能再用 @latest。 */
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
- // 钉住版本(Hub 自我更新):服务端据此执行 `add <pkg>@<version>` 而不是 @latest
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 同口径);入口渠道固定为 npm(NPM 包卡片)。 */
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
  }