dsh-plugin 1.4.9 → 1.4.11

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 (33) hide show
  1. package/client/client.js +355 -365
  2. package/client/client.js.map +1 -1
  3. package/lib/http/routes.js +45 -19
  4. package/lib/services/install/npm-check.js +4 -5
  5. package/lib/services/install/task-queue.js +51 -13
  6. package/lib/services/probe.js +20 -7
  7. package/lib/services/profile/profile.js +1 -1
  8. package/lib/types/http/routes.d.ts +3 -0
  9. package/lib/types/services/install/install-types.d.ts +2 -0
  10. package/lib/types/services/install/npm-check.d.ts +1 -1
  11. package/lib/types/services/install/task-queue.d.ts +7 -0
  12. package/lib/types/services/probe.d.ts +2 -2
  13. package/lib/types/services/profile/profile.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/client/components/PluginHubSection.tsx +1 -1
  16. package/src/client/components/catalog/PluginCard.tsx +2 -2
  17. package/src/client/components/modals/NotificationsModal.tsx +13 -9
  18. package/src/client/components/modals/modals.tsx +10 -6
  19. package/src/client/data/catalog.ts +1 -1
  20. package/src/client/hooks/useCatalog.ts +2 -2
  21. package/src/client/locales.ts +7 -0
  22. package/src/client/logic/constants.ts +1 -1
  23. package/src/client/logic/failures.ts +76 -95
  24. package/src/client/logic/urls.ts +5 -2
  25. package/src/client/styles/InstalledView.module.css +1 -1
  26. package/src/client/styles/Modal.module.css +2 -2
  27. package/src/client/types.ts +1 -1
  28. package/src/server/http/routes.ts +41 -19
  29. package/src/server/services/install/install-types.ts +2 -0
  30. package/src/server/services/install/npm-check.ts +4 -5
  31. package/src/server/services/install/task-queue.ts +49 -12
  32. package/src/server/services/probe.ts +20 -6
  33. package/src/server/services/profile/profile.ts +1 -1
@@ -22,7 +22,7 @@ import { loadSettings, saveSettings, resetSettings } from '../services/settings.
22
22
  import { appendLog, clearLog, readLog, logFilePath, defaultLogFilePath, customLogFile } from '../services/log.js';
23
23
  /**
24
24
  * 跨平台宿主重启脚本(以 `node -e` 运行,独立于宿主进程)。
25
- * 取代原先的 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
25
+ * 不用 `/bin/sh -c`:Windows 无 /bin/sh,且 lsof/nohup 仅 POSIX 存在。
26
26
  * 由 `spawn(process.execPath, ['-e', SCRIPT, port], { detached, stdio:'ignore' })` 孵化,
27
27
  * 宿主进程被 kill 后仍能完成「停旧 → 等端口释放 → 拉起新 dsh web」。
28
28
  */
@@ -137,8 +137,7 @@ function installedRepoOf(profile, name) {
137
137
  }
138
138
  }
139
139
  /** 宿主 dsh CLI 版本:优先直接跑 `dsh --version`(与 pnpm/npm/git 同款同步探测,1500ms 超时兜底,
140
- * 绝不挂起)—— 原先从 process.argv[1] 向上找 package.json 猜版本在部分宿主加载方式下取不到,
141
- * 导致 issue 环境快照 DSH: unknown;拿不到再退回入口 package.json 查找(dsh 不在 PATH 时兜底)。 */
140
+ * 绝不挂起);拿不到再退回从入口 `process.argv[1]` 向上找 package.json(dsh 不在 PATH 时兜底)。 */
142
141
  function hostDshVersion() {
143
142
  const viaCli = toolVersion('dsh', ['--version']);
144
143
  if (viaCli)
@@ -196,7 +195,7 @@ function hostEnv(profile) {
196
195
  };
197
196
  }
198
197
  /** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
199
- * npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
198
+ * 不注入 npm registry —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
200
199
  * 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
201
200
  * 使安装通道与诊断使用相同的代理来源。 */
202
201
  function mutationEnv(settings) {
@@ -229,14 +228,24 @@ function sendJson(response, status, value) {
229
228
  });
230
229
  response.end(JSON.stringify(value));
231
230
  }
