dsh-plugin 1.6.12 → 2.0.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 (131) hide show
  1. package/README.en.md +13 -13
  2. package/README.md +13 -13
  3. package/client/client.js +791 -761
  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/operation-log.js +53 -1
  20. package/lib/services/install/package-entry.js +6 -11
  21. package/lib/services/install/registry-check.js +2 -2
  22. package/lib/services/install/release-target.js +8 -9
  23. package/lib/services/install/task-queue.js +44 -48
  24. package/lib/services/npm-release-age.js +119 -0
  25. package/lib/services/probe.js +4 -4
  26. package/lib/services/profile/installed-versions.js +15 -21
  27. package/lib/services/profile/profile.js +16 -44
  28. package/lib/services/profile/progress.js +8 -11
  29. package/lib/services/settings.js +2 -5
  30. package/lib/types/http/catalog.d.ts +1 -1
  31. package/lib/types/http/install.d.ts +2 -2
  32. package/lib/types/http/npm-release-age.d.ts +2 -0
  33. package/lib/types/http/routes.d.ts +10 -12
  34. package/lib/types/http/shared.d.ts +5 -6
  35. package/lib/types/index.d.ts +7 -8
  36. package/lib/types/services/install/install-types.d.ts +12 -13
  37. package/lib/types/services/install/install.d.ts +1 -1
  38. package/lib/types/services/install/npm-resolve.d.ts +4 -16
  39. package/lib/types/services/install/operation-log.d.ts +9 -0
  40. package/lib/types/services/install/package-entry.d.ts +6 -11
  41. package/lib/types/services/install/release-target.d.ts +7 -8
  42. package/lib/types/services/install/task-queue.d.ts +25 -30
  43. package/lib/types/services/npm-release-age.d.ts +15 -0
  44. package/lib/types/services/probe.d.ts +4 -4
  45. package/lib/types/services/profile/installed-versions.d.ts +5 -7
  46. package/lib/types/services/profile/profile.d.ts +14 -18
  47. package/lib/types/services/profile/progress.d.ts +7 -10
  48. package/lib/types/services/settings.d.ts +0 -4
  49. package/package.json +1 -1
  50. package/src/client/components/PluginHubSection.tsx +26 -22
  51. package/src/client/components/catalog/CatalogList.tsx +2 -4
  52. package/src/client/components/catalog/PluginCard.tsx +2 -3
  53. package/src/client/components/layout/CatalogControls.tsx +5 -6
  54. package/src/client/components/layout/CatalogHeader.tsx +1 -2
  55. package/src/client/components/layout/CategoryTabs.tsx +2 -3
  56. package/src/client/components/layout/SectionTabs.tsx +3 -5
  57. package/src/client/components/modals/ConfirmDialog.tsx +4 -6
  58. package/src/client/components/modals/InstalledDetailModal.tsx +3 -6
  59. package/src/client/components/modals/LogsModal.tsx +7 -12
  60. package/src/client/components/modals/NotificationsModal.tsx +74 -24
  61. package/src/client/components/modals/ProgressView.tsx +3 -4
  62. package/src/client/components/modals/modals.tsx +75 -26
  63. package/src/client/components/ui/Dropdown.tsx +4 -6
  64. package/src/client/components/ui/Toggle.tsx +2 -3
  65. package/src/client/components/ui/icons.tsx +1 -2
  66. package/src/client/components/views/CustomInstallView.tsx +20 -178
  67. package/src/client/components/views/DiagnosticsView.tsx +3 -6
  68. package/src/client/components/views/InstalledView.tsx +6 -11
  69. package/src/client/components/views/MarketView.tsx +2 -4
  70. package/src/client/components/views/SettingsView.tsx +9 -32
  71. package/src/client/css-modules.d.ts +1 -1
  72. package/src/client/data/catalog.ts +2 -3
  73. package/src/client/data/host.ts +26 -3
  74. package/src/client/data/hub.ts +2 -3
  75. package/src/client/hooks/useCatalog.ts +79 -24
  76. package/src/client/hooks/useIncrementalList.ts +3 -5
  77. package/src/client/hooks/useLanguage.ts +4 -6
  78. package/src/client/hooks/useSettings.ts +4 -11
  79. package/src/client/hooks/useTaskQueue.ts +36 -42
  80. package/src/client/index.tsx +2 -3
  81. package/src/client/locales.ts +187 -114
  82. package/src/client/logic/constants.ts +1 -2
  83. package/src/client/logic/ensure-plugin-css.ts +8 -12
  84. package/src/client/logic/failures.ts +20 -19
  85. package/src/client/logic/format.ts +1 -1
  86. package/src/client/logic/install-command.ts +14 -35
  87. package/src/client/logic/installed.ts +15 -10
  88. package/src/client/logic/normalize.ts +3 -3
  89. package/src/client/logic/release-age.ts +19 -0
  90. package/src/client/logic/urls.ts +24 -21
  91. package/src/client/styles/CustomInstallView.module.css +3 -30
  92. package/src/client/styles/DiagnosticsView.module.css +3 -5
  93. package/src/client/styles/Dropdown.module.css +2 -4
  94. package/src/client/styles/Header.module.css +6 -7
  95. package/src/client/styles/InstalledView.module.css +4 -7
  96. package/src/client/styles/List.module.css +4 -5
  97. package/src/client/styles/LogsView.module.css +2 -3
  98. package/src/client/styles/Modal.module.css +46 -6
  99. package/src/client/styles/SectionTabs.module.css +2 -4
  100. package/src/client/styles/SettingsView.module.css +3 -5
  101. package/src/client/styles/Toggle.module.css +2 -4
  102. package/src/client/styles/tokens.module.css +14 -18
  103. package/src/client/types.ts +11 -11
  104. package/src/server/http/catalog.ts +2 -3
  105. package/src/server/http/diagnostics.ts +6 -7
  106. package/src/server/http/install.ts +47 -208
  107. package/src/server/http/logs.ts +1 -2
  108. package/src/server/http/npm-release-age.ts +42 -0
  109. package/src/server/http/routes.ts +12 -12
  110. package/src/server/http/settings.ts +2 -2
  111. package/src/server/http/shared.ts +5 -6
  112. package/src/server/http/task.ts +1 -2
  113. package/src/server/index.ts +7 -8
  114. package/src/server/services/install/install-types.ts +12 -13
  115. package/src/server/services/install/install.ts +0 -1
  116. package/src/server/services/install/npm-check.ts +3 -2
  117. package/src/server/services/install/npm-resolve.ts +9 -99
  118. package/src/server/services/install/operation-log.ts +47 -1
  119. package/src/server/services/install/package-entry.ts +6 -11
  120. package/src/server/services/install/registry-check.ts +2 -2
  121. package/src/server/services/install/release-target.ts +8 -9
  122. package/src/server/services/install/task-queue.ts +51 -54
  123. package/src/server/services/npm-release-age.ts +107 -0
  124. package/src/server/services/probe.ts +4 -4
  125. package/src/server/services/profile/installed-versions.ts +16 -22
  126. package/src/server/services/profile/profile.ts +16 -42
  127. package/src/server/services/profile/progress.ts +8 -11
  128. package/src/server/services/settings.ts +2 -9
  129. package/lib/services/install/preflight.js +0 -178
  130. package/lib/types/services/install/preflight.d.ts +0 -23
  131. package/src/server/services/install/preflight.ts +0 -184
