dsh-plugin 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/client/client.js +4465 -1442
  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 +86 -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
@@ -0,0 +1,24 @@
1
+ export interface HubSettings {
2
+ /** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
3
+ checkUpdatesOnStart: boolean;
4
+ /** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
5
+ npmRegistry: string;
6
+ /** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
7
+ proxy: string;
8
+ /** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
9
+ enableNpmInstall: boolean;
10
+ /** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
11
+ enableGitInstall: boolean;
12
+ /** 启用命令行粘贴 dsh plugin 命令安装(默认开;关掉后命令输入被禁用) */
13
+ enableDshInstall: boolean;
14
+ /** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log;可填目录或 .log 文件路径) */
15
+ logPath: string;
16
+ }
17
+ export declare const DEFAULT_SETTINGS: HubSettings;
18
+ export declare function settingsFile(profile: string): string;
19
+ /** 读取当前 profile 的设置;文件缺失/损坏时回退默认值(只读路径绝不抛错)。 */
20
+ export declare function loadSettings(profile: string): HubSettings;
21
+ /** 合并并持久化设置(部分更新),返回合并后的完整设置。 */
22
+ export declare function saveSettings(profile: string, patch: Partial<HubSettings>): HubSettings;
23
+ /** 重置为默认设置:删除设置文件,下次读取全部回退默认值(陈旧/白名单外键一并清除)。 */
24
+ export declare function resetSettings(profile: string): HubSettings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.1.2",
3
+ "version": "1.2.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",
@@ -8,43 +8,41 @@
8
8
  * rendering is delegated to the small presentational components in this
9
9
  * folder; it hosts the dialogs/toast and the section-level copy actions.
10
10
  */
11
- import { createElement as h, useEffect, useState } from 'react'
11
+ import { createElement as h, useEffect, useRef, useState } from 'react'
12
12
  import styles from '../styles/Header.module.css'
13
- import { en, zh } from '../locales.ts'
14
- import type { EnvInfo, HubPlugin, LocaleId, SectionProps, ToastState } from '../types.ts'
15
- import { langPathOf, installCommandOf, HUB_REPO, PLUGIN_VERSION } from '../lib/catalog.ts'
16
- import { getEnv } from '../lib/env.ts'
13
+ import type { EnvInfo, HubPlugin, SectionProps, ToastState } from '../types.ts'
14
+ import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
15
+ import { PLUGIN_VERSION } from '../logic/constants.ts'
16
+ import { getEnv, revealInstallFolder } from '../data/host.ts'
17
17
  import { useCatalog } from '../hooks/useCatalog.ts'
18
+ import { useLanguage } from '../hooks/useLanguage.ts'
19
+ import { useSettings } from '../hooks/useSettings.ts'
18
20
  import { useTaskQueue } from '../hooks/useTaskQueue.ts'
19
- import { ErrorModal, InstallModal, UninstallModal, Toast } from './modals.tsx'
20
- import { AboutModal } from './AboutModal.tsx'
21
- import { HubUpdateModal } from './HubUpdateModal.tsx'
22
- import { NotificationsModal } from './NotificationsModal.tsx'
23
- import { addFailure, addSuccess, clearNotifications, loadNotifications, removeNotification } from '../lib/failures.ts'
24
- import type { NotificationRecord } from '../lib/failures.ts'
25
- import { CatalogHeader } from './CatalogHeader.tsx'
26
- import { CategoryTabs } from './CategoryTabs.tsx'
27
- import { CatalogControls } from './CatalogControls.tsx'
28
- import { CatalogList } from './CatalogList.tsx'
21
+ import { ErrorModal, InstallModal, RestartConfirmModal, UninstallModal, Toast } from './modals/modals.tsx'
22
+ import { AboutModal } from './modals/AboutModal.tsx'
23
+ import { HubUpdateModal } from './modals/HubUpdateModal.tsx'
24
+ import { InstalledDetailModal } from './modals/InstalledDetailModal.tsx'
25
+ import { NotificationsModal } from './modals/NotificationsModal.tsx'
26
+ import { addFailure, addSuccess, addUpdateNotice, clearNotifications, loadNotifications, removeNotification } from '../logic/failures.ts'
27
+ import type { NotificationRecord } from '../logic/failures.ts'
28
+ import { CatalogHeader } from './layout/CatalogHeader.tsx'
29
+ import { MarketView } from './views/MarketView.tsx'
30
+ import { SectionTabs } from './layout/SectionTabs.tsx'
31
+ import type { SectionView } from './layout/SectionTabs.tsx'
32
+ import { InstalledView } from './views/InstalledView.tsx'
33
+ import { CustomInstallView } from './views/CustomInstallView.tsx'
34
+ import { SettingsView } from './views/SettingsView.tsx'
35
+ import type { InstalledItem } from '../logic/installed.ts'
36
+ import { pluginOfItem } from '../logic/installed.ts'
29
37
 
