dsh-plugin 0.1.1 → 1.1.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 (67) hide show
  1. package/README.en.md +56 -28
  2. package/README.md +56 -28
  3. package/client/client.js +2282 -230
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +432 -0
  6. package/lib/index.js +17 -8
  7. package/lib/services/install-types.js +11 -0
  8. package/lib/services/install.js +4 -0
  9. package/lib/services/installed-versions.js +66 -0
  10. package/lib/services/loader.js +98 -0
  11. package/lib/services/npm-resolve.js +67 -0
  12. package/lib/services/pending-restart.js +34 -0
  13. package/lib/services/preflight.js +94 -0
  14. package/lib/services/profile.js +88 -0
  15. package/lib/services/progress.js +50 -0
  16. package/lib/services/task-queue.js +457 -0
  17. package/lib/types/http/routes.d.ts +23 -0
  18. package/lib/types/index.d.ts +27 -0
  19. package/lib/types/services/install-types.d.ts +69 -0
  20. package/lib/types/services/install.d.ts +18 -0
  21. package/lib/types/services/installed-versions.d.ts +24 -0
  22. package/lib/types/services/loader.d.ts +43 -0
  23. package/lib/types/services/npm-resolve.d.ts +6 -0
  24. package/lib/types/services/pending-restart.d.ts +13 -0
  25. package/lib/types/services/preflight.d.ts +6 -0
  26. package/lib/types/services/profile.d.ts +24 -0
  27. package/lib/types/services/progress.d.ts +17 -0
  28. package/lib/types/services/task-queue.d.ts +82 -0
  29. package/package.json +27 -6
  30. package/src/client/components/CatalogControls.tsx +104 -0
  31. package/src/client/components/CatalogHeader.tsx +78 -0
  32. package/src/client/components/CatalogList.tsx +75 -0
  33. package/src/client/components/CategoryTabs.tsx +35 -0
  34. package/src/client/components/Dropdown.tsx +80 -0
  35. package/src/client/components/NotificationsModal.tsx +243 -0
  36. package/src/client/components/PluginCard.tsx +104 -0
  37. package/src/client/components/PluginHubSection.tsx +342 -0
  38. package/src/client/components/ProgressView.tsx +23 -0
  39. package/src/client/components/icons.tsx +125 -0
  40. package/src/client/components/modals.tsx +368 -0
  41. package/src/client/hooks/useCatalog.ts +202 -0
  42. package/src/client/hooks/useTaskQueue.ts +502 -0
  43. package/src/client/index.tsx +8 -422
  44. package/src/client/lib/catalog.ts +240 -0
  45. package/src/client/lib/env.ts +16 -0
  46. package/src/client/lib/failures.ts +165 -0
  47. package/src/client/lib/format.ts +22 -0
  48. package/src/client/locales.ts +188 -18
  49. package/src/client/styles/Dropdown.module.css +123 -0
  50. package/src/client/styles/Header.module.css +432 -0
  51. package/src/client/styles/List.module.css +324 -0
  52. package/src/client/styles/Modal.module.css +1006 -0
  53. package/src/client/styles/tokens.module.css +91 -0
  54. package/src/client/types.ts +80 -0
  55. package/src/server/http/routes.ts +432 -0
  56. package/src/server/index.ts +35 -0
  57. package/src/server/services/install-types.ts +69 -0
  58. package/src/server/services/install.ts +18 -0
  59. package/src/server/services/installed-versions.ts +84 -0
  60. package/src/server/services/loader.ts +110 -0
  61. package/src/server/services/npm-resolve.ts +67 -0
  62. package/src/server/services/pending-restart.ts +36 -0
  63. package/src/server/services/preflight.ts +97 -0
  64. package/src/server/services/profile.ts +89 -0
  65. package/src/server/services/progress.ts +48 -0
  66. package/src/server/services/task-queue.ts +492 -0
  67. package/src/client/Section.module.css +0 -589
@@ -1,435 +1,21 @@
1
1
  /**
2
- * dsh-plugin client: a native "Plugin Hub" settings section that renders
3
- * the community marketplace (dsh-plugin.org) as a first-class catalog —
4
- * category tabs (11 categories + All), search, sorting and one-click
5
- * install-command copy. Plugin data is fetched live from dsh-plugin.org,
6
- * so the browser bundle stays small and always in sync with the site.
7
- *
8
- * Built by tsdown into the __ModuleLoader__ factory bundle at client/client.js.
2
+ * dsh-plugin client entry: wires the Plugin Hub into the host settings via
3
+ * the module-loader bundle (see tsdown.config.ts). All UI logic lives in
4
+ * PluginHubSection; this file only performs the cordis apply wiring.
9
5
  */
