dsh-plugin 1.6.11 → 2.0.0

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 (128) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/client/client.js +705 -803
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +2 -3
  6. package/lib/http/diagnostics.js +6 -7
  7. package/lib/http/install.js +47 -213
  8. package/lib/http/logs.js +1 -2
  9. package/lib/http/npm-release-age.js +42 -0
  10. package/lib/http/routes.js +8 -7
  11. package/lib/http/settings.js +2 -2
  12. package/lib/http/shared.js +2 -2
  13. package/lib/http/task.js +1 -2
  14. package/lib/index.js +5 -6
  15. package/lib/services/install/install-types.js +4 -4
  16. package/lib/services/install/install.js +1 -1
  17. package/lib/services/install/npm-check.js +3 -2
  18. package/lib/services/install/npm-resolve.js +9 -96
  19. package/lib/services/install/package-entry.js +6 -11
  20. package/lib/services/install/registry-check.js +5 -16
  21. package/lib/services/install/release-target.js +8 -9
  22. package/lib/services/install/task-queue.js +74 -57
  23. package/lib/services/npm-release-age.js +119 -0
  24. package/lib/services/probe.js +4 -4
  25. package/lib/services/profile/installed-versions.js +15 -21
  26. package/lib/services/profile/profile.js +16 -44
  27. package/lib/services/profile/progress.js +8 -11
  28. package/lib/services/settings.js +2 -5
  29. package/lib/types/http/catalog.d.ts +1 -1
  30. package/lib/types/http/install.d.ts +2 -2
  31. package/lib/types/http/npm-release-age.d.ts +2 -0
  32. package/lib/types/http/routes.d.ts +10 -12
  33. package/lib/types/http/shared.d.ts +5 -6
  34. package/lib/types/index.d.ts +7 -8
  35. package/lib/types/services/install/install-types.d.ts +12 -13
  36. package/lib/types/services/install/install.d.ts +1 -1
  37. package/lib/types/services/install/npm-resolve.d.ts +4 -16
  38. package/lib/types/services/install/package-entry.d.ts +6 -11
  39. package/lib/types/services/install/release-target.d.ts +7 -8
  40. package/lib/types/services/install/task-queue.d.ts +32 -31
  41. package/lib/types/services/npm-release-age.d.ts +15 -0
  42. package/lib/types/services/probe.d.ts +4 -4
  43. package/lib/types/services/profile/installed-versions.d.ts +5 -7
  44. package/lib/types/services/profile/profile.d.ts +14 -18
  45. package/lib/types/services/profile/progress.d.ts +7 -10
  46. package/lib/types/services/settings.d.ts +0 -4
  47. package/package.json +1 -1
  48. package/src/client/components/PluginHubSection.tsx +23 -27
  49. package/src/client/components/catalog/CatalogList.tsx +2 -4
  50. package/src/client/components/catalog/PluginCard.tsx +2 -3
  51. package/src/client/components/layout/CatalogControls.tsx +5 -6
  52. package/src/client/components/layout/CatalogHeader.tsx +1 -2
  53. package/src/client/components/layout/CategoryTabs.tsx +2 -3
  54. package/src/client/components/layout/SectionTabs.tsx +3 -5
  55. package/src/client/components/modals/ConfirmDialog.tsx +4 -6
  56. package/src/client/components/modals/InstalledDetailModal.tsx +3 -6
  57. package/src/client/components/modals/LogsModal.tsx +7 -12
  58. package/src/client/components/modals/NotificationsModal.tsx +52 -24
  59. package/src/client/components/modals/ProgressView.tsx +3 -4
  60. package/src/client/components/modals/modals.tsx +58 -30
  61. package/src/client/components/ui/Dropdown.tsx +4 -6
  62. package/src/client/components/ui/Toggle.tsx +2 -3
  63. package/src/client/components/ui/icons.tsx +1 -2
  64. package/src/client/components/views/CustomInstallView.tsx +20 -178
  65. package/src/client/components/views/DiagnosticsView.tsx +3 -6
  66. package/src/client/components/views/InstalledView.tsx +6 -11
  67. package/src/client/components/views/MarketView.tsx +2 -4
  68. package/src/client/components/views/SettingsView.tsx +9 -32
  69. package/src/client/css-modules.d.ts +1 -1
  70. package/src/client/data/catalog.ts +2 -3
  71. package/src/client/data/host.ts +26 -3
  72. package/src/client/data/hub.ts +2 -3
  73. package/src/client/hooks/useCatalog.ts +79 -24
  74. package/src/client/hooks/useIncrementalList.ts +3 -5
  75. package/src/client/hooks/useLanguage.ts +4 -6
  76. package/src/client/hooks/useSettings.ts +4 -11
  77. package/src/client/hooks/useTaskQueue.ts +36 -42
  78. package/src/client/index.tsx +2 -3
  79. package/src/client/locales.ts +96 -123
  80. package/src/client/logic/constants.ts +1 -2
  81. package/src/client/logic/ensure-plugin-css.ts +8 -12
  82. package/src/client/logic/failures.ts +41 -28
  83. package/src/client/logic/format.ts +1 -1
  84. package/src/client/logic/install-command.ts +14 -35
  85. package/src/client/logic/installed.ts +15 -10
  86. package/src/client/logic/normalize.ts +3 -3
  87. package/src/client/logic/release-age.ts +19 -0
  88. package/src/client/logic/urls.ts +55 -50
  89. package/src/client/styles/CustomInstallView.module.css +3 -30
  90. package/src/client/styles/DiagnosticsView.module.css +3 -5
  91. package/src/client/styles/Dropdown.module.css +2 -4
  92. package/src/client/styles/Header.module.css +6 -7
  93. package/src/client/styles/InstalledView.module.css +4 -7
  94. package/src/client/styles/List.module.css +4 -5
  95. package/src/client/styles/LogsView.module.css +2 -3
  96. package/src/client/styles/Modal.module.css +4 -5
  97. package/src/client/styles/SectionTabs.module.css +2 -4
  98. package/src/client/styles/SettingsView.module.css +3 -5
  99. package/src/client/styles/Toggle.module.css +2 -4
  100. package/src/client/styles/tokens.module.css +14 -18
  101. package/src/client/types.ts +11 -11
  102. package/src/server/http/catalog.ts +2 -3
  103. package/src/server/http/diagnostics.ts +6 -7
  104. package/src/server/http/install.ts +47 -208
  105. package/src/server/http/logs.ts +1 -2
  106. package/src/server/http/npm-release-age.ts +42 -0
  107. package/src/server/http/routes.ts +12 -12
  108. package/src/server/http/settings.ts +2 -2
  109. package/src/server/http/shared.ts +5 -6
  110. package/src/server/http/task.ts +1 -2
  111. package/src/server/index.ts +7 -8
  112. package/src/server/services/install/install-types.ts +12 -13
  113. package/src/server/services/install/install.ts +0 -1
  114. package/src/server/services/install/npm-check.ts +3 -2
  115. package/src/server/services/install/npm-resolve.ts +9 -99
  116. package/src/server/services/install/package-entry.ts +6 -11
  117. package/src/server/services/install/registry-check.ts +5 -16
  118. package/src/server/services/install/release-target.ts +8 -9
  119. package/src/server/services/install/task-queue.ts +79 -63
  120. package/src/server/services/npm-release-age.ts +107 -0
  121. package/src/server/services/probe.ts +4 -4
  122. package/src/server/services/profile/installed-versions.ts +16 -22
  123. package/src/server/services/profile/profile.ts +16 -42
  124. package/src/server/services/profile/progress.ts +8 -11
  125. package/src/server/services/settings.ts +2 -9
  126. package/lib/services/install/preflight.js +0 -178
  127. package/lib/types/services/install/preflight.d.ts +0 -23
  128. package/src/server/services/install/preflight.ts +0 -184
