dsh-plugin 1.1.2 → 1.2.1

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 (110) hide show
  1. package/client/client.js +4465 -1442
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +438 -22
  4. package/lib/index.js +1 -1
  5. package/lib/services/install/install.js +4 -0
  6. package/lib/services/install/npm-check.js +55 -0
  7. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
  8. package/lib/services/{preflight.js → install/preflight.js} +15 -4
  9. package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
  10. package/lib/services/loader.js +56 -1
  11. package/lib/services/log.js +138 -0
  12. package/lib/services/probe.js +157 -0
  13. package/lib/services/{profile.js → profile/profile.js} +29 -1
  14. package/lib/services/settings.js +60 -0
  15. package/lib/types/http/routes.d.ts +1 -1
  16. package/lib/types/index.d.ts +1 -1
  17. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
  18. package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
  19. package/lib/types/services/install/npm-check.d.ts +12 -0
  20. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  21. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  22. package/lib/types/services/loader.d.ts +14 -8
  23. package/lib/types/services/log.d.ts +49 -0
  24. package/lib/types/services/probe.d.ts +30 -0
  25. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
  26. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
  27. package/lib/types/services/settings.d.ts +24 -0
  28. package/package.json +1 -1
  29. package/src/client/components/PluginHubSection.tsx +341 -120
  30. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
  31. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
  32. package/src/client/components/layout/CatalogControls.tsx +80 -0
  33. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
  34. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
  35. package/src/client/components/layout/SectionTabs.tsx +60 -0
  36. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
  37. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
  38. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  39. package/src/client/components/modals/LogsModal.tsx +301 -0
  40. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  41. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
  42. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
  43. package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
  44. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
  45. package/src/client/components/ui/Toggle.tsx +28 -0
  46. package/src/client/components/ui/icons.tsx +343 -0
  47. package/src/client/components/views/CustomInstallView.tsx +250 -0
  48. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  49. package/src/client/components/views/InstalledView.tsx +328 -0
  50. package/src/client/components/views/LogsView.tsx +150 -0
  51. package/src/client/components/views/MarketView.tsx +66 -0
  52. package/src/client/components/views/SettingsView.tsx +273 -0
  53. package/src/client/data/catalog.ts +38 -0
  54. package/src/client/data/host.ts +107 -0
  55. package/src/client/data/hub.ts +41 -0
  56. package/src/client/hooks/useCatalog.ts +71 -115
  57. package/src/client/hooks/useIncrementalList.ts +46 -0
  58. package/src/client/hooks/useLanguage.ts +40 -0
  59. package/src/client/hooks/useSettings.ts +90 -0
  60. package/src/client/hooks/useTaskQueue.ts +156 -39
  61. package/src/client/locales.ts +358 -38
  62. package/src/client/logic/constants.ts +86 -0
  63. package/src/client/{lib → logic}/failures.ts +95 -74
  64. package/src/client/logic/install-command.ts +56 -0
  65. package/src/client/logic/installed.ts +207 -0
  66. package/src/client/logic/normalize.ts +52 -0
  67. package/src/client/logic/urls.ts +125 -0
  68. package/src/client/styles/CustomInstallView.module.css +191 -0
  69. package/src/client/styles/DiagnosticsView.module.css +231 -0
  70. package/src/client/styles/Dropdown.module.css +42 -41
  71. package/src/client/styles/Header.module.css +100 -132
  72. package/src/client/styles/InstalledView.module.css +447 -0
  73. package/src/client/styles/List.module.css +6 -0
  74. package/src/client/styles/LogsView.module.css +503 -0
  75. package/src/client/styles/Modal.module.css +352 -70
  76. package/src/client/styles/SectionTabs.module.css +142 -0
  77. package/src/client/styles/SettingsView.module.css +232 -0
  78. package/src/client/styles/Toggle.module.css +60 -0
  79. package/src/client/styles/tokens.module.css +1 -0
  80. package/src/client/types.ts +23 -3
  81. package/src/server/http/routes.ts +414 -22
  82. package/src/server/index.ts +1 -1
  83. package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
  84. package/src/server/services/{install.ts → install/install.ts} +5 -5
  85. package/src/server/services/install/npm-check.ts +53 -0
  86. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
  87. package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
  88. package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
  89. package/src/server/services/loader.ts +55 -1
  90. package/src/server/services/log.ts +167 -0
  91. package/src/server/services/probe.ts +157 -0
  92. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
  93. package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
  94. package/src/server/services/settings.ts +79 -0
  95. package/lib/services/install.js +0 -4
  96. package/src/client/components/CatalogControls.tsx +0 -108
  97. package/src/client/components/icons.tsx +0 -129
  98. package/src/client/lib/catalog.ts +0 -293
  99. package/src/client/lib/env.ts +0 -20
  100. /package/lib/services/{install-types.js → install/install-types.js} +0 -0
  101. /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
  102. /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
  103. /package/lib/services/{progress.js → profile/progress.js} +0 -0
  104. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  105. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
  106. /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
  107. /package/src/client/{lib → logic}/format.ts +0 -0
  108. /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
  109. /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
  110. /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