30
38
  export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
31
- /** 界面语言:默认跟随宿主(系统)语言;右上角按钮可手动切换,切换后以手动选择为准 */
32
- const [manualLang, setManualLang] = useState<LocaleId | null>(null)
33
- const lang: LocaleId = manualLang ?? locale.getSnapshot().active
34
- const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
35
- // dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
36
- const langPath = langPathOf(lang)
37
-
38
- // 界面语言跟随宿主(系统)locale 自动切换;宿主的 t() 绑定宿主 locale,
39
- // 这里基于本地字典自建翻译函数,与宿主 locale 保持一致。
40
- // (settings.section 的导航 label 仍在 apply 里用宿主 t(),跟随宿主语言。)
41
- const dict: Record<string, string> = langKey === 'en' ? en : zh
42
- const t = (key: string, params?: Record<string, string | number>): string => {
43
- const raw = dict[key] ?? key
44
- return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? '')) : raw
45
- }
46
-
39
+ const { lang, langKey, langPath, t, toggleLang } = useLanguage(locale)
47
40
  const catalog = useCatalog(lang)
41
+ /** 设置状态:服务端 hub-settings.json 持久化,本地乐观更新即时生效 */
42
+ const { settings: hubSettings, ready: settingsReady, update: updateSettings, reset: resetSettings } = useSettings()
43
+
44
+ /** 一级导航:插件中心 / 已安装 / 自定义安装 / 设置 */
45
+ const [view, setView] = useState<SectionView>('market')
48
46
 
49
47
  /** 全局反馈 Toast:{id} 用于重复触发时重新走入场动画,kind 决定文案与配色 */
50
48
  const [toast, setToast] = useState<ToastState | null>(null)
@@ -54,12 +52,25 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
54
52
  const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
55
53
  /** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
56
54
  const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
55
+ /** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名 / GitHub 地址,DSH 命令已剥成裸目标)——
56
+ * 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
57
+ const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
58
+ /** 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):记录原始命令 + 解析出的包列表,
59
+ * 走与应用商店同一套确认/进度/结果弹窗(plugin 传 null,InstallModal 走 globalNpm 模式) */
60
+ const [confirmGlobalNpm, setConfirmGlobalNpm] = useState<{ raw: string; pkgs: string[] } | null>(null)
57
61
  /** Hub 自我更新说明弹窗:点「可更新」徽标先展示版本 + Markdown 变更记录,确认后再进安装弹窗 */
58
62
  const [showHubUpdate, setShowHubUpdate] = useState(false)
59
63
  /** 「关注我们」弹窗:GitHub 图标后按钮点击打开,展示平台介绍 + 用户反馈群二维码(Worker /about 推送) */
60
64
  const [showAbout, setShowAbout] = useState(false)
61
- /** 卸载确认弹窗:记录待卸载的插件 */
65
+ /** 卸载确认弹窗:记录待卸载的插件(目录内) */
62
66
  const [uninstallPlugin, setUninstallPlugin] = useState<HubPlugin | null>(null)
67
+ /** 卸载确认弹窗:记录待卸载的已安装项(自定义安装无目录数据,按包名直卸) */
68
+ const [uninstallItem, setUninstallItem] = useState<InstalledItem | null>(null)
69
+ /** 已安装详情弹窗:记录当前查看的已安装项(行点击 / 详情按钮打开) */
70
+ const [detailItem, setDetailItem] = useState<InstalledItem | null>(null)
71
+ /** 待重启确认弹窗:已安装列表行内「重启」先弹「立即重启 / 稍后重启」确认(与通知中心一致),
72
+ * 确认后才真正触发宿主重启,避免误触 */
73
+ const [showRestartConfirm, setShowRestartConfirm] = useState(false)
63
74
  /** 安装/卸载完成后的结果视图:停留弹窗内,点「完成」关闭 */
64
75
  const [installDone, setInstallDone] = useState(false)
65
76
  const [uninstallDone, setUninstallDone] = useState(false)