@@ -3,11 +3,10 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Catalog data + view state for the Plugin Hub section.
6
+ * 插件中心的数据 + 视图状态。
7
7
  *
8
- * Owns the online-data pipeline (live fetch from api.dsh-plugin.org), the local
9
- * installed-plugin table, and the filter/search/sort/install-status view
10
- * state, exposing the derived visible list and per-category counts.
8
+ * 掌管在线数据管道(从 api.dsh-plugin.org 实时拉取)、本地已装插件表,以及
9
+ * 筛选/搜索/排序/安装状态等视图状态,对外暴露派生出的可见列表与各分类计数。
11
10
  */
12
11
  import { useEffect, useMemo, useRef, useState } from 'react'
13
12
  import type { HubPlugin, LocaleId } from '../types.ts'
@@ -16,15 +15,19 @@ import type { HubAboutInfo, HubUpdateInfo, HubUpdateState } from '../types.ts'
16
15
  import type { SortKey } from '../logic/constants.ts'
17
16
  import { fetchCatalog, fetchStats } from '../data/catalog.ts'
18
17
  import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
19
- import { fetchInstalled } from '../data/host.ts'
18
+ import { fetchInstalled, fetchNpmReleaseAge } from '../data/host.ts'
19
+ import { isReleaseTooFresh } from '../logic/release-age.ts'
20
20
  import {
21
- hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf,
21
+ hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf, npmPackageOf,
22
22
  } from '../logic/installed.ts'
23
23
  import type { InstalledItem, InstalledVersionSignal } from '../logic/installed.ts'
24
24
 
25
25
  /** 插件市场自身仓库:DSH Plugin Hub 不显示在目录里(自己不进自己的插件列表) */
26
26
  const SELF_REPO = 'dshplugin/dsh-plugin-hub'
