dsh-plugin 1.1.0 → 1.1.2

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 (64) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/client/client.js +648 -190
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +15 -8
  6. package/lib/index.js +4 -0
  7. package/lib/services/install-types.js +8 -2
  8. package/lib/services/install.js +1 -1
  9. package/lib/services/installed-versions.js +5 -1
  10. package/lib/services/loader.js +16 -5
  11. package/lib/services/npm-resolve.js +4 -0
  12. package/lib/services/pending-restart.js +1 -1
  13. package/lib/services/preflight.js +9 -4
  14. package/lib/services/profile.js +30 -3
  15. package/lib/services/progress.js +4 -0
  16. package/lib/services/task-queue.js +21 -10
  17. package/lib/types/index.d.ts +4 -0
  18. package/lib/types/services/install-types.d.ts +12 -4
  19. package/lib/types/services/install.d.ts +5 -1
  20. package/lib/types/services/loader.d.ts +10 -4
  21. package/lib/types/services/pending-restart.d.ts +8 -2
  22. package/lib/types/services/profile.d.ts +4 -2
  23. package/lib/types/services/progress.d.ts +4 -0
  24. package/package.json +1 -1
  25. package/src/client/components/AboutModal.tsx +76 -0
  26. package/src/client/components/CatalogControls.tsx +4 -0
  27. package/src/client/components/CatalogHeader.tsx +34 -5
  28. package/src/client/components/CatalogList.tsx +4 -0
  29. package/src/client/components/CategoryTabs.tsx +4 -0
  30. package/src/client/components/Dropdown.tsx +4 -0
  31. package/src/client/components/HubUpdateModal.tsx +88 -0
  32. package/src/client/components/NotificationsModal.tsx +25 -8
  33. package/src/client/components/PluginCard.tsx +8 -1
  34. package/src/client/components/PluginHubSection.tsx +67 -7
  35. package/src/client/components/ProgressView.tsx +4 -0
  36. package/src/client/components/icons.tsx +4 -0
  37. package/src/client/components/modals.tsx +35 -10
  38. package/src/client/hooks/useCatalog.ts +89 -17
  39. package/src/client/hooks/useTaskQueue.ts +52 -35
  40. package/src/client/index.tsx +4 -0
  41. package/src/client/lib/catalog.ts +59 -6
  42. package/src/client/lib/env.ts +4 -0
  43. package/src/client/lib/failures.ts +5 -1
  44. package/src/client/lib/format.ts +4 -0
  45. package/src/client/lib/renderMarkdown.ts +120 -0
  46. package/src/client/locales.ts +48 -1
  47. package/src/client/styles/Dropdown.module.css +5 -1
  48. package/src/client/styles/Header.module.css +88 -4
  49. package/src/client/styles/List.module.css +5 -1
  50. package/src/client/styles/Modal.module.css +279 -1
  51. package/src/client/styles/tokens.module.css +5 -1
  52. package/src/client/types.ts +7 -1
  53. package/src/server/http/routes.ts +14 -8
  54. package/src/server/index.ts +4 -0
  55. package/src/server/services/install-types.ts +12 -4
  56. package/src/server/services/install.ts +5 -1
  57. package/src/server/services/installed-versions.ts +5 -1
  58. package/src/server/services/loader.ts +16 -5
  59. package/src/server/services/npm-resolve.ts +4 -0
  60. package/src/server/services/pending-restart.ts +8 -2
  61. package/src/server/services/preflight.ts +9 -4
  62. package/src/server/services/profile.ts +27 -3
  63. package/src/server/services/progress.ts +4 -0
  64. package/src/server/services/task-queue.ts +21 -10
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Section header: brand title row (H1 + open-site button) + tagline, followed
3
7
  * by the purple ad banner that promotes the catalog stats.
4
8
  */
@@ -8,12 +12,18 @@ import type { Translate } from '../types.ts'
8
12
  import { GITHUB_URL, PLUGIN_VERSION, SITE_URL } from '../lib/catalog.ts'
9
13
  import { GitHubIcon, LogoIcon } from './icons.tsx'
10
14
 
