dsh-plugin 1.6.7 → 1.6.8
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 +305 -300
- package/client/client.js.map +1 -1
- package/lib/http/install.js +7 -8
- package/lib/services/install/operation-log.js +49 -0
- package/lib/services/install/task-queue.js +17 -0
- package/lib/types/services/install/operation-log.d.ts +11 -0
- package/package.json +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +6 -0
- package/src/client/components/modals/modals.tsx +5 -0
- package/src/client/locales.ts +4 -0
- package/src/client/logic/failures.ts +7 -1
- package/src/client/logic/urls.ts +1 -0
- package/src/server/http/install.ts +6 -8
- package/src/server/services/install/operation-log.ts +46 -0
- package/src/server/services/install/task-queue.ts +15 -0
package/lib/http/install.js
CHANGED
|
@@ -208,16 +208,15 @@ export function registerInstallRoutes(webServer, profile, loader) {
|
|
|
208
208
|
return;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
211
|
+
// 命中已安装目标时一律按「更新」语义执行(pnpm add 对已存在依赖本就是原位覆盖重装),
|
|
212
|
+
// 不再抛 409:客户端的已装列表与服务端实读可能不同步(如页面列表滞后),此时用户点
|
|
213
|
+
// 「安装」会撞上重复安装守卫,直接覆盖重装比让用户看到一句看不懂的 already installed
|
|
214
|
+
// 报错友好,最终状态也一致(都是已安装)。命中判定双通道:npm 包名按 dependencies
|
|
215
|
+
// 键直接命中,GitHub 目标按仓库身份匹配。
|
|
215
216
|
const installed = readInstalled(profile);
|
|
216
217
|
const already = installedByRepo(installed, target) ?? (installed[target] !== undefined ? target : null);
|
|
217
|
-
if (already !== null
|
|
218
|
-
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
218
|
+
if (already !== null)
|
|
219
|
+
mode = 'update';
|
|
221
220
|
// 队列级去重:同一目标已在排队/执行中时拒绝,避免客户端防重竞态导致重复入队
|
|
222
221
|
if (hasQueuedTarget(target)) {
|
|
223
222
|
sendJson(response, 409, { error: `already queued: ${target}` });
|
|
@@ -0,0 +1,49 @@
|
|
|
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: plugin command failed; diagnostics: <path>`)
|
|
7
|
+
* 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
|
|
8
|
+
* 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
|
|
9
|
+
* 只允许读 DSH 自己的 operation 日志(路径含 .plugin-manager 且以 pnpm.log 结尾),
|
|
10
|
+
* 防止插件把任意路径打印进日志造成越权读取;任何异常都静默降级(返回 null),
|
|
11
|
+
* 不改变原有失败处理。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { CAPTURE_LIMIT_BYTES } from './install-types.js';
|
|
15
|
+
/**
|
|
16
|
+
* 从 DSH 输出里解析 operation 日志路径(`diagnostics: <path>`)。
|
|
17
|
+
* 只认 DSH 的 operation 日志:路径含 `.plugin-manager` 且以 `pnpm.log` 结尾;
|
|
18
|
+
* 不是这种路径(或压根没有该行)返回 null,调用方据此跳过回捞。
|
|
19
|
+
*/
|
|
20
|
+
export function operationLogPathOf(output) {
|
|
21
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
22
|
+
const m = /diagnostics:\s*"?(.+?)"?\s*$/i.exec(raw.trim());
|
|
23
|
+
if (m === null)
|
|
24
|
+
continue;
|
|
25
|
+
const path = m[1].trim();
|
|
26
|
+
if (!path.includes('.plugin-manager') || !path.toLowerCase().endsWith('pnpm.log'))
|
|
27
|
+
continue;
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 回捞 operation 日志尾部,作为失败详情返回;拿不到(无路径 / 文件不存在 / 读失败 /
|
|
34
|
+
* 内容为空)返回 null。尾部截到与子进程输出相同的上限,避免超大日志撑爆内存与消息。
|
|
35
|
+
*/
|
|
36
|
+
export function backfillOperationLog(output) {
|
|
37
|
+
const path = operationLogPathOf(output);
|
|
38
|
+
if (path === null)
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const text = readFileSync(path, 'utf8').trim();
|
|
42
|
+
if (text === '')
|
|
43
|
+
return null;
|
|
44
|
+
return text.length > CAPTURE_LIMIT_BYTES ? text.slice(-CAPTURE_LIMIT_BYTES) : text;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -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 { backfillOperationLog } from './operation-log.js';
|
|
16
17
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
17
18
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
18
19
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
@@ -734,6 +735,22 @@ export async function runPluginMutation(options) {
|
|
|
734
735
|
return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` };
|
|
735
736
|
}
|
|
736
737
|
}
|
|
738
|
+
// DSH 只把一行摘要(`dsh: plugin command failed; diagnostics: <path>`)回显到控制台时,
|
|
739
|
+
// 控制台里没有任何错误特征,前端只能把失败归为「未知」(issue #192)。这里按该路径回捞
|
|
740
|
+
// operation 日志尾部补进失败信息:用户直接看到真实报错,自动归类也有据可依。
|
|
741
|
+
// 只在本就不含任何错误码时回捞 —— pnpm 已把报错回显出来时无需多读一次文件。
|
|
742
|
+
if (result.exitCode !== 0 && !result.timedOut && !/\bERR_[A-Z0-9_]+\b/.test(`${result.stderr}\n${result.stdout}`)) {
|
|
743
|
+
const backfilled = backfillOperationLog(`${result.stderr}\n${result.stdout}`);
|
|
744
|
+
if (backfilled !== null) {
|
|
745
|
+
if (options.task && options.task.status !== 'cancelled') {
|
|
746
|
+
pushLine(options.task, '[pnpm log] DSH did not echo pnpm output — recovered the operation log from disk');
|
|
747
|
+
for (const line of backfilled.split(/\r?\n/))
|
|
748
|
+
if (line.trim() !== '')
|
|
749
|
+
pushLine(options.task, line.trimEnd());
|
|
750
|
+
}
|
|
751
|
+
result = { ...result, stderr: `${result.stderr}\n${backfilled}` };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
737
754
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
738
755
|
return annotateNpmTooLow(result, options);
|
|
739
756
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从 DSH 输出里解析 operation 日志路径(`diagnostics: <path>`)。
|
|
3
|
+
* 只认 DSH 的 operation 日志:路径含 `.plugin-manager` 且以 `pnpm.log` 结尾;
|
|
4
|
+
* 不是这种路径(或压根没有该行)返回 null,调用方据此跳过回捞。
|
|
5
|
+
*/
|
|
6
|
+
export declare function operationLogPathOf(output: string): string | null;
|
|
7
|
+
/**
|
|
8
|
+
* 回捞 operation 日志尾部,作为失败详情返回;拿不到(无路径 / 文件不存在 / 读失败 /
|
|
9
|
+
* 内容为空)返回 null。尾部截到与子进程输出相同的上限,避免超大日志撑爆内存与消息。
|
|
10
|
+
*/
|
|
11
|
+
export declare function backfillOperationLog(output: string): string | null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.8",
|
|
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",
|
|
@@ -377,6 +377,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
377
377
|
}, t('failIssueBig')) : null,
|
|
378
378
|
])
|
|
379
379
|
}
|
|
380
|
+
if (kind === 'hubState') {
|
|
381
|
+
// 插件中心的列表状态已过期(安装器自身守卫消息):非插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
382
|
+
return h('div', null, [
|
|
383
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
384
|
+
])
|
|
385
|
+
}
|
|
380
386
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
381
387
|
return h('div', null, [
|
|
382
388
|
kind === 'pnpmIgnoredBuild'
|
|
@@ -497,6 +497,11 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
497
497
|
title: t('failIssueHint'),
|
|
498
498
|
}, t('failIssueBig')) : null,
|
|
499
499
|
])
|
|
500
|
+
: failureKind === 'hubState'
|
|
501
|
+
? h('div', null, [
|
|
502
|
+
// 插件中心的列表状态已过期(安装器自身守卫消息):不是插件问题、也不是 bug,只给可行动提示,不引导提 Issue
|
|
503
|
+
h('div', { className: styles.failPrepareHint }, t('failHubStateHint')),
|
|
504
|
+
])
|
|
500
505
|
: h('div', null, [
|
|
501
506
|
// pnpm allowBuilds 拦截与未知失败:不归到插件作者,改为引导到插件中心仓库排查
|
|
502
507
|
failureKind === 'pnpmIgnoredBuild'
|
package/src/client/locales.ts
CHANGED
|
@@ -315,6 +315,8 @@ export const zh = {
|
|
|
315
315
|
failHubIssueBig: '反馈给插件中心排查',
|
|
316
316
|
// 未知失败兜底:不归到插件作者,引导到插件中心仓库排查
|
|
317
317
|
failUnclassifiedHint: '安装失败,原因未能自动识别。这通常不是插件自身的问题(可能与本机环境或插件中心有关),请将错误信息反馈给插件中心,由维护者排查。',
|
|
318
|
+
// 插件中心自身状态类消息(插件未安装 / 不在此处卸载 / 已在队列中):已安装列表与实读快照不同步,非插件问题,不引导提 Issue
|
|
319
|
+
failHubStateHint: '插件列表状态已过期或与本地不一致:该插件可能已经装过了(无需重复安装,要升级请点「更新」),也可能已不在已安装列表中,或已在队列中。这不是插件本身的问题 —— 请刷新页面后重试。',
|
|
318
320
|
// prepare 构建脚本实际执行失败(git 分发缺子模块/构建产物):插件打包分发问题,应提 Issue
|
|
319
321
|
failPrepareHint: '该插件安装时的构建脚本执行失败(git 分发常缺失子模块或构建产物),属插件打包分发问题。请向作者仓库提交 Issue 反馈。',
|
|
320
322
|
// 安装前预检 / 装后校验拦截([packaging]):git 分发缺插件需要的文件(入口 / 补丁 mount 的入口 / bundle 补丁)= 插件未适配官方默认安装方式
|
|
@@ -713,6 +715,8 @@ export const en = {
|
|
|
713
715
|
failHubIssueBig: 'Report to the Plugin Hub for triage',
|
|
714
716
|
// Unknown-failure fallback: not routed to the plugin author, but to the hub repo for triage
|
|
715
717
|
failUnclassifiedHint: 'The install 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.',
|
|
718
|
+
// 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
|
|
719
|
+
failHubStateHint: 'The plugin list is out of date or out of sync: this plugin may already be installed (no need to reinstall — use "Update" to upgrade), or may no longer be in the installed list, or may already be queued. This is not a plugin problem — refresh the page and retry.',
|
|
716
720
|
// prepare script actually failed (git tarball misses submodules/build output): a packaging issue of the plugin itself — file an issue
|
|
717
721
|
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.',
|
|
718
722
|
// 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
|
|
@@ -151,7 +151,7 @@ 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' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hub'
|
|
154
|
+
export type FailureKind = 'npmTooOld' | 'dshMissing' | 'gitMissing' | 'pnpmMissing' | 'npmMissing' | 'pnpmStore' | 'pnpmWorkspace' | 'originRejected' | 'pnpmPolicy' | 'pnpmUnusedPatch' | 'pnpmMissingDep' | 'registryVersionStale' | 'pnpmLinkedDirMissing' | 'fileLocked' | 'accessDenied' | 'fsUnavailable' | 'pnpmIgnoredBuild' | 'pluginPrepare' | 'network' | 'hubState' | 'hub'
|
|
155
155
|
|
|
156
156
|
/** Match the pnpm error code, not a generic mention of checksum or integrity. */
|
|
157
157
|
function missingTarballIntegrity(message: string): boolean {
|
|
@@ -238,6 +238,12 @@ export function classifyFailure(message: string): FailureKind {
|
|
|
238
238
|
// 构建脚本真的跑挂了才算插件问题。Command failed 区分大小写:只匹配 pnpm 的包装,
|
|
239
239
|
// 不匹配宿主那句小写 dsh: plugin command failed
|
|
240
240
|
if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|prepare-guard/i.test(message) || /Command failed/.test(message)) return 'pluginPrepare'
|
|
241
|
+
// 安装器自身产生的状态类消息(未安装 / 不在此处卸载 / 已在队列中 / 已安装):这几句由本服务的
|
|
242
|
+
// 守卫发出,不是 CLI 输出、也不是插件问题,而是插件中心的已安装列表与实读快照不同步。
|
|
243
|
+
// 单独归类,让前端给「刷新后重试」这类可行动提示,而不是当作未知失败引导去提 Issue。
|
|
244
|
+
// already installed 只认「整行就是它」的短形态(旧版本守卫留下的历史记录);带「——」说明的
|
|
245
|
+
// 包名冲突另有处置,不能按状态不同步提示。
|
|
246
|
+
if (/plugin is not installed|plugin cannot be uninstalled here|already queued:|^already installed: \S+$/im.test(message)) return 'hubState'
|
|
241
247
|
// 其余未知失败:引导到插件中心仓库排查
|
|
242
248
|
return 'hub'
|
|
243
249
|
}
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -63,6 +63,7 @@ function reasonTitleOf(kind: FailureKind): string {
|
|
|
63
63
|
case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
|
|
64
64
|
case 'pluginPrepare': return 'plugin distribution incomplete'
|
|
65
65
|
case 'network': return 'network failure on the user side'
|
|
66
|
+
case 'hubState': return 'stale install state in the plugin hub'
|
|
66
67
|
case 'hub': return 'install failed (cause unclassified)'
|
|
67
68
|
default: return 'plugin install failed'
|
|
68
69
|
}
|
|
@@ -206,16 +206,14 @@ export function registerInstallRoutes(webServer: WebServerService, profile: stri
|
|
|
206
206
|
return
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
209
|
+
// 命中已安装目标时一律按「更新」语义执行(pnpm add 对已存在依赖本就是原位覆盖重装),
|
|
210
|
+
// 不再抛 409:客户端的已装列表与服务端实读可能不同步(如页面列表滞后),此时用户点
|
|
211
|
+
// 「安装」会撞上重复安装守卫,直接覆盖重装比让用户看到一句看不懂的 already installed
|
|
212
|
+
// 报错友好,最终状态也一致(都是已安装)。命中判定双通道:npm 包名按 dependencies
|
|
213
|
+
// 键直接命中,GitHub 目标按仓库身份匹配。
|
|
213
214
|
const installed = readInstalled(profile)
|
|
214
215
|
const already = installedByRepo(installed, target) ?? (installed[target] !== undefined ? target : null)
|
|
215
|
-
if (already !== null
|
|
216
|
-
sendJson(response, 409, { error: `already installed: ${already}` })
|
|
217
|
-
return
|
|
218
|
-
}
|
|
216
|
+
if (already !== null) mode = 'update'
|
|
219
217
|
// 队列级去重:同一目标已在排队/执行中时拒绝,避免客户端防重竞态导致重复入队
|
|
220
218
|
if (hasQueuedTarget(target)) {
|
|
221
219
|
sendJson(response, 409, { error: `already queued: ${target}` })
|
|
@@ -0,0 +1,46 @@
|
|
|
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: plugin command failed; diagnostics: <path>`)
|
|
7
|
+
* 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
|
|
8
|
+
* 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
|
|
9
|
+
* 只允许读 DSH 自己的 operation 日志(路径含 .plugin-manager 且以 pnpm.log 结尾),
|
|
10
|
+
* 防止插件把任意路径打印进日志造成越权读取;任何异常都静默降级(返回 null),
|
|
11
|
+
* 不改变原有失败处理。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from 'node:fs'
|
|
14
|
+
import { CAPTURE_LIMIT_BYTES } from './install-types.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从 DSH 输出里解析 operation 日志路径(`diagnostics: <path>`)。
|
|
18
|
+
* 只认 DSH 的 operation 日志:路径含 `.plugin-manager` 且以 `pnpm.log` 结尾;
|
|
19
|
+
* 不是这种路径(或压根没有该行)返回 null,调用方据此跳过回捞。
|
|
20
|
+
*/
|
|
21
|
+
export function operationLogPathOf(output: string): string | null {
|
|
22
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
23
|
+
const m = /diagnostics:\s*"?(.+?)"?\s*$/i.exec(raw.trim())
|
|
24
|
+
if (m === null) continue
|
|
25
|
+
const path = m[1].trim()
|
|
26
|
+
if (!path.includes('.plugin-manager') || !path.toLowerCase().endsWith('pnpm.log')) continue
|
|
27
|
+
return path
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 回捞 operation 日志尾部,作为失败详情返回;拿不到(无路径 / 文件不存在 / 读失败 /
|
|
34
|
+
* 内容为空)返回 null。尾部截到与子进程输出相同的上限,避免超大日志撑爆内存与消息。
|
|
35
|
+
*/
|
|
36
|
+
export function backfillOperationLog(output: string): string | null {
|
|
37
|
+
const path = operationLogPathOf(output)
|
|
38
|
+
if (path === null) return null
|
|
39
|
+
try {
|
|
40
|
+
const text = readFileSync(path, 'utf8').trim()
|
|
41
|
+
if (text === '') return null
|
|
42
|
+
return text.length > CAPTURE_LIMIT_BYTES ? text.slice(-CAPTURE_LIMIT_BYTES) : text
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
14
14
|
import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
|
|
15
15
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
|
|
16
16
|
import { npmTooLowMarker } from './npm-check.ts'
|
|
17
|
+
import { backfillOperationLog } from './operation-log.ts'
|
|
17
18
|
import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
18
19
|
import type { LoaderHandle } from '../loader.ts'
|
|
19
20
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.ts'
|
|
@@ -776,6 +777,20 @@ export async function runPluginMutation(options: {
|
|
|
776
777
|
return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` }
|
|
777
778
|
}
|
|
778
779
|
}
|
|
780
|
+
// DSH 只把一行摘要(`dsh: plugin command failed; diagnostics: <path>`)回显到控制台时,
|
|
781
|
+
// 控制台里没有任何错误特征,前端只能把失败归为「未知」(issue #192)。这里按该路径回捞
|
|
782
|
+
// operation 日志尾部补进失败信息:用户直接看到真实报错,自动归类也有据可依。
|
|
783
|
+
// 只在本就不含任何错误码时回捞 —— pnpm 已把报错回显出来时无需多读一次文件。
|
|
784
|
+
if (result.exitCode !== 0 && !result.timedOut && !/\bERR_[A-Z0-9_]+\b/.test(`${result.stderr}\n${result.stdout}`)) {
|
|
785
|
+
const backfilled = backfillOperationLog(`${result.stderr}\n${result.stdout}`)
|
|
786
|
+
if (backfilled !== null) {
|
|
787
|
+
if (options.task && options.task.status !== 'cancelled') {
|
|
788
|
+
pushLine(options.task, '[pnpm log] DSH did not echo pnpm output — recovered the operation log from disk')
|
|
789
|
+
for (const line of backfilled.split(/\r?\n/)) if (line.trim() !== '') pushLine(options.task, line.trimEnd())
|
|
790
|
+
}
|
|
791
|
+
result = { ...result, stderr: `${result.stderr}\n${backfilled}` }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
779
794
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
780
795
|
return annotateNpmTooLow(result, options)
|
|
781
796
|
}
|