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.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +4772 -1659
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +451 -28
- package/lib/index.js +5 -1
- package/lib/services/{install-types.js → install/install-types.js} +8 -2
- package/lib/services/install/install.js +4 -0
- package/lib/services/install/npm-check.js +55 -0
- package/lib/services/{npm-resolve.js → install/npm-resolve.js} +11 -5
- package/lib/services/{preflight.js → install/preflight.js} +23 -7
- package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
- package/lib/services/loader.js +60 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
- package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
- package/lib/services/{profile.js → profile/profile.js} +59 -4
- package/lib/services/{progress.js → profile/progress.js} +4 -0
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +5 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
- package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -5
- package/lib/types/services/install/npm-check.d.ts +12 -0
- package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
- package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
- package/lib/types/services/loader.d.ts +14 -4
- package/lib/types/services/log.d.ts +49 -0
- package/lib/types/services/probe.d.ts +30 -0
- package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +9 -3
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
- package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +346 -121
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -5
- package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
- package/src/client/components/modals/LogsModal.tsx +301 -0
- package/src/client/components/modals/LogsPathModal.tsx +161 -0
- package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +51 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -6
- package/src/client/components/ui/Toggle.tsx +28 -0
- package/src/client/components/ui/icons.tsx +343 -0
- package/src/client/components/views/CustomInstallView.tsx +250 -0
- package/src/client/components/views/DiagnosticsView.tsx +172 -0
- package/src/client/components/views/InstalledView.tsx +328 -0
- package/src/client/components/views/LogsView.tsx +150 -0
- package/src/client/components/views/MarketView.tsx +66 -0
- package/src/client/components/views/SettingsView.tsx +273 -0
- package/src/client/data/catalog.ts +38 -0
- package/src/client/data/host.ts +107 -0
- package/src/client/data/hub.ts +41 -0
- package/src/client/hooks/useCatalog.ts +77 -117
- package/src/client/hooks/useIncrementalList.ts +46 -0
- package/src/client/hooks/useLanguage.ts +40 -0
- package/src/client/hooks/useSettings.ts +90 -0
- package/src/client/hooks/useTaskQueue.ts +168 -47
- package/src/client/index.tsx +4 -0
- package/src/client/locales.ts +365 -39
- package/src/client/logic/constants.ts +87 -0
- package/src/client/{lib → logic}/failures.ts +99 -74
- package/src/client/{lib → logic}/format.ts +4 -0
- package/src/client/logic/install-command.ts +56 -0
- package/src/client/logic/installed.ts +207 -0
- package/src/client/logic/normalize.ts +52 -0
- package/src/client/{lib → logic}/renderMarkdown.ts +4 -0
- package/src/client/logic/urls.ts +125 -0
- package/src/client/styles/CustomInstallView.module.css +191 -0
- package/src/client/styles/DiagnosticsView.module.css +231 -0
- package/src/client/styles/Dropdown.module.css +46 -41
- package/src/client/styles/Header.module.css +105 -133
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +11 -1
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +357 -71
- package/src/client/styles/SectionTabs.module.css +142 -0
- package/src/client/styles/SettingsView.module.css +232 -0
- package/src/client/styles/Toggle.module.css +60 -0
- package/src/client/styles/tokens.module.css +6 -1
- package/src/client/types.ts +28 -4
- package/src/server/http/routes.ts +426 -28
- package/src/server/index.ts +5 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
- package/src/server/services/{install.ts → install/install.ts} +9 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
- package/src/server/services/loader.ts +59 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
- package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
- package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -104
- package/src/client/components/icons.tsx +0 -125
- package/src/client/lib/catalog.ts +0 -272
- package/src/client/lib/env.ts +0 -16
- /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
- /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
|
@@ -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
|
* Background install/remove task queue for the Plugin Hub.
|
|
3
7
|
*
|
|
4
8
|
* Mirrors the server-side FIFO active queue by polling /active, settles
|
|
@@ -7,7 +11,12 @@
|
|
|
7
11
|
*/
|
|
8
12
|
import { useEffect, useRef, useState } from 'react'
|
|
9
13
|
import type { HubPlugin, Translate } from '../types.ts'
|
|
10
|
-
import { installCommandOf, installTargetOf } from '../
|
|
14
|
+
import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
|
|
15
|
+
import type { InstalledItem } from '../logic/installed.ts'
|
|
16
|
+
|
|
17
|
+
/** 安装/卸载请求超时(ms):服务端 preflight + npm 反查可能耗时(各自都有超时,合计约 20~30s),
|
|
18
|
+
* 给足余量;超时主动中止,避免服务端 preflight 网络挂起时前端永久卡「安装中 0%」。 */
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 60_000
|
|
11
20
|
|
|
12
21
|
/** 客户端任务队列项:镜像服务端 active 任务(running 在前、pending 在后)。 */
|
|
13
22
|
export interface QueueTask {
|
|
@@ -15,7 +24,7 @@ export interface QueueTask {
|
|
|
15
24
|
kind: 'install' | 'uninstall'
|
|
16
25
|
/** 展示目标:owner/repo(安装)或 npm 包名(卸载) */
|
|
17
26
|
target: string
|
|
18
|
-
/**
|
|
27
|
+
/** 插件中文简介(入队时从插件数据带过来,轮询合并时保留):展示在仓库名下方,标识排队中的插件 */
|
|
19
28
|
desc?: string
|
|
20
29
|
/** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
|
|
21
30
|
repo: string | null
|
|
@@ -41,7 +50,7 @@ export interface QueueTask {
|
|
|
41
50
|
|
|
42
51
|
/** 客户端待重启项:镜像服务端内存列表 + 本地补齐的展示信息(简介/版本)。 */
|
|
43
52
|
export interface PendingRestart {
|
|
44
|
-
/** owner/repo
|
|
53
|
+
/** owner/repo(展示用,已从安装目标归一化) */
|
|
45
54
|
target: string
|
|
46
55
|
/** 待重启语义:install 装完等挂载 / uninstall 卸完等清理 */
|
|
47
56
|
kind: 'install' | 'uninstall'
|
|
@@ -61,7 +70,14 @@ export interface TaskQueueOptions {
|
|
|
61
70
|
onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[]) => void
|
|
62
71
|
/** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
|
|
63
72
|
installPlugin: HubPlugin | null
|
|
73
|
+
/** 当前打开的自定义安装(命令行)确认弹窗目标:与应用商店同一弹窗机制,匹配进行中的 custom 安装任务。 */
|
|
74
|
+
installCustomTarget?: string | null
|
|
75
|
+
/** 当前打开的全局 npm 安装(`npm install -g <pkgs>`)确认弹窗身份(包列表空格拼接):
|
|
76
|
+
* 匹配服务端登记的全局安装任务(target = 包列表 join(' '))。 */
|
|
77
|
+
installGlobalTarget?: string | null
|
|
64
78
|
uninstallPlugin: HubPlugin | null
|
|
79
|
+
/** 已安装视图发起的卸载目标(npm 包名):目录插件走 uninstallPlugin,自定义安装走包名直卸。 */
|
|
80
|
+
uninstallName: string | null
|
|
65
81
|
installedName: (p: HubPlugin) => string | null
|
|
66
82
|
/** 待重启项展示信息补齐:按 owner/repo 从插件目录解析简介/版本;找不到返回 null。 */
|
|
67
83
|
resolvePending: (repo: string) => { desc?: string; version?: string } | null
|
|
@@ -70,7 +86,7 @@ export interface TaskQueueOptions {
|
|
|
70
86
|
export function useTaskQueue(opts: TaskQueueOptions) {
|
|
71
87
|
const {
|
|
72
88
|
t, refreshInstalled, onInstallDone, onUninstallDone, onError,
|
|
73
|
-
installPlugin, uninstallPlugin, installedName, resolvePending,
|
|
89
|
+
installPlugin, installCustomTarget, installGlobalTarget, uninstallPlugin, uninstallName, installedName, resolvePending,
|
|
74
90
|
} = opts
|
|
75
91
|
const [queue, setQueue] = useState<QueueTask[]>([])
|
|
76
92
|
const queueRef = useRef<QueueTask[]>([])
|
|
@@ -120,7 +136,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
120
136
|
setQueue(next)
|
|
121
137
|
}
|
|
122
138
|
|
|
123
|
-
/**
|
|
139
|
+
/** 解析服务端待重启列表(安装目标归一化为 owner/repo,与安装任务对齐)。 */
|
|
124
140
|
const parsePendingRestarts = (value: unknown): PendingRestart[] => {
|
|
125
141
|
if (!Array.isArray(value)) return []
|
|
126
142
|
const out: PendingRestart[] = []
|
|
@@ -130,7 +146,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
130
146
|
if (typeof target !== 'string' || target === '') continue
|
|
131
147
|
const at = (x as { at?: unknown }).at
|
|
132
148
|
out.push({
|
|
133
|
-
target: target
|
|
149
|
+
target: repoFromInstallTarget(target),
|
|
134
150
|
kind: (x as { kind?: unknown }).kind === 'uninstall' ? 'uninstall' : 'install',
|
|
135
151
|
at: typeof at === 'number' ? at : 0,
|
|
136
152
|
})
|
|
@@ -280,9 +296,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
280
296
|
next.push({
|
|
281
297
|
id,
|
|
282
298
|
kind: isRemove ? 'uninstall' : 'install',
|
|
283
|
-
target: typeof a.target === 'string' ? a.target
|
|
299
|
+
target: typeof a.target === 'string' ? repoFromInstallTarget(a.target) : (prevTask?.target ?? ''),
|
|
284
300
|
desc: prevTask?.desc,
|
|
285
|
-
repo: prevTask?.repo ?? (isRemove ? null : (typeof a.target === 'string' ? a.target
|
|
301
|
+
repo: prevTask?.repo ?? (isRemove ? null : (typeof a.target === 'string' ? repoFromInstallTarget(a.target) : null)),
|
|
286
302
|
// 安装时快照的目录信号:轮询合并时保留,任务成功后记录到本地
|
|
287
303
|
version: prevTask?.version,
|
|
288
304
|
updatedAt: prevTask?.updatedAt,
|
|
@@ -341,11 +357,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
341
357
|
.filter((a) => typeof a.id === 'number')
|
|
342
358
|
.map((a) => {
|
|
343
359
|
const isRemove = a.action === 'remove'
|
|
344
|
-
// 服务端任务的 target
|
|
360
|
+
// 服务端任务的 target:显式 HTTPS Git URL(安装)/ npm 包名(npm 安装/卸载)。
|
|
345
361
|
// 恢复展示统一用 displayTarget(owner/repo,服务端固定传):npm 安装时 target 是包名,
|
|
346
362
|
// 直接展示会让用户看到「安装方式」的内部细节,违背无感知原则;卸载保持包名(匹配已装表)。
|
|
347
|
-
const display = typeof (a as { displayTarget?: unknown }).displayTarget === 'string' ? (a as { displayTarget: string }).displayTarget
|
|
348
|
-
const fallback = typeof a.target === 'string' ? a.target
|
|
363
|
+
const display = typeof (a as { displayTarget?: unknown }).displayTarget === 'string' ? repoFromInstallTarget((a as { displayTarget: string }).displayTarget) : ''
|
|
364
|
+
const fallback = typeof a.target === 'string' ? repoFromInstallTarget(a.target) : ''
|
|
349
365
|
return {
|
|
350
366
|
id: a.id as number,
|
|
351
367
|
kind: (isRemove ? 'uninstall' : 'install') as 'install' | 'uninstall',
|
|
@@ -370,46 +386,67 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
370
386
|
}, [])
|
|
371
387
|
|
|
372
388
|
/** 弹窗动作:直接安装。请求宿主本地路由,任务进入服务端队列(FIFO),弹窗内实时显示进度。 */
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
389
|
+
/** 安装入队核心:目录插件(catalog,后端走目录白名单)与命令行安装(custom,
|
|
390
|
+
* 受安全开关控制)共用。队列条目一律用仓库名(repo)展示/防重,只有发给后端的
|
|
391
|
+
* 实际安装目标(target)随通道变化(npm 包名 / github: 源)。 */
|
|
392
|
+
const submitInstall = async (input: {
|
|
393
|
+
/** 发给后端的实际安装目标:npm 包名或 github: 源(服务端再决定最终通道) */
|
|
394
|
+
target: string
|
|
395
|
+
/** 队列展示与防重用的仓库名 */
|
|
396
|
+
repo: string
|
|
397
|
+
desc?: string
|
|
398
|
+
version?: string
|
|
399
|
+
updatedAt?: string
|
|
400
|
+
command: string
|
|
401
|
+
source: 'catalog' | 'custom'
|
|
402
|
+
/** 更新已安装目标(放行 add 覆盖重装) */
|
|
403
|
+
update?: boolean
|
|
404
|
+
/** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
|
|
405
|
+
globalNpm?: string[]
|
|
406
|
+
}) => {
|
|
380
407
|
// 防重复入队:同一目标已在排队/执行中则忽略
|
|
381
|
-
if (queueRef.current.some((q) => q.kind === 'install' && q.target === repo)) return
|
|
408
|
+
if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
|
|
382
409
|
// 请求在途防重:任务要等响应回来才进本地队列,这之前再次点击(双击)直接忽略
|
|
383
|
-
if (submittingRef.current.has(repo)) return
|
|
384
|
-
submittingRef.current.add(repo)
|
|
410
|
+
if (submittingRef.current.has(input.repo)) return
|
|
411
|
+
submittingRef.current.add(input.repo)
|
|
385
412
|
// 乐观入队:点击安装立即显示「正在安装中」,不等 POST 响应。
|
|
386
413
|
// 服务端 /install 会先跑预检(preflight,可能耗时数秒)再入队,若等响应才入队列,
|
|
387
|
-
//
|
|
414
|
+
// 用户点完安装立刻去看任务会看不到 —— 只有刷新后 restore 才拉回来。
|
|
388
415
|
// 用负临时 id 占位,响应回来换成真实 task id;失败/出错则移除该占位。
|
|
389
416
|
tempIdRef.current -= 1
|
|
390
417
|
const tempId = tempIdRef.current
|
|
391
418
|
modalTaskRef.current = tempId
|
|
392
419
|
applyQueue((prev) => [...prev, {
|
|
393
|
-
id: tempId, kind: 'install', target: repo, repo, desc:
|
|
394
|
-
version:
|
|
420
|
+
id: tempId, kind: 'install', target: input.repo, repo: input.repo, desc: input.desc,
|
|
421
|
+
version: input.version, updatedAt: input.updatedAt,
|
|
395
422
|
status: 'running', progress: 0, lines: [], optimistic: true,
|
|
396
|
-
command:
|
|
423
|
+
command: input.command,
|
|
397
424
|
// 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
|
|
398
425
|
attempts: [], needsRestart: true,
|
|
399
426
|
}])
|
|
400
427
|
setSubmitting(true)
|
|
428
|
+
const controller = new AbortController()
|
|
429
|
+
const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
401
430
|
try {
|
|
402
431
|
const res = await fetch('/dsh-plugin-hub/install', {
|
|
403
432
|
method: 'POST',
|
|
404
433
|
headers: { 'content-type': 'application/json' },
|
|
405
|
-
body: JSON.stringify({
|
|
434
|
+
body: JSON.stringify({
|
|
435
|
+
repo: input.target,
|
|
436
|
+
display: input.repo,
|
|
437
|
+
source: input.source,
|
|
438
|
+
mode: input.update ? 'update' : undefined,
|
|
439
|
+
...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
|
|
440
|
+
}),
|
|
441
|
+
signal: controller.signal,
|
|
406
442
|
})
|
|
407
443
|
const data = await res.json() as { ok?: boolean; task?: number; error?: string; attempts?: string[] }
|
|
408
444
|
if (!data.ok || typeof data.task !== 'number') {
|
|
409
|
-
// 请求层失败(重复安装 409 /
|
|
445
|
+
// 请求层失败(重复安装 409 / 参数错误 / 安全开关 403 等):移除乐观条目 + 完整错误弹窗
|
|
410
446
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
411
|
-
// 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue
|
|
412
|
-
|
|
447
|
+
// 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
|
|
448
|
+
// 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白
|
|
449
|
+
onError(data.error || `HTTP ${res.status}`, input.repo, 'install', input.command, data.attempts)
|
|
413
450
|
return
|
|
414
451
|
}
|
|
415
452
|
const taskId = data.task
|
|
@@ -419,53 +456,129 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
419
456
|
? { ...x, id: taskId, optimistic: undefined }
|
|
420
457
|
: x)))
|
|
421
458
|
// 提前缓存展示信息:安装成功后服务端登记待重启,轮询合并时不用等目录解析
|
|
422
|
-
pendingInfoRef.current.set(repo, { desc:
|
|
459
|
+
pendingInfoRef.current.set(input.repo, { desc: input.desc, version: input.version })
|
|
423
460
|
// 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
|
|
424
461
|
pollQueue()
|
|
425
|
-
} catch {
|
|
426
|
-
// 网络异常:移除乐观条目 +
|
|
462
|
+
} catch (err) {
|
|
463
|
+
// 网络异常:移除乐观条目 + 兜底错误弹窗(附上底层原因,避免只剩一句笼统的「安装失败」)
|
|
427
464
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
428
|
-
|
|
465
|
+
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
466
|
+
const reason = aborted
|
|
467
|
+
? t('requestTimeout')
|
|
468
|
+
: err instanceof Error && err.message ? err.message : String(err ?? '')
|
|
469
|
+
onError(reason ? `${t('installFail')} — ${reason}` : t('installFail'), input.repo, 'install', input.command)
|
|
429
470
|
} finally {
|
|
430
|
-
|
|
471
|
+
window.clearTimeout(timeout)
|
|
472
|
+
submittingRef.current.delete(input.repo)
|
|
431
473
|
setSubmitting(false)
|
|
432
474
|
}
|
|
433
475
|
}
|
|
434
476
|
|
|
435
|
-
/**
|
|
436
|
-
const
|
|
437
|
-
const
|
|
477
|
+
/** 目录插件安装:通道由目录数据(npmPackage)决定,走 catalog 白名单。 */
|
|
478
|
+
const installNow = async (p: HubPlugin, opts?: { update?: boolean }) => {
|
|
479
|
+
const repo = p.source?.repo ?? ''
|
|
480
|
+
if (!repo) return
|
|
481
|
+
// 安装通道决策(用户无感知):有 npm 包名 → 走 npm;否则 git 直装。
|
|
482
|
+
const { target } = installTargetOf(p)
|
|
483
|
+
if (!target) return
|
|
484
|
+
await submitInstall({
|
|
485
|
+
target,
|
|
486
|
+
repo,
|
|
487
|
+
desc: p.description,
|
|
488
|
+
version: p.version,
|
|
489
|
+
updatedAt: p.dates?.repoUpdatedAt,
|
|
490
|
+
command: installCommandOf(p, true),
|
|
491
|
+
source: 'catalog',
|
|
492
|
+
update: opts?.update,
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** 命令行安装:用户手输 npm 包名或 GitHub 地址,走 custom 源(受安全开关控制)。
|
|
497
|
+
* update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
|
|
498
|
+
* 队列展示/防重一律用归一化身份(owner/repo 或 npm 包名):与服务端 /active 合并、
|
|
499
|
+
* 弹窗目标匹配同口径,npm 包名透传不受影响。 */
|
|
500
|
+
const installCustom = async (raw: string, opts?: { update?: boolean }) => {
|
|
501
|
+
const target = raw.trim()
|
|
502
|
+
if (!target) return
|
|
503
|
+
const repo = repoFromInstallTarget(target)
|
|
504
|
+
await submitInstall({
|
|
505
|
+
target,
|
|
506
|
+
repo,
|
|
507
|
+
command: `pnpm add ${target}`,
|
|
508
|
+
source: 'custom',
|
|
509
|
+
update: opts?.update,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
514
|
+
* 队列展示/防重用包列表空格拼接的身份(与服务端 target 同口径)。 */
|
|
515
|
+
const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean }) => {
|
|
516
|
+
const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
|
|
517
|
+
if (list.length === 0) return
|
|
518
|
+
const identity = list.join(' ')
|
|
519
|
+
await submitInstall({
|
|
520
|
+
target: identity,
|
|
521
|
+
repo: identity,
|
|
522
|
+
command: `npm install -g ${identity}`,
|
|
523
|
+
source: 'custom',
|
|
524
|
+
globalNpm: list,
|
|
525
|
+
update: opts?.update,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** 卸载入队核心:按 npm 包名直卸(目录插件与自定义安装共用,弹窗进度匹配同源)。 */
|
|
530
|
+
const enqueueUninstall = async (name: string, repo: string | null, desc?: string) => {
|
|
438
531
|
if (!name) return
|
|
439
|
-
const repo = p.source?.repo ?? null
|
|
440
532
|
if (queueRef.current.some((q) => q.kind === 'uninstall' && q.target === name)) return
|
|
441
533
|
// 请求在途防重:与安装一致,响应回来前再次点击直接忽略
|
|
442
534
|
if (submittingRef.current.has(name)) return
|
|
443
535
|
submittingRef.current.add(name)
|
|
444
536
|
setSubmitting(true)
|
|
537
|
+
const controller = new AbortController()
|
|
538
|
+
const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
445
539
|
try {
|
|
446
540
|
const res = await fetch('/dsh-plugin-hub/uninstall', {
|
|
447
541
|
method: 'POST',
|
|
448
542
|
headers: { 'content-type': 'application/json' },
|
|
449
543
|
body: JSON.stringify({ name, repo: repo ?? undefined }),
|
|
544
|
+
signal: controller.signal,
|
|
450
545
|
})
|
|
451
546
|
const data = await res.json() as { ok?: boolean; task?: number; error?: string }
|
|
452
547
|
if (!data.ok || typeof data.task !== 'number') {
|
|
453
|
-
onError(data.error
|
|
548
|
+
onError(data.error || `HTTP ${res.status}`, repo, 'uninstall')
|
|
454
549
|
return
|
|
455
550
|
}
|
|
456
551
|
const taskId = data.task
|
|
457
552
|
modalTaskRef.current = taskId
|
|
458
|
-
applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc
|
|
553
|
+
applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc, repo, status: 'pending', progress: 0, lines: [], needsRestart: true }])
|
|
459
554
|
// 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
|
|
460
555
|
pollQueue()
|
|
461
|
-
} catch {
|
|
462
|
-
|
|
556
|
+
} catch (err) {
|
|
557
|
+
// 网络异常:附上底层原因,避免只剩一句笼统的「卸载失败」
|
|
558
|
+
const aborted = (err as { name?: string } | null)?.name === 'AbortError'
|
|
559
|
+
const reason = aborted
|
|
560
|
+
? t('requestTimeout')
|
|
561
|
+
: err instanceof Error && err.message ? err.message : String(err ?? '')
|
|
562
|
+
onError(reason ? `${t('uninstallFail')} — ${reason}` : t('uninstallFail'), repo, 'uninstall')
|
|
463
563
|
} finally {
|
|
564
|
+
window.clearTimeout(timeout)
|
|
464
565
|
submittingRef.current.delete(name)
|
|
465
566
|
setSubmitting(false)
|
|
466
567
|
}
|
|
467
568
|
}
|
|
468
569
|
|
|
570
|
+
/** 弹窗动作:直接卸载(目录插件入口)。与安装同一队列机制,弹窗内实时显示进度。 */
|
|
571
|
+
const uninstallNow = async (p: HubPlugin) => {
|
|
572
|
+
const name = installedName(p)
|
|
573
|
+
if (!name) return
|
|
574
|
+
await enqueueUninstall(name, p.source?.repo ?? null, p.description)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** 已安装视图动作:按已安装项卸载(覆盖目录外自定义安装,无目录数据也能卸)。 */
|
|
578
|
+
const uninstallItem = async (item: InstalledItem) => {
|
|
579
|
+
await enqueueUninstall(item.name, item.repo, item.plugin?.description)
|
|
580
|
+
}
|
|
581
|
+
|
|
469
582
|
/** 取消任务:排队中立即出队,执行中终止子进程;先标记「正在取消」短暂过渡后再移除。 */
|
|
470
583
|
const cancelTask = async (id: number) => {
|
|
471
584
|
if (modalTaskRef.current === id) modalTaskRef.current = null
|
|
@@ -489,15 +602,20 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
489
602
|
const clearModalTask = () => { modalTaskRef.current = null }
|
|
490
603
|
|
|
491
604
|
// 弹窗对应任务:优先当前弹窗发起任务,其次按目标匹配(重新打开弹窗 / 目标已在队列时也能展示实时进度)
|
|
605
|
+
// 目录插件按 source.repo 匹配;命令行安装(custom)按归一化后的仓库身份/包名匹配;
|
|
606
|
+
// 全局 npm 安装按包列表身份(空格拼接)匹配;
|
|
492
607
|
// 排除 cancelling:取消中的任务不再关联到弹窗,弹窗回到「确认」态可重新操作
|
|
493
|
-
const
|
|
494
|
-
? (
|
|
495
|
-
|
|
608
|
+
const installModalTarget = installPlugin?.source?.repo
|
|
609
|
+
?? (installCustomTarget ? repoFromInstallTarget(installCustomTarget) : null)
|
|
610
|
+
?? installGlobalTarget ?? null
|
|
611
|
+
const installModalTask = (installPlugin || installCustomTarget || installGlobalTarget)
|
|
612
|
+
? (queue.find((q) => q.id === modalTaskRef.current && q.kind === 'install' && q.status !== 'cancelling')
|
|
613
|
+
?? queue.find((q) => q.kind === 'install' && q.target === installModalTarget && q.status !== 'cancelling')
|
|
496
614
|
?? null)
|
|
497
615
|
: null
|
|
498
|
-
const uninstallModalTask = uninstallPlugin
|
|
616
|
+
const uninstallModalTask = (uninstallPlugin || uninstallName)
|
|
499
617
|
? (queue.find((q) => q.id === modalTaskRef.current && q.status !== 'cancelling')
|
|
500
|
-
?? queue.find((q) => q.kind === 'uninstall' && q.target === (installedName(uninstallPlugin) ?? '') && q.status !== 'cancelling')
|
|
618
|
+
?? queue.find((q) => q.kind === 'uninstall' && q.target === (uninstallName ?? installedName(uninstallPlugin!) ?? '') && q.status !== 'cancelling')
|
|
501
619
|
?? null)
|
|
502
620
|
: null
|
|
503
621
|
|
|
@@ -508,7 +626,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
508
626
|
uninstallModalTask,
|
|
509
627
|
submitting,
|
|
510
628
|
installNow,
|
|
629
|
+
installCustom,
|
|
630
|
+
installGlobalNpm,
|
|
511
631
|
uninstallNow,
|
|
632
|
+
uninstallItem,
|
|
512
633
|
cancelTask,
|
|
513
634
|
clearModalTask,
|
|
514
635
|
}
|
package/src/client/index.tsx
CHANGED
|
@@ -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
|
* dsh-plugin client entry: wires the Plugin Hub into the host settings via
|
|
3
7
|
* the module-loader bundle (see tsdown.config.ts). All UI logic lives in
|
|
4
8
|
* PluginHubSection; this file only performs the cordis apply wiring.
|