@@ -11,7 +11,12 @@
11
11
  */
12
12
  import { useEffect, useRef, useState } from 'react'
13
13
  import type { HubPlugin, Translate } from '../types.ts'
14
- import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../lib/catalog.ts'
14
+ import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
+ import type { InstalledItem } from '../logic/installed.ts'
16
+
17
+ /** 安装/卸载请求超时(ms):服务端 preflight + npm 反查可能耗时(各自都有超时,合计约 20~30s),
18
+ * 给足余量;超时主动中止,避免服务端 preflight 网络挂起时前端永久卡「安装中 0%」。 */
19
+ const REQUEST_TIMEOUT_MS = 60_000
15
20
 
16
21
  /** 客户端任务队列项:镜像服务端 active 任务(running 在前、pending 在后)。 */
17
22
  export interface QueueTask {
@@ -19,7 +24,7 @@ export interface QueueTask {
19
24
  kind: 'install' | 'uninstall'
20
25
  /** 展示目标:owner/repo(安装)或 npm 包名(卸载) */
21
26
  target: string
22
- /** 插件中文简介(入队时从插件数据带过来,轮询合并时保留):展示在仓库名下方,让人知道排队的到底是什么 */
27
+ /** 插件中文简介(入队时从插件数据带过来,轮询合并时保留):展示在仓库名下方,标识排队中的插件 */
23
28
  desc?: string
24
29
  /** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
25
30
  repo: string | null
@@ -65,7 +70,14 @@ export interface TaskQueueOptions {
65
70
  onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[]) => void
66
71
  /** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
67
72
  installPlugin: HubPlugin | null
73
+ /** 当前打开的自定义安装(命令行)确认弹窗目标:与应用商店同一弹窗机制,匹配进行中的 custom 安装任务。 */
74
+ installCustomTarget?: string | null
75
+ /** 当前打开的全局 npm 安装(`npm install -g <pkgs>`)确认弹窗身份(包列表空格拼接):
76
+ * 匹配服务端登记的全局安装任务(target = 包列表 join(' '))。 */
77
+ installGlobalTarget?: string | null
68
78
  uninstallPlugin: HubPlugin | null
79
+ /** 已安装视图发起的卸载目标(npm 包名):目录插件走 uninstallPlugin,自定义安装走包名直卸。 */
80
+ uninstallName: string | null
69
81
  installedName: (p: HubPlugin) => string | null
70
82
  /** 待重启项展示信息补齐:按 owner/repo 从插件目录解析简介/版本;找不到返回 null。 */
71
83
  resolvePending: (repo: string) => { desc?: string; version?: string } | null
@@ -74,7 +86,7 @@ export interface TaskQueueOptions {
74
86
  export function useTaskQueue(opts: TaskQueueOptions) {
75
87
  const {
76
88
  t, refreshInstalled, onInstallDone, onUninstallDone, onError,
77
- installPlugin, uninstallPlugin, installedName, resolvePending,
89
+ installPlugin, installCustomTarget, installGlobalTarget, uninstallPlugin, uninstallName, installedName, resolvePending,
78
90
  } = opts
79
91
  const [queue, setQueue] = useState<QueueTask[]>([])
80
92
  const queueRef = useRef<QueueTask[]>([])
@@ -374,46 +386,67 @@ export function useTaskQueue(opts: TaskQueueOptions) {
374
386
  }, [])
375
387
 
376
388
  /** 弹窗动作:直接安装。请求宿主本地路由,任务进入服务端队列(FIFO),弹窗内实时显示进度。 */
377
- const installNow = async (p: HubPlugin, opts?: { update?: boolean }) => {
378
- const repo = p.source?.repo ?? ''
379
- if (!repo) return
380
- // 安装通道决策(用户无感知):有 npm 包名 → 走 npm;否则 git 直装。
381
- // 队列条目一律用仓库名展示/防重,只有发给后端的实际安装目标随通道变化。
382
- const { target } = installTargetOf(p)
383
- if (!target) return
389
+ /** 安装入队核心:目录插件(catalog,后端走目录白名单)与命令行安装(custom,
390
+ * 受安全开关控制)共用。队列条目一律用仓库名(repo)展示/防重,只有发给后端的
391
+ * 实际安装目标(target)随通道变化(npm 包名 / github: 源)。 */
392
+ const submitInstall = async (input: {
393
+ /** 发给后端的实际安装目标:npm 包名或 github: 源(服务端再决定最终通道) */
394
+ target: string
395
+ /** 队列展示与防重用的仓库名 */
396
+ repo: string
397
+ desc?: string
398
+ version?: string
399
+ updatedAt?: string
400
+ command: string
401
+ source: 'catalog' | 'custom'
402
+ /** 更新已安装目标(放行 add 覆盖重装) */
403
+ update?: boolean
404
+ /** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
405
+ globalNpm?: string[]
406
+ }) => {
384
407
  // 防重复入队:同一目标已在排队/执行中则忽略
385
- if (queueRef.current.some((q) => q.kind === 'install' && q.target === repo)) return
408
+ if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
386
409
  // 请求在途防重:任务要等响应回来才进本地队列,这之前再次点击(双击)直接忽略
387
- if (submittingRef.current.has(repo)) return
388
- submittingRef.current.add(repo)
410
+ if (submittingRef.current.has(input.repo)) return
411
+ submittingRef.current.add(input.repo)
389
412
  // 乐观入队:点击安装立即显示「正在安装中」,不等 POST 响应。
390
413
  // 服务端 /install 会先跑预检(preflight,可能耗时数秒)再入队,若等响应才入队列,
391
- // 用户点完安装立刻去通知中心会看不到任务 —— 只有刷新后 restore 才拉回来。
414
+ // 用户点完安装立刻去看任务会看不到 —— 只有刷新后 restore 才拉回来。
392
415
  // 用负临时 id 占位,响应回来换成真实 task id;失败/出错则移除该占位。
393
416
  tempIdRef.current -= 1
394
417
  const tempId = tempIdRef.current
395
418
  modalTaskRef.current = tempId
396
419
  applyQueue((prev) => [...prev, {
397
- id: tempId, kind: 'install', target: repo, repo, desc: p.description,
398
- version: p.version, updatedAt: p.dates?.repoUpdatedAt,
420
+ id: tempId, kind: 'install', target: input.repo, repo: input.repo, desc: input.desc,
421
+ version: input.version, updatedAt: input.updatedAt,
399
422
  status: 'running', progress: 0, lines: [], optimistic: true,
400
- command: installCommandOf(p, true),
423
+ command: input.command,
401
424
  // 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
402
425
  attempts: [], needsRestart: true,
403
426
  }])
404
427
  setSubmitting(true)
428
+ const controller = new AbortController()
429
+ const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
405
430
  try {
406
431
  const res = await fetch('/dsh-plugin-hub/install', {
407
432
  method: 'POST',
408
433
  headers: { 'content-type': 'application/json' },
409
- body: JSON.stringify({ repo: target, display: repo, mode: opts?.update ? 'update' : undefined }),
434
+ body: JSON.stringify({
435
+ repo: input.target,
436
+ display: input.repo,
437
+ source: input.source,
438
+ mode: input.update ? 'update' : undefined,
439
+ ...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
440
+ }),
441
+ signal: controller.signal,
410
442
  })
411
443
  const data = await res.json() as { ok?: boolean; task?: number; error?: string; attempts?: string[] }
412
444
  if (!data.ok || typeof data.task !== 'number') {
413
- // 请求层失败(重复安装 409 / 参数错误等):移除乐观条目 + 完整错误弹窗
445
+ // 请求层失败(重复安装 409 / 参数错误 / 安全开关 403 等):移除乐观条目 + 完整错误弹窗
414
446
  applyQueue((prev) => prev.filter((x) => x.id !== tempId))
415
- // 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示
416
- onError(data.error ?? `HTTP ${res.status}`, repo, 'install', installCommandOf(p, true), data.attempts)
447
+ // 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
448
+ // 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白
449
+ onError(data.error || `HTTP ${res.status}`, input.repo, 'install', input.command, data.attempts)
417
450
  return
418
451
  }
419
452
  const taskId = data.task
@@ -423,53 +456,129 @@ export function useTaskQueue(opts: TaskQueueOptions) {
423
456
  ? { ...x, id: taskId, optimistic: undefined }
424
457
  : x)))
425
458
  // 提前缓存展示信息:安装成功后服务端登记待重启,轮询合并时不用等目录解析
426
- pendingInfoRef.current.set(repo, { desc: p.description, version: p.version })
459
+ pendingInfoRef.current.set(input.repo, { desc: input.desc, version: input.version })
427
460
  // 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
428
461
  pollQueue()
429
- } catch {
430
- // 网络异常:移除乐观条目 + 兜底错误弹窗
462
+ } catch (err) {
463
+ // 网络异常:移除乐观条目 + 兜底错误弹窗(附上底层原因,避免只剩一句笼统的「安装失败」)
431
464
  applyQueue((prev) => prev.filter((x) => x.id !== tempId))
432
- onError(t('installFail'), repo, 'install', installCommandOf(p, true))
465
+ const aborted = (err as { name?: string } | null)?.name === 'AbortError'
466
+ const reason = aborted
467
+ ? t('requestTimeout')
468
+ : err instanceof Error && err.message ? err.message : String(err ?? '')
469
+ onError(reason ? `${t('installFail')} — ${reason}` : t('installFail'), input.repo, 'install', input.command)
433
470
  } finally {
434
- submittingRef.current.delete(repo)
471
+ window.clearTimeout(timeout)
472
+ submittingRef.current.delete(input.repo)
435
473
  setSubmitting(false)
436
474
  }
437
475
  }
438
476
 
439
- /** 弹窗动作:直接卸载。与安装同一队列机制,弹窗内实时显示进度。 */
440
- const uninstallNow = async (p: HubPlugin) => {
441
- const name = installedName(p)
477
+ /** 目录插件安装:通道由目录数据(npmPackage)决定,走 catalog 白名单。 */
478
+ const installNow = async (p: HubPlugin, opts?: { update?: boolean }) => {
479
+ const repo = p.source?.repo ?? ''
480
+ if (!repo) return
481
+ // 安装通道决策(用户无感知):有 npm 包名 → 走 npm;否则 git 直装。
482
+ const { target } = installTargetOf(p)
483
+ if (!target) return
484
+ await submitInstall({
485
+ target,
486
+ repo,
487
+ desc: p.description,
488
+ version: p.version,
489
+ updatedAt: p.dates?.repoUpdatedAt,
490
+ command: installCommandOf(p, true),
491
+ source: 'catalog',
492
+ update: opts?.update,
493
+ })
494
+ }
495
+
496
+ /** 命令行安装:用户手输 npm 包名或 GitHub 地址,走 custom 源(受安全开关控制)。
497
+ * update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
498
+ * 队列展示/防重一律用归一化身份(owner/repo 或 npm 包名):与服务端 /active 合并、
499
+ * 弹窗目标匹配同口径,npm 包名透传不受影响。 */
500
+ const installCustom = async (raw: string, opts?: { update?: boolean }) => {
501
+ const target = raw.trim()
502
+ if (!target) return
503
+ const repo = repoFromInstallTarget(target)
504
+ await submitInstall({
505
+ target,
506
+ repo,
507
+ command: `pnpm add ${target}`,
508
+ source: 'custom',
509
+ update: opts?.update,
510
+ })
511
+ }
512
+
513
+ /** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
514
+ * 队列展示/防重用包列表空格拼接的身份(与服务端 target 同口径)。 */
515
+ const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean }) => {
516
+ const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
517
+ if (list.length === 0) return
518
+ const identity = list.join(' ')
519
+ await submitInstall({
520
+ target: identity,
521
+ repo: identity,
522
+ command: `npm install -g ${identity}`,
523
+ source: 'custom',
524
+ globalNpm: list,
525
+ update: opts?.update,
526
+ })
527
+ }
528
+
529
+ /** 卸载入队核心:按 npm 包名直卸(目录插件与自定义安装共用,弹窗进度匹配同源)。 */
530
+ const enqueueUninstall = async (name: string, repo: string | null, desc?: string) => {
442
531
  if (!name) return
443
- const repo = p.source?.repo ?? null
444
532
  if (queueRef.current.some((q) => q.kind === 'uninstall' && q.target === name)) return
445
533
  // 请求在途防重:与安装一致,响应回来前再次点击直接忽略
446
534
  if (submittingRef.current.has(name)) return
447
535
  submittingRef.current.add(name)
448
536
  setSubmitting(true)
537
+ const controller = new AbortController()
538
+ const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
449
539
  try {
450
540
  const res = await fetch('/dsh-plugin-hub/uninstall', {
451
541
  method: 'POST',
452
542
  headers: { 'content-type': 'application/json' },
453
543
  body: JSON.stringify({ name, repo: repo ?? undefined }),
544
+ signal: controller.signal,
454
545
  })
455
546
  const data = await res.json() as { ok?: boolean; task?: number; error?: string }
456
547
  if (!data.ok || typeof data.task !== 'number') {
457
- onError(data.error ?? `HTTP ${res.status}`, repo, 'uninstall')
548
+ onError(data.error || `HTTP ${res.status}`, repo, 'uninstall')
458
549
  return
459
550
  }
460
551
  const taskId = data.task
461
552
  modalTaskRef.current = taskId
462
- applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc: p.description, repo, status: 'pending', progress: 0, lines: [], needsRestart: true }])
553
+ applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc, repo, status: 'pending', progress: 0, lines: [], needsRestart: true }])
463
554
  // 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
464
555
  pollQueue()
465
- } catch {
466
- onError(t('uninstallFail'), repo, 'uninstall')
556
+ } catch (err) {
557
+ // 网络异常:附上底层原因,避免只剩一句笼统的「卸载失败」
558
+ const aborted = (err as { name?: string } | null)?.name === 'AbortError'
559
+ const reason = aborted
560
+ ? t('requestTimeout')
561
+ : err instanceof Error && err.message ? err.message : String(err ?? '')
562
+ onError(reason ? `${t('uninstallFail')} — ${reason}` : t('uninstallFail'), repo, 'uninstall')
467
563
  } finally {
564
+ window.clearTimeout(timeout)
468
565
  submittingRef.current.delete(name)
469
566
  setSubmitting(false)
470
567
  }