27
27
 
28
+ /** 共享的空门禁集合:无暂缓包时复用,避免每次渲染新建 Set。 */
29
+ const EMPTY_PACKAGES: ReadonlySet<string> = new Set<string>()
30
+
28
31
  /** 市场各排序的默认方向:全部按倒序(Star/Fork 多、更新/收录近的在前) */
29
32
  const SORT_DEFAULT_DIR: Record<SortKey, 'asc' | 'desc'> = {
30
33
  sortStars: 'desc', sortForks: 'desc', sortUpdated: 'desc', sortNewest: 'desc',
@@ -75,7 +78,7 @@ export function useCatalog(lang: LocaleId) {
75
78
  /** 当前 profile 已安装插件:npm 包名 -> manifest spec(来自宿主本地路由) */
76
79
  const [installed, setInstalled] = useState<Record<string, string>>({})
77
80
  /** 安装时记录的目录信号:repo(小写) -> { version, updatedAt }(来自宿主本地路由);
78
- * npmPackage 为 npm 优先通道反查命中的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
81
+ * npmPackage 为安装时记录的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
79
82
  const [versions, setVersions] = useState<Record<string, InstalledVersionSignal>>({})
80
83
  /** 每个依赖在系统上的安装目录(profile/node_modules/<包名>),详情视图展示用 */
81
84
  const [installPaths, setInstallPaths] = useState<Record<string, string> | null>(null)
@@ -176,25 +179,83 @@ export function useCatalog(lang: LocaleId) {
176
179
  return () => { cancelled = true }
177
180
  }, [reloadKey])
178
181
 
179
- /** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
182
+ /** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 安装的依赖包名;命中返回 npm 包名。 */
180
183
  const installedName = (p: HubPlugin): string | null => installedNameOf(p, installed, versions)
181
184
 
182
185
  /** 该插件安装时记录的目录版本(无记录/未安装 → null)。 */
183
186
  const installedVersion = (p: HubPlugin): string | null => installedVersionOf(p, versions)
184
187
 
185
188
  /**
186
- * 是否有更新:仅对已安装插件有意义。
187
- * 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
188
- * (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
189
+ * 供应链门禁状态:包名集合为当前「暂缓提示」的 npm 包(最新版本发布未满门槛)。
190
+ * key 是这批候选包名的指纹,与已查出的 key 一致才算就绪,避免查询未回来时先显示更新。
189
191
  */
190
- const hasUpdate = (p: HubPlugin): boolean => hasUpdateOf(p, installed, versions)
192
+ const [releaseAge, setReleaseAge] = useState<{ key: string; held: ReadonlySet<string> }>({ key: '', held: EMPTY_PACKAGES })
191
193
 
192
- /** 已安装项统一列表(目录元数据 + 运行时信息合并):驱动「已安装」tab。 */
193
- const installedItems = useMemo<InstalledItem[]>(
194
+ /** 已安装项原始列表(未套门禁):先算出来才能收集需要查发布时间的候选包名。 */
195
+ const rawInstalledItems = useMemo<InstalledItem[]>(
194
196
  () => installedItemsOf(plugins, installed, versions, installPaths, loadedNames, dshCapableNames),
195
197
  [plugins, installed, versions, installPaths, loadedNames, dshCapableNames],
196
198
  )
197
199
 
200
+ /** 门禁候选:有更新、且有 npm 包名(更新走 npm registry)的已安装插件包名。 */
201
+ const releaseAgeCandidates = useMemo(() => {
202
+ const names = new Set<string>()
203
+ for (const item of rawInstalledItems) {
204
+ if (!item.hasUpdate || !item.plugin) continue
205
+ const pkg = npmPackageOf(item.plugin, versions)
206
+ if (pkg !== null) names.add(pkg)
207
+ }
208
+ return [...names].sort()
209
+ }, [rawInstalledItems, versions])
210
+ const releaseAgeKey = releaseAgeCandidates.join('\n')
211
+
212
+ useEffect(() => {
213
+ if (releaseAgeKey === releaseAge.key) return
214
+ if (releaseAgeCandidates.length === 0) {
215
+ setReleaseAge({ key: '', held: EMPTY_PACKAGES })
216
+ return
217
+ }
218
+ let cancelled = false
219
+ // 批量一次查询;失败/返回 null 时 held 为空 → 按原逻辑照常提示,网络问题不吃掉真实更新
220
+ void fetchNpmReleaseAge(releaseAgeCandidates).then((result) => {
221
+ if (cancelled) return
222
+ const now = Date.now()
223
+ const held = new Set<string>()
224
+ for (const name of releaseAgeCandidates) {
225
+ const info = result?.[name]
226
+ if (info && isReleaseTooFresh(info.publishedAt, now)) held.add(name)
227
+ }
228
+ setReleaseAge({ key: releaseAgeKey, held })
229
+ })
230
+ return () => { cancelled = true }
231
+ }, [releaseAgeKey, releaseAge.key])
232
+
233
+ /** 门禁结果是否已就绪(候选为空也算就绪):未就绪时不写更新提醒,等查询回来再定。 */
234
+ const releaseAgeReady = releaseAgeKey === releaseAge.key
235
+ const heldPackages = releaseAgeReady ? releaseAge.held : EMPTY_PACKAGES
236
+
237
+ /**
238
+ * 是否有更新:仅对已安装插件有意义。
239
+ * 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
240
+ * (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
241
+ * 另加供应链门禁:npm 包最新版本发布未满门槛时视为暂缓,不打更新标记。
242
+ */
243
+ const hasUpdate = (p: HubPlugin): boolean => {
244
+ if (!hasUpdateOf(p, installed, versions)) return false
245
+ const pkg = npmPackageOf(p, versions)
246
+ return pkg === null || !heldPackages.has(pkg)
247
+ }
248
+
249
+ /** 已安装项统一列表(目录元数据 + 运行时信息合并):门禁命中的去掉更新标记,驱动「已安装」tab。 */
250
+ const installedItems = useMemo<InstalledItem[]>(() => {
251
+ if (heldPackages.size === 0) return rawInstalledItems
252
+ return rawInstalledItems.map((item) => {
253
+ if (!item.hasUpdate || !item.plugin) return item
254
+ const pkg = npmPackageOf(item.plugin, versions)
255
+ return pkg !== null && heldPackages.has(pkg) ? { ...item, hasUpdate: false } : item
256
+ })
257
+ }, [rawInstalledItems, heldPackages, versions])
258
+
198
259
  /**
199
260
  * Hub 自身更新状态:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
200
261
  * 以 Worker 版本控制中心返回的「最新版本」为准,与当前运行的版本号比对——
@@ -210,15 +271,8 @@ export function useCatalog(lang: LocaleId) {
210
271
  const hubUpdateState: HubUpdateState = (() => {
211
272
  if (!hubUpdateInfo) return 'none'
212
273
  if (hubCurrentVersion === '' || hubUpdateInfo.version === hubCurrentVersion) return 'latest'
213
- const publishedAt = hubUpdateInfo.publishedAt
214
- if (publishedAt) {
215
- const publishedMs = Date.parse(publishedAt)
216
- if (!Number.isNaN(publishedMs)) {
217
- // 24 小时 + 30 分钟缓冲(防网络/CDN 边缘时间偏差)
218
- const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
219
- if (Date.now() - publishedMs < MIN_RELEASE_AGE_MS) return 'pending'
220
- }
221
- }
274
+ // 供应链门禁(24h + 30 分钟缓冲)与插件更新同一口径:发布未满门槛的版本暂缓提示
275
+ if (isReleaseTooFresh(hubUpdateInfo.publishedAt, Date.now())) return 'pending'
222
276
  return 'available'
223
277
  })()
224
278
  const hubHasUpdate = hubUpdateState === 'available'
@@ -252,7 +306,7 @@ export function useCatalog(lang: LocaleId) {
252
306
  })
253
307
  }, [plugins, category, query, sort, sortDir, installed])
254
308
 
255
- /** Per-category plugin counts shown on the category chips. */
309
+ /** 分类 chip 上展示的各分类插件计数。 */
256
310
  const categoryCounts = useMemo(() => {
257
311
  const counts: Record<string, number> = {}
258
312
  for (const p of plugins ?? []) {
@@ -274,6 +328,7 @@ export function useCatalog(lang: LocaleId) {
274
328
  hasUpdate,
275
329
  hubHasUpdate,
276
330
  hubUpdateState,
331
+ releaseAgeReady,
277
332
  hubCurrentVersion,
278
333
  hubUpdateInfo,
279
334
  hubAboutInfo,
@@ -3,11 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Incremental list: renders only the first batch of a (potentially huge)
7
- * result set and appends the next batch whenever the end sentinel scrolls
8
- * into view, so the plugin market never mounts thousands of cards at once.
9
- * The items reference is the list identity — category / search / sort
10
- * changes produce a new array, which resets the window to the first batch.
6
+ * 增量列表:只渲染(可能很大的)结果集的第一批,末尾哨兵滚入视口时就追加下一批,
7
+ * 让插件市场绝不一次性挂载成千上万张卡片。
8
+ * items 引用即列表身份 —— 分类/搜索/排序变化会产出新数组,窗口随之重置回第一批。
11
9
  */
12
10
  import { useEffect, useRef, useState } from 'react'
13
11
 
@@ -3,11 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Language state for the section: the Hub shares the host (system) locale —
7
- * the header language toggle writes the host locale preference via setLocale,
8
- * so the host chrome (left sidebar, settings dialog) and the Hub panel switch
9
- * together instead of drifting apart. Exposes the localized dictionary lookup
10
- * `t()` plus the zh/en language flag and the /zh/ URL path prefix.
6
+ * 板块的语言状态:Hub 与宿主(系统)locale 共用 —— 头部语言开关通过 setLocale 写入
7
+ * 宿主语言偏好,让宿主界面(左侧菜单、设置弹窗)与 Hub 面板一起切换、不会各说各话。
8
+ * 对外暴露本地化字典查询 `t()`、zh/en 语言标记与 /zh/ URL 路径前缀。
11
9
  */
12
10
  import { useEffect, useState } from 'react'
13
11
  import type { LocaleId, SectionProps } from '../types.ts'
@@ -22,7 +20,7 @@ export function useLanguage(locale: SectionProps['locale']) {
22
20
  useEffect(() => locale.subscribe(() => setRev((r) => r + 1)), [locale])
23
21
  const lang: LocaleId = locale.getSnapshot().active
24
22
  const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
25
- // dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
23
+ // dsh-plugin.org 的 zh 页面挂在 /zh/ 前缀下,en 在根路径。
26
24
  const langPath = langPathOf(lang)
27
25
 
28
26
  // 界面语言跟随宿主(系统)locale;宿主的 t() 绑定宿主 locale,
@@ -3,11 +3,10 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Hub settings state: loaded once from the host's local /settings route
7
- * (persisted at ~/.dsh/profiles/<profile>/hub-settings.json), updated
8
- * optimistically. Every change POSTs the patch immediately so a toggled
9
- * switch survives a reload; a host without the server routes falls back to
10
- * the defaults and keeps working (settings simply do not persist).
6
+ * 插件中心设置状态:从宿主本地 /settings 路由加载一次(持久化在
7
+ * ~/.dsh/profiles/<profile>/hub-settings.json),并按乐观方式更新。
8
+ * 每次改动立即 POST 补丁,切换过的开关刷新后仍在;宿主未挂服务端路由时退回默认值
9
+ * 继续可用(设置不持久化而已)。
11
10
  */
12
11
  import { useCallback, useEffect, useState } from 'react'
13
12
 
@@ -20,10 +19,6 @@ export interface HubSettings {
20
19
  npmRegistry: string
21
20
  /** 启用命令行安装 NPM 包(默认开) */
22
21
  enableNpmInstall: boolean
23
- /** 启用命令行安装 GitHub 源码(默认开) */
24
- enableGitInstall: boolean
25
- /** 启用命令行粘贴 dsh plugin 命令安装(默认开) */
26
- enableDshInstall: boolean
27
22
  /** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log) */
28
23
  logPath: string
29
24
  }
@@ -33,8 +28,6 @@ export const DEFAULT_SETTINGS: HubSettings = {
33
28
  proxy: '',
34
29
  npmRegistry: '',
35
30
  enableNpmInstall: true,
36
- enableGitInstall: true,
37
- enableDshInstall: true,
38
31
  logPath: '',
39
32
  }
40
33
 
@@ -3,19 +3,18 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * Background install/remove task queue for the Plugin Hub.
6
+ * 插件中心的后台安装/卸载任务队列。
7
7
  *
8
- * Mirrors the server-side FIFO active queue by polling /active, settles
9
- * vanished tasks against /status, and exposes the queue actions (install,
10
- * uninstall, cancel) plus the modal task lookups used by the dialogs.
8
+ * 通过轮询 /active 镜像服务端 FIFO 活动队列,对消失的任务用 /status 收尾,并暴露
9
+ * 队列动作(安装、卸载、取消)以及弹窗用的任务查找。
11
10
  */
12
11
  import { useEffect, useRef, useState } from 'react'
13
12
  import type { HubPlugin, InstallChannel, LocaleId, Translate } from '../types.ts'
14
13
  import { executedCommandOf, installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
14
  import type { InstalledItem } from '../logic/installed.ts'
16
15
 
17
- /** 安装/卸载请求超时(ms):服务端 preflight + npm 反查可能耗时(各自都有超时,合计约 20~30s),
18
- * 给足余量;超时主动中止,避免服务端 preflight 网络挂起时前端永久卡「安装中 0%」。 */
16
+ /** 安装/卸载请求超时(ms):服务端入队前要做目录包校验与网络探测(各自有超时,合计可达十几秒),
17
+ * 给足余量;超时主动中止,避免服务端网络挂起时前端永久卡「安装中 0%」。 */
19
18
  const REQUEST_TIMEOUT_MS = 60_000
20
19
 
21
20
  /** 客户端任务队列项:镜像服务端 active 任务(running 在前、pending 在后)。 */
@@ -28,8 +27,8 @@ export interface QueueTask {
28
27
  desc?: string
29
28
  /** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
30
29
  repo: string | null
31
- /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令三张卡片):失败时错误/通知精准
32
- * 溯源「从哪个入口发起」;目录插件安装无此字段(非自定义入口)。 */
30
+ /** 自定义安装入口渠道(NPM 包卡片):失败时错误/通知精准溯源「从哪个入口发起」;
31
+ * 目录插件安装无此字段(非自定义入口)。 */
33
32
  channel?: InstallChannel
34
33
  /** 实际动作(add/update/remove):成功通知据此区分「安装成功」与「更新成功」。
35
34
  * 服务端 /active 带回真实动作;本地乐观入队按 update 标记推断。 */
@@ -42,11 +41,11 @@ export interface QueueTask {
42
41
  progress: number
43
42
  lines: string[]
44
43
  /** 乐观入队条目:点击安装后立即显示「正在安装中」,等 POST 响应回来再换成真实任务 id。
45
- * 服务端还没登记(preflight 中),轮询合并/消失检测都必须跳过,避免被当作已结束任务收尾。 */
44
+ * 服务端还没登记,轮询合并/消失检测都必须跳过,避免被当作已结束任务收尾。 */
46
45
  optimistic?: boolean
47
- /** 实际执行的安装命令(npm/git 通道随插件数据决定):失败时 issue 预填如实展示 */
46
+ /** 实际执行的安装命令(随插件数据决定):失败时 issue 预填如实展示 */
48
47
  command?: string
49
- /** 尝试过的安装方式(npm registry 反查 + 实际执行命令,按先后顺序):失败时 issue 预填一并贴给作者 */
48
+ /** 实际执行/重试过的安装命令(按先后顺序):失败时 issue 预填一并贴给作者 */
50
49
  attempts?: string[]
51
50
  /** 完成结果是否需要重启才生效(服务端任务终态带出)。默认 true 保底:数据缺失时不静默视为已生效。
52
51
  * 仅刷新解决不了的兜底场景(热挂载失败 / 非 dsh 插件 / 更新)为 true,结果视图给「立即重启」;
@@ -70,7 +69,7 @@ export interface PendingRestart {
70
69
 
71
70
  export interface TaskQueueOptions {
72
71
  t: Translate
73
- /** 界面语言(zh/en):随安装请求带给服务端,网络预检等错误消息按用户语言提示 */
72
+ /** 界面语言(zh/en):随安装请求带给服务端,安装前网络探测等错误消息按用户语言提示 */
74
73
  langKey?: LocaleId
75
74
  refreshInstalled: () => void
76
75
  /** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
@@ -79,7 +78,7 @@ export interface TaskQueueOptions {
79
78
  * update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
80
79
  onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean, needsReload?: boolean) => void
81
80
  onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, needsReload?: boolean) => void
82
- /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)+ 是否更新。 */
81
+ /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 实际执行/重试过的安装命令(可缺省)+ 是否更新。 */
83
82
  onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
84
83
  /** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
85
84
  installPlugin: HubPlugin | null
@@ -96,14 +95,10 @@ export interface TaskQueueOptions {
96
95
  resolvePending: (repo: string) => { desc?: string; version?: string } | null
97
96
  }
98
97
 
99
- /** 入口渠道的 i18n key:NPM 包 / GitHub 源码 / DSH 命令。 */
100
- const channelKeyOf = (channel: InstallChannel): string =>
101
- channel === 'git' ? 'installChannelGit' : channel === 'dsh' ? 'installChannelDsh' : 'installChannelNpm'
102
-
103
98
  /** 失败消息的入口溯源前缀:自定义安装(带渠道)在错误正文顶部精准指出从哪张卡片发起;
104
99
  * 目录插件安装(无渠道)返回 null,错误正文保持原样。 */
105
100
  const entryLineOf = (t: Translate, channel: InstallChannel | undefined): string | null =>
106
- channel ? t('installEntryLine', { c: t(channelKeyOf(channel)) }) : null
101
+ channel ? t('installEntryLine', { c: t('installChannelNpm') }) : null
107
102
 
108
103
  export function useTaskQueue(opts: TaskQueueOptions) {
109
104
  const {
@@ -332,13 +327,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
332
327
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : (prevTask?.lines ?? []),
333
328
  attempts: Array.isArray(a.attempts) ? (a.attempts as string[]) : (prevTask?.attempts ?? []),
334
329
  needsRestart: typeof a.needsRestart === 'boolean' ? a.needsRestart : (prevTask?.needsRestart ?? true),
335
- // 入口渠道:本地乐观入队已带;服务端 /active 也带回(刷新恢复用),服务端优先
336
- channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh')
337
- ? a.installChannel
338
- : prevTask?.channel,
330
+ // 入口渠道:本地乐观入队已带;服务端 /active 也带回(刷新恢复用),服务端优先。
331
+ // 旧服务端可能回传已废弃的 'git'/'dsh' 渠道,忽略即可、回退本地值。
332
+ channel: a.installChannel === 'npm' ? 'npm' : prevTask?.channel,
339
333
  })
340
334
  }
341
- // 保留乐观条目:服务端尚未登记(preflight 中),无真实 id,不能因 active 缺它就被合并吞掉
335
+ // 保留乐观条目:服务端尚未登记,无真实 id,不能因 active 缺它就被合并吞掉
342
336
  const keepOpt = prev.filter((q) => q.optimistic && !next.some((x) => x.id === q.id))
343
337
  if (keepOpt.length > 0) next.push(...keepOpt)
344
338
  // 保留「已从服务端消失、但尚未收尾」的任务:收尾(settleTask)负责移除。
@@ -402,8 +396,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
402
396
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
403
397
  attempts: Array.isArray((a as { attempts?: unknown }).attempts) ? ((a as { attempts: string[] }).attempts) : [],
404
398
  needsRestart: typeof (a as { needsRestart?: unknown }).needsRestart === 'boolean' ? (a as { needsRestart: boolean }).needsRestart : true,
405
- // 服务端登记的入口渠道:自定义安装刷新后也能精准溯源(目录安装无此字段)
406
- channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh') ? a.installChannel : undefined,
399
+ // 服务端登记的入口渠道:自定义安装刷新后也能精准溯源(目录安装无此字段)。
400
+ // 旧服务端可能回传已废弃的 'git'/'dsh' 渠道,忽略即可。
401
+ channel: a.installChannel === 'npm' ? 'npm' : undefined,
407
402
  }
408
403
  })
409
404
  if (items.length > 0 || pendingRef.current.length > 0) {
@@ -411,7 +406,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
411
406
  // 刷新后恢复的任务同样保持折叠:摘要条提示进行中,点开才看明细/日志
412
407
  pollQueue()
413
408
  }
414
- } catch { /* host without the plugin's server routes */ }
409
+ } catch { /* 宿主未挂本插件服务端路由 */ }
415
410
  }
416
411
  void restore()
417
412
  return () => { cancelled = true }
@@ -420,9 +415,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
420
415
  /** 弹窗动作:直接安装。请求宿主本地路由,任务进入服务端队列(FIFO),弹窗内实时显示进度。 */
421
416
  /** 安装入队核心:目录插件(catalog,后端走目录白名单)与命令行安装(custom,
422
417
  * 受安全开关控制)共用。队列条目一律用仓库名(repo)展示/防重,只有发给后端的
423
- * 实际安装目标(target)随通道变化(npm 包名 / github: 源)。 */
418
+ * 实际安装目标(target)为 npm 包名。 */
424
419
  const submitInstall = async (input: {
425
- /** 发给后端的实际安装目标:npm 包名或 github: 源(服务端再决定最终通道) */
420
+ /** 发给后端的实际安装目标:npm 包名 */
426
421
  target: string
427
422
  /** 队列展示与防重用的仓库名 */
428
423
  repo: string
@@ -435,7 +430,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
435
430
  update?: boolean
436
431
  /** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
437
432
  globalNpm?: string[]
438
- /** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):随请求上报服务端落日志,
433
+ /** 自定义安装入口渠道(NPM 包卡片):随请求上报服务端落日志,
439
434
  * 失败弹窗/通知据此精准溯源「从哪个入口发起」;目录安装不传。 */
440
435
  channel?: InstallChannel
441
436
  }) => {
@@ -445,7 +440,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
445
440
  if (submittingRef.current.has(input.repo)) return
446
441
  submittingRef.current.add(input.repo)
447
442
  // 乐观入队:点击安装立即显示「正在安装中」,不等 POST 响应。
448
- // 服务端 /install 会先跑预检(preflight,可能耗时数秒)再入队,若等响应才入队列,
443
+ // 服务端 /install 会先做校验与网络探测再入队,若等响应才入队列,
449
444
  // 用户点完安装立刻去看任务会看不到 —— 只有刷新后 restore 才拉回来。
450
445
  // 用负临时 id 占位,响应回来换成真实 task id;失败/出错则移除该占位。
451
446
  tempIdRef.current -= 1
@@ -458,7 +453,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
458
453
  command: input.command,
459
454
  channel: input.channel,
460
455
  action: input.update ? 'update' : 'add',
461
- // 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
456
+ // 服务端登记后 /active 会带回真实执行过的命令,先占位空列表
462
457
  attempts: [], needsRestart: true,
463
458
  }])
464
459
  setSubmitting(true)
@@ -473,9 +468,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
473
468
  display: input.repo,
474
469
  source: input.source,
475
470
  mode: input.update ? 'update' : undefined,
476
- // 界面语言带给服务端:网络预检等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
471
+ // 界面语言带给服务端:安装前网络探测等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
477
472
  ...(langKey ? { lang: langKey } : {}),
478
- // 自定义安装入口渠道(三张卡片之一):服务端日志/报错提示据此溯源
473
+ // 安装入口渠道(只可能是 npm):服务端日志/报错提示据此溯源
479
474
  ...(input.channel ? { installChannel: input.channel } : {}),
480
475
  ...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
481
476
  }),