@@ -71,8 +82,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
71
82
  const [restarting, setRestarting] = useState(false)
72
83
  /** 操作失败完整信息 + 所属插件仓库 + 失败类型(决定弹窗标题「安装失败/卸载失败」)+ 实际执行的安装命令 + 尝试过的安装方式(issue 预填用) */
73
84
  const [errorMsg, setErrorMsg] = useState<{ message: string; repo: string | null; kind: 'install' | 'uninstall'; command?: string; attempts?: string[] } | null>(null)
74
- /** 安装/卸载任务通知记录:localStorage 持久化,成败即落盘,即使错过弹窗也能回来查看 */
85
+ /** 通知中心记录:localStorage 持久化,每次安装/卸载任务成败都留痕(启动时读回) */
75
86
  const [notifications, setNotifications] = useState<NotificationRecord[]>(() => loadNotifications())
87
+ /** 通知中心弹窗:一级导航「设置」tab 后边的铃铛入口按钮打开 */
76
88
  const [showNotifications, setShowNotifications] = useState(false)
77
89
  /** 宿主机器环境快照:提交 bug 的 issue 正文附带;取不到为 null(链接少环境段,不阻塞) */
78
90
  const [env, setEnv] = useState<EnvInfo | null>(null)
@@ -104,7 +116,10 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
104
116
  setNotifications(addFailure({ kind, repo: repo ?? '', message, command, attempts }))
105
117
  },
106
118
  installPlugin: confirmPlugin,
119
+ installCustomTarget: confirmCustomTarget,
120
+ installGlobalTarget: confirmGlobalNpm ? confirmGlobalNpm.pkgs.join(' ') : null,
107
121
  uninstallPlugin,
122
+ uninstallName: uninstallItem ? uninstallItem.name : null,
108
123
  installedName: catalog.installedName,
109
124
  // 待重启项展示信息补齐:从目录按 owner/repo 解析插件简介/版本(找不到返回 null,行内只显示仓库名)
110
125
  resolvePending: (repo) => {
@@ -113,13 +128,36 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
113
128
  },
114
129
  })
115
130
 
116
- // 信任/卸载/更新说明/通知记录弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
131
+ // 启动更新策略:目录数据与设置都就绪后触发一次 —— 开启「启动时检查更新」→ 发现可更新插件
132
+ // 即写入通知中心(每条一条,点击可去更新),toast 同步提示已入通知。
133
+ // 绝不自动安装:是否更新完全由用户决定。
134
+ // ref 守卫保证只跑一次,后续目录刷新/设置变化不会重复打扰。
135
+ const startupCheckedRef = useRef(false)
136
+ useEffect(() => {
137
+ if (startupCheckedRef.current) return
138
+ if (!settingsReady || catalog.plugins === null || catalog.failed) return
139
+ startupCheckedRef.current = true
140
+ if (!hubSettings.checkUpdatesOnStart) return
141
+ const updatable = catalog.installedItems.filter((i) => i.hasUpdate && i.plugin)
142
+ if (updatable.length === 0) return
143
+ for (const item of updatable) {
144
+ if (item.repo) addUpdateNotice({ kind: 'update', repo: item.repo, version: item.catalogVersion ?? undefined })
145
+ }
146
+ setNotifications(loadNotifications())
147
+ setToast({ id: Date.now(), kind: 'updates', n: updatable.length })
148
+ }, [settingsReady, hubSettings, catalog.plugins, catalog.failed, catalog.installedItems, queue])
149
+
150
+ // 信任/卸载/更新说明弹窗打开时按 Esc 关闭;任务进入后台队列后可随时关闭(任务继续)
117
151
  useEffect(() => {
118
- if (!confirmPlugin && !uninstallPlugin && !showNotifications && !showHubUpdate && !showAbout) return
152
+ if (!confirmPlugin && !confirmCustomTarget && !confirmGlobalNpm && !uninstallPlugin && !uninstallItem && !detailItem && !showHubUpdate && !showAbout && !showNotifications) return
119
153
  const onKey = (e: KeyboardEvent) => {
120
154
  if (e.key === 'Escape') {
121
155
  setConfirmPlugin(null)
156
+ setConfirmCustomTarget(null)
157
+ setConfirmGlobalNpm(null)
122
158
  setUninstallPlugin(null)
159
+ setUninstallItem(null)
160
+ setDetailItem(null)
123
161
  setUninstallDone(false)
124
162
  setShowHubUpdate(false)
125
163
  setShowAbout(false)
@@ -128,7 +166,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
128
166
  }
129
167
  window.addEventListener('keydown', onKey)
130
168
  return () => window.removeEventListener('keydown', onKey)
131
- }, [confirmPlugin, uninstallPlugin, showNotifications, showHubUpdate, showAbout])
169
+ }, [confirmPlugin, confirmCustomTarget, uninstallPlugin, uninstallItem, detailItem, showHubUpdate, showAbout, showNotifications])
132
170
 
