dsh-plugin 1.1.2 → 1.2.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 (110) hide show
  1. package/client/client.js +4469 -1445
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +438 -22
  4. package/lib/index.js +1 -1
  5. package/lib/services/install/install.js +4 -0
  6. package/lib/services/install/npm-check.js +55 -0
  7. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
  8. package/lib/services/{preflight.js → install/preflight.js} +15 -4
  9. package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
  10. package/lib/services/loader.js +56 -1
  11. package/lib/services/log.js +138 -0
  12. package/lib/services/probe.js +157 -0
  13. package/lib/services/{profile.js → profile/profile.js} +29 -1
  14. package/lib/services/settings.js +60 -0
  15. package/lib/types/http/routes.d.ts +1 -1
  16. package/lib/types/index.d.ts +1 -1
  17. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
  18. package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
  19. package/lib/types/services/install/npm-check.d.ts +12 -0
  20. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  21. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  22. package/lib/types/services/loader.d.ts +14 -8
  23. package/lib/types/services/log.d.ts +49 -0
  24. package/lib/types/services/probe.d.ts +30 -0
  25. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
  26. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
  27. package/lib/types/services/settings.d.ts +24 -0
  28. package/package.json +1 -1
  29. package/src/client/components/PluginHubSection.tsx +341 -120
  30. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
  31. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
  32. package/src/client/components/layout/CatalogControls.tsx +80 -0
  33. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
  34. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
  35. package/src/client/components/layout/SectionTabs.tsx +60 -0
  36. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
  37. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
  38. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  39. package/src/client/components/modals/LogsModal.tsx +301 -0
  40. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  41. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
  42. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
  43. package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
  44. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
  45. package/src/client/components/ui/Toggle.tsx +28 -0
  46. package/src/client/components/ui/icons.tsx +343 -0
  47. package/src/client/components/views/CustomInstallView.tsx +250 -0
  48. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  49. package/src/client/components/views/InstalledView.tsx +328 -0
  50. package/src/client/components/views/LogsView.tsx +150 -0
  51. package/src/client/components/views/MarketView.tsx +66 -0
  52. package/src/client/components/views/SettingsView.tsx +273 -0
  53. package/src/client/data/catalog.ts +38 -0
  54. package/src/client/data/host.ts +107 -0
  55. package/src/client/data/hub.ts +41 -0
  56. package/src/client/hooks/useCatalog.ts +71 -115
  57. package/src/client/hooks/useIncrementalList.ts +46 -0
  58. package/src/client/hooks/useLanguage.ts +40 -0
  59. package/src/client/hooks/useSettings.ts +90 -0
  60. package/src/client/hooks/useTaskQueue.ts +156 -39
  61. package/src/client/locales.ts +358 -38
  62. package/src/client/logic/constants.ts +87 -0
  63. package/src/client/{lib → logic}/failures.ts +95 -74
  64. package/src/client/logic/install-command.ts +56 -0
  65. package/src/client/logic/installed.ts +207 -0
  66. package/src/client/logic/normalize.ts +52 -0
  67. package/src/client/logic/urls.ts +125 -0
  68. package/src/client/styles/CustomInstallView.module.css +191 -0
  69. package/src/client/styles/DiagnosticsView.module.css +231 -0
  70. package/src/client/styles/Dropdown.module.css +42 -41
  71. package/src/client/styles/Header.module.css +100 -132
  72. package/src/client/styles/InstalledView.module.css +447 -0
  73. package/src/client/styles/List.module.css +6 -0
  74. package/src/client/styles/LogsView.module.css +503 -0
  75. package/src/client/styles/Modal.module.css +352 -70
  76. package/src/client/styles/SectionTabs.module.css +142 -0
  77. package/src/client/styles/SettingsView.module.css +232 -0
  78. package/src/client/styles/Toggle.module.css +60 -0
  79. package/src/client/styles/tokens.module.css +1 -0
  80. package/src/client/types.ts +23 -3
  81. package/src/server/http/routes.ts +414 -22
  82. package/src/server/index.ts +1 -1
  83. package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
  84. package/src/server/services/{install.ts → install/install.ts} +5 -5
  85. package/src/server/services/install/npm-check.ts +53 -0
  86. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
  87. package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
  88. package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
  89. package/src/server/services/loader.ts +55 -1
  90. package/src/server/services/log.ts +167 -0
  91. package/src/server/services/probe.ts +157 -0
  92. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
  93. package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
  94. package/src/server/services/settings.ts +79 -0
  95. package/lib/services/install.js +0 -4
  96. package/src/client/components/CatalogControls.tsx +0 -108
  97. package/src/client/components/icons.tsx +0 -129
  98. package/src/client/lib/catalog.ts +0 -293
  99. package/src/client/lib/env.ts +0 -20
  100. /package/lib/services/{install-types.js → install/install-types.js} +0 -0
  101. /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
  102. /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
  103. /package/lib/services/{progress.js → profile/progress.js} +0 -0
  104. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  105. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
  106. /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
  107. /package/src/client/{lib → logic}/format.ts +0 -0
  108. /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
  109. /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
  110. /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
