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,21 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Failure classification, issue-body summarization and persistent
|
|
7
|
+
* install/remove notifications.
|
|
8
|
+
*
|
|
9
|
+
* Every settled task is recorded two ways: the server-side system log
|
|
10
|
+
* (hub.log) and a client-side localStorage record that feeds the
|
|
11
|
+
* notification center — so a result is never lost even when the dialog
|
|
12
|
+
* was dismissed or nobody was watching the progress strip.
|
|
9
13
|
*/
|
|
10
14
|
export interface NotificationRecord {
|
|
11
15
|
id: number
|
|
12
|
-
kind: 'install' | 'uninstall'
|
|
16
|
+
kind: 'install' | 'uninstall' | 'update'
|
|
13
17
|
/** true = 成功(轻量记录);false = 失败(message 携带完整错误日志) */
|
|
14
18
|
ok: boolean
|
|
15
19
|
/** owner/repo(可能为空:请求层失败但拿不到仓库时) */
|
|
16
20
|
repo: string
|
|
17
21
|
/** 失败时的完整错误日志;成功记录为空串 */
|
|
18
22
|
message: string
|
|
23
|
+
/** 更新提醒:目录里的新版本号(展示用;其余通知省略) */
|
|
24
|
+
version?: string
|
|
19
25
|
/** 实际执行的安装/卸载命令(issue 预填时如实展示);历史记录可能缺失 */
|
|
20
26
|
command?: string
|
|
21
27
|
/** 尝试过的安装方式(npm registry 反查 + 实际执行命令,按先后顺序):失败提 Issue 时贴给作者,便于反推正确的 npm 包名 */
|
|
@@ -27,12 +33,84 @@ export interface NotificationRecord {
|
|
|
27
33
|
const KEY = 'gro.ngilp-hsd.failure-records'
|
|
28
34
|
const MAX = 50
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
/** 运行时 localStorage 访问:类型上不依赖 DOM lib(Node 测试环境也能编译),浏览器里取 window。 */
|
|
37
|
+
const storage = (): Storage | undefined =>
|
|
38
|
+
(globalThis as { localStorage?: Storage }).localStorage
|
|
39
|
+
|
|
40
|
+
/** 读取本地通知记录(损坏/不可用时返回空列表,不抛错)。列表最新在前,全部保留,只由「清空」按钮手动移除。 */
|
|
41
|
+
export function loadNotifications(): NotificationRecord[] {
|
|
42
|
+
try {
|
|
43
|
+
const raw = storage()?.getItem(KEY)
|
|
44
|
+
if (!raw) return []
|
|
45
|
+
const list = JSON.parse(raw) as unknown
|
|
46
|
+
if (!Array.isArray(list)) return []
|
|
47
|
+
return list
|
|
48
|
+
.filter((r): r is NotificationRecord =>
|
|
49
|
+
!!r && typeof r === 'object' && typeof (r as { message?: unknown }).message === 'string')
|
|
50
|
+
// ok 字段缺省时按失败渲染
|
|
51
|
+
.map((r) => ({ ...r, ok: r.ok === true }))
|
|
52
|
+
} catch {
|
|
53
|
+
return []
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function save(list: NotificationRecord[]): void {
|
|
58
|
+
try {
|
|
59
|
+
storage()?.setItem(KEY, JSON.stringify(list))
|
|
60
|
+
} catch {
|
|
61
|
+
/* storage full / unavailable:仅保留内存态,界面仍可查看 */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 追加一条通知记录并持久化,返回更新后的完整列表(最新在前,超上限裁剪)。
|
|
66
|
+
* 每次成功/失败都各自留痕:同一插件的安装/卸载记录都完整保留、只由「清空」按钮手动移除,
|
|
67
|
+
* 不会因后续操作被自动覆盖清除。 */
|
|
68
|
+
export function addNotification(record: Omit<NotificationRecord, 'id' | 'at'>): NotificationRecord[] {
|
|
69
|
+
const prev = loadNotifications()
|
|
70
|
+
let id = Date.now()
|
|
71
|
+
while (prev.some((r) => r.id === id)) id += 1
|
|
72
|
+
const next = [{ ...record, id, at: id }, ...prev].slice(0, MAX)
|
|
73
|
+
save(next)
|
|
74
|
+
return next
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 记录一次失败:携带完整错误日志,供通知中心查看/复制/提 Issue。 */
|
|
78
|
+
export function addFailure(record: Omit<NotificationRecord, 'id' | 'at' | 'ok'>): NotificationRecord[] {
|
|
79
|
+
return addNotification({ ...record, ok: false })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 记录一次成功:轻量记录,不带日志。 */
|
|
83
|
+
export function addSuccess(record: Omit<NotificationRecord, 'id' | 'at' | 'ok' | 'message'>): NotificationRecord[] {
|
|
84
|
+
return addNotification({ ...record, ok: true, message: '' })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 记录一条「发现新版本」更新提醒:成功类轻量记录,携带目录新版本号供通知中心展示。 */
|
|
88
|
+
export function addUpdateNotice(record: Omit<NotificationRecord, 'id' | 'at' | 'ok' | 'message'>): NotificationRecord[] {
|
|
89
|
+
return addNotification({ ...record, ok: true, message: '' })
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 清空全部通知记录,返回空列表。 */
|
|
93
|
+
export function clearNotifications(): NotificationRecord[] {
|
|
94
|
+
save([])
|
|
95
|
+
return []
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 按 id 删除单条通知记录,返回更新后的列表(仅移除该条,不影响其余记录)。 */
|
|
99
|
+
export function removeNotification(id: number): NotificationRecord[] {
|
|
100
|
+
const next = loadNotifications().filter((r) => r.id !== id)
|
|
101
|
+
save(next)
|
|
102
|
+
return next
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type FailureKind = 'npmTooOld' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
|
|
31
106
|
|
|
32
107
|
/**
|
|
33
|
-
*
|
|
108
|
+
* 失败归类,四态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
|
|
34
109
|
* 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
|
|
35
|
-
* 一律引导提 Issue
|
|
110
|
+
* 一律引导提 Issue;唯一例外是本机 npm 自身版本过低导致的内部崩溃(见 npmTooOld):
|
|
111
|
+
* - npmTooOld:失败输出含 npm arborist 的 `edgesOut` 崩溃特征(build-ideal-tree.js 解 peer 依赖时
|
|
112
|
+
* 内部抛错,npm 11.6.0 前必现的已知缺陷,npm/cli#8261、#9787),或服务端已核实本机版本低于
|
|
113
|
+
* 阈值并打了 `[npm-too-low]` 标记 —— 是本机 npm 版本过低/自身缺陷,不是插件问题 → 引导升级 npm
|
|
36
114
|
* - pnpmIgnoredBuild:插件依赖里的原生模块构建脚本被 pnpm 默认拦截(如 node-pty,
|
|
37
115
|
* `ERR_PNPM_IGNORED_BUILDS`)。只影响带原生模块的插件,其他插件不受影响 —— 差异在
|
|
38
116
|
* 插件的依赖选择,属插件依赖/打包问题 → 引导去仓库提 Issue(建议改用预编译版本)
|
|
@@ -41,6 +119,10 @@ export type FailureKind = 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
|
|
|
41
119
|
* - repo:其余失败(含 git prepare 被 pnpm 白名单拦截等),默认按插件仓库问题引导提 Issue
|
|
42
120
|
*/
|
|
43
121
|
export function classifyFailure(message: string): FailureKind {
|
|
122
|
+
// npm 内部崩溃(edgesOut)或服务端 [npm-too-low] 标记:是本机 npm 版本过低/自身缺陷,
|
|
123
|
+
// 不是插件问题 —— 必须最先判,否则该报错会被外层 ERR_PNPM_PREPARE_PACKAGE 吞成
|
|
124
|
+
// 「插件打包分发问题」,误导用户去提 Issue
|
|
125
|
+
if (/\[npm-too-low\]|edgesOut/i.test(message)) return 'npmTooOld'
|
|
44
126
|
// 装后校验拦截(服务端 verifyInstalledEntry 标记):入口文件缺失 = git 分发缺构建产物,
|
|
45
127
|
// 与 pluginPrepare 同类(插件打包/分发问题),引导去仓库提 Issue
|
|
46
128
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
@@ -53,6 +135,13 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
53
135
|
return 'repo'
|
|
54
136
|
}
|
|
55
137
|
|
|
138
|
+
/** 从服务端 `[npm-too-low]` 标记行提取本机 npm 版本(如 `[npm-too-low] npm@11.3.0` → "11.3.0");
|
|
139
|
+
* 历史记录无标记时返回 null,前端据此决定提示文案是否带具体版本。 */
|
|
140
|
+
export function npmTooLowVersion(message: string): string | null {
|
|
141
|
+
const m = message.match(/\[npm-too-low\]\s*npm@(\d+\.\d+\.\d+)/i)
|
|
142
|
+
return m ? m[1] : null
|
|
143
|
+
}
|
|
144
|
+
|
|
56
145
|
/** 核心行特征:错误代码 / 生命周期脚本失败 / prepare 失败 / 描述性报错(子模块缺失、找不到等)/ 退出与宿主提示信息。 */
|
|
57
146
|
const CORE_LINE_RE = /ERR_[A-Z_]+|ELIFECYCLE|Command failed|prepare-guard|Failed to prepare|exit code|\bprepare\b|pnpm failed in profile|git-hosted plugins build|submodule|not found|cannot find|no such|unable to|fatal|missing|error/i
|
|
58
147
|
/** 提交 issue 时正文里错误摘要的上限字符数。GitHub 请求行上限 8192 字节,
|
|
@@ -99,67 +188,3 @@ export function coreErrorCode(message: string): string | null {
|
|
|
99
188
|
const m = message.match(/\[?ERR_[A-Z_]+\]?/)
|
|
100
189
|
return m ? m[0].replace(/^\[|\]$/g, '') : null
|
|
101
190
|
}
|
|
102
|
-
|
|
103
|
-
/** 运行时 localStorage 访问:类型上不依赖 DOM lib(Node 测试环境也能编译),浏览器里取 window。 */
|
|
104
|
-
const storage = (): Storage | undefined =>
|
|
105
|
-
(globalThis as { localStorage?: Storage }).localStorage
|
|
106
|
-
|
|
107
|
-
/** 读取本地通知记录(损坏/不可用时返回空列表,不抛错)。列表最新在前,全部保留,只由「清空」按钮手动移除。 */
|
|
108
|
-
export function loadNotifications(): NotificationRecord[] {
|
|
109
|
-
try {
|
|
110
|
-
const raw = storage()?.getItem(KEY)
|
|
111
|
-
if (!raw) return []
|
|
112
|
-
const list = JSON.parse(raw) as unknown
|
|
113
|
-
if (!Array.isArray(list)) return []
|
|
114
|
-
return list
|
|
115
|
-
.filter((r): r is NotificationRecord =>
|
|
116
|
-
!!r && typeof r === 'object' && typeof (r as { message?: unknown }).message === 'string')
|
|
117
|
-
// 历史失败记录没有 ok 字段:缺省按失败渲染,保证旧数据照常可查
|
|
118
|
-
.map((r) => ({ ...r, ok: r.ok === true }))
|
|
119
|
-
} catch {
|
|
120
|
-
return []
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function save(list: NotificationRecord[]): void {
|
|
125
|
-
try {
|
|
126
|
-
storage()?.setItem(KEY, JSON.stringify(list))
|
|
127
|
-
} catch {
|
|
128
|
-
/* storage full / unavailable:仅保留内存态,界面仍可查看 */
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** 追加一条通知记录并持久化,返回更新后的完整列表(最新在前,超上限裁剪)。
|
|
133
|
-
* 每次成功/失败都各自留痕:同一插件的安装/卸载记录都完整保留、只由「清空」按钮手动移除,
|
|
134
|
-
* 不会因后续操作被自动覆盖清除。 */
|
|
135
|
-
export function addNotification(record: Omit<NotificationRecord, 'id' | 'at'>): NotificationRecord[] {
|
|
136
|
-
const prev = loadNotifications()
|
|
137
|
-
let id = Date.now()
|
|
138
|
-
while (prev.some((r) => r.id === id)) id += 1
|
|
139
|
-
const next = [{ ...record, id, at: id }, ...prev].slice(0, MAX)
|
|
140
|
-
save(next)
|
|
141
|
-
return next
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** 记录一次失败:携带完整错误日志,供通知中心查看/复制/提 Issue。 */
|
|
145
|
-
export function addFailure(record: Omit<NotificationRecord, 'id' | 'at' | 'ok'>): NotificationRecord[] {
|
|
146
|
-
return addNotification({ ...record, ok: false })
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** 记录一次成功:轻量记录,不带日志。 */
|
|
150
|
-
export function addSuccess(record: Omit<NotificationRecord, 'id' | 'at' | 'ok' | 'message'>): NotificationRecord[] {
|
|
151
|
-
return addNotification({ ...record, ok: true, message: '' })
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** 清空全部通知记录,返回空列表。 */
|
|
155
|
-
export function clearNotifications(): NotificationRecord[] {
|
|
156
|
-
save([])
|
|
157
|
-
return []
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/** 按 id 删除单条通知记录,返回更新后的列表(仅移除该条,不影响其余记录)。 */
|
|
161
|
-
export function removeNotification(id: number): NotificationRecord[] {
|
|
162
|
-
const next = loadNotifications().filter((r) => r.id !== id)
|
|
163
|
-
save(next)
|
|
164
|
-
return next
|
|
165
|
-
}
|
|
@@ -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'
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* Install target/command helpers: decide the install channel (npm package
|
|
7
|
+
* vs explicit-HTTPS GitHub), build the display command and normalize a raw
|
|
8
|
+
* install spec back to its owner/repo identity.
|
|
9
|
+
*/
|
|
10
|
+
import type { HubPlugin } from '../types.ts'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 安装通道决策(用户无感知):目录探测到 npm 包名 → 用 npm 包名安装
|
|
14
|
+
* (走 npm registry tarball,更快、与 GitHub 网络无关);无 npm 包名 → git 直装。
|
|
15
|
+
* 返回值 target 即传给后端 /install 的安装目标(npm 包名 或 owner/repo)。
|
|
16
|
+
*/
|
|
17
|
+
export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'github' } {
|
|
18
|
+
const pkg = (p.source?.npmPackage ?? '').trim()
|
|
19
|
+
const repo = (p.source?.repo ?? '').trim()
|
|
20
|
+
if (pkg && repo) return { target: pkg, via: 'npm' }
|
|
21
|
+
return { target: repo, via: 'github' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 展示用安装命令(复制/弹窗):目录下发的权威命令优先(CLI-only 插件如 dsh-tui
|
|
25
|
+
* 需专属 profile(--profile dsh-tui),无法从 repo/npm 包名推断,必须用官方命令);
|
|
26
|
+
* 常规插件无目录命令时按通道回退生成:npm 显示包名,git 显示显式 HTTPS URL。 */
|
|
27
|
+
export function installCommandOf(p: HubPlugin, withProfile = false): string {
|
|
28
|
+
const { target, via } = installTargetOf(p)
|
|
29
|
+
if (via === 'npm') {
|
|
30
|
+
const cmd = p.install?.command
|
|
31
|
+
if (cmd) return cmd
|
|
32
|
+
} else {
|
|
33
|
+
const cmd = p.install?.githubCommand
|
|
34
|
+
if (cmd) return cmd
|
|
35
|
+
}
|
|
36
|
+
return via === 'npm'
|
|
37
|
+
? `dsh plugin${withProfile ? ' --profile web' : ''} add ${target}`
|
|
38
|
+
: `dsh plugin${withProfile ? ' --profile web' : ''} add git+https://github.com/${target}.git`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Normalize a task/install target to its owner/repo display identity. */
|
|
42
|
+
export function repoFromInstallTarget(value: string): string {
|
|
43
|
+
const input = value.trim()
|
|
44
|
+
if (/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(input)) return input
|
|
45
|
+
const patterns = [
|
|
46
|
+
/^github:([^/]+)\/([^/]+)$/i,
|
|
47
|
+
/^(?:git\+)?https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
48
|
+
/^(?:git\+)?ssh:\/\/(?:git@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
49
|
+
/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?(?:[?#].*)?$/i,
|
|
50
|
+
]
|
|
51
|
+
for (const pattern of patterns) {
|
|
52
|
+
const match = pattern.exec(input)
|
|
53
|
+
if (match !== null) return `${match[1]}/${match[2]}`
|
|
54
|
+
}
|
|
55
|
+
return value
|
|
56
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
* Installed-plugin model (pure logic, zero React/UI dependencies).
|
|
7
|
+
*
|
|
8
|
+
* Merges the local installed table (from the host's `/installed` route:
|
|
9
|
+
* npm name → manifest spec) with the catalog projection into a single
|
|
10
|
+
* `InstalledItem` view-model consumed by the Installed view. Also hosts the
|
|
11
|
+
* install matching / update detection used by the market view, so both
|
|
12
|
+
* views share one source of truth.
|
|
13
|
+
*/
|
|
14
|
+
import type { HubPlugin } from '../types.ts'
|
|
15
|
+
import { HUB_REPO } from './constants.ts'
|
|
16
|
+
import { repoFromInstallTarget } from './install-command.ts'
|
|
17
|
+
|
|
18
|
+
/** 安装时记录的目录信号(versions 表项)。 */
|
|
19
|
+
export interface InstalledVersionSignal {
|
|
20
|
+
version: string
|
|
21
|
+
updatedAt: string
|
|
22
|
+
installedAt?: string
|
|
23
|
+
npmPackage?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 已安装项统一模型:目录元数据 + 宿主运行时信息的合并视图(驱动「已安装」tab)。 */
|
|
27
|
+
export interface InstalledItem {
|
|
28
|
+
/** npm 包名(profile 依赖 key) */
|
|
29
|
+
name: string
|
|
30
|
+
/** manifest spec(github:owner/repo、git+https://…、或 npm 版本号) */
|
|
31
|
+
spec: string
|
|
32
|
+
/** 目录里匹配到的插件;目录外/未收录的安装为 null */
|
|
33
|
+
plugin: HubPlugin | null
|
|
34
|
+
/** 仓库身份(owner/repo);纯 npm 安装无法识别时为 null */
|
|
35
|
+
repo: string | null
|
|
36
|
+
/** 安装到系统上的路径(profile/node_modules/<包名>);宿主未提供为 null */
|
|
37
|
+
installPath: string | null
|
|
38
|
+
/** 安装时记录的目录版本(无记录为 null) */
|
|
39
|
+
installedVersion: string | null
|
|
40
|
+
/** 安装时间(ISO,无记录为 null) */
|
|
41
|
+
installedAt: string | null
|
|
42
|
+
/** 目录当前最新版本(仓库无 release 为 null) */
|
|
43
|
+
catalogVersion: string | null
|
|
44
|
+
/** 是否有可用更新(有版本比版本,无版本比仓库更新时间) */
|
|
45
|
+
hasUpdate: boolean
|
|
46
|
+
/** 是否已加载进运行中 loader(官方 ctx.loader 对账);false = 已安装但未挂载(装完未重启) */
|
|
47
|
+
loaded: boolean
|
|
48
|
+
/** 是否真正的 dsh 插件(包内声明 dsh 配置 / 在 profile bundles 清单):
|
|
49
|
+
* 非 dsh 插件(如 GitHub 官方示例仓库)装上也不会被宿主加载,不提示「待重启」 */
|
|
50
|
+
dshCapable: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 安装时记录的目录信号(按仓库小写 key 查表)。 */
|
|
54
|
+
function signalOf(repo: string | null, versions: Record<string, InstalledVersionSignal>): InstalledVersionSignal | null {
|
|
55
|
+
if (!repo) return null
|
|
56
|
+
return versions[repo.toLowerCase()] ?? null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
|
|
60
|
+
export function installedNameOf(
|
|
61
|
+
plugin: HubPlugin,
|
|
62
|
+
installed: Record<string, string>,
|
|
63
|
+
versions: Record<string, InstalledVersionSignal>,
|
|
64
|
+
): string | null {
|
|
65
|
+
const repo = plugin.source?.repo
|
|
66
|
+
if (!repo) return null
|
|
67
|
+
const needle = repo.toLowerCase()
|
|
68
|
+
for (const [name, spec] of Object.entries(installed)) {
|
|
69
|
+
if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
|
|
70
|
+
}
|
|
71
|
+
// npm 通道安装:profile 依赖 key 直接是 npm 包名。包名来源两处——目录数据(npmPackage),
|
|
72
|
+
// 或服务端 npm 优先反查持久化的映射(versions[repo].npmPackage,覆盖组织 scope 与 GitHub
|
|
73
|
+
// 用户名不一致、目录未下发包名的场景);命中任一并依赖 key 真实存在即视为已安装。
|
|
74
|
+
// 只认 npm 安装的依赖(spec 无法解析出仓库身份):git/本地安装的依赖身份由仓库匹配决定,
|
|
75
|
+
// 包名同名不代表同仓库——否则「git 装 A 仓库」会被「npm 包名同名但仓库不同」的目录条目
|
|
76
|
+
// 误认领成已安装(假阳性),installedItems 里出现同 key 双条目,排序一变化就渲染出幽灵行。
|
|
77
|
+
const pkg = ((plugin.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? '').toLowerCase()
|
|
78
|
+
if (pkg) {
|
|
79
|
+
for (const [name, spec] of Object.entries(installed)) {
|
|
80
|
+
if (isRepoLike(repoFromInstallTarget(spec))) continue
|
|
81
|
+
if (name.toLowerCase() === pkg) return name
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 该插件安装时记录的目录版本(无记录/未安装 → null)。 */
|
|
88
|
+
export function installedVersionOf(plugin: HubPlugin, versions: Record<string, InstalledVersionSignal>): string | null {
|
|
89
|
+
const repo = plugin.source?.repo
|
|
90
|
+
if (!repo) return null
|
|
91
|
+
return versions[repo.toLowerCase()]?.version ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 是否有更新:仅对已安装插件有意义。
|
|
96
|
+
* 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
|
|
97
|
+
* (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
|
|
98
|
+
*/
|
|
99
|
+
export function hasUpdateOf(
|
|
100
|
+
plugin: HubPlugin,
|
|
101
|
+
installed: Record<string, string>,
|
|
102
|
+
versions: Record<string, InstalledVersionSignal>,
|
|
103
|
+
): boolean {
|
|
104
|
+
if (installedNameOf(plugin, installed, versions) === null) return false
|
|
105
|
+
const repo = plugin.source?.repo
|
|
106
|
+
if (!repo) return false
|
|
107
|
+
const rec = versions[repo.toLowerCase()]
|
|
108
|
+
if (!rec) return false
|
|
109
|
+
if (plugin.version) return plugin.version !== rec.version
|
|
110
|
+
const current = plugin.dates?.repoUpdatedAt
|
|
111
|
+
return Boolean(current && rec.updatedAt && current > rec.updatedAt)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 依赖 spec 能否解析出仓库身份(owner/repo 形态才算)。 */
|
|
115
|
+
function isRepoLike(value: string): boolean {
|
|
116
|
+
return /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(value)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 已安装项 → 弹窗可用的伪插件对象:卸载确认弹窗只读名称/来源行,
|
|
120
|
+
* 自定义安装(目录外)没有完整目录数据,用它把 InstalledItem 适配成 HubPlugin。 */
|
|
121
|
+
export function pluginOfItem(item: InstalledItem): HubPlugin {
|
|
122
|
+
return {
|
|
123
|
+
slug: item.name,
|
|
124
|
+
displayName: item.plugin?.displayName ?? item.name,
|
|
125
|
+
source: item.repo ? { repo: item.repo } : undefined,
|
|
126
|
+
description: item.plugin?.description,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Hub 自身(本插件 dsh-plugin)的识别:三维互证,杜绝误伤同名第三方包。
|
|
132
|
+
* 1) 依赖 key 固定为 `dsh-plugin`(本插件 npm 包名);
|
|
133
|
+
* 2) spec 为 `file:`/`link:` 本地链接(hub 以 file: 安装进宿主);
|
|
134
|
+
* 3) 或 spec 解析出的仓库身份就是自营仓库 `dshplugin/dsh-plugin-hub`。
|
|
135
|
+
* 命中即视为宿主内置本体:永不出现在「自定义安装」里,卸载接口也早已对
|
|
136
|
+
* 它 400 拒绝(服务端多维防线之一)——显示出来只会让用户误点卸载。
|
|
137
|
+
*/
|
|
138
|
+
function isHubSelf(name: string, spec: string): boolean {
|
|
139
|
+
if (name !== 'dsh-plugin') return false
|
|
140
|
+
if (spec.startsWith('file:') || spec.startsWith('link:')) return true
|
|
141
|
+
const repo = repoFromInstallTarget(spec)
|
|
142
|
+
return isRepoLike(repo) && repo.toLowerCase() === HUB_REPO.toLowerCase()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 构建已安装项列表:目录插件按命中关系合并(拥有完整元数据),
|
|
147
|
+
* 剩余未认领的依赖 key 归为自定义安装(目录外,仅运行时信息)。
|
|
148
|
+
*/
|
|
149
|
+
export function installedItemsOf(
|
|
150
|
+
plugins: HubPlugin[] | null,
|
|
151
|
+
installed: Record<string, string>,
|
|
152
|
+
versions: Record<string, InstalledVersionSignal>,
|
|
153
|
+
paths: Record<string, string> | null,
|
|
154
|
+
loadedNames: string[] | null,
|
|
155
|
+
dshCapableNames: string[] | null,
|
|
156
|
+
): InstalledItem[] {
|
|
157
|
+
const items: InstalledItem[] = []
|
|
158
|
+
// 已被目录条目认领的依赖 key:同一依赖只归属一个目录条目(React key 必须唯一,
|
|
159
|
+
// 否则同名双条目排序一变化就渲染出幽灵行);剩余未认领的归自定义安装。
|
|
160
|
+
const claimed = new Set<string>()
|
|
161
|
+
// 目录匹配:遍历目录插件,命中 installedNameOf 即视为已安装并合并元数据
|
|
162
|
+
for (const p of plugins ?? []) {
|
|
163
|
+
const name = installedNameOf(p, installed, versions)
|
|
164
|
+
if (name === null || claimed.has(name)) continue
|
|
165
|
+
claimed.add(name)
|
|
166
|
+
const repo = p.source?.repo ?? null
|
|
167
|
+
const rec = signalOf(repo, versions)
|
|
168
|
+
items.push({
|
|
169
|
+
name,
|
|
170
|
+
spec: installed[name],
|
|
171
|
+
plugin: p,
|
|
172
|
+
repo,
|
|
173
|
+
installPath: paths?.[name] ?? null,
|
|
174
|
+
installedVersion: rec?.version ?? null,
|
|
175
|
+
installedAt: rec?.installedAt ?? null,
|
|
176
|
+
catalogVersion: p.version ?? null,
|
|
177
|
+
hasUpdate: hasUpdateOf(p, installed, versions),
|
|
178
|
+
loaded: loadedNames?.includes(name) ?? false,
|
|
179
|
+
dshCapable: dshCapableNames?.includes(name) ?? false,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
// 自定义安装:依赖表里没被任何目录插件认领的条目(命令行安装的目录外插件)
|
|
183
|
+
for (const [name, spec] of Object.entries(installed)) {
|
|
184
|
+
if (claimed.has(name)) continue
|
|
185
|
+
// Hub 自身(本插件 dsh-plugin)由宿主内置加载,不是可管理的插件:永不进已安装列表,
|
|
186
|
+
// 避免出现在「自定义安装」里被误点卸载(服务端卸载接口对它的 400 拒绝是最后一道防线)
|
|
187
|
+
if (isHubSelf(name, spec)) continue
|
|
188
|
+
const parsed = repoFromInstallTarget(spec)
|
|
189
|
+
const repo = isRepoLike(parsed) ? parsed : null
|
|
190
|
+
const rec = signalOf(repo, versions)
|
|
191
|
+
items.push({
|
|
192
|
+
name,
|
|
193
|
+
spec,
|
|
194
|
+
plugin: null,
|
|
195
|
+
repo,
|
|
196
|
+
installPath: paths?.[name] ?? null,
|
|
197
|
+
installedVersion: rec?.version ?? null,
|
|
198
|
+
installedAt: rec?.installedAt ?? null,
|
|
199
|
+
catalogVersion: null,
|
|
200
|
+
// 目录外没有目录信号可比,无法判断是否有更新
|
|
201
|
+
hasUpdate: false,
|
|
202
|
+
loaded: loadedNames?.includes(name) ?? false,
|
|
203
|
+
dshCapable: dshCapableNames?.includes(name) ?? false,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
return items
|
|
207
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
* Normalizer: maps the online API's short-key payload (s/o/n/vr/c/t/f/d/r/v/u/a/sg/fk)
|
|
7
|
+
* to HubPlugin so rendering only ever sees one shape.
|
|
8
|
+
*/
|
|
9
|
+
import type { HubPlugin } from '../types.ts'
|
|
10
|
+
|
|
11
|
+
export function normalize(raw: Record<string, unknown>): HubPlugin {
|
|
12
|
+
if (typeof raw.s === 'string') {
|
|
13
|
+
return {
|
|
14
|
+
slug: raw.s,
|
|
15
|
+
ownerSlug: typeof raw.o === 'string' ? raw.o : undefined,
|
|
16
|
+
displayName: typeof raw.n === 'string' ? raw.n : undefined,
|
|
17
|
+
version: typeof raw.vr === 'string' ? raw.vr : undefined,
|
|
18
|
+
category: typeof raw.c === 'string' ? raw.c : undefined,
|
|
19
|
+
topics: Array.isArray(raw.t) ? (raw.t as string[]) : undefined,
|
|
20
|
+
features: Array.isArray(raw.f) ? (raw.f as string[]) : undefined,
|
|
21
|
+
description: typeof raw.d === 'string' ? raw.d : undefined,
|
|
22
|
+
// source 双形态:字符串 repo,或 { repo, npmPackage } 对象
|
|
23
|
+
source: typeof raw.r === 'string'
|
|
24
|
+
? { repo: raw.r }
|
|
25
|
+
: raw.r !== null && typeof raw.r === 'object'
|
|
26
|
+
? {
|
|
27
|
+
repo: typeof (raw.r as { repo?: unknown }).repo === 'string' ? (raw.r as { repo: string }).repo : undefined,
|
|
28
|
+
npmPackage: typeof (raw.r as { npmPackage?: unknown }).npmPackage === 'string' ? (raw.r as { npmPackage: string }).npmPackage : undefined,
|
|
29
|
+
}
|
|
30
|
+
: undefined,
|
|
31
|
+
// 安装信息:wi 仅在 false 时下发(缺省视为 true 可一键安装);
|
|
32
|
+
// ic/igc = 目录下发的权威安装命令(npm/git 通道各一条,CLI-only 插件如 dsh-tui 必须用)
|
|
33
|
+
install: raw.wi === false || typeof raw.ic === 'string' || typeof raw.igc === 'string'
|
|
34
|
+
? {
|
|
35
|
+
webInstallable: raw.wi === false ? false : undefined,
|
|
36
|
+
command: typeof raw.ic === 'string' ? raw.ic : undefined,
|
|
37
|
+
githubCommand: typeof raw.igc === 'string' ? raw.igc : undefined,
|
|
38
|
+
}
|
|
39
|
+
: undefined,
|
|
40
|
+
compatibility: typeof raw.v === 'string' ? { status: raw.v } : undefined,
|
|
41
|
+
dates: {
|
|
42
|
+
repoUpdatedAt: typeof raw.u === 'string' ? raw.u : undefined,
|
|
43
|
+
addedAt: typeof raw.a === 'string' ? raw.a : undefined,
|
|
44
|
+
},
|
|
45
|
+
stats: {
|
|
46
|
+
stargazers_count: typeof raw.sg === 'number' ? raw.sg : undefined,
|
|
47
|
+
forks_count: typeof raw.fk === 'number' ? raw.fk : undefined,
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return raw as unknown as HubPlugin
|
|
52
|
+
}
|
|
@@ -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
|
* 轻量 Markdown 渲染器:仅用于 Hub 自我更新的变更记录(Worker 下发的 notes)。
|
|
3
7
|
*
|
|
4
8
|
* 内容由插件作者自己编写、属可信内容,但渲染端仍先整体 HTML 转义,再按
|