dsh-plugin 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.en.md +5 -5
  2. package/README.md +5 -5
  3. package/client/client.js +4772 -1659
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +451 -28
  6. package/lib/index.js +5 -1
  7. package/lib/services/{install-types.js → install/install-types.js} +8 -2
  8. package/lib/services/install/install.js +4 -0
  9. package/lib/services/install/npm-check.js +55 -0
  10. package/lib/services/{npm-resolve.js → install/npm-resolve.js} +11 -5
  11. package/lib/services/{preflight.js → install/preflight.js} +23 -7
  12. package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
  13. package/lib/services/loader.js +60 -1
  14. package/lib/services/log.js +138 -0
  15. package/lib/services/probe.js +157 -0
  16. package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
  17. package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
  18. package/lib/services/{profile.js → profile/profile.js} +59 -4
  19. package/lib/services/{progress.js → profile/progress.js} +4 -0
  20. package/lib/services/settings.js +60 -0
  21. package/lib/types/http/routes.d.ts +1 -1
  22. package/lib/types/index.d.ts +5 -1
  23. package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
  24. package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -5
  25. package/lib/types/services/install/npm-check.d.ts +12 -0
  26. package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
  27. package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
  28. package/lib/types/services/loader.d.ts +14 -4
  29. package/lib/types/services/log.d.ts +49 -0
  30. package/lib/types/services/probe.d.ts +30 -0
  31. package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +9 -3
  32. package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
  33. package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
  34. package/lib/types/services/settings.d.ts +24 -0
  35. package/package.json +1 -1
  36. package/src/client/components/PluginHubSection.tsx +346 -121
  37. package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
  38. package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
  39. package/src/client/components/layout/CatalogControls.tsx +80 -0
  40. package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
  41. package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
  42. package/src/client/components/layout/SectionTabs.tsx +60 -0
  43. package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
  44. package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -5
  45. package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
  46. package/src/client/components/modals/LogsModal.tsx +301 -0
  47. package/src/client/components/modals/LogsPathModal.tsx +161 -0
  48. package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +51 -20
  49. package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
  50. package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
  51. package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -6
  52. package/src/client/components/ui/Toggle.tsx +28 -0
  53. package/src/client/components/ui/icons.tsx +343 -0
  54. package/src/client/components/views/CustomInstallView.tsx +250 -0
  55. package/src/client/components/views/DiagnosticsView.tsx +172 -0
  56. package/src/client/components/views/InstalledView.tsx +328 -0
  57. package/src/client/components/views/LogsView.tsx +150 -0
  58. package/src/client/components/views/MarketView.tsx +66 -0
  59. package/src/client/components/views/SettingsView.tsx +273 -0
  60. package/src/client/data/catalog.ts +38 -0
  61. package/src/client/data/host.ts +107 -0
  62. package/src/client/data/hub.ts +41 -0
  63. package/src/client/hooks/useCatalog.ts +77 -117
  64. package/src/client/hooks/useIncrementalList.ts +46 -0
  65. package/src/client/hooks/useLanguage.ts +40 -0
  66. package/src/client/hooks/useSettings.ts +90 -0
  67. package/src/client/hooks/useTaskQueue.ts +168 -47
  68. package/src/client/index.tsx +4 -0
  69. package/src/client/locales.ts +365 -39
  70. package/src/client/logic/constants.ts +87 -0
  71. package/src/client/{lib → logic}/failures.ts +99 -74
  72. package/src/client/{lib → logic}/format.ts +4 -0
  73. package/src/client/logic/install-command.ts +56 -0
  74. package/src/client/logic/installed.ts +207 -0
  75. package/src/client/logic/normalize.ts +52 -0
  76. package/src/client/{lib → logic}/renderMarkdown.ts +4 -0
  77. package/src/client/logic/urls.ts +125 -0
  78. package/src/client/styles/CustomInstallView.module.css +191 -0
  79. package/src/client/styles/DiagnosticsView.module.css +231 -0
  80. package/src/client/styles/Dropdown.module.css +46 -41
  81. package/src/client/styles/Header.module.css +105 -133
  82. package/src/client/styles/InstalledView.module.css +447 -0
  83. package/src/client/styles/List.module.css +11 -1
  84. package/src/client/styles/LogsView.module.css +503 -0
  85. package/src/client/styles/Modal.module.css +357 -71
  86. package/src/client/styles/SectionTabs.module.css +142 -0
  87. package/src/client/styles/SettingsView.module.css +232 -0
  88. package/src/client/styles/Toggle.module.css +60 -0
  89. package/src/client/styles/tokens.module.css +6 -1
  90. package/src/client/types.ts +28 -4
  91. package/src/server/http/routes.ts +426 -28
  92. package/src/server/index.ts +5 -1
  93. package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
  94. package/src/server/services/{install.ts → install/install.ts} +9 -5
  95. package/src/server/services/install/npm-check.ts +53 -0
  96. package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
  97. package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
  98. package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
  99. package/src/server/services/loader.ts +59 -1
  100. package/src/server/services/log.ts +167 -0
  101. package/src/server/services/probe.ts +157 -0
  102. package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
  103. package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
  104. package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
  105. package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
  106. package/src/server/services/settings.ts +79 -0
  107. package/lib/services/install.js +0 -4
  108. package/src/client/components/CatalogControls.tsx +0 -104
  109. package/src/client/components/icons.tsx +0 -125
  110. package/src/client/lib/catalog.ts +0 -272
  111. package/src/client/lib/env.ts +0 -16
  112. /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
  113. /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
