dsh-plugin 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,75 +5,14 @@
5
5
  *
6
6
  * 诊断路由:流式连通性自检、代理校验,以及与任务模块 /env 路由共享的宿主环境快照。
7
7
  */
8
- import { readFileSync } from 'node:fs';
9
8
  import { homedir, release } from 'node:os';
10
- import { dirname, join, resolve } from 'node:path';
9
+ import { join } from 'node:path';
11
10
  import { spawnSync } from 'node:child_process';
12
11
  import { loadSettings } from '../services/settings.js';
12
+ import { hostDshVersion, toolVersion } from '../services/host-version.js';
13
13
  import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
14
14
  import { appendLog } from '../services/log.js';
15
15
  import { readJsonBody, requireTrustedPost, sendJson } from './shared.js';
16
- /** 宿主 dsh CLI 版本:优先直接跑 `dsh --version`(与 pnpm/npm/git 同款同步探测,1500ms 超时兜底,
17
- * 绝不挂起);拿不到再退回从入口 `process.argv[1]` 向上找 package.json(dsh 不在 PATH 时兜底)。 */
18
- function hostDshVersion() {
19
- const viaCli = toolVersion('dsh', ['--version']);
20
- if (viaCli)
21
- return viaCli;
22
- const entry = process.argv[1];
23
- if (entry === undefined)
24
- return null;
25
- let dir = dirname(resolve(entry));
26
- for (let depth = 0; depth < 4; depth++) {
27
- try {
28
- const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
29
- const name = manifest.name ?? '';
30
- if (typeof manifest.version === 'string' && (name === 'dsh' || name.includes('dsh') || name.includes('harness'))) {
31
- return manifest.version;
32
- }
33
- }
34
- catch {
35
- /* 不是包根,继续向上查找 */
36
- }
37
- const parent = dirname(dir);
38
- if (parent === dir)
39
- break;
40
- dir = parent;
41
- }
42
- return null;
43
- }
44
- /** 从命令输出里抽出版本号本体:pnpm/npm 直接打印 `11.7.0`,而 `git --version` 打印
45
- * `git version 2.55.0.windows.5` —— 原样取首行会让 issue 正文(统一再加 `v` 前缀)拼成
46
- * `vgit version 2.55.0.windows.5`。只取版本号,顺带兼容 `v1.2.3` 这类自带前缀的输出。 */
47
- function versionOf(output) {
48
- const first = output.trim().split(/\r?\n/)[0]?.trim() ?? '';
49
- if (!first)
50
- return null;
51
- const match = /\d+\.\d+[0-9A-Za-z.\-+]*/.exec(first);
52
- return match === null ? null : match[0];
53
- }
54
- /** 探测命令行工具的版本号(pnpm --version / npm --version / git --version):
55
- * 未安装 / 不在 PATH / 超时一律返回 null —— issue 环境快照如实写 unknown,绝不挂起。
56
- * (同步探测仅用于启动期/请求期的低频率 /env 快照,安装通道不经过这里。)
57
- *
58
- * Windows 上 pnpm/npm 是 .cmd shim,不带 shell 的 spawnSync 起不来 —— 上报的 issue 里
59
- * 因此长期是 `pnpm: unknown` / `npm: unknown`,而 pnpm 版本恰恰是 allowBuilds 那类问题的
60
- * 关键信息(门禁行为随版本变)。故 win32 走 shell 让 cmd 解析 shim;参数是固定的
61
- * `--version`,没有注入面。 */
62
- function toolVersion(cmd, args) {
63
- try {
64
- const r = spawnSync(cmd, args, {
65
- timeout: 1500,
66
- encoding: 'utf8',
67
- shell: process.platform === 'win32',
68
- });
69
- if (r.error || r.status !== 0)
70
- return null;
71
- return versionOf(r.stdout ?? '');
72
- }
73
- catch {
74
- return null;
75
- }
76
- }
77
16
  /** 本机实际生效的 npm registry:诊断与安装同口径 —— 设置里留空时读用户 npm 配置
78
17
  * (含 profile 目录的 .npmrc),而不是硬探官方源,避免「能装但诊断说源不通」。
79
18
  * 读不到(npm 缺失/超时/输出异常)时回退官方源。 */