@@ -5,24 +5,37 @@
5
5
  *
6
6
  * Catalog data + view state for the Plugin Hub section.
7
7
  *
8
- * Owns the online-data pipeline (live fetch from dsh-plugin.org), the local
8
+ * Owns the online-data pipeline (live fetch from api.dsh-plugin.org), the local
9
9
  * installed-plugin table, and the filter/search/sort/install-status view
10
10
  * state, exposing the derived visible list and per-category counts.
11
11
  */
12
12
  import { useEffect, useMemo, useState } from 'react'
13
13
  import type { HubPlugin, LocaleId } from '../types.ts'
14
- import { HUB_ABOUT_URL, HUB_REPO, HUB_UPDATE_URL, normalize, PLUGIN_VERSION, repoFromInstallTarget } from '../lib/catalog.ts'
15
- import type { HubAboutInfo, HubUpdateInfo } from '../lib/catalog.ts'
16
- import type { SortKey } from '../lib/catalog.ts'
14
+ import { HUB_REPO, PLUGIN_VERSION } from '../logic/constants.ts'
15
+ import type { HubAboutInfo, HubUpdateInfo } from '../types.ts'
16
+ import type { SortKey } from '../logic/constants.ts'
17
+ import { fetchCatalog, fetchStats } from '../data/catalog.ts'
18
+ import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
19
+ import { fetchInstalled } from '../data/host.ts'
20
+ import {
21
+ hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf,
22
+ } from '../logic/installed.ts'
23
+ import type { InstalledItem, InstalledVersionSignal } from '../logic/installed.ts'
17
24
 
18
- const PLUGINS_URL = (lang: LocaleId) => `https://dsh-plugin.org/api/plugins.${lang}.json`
19
- const STATS_URL = 'https://dsh-plugin.org/api/stats.json'
20
25
  /** 插件市场自身仓库:DSH-Plugin Hub 不显示在目录里(自己不进自己的插件列表) */
21
26
  const SELF_REPO = 'dshplugin/dsh-plugin-hub'
22
27
 