133
171
  // Toast 统一自动消失
134
172
  useEffect(() => {
@@ -162,6 +200,23 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
162
200
  }
163
201
  }
164
202
 
203
+ /** 命令行安装目标是否已安装(与服务端 already 判定同口径):GitHub 地址按仓库身份匹配
204
+ * (归一化后比对已装项的 repo/spec),npm 包名按依赖 key 直查。命中 → 弹窗转「更新」语义
205
+ * (mode=update 放行覆盖重装),避免重复点击撞 409 报错。 */
206
+ const customTargetInstalled = (raw: string): boolean => {
207
+ const target = raw.trim()
208
+ if (!target) return false
209
+ const repo = repoFromInstallTarget(target)
210
+ const isRepo = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)
211
+ if (isRepo) {
212
+ const needle = repo.toLowerCase()
213
+ return catalog.installedItems.some((i) =>
214
+ (i.repo !== null && i.repo.toLowerCase() === needle) ||
215
+ repoFromInstallTarget(i.spec).toLowerCase() === needle)
216
+ }
217
+ return catalog.installedItems.some((i) => i.name === target)
218
+ }
219
+
165
220
  /** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行(npm 通道显示包名命令)。 */
166
221
  const copyCommand = async (p: HubPlugin) => {
167
222
  const repo = p.source?.repo ?? ''
@@ -174,14 +229,21 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
174
229
  setConfirmPlugin(null)
175
230
  }
176
231
 
177
- /** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。 */
232
+ /** 卸载弹窗动作:复制卸载命令,万一直接卸载失败可去终端手动执行。
233
+ * 自定义安装(无目录数据)按已安装项包名直卸,同样给复制通道。 */
178
234
  const copyUninstallCommand = async () => {
179
- const name = uninstallPlugin ? catalog.installedName(uninstallPlugin) : null
235
+ const name = uninstallItem ? uninstallItem.name : (uninstallPlugin ? catalog.installedName(uninstallPlugin) : null)
180
236
  if (!name) return
181
237
  const ok = await doCopy(`dsh plugin remove ${name}`)
182
238
  if (ok) setToast({ id: Date.now(), kind: 'copied' })
183
239
  }
184
240
 
241
+ /** 详情弹窗动作:在系统文件管理器里定位安装目录;服务端失败给 toast 提示。 */
242
+ const revealFolder = async (item: InstalledItem) => {
243
+ const ok = await revealInstallFolder(item.name)
244
+ if (!ok) setToast({ id: Date.now(), kind: 'revealFail' })
245
+ }
246
+
185
247
  /** 「立即重启」:POST 同源 /restart,宿主进程自杀重启;随后轮询服务恢复后整页刷新。 */
