dsh-plugin 1.1.0 → 1.1.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.
@@ -44,13 +44,15 @@ function nameMatches(entryName, target) {
44
44
  return false;
45
45
  }
46
46
  /**
47
- * 卸载成功后从运行中 loader 停用指定包的条目,使卸载立即生效(刷新页面不再
48
- * 加载已卸载的 client bundle,也无需重启)。做法:按 name 扫描
47
+ * 卸载成功后从运行中 loader 停用指定包的条目。做法:按 name 扫描
49
48
  * `loader.entries()`(含嵌套子树),对每个匹配条目
50
49
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
51
50
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
52
- * 返回 true = 无需重启(条目已停用,或本就没加载过);false = 未能停用,
53
- * 需登记「待重启清理」兜底。宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
51
+ * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
52
+ * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
53
+ * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
54
+ * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
55
+ * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
54
56
  */
55
57
  export async function removeLoadedEntry(loader, name) {
56
58
  if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/'))
@@ -83,8 +85,13 @@ export async function removeLoadedEntry(loader, name) {
83
85
  if (!found) {
84
86
  // 运行中 loader 无该条目:宿主从未加载过它(装完未重启),磁盘已干净,无需重启
85
87
  console.log(`[hub-uninstall] no live entry for ${name}; nothing to disable`);
88
+ return true;
86
89
  }
87
- return true;
90
+ // 找到并停用了 live entry:逻辑层已卸载(刷新页面不再加载其 client bundle),
91
+ // 但带 UI 的插件(侧边栏面板等)是宿主启动时渲染的槽位,disable 不会主动摘除,
92
+ // 不重启面板会一直残留 → 返回「需要重启」,弹窗给出「立即重启」,面板立即消失
93
+ console.log(`[hub-uninstall] disabled live entry for ${name}; UI may persist until restart`);
94
+ return false;
88
95
  })();