@@ -3,11 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Primary navigation: a compact segmented control switching between the
7
- * four top-level sections — Market (browse), Installed (manage), Custom
8
- * (manual command installs) and Settings (network sources & preferences).
9
- * Kept visually distinct from the category chips so the hierarchy reads:
10
- * section → category → plugin.
6
+ * 一级导航:紧凑分段控件,在四个顶层板块间切换 —— 市场(浏览)、已安装(管理)、
7
+ * 自定义(手动命令安装)与设置(网络源与偏好)。与分类 chip 视觉上区分开,
8
+ * 让层级清晰:板块 → 分类 → 插件。
11
9
  */
12
10
  import { createElement as h } from 'react'
13
11
  import styles from '../../styles/SectionTabs.module.css'
@@ -3,13 +3,11 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Generic confirm dialog — the single entry point for every "are you sure?"
7
- * question in the app. Rule: any question must be a modal dialog, never an
8
- * in-button double-click.
6
+ * 通用确认弹窗 —— 应用里所有「确定吗?」问题的唯一入口。规则:任何询问都必须是
7
+ * 弹窗,绝不用按钮内双击。
9
8
  *
10
- * The icon in the middle tells the type at a glance: a blue question mark
11
- * for routine confirmations (install / update / restart / ignore), a red
12
- * action glyph for destructive ones (clear / delete / uninstall / reset).
9
+ * 中间的图标一眼说明类型:常规确认(安装/更新/重启/忽略)用蓝色问号,
10
+ * 危险操作(清空/删除/卸载/重置)用红色动作图形。
13
11
  */
