dsh-plugin 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.en.md +38 -1
  2. package/README.md +38 -1
  3. package/client/client.js +449 -410
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +9 -5
  6. package/lib/services/install/npm-resolve.js +3 -1
  7. package/lib/services/install/task-queue.js +29 -15
  8. package/lib/services/profile/profile.js +4 -3
  9. package/lib/services/settings.js +1 -1
  10. package/lib/types/services/install/install-types.d.ts +2 -2
  11. package/lib/types/services/install/npm-resolve.d.ts +3 -1
  12. package/lib/types/services/install/task-queue.d.ts +3 -3
  13. package/lib/types/services/settings.d.ts +2 -2
  14. package/package.json +1 -1
  15. package/src/client/components/PluginHubSection.tsx +13 -7
  16. package/src/client/components/modals/NotificationsModal.tsx +9 -3
  17. package/src/client/components/views/CustomInstallView.tsx +35 -14
  18. package/src/client/components/views/DiagnosticsView.tsx +7 -3
  19. package/src/client/components/views/SettingsView.tsx +20 -33
  20. package/src/client/hooks/useSettings.ts +3 -3
  21. package/src/client/hooks/useTaskQueue.ts +12 -6
  22. package/src/client/locales.ts +31 -15
  23. package/src/client/logic/failures.ts +3 -0
  24. package/src/client/logic/renderMarkdown.ts +9 -5
  25. package/src/client/logic/urls.ts +12 -1
  26. package/src/client/styles/CustomInstallView.module.css +25 -0
  27. package/src/client/styles/Dropdown.module.css +10 -0
  28. package/src/client/styles/Modal.module.css +11 -0
  29. package/src/server/http/routes.ts +9 -4
  30. package/src/server/services/install/install-types.ts +2 -2
  31. package/src/server/services/install/npm-resolve.ts +3 -1
  32. package/src/server/services/install/task-queue.ts +36 -20
  33. package/src/server/services/profile/profile.ts +4 -3
  34. package/src/server/services/settings.ts +3 -3
@@ -14,10 +14,10 @@ import { useCallback, useEffect, useState } from 'react'
14
14
  export interface HubSettings {
15
15
  /** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
16
16
  checkUpdatesOnStart: boolean
17
- /** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
18
- npmRegistry: string
19
17
  /** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
20
18
  proxy: string
19
+ /** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置,不注入任何 registry) */
20
+ npmRegistry: string
21
21
  /** 启用命令行安装 NPM 包(默认开) */
22
22
  enableNpmInstall: boolean
23
23
  /** 启用命令行安装 GitHub 源码(默认开) */
@@ -30,8 +30,8 @@ export interface HubSettings {
30
30
 
31
31
  export const DEFAULT_SETTINGS: HubSettings = {
32
32
  checkUpdatesOnStart: true,
33
- npmRegistry: '',
34
33
  proxy: '',
34
+ npmRegistry: '',
35
35
  enableNpmInstall: true,
36
36
  enableGitInstall: true,
37
37
  enableDshInstall: true,
@@ -28,6 +28,9 @@ export interface QueueTask {
28
28
  desc?: string
29
29
  /** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
30
30
  repo: string | null
31
+ /** 实际动作(add/update/remove):成功通知据此区分「安装成功」与「更新成功」。
32
+ * 服务端 /active 带回真实动作;本地乐观入队按 update 标记推断。 */
33
+ action?: 'add' | 'update' | 'remove'
31
34
  /** 安装时的目录信号快照(版本 + 仓库更新时间):任务成功后在本地记录,供「有更新」比对 */
32
35
  version?: string
33
36
  updatedAt?: string
@@ -63,11 +66,12 @@ export interface TaskQueueOptions {
63
66
  t: Translate
64
67
  refreshInstalled: () => void
65
68
  /** 任务成功完成:viaModal 表示任务对应当前打开弹窗(弹窗切结果视图),否则走 Toast;repo 供成功通知记录;
66
- * needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。 */
67
- onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
69
+ * needsRestart 表示该任务完成后是否仍需宿主重启(结果视图据此给「重启 / 仅完成」)。
70
+ * update 表示实际执行的是更新(覆盖重装)而非首次安装:通知中心据此显示「更新成功」而非「安装成功」。 */
71
+ onInstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean, update?: boolean) => void
68
72
  onUninstallDone: (viaModal: boolean, repo: string | null, needsRestart: boolean) => void
69
- /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)。 */
70
- onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[]) => void
73
+ /** 任务失败:完整输出或兜底文案 + 所属插件仓库 + 操作类型(安装/卸载)+ 实际执行的安装命令(issue 预填用,可缺省)+ 尝试过的安装方式(npm 反查/执行命令,可缺省)+ 是否更新。 */
74
+ onError: (message: string, repo: string | null, kind: 'install' | 'uninstall', command?: string, attempts?: string[], update?: boolean) => void
71
75
  /** 当前打开的安装/卸载弹窗插件:用于在弹窗内匹配进行中的任务。 */
72
76
  installPlugin: HubPlugin | null
73
77
  /** 当前打开的自定义安装(命令行)确认弹窗目标:与应用商店同一弹窗机制,匹配进行中的 custom 安装任务。 */
@@ -195,14 +199,14 @@ export function useTaskQueue(opts: TaskQueueOptions) {
195
199
  // 同时缓存展示信息,待重启列表(服务端已登记)合并时直接补齐简介/版本
196
200
  void syncInstalledVersion(q.repo, q.version, q.updatedAt)
197
201
  pendingInfoRef.current.set(q.target, { desc: q.desc, version: q.version })
198
- onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart)
202
+ onInstallDone(modalTaskRef.current === q.id, q.repo, q.needsRestart, q.action === 'update')
199
203
  }
200
204
  } else {
201
205
  // 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
202
206
  const detail = lines.length > 0
203
207
  ? [...lines].reverse().join('\n')
204
208
  : q.kind === 'uninstall' ? t('uninstallFail') : t('installFail')
205
- onError(detail, q.repo, q.kind, q.command, q.attempts)
209
+ onError(detail, q.repo, q.kind, q.command, q.attempts, q.action === 'update')
206
210
  }