28
+ /** 市场各排序的默认方向:全部按倒序(Star/Fork 多、更新/收录近的在前) */
29
+ const SORT_DEFAULT_DIR: Record<SortKey, 'asc' | 'desc'> = {
30
+ sortStars: 'desc', sortForks: 'desc', sortUpdated: 'desc', sortNewest: 'desc',
31
+ }
32
+
23
33
  export function useCatalog(lang: LocaleId) {
24
34
  /** 目录插件(仅保留人工验证通过的条目) */
25
35
  const [plugins, setPlugins] = useState<HubPlugin[] | null>(null)
36
+ /** Hub 自身目录条目(dshplugin/dsh-plugin-hub):目录过滤会排除自身(不进插件列表),
37
+ * 此处单独保留,供头部「可更新」徽标 → 「直接更新」覆盖重装使用。 */
38
+ const [hubPlugin, setHubPlugin] = useState<HubPlugin | null>(null)
26
39
  /** 收录/精选统计(官网 /api/stats.json 实时拉取) */
27
40
  const [stats, setStats] = useState<{ total: number; verified: number } | null>(null)
28
41
  const [failed, setFailed] = useState(false)
@@ -30,44 +43,51 @@ export function useCatalog(lang: LocaleId) {
30
43
  const [category, setCategory] = useState('all')
31
44
  const [query, setQuery] = useState('')
32
45
  const [sort, setSort] = useState<SortKey>('sortStars')
33
- /** 列表安装状态筛选:全部 / 已安装 / 未安装(分段按钮,单列表内切换,不引入第二个列表) */
34
- const [installedFilter, setInstalledFilter] = useState<'all' | 'installed' | 'notInstalled'>('all')
46
+ /** 当前排序方向:点同一个排序按钮切换 正序/倒序;点新排序用该排序的默认方向(与已安装视图一致) */
47
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
48
+
49
+ /** 点击排序按钮:同一按钮切换正/倒序,新按钮用默认方向 */
50
+ function toggleSort(key: SortKey) {
51
+ if (key === sort) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
52
+ else { setSort(key); setSortDir(SORT_DEFAULT_DIR[key]) }
53
+ }
35
54
  /** 当前 profile 已安装插件:npm 包名 -> manifest spec(来自宿主本地路由) */
36
55
  const [installed, setInstalled] = useState<Record<string, string>>({})
37
56
  /** 安装时记录的目录信号:repo(小写) -> { version, updatedAt }(来自宿主本地路由);
38
57
  * npmPackage 为 npm 优先通道反查命中的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
39
- const [versions, setVersions] = useState<Record<string, { version: string; updatedAt: string; npmPackage?: string }>>({})
40
- /** Hub 自我更新信息:来自 CF Worker 版本控制中心(hub.dsh-plugin.org),与目录数据解耦 */
58
+ const [versions, setVersions] = useState<Record<string, InstalledVersionSignal>>({})
59
+ /** 每个依赖在系统上的安装目录(profile/node_modules/<包名>),详情视图展示用 */
60
+ const [installPaths, setInstallPaths] = useState<Record<string, string> | null>(null)
61
+ /** 已加载进运行中 loader 的包名(官方 ctx.loader 对账):装完未重启的新插件不在其中 */
62
+ const [loadedNames, setLoadedNames] = useState<string[] | null>(null)
63
+ /** 真正的 dsh 插件包名(包内声明 dsh 配置 / 在 profile bundles 清单):非 dsh 插件不提示「待重启」 */
64
+ const [dshCapableNames, setDshCapableNames] = useState<string[] | null>(null)
65
+ /** Hub 自我更新信息:来自接口中心 Pages(api.dsh-plugin.org),与目录数据解耦 */
41
66
  const [hubUpdateInfo, setHubUpdateInfo] = useState<HubUpdateInfo | null>(null)
42
- /** 头部「关注我们」弹窗内容(平台介绍 + 反馈群二维码):来自同款 Worker /about,Markdown 推送非写死 */
67
+ /** 头部「关注我们」弹窗内容(平台介绍 + 反馈群二维码):来自接口中心 /about,Markdown 推送非写死 */
43
68
  const [hubAboutInfo, setHubAboutInfo] = useState<HubAboutInfo | null>(null)
44
69
 
45
70
  // 拉取目录 + 统计。在线 API 已只返回 verified;再过滤一次,保证只展示人工验证通过的插件。
46
71
  useEffect(() => {
47
72
  let cancelled = false
48
73
  setPlugins(null)
74
+ setHubPlugin(null)
49
75
  setStats(null)
50
76
  setFailed(false)
51
- const fetchData = (url: string): Promise<unknown> =>
52
- fetch(url, { cache: 'no-store' }).then((res) => {
53
- if (!res.ok) throw new Error(String(res.status))
54
- return res.json()
55
- })
56
77
  // 目录数据直拉:hub 自身版本由独立 Worker 版本控制中心管理,不再依赖主站版本号接口;
57
78
  // 数据接口 CDN 缓存 1 小时,发布新数据后最长 1 小时全网生效。
58
- const load = () =>
59
- Promise.all([fetchData(PLUGINS_URL(lang)), fetchData(STATS_URL)])
79
+ const load = () => Promise.all([fetchCatalog(lang), fetchStats()])
60
80
  load()
61
- .then(([data, s]) => {
81
+ .then(([list, stats]) => {
62
82
  if (cancelled) return
63
- const list = (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
64
83
  // 插件市场不显示自己:DSH-Plugin Hub 从目录里排除自身条目,
65
84
  // 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
66
85
  // 主站数据已同步排除自身,此处为 CDN 缓存期的兜底。
67
86
  const hadSelf = list.some((p) => p.source?.repo === SELF_REPO)
87
+ // 过滤前单独保留 hub 自身条目:它不进目录列表,但「可更新」徽标的直接更新需要它作为重装目标
88
+ setHubPlugin(list.find((p) => p.source?.repo === SELF_REPO) ?? null)
68
89
  setPlugins(list.filter((p) => p.compatibility?.status === 'verified' && p.source?.repo !== SELF_REPO))
69
- const stats = s as { total?: number; verified?: number }
70
- if (stats && typeof stats.total === 'number' && typeof stats.verified === 'number') {
90
+ if (stats) {
71
91
  setStats(hadSelf
72
92
  ? { total: stats.total - 1, verified: stats.verified - 1 }
73
93
  : { total: stats.total, verified: stats.verified })
@@ -83,15 +103,13 @@ export function useCatalog(lang: LocaleId) {
83
103
 
84
104
  /** 刷新当前 profile 已安装插件表;宿主未挂本地路由时静默降级为空表。 */
85
105
  const refreshInstalled = async () => {
86
- try {
87
- const res = await fetch('/dsh-plugin-hub/installed', { cache: 'no-store' })
88
- if (!res.ok) return
89
- const data = await res.json() as { installed?: Record<string, string>; versions?: Record<string, { version: string; updatedAt: string; npmPackage?: string }> }
90
- setInstalled(data.installed ?? {})
91
- setVersions(data.versions ?? {})
92
- } catch {
93
- // host without the plugin's server routes — keep the empty table
94
- }
106
+ const data = await fetchInstalled()
107
+ if (data === null) return
108
+ setInstalled(data.installed)
109
+ setVersions(data.versions)
110
+ setInstallPaths(data.paths)
111
+ setLoadedNames(data.loaded)
112
+ setDshCapableNames(data.dshCapable)
95
113
  }
96
114
 
97
115
  // 首次进入拉取已安装表(依赖宿主 webServer 服务)
@@ -102,16 +120,7 @@ export function useCatalog(lang: LocaleId) {
102
120
  // 失败静默降级为 null(徽标不出现,不影响目录本身加载)。
103
121
  useEffect(() => {
104
122
  let cancelled = false
105
- fetch(HUB_UPDATE_URL, { cache: 'no-store' })
106
- .then((res): Promise<HubUpdateInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
107
- .then((data) => {
108
- if (cancelled || !data) { if (!cancelled) setHubUpdateInfo(null); return }
109
- const version = typeof data.version === 'string' && data.version.length > 0 ? data.version : null
110
- setHubUpdateInfo(version === null
111
- ? null
112
- : { version, publishedAt: data.publishedAt ?? null, notes: data.notes ?? null })
113
- })
114
- .catch(() => { if (!cancelled) setHubUpdateInfo(null) })
123
+ void fetchHubUpdate().then((info) => { if (!cancelled) setHubUpdateInfo(info) })
115
124
  return () => { cancelled = true }
116
125
  }, [reloadKey])
117
126
 
@@ -119,62 +128,28 @@ export function useCatalog(lang: LocaleId) {
119
128
  // 客户端下次进入拉 /about 即可看到最新,非写死。失败静默为 null(弹窗展示兜底文案,不阻塞)。
120
129
  useEffect(() => {
121
130
  let cancelled = false
122
- fetch(HUB_ABOUT_URL, { cache: 'no-store' })
123
- .then((res): Promise<HubAboutInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
124
- .then((data) => {
125
- if (cancelled) return
126
- if (!data || data.content === null || data.content === undefined) { setHubAboutInfo(null); return }
127
- const content = typeof data.content === 'string' || typeof data.content === 'object' ? data.content : null
128
- setHubAboutInfo(content === null
129
- ? null
130
- : { content, updatedAt: data.updatedAt ?? null })
131
- })
132
- .catch(() => { if (!cancelled) setHubAboutInfo(null) })
131
+ void fetchHubAbout().then((info) => { if (!cancelled) setHubAboutInfo(info) })
133
132
  return () => { cancelled = true }
134
133
  }, [reloadKey])
135
134
 
136
135
  /** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
137
- const installedName = (p: HubPlugin): string | null => {
138
- const repo = p.source?.repo
139
- if (!repo) return null
140
- const needle = repo.toLowerCase()
141
- for (const [name, spec] of Object.entries(installed)) {
142
- if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
143
- }
144
- // npm 通道安装:profile 依赖 key 直接是 npm 包名。包名来源两处——目录数据(npmPackage),
145
- // 或服务端 npm 优先反查持久化的映射(versions[repo].npmPackage,覆盖组织 scope 与 GitHub
146
- // 用户名不一致、目录未下发包名的场景);命中任一并依赖 key 真实存在即视为已安装
147
- const pkg = ((p.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? '').toLowerCase()
148
- if (pkg) {
149
- for (const name of Object.keys(installed)) {
150
- if (name.toLowerCase() === pkg) return name
151
- }
152
- }
153
- return null
154
- }
136
+ const installedName = (p: HubPlugin): string | null => installedNameOf(p, installed, versions)
155
137
 
156
138
  /** 该插件安装时记录的目录版本(无记录/未安装 → null)。 */
157
- const installedVersion = (p: HubPlugin): string | null => {
158
- const repo = p.source?.repo
159
- if (!repo) return null
160
- return versions[repo.toLowerCase()]?.version ?? null
161
- }
139
+ const installedVersion = (p: HubPlugin): string | null => installedVersionOf(p, versions)
162
140
 
163
141
  /**
164
142
  * 是否有更新:仅对已安装插件有意义。
165
143
  * 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
166
144
  * (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
167
145
  */
168
- const hasUpdate = (p: HubPlugin): boolean => {
169
- if (installedName(p) === null) return false
170
- const repo = p.source?.repo
171
- if (!repo) return false
172
- const rec = versions[repo.toLowerCase()]
173
- if (!rec) return false
174
- if (p.version) return p.version !== rec.version
175
- const current = p.dates?.repoUpdatedAt
176
- return Boolean(current && rec.updatedAt && current > rec.updatedAt)
177
- }
146
+ const hasUpdate = (p: HubPlugin): boolean => hasUpdateOf(p, installed, versions)
147
+
148
+ /** 已安装项统一列表(目录元数据 + 运行时信息合并):驱动「已安装」tab。 */
149
+ const installedItems = useMemo<InstalledItem[]>(
150
+ () => installedItemsOf(plugins, installed, versions, installPaths, loadedNames, dshCapableNames),
151
+ [plugins, installed, versions, installPaths, loadedNames, dshCapableNames],
152
+ )
178
153
 
179
154
  /**
180
155
  * Hub 自身是否有可用更新:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
@@ -195,31 +170,11 @@ export function useCatalog(lang: LocaleId) {
195
170
  return plugins.filter((p) => p.category === category)
196
171
  }, [plugins, category])
197
172
 
198
- /** 当前分类下已安装插件数:已安装/未安装按钮上的计数跟随分类,不再用全局口径。 */
199
- const installedCountInCategory = useMemo(() => {
200
- return categoryPlugins.reduce((n, p) => n + (installedName(p) !== null ? 1 : 0), 0)
201
- }, [categoryPlugins, installed])
202
-
203
- /** 当前分类下未安装插件数。 */
204
- const notInstalledCountInCategory = Math.max(0, categoryPlugins.length - installedCountInCategory)
205
-
206
- // 自动重置:切换分类(或已安装表变化)后,若当前安装状态筛选在新分类下无结果,
207
- // 自动退回「全部」,避免列表空置死胡同、也避免 0 计数按钮被误触。
208
- useEffect(() => {
209
- if (installedFilter === 'all') return
210
- const count = installedFilter === 'installed'
211
- ? installedCountInCategory
212
- : notInstalledCountInCategory
213
- if (count === 0) setInstalledFilter('all')
214
- }, [installedFilter, installedCountInCategory, notInstalledCountInCategory])
215
-
216
173
  const visible = useMemo(() => {
217
174
  if (!plugins) return []
218
175
  const q = query.trim().toLowerCase()
219
176
  const list = plugins.filter((p) => {
220
177
  if (category !== 'all' && p.category !== category) return false
221
- if (installedFilter === 'installed' && installedName(p) === null) return false
222
- if (installedFilter === 'notInstalled' && installedName(p) !== null) return false
223
178
  if (!q) return true
224
179
  return (
225
180
  (p.displayName ?? '').toLowerCase().includes(q) ||
@@ -227,13 +182,15 @@ export function useCatalog(lang: LocaleId) {
227
182
  (p.topics ?? []).some((topic) => topic.toLowerCase().includes(q))
228
183
  )
229
184
  })
185
+ const dir = sortDir === 'asc' ? 1 : -1
230
186
  return [...list].sort((a, b) => {
231
- if (sort === 'sortStars') return (b.stats?.stargazers_count ?? 0) - (a.stats?.stargazers_count ?? 0)
232
- if (sort === 'sortForks') return (b.stats?.forks_count ?? 0) - (a.stats?.forks_count ?? 0)
233
- if (sort === 'sortNewest') return (b.dates?.addedAt ?? '').localeCompare(a.dates?.addedAt ?? '')
234
- return (b.dates?.repoUpdatedAt ?? '').localeCompare(a.dates?.repoUpdatedAt ?? '')
187
+ // 比较器统一用「升序写法」再乘 dir:desc 时 dir=-1 翻成降序(Star/Fork 最多、更新/收录最近在前)
188
+ if (sort === 'sortStars') return ((a.stats?.stargazers_count ?? 0) - (b.stats?.stargazers_count ?? 0)) * dir
189
+ if (sort === 'sortForks') return ((a.stats?.forks_count ?? 0) - (b.stats?.forks_count ?? 0)) * dir
190
+ if (sort === 'sortNewest') return (a.dates?.addedAt ?? '').localeCompare(b.dates?.addedAt ?? '') * dir
191
+ return (a.dates?.repoUpdatedAt ?? '').localeCompare(b.dates?.repoUpdatedAt ?? '') * dir
235
192
  })
236
- }, [plugins, category, query, sort, installed, installedFilter])
193
+ }, [plugins, category, query, sort, sortDir, installed])
237
194
 
238
195
  /** Per-category plugin counts shown on the category chips. */
239
196
  const categoryCounts = useMemo(() => {
@@ -246,10 +203,12 @@ export function useCatalog(lang: LocaleId) {
246
203
 
247
204
  return {
248
205
  plugins,
206
+ hubPlugin,
249
207
  stats,
250
208
  failed,
251
209
  reload: () => setReloadKey((k) => k + 1),
252
210
  installed,
211
+ installedItems,
253
212
  installedName,
254
213
  installedVersion,
255
214
  hasUpdate,
@@ -262,13 +221,10 @@ export function useCatalog(lang: LocaleId) {
262
221
  query,
263
222
  setQuery,
264
223
  sort,
265
- setSort,
266
- installedFilter,
267
- setInstalledFilter,
224
+ sortDir,
225
+ toggleSort,
268
226
  visible,
269
227
  total: plugins?.length ?? 0,
270
- installedCountInCategory,
271
- notInstalledCountInCategory,
272
228
  categoryCounts,
273
229
  }
274
230
  }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
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 center 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.
11
+ */
12
+ import { useEffect, useRef, useState } from 'react'
13
+
14
+ /** 首批渲染条数 & 每次滚到底追加的增量(单卡约 80px 高,100 条已远超一屏) */
15
+ const PAGE_SIZE = 100
16
+ /** 哨兵进入视口底部前 800px 就提前加载下一批,滚到底时无感知衔接 */
17
+ const PRELOAD_MARGIN = '800px 0px'
18
+
19
+ export function useIncrementalList<T>(items: T[]) {
20
+ const [limit, setLimit] = useState(PAGE_SIZE)
21
+ const sentinelRef = useRef<HTMLDivElement | null>(null)
22
+
23
+ // 筛选/分类/排序变化 → items 引用变化 → 窗口重置回首批
24
+ useEffect(() => {
25
+ setLimit(PAGE_SIZE)
26
+ }, [items])
27
+
28
+ // 哨兵进入视口(含预加载区)→ 追加下一批;已全部渲染则不再观察
29
+ useEffect(() => {
30
+ const el = sentinelRef.current
31
+ if (!el || limit >= items.length) return
32
+ const io = new IntersectionObserver((entries) => {
33
+ for (const entry of entries) {
34
+ if (entry.isIntersecting) setLimit((cur) => Math.min(cur + PAGE_SIZE, items.length))
35
+ }
36
+ }, { rootMargin: PRELOAD_MARGIN })
37
+ io.observe(el)
38
+ return () => io.disconnect()
39
+ }, [items.length, limit])
40
+
41
+ return {
42
+ shown: items.slice(0, limit),
43
+ hasMore: limit < items.length,
44
+ sentinelRef,
45
+ }
46
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Language state for the section: follows the host (system) locale by
7
+ * default, with an optional manual toggle that wins until the user changes
8
+ * it back. Exposes the localized dictionary lookup `t()` plus the zh/en
9
+ * language flag and the /zh/ URL path prefix.
10
+ */
11
+ import { useState } from 'react'
12
+ import type { LocaleId, SectionProps } from '../types.ts'
13
+ import { en, zh } from '../locales.ts'
14
+ import { langPathOf } from '../logic/urls.ts'
15
+
16
+ export function useLanguage(locale: SectionProps['locale']) {
17
+ /** 界面语言:默认跟随宿主(系统)语言;右上角按钮可手动切换,切换后以手动选择为准 */
18
+ const [manualLang, setManualLang] = useState<LocaleId | null>(null)
19
+ const lang: LocaleId = manualLang ?? locale.getSnapshot().active
20
+ const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
21
+ // dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
22
+ const langPath = langPathOf(lang)
23
+
24
+ // 界面语言跟随宿主(系统)locale 自动切换;宿主的 t() 绑定宿主 locale,
25
+ // 这里基于本地字典自建翻译函数,与宿主 locale 保持一致。
26
+ // (settings.section 的导航 label 仍在 apply 里用宿主 t(),跟随宿主语言。)
27
+ const dict: Record<string, string> = langKey === 'en' ? en : zh
28
+ const t = (key: string, params?: Record<string, string | number>): string => {
29
+ const raw = dict[key] ?? key
30
+ return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? '')) : raw
31
+ }
32
+
33
+ return {
34
+ lang,
35
+ langKey,
36
+ langPath,
37
+ t,
38
+ toggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
39
+ }
40
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
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).
11
+ */
12
+ import { useCallback, useEffect, useState } from 'react'
13
+
14
+ export interface HubSettings {
15
+ /** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
16
+ checkUpdatesOnStart: boolean
17
+ /** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
18
+ npmRegistry: string
19
+ /** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
20
+ proxy: string
21
+ /** 启用命令行安装 NPM 包(默认开) */
22
+ enableNpmInstall: boolean
23
+ /** 启用命令行安装 GitHub 源码(默认开) */
24
+ enableGitInstall: boolean
25
+ /** 启用命令行粘贴 dsh plugin 命令安装(默认开) */
26
+ enableDshInstall: boolean
27
+ /** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log) */
28
+ logPath: string
29
+ }
30
+
31
+ export const DEFAULT_SETTINGS: HubSettings = {
32
+ checkUpdatesOnStart: true,
33
+ npmRegistry: '',
34
+ proxy: '',
35
+ enableNpmInstall: true,
36
+ enableGitInstall: true,
37
+ enableDshInstall: true,
38
+ logPath: '',
39
+ }
40
+
41
+ async function loadRemote(): Promise<HubSettings> {
42
+ try {
43
+ const res = await fetch('/dsh-plugin-hub/settings', { cache: 'no-store' })
44
+ if (!res.ok) throw new Error(`settings ${res.status}`)
45
+ const data = await res.json() as Partial<HubSettings>
46
+ return { ...DEFAULT_SETTINGS, ...data }
47
+ } catch {
48
+ return DEFAULT_SETTINGS
49
+ }
50
+ }
51
+
52
+ export function useSettings() {
53
+ const [settings, setSettings] = useState<HubSettings>(DEFAULT_SETTINGS)
54
+ /** 服务端配置是否已加载:更新策略等启动期逻辑须等真实值,避免用默认值误触发 */
55
+ const [ready, setReady] = useState(false)
56
+
57
+ useEffect(() => {
58
+ let alive = true
59
+ void loadRemote().then((s) => {
60
+ if (!alive) return
61
+ setSettings(s)
62
+ setReady(true)
63
+ })
64
+ return () => { alive = false }
65
+ }, [])
66
+
67
+ /** 部分更新:本地立即生效(乐观),同时 POST 持久化;持久化失败静默(本次会话仍生效)。 */
68
+ const update = useCallback((patch: Partial<HubSettings>) => {
69
+ setSettings((prev) => ({ ...prev, ...patch }))
70
+ void fetch('/dsh-plugin-hub/settings', {
71
+ method: 'POST',
72
+ headers: { 'content-type': 'application/json' },
73
+ body: JSON.stringify(patch),
74
+ cache: 'no-store',
75
+ }).catch(() => { /* 宿主未挂路由:仅本次会话生效 */ })
76
+ }, [])
77
+
78
+ /** 重置为默认设置:本地立即回默认,同时通知服务端删除设置文件;失败静默。 */
79
+ const reset = useCallback(() => {
80
+ setSettings({ ...DEFAULT_SETTINGS })
81
+ void fetch('/dsh-plugin-hub/settings/reset', {
82
+ method: 'POST',
83
+ headers: { 'content-type': 'application/json' },
84
+ body: '{}',
85
+ cache: 'no-store',
86
+ }).catch(() => { /* 宿主未挂路由:仅本次会话回默认 */ })
87
+ }, [])
88
+
89
+ return { settings, ready, update, reset }
90
+ }