@@ -485,7 +480,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
485
480
  if (!data.ok || typeof data.task !== 'number') {
486
481
  // 请求层失败(重复安装 409 / 参数错误 / 安全开关 403 等):移除乐观条目 + 完整错误弹窗
487
482
  applyQueue((prev) => prev.filter((x) => x.id !== tempId))
488
- // 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
483
+ // 同步 400(如安装前网络探测拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
489
484
  // 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白。
490
485
  // 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
491
486
  const entry = entryLineOf(t, input.channel)
@@ -521,12 +516,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
521
516
  }
522
517
  }
523
518
 
524
- /** 目录插件安装:通道由目录数据(npmPackage)决定,走 catalog 白名单。 */
519
+ /** 目录插件安装:安装目标取目录数据的 npm 包名,走 catalog 白名单。 */
525
520
  const installNow = async (p: HubPlugin, opts?: { update?: boolean }) => {
526
521
  const repo = p.source?.repo ?? ''
527
522
  if (!repo) return
528
- // 安装通道决策(用户无感知):有 npm 包名 → 走 npm;否则 git 直装。
529
- const { target } = installTargetOf(p)
523
+ // 目录条目必带 npm 包名:直接用包名安装(用户无感知)。
524
+ const target = installTargetOf(p)
530
525
  if (!target) return
531
526
  await submitInstall({
532
527
  target,
@@ -540,12 +535,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
540
535
  })
541
536
  }