14
12
  import { createElement as h } from 'react'
15
13
  import type { MouseEvent } from 'react'
@@ -3,12 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Installed-plugin detail dialog: opens from the Installed view (row click
7
- * or the detail button) and shows everything we know about an installed
8
- * entry — the catalog metadata (category, catalog entry, stars/forks,
9
- * latest version) merged with the host runtime facts (install path, installed
10
- * version, install time, last update). Update / uninstall actions are
11
- * offered from here too, mirroring the row actions.
6
+ * 已安装插件详情弹窗:从「已安装」视图打开(行点击或详情按钮),展示我们对某个已安装项
7
+ * 掌握的全部信息 —— 目录元数据(分类、收录入口、Star/Fork、最新版本)与宿主运行时事实
8
+ * (安装路径、已装版本、安装时间、最后更新)合并呈现。此处的更新/卸载动作与行内动作一致。
12
9
  */
13
10
  import { createElement as h } from 'react'
14
11
  import type { MouseEvent } from 'react'
@@ -3,20 +3,15 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Built-in log viewer dialog: no external software needed — the app opens
7
- * its own window and loads the local system log (JSONL at
8
- * ~/.dsh/profiles/<profile>/hub.log) straight from the server, one page at
9
- * a time.
6
+ * 内置日志查看器弹窗:无需外部软件 —— 应用自己开窗,从服务端直接分页加载本地系统日志
7
+ * (JSONL,位于 ~/.dsh/profiles/<profile>/hub.log)。
10
8
  *
11
- * Every line is categorized twice: a function area (install / uninstall /
12
- * update / diagnostics / settings / system) and a severity level (debug /
13
- * info / success / warn / error). Filtering on both axes lets you target,
14
- * say, only install errors — so the exact incompatible step of an install
15
- * (or uninstall) is easy to spot. Search narrows by event code or message.
9
+ * 每行双重归类:功能域(安装/卸载/更新/诊断/设置/系统)与严重级别(debug/info/success/
10
+ * warn/error)。两轴筛选可只盯某类,比如只看安装错误 —— 便于定位安装(或卸载)中真正
11
+ * 卡住的那一步;搜索按事件码或消息缩小范围。
16
12
  *
17
- * Server pagination: GET /dsh-plugin-hub/logs?offset=&limit=&category=
18
- * &level=&query= → { entries, total, hasMore, path }. The footer shows the
19
- * real file location and reveals it in the system file manager.
13
+ * 服务端分页:GET /dsh-plugin-hub/logs?offset=&limit=&category=&level=&query=
14
+ * → { entries, total, hasMore, path }。底部展示日志真实路径并可在系统文件管理器中定位。
20
15
  */
21
16
  import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
22
17
  import type { MouseEvent } from 'react'
@@ -3,22 +3,18 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Notification-center dialog: a persistent log of every settled install /
7
- * remove task — successes and failures alike.
6
+ * 通知中心弹窗:每个已结算的安装/卸载任务(成功与失败)的持久化清单。
8
7
  *
9
- * Records are written to localStorage at settle time (see logic/failures.ts),
10
- * so a result is never lost even when the dialog was dismissed or the user
11
- * was away. Each entry carries a circular status badge (green check for
12
- * success, red cross for failure) with white glyph and message text;
13
- * failures keep their copy / fix / file-an-issue actions. Opened from the
14
- * header entry button (after the Settings tab).
8
+ * 记录在结算时写入 localStorage(见 logic/failures.ts),即使弹窗被关闭或用户离开,
9
+ * 结果也不会丢失。每条带圆形状态徽标(成功绿勾、失败红叉,白色图形)与消息文本;
10
+ * 失败项保留复制/修复/提 Issue 动作。由头部入口按钮(「设置」tab 之后)打开。
15
11
  */
