dsh-plugin 1.3.0 → 1.3.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/client/client.js +328 -296
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +2 -0
- package/lib/services/install/task-queue.js +9 -3
- package/lib/types/services/install/task-queue.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +15 -4
- package/src/client/components/modals/NotificationsModal.tsx +21 -13
- package/src/client/logic/installed.ts +40 -1
- package/src/server/http/routes.ts +2 -0
- package/src/server/services/install/task-queue.ts +13 -3
package/lib/http/routes.js
CHANGED
|
@@ -660,6 +660,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
660
660
|
attempts,
|
|
661
661
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
662
662
|
env: mutationEnv(settings),
|
|
663
|
+
// 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
664
|
+
updateNpm: mode === 'update',
|
|
663
665
|
});
|
|
664
666
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
665
667
|
}
|
|
@@ -227,20 +227,26 @@ function pumpQueue() {
|
|
|
227
227
|
* Never rejects; failures surface through the result / task state.
|
|
228
228
|
*/
|
|
229
229
|
function spawnMutation(options) {
|
|
230
|
-
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm } = options;
|
|
230
|
+
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm, updateNpm } = options;
|
|
231
231
|
// 全局 npm 安装:直接 spawn 本机 npm(`npm install -g <pkgs>`),不是 dsh plugin 命令
|
|
232
232
|
const isGlobalNpm = (globalNpm ?? []).length > 0;
|
|
233
233
|
const invocation = isGlobalNpm
|
|
234
234
|
? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
235
235
|
: cliInvocation();
|
|
236
|
+
// 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
|
|
237
|
+
// (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
|
|
238
|
+
// 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
|
|
239
|
+
const npmUpdateTarget = updateNpm && !isGlobalNpm && githubRepoOf(target) === null
|
|
240
|
+
? `${target}@latest`
|
|
241
|
+
: target;
|
|
236
242
|
const args = isGlobalNpm
|
|
237
243
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
238
|
-
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action,
|
|
244
|
+
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action, npmUpdateTarget];
|
|
239
245
|
// 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
|
|
240
246
|
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
241
247
|
const executed = isGlobalNpm
|
|
242
248
|
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
243
|
-
: `dsh plugin --profile ${profile} ${action} ${
|
|
249
|
+
: `dsh plugin --profile ${profile} ${action} ${npmUpdateTarget}`;
|
|
244
250
|
if (task && !task.attempts.includes(executed)) {
|
|
245
251
|
task.attempts.push(executed);
|
|
246
252
|
}
|
|
@@ -50,6 +50,8 @@ export declare function startPluginMutation(options: {
|
|
|
50
50
|
uninstallLoader?: LoaderHandle;
|
|
51
51
|
/** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
|
|
52
52
|
attempts?: string[];
|
|
53
|
+
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
54
|
+
updateNpm?: boolean;
|
|
53
55
|
}): InstallTask;
|
|
54
56
|
/**
|
|
55
57
|
* 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -36,7 +36,7 @@ import { CustomInstallView } from './views/CustomInstallView.tsx'
|
|
|
36
36
|
import { SettingsView } from './views/SettingsView.tsx'
|
|
37
37
|
import { CloseIcon } from './ui/icons.tsx'
|
|
38
38
|
import type { InstalledItem } from '../logic/installed.ts'
|
|
39
|
-
import { pluginOfItem } from '../logic/installed.ts'
|
|
39
|
+
import { issueRepoOf, pluginOfItem } from '../logic/installed.ts'
|
|
40
40
|
|
|
41
41
|
export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
42
42
|
const { lang, langKey, langPath, t, toggleLang } = useLanguage(locale)
|
|
@@ -116,9 +116,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
116
116
|
setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
|
|
117
117
|
},
|
|
118
118
|
onError: (message, repo, kind, command, attempts) => {
|
|
119
|
-
|
|
119
|
+
// 提 Issue 目标先反查仓库身份:npm 包名能在已安装表/目录里反查到 owner/repo 才保留,
|
|
120
|
+
// 反查不到置 null —— 错误弹窗/通知中心据此不显示「一键提 Issue」按钮(防提到错误仓库)
|
|
121
|
+
const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
|
|
122
|
+
setErrorMsg({ message, repo: issueRepo, kind, command, attempts })
|
|
120
123
|
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕
|
|
121
|
-
setNotifications(addFailure({ kind, repo:
|
|
124
|
+
setNotifications(addFailure({ kind, repo: issueRepo ?? '', message, command, attempts }))
|
|
122
125
|
},
|
|
123
126
|
installPlugin: confirmPlugin,
|
|
124
127
|
installCustomTarget: confirmCustomTarget,
|
|
@@ -237,7 +240,12 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
237
240
|
(i.repo !== null && i.repo.toLowerCase() === needle) ||
|
|
238
241
|
repoFromInstallTarget(i.spec).toLowerCase() === needle)
|
|
239
242
|
}
|
|
240
|
-
|
|
243
|
+
// npm 包名(可带 @version):剥掉版本号后同时查已安装列表与原始依赖表。
|
|
244
|
+
// Hub 自身(file:/link: 安装)被 installedItems 刻意排除(防误卸载),列表里查不到,
|
|
245
|
+
// 但依赖表真实存在——补齐后「命令窗口重装 hub 自身」也能转「更新」语义,不再撞 already installed
|
|
246
|
+
const pkg = repo.replace(/@[^@]*$/, '').toLowerCase()
|
|
247
|
+
return catalog.installedItems.some((i) => i.name.toLowerCase() === pkg) ||
|
|
248
|
+
Object.keys(catalog.installed).some((k) => k.toLowerCase() === pkg)
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
/** 弹窗动作一:复制安装命令到剪贴板,引导去终端粘贴执行(npm 通道显示包名命令)。 */
|
|
@@ -502,6 +510,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
502
510
|
cancelTask: queue.cancelTask,
|
|
503
511
|
restarting,
|
|
504
512
|
onRestart: () => { void requestRestart() },
|
|
513
|
+
// 失败记录仓库反查:历史记录可能存的是裸 npm 包名,反查成 owner/repo 才显示仓库链接
|
|
514
|
+
// 与提 Issue 按钮;查不到则隐藏(防提到错误仓库)
|
|
515
|
+
resolveRepo: (repo: string) => issueRepoOf(repo, catalog.installedItems, catalog.plugins),
|
|
505
516
|
}),
|
|
506
517
|
// 「忽略本次更新」确认弹窗:确认后该版本写入忽略集(持久化)并从通知中心移除,
|
|
507
518
|
// 本次版本不再提醒,直到插件发布下一个新版本
|
|
@@ -55,7 +55,7 @@ function BadgeGlyph({ ok }: { ok: boolean }) {
|
|
|
55
55
|
}))
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, onUpdate, onIgnoreUpdate, cancelTask, restarting, onRestart }: {
|
|
58
|
+
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, onUpdate, onIgnoreUpdate, cancelTask, restarting, onRestart, resolveRepo }: {
|
|
59
59
|
records: NotificationRecord[]
|
|
60
60
|
/** 进行中的安装/卸载任务(实时进度,与队列弹窗同一数据源) */
|
|
61
61
|
tasks: QueueTask[]
|
|
@@ -75,6 +75,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
75
75
|
cancelTask: (id: number) => void
|
|
76
76
|
restarting: boolean
|
|
77
77
|
onRestart: () => void
|
|
78
|
+
/** 失败记录 repo 反查:npm 包名(历史记录存的是裸包名)反查成 owner/repo 再显示/提 Issue;
|
|
79
|
+
* 查不到返回 null,按钮与仓库链接一并隐藏(防提到错误仓库)。不传则按记录原样展示。 */
|
|
80
|
+
resolveRepo?: (repo: string) => string | null
|
|
78
81
|
}) {
|
|
79
82
|
return h('div', {
|
|
80
83
|
className: styles.overlay,
|
|
@@ -165,6 +168,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
165
168
|
: h('div', { className: styles.noticeList },
|
|
166
169
|
records.map((r) => {
|
|
167
170
|
const isUpdate = r.kind === 'update'
|
|
171
|
+
// 展示用仓库:历史记录存的可能是裸 npm 包名,反查成 owner/repo 才显示/提 Issue;
|
|
172
|
+
// 查不到为空 → 仓库链接与提 Issue 按钮都不渲染
|
|
173
|
+
const shownRepo = resolveRepo ? (resolveRepo(r.repo) ?? '') : r.repo
|
|
168
174
|
return h('div', {
|
|
169
175
|
key: r.id,
|
|
170
176
|
// 更新提醒:整行可点击直达更新确认弹窗;install/uninstall 记录维持原交互
|
|
@@ -191,14 +197,16 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
191
197
|
r.repo
|
|
192
198
|
? (isUpdate
|
|
193
199
|
? h('span', { className: styles.failRepo, title: r.repo }, r.repo)
|
|
194
|
-
:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
: shownRepo
|
|
201
|
+
? h('a', {
|
|
202
|
+
className: styles.failRepo,
|
|
203
|
+
// 跳转到官网详情页(含插件收录信息),不在通知弹窗直接跳 GitHub
|
|
204
|
+
href: pluginSiteUrl(shownRepo),
|
|
205
|
+
target: '_blank',
|
|
206
|
+
rel: 'noopener noreferrer',
|
|
207
|
+
title: shownRepo,
|
|
208
|
+
}, shownRepo)
|
|
209
|
+
: null)
|
|
202
210
|
: null,
|
|
203
211
|
!r.ok && h('button', {
|
|
204
212
|
className: styles.failCopy,
|
|
@@ -252,18 +260,18 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
252
260
|
h('div', { className: styles.failPrepareHint }, kind === 'pnpmIgnoredBuild'
|
|
253
261
|
? t('failIgnoredBuild')
|
|
254
262
|
: /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
|
|
255
|
-
r.repo ? h('a', {
|
|
263
|
+
r.repo && shownRepo ? h('a', {
|
|
256
264
|
className: styles.failBigIssue,
|
|
257
|
-
href: pluginIssueUrl(
|
|
265
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
|
|
258
266
|
target: '_blank',
|
|
259
267
|
rel: 'noopener noreferrer',
|
|
260
268
|
title: t('failIssueHint'),
|
|
261
269
|
}, t('failIssueBig')) : null,
|
|
262
270
|
])
|
|
263
271
|
}
|
|
264
|
-
return r.repo ? h('a', {
|
|
272
|
+
return r.repo && shownRepo ? h('a', {
|
|
265
273
|
className: styles.failBigIssue,
|
|
266
|
-
href: pluginIssueUrl(
|
|
274
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
|
|
267
275
|
target: '_blank',
|
|
268
276
|
rel: 'noopener noreferrer',
|
|
269
277
|
title: t('failIssueHint'),
|
|
@@ -116,6 +116,40 @@ function isRepoLike(value: string): boolean {
|
|
|
116
116
|
return /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(value)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 提 Issue 的仓库身份解析:失败目标可能是 owner/repo 也可能是 npm 包名(含 @scope/pkg、
|
|
121
|
+
* 可带 @version,如 dsh-plugin、@scope/pkg@1.2.3)。
|
|
122
|
+
* - 目标本身就能解析出 owner/repo → 直接用;
|
|
123
|
+
* - npm 包名 → 反查仓库身份:先查已安装表(该包名的条目若带仓库身份即命中),再查目录插件
|
|
124
|
+
* (source.npmPackage 与包名一致,取目录收录的仓库身份)。
|
|
125
|
+
* 反查到才返回,查不到返回 null —— 调用方据此不显示「一键提 Issue」按钮,避免把 Issue
|
|
126
|
+
* 提到错误的地址(别人发的 npm 包名不是 GitHub 仓库,直接拼 github.com/<包名> 是打不开的)。
|
|
127
|
+
*/
|
|
128
|
+
export function issueRepoOf(
|
|
129
|
+
target: string,
|
|
130
|
+
installedItems: InstalledItem[],
|
|
131
|
+
plugins: HubPlugin[] | null,
|
|
132
|
+
): string | null {
|
|
133
|
+
const input = target.trim()
|
|
134
|
+
if (!input) return null
|
|
135
|
+
const parsed = repoFromInstallTarget(input)
|
|
136
|
+
if (isRepoLike(parsed)) return parsed
|
|
137
|
+
const pkg = parsed.replace(/@[^@]*$/, '').toLowerCase()
|
|
138
|
+
// 已安装表:包名命中的条目(目录收录条目直接带仓库身份;自定义安装按 github: spec 解析)
|
|
139
|
+
for (const item of installedItems) {
|
|
140
|
+
if (item.name.toLowerCase() !== pkg) continue
|
|
141
|
+
if (item.repo) return item.repo
|
|
142
|
+
const repo = repoFromInstallTarget(item.spec)
|
|
143
|
+
if (isRepoLike(repo)) return repo
|
|
144
|
+
}
|
|
145
|
+
// 目录插件:npmPackage 与包名一致 → 用目录收录的仓库身份
|
|
146
|
+
for (const p of plugins ?? []) {
|
|
147
|
+
const pkgName = (p.source?.npmPackage ?? '').trim()
|
|
148
|
+
if (pkgName !== '' && pkgName.toLowerCase() === pkg && p.source?.repo) return p.source.repo
|
|
149
|
+
}
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
/** 已安装项 → 弹窗可用的伪插件对象:卸载确认弹窗只读名称/来源行,
|
|
120
154
|
* 自定义安装(目录外)没有完整目录数据,用它把 InstalledItem 适配成 HubPlugin。 */
|
|
121
155
|
export function pluginOfItem(item: InstalledItem): HubPlugin {
|
|
@@ -139,7 +173,12 @@ function isHubSelf(name: string, spec: string): boolean {
|
|
|
139
173
|
if (name !== 'dsh-plugin') return false
|
|
140
174
|
if (spec.startsWith('file:') || spec.startsWith('link:')) return true
|
|
141
175
|
const repo = repoFromInstallTarget(spec)
|
|
142
|
-
|
|
176
|
+
if (isRepoLike(repo) && repo.toLowerCase() === HUB_REPO.toLowerCase()) return true
|
|
177
|
+
// npm 通道覆盖重装 hub 自身后,file:/link: 本地链接会被替换成 registry 版本号/范围
|
|
178
|
+
// (如 1.3.0、^1.3.0)——依赖 key `dsh-plugin` 在 npm 上唯一属于本插件(不存在第三方
|
|
179
|
+
// 同名包),版本号形态与仓库身份同样视为 hub 自身:命令窗口「自己装自己」成功后,
|
|
180
|
+
// 条目仍不进已安装列表(卸载接口对它的 400 拒绝是服务端最后一道防线)。
|
|
181
|
+
return !isRepoLike(repo)
|
|
143
182
|
}
|
|
144
183
|
|
|
145
184
|
/**
|
|
@@ -659,6 +659,8 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
|
|
|
659
659
|
attempts,
|
|
660
660
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
661
661
|
env: mutationEnv(settings),
|
|
662
|
+
// 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
663
|
+
updateNpm: mode === 'update',
|
|
662
664
|
})
|
|
663
665
|
sendJson(response, 200, { ok: true, task: task.id })
|
|
664
666
|
} catch (error) {
|
|
@@ -177,6 +177,8 @@ export function startPluginMutation(options: {
|
|
|
177
177
|
uninstallLoader?: LoaderHandle
|
|
178
178
|
/** 入队前已尝试的安装方式(npm registry 反查等):失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
|
|
179
179
|
attempts?: string[]
|
|
180
|
+
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
181
|
+
updateNpm?: boolean
|
|
180
182
|
}): InstallTask {
|
|
181
183
|
const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false }
|
|
182
184
|
nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
|
|
@@ -266,21 +268,29 @@ function spawnMutation(options: {
|
|
|
266
268
|
uninstallLoader?: LoaderHandle
|
|
267
269
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
268
270
|
globalNpm?: string[]
|
|
271
|
+
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
272
|
+
updateNpm?: boolean
|
|
269
273
|
}): Promise<InstallResult> {
|
|
270
|
-
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm } = options
|
|
274
|
+
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, uninstallLoader, globalNpm, updateNpm } = options
|
|
271
275
|
// 全局 npm 安装:直接 spawn 本机 npm(`npm install -g <pkgs>`),不是 dsh plugin 命令
|
|
272
276
|
const isGlobalNpm = (globalNpm ?? []).length > 0
|
|
273
277
|
const invocation = isGlobalNpm
|
|
274
278
|
? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
275
279
|
: cliInvocation()
|
|
280
|
+
// 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
|
|
281
|
+
// (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
|
|
282
|
+
// 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
|
|
283
|
+
const npmUpdateTarget = updateNpm && !isGlobalNpm && githubRepoOf(target) === null
|
|
284
|
+
? `${target}@latest`
|
|
285
|
+
: target
|
|
276
286
|
const args = isGlobalNpm
|
|
277
287
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
278
|
-
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action,
|
|
288
|
+
: [...invocation.prefixArgs, 'plugin', '--profile', profile, action, npmUpdateTarget]
|
|
279
289
|
// 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
|
|
280
290
|
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
281
291
|
const executed = isGlobalNpm
|
|
282
292
|
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
283
|
-
: `dsh plugin --profile ${profile} ${action} ${
|
|
293
|
+
: `dsh plugin --profile ${profile} ${action} ${npmUpdateTarget}`
|
|
284
294
|
if (task && !task.attempts.includes(executed)) {
|
|
285
295
|
task.attempts.push(executed)
|
|
286
296
|
}
|