471
568
  }
472
569
 
570
+ /** 弹窗动作:直接卸载(目录插件入口)。与安装同一队列机制,弹窗内实时显示进度。 */
571
+ const uninstallNow = async (p: HubPlugin) => {
572
+ const name = installedName(p)
573
+ if (!name) return
574
+ await enqueueUninstall(name, p.source?.repo ?? null, p.description)
575
+ }
576
+
577
+ /** 已安装视图动作:按已安装项卸载(覆盖目录外自定义安装,无目录数据也能卸)。 */
578
+ const uninstallItem = async (item: InstalledItem) => {
579
+ await enqueueUninstall(item.name, item.repo, item.plugin?.description)
580
+ }
581
+
473
582
  /** 取消任务:排队中立即出队,执行中终止子进程;先标记「正在取消」短暂过渡后再移除。 */
474
583
  const cancelTask = async (id: number) => {
475
584
  if (modalTaskRef.current === id) modalTaskRef.current = null
@@ -493,15 +602,20 @@ export function useTaskQueue(opts: TaskQueueOptions) {
493
602
  const clearModalTask = () => { modalTaskRef.current = null }
494
603
 
495
604
  // 弹窗对应任务:优先当前弹窗发起任务,其次按目标匹配(重新打开弹窗 / 目标已在队列时也能展示实时进度)
605
+ // 目录插件按 source.repo 匹配;命令行安装(custom)按归一化后的仓库身份/包名匹配;
606
+ // 全局 npm 安装按包列表身份(空格拼接)匹配;
496
607
  // 排除 cancelling:取消中的任务不再关联到弹窗,弹窗回到「确认」态可重新操作
497
- const installModalTask = installPlugin
498
- ? (queue.find((q) => q.id === modalTaskRef.current && q.status !== 'cancelling')
499
- ?? queue.find((q) => q.kind === 'install' && q.target === (installPlugin.source?.repo ?? '') && q.status !== 'cancelling')
608
+ const installModalTarget = installPlugin?.source?.repo
609
+ ?? (installCustomTarget ? repoFromInstallTarget(installCustomTarget) : null)
610
+ ?? installGlobalTarget ?? null
611
+ const installModalTask = (installPlugin || installCustomTarget || installGlobalTarget)
612
+ ? (queue.find((q) => q.id === modalTaskRef.current && q.kind === 'install' && q.status !== 'cancelling')
613
+ ?? queue.find((q) => q.kind === 'install' && q.target === installModalTarget && q.status !== 'cancelling')
500
614
  ?? null)
501
615
  : null
502
- const uninstallModalTask = uninstallPlugin
616
+ const uninstallModalTask = (uninstallPlugin || uninstallName)
503
617
  ? (queue.find((q) => q.id === modalTaskRef.current && q.status !== 'cancelling')
504
- ?? queue.find((q) => q.kind === 'uninstall' && q.target === (installedName(uninstallPlugin) ?? '') && q.status !== 'cancelling')
618
+ ?? queue.find((q) => q.kind === 'uninstall' && q.target === (uninstallName ?? installedName(uninstallPlugin!) ?? '') && q.status !== 'cancelling')
505
619
  ?? null)
506
620
  : null
507
621
 
@@ -512,7 +626,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
512
626
  uninstallModalTask,
513
627
  submitting,
514
628
  installNow,
629
+ installCustom,
630
+ installGlobalNpm,
515
631
  uninstallNow,
632
+ uninstallItem,
516
633
  cancelTask,
517
634
  clearModalTask,
518
635
  }