207
211
  maybeStopPoll()
208
212
  }
@@ -367,6 +371,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
367
371
  kind: (isRemove ? 'uninstall' : 'install') as 'install' | 'uninstall',
368
372
  target: isRemove ? fallback : (display || fallback),
369
373
  repo: isRemove ? null : (display || fallback),
374
+ action: (a.action === 'add' || a.action === 'update' || a.action === 'remove') ? a.action : 'add',
370
375
  status: a.status === 'running' ? 'running' : 'pending',
371
376
  progress: typeof a.progress === 'number' ? a.progress : 0,
372
377
  lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
@@ -421,6 +426,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
421
426
  version: input.version, updatedAt: input.updatedAt,
422
427
  status: 'running', progress: 0, lines: [], optimistic: true,
423
428
  command: input.command,
429
+ action: input.update ? 'update' : 'add',
424
430
  // 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
425
431
  attempts: [], needsRestart: true,
426
432
  }])
@@ -56,13 +56,14 @@ export const zh = {
56
56
  gitInstallInvalid: '请输入 GitHub 仓库地址,例如:owner/repo 或 https://github.com/owner/repo',
57
57
  dshCmdLabel: 'DeepSeek Harness 命令',
58
58
  dshCmdPlaceholder: '粘贴官方 dsh plugin 命令',
59
- dshCmdExample: '例如:dsh plugin --profile web add github:owner/repo',
60
- dshCmdInvalid: '请输入官方 dsh plugin 安装命令(--profile 必填)。例如:dsh plugin --profile web add github:owner/repo',
59
+ dshCmdInsertHubUpdate: '插入更新命令',
60
+ dshCmdExample: '例如:dsh plugin --profile web add github:owner/repo,或 dsh plugin --profile web update dsh-plugin',
61
+ dshCmdInvalid: '请输入官方 dsh plugin 命令(--profile 必填,支持 add / update)。例如:dsh plugin --profile web update dsh-plugin',
61
62
  installHelp: '帮助',
62
63
  helpModalTitle: '支持的命令格式',
63
64
  npmInstallHelp: 'lodash、@scope/pkg\t直接输入包名\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\t全局安装 CLI 工具,不进「已安装」列表',
64
65
  gitInstallHelp: 'owner/repo\nhttps://github.com/owner/repo\ngithub:owner/repo\ngit+https://github.com/owner/repo.git\ngit@github.com:owner/repo.git',
65
- dshCmdHelp: 'dsh plugin --profile web add <目标>\ndsh plugin --profile=web add <目标>\n\t--profile 必填,官方 CLI 无 -p 简写;<目标> 可为 npm 包名或 github:owner/repo',
66
+ dshCmdHelp: 'dsh plugin --profile web add <目标>\t安装\ndsh plugin --profile web update <目标>\t更新已安装目标到最新版本\ndsh plugin --profile=web add <目标>\ndsh plugin --profile=web update <目标>\n\t--profile 必填,官方 CLI 无 -p 简写;<目标> 可为 npm 包名或 github:owner/repo;卸载请用「已安装」列表',
66
67
  globalNpmHint: '将全局安装 CLI 工具(npm install -g),不会出现在「已安装」列表,无需重启宿主',
67
68
  installCliBtn: '安装',
68
69
  // 安装通道关闭(安全信任开关关掉后,对应卡片禁用并引导去设置打开)
@@ -107,14 +108,15 @@ export const zh = {
107
108
  settingsUpdateDesc: '更新检查方式与安装走到的网络通道',
108
109
  settingsCheckOnStart: '启动时检查更新',
109
110
  settingsCheckOnStartDesc: '打开插件中心时检查已安装插件是否有新版,发现更新会提示',
111
+ settingsProxy: 'HTTP 代理',
112
+ settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
110
113
  settingsMirror: 'npm 镜像源',
111
- settingsMirrorDesc: 'npm 安装与版本反查走自定义镜像,网络不佳时更稳定;留空使用官方源',
114
+ settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
115
+ mirrorNone: '未配置(跟随本机 npm 配置)',
112
116
  mirrorOfficial: '官方源 registry.npmjs.org',
113
117
  mirrorNpmmirror: '阿里云 npmmirror',
114
118
  mirrorTencent: '腾讯云镜像',
115
- mirrorCustom: '自定义镜像源',
116
- settingsProxy: 'HTTP 代理',
117
- settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
119
+ mirrorTsinghua: '清华大学 TUNA 镜像',
118
120
  settingsSecurity: '安全信任',
119
121
  settingsSecurityDesc: '命令行安装的三个通道开关:NPM 包、GitHub 源码与 DSH 命令',
120
122
  settingsEnableNpm: '启用 NPM 安装',
@@ -180,6 +182,8 @@ export const zh = {
180
182
  settingsDiagFail: '不可达',
181
183
  // 系统诊断通道列表:一键全量诊断 + 一行一个,可点重测
182
184
  diagNpm: 'npm 源',
185
+ // 诊断页 npm 行:未配置镜像时的展示(探测目标仍为官方源,代表本机无配置时的回退)
186
+ diagNpmUnset: '未配置(跟随本机 npm 配置)',
183
187
  diagGithub: 'GitHub',
184
188
  diagCatalog: '目录站点',
185
189
  diagOk: 'OK',
@@ -230,6 +234,8 @@ export const zh = {
230
234
  installNow: '直接安装',
231
235
  installing: '安装中…',
232
236
  installDone: '安装成功',
237
+ // 更新成功(通知中心区分安装/更新):更新 = 已安装目标覆盖重装到最新,与首次安装不同语义
238
+ updateDone: '更新成功',
233
239
  installResultTitle: '插件安装成功',
234
240
  installResultDesc: '',
235
241
  // 更新语义:已安装插件点「更新」走同一条 add 命令原位覆盖重装,弹窗文案区分安装/更新
@@ -274,6 +280,8 @@ export const zh = {
274
280
  errorTitle: '操作失败',
275
281
  errorTitleInstall: '安装失败',
276
282
  errorTitleUninstall: '卸载失败',
283
+ // 更新失败(通知中心区分安装/更新):更新目标覆盖重装失败,与首次安装失败不同语义
284
+ errorTitleUpdate: '更新失败',
277
285
  errorPlugin: '插件',
278
286
  errorClose: '知道了',
279
287
  errorCopy: '复制错误信息',
@@ -381,13 +389,14 @@ export const en = {
381
389
  gitInstallInvalid: 'Enter a GitHub repository address, e.g. owner/repo or https://github.com/owner/repo',
382
390
  dshCmdLabel: 'DeepSeek Harness command',
383
391
  dshCmdPlaceholder: 'Paste an official dsh plugin command',
384
- dshCmdExample: 'e.g. dsh plugin --profile web add github:owner/repo',
385
- dshCmdInvalid: 'Paste an official dsh plugin install command (--profile required), e.g. dsh plugin --profile web add github:owner/repo',
392
+ dshCmdInsertHubUpdate: 'Insert update command',
393
+ dshCmdExample: 'e.g. dsh plugin --profile web add github:owner/repo, or dsh plugin --profile web update dsh-plugin',
394
+ dshCmdInvalid: 'Paste an official dsh plugin command (--profile required, add / update), e.g. dsh plugin --profile web update dsh-plugin',
386
395
  installHelp: 'Help',
387
396
  helpModalTitle: 'Supported command formats',
388
397
  npmInstallHelp: 'lodash or @scope/pkg\tjust type the package name\nnpm install lodash\nnpm i lodash\npnpm add @scope/pkg\npnpm i @scope/pkg\nnpm install -g @scope/cli\tinstalls the CLI globally, not shown in Installed',
389
398
  gitInstallHelp: 'owner/repo\nhttps://github.com/owner/repo\ngithub:owner/repo\ngit+https://github.com/owner/repo.git\ngit@github.com:owner/repo.git',
390
- dshCmdHelp: 'dsh plugin --profile web add <target>\ndsh plugin --profile=web add <target>\n\t--profile is required and there is no -p shorthand; <target> can be an npm package or github:owner/repo',
399
+ dshCmdHelp: 'dsh plugin --profile web add <target>\tinstall\ndsh plugin --profile web update <target>\tupdate an installed target to the latest version\ndsh plugin --profile=web add <target>\ndsh plugin --profile=web update <target>\n\t--profile is required and there is no -p shorthand; <target> can be an npm package or github:owner/repo; uninstall from the Installed list',
391
400
  globalNpmHint: 'This installs CLI tools globally (npm install -g). They won\'t appear in Installed and no host restart is needed.',
392
401
  installCliBtn: 'Install',
393
402
  // Install channel disabled (security-trust toggle off → card disabled, guides to settings)
@@ -434,14 +443,15 @@ export const en = {
434
443
  settingsCheckOnStartDesc: 'Check installed plugins for new versions when the hub opens; you will be notified when updates are found',
435
444
  settingsAutoInstall: 'Auto-install updates',
436
445
  settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
437
- settingsMirror: 'npm registry mirror',
438
- settingsMirrorDesc: 'Route npm installs and lookups through a mirror for unstable networks; leave empty for the official registry',
446
+ settingsProxy: 'HTTP proxy',
447
+ settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
448
+ settingsMirror: 'npm mirror',
449
+ settingsMirrorDesc: 'Unset by default — follows local npm config; pick a domestic mirror if slow',
450
+ mirrorNone: 'Unset (follow local npm config)',
439
451
  mirrorOfficial: 'Official registry.npmjs.org',
440
452
  mirrorNpmmirror: 'Alibaba npmmirror',
441
453
  mirrorTencent: 'Tencent Cloud mirror',
442
- mirrorCustom: 'Custom mirror',
443
- settingsProxy: 'HTTP proxy',
444
- settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
454
+ mirrorTsinghua: 'Tsinghua TUNA mirror',
445
455
  settingsSecurity: 'Security & Trust',
446
456
  settingsSecurityDesc: 'Toggles for the three command-line install channels: npm packages, GitHub sources and DSH commands',
447
457
  settingsEnableNpm: 'Enable npm installs',
@@ -507,6 +517,8 @@ export const en = {
507
517
  settingsDiagFail: 'Unreachable',
508
518
  // System diagnostics channel list: one-click full check + one row per channel, click to re-probe
509
519
  diagNpm: 'npm registry',
520
+ // Diagnostics npm row: shown when no mirror is configured (probe still targets the official registry, the fallback)
521
+ diagNpmUnset: 'Unset (follow local npm config)',
510
522
  diagGithub: 'GitHub',
511
523
  diagCatalog: 'Catalog site',
512
524
  diagOk: 'OK',
@@ -557,6 +569,8 @@ export const en = {
557
569
  installNow: 'Install',
558
570
  installing: 'Installing…',
559
571
  installDone: 'Installed',
572
+ // Updated (notification center distinguishes install/update): update = in-place reinstall to latest
573
+ updateDone: 'Updated',
560
574
  installResultTitle: 'Plugin installed successfully',
561
575
  installResultDesc: '',
562
576
  // Update semantics: the "update" action re-runs add in place — copy differentiates install vs update
@@ -600,6 +614,8 @@ export const en = {
600
614
  errorTitle: 'Operation failed',
601
615
  errorTitleInstall: 'Install failed',
602
616
  errorTitleUninstall: 'Removal failed',
617
+ // Update failed (notification center distinguishes install/update): update target reinstall failed
618
+ errorTitleUpdate: 'Update failed',
603
619
  errorPlugin: 'Plugin',
604
620
  errorClose: 'Got it',
605
621
  errorCopy: 'Copy error',
@@ -18,6 +18,9 @@ export interface NotificationRecord {
18
18
  ok: boolean
19
19
  /** owner/repo(可能为空:请求层失败但拿不到仓库时) */
20
20
  repo: string
21
+ /** 实际动作(install/update/uninstall):成功/失败通知据此区分「安装成功」与「更新成功」。
22
+ * 缺省按 kind 兜底(历史记录可能没有该字段)。更新提醒(kind='update')不用此字段。 */
23
+ action?: 'install' | 'update' | 'uninstall'
21
24
  /** 失败时的完整错误日志;成功记录为空串 */
22
25
  message: string
23
26
  /** 更新提醒:目录里的新版本号(展示用;其余通知省略) */
@@ -33,11 +33,15 @@ function inline(s: string): string {
33
33
  return `\u0000${codeSpans.length - 1}\u0000`
34
34
  })
35
35
  // 图片 ![alt](url):仅放行 http(s),用于展示反馈群二维码等;limited by CSS
36
- out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) =>
37
- /^https?:\/\//i.test(String(url)) && !/[\u0000]/.test(String(url))
38
- ? `<img src="${String(url)}" alt="${String(alt)}" loading="lazy">`
39
- : m,
40
- )
36
+ // 文件名为 dsh-plugin-user-group-qr- 前缀的视为用户反馈群二维码,固定 250×250 不撑破弹窗。
37
+ out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) => {
38
+ const src = String(url)
39
+ if (!/^https?:\/\//i.test(src) || /[\u0000]/.test(src)) return m
40
+ if (/\/dsh-plugin-user-group-qr-[^/]*$/i.test(src)) {
41
+ return `<img src="${src}" alt="${String(alt)}" width="250" height="250" loading="lazy">`
42
+ }
43
+ return `<img src="${src}" alt="${String(alt)}" loading="lazy">`
44
+ })
41
45
  // 链接 [text](url):仅放行 http(s),新窗口打开
42
46
  out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, text, url) =>
43
47
  /^https?:\/\//i.test(String(url)) && !/[\u0000]/.test(String(url))
@@ -33,6 +33,15 @@ export function pluginSiteUrl(repo: string): string {
33
33
  return `${SITE_URL}plugins/${repo}`
34
34
  }
35
35
 
36
+ /** 根据实际执行的命令判定失败动作标签(安装/更新/卸载):
37
+ * 命令形如 `dsh plugin --profile web add|update|remove <target>`,
38
+ * 出现 remove/update 动词即对应归类,其余(add、全局 npm 命令、未知)一律算安装。 */
39
+ function actionLabelOf(command: string | undefined): string {
40
+ if (command && /\s+remove\s+/i.test(command)) return 'Remove'
41
+ if (command && /\s+update\s+/i.test(command)) return 'Update'
42
+ return 'Install'
43
+ }
44
+
36
45
  /**
37
46
  * 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识,正文只带
38
47
  * 核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
@@ -43,7 +52,9 @@ export function pluginSiteUrl(repo: string): string {
43
52
  * 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
44
53
  */
45
54
  export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
46
- const title = `[dsh-plugin.org | dsh-plugin-hub] Install/Remove failed: ${repo}`
55
+ // 标题按实际执行的动作区分(安装/更新/卸载),避免作者误读 —— 旧版固定写死
56
+ // 「Install/Remove」会让更新失败被当成删除失败。
57
+ const title = `[dsh-plugin.org | dsh-plugin-hub] ${actionLabelOf(command)} failed: ${repo}`
47
58
  // 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
48
59
  // [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
49
60
  // (package.json 声明的入口文件在发布物里缺失),作者照此补齐即可。
@@ -84,6 +84,31 @@
84
84
  border-color: var(--hub-brand-border-soft);
85
85
  }
86
86
 
87
+ /* 命令卡片标题旁的「一键插入更新命令」:右对齐小号描边按钮,弱化但可点,hover 提亮品牌色 */
88
+ .installInsertBtn {
89
+ flex-shrink: 0;
90
+ margin-left: auto;
91
+ font-family: inherit;
92
+ font-size: 11px;
93
+ line-height: 16px;
94
+ font-weight: 500;
95
+ color: var(--hub-text-secondary);
96
+ background: transparent;
97
+ border: 1px solid var(--hub-border-2);
98
+ border-radius: 6px;
99
+ padding: 2px 9px;
100
+ cursor: pointer;
101
+ white-space: nowrap;
102
+ user-select: none;
103
+ transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
104
+ }
105
+
106
+ .installInsertBtn:hover {
107
+ color: var(--hub-brand);
108
+ border-color: var(--hub-brand-border-soft);
109
+ background: var(--hub-brand-tint);
110
+ }
111
+
87
112
  /* 文本框上方的示例:灰字小字,告诉用户该通道的推荐写法 */
88
113
  .installExample {
89
114
  font-size: 11px;
@@ -12,6 +12,16 @@
12
12
  flex-shrink: 0;
13
13
  }
14
14
 
15
+ /* 全宽变体:设置页「独占一行」的下拉与文本框同宽,按钮铺满整行 */
16
+ .dropdownFill {
17
+ width: 100%;
18
+ }
19
+
20
+ .dropdownFill > .dropdownBtn {
21
+ width: 100%;
22
+ justify-content: space-between;
23
+ }
24
+
15
25
  /* 触发器:工具栏按钮风格 —— 细边框 + 次级灰底,与通知入口按钮同一视觉语言,
16
26
  只占一个按钮位,给工具栏后续新增按钮留空间 */
17
27
  .dropdownBtn {
@@ -1355,6 +1355,17 @@
1355
1355
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
1356
1356
  }
1357
1357
 
1358
+ /* 用户反馈群二维码(renderMarkdown 对 dsh-plugin-user-group-qr- 前缀图片加了
1359
+ width/height="250"):固定 250×250 正方形。必须显式覆盖通用 max-height 上限 ——
1360
+ CSS max-height 对 height 有最终裁决权,不覆盖的话高度被压成 220 而宽度保持 250,
1361
+ 图片就会变成非正方形(看起来像「250×250 没生效」) */
1362
+ .aboutContent img[width="250"] {
1363
+ width: 250px;
1364
+ height: 250px;
1365
+ max-height: 250px;
1366
+ object-fit: contain;
1367
+ }
1368
+
1358
1369
  /* 更新时间小字:弱化展示在内容区下方 */
1359
1370
  .aboutMeta {
1360
1371
  font-size: 11px;
@@ -184,12 +184,12 @@ function hostEnv(profile: string): Record<string, string | null> {
184
184
  }
185
185
  }
186
186
 
187
- /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 npm 镜像源与 HTTP(S) 代理。
187
+ /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
188
+ * npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
188
189
  * 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
189
190
  * 使安装通道与诊断使用相同的代理来源。 */
190
191
  function mutationEnv(settings: HubSettings): NodeJS.ProcessEnv {
191
192
  const env: NodeJS.ProcessEnv = { ...process.env, CI: 'true' }
192
- if (settings.npmRegistry !== '') env.npm_config_registry = settings.npmRegistry
193
193
  const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '')
194
194
  if (proxy !== '') {
195
195
  env.HTTP_PROXY = proxy
@@ -488,11 +488,14 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
488
488
  // 空 body / 非 JSON:视为全量探测
489
489
  }
490
490
  const settings = loadSettings(profile)
491
+ // npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
492
+ // 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
491
493
  const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org'
492
494
  // 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
493
495
  // 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
496
+ // npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
494
497
  const allChecks: Array<{ key: string; url: string; display: string; cmd: string }> = [
495
- { key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? settings.npmRegistry : 'registry.npmjs.org', cmd: `npm view dsh-plugin version --registry ${registry}` },
498
+ { key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
496
499
  { key: 'github', url: 'https://github.com/dshplugin/dsh-plugin-hub', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/dsh-plugin-hub' },
497
500
  { key: 'catalog', url: 'https://api.dsh-plugin.org/stats.json', display: 'api.dsh-plugin.org', cmd: 'curl -s https://api.dsh-plugin.org/stats.json' },
498
501
  ]
@@ -725,7 +728,9 @@ export function mountPluginHubRoutes(webServer: WebServerService, profile: strin
725
728
  }
726
729
  // Kick off the CLI in the background; the client polls /status for progress
727
730
  const task = startPluginMutation({
728
- action: 'add',
731
+ // mode=update(已安装目标的覆盖更新)以 update 动作入队:
732
+ // 日志按 update 类别落盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
733
+ action: mode === 'update' ? 'update' : 'add',
729
734
  profile,
730
735
  target,
731
736
  // npm 安装时 target 是包名:displayTarget 固定展示仓库名,待重启行/前端恢复不受通道变化影响
@@ -27,7 +27,7 @@ export interface InstallTask {
27
27
  attempts: string[]
28
28
  /** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
29
29
  globalNpm?: string[]
30
- action: 'add' | 'remove'
30
+ action: 'add' | 'remove' | 'update'
31
31
  /** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
32
32
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
33
33
  timedOut: boolean
@@ -51,7 +51,7 @@ export interface Invocation {
51
51
  /** 排队中的变更任务及其启动参数。 */
52
52
  export interface QueueItem {
53
53
  task: InstallTask
54
- options: { action: 'add' | 'remove'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
54
+ options: { action: 'add' | 'remove' | 'update'; profile: string; target: string; timeoutMs?: number; env?: NodeJS.ProcessEnv; globalNpm?: string[] }
55
55
  }
56
56
 
57
57
  /**
@@ -23,7 +23,9 @@ const REQUEST_TIMEOUT_MS = 8000
23
23
  * 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
24
24
  * 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
25
25
  * 原有错误路径,反查只是额外的一次尝试。
26
- * @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
26
+ * registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
27
+ * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
28
+ * 慢网络下失败不阻塞安装。
27
29
  */
28
30
  export function resolveNpmPackage(repo: string, registry = ''): Promise<string | null> {
29
31
  const key = repo.toLowerCase()
@@ -38,7 +38,7 @@ export function getTask(id: number): InstallTask | undefined {
38
38
  export interface ActiveTaskInfo {
39
39
  id: number
40
40
  target: string
41
- action: 'add' | 'remove'
41
+ action: 'add' | 'remove' | 'update'
42
42
  status: 'pending' | 'running'
43
43
  progress: number
44
44
  lines: string[]
@@ -158,13 +158,23 @@ function stopChild(child: ReturnType<typeof spawn>): void {
158
158
  child.kill('SIGKILL')
159
159
  }
160
160
 
161
+ /** 系统日志类别:安装/卸载/更新三分,针对性地判断「哪一步不兼容」。 */
162
+ function mutationCategory(action: 'add' | 'remove' | 'update'): 'install' | 'uninstall' | 'update' {
163
+ return action === 'remove' ? 'uninstall' : action === 'update' ? 'update' : 'install'
164
+ }
165
+
166
+ /** 系统日志动作词:开始安装/开始卸载/开始更新。 */
167
+ function mutationVerb(action: 'add' | 'remove' | 'update'): string {
168
+ return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装'
169
+ }
170
+
161
171
  /**
162
172
  * Enqueue a plugin mutation and return its task. Tasks run strictly serially:
163
173
  * the queue worker starts the next one only after the previous finishes.
164
174
  * Progress is visible through `getTask(id)` / `activeTask()` until done.
165
175
  */
166
176
  export function startPluginMutation(options: {
167
- action: 'add' | 'remove'
177
+ action: 'add' | 'remove' | 'update'
168
178
  profile: string
169
179
  target: string
170
180
  timeoutMs?: number
@@ -193,14 +203,14 @@ export function startPluginMutation(options: {
193
203
  })
194
204
  }
195
205
  queue.push({ task, options: { ...options } })
196
- // 系统日志:记录任务入队(安装/卸载开始)
197
- const category = options.action === 'add' ? 'install' : 'uninstall'
206
+ // 系统日志:记录任务入队(安装/卸载/更新开始)
207
+ const category = mutationCategory(options.action)
198
208
  appendLog(options.profile, {
199
209
  at: Date.now(),
200
210
  level: 'info',
201
211
  category,
202
212
  event: `${category}.start`,
203
- message: `${options.action === 'add' ? '开始安装' : '开始卸载'} ${options.displayTarget ?? options.target}`,
213
+ message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
204
214
  })
205
215
  pumpQueue()
206
216
  return task
@@ -215,16 +225,17 @@ function pumpQueue(): void {
215
225
  task.status = 'running'
216
226
  void runPluginMutation({ ...options, task }).finally(() => {
217
227
  runningChildren.delete(task.id)
218
- // 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载日志
228
+ // 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载/更新日志
219
229
  const show = task.displayTarget ?? task.target
220
- const category = task.action === 'add' ? 'install' : 'uninstall'
230
+ const category = mutationCategory(task.action)
231
+ const verb = (v: '成功' | '失败') => task.action === 'remove' ? `卸载${v}` : task.action === 'update' ? `更新${v}` : `安装${v}`
221
232
  if (task.status === 'done') {
222
233
  appendLog(options.profile, {
223
234
  at: Date.now(),
224
235
  level: 'success',
225
236
  category,
226
237
  event: `${category}.done`,
227
- message: `${task.action === 'add' ? '安装成功' : '卸载成功'} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
238
+ message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
228
239
  })
229
240
  } else if (task.status === 'failed') {
230
241
  appendLog(options.profile, {
@@ -232,7 +243,7 @@ function pumpQueue(): void {
232
243
  level: 'error',
233
244
  category,
234
245
  event: `${category}.fail`,
235
- message: `${task.action === 'add' ? '安装失败' : '卸载失败'} ${show}`,
246
+ message: `${verb('失败')} ${show}`,
236
247
  })
237
248
  } else if (task.status === 'cancelled') {
238
249
  appendLog(options.profile, {
@@ -256,7 +267,7 @@ function pumpQueue(): void {
256
267
  * Never rejects; failures surface through the result / task state.
257
268
  */
258
269
  function spawnMutation(options: {
259
- action: 'add' | 'remove'
270
+ action: 'add' | 'remove' | 'update'
260
271
  profile: string
261
272
  target: string
262
273
  timeoutMs?: number
@@ -280,17 +291,21 @@ function spawnMutation(options: {
280
291
  // 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
281
292
  // (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
282
293
  // 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
283
- const npmUpdateTarget = updateNpm && !isGlobalNpm && githubRepoOf(target) === null
294
+ // update 动作实际执行 `add <target>@latest`(pnpm 对已存在依赖带显式版本即原位覆盖升级),
295
+ // CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
296
+ const isUpdate = action === 'update' || updateNpm === true
297
+ const cliAction = action === 'update' ? 'add' : action
298
+ const npmUpdateTarget = isUpdate && !isGlobalNpm && githubRepoOf(target) === null
284
299
  ? `${target}@latest`
285
300
  : target
286
301
  const args = isGlobalNpm
287
302
  ? ['install', '-g', ...(globalNpm ?? [])]
288
- : [...invocation.prefixArgs, 'plugin', '--profile', profile, action, npmUpdateTarget]
303
+ : [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget]
289
304
  // 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
290
305
  // 失败提 Issue 时作为「已尝试的安装方式」随附
291
306
  const executed = isGlobalNpm
292
307
  ? `npm install -g ${(globalNpm ?? []).join(' ')}`
293
- : `dsh plugin --profile ${profile} ${action} ${npmUpdateTarget}`
308
+ : `dsh plugin --profile ${profile} ${cliAction} ${npmUpdateTarget}`
294
309
  if (task && !task.attempts.includes(executed)) {
295
310
  task.attempts.push(executed)
296
311
  }
@@ -443,10 +458,10 @@ function spawnMutation(options: {
443
458
  if (removed) clearPendingRestart(displayTarget ?? target)
444
459
  else addPendingRestart(displayTarget ?? target, 'uninstall')
445
460
  } else {
446
- // 安装成功(或卸载但无 loader 引用)→ 登记待重启:
461
+ // 安装/更新成功(或卸载但无 loader 引用)→ 登记待重启:
447
462
  // 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
448
463
  task.needsRestart = true
449
- addPendingRestart(displayTarget ?? target, action === 'add' ? 'install' : 'uninstall')
464
+ addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install')
450
465
  }
451
466
  }
452
467
  pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]`)
@@ -513,11 +528,11 @@ function ignoredBuildKeys(output: string): string[] {
513
528
  * 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
514
529
  * 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
515
530
  function annotateNpmTooLow(result: InstallResult, options: {
516
- action: 'add' | 'remove'
531
+ action: 'add' | 'remove' | 'update'
517
532
  task?: InstallTask
518
533
  env?: NodeJS.ProcessEnv
519
534
  }): InstallResult {
520
- if (options.action !== 'add' || result.exitCode === 0 || result.timedOut) return result
535
+ if (options.action === 'remove' || result.exitCode === 0 || result.timedOut) return result
521
536
  const line = npmTooLowMarker(`${result.stderr}\n${result.stdout}`, options.env)
522
537
  if (line === null) return result
523
538
  if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
@@ -534,7 +549,7 @@ function annotateNpmTooLow(result: InstallResult, options: {
534
549
  * Other failures fall through to the single-spawn result.
535
550
  */
536
551
  export async function runPluginMutation(options: {
537
- action: 'add' | 'remove'
552
+ action: 'add' | 'remove' | 'update'
538
553
  profile: string
539
554
  target: string
540
555
  timeoutMs?: number
@@ -550,7 +565,8 @@ export async function runPluginMutation(options: {
550
565
  let result = await spawnMutation(options)
551
566
  // allowBuilds 放行 / 装后入口校验都是 profile 专属(pnpm 装进 profile 的场景);
552
567
  // 全局 npm 安装直接走 npm,无 pnpm allowBuilds 拦截、也不进 profile,跳过这两类恢复
553
- if (options.action === 'add' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
568
+ // 安装与更新(同为 add 通道执行)共享同一套恢复路径;卸载不适用
569
+ if (options.action !== 'remove' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
554
570
  const output = `${result.stderr}\n${result.stdout}`
555
571
  // git 源:pnpm 打印的 key@url 提示
556
572
  if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
@@ -588,7 +604,7 @@ export async function runPluginMutation(options: {
588
604
  // pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
589
605
  // 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
590
606
  // 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
591
- if (options.action === 'add' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
607
+ if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
592
608
  const verified = verifyInstalledEntry(options.profile, options.target)
593
609
  if (verified.name !== null && verified.missing !== null) {
594
610
  clearPendingRestart(options.displayTarget ?? options.target)
@@ -30,17 +30,18 @@ export function githubTarget(repo: string): string | null {
30
30
  }
31
31
 
32
32
  /**
33
- * DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> add <target>`)—— 提取其中的安装目标;
33
+ * DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的安装目标;
34
34
  * 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
35
35
  * (官方 CLI 无 -p 简写,省略即报错),支持 `--profile=<name>` 等号形式,命令大小写不敏感。
36
+ * update = 更新已安装目标到最新;remove 不在此列(卸载走 /uninstall 路由,按已装包名直卸)。
36
37
  * 与客户端 CustomInstallView 口径一致。
37
38
  */
38
- const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+add\s+(.+)$/i
39
+ const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i
39
40
 
40
41
  export function installTargetOf(value: string): string {
41
42
  const input = typeof value === 'string' ? value.trim() : ''
42
43
  const match = DSH_PLUGIN_CMD_RE.exec(input)
43
- return match !== null ? match[1].trim() : input
44
+ return match !== null ? match[2].trim() : input
44
45
  }
45
46
 
46
47
  /**