dsh-plugin 1.6.11 → 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 +386 -358
- package/client/client.js.map +1 -1
- package/lib/services/install/registry-check.js +3 -14
- package/lib/services/install/task-queue.js +34 -9
- 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 +18 -9
- package/src/client/components/modals/modals.tsx +24 -16
- package/src/client/locales.ts +26 -14
- package/src/client/logic/failures.ts +25 -10
- package/src/client/logic/urls.ts +36 -31
- package/src/server/services/install/registry-check.ts +3 -14
- package/src/server/services/install/task-queue.ts +32 -9
|
@@ -3,20 +3,9 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 官方 registry
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* ① 缺的包是 **profile 自己声明的依赖**(历史残留条目,指向一个从未发布的包)—— 用户环境问题:
|
|
10
|
-
* pnpm 每次解析依赖都会先卡在它上面,任何插件都装不进来(issue #73),提示删掉该条目即可;
|
|
11
|
-
* ② 缺的包来自**插件的依赖树**,且该包在官方 registry 上也不存在 —— 作者侧问题(漏声明依赖、
|
|
12
|
-
* 依赖被删除或从未发布),应引导用户投插件作者仓库;
|
|
13
|
-
* ③ 缺的包来自插件依赖树,但官方 registry 上有 —— 用户本机 registry 指向的镜像/私有源未同步
|
|
14
|
-
* 该包,属用户环境问题,提示换源即可。
|
|
15
|
-
* 故本模块先排除 ①(读 profile 的 package.json),再用**官方** registry(registry.npmjs.org,
|
|
16
|
-
* 绕开本机可能配置的镜像)复查存在性,产出 ② / ③ 的标记行。
|
|
17
|
-
*
|
|
18
|
-
* 与 npm-check.ts 同构:本模块只产出「标记行」,由 task-queue 追加到任务输出,前端按标记分类。
|
|
19
|
-
* fetch 与 profile 目录都可注入,便于单测覆盖全部结论而不依赖网络。
|
|
6
|
+
* 官方 registry 存在性复核:用官方源(绕开本机镜像)复查 404 的包,产出标记行区分
|
|
7
|
+
* 「profile 残留依赖」「插件依赖在官方源也不存在(作者侧)」「本机镜像未同步」三种情形;
|
|
8
|
+
* 标记由 task-queue 追加到任务输出,前端按标记归类。fetch 与 profile 目录可注入,便于单测。
|
|
20
9
|
*/
|
|
21
10
|
import { readFileSync } from 'node:fs';
|
|
22
11
|
import { join } from 'node:path';
|
|
@@ -132,6 +132,12 @@ function pushLine(task, line) {
|
|
|
132
132
|
task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2));
|
|
133
133
|
}
|
|
134
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
|
+
}
|
|
135
141
|
function cliInvocation(profile) {
|
|
136
142
|
const entry = process.argv[1];
|
|
137
143
|
if (entry !== undefined) {
|
|
@@ -145,8 +151,8 @@ function cliInvocation(profile) {
|
|
|
145
151
|
return {
|
|
146
152
|
file: process.execPath,
|
|
147
153
|
prefixArgs: ['--expose-internals', bootstrap],
|
|
148
|
-
//
|
|
149
|
-
cwd:
|
|
154
|
+
// cwd 用 profile 目录(asar 路径不能作 cwd),不存在时由 spawnCwdFor 回退
|
|
155
|
+
cwd: spawnCwdFor(profile),
|
|
150
156
|
useShell: false,
|
|
151
157
|
// Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
|
|
152
158
|
env: { ELECTRON_RUN_AS_NODE: '1' },
|
|
@@ -427,15 +433,20 @@ function spawnMutation(options) {
|
|
|
427
433
|
});
|
|
428
434
|
}
|
|
429
435
|
catch (error) {
|
|
436
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
437
|
+
// 宿主自身入口起不来 → 补标记,前端据此归因
|
|
438
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
430
439
|
if (task) {
|
|
431
440
|
task.status = 'failed';
|
|
432
441
|
task.exitCode = null;
|
|
433
|
-
pushLine(task, `[error] ${
|
|
442
|
+
pushLine(task, `[error] ${detail}`);
|
|
443
|
+
if (marker !== null)
|
|
444
|
+
pushLine(task, marker);
|
|
434
445
|
}
|
|
435
446
|
resolveOnce({
|
|
436
447
|
exitCode: null,
|
|
437
448
|
timedOut: false,
|
|
438
|
-
error:
|
|
449
|
+
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
439
450
|
stdout,
|
|
440
451
|
stderr,
|
|
441
452
|
});
|
|
@@ -515,12 +526,16 @@ function spawnMutation(options) {
|
|
|
515
526
|
}, 3_000);
|
|
516
527
|
});
|
|
517
528
|
child.once('error', (error) => {
|
|
529
|
+
// 同同步抛出那条路径:宿主自身入口起不来 → 补标记
|
|
530
|
+
const marker = hostSpawnMarker(invocation, error);
|
|
518
531
|
if (task && task.status !== 'cancelled') {
|
|
519
532
|
task.status = 'failed';
|
|
520
533
|
task.exitCode = null;
|
|
521
534
|
pushLine(task, `[error] ${error.message}`);
|
|
535
|
+
if (marker !== null)
|
|
536
|
+
pushLine(task, marker);
|
|
522
537
|
}
|
|
523
|
-
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 });
|
|
524
539
|
});
|
|
525
540
|
child.once('close', (code) => {
|
|
526
541
|
settleOnce({ exitCode: code, timedOut, error: null, stdout, stderr });
|
|
@@ -682,10 +697,8 @@ function annotateNpmTooLow(result, options) {
|
|
|
682
697
|
pushLine(options.task, line);
|
|
683
698
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
684
699
|
}
|
|
685
|
-
/**
|
|
686
|
-
*
|
|
687
|
-
* registry-check.ts;这里之所以是异步,是因为要联网查官方 registry —— 只在安装失败且日志含 404
|
|
688
|
-
* 时发生,代价可忽略。 */
|
|
700
|
+
/** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
|
|
701
|
+
* 异步是因为要联网;只在失败路径上发生。 */
|
|
689
702
|
async function annotateRegistryMissing(result, options) {
|
|
690
703
|
if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
|
|
691
704
|
return result;
|
|
@@ -698,6 +711,18 @@ async function annotateRegistryMissing(result, options) {
|
|
|
698
711
|
pushLine(options.task, line);
|
|
699
712
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
700
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
|
+
}
|
|
701
726
|
/**
|
|
702
727
|
* Run a plugin mutation with one recovery path: when `add` fails because
|
|
703
728
|
* pnpm's allowBuilds gate blocks a build script — either the git-hosted
|
|
@@ -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, registryMissingHintOf, 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
|
|
@@ -324,7 +327,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
324
327
|
h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
|
|
325
328
|
r.repo && shownRepo ? h('a', {
|
|
326
329
|
className: styles.failBigIssue,
|
|
327
|
-
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
|
|
330
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
|
|
328
331
|
target: '_blank',
|
|
329
332
|
rel: 'noopener noreferrer',
|
|
330
333
|
title: t('failIssueHint'),
|
|
@@ -373,7 +376,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
373
376
|
// registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
|
|
374
377
|
// 否则用户外网是通的会困惑「为什么连不上」
|
|
375
378
|
(() => { const host = registryHostOf(r.message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
|
|
376
|
-
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
379
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
|
|
377
380
|
onRunDiagnostics ? h('button', {
|
|
378
381
|
className: styles.failDiagBtn,
|
|
379
382
|
onClick: onRunDiagnostics,
|
|
@@ -387,7 +390,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
387
390
|
h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(r.message) ? t('failPackagingHint') : t('failPrepareHint')),
|
|
388
391
|
r.repo && shownRepo ? h('a', {
|
|
389
392
|
className: styles.failBigIssue,
|
|
390
|
-
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts),
|
|
393
|
+
href: pluginIssueUrl(shownRepo, r.message, env, r.command, r.attempts, action),
|
|
391
394
|
target: '_blank',
|
|
392
395
|
rel: 'noopener noreferrer',
|
|
393
396
|
title: t('failIssueHint'),
|
|
@@ -398,7 +401,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
398
401
|
h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
|
|
399
402
|
h('a', {
|
|
400
403
|
className: styles.failMisjudgeLink,
|
|
401
|
-
href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge'),
|
|
404
|
+
href: hubIssueUrl(shownRepo, r.message, env, r.command, r.attempts, 'misjudge', action),
|
|
402
405
|
target: '_blank',
|
|
403
406
|
rel: 'noopener noreferrer',
|
|
404
407
|
title: t('failHubIssueHint'),
|
|
@@ -409,7 +412,7 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
409
412
|
if (kind === 'hubState') {
|
|
410
413
|
// 插件中心的列表状态已过期(安装器自身守卫消息):非插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
411
414
|
return h('div', null, [
|
|
412
|
-
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
415
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
|
|
413
416
|
])
|
|
414
417
|
}
|
|
415
418
|
if (kind === 'timeout') {
|
|
@@ -421,17 +424,23 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
421
424
|
if (kind === 'profileLock') {
|
|
422
425
|
// DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):只给可行动提示,不引导提 Issue
|
|
423
426
|
return h('div', null, [
|
|
424
|
-
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')),
|
|
425
434
|
])
|
|
426
435
|
}
|
|
427
436
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
428
437
|
return h('div', null, [
|
|
429
438
|
kind === 'pnpmIgnoredBuild'
|
|
430
439
|
? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
|
|
431
|
-
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
|
|
440
|
+
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
|
|
432
441
|
h('a', {
|
|
433
442
|
className: styles.failBigIssue,
|
|
434
|
-
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),
|
|
435
444
|
target: '_blank',
|
|
436
445
|
rel: 'noopener noreferrer',
|
|
437
446
|
title: t('failHubIssueHint'),
|
|
@@ -18,6 +18,7 @@ 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
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,
|
|
@@ -458,7 +461,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
458
461
|
h('div', { className: styles.failPrepareHint }, t('failPluginMissingDepHint')),
|
|
459
462
|
repo ? h('a', {
|
|
460
463
|
className: styles.failBigIssue,
|
|
461
|
-
href: pluginIssueUrl(repo, message, env, command, attempts),
|
|
464
|
+
href: pluginIssueUrl(repo, message, env, command, attempts, action),
|
|
462
465
|
target: '_blank',
|
|
463
466
|
rel: 'noopener noreferrer',
|
|
464
467
|
title: t('failIssueHint'),
|
|
@@ -498,7 +501,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
498
501
|
// registry 被指向内网/自定义源(tarball 从非官方源下载失败):直接给出换源指引,
|
|
499
502
|
// 否则用户外网是通的会困惑「为什么连不上」
|
|
500
503
|
(() => { const host = registryHostOf(message); return host ? h('div', { className: styles.failPrepareHint }, t('failNetworkRegistryHint', { host })) : null })(),
|
|
501
|
-
h('div', { className: styles.failPrepareHint }, t('failNetworkHint')),
|
|
504
|
+
h('div', { className: styles.failPrepareHint }, t('failNetworkHint', { action: actionWord })),
|
|
502
505
|
onRunDiagnostics ? h('button', {
|
|
503
506
|
className: styles.failDiagBtn,
|
|
504
507
|
onClick: onRunDiagnostics,
|
|
@@ -511,7 +514,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
511
514
|
h('div', { className: styles.failPrepareHint }, /\[packaging\]/i.test(message) ? t('failPackagingHint') : t('failPrepareHint')),
|
|
512
515
|
repo ? h('a', {
|
|
513
516
|
className: styles.failBigIssue,
|
|
514
|
-
href: pluginIssueUrl(repo, message, env, command, attempts),
|
|
517
|
+
href: pluginIssueUrl(repo, message, env, command, attempts, action),
|
|
515
518
|
target: '_blank',
|
|
516
519
|
rel: 'noopener noreferrer',
|
|
517
520
|
title: t('failIssueHint'),
|
|
@@ -523,7 +526,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
523
526
|
h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
|
|
524
527
|
h('a', {
|
|
525
528
|
className: styles.failMisjudgeLink,
|
|
526
|
-
href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge'),
|
|
529
|
+
href: hubIssueUrl(repo, message, env, command, attempts, 'misjudge', action),
|
|
527
530
|
target: '_blank',
|
|
528
531
|
rel: 'noopener noreferrer',
|
|
529
532
|
title: t('failHubIssueHint'),
|
|
@@ -533,7 +536,7 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
533
536
|
: failureKind === 'hubState'
|
|
534
537
|
? h('div', null, [
|
|
535
538
|
// 插件中心的列表状态已过期(安装器自身守卫消息):不是插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
536
|
-
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
539
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint', { action: actionWord })),
|
|
537
540
|
])
|
|
538
541
|
: failureKind === 'timeout'
|
|
539
542
|
? h('div', null, [
|
|
@@ -543,16 +546,21 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
543
546
|
: failureKind === 'profileLock'
|
|
544
547
|
? h('div', null, [
|
|
545
548
|
// DSH 写 profile 的锁超时(同一 profile 被别的 dsh/安装任务占着):给可行动提示,不引导提 Issue
|
|
546
|
-
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')),
|
|
547
555
|
])
|
|
548
556
|
: h('div', null, [
|
|
549
557
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
550
558
|
failureKind === 'pnpmIgnoredBuild'
|
|
551
559
|
? h('div', { className: styles.failPrepareHint }, t('failIgnoredBuild'))
|
|
552
|
-
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint')),
|
|
560
|
+
: h('div', { className: styles.failPrepareHint }, t('failUnclassifiedHint', { action: actionWord })),
|
|
553
561
|
h('a', {
|
|
554
562
|
className: styles.failBigIssue,
|
|
555
|
-
href: hubIssueUrl(repo, message, env, command, attempts),
|
|
563
|
+
href: hubIssueUrl(repo, message, env, command, attempts, 'triage', action),
|
|
556
564
|
target: '_blank',
|
|
557
565
|
rel: 'noopener noreferrer',
|
|
558
566
|
title: t('failHubIssueHint'),
|
package/src/client/locales.ts
CHANGED
|
@@ -308,6 +308,10 @@ export const zh = {
|
|
|
308
308
|
errorTitleUninstall: '卸载失败',
|
|
309
309
|
// 更新失败(通知中心区分安装/更新):更新目标覆盖重装失败,与首次安装失败不同语义
|
|
310
310
|
errorTitleUpdate: '更新失败',
|
|
311
|
+
// 动作词:失败提示与上报正文统一取词,不写死「安装」
|
|
312
|
+
actionInstall: '安装',
|
|
313
|
+
actionUpdate: '更新',
|
|
314
|
+
actionUninstall: '卸载',
|
|
311
315
|
errorPlugin: '插件',
|
|
312
316
|
errorClose: '知道了',
|
|
313
317
|
errorCopy: '复制错误信息',
|
|
@@ -319,12 +323,14 @@ export const zh = {
|
|
|
319
323
|
failHubIssueHint: '此失败已被判定为环境或插件中心自身问题(非插件作者问题),带着错误日志到插件中心仓库一键提交 Issue',
|
|
320
324
|
failHubIssueBig: '反馈给插件中心排查',
|
|
321
325
|
// 未知失败兜底:不归到插件作者,引导到插件中心仓库排查
|
|
322
|
-
failUnclassifiedHint: '
|
|
326
|
+
failUnclassifiedHint: '{action}失败,原因未能自动识别。这通常不是插件自身的问题(可能与本机环境或插件中心有关),请将错误信息反馈给插件中心,由维护者排查。',
|
|
323
327
|
// 插件中心自身状态类消息(插件未安装 / 不在此处卸载 / 已在队列中):已安装列表与实读快照不同步,非插件问题,不引导提 Issue
|
|
324
|
-
failHubStateHint: '
|
|
325
|
-
//
|
|
326
|
-
failTimeoutHint: '
|
|
327
|
-
failProfileLockHint: 'DSH 在写入插件清单时等不到文件锁、超时中止 —— 通常是同时有另一个 DSH 进程或安装任务在写同一个 profile(也可能是上一次的进程没退干净留下的锁)。请先关掉其他 dsh 窗口或正在跑的命令,再重试一次;若反复出现,重启一次 DSH
|
|
328
|
+
failHubStateHint: '插件列表状态已过期或与本地不一致:这次{action}的目标可能已经在列表里了(要升级请点「更新」),也可能已经不在已安装列表中,或已在队列中。这不是插件本身的问题 —— 请刷新页面后重试。',
|
|
329
|
+
// 超时被强杀,可能其实已生效:给提示,不引导提 Issue。措辞动作中性(卸载场景说「已经装上了」不通)
|
|
330
|
+
failTimeoutHint: '本次操作耗时超过本地上限、已被中断 —— 但很可能其实已经生效了(安装器在超时后会先核对本地已安装列表)。请刷新页面确认当前状态;若确实没生效,可在终端手动执行一次对应命令查看完整输出。这不是插件本身的问题。',
|
|
331
|
+
failProfileLockHint: 'DSH 在写入插件清单时等不到文件锁、超时中止 —— 通常是同时有另一个 DSH 进程或安装任务在写同一个 profile(也可能是上一次的进程没退干净留下的锁)。请先关掉其他 dsh 窗口或正在跑的命令,再重试一次;若反复出现,重启一次 DSH。这不是插件本身的问题,也不需要重复{action}。',
|
|
332
|
+
// 宿主自身入口起不来:环境问题,给提示、不引导提 Issue
|
|
333
|
+
failHostCliSpawnHint: 'DSH 没能启动自身来完成这次操作(系统报「找不到可执行文件」):通常是因为 DSH 的安装目录被移动或正在更新,或该 profile 目录还不存在。请重启一次 DSH 后重试;若反复出现,重新安装 DSH。这不是插件本身的问题,换别的插件也一样。',
|
|
328
334
|
// prepare 构建脚本实际执行失败(git 分发缺子模块/构建产物):插件打包分发问题,应提 Issue
|
|
329
335
|
failPrepareHint: '该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。',
|
|
330
336
|
// 「插件打包问题」分支的次要入口:该判据由插件中心自己的预检/装后校验产出(不是 pnpm 原文),
|
|
@@ -334,12 +340,12 @@ export const zh = {
|
|
|
334
340
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺插件需要的文件(入口 / 补丁 mount 的入口 / bundle 补丁)= 插件未适配官方默认安装方式
|
|
335
341
|
failPackagingHint: '该插件不支持官方默认安装方式:其 git 分发缺少它需要的文件(`main` / `exports` 声明的入口、补丁按本包名 mount 的入口,或 `dsh.bundle.patch` 声明的补丁),说明作者未适配官方安装流程。请到作者仓库提交 Issue 反馈,请其提交构建产物、改用预构建 release `.tgz` 直链(`releases/download/<tag>/<asset>.tgz`),或发布完整的 npm 版。',
|
|
336
342
|
// 原生依赖构建脚本被 pnpm 默认拦截(如 node-pty 的 ERR_PNPM_IGNORED_BUILDS):不单独算插件问题,引导到插件中心仓库排查
|
|
337
|
-
failIgnoredBuild: '该插件(或其依赖)需要在安装时执行构建脚本,被 pnpm 的安全白名单(allowBuilds
|
|
343
|
+
failIgnoredBuild: '该插件(或其依赖)需要在安装时执行构建脚本,被 pnpm 的安全白名单(allowBuilds)默认拦截,本次操作因此失败(如原生模块 node-pty,或 git 安装时插件自己的 prepare 脚本)。安装器已自动放行该 key 并重试过一次;仍失败通常说明构建本身没通过。可先在终端手动执行一次安装命令查看完整构建错误;若确属插件的构建/分发问题,建议作者预置构建产物、发布 npm 包或预构建 .tgz。需要协助排查时也可反馈给插件中心。',
|
|
338
344
|
// npm arborist 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题,升级 npm 即可
|
|
339
345
|
failNpmTooLow: '怀疑本机 npm 版本过低:npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
340
346
|
failNpmTooLowV: '本机 npm 版本过低(当前 v{v}):npm 解析插件依赖时内部崩溃(这是 npm 自身的已知缺陷,不是插件问题)。请升级 npm 后重试:npm install -g npm@latest',
|
|
341
347
|
// 安装前连通性预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):本机网络或代理问题,不是插件问题,不引导提 Issue
|
|
342
|
-
failNetworkHint: '
|
|
348
|
+
failNetworkHint: '{action}前检测到网络不通:无法连接到安装源(可能是断网、DNS 解析失败、被防火墙拦截,或代理配置有问题)。这不是插件本身的问题 —— 请检查网络连接,或到「设置」里确认 HTTP 代理配置,再到「系统诊断」运行一次连通性检测,然后重试。',
|
|
343
349
|
// 网络失败弹窗里的「无法访问 …」醒目行:直接告诉用户具体哪个地址连不上({url} 为提取的不可达地址)
|
|
344
350
|
failNetworkRegistryHint: '你的 npm/pnpm 源(registry)被配置为 {host},插件包需从该源下载但失败了。这通常是内网/自定义源没有同步该包(或需要内网认证)—— 请把 npm/pnpm 的 registry 指回官方源 https://registry.npmjs.org(或确认此源可访问该包)后重试。',
|
|
345
351
|
failNetworkTarget: '无法访问 {url} —— 你的网络连接不通',
|
|
@@ -722,6 +728,10 @@ export const en = {
|
|
|
722
728
|
errorTitleUninstall: 'Removal failed',
|
|
723
729
|
// Update failed (notification center distinguishes install/update): update target reinstall failed
|
|
724
730
|
errorTitleUpdate: 'Update failed',
|
|
731
|
+
// Action words: failure hints and filed reports take their wording from here
|
|
732
|
+
actionInstall: 'install',
|
|
733
|
+
actionUpdate: 'update',
|
|
734
|
+
actionUninstall: 'removal',
|
|
725
735
|
errorPlugin: 'Plugin',
|
|
726
736
|
errorClose: 'Got it',
|
|
727
737
|
errorCopy: 'Copy error',
|
|
@@ -733,12 +743,14 @@ export const en = {
|
|
|
733
743
|
failHubIssueHint: 'This failure was classified as an environment or Plugin Hub issue (not a plugin-author issue). Open a pre-filled issue on the Plugin Hub repo with this error log.',
|
|
734
744
|
failHubIssueBig: 'Report to the Plugin Hub for triage',
|
|
735
745
|
// Unknown-failure fallback: not routed to the plugin author, but to the hub repo for triage
|
|
736
|
-
failUnclassifiedHint: 'The
|
|
746
|
+
failUnclassifiedHint: 'The {action} failed and the cause could not be identified automatically. This is usually not the plugin itself (it may relate to your local environment or the Plugin Hub). Please report the error to the Plugin Hub so the maintainers can triage it.',
|
|
737
747
|
// The hub's own state-guard messages (plugin not installed / cannot be uninstalled here / already queued / already installed): the installed list is out of sync with the live snapshot — not a plugin issue, no issue prompt
|
|
738
|
-
failHubStateHint: 'The plugin list is out of date or out of sync: this
|
|
739
|
-
// The
|
|
740
|
-
failTimeoutHint: '
|
|
741
|
-
failProfileLockHint: 'DSH timed out waiting for the file lock while writing the plugin list — usually because another DSH process or install task is writing the same profile (sometimes a previous run left the lock behind). Close other dsh windows or running commands, then retry; if it keeps happening, restart DSH once. This is not a plugin problem, and
|
|
748
|
+
failHubStateHint: 'The plugin list is out of date or out of sync: the target of this {action} may already be in the list (use "Update" to upgrade), may no longer be in the installed list, or may already be queued. This is not a plugin problem — refresh the page and retry.',
|
|
749
|
+
// The command hit the local time limit and was killed: it may well have taken effect already (the installer re-checks the installed list after a timeout) — actionable hint, no issue prompt
|
|
750
|
+
failTimeoutHint: 'This operation exceeded the local time limit and was interrupted — but it may well have taken effect already (the installer re-checks the installed list after a timeout). Refresh the page to confirm the current state; if it did not take effect, run the corresponding command in a terminal to see the full output. This is not a plugin problem.',
|
|
751
|
+
failProfileLockHint: 'DSH timed out waiting for the file lock while writing the plugin list — usually because another DSH process or install task is writing the same profile (sometimes a previous run left the lock behind). Close other dsh windows or running commands, then retry; if it keeps happening, restart DSH once. This is not a plugin problem, and there is no need to repeat the {action}.',
|
|
752
|
+
// The host could not launch itself to run the operation: not a plugin issue — hint only, no issue prompt
|
|
753
|
+
failHostCliSpawnHint: 'DSH could not start itself to perform this operation (the system reported that the executable was not found). This usually means the DSH installation folder has been moved or is being updated, or that the profile directory does not exist yet. Restart DSH and retry; if it keeps happening, reinstall DSH. This is not a plugin problem — other plugins would fail the same way.',
|
|
742
754
|
// prepare script actually failed (git tarball misses submodules/build output): a packaging issue of the plugin itself — file an issue
|
|
743
755
|
failPrepareHint: 'This plugin failed while running its build scripts during install (git tarballs often miss submodules or build output). This is a packaging issue of the plugin itself — please report it to the author repository.',
|
|
744
756
|
// Secondary entry on the "plugin packaging" branch: that verdict comes from the hub's own
|
|
@@ -749,11 +761,11 @@ export const en = {
|
|
|
749
761
|
// Pre-install / post-install guard hit ([packaging]): git distribution lacks a file the package needs (entry / entry a patch mounts / bundle patch) = not adapted to the official default install method
|
|
750
762
|
failPackagingHint: 'This plugin does not support the official default install method: its git distribution lacks a file the package needs (the entry from `main` / `exports`, an entry its own patch mounts by name, or the patch declared by `dsh.bundle.patch`), so it has not been adapted to the official install flow. Please file an Issue on the author\'s repo asking for committed build output, a prebuilt release `.tgz` (`releases/download/<tag>/<asset>.tgz`), or a complete npm release.',
|
|
751
763
|
// native-module build scripts ignored by pnpm by default (e.g. node-pty, ERR_PNPM_IGNORED_BUILDS): not a plugin-side issue on its own, route to the hub repo for triage
|
|
752
|
-
failIgnoredBuild: 'This plugin (or one of its dependencies) needs to run build scripts at install time, which pnpm blocks by default via its allowBuilds allowlist — so
|
|
764
|
+
failIgnoredBuild: 'This plugin (or one of its dependencies) needs to run build scripts at install time, which pnpm blocks by default via its allowBuilds allowlist — so this operation fails (e.g. native modules like node-pty, or the plugin\'s own prepare script on git installs). The installer already auto-allows that key and retries once; if it still fails, the build itself is usually the problem. Run the install command in a terminal to see the full build error; if it is indeed the plugin\'s build or distribution, the author can ship prebuilt output, an npm release, or a prebuilt .tgz. You can also report it to the Plugin Hub for triage.',
|
|
753
765
|
failNpmTooLow: 'Your local npm may be too old: npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
754
766
|
failNpmTooLowV: 'Your local npm is too old (v{v}): npm crashed internally while resolving the plugin\'s dependencies (a known npm bug — not a plugin issue). Please upgrade npm and retry: npm install -g npm@latest',
|
|
755
767
|
// Pre-install connectivity probe / connection failure in the install log ([network] / git fetch timeout / DNS / TLS): a local network or proxy issue, not a plugin issue — no issue link
|
|
756
|
-
failNetworkHint: 'The network could not be reached before
|
|
768
|
+
failNetworkHint: 'The network could not be reached before the {action}: the install source is unreachable (offline, DNS failure, firewall block, or a misconfigured proxy). This is not a problem with the plugin itself — please check your connection, confirm the HTTP proxy in Settings, run a connectivity check under System Diagnostics, then retry.',
|
|
757
769
|
// Prominent "cannot reach …" line in the network-failure dialog: shows the exact unreachable address ({url})
|
|
758
770
|
failNetworkRegistryHint: 'Your npm/pnpm registry is set to {host}, and the package tarball could not be downloaded from it. This usually means a private/internal mirror that has not synced the package (or requires intranet auth) — point the registry back to https://registry.npmjs.org (or make sure this source can serve the package), then retry.',
|
|
759
771
|
failNetworkTarget: 'Cannot reach {url} — your network connection is down',
|
|
@@ -151,7 +151,26 @@ export function removeNotification(id: number): NotificationRecord[] {
|
|
|
151
151
|
return next
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'pluginMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hostCliSpawn' | 'hubState' | 'timeout' | 'profileLock' | 'hub'
|
|
155
|
+
|
|
156
|
+
/** 失败动作口径:标题、上报正文与提示文案按安装/更新/卸载取词,不写死「安装」。 */
|
|
157
|
+
export type FailureAction = 'install' | 'update' | 'uninstall'
|
|
158
|
+
|
|
159
|
+
/** 动作词(英文):名词短语,标题与句子里都成立。 */
|
|
160
|
+
export function actionWordEn(action: FailureAction): string {
|
|
161
|
+
return action === 'uninstall' ? 'removal' : action === 'update' ? 'update' : 'install'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 动作词(中文):安装 / 更新 / 卸载。 */
|
|
165
|
+
export function actionWordZh(action: FailureAction): string {
|
|
166
|
+
return action === 'uninstall' ? '卸载' : action === 'update' ? '更新' : '安装'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 记录的动作:老记录只有 kind,按 kind 兜底。 */
|
|
170
|
+
export function actionOfRecord(rec: { kind: NotificationRecord['kind']; action?: FailureAction }): FailureAction {
|
|
171
|
+
if (rec.action !== undefined) return rec.action
|
|
172
|
+
return rec.kind === 'uninstall' ? 'uninstall' : rec.kind === 'update' ? 'update' : 'install'
|
|
173
|
+
}
|
|
155
174
|
|
|
156
175
|
/** Match the pnpm error code, not a generic mention of checksum or integrity. */
|
|
157
176
|
function missingTarballIntegrity(message: string): boolean {
|
|
@@ -230,10 +249,7 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
230
249
|
// 装后校验发现入口文件缺失([packaging])= git 分发缺构建产物
|
|
231
250
|
if (/\[packaging\]|entry file missing/i.test(message)) return 'pluginPrepare'
|
|
232
251
|
// registry 上不存在的依赖(404),profile 任何安装都会卡在这条。必须在 pluginPrepare 之前判
|
|
233
|
-
//
|
|
234
|
-
// 该包来自插件的依赖树,属**作者侧**问题(依赖没发布/已被删除)。标记由服务端联网复查后注入。
|
|
235
|
-
// 必须排在 pnpmMissingDep 之前 —— 日志里同样含 ERR_PNPM_FETCH_404,靠标记区分「作者的锅」
|
|
236
|
-
// 与「本机 profile 残留依赖(#73)/ 镜像源滞后」。
|
|
252
|
+
// [registry-missing](服务端官方源复核后注入)= 作者侧缺依赖,须先于 pnpmMissingDep(同样含 404 文本)
|
|
237
253
|
if (/\[registry-missing\]/i.test(message)) return 'pluginMissingDep'
|
|
238
254
|
if (/ERR_PNPM_FETCH_404|404 Not Found|Not Found - 404/i.test(message)) return 'pnpmMissingDep'
|
|
239
255
|
// 要装的版本在实际安装源里没有(镜像滞后或版本写错)。必须在 pluginPrepare 之前判
|
|
@@ -257,12 +273,11 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
257
273
|
// dsh 进程或安装任务在写,锁一直没释放。既不是插件问题、也不是用户环境坏了 —— 把并发收掉重试即可。
|
|
258
274
|
// 摆放位置同样在一切错误码分支之后:日志里真有 pnpm 报错时仍以真实报错为准。
|
|
259
275
|
if (/atomic-write: timed out waiting for the writer lock|timed out waiting for the writer lock/i.test(message)) return 'profileLock'
|
|
260
|
-
//
|
|
261
|
-
// 终端里只留这一句「git-hosted plugins build … add the exact key … then re-run」,日志里因此
|
|
262
|
-
// 一条原始 ERR_PNPM_* 码都没有 —— 只能靠这句话识别为「构建脚本被白名单拦截」。
|
|
263
|
-
// 摆放位置在一切错误码分支之后:日志里真有 ERR_PNPM_PREPARE_PACKAGE / ERR_PNPM_IGNORED_BUILDS
|
|
264
|
-
// 时(构建确实跑挂、或不带摘要的旧宿主),仍以原始报错为准,不会被这句摘要改写。
|
|
276
|
+
// 宿主自己的 allowBuilds 摘要行(pnpm 原文只写进 diagnostics 日志文件,终端只剩这一句)
|
|
265
277
|
if (/git-hosted plugins build on install via their prepare script|add the exact key pnpm printed above under allowBuilds/i.test(message)) return 'pnpmIgnoredBuild'
|
|
278
|
+
// 宿主自身入口起不来(服务端 [host-spawn] 标记):环境问题,给提示、不引导提 Issue。
|
|
279
|
+
// 不匹配 `spawn … ENOENT` 文本 —— 那个形态任何 spawn(curl/git/taskkill…)都会产出
|
|
280
|
+
if (/\[host-spawn\]/i.test(message)) return 'hostCliSpawn'
|
|
266
281
|
// 其余未知失败:引导到插件中心仓库排查
|
|
267
282
|
return 'hub'
|
|
268
283
|
}
|