186
248
  const requestRestart = async () => {
187
249
  if (restarting) return
@@ -223,8 +285,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
223
285
  const count = catalog.visible.length
224
286
 
225
287
  // Hub 自身条目(目录数据里的 dshplugin/dsh-plugin-hub):头部「可更新」徽标的更新目标。
226
- // 目录可能尚未加载(此时 hubEntry 为 null,徽标不渲染,加载完成后自然出现)。
227
- const hubEntry = catalog.plugins?.find((p) => p.source?.repo === HUB_REPO) ?? null
288
+ // useCatalog 在过滤目录(自身不进插件列表)前单独保留该条目;目录可能尚未加载(此时为 null,徽标不渲染)。
289
+ const hubEntry = catalog.hubPlugin
228
290
 
229
291
  return h('div', { className: styles.root },
230
292
  h(CatalogHeader, {
@@ -232,7 +294,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
232
294
  langPath,
233
295
  statsTotal,
234
296
  statsVerified,
235
- onToggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
297
+ onToggleLang: toggleLang,
236
298
  // Hub 自身版本信息入口:版本号常驻可点(无更新也看得到当前版本更新内容),
237
299
  // 有可用更新时版本号后紧跟红色「可更新」徽标(同一入口),点击都打开同一个更新说明弹窗。
238
300
  // 有更新弹窗显示新版本 + Markdown 变更记录,点「直接更新」进安装弹窗覆盖重装。
@@ -240,64 +302,117 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
240
302
  onVersionClick: () => setShowHubUpdate(true),
241
303
  onAboutClick: () => setShowAbout(true),
242
304
  }),
243
- h(CategoryTabs, {
244
- category: catalog.category,
245
- setCategory: catalog.setCategory,
246
- allLabel: t('all'),
247
- totalCount: catalog.total,
248
- langKey,
249
- }),
250
- h(CatalogControls, {
251
- query: catalog.query,
252
- setQuery: catalog.setQuery,
253
- sort: catalog.sort,
254
- setSort: catalog.setSort,
255
- installedFilter: catalog.installedFilter,
256
- setInstalledFilter: catalog.setInstalledFilter,
257
- // 已安装/未安装按钮计数跟随当前分类:切到某分类即显示该分类下的已装/未装数量
258
- installedCount: catalog.installedCountInCategory,
259
- notInstalledCount: catalog.notInstalledCountInCategory,
260
- t,
261
- // 列表头部「筛选出 N 条」:分类/搜索/安装状态筛选后的结果数;全部视图且未筛选时显示总数
262
- resultText: catalog.plugins === null || catalog.failed
263
- ? null
264
- : catalog.category === 'all' && count === total
265
- ? t('pluginsTotal', { n: count })
266
- : t('filterResults', { n: count }),
267
- // 通知入口计数:历史通知记录数 + 进行中任务数 + 待重启数 ——
268
- // 待重启不可清除,装完等重启时必须持续亮着提示「必须重启」,不能因为队列空了就归零
269
- // 红圈白字数字(进行中的任务见通知中心「进行中」分区,待重启见「待重启」分区)
270
- noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
271
- onOpenNotifications: () => setShowNotifications(true),
272
- }),
273
- h(CatalogList, {
274
- plugins: catalog.plugins,
275
- failed: catalog.failed,
276
- visible: catalog.visible,
277
- total,
278
- t,
279
- langPath,
280
- reload: catalog.reload,
281
- category: catalog.category,
282
- installedFilter: catalog.installedFilter,
283
- copied,
284
- installedName,
285
- installedVersion,
286
- hasUpdate,
287
- langKey,
288
- onInstall: (p, opts) => {
289
- setInstallDone(false)
290
- setConfirmIsUpdate(opts?.update ?? false)
291
- // 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
292
- queue.clearModalTask()
293
- setConfirmPlugin(p)
294
- },
295
- onUninstall: (p) => {
296
- setUninstallDone(false)
297
- queue.clearModalTask()
298
- setUninstallPlugin(p)
299
- },
300
- }),
305
+ // 一级导航行:tab 占满左侧,通知入口靠右(「设置」tab 后边,右对齐)
306
+ h('div', { className: styles.tabsRow },
307
+ // 「已安装」徽标 = 目录插件 + 自定义安装总数(installedItems 已排除 hub 自身)
308
+ h(SectionTabs, {
309
+ view,
310
+ setView,
311
+ installedCount: catalog.installedItems.length,
312
+ t,
313
+ // 通知中心入口:红圈徽标 = 记录数 + 进行中任务 + 待重启(有动静才醒目)
314
+ noticeCount: notifications.length + queue.queue.length + queue.pendingRestarts.length,
315
+ onOpenNotifications: () => setShowNotifications(true),
316
+ }),
317
+ ),
318
+ view === 'market'
319
+ ? h(MarketView, {
320
+ catalog,
321
+ t,
322
+ langPath,
323
+ langKey,
324
+ copied,
325
+ // 列表头部「筛选出 N 条」:分类/搜索/安装状态筛选后的结果数;全部视图且未筛选时显示总数
326
+ resultText: catalog.plugins === null || catalog.failed
327
+ ? null
328
+ : catalog.category === 'all' && count === total
329
+ ? t('pluginsTotal', { n: count })
330
+ : t('filterResults', { n: count }),
331
+ onInstall: (p, opts) => {
332
+ setInstallDone(false)
333
+ setConfirmIsUpdate(opts?.update ?? false)
334
+ // 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
335
+ queue.clearModalTask()
336
+ setConfirmPlugin(p)
337
+ },
338
+ onUninstall: (p) => {
339
+ setUninstallDone(false)
340
+ queue.clearModalTask()
341
+ setUninstallPlugin(p)
342
+ },
343
+ })
344
+ : view === 'installed'
345
+ ? h(InstalledView, {
346
+ items: catalog.installedItems,
347
+ langKey,
348
+ t,
349
+ // 支持系统文件管理器定位的平台(macOS / Linux)上,行内「打开详情」替换为
350
+ // 打开安装目录按钮(行点击本身即打开详情弹窗);Windows 不支持则按钮不显示
351
+ platform: env?.platform ?? '',
352
+ // 行点击 / 详情按钮:打开已安装详情弹窗(完整元数据 + 运行时信息)
353
+ onOpenDetail: (item) => setDetailItem(item),
354
+ onReveal: (item) => { void revealFolder(item) },
355
+ // 更新:已安装视图的「更新」= 目录插件覆盖重装,与市场列表内更新同一流程
356
+ onUpdate: (item) => {
357
+ if (!item.plugin) return
358
+ setDetailItem(null)
359
+ setInstallDone(false)
360
+ setConfirmIsUpdate(true)
361
+ queue.clearModalTask()
362
+ setConfirmPlugin(item.plugin)
363
+ },
364
+ // 卸载:目录插件与自定义安装统一按已安装项走卸载确认弹窗(自定义无目录数据,按包名直卸)
365
+ onUninstall: (item) => {
366
+ setDetailItem(null)
367
+ setUninstallDone(false)
368
+ queue.clearModalTask()
369
+ setUninstallItem(item)
370
+ },
371
+ // 重启:待重启条目行内按钮 → 先弹「立即重启 / 稍后重启」确认(与通知中心一致),确认后才触发宿主重启
372
+ onRestart: () => setShowRestartConfirm(true),
373
+ })
374
+ : view === 'custom'
375
+ ? h(CustomInstallView, {
376
+ t,
377
+ // 命令行安装:与应用商店同一套确认/进度/结果弹窗 —— 输入 npm 包名 / GitHub 地址 /
378
+ // dsh plugin 命令即装(custom 源,受安全设置三开关控制)。点安装先弹确认窗,
379
+ // 确认后走队列安装(实时进度 + 成功结果视图);目标已安装 → 弹窗转「更新」覆盖重装,
380
+ // 不再撞「already installed」报错(第一次安装没有反馈才导致用户重复点击)。
381
+ // 安全信任开关关掉对应通道 → 卡片禁用并提示去设置打开(onOpenSettings 跳到设置页)。
382
+ enableNpm: hubSettings.enableNpmInstall,
383
+ enableGit: hubSettings.enableGitInstall,
384
+ enableDsh: hubSettings.enableDshInstall,
385
+ onOpenSettings: () => setView('settings'),
386
+ onInstallCustom: (raw, opts) => {
387
+ const target = raw.trim()
388
+ if (!target) return
389
+ setInstallDone(false)
390
+ // 与目录安装一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务
391
+ queue.clearModalTask()
392
+ // 官方全局 npm 安装(npm install -g <pkgs>):弹窗走 globalNpm 模式(系统级 CLI 工具,
393
+ // 不进插件列表,不做「已安装→更新」判定 —— npm install -g 对已装包是原位覆盖更新,天然幂等)
394
+ if (opts?.globalNpm && opts.globalNpm.length > 0) {
395
+ setConfirmCustomTarget(null)
396
+ setConfirmIsUpdate(false)
397
+ setConfirmGlobalNpm({ raw, pkgs: opts.globalNpm })
398
+ return
399
+ }
400
+ setConfirmGlobalNpm(null)
401
+ setConfirmIsUpdate(customTargetInstalled(target))
402
+ setConfirmCustomTarget(target)
403
+ },
404
+ })
405
+ : h(SettingsView, {
406
+ t,
407
+ settings: hubSettings,
408
+ update: updateSettings,
409
+ reset: resetSettings,
410
+ env,
411
+ onCopy: (text: string) => {
412
+ doCopy(text)
413
+ setToast({ id: Date.now(), kind: 'copied' })
414
+ },
415
+ }),
301
416
  // Hub 版本信息弹窗:版本号/徽标点击打开 —— 有更新显示新版本 + 变更记录 + 「直接更新」,
302
417
  // 无更新显示当前版本 + 记录 + 「已是最新」。Worker 拉取失败时兜底展示当前版本号。
303
418
  showHubUpdate && h(HubUpdateModal, {
@@ -326,6 +441,35 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
326
441
  t,
327
442
  onClose: () => setShowAbout(false),
328
443
  }),
444
+ // 安装/卸载任务通知中心:本地持久化的成败清单,随时可回来查看/复制/清空;
445
+ // 进行中的任务(实时进度)与待重启插件也一并展示,关掉任务弹窗后仍可盯着
446
+ showNotifications && h(NotificationsModal, {
447
+ records: notifications,
448
+ tasks: queue.queue,
449
+ pendingRestarts: queue.pendingRestarts,
450
+ t,
451
+ env,
452
+ onClose: () => setShowNotifications(false),
453
+ onCopy: (text) => {
454
+ doCopy(text)
455
+ setToast({ id: Date.now(), kind: 'errCopied' })
456
+ },
457
+ onClear: () => setNotifications(clearNotifications()),
458
+ onRemove: (id: number) => setNotifications(removeNotification(id)),
459
+ // 更新提醒通知:点击直接进该插件的更新确认弹窗(与已安装列表「更新」同一流程)
460
+ onUpdate: (repo: string) => {
461
+ const plugin = catalog.installedItems.find((i) => i.repo === repo)?.plugin
462
+ if (!plugin) return
463
+ setShowNotifications(false)
464
+ setInstallDone(false)
465
+ setConfirmIsUpdate(true)
466
+ queue.clearModalTask()
467
+ setConfirmPlugin(plugin)
468
+ },
469
+ cancelTask: queue.cancelTask,
470
+ restarting,
471
+ onRestart: () => { void requestRestart() },
472
+ }),
329
473
  // 安装确认弹窗:锁定/进度/结果视图逻辑收敛在 modals.tsx;更新与安装同入口,仅标记 update
330
474
  confirmPlugin && h(InstallModal, {
331
475
  plugin: confirmPlugin,
@@ -344,9 +488,61 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
344
488
  onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
345
489
  onRestart: () => { void requestRestart() },
346
490
  }),
347
- // 卸载确认弹窗:确认/进度/结果视图逻辑收敛在 modals.tsx
348
- uninstallPlugin && h(UninstallModal, {
349
- plugin: uninstallPlugin,
491
+ // 命令行安装确认弹窗:与目录安装同一套确认/进度/结果流程(plugin 传 null 走 customTarget 模式)。
492
+ // 已安装目标 → update 标记,确认后走覆盖重装(服务端 mode=update 放行),不再 409 报错。
493
+ confirmCustomTarget && h(InstallModal, {
494
+ plugin: null,
495
+ customTarget: confirmCustomTarget,
496
+ done: installDone,
497
+ task: queue.installModalTask,
498
+ t,
499
+ langPath,
500
+ restarting,
501
+ update: confirmIsUpdate,
502
+ cliOnly: false,
503
+ submitting: queue.submitting,
504
+ needsRestart: installNeedsRestart,
505
+ onClose: () => {
506
+ setInstallDone(false)
507
+ setConfirmCustomTarget(null)
508
+ },
509
+ onCopy: () => {
510
+ doCopy(`pnpm add ${confirmCustomTarget}`)
511
+ setToast({ id: Date.now(), kind: 'copied' })
512
+ },
513
+ onInstall: () => void queue.installCustom(confirmCustomTarget, confirmIsUpdate ? { update: true } : undefined),
514
+ onRestart: () => { void requestRestart() },
515
+ }),
516
+ // 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):与应用商店同一套确认/进度/结果流程。
517
+ // 装的是系统级 CLI 工具(如 @deepseek-ai/dsh),不进任何 profile、无需宿主重启;
518
+ // 服务端任务终态 needsRestart=false → 结果视图仅「完成」。
519
+ confirmGlobalNpm && h(InstallModal, {
520
+ plugin: null,
521
+ globalNpm: confirmGlobalNpm.pkgs,
522
+ done: installDone,
523
+ task: queue.installModalTask,
524
+ t,
525
+ langPath,
526
+ restarting,
527
+ update: false,
528
+ cliOnly: false,
529
+ submitting: queue.submitting,
530
+ needsRestart: installNeedsRestart,
531
+ onClose: () => {
532
+ setInstallDone(false)
533
+ setConfirmGlobalNpm(null)
534
+ },
535
+ onCopy: () => {
536
+ doCopy(`npm install -g ${confirmGlobalNpm.pkgs.join(' ')}`)
537
+ setToast({ id: Date.now(), kind: 'copied' })
538
+ },
539
+ onInstall: () => void queue.installGlobalNpm(confirmGlobalNpm.pkgs),
540
+ onRestart: () => { void requestRestart() },
541
+ }),
542
+ // 卸载确认弹窗:确认/进度/结果视图逻辑收敛在 modals.tsx。
543
+ // 已安装视图的自定义安装(目录外)没有完整目录数据,按已安装项伪插件适配后走同一弹窗
544
+ (uninstallPlugin || uninstallItem) && h(UninstallModal, {
545
+ plugin: uninstallPlugin ?? pluginOfItem(uninstallItem!),
350
546
  done: uninstallDone,
351
547
  task: queue.uninstallModalTask,
352
548
  t,
@@ -357,12 +553,46 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
357
553
  onClose: () => {
358
554
  setUninstallDone(false)
359
555
  setUninstallPlugin(null)
556
+ setUninstallItem(null)
557
+ },
558
+ onCancel: () => {
559
+ setUninstallPlugin(null)
560
+ setUninstallItem(null)
360
561
  },
361
- onCancel: () => setUninstallPlugin(null),
362
562
  onCopyCommand: copyUninstallCommand,
363
- onConfirm: () => queue.uninstallNow(uninstallPlugin),
563
+ onConfirm: () => {
564
+ if (uninstallItem) void queue.uninstallItem(uninstallItem)
565
+ else if (uninstallPlugin) void queue.uninstallNow(uninstallPlugin)
566
+ },
364
567
  onRestart: () => { void requestRestart() },
365
568
  }),
569
+ // 已安装详情弹窗:目录元数据 + 宿主运行时信息的完整展示,操作(更新/卸载/复制路径)由此发起
570
+ detailItem && h(InstalledDetailModal, {
571
+ item: detailItem,
572
+ t,
573
+ lang: langKey,
574
+ langPath,
575
+ onClose: () => setDetailItem(null),
576
+ onUpdate: (item) => {
577
+ if (!item.plugin) return
578
+ setDetailItem(null)
579
+ setInstallDone(false)
580
+ setConfirmIsUpdate(true)
581
+ queue.clearModalTask()
582
+ setConfirmPlugin(item.plugin)
583
+ },
584
+ onUninstall: (item) => {
585
+ setDetailItem(null)
586
+ setUninstallDone(false)
587
+ queue.clearModalTask()
588
+ setUninstallItem(item)
589
+ },
590
+ onCopyPath: (path) => {
591
+ doCopy(path)
592
+ setToast({ id: Date.now(), kind: 'copied' })
593
+ },
594
+ onReveal: (item) => { void revealFolder(item) },
595
+ }),
366
596
  toast && h(Toast, { toast, t }),
367
597
  // 操作失败:完整错误弹窗(重复安装、CLI 失败、请求错误等),可复制并去插件仓库反馈
368
598
  errorMsg && h(ErrorModal, {
@@ -379,24 +609,15 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
379
609
  },
380
610
  onClose: () => setErrorMsg(null),
381
611
  }),
382
- // 安装/卸载任务通知记录:本地持久化的成败清单,随时可回来查看/复制/清空;
383
- // 进行中的任务(实时进度)与待重启插件也一并展示,关掉任务弹窗后仍可盯着
384
- showNotifications && h(NotificationsModal, {
385
- records: notifications,
386
- tasks: queue.queue,
387
- pendingRestarts: queue.pendingRestarts,
612
+ // 待重启确认弹窗:已安装列表行内「重启」按钮点击后弹出,稍后重启 / 立即重启
613
+ showRestartConfirm && h(RestartConfirmModal, {
388
614
  t,
389
- env,
390
- onClose: () => setShowNotifications(false),
391
- onCopy: (text) => {
392
- doCopy(text)
393
- setToast({ id: Date.now(), kind: 'errCopied' })
394
- },
395
- onClear: () => setNotifications(clearNotifications()),
396
- onRemove: (id: number) => setNotifications(removeNotification(id)),
397
- cancelTask: queue.cancelTask,
398
615
  restarting,
399
- onRestart: () => { void requestRestart() },
616
+ onClose: () => setShowRestartConfirm(false),
617
+ onRestartNow: () => {
618
+ setShowRestartConfirm(false)
619
+ void requestRestart()
620
+ },
400
621
  }),
401
622
  )
402
623
  }