232
- /** POST mutations are only accepted from the local web server origin. */
233
- function isSameOrigin(request) {
231
+ /** POST mutations are only accepted from the local web server origin or the desktop host page.
232
+ * Exported for tests (tests/routes.test.ts) — the origin check is the whole CSRF defence. */
233
+ export function isSameOrigin(request) {
234
234
  const origin = request.headers.origin;
235
- const host = request.headers.host;
236
- if (origin === undefined || host === undefined)
235
+ if (origin === undefined)
237
236
  return false;
238
237
  try {
239
238
  const url = new URL(origin);
239
+ // 桌面端(Electron 宿主)的页面跑在宿主自定义协议下,origin 形如 `dsh-app://app`。
240
+ // 客户端一律用相对路径请求,宿主把 `/dsh-plugin-hub/*` 转发给本地 http server 时
241
+ // Origin 头原样保留,与 Host(如 localhost:3081)不等,因此必须显式放行该协议,
242
+ // 否则桌面端全部 POST 都会被判成 untrusted origin。该协议只有宿主自身能产生,
243
+ // 外部网页无法把 Origin 伪造成 `dsh-app://…`,放行不扩大 CSRF 面。
244
+ if (url.protocol === 'dsh-app:')
245
+ return true;
246
+ const host = request.headers.host;
247
+ if (host === undefined)
248
+ return false;
240
249
  const localHostnames = new Set(['localhost', '127.0.0.1', '[::1]']);
241
250
  return url.host === host && localHostnames.has(url.hostname);
242
251
  }
@@ -638,7 +647,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
638
647
  handler: async (request, response) => {
639
648
  if (!requireMethod(request, response, 'GET'))
640
649
  return;
641
- // 目录/统计数据服务端代理:浏览器不再直连 dsh-plugin.org,改经此路由
650
+ // 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
642
651
  // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
643
652
  // 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
644
653
  const url = new URL(request.url ?? '/', 'http://localhost');
@@ -696,7 +705,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
696
705
  handler: async (request, response) => {
697
706
  if (!requireTrustedPost(request, response))
698
707
  return;
699
- // 任务自动入队:即使已有插件操作在跑也接受请求(FIFO 串行执行),不再 409 拒绝
708
+ // 任务自动入队:即使已有插件操作在跑也接受请求(FIFO 串行执行),不做 409 拒绝
700
709
  try {
701
710
  const body = await readJsonBody(request);
702
711
  // mode: 'update' = 已安装目标的覆盖更新(放行 add,pnpm 对已存在依赖原位覆盖重装)。
@@ -835,20 +844,37 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
835
844
  // 页 —— 404 表示「目标不存在」而非网络不通,不该被当成网络故障拦截。
836
845
  // 仅新安装预检(更新是已信任目标的覆盖重装,跳过);不通直接 400 拦下并打
837
846
  // [network] 标记,客户端据此提示「你的网络不通」,而不是把网络失败当成
838
- // 插件侧问题引导去作者仓库提 Issue(issue #10:git fetch 超时被误报为插件问题)。
847
+ // 插件侧问题引导去作者仓库提 Issue。
839
848
  // 代理口径与诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连。
840
849
  if (already === null) {
841
850
  const effectiveProxy = settings.proxy !== ''
842
851
  ? settings.proxy
843
852
  : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
844
853
  const isGitChannel = repoTarget !== null && target === repoTarget;
845
- const probeTarget = isGitChannel
846
- ? 'https://github.com/'
847
- : `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`;
848
- const net = await probeUrl(probeTarget, effectiveProxy, 6000);
854
+ const npmProbeTarget = `${(settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org')}/`;
855
+ // git 通道的探针换成克隆握手本身:git ls-remote 走的就是 pnpm 克隆前的同一套
856
+ // https 传输。curl 打 github.com 主页会被按协议/端口区分的防火墙与 TLS 复检
857
+ // 拦截(能 git 克隆、打不开网页),把可装的仓库误判成 [network] 而中止
858
+ // (见 issue #71)。握手失败时再用 curl 复核一次主页连通性:只有两者都不通
859
+ // 才判「网络不通」;仅握手失败(仓库不存在、私有仓库、需要凭据)放行,
860
+ // 交给后续预检与真实安装给出准确原因。
861
+ const gitProbeTarget = repoTarget === null ? null : repoTarget.replace(/^git\+/, '');
862
+ let net;
863
+ if (gitProbeTarget !== null) {
864
+ net = await gitLsRemote(gitProbeTarget, effectiveProxy, 6000);
865
+ if (!net.ok) {
866
+ const reachable = await probeUrl('https://github.com/', effectiveProxy, 6000);
867
+ if (reachable.ok)
868
+ net = reachable;
869
+ }
870
+ }
871
+ else {
872
+ net = await probeUrl(npmProbeTarget, effectiveProxy, 6000);
873
+ }
874
+ const probeTarget = gitProbeTarget ?? npmProbeTarget;
849
875
  if (!net.ok) {
850
876
  // 错误消息按客户端界面语言提示:中文界面给中文、英文界面给英文,
851
- // 不再写死英文(用户一眼看懂是网络问题,而不是插件问题)
877
+ // 让用户一眼看懂是网络问题而非插件问题
852
878
  const channelName = isGitChannel
853
879
  ? (lang === 'zh' ? 'GitHub' : 'github.com')
854
880
  : (lang === 'zh' ? 'npm 源' : 'the npm registry');
@@ -875,9 +901,9 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
875
901
  return;
876
902
  }
877
903
  // 包名冲突检测(仅 git 通道):git 目标包内声明的 name 若与 profile 已装依赖同名,
878
- // pnpm 会以该 name 做依赖键撞车,抛出的 CLI 报错晦涩难懂(issue #25:sandbase-harness
879
- // 声明 managed-agents,但该包名在 registry 上被另一仓库占用,npm 反查搜不到 → git 直装
880
- // 撞已装同名依赖)。到这里已装依赖要么来源不同仓库、要么 spec 是版本号解析不出仓库身份。
904
+ // pnpm 会以该 name 做依赖键撞车,抛出的 CLI 报错晦涩难懂 —— 且该包名在 registry 上
905
+ // 可能被别的仓库占用,npm 反查搜不到,git 直装就会撞上已装的同名依赖。
906
+ // 到这里已装依赖要么来源不同仓库、要么 spec 是版本号解析不出仓库身份。
881
907
  // 读 node_modules 里已装包的真实 repository 区分两种情况:
882
908
  // - 指向同一仓库 → 是「同仓库残留」(spec 丢了仓库身份),就地转 update 覆盖重装(幂等);
883
909
  // - 指向别的仓库 / 读不到 → 跨仓库包名冲突,入队前转成明确 409,指导用户先卸载或改装。
@@ -5,10 +5,9 @@
5
5
  *
6
6
  * 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
7
7
  *
8
- * 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时
9
- * 有已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`(build-ideal-tree.js
10
- * 的 #loadPeerSet),官方标记 Cannot Reproduce(npm/cli#8261、#9787),npm 11.6.0 起不再复现。
11
- * 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,若按插件打包问题引导提 Issue 就误导了用户。
8
+ * 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时有
9
+ * 已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`。这是 npm 自身缺陷,
10
+ * 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,不能按插件打包问题引导用户提 Issue。
12
11
  *
13
12
  * 这里的 `npmTooLowMarker` 是纯函数(输出文本 + 环境 → 标记行或 null),不触碰任务/队列,
14
13
  * 由调用方(task-queue.ts)负责把标记追加到任务输出;spawn 读 npm 版本是唯一副作用,独立可测。
@@ -16,7 +15,7 @@
16
15
  import { spawnSync } from 'node:child_process';
17
16
  /** npm arborist 内部崩溃特征:`Cannot read properties of null (reading 'edgesOut')`。 */
18
17
  export const NPM_CRASH_EDGES_OUT_RE = /Cannot read properties of null \(reading 'edgesOut'\)|edgesOut/i;
19
- /** 该缺陷在 npm 11.6.0 起不再复现;低于此版本遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
18
+ /** 低于该版本(该缺陷在 npm 11.6.0 修复)遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
20
19
  export const NPM_MIN_VERSION = [11, 6, 0];
21
20
  /** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */
22
21
  export function npmVersionOf(env) {
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { spawn, spawnSync } from 'node:child_process';
11
11
  import { resolvePackageEntry } from './package-entry.js';
12
- import { readFileSync, statSync } from 'node:fs';
12
+ import { existsSync, readFileSync, statSync } from 'node:fs';
13
13
  import { dirname, join, resolve } from 'node:path';
14
14
  import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
15
15
  import { npmTooLowMarker } from './npm-check.js';
@@ -112,19 +112,55 @@ function pushLine(task, line) {
112
112
  task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
113
113
  }
114
114
  }
115
- function cliInvocation() {
115
+ function cliInvocation(profile) {
116
116
  const entry = process.argv[1];
117
- if (entry !== undefined && /[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
118
- const absoluteEntry = resolve(entry);
119
- return {
120
- file: process.execPath,
121
- prefixArgs: [...process.execArgv, absoluteEntry],
122
- cwd: dirname(absoluteEntry),
123
- useShell: false,
124
- };
117
+ if (entry !== undefined) {
118
+ // 打包桌面端(Electron):宿主入口在 app.asar 内,机器上根本没有 `dsh` 可执行文件
119
+ //(Windows 那条 dsh.cmd shim 只存在于宿主进程自己的 PATH 里),直接回退 PATH 必然
120
+ // 报 [dsh-missing](见 issue #71)。这里改用应用自己的 CLI 引导脚本调起同一份打包
121
+ // CLI:Electron 可执行文件 + --expose-internals + desktop-cli.js,与内置终端、
122
+ // 官方插件页安装完全同一条路径 —— desktop profile 由应用独占管理的授权也在其中。
123
+ const bootstrap = desktopCliBootstrap(entry);
124
+ if (bootstrap !== null) {
125
+ return {
126
+ file: process.execPath,
127
+ prefixArgs: ['--expose-internals', bootstrap],
128
+ // 工作目录用 profile 目录(真实存在的目录):asar 内的路径不能作为 spawn 的 cwd
129
+ cwd: profileDirectory(profile),
130
+ useShell: false,
131
+ // Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
132
+ env: { ELECTRON_RUN_AS_NODE: '1' },
133
+ };
134
+ }
135
+ if (/[\\/](?:bin\.(?:js|ts)|dsh)$/.test(entry)) {
136
+ const absoluteEntry = resolve(entry);
137
+ return {
138
+ file: process.execPath,
139
+ prefixArgs: [...process.execArgv, absoluteEntry],
140
+ cwd: dirname(absoluteEntry),
141
+ useShell: false,
142
+ };
143
+ }
125
144
  }
126
145
  return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' };
127
146
  }
147
+ /**
148
+ * 打包桌面端宿主的 CLI 引导脚本路径(`<asar 根>/lib/desktop-cli.js`,即应用内
149
+ * pnpm 服务的 dshBootstrapPath)。宿主入口落在 app.asar 内时按 asar 根反推候选路径,
150
+ * 命中存在的那个才采用;布局不认识(官方改版)时返回 null,调用方回退原逻辑。
151
+ * 导出供单测覆盖路径推导。
152
+ */
153
+ export function desktopCliBootstrap(entry) {
154
+ const marker = /[\\/]app\.asar(?=[\\/]|$)/i.exec(entry);
155
+ if (marker === null)
156
+ return null;
157
+ const root = entry.slice(0, marker.index + marker[0].length);
158
+ for (const candidate of [join(root, 'lib', 'desktop-cli.js'), join(root, 'dsh', 'lib', 'desktop-cli.js')]) {
159
+ if (existsSync(candidate))
160
+ return candidate;
161
+ }
162
+ return null;
163
+ }
128
164
  /**
129
165
  * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
130
166
  * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
@@ -286,7 +322,7 @@ function spawnMutation(options) {
286
322
  const isGlobalNpm = (globalNpm ?? []).length > 0;
287
323
  const invocation = isGlobalNpm
288
324
  ? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
289
- : cliInvocation();
325
+ : cliInvocation(profile);
290
326
  // 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
291
327
  // (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
292
328
  // 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
@@ -334,7 +370,9 @@ function spawnMutation(options) {
334
370
  }
335
371
  child = spawn(invocation.file, attemptArgs, {
336
372
  cwd: invocation.cwd,
337
- env,
373
+ // invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
374
+ // 没有它时保持 env 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
375
+ env: invocation.env === undefined ? env : { ...(env ?? process.env), ...invocation.env },
338
376
  shell: invocation.useShell,
339
377
  // detached 使子进程成为独立进程组组长:取消时能整组清理(见 stopChild),
340
378
  // 且宿主进程崩溃后安装任务不会被连带误杀
@@ -444,7 +482,7 @@ function spawnMutation(options) {
444
482
  });
445
483
  return (async () => {
446
484
  let result = await spawnOnce(args);
447
- // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`,npm/cli#9911):
485
+ // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`):
448
486
  // 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
449
487
  // 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
450
488
  // 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
@@ -19,7 +19,7 @@ const probeFail = () => ({ ok: false, ms: null, status: null });
19
19
  * 代理受限或吊销服务不可达时整个握手直接失败(CRYPT_E_NO_REVOCATION_CHECK),
20
20
  * 表现为 `curl: (35) schannel: next InitializeSecurityContext failed` —— 而 Node/npm/
21
21
  * 浏览器各有自己的 TLS 实现,照常可用,用户只会看到「插件市场打不开」。
22
- * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,握手不再被它拦死;
22
+ * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,避免它拦死握手;
23
23
  * 其它平台后端(SecureTransport / OpenSSL)没有这个强制行为,不加参数。
24
24
  * 运行时读 process.platform(而非模块级常量),便于测试覆盖两条分支。
25
25
  */
@@ -30,6 +30,8 @@ export function curlTlsArgs() {
30
30
  export function curlRevokeFlagUnsupported(code, stderr) {
31
31
  return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr);
32
32
  }
33
+ /** 彻底关闭证书吊销检查的兜底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
34
+ const NO_REVOKE_ARG = '--ssl-no-revoke';
33
35
  /** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
34
36
  export function tlsRevocationBlocked(stderr) {
35
37
  return /CRYPT_E_NO_REVOCATION_CHECK|CRYPT_E_REVOCATION_OFFLINE|0x80092012/i.test(stderr);
@@ -77,7 +79,7 @@ function macSystemProxy() {
77
79
  * `http=host:port;https=host:port`(按协议分别配置)。
78
80
  * 字符类必须同时排除 `/` 与 `=`:只排除 `=` 时,带 scheme 的值会让「主机名」吞掉 `//`,
79
81
  * 拼出 `http:////127.0.0.1:10793` 这种立刻失败的非法地址 —— 代理本来是通的,却把诊断、
80
- * 目录拉取、npm/git 预检全判成「不可达」(dsh-plugin-hub#66)。
82
+ * 目录拉取、npm/git 预检全判成「不可达」。
81
83
  */
82
84
  export function parseWinProxyServer(stdout) {
83
85
  const m = /([^:\s=/]+):(\d+)/.exec(stdout);
@@ -135,13 +137,24 @@ function runCurl(args, env, timeoutMs) {
135
137
  });
136
138
  }
137
139
  /** 带 TLS 兜底的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
138
- * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败。 */
140
+ * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
141
+ * 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
142
+ * 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
143
+ * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达(见 issue #71)。
144
+ * 吊销检查只影响本次探测自身的信任判断,不改变系统 TLS 配置。 */
139
145
  async function curlWithTlsFallback(args, env, timeoutMs) {
140
146
  const tlsArgs = curlTlsArgs();
141
- const first = await runCurl([...tlsArgs, ...args], env, timeoutMs);
142
- if (tlsArgs.length === 0 || !curlRevokeFlagUnsupported(first.code, first.stderr))
143
- return first;
144
- return runCurl(args, env, timeoutMs);
147
+ let run = await runCurl([...tlsArgs, ...args], env, timeoutMs);
148
+ if (tlsArgs.length > 0 && curlRevokeFlagUnsupported(run.code, run.stderr)) {
149
+ run = await runCurl(args, env, timeoutMs);
150
+ }
151
+ if (tlsArgs.length > 0 && tlsRevocationBlocked(run.stderr)) {
152
+ const retry = await runCurl([NO_REVOKE_ARG, ...args], env, timeoutMs);
153
+ // 只在兜底本身跑起来(非「不认识该参数」)时采用其结果,否则保留原始失败线索
154
+ if (retry.code !== null && !curlRevokeFlagUnsupported(retry.code, retry.stderr))
155
+ run = retry;
156
+ }
157
+ return run;
145
158
  }
146
159
  /**
147
160
  * curl 子进程探测:spawn curl -s -w '%{http_code}' 并注入代理 env。
@@ -20,7 +20,7 @@ export function readProfileArg(fallback = 'web') {
20
20
  * - Electron 桌面宿主:不传 `--profile`,而是把 profile 目录作为位置参数传入
21
21
  * (`… <dshRoot> <home>\profiles\desktop …`)。
22
22
  * 桌面端只认 `--profile` 会永远落回 fallback,把路由/日志/安装全挂到错误的 profile 上
23
- * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」,dsh-plugin-hub#65)。
23
+ * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」)。
24
24
  * 位置参数按「父目录名为 profiles」精确识别 profile 目录,避免误取其它位置参数。
25
25
  */
26
26
  export function profileFromArgv(argv, fallback = 'web') {
@@ -11,6 +11,9 @@ export interface WebServerService {
11
11
  }
12
12
  /** Read non-official dependencies installed into one profile. */
13
13
  export declare function readInstalled(profile: string): Record<string, string>;
14
+ /** POST mutations are only accepted from the local web server origin or the desktop host page.
15
+ * Exported for tests (tests/routes.test.ts) — the origin check is the whole CSRF defence. */
16
+ export declare function isSameOrigin(request: IncomingMessage): boolean;
14
17
  /**
15
18
  * Register the Plugin Hub API on the host web server and return a disposer.
16
19
  * @param webServer - DSH web server service.
@@ -46,6 +46,8 @@ export interface Invocation {
46
46
  prefixArgs: string[];
47
47
  cwd: string;
48
48
  useShell: boolean;
49
+ /** 必须叠加到子进程环境上的变量(打包桌面端要带 ELECTRON_RUN_AS_NODE) */
50
+ env?: NodeJS.ProcessEnv;
49
51
  }
50
52
  /** 排队中的变更任务及其启动参数。 */
51
53
  export interface QueueItem {
@@ -1,6 +1,6 @@
1
1
  /** npm arborist 内部崩溃特征:`Cannot read properties of null (reading 'edgesOut')`。 */
2
2
  export declare const NPM_CRASH_EDGES_OUT_RE: RegExp;
3
- /** 该缺陷在 npm 11.6.0 起不再复现;低于此版本遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
3
+ /** 低于该版本(该缺陷在 npm 11.6.0 修复)遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
4
4
  export declare const NPM_MIN_VERSION: number[];
5
5
  /** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */
6
6
  export declare function npmVersionOf(env: NodeJS.ProcessEnv | undefined): [number, number, number] | null;
@@ -33,6 +33,13 @@ export declare function hasQueuedTarget(target: string): boolean;
33
33
  * Returns false when the task is unknown or already finished.
34
34
  */
35
35
  export declare function cancelTask(id: number): boolean;
36
+ /**
37
+ * 打包桌面端宿主的 CLI 引导脚本路径(`<asar 根>/lib/desktop-cli.js`,即应用内
38
+ * pnpm 服务的 dshBootstrapPath)。宿主入口落在 app.asar 内时按 asar 根反推候选路径,
39
+ * 命中存在的那个才采用;布局不认识(官方改版)时返回 null,调用方回退原逻辑。
40
+ * 导出供单测覆盖路径推导。
41
+ */
42
+ export declare function desktopCliBootstrap(entry: string): string | null;
36
43
  /**
37
44
  * Enqueue a plugin mutation and return its task. Tasks run strictly serially:
38
45
  * the queue worker starts the next one only after the previous finishes.
@@ -18,7 +18,7 @@ export type ProbeFailReason = 'tlsRevocation';
18
18
  * 代理受限或吊销服务不可达时整个握手直接失败(CRYPT_E_NO_REVOCATION_CHECK),
19
19
  * 表现为 `curl: (35) schannel: next InitializeSecurityContext failed` —— 而 Node/npm/
20
20
  * 浏览器各有自己的 TLS 实现,照常可用,用户只会看到「插件市场打不开」。
21
- * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,握手不再被它拦死;
21
+ * --ssl-revoke-best-effort(curl ≥ 7.70)把吊销检查降级为尽力而为,避免它拦死握手;
22
22
  * 其它平台后端(SecureTransport / OpenSSL)没有这个强制行为,不加参数。
23
23
  * 运行时读 process.platform(而非模块级常量),便于测试覆盖两条分支。
24
24
  */
@@ -33,7 +33,7 @@ export declare function tlsRevocationBlocked(stderr: string): boolean;
33
33
  * `http=host:port;https=host:port`(按协议分别配置)。
34
34
  * 字符类必须同时排除 `/` 与 `=`:只排除 `=` 时,带 scheme 的值会让「主机名」吞掉 `//`,
35
35
  * 拼出 `http:////127.0.0.1:10793` 这种立刻失败的非法地址 —— 代理本来是通的,却把诊断、
36
- * 目录拉取、npm/git 预检全判成「不可达」(dsh-plugin-hub#66)。
36
+ * 目录拉取、npm/git 预检全判成「不可达」。
37
37
  */
38
38
  export declare function parseWinProxyServer(stdout: string): string | null;
39
39
  /**
@@ -6,7 +6,7 @@ export declare function readProfileArg(fallback?: string): string;
6
6
  * - Electron 桌面宿主:不传 `--profile`,而是把 profile 目录作为位置参数传入
7
7
  * (`… <dshRoot> <home>\profiles\desktop …`)。
8
8
  * 桌面端只认 `--profile` 会永远落回 fallback,把路由/日志/安装全挂到错误的 profile 上
9
- * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」,dsh-plugin-hub#65)。
9
+ * (日志写进 profiles/web/hub.log、市场必然「插件数据加载失败」)。
10
10
  * 位置参数按「父目录名为 profiles」精确识别 profile 目录,避免误取其它位置参数。
11
11
  */
12
12
  export declare function profileFromArgv(argv: readonly string[], fallback?: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -456,7 +456,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
456
456
  // 命令行安装:与应用商店同一套确认/进度/结果弹窗 —— 输入 npm 包名 / GitHub 地址 /
457
457
  // dsh plugin 命令即装(custom 源,受安全设置三开关控制)。点安装先弹确认窗,
458
458
  // 确认后走队列安装(实时进度 + 成功结果视图);目标已安装 → 弹窗转「更新」覆盖重装,
459
- // 不再撞「already installed」报错(第一次安装没有反馈才导致用户重复点击)。
459
+ // 不会撞「already installed」报错。
460
460
  // 安全信任开关关掉对应通道 → 卡片禁用并提示去设置打开(onOpenSettings 跳到设置页)。
461
461
  enableNpm: hubSettings.enableNpmInstall,
462
462
  enableGit: hubSettings.enableGitInstall,
@@ -50,8 +50,8 @@ export function PluginCard({ plugin: p, copied, installedName, installedVersion,
50
50
  ? h('span', { className: styles.verified }, t('verified'))
51
51
  : null,
52
52
  ),
53
- // 描述始终显示:站点英文数据缺翻译时 description 回退成中文也照常展示,
54
- // 描述是了解插件的第一入口,比隐藏更实用(不再为求语言一致而砍内容)。
53
+ // 描述始终显示:站点英文数据缺翻译时 description 回退成中文也照常展示 ——
54
+ // 描述是了解插件的第一入口,语言不一致也不该省略。
55
55
  p.description ? h('p', { className: styles.desc }, p.description) : null,
56
56
  (p.topics?.length ?? 0) > 0
57
57
  ? h('div', { className: styles.topics },
@@ -225,8 +225,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
225
225
  className: styles.failCopy,
226
226
  onClick: () => onCopy(r.message),
227
227
  }, t('failCopy')),
228
- // 成功通知:时间戳并入本行右侧,与图标/文字垂直居中 ——
229
- // 不再单独占底部一行,卡片只有一行内容,图标文字整体居中不顶头
228
+ // 成功通知:时间戳在行右侧,与图标/文字垂直居中,卡片保持单行内容
230
229
  r.ok
231
230
  ? h('span', {
232
231
  className: styles.noticeTime,
@@ -288,12 +287,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
288
287
  }
289
288
  if (kind === 'pnpmWorkspace') {
290
289
  // pnpm 拒绝把依赖装到 workspace 根目录(ERR_PNPM_ADDING_TO_ROOT):宿主在 profile
291
- // 目录调 pnpm add 未声明在根操作,任何插件都会失败,不是插件问题 ——
292
- // 给 .npmrc 豁免指引,不引导提 Issue(dsh-plugin-hub#40)
290
+ // 目录调 pnpm add 未声明在根操作,任何插件都会失败 ——
291
+ // 给 .npmrc 豁免指引,不引导提 Issue
293
292
  return h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
294
293
  }
295
294
  if (kind === 'pnpmStore') {
296
- // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录旧依赖是
295
+ // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
297
296
  // 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
298
297
  // 给清理重建指引,不引导提 Issue
299
298
  return h('div', { className: styles.failPrepareHint }, t('failPnpmStoreHint'))
@@ -305,17 +304,17 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
305
304
  }
306
305
  if (kind === 'pnpmUnusedPatch') {
307
306
  // profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm
308
- // 发现补丁没被用上就中止安装,任何插件都装不进来 —— 不引导提 Issue(#48)
307
+ // 发现补丁没被用上就中止安装,任何插件都装不进来 —— 不引导提 Issue
309
308
  return h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
310
309
  }
311
310
  if (kind === 'fileLocked') {
312
311
  // profile 里的文件被其他进程占用(Windows os error 32):通常是宿主或杀毒软件
313
- // 持有句柄 —— 给出完全退出宿主的指引,不引导提 Issue(#47)
312
+ // 持有句柄 —— 给出完全退出宿主的指引,不引导提 Issue
314
313
  return h('div', { className: styles.failPrepareHint }, t('failFileLockedHint'))
315
314
  }
316
315
  if (kind === 'accessDenied') {
317
316
  // profile 里的文件被系统拒绝写入(Windows os error 5「拒绝访问」/ EPERM / EACCES):
318
- // 占用、只读属性、目录 ACL 或杀软拦截 —— 给退出宿主 + 检查权限的指引,不引导提 Issue(#50)
317
+ // 占用、只读属性、目录 ACL 或杀软拦截 —— 给退出宿主 + 检查权限的指引,不引导提 Issue
319
318
  return h('div', { className: styles.failPrepareHint }, t('failAccessDeniedHint'))
320
319
  }
321
320
  if (kind === 'fsUnavailable') {
@@ -323,6 +322,11 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
323
322
  // 空间不足、盘只读、句柄耗尽,任何插件都装不进来 —— 按子场景给指引,不引导提 Issue
324
323
  return h('div', { className: styles.failPrepareHint }, t('failFsUnavailableHint'))
325
324
  }
325
+ if (kind === 'originRejected') {
326
+ // 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
327
+ // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
328
+ return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint'))
329
+ }
326
330
  if (kind === 'network') {
327
331
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
328
332
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
@@ -331,7 +335,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
331
335
  // 「无法访问 …」醒目行:精准告诉用户具体哪个地址连不上(消息里取不到地址就跳过这行)
332
336
  (() => { const target = unreachableTargetOf(r.message); return target ? h('div', { className: styles.failNetworkTarget }, t('failNetworkTarget', { url: target })) : null })(),
333
337
  // registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
334
- // 否则用户外网是通的会困惑「为什么连不上」(dsh-plugin-hub#32)
338
+ // 否则用户外网是通的会困惑「为什么连不上」
335
339
  (() => { const host = registryHostOf(r.message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
336
340
  h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
337
341
  onRunDiagnostics ? h('button', {
@@ -406,10 +406,10 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
406
406
  : failureKind === 'pnpmWorkspace'
407
407
  // pnpm 拒绝把依赖装到 workspace 根目录(ERR_PNPM_ADDING_TO_ROOT):宿主在 profile 目录
408
408
  // 调 pnpm add 时未声明在根操作,任何插件都会失败,不是插件问题 ——
409
- // 给 .npmrc 豁免指引,不引导提 Issue(dsh-plugin-hub#40)
409
+ // 给 .npmrc 豁免指引,不引导提 Issue
410
410
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
411
411
  : failureKind === 'pnpmStore'
412
- // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录旧依赖是
412
+ // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
413
413
  // 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
414
414
  // 给清理重建指引,不引导提 Issue
415
415
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmStoreHint'))
@@ -419,20 +419,24 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
419
419
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmPolicyHint'))
420
420
  : failureKind === 'pnpmUnusedPatch'
421
421
  // profile 里留着指向旧版本 dsh-plugin 的补丁声明(ERR_PNPM_UNUSED_PATCH):pnpm 发现
422
- // 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue(#48)
422
+ // 补丁没被用上就中止整次安装,任何插件都装不进来 —— 给删除条目的指引,不引导提 Issue
423
423
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmUnusedPatchHint'))
424
424
  : failureKind === 'fileLocked'
425
425
  // profile 里的文件被其他进程占用(Windows os error 32):pnpm 无法替换被占用的文件,
426
- // 通常是宿主或杀毒软件持有句柄 —— 给退出宿主的指引,不引导提 Issue(#47)
426
+ // 通常是宿主或杀毒软件持有句柄 —— 给退出宿主的指引,不引导提 Issue
427
427
  ? h('div', { className: styles.failPrepareHint }, t('failFileLockedHint'))
428
428
  : failureKind === 'accessDenied'
429
429
  // profile 里的文件被系统拒绝写入(Windows os error 5「拒绝访问」/ EPERM / EACCES):
430
- // 占用、只读属性、目录 ACL 或杀软拦截 —— 同样给退出宿主/检查权限的指引,不引导提 Issue(#50)
430
+ // 占用、只读属性、目录 ACL 或杀软拦截 —— 同样给退出宿主/检查权限的指引,不引导提 Issue
431
431
  ? h('div', { className: styles.failPrepareHint }, t('failAccessDeniedHint'))
432
432
  : failureKind === 'fsUnavailable'
433
433
  // 本机磁盘写不进去(ENOSPC / EROFS / EMFILE 及 Windows os error 112、19):
434
434
  // 空间不足、盘只读、句柄耗尽,任何插件都装不进来 —— 按子场景给指引,不引导提 Issue
435
435
  ? h('div', { className: styles.failPrepareHint }, t('failFsUnavailableHint'))
436
+ : failureKind === 'originRejected'
437
+ // 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
438
+ // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
439
+ ? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint'))
436
440
  : failureKind === 'network'
437
441
  // 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
438
442
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
@@ -441,7 +445,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
441
445
  // 「无法访问 …」醒目行:精准告诉用户具体哪个地址连不上(消息里取不到地址就跳过这行)
442
446
  (() => { const target = unreachableTargetOf(message); return target ? h('div', { className: styles.failNetworkTarget }, t('failNetworkTarget', { url: target })) : null })(),
443
447
  // registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
444
- // 否则用户外网是通的会困惑「为什么连不上」(dsh-plugin-hub#32)
448
+ // 否则用户外网是通的会困惑「为什么连不上」
445
449
  (() => { const host = registryHostOf(message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
446
450
  h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
447
451
  onRunDiagnostics ? h('button', {
@@ -11,7 +11,7 @@ import type { HubPlugin, LocaleId } from '../types.ts'
11
11
  import { normalize } from '../logic/normalize.ts'
12
12
 
13
13
  // 目录/统计数据经服务端 /catalog 代理路由获取(curl 子进程注入设置里的代理),
14
- // 与 npm / git 安装通道同一代理口径 —— 浏览器不再直连 api.dsh-plugin.org。
14
+ // 与 npm / git 安装通道同一代理口径 —— 浏览器不直连 api.dsh-plugin.org。
15
15
  const PROXY_BASE = '/dsh-plugin-hub/catalog'
16
16
  const PLUGINS_URL = (lang: LocaleId) => `${PROXY_BASE}?lang=${lang}`
17
17
  const STATS_URL = `${PROXY_BASE}?stats=1`
@@ -100,7 +100,7 @@ export function useCatalog(lang: LocaleId) {
100
100
  setHubPlugin(null)
101
101
  setStats(null)
102
102
  setFailed(false)
103
- // 目录数据直拉:hub 自身版本由独立 Worker 版本控制中心管理,不再依赖主站版本号接口。
103
+ // 此处只拉目录与统计;hub 自身版本由独立 Worker 版本控制中心提供(见 fetchHubUpdate)。
104
104
  const load = () => Promise.all([fetchCatalog(lang, controller.signal), fetchStats(controller.signal)])
105
105
  load()
106
106
  .then(([list, stats]) => {
@@ -160,7 +160,7 @@ export function useCatalog(lang: LocaleId) {
160
160
  useEffect(() => { refreshInstalled() }, [])
161
161
 
162
162
  // Hub 自身版本检查走独立 Worker(版本控制中心):发新版后写一次 Worker KV,
163
- // 所有已装用户立即看到「可更新」+ Markdown 变更记录,不再依赖主站目录数据管道。
163
+ // 所有已装用户立即看到「可更新」+ Markdown 变更记录,不经过主站目录数据管道。
164
164
  // 失败静默降级为 null(徽标不出现,不影响目录本身加载)。
165
165
  useEffect(() => {
166
166
  let cancelled = false
@@ -317,6 +317,9 @@ export const zh = {
317
317
  failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
318
318
  // 「去系统诊断」直达按钮:网络不通提示下方的品牌实底按钮,跳到设置 → 系统诊断跑连通性检测
319
319
  failNetworkRunDiag: '去系统诊断检测网络',
320
+ // 请求来源没通过本地 hub 服务校验(服务端 403 untrusted origin):请求根本没进安装流程,
321
+ // 与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
322
+ failOriginRejectedHint: '这次安装请求没有通过插件市场的来源校验,被本地服务直接拒绝,所以安装根本没有开始 —— 与插件本身无关。通常是因为市场页面不是从本机地址打开的(比如用了局域网 IP),或宿主页面的来源未被识别。请从本机的插件市场地址(localhost)重新打开页面后重试;桌面端用户重启一次宿主即可。',
320
323
  failDshMissingHint: '系统找不到 dsh 命令(dsh 未加入系统 PATH),无法调用安装器执行安装。请确认 DeepSeek Harness 已正确安装、dsh 已加入 PATH(或重新安装),然后重试。这不是插件本身的问题。',
321
324
  failGitMissingHint: '系统找不到 git 命令(Git 未安装或未加入系统 PATH),无法从 GitHub 源安装插件。请先安装 Git(如 git-scm.com/downloads)或把 git 加入 PATH,重启 DSH 后重试。这不是插件本身的问题。',
322
325
  failPnpmMissingHint: '安装器找到了 dsh,但系统里没有 pnpm 命令(dsh 用它来管理 profile 插件)。请先安装 pnpm(如 npm install -g pnpm),确认 pnpm 已加入 PATH 后重试。这不是插件本身的问题。',
@@ -687,6 +690,10 @@ export const en = {
687
690
  failNetworkTarget: 'Cannot reach {url} — your network connection is down',
688
691
  // Diagnostics shortcut button under the network-down hint: jumps to Settings → System Diagnostics
689
692
  failNetworkRunDiag: 'Run a network diagnostic',
693
+ // The request origin did not pass the local hub service check (403 untrusted origin): the request
694
+ // never reached the install flow — nothing to do with the plugin, so point at the local address
695
+ // and do not offer an issue link
696
+ failOriginRejectedHint: 'This install request did not pass the marketplace origin check and was rejected by the local service, so the install never started — this is not about the plugin itself. It usually means the marketplace page was not opened from a local address (for example a LAN IP), or the host page origin was not recognized. Reopen the marketplace from localhost and retry; on the desktop app, restart the host once.',
690
697
  failDshMissingHint: 'The dsh command could not be found (it is not on the system PATH), so the installer could not run. Please make sure DeepSeek Harness is installed correctly and dsh is on your PATH (or reinstall it), then retry. This is not a problem with the plugin itself.',
691
698
  failGitMissingHint: 'The git command could not be found (Git is not installed or not on the system PATH), so GitHub-source plugins cannot be installed. Please install Git (e.g. from git-scm.com/downloads) or add git to your PATH, restart DSH, then retry. This is not a problem with the plugin itself.',
692
699
  failPnpmMissingHint: 'The installer found dsh, but pnpm is missing on this machine (dsh uses it to manage profile plugins). Please install pnpm first (e.g. npm install -g pnpm), make sure it is on your PATH, then retry. This is not a problem with the plugin itself.',