dsh-plugin 2.3.1 → 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.
Files changed (84) hide show
  1. package/README.en.md +15 -21
  2. package/README.md +15 -21
  3. package/client/client.js +412 -382
  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 +21 -10
  8. package/lib/http/npm-release-age.js +9 -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 +46 -9
  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 +14 -2
  32. package/lib/types/services/probe.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/src/client/components/PluginHubSection.tsx +59 -28
  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 +13 -11
  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 +3 -1
  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 +31 -24
  53. package/src/client/locales.ts +21 -21
  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 +5 -3
  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 +3 -3
  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 +20 -10
  69. package/src/server/http/npm-release-age.ts +9 -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 +50 -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
@@ -11,10 +11,10 @@ import type { MouseEvent } from 'react'
11
11
  import styles from '../styles/Header.module.css'
12
12
  import modalStyles from '../styles/Modal.module.css'
13
13
  import type { EnvInfo, HubPlugin, InstallChannel, SectionProps, ToastState } from '../types.ts'
14
- import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
14
+ import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
15
  import { PLUGIN_VERSION } from '../logic/constants.ts'
16
16
  import { ensurePluginCss } from '../logic/ensure-plugin-css.ts'
17
- import { getEnv, revealInstallFolder } from '../data/host.ts'
17
+ import { fetchNpmReleaseAge, getEnv, revealInstallFolder } from '../data/host.ts'
18
18
  import { useCatalog } from '../hooks/useCatalog.ts'
19
19
  import { useLanguage } from '../hooks/useLanguage.ts'
20
20
  import { useSettings } from '../hooks/useSettings.ts'
@@ -76,9 +76,11 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
76
76
  const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
77
77
  /** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
78
78
  const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
79
- /** Hub 自我更新指定的精确版本:连续发版时提示的是「已过供应链门禁」的旧一版,
80
- * 安装必须钉在该版本上,否则又会被 @latest 带回门禁内的最新版。 */
79
+ /** 安装要锁定的精确版本:自我更新提示的版本、新安装的已过冷却期版本都要锁定,
80
+ * 否则 @latest 会被 pnpm 带回冷却期内的旧版。 */
81
81
  const [confirmPinVersion, setConfirmPinVersion] = useState<string | null>(null)
82
+ /** 冷却期查询的进行中序号:只采用最后一次弹窗的查询结果,避免切换插件后旧结果覆盖 */
83
+ const installPinSeq = useRef(0)
82
84
  /** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名)——
83
85
  * 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
84
86
  const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
@@ -104,12 +106,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
104
106
  /** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
105
107
  const [installDone, setInstallDone] = useState(false)
106
108
  const [uninstallDone, setUninstallDone] = useState(false)
107
- /** 结果视图是否给「立即重启」:服务端任务终态带出。仅刷新解决不了的兜底场景
108
- * (热挂载失败 / 非 dsh 插件 / 更新)为 true;其余(热挂载成功、卸载已即时停用)为 false,
109
+ /** 结果视图是否给「立即重启」:服务端任务终态带出。仅刷新解决不了的例外场景
110
+ * (热加载失败 / 非 dsh 插件 / 更新)为 true;其余(热加载成功、卸载已即时停用)为 false,
109
111
  * 结果视图只给「完成」,关闭时自动刷新页面生效 */
110
112
  const [installNeedsRestart, setInstallNeedsRestart] = useState(false)
111
113
  const [uninstallNeedsRestart, setUninstallNeedsRestart] = useState(false)
112
- /** 结果视图是否显示「关闭后自动刷新」说明:插件已热挂进运行中 loader(安装)、或卸载后
114
+ /** 结果视图是否显示「关闭后自动刷新」说明:插件已热加载进运行中 loader(安装)、或卸载后
113
115
  * 插件面板仍残留在当前页面(卸载)。关闭弹窗即整页刷新,不给手动刷新按钮。 */
114
116
  const [installNeedsReload, setInstallNeedsReload] = useState(false)
115
117
  /** 市场自更新成功、但 profile 的 bundle 列表里已不含 dsh-plugin:结果页提示重新勾选 */
@@ -133,7 +135,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
133
135
  return () => { alive = false }
134
136
  }, [])
135
137
 