542
537
 
543
- /** 命令行安装:用户手输 npm 包名或 GitHub 地址,走 custom 源(受安全开关控制)。
538
+ /** 命令行安装:用户手输 npm 包名,走 custom 源(受安全开关控制)。
544
539
  * update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
545
- * channel=入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):上报服务端落日志,
546
- * 失败弹窗/通知据此精准溯源。
547
- * 队列展示/防重一律用归一化身份(owner/repo 或 npm 包名):与服务端 /active 合并、
548
- * 弹窗目标匹配同口径,npm 包名透传不受影响。 */
540
+ * channel=入口渠道(NPM 包卡片):上报服务端落日志,失败弹窗/通知据此精准溯源。
541
+ * 队列展示/防重一律用归一化身份:与服务端 /active 合并、弹窗目标匹配同口径,
542
+ * npm 包名透传不受影响。 */
549
543
  const installCustom = async (raw: string, opts?: { update?: boolean; channel?: InstallChannel }) => {
550
544
  const target = raw.trim()
551
545
  if (!target) return
@@ -3,9 +3,8 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * dsh-plugin client entry: wires the Plugin Hub into the host settings via
7
- * the module-loader bundle (see tsdown.config.ts). All UI logic lives in
8
- * PluginHubSection; this file only performs the cordis apply wiring.
6
+ * dsh-plugin 客户端入口:通过模块加载器 bundle(见 tsdown.config.ts)把插件中心接入宿主设置。
7
+ * 全部 UI 逻辑都在 PluginHubSection;本文件只做 cordis apply 接线。
9
8
  */
10
9
  import { createElement as h } from 'react'
11
10
  import type { HubClientContext } from './types.ts'