dsh-plugin 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +648 -190
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +15 -8
- package/lib/index.js +4 -0
- package/lib/services/install-types.js +8 -2
- package/lib/services/install.js +1 -1
- package/lib/services/installed-versions.js +5 -1
- package/lib/services/loader.js +16 -5
- package/lib/services/npm-resolve.js +4 -0
- package/lib/services/pending-restart.js +1 -1
- package/lib/services/preflight.js +9 -4
- package/lib/services/profile.js +30 -3
- package/lib/services/progress.js +4 -0
- package/lib/services/task-queue.js +21 -10
- package/lib/types/index.d.ts +4 -0
- package/lib/types/services/install-types.d.ts +12 -4
- package/lib/types/services/install.d.ts +5 -1
- package/lib/types/services/loader.d.ts +10 -4
- package/lib/types/services/pending-restart.d.ts +8 -2
- package/lib/types/services/profile.d.ts +4 -2
- package/lib/types/services/progress.d.ts +4 -0
- package/package.json +1 -1
- package/src/client/components/AboutModal.tsx +76 -0
- package/src/client/components/CatalogControls.tsx +4 -0
- package/src/client/components/CatalogHeader.tsx +34 -5
- package/src/client/components/CatalogList.tsx +4 -0
- package/src/client/components/CategoryTabs.tsx +4 -0
- package/src/client/components/Dropdown.tsx +4 -0
- package/src/client/components/HubUpdateModal.tsx +88 -0
- package/src/client/components/NotificationsModal.tsx +25 -8
- package/src/client/components/PluginCard.tsx +8 -1
- package/src/client/components/PluginHubSection.tsx +67 -7
- package/src/client/components/ProgressView.tsx +4 -0
- package/src/client/components/icons.tsx +4 -0
- package/src/client/components/modals.tsx +35 -10
- package/src/client/hooks/useCatalog.ts +89 -17
- package/src/client/hooks/useTaskQueue.ts +52 -35
- package/src/client/index.tsx +4 -0
- package/src/client/lib/catalog.ts +59 -6
- package/src/client/lib/env.ts +4 -0
- package/src/client/lib/failures.ts +5 -1
- package/src/client/lib/format.ts +4 -0
- package/src/client/lib/renderMarkdown.ts +120 -0
- package/src/client/locales.ts +48 -1
- package/src/client/styles/Dropdown.module.css +5 -1
- package/src/client/styles/Header.module.css +88 -4
- package/src/client/styles/List.module.css +5 -1
- package/src/client/styles/Modal.module.css +279 -1
- package/src/client/styles/tokens.module.css +5 -1
- package/src/client/types.ts +7 -1
- package/src/server/http/routes.ts +14 -8
- package/src/server/index.ts +4 -0
- package/src/server/services/install-types.ts +12 -4
- package/src/server/services/install.ts +5 -1
- package/src/server/services/installed-versions.ts +5 -1
- package/src/server/services/loader.ts +16 -5
- package/src/server/services/npm-resolve.ts +4 -0
- package/src/server/services/pending-restart.ts +8 -2
- package/src/server/services/preflight.ts +9 -4
- package/src/server/services/profile.ts +27 -3
- package/src/server/services/progress.ts +4 -0
- package/src/server/services/task-queue.ts +21 -10
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
2
6
|
* Catalog data + view state for the Plugin Hub section.
|
|
3
7
|
*
|
|
4
8
|
* Owns the online-data pipeline (live fetch from dsh-plugin.org), the local
|
|
@@ -7,11 +11,14 @@
|
|
|
7
11
|
*/
|
|
8
12
|
import { useEffect, useMemo, useState } from 'react'
|
|
9
13
|
import type { HubPlugin, LocaleId } from '../types.ts'
|
|
10
|
-
import { normalize } from '../lib/catalog.ts'
|
|
14
|
+
import { HUB_ABOUT_URL, HUB_REPO, HUB_UPDATE_URL, normalize, PLUGIN_VERSION, repoFromInstallTarget } from '../lib/catalog.ts'
|
|
15
|
+
import type { HubAboutInfo, HubUpdateInfo } from '../lib/catalog.ts'
|
|
11
16
|
import type { SortKey } from '../lib/catalog.ts'
|
|
12
17
|
|
|
13
18
|
const PLUGINS_URL = (lang: LocaleId) => `https://dsh-plugin.org/api/plugins.${lang}.json`
|
|
14
19
|
const STATS_URL = 'https://dsh-plugin.org/api/stats.json'
|
|
20
|
+
/** 插件市场自身仓库:DSH-Plugin Hub 不显示在目录里(自己不进自己的插件列表) */
|
|
21
|
+
const SELF_REPO = 'dshplugin/dsh-plugin-hub'
|
|
15
22
|
|
|
16
23
|
export function useCatalog(lang: LocaleId) {
|
|
17
24
|
/** 目录插件(仅保留人工验证通过的条目) */
|
|
@@ -30,6 +37,10 @@ export function useCatalog(lang: LocaleId) {
|
|
|
30
37
|
/** 安装时记录的目录信号:repo(小写) -> { version, updatedAt }(来自宿主本地路由);
|
|
31
38
|
* npmPackage 为 npm 优先通道反查命中的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
|
|
32
39
|
const [versions, setVersions] = useState<Record<string, { version: string; updatedAt: string; npmPackage?: string }>>({})
|
|
40
|
+
/** Hub 自我更新信息:来自 CF Worker 版本控制中心(hub.dsh-plugin.org),与目录数据解耦 */
|
|
41
|
+
const [hubUpdateInfo, setHubUpdateInfo] = useState<HubUpdateInfo | null>(null)
|
|
42
|
+
/** 头部「关注我们」弹窗内容(平台介绍 + 反馈群二维码):来自同款 Worker /about,Markdown 推送非写死 */
|
|
43
|
+
const [hubAboutInfo, setHubAboutInfo] = useState<HubAboutInfo | null>(null)
|
|
33
44
|
|
|
34
45
|
// 拉取目录 + 统计。在线 API 已只返回 verified;再过滤一次,保证只展示人工验证通过的插件。
|
|
35
46
|
useEffect(() => {
|
|
@@ -42,19 +53,29 @@ export function useCatalog(lang: LocaleId) {
|
|
|
42
53
|
if (!res.ok) throw new Error(String(res.status))
|
|
43
54
|
return res.json()
|
|
44
55
|
})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// 目录数据直拉:hub 自身版本由独立 Worker 版本控制中心管理,不再依赖主站版本号接口;
|
|
57
|
+
// 数据接口 CDN 缓存 1 小时,发布新数据后最长 1 小时全网生效。
|
|
58
|
+
const load = () =>
|
|
59
|
+
Promise.all([fetchData(PLUGINS_URL(lang)), fetchData(STATS_URL)])
|
|
60
|
+
load()
|
|
61
|
+
.then(([data, s]) => {
|
|
62
|
+
if (cancelled) return
|
|
63
|
+
const list = (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
|
|
64
|
+
// 插件市场不显示自己:DSH-Plugin Hub 从目录里排除自身条目,
|
|
65
|
+
// 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
|
|
66
|
+
// 主站数据已同步排除自身,此处为 CDN 缓存期的兜底。
|
|
67
|
+
const hadSelf = list.some((p) => p.source?.repo === SELF_REPO)
|
|
68
|
+
setPlugins(list.filter((p) => p.compatibility?.status === 'verified' && p.source?.repo !== SELF_REPO))
|
|
69
|
+
const stats = s as { total?: number; verified?: number }
|
|
70
|
+
if (stats && typeof stats.total === 'number' && typeof stats.verified === 'number') {
|
|
71
|
+
setStats(hadSelf
|
|
72
|
+
? { total: stats.total - 1, verified: stats.verified - 1 }
|
|
73
|
+
: { total: stats.total, verified: stats.verified })
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
if (!cancelled) setFailed(true)
|
|
78
|
+
})
|
|
58
79
|
return () => { cancelled = true }
|
|
59
80
|
// lang 必须进依赖:切换界面语言要重新拉对应语言的数据文件,
|
|
60
81
|
// 否则英文模式仍停留在 plugins.zh.json,描述/名称全是中文
|
|
@@ -76,13 +97,49 @@ export function useCatalog(lang: LocaleId) {
|
|
|
76
97
|
// 首次进入拉取已安装表(依赖宿主 webServer 服务)
|
|
77
98
|
useEffect(() => { refreshInstalled() }, [])
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
// Hub 自身版本检查走独立 Worker(版本控制中心):发新版后写一次 Worker KV,
|
|
101
|
+
// 所有已装用户立即看到「可更新」+ Markdown 变更记录,不再依赖主站目录数据管道。
|
|
102
|
+
// 失败静默降级为 null(徽标不出现,不影响目录本身加载)。
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
let cancelled = false
|
|
105
|
+
fetch(HUB_UPDATE_URL, { cache: 'no-store' })
|
|
106
|
+
.then((res): Promise<HubUpdateInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
|
|
107
|
+
.then((data) => {
|
|
108
|
+
if (cancelled || !data) { if (!cancelled) setHubUpdateInfo(null); return }
|
|
109
|
+
const version = typeof data.version === 'string' && data.version.length > 0 ? data.version : null
|
|
110
|
+
setHubUpdateInfo(version === null
|
|
111
|
+
? null
|
|
112
|
+
: { version, publishedAt: data.publishedAt ?? null, notes: data.notes ?? null })
|
|
113
|
+
})
|
|
114
|
+
.catch(() => { if (!cancelled) setHubUpdateInfo(null) })
|
|
115
|
+
return () => { cancelled = true }
|
|
116
|
+
}, [reloadKey])
|
|
117
|
+
|
|
118
|
+
// 「关注我们」内容走同一套 Worker:作者写好 Markdown(平台介绍 + 反馈群二维码)推送 /admin/about,
|
|
119
|
+
// 客户端下次进入拉 /about 即可看到最新,非写死。失败静默为 null(弹窗展示兜底文案,不阻塞)。
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
let cancelled = false
|
|
122
|
+
fetch(HUB_ABOUT_URL, { cache: 'no-store' })
|
|
123
|
+
.then((res): Promise<HubAboutInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
|
|
124
|
+
.then((data) => {
|
|
125
|
+
if (cancelled) return
|
|
126
|
+
if (!data || data.content === null || data.content === undefined) { setHubAboutInfo(null); return }
|
|
127
|
+
const content = typeof data.content === 'string' || typeof data.content === 'object' ? data.content : null
|
|
128
|
+
setHubAboutInfo(content === null
|
|
129
|
+
? null
|
|
130
|
+
: { content, updatedAt: data.updatedAt ?? null })
|
|
131
|
+
})
|
|
132
|
+
.catch(() => { if (!cancelled) setHubAboutInfo(null) })
|
|
133
|
+
return () => { cancelled = true }
|
|
134
|
+
}, [reloadKey])
|
|
135
|
+
|
|
136
|
+
/** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
|
|
80
137
|
const installedName = (p: HubPlugin): string | null => {
|
|
81
138
|
const repo = p.source?.repo
|
|
82
139
|
if (!repo) return null
|
|
83
|
-
const needle =
|
|
140
|
+
const needle = repo.toLowerCase()
|
|
84
141
|
for (const [name, spec] of Object.entries(installed)) {
|
|
85
|
-
if (spec.toLowerCase()
|
|
142
|
+
if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
|
|
86
143
|
}
|
|
87
144
|
// npm 通道安装:profile 依赖 key 直接是 npm 包名。包名来源两处——目录数据(npmPackage),
|
|
88
145
|
// 或服务端 npm 优先反查持久化的映射(versions[repo].npmPackage,覆盖组织 scope 与 GitHub
|
|
@@ -119,6 +176,18 @@ export function useCatalog(lang: LocaleId) {
|
|
|
119
176
|
return Boolean(current && rec.updatedAt && current > rec.updatedAt)
|
|
120
177
|
}
|
|
121
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Hub 自身是否有可用更新:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
|
|
181
|
+
* 以 Worker 版本控制中心返回的「最新版本」为准,与当前运行的版本号比对——
|
|
182
|
+
* 构建时注入的 PLUGIN_VERSION 是运行 bundle 的真实版本;测试/异常场景缺失时
|
|
183
|
+
* 回退到安装时记录的版本。版本号不等即新版(版本号只在发版时变更,绝无降级场景)。
|
|
184
|
+
*/
|
|
185
|
+
const hubHasUpdate = (() => {
|
|
186
|
+
if (!hubUpdateInfo) return false
|
|
187
|
+
const current = PLUGIN_VERSION || versions[HUB_REPO]?.version || null
|
|
188
|
+
return current !== null && hubUpdateInfo.version !== current
|
|
189
|
+
})()
|
|
190
|
+
|
|
122
191
|
/** 当前分类下的插件(「全部」时为整个目录)。 */
|
|
123
192
|
const categoryPlugins = useMemo(() => {
|
|
124
193
|
if (!plugins) return []
|
|
@@ -184,6 +253,9 @@ export function useCatalog(lang: LocaleId) {
|
|
|
184
253
|
installedName,
|
|
185
254
|
installedVersion,
|
|
186
255
|
hasUpdate,
|
|
256
|
+
hubHasUpdate,
|
|
257
|
+
hubUpdateInfo,
|
|
258
|
+
hubAboutInfo,
|
|
187
259
|
refreshInstalled,
|
|
188
260
|
category,
|
|
189
261
|
setCategory,
|
|
@@ -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,7 @@
|
|
|
7
11
|
*/
|
|
8
12
|
import { useEffect, useRef, useState } from 'react'
|
|
9
13
|
import type { HubPlugin, Translate } from '../types.ts'
|
|
10
|
-
import { installCommandOf, installTargetOf } from '../lib/catalog.ts'
|
|
14
|
+
import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../lib/catalog.ts'
|
|
11
15
|
|
|
12
16
|
/** 客户端任务队列项:镜像服务端 active 任务(running 在前、pending 在后)。 */
|
|
13
17
|
export interface QueueTask {
|
|
@@ -33,11 +37,15 @@ export interface QueueTask {
|
|
|
33
37
|
command?: string
|
|
34
38
|
/** 尝试过的安装方式(npm registry 反查 + 实际执行命令,按先后顺序):失败时 issue 预填一并贴给作者 */
|
|
35
39
|
attempts?: string[]
|
|
40
|
+
/** 完成结果是否需要重启才生效(服务端任务终态带出;默认 true 保持老行为:弹窗给重启选项)。
|
|
41
|
+
* 卸载时 loader 已即时移除 → false,结果视图只显示「完成」;true 时弹窗给「稍后重启/立即重启」,
|
|
42
|
+
* 通知中心待重启条目(服务端登记)同步常驻,直到用户点「立即重启」。 */
|
|
43
|
+
needsRestart: boolean
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/** 客户端待重启项:镜像服务端内存列表 + 本地补齐的展示信息(简介/版本)。 */
|
|
39
47
|
export interface PendingRestart {
|
|
40
|
-
/** owner/repo
|
|
48
|
+
/** owner/repo(展示用,已从安装目标归一化) */
|
|
41
49
|
target: string
|
|
42
50
|
/** 待重启语义:install 装完等挂载 / uninstall 卸完等清理 */
|
|
43
51
|
kind: 'install' | 'uninstall'
|
|
@@ -49,9 +57,10 @@ export interface PendingRestart {
|
|
|
49
57
|
export interface TaskQueueOptions {
|
|
50
58
|
t: Translate
|
|
51
59
|
refreshInstalled: () => void
|
|
52
|
-
/** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
/** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
|
|
61
|
+
* needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。 */
|
|
62
|
+
onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
|
|
63
|
+
onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
|
|
55
64
|
/** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)。 */
|
|
56
65
|
onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[]) => void
|
|
57
66
|
/** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
|
|
@@ -115,7 +124,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
115
124
|
setQueue(next)
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
/**
|
|
127
|
+
/** 解析服务端待重启列表(安装目标归一化为 owner/repo,与安装任务对齐)。 */
|
|
119
128
|
const parsePendingRestarts = (value: unknown): PendingRestart[] => {
|
|
120
129
|
if (!Array.isArray(value)) return []
|
|
121
130
|
const out: PendingRestart[] = []
|
|
@@ -125,7 +134,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
125
134
|
if (typeof target !== 'string' || target === '') continue
|
|
126
135
|
const at = (x as { at?: unknown }).at
|
|
127
136
|
out.push({
|
|
128
|
-
target: target
|
|
137
|
+
target: repoFromInstallTarget(target),
|
|
129
138
|
kind: (x as { kind?: unknown }).kind === 'uninstall' ? 'uninstall' : 'install',
|
|
130
139
|
at: typeof at === 'number' ? at : 0,
|
|
131
140
|
})
|
|
@@ -168,13 +177,13 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
168
177
|
if (q.kind === 'uninstall') {
|
|
169
178
|
// 卸载成功:清掉本地版本记录
|
|
170
179
|
void syncInstalledVersion(q.repo, undefined, undefined)
|
|
171
|
-
onUninstallDone(modalTaskRef.current === q.id, q.repo)
|
|
180
|
+
onUninstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart)
|
|
172
181
|
} else {
|
|
173
182
|
// 安装成功:记录安装时的目录信号(版本 + 仓库更新时间),供「有更新」比对;
|
|
174
183
|
// 同时缓存展示信息,待重启列表(服务端已登记)合并时直接补齐简介/版本
|
|
175
184
|
void syncInstalledVersion(q.repo, q.version, q.updatedAt)
|
|
176
185
|
pendingInfoRef.current.set(q.target, { desc: q.desc, version: q.version })
|
|
177
|
-
onInstallDone(modalTaskRef.current === q.id, q.repo)
|
|
186
|
+
onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart)
|
|
178
187
|
}
|
|
179
188
|
} else {
|
|
180
189
|
// 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
|
|
@@ -220,25 +229,31 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
220
229
|
try {
|
|
221
230
|
const res = await fetch(`/dsh-plugin-hub/status?task=${q.id}`, { cache: 'no-store' })
|
|
222
231
|
if (res.ok) {
|
|
223
|
-
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[] } }
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
const data = await res.json() as { task?: { status?: string; lines?: string[]; attempts?: string[]; needsRestart?: unknown } }
|
|
233
|
+
const task = data.task
|
|
234
|
+
if (task !== undefined) {
|
|
235
|
+
status = task.status ?? 'failed'
|
|
236
|
+
lines = task.lines ?? []
|
|
237
|
+
attempts = Array.isArray(task.attempts) ? task.attempts : undefined
|
|
238
|
+
// 终态带出的「是否需要重启」覆盖本地(服务端 done 时按 loader 即时移除情况设置)
|
|
239
|
+
const needsRestart = typeof task.needsRestart === 'boolean' ? task.needsRestart : q.needsRestart
|
|
240
|
+
if (status === 'done') {
|
|
241
|
+
const settled = { ...q, needsRestart }
|
|
242
|
+
// 卸载成功走进度过渡动画;安装成功直接收尾
|
|
243
|
+
if (q.kind === 'uninstall') settleDone(settled, lines)
|
|
244
|
+
else finishQueueTask(true, settled, lines)
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if (status === 'failed') {
|
|
248
|
+
// 服务端终态带出的尝试记录覆盖本地:以实际执行为准
|
|
249
|
+
finishQueueTask(false, { ...q, attempts }, lines)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
}
|
|
227
253
|
}
|
|
228
254
|
} catch { /* 服务端不可达:按失败处理 */ }
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (q.kind === 'uninstall') settleDone(q, lines)
|
|
232
|
-
else finishQueueTask(true, q, lines)
|
|
233
|
-
}
|
|
234
|
-
else if (status === 'failed') {
|
|
235
|
-
// 服务端终态带出的尝试记录覆盖本地:以实际执行为准
|
|
236
|
-
finishQueueTask(false, { ...q, attempts }, lines)
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
applyQueue((prev) => prev.filter((x) => x.id !== q.id)) // cancelled / 未知
|
|
240
|
-
maybeStopPoll()
|
|
241
|
-
}
|
|
255
|
+
applyQueue((prev) => prev.filter((x) => x.id !== q.id)) // cancelled / 未知
|
|
256
|
+
maybeStopPoll()
|
|
242
257
|
}
|
|
243
258
|
|
|
244
259
|
/** 轮询整个队列:合并服务端 /active(running 在前、pending 在后),
|
|
@@ -250,7 +265,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
250
265
|
const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
|
|
251
266
|
if (!res.ok) throw new Error(`active ${res.status}`)
|
|
252
267
|
const data = await res.json() as {
|
|
253
|
-
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown }[]
|
|
268
|
+
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
|
|
254
269
|
pendingRestarts?: unknown
|
|
255
270
|
}
|
|
256
271
|
const active = (data.tasks ?? []).filter((a) => typeof a.id === 'number')
|
|
@@ -269,9 +284,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
269
284
|
next.push({
|
|
270
285
|
id,
|
|
271
286
|
kind: isRemove ? 'uninstall' : 'install',
|
|
272
|
-
target: typeof a.target === 'string' ? a.target
|
|
287
|
+
target: typeof a.target === 'string' ? repoFromInstallTarget(a.target) : (prevTask?.target ?? ''),
|
|
273
288
|
desc: prevTask?.desc,
|
|
274
|
-
repo: prevTask?.repo ?? (isRemove ? null : (typeof a.target === 'string' ? a.target
|
|
289
|
+
repo: prevTask?.repo ?? (isRemove ? null : (typeof a.target === 'string' ? repoFromInstallTarget(a.target) : null)),
|
|
275
290
|
// 安装时快照的目录信号:轮询合并时保留,任务成功后记录到本地
|
|
276
291
|
version: prevTask?.version,
|
|
277
292
|
updatedAt: prevTask?.updatedAt,
|
|
@@ -279,6 +294,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
279
294
|
progress: typeof a.progress === 'number' ? a.progress : prevTask?.progress ?? 0,
|
|
280
295
|
lines: Array.isArray(a.lines) ? (a.lines as string[]) : (prevTask?.lines ?? []),
|
|
281
296
|
attempts: Array.isArray(a.attempts) ? (a.attempts as string[]) : (prevTask?.attempts ?? []),
|
|
297
|
+
needsRestart: typeof a.needsRestart === 'boolean' ? a.needsRestart : (prevTask?.needsRestart ?? true),
|
|
282
298
|
})
|
|
283
299
|
}
|
|
284
300
|
// 保留乐观条目:服务端尚未登记(preflight 中),无真实 id,不能因 active 缺它就被合并吞掉
|
|
@@ -319,7 +335,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
319
335
|
const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
|
|
320
336
|
if (!res.ok) return
|
|
321
337
|
const data = await res.json() as {
|
|
322
|
-
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown }[]
|
|
338
|
+
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
|
|
323
339
|
pendingRestarts?: unknown
|
|
324
340
|
}
|
|
325
341
|
if (cancelled) return
|
|
@@ -329,11 +345,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
329
345
|
.filter((a) => typeof a.id === 'number')
|
|
330
346
|
.map((a) => {
|
|
331
347
|
const isRemove = a.action === 'remove'
|
|
332
|
-
// 服务端任务的 target
|
|
348
|
+
// 服务端任务的 target:显式 HTTPS Git URL(安装)/ npm 包名(npm 安装/卸载)。
|
|
333
349
|
// 恢复展示统一用 displayTarget(owner/repo,服务端固定传):npm 安装时 target 是包名,
|
|
334
350
|
// 直接展示会让用户看到「安装方式」的内部细节,违背无感知原则;卸载保持包名(匹配已装表)。
|
|
335
|
-
const display = typeof (a as { displayTarget?: unknown }).displayTarget === 'string' ? (a as { displayTarget: string }).displayTarget
|
|
336
|
-
const fallback = typeof a.target === 'string' ? a.target
|
|
351
|
+
const display = typeof (a as { displayTarget?: unknown }).displayTarget === 'string' ? repoFromInstallTarget((a as { displayTarget: string }).displayTarget) : ''
|
|
352
|
+
const fallback = typeof a.target === 'string' ? repoFromInstallTarget(a.target) : ''
|
|
337
353
|
return {
|
|
338
354
|
id: a.id as number,
|
|
339
355
|
kind: (isRemove ? 'uninstall' : 'install') as 'install' | 'uninstall',
|
|
@@ -343,6 +359,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
343
359
|
progress: typeof a.progress === 'number' ? a.progress : 0,
|
|
344
360
|
lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
|
|
345
361
|
attempts: Array.isArray((a as { attempts?: unknown }).attempts) ? ((a as { attempts: string[] }).attempts) : [],
|
|
362
|
+
needsRestart: typeof (a as { needsRestart?: unknown }).needsRestart === 'boolean' ? (a as { needsRestart: boolean }).needsRestart : true,
|
|
346
363
|
}
|
|
347
364
|
})
|
|
348
365
|
if (items.length > 0 || pendingRef.current.length > 0) {
|
|
@@ -382,7 +399,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
382
399
|
status: 'running', progress: 0, lines: [], optimistic: true,
|
|
383
400
|
command: installCommandOf(p, true),
|
|
384
401
|
// 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
|
|
385
|
-
attempts: [],
|
|
402
|
+
attempts: [], needsRestart: true,
|
|
386
403
|
}])
|
|
387
404
|
setSubmitting(true)
|
|
388
405
|
try {
|
|
@@ -442,7 +459,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
442
459
|
}
|
|
443
460
|
const taskId = data.task
|
|
444
461
|
modalTaskRef.current = taskId
|
|
445
|
-
applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc: p.description, repo, status: 'pending', progress: 0, lines: [] }])
|
|
462
|
+
applyQueue((prev) => [...prev, { id: taskId, kind: 'uninstall', target: name, desc: p.description, repo, status: 'pending', progress: 0, lines: [], needsRestart: true }])
|
|
446
463
|
// 摘要条常驻顶部(含实时进度),明细面板保持折叠,想看时再点开
|
|
447
464
|
pollQueue()
|
|
448
465
|
} catch {
|
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.
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
2
6
|
* Catalog constants and normalizers: site URLs, category metadata, sort keys
|
|
3
7
|
* and the mapping from the online API's short-key payloads to HubPlugin.
|
|
4
8
|
*/
|
|
@@ -11,6 +15,36 @@ export const SITE_URL = 'https://dsh-plugin.org/'
|
|
|
11
15
|
/** 插件中心源码仓库:头部右上角 GitHub 图标的跳转地址 */
|
|
12
16
|
export const GITHUB_URL = 'https://github.com/dshplugin/dsh-plugin-hub'
|
|
13
17
|
|
|
18
|
+
/** Hub 自身在目录数据里的仓库标识:头部版本号旁的「可更新」徽标按它定位自己。 */
|
|
19
|
+
export const HUB_REPO = 'dshplugin/dsh-plugin-hub'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Hub 自我更新版本控制中心(Cloudflare Worker:dsh-update)。
|
|
23
|
+
* 发新版后向 Worker 管理接口写入最新版本 + Markdown 变更记录(见
|
|
24
|
+
* dsh-update-worker 的 POST /admin/hub),所有已装用户的「可更新」徽标即时出现,
|
|
25
|
+
* 不再依赖主站目录数据管道(每日刷新 + 重建站点)。
|
|
26
|
+
* 响应:{ version: string | null, publishedAt: string | null, notes: string | {zh,en} | null }
|
|
27
|
+
*/
|
|
28
|
+
export const HUB_UPDATE_URL = 'https://hub.dsh-plugin.org/hub'
|
|
29
|
+
|
|
30
|
+
/** Worker 版本控制中心返回的 Hub 自我更新信息(notes 为 Markdown 变更记录)。 */
|
|
31
|
+
export interface HubUpdateInfo {
|
|
32
|
+
version: string
|
|
33
|
+
publishedAt?: string | null
|
|
34
|
+
/** Markdown 变更记录;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
|
|
35
|
+
notes?: string | Record<string, string> | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Worker「关注我们」内容接口(同款 Worker,key = hub:about,Markdown 推送非写死)。 */
|
|
39
|
+
export const HUB_ABOUT_URL = 'https://hub.dsh-plugin.org/about'
|
|
40
|
+
|
|
41
|
+
/** Worker 返回的「关注我们」内容(content 为 Markdown,可字符串或 {zh,en} 双语言对象)。 */
|
|
42
|
+
export interface HubAboutInfo {
|
|
43
|
+
/** Markdown 平台介绍 + 反馈群二维码;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
|
|
44
|
+
content?: string | Record<string, string> | null
|
|
45
|
+
updatedAt?: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
14
48
|
/** 构建时由 tsdown 从 package.json 注入的插件版本号(见 tsdown.config.ts define)。 */
|
|
15
49
|
declare const __PLUGIN_VERSION__: string
|
|
16
50
|
|
|
@@ -29,7 +63,7 @@ export function langPathOf(lang: LocaleId): string {
|
|
|
29
63
|
|
|
30
64
|
/**
|
|
31
65
|
* 官网详情页两级路径:/plugins/{ownerSlug}/{slug};
|
|
32
|
-
*
|
|
66
|
+
* 缺 ownerSlug 时从 repo 推导(卡片详情按钮与弹窗来源行共用)。
|
|
33
67
|
*/
|
|
34
68
|
export function pluginDetailUrl(plugin: HubPlugin, langPath: string): string {
|
|
35
69
|
const repo = plugin.source?.repo ?? ''
|
|
@@ -77,7 +111,7 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
77
111
|
// 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的安装命令(含 --profile)与执行结果,链接由常量动态拼接
|
|
78
112
|
`## [DSH-Plugin 插件中心](${SITE_URL}) · 安装 Plugin 失败错误信息`,
|
|
79
113
|
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件中心的安装程序自动生成,随本次安装失败一并提交。`,
|
|
80
|
-
`- 实际执行的安装命令:\`${command ?? `dsh plugin${env?.profile ? ` --profile ${env.profile}` : ''} add github
|
|
114
|
+
`- 实际执行的安装命令:\`${command ?? `dsh plugin${env?.profile ? ` --profile ${env.profile}` : ''} add git+https://github.com/${repo}.git`}\``,
|
|
81
115
|
`- 执行结果:安装失败,未能安装该插件。`,
|
|
82
116
|
// 尝试过的安装方式(npm 反查 + 实际执行命令,按先后顺序):作者据此反推正确的
|
|
83
117
|
// npm 包名 —— 组织 scope 与 GitHub 用户名不一致时仅凭仓库名猜不到,作者看到我们
|
|
@@ -90,7 +124,7 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
90
124
|
? [
|
|
91
125
|
'',
|
|
92
126
|
'## 安装方式说明',
|
|
93
|
-
'DSH 插件支持两种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)与 `dsh plugin add github
|
|
127
|
+
'DSH 插件支持两种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)与 `dsh plugin add git+https://github.com/owner/repo.git`(Git 直装,仓库需提交构建产物或在 package.json 提供 `prepare` 脚本)。当前插件的分发物缺少 package.json 声明的入口文件,请按所用通道补齐后重新发布。',
|
|
94
128
|
]
|
|
95
129
|
: []),
|
|
96
130
|
'',
|
|
@@ -164,7 +198,7 @@ export function normalize(raw: Record<string, unknown>): HubPlugin {
|
|
|
164
198
|
topics: Array.isArray(raw.t) ? (raw.t as string[]) : undefined,
|
|
165
199
|
features: Array.isArray(raw.f) ? (raw.f as string[]) : undefined,
|
|
166
200
|
description: typeof raw.d === 'string' ? raw.d : undefined,
|
|
167
|
-
//
|
|
201
|
+
// source 双形态:字符串 repo,或 { repo, npmPackage } 对象
|
|
168
202
|
source: typeof raw.r === 'string'
|
|
169
203
|
? { repo: raw.r }
|
|
170
204
|
: raw.r !== null && typeof raw.r === 'object'
|
|
@@ -173,6 +207,8 @@ export function normalize(raw: Record<string, unknown>): HubPlugin {
|
|
|
173
207
|
npmPackage: typeof (raw.r as { npmPackage?: unknown }).npmPackage === 'string' ? (raw.r as { npmPackage: string }).npmPackage : undefined,
|
|
174
208
|
}
|
|
175
209
|
: undefined,
|
|
210
|
+
// 能否网页一键安装:API 仅在 false 时下发 wi(缺省视为 true),据此禁用一键安装、只提示命令行
|
|
211
|
+
install: raw.wi === false ? { webInstallable: false } : undefined,
|
|
176
212
|
compatibility: typeof raw.v === 'string' ? { status: raw.v } : undefined,
|
|
177
213
|
dates: {
|
|
178
214
|
repoUpdatedAt: typeof raw.u === 'string' ? raw.u : undefined,
|
|
@@ -199,12 +235,29 @@ export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'g
|
|
|
199
235
|
return { target: repo, via: 'github' }
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
/** 展示用安装命令(复制/弹窗):npm 通道显示包名,git
|
|
238
|
+
/** 展示用安装命令(复制/弹窗):npm 通道显示包名,git 通道显示显式 HTTPS URL。 */
|
|
203
239
|
export function installCommandOf(p: HubPlugin, withProfile = false): string {
|
|
204
240
|
const { target, via } = installTargetOf(p)
|
|
205
241
|
return via === 'npm'
|
|
206
242
|
? `dsh plugin${withProfile ? ' --profile web' : ''} add ${target}`
|
|
207
|
-
: `dsh plugin${withProfile ? ' --profile web' : ''} add github
|
|
243
|
+
: `dsh plugin${withProfile ? ' --profile web' : ''} add git+https://github.com/${target}.git`
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Normalize a task/install target to its owner/repo display identity. */
|
|
247
|
+
export function repoFromInstallTarget(value: string): string {
|
|
248
|
+
const input = value.trim()
|
|
249
|
+
if (/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(input)) return input
|
|
250
|
+
const patterns = [
|
|
251
|
+
/^github:([^/]+)\/([^/]+)$/i,
|
|
252
|
+
/^(?:git\+)?https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
253
|
+
/^(?:git\+)?ssh:\/\/(?:git@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
254
|
+
/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
255
|
+
]
|
|
256
|
+
for (const pattern of patterns) {
|
|
257
|
+
const match = pattern.exec(input)
|
|
258
|
+
if (match !== null) return `${match[1]}/${match[2]}`
|
|
259
|
+
}
|
|
260
|
+
return value
|
|
208
261
|
}
|
|
209
262
|
|
|
210
263
|
export const CATEGORY_LABELS: Record<string, { zh: string; en: string }> = {
|
package/src/client/lib/env.ts
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
|
* One-shot host environment snapshot for bug reports. Fetched lazily and
|
|
3
7
|
* cached at module level: issue URLs embed this info, and the environment
|
|
4
8
|
* does not change during a session. A failed fetch degrades to null so the
|
|
@@ -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
|
* Persistent install/remove notifications.
|
|
3
7
|
*
|
|
4
8
|
* Every settled task — success or failure — is appended to localStorage the
|
|
@@ -114,7 +118,7 @@ export function loadNotifications(): NotificationRecord[] {
|
|
|
114
118
|
return list
|
|
115
119
|
.filter((r): r is NotificationRecord =>
|
|
116
120
|
!!r && typeof r === 'object' && typeof (r as { message?: unknown }).message === 'string')
|
|
117
|
-
//
|
|
121
|
+
// ok 字段缺省时按失败渲染
|
|
118
122
|
.map((r) => ({ ...r, ok: r.ok === true }))
|
|
119
123
|
} catch {
|
|
120
124
|
return []
|
package/src/client/lib/format.ts
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
|
* Presentation formatters shared across the catalog list and the cards.
|
|
3
7
|
*/
|
|
4
8
|
import type { Translate } from '../types.ts'
|