136
- // 样式自愈:宿主重启/热更可能移除 <style> 标签而模块缓存未失效(factory 不重跑、CSS 不回来)。
138
+ // 样式自动恢复:宿主重启/热更可能移除 <style> 标签而模块缓存未失效(factory 不重跑、CSS 不回来)。
137
139
  // 挂载与每次切换视图时比对全局 CSS 清单,把缺失的样式补注入回去 —— 用户无需再手动点击恢复。
138
140
  useEffect(() => { ensurePluginCss() }, [view])
139
141
 
@@ -154,7 +156,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
154
156
  setInstallNeedsReload(reload)
155
157
  setInstallBundleDeselected(bundleDeselected === true)
156
158
  if (viaModal) setInstallDone(true)
157
- // 弹窗外的后台安装:给结果 toast;需要页面重载时(热挂载的客户端 UI)读完后自动刷新
159
+ // 弹窗外的后台安装:给结果 toast;需要页面重载时(热加载的客户端 UI)读完后自动刷新
158
160
  else setToast({
159
161
  id: Date.now(),
160
162
  kind: reload ? 'installedReload' : needsRestart ? 'done' : 'installedLive',
@@ -191,8 +193,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
191
193
  // 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
192
194
  // 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
193
195
  // 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
194
- // 若这里就丢掉原始值,通知里会连名字都看不到;action 区分「安装失败/更新失败」
195
- setNotifications(addFailure({ kind, action: update ? 'update' : 'install', repo: repo ?? '', message, command, attempts }))
196
+ // 若这里就丢掉原始值,通知里会连名字都看不到;action 按任务类型区分「安装/更新/卸载失败」
197
+ setNotifications(addFailure({ kind, action: update ? 'update' : kind === 'uninstall' ? 'uninstall' : 'install', repo: repo ?? '', message, command, attempts }))
196
198
  },
197
199
  installPlugin: confirmPlugin,
198
200
  installCustomTarget: confirmCustomTarget,
@@ -237,7 +239,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
237
239
  useEffect(() => {
238
240
  if (startupCheckedRef.current) return
239
241
  if (!settingsReady || catalog.plugins === null || catalog.failed) return
240
- // 供应链门禁结果未就绪时先等:否则刚发布的插件会先写进通知中心,冒出装不上的更新提醒
242
+ // 发布冷却期结果未就绪时先等:否则刚发布的插件会先写进通知中心,冒出装不上的更新提醒
241
243
  if (!catalog.releaseAgeReady) return
242
244
  startupCheckedRef.current = true
243
245
  if (!hubSettings.checkUpdatesOnStart) return
@@ -274,7 +276,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
274
276
  return () => window.clearTimeout(timer)
275
277
  }, [toast])
276
278
 
277
- /** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea 兜底)。 */
279
+ /** 复制文本到剪贴板,返回是否成功(Clipboard API + 隐藏 textarea 作为后备)。 */
278
280
  const doCopy = async (text: string): Promise<boolean> => {
279
281
  try {
280
282
  await navigator.clipboard.writeText(text)
@@ -298,7 +300,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
298
300
  }
299
301
  }
300
302
 
301
- /** 命令行安装目标是否已安装(与服务端 already 判定同口径):GitHub 地址按仓库身份匹配
303
+ /** 命令行安装目标是否已安装(与服务端 already 判定同一套规则):GitHub 地址按仓库身份匹配
302
304
  * (归一化后比对已装项的 repo/spec),npm 包名按依赖 key 直查。命中 → 弹窗转「更新」语义
303
305
  * (mode=update 放行覆盖重装),避免重复点击撞 409 报错。 */
304
306
  const customTargetInstalled = (raw: string): boolean => {
@@ -321,9 +323,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
321
323
  }
322
324
 
323
325
  /** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行(npm 安装显示包名命令)。 */
324
- const copyCommand = async (p: HubPlugin) => {
326
+ const copyCommand = async (p: HubPlugin, version?: string) => {
325
327
  const repo = p.source?.repo ?? ''
326
- const ok = await doCopy(installCommandOf(p))
328
+ const ok = await doCopy(installCommandOf(p, false, version))
327
329
  if (ok) {
328
330
  setCopied(repo)
329
331
  setToast({ id: Date.now(), kind: 'copied' })
@@ -332,6 +334,21 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
332
334
  setConfirmPlugin(null)
333
335
  }
334
336
 
337
+ /** 新安装先查「已过 pnpm 发布冷却期」的版本:命令条与执行都锁定它,
338
+ * 否则 pnpm 会把发布未满 24h 的最新版不提示地回退成很旧的版本。查不到则不锁定,由服务端再解析一次。 */
339
+ const resolveInstallPin = (p: HubPlugin) => {
340
+ // 目录下发了官方安装命令的条目(CLI-only / 人工干预)只展示该命令,追加版本会改坏它
341
+ if (p.install?.command !== undefined && p.install.command !== '') return
342
+ const pkg = installTargetOf(p)
343
+ if (!pkg) return
344
+ const seq = ++installPinSeq.current
345
+ void fetchNpmReleaseAge([pkg]).then((res) => {
346
+ if (installPinSeq.current !== seq) return
347
+ const version = res?.[pkg]?.installable?.version
348
+ if (version !== undefined && version !== '') setConfirmPinVersion(version)
349
+ })
350
+ }
351
+
335
352
  /** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。
336
353
  * 自定义安装(无目录数据)按已安装项包名直卸,同样给复制通道。 */
337
354
  const copyUninstallCommand = async () => {
@@ -348,7 +365,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
348
365
  }
349
366
 
350
367
  /** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。
351
- * 只在刷新解决不了的兜底场景(热挂载失败 / 非 dsh 插件 / 更新)出现。
368
+ * 只在刷新解决不了的例外场景(热加载失败 / 非 dsh 插件 / 更新)出现。
352
369
  * 桌面端宿主归应用壳管(服务端回 `desktop: true` 且不重启):不轮询,改提示退出重开。 */
353
370
  const requestRestart = async () => {
354
371
  if (restarting) return
@@ -363,7 +380,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
363
380
  /* 服务已终止,无需处理 */
364
381
  }
365
382
  if (desktop) {
366
- // 宿主不会重启,轮询等不到新服务:直接给出正确口径
383
+ // 宿主不会重启,轮询等不到新服务:直接给出正确规则
367
384
  setRestarting(false)
368
385
  setToast({ id: Date.now(), kind: 'restartDesktop' })
369
386
  return
@@ -392,7 +409,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
392
409
  }
393
410
 
394
411
  const { total, installedName, installedVersion, hasUpdate } = catalog
395
- // 统计数字:官网 /api/stats.json 优先;拉取失败时用已加载列表兜底,避免展示 undefined。
412
+ // 统计数字:官网 /api/stats.json 优先;拉取失败时改用已加载列表,避免展示 undefined。
396
413
  const statsTotal = catalog.stats?.total ?? total
397
414
  const statsVerified = catalog.stats?.verified ?? 0
398
415
 
@@ -431,7 +448,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
431
448
  noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
432
449
  onOpenNotifications: () => {
433
450
  // 打开弹窗时补齐更新提醒(受「启动时检查更新」开关约束):同一版本只一条,
434
- // 清空/删除后重开仍恢复,直到插件更新完成才消失;门禁结果未就绪时先不写,避免提示装不上的更新
451
+ // 清空/删除后重开仍恢复,直到插件更新完成才消失;冷却期查询结果未就绪时先不写,避免提示装不上的更新
435
452
  if (hubSettings.checkUpdatesOnStart && catalog.releaseAgeReady) syncUpdateNotices()
436
453
  setShowNotifications(true)
437
454
  },
@@ -451,12 +468,17 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
451
468
  ? t('pluginsTotal', { n: count })
452
469
  : t('filterResults', { n: count }),
453
470
  onInstall: (p, opts) => {
471
+ const update = opts?.update ?? false
454
472
  setInstallDone(false)
455
- setConfirmIsUpdate(opts?.update ?? false)
473
+ setConfirmIsUpdate(update)
456
474
  setConfirmPinVersion(null)
475
+ // 本次弹窗重新开始:作废上一次还没返回的冷却期查询
476
+ ++installPinSeq.current
457
477
  // 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
458
478
  queue.clearModalTask()
459
479
  setConfirmPlugin(p)
480
+ // 更新走 @latest 语义(客户端已按冷却期筛选),只有新安装需要锁定冷却期内可用的版本
481
+ if (!update) resolveInstallPin(p)
460
482
  },
461
483
  onUninstall: (p) => {
462
484
  setUninstallDone(false)
@@ -489,6 +511,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
489
511
  setInstallDone(false)
490
512
  setConfirmIsUpdate(true)
491
513
  setConfirmPinVersion(null)
514
+ // 更新不锁定版本:作废可能还没返回的新安装冷却期查询
515
+ ++installPinSeq.current
492
516
  queue.clearModalTask()
493
517
  setConfirmPlugin(item.plugin)
494
518
  },
@@ -552,7 +576,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
552
576
  // 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
553
577
  // 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
554
578
  showHubUpdate && h(HubUpdateModal, {
555
- // 提示目标是「已过供应链门禁」的版本,可能不是 latest —— 展示它与它的变更记录
579
+ // 提示目标是「已过发布冷却期」的版本,可能不是 latest —— 展示它与它的变更记录
556
580
  info: catalog.hubUpdateTarget ?? catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
557
581
  state: catalog.hubUpdateState,
558
582
  currentVersion: catalog.hubCurrentVersion,
@@ -565,8 +589,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
565
589
  if (hubEntry) {
566
590
  setInstallDone(false)
567
591
  setConfirmIsUpdate(true)
568
- // 钉住本次提示的版本:最新版可能仍在门禁内,@latest 会被 pnpm 拒绝
592
+ // 锁定本次提示的版本:最新版可能仍在冷却期内,@latest 会被 pnpm 拒绝
569
593
  setConfirmPinVersion(catalog.hubUpdateTarget?.version ?? null)
594
+ // 作废可能还没返回的新安装冷却期查询,避免它覆盖本次锁定的版本
595
+ ++installPinSeq.current
570
596
  queue.clearModalTask()
571
597
  setConfirmPlugin(hubEntry)
572
598
  }
@@ -574,7 +600,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
574
600
  }),
575
601
  // 「关注我们」弹窗:GitHub 图标后按钮点击打开 —— 平台介绍 + 用户反馈群二维码,
576
602
  // 内容由 dsh-update Worker 的 /about 接口以 Markdown 形式推送,作者随时可改,非写死。
577
- // Worker 未推送(info 为 null)时弹窗仍能打开并展示兜底文案,不影响其他功能。
603
+ // Worker 未推送(info 为 null)时弹窗仍能打开并展示备用文案,不影响其他功能。
578
604
  showAbout && h(AboutModal, {
579
605
  info: catalog.hubAboutInfo ?? null,
580
606
  lang,
@@ -668,21 +694,26 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
668
694
  langPath,
669
695
  restarting,
670
696
  update: confirmIsUpdate,
697
+ pinVersion: confirmPinVersion ?? undefined,
671
698
  // 仅命令行插件(webInstallable=false / 人工干预 manualInstall):弹窗只展示命令供复制,不提供一键安装
672
699
  cliOnly: confirmPlugin.install?.webInstallable === false || confirmPlugin.install?.manualInstall === true,
673
700
  submitting: queue.submitting,
674
701
  needsRestart: installNeedsRestart,
675
702
  needsReload: installNeedsReload,
676
703
  bundleDeselected: installBundleDeselected,
677
- // 结果弹窗关闭即生效:安装热挂载的插件要页面重载才渲染客户端 UI → 自动刷新,无需用户点按钮
704
+ // 结果弹窗关闭即生效:安装热加载的插件要页面重载才渲染客户端 UI → 自动刷新,无需用户点按钮
678
705
  onClose: () => {
679
706
  const reload = installDone && installNeedsReload
680
707
  setConfirmPlugin(null)
681
708
  setInstallBundleDeselected(false)
682
709
  if (reload) window.location.reload()
683
710
  },
684
- onCopy: () => copyCommand(confirmPlugin),
685
- onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true, pinVersion: confirmPinVersion ?? undefined } : undefined),
711
+ onCopy: () => copyCommand(confirmPlugin, confirmPinVersion ?? undefined),
712
+ onInstall: () => queue.installNow(confirmPlugin, {
713
+ update: confirmIsUpdate,
714
+ // 新安装锁定已过冷却期的版本:查询尚未回来时为空,服务端会自行解析
715
+ pinVersion: confirmPinVersion ?? undefined,
716
+ }),
686
717
  // 重启:结果视图「立即重启」也会中断进行中的任务 → 先弹重启确认弹窗
687
718
  onRestart: () => setShowRestartConfirm(true),
688
719
  }),
@@ -825,7 +856,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
825
856
  onRunDiagnostics: openDiagnostics,
826
857
  onClose: () => setErrorMsg(null),
827
858
  }),