10
- import { createElement as h, useEffect, useMemo, useRef, useState } from 'react'
11
- import styles from './Section.module.css'
6
+ import { createElement as h } from 'react'
7
+ import type { HubClientContext } from './types.ts'
12
8
  import { en, zh } from './locales.ts'
9
+ import { PluginHubSection } from './components/PluginHubSection.tsx'
10
+ import './styles/tokens.module.css'
13
11
 
14
12
  const NS = 'dsh-plugin'
15
- const SITE_URL = 'https://dsh-plugin.org/'
16
-
17
- type LocaleId = 'zh' | 'en'
18
-
19
- /** The subset of the locale service this plugin touches. */
20
- interface LocaleService {
21
- register(namespace: string, dicts: { zh: Record<string, string>; en: Record<string, string> }): unknown
22
- bind(namespace: string): (key: string, params?: Record<string, string | number>) => string
23
- getSnapshot(): { active: LocaleId }
24
- subscribe(fn: () => void): () => void
25
- }
26
-
27
- /** The subset of the slots service this plugin touches. */
28
- interface SlotsService {
29
- inject(slot: string, register: () => unknown): void
30
- register(meta: Record<string, unknown>, component: () => unknown): unknown
31
- }
32
-
33
- /** The client cordis context shape this plugin relies on (structural). */
34
- interface HubClientContext {
35
- effect(callback: () => unknown, label?: string): void
36
- slots: SlotsService
37
- locale: LocaleService
38
- }
39
-
40
- interface SectionProps {
41
- t: (key: string, params?: Record<string, string | number>) => string
42
- locale: LocaleService
43
- }
44
-
45
- /** Slim projection of the site's DirectoryPlugin entries. */
46
- interface HubPlugin {
47
- /** 作者小写 key,与 slug 共同构成唯一身份(两级路径 /plugins/{ownerSlug}/{slug});旧数据可能缺省 */
48
- ownerSlug?: string
49
- slug: string
50
- displayName?: string
51
- category?: string
52
- topics?: string[]
53
- features?: string[]
54
- description?: string
55
- source?: { repo?: string }
56
- compatibility?: { status?: string }
57
- dates?: { repoUpdatedAt?: string; addedAt?: string }
58
- stats?: { stargazers_count?: number; forks_count?: number }
59
- }
60
-
61
- const CATEGORY_ORDER = [
62
- 'interface', 'session', 'memory', 'tools', 'agent', 'workflow',
63
- 'integration', 'model', 'dev', 'knowledge', 'fun',
64
- ]
65
-
66
- /**
67
- * 归一化在线 API(dsh-plugin.org/api/plugins.{lang}.json)返回的短 key 结构
68
- * (s/o/n/c/t/f/d/r/v/u/a/sg/fk),统一为 HubPlugin,保证渲染逻辑只认一种结构。
69
- */
70
- function normalize(raw: Record<string, unknown>): HubPlugin {
71
- if (typeof raw.s === 'string') {
72
- return {
73
- slug: raw.s,
74
- ownerSlug: typeof raw.o === 'string' ? raw.o : undefined,
75
- displayName: typeof raw.n === 'string' ? raw.n : undefined,
76
- category: typeof raw.c === 'string' ? raw.c : undefined,
77
- topics: Array.isArray(raw.t) ? (raw.t as string[]) : undefined,
78
- features: Array.isArray(raw.f) ? (raw.f as string[]) : undefined,
79
- description: typeof raw.d === 'string' ? raw.d : undefined,
80
- source: typeof raw.r === 'string' ? { repo: raw.r } : undefined,
81
- compatibility: typeof raw.v === 'string' ? { status: raw.v } : undefined,
82
- dates: {
83
- repoUpdatedAt: typeof raw.u === 'string' ? raw.u : undefined,
84
- addedAt: typeof raw.a === 'string' ? raw.a : undefined,
85
- },
86
- stats: {
87
- stargazers_count: typeof raw.sg === 'number' ? raw.sg : undefined,
88
- forks_count: typeof raw.fk === 'number' ? raw.fk : undefined,
89
- },
90
- }
91
- }
92
- return raw as unknown as HubPlugin
93
- }
94
-
95
- const CATEGORY_LABELS: Record<string, { zh: string; en: string }> = {
96
- interface: { zh: '界面与体验', en: 'UI & Experience' },
97
- session: { zh: '会话与消息', en: 'Sessions & Messages' },
98
- memory: { zh: '记忆与上下文', en: 'Memory & Context' },
99
- tools: { zh: '工具与能力', en: 'Tools & Capabilities' },
100
- agent: { zh: '技能与智能体', en: 'Skills & Agents' },
101
- workflow: { zh: '工作流与自动化', en: 'Workflow & Automation' },
102
- integration: { zh: '集成与连接', en: 'Integrations & Connections' },
103
- model: { zh: '模型与推理', en: 'Models & Reasoning' },
104
- dev: { zh: '开发与运维', en: 'Development & Operations' },
105
- knowledge: { zh: '数据与知识', en: 'Data & Knowledge' },
106
- fun: { zh: '娱乐', en: 'Entertainment' },
107
- }
108
-
109
- const CATEGORY_SHORT_LABELS: Record<string, { zh: string; en: string }> = {
110
- interface: { zh: '界面体验', en: 'UI Exp' },
111
- session: { zh: '会话消息', en: 'Sessions' },
112
- memory: { zh: '记忆上下文', en: 'Memory' },
113
- tools: { zh: '工具能力', en: 'Tools' },
114
- agent: { zh: '技能智能体', en: 'Agents' },
115
- workflow: { zh: '工作流', en: 'Workflow' },
116
- integration: { zh: '集成连接', en: 'Integrations' },
117
- model: { zh: '模型推理', en: 'Models' },
118
- dev: { zh: '开发运维', en: 'Dev' },
119
- knowledge: { zh: '数据知识', en: 'Knowledge' },
120
- fun: { zh: '娱乐', en: 'Fun' },
121
- }
122
-
123
- const SORTS = ['sortStars', 'sortUpdated', 'sortNewest'] as const
124
- type SortKey = (typeof SORTS)[number]
125
-
126
- function fmtStars(count?: number): string {
127
- if (!count || count <= 0) return '0'
128
- if (count < 1000) return String(count)
129
- const k = count / 1000
130
- return `${k >= 100 ? Math.round(k) : k.toFixed(1).replace(/\.0$/, '')}k`
131
- }
132
-
133
- function relTime(iso: string | undefined, t: SectionProps['t']): string {
134
- if (!iso) return ''
135
- const days = Math.floor((Date.now() - new Date(iso).getTime()) / 86400000)
136
- if (days <= 0) return t('today')
137
- if (days < 30) return t('daysAgo', { days })
138
- if (days < 365) return t('monthsAgo', { months: Math.floor(days / 30) })
139
- return t('yearsAgo', { years: Math.floor(days / 365) })
140
- }
141
-
142
- function PluginHubSection({ t: _hostT, locale }: SectionProps) {
143
- const [lang, setLang] = useState<LocaleId>(locale.getSnapshot().active)
144
- const [plugins, setPlugins] = useState<HubPlugin[] | null>(null)
145
- /** 收录/精选统计(官网 /api/stats.json 实时拉取) */
146
- const [stats, setStats] = useState<{ total: number; verified: number } | null>(null)
147
- const [failed, setFailed] = useState(false)
148
- const [reloadKey, setReloadKey] = useState(0)
149
- const [category, setCategory] = useState('all')
150
- const [query, setQuery] = useState('')
151
- const [sort, setSort] = useState<SortKey>('sortStars')
152
- const [copied, setCopied] = useState<string | null>(null)
153
- /** 全局复制成功 Toast:{id} 用于重复点击时重新触发入场动画 */
154
- const [toast, setToast] = useState<{ id: number } | null>(null)
155
- /** 列表滚动容器:分类/搜索切换后列表内容替换但 scrollTop 保留,会让用户误以为列表没更新,需重置回顶部 */
156
- const listRef = useRef<HTMLDivElement | null>(null)
157
-
158
- useEffect(() => {
159
- listRef.current?.scrollTo({ top: 0 })
160
- }, [category])
161
-
162
- useEffect(() => locale.subscribe(() => setLang(locale.getSnapshot().active)), [locale])
163
-
164
- useEffect(() => {
165
- let cancelled = false
166
- setPlugins(null)
167
- setStats(null)
168
- setFailed(false)
169
- const apply = (data: unknown) => {
170
- if (cancelled) return
171
- // 在线 API 已只返回 verified;再过滤一次,保证只展示人工验证通过的插件。
172
- const list = (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
173
- setPlugins(list.filter((p) => p.compatibility?.status === 'verified'))
174
- }
175
- const fail = () => {
176
- if (!cancelled) setFailed(true)
177
- }
178
- const fetchData = (url: string): Promise<unknown> =>
179
- fetch(url).then((res) => {
180
- if (!res.ok) throw new Error(String(res.status))
181
- return res.json()
182
- })
183
- // 在线拉取最新数据(dsh-plugin.org,走 CDN 缓存),插件不发布也能拿到每日更新。
184
- const remote = `https://dsh-plugin.org/api/plugins.${lang}.json`
185
- fetchData(remote).then(apply).catch(fail)
186
- // 收录/精选统计:实时拉取官网 /api/stats.json。
187
- const applyStats = (s: { total: number; verified: number }) => {
188
- if (!cancelled && s && typeof s.total === 'number' && typeof s.verified === 'number') setStats(s)
189
- }
190
- fetchData('https://dsh-plugin.org/api/stats.json')
191
- .then((s) => applyStats(s as { total: number; verified: number }))
192
- .catch(() => {})
193
- return () => { cancelled = true }
194
- }, [lang, reloadKey])
195
-
196
- const visible = useMemo(() => {
197
- if (!plugins) return []
198
- const q = query.trim().toLowerCase()
199
- const list = plugins.filter((p) => {
200
- if (category !== 'all' && p.category !== category) return false
201
- if (!q) return true
202
- return (
203
- (p.displayName ?? '').toLowerCase().includes(q) ||
204
- (p.description ?? '').toLowerCase().includes(q) ||
205
- (p.topics ?? []).some((topic) => topic.toLowerCase().includes(q))
206
- )
207
- })
208
- return [...list].sort((a, b) => {
209
- if (sort === 'sortStars') return (b.stats?.stargazers_count ?? 0) - (a.stats?.stargazers_count ?? 0)
210
- if (sort === 'sortNewest') return (b.dates?.addedAt ?? '').localeCompare(a.dates?.addedAt ?? '')
211
- return (b.dates?.repoUpdatedAt ?? '').localeCompare(a.dates?.repoUpdatedAt ?? '')
212
- })
213
- }, [plugins, category, query, sort])
214
-
215
- const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
216
- // dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
217
- const langPath = lang === 'zh' ? 'zh/' : ''
218
-
219
- // 插件界面语言由组件自身 lang 控制(左上角 EN/中文 按钮),宿主的 t() 绑定宿主 locale、
220
- // 不跟随组件内 lang,因此这里基于本地字典自建翻译函数,保证切换语言后所有文本即时刷新。
221
- // (settings.section 的导航 label 仍在 apply 里用宿主 t(),跟随宿主语言。)
222
- const dict: Record<string, string> = langKey === 'en' ? en : zh
223
- const t = (key: string, params?: Record<string, string | number>): string => {
224
- const raw = dict[key] ?? key
225
- return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? '')) : raw
226
- }
227
- const catLabel = (map: Record<string, { zh: string; en: string }>, key: string): string =>
228
- map[key]?.[langKey] ?? key
229
-
230
- const copyInstall = async (repo: string) => {
231
- const text = `dsh plugin add ${repo}`
232
- let ok = false
233
- try {
234
- await navigator.clipboard.writeText(text)
235
- ok = true
236
- } catch {
237
- // Clipboard API unavailable (permissions/iframe) — fall back to the
238
- // legacy hidden-textarea trick, which works in any context.
239
- const ta = document.createElement('textarea')
240
- ta.value = text
241
- ta.style.position = 'fixed'
242
- ta.style.opacity = '0'
243
- document.body.appendChild(ta)
244
- ta.select()
245
- try {
246
- ok = document.execCommand('copy')
247
- } catch {
248
- ok = false
249
- }
250
- document.body.removeChild(ta)
251
- }
252
- if (ok) {
253
- setCopied(repo)
254
- setToast({ id: Date.now() })
255
- window.setTimeout(() => setCopied((cur) => (cur === repo ? null : cur)), 1600)
256
- window.setTimeout(() => setToast(null), 1800)
257
- } else {
258
- setCopied(null)
259
- }
260
- }
261
-
262
- const total = plugins?.length ?? 0
263
- const count = visible.length
264
-
265
- // 统计数字:官网 /api/stats.json 优先;拉取失败时用已加载列表兜底,避免展示 undefined。
266
- const statsTotal = stats?.total ?? total
267
- const statsVerified = stats?.verified ?? 0
268
-
269
- // Per-category plugin counts shown on the category chips.
270
- const categoryCounts = useMemo(() => {
271
- const counts: Record<string, number> = { all: total }
272
- for (const p of plugins ?? []) {
273
- if (p.category) counts[p.category] = (counts[p.category] ?? 0) + 1
274
- }
275
- return counts
276
- }, [plugins, total])
277
-
278
- return h('div', { className: styles.root },
279
- h('div', { className: styles.header },
280
- h('div', { className: styles.brand },
281
- h('div', { className: styles.brandText },
282
- // 市场主标题即本区块的 H1:与官网首页 H1 同文案(DSH-Plugin Hub for DeepSeek
283
- // Harness),命中品牌词 DSH-Plugin + 产品词 DeepSeek Harness;关键词落在 tagline。
284
- h('h1', { className: styles.title }, t('title')),
285
- h('div', { className: styles.tagline }, t('tagline', { total: statsTotal, verified: statsVerified })),
286
- ),
287
- ),
288
- h('div', { className: styles.controls },
289
- h('input', {
290
- className: styles.search,
291
- type: 'search',
292
- placeholder: t('search'),
293
- value: query,
294
- spellCheck: false,
295
- onInput: (e: React.FormEvent<HTMLInputElement>) =>
296
- setQuery((e.target as HTMLInputElement).value),
297
- }),
298
- h('select', {
299
- className: styles.sort,
300
- value: sort,
301
- onChange: (e: React.FormEvent<HTMLSelectElement>) =>
302
- setSort((e.target as HTMLSelectElement).value as SortKey),
303
- }, SORTS.map((key) => h('option', { key, value: key }, t(key)))),
304
- h('button', {
305
- className: styles.langBtn,
306
- title: lang === 'zh' ? t('toEn') : t('toZh'),
307
- onClick: () => setLang(lang === 'zh' ? 'en' : 'zh'),
308
- }, lang === 'zh' ? 'EN' : '中文'),
309
- h('a', {
310
- className: styles.openBtn,
311
- href: `${SITE_URL}${langPath}`,
312
- target: '_blank',
313
- rel: 'noopener noreferrer',
314
- title: t('openHint'),
315
- }, t('openBtn'), h('span', { className: styles.openArrow }, '\u2197')),
316
- ),
317
- ),
318
-
319
- h('a', {
320
- className: styles.adBanner,
321
- href: `${SITE_URL}${langPath}`,
322
- target: '_blank',
323
- rel: 'noopener noreferrer',
324
- },
325
- h('span', { className: styles.adBadge }, t('adBadge')),
326
- h('span', { className: styles.adText }, t('ad', { total: statsTotal, verified: statsVerified })),
327
- h('span', { className: styles.adArrow }, '\u2197')),
328
-
329
- h('div', { className: styles.tabs },
330
- h('button', {
331
- key: 'all',
332
- className: category === 'all' ? styles.tabActive : styles.tab,
333
- onClick: () => setCategory('all'),
334
- },
335
- t('all'),
336
- h('span', { className: category === 'all' ? styles.tabCountActive : styles.tabCount }, total)),
337
- CATEGORY_ORDER.map((id) => h('button', {
338
- key: id,
339
- className: category === id ? styles.tabActive : styles.tab,
340
- onClick: () => setCategory(id),
341
- },
342
- catLabel(CATEGORY_SHORT_LABELS, id),
343
- h('span', { className: category === id ? styles.tabCountActive : styles.tabCount }, categoryCounts[id] ?? 0))),
344
- ),
345
-
346
- h('div', { className: styles.body },
347
- h('div', { ref: listRef, className: styles.list },
348
- plugins === null && !failed && h('div', { className: styles.state }, t('loading')),
349
- failed && h('div', { className: styles.state },
350
- h('div', { className: styles.stateTitle }, t('failed')),
351
- h('div', { className: styles.stateDesc }, t('failedDesc')),
352
- h('button', { className: styles.retryBtn, onClick: () => setReloadKey((k) => k + 1) }, t('retry')),
353
- ),
354
- plugins !== null && !failed && count === 0 && h('div', { className: styles.state },
355
- h('div', { className: styles.stateTitle }, t('noResult')),
356
- h('div', { className: styles.stateDesc }, t('noResultDesc')),
357
- ),
358
- plugins !== null && !failed && visible.map((p) => {
359
- const repo = p.source?.repo ?? ''
360
- const isCopied = copied === repo
361
- return h('div', {
362
- // 唯一 key:数据重构后同一 slug 可能对应多个作者仓库,仅用 slug 会撞 key,
363
- // 导致切换分类时 React 复用旧 DOM、列表不刷新。
364
- key: p.ownerSlug ? `${p.ownerSlug}/${p.slug}` : p.slug,
365
- className: styles.card,
366
- },
367
- h('div', { className: styles.cardMain },
368
- h('div', { className: styles.cardHead },
369
- h('div', { className: styles.cardTitle, title: p.description ?? '' }, p.displayName ?? p.slug),
370
- p.category ? h('span', { className: styles.categoryBadge }, catLabel(CATEGORY_LABELS, p.category)) : null,
371
- p.compatibility?.status === 'verified'
372
- ? h('span', { className: styles.verified }, t('verified'))
373
- : null,
374
- ),
375
- // 英文模式下隐藏仍为中文的描述:站点英文数据缺翻译时 description 会回退成中文,
376
- // 不显示可保证界面语言一致,不残留中文字符。
377
- p.description && (langKey === 'zh' || !/[\u4e00-\u9fff]/.test(p.description))
378
- ? h('p', { className: styles.desc }, p.description)
379
- : null,
380
- (p.topics?.length ?? 0) > 0
381
- ? h('div', { className: styles.topics },
382
- p.topics!.slice(0, 3).map((topic) => h('span', { key: topic, className: styles.topic }, topic)),
383
- )
384
- : null,
385
- ),
386
- h('div', { className: styles.cardSide },
387
- h('div', { className: styles.stats },
388
- h('span', { className: styles.star }, '\u2605 ', fmtStars(p.stats?.stargazers_count)),
389
- h('span', { className: styles.fork }, t('fork'), ' ', fmtStars(p.stats?.forks_count)),
390
- h('span', { className: styles.date }, relTime(p.dates?.repoUpdatedAt, t)),
391
- ),
392
- repo
393
- ? h('div', { className: styles.actions },
394
- h('a', {
395
- className: styles.detailBtn,
396
- // 两级路径:/plugins/{ownerSlug}/{slug};旧数据缺 ownerSlug 时从 repo 推导
397
- href: `${SITE_URL}${langPath}plugins/${p.ownerSlug ?? repo.split('/')[0]?.toLowerCase() ?? ''}/${p.slug}`,
398
- target: '_blank',
399
- rel: 'noopener noreferrer',
400
- title: p.slug,
401
- }, t('detail')),
402
- h('button', {
403
- className: isCopied ? styles.installBtnCopied : styles.installBtn,
404
- // 文字恒定避免按钮宽度变化导致卡片跳动;成功反馈 = 蓝底高亮 + 右下角 Toast
405
- onClick: () => copyInstall(repo),
406
- }, t('copy')),
407
- )
408
- : null,
409
- ),
410
- )
411
- }),
412
- ),
413
- plugins !== null && !failed && h('div', { className: styles.footer },
414
- h('span', { className: styles.footText }, count === total ? t('pluginsTotal', { n: total }) : t('filteredCount', { n: count })),
415
- h('a', {
416
- className: styles.footLink,
417
- href: `${SITE_URL}${langPath}`,
418
- target: '_blank',
419
- rel: 'noopener noreferrer',
420
- }, t('browseAll', { n: total })),
421
- ),
422
- ),
423
- // 复制成功提示条:右下角纯黑色文字条,1800ms 后自动消失
424
- toast && h('div', { key: toast.id, className: styles.toast }, t('toastCopied')),
425
- )
426
- }
427
13
 
428
14
  export const name = NS
429
15
  export const inject = ['slots', 'locale']
430
16
 
431
17
  export function apply(ctx: HubClientContext): void {
432
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin: dictionaries')
18
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${NS}: dictionaries`)
433
19
  const t = ctx.locale.bind(NS)
434
20
  ctx.slots.inject('settings.section', () => ctx.slots.register({
435
21
  name: 'settings.section',