dsh-plugin 1.6.10 → 1.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/client.js +430 -360
- package/client/client.js.map +1 -1
- package/lib/services/install/registry-check.js +78 -0
- package/lib/services/install/task-queue.js +49 -5
- package/lib/types/services/install/registry-check.d.ts +15 -0
- package/lib/types/services/install/task-queue.d.ts +7 -1
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +6 -5
- package/src/client/components/modals/NotificationsModal.tsx +35 -9
- package/src/client/components/modals/modals.tsx +43 -17
- package/src/client/locales.ts +32 -14
- package/src/client/logic/failures.ts +36 -2
- package/src/client/logic/urls.ts +40 -31
- package/src/server/services/install/registry-check.ts +83 -0
- package/src/server/services/install/task-queue.ts +49 -5
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
* 官方 registry 存在性复核:用官方源(绕开本机镜像)复查 404 的包,产出标记行区分
|
|
7
|
+
* 「profile 残留依赖」「插件依赖在官方源也不存在(作者侧)」「本机镜像未同步」三种情形;
|
|
8
|
+
* 标记由 task-queue 追加到任务输出,前端按标记归类。fetch 与 profile 目录可注入,便于单测。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
/** pnpm 输出里 scope 包是 URL 编码形态(`@scope%2Fname`):比较依赖名、拼查询 URL 前先还原成
|
|
13
|
+
* `@scope/name`。 */
|
|
14
|
+
function decodePackageName(raw) {
|
|
15
|
+
return raw.replace(/%40/gi, '@').replace(/%2f/gi, '/');
|
|
16
|
+
}
|
|
17
|
+
/** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
|
|
18
|
+
* 提取不到返回 null。 */
|
|
19
|
+
export function missingPackageOf(output) {
|
|
20
|
+
for (const line of output.split(/\r?\n/)) {
|
|
21
|
+
if (!/ERR_PNPM_FETCH_404|404/.test(line))
|
|
22
|
+
continue;
|
|
23
|
+
const m = line.match(/GET\s+https?:\/\/[^\s/]+\/((?:@[^/\s:]+\/)?[^/\s:]+)/);
|
|
24
|
+
if (m)
|
|
25
|
+
return decodePackageName(m[1]);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
|
|
30
|
+
export function profileDependencyNames(profileDir) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
33
|
+
return Object.keys(raw.dependencies ?? {});
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** 官方 registry 上是否存在该包:明确 404 → false(不存在),2xx → true,其余(限流/网络异常)→ null
|
|
40
|
+
* —— 判不了就不判,绝不妄下结论。 */
|
|
41
|
+
async function existsOnOfficialRegistry(name, fetchImpl, timeoutMs) {
|
|
42
|
+
// scope 包的路径要编码:`@scope/name` → `%40scope%2Fname`
|
|
43
|
+
const url = `https://registry.npmjs.org/${name.replace('@', '%40').replace('/', '%2F')}`;
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetchImpl(url, {
|
|
46
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
47
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
48
|
+
});
|
|
49
|
+
if (res.status === 404)
|
|
50
|
+
return false;
|
|
51
|
+
return res.ok ? true : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
|
|
59
|
+
* - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
|
|
60
|
+
* - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
|
|
61
|
+
*/
|
|
62
|
+
export async function registryMissingMarker(output, opts) {
|
|
63
|
+
const name = missingPackageOf(output);
|
|
64
|
+
if (name === null)
|
|
65
|
+
return null;
|
|
66
|
+
// ① profile 自己声明的残留依赖(#73):用户环境问题,交给既有的 pnpmMissingDep 提示(该提示就是
|
|
67
|
+
// 教用户删 profile 里那条依赖),这里不再产出标记、不改归属
|
|
68
|
+
if (profileDependencyNames(opts.profileDir).some((dep) => dep.toLowerCase() === name.toLowerCase()))
|
|
69
|
+
return null;
|
|
70
|
+
const exists = await existsOnOfficialRegistry(name, opts.fetchImpl ?? fetch, opts.timeoutMs ?? 8000);
|
|
71
|
+
if (exists === false) {
|
|
72
|
+
return `[registry-missing] ${name} is not published on the official npm registry (registry.npmjs.org) — a dependency of this plugin is missing or was removed; author-side issue`;
|
|
73
|
+
}
|
|
74
|
+
if (exists === true) {
|
|
75
|
+
return `[registry-mirror-lag] ${name} exists on the official npm registry — the configured registry or mirror has not synced it; local environment issue`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
@@ -13,6 +13,7 @@ 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';
|
|
16
|
+
import { registryMissingMarker } from './registry-check.js';
|
|
16
17
|
import { backfillOperationLog } from './operation-log.js';
|
|
17
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
18
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
@@ -131,6 +132,12 @@ function pushLine(task, line) {
|
|
|
131
132
|
task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
|
|
132
133
|
}
|
|
133
134
|
}
|
|
135
|
+
/** spawn 的 cwd:profile 目录(asar 路径不能作 cwd);目录不存在时回退进程工作目录,
|
|
136
|
+
* 否则 Node 报的是 `spawn <file> ENOENT`,与「可执行文件不存在」无法区分。 */
|
|
137
|
+
function spawnCwdFor(profile) {
|
|
138
|
+
const dir = profileDirectory(profile);
|
|
139
|
+
return existsSync(dir) ? dir : process.cwd();
|
|
140
|
+
}
|
|
134
141
|
function cliInvocation(profile) {
|
|
135
142
|
const entry = process.argv[1];
|
|
136
143
|
if (entry !== undefined) {
|
|
@@ -144,8 +151,8 @@ function cliInvocation(profile) {
|
|
|
144
151
|
return {
|
|
145
152
|
file: process.execPath,
|
|
146
153
|
prefixArgs: ['--expose-internals', bootstrap],
|
|
147
|
-
//
|
|
148
|
-
cwd:
|
|
154
|
+
// cwd 用 profile 目录(asar 路径不能作 cwd),不存在时由 spawnCwdFor 回退
|
|
155
|
+
cwd: spawnCwdFor(profile),
|
|
149
156
|
useShell: false,
|
|
150
157
|
// Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
|
|
151
158
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
|
@@ -426,15 +433,20 @@ function spawnMutation(options) {
|
|
|
426
433
|
});
|
|
427
434
|
}
|
|
428
435
|
catch (error) {
|
|
436
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
437
|
+
// 宿主自身入口起不来 → 补标记,前端据此归因
|
|
438
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
429
439
|
if (task) {
|
|
430
440
|
task.status = 'failed';
|
|
431
441
|
task.exitCode = null;
|
|
432
|
-
pushLine(task, `[error] ${
|
|
442
|
+
pushLine(task, `[error] ${detail}`);
|
|
443
|
+
if (marker !== null)
|
|
444
|
+
pushLine(task, marker);
|
|
433
445
|
}
|
|
434
446
|
resolveOnce({
|
|
435
447
|
exitCode: null,
|
|
436
448
|
timedOut: false,
|
|
437
|
-
error:
|
|
449
|
+
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
438
450
|
stdout,
|
|
439
451
|
stderr,
|
|
440
452
|
});
|
|
@@ -514,12 +526,16 @@ function spawnMutation(options) {
|
|
|
514
526
|
}, 3_000);
|
|
515
527
|
});
|
|
516
528
|
child.once('error', (error) => {
|
|
529
|
+
// 同同步抛出那条路径:宿主自身入口起不来 → 补标记
|
|
530
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
517
531
|
if (task && task.status !== 'cancelled') {
|
|
518
532
|
task.status = 'failed';
|
|
519
533
|
task.exitCode = null;
|
|
520
534
|
pushLine(task, `[error] ${error.message}`);
|
|
535
|
+
if (marker !== null)
|
|
536
|
+
pushLine(task, marker);
|
|
521
537
|
}
|
|
522
|
-
settleOnce({ exitCode: null, timedOut: false, error: error.message
|
|
538
|
+
settleOnce({ exitCode: null, timedOut: false, error: marker === null ? error.message : `${error.message}\n${marker}`, stdout, stderr });
|
|
523
539
|
});
|
|
524
540
|
child.once('close', (code) => {
|
|
525
541
|
settleOnce({ exitCode: code, timedOut, error: null, stdout, stderr });
|
|
@@ -681,6 +697,32 @@ function annotateNpmTooLow(result, options) {
|
|
|
681
697
|
pushLine(options.task, line);
|
|
682
698
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
683
699
|
}
|
|
700
|
+
/** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
|
|
701
|
+
* 异步是因为要联网;只在失败路径上发生。 */
|
|
702
|
+
async function annotateRegistryMissing(result, options) {
|
|
703
|
+
if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
|
|
704
|
+
return result;
|
|
705
|
+
const line = await registryMissingMarker(`${result.stderr}\n${result.stdout}`, {
|
|
706
|
+
profileDir: profileDirectory(options.profile),
|
|
707
|
+
});
|
|
708
|
+
if (line === null)
|
|
709
|
+
return result;
|
|
710
|
+
if (options.task && options.task.status !== 'cancelled')
|
|
711
|
+
pushLine(options.task, line);
|
|
712
|
+
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
|
|
716
|
+
* `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
|
|
717
|
+
* 导出供单测覆盖判据。
|
|
718
|
+
*/
|
|
719
|
+
export function hostSpawnMarker(invocation, error) {
|
|
720
|
+
if (error?.code !== 'ENOENT')
|
|
721
|
+
return null;
|
|
722
|
+
if (invocation.file === 'dsh' || invocation.file === 'npm')
|
|
723
|
+
return null;
|
|
724
|
+
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`;
|
|
725
|
+
}
|
|
684
726
|
/**
|
|
685
727
|
* Run a plugin mutation with one recovery path: when `add` fails because
|
|
686
728
|
* pnpm's allowBuilds gate blocks a build script — either the git-hosted
|
|
@@ -765,6 +807,8 @@ export async function runPluginMutation(options) {
|
|
|
765
807
|
result = { ...result, stderr: `${result.stderr}\n${backfilled}` };
|
|
766
808
|
}
|
|
767
809
|
}
|
|
810
|
+
// 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
|
|
811
|
+
result = await annotateRegistryMissing(result, options);
|
|
768
812
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
769
813
|
return annotateNpmTooLow(result, options);
|
|
770
814
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 从失败输出里提取「registry 上不存在的包名」(含 scope,如 `@scope/name`):读 404 行的请求路径。
|
|
2
|
+
* 提取不到返回 null。 */
|
|
3
|
+
export declare function missingPackageOf(output: string): string | null;
|
|
4
|
+
/** 读 profile 自己声明的依赖名(profile 目录下 package.json 的 dependencies 键)。读不到返回空数组。 */
|
|
5
|
+
export declare function profileDependencyNames(profileDir: string): string[];
|
|
6
|
+
/**
|
|
7
|
+
* 产出「包不存在」失败的责任方标记行;判不了返回 null(前端维持既有提示)。
|
|
8
|
+
* - `[registry-missing]` 缺的包在官方源上也不存在 → 作者侧问题(前端引导投作者)
|
|
9
|
+
* - `[registry-mirror-lag]` 官方源上有 → 本机镜像源未同步(前端提示换源,不投任何一方)
|
|
10
|
+
*/
|
|
11
|
+
export declare function registryMissingMarker(output: string, opts: {
|
|
12
|
+
profileDir: string;
|
|
13
|
+
fetchImpl?: typeof fetch;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}): Promise<string | null>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InstallResult, InstallTask } from './install-types.ts';
|
|
1
|
+
import type { InstallResult, InstallTask, Invocation } from './install-types.ts';
|
|
2
2
|
import type { LoaderHandle } from '../loader.ts';
|
|
3
3
|
/** Snapshot of a task (keeps the live object untouched by consumers). */
|
|
4
4
|
export declare function getTask(id: number): InstallTask | undefined;
|
|
@@ -78,6 +78,12 @@ export declare function verifyInstalledEntry(profile: string, target: string): {
|
|
|
78
78
|
name: string | null;
|
|
79
79
|
missing: string | null;
|
|
80
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
|
|
83
|
+
* `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
|
|
84
|
+
* 导出供单测覆盖判据。
|
|
85
|
+
*/
|
|
86
|
+
export declare function hostSpawnMarker(invocation: Invocation, error: unknown): string | null;
|
|
81
87
|
/**
|
|
82
88
|
* Run a plugin mutation with one recovery path: when `add` fails because
|
|
83
89
|
* pnpm's allowBuilds gate blocks a build script — either the git-hosted
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.12",
|
|
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",
|
|
@@ -27,7 +27,7 @@ import { HubUpdateModal } from './modals/HubUpdateModal.tsx'
|
|
|
27
27
|
import { InstalledDetailModal } from './modals/InstalledDetailModal.tsx'
|
|
28
28
|
import { NotificationsModal } from './modals/NotificationsModal.tsx'
|
|
29
29
|
import { addFailure, addSuccess, addUpdateNotice, clearNotifications, ignoreUpdate, loadIgnoredUpdates, loadNotifications, removeNotification, removeUpdateNotice } from '../logic/failures.ts'
|
|
30
|
-
import type { NotificationRecord } from '../logic/failures.ts'
|
|
30
|
+
import type { FailureAction, NotificationRecord } from '../logic/failures.ts'
|
|
31
31
|
import { CatalogHeader } from './layout/CatalogHeader.tsx'
|
|
32
32
|
import { MarketView } from './views/MarketView.tsx'
|
|
33
33
|
import { SectionTabs } from './layout/SectionTabs.tsx'
|
|
@@ -114,8 +114,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
114
114
|
const [uninstallNeedsReload, setUninstallNeedsReload] = useState(false)
|
|
115
115
|
/** 结果视图「立即重启」:请求宿主重启后进入等待,服务回来后整页刷新 */
|
|
116
116
|
const [restarting, setRestarting] = useState(false)
|
|
117
|
-
/**
|
|
118
|
-
const [errorMsg, setErrorMsg] = useState<{ message: string; repo: string | null;
|
|
117
|
+
/** 操作失败信息 + 归属仓库 + 动作(决定弹窗标题与上报措辞)+ 命令与尝试记录(issue 预填用) */
|
|
118
|
+
const [errorMsg, setErrorMsg] = useState<{ message: string; repo: string | null; action: FailureAction; command?: string; attempts?: string[] } | null>(null)
|
|
119
119
|
/** 通知中心记录:localStorage 持久化,每次安装/卸载任务成败都留痕(启动时读回) */
|
|
120
120
|
const [notifications, setNotifications] = useState<NotificationRecord[]>(() => loadNotifications())
|
|
121
121
|
/** 通知中心弹窗:一级导航「设置」tab 后边的铃铛入口按钮打开 */
|
|
@@ -173,7 +173,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
173
173
|
// 提 Issue 目标先反查仓库身份:npm 包名能在已安装表/目录里反查到 owner/repo 才保留,
|
|
174
174
|
// 反查不到置 null —— 错误弹窗据此不显示「一键提 Issue」按钮(防提到错误仓库)
|
|
175
175
|
const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
|
|
176
|
-
|
|
176
|
+
// 动作:更新失败要显示「更新失败」,其余按安装/卸载
|
|
177
|
+
setErrorMsg({ message, repo: issueRepo, action: update ? 'update' : kind, command, attempts })
|
|
177
178
|
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
|
|
178
179
|
// 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
|
|
179
180
|
// 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
|
|
@@ -793,7 +794,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
793
794
|
errorMsg && h(ErrorModal, {
|
|
794
795
|
message: errorMsg.message,
|
|
795
796
|
repo: errorMsg.repo,
|
|
796
|
-
|
|
797
|
+
action: errorMsg.action,
|
|
797
798
|
command: errorMsg.command,
|
|
798
799
|
attempts: errorMsg.attempts,
|
|
799
800
|
t,
|
|
@@ -18,7 +18,7 @@ import type { MouseEvent } from 'react'
|
|
|
18
18
|
import styles from '../../styles/Modal.module.css'
|
|
19
19
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
20
20
|
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
21
|
-
import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
21
|
+
import { actionOfRecord, classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
22
22
|
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
23
23
|
import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
|
|
24
24
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
@@ -255,6 +255,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
255
255
|
// 卡片不展示错误日志预览,完整日志靠「复制完整日志」按钮带走,避免每条卡片被日志撑高
|
|
256
256
|
!r.ok && (() => {
|
|
257
257
|
const kind = classifyFailure(r.message)
|
|
258
|
+
// 动作词:该条提示与上报正文按真实动作取词
|
|
259
|
+
const action = actionOfRecord(r)
|
|
260
|
+
const actionWord = t(action === 'uninstall' ? 'actionUninstall' : action === 'update' ? 'actionUpdate' : 'actionInstall')
|
|
258
261
|
if (kind === 'npmTooOld') {
|
|
259
262
|
// npm 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题 ——
|
|
260
263
|
// 直接给升级指引,不引导提 Issue
|
|
@@ -311,7 +314,24 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
311
314
|
const pkg = missingRegistryPackageOf(r.message)
|
|
312
315
|
return h('div', null, [
|
|
313
316
|
pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null,
|
|
314
|
-
h('div', { className: styles.failPrepareHint }, t(
|
|
317
|
+
h('div', { className: styles.failPrepareHint }, t(registryMissingHintOf(r.message))),
|
|
318
|
+
])
|
|
319
|
+
}
|
|
320
|
+
if (kind === 'pluginMissingDep') {
|
|
321
|
+
// 服务端用**官方** registry 复核过:插件依赖树里的包在官方源上也不存在,且不是用户
|
|
322
|
+
// profile 的残留依赖 → 作者侧问题(依赖没发布/已被删除),引导到插件作者仓库提 Issue
|
|
323
|
+
//(判据是源上的客观存在性,不是我们的启发式判断,故不设「市场误判」次要入口)
|
|
324
|
+
const pkg = missingRegistryPackageOf(r.message)
|
|
325
|
+
return h('div', null, [
|
|
326
|
+
pkg ? h('div', { className: styles.failNetworkTarget }, t('failPluginMissingDepTarget', { pkg })) : null,
|
|
327
|
+
h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
|
|
328
|
+
r.repo && shownRepo ? h('a', {
|
|
329
|
+
className: styles.failBigIssue,
|
|
330
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
|
|
331
|
+
target: '_blank',
|
|
332
|
+
rel: 'noopener noreferrer',
|
|
333
|
+
title: t('failIssueHint'),
|
|
334
|
+
}, t('failIssueBig')) : null,
|
|
315
335
|
])
|
|
316
336
|
}
|
|
317
337
|
if (kind === 'registryVersionStale') {
|
|
@@ -356,7 +376,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
356
376
|
// registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
|
|
357
377
|
// 否则用户外网是通的会困惑「为什么连不上」
|
|
358
378
|
(() => { const host = registryHostOf(r.message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
|
|
359
|
-
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
379
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
|
|
360
380
|
onRunDiagnostics ? h('button', {
|
|
361
381
|
className: styles.failDiagBtn,
|
|
362
382
|
onClick: onRunDiagnostics,
|
|
@@ -370,7 +390,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
370
390
|
h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
|
|
371
391
|
r.repo && shownRepo ? h('a', {
|
|
372
392
|
className: styles.failBigIssue,
|
|
373
|
-
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
|
|
393
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
|
|
374
394
|
target: '_blank',
|
|
375
395
|
rel: 'noopener noreferrer',
|
|
376
396
|
title: t('failIssueHint'),
|
|
@@ -381,7 +401,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
381
401
|
h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
|
|
382
402
|
h('a', {
|
|
383
403
|
className: styles.failMisjudgeLink,
|
|
384
|
-
href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge'),
|
|
404
|
+
href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge', action),
|
|
385
405
|
target: '_blank',
|
|
386
406
|
rel: 'noopener noreferrer',
|
|
387
407
|
title: t('failHubIssueHint'),
|
|
@@ -392,7 +412,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
392
412
|
if (kind === 'hubState') {
|
|
393
413
|
// 插件中心的列表状态已过期(安装器自身守卫消息):非插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
394
414
|
return h('div', null, [
|
|
395
|
-
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
415
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
|
|
396
416
|
])
|
|
397
417
|
}
|
|
398
418
|
if (kind === 'timeout') {
|
|
@@ -404,17 +424,23 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
404
424
|
if (kind === 'profileLock') {
|
|
405
425
|
// DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):只给可行动提示,不引导提 Issue
|
|
406
426
|
return h('div', null, [
|
|
407
|
-
h('div', { className: styles.failPrepareHint }, t('failProfileLockHint')),
|
|
427
|
+
h('div', { className: styles.failPrepareHint }, t('failProfileLockHint', { action: actionWord })),
|
|
428
|
+
])
|
|
429
|
+
}
|
|
430
|
+
if (kind === 'hostCliSpawn') {
|
|
431
|
+
// 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
|
|
432
|
+
return h('div', null, [
|
|
433
|
+
h('div', { className: styles.failPrepareHint }, t('failHostCliSpawnHint')),
|
|
408
434
|
])
|
|
409
435
|
}
|
|
410
436
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
411
437
|
return h('div', null, [
|
|
412
438
|
kind === 'pnpmIgnoredBuild'
|
|
413
439
|
? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
|
|
414
|
-
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
|
|
440
|
+
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
|
|
415
441
|
h('a', {
|
|
416
442
|
className: styles.failBigIssue,
|
|
417
|
-
href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts),
|
|
443
|
+
href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
|
|
418
444
|
target: '_blank',
|
|
419
445
|
rel: 'noopener noreferrer',
|
|
420
446
|
title: t('failHubIssueHint'),
|
|
@@ -17,7 +17,8 @@ import { CloseIcon, ConfirmIcon, CopyIcon, LinkIcon } from '../ui/icons.tsx'
|
|
|
17
17
|
import { ProgressView } from './ProgressView.tsx'
|
|
18
18
|
import { installCommandOf } from '../../logic/install-command.ts'
|
|
19
19
|
import { pluginDetailUrl, pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
|
|
20
|
-
import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
20
|
+
import { classifyFailure, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
21
|
+
import type { FailureAction } from '../../logic/failures.ts'
|
|
21
22
|
|
|
22
23
|
/** 完成结果视图:绿色对勾 + 标题/描述;
|
|
23
24
|
* 终态只有两种:
|
|
@@ -336,12 +337,11 @@ export function UninstallModal(props: UninstallModalProps) {
|
|
|
336
337
|
)
|
|
337
338
|
}
|
|
338
339
|
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
export function ErrorModal({ message, repo, kind, command, attempts, t, env, onCopy, onClose, onRunDiagnostics }: {
|
|
340
|
+
/** 安装/更新/卸载失败弹窗:标题、徽标、提示文案与 issue 正文都按传入的动作取词。 */
|
|
341
|
+
export function ErrorModal({ message, repo, action, command, attempts, t, env, onCopy, onClose, onRunDiagnostics }: {
|
|
342
342
|
message: string
|
|
343
343
|
repo: string | null
|
|
344
|
-
|
|
344
|
+
action: FailureAction
|
|
345
345
|
command?: string
|
|
346
346
|
/** 尝试过的安装方式(npm 反查 + 实际执行命令):issue 预填一并贴给作者,便于反推正确的 npm 包名 */
|
|
347
347
|
attempts?: string[]
|
|
@@ -354,6 +354,8 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
354
354
|
}) {
|
|
355
355
|
// 失败归类:当前安装通道(npm/git)装不上 = 插件分发/依赖的问题,一律引导提 Issue
|
|
356
356
|
const failureKind = classifyFailure(message)
|
|
357
|
+
// 动作词:标题、徽标与提示里的 {action} 统一取这一个词
|
|
358
|
+
const actionWord = t(action === 'uninstall' ? 'actionUninstall' : action === 'update' ? 'actionUpdate' : 'actionInstall')
|
|
357
359
|
// 服务端 [npm-too-low] 标记里带的本机 npm 版本(无标记为 null,提示文案降级为「怀疑」)
|
|
358
360
|
const npmVersion = npmTooLowVersion(message)
|
|
359
361
|
// 复制完整错误:实际执行/尝试过的安装命令(如有)+ 完整错误正文。
|
|
@@ -372,8 +374,9 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
372
374
|
},
|
|
373
375
|
h('div', { className: styles.errorModal, role: 'dialog', 'aria-modal': 'true' },
|
|
374
376
|
h('div', { className: styles.modalHead },
|
|
375
|
-
//
|
|
376
|
-
h('div', { className: styles.errorTitle },
|
|
377
|
+
// 标题按动作明确区分:安装失败 / 更新失败 / 卸载失败
|
|
378
|
+
h('div', { className: styles.errorTitle },
|
|
379
|
+
t(action === 'uninstall' ? 'errorTitleUninstall' : action === 'update' ? 'errorTitleUpdate' : 'errorTitleInstall')),
|
|
377
380
|
h('button', {
|
|
378
381
|
className: styles.modalClose,
|
|
379
382
|
'aria-label': t('errorClose'),
|
|
@@ -385,8 +388,8 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
385
388
|
h('div', { className: styles.failRow },
|
|
386
389
|
h('div', { className: styles.failHead },
|
|
387
390
|
h('span', {
|
|
388
|
-
className:
|
|
389
|
-
},
|
|
391
|
+
className: action === 'install' ? styles.failKindInstall : styles.failKindUninstall,
|
|
392
|
+
}, actionWord),
|
|
390
393
|
// 仓库地址(用户名/仓库)统一显示为可点击超链接,跳转到官网详情页(含插件收录信息),而非直接跳 GitHub
|
|
391
394
|
repo ? h('a', {
|
|
392
395
|
className: styles.failRepo,
|
|
@@ -444,7 +447,25 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
444
447
|
const pkg = missingRegistryPackageOf(message)
|
|
445
448
|
return pkg ? h('div', { className: styles.failNetworkTarget }, t('failPnpmMissingDepTarget', { pkg })) : null
|
|
446
449
|
})(),
|
|
447
|
-
h('div', { className: styles.failPrepareHint }, t(
|
|
450
|
+
h('div', { className: styles.failPrepareHint }, t(registryMissingHintOf(message))),
|
|
451
|
+
])
|
|
452
|
+
: failureKind === 'pluginMissingDep'
|
|
453
|
+
// 服务端用**官方** registry 复核过:插件依赖树里的某个包在官方源上也不存在,且不是用户
|
|
454
|
+
// profile 的残留依赖 → 作者侧问题(依赖没发布/已被删除)。判据是源上的客观存在性,
|
|
455
|
+
// 不是我们的启发式判断,故不给「市场误判」次要入口,直接引导到插件作者仓库提 Issue。
|
|
456
|
+
? h('div', null, [
|
|
457
|
+
(() => {
|
|
458
|
+
const pkg = missingRegistryPackageOf(message)
|
|
459
|
+
return pkg ? h('div', { className: styles.failNetworkTarget }, t('failPluginMissingDepTarget', { pkg })) : null
|
|
460
|
+
})(),
|
|
461
|
+
h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
|
|
462
|
+
repo ? h('a', {
|
|
463
|
+
className: styles.failBigIssue,
|
|
464
|
+
href: pluginIssueUrl(repo, message, env, command, attempts, action),
|
|
465
|
+
target: '_blank',
|
|
466
|
+
rel: 'noopener noreferrer',
|
|
467
|
+
title: t('failIssueHint'),
|
|
468
|
+
}, t('failIssueBig')) : null,
|
|
448
469
|
])
|
|
449
470
|
: failureKind === 'registryVersionStale'
|
|
450
471
|
// 要装的版本在「实际执行安装的那个源」里查不到(ERR_PNPM_NO_MATCHING_VERSION / ETARGET):
|
|
@@ -480,7 +501,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
480
501
|
// registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
|
|
481
502
|
// 否则用户外网是通的会困惑「为什么连不上」
|
|
482
503
|
(() => { const host = registryHostOf(message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
|
|
483
|
-
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
504
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
|
|
484
505
|
onRunDiagnostics ? h('button', {
|
|
485
506
|
className: styles.failDiagBtn,
|
|
486
507
|
onClick: onRunDiagnostics,
|
|
@@ -493,7 +514,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
493
514
|
h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
|
|
494
515
|
repo ? h('a', {
|
|
495
516
|
className: styles.failBigIssue,
|
|
496
|
-
href: pluginIssueUrl(repo, message, env, command, attempts),
|
|
517
|
+
href: pluginIssueUrl(repo, message, env, command, attempts, action),
|
|
497
518
|
target: '_blank',
|
|
498
519
|
rel: 'noopener noreferrer',
|
|
499
520
|
title: t('failIssueHint'),
|
|
@@ -505,7 +526,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
505
526
|
h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
|
|
506
527
|
h('a', {
|
|
507
528
|
className: styles.failMisjudgeLink,
|
|
508
|
-
href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge'),
|
|
529
|
+
href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge', action),
|
|
509
530
|
target: '_blank',
|
|
510
531
|
rel: 'noopener noreferrer',
|
|
511
532
|
title: t('failHubIssueHint'),
|
|
@@ -515,7 +536,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
515
536
|
: failureKind === 'hubState'
|
|
516
537
|
? h('div', null, [
|
|
517
538
|
// 插件中心的列表状态已过期(安装器自身守卫消息):不是插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
518
|
-
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
539
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
|
|
519
540
|
])
|
|
520
541
|
: failureKind === 'timeout'
|
|
521
542
|
? h('div', null, [
|
|
@@ -525,16 +546,21 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
525
546
|
: failureKind === 'profileLock'
|
|
526
547
|
? h('div', null, [
|
|
527
548
|
// DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):给可行动提示,不引导提 Issue
|
|
528
|
-
h('div', { className: styles.failPrepareHint }, t('failProfileLockHint')),
|
|
549
|
+
h('div', { className: styles.failPrepareHint }, t('failProfileLockHint', { action: actionWord })),
|
|
550
|
+
])
|
|
551
|
+
: failureKind === 'hostCliSpawn'
|
|
552
|
+
? h('div', null, [
|
|
553
|
+
// 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
|
|
554
|
+
h('div', { className: styles.failPrepareHint }, t('failHostCliSpawnHint')),
|
|
529
555
|
])
|
|
530
556
|
: h('div', null, [
|
|
531
557
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
532
558
|
failureKind === 'pnpmIgnoredBuild'
|
|
533
559
|
? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
|
|
534
|
-
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
|
|
560
|
+
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
|
|
535
561
|
h('a', {
|
|
536
562
|
className: styles.failBigIssue,
|
|
537
|
-
href: hubIssueUrl(repo, message, env, command, attempts),
|
|
563
|
+
href: hubIssueUrl(repo, message, env, command, attempts, 'triage', action),
|
|
538
564
|
target: '_blank',
|
|
539
565
|
rel: 'noopener noreferrer',
|
|
540
566
|
title: t('failHubIssueHint'),
|