828
- // 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出(兜底场景),单个「立即重启」,关闭即视为稍后
859
+ // 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出(例外场景),单个「立即重启」,关闭即视为稍后
829
860
  showRestartConfirm && h(RestartConfirmModal, {
830
861
  t,
831
862
  restarting,
@@ -28,7 +28,7 @@ export function CatalogList({ plugins, failed, visible, total, t, langPath, relo
28
28
  installedName: (p: HubPlugin) => string | null
29
29
  installedVersion: (p: HubPlugin) => string | null
30
30
  hasUpdate: (p: HubPlugin) => boolean
31
- /** 更新被供应链门禁暂缓时的可重试时刻(毫秒);无则 null */
31
+ /** 更新暂缓(新版本还没过冷却期)的可重试时刻(毫秒);无则 null */
32
32
  updatePendingUntil: (p: HubPlugin) => number | null
33
33
  langKey: LocaleId
34
34
  onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
@@ -19,9 +19,9 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
19
19
  installedName: (p: HubPlugin) => string | null
20
20
  /** 已安装版本号(安装时从目录信号记录;无 release 的仓库为空,无版本号可展示) */
21
21
  installedVersion: (p: HubPlugin) => string | null
22
- /** 已安装且目录有更新(有版本比版本、无版本比仓库更新时间);门禁复核未就绪时为 false */
22
+ /** 已安装且目录有更新(有版本比版本、无版本比仓库更新时间);冷却期复核未就绪时为 false */
23
23
  hasUpdate: (p: HubPlugin) => boolean
24
- /** 更新被供应链门禁暂缓时的可重试时刻(毫秒);无则 null */
24
+ /** 更新暂缓(新版本还没过冷却期)的可重试时刻(毫秒);无则 null */
25
25
  updatePendingUntil: (p: HubPlugin) => number | null
26
26
  t: Translate
27
27
  langKey: LocaleId
@@ -35,7 +35,7 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
35
35
  const pkg = installedName(p)
36
36
  const isInstalled = pkg !== null
37
37
  const update = hasUpdate(p)
38
- // 门禁暂缓:更新确实存在但现在装不上,给确切时刻而不是让徽标静默消失
38
+ // 冷却期未到:更新确实存在但现在装不上,给确切时刻而不是让徽标悄悄消失
39
39
  const pendingUntil = isInstalled && !update ? updatePendingUntil(p) : null
40
40
  // 版本号展示:已安装 → 当前已装版本(最能反映用户状态);未安装 → 目录最新版本
41
41
  const versionShown = isInstalled ? installedVersion(p) : p.version
@@ -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,13 +90,15 @@ export interface InstallModalProps {
90
90
  restarting: boolean
91
91
  /** 已安装插件的覆盖更新(走同一条 add 命令原位重装,文案区分安装/更新) */
92
92
  update: boolean
93
+ /** 已过发布冷却期的安装版本:命令条带上它,手抄执行与一键安装结果一致(未查到时不传) */
94
+ pinVersion?: string
93
95
  /** 仅命令行插件(webInstallable=false):不提供一键安装,只展示命令供复制到 dsh 终端 */
94
96
  cliOnly: boolean
95
- /** 安装请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
97
+ /** 安装请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
96
98
  submitting: boolean
97
99
  /** 完成结果是否需重启才生效:true → 仅给「立即重启」;false → 仅「完成」(关闭即自动刷新) */
98
100
  needsRestart: boolean
99
- /** 完成结果是否只需刷新页面即生效(插件已热挂载):true → 结果视图显示自动刷新说明 */
101
+ /** 完成结果是否只需刷新页面即生效(插件已热加载):true → 结果视图显示自动刷新说明 */
100
102
  needsReload: boolean
101
103
  /** 市场自更新成功、但 profile 的 bundle 列表已不含 dsh-plugin:结果页提示重新勾选 */
102
104
  bundleDeselected?: boolean
@@ -112,7 +114,7 @@ export interface InstallModalProps {
112
114
  * 目录插件与命令行安装(customTarget 模式)共用同一套确认/进度/结果流程。
113
115
  */
114
116
  export function InstallModal(props: InstallModalProps) {
115
- const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, needsReload, bundleDeselected, onClose, onCopy, onInstall, onRestart } = props
117
+ const { plugin, customTarget, globalNpm, done, task, t, langPath, restarting, submitting, update, pinVersion, cliOnly, needsRestart, needsReload, bundleDeselected, onClose, onCopy, onInstall, onRestart } = props
116
118
  const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
117
119
  // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分;
118
120
  // 命令行安装无插件元数据 → 直接用输入的目标展示;全局 npm 安装 → 包列表空格拼接
@@ -191,7 +193,7 @@ export function InstallModal(props: InstallModalProps) {
191
193
  h('span', { className: styles.modalCmdText },
192
194
  globalNpm !== undefined && globalNpm.length > 0
193
195
  ? `npm install -g ${globalNpm.join(' ')}`
194
- : customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin!)),
196
+ : customTarget ? `pnpm add ${customTarget}` : installCommandOf(plugin!, false, pinVersion)),
195
197
  h('span', { className: styles.modalCmdCopy }, h(CopyIcon), t('copyCmdLabel')),
196
198
  ),
197
199
  // 全局 npm 安装说明:装的是系统级 CLI 工具,不进插件列表、无需重启宿主
@@ -236,7 +238,7 @@ export interface UninstallModalProps {
236
238
  t: Translate
237
239
  langPath: string
238
240
  restarting: boolean
239
- /** 卸载请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
241
+ /** 卸载请求进行中(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
240
242
  submitting: boolean
241
243
  /** 完成结果是否需重启才生效:true(停用失败等刷新解决不了的场景)→ 仅给「立即重启」;
242
244
  * false(loader 已即时停用)→ 仅「完成」,关闭即自动刷新页面摘除插件面板 */
@@ -270,7 +272,7 @@ export function UninstallModal(props: UninstallModalProps) {
270
272
  h('div', { className: styles.modalHead },
271
273
  h('div', {
272
274
  className: busy
273
- // 注意 task 判空:submitting(请求在途)时任务尚未入队,task 为 null,不能直接解引用
275
+ // 注意 task 判空:submitting(请求进行中)时任务尚未入队,task 为 null,不能直接解引用
274
276
  ? `${styles.modalTitle} ${task !== null && task.status === 'pending' ? styles.modalTitleQueued : styles.modalTitleBusy}`
275
277
  : styles.modalTitle,
276
278
  }, title),
@@ -365,7 +367,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
365
367
  // 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
366
368
  // 只复制 message 会丢上下文 —— 命令行安装输入的目标、执行过的命令都在 command/attempts 里,
367
369
  // 光拷一句「unsupported install target」出去没人看得懂
368
- // 展示与复制都用展开后的文本:宿主门禁消息里的 peerDependencies 是压缩 JSON,
370
+ // 展示与复制都用展开后的文本:宿主兼容性提示里的 peerDependencies 是压缩 JSON,
369
371
  // 原样展示会挤成一整行;格式化只影响可读性,不改变归类与上报逻辑
370
372
  const displayMessage = formatFailureMessage(message)
371
373
  const copyText = [
@@ -509,7 +511,7 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
509
511
  }, t('failIssueBig')) : null,
510
512
  ])
511
513
  : failureKind === 'hostIncompatible'
512
- // 宿主版本门禁:插件声明的兼容范围不含当前 dsh 版本,安装前就被宿主拒绝。
514
+ // 宿主版本要求:插件声明的兼容范围不含当前 dsh 版本,安装前就被宿主拒绝。
513
515
  // 这是作者能修的(把该版本加进兼容清单),故直接引导到作者仓库;同时说明本地可临时授权
514
516
  ? h('div', null, [
515
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,9 +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。 */
52
+ installable?: NpmReleaseInfo | null
51
53
  }
52
54
 
53
- /** 批量查询已安装插件 npm 包的最新版本发布时间(供应链门禁用);失败返回 null(调用方按原逻辑照常提示)。 */
55
+ /** 批量查询已安装插件 npm 包的最新版本发布时间(发布冷却期用);失败返回 null(调用方按原逻辑照常提示)。 */
54
56
  export async function fetchNpmReleaseAge(packages: string[]): Promise<Record<string, NpmReleaseInfo | null> | null> {
55
57
  if (packages.length === 0) return {}
56
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' })