11
- export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggleLang }: {
15
+ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggleLang, hubUpdate, onVersionClick, onAboutClick }: {
12
16
  t: Translate
13
17
  langPath: string
14
18
  statsTotal: number
15
19
  statsVerified: number
16
20
  onToggleLang: () => void
21
+ /** Hub 自身是否有可用更新:有则版本号后紧跟红色「可更新」徽标(整体一个可点入口) */
22
+ hubUpdate: boolean
23
+ /** 点击版本号(含红色徽标,同一入口):无论有无更新都打开更新记录弹窗 */
24
+ onVersionClick: () => void
25
+ /** 点击「关注我们」:打开平台介绍 + 用户反馈群二维码弹窗 */
26
+ onAboutClick: () => void
17
27
  }) {
18
28
  return h(Fragment, null,
19
29
  h('div', { className: styles.header },
@@ -28,10 +38,20 @@ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggle
28
38
  'aria-label': t('openHint'),
29
39
  },
30
40
  h(LogoIcon),
31
- h('h1', { className: styles.title },
32
- t('title'),
33
- h('span', { className: styles.version }, ` v${PLUGIN_VERSION}`),
34
- ),
41
+ h('h1', { className: styles.title }, t('title')),
42
+ ),
43
+ // 版本号:紧跟标题「DSH-Plugin Hub」之后,常驻可点入口(<a> 内不能嵌套按钮,故从官网链接里移出放在旁边)。
44
+ // 点击打开更新记录弹窗 —— 有更新看新版本内容,无更新看当前版本内容。
45
+ // 有更新时版本号后直接跟一个红色「可更新」徽标(同一按钮内,不分开成两个元素),静态显示不闪烁。
46
+ h('button', {
47
+ className: styles.versionBtn,
48
+ type: 'button',
49
+ onClick: onVersionClick,
50
+ title: t(hubUpdate ? 'hubUpdateHint' : 'versionHint'),
51
+ 'aria-label': t(hubUpdate ? 'hubUpdateHint' : 'versionHint'),
52
+ },
53
+ `v${PLUGIN_VERSION}`,
54
+ hubUpdate && h('span', { className: styles.hubUpdateBadge }, t('update')),
35
55
  ),
36
56
  // 右侧控件组:语言切换 + GitHub 源码图标(独立链接,brandTitle 是官网 <a> 不能嵌套),
37
57
  // 一起 space-between 顶到标题行最右
@@ -44,6 +64,15 @@ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggle
44
64
  title: t('toggleLangHint'),
45
65
  'aria-label': t('toggleLangHint'),
46
66
  }, langPath === 'zh/' ? 'EN' : '中文'),