@@ -0,0 +1,66 @@
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
+ * Market view: the browse experience for the catalog — category tabs,
7
+ * search/sort/install-state controls and the plugin list. Owns nothing;
8
+ * it wires catalog state (query/sort/filter/category) to the small
9
+ * presentational components so PluginHubSection stays free of view internals.
10
+ */
11
+ import { createElement as h, Fragment } from 'react'
12
+ import type { HubPlugin, LocaleId } from '../../types.ts'
13
+ import type { useCatalog } from '../../hooks/useCatalog.ts'
14
+ import { CategoryTabs } from '../layout/CategoryTabs.tsx'
15
+ import { CatalogControls } from '../layout/CatalogControls.tsx'
16
+ import { CatalogList } from '../catalog/CatalogList.tsx'
17
+
18
+ type CatalogState = ReturnType<typeof useCatalog>
19
+
20
+ export function MarketView({ catalog, t, langPath, langKey, copied, resultText, onInstall, onUninstall }: {
21
+ catalog: CatalogState
22
+ t: (key: string, params?: Record<string, string | number>) => string
23
+ langPath: string
24
+ langKey: LocaleId
25
+ copied: string | null
26
+ resultText: string | null
27
+ onInstall: (p: HubPlugin, opts?: { update?: boolean }) => void
28
+ onUninstall: (p: HubPlugin) => void
29
+ }) {
30
+ return h(Fragment, null,
31
+ // 分类行保持原位原样:全部 + 各分类 chips
32
+ h(CategoryTabs, {
33
+ category: catalog.category,
34
+ setCategory: catalog.setCategory,
35
+ allLabel: t('all'),
36
+ totalCount: catalog.total,
37
+ langKey,
38
+ }),
39
+ h(CatalogControls, {
40
+ query: catalog.query,
41
+ setQuery: catalog.setQuery,
42
+ sort: catalog.sort,
43
+ sortDir: catalog.sortDir,
44
+ toggleSort: catalog.toggleSort,
45
+ t,
46
+ resultText,
47
+ }),
48
+ h(CatalogList, {
49
+ plugins: catalog.plugins,
50
+ failed: catalog.failed,
51
+ visible: catalog.visible,
52
+ total: catalog.total,
53
+ t,
54
+ langPath,
55
+ reload: catalog.reload,
56
+ category: catalog.category,
57
+ copied,
58
+ installedName: catalog.installedName,
59
+ installedVersion: catalog.installedVersion,
60
+ hasUpdate: catalog.hasUpdate,
61
+ langKey,
62
+ onInstall,
63
+ onUninstall,
64
+ }),
65
+ )
66
+ }
@@ -0,0 +1,273 @@
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
+ * Settings view: desktop-settings navigation — a fixed left sidebar lists
7
+ * the sections (icon + label, click to switch), the right pane shows the
8
+ * active section's rows. New settings only add rows, new areas only add a
9
+ * sidebar entry.
10
+ *
11
+ * Sections mirror what the local server actually enforces:
12
+ * Updates & Sources — startup update check / npm mirror / HTTP proxy
13
+ * (all wired into the install command path)
14
+ * Security & Trust — restricted custom installs (server 403 gate)
15
+ * Diagnostics — live connectivity probe + environment snapshot
16
+ * Logs — system log viewer + storage location
17
+ */
18
+ import { createElement as h, useState } from 'react'
19
+ import type { ChangeEvent, MouseEvent, ReactNode } from 'react'
20
+ import styles from '../../styles/SettingsView.module.css'
21
+ import modalStyles from '../../styles/Modal.module.css'
22
+ import { Dropdown } from '../ui/Dropdown.tsx'
23
+ import { Toggle } from '../ui/Toggle.tsx'
24
+ import { DiagnosticsView } from './DiagnosticsView.tsx'
25
+ import { LogsView } from './LogsView.tsx'
26
+ import {
27
+ CloseIcon, DiagnosticsIcon, LogsIcon, ResetIcon, SecurityIcon, UpdatesIcon,
28
+ } from '../ui/icons.tsx'
29
+ import type { EnvInfo, Translate } from '../../types.ts'
30
+ import type { HubSettings } from '../../hooks/useSettings.ts'
31
+
32
+ type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
33
+
34
+ /** 左侧导航:顺序即展示顺序;labelKey 是分组标题,icon 是行内小图标。 */
35
+ const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnType<typeof h> }> = [
36
+ { key: 'updates', labelKey: 'settingsUpdate', icon: UpdatesIcon },
37
+ { key: 'security', labelKey: 'settingsSecurity', icon: SecurityIcon },
38
+ { key: 'diagnostics', labelKey: 'settingsDiagnostics', icon: DiagnosticsIcon },
39
+ { key: 'logs', labelKey: 'settingsLogs', icon: LogsIcon },
40
+ { key: 'reset', labelKey: 'settingsReset', icon: ResetIcon },
41
+ ]
42
+
43
+ /** npm 镜像源预设:空串 = 官方源;值相等即视为选中对应预设,其余进入「自定义」输入。 */
44
+ const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
45
+ { value: '', labelKey: 'mirrorOfficial' },
46
+ { value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
47
+ { value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
48
+ ]
49
+
50
+ /** 设置行:默认标签左控件右;stack=true 时控件独占一行(文本框整行显示),hairline 分隔。 */
51
+ function SettingRow({ title, desc, children, stack = false }: {
52
+ title: string
53
+ desc?: string
54
+ children: ReactNode
55
+ stack?: boolean
56
+ }) {
57
+ return h('div', { className: stack ? styles.settingRowStack : styles.settingRow },
58
+ h('div', { className: styles.settingLabel },
59
+ h('div', { className: styles.settingTitle }, title),
60
+ desc ? h('div', { className: styles.settingDesc }, desc) : null,
61
+ ),
62
+ h('div', { className: stack ? styles.settingControlStack : styles.settingControl }, children),
63
+ )
64
+ }
65
+
66
+ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
67
+ t: Translate
68
+ settings: HubSettings
69
+ update: (patch: Partial<HubSettings>) => void
70
+ reset: () => void
71
+ env: EnvInfo | null
72
+ onCopy: (text: string) => void
73
+ }) {
74
+ /** 当前激活的分组:左侧导航点击切换,右侧只渲染这一组 */
75
+ const [section, setSection] = useState<SettingsSection>('updates')
76
+ /** 恢复默认确认弹窗:点按钮先弹窗询问,确认后才真正 reset(不做两段式按钮) */
77
+ const [resetConfirm, setResetConfirm] = useState(false)
78
+
79
+ const mirrorCustom = !MIRROR_PRESETS.some((p) => p.value === settings.npmRegistry)
80
+ const onMirrorChange = (value: string) => {
81
+ if (value === 'custom') return // 进入自定义输入:保持当前值,下方出现输入框
82
+ update({ npmRegistry: value })
83
+ }
84
+ const onMirrorInput = (e: ChangeEvent<HTMLInputElement>) => update({ npmRegistry: e.target.value })
85
+ const onProxyChange = (e: ChangeEvent<HTMLInputElement>) => update({ proxy: e.target.value })
86
+
87
+ /** 每个分组的页面副标题(导航标题之上再补一句说明,保持桌面设置质感) */
88
+ const pageDesc: Record<SettingsSection, string> = {
89
+ updates: t('settingsUpdateDesc'),
90
+ security: t('settingsSecurityDesc'),
91
+ diagnostics: t('settingsDiagnosticsDesc'),
92
+ logs: t('settingsLogsDesc'),
93
+ reset: t('settingsResetDesc'),
94
+ }
95
+ const activeNav = NAV.find((n) => n.key === section) ?? NAV[0]
96
+
97
+ // —— 各分组的设置卡片(一页一个 card,内部一行一设置) ——
98
+
99
+ const updatesCard = h('div', { className: styles.card },
100
+ h(SettingRow, {
101
+ title: t('settingsCheckOnStart'),
102
+ desc: t('settingsCheckOnStartDesc'),
103
+ children: h(Toggle, {
104
+ checked: settings.checkUpdatesOnStart,
105
+ onChange: (v) => update({ checkUpdatesOnStart: v }),
106
+ title: t('settingsCheckOnStart'),
107
+ }),
108
+ }),
109
+ h(SettingRow, {
110
+ title: t('settingsMirror'),
111
+ desc: t('settingsMirrorDesc'),
112
+ children: h(Dropdown, {
113
+ value: mirrorCustom ? 'custom' : settings.npmRegistry,
114
+ options: [
115
+ ...MIRROR_PRESETS.map((p) => ({ value: p.value, label: t(p.labelKey) })),
116
+ { value: 'custom', label: t('mirrorCustom') },
117
+ ],
118
+ onChange: onMirrorChange,
119
+ title: t('settingsMirror'),
120
+ className: styles.controlDropdown,
121
+ }),
122
+ }),
123
+ // 自定义镜像源:下拉选中「自定义」或已存非预设值时,追加一行输入框(文本框独占一行)
124
+ mirrorCustom && h(SettingRow, {
125
+ title: t('mirrorCustom'),
126
+ stack: true,
127
+ children: h('input', {
128
+ className: styles.textInput,
129
+ type: 'url',
130
+ value: settings.npmRegistry,
131
+ placeholder: 'https://registry.npmjs.org',
132
+ spellCheck: false,
133
+ onChange: onMirrorInput,
134
+ }),
135
+ }),
136
+ // 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址
137
+ h(SettingRow, {
138
+ title: t('settingsProxy'),
139
+ desc: t('settingsProxyDesc'),
140
+ stack: true,
141
+ children: h('input', {
142
+ className: styles.textInput,
143
+ type: 'text',
144
+ value: settings.proxy,
145
+ placeholder: 'http://127.0.0.1:7890',
146
+ spellCheck: false,
147
+ onChange: onProxyChange,
148
+ }),
149
+ }),
150
+ )
151
+
152
+ const securityCard = h('div', { className: styles.card },
153
+ // 安全与信任:命令行安装的三个通道开关(NPM 包 / GitHub 源码 / DSH 命令)——
154
+ // 服务端按通道门禁 403 拦截(npm 目标吃 enableNpmInstall、git 目标吃 enableGitInstall),
155
+ // DSH 命令框为纯客户端通道:关掉后命令输入卡片禁用并引导去设置打开;
156
+ // 目录内收录的插件安装不受影响(默认全部开启)
157
+ h(SettingRow, {
158
+ title: t('settingsEnableNpm'),
159
+ desc: t('settingsEnableNpmDesc'),
160
+ children: h(Toggle, {
161
+ checked: settings.enableNpmInstall,
162
+ onChange: (v) => update({ enableNpmInstall: v }),
163
+ title: t('settingsEnableNpm'),
164
+ }),
165
+ }),
166
+ h(SettingRow, {
167
+ title: t('settingsEnableGit'),
168
+ desc: t('settingsEnableGitDesc'),
169
+ children: h(Toggle, {
170
+ checked: settings.enableGitInstall,
171
+ onChange: (v) => update({ enableGitInstall: v }),
172
+ title: t('settingsEnableGit'),
173
+ }),
174
+ }),
175
+ h(SettingRow, {
176
+ title: t('settingsEnableDsh'),
177
+ desc: t('settingsEnableDshDesc'),
178
+ children: h(Toggle, {
179
+ checked: settings.enableDshInstall,
180
+ onChange: (v) => update({ enableDshInstall: v }),
181
+ title: t('settingsEnableDsh'),
182
+ }),
183
+ }),
184
+ )
185
+
186
+ const diagnosticsCard = h('div', { className: styles.card },
187
+ // 系统版本(最顶部)+ 通道状态列表:进入页面自动探测一轮,整行可点击重测单项,实时反馈
188
+ h(DiagnosticsView, { t, env, onCopy }),
189
+ )
190
+
191
+ const logsCard = h('div', { className: styles.card },
192
+ // 系统日志列表:操作记录(安装/卸载/设置/诊断),可刷新/复制/导出,点击弹窗查看;
193
+ // 日志存放位置可设置(留空用默认),改动会写入设置并立即生效
194
+ h(LogsView, {
195
+ t,
196
+ logPath: settings.logPath,
197
+ onLogPathSaved: (v: string) => update({ logPath: v }),
198
+ }),
199
+ )
200
+
201
+ const resetCard = h('div', { className: styles.card },
202
+ h(SettingRow, {
203
+ title: t('settingsReset'),
204
+ desc: t('settingsResetDetail'),
205
+ children: h('button', {
206
+ type: 'button',
207
+ className: styles.resetBtn,
208
+ onClick: () => setResetConfirm(true),
209
+ }, t('settingsResetRun')),
210
+ }),
211
+ )
212
+
213
+ return h('div', { className: styles.root },
214
+ // 左侧导航:固定宽度菜单栏,图标 + 名称,点击切换右侧内容
215
+ h('nav', { className: styles.sidebar, 'aria-label': t('viewSettings') },
216
+ NAV.map((item) => h('button', {
217
+ key: item.key,
218
+ type: 'button',
219
+ className: section === item.key ? styles.navItemActive : styles.navItem,
220
+ onClick: () => setSection(item.key),
221
+ 'aria-current': section === item.key ? 'page' : undefined,
222
+ title: t(item.labelKey),
223
+ },
224
+ h('span', { className: styles.navIcon }, h(item.icon)),
225
+ h('span', { className: styles.navLabel }, t(item.labelKey)),
226
+ )),
227
+ ),
228
+ // 右侧内容:只渲染当前分组,独立滚动
229
+ h('div', { className: styles.content },
230
+ h('div', { className: styles.pageHeader },
231
+ h('div', { className: styles.pageTitle }, t(activeNav.labelKey)),
232
+ h('div', { className: styles.pageDesc }, pageDesc[section]),
233
+ ),
234
+ section === 'updates' ? updatesCard
235
+ : section === 'security' ? securityCard
236
+ : section === 'diagnostics' ? diagnosticsCard
237
+ : section === 'logs' ? logsCard
238
+ : resetCard,
239
+ ),
240
+ // 恢复默认确认弹窗:点「恢复默认」先弹窗确认,确认后才真正 reset(替代两段式按钮)
241
+ resetConfirm && h('div', {
242
+ className: modalStyles.overlay,
243
+ onClick: (e: MouseEvent<HTMLDivElement>) => { if (e.target === e.currentTarget) setResetConfirm(false) },
244
+ },
245
+ h('div', { className: modalStyles.modal, role: 'dialog', 'aria-modal': 'true' },
246
+ h('div', { className: modalStyles.modalHead },
247
+ h('div', { className: modalStyles.modalTitle }, t('settingsReset')),
248
+ h('button', {
249
+ className: modalStyles.modalClose,
250
+ type: 'button',
251
+ 'aria-label': t('errorClose'),
252
+ onClick: () => setResetConfirm(false),
253
+ }, h(CloseIcon)),
254
+ ),
255
+ h('div', { className: modalStyles.modalBody },
256
+ h('div', { className: modalStyles.failPrepareHint }, t('settingsResetConfirmDetail')),
257
+ h('div', { className: modalStyles.modalActions },
258
+ h('button', {
259
+ className: modalStyles.restartLater,
260
+ type: 'button',
261
+ onClick: () => setResetConfirm(false),
262
+ }, t('cancel')),
263
+ h('button', {
264
+ className: modalStyles.restartNowWarning,
265
+ type: 'button',
266
+ onClick: () => { setResetConfirm(false); reset() },
267
+ }, t('settingsResetConfirm')),
268
+ ),
269
+ ),
270
+ ),
271
+ ),
272
+ )
273
+ }
@@ -0,0 +1,38 @@
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
+ * Catalog repository: fetches the plugin directory and the collection stats
7
+ * from the online API and normalizes them. Components never see URLs or
8
+ * fetch — the repository is the only place that knows where data lives.
9
+ */
10
+ import type { HubPlugin, LocaleId } from '../types.ts'
11
+ import { normalize } from '../logic/normalize.ts'
12
+
13
+ // 目录/统计数据经服务端 /catalog 代理路由获取(curl 子进程注入设置里的代理),
14
+ // 与 npm / git 安装通道同一代理口径 —— 浏览器不再直连 api.dsh-plugin.org。
15
+ const PROXY_BASE = '/dsh-plugin-hub/catalog'
16
+ const PLUGINS_URL = (lang: LocaleId) => `${PROXY_BASE}?lang=${lang}`
17
+ const STATS_URL = `${PROXY_BASE}?stats=1`
18
+
19
+ async function fetchJson(url: string): Promise<unknown> {
20
+ const res = await fetch(url, { cache: 'no-store' })
21
+ if (!res.ok) throw new Error(String(res.status))
22
+ return res.json()
23
+ }
24
+
25
+ /** 目录插件列表(在线 API 已只返回 verified;过滤与排序由上层负责)。 */
26
+ export async function fetchCatalog(lang: LocaleId): Promise<HubPlugin[]> {
27
+ const data = await fetchJson(PLUGINS_URL(lang))
28
+ return (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
29
+ }
30
+
31
+ /** 收录/精选统计(/api/stats.json);字段不完整返回 null。 */
32
+ export async function fetchStats(): Promise<{ total: number; verified: number } | null> {
33
+ const s = await fetchJson(STATS_URL) as { total?: number; verified?: number }
34
+ if (s && typeof s.total === 'number' && typeof s.verified === 'number') {
35
+ return { total: s.total, verified: s.verified }
36
+ }
37
+ return null
38
+ }
@@ -0,0 +1,107 @@
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
+ * Host repository: the local routes served by the DSH harness (installed
7
+ * plugin table + environment snapshot). A host without the plugin's server
8
+ * routes degrades to null / empty tables instead of throwing.
9
+ */
10
+ import type { EnvInfo } from '../types.ts'
11
+ import type { InstalledVersionSignal } from '../logic/installed.ts'
12
+
13
+ export interface HostInstalled {
14
+ installed: Record<string, string>
15
+ versions: Record<string, InstalledVersionSignal>
16
+ /** 每个依赖在系统上的安装目录(profile/node_modules/<包名>),宿主未提供为 null */
17
+ paths: Record<string, string> | null
18
+ /** 已加载进运行中 loader 的包名(官方 ctx.loader.entries() 对账):装完未重启的新插件不在其中 */
19
+ loaded: string[] | null
20
+ /** 真正的 dsh 插件包名(包内声明 dsh 配置 / 在 profile bundles 清单):非 dsh 插件(如官方示例仓库)装上也不会被宿主加载,不提示待重启 */
21
+ dshCapable: string[] | null
22
+ }
23
+
24
+ /** 当前 profile 已安装插件表(npm 包名 -> manifest spec)+ 安装时记录的目录信号 + 安装路径 + 运行状态。 */
25
+ export async function fetchInstalled(): Promise<HostInstalled | null> {
26
+ try {
27
+ const res = await fetch('/dsh-plugin-hub/installed', { cache: 'no-store' })
28
+ if (!res.ok) return null
29
+ const data = await res.json() as {
30
+ installed?: Record<string, string>
31
+ versions?: Record<string, InstalledVersionSignal>
32
+ paths?: Record<string, string>
33
+ loaded?: string[]
34
+ dshCapable?: string[]
35
+ }
36
+ return {
37
+ installed: data.installed ?? {},
38
+ versions: data.versions ?? {},
39
+ paths: data.paths ?? null,
40
+ loaded: data.loaded ?? null,
41
+ dshCapable: data.dshCapable ?? null,
42
+ }
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ let envPromise: Promise<EnvInfo | null> | null = null
49
+
50
+ /** 在系统文件管理器里定位并打开已安装插件的目录(服务端 spawn open,跨平台);失败返回 false。 */
51
+ export async function revealInstallFolder(name: string): Promise<boolean> {
52
+ try {
53
+ const res = await fetch('/dsh-plugin-hub/open-path', {
54
+ method: 'POST',
55
+ headers: { 'content-type': 'application/json' },
56
+ body: JSON.stringify({ name }),
57
+ cache: 'no-store',
58
+ })
59
+ return res.ok
60
+ } catch {
61
+ return false
62
+ }
63
+ }
64
+
65
+ /** 在系统文件管理器里定位日志文件(服务端 spawn open,跨平台);失败返回 false。 */
66
+ export async function openLogFile(): Promise<boolean> {
67
+ try {
68
+ const res = await fetch('/dsh-plugin-hub/open-log', {
69
+ method: 'POST',
70
+ cache: 'no-store',
71
+ })
72
+ return res.ok
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 系统目录选择器:弹原生文件夹对话框让用户挑日志存放目录(服务端 spawn
80
+ * osascript / FolderBrowserDialog / zenity);选中返回绝对路径,取消或
81
+ * 平台不支持(无 osascript / zenity)时返回 null。
82
+ */
83
+ export async function chooseLogDir(): Promise<string | null> {
84
+ try {
85
+ const res = await fetch('/dsh-plugin-hub/choose-log-dir', {
86
+ method: 'POST',
87
+ cache: 'no-store',
88
+ })
89
+ if (!res.ok) return null
90
+ const data = await res.json() as { path?: string }
91
+ return typeof data.path === 'string' && data.path !== '' ? data.path : null
92
+ } catch {
93
+ return null
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 宿主机器环境快照(/dsh-plugin-hub/env):提交 bug 时拼进 issue 正文。
99
+ * 懒加载 + 模块级缓存:环境在会话期间不会变化;拉取失败降级为 null
100
+ * (issue 链接照常生成,只是少环境段)。
101
+ */
102
+ export function getEnv(): Promise<EnvInfo | null> {
103
+ envPromise ??= fetch('/dsh-plugin-hub/env', { cache: 'no-store' })
104
+ .then((res) => (res.ok ? (res.json() as Promise<EnvInfo>) : null))
105
+ .catch(() => null)
106
+ return envPromise
107
+ }
@@ -0,0 +1,41 @@
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 meta repository: the Cloudflare Worker endpoints behind the self-update
7
+ * check and the "About us" content. Failures degrade to null — the caller
8
+ * decides how to fall back (no badge, placeholder copy), never throws.
9
+ */
10
+ import type { HubAboutInfo, HubUpdateInfo } from '../types.ts'
11
+ import { HUB_ABOUT_URL, HUB_UPDATE_URL } from '../logic/constants.ts'
12
+
13
+ /** Worker 版本控制中心:最新版本 + Markdown 变更记录;不可用返回 null。 */
14
+ export async function fetchHubUpdate(): Promise<HubUpdateInfo | null> {
15
+ try {
16
+ const res = await fetch(HUB_UPDATE_URL, { cache: 'no-store' })
17
+ if (!res.ok) return null
18
+ const data = await res.json() as HubUpdateInfo | null
19
+ if (!data) return null
20
+ const version = typeof data.version === 'string' && data.version.length > 0 ? data.version : null
21
+ if (version === null) return null
22
+ return { version, publishedAt: data.publishedAt ?? null, notes: data.notes ?? null }
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ /** Worker「关注我们」内容(平台介绍 + 反馈群二维码,Markdown);未推送返回 null。 */
29
+ export async function fetchHubAbout(): Promise<HubAboutInfo | null> {
30
+ try {
31
+ const res = await fetch(HUB_ABOUT_URL, { cache: 'no-store' })
32
+ if (!res.ok) return null
33
+ const data = await res.json() as HubAboutInfo | null
34
+ if (!data || data.content === null || data.content === undefined) return null
35
+ const content = typeof data.content === 'string' || typeof data.content === 'object' ? data.content : null
36
+ if (content === null) return null
37
+ return { content, updatedAt: data.updatedAt ?? null }
38
+ } catch {
39
+ return null
40
+ }
41
+ }