dsh-plugin 1.3.5 → 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.
@@ -157,13 +157,12 @@ function hostEnv(profile) {
157
157
  dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
158
158
  };
159
159
  }
160
- /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 npm 镜像源与 HTTP(S) 代理。
160
+ /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
161
+ * npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
161
162
  * 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
162
163
  * 使安装通道与诊断使用相同的代理来源。 */
163
164
  function mutationEnv(settings) {
164
165
  const env = { ...process.env, CI: 'true' };
165
- if (settings.npmRegistry !== '')
166
- env.npm_config_registry = settings.npmRegistry;
167
166
  const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '');
168
167
  if (proxy !== '') {
169
168
  env.HTTP_PROXY = proxy;
@@ -485,11 +484,14 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
485
484
  // 空 body / 非 JSON:视为全量探测
486
485
  }
487
486
  const settings = loadSettings(profile);
487
+ // npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
488
+ // 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
488
489
  const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org';
489
490
  // 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
490
491
  // 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
492
+ // npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
491
493
  const allChecks = [
492
- { key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? settings.npmRegistry : 'registry.npmjs.org', cmd: `npm view dsh-plugin version --registry ${registry}` },
494
+ { key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
493
495
  { key: 'github', url: 'https://github.com/dshplugin/dsh-plugin-hub', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/dsh-plugin-hub' },
494
496
  { 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' },
495
497
  ];
@@ -20,7 +20,9 @@ const REQUEST_TIMEOUT_MS = 8000;
20
20
  * 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
21
21
  * 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
22
22
  * 原有错误路径,反查只是额外的一次尝试。
23
- * @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
23
+ * registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
24
+ * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
25
+ * 慢网络下失败不阻塞安装。
24
26
  */
25
27
  export function resolveNpmPackage(repo, registry = '') {
26
28
  const key = repo.toLowerCase();
@@ -12,8 +12,8 @@ import { dirname, join } from 'node:path';
12
12
  import { profileDirectory } from './profile/profile.js';
13
13
  export const DEFAULT_SETTINGS = {
14
14
  checkUpdatesOnStart: true,
15
- npmRegistry: '',
16
15
  proxy: '',
16
+ npmRegistry: '',
17
17
  enableNpmInstall: true,
18
18
  enableGitInstall: true,
19
19
  enableDshInstall: true,
@@ -2,6 +2,8 @@
2
2
  * 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
3
3
  * 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
4
4
  * 原有错误路径,反查只是额外的一次尝试。
5
- * @param registry - npm 镜像源地址(空串 = 官方 https://registry.npmjs.org)
5
+ * registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
6
+ * 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
7
+ * 慢网络下失败不阻塞安装。
6
8
  */
7
9
  export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
@@ -1,10 +1,10 @@
1
1
  export interface HubSettings {
2
2
  /** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
3
3
  checkUpdatesOnStart: boolean;
4
- /** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
5
- npmRegistry: string;
6
4
  /** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
7
5
  proxy: string;
6
+ /** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置,本插件不注入任何 registry) */
7
+ npmRegistry: string;
8
8
  /** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
9
9
  enableNpmInstall: boolean;
10
10
  /** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
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",
@@ -107,12 +107,13 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
107
107
  const queue = useTaskQueue({
108
108
  t,
109
109
  refreshInstalled: catalog.refreshInstalled,
110
- onInstallDone: (viaModal, repo, needsRestart) => {
110
+ onInstallDone: (viaModal, repo, needsRestart, update) => {
111
111
  setInstallNeedsRestart(needsRestart)
112
112
  if (viaModal) setInstallDone(true)
113
113
  else setToast({ id: Date.now(), kind: 'done' })
114
- // 安装成功也写入通知记录:通知中心里成功与失败都能看到
115
- setNotifications(addSuccess({ kind: 'install', repo: repo ?? '' }))
114
+ // 安装成功也写入通知记录:通知中心里成功与失败都能看到。
115
+ // action 区分「安装成功」与「更新成功」(更新 = 覆盖重装,同一执行通道)
116
+ setNotifications(addSuccess({ kind: 'install', action: update ? 'update' : 'install', repo: repo ?? '' }))
116
117
  },
117
118
  onUninstallDone: (viaModal, repo, needsRestart) => {
118
119
  setUninstallNeedsRestart(needsRestart)
@@ -120,13 +121,16 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
120
121
  else setToast({ id: Date.now(), kind: 'removed' })
121
122
  setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
122
123
  },
123
- onError: (message, repo, kind, command, attempts) => {
124
+ onError: (message, repo, kind, command, attempts, update) => {
124
125
  // 提 Issue 目标先反查仓库身份:npm 包名能在已安装表/目录里反查到 owner/repo 才保留,
125
- // 反查不到置 null —— 错误弹窗/通知中心据此不显示「一键提 Issue」按钮(防提到错误仓库)
126
+ // 反查不到置 null —— 错误弹窗据此不显示「一键提 Issue」按钮(防提到错误仓库)
126
127
  const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
127
128
  setErrorMsg({ message, repo: issueRepo, kind, command, attempts })
128
- // 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕
129
- setNotifications(addFailure({ kind, repo: issueRepo ?? '', message, command, attempts }))
129
+ // 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
130
+ // 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
131
+ // 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
132
+ // 若这里就丢掉原始值,通知里会连名字都看不到;action 区分「安装失败/更新失败」
133
+ setNotifications(addFailure({ kind, action: update ? 'update' : 'install', repo: repo ?? '', message, command, attempts }))
130
134
  },
131
135
  installPlugin: confirmPlugin,
132
136
  installCustomTarget: confirmCustomTarget,
@@ -193,8 +193,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
193
193
  h('span', {
194
194
  className: r.ok ? styles.noticeTextOk : styles.noticeTextFail,
195
195
  }, r.ok
196
- ? (isUpdate ? t('updateNoticeTitle') : (r.kind === 'install' ? t('installDone') : t('uninstallDone')))
197
- : (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall'))),
196
+ ? (isUpdate
197
+ ? t('updateNoticeTitle')
198
+ : (r.action === 'update' ? t('updateDone') : (r.kind === 'install' ? t('installDone') : t('uninstallDone'))))
199
+ : (r.action === 'update'
200
+ ? t('errorTitleUpdate')
201
+ : (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall')))),
198
202
  // 更新提醒:新版本号紧随标题展示;repo 为纯文本(整行点击即去更新,不跳官网外链)
199
203
  isUpdate && r.version
200
204
  ? h('span', { className: styles.noticeVersion, title: r.version }, `v${r.version}`)
@@ -211,7 +215,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
211
215
  rel: 'noopener noreferrer',
212
216
  title: shownRepo,
213
217
  }, shownRepo)
214
- : null)
218
+ // 反查不到仓库身份(如命令行安装的裸 npm 包名):回退显示记录里的原始值,
219
+ // 保证通知永不只剩一句「安装/更新成功」没有名字 —— 只是不给官网外链(查不到不敢乱跳)
220
+ : h('span', { className: styles.failRepo, title: r.repo }, r.repo))
215
221
  : null,
216
222
  !r.ok && h('button', {
217
223
  className: styles.failCopy,
@@ -40,7 +40,8 @@ interface RowState {
40
40
  }
41
41
 
42
42
  const INITIAL_ROWS: RowState[] = [
43
- { key: 'npm', display: 'registry.npmjs.org', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
43
+ // npm 行 display 初始置空:设置里未配置镜像时显示「未配置(跟随本机)」,探测结果回来再用服务端 display 覆盖
44
+ { key: 'npm', display: '', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
44
45
  { key: 'github', display: 'github.com', nameKey: 'diagGithub', status: 'idle', ms: null, statusCode: null },
45
46
  { key: 'catalog', display: 'api.dsh-plugin.org', nameKey: 'diagCatalog', status: 'idle', ms: null, statusCode: null },
46
47
  ]
@@ -87,12 +88,14 @@ export function DiagnosticsView({ t, env, onCopy }: {
87
88
  const raw = buf.slice(0, nl).trim()
88
89
  buf = buf.slice(nl + 1)
89
90
  if (raw !== '') {
90
- const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null }
91
+ const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null; display?: string }
91
92
  if (ev.type === 'ok' || ev.type === 'fail') {
92
93
  patchRows(ev.key as ProbeKey, {
93
94
  status: ev.type === 'ok' ? 'ok' : 'fail',
94
95
  ms: typeof ev.ms === 'number' ? ev.ms : null,
95
96
  statusCode: typeof ev.status === 'number' ? ev.status : null,
97
+ // 服务端带出实际使用的源地址(配置镜像 → 镜像地址;未配置 → 空串)
98
+ display: typeof ev.display === 'string' ? ev.display : '',
96
99
  })
97
100
  }
98
101
  }
@@ -159,7 +162,8 @@ export function DiagnosticsView({ t, env, onCopy }: {
159
162
  title: t('diagRecheck'),
160
163
  },
161
164
  h('span', { className: styles.name }, t(r.nameKey)),
162
- h('span', { className: styles.display }, r.display),
165
+ // npm 行未配置镜像:display 为空 → 显示「未配置(跟随本机)」,不误导成官方源
166
+ h('span', { className: styles.display }, r.display !== '' ? r.display : t('diagNpmUnset')),
163
167
  h('span', { className: styles.meta }, r.status === 'ok' && r.ms !== null ? `${r.ms} ms` : ''),
164
168
  badge,
165
169
  )
@@ -18,9 +18,10 @@
18
18
  import { createElement as h, useState } from 'react'
19
19
  import type { ChangeEvent, MouseEvent, ReactNode } from 'react'
20
20
  import styles from '../../styles/SettingsView.module.css'
21
+ import dropdownStyles from '../../styles/Dropdown.module.css'
21
22
  import modalStyles from '../../styles/Modal.module.css'
22
- import { Dropdown } from '../ui/Dropdown.tsx'
23
23
  import { Toggle } from '../ui/Toggle.tsx'
24
+ import { Dropdown } from '../ui/Dropdown.tsx'
24
25
  import { DiagnosticsView } from './DiagnosticsView.tsx'
25
26
  import { LogsView } from './LogsView.tsx'
26
27
  import {
@@ -31,6 +32,16 @@ import type { HubSettings } from '../../hooks/useSettings.ts'
31
32
 
32
33
  type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
33
34
 
35
+ /** npm 镜像源预设:空串 = 未配置(跟随用户本机 npm 配置,本插件不注入任何 registry)。
36
+ * 只提供常见国内顶级镜像下拉,不做自定义输入 —— 想用其它镜像直接在 ~/.npmrc 配置即可。 */
37
+ const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
38
+ { value: '', labelKey: 'mirrorNone' },
39
+ { value: 'https://registry.npmjs.org', labelKey: 'mirrorOfficial' },
40
+ { value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
41
+ { value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
42
+ { value: 'https://mirrors.tuna.tsinghua.edu.cn/npm/', labelKey: 'mirrorTsinghua' },
43
+ ]
44
+
34
45
  /** 左侧导航:顺序即展示顺序;labelKey 是分组标题,icon 是行内小图标。 */
35
46
  const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnType<typeof h> }> = [
36
47
  { key: 'updates', labelKey: 'settingsUpdate', icon: UpdatesIcon },
@@ -40,13 +51,6 @@ const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnTyp
40
51
  { key: 'reset', labelKey: 'settingsReset', icon: ResetIcon },
41
52
  ]
42
53
 
43
- /** npm 镜像源预设:空串 = 官方源;值相等即视为选中对应预设,其余进入「自定义」输入。 */
44
- const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
45
- { value: '', labelKey: 'mirrorOfficial' },
46
- { value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
47
- { value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
48
- ]
49
-
50
54
  /** 设置行:默认标签左控件右;stack=true 时控件独占一行(文本框整行显示),hairline 分隔。 */
51
55
  function SettingRow({ title, desc, children, stack = false }: {
52
56
  title: string
@@ -76,13 +80,8 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
76
80
  /** 恢复默认确认弹窗:点按钮先弹窗询问,确认后才真正 reset(不做两段式按钮) */
77
81
  const [resetConfirm, setResetConfirm] = useState(false)
78
82
 
79
- const mirrorCustom = !MIRROR_PRESETS.some((p) => p.value === settings.npmRegistry)
80
- const onMirrorChange = (value: string) => {
81
- if (value === 'custom') return // 进入自定义输入:保持当前值,下方出现输入框
82
- update({ npmRegistry: value })
83
- }
84
- const onMirrorInput = (e: ChangeEvent<HTMLInputElement>) => update({ npmRegistry: e.target.value })
85
83
  const onProxyChange = (e: ChangeEvent<HTMLInputElement>) => update({ proxy: e.target.value })
84
+ const onMirrorChange = (value: string) => update({ npmRegistry: value })
86
85
 
87
86
  /** 每个分组的页面副标题(导航标题之上再补一句说明,保持桌面设置质感) */
88
87
  const pageDesc: Record<SettingsSection, string> = {
@@ -106,31 +105,19 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
106
105
  title: t('settingsCheckOnStart'),
107
106
  }),
108
107
  }),
108
+ // npm 镜像源:预设下拉(默认未配置跟随本机),选国内顶级镜像可加速安装;
109
+ // 不做自定义输入,其它镜像写在 ~/.npmrc 即可被天然尊重。
110
+ // 独占一行(同代理文本框):标签在上、下拉在下整行展开,长描述不会挤压控件
109
111
  h(SettingRow, {
110
112
  title: t('settingsMirror'),
111
113
  desc: t('settingsMirrorDesc'),
114
+ stack: true,
112
115
  children: h(Dropdown, {
113
- value: mirrorCustom ? 'custom' : settings.npmRegistry,
114
- options: [
115
- ...MIRROR_PRESETS.map((p) => ({ value: p.value, label: t(p.labelKey) })),
116
- { value: 'custom', label: t('mirrorCustom') },
117
- ],
116
+ value: settings.npmRegistry,
117
+ options: MIRROR_PRESETS.map((m) => ({ value: m.value, label: t(m.labelKey) })),
118
118
  onChange: onMirrorChange,
119
119
  title: t('settingsMirror'),
120
- className: styles.controlDropdown,
121
- }),
122
- }),
123
- // 自定义镜像源:下拉选中「自定义」或已存非预设值时,追加一行输入框(文本框独占一行)
124
- mirrorCustom && h(SettingRow, {
125
- title: t('mirrorCustom'),
126
- stack: true,
127
- children: h('input', {
128
- className: styles.textInput,
129
- type: 'url',
130
- value: settings.npmRegistry,
131
- placeholder: 'https://registry.npmjs.org',
132
- spellCheck: false,
133
- onChange: onMirrorInput,
120
+ className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
134
121
  }),
135
122
  }),
136
123
  // 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址
@@ -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
  }])
@@ -108,14 +108,15 @@ export const zh = {
108
108
  settingsUpdateDesc: '更新检查方式与安装走到的网络通道',
109
109
  settingsCheckOnStart: '启动时检查更新',
110
110
  settingsCheckOnStartDesc: '打开插件中心时检查已安装插件是否有新版,发现更新会提示',
111
+ settingsProxy: 'HTTP 代理',
112
+ settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
111
113
  settingsMirror: 'npm 镜像源',
112
- settingsMirrorDesc: 'npm 安装与版本反查走自定义镜像,网络不佳时更稳定;留空使用官方源',
114
+ settingsMirrorDesc: '默认跟随本机 npm 配置;官方源慢可换国内镜像',
115
+ mirrorNone: '未配置(跟随本机 npm 配置)',
113
116
  mirrorOfficial: '官方源 registry.npmjs.org',
114
117
  mirrorNpmmirror: '阿里云 npmmirror',
115
118
  mirrorTencent: '腾讯云镜像',
116
- mirrorCustom: '自定义镜像源',
117
- settingsProxy: 'HTTP 代理',
118
- settingsProxyDesc: 'npm / git / 目录数据请求统一走该代理;留空直连',
119
+ mirrorTsinghua: '清华大学 TUNA 镜像',
119
120
  settingsSecurity: '安全信任',
120
121
  settingsSecurityDesc: '命令行安装的三个通道开关:NPM 包、GitHub 源码与 DSH 命令',
121
122
  settingsEnableNpm: '启用 NPM 安装',
@@ -181,6 +182,8 @@ export const zh = {
181
182
  settingsDiagFail: '不可达',
182
183
  // 系统诊断通道列表:一键全量诊断 + 一行一个,可点重测
183
184
  diagNpm: 'npm 源',
185
+ // 诊断页 npm 行:未配置镜像时的展示(探测目标仍为官方源,代表本机无配置时的回退)
186
+ diagNpmUnset: '未配置(跟随本机 npm 配置)',
184
187
  diagGithub: 'GitHub',
185
188
  diagCatalog: '目录站点',
186
189
  diagOk: 'OK',
@@ -231,6 +234,8 @@ export const zh = {
231
234
  installNow: '直接安装',
232
235
  installing: '安装中…',
233
236
  installDone: '安装成功',
237
+ // 更新成功(通知中心区分安装/更新):更新 = 已安装目标覆盖重装到最新,与首次安装不同语义
238
+ updateDone: '更新成功',
234
239
  installResultTitle: '插件安装成功',
235
240
  installResultDesc: '',
236
241
  // 更新语义:已安装插件点「更新」走同一条 add 命令原位覆盖重装,弹窗文案区分安装/更新
@@ -275,6 +280,8 @@ export const zh = {
275
280
  errorTitle: '操作失败',
276
281
  errorTitleInstall: '安装失败',
277
282
  errorTitleUninstall: '卸载失败',
283
+ // 更新失败(通知中心区分安装/更新):更新目标覆盖重装失败,与首次安装失败不同语义
284
+ errorTitleUpdate: '更新失败',
278
285
  errorPlugin: '插件',
279
286
  errorClose: '知道了',
280
287
  errorCopy: '复制错误信息',
@@ -436,14 +443,15 @@ export const en = {
436
443
  settingsCheckOnStartDesc: 'Check installed plugins for new versions when the hub opens; you will be notified when updates are found',
437
444
  settingsAutoInstall: 'Auto-install updates',
438
445
  settingsAutoInstallDesc: 'Install detected updates in the background without asking (a host restart applies them)',
439
- settingsMirror: 'npm registry mirror',
440
- 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)',
441
451
  mirrorOfficial: 'Official registry.npmjs.org',
442
452
  mirrorNpmmirror: 'Alibaba npmmirror',
443
453
  mirrorTencent: 'Tencent Cloud mirror',
444
- mirrorCustom: 'Custom mirror',
445
- settingsProxy: 'HTTP proxy',
446
- settingsProxyDesc: 'Route npm, git and catalog requests through this proxy; leave empty to connect directly',
454
+ mirrorTsinghua: 'Tsinghua TUNA mirror',
447
455
  settingsSecurity: 'Security & Trust',
448
456
  settingsSecurityDesc: 'Toggles for the three command-line install channels: npm packages, GitHub sources and DSH commands',
449
457
  settingsEnableNpm: 'Enable npm installs',
@@ -509,6 +517,8 @@ export const en = {
509
517
  settingsDiagFail: 'Unreachable',
510
518
  // System diagnostics channel list: one-click full check + one row per channel, click to re-probe
511
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)',
512
522
  diagGithub: 'GitHub',
513
523
  diagCatalog: 'Catalog site',
514
524
  diagOk: 'OK',
@@ -559,6 +569,8 @@ export const en = {
559
569
  installNow: 'Install',
560
570
  installing: 'Installing…',
561
571
  installDone: 'Installed',
572
+ // Updated (notification center distinguishes install/update): update = in-place reinstall to latest
573
+ updateDone: 'Updated',
562
574
  installResultTitle: 'Plugin installed successfully',
563
575
  installResultDesc: '',
564
576
  // Update semantics: the "update" action re-runs add in place — copy differentiates install vs update
@@ -602,6 +614,8 @@ export const en = {
602
614
  errorTitle: 'Operation failed',
603
615
  errorTitleInstall: 'Install failed',
604
616
  errorTitleUninstall: 'Removal failed',
617
+ // Update failed (notification center distinguishes install/update): update target reinstall failed
618
+ errorTitleUpdate: 'Update failed',
605
619
  errorPlugin: 'Plugin',
606
620
  errorClose: 'Got it',
607
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,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 声明的入口文件在发布物里缺失),作者照此补齐即可。
@@ -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
  ]
@@ -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()
@@ -14,10 +14,10 @@ import { profileDirectory } from './profile/profile.ts'
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 包(默认开;关掉后输入 npm 包名安装被拦截) */
22
22
  enableNpmInstall: boolean
23
23
  /** 启用命令行安装 GitHub 源码(默认开;关掉后输入 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,