16
12
  import { createElement as h, useState } from 'react'
17
13
  import type { MouseEvent } from 'react'
18
14
  import styles from '../../styles/Modal.module.css'
19
15
  import type { EnvInfo, Translate } from '../../types.ts'
20
16
  import type { NotificationRecord } from '../../logic/failures.ts'
21
- import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
17
+ import { actionOfRecord, classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
22
18
  import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
23
19
  import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
24
20
  import { CloseIcon } from '../ui/icons.tsx'
@@ -255,6 +251,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
255
251
  // 卡片不展示错误日志预览,完整日志靠「复制完整日志」按钮带走,避免每条卡片被日志撑高
256
252
  !r.ok && (() => {
257
253
  const kind = classifyFailure(r.message)
254
+ // 动作词:该条提示与上报正文按真实动作取词
255
+ const action = actionOfRecord(r)
256
+ const actionWord = t(action === 'uninstall' ? 'actionUninstall' : action === 'update' ? 'actionUpdate' : 'actionInstall')
258
257
  if (kind === 'npmTooOld') {
259
258
  // npm 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题 ——
260
259
  // 直接给升级指引,不引导提 Issue
@@ -324,7 +323,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
324
323
  h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
325
324
  r.repo && shownRepo ? h('a', {
326
325
  className: styles.failBigIssue,
327
- href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
326
+ href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
328
327
  target: '_blank',
329
328
  rel: 'noopener noreferrer',
330
329
  title: t('failIssueHint'),
@@ -333,7 +332,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
333
332
  }
334
333
  if (kind === 'registryVersionStale') {
335
334
  // 要装的版本在「实际执行安装的那个源」里查不到(ERR_PNPM_NO_MATCHING_VERSION / ETARGET):
336
- // 常见于镜像滞后 —— 反查包名走设置里的 npmRegistry,实际安装沿用本机 ~/.npmrc 的源;
335
+ // 常见于镜像滞后 —— 实际安装沿用本机 ~/.npmrc 的源;
337
336
  // 也可能只是版本号写错。与插件作者无关 —— 给对齐源 / 显式走官方源 / 离线包指引,不引导提 Issue
338
337
  return h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
339
338
  }
@@ -364,7 +363,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
364
363
  return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
365
364
  }
366
365
  if (kind === 'network') {
367
- // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
366
+ // 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
368
367
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
369
368
  // 不引导提 Issue
370
369
  return h('div', null, [
@@ -373,7 +372,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
373
372
  // registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
374
373
  // 否则用户外网是通的会困惑「为什么连不上」
375
374
  (() => { const host = registryHostOf(r.message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
376
- h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
375
+ h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
377
376
  onRunDiagnostics ? h('button', {
378
377
  className: styles.failDiagBtn,
379
378
  onClick: onRunDiagnostics,
@@ -381,24 +380,24 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
381
380
  ])
382
381
  }
383
382
  if (kind === 'pluginPrepare') {
384
- // [packaging](预检/装后校验拦截:git 分发缺产物)与
383
+ // [packaging](装后校验拦截:分发缺产物)与
385
384
  // prepare 构建脚本实际执行失败:唯一确有证据的插件打包分发问题 —— 引导到插件仓库提 Issue
386
385
  return h('div', null, [
387
386
  h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
388
387
  r.repo && shownRepo ? h('a', {
389
388
  className: styles.failBigIssue,
390
- href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
389
+ href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
391
390
  target: '_blank',
392
391
  rel: 'noopener noreferrer',
393
392
  title: t('failIssueHint'),
394
393
  }, t('failIssueBig')) : null,
395
- // 次要入口:这条分支的判据([packaging] 预检/装后校验、prepare 摘要)是插件中心
394
+ // 次要入口:这条分支的判据([packaging] 装后校验、prepare 摘要)是插件中心
396
395
  // 自己产出的,不是 pnpm 原文 —— 判错时用户会被送去作者仓库、我们永远收不到反馈
397
396
  r.repo && shownRepo ? h('div', null, [
398
397
  h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
399
398
  h('a', {
400
399
  className: styles.failMisjudgeLink,
401
- href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge'),
400
+ href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge', action),
402
401
  target: '_blank',
403
402
  rel: 'noopener noreferrer',
404
403
  title: t('failHubIssueHint'),
@@ -409,7 +408,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
409
408
  if (kind === 'hubState') {
410
409
  // 插件中心的列表状态已过期(安装器自身守卫消息):非插件问题、也不是 bug,只给可行动提示,不引导提 Issue
411
410
  return h('div', null, [
412
- h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
411
+ h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
413
412
  ])
414
413
  }
415
414
  if (kind === 'timeout') {
@@ -421,17 +420,46 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
421
420
  if (kind === 'profileLock') {
422
421
  // DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):只给可行动提示,不引导提 Issue
423
422
  return h('div', null, [
424
- h('div', { className: styles.failPrepareHint }, t('failProfileLockHint')),
423
+ h('div', { className: styles.failPrepareHint }, t('failProfileLockHint', { action: actionWord })),
425
424
  ])
426
425
  }
427
- // pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
426
+ if (kind === 'hostCliSpawn') {
427
+ // 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
428
+ return h('div', null, [
429
+ h('div', { className: styles.failPrepareHint }, t('failHostCliSpawnHint')),
430
+ ])
431
+ }
432
+ if (kind === 'pnpmIgnoredBuild') {
433
+ // 插件的依赖需要在用户机器上编译原生模块,pnpm 默认拦住;我们已自动放行
434
+ // allowBuilds 并重试过一次,仍失败说明任何用户都装不上 —— 属作者侧(发预构建产物 / 走 release 包)
435
+ return h('div', null, [
436
+ h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild')),
437
+ r.repo && shownRepo ? h('a', {
438
+ className: styles.failBigIssue,
439
+ href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
440
+ target: '_blank',
441
+ rel: 'noopener noreferrer',
442
+ title: t('failIssueHint'),
443
+ }, t('failIssueBig')) : null,
444
+ // 这条判定里也有我们的动作(自动放行过 allowBuilds),判错时得留一条回到插件中心的路
445
+ r.repo && shownRepo ? h('div', null, [
446
+ h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
447
+ h('a', {
448
+ className: styles.failMisjudgeLink,
449
+ href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge', action),
450
+ target: '_blank',
451
+ rel: 'noopener noreferrer',
452
+ title: t('failHubIssueHint'),
453
+ }, t('failMisjudgeLink')),
454
+ ]) : null,
455
+ ])
456
+ }
457
+ // 未知失败:不归到插件作者,改为引导到插件中心仓库排查
428
458
  return h('div', null, [
429
- kind === 'pnpmIgnoredBuild'
430
- ? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
431
- : h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
459
+ h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
432
460
  h('a', {
433
461
  className: styles.failBigIssue,
434
- href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts),
462
+ href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
435
463
  target: '_blank',
436
464
  rel: 'noopener noreferrer',
437
465
  title: t('failHubIssueHint'),
@@ -3,10 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Inline progress block shared by the install and uninstall dialogs:
7
- * a real percentage bar (server-estimated, client-polled). No live terminal
8
- * window — pnpm emits nothing during git clone/build, so a log pane would be
9
- * empty for most of the run; failures surface their full output in ErrorModal.
6
+ * 安装与卸载弹窗共用的内联进度块:真实百分比进度条(服务端估算、客户端轮询)。
7
+ * 不做实时终端窗口 —— pnpm 在 git clone/构建期间几乎无输出,日志面板大部分时间是空的;
8
+ * 失败时完整输出在 ErrorModal 里展示。
10
9
  */
11
10
  import { createElement as h } from 'react'
12
11
  import styles from '../../styles/Modal.module.css'
@@ -3,11 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Dialog layer for the Plugin Hub: the install-confirm dialog, the uninstall
7
- * dialog and the global toast. Both dialogs lock themselves while a mutation
8
- * is running, then switch to a result view with a single action — "done" for
9
- * the common case (the page refreshes on close so the change applies), or a
10
- * single "restart now" for the rare changes a refresh cannot pick up.
6
+ * 插件中心的弹窗层:安装确认弹窗、卸载弹窗与全局 Toast。两个弹窗在变更执行期间
7
+ * 自我锁定,随后切到只有一个动作的结果视图 —— 常见情况给「完成」(关闭时刷新页面即生效),
8
+ * 刷新解决不了的少数情况才给单个「立即重启」。
11
9
  */
12
10
  import { createElement as h } from 'react'
13
11
  import type { MouseEvent } from 'react'
@@ -18,6 +16,7 @@ import { ProgressView } from './ProgressView.tsx'
18
16
  import { installCommandOf } from '../../logic/install-command.ts'
19
17
  import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
20
18
  import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
19
+ import type { FailureAction } from '../../logic/failures.ts'
21
20
 
22
21
  /** 完成结果视图:绿色对勾 + 标题/描述;
23
22
  * 终态只有两种:
@@ -336,14 +335,13 @@ export function UninstallModal(props: UninstallModalProps) {
336
335
  )
337
336
  }
338
337
 
339
- /** 预填插件仓库的 GitHub Issue 链接:标题带插件名,正文附完整错误信息,方便用户一键反馈。 */
340
- /** 安装/卸载失败弹窗:布局与失败记录一致(类型徽标 + 仓库超链接 + 隐蔽复制按钮),报错完整展示,底部一键提交 Issue。 */
341
- export function ErrorModal({ message, repo, kind, command, attempts, t, env, onCopy, onClose, onRunDiagnostics }: {
338
+ /** 安装/更新/卸载失败弹窗:标题、徽标、提示文案与 issue 正文都按传入的动作取词。 */
339
+ export function ErrorModal({ message, repo, action, command, attempts, t, env, onCopy, onClose, onRunDiagnostics }: {
342
340
  message: string
343
341
  repo: string | null
344
- kind: 'install' | 'uninstall'
342
+ action: FailureAction
345
343
  command?: string
346
- /** 尝试过的安装方式(npm 反查 + 实际执行命令):issue 预填一并贴给作者,便于反推正确的 npm 包名 */
344
+ /** 实际执行/重试过的安装命令:issue 预填一并贴给作者 */
347
345
  attempts?: string[]
348
346
  t: Translate
349
347
  env: EnvInfo | null
@@ -352,8 +350,10 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
352
350
  /** 网络不通时的「去系统诊断」直达按钮:跳到设置 → 系统诊断跑连通性检测(由宿主提供) */
353
351
  onRunDiagnostics?: () => void
354
352
  }) {
355
- // 失败归类:当前安装通道(npm/git)装不上 = 插件分发/依赖的问题,一律引导提 Issue
353
+ // 失败归类:装不上 = 插件分发/依赖的问题,一律引导提 Issue
356
354
  const failureKind = classifyFailure(message)
355
+ // 动作词:标题、徽标与提示里的 {action} 统一取这一个词
356
+ const actionWord = t(action === 'uninstall' ? 'actionUninstall' : action === 'update' ? 'actionUpdate' : 'actionInstall')
357
357
  // 服务端 [npm-too-low] 标记里带的本机 npm 版本(无标记为 null,提示文案降级为「怀疑」)
358
358
  const npmVersion = npmTooLowVersion(message)
359
359
  // 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
@@ -372,8 +372,9 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
372
372
  },
373
373
  h('div', { className: styles.errorModal, role: 'dialog', 'aria-modal': 'true' },
374
374
  h('div', { className: styles.modalHead },
375
- // 标题按操作类型明确区分:安装失败 / 卸载失败
376
- h('div', { className: styles.errorTitle }, kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall')),
375
+ // 标题按动作明确区分:安装失败 / 更新失败 / 卸载失败
376
+ h('div', { className: styles.errorTitle },
377
+ t(action === 'uninstall' ? 'errorTitleUninstall' : action === 'update' ? 'errorTitleUpdate' : 'errorTitleInstall')),
377
378
  h('button', {
378
379
  className: styles.modalClose,
379
380
  'aria-label': t('errorClose'),
@@ -385,8 +386,8 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
385
386
  h('div', { className: styles.failRow },
386
387
  h('div', { className: styles.failHead },
387
388
  h('span', {
388
- className: kind === 'install' ? styles.failKindInstall : styles.failKindUninstall,
389
- }, kind === 'install' ? t('install') : t('uninstall')),
389
+ className: action === 'install' ? styles.failKindInstall : styles.failKindUninstall,
390
+ }, actionWord),
390
391
  // 仓库地址(用户名/仓库)统一显示为可点击超链接,跳转到官网详情页(含插件收录信息),而非直接跳 GitHub
391
392
  repo ? h('a', {
392
393
  className: styles.failRepo,
@@ -458,7 +459,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
458
459
  h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
459
460
  repo ? h('a', {
460
461
  className: styles.failBigIssue,
461
- href: pluginIssueUrl(repo, message, env, command, attempts),
462
+ href: pluginIssueUrl(repo, message, env, command, attempts, action),
462
463
  target: '_blank',
463
464
  rel: 'noopener noreferrer',
464
465
  title: t('failIssueHint'),
@@ -466,7 +467,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
466
467
  ])
467
468
  : failureKind === 'registryVersionStale'
468
469
  // 要装的版本在「实际执行安装的那个源」里查不到(ERR_PNPM_NO_MATCHING_VERSION / ETARGET):
469
- // 常见于镜像滞后 —— 反查包名走设置里的 npmRegistry,实际安装沿用本机 ~/.npmrc 的源,
470
+ // 常见于镜像滞后 —— 实际安装沿用本机 ~/.npmrc 的源,
470
471
  // 官方源刚发布而镜像还没同步;也可能只是版本号写错。与插件作者无关 ——
471
472
  // 给「对齐源 / 那一次显式走官方源 / 用离线包」的指引,不引导提 Issue
472
473
  ? h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
@@ -489,7 +490,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
489
490
  // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
490
491
  ? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
491
492
  : failureKind === 'network'
492
- // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
493
+ // 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
493
494
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
494
495
  // 不引导提 Issue
495
496
  ? h('div', null, [
@@ -498,7 +499,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
498
499
  // registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
499
500
  // 否则用户外网是通的会困惑「为什么连不上」
500
501
  (() => { const host = registryHostOf(message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
501
- h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
502
+ h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
502
503
  onRunDiagnostics ? h('button', {
503
504
  className: styles.failDiagBtn,
504
505
  onClick: onRunDiagnostics,
@@ -506,24 +507,24 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
506
507
  ])
507
508
  : failureKind === 'pluginPrepare'
508
509
  ? h('div', null, [
509
- // [packaging](预检/装后校验拦截:git 分发缺产物)与
510
+ // [packaging](装后校验拦截:分发缺产物)与
510
511
  // prepare 构建脚本实际执行失败:唯一确有证据的插件打包分发问题 —— 引导到插件仓库提 Issue
511
512
  h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
512
513
  repo ? h('a', {
513
514
  className: styles.failBigIssue,
514
- href: pluginIssueUrl(repo, message, env, command, attempts),
515
+ href: pluginIssueUrl(repo, message, env, command, attempts, action),
515
516
  target: '_blank',
516
517
  rel: 'noopener noreferrer',
517
518
  title: t('failIssueHint'),
518
519
  }, t('failIssueBig')) : null,
519
- // 次要入口:这条分支的判据([packaging] 预检/装后校验、prepare 摘要)是插件中心
520
+ // 次要入口:这条分支的判据([packaging] 装后校验、prepare 摘要)是插件中心
520
521
  // 自己产出的,不是 pnpm 原文 —— 判错时用户会被送去作者仓库、我们永远收不到反馈,
521
522
  // 故另给一条「可能是市场误判」的反馈通道(正文自我说明它来自哪条分支)
522
523
  repo ? h('div', null, [
523
524
  h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
524
525
  h('a', {
525
526
  className: styles.failMisjudgeLink,
526
- href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge'),
527
+ href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge', action),
527
528
  target: '_blank',
528
529
  rel: 'noopener noreferrer',
529
530
  title: t('failHubIssueHint'),
@@ -533,7 +534,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
533
534
  : failureKind === 'hubState'
534
535
  ? h('div', null, [
535
536
  // 插件中心的列表状态已过期(安装器自身守卫消息):不是插件问题、也不是 bug,只给可行动提示,不引导提 Issue
536
- h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
537
+ h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
537
538
  ])
538
539
  : failureKind === 'timeout'
539
540
  ? h('div', null, [
@@ -543,16 +544,43 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
543
544
  : failureKind === 'profileLock'
544
545
  ? h('div', null, [
545
546
  // DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):给可行动提示,不引导提 Issue
546
- h('div', { className: styles.failPrepareHint }, t('failProfileLockHint')),
547
+ h('div', { className: styles.failPrepareHint }, t('failProfileLockHint', { action: actionWord })),
548
+ ])
549
+ : failureKind === 'hostCliSpawn'
550
+ ? h('div', null, [
551
+ // 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
552
+ h('div', { className: styles.failPrepareHint }, t('failHostCliSpawnHint')),
553
+ ])
554
+ : failureKind === 'pnpmIgnoredBuild'
555
+ // 该类型的插件依赖需要在用户机器上编译原生模块,pnpm 默认拦住;我们已自动放行
556
+ // allowBuilds 并重试过一次,仍失败说明任何用户都装不上 —— 属作者侧(发预构建产物 / 走 release 包)。
557
+ ? h('div', null, [
558
+ h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild')),
559
+ repo ? h('a', {
560
+ className: styles.failBigIssue,
561
+ href: pluginIssueUrl(repo, message, env, command, attempts, action),
562
+ target: '_blank',
563
+ rel: 'noopener noreferrer',
564
+ title: t('failIssueHint'),
565
+ }, t('failIssueBig')) : null,
566
+ // 这条判定里也有我们的动作(我们自动放行过 allowBuilds),判错时得留一条回到插件中心的路
567
+ repo ? h('div', null, [
568
+ h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
569
+ h('a', {
570
+ className: styles.failMisjudgeLink,
571
+ href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge', action),
572
+ target: '_blank',
573
+ rel: 'noopener noreferrer',
574
+ title: t('failHubIssueHint'),
575
+ }, t('failMisjudgeLink')),
576
+ ]) : null,
547
577
  ])
548
578
  : h('div', null, [
549
- // pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
550
- failureKind === 'pnpmIgnoredBuild'
551
- ? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
552
- : h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
579
+ // 未分类(hub):不归到插件作者,改为引导到插件中心仓库分诊
580
+ h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
553
581
  h('a', {
554
582
  className: styles.failBigIssue,
555
- href: hubIssueUrl(repo, message, env, command, attempts),
583
+ href: hubIssueUrl(repo, message, env, command, attempts, 'triage', action),
556
584
  target: '_blank',
557
585
  rel: 'noopener noreferrer',
558
586
  title: t('failHubIssueHint'),
@@ -3,13 +3,11 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Button-style dropdown (native <select> replacement).
6
+ * 按钮式下拉框(替代原生 <select>)。
7
7
  *
8
- * The trigger is a compact toolbar button — hairline border + subtle grey
9
- * fill + current value + chevron — so it sits naturally next to the other
10
- * toolbar buttons and takes a single slot of horizontal space. Clicking
11
- * outside or pressing Esc closes the panel; options get a hover fill and
12
- * the active one a brand highlight, all driven by the --hub-* tokens.
8
+ * 触发器是紧凑的工具栏按钮 —— 细边框 + 淡灰填充 + 当前值 + 箭头 —— 与其它工具栏按钮
9
+ * 并排自然,只占一格横向空间。点击外部或按 Esc 关闭面板;选项有 hover 填充、
10
+ * 激活项为品牌高亮,全部由 --hub-* 变量驱动。
13
11
  */
14
12
  import { createElement as h, useEffect, useRef, useState } from 'react'
15
13
  import type { JSX } from 'react'
@@ -3,9 +3,8 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Compact switch used by the settings rows. Native-button based so it is
7
- * keyboard focusable and announces state via aria-checked; styling driven
8
- * by the --hub-* tokens (on = brand fill, off = neutral track).
6
+ * 设置行用的紧凑开关。基于原生 button,可键盘聚焦并通过 aria-checked 播报状态;
7
+ * 样式由 --hub-* 变量驱动(开 = 品牌填充,关 = 中性轨道)。
9
8
  */
10
9
  import { createElement as h } from 'react'
11
10
  import styles from '../../styles/Toggle.module.css'
@@ -3,8 +3,7 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Inline SVG icons used across the client. Icons inherit currentColor so
7
- * they adapt to themes and disabled states automatically.
6
+ * 客户端各处使用的内联 SVG 图标。图标继承 currentColor,自动适配主题与禁用态。
8
7
  */
9
8
  import { createElement as h } from 'react'
10
9
  import headerStyles from '../../styles/Header.module.css'