@@ -0,0 +1,72 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 宿主与命令行工具的版本探测:dsh 版本同时用于诊断快照和「插件要求更高 DSH」判定,
7
+ * pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时兜底,拿不到一律 null,绝不挂起。
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { dirname, join, resolve } from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ /** 从命令输出里抽出版本号本体:pnpm/npm 直接打印 `11.7.0`,而 `git --version` 打印
13
+ * `git version 2.55.0.windows.5` —— 原样取首行会让 issue 正文拼成 `vgit version …`。
14
+ * 只取版本号,顺带兼容 `v1.2.3` 这类自带前缀的输出。 */
15
+ export function versionOf(output) {
16
+ const first = output.trim().split(/\r?\n/)[0]?.trim() ?? '';
17
+ if (!first)
18
+ return null;
19
+ const match = /\d+\.\d+[0-9A-Za-z.\-+]*/.exec(first);
20
+ return match === null ? null : match[0];
21
+ }
22
+ /** 探测命令行工具版本:未安装 / 不在 PATH / 超时一律 null。
23
+ * Windows 上 pnpm/npm 是 .cmd shim,不走 shell 起不来;参数是固定的 `--version`,没有注入面。 */
24
+ export function toolVersion(cmd, args) {
25
+ try {
26
+ const r = spawnSync(cmd, args, {
27
+ timeout: 1500,
28
+ encoding: 'utf8',
29
+ shell: process.platform === 'win32',
30
+ });
31
+ if (r.error || r.status !== 0)
32
+ return null;
33
+ return versionOf(r.stdout ?? '');
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ let cachedDshVersion = null;
40
+ /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
41
+ * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
42
+ export function hostDshVersion() {
43
+ if (cachedDshVersion !== null)
44
+ return cachedDshVersion;
45
+ const viaCli = toolVersion('dsh', ['--version']);
46
+ if (viaCli !== null) {
47
+ cachedDshVersion = viaCli;
48
+ return viaCli;
49
+ }
50
+ const entry = process.argv[1];
51
+ if (entry === undefined)
52
+ return null;
53
+ let dir = dirname(resolve(entry));
54
+ for (let depth = 0; depth < 4; depth++) {
55
+ try {
56
+ const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
57
+ const name = manifest.name ?? '';
58
+ if (typeof manifest.version === 'string' && (name === 'dsh' || name.includes('dsh') || name.includes('harness'))) {
59
+ cachedDshVersion = manifest.version;
60
+ return cachedDshVersion;
61
+ }
62
+ }
63
+ catch {
64
+ /* 不是包根,继续向上查找 */
65
+ }
66
+ const parent = dirname(dir);
67
+ if (parent === dir)
68
+ break;
69
+ dir = parent;
70
+ }
71
+ return null;
72
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * 「插件要求的 DSH 版本高于当前宿主」判定:安装失败后读一次 npm 元数据,取
7
+ * peerDependencies(@deepseek-ai/dsh-*)与 engines.dsh 声明的最低版本里最高的那个,
8
+ * 与宿主版本比较;宿主更低就产出 [dsh-too-old] 标记 —— 前端据此提示「先升级 DSH」,
9
+ * 不再把这类失败归成「未分类」丢给用户或插件作者。
10
+ *
11
+ * 只在失败路径调用:peer 不满足本身通常只是警告,成功安装时不该出现这个提示;
12
+ * 任何网络/解析异常都静默返回 null,不改变原有失败处理。
13
+ */
14
+ import { hostDshVersion } from '../host-version.js';
15
+ /** 从安装目标里取 npm 包名:`@scope/pkg@1.2.3` → `@scope/pkg`。
16
+ * git / URL / 本地路径等非 npm 目标返回 null。 */
17
+ export function npmNameOf(target) {
18
+ const raw = target.trim();
19
+ if (raw === '')
20
+ return null;
21
+ if (/^(?:github:|git\+|https?:|file:|link:|\.{0,2}[\\/])/i.test(raw))
22
+ return null;
23
+ // owner/repo(非 scope):git 目标的另一种写法,不是包名
24
+ if (!raw.startsWith('@') && /^[^\s@]+\/[^\s]+$/.test(raw))
25
+ return null;
26
+ const at = raw.lastIndexOf('@');
27
+ const name = at > 0 ? raw.slice(0, at) : raw;
28
+ return /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(name) ? name : null;
29
+ }
30
+ /** `0.2.1-rc.1` / `^0.2.1` / `>=0.2.0 <0.3.0` → [0,2,1];解析不出返回 null。
31
+ * 只取版本三段:DSH 的 prerelease 序号(rc.1)不参与「太低」判断,避免比较误差。 */
32
+ export function versionTriple(raw) {
33
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(raw);
34
+ return m === null ? null : [Number(m[1]), Number(m[2]), Number(m[3])];
35
+ }
36
+ function compareTriple(a, b) {
37
+ for (let i = 0; i < 3; i++) {
38
+ if (a[i] !== b[i])
39
+ return a[i] - b[i];
40
+ }
41
+ return 0;
42
+ }
43
+ /** 插件声明的最低 DSH 版本:engines.dsh 与 peerDependencies 里 @deepseek-ai/dsh-* 的下限取最大。
44
+ * 返回原始声明文本(如 `^0.2.1-rc.1`)供提示直接展示;没有声明返回 null。 */
45
+ export function dshFloorOf(manifest) {
46
+ const m = manifest;
47
+ const ranges = [];
48
+ const engineDsh = m?.engines?.dsh;
49
+ if (typeof engineDsh === 'string')
50
+ ranges.push(engineDsh);
51
+ for (const [name, range] of Object.entries(m?.peerDependencies ?? {})) {
52
+ if (/^@deepseek-ai\/dsh(?:-|$)/.test(name) && typeof range === 'string')
53
+ ranges.push(range);
54
+ }
55
+ let floor = null;
56
+ let floorTriple = null;
57
+ for (const range of ranges) {
58
+ const triple = versionTriple(range);
59
+ if (triple === null)
60
+ continue;
61
+ if (floorTriple === null || compareTriple(triple, floorTriple) > 0) {
62
+ floor = range;
63
+ floorTriple = triple;
64
+ }
65
+ }
66
+ return floor;
67
+ }
68
+ /**
69
+ * 产出「插件要求更高的 DSH」标记行;没有声明、读不到元数据、宿主版本未知或已满足时返回 null。
70
+ * 用版本级 manifest(`/<pkg>/latest`):packument 的精简形态不带 peerDependencies。
71
+ */
72
+ export async function dshTooOldMarker(target, opts = {}) {
73
+ const name = npmNameOf(target);
74
+ if (name === null)
75
+ return null;
76
+ const current = opts.currentVersion ?? hostDshVersion();
77
+ if (current === null)
78
+ return null;
79
+ const currentTriple = versionTriple(current);
80
+ if (currentTriple === null)
81
+ return null;
82
+ const base = (opts.registry ?? 'https://registry.npmjs.org').replace(/\/+$/, '');
83
+ // scope 包的路径要编码:`@scope/name` → `%40scope%2Fname`
84
+ const url = `${base}/${name.replace('@', '%40').replace('/', '%2F')}/latest`;
85
+ let manifest;
86
+ try {
87
+ const res = await (opts.fetchImpl ?? fetch)(url, {
88
+ headers: { accept: 'application/json' },
89
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 4000),
90
+ });
91
+ if (!res.ok)
92
+ return null;
93
+ manifest = await res.json();
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ const floor = dshFloorOf(manifest);
99
+ const floorTriple = floor === null ? null : versionTriple(floor);
100
+ if (floor === null || floorTriple === null)
101
+ return null;
102
+ if (compareTriple(currentTriple, floorTriple) >= 0)
103
+ return null;
104
+ return `[dsh-too-old] ${name} requires DSH >= ${floor} (declared in engines / peerDependencies), but this host runs ${current} — upgrade DSH before installing`;
105
+ }
@@ -22,6 +22,7 @@ import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, p
22
22
  import { appendLog } from '../log.js';
23
23
  import { commandOnPath, gitMissingDetail } from './preflight.js';
24
24
  import { isTransportFailure, registryHost } from './registry-fallback.js';
25
+ import { dshTooOldMarker } from './dsh-compat.js';
25
26
  import { OutputDecoder } from './decode-output.js';
26
27
  /** 内存任务注册表,按任务 id 索引。 */
27
28
  const tasks = new Map();
@@ -365,6 +366,12 @@ function spawnMutation(options) {
365
366
  const invocation = isGlobalNpm
366
367
  ? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
367
368
  : cliInvocation(profile);
369
+ // 打包桌面端却回退到 PATH 上的 dsh:说明 desktop CLI 引导路径没命中,desktop profile 必被宿主拒绝。
370
+ // 留一行 ASCII 标记,后续同类上报可直接区分「用户自起 CLI」与「我们没走对引导路径」。
371
+ if (task && task.status !== 'cancelled' && invocation.file === 'dsh'
372
+ && process.versions.electron !== undefined) {
373
+ pushLine(task, '[desktop-cli-fallback] running inside Electron but no desktop CLI bootstrap was found — falling back to dsh on PATH (a desktop-managed profile will be rejected)');
374
+ }
368
375
  // 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
369
376
  // (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
370
377
  // 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
@@ -746,6 +753,23 @@ async function annotateRegistryMissing(result, options) {
746
753
  pushLine(options.task, line);
747
754
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
748
755
  }
756
+ /** 安装失败且插件声明的最低 DSH 版本高于当前宿主 → 注入 [dsh-too-old] 标记。
757
+ * 只在失败路径上发生:peer 不满足本身通常只是警告,装成功时不该冒这个提示。
758
+ * 超时也算失败 —— 旧宿主上插件可能卡在半兼容状态,这个信息同样有用。 */
759
+ async function annotateDshTooOld(result, options) {
760
+ if (options.action === 'remove' || result.exitCode === 0)
761
+ return result;
762
+ // 全局 npm 装的是本机 CLI 工具,不进 profile,没有宿主版本要求
763
+ if ((options.globalNpm ?? []).length > 0)
764
+ return result;
765
+ // 用安装实际使用的源读元数据:私有源/镜像里也能读到同一个包的声明
766
+ const line = await dshTooOldMarker(options.target, { registry: options.registries?.[0] });
767
+ if (line === null)
768
+ return result;
769
+ if (options.task && options.task.status !== 'cancelled')
770
+ pushLine(options.task, line);
771
+ return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
772
+ }
749
773
  /**
750
774
  * 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
751
775
  * `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
@@ -860,6 +884,8 @@ export async function runPluginMutation(options) {
860
884
  }
861
885
  // 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
862
886
  result = await annotateRegistryMissing(result, options);
887
+ // 插件声明的最低 DSH 版本高于当前宿主 → 追加 [dsh-too-old] 标记,前端提示先升级 DSH
888
+ result = await annotateDshTooOld(result, options);
863
889
  // npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
864
890
  return annotateNpmTooLow(result, options);
865
891
  }
@@ -0,0 +1,10 @@
1
+ /** 从命令输出里抽出版本号本体:pnpm/npm 直接打印 `11.7.0`,而 `git --version` 打印
2
+ * `git version 2.55.0.windows.5` —— 原样取首行会让 issue 正文拼成 `vgit version …`。
3
+ * 只取版本号,顺带兼容 `v1.2.3` 这类自带前缀的输出。 */
4
+ export declare function versionOf(output: string): string | null;
5
+ /** 探测命令行工具版本:未安装 / 不在 PATH / 超时一律 null。
6
+ * Windows 上 pnpm/npm 是 .cmd shim,不走 shell 起不来;参数是固定的 `--version`,没有注入面。 */
7
+ export declare function toolVersion(cmd: string, args: string[]): string | null;
8
+ /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
9
+ * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
10
+ export declare function hostDshVersion(): string | null;
@@ -0,0 +1,19 @@
1
+ /** 从安装目标里取 npm 包名:`@scope/pkg@1.2.3` → `@scope/pkg`。
2
+ * git / URL / 本地路径等非 npm 目标返回 null。 */
3
+ export declare function npmNameOf(target: string): string | null;
4
+ /** `0.2.1-rc.1` / `^0.2.1` / `>=0.2.0 <0.3.0` → [0,2,1];解析不出返回 null。
5
+ * 只取版本三段:DSH 的 prerelease 序号(rc.1)不参与「太低」判断,避免比较误差。 */
6
+ export declare function versionTriple(raw: string): [number, number, number] | null;
7
+ /** 插件声明的最低 DSH 版本:engines.dsh 与 peerDependencies 里 @deepseek-ai/dsh-* 的下限取最大。
8
+ * 返回原始声明文本(如 `^0.2.1-rc.1`)供提示直接展示;没有声明返回 null。 */
9
+ export declare function dshFloorOf(manifest: unknown): string | null;
10
+ /**
11
+ * 产出「插件要求更高的 DSH」标记行;没有声明、读不到元数据、宿主版本未知或已满足时返回 null。
12
+ * 用版本级 manifest(`/<pkg>/latest`):packument 的精简形态不带 peerDependencies。
13
+ */
14
+ export declare function dshTooOldMarker(target: string, opts?: {
15
+ currentVersion?: string | null;
16
+ fetchImpl?: typeof fetch;
17
+ timeoutMs?: number;
18
+ registry?: string;
19
+ }): Promise<string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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",
@@ -14,7 +14,7 @@ import type { MouseEvent } from 'react'
14
14
  import styles from '../../styles/Modal.module.css'
15
15
  import type { EnvInfo, Translate } from '../../types.ts'
16
16
  import type { NotificationRecord } from '../../logic/failures.ts'
17
- import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf } from '../../logic/failures.ts'
17
+ import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, missingLocalDirOf } from '../../logic/failures.ts'
18
18
  import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
19
19
  import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
20
20
  import { CloseIcon } from '../ui/icons.tsx'
@@ -306,6 +306,11 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
306
306
  // 给 .npmrc 豁免指引,不引导提 Issue
307
307
  return h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
308
308
  }
309
+ if (kind === 'pnpmVirtualStore') {
310
+ // profile 的 modules 目录与当前 pnpm 的 virtual-store-dir-max-length 不一致:
311
+ // 该 profile 内的安装/卸载都会失败 —— 重建 modules 目录或统一配置即可,不引导提 Issue
312
+ return h('div', { className: styles.failPrepareHint }, t('failPnpmVirtualStoreHint'))
313
+ }
309
314
  if (kind === 'pnpmStore') {
310
315
  // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
311
316
  // 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
@@ -372,10 +377,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
372
377
  return h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
373
378
  }
374
379
  if (kind === 'pnpmLinkedDirMissing') {
375
- // profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND):
376
- // 常见于 link:/本地路径装过开发中的插件、之后目录被删;与本次要装的插件无关
377
- // —— 给删除悬空条目的指引,不引导提 Issue
378
- return h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint'))
380
+ // profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND /
381
+ // pnpm ENOENT lstat):点名缺失目录 + 删除悬空条目,不引导提 Issue
382
+ const dir = missingLocalDirOf(r.message)
383
+ return h('div', null, [
384
+ dir ? h('div', { className: styles.failNetworkTarget }, t('failLinkedDirTarget', { path: dir })) : null,
385
+ h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint')),
386
+ ])
379
387
  }
380
388
  if (kind === 'fileLocked') {
381
389
  // profile 里的文件被其他进程占用(Windows os error 32):通常是宿主或杀毒软件
@@ -397,6 +405,24 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
397
405
  // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
398
406
  return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
399
407
  }
408
+ if (kind === 'profileExclusive') {
409
+ // desktop profile 只能由桌面应用管理:外部 dsh CLI 被宿主拒绝,属环境约束。
410
+ // 带 [desktop-cli-fallback] 则是安装器没找到应用内 CLI 引导脚本 —— 我们的问题,给反馈入口
411
+ const profile = exclusiveProfileOf(r.message)
412
+ const ourBug = desktopCliFallbackOf(r.message)
413
+ return h('div', null, [
414
+ h('div', { className: styles.failPrepareHint },
415
+ t(profile ? 'failProfileExclusiveHint' : 'failProfileExclusiveHintNoProfile', profile ? { profile } : undefined)),
416
+ ourBug ? h('div', { className: styles.failPrepareHint }, t('failDesktopCliFallbackHint')) : null,
417
+ ourBug ? h('a', {
418
+ className: styles.failBigIssue,
419
+ href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
420
+ target: '_blank',
421
+ rel: 'noopener noreferrer',
422
+ title: t('failHubIssueHint'),
423
+ }, t('failIssueBig')) : null,
424
+ ])
425
+ }
400
426
  if (kind === 'registryAuth') {
401
427
  // npm 源返回 401/403:私有源需要凭据,或当前网络/IP 不被允许 —— 本机环境问题,
402
428
  // 给「配认证 / 换回官方源 / 切公共镜像」指引,不引导提 Issue
@@ -404,6 +430,30 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
404
430
  return h('div', { className: styles.failPrepareHint },
405
431
  t(host ? 'failRegistryAuthHint' : 'failRegistryAuthHintNoHost', host ? { host } : undefined))
406
432
  }
433
+ if (kind === 'dshTooOld') {
434
+ // 插件要求的 DSH 下限高于本机宿主:升级 DSH 即可,不属作者、也不属插件中心
435
+ const info = dshTooOldInfoOf(r.message)
436
+ return h('div', { className: styles.failPrepareHint },
437
+ t('failDshTooOldHint', info ?? { pkg: shownRepo || r.repo || 'plugin', floor: '?', current: '?' }))
438
+ }
439
+ if (kind === 'localEnv') {
440
+ // 未命中具体成因、但特征指向本机环境:给通用自查清单 + 系统诊断,默认不推给我们,
441
+ // 只留小字反馈入口(判据是我们自己的启发式,可能误判)
442
+ return h('div', null, [
443
+ h('div', { className: styles.failPrepareHint }, t('failLocalEnvHint')),
444
+ onRunDiagnostics ? h('button', { className: styles.failDiagBtn, onClick: onRunDiagnostics }, t('failNetworkRunDiag')) : null,
445
+ h('div', null, [
446
+ h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
447
+ h('a', {
448
+ className: styles.failMisjudgeLink,
449
+ href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
450
+ target: '_blank',
451
+ rel: 'noopener noreferrer',
452
+ title: t('failHubIssueHint'),
453
+ }, t('failMisjudgeLink')),
454
+ ]),
455
+ ])
456
+ }
407
457
  if (kind === 'network') {
408
458
  // 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
409
459
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
@@ -15,7 +15,7 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
15
15
  import { ProgressView } from './ProgressView.tsx'
16
16
  import { installCommandOf } from '../../logic/install-command.ts'
17
17
  import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
18
- import { classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf } from '../../logic/failures.ts'
18
+ import { classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, missingLocalDirOf } from '../../logic/failures.ts'
19
19
  import type { FailureAction } from '../../logic/failures.ts'
20
20
 
21
21
  /** 完成结果视图:绿色对勾 + 标题/描述;
@@ -445,6 +445,10 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
445
445
  // 调 pnpm add 时未声明在根操作,任何插件都会失败,不是插件问题 ——
446
446
  // 给 .npmrc 豁免指引,不引导提 Issue
447
447
  ? h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
448
+ : failureKind === 'pnpmVirtualStore'
449
+ // profile 的 modules 目录与当前 pnpm 的 virtual-store-dir-max-length 不一致:
450
+ // 该 profile 内的安装/卸载都会失败 —— 重建 modules 目录或统一配置即可,不引导提 Issue
451
+ ? h('div', { className: styles.failPrepareHint }, t('failPnpmVirtualStoreHint'))
448
452
  : failureKind === 'pnpmStore'
449
453
  // pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
450
454
  // 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
@@ -513,10 +517,16 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
513
517
  // 给「对齐源 / 那一次显式走官方源 / 用离线包」的指引,不引导提 Issue
514
518
  ? h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
515
519
  : failureKind === 'pnpmLinkedDirMissing'
516
- // profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND):常见于
517
- // link:/本地路径装过开发中的插件、之后目录被删;pnpm 每次解析 profile 依赖都先卡在它上面,
518
- // 与本次要装的插件无关 —— 给删除悬空条目的指引,不引导提 Issue
519
- ? h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint'))
520
+ // profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND / pnpm ENOENT lstat):
521
+ // 常见于 link:/本地路径装过开发中的插件、之后目录被删;pnpm 每次解析 profile 依赖都先卡在它上面,
522
+ // 与本次要装的插件无关 —— 点名缺失目录 + 删除悬空条目的指引,不引导提 Issue
523
+ ? h('div', null, [
524
+ (() => {
525
+ const dir = missingLocalDirOf(message)
526
+ return dir ? h('div', { className: styles.failNetworkTarget }, t('failLinkedDirTarget', { path: dir })) : null
527
+ })(),
528
+ h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint')),
529
+ ])
520
530
  : failureKind === 'fileLocked'
521
531
  // profile 文件被其他进程占用
522
532
  ? h('div', { className: styles.failPrepareHint }, t('failFileLockedHint'))
@@ -530,6 +540,26 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
530
540
  // 请求来源没通过本地 hub 服务的校验(服务端 403 untrusted origin):请求根本没进
531
541
  // 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
532
542
  ? h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
543
+ : failureKind === 'profileExclusive'
544
+ // desktop profile 只能由桌面应用管理:外部 dsh CLI 被宿主拒绝,属环境约束。
545
+ // 但日志里有 [desktop-cli-fallback] 时是我们没找到应用内 CLI 引导脚本才退回 PATH
546
+ // —— 那是安装器自身的问题,必须留一条回到插件中心的通道
547
+ ? (() => {
548
+ const profile = exclusiveProfileOf(message)
549
+ const ourBug = desktopCliFallbackOf(message)
550
+ return h('div', null, [
551
+ h('div', { className: styles.failPrepareHint },
552
+ t(profile ? 'failProfileExclusiveHint' : 'failProfileExclusiveHintNoProfile', profile ? { profile } : undefined)),
553
+ ourBug ? h('div', { className: styles.failPrepareHint }, t('failDesktopCliFallbackHint')) : null,
554
+ ourBug ? h('a', {
555
+ className: styles.failBigIssue,
556
+ href: hubIssueUrl(repo, message, env, command, attempts, 'triage', action),
557
+ target: '_blank',
558
+ rel: 'noopener noreferrer',
559
+ title: t('failHubIssueHint'),
560
+ }, t('failIssueBig')) : null,
561
+ ])
562
+ })()
533
563
  : failureKind === 'registryAuth'
534
564
  // npm 源返回 401/403:私有源需要凭据,或当前网络/IP 不被允许 —— 本机环境问题,
535
565
  // 给「配认证 / 换回官方源 / 切公共镜像」指引,不引导提 Issue
@@ -538,6 +568,31 @@ export function ErrorModal({ message, repo, action, command, attempts, t, env, o
538
568
  return h('div', { className: styles.failPrepareHint },
539
569
  t(host ? 'failRegistryAuthHint' : 'failRegistryAuthHintNoHost', host ? { host } : undefined))
540
570
  })()
571
+ : failureKind === 'dshTooOld'
572
+ // 安装器读插件 npm 元数据得出的结论:插件要求的 DSH 下限高于本机宿主。
573
+ // 用户升级 DSH 即可解决,不属插件作者、也不属插件中心 —— 不引导提 Issue
574
+ ? (() => {
575
+ const info = dshTooOldInfoOf(message)
576
+ return h('div', { className: styles.failPrepareHint },
577
+ t('failDshTooOldHint', info ?? { pkg: repo ?? 'plugin', floor: '?', current: '?' }))
578
+ })()
579
+ : failureKind === 'localEnv'
580
+ // 未命中具体成因、但特征指向本机环境:给通用自查清单 + 直达系统诊断,不推给作者,
581
+ // 也默认不推给我们 —— 只留小字反馈入口,防环境问题堆进插件中心的 issue 列表
582
+ ? h('div', null, [
583
+ h('div', { className: styles.failPrepareHint }, t('failLocalEnvHint')),
584
+ onRunDiagnostics ? h('button', { className: styles.failDiagBtn, onClick: onRunDiagnostics }, t('failNetworkRunDiag')) : null,
585
+ h('div', null, [
586
+ h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
587
+ h('a', {
588
+ className: styles.failMisjudgeLink,
589
+ href: hubIssueUrl(repo, message, env, command, attempts, 'triage', action),
590
+ target: '_blank',
591
+ rel: 'noopener noreferrer',
592
+ title: t('failHubIssueHint'),
593
+ }, t('failMisjudgeLink')),
594
+ ]),
595
+ ])
541
596
  : failureKind === 'network'
542
597
  // 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
543
598
  // 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,