67
+ // 「关注我们」:点击打开平台介绍 + 用户反馈群二维码弹窗
68
+ h('button', {
69
+ className: styles.aboutBtn,
70
+ type: 'button',
71
+ onClick: onAboutClick,
72
+ title: t('aboutDesc'),
73
+ 'aria-label': t('aboutTitle'),
74
+ }, t('followUs')),
75
+ // GitHub 源码图标:独立链接(brandTitle 是官网 <a> 不能嵌套),放在控件组最后
47
76
  h('a', {
48
77
  className: styles.githubLink,
49
78
  href: GITHUB_URL,
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Catalog list: the scrollable container with the loading / failed / empty
3
7
  * states, the plugin cards, and the count footer. Resets scroll position when
4
8
  * the category or install-status filter changes so the replaced content is
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Category tabs: an "all" chip followed by one chip per catalog category.
3
7
  * The "all" chip carries the total plugin count; the per-category chips
4
8
  * carry no counts so their widths stay uniform.
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Custom dropdown (native <select> replacement).
3
7
  *
4
8
  * macOS/浏览器原生下拉无法跟随宿主主题,这里自绘:触发器按钮 + 绝对定位面板,
@@ -0,0 +1,88 @@
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 版本信息 / 自我更新说明弹窗:
7
+ * - 有更新(hasUpdate=true):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
8
+ * Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
9
+ * 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
10
+ * - 无更新(hasUpdate=false):标题「当前版本」,展示当前版本信息与更新记录,已是最新。
11
+ * 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
12
+ */
13
+ import { createElement as h } from 'react'
14
+ import type { MouseEvent } from 'react'
15
+ import styles from '../styles/Modal.module.css'
16
+ import type { HubUpdateInfo } from '../lib/catalog.ts'
17
+ import type { LocaleId, Translate } from '../types.ts'
18
+ import { renderMarkdown } from '../lib/renderMarkdown.ts'
19
+ import { CloseIcon } from './icons.tsx'
20
+
21
+ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }: {
22
+ info: HubUpdateInfo
23
+ lang: LocaleId
24
+ t: Translate
25
+ /** 是否有可用更新:有则显示「直接更新」,无则显示「已是最新」 */
26
+ hasUpdate: boolean
27
+ /** 点「直接更新」:关闭本弹窗,进入安装弹窗的更新流程 */
28
+ onProceed: () => void
29
+ onClose: () => void
30
+ }) {
31
+ // 变更记录:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则不出记录区
32
+ const notesRaw = typeof info.notes === 'string'
33
+ ? info.notes
34
+ : info.notes && typeof info.notes === 'object'
35
+ ? lang === 'en'
36
+ ? (info.notes.en ?? info.notes.zh ?? '')
37
+ : (info.notes.zh ?? info.notes.en ?? '')
38
+ : ''
39
+ const notesHtml = notesRaw.trim() ? renderMarkdown(notesRaw) : null
40
+ // 发布时间:ISO 字符串转本地可读格式;非法值静默隐藏
41
+ let published: string | null = null
42
+ if (info.publishedAt) {
43
+ const d = new Date(info.publishedAt)
44
+ if (!Number.isNaN(d.getTime())) {
45
+ published = d.toLocaleString(lang === 'en' ? 'en-US' : 'zh-CN')
46
+ }
47
+ }
48
+
49
+ return h('div', {
50
+ className: styles.overlay,
51
+ onClick: (e: MouseEvent<HTMLDivElement>) => {
52
+ if (e.target === e.currentTarget) onClose()
53
+ },
54
+ },
55
+ h('div', { className: `${styles.modal} ${styles.hubUpdateModal}`, role: 'dialog', 'aria-modal': 'true' },
56
+ h('div', { className: styles.modalHead },
57
+ h('div', { className: styles.modalTitle }, t(hasUpdate ? 'hubUpdateTitle' : 'hubCurrentTitle')),
58
+ h('button', {
59
+ className: styles.modalClose,
60
+ 'aria-label': t('confirmCancel'),
61
+ onClick: () => onClose(),
62
+ }, h(CloseIcon)),
63
+ ),
64
+ h('div', { className: styles.modalDesc },
65
+ t(hasUpdate ? 'hubUpdateDesc' : 'hubCurrentDesc', { version: info.version })),
66
+ h('div', { className: styles.hubUpdateMeta },
67
+ h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${info.version}`),
68
+ published
69
+ ? h('span', { className: styles.hubUpdateMetaItem }, `${t('hubUpdatePublished')} ${published}`)
70
+ : null,
71
+ ),
72
+ notesHtml
73
+ ? h('div', {
74
+ className: styles.hubUpdateNotes,
75
+ dangerouslySetInnerHTML: { __html: notesHtml },
76
+ })
77
+ : null,
78
+ h('div', { className: styles.modalActions },
79
+ hasUpdate
80
+ ? [
81
+ h('button', { className: styles.restartLater, onClick: onClose }, t('hubUpdateLater')),
82
+ h('button', { className: styles.modalInstall, onClick: onProceed }, t('updateNow')),
83
+ ]
84
+ : h('button', { className: styles.modalInstall, onClick: onClose }, t('hubUpToDate')),
85
+ ),
86
+ ),
87
+ )
88
+ }
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Notification-center dialog: a persistent log of every settled install /
3
7
  * remove task — successes and failures alike.
4
8
  *
@@ -156,7 +160,10 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
156
160
  ? h('div', { className: styles.failEmpty }, t('notificationsEmpty'))
157
161
  : h('div', { className: styles.noticeList },
158
162
  records.map((r) => {
159
- return h('div', { key: r.id, className: styles.noticeRow },
163
+ return h('div', {
164
+ key: r.id,
165
+ className: r.ok ? `${styles.noticeRow} ${styles.noticeRowOk}` : styles.noticeRow,
166
+ },
160
167
  h('div', { className: styles.noticeRowMain },
161
168
  h('div', {
162
169
  className: r.ok ? styles.noticeBadgeOk : styles.noticeBadgeFail,
@@ -182,6 +189,14 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
182
189
  className: styles.failCopy,
183
190
  onClick: () => onCopy(r.message),
184
191
  }, t('failCopy')),
192
+ // 成功通知:时间戳并入本行右侧,与图标/文字垂直居中 ——
193
+ // 不再单独占底部一行,卡片只有一行内容,图标文字整体居中不顶头
194
+ r.ok
195
+ ? h('span', {
196
+ className: styles.noticeTime,
197
+ title: new Date(r.at).toLocaleString(),
198
+ }, fmtTime(r.at))
199
+ : null,
185
200
  // 每条通知右侧的删除按钮:单独移除这一条
186
201
  h('button', {
187
202
  className: styles.noticeRemove,
@@ -220,13 +235,15 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
220
235
  })(),
221
236
  ),
222
237
  ),
223
- // 每条通知右下角:完整年月日时分秒时间戳(独立一行,不参与徽标垂直居中)
224
- h('div', { className: styles.noticeFoot },
225
- h('span', {
226
- className: styles.noticeTime,
227
- title: new Date(r.at).toLocaleString(),
228
- }, fmtTime(r.at)),
229
- ),
238
+ // 失败通知:时间戳保留独立底部行(右下角);成功通知已并入头部行右侧
239
+ !r.ok
240
+ ? h('div', { className: styles.noticeFoot },
241
+ h('span', {
242
+ className: styles.noticeTime,
243
+ title: new Date(r.at).toLocaleString(),
244
+ }, fmtTime(r.at)),
245
+ )
246
+ : null,
230
247
  )
231
248
  }),
232
249
  ),
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * One plugin card in the catalog list: name/category/verified badge,
3
7
  * description + topics, star/fork/date stats, and the detail/install/
4
8
  * uninstall actions.
@@ -65,7 +69,7 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
65
69
  // 查看详情:未安装/已安装都始终显示,跳转官网详情页
66
70
  h('a', {
67
71
  className: styles.detailBtn,
68
- // 两级路径:/plugins/{ownerSlug}/{slug};旧数据缺 ownerSlug 时从 repo 推导
72
+ // 两级路径:/plugins/{ownerSlug}/{slug};缺 ownerSlug 时从 repo 推导
69
73
  href: pluginDetailUrl(p, langPath),
70
74
  target: '_blank',
71
75
  rel: 'noopener noreferrer',
@@ -84,6 +88,9 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
84
88
  disabled: true,
85
89
  title: t('installed'),
86
90
  }, t('installed')))
91
+ // 未安装:按钮恒为「安装」,点击先弹信任确认;AI 识别为「可能需要命令行辅助」
92
+ // 的插件(webInstallable=false)不拦截,允许尝试一键安装,弹窗内会给出提示与
93
+ // 复制命令通道,安装失败时引导去 dsh 终端手动执行
87
94
  : h('button', {
88
95
  className: isCopied ? styles.installBtnCopied : styles.installBtn,
89
96
  // 文字恒定避免按钮宽度变化导致卡片跳动;点击先弹信任确认,
@@ -1,18 +1,24 @@
1
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
+ *
2
6
  * Plugin Hub section: wires the catalog data pipeline, the background task
3
7
  * queue and the feedback state together. Data/queue logic lives in hooks/,
4
8
  * rendering is delegated to the small presentational components in this
5
- * folder; only the dialogs/toast and the section-level copy actions remain.
9
+ * folder; it hosts the dialogs/toast and the section-level copy actions.
6
10
  */
7
11
  import { createElement as h, useEffect, useState } from 'react'
8
12
  import styles from '../styles/Header.module.css'
9
13
  import { en, zh } from '../locales.ts'
10
14
  import type { EnvInfo, HubPlugin, LocaleId, SectionProps, ToastState } from '../types.ts'
11
- import { langPathOf, installCommandOf } from '../lib/catalog.ts'
15
+ import { langPathOf, installCommandOf, HUB_REPO, PLUGIN_VERSION } from '../lib/catalog.ts'
12
16
  import { getEnv } from '../lib/env.ts'
13
17
  import { useCatalog } from '../hooks/useCatalog.ts'
14
18
  import { useTaskQueue } from '../hooks/useTaskQueue.ts'
15
19
  import { ErrorModal, InstallModal, UninstallModal, Toast } from './modals.tsx'
20
+ import { AboutModal } from './AboutModal.tsx'
21
+ import { HubUpdateModal } from './HubUpdateModal.tsx'
16
22
  import { NotificationsModal } from './NotificationsModal.tsx'
17
23
  import { addFailure, addSuccess, clearNotifications, loadNotifications, removeNotification } from '../lib/failures.ts'
18
24
  import type { NotificationRecord } from '../lib/failures.ts'
@@ -48,11 +54,19 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
48
54
  const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
49
55
  /** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
50
56
  const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
57
+ /** Hub 自我更新说明弹窗:点「可更新」徽标先展示版本 + Markdown 变更记录,确认后再进安装弹窗 */
58
+ const [showHubUpdate, setShowHubUpdate] = useState(false)
59
+ /** 「关注我们」弹窗:GitHub 图标后按钮点击打开,展示平台介绍 + 用户反馈群二维码(Worker /about 推送) */
60
+ const [showAbout, setShowAbout] = useState(false)
51
61
  /** 卸载确认弹窗:记录待卸载的插件 */
52
62
  const [uninstallPlugin, setUninstallPlugin] = useState<HubPlugin | null>(null)
53
63
  /** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
54
64
  const [installDone, setInstallDone] = useState(false)
55
65
  const [uninstallDone, setUninstallDone] = useState(false)
66
+ /** 结果视图是否给「立即重启」:服务端任务终态带出(卸载时 loader 已即时移除 → false 只给「完成」;
67
+ * true 时通知中心待重启条目同步常驻,直到用户点「立即重启」真正重启后才消失) */
68
+ const [installNeedsRestart, setInstallNeedsRestart] = useState(true)
69
+ const [uninstallNeedsRestart, setUninstallNeedsRestart] = useState(true)
56
70
  /** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
57
71
  const [restarting, setRestarting] = useState(false)
58
72
  /** 操作失败完整信息 + 所属插件仓库 + 失败类型(决定弹窗标题「安装失败/卸载失败」)+ 实际执行的安装命令 + 尝试过的安装方式(issue 预填用) */
@@ -71,13 +85,15 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
71
85
  const queue = useTaskQueue({
72
86
  t,
73
87
  refreshInstalled: catalog.refreshInstalled,
74
- onInstallDone: (viaModal, repo) => {
88
+ onInstallDone: (viaModal, repo, needsRestart) => {
89
+ setInstallNeedsRestart(needsRestart)
75
90
  if (viaModal) setInstallDone(true)
76
91
  else setToast({ id: Date.now(), kind: 'done' })
77
92
  // 安装成功也写入通知记录:通知中心里成功与失败都能看到
78
93
  setNotifications(addSuccess({ kind: 'install', repo: repo ?? '' }))
79
94
  },
80
- onUninstallDone: (viaModal, repo) => {
95
+ onUninstallDone: (viaModal, repo, needsRestart) => {
96
+ setUninstallNeedsRestart(needsRestart)
81
97
  if (viaModal) setUninstallDone(true)
82
98
  else setToast({ id: Date.now(), kind: 'removed' })
83
99
  setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
@@ -97,20 +113,22 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
97
113
  },
98
114
  })
99
115
 
100
- // 信任/卸载/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
116
+ // 信任/卸载/更新说明/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
101
117
  useEffect(() => {
102
- if (!confirmPlugin && !uninstallPlugin && !showNotifications) return
118
+ if (!confirmPlugin && !uninstallPlugin && !showNotifications && !showHubUpdate && !showAbout) return
103
119
  const onKey = (e: KeyboardEvent) => {
104
120
  if (e.key === 'Escape') {
105
121
  setConfirmPlugin(null)
106
122
  setUninstallPlugin(null)
107
123
  setUninstallDone(false)
124
+ setShowHubUpdate(false)
125
+ setShowAbout(false)
108
126
  setShowNotifications(false)
109
127
  }
110
128
  }
111
129
  window.addEventListener('keydown', onKey)
112
130
  return () => window.removeEventListener('keydown', onKey)
113
- }, [confirmPlugin, uninstallPlugin, showNotifications])
131
+ }, [confirmPlugin, uninstallPlugin, showNotifications, showHubUpdate, showAbout])
114
132
 
115
133
  // Toast 统一自动消失
116
134
  useEffect(() => {
@@ -204,6 +222,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
204
222
 
205
223
  const count = catalog.visible.length
206
224
 
225
+ // Hub 自身条目(目录数据里的 dshplugin/dsh-plugin-hub):头部「可更新」徽标的更新目标。
226
+ // 目录可能尚未加载(此时 hubEntry 为 null,徽标不渲染,加载完成后自然出现)。
227
+ const hubEntry = catalog.plugins?.find((p) => p.source?.repo === HUB_REPO) ?? null
228
+
207
229
  return h('div', { className: styles.root },
208
230
  h(CatalogHeader, {
209
231
  t,
@@ -211,6 +233,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
211
233
  statsTotal,
212
234
  statsVerified,
213
235
  onToggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
236
+ // Hub 自身版本信息入口:版本号常驻可点(无更新也看得到当前版本更新内容),
237
+ // 有可用更新时版本号后紧跟红色「可更新」徽标(同一入口),点击都打开同一个更新说明弹窗。
238
+ // 有更新弹窗显示新版本 + Markdown 变更记录,点「直接更新」进安装弹窗覆盖重装。
239
+ hubUpdate: catalog.hubHasUpdate,
240
+ onVersionClick: () => setShowHubUpdate(true),
241
+ onAboutClick: () => setShowAbout(true),
214
242
  }),
215
243
  h(CategoryTabs, {
216
244
  category: catalog.category,
@@ -270,6 +298,34 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
270
298
  setUninstallPlugin(p)
271
299
  },
272
300
  }),
301
+ // Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
302
+ // 无更新显示当前版本 + 记录 + 「已是最新」。Worker 拉取失败时兜底展示当前版本号。
303
+ showHubUpdate && h(HubUpdateModal, {
304
+ info: catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
305
+ lang,
306
+ t,
307
+ hasUpdate: catalog.hubHasUpdate,
308
+ onClose: () => setShowHubUpdate(false),
309
+ onProceed: () => {
310
+ setShowHubUpdate(false)
311
+ // 与列表内更新同一流程:覆盖重装 dsh-plugin + 结果视图「立即重启」
312
+ if (hubEntry) {
313
+ setInstallDone(false)
314
+ setConfirmIsUpdate(true)
315
+ queue.clearModalTask()
316
+ setConfirmPlugin(hubEntry)
317
+ }
318
+ },
319
+ }),
320
+ // 「关注我们」弹窗:GitHub 图标后按钮点击打开 —— 平台介绍 + 用户反馈群二维码,
321
+ // 内容由 dsh-update Worker 的 /about 接口以 Markdown 形式推送,作者随时可改,非写死。
322
+ // Worker 未推送(info 为 null)时弹窗仍能打开并展示兜底文案,不影响其他功能。
323
+ showAbout && h(AboutModal, {
324
+ info: catalog.hubAboutInfo ?? null,
325
+ lang,
326
+ t,
327
+ onClose: () => setShowAbout(false),
328
+ }),
273
329
  // 安装确认弹窗:锁定/进度/结果视图逻辑收敛在 modals.tsx;更新与安装同入口,仅标记 update
274
330
  confirmPlugin && h(InstallModal, {
275
331
  plugin: confirmPlugin,
@@ -279,7 +335,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
279
335
  langPath,
280
336
  restarting,
281
337
  update: confirmIsUpdate,
338
+ // 仅命令行插件(webInstallable=false):弹窗只展示命令供复制,不提供一键安装
339
+ cliOnly: confirmPlugin.install?.webInstallable === false,
282
340
  submitting: queue.submitting,
341
+ needsRestart: installNeedsRestart,
283
342
  onClose: () => setConfirmPlugin(null),
284
343
  onCopy: () => copyCommand(confirmPlugin),
285
344
  onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
@@ -294,6 +353,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
294
353
  langPath,
295
354
  restarting,
296
355
  submitting: queue.submitting,
356
+ needsRestart: uninstallNeedsRestart,
297
357
  onClose: () => {
298
358
  setUninstallDone(false)
299
359
  setUninstallPlugin(null)
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Inline progress block shared by the install and uninstall dialogs:
3
7
  * a real percentage bar (server-estimated, client-polled). No live terminal
4
8
  * window — pnpm emits nothing during git clone/build, so a log pane would be
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Inline SVG icons used across the client. Icons inherit currentColor so
3
7
  * they adapt to themes and disabled states automatically.
4
8
  */
@@ -1,4 +1,8 @@
1
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
+ *
2
6
  * Dialog layer for the Plugin Hub: the install-confirm dialog, the uninstall
3
7
  * dialog and the global toast. Both dialogs lock themselves while a mutation
4
8
  * or restart is running, then switch to a result view offering an immediate
@@ -13,14 +17,18 @@ import { ProgressView } from './ProgressView.tsx'
13
17
  import { installCommandOf, pluginDetailUrl, pluginIssueUrl, pluginSiteUrl } from '../lib/catalog.ts'
14
18
  import { classifyFailure } from '../lib/failures.ts'
15
19
 
16
- /** 完成结果视图:绿色对勾 + 标题/描述 + 「稍后重启 / 立即重启」按钮对(部分插件需重启后才会挂载) */
20
+ /** 完成结果视图:绿色对勾 + 标题/描述;
21
+ * needsRestart=true(插件需重启才生效)→ 「稍后重启 / 立即重启」按钮对,点稍后重启后
22
+ * 通知中心待重启条目常驻(服务端登记,内存态),直到用户点「立即重启」真正重启后才消失;
23
+ * needsRestart=false(卸载已即时生效)→ 仅「完成」关闭。 */
17
24
  function ResultView({
18
- title, desc, t, restarting, onRestart, onClose,
25
+ title, desc, t, restarting, needsRestart, onRestart, onClose,
19
26
  }: {
20
27
  title: string
21
28
  desc: string
22
29
  t: Translate
23
30
  restarting: boolean
31
+ needsRestart: boolean
24
32
  onRestart: () => void
25
33
  onClose: () => void
26
34
  }) {
@@ -43,12 +51,17 @@ function ResultView({
43
51
  ),
44
52
  h('div', { className: styles.resultTitle }, title),
45
53
  desc ? h('div', { className: styles.resultDesc }, desc) : null,
46
- h('div', { className: styles.resultRestarting }, restarting ? t('restarting') : t('restartHint')),
47
- h('div', { className: styles.modalActions },
48
- h('button', { className: styles.restartLater, onClick: onClose, disabled: restarting }, t('restartLater')),
49
- h('button', { className: styles.restartNow, onClick: onRestart, disabled: restarting },
50
- restarting ? t('restarting') : t('restartNow')),
51
- ),
54
+ needsRestart
55
+ ? h('div', null, [
56
+ h('div', { className: styles.resultRestarting }, restarting ? t('restarting') : t('restartHint')),
57
+ h('div', { className: styles.modalActions },
58
+ h('button', { className: styles.restartLater, onClick: onClose, disabled: restarting }, t('restartLater')),
59
+ h('button', { className: styles.restartNow, onClick: onRestart, disabled: restarting },
60
+ restarting ? t('restarting') : t('restartNow')),
61
+ ),
62
+ ])
63
+ : h('div', { className: styles.modalActions },
64
+ h('button', { className: styles.restartNow, onClick: onClose }, t('done'))),
52
65
  )
53
66
  }
54
67
 
@@ -61,8 +74,12 @@ export interface InstallModalProps {
61
74
  restarting: boolean
62
75
  /** 已安装插件的覆盖更新(走同一条 add 命令原位重装,文案区分安装/更新) */
63
76
  update: boolean
77
+ /** 仅命令行插件(webInstallable=false):不提供一键安装,只展示命令供复制到 dsh 终端 */
78
+ cliOnly: boolean
64
79
  /** 安装请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
65
80
  submitting: boolean
81
+ /** 完成结果是否需重启才生效:true → 「稍后重启 / 立即重启」;false → 仅「完成」 */
82
+ needsRestart: boolean
66
83
  onClose: () => void
67
84
  onCopy: () => void
68
85
  onInstall: () => void
@@ -74,7 +91,7 @@ export interface InstallModalProps {
74
91
  * 只在本任务执行中展示实时进度;完成后切换为结果视图,与卸载一致。
75
92
  */
76
93
  export function InstallModal(props: InstallModalProps) {
77
- const { plugin, done, task, t, langPath, restarting, submitting, update, onClose, onCopy, onInstall, onRestart } = props
94
+ const { plugin, done, task, t, langPath, restarting, submitting, update, cliOnly, needsRestart, onClose, onCopy, onInstall, onRestart } = props
78
95
  const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
79
96
  // 进行中标题带上插件名(中文「XX 插件安装中」;英文状态词在前更自然),并用状态色区分
80
97
  const name = plugin.displayName ?? plugin.slug
@@ -112,6 +129,7 @@ export function InstallModal(props: InstallModalProps) {
112
129
  desc: update ? t('updateResultDesc') : t('installResultDesc'),
113
130
  t,
114
131
  restarting,
132
+ needsRestart,
115
133
  onRestart,
116
134
  onClose,
117
135
  })
@@ -145,8 +163,12 @@ export function InstallModal(props: InstallModalProps) {
145
163
  h('span', { className: styles.modalCmdText }, installCommandOf(plugin)),
146
164
  h('span', { className: styles.modalCmdCopy }, h(CopyIcon), t('copyCmdLabel')),
147
165
  ),
166
+ // AI 识别可能需要命令行辅助的插件(webInstallable=false):提示但不拦截,仍允许尝试一键安装
167
+ cliOnly ? h('div', { className: styles.cliOnlyHint }, t('cliOnlyHint')) : null,
148
168
  task && task.status === 'pending' ? h('div', { className: styles.queuedHint }, t('queuedHint')) : null,
149
169
  task ? h(ProgressView, { task }) : null,
170
+ // 安装失败:引导用户复制上方命令到 dsh 终端手动安装(如 node-pty 等原生依赖构建脚本需人工放行)
171
+ task && task.status === 'failed' ? h('div', { className: styles.failedCopyHint }, t('failedCopyHint')) : null,
150
172
  h('div', { className: styles.modalActions },
151
173
  h('button', {
152
174
  className: styles.modalCopy,
@@ -177,6 +199,8 @@ export interface UninstallModalProps {
177
199
  restarting: boolean
178
200
  /** 卸载请求在途(fetch 等待响应):此时任务尚未入队,需禁用确认按钮防止二次点击 */
179
201
  submitting: boolean
202
+ /** 完成结果是否需重启才生效:true → 「稍后重启 / 立即重启」;false(loader 已即时移除)→ 仅「完成」 */
203
+ needsRestart: boolean
180
204
  onClose: () => void
181
205
  onCancel: () => void
182
206
  onCopyCommand: () => void
@@ -186,7 +210,7 @@ export interface UninstallModalProps {
186
210
 
187
211
  /** 卸载确认弹窗:确认/进行中(后台队列,可关闭);完成后切换为结果视图(成功即生效,仅「完成」关闭)。 */
188
212
  export function UninstallModal(props: UninstallModalProps) {
189
- const { plugin, done, task, t, langPath, restarting, submitting, onClose, onCancel, onCopyCommand, onConfirm, onRestart } = props
213
+ const { plugin, done, task, t, langPath, restarting, submitting, needsRestart, onClose, onCancel, onCopyCommand, onConfirm, onRestart } = props
190
214
  const busy = submitting || (task !== null && (task.status === 'pending' || task.status === 'running'))
191
215
  // 进行中标题带上插件名,与安装弹窗一致,并用状态色区分
192
216
  const name = plugin.displayName ?? plugin.slug
@@ -221,6 +245,7 @@ export function UninstallModal(props: UninstallModalProps) {
221
245
  desc: t('uninstallResultDesc'),
222
246
  t,
223
247
  restarting,
248
+ needsRestart,
224
249
  onRestart,
225
250
  onClose,
226
251
  })