89
96
  const result = await Promise.race([
90
97
  removal.catch((error) => {
@@ -142,7 +142,7 @@ function stopChild(child) {
142
142
  * Progress is visible through `getTask(id)` / `activeTask()` until done.
143
143
  */
144
144
  export function startPluginMutation(options) {
145
- const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [] };
145
+ const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false };
146
146
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1;
147
147
  tasks.set(task.id, task);
148
148
  if (tasks.size > MAX_TASKS) {
@@ -316,9 +316,13 @@ function spawnMutation(options) {
316
316
  if (task.status === 'done') {
317
317
  task.progress = 100;
318
318
  if (action === 'remove' && uninstallLoader) {
319
- // 卸载成功 → 主动从运行中 loader 移除该包条目,立即生效、无需重启;
320
- // 移除失败(或从未加载过但 loader 不可用)才登记「待重启清理」兜底
319
+ // 卸载成功 → 主动从运行中 loader 停用该包条目;
320
+ // 仅当宿主从未加载过它(磁盘已干净)才判「无需重启」;
321
+ // 只要曾在 loader 中存活(停用过 live entry)就仍要求重启——
322
+ // 带 UI 的插件(侧边栏面板等宿主启动时渲染的槽位)disable 后不会
323
+ // 主动消失,重启才能立即摘除面板
321
324
  const removed = await removeLoadedEntry(uninstallLoader, target);
325
+ task.needsRestart = !removed;
322
326
  if (removed)
323
327
  clearPendingRestart(displayTarget ?? target);
324
328
  else
@@ -327,6 +331,7 @@ function spawnMutation(options) {
327
331
  else {
328
332
  // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
329
333
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
334
+ task.needsRestart = true;
330
335
  addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall');
331
336
  }
332
337
  }
@@ -26,6 +26,8 @@ export interface InstallTask {
26
26
  progress: number;
27
27
  /** Newest output lines first (consumer shows the tail). */
28
28
  lines: string[];
29
+ /** 完成后是否仍需宿主重启才生效:卸载时 loader 已即时移除 → false;否则 true(弹窗据此给重启选项) */
30
+ needsRestart: boolean;
29
31
  }
30
32
  /** 子进程启动参数(复用宿主 dsh 入口,或回退到 PATH 上的 `dsh`)。 */
31
33
  export interface Invocation {
@@ -32,12 +32,14 @@ export declare function dumpLoaderEntries(loader: LoaderHandle | undefined): Arr
32
32
  name?: string;
33
33
  }>;
34
34
  /**
35
- * 卸载成功后从运行中 loader 停用指定包的条目,使卸载立即生效(刷新页面不再
36
- * 加载已卸载的 client bundle,也无需重启)。做法:按 name 扫描
35
+ * 卸载成功后从运行中 loader 停用指定包的条目。做法:按 name 扫描
37
36
  * `loader.entries()`(含嵌套子树),对每个匹配条目
38
37
  * `entry.update({ disabled: true })` 做 live-disable —— fiber 被 dispose,
39
38
  * client-modules 对账后不再把它写进 `__DSH_BOOT__`,页面刷新即恢复。
40
- * 返回 true = 无需重启(条目已停用,或本就没加载过);false = 未能停用,
41
- * 需登记「待重启清理」兜底。宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
39
+ * 返回 true = 无需重启(仅当宿主从未加载过它:磁盘已干净);
40
+ * 其余一律 false = 需要重启:要么 disable 失败需「待重启清理」兜底,
41
+ * 要么 disable 成功但该插件曾在 loader 中存活——带 UI 的插件(侧边栏面板等
42
+ * 宿主启动时渲染的槽位)disable 后不会主动摘除,不重启面板一直残留。
43
+ * 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
42
44
  */
43
45
  export declare function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 头部「关注我们」弹窗:介绍 DSH-Plugin Hub 是干什么的 + 用户反馈群二维码。
3
+ * 内容以 Markdown 形式由 dsh-update Worker 的 /about 接口下发(key = hub:about),
4
+ * 作者想怎么写就怎么写、随写随推,客户端只负责渲染 —— 非写死。
5
+ * 图片(反馈群二维码)经 renderMarkdown 的 ![](url) 语法嵌入,居中、最大高度受控。
6
+ */
7
+ import { createElement as h } from 'react'
8
+ import type { MouseEvent } from 'react'
9
+ import styles from '../styles/Modal.module.css'
10
+ import type { HubAboutInfo } from '../lib/catalog.ts'
11
+ import type { LocaleId, Translate } from '../types.ts'
12
+ import { renderMarkdown } from '../lib/renderMarkdown.ts'
13
+ import { CloseIcon } from './icons.tsx'
14
+
15
+ export function AboutModal({ info, lang, t, onClose }: {
16
+ info: HubAboutInfo | null
17
+ lang: LocaleId
18
+ t: Translate
19
+ onClose: () => void
20
+ }) {
21
+ // 内容:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则展示兜底文案。
22
+ // Worker 未推送(info 为 null / content 为空)时弹窗仍能打开,显示「暂未发布」而非报错。
23
+ const contentRaw = info
24
+ ? typeof info.content === 'string'
25
+ ? info.content
26
+ : info.content && typeof info.content === 'object'
27
+ ? lang === 'en'
28
+ ? (info.content.en ?? info.content.zh ?? '')
29
+ : (info.content.zh ?? info.content.en ?? '')
30
+ : ''
31
+ : ''
32
+ const contentHtml = contentRaw.trim() ? renderMarkdown(contentRaw) : null
33
+ // 更新时间:ISO 字符串转本地可读格式;非法值静默隐藏
34
+ let updated: string | null = null
35
+ if (info?.updatedAt) {
36
+ const d = new Date(info.updatedAt)
37
+ if (!Number.isNaN(d.getTime())) {
38
+ updated = d.toLocaleString(lang === 'en' ? 'en-US' : 'zh-CN')
39
+ }
40
+ }
41
+
42
+ return h('div', {
43
+ className: styles.overlay,
44
+ onClick: (e: MouseEvent<HTMLDivElement>) => {
45
+ if (e.target === e.currentTarget) onClose()
46
+ },
47
+ },
48
+ h('div', { className: `${styles.modal} ${styles.aboutModal}`, role: 'dialog', 'aria-modal': 'true' },
49
+ h('div', { className: styles.modalHead },
50
+ h('div', { className: styles.modalTitle }, t('aboutTitle')),
51
+ h('button', {
52
+ className: styles.modalClose,
53
+ 'aria-label': t('confirmCancel'),
54
+ onClick: () => onClose(),
55
+ }, h(CloseIcon)),
56
+ ),
57
+ h('div', { className: styles.modalDesc }, t('aboutDesc')),
58
+ contentHtml
59
+ ? h('div', {
60
+ className: styles.aboutContent,
61
+ dangerouslySetInnerHTML: { __html: contentHtml },
62
+ })
63
+ : h('div', { className: styles.aboutContent }, t('aboutEmpty')),
64
+ updated
65
+ ? h('div', { className: styles.aboutMeta }, `${t('aboutUpdated')} ${updated}`)
66
+ : null,
67
+ h('div', { className: styles.modalActions },
68
+ h('button', { className: styles.modalInstall, onClick: onClose }, t('doneBtn')),
69
+ ),
70
+ ),
71
+ )
72
+ }
@@ -8,12 +8,18 @@ import type { Translate } from '../types.ts'
8
8
  import { GITHUB_URL, PLUGIN_VERSION, SITE_URL } from '../lib/catalog.ts'
9
9
  import { GitHubIcon, LogoIcon } from './icons.tsx'
10
10
 
11
- export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggleLang }: {
11
+ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggleLang, hubUpdate, onVersionClick, onAboutClick }: {
12
12
  t: Translate
13
13
  langPath: string
14
14
  statsTotal: number
15
15
  statsVerified: number
16
16
  onToggleLang: () => void
17
+ /** Hub 自身是否有可用更新:有则版本号后紧跟红色「可更新」徽标(整体一个可点入口) */
18
+ hubUpdate: boolean
19
+ /** 点击版本号(含红色徽标,同一入口):无论有无更新都打开更新记录弹窗 */
20
+ onVersionClick: () => void
21
+ /** 点击「关注我们」:打开平台介绍 + 用户反馈群二维码弹窗 */
22
+ onAboutClick: () => void
17
23
  }) {
18
24
  return h(Fragment, null,
19
25
  h('div', { className: styles.header },
@@ -28,10 +34,20 @@ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggle
28
34
  'aria-label': t('openHint'),
29
35
  },
30
36
  h(LogoIcon),
31
- h('h1', { className: styles.title },
32
- t('title'),
33
- h('span', { className: styles.version }, ` v${PLUGIN_VERSION}`),
34
- ),
37
+ h('h1', { className: styles.title }, t('title')),
38
+ ),
39
+ // 版本号:紧跟标题「DSH-Plugin Hub」之后,常驻可点入口(<a> 内不能嵌套按钮,故从官网链接里移出放在旁边)。
40
+ // 点击打开更新记录弹窗 —— 有更新看新版本内容,无更新看当前版本内容。
41
+ // 有更新时版本号后直接跟一个红色「可更新」徽标(同一按钮内,不分开成两个元素),静态显示不闪烁。
42
+ h('button', {
43
+ className: styles.versionBtn,
44
+ type: 'button',
45
+ onClick: onVersionClick,
46
+ title: t(hubUpdate ? 'hubUpdateHint' : 'versionHint'),
47
+ 'aria-label': t(hubUpdate ? 'hubUpdateHint' : 'versionHint'),
48
+ },
49
+ `v${PLUGIN_VERSION}`,
50
+ hubUpdate && h('span', { className: styles.hubUpdateBadge }, t('update')),
35
51
  ),
36
52
  // 右侧控件组:语言切换 + GitHub 源码图标(独立链接,brandTitle 是官网 <a> 不能嵌套),
37
53
  // 一起 space-between 顶到标题行最右
@@ -44,6 +60,15 @@ export function CatalogHeader({ t, langPath, statsTotal, statsVerified, onToggle
44
60
  title: t('toggleLangHint'),
45
61
  'aria-label': t('toggleLangHint'),
46
62
  }, langPath === 'zh/' ? 'EN' : '中文'),
63
+ // 「关注我们」:点击打开平台介绍 + 用户反馈群二维码弹窗
64
+ h('button', {
65
+ className: styles.aboutBtn,
66
+ type: 'button',
67
+ onClick: onAboutClick,
68
+ title: t('aboutDesc'),
69
+ 'aria-label': t('aboutTitle'),
70
+ }, t('followUs')),
71
+ // GitHub 源码图标:独立链接(brandTitle 是官网 <a> 不能嵌套),放在控件组最后
47
72
  h('a', {
48
73
  className: styles.githubLink,
49
74
  href: GITHUB_URL,
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Hub 版本信息 / 自我更新说明弹窗:
3
+ * - 有更新(hasUpdate=true):标题「有新版本」,展示新版本号、发布时间与 Worker 下发的
4
+ * Markdown 变更记录(renderMarkdown 渲染,双语言按界面语言取 {zh,en} 对象),
5
+ * 按钮「稍后再说 / 直接更新」,确认后进入安装弹窗执行覆盖重装。
6
+ * - 无更新(hasUpdate=false):标题「当前版本」,展示当前版本信息与更新记录,已是最新。
7
+ * 点击头部版本号(或「可更新」徽标)打开 —— 无论有无更新都能看到更新内容。
8
+ */
9
+ import { createElement as h } from 'react'
10
+ import type { MouseEvent } from 'react'
11
+ import styles from '../styles/Modal.module.css'
12
+ import type { HubUpdateInfo } from '../lib/catalog.ts'
13
+ import type { LocaleId, Translate } from '../types.ts'
14
+ import { renderMarkdown } from '../lib/renderMarkdown.ts'
15
+ import { CloseIcon } from './icons.tsx'
16
+
17
+ export function HubUpdateModal({ info, lang, t, hasUpdate, onProceed, onClose }: {
18
+ info: HubUpdateInfo
19
+ lang: LocaleId
20
+ t: Translate
21
+ /** 是否有可用更新:有则显示「直接更新」,无则显示「已是最新」 */
22
+ hasUpdate: boolean
23
+ /** 点「直接更新」:关闭本弹窗,进入安装弹窗的更新流程 */
24
+ onProceed: () => void
25
+ onClose: () => void
26
+ }) {
27
+ // 变更记录:字符串直接使用;{zh,en} 对象按界面语言取,缺哪种补哪种,全缺则不出记录区
28
+ const notesRaw = typeof info.notes === 'string'
29
+ ? info.notes
30
+ : info.notes && typeof info.notes === 'object'
31
+ ? lang === 'en'
32
+ ? (info.notes.en ?? info.notes.zh ?? '')
33
+ : (info.notes.zh ?? info.notes.en ?? '')
34
+ : ''
35
+ const notesHtml = notesRaw.trim() ? renderMarkdown(notesRaw) : null
36
+ // 发布时间:ISO 字符串转本地可读格式;非法值静默隐藏
37
+ let published: string | null = null
38
+ if (info.publishedAt) {
39
+ const d = new Date(info.publishedAt)
40
+ if (!Number.isNaN(d.getTime())) {
41
+ published = d.toLocaleString(lang === 'en' ? 'en-US' : 'zh-CN')
42
+ }
43
+ }
44
+
45
+ return h('div', {
46
+ className: styles.overlay,
47
+ onClick: (e: MouseEvent<HTMLDivElement>) => {
48
+ if (e.target === e.currentTarget) onClose()
49
+ },
50
+ },
51
+ h('div', { className: `${styles.modal} ${styles.hubUpdateModal}`, role: 'dialog', 'aria-modal': 'true' },
52
+ h('div', { className: styles.modalHead },
53
+ h('div', { className: styles.modalTitle }, t(hasUpdate ? 'hubUpdateTitle' : 'hubCurrentTitle')),
54
+ h('button', {
55
+ className: styles.modalClose,
56
+ 'aria-label': t('confirmCancel'),
57
+ onClick: () => onClose(),
58
+ }, h(CloseIcon)),
59
+ ),
60
+ h('div', { className: styles.modalDesc },
61
+ t(hasUpdate ? 'hubUpdateDesc' : 'hubCurrentDesc', { version: info.version })),
62
+ h('div', { className: styles.hubUpdateMeta },
63
+ h('span', { className: styles.hubUpdateMetaItem }, `${t('version')} ${info.version}`),
64
+ published
65
+ ? h('span', { className: styles.hubUpdateMetaItem }, `${t('hubUpdatePublished')} ${published}`)
66
+ : null,
67
+ ),
68
+ notesHtml
69
+ ? h('div', {
70
+ className: styles.hubUpdateNotes,
71
+ dangerouslySetInnerHTML: { __html: notesHtml },
72
+ })
73
+ : null,
74
+ h('div', { className: styles.modalActions },
75
+ hasUpdate
76
+ ? [
77
+ h('button', { className: styles.restartLater, onClick: onClose }, t('hubUpdateLater')),
78
+ h('button', { className: styles.modalInstall, onClick: onProceed }, t('updateNow')),
79
+ ]
80
+ : h('button', { className: styles.modalInstall, onClick: onClose }, t('hubUpToDate')),
81
+ ),
82
+ ),
83
+ )
84
+ }
@@ -156,7 +156,10 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
156
156
  ? h('div', { className: styles.failEmpty }, t('notificationsEmpty'))
157
157
  : h('div', { className: styles.noticeList },
158
158
  records.map((r) => {
159
- return h('div', { key: r.id, className: styles.noticeRow },
159
+ return h('div', {
160
+ key: r.id,
161
+ className: r.ok ? `${styles.noticeRow} ${styles.noticeRowOk}` : styles.noticeRow,
162
+ },
160
163
  h('div', { className: styles.noticeRowMain },
161
164
  h('div', {
162
165
  className: r.ok ? styles.noticeBadgeOk : styles.noticeBadgeFail,
@@ -182,6 +185,14 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
182
185
  className: styles.failCopy,
183
186
  onClick: () => onCopy(r.message),
184
187
  }, t('failCopy')),
188
+ // 成功通知:时间戳并入本行右侧,与图标/文字垂直居中 ——
189
+ // 不再单独占底部一行,卡片只有一行内容,图标文字整体居中不顶头
190
+ r.ok
191
+ ? h('span', {
192
+ className: styles.noticeTime,
193
+ title: new Date(r.at).toLocaleString(),
194
+ }, fmtTime(r.at))
195
+ : null,
185
196
  // 每条通知右侧的删除按钮:单独移除这一条
186
197
  h('button', {
187
198
  className: styles.noticeRemove,
@@ -220,13 +231,15 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
220
231
  })(),
221
232
  ),
222
233
  ),
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
- ),
234
+ // 失败通知:时间戳保留独立底部行(右下角);成功通知已并入头部行右侧
235
+ !r.ok
236
+ ? h('div', { className: styles.noticeFoot },
237
+ h('span', {
238
+ className: styles.noticeTime,
239
+ title: new Date(r.at).toLocaleString(),
240
+ }, fmtTime(r.at)),
241
+ )
242
+ : null,
230
243
  )
231
244
  }),
232
245
  ),
@@ -84,6 +84,9 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
84
84
  disabled: true,
85
85
  title: t('installed'),
86
86
  }, t('installed')))
87
+ // 未安装:按钮恒为「安装」,点击先弹信任确认;AI 识别为「可能需要命令行辅助」
88
+ // 的插件(webInstallable=false)不拦截,允许尝试一键安装,弹窗内会给出提示与
89
+ // 复制命令通道,安装失败时引导去 dsh 终端手动执行
87
90
  : h('button', {
88
91
  className: isCopied ? styles.installBtnCopied : styles.installBtn,
89
92
  // 文字恒定避免按钮宽度变化导致卡片跳动;点击先弹信任确认,
@@ -8,11 +8,13 @@ import { createElement as h, useEffect, useState } from 'react'
8
8
  import styles from '../styles/Header.module.css'
9
9
  import { en, zh } from '../locales.ts'
10
10
  import type { EnvInfo, HubPlugin, LocaleId, SectionProps, ToastState } from '../types.ts'
11
- import { langPathOf, installCommandOf } from '../lib/catalog.ts'
11
+ import { langPathOf, installCommandOf, HUB_REPO, PLUGIN_VERSION } from '../lib/catalog.ts'
12
12
  import { getEnv } from '../lib/env.ts'
13
13
  import { useCatalog } from '../hooks/useCatalog.ts'
14
14
  import { useTaskQueue } from '../hooks/useTaskQueue.ts'
15
15
  import { ErrorModal, InstallModal, UninstallModal, Toast } from './modals.tsx'
16
+ import { AboutModal } from './AboutModal.tsx'
17
+ import { HubUpdateModal } from './HubUpdateModal.tsx'
16
18
  import { NotificationsModal } from './NotificationsModal.tsx'
17
19
  import { addFailure, addSuccess, clearNotifications, loadNotifications, removeNotification } from '../lib/failures.ts'
18
20
  import type { NotificationRecord } from '../lib/failures.ts'
@@ -48,11 +50,19 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
48
50
  const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
49
51
  /** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
50
52
  const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
53
+ /** Hub 自我更新说明弹窗:点「可更新」徽标先展示版本 + Markdown 变更记录,确认后再进安装弹窗 */
54
+ const [showHubUpdate, setShowHubUpdate] = useState(false)
55
+ /** 「关注我们」弹窗:GitHub 图标后按钮点击打开,展示平台介绍 + 用户反馈群二维码(Worker /about 推送) */
56
+ const [showAbout, setShowAbout] = useState(false)
51
57
  /** 卸载确认弹窗:记录待卸载的插件 */
52
58
  const [uninstallPlugin, setUninstallPlugin] = useState<HubPlugin | null>(null)
53
59
  /** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
54
60
  const [installDone, setInstallDone] = useState(false)
55
61
  const [uninstallDone, setUninstallDone] = useState(false)
62
+ /** 结果视图是否给「立即重启」:服务端任务终态带出(卸载时 loader 已即时移除 → false 只给「完成」;
63
+ * true 时通知中心待重启条目同步常驻,直到用户点「立即重启」真正重启后才消失) */
64
+ const [installNeedsRestart, setInstallNeedsRestart] = useState(true)
65
+ const [uninstallNeedsRestart, setUninstallNeedsRestart] = useState(true)
56
66
  /** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
57
67
  const [restarting, setRestarting] = useState(false)
58
68
  /** 操作失败完整信息 + 所属插件仓库 + 失败类型(决定弹窗标题「安装失败/卸载失败」)+ 实际执行的安装命令 + 尝试过的安装方式(issue 预填用) */
@@ -71,13 +81,15 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
71
81
  const queue = useTaskQueue({
72
82
  t,
73
83
  refreshInstalled: catalog.refreshInstalled,
74
- onInstallDone: (viaModal, repo) => {
84
+ onInstallDone: (viaModal, repo, needsRestart) => {
85
+ setInstallNeedsRestart(needsRestart)
75
86
  if (viaModal) setInstallDone(true)
76
87
  else setToast({ id: Date.now(), kind: 'done' })
77
88
  // 安装成功也写入通知记录:通知中心里成功与失败都能看到
78
89
  setNotifications(addSuccess({ kind: 'install', repo: repo ?? '' }))
79
90
  },
80
- onUninstallDone: (viaModal, repo) => {
91
+ onUninstallDone: (viaModal, repo, needsRestart) => {
92
+ setUninstallNeedsRestart(needsRestart)
81
93
  if (viaModal) setUninstallDone(true)
82
94
  else setToast({ id: Date.now(), kind: 'removed' })
83
95
  setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
@@ -97,20 +109,22 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
97
109
  },
98
110
  })
99
111
 
100
- // 信任/卸载/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
112
+ // 信任/卸载/更新说明/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
101
113
  useEffect(() => {
102
- if (!confirmPlugin && !uninstallPlugin && !showNotifications) return
114
+ if (!confirmPlugin && !uninstallPlugin && !showNotifications && !showHubUpdate && !showAbout) return
103
115
  const onKey = (e: KeyboardEvent) => {
104
116
  if (e.key === 'Escape') {
105
117
  setConfirmPlugin(null)
106
118
  setUninstallPlugin(null)
107
119
  setUninstallDone(false)
120
+ setShowHubUpdate(false)
121
+ setShowAbout(false)
108
122
  setShowNotifications(false)
109
123
  }
110
124
  }
111
125
  window.addEventListener('keydown', onKey)
112
126
  return () => window.removeEventListener('keydown', onKey)
113
- }, [confirmPlugin, uninstallPlugin, showNotifications])
127
+ }, [confirmPlugin, uninstallPlugin, showNotifications, showHubUpdate, showAbout])
114
128
 
115
129
  // Toast 统一自动消失
116
130
  useEffect(() => {
@@ -204,6 +218,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
204
218
 
205
219
  const count = catalog.visible.length
206
220
 
221
+ // Hub 自身条目(目录数据里的 dshplugin/dsh-plugin-hub):头部「可更新」徽标的更新目标。
222
+ // 目录可能尚未加载(此时 hubEntry 为 null,徽标不渲染,加载完成后自然出现)。
223
+ const hubEntry = catalog.plugins?.find((p) => p.source?.repo === HUB_REPO) ?? null
224
+
207
225
  return h('div', { className: styles.root },
208
226
  h(CatalogHeader, {
209
227
  t,
@@ -211,6 +229,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
211
229
  statsTotal,
212
230
  statsVerified,
213
231
  onToggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
232
+ // Hub 自身版本信息入口:版本号常驻可点(无更新也看得到当前版本更新内容),
233
+ // 有可用更新时版本号后紧跟红色「可更新」徽标(同一入口),点击都打开同一个更新说明弹窗。
234
+ // 有更新弹窗显示新版本 + Markdown 变更记录,点「直接更新」进安装弹窗覆盖重装。
235
+ hubUpdate: catalog.hubHasUpdate,
236
+ onVersionClick: () => setShowHubUpdate(true),
237
+ onAboutClick: () => setShowAbout(true),
214
238
  }),
215
239
  h(CategoryTabs, {
216
240
  category: catalog.category,
@@ -270,6 +294,34 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
270
294
  setUninstallPlugin(p)
271
295
  },
272
296
  }),
297
+ // Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
298
+ // 无更新显示当前版本 + 记录 + 「已是最新」。Worker 拉取失败时兜底展示当前版本号。
299
+ showHubUpdate && h(HubUpdateModal, {
300
+ info: catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
301
+ lang,
302
+ t,
303
+ hasUpdate: catalog.hubHasUpdate,
304
+ onClose: () => setShowHubUpdate(false),
305
+ onProceed: () => {
306
+ setShowHubUpdate(false)
307
+ // 与列表内更新同一流程:覆盖重装 dsh-plugin + 结果视图「立即重启」
308
+ if (hubEntry) {
309
+ setInstallDone(false)
310
+ setConfirmIsUpdate(true)
311
+ queue.clearModalTask()
312
+ setConfirmPlugin(hubEntry)
313
+ }
314
+ },
315
+ }),
316
+ // 「关注我们」弹窗:GitHub 图标后按钮点击打开 —— 平台介绍 + 用户反馈群二维码,
317
+ // 内容由 dsh-update Worker 的 /about 接口以 Markdown 形式推送,作者随时可改,非写死。
318
+ // Worker 未推送(info 为 null)时弹窗仍能打开并展示兜底文案,不影响其他功能。
319
+ showAbout && h(AboutModal, {
320
+ info: catalog.hubAboutInfo ?? null,
321
+ lang,
322
+ t,
323
+ onClose: () => setShowAbout(false),
324
+ }),
273
325
  // 安装确认弹窗:锁定/进度/结果视图逻辑收敛在 modals.tsx;更新与安装同入口,仅标记 update
274
326
  confirmPlugin && h(InstallModal, {
275
327
  plugin: confirmPlugin,
@@ -279,7 +331,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
279
331
  langPath,
280
332
  restarting,
281
333
  update: confirmIsUpdate,
334
+ // 仅命令行插件(webInstallable=false):弹窗只展示命令供复制,不提供一键安装
335
+ cliOnly: confirmPlugin.install?.webInstallable === false,
282
336
  submitting: queue.submitting,
337
+ needsRestart: installNeedsRestart,
283
338
  onClose: () => setConfirmPlugin(null),
284
339
  onCopy: () => copyCommand(confirmPlugin),
285
340
  onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
@@ -294,6 +349,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
294
349
  langPath,
295
350
  restarting,
296
351
  submitting: queue.submitting,
352
+ needsRestart: uninstallNeedsRestart,
297
353
  onClose: () => {
298
354
  setUninstallDone(false)
299
355
  setUninstallPlugin(null)