@luolingp/request-doctor 0.13.2 → 0.13.4
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/docs/01-/350/275/257/344/273/266/351/234/200/346/261/202/350/247/204/346/240/274/350/257/264/346/230/216.md +1 -1
- package/docs/02-/350/275/257/344/273/266/350/256/276/350/256/241/350/257/264/346/230/216.md +1 -1
- package/docs/03-/350/275/257/344/273/266/346/265/213/350/257/225/346/212/245/345/221/212.md +5 -3
- package/docs/04-/345/256/211/350/243/205/351/203/250/347/275/262/350/257/264/346/230/216.md +1 -1
- package/docs/05-/347/224/250/346/210/267/346/211/213/345/206/214.md +1 -1
- package/docs/06-/350/275/257/344/273/266/350/264/250/351/207/217/345/272/246/351/207/217/346/212/245/345/221/212.md +1 -1
- package/docs/quality-report.json +9 -9
- package/index.js +11 -42
- package/lib/allow-version.js +38 -0
- package/lib/panel-blocked.js +18 -0
- package/lib/refused-list.js +26 -0
- package/lib/verdict-report.js +168 -159
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 软件测试报告
|
|
2
2
|
|
|
3
3
|
**产品名称**:request-doctor(请求急救员)
|
|
4
|
-
**版本**:0.13.
|
|
4
|
+
**版本**:0.13.4
|
|
5
5
|
**编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.6;GB/T 8566-2007 验证与确认过程
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -62,9 +62,11 @@
|
|
|
62
62
|
| `test/client-half.test.mjs` | 9 | 客户端半:模块身份、注入声明、apply 注册、组件能渲染 |
|
|
63
63
|
| `test/diagnostics.test.mjs` | 3 | 快照可选写入:不配置不写盘、突发合并、坏路径不致命 |
|
|
64
64
|
| `test/restart-notice.test.mjs` | 3 |
|
|
65
|
+
| `test/allow-version.test.mjs` | 5 | 放行:版本必填、缺管理器拒绝、成功只调一次、失败如实报 |
|
|
66
|
+
| `test/panel-blocked.test.mjs` | 7 | 冲突判定:审计兼容却被拒装、清单形状解析、给 allow 动作 |
|
|
65
67
|
| `test/panel-briefs.test.mjs` | 6 | 界面摘要:按名索引、只送可见行、缺数据不抛、不污染诊断 |
|
|
66
68
|
| `test/verdict-report.test.mjs` | 29 | why 与 list:判定、理由、环境摘要、自动扫描、分组、缺口、按形态给建议 | 缺导出判为需要重启;其它失败不误判;多次累积不覆盖 |
|
|
67
|
-
| **合计** | **
|
|
69
|
+
| **合计** | **211** | **全部通过** |
|
|
68
70
|
|
|
69
71
|
执行结果:
|
|
70
72
|
|
|
@@ -217,4 +219,4 @@
|
|
|
217
219
|
|
|
218
220
|
### 7.2 分文件执行结果
|
|
219
221
|
|
|
220
|
-
|
|
222
|
+
34 个测试文件逐个单独执行,每个都有用例且全部通过,不存在因导入失败而静默跳过的文件。用例数分布:compat.test.mjs 17、plugin.test.mjs 15、compat-env.test.mjs 14、classify.test.mjs 13,其余按职能分散,合计 211。
|
package/docs/quality-report.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"standard": "GB/T 45717-2025 信息技术 软件测量 软件质量测量 自动化的源代码质量测度",
|
|
3
|
-
"generatedAt": "2026-10-29T16:
|
|
3
|
+
"generatedAt": "2026-10-29T16:21:27.245Z",
|
|
4
4
|
"scope": {
|
|
5
|
-
"files":
|
|
6
|
-
"totalLines":
|
|
7
|
-
"sourceLines":
|
|
8
|
-
"commentLines":
|
|
5
|
+
"files": 35,
|
|
6
|
+
"totalLines": 5313,
|
|
7
|
+
"sourceLines": 3841,
|
|
8
|
+
"commentLines": 982
|
|
9
9
|
},
|
|
10
10
|
"measures": {
|
|
11
|
-
"commentDensity": 25.
|
|
12
|
-
"duplicationPercent": 4.
|
|
13
|
-
"functionCount":
|
|
11
|
+
"commentDensity": 25.57,
|
|
12
|
+
"duplicationPercent": 4.97,
|
|
13
|
+
"functionCount": 406,
|
|
14
14
|
"maxComplexity": 10,
|
|
15
15
|
"maxFunctionLines": 59,
|
|
16
16
|
"maxFileLines": 500,
|
|
17
|
-
"averageComplexity": 3.
|
|
17
|
+
"averageComplexity": 3.87
|
|
18
18
|
},
|
|
19
19
|
"limits": {
|
|
20
20
|
"maxComplexity": 10,
|
package/index.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// 这个文件是装配处:只管生命周期和监听器,具体判断都在 lib/ 里。
|
|
20
20
|
import { explainVerdict, listAudited } from './lib/verdict-report.js';
|
|
21
21
|
import { panelBriefs } from './lib/panel-briefs.js';
|
|
22
|
+
import { readRefused, refusedRowsOf } from './lib/refused-list.js';
|
|
23
|
+
import { allowVersion } from './lib/allow-version.js';
|
|
22
24
|
import { parseRetryConfig } from './lib/classify.js';
|
|
23
25
|
import { collectPackageFacts, entryPathOf } from './lib/compat-repair.js';
|
|
24
26
|
import { unmetPeers } from './lib/compat.js';
|
|
@@ -219,18 +221,6 @@ function registerDoctorCommand(ctx, deps) {
|
|
|
219
221
|
});
|
|
220
222
|
}
|
|
221
223
|
|
|
222
|
-
// 一行清单里被拒的那个包;没有就返回 undefined。
|
|
223
|
-
function refusedRowOf(row) {
|
|
224
|
-
const first = row?.error?.incompatible?.[0];
|
|
225
|
-
if (first === undefined) return undefined;
|
|
226
|
-
return { name: first.name, version: first.version, runtimeVersion: first.runtimeVersion };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 从管理器给的清单里挑出被拒装的包:只有带 incompatible 的行才算。
|
|
230
|
-
function refusedRowsOf(result) {
|
|
231
|
-
return (result?.value ?? []).map(refusedRowOf).filter((row) => row !== undefined);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
224
|
// 汇总当前需要处理的插件:本插件扫描的审计结果,加上管理器的拒装记录,按名去重。
|
|
235
225
|
async function collectPanelRows(deps, manager) {
|
|
236
226
|
const runtimeVersion = deps.probe.runtimeFacts().runtimeVersion;
|
|
@@ -353,13 +343,13 @@ async function runDoctorCommand(invocation, deps) {
|
|
|
353
343
|
if (sub === 'allow') return allowVersionCommand(rest.join(' '), deps);
|
|
354
344
|
// why 回答「这个插件为什么被判为不可兼容」,答案就在最近一轮审计里。
|
|
355
345
|
// list 列出所有审计过的插件,先看全貌再用 why 追问。
|
|
356
|
-
if (sub === 'list') return listAudited(deps);
|
|
346
|
+
if (sub === 'list') return listAudited({ ...deps, refusedRows: await readRefused(deps.probe?.get?.('pluginManager')) });
|
|
357
347
|
if (sub === 'why') return explainVerdict(rest.join(' '), deps);
|
|
358
348
|
return summarizeRepairs(deps);
|
|
359
349
|
}
|
|
360
350
|
|
|
361
351
|
|
|
362
|
-
|
|
352
|
+
|
|
363
353
|
async function prepareInstallCommand(spec, deps) {
|
|
364
354
|
if (spec === '') return { kind: 'error', text: 'usage: /doctor install <目录路径或包名> — 两者都行' };
|
|
365
355
|
|
|
@@ -403,33 +393,12 @@ async function prepareInstallCommand(spec, deps) {
|
|
|
403
393
|
// 而检查只认显式授权。所以给用户一个在点安装之前把这件事做掉的入口:
|
|
404
394
|
// 先 /doctor allow,再在界面上装,装完扫描会自动把声明对齐、环境建好。
|
|
405
395
|
async function allowVersionCommand(spec, deps) {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// 授权必须有明确的运行时版本和管理器实例,缺一个都做不了。
|
|
413
|
-
const runtimeVersion = deps.probe.runtimeFacts().runtimeVersion ?? '';
|
|
414
|
-
const ready = runtimeVersion !== '' && deps.pluginManager !== undefined;
|
|
415
|
-
if (ready === false) return { kind: 'error', text: '拿不到当前 DSH 版本或管理器不可用,无法授权' };
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
await deps.pluginManager.setVersionExemption(trimmed, runtimeVersion, true, true);
|
|
419
|
-
} catch (error) {
|
|
420
|
-
return { kind: 'error', text: `授权失败:${String(error)}` };
|
|
421
|
-
}
|
|
422
|
-
const note = `已放行 ${trimmed} 在 ${runtimeVersion} 上运行`;
|
|
423
|
-
deps.record('version-allowed', note);
|
|
424
|
-
|
|
425
|
-
return {
|
|
426
|
-
kind: 'success',
|
|
427
|
-
text: [
|
|
428
|
-
`已授权 ${trimmed} 运行在 ${runtimeVersion}`,
|
|
429
|
-
'现在可以在插件面板里正常安装它了。',
|
|
430
|
-
'装完之后,声明对齐和兼容环境会自动补上。',
|
|
431
|
-
].join('\n'),
|
|
432
|
-
};
|
|
396
|
+
const facts = deps.probe.runtimeFacts();
|
|
397
|
+
return allowVersion(spec, {
|
|
398
|
+
runtimeVersion: facts.runtimeVersion,
|
|
399
|
+
manager: deps.pluginManager,
|
|
400
|
+
record: deps.record,
|
|
401
|
+
});
|
|
433
402
|
}
|
|
434
403
|
|
|
435
404
|
// 自己装一个注册表里的包。
|
|
@@ -494,4 +463,4 @@ async function summarizeRepairs(deps) {
|
|
|
494
463
|
for (const item of waiting) lines.push(`- ${item.hint}`);
|
|
495
464
|
}
|
|
496
465
|
return { kind: 'success', text: lines.join('\n') };
|
|
497
|
-
}
|
|
466
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 放行一个插件在某个运行时版本上运行。
|
|
2
|
+
//
|
|
3
|
+
// /doctor allow 与界面上的按钮都要做这件事,规则只有一份:
|
|
4
|
+
// 必须写清版本,必须有当前运行时版本和管理器实例。
|
|
5
|
+
|
|
6
|
+
// @param {string} spec - 形如 包名@版本。
|
|
7
|
+
// @param {object} input - { runtimeVersion, manager, record }。
|
|
8
|
+
// @returns {Promise<{kind: string, text: string}>} 命令结果。
|
|
9
|
+
export async function allowVersion(spec, input) {
|
|
10
|
+
const trimmed = String(spec ?? '').trim();
|
|
11
|
+
if (trimmed === '') return { kind: 'error', text: '用法:/doctor allow <包名@版本>' };
|
|
12
|
+
if (trimmed.includes('@', 1) === false) return { kind: 'error', text: '要写清楚版本,例如 /doctor allow dsh-ssh-ops@0.4.0' };
|
|
13
|
+
|
|
14
|
+
const runtimeVersion = input.runtimeVersion ?? '';
|
|
15
|
+
if (runtimeVersion === '' || input.manager === undefined) return { kind: 'error', text: '拿不到当前 DSH 版本或管理器不可用,无法授权' };
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await input.manager.setVersionExemption(trimmed, runtimeVersion, true, true);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return { kind: 'error', text: `授权失败:${String(error)}` };
|
|
21
|
+
}
|
|
22
|
+
input.record?.('version-allowed', `已放行 ${trimmed} 在 ${runtimeVersion} 上运行`);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
kind: 'success',
|
|
26
|
+
text: [
|
|
27
|
+
`已授权 ${trimmed} 运行在 ${runtimeVersion}`,
|
|
28
|
+
'现在可以在插件面板里正常安装它了。',
|
|
29
|
+
'装完之后,声明对齐和兼容环境会自动补上。',
|
|
30
|
+
].join('\n'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// 没写版本时给一条能直接照抄的例子,省得去翻文档。
|
|
34
|
+
export function allowHint(name) {
|
|
35
|
+
const base = String(name ?? '').trim();
|
|
36
|
+
if (base === '') return '用法:/doctor allow <包名@版本>';
|
|
37
|
+
return `要在插件页安装它,先执行:/doctor allow ${base}@<版本>`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// 审计兼容但管理器拒装的插件。
|
|
2
|
+
//
|
|
3
|
+
// 两边结论冲突时,用户看到的是一句「不兼容」却查不到原因。
|
|
4
|
+
// 这里把这种冲突找出来,交给调用方明说并给出授权动作。
|
|
5
|
+
|
|
6
|
+
// @param {object[]} audited - 审计条目。
|
|
7
|
+
// @param {object[]} refused - 管理器拒装记录,每项至少含 name。
|
|
8
|
+
// @returns {string[]} 冲突的插件名。
|
|
9
|
+
export function blockedButCompatible(audited, refused) {
|
|
10
|
+
const refusedNames = new Set((refused ?? []).map((row) => row.name));
|
|
11
|
+
const names = [];
|
|
12
|
+
for (const item of audited ?? []) {
|
|
13
|
+
if (item.verdict !== 'compatible') continue;
|
|
14
|
+
if (!refusedNames.has(item.name)) continue;
|
|
15
|
+
names.push(item.name);
|
|
16
|
+
}
|
|
17
|
+
return names;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// 读一次插件管理器的拒装记录。
|
|
2
|
+
//
|
|
3
|
+
// /doctor list 要报「审计说兼容、管理器仍拒装」的冲突,界面也要用它标来源。
|
|
4
|
+
// 两处都需要这份清单,读法就放在这里,读不动时各自决定怎么兜。
|
|
5
|
+
|
|
6
|
+
// 一行清单里被拒的那个包;没有就返回 undefined。
|
|
7
|
+
export function refusedRowOf(row) {
|
|
8
|
+
const first = row?.error?.incompatible?.[0];
|
|
9
|
+
if (first === undefined) return undefined;
|
|
10
|
+
return { name: first.name, version: first.version, runtimeVersion: first.runtimeVersion };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 从管理器给的清单里挑出被拒装的包:只有带 incompatible 的行才算。
|
|
14
|
+
export function refusedRowsOf(result) {
|
|
15
|
+
return (result?.value ?? []).map(refusedRowOf).filter((row) => row !== undefined);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 直接问管理器要一份。读不动就是空数组——调用方据此只报审计结论。
|
|
19
|
+
export async function readRefused(manager) {
|
|
20
|
+
if (manager === undefined) return [];
|
|
21
|
+
try {
|
|
22
|
+
return refusedRowsOf(await manager.listBundles());
|
|
23
|
+
} catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
package/lib/verdict-report.js
CHANGED
|
@@ -1,159 +1,168 @@
|
|
|
1
|
-
import { linesText } from './text-lines.js';
|
|
2
|
-
|
|
3
|
-
// 已经在扫的时候,告诉用户等一下,而不是报「没有结果」。
|
|
4
|
-
// 有插件需要处理时给出的动作提示。诊断必须配一句能照做的事。
|
|
5
|
-
const NEXT_STEP = '修复会在下一轮扫描时自动尝试;想立刻重试:/doctor install <包名>';
|
|
6
|
-
|
|
7
|
-
const BUSY_TEXT = '正在扫描,几秒后再执行 /doctor list 就能看到结果。';
|
|
8
|
-
|
|
9
|
-
// 最近一轮审计的清单。没扫过就是空数组。
|
|
10
|
-
function auditedOf(deps) {
|
|
11
|
-
return deps.diagnostics?.versionScan?.audited ?? [];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
import { ensureAudited } from './scan-bridge.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// @param {
|
|
23
|
-
// @
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return { kind: '
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (hit.
|
|
48
|
-
lines.push(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return `没有 ${name}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
lines.push('',
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
for (const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const gaps =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
1
|
+
import { linesText } from './text-lines.js';
|
|
2
|
+
|
|
3
|
+
// 已经在扫的时候,告诉用户等一下,而不是报「没有结果」。
|
|
4
|
+
// 有插件需要处理时给出的动作提示。诊断必须配一句能照做的事。
|
|
5
|
+
const NEXT_STEP = '修复会在下一轮扫描时自动尝试;想立刻重试:/doctor install <包名>';
|
|
6
|
+
|
|
7
|
+
const BUSY_TEXT = '正在扫描,几秒后再执行 /doctor list 就能看到结果。';
|
|
8
|
+
|
|
9
|
+
// 最近一轮审计的清单。没扫过就是空数组。
|
|
10
|
+
function auditedOf(deps) {
|
|
11
|
+
return deps.diagnostics?.versionScan?.audited ?? [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
import { ensureAudited } from './scan-bridge.js';
|
|
15
|
+
import { blockedButCompatible } from './panel-blocked.js';
|
|
16
|
+
|
|
17
|
+
// 讲清某个插件为什么被拦下。
|
|
18
|
+
//
|
|
19
|
+
// 审计把判定、理由、缺的符号都记在诊断里,平时只在整轮扫描的输出里一闪而过。
|
|
20
|
+
// 装不上的人真正想问的就是「为什么」,所以要能直接查。
|
|
21
|
+
|
|
22
|
+
// @param {string} name - 插件包名。
|
|
23
|
+
// @param {object} deps - 组合根传进来的依赖,只需要 diagnostics。
|
|
24
|
+
// @returns {{kind: string, text: string}} 命令结果。
|
|
25
|
+
export async function explainVerdict(name, deps) {
|
|
26
|
+
if (name === '') return { kind: 'error', text: 'usage: /doctor why <包名>' };
|
|
27
|
+
const found = find(name, deps);
|
|
28
|
+
if (found !== undefined) return { kind: 'success', text: linesText(reportLines(found)) };
|
|
29
|
+
|
|
30
|
+
// 和 list 一样:没有扫描结果就先扫一轮,正在扫就告诉用户等一下。
|
|
31
|
+
const state = await ensureAudited(deps.versionRepair);
|
|
32
|
+
if (state.busy === true) return { kind: 'success', text: BUSY_TEXT };
|
|
33
|
+
const again = find(name, deps);
|
|
34
|
+
if (again !== undefined) return { kind: 'success', text: linesText(reportLines(again)) };
|
|
35
|
+
return { kind: 'error', text: missingText(name, auditedOf(deps)) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 在最近一轮审计里按包名找一条。
|
|
39
|
+
function find(name, deps) {
|
|
40
|
+
return auditedOf(deps).find((item) => item.name === name);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 一条一条拼出结论:判定、理由、缺的符号、环境,最后给一句能照做的建议。
|
|
44
|
+
function reportLines(hit) {
|
|
45
|
+
const lines = [`${hit.name}@${hit.version}:${hit.verdict}`];
|
|
46
|
+
for (const reason of hit.reasons ?? []) lines.push(` ${reason}`);
|
|
47
|
+
if ((hit.missing ?? []).length > 0) lines.push(` 缺符号:${hit.missing.join(', ')}`);
|
|
48
|
+
if (hit.environment !== undefined) lines.push(` 环境:${hit.environment}`);
|
|
49
|
+
lines.push(advice(hit));
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 没查到这个插件时说清楚现状,顺便列出已经审计过谁。
|
|
54
|
+
function missingText(name, audited) {
|
|
55
|
+
if (audited.length === 0) return `没有 ${name} 的审计结论:还没有扫描结果,先执行 /doctor 或等下一轮扫描`;
|
|
56
|
+
return `没有 ${name} 的审计结论。已审计的插件:${audited.map((item) => item.name).join(', ')}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 结论后面跟一句能直接照做的话。
|
|
60
|
+
//
|
|
61
|
+
// 环境形态决定下一步:旧版本已经装好的只差重启,没环境的等下一轮扫描,
|
|
62
|
+
// 兼容的则要去走安装路径——三种情况给同一句话等于没说。
|
|
63
|
+
function advice(hit) {
|
|
64
|
+
if (hit.verdict === 'compatible') return ' 结论:可以装。装不上就是管理器那边的检查没过,用 /doctor install 走本插件的路径。';
|
|
65
|
+
|
|
66
|
+
const environment = hit.environment ?? '';
|
|
67
|
+
if (environment.includes('真实旧版本')) {
|
|
68
|
+
return ' 结论:它要的旧版本已经装好,重启 DSH 后重新扫描即可生效。';
|
|
69
|
+
}
|
|
70
|
+
if (environment.includes('薄垫片') && (hit.missing ?? []).length === 0) {
|
|
71
|
+
return ' 结论:环境已按现行实现转发,若仍装不上用 /doctor install <包名> 重试。';
|
|
72
|
+
}
|
|
73
|
+
return ' 结论:下一轮扫描会尝试装入插件声明的旧版本;也可直接 /doctor install <包名> 重试。';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// /doctor list 的入口:没有扫描结果就先扫一轮。
|
|
77
|
+
//
|
|
78
|
+
// 刚启动时诊断里什么都没有,直接说「还没有结果」等于把活推回给用户。
|
|
79
|
+
// 扫描本身是幂等的,顺手做掉更省事。
|
|
80
|
+
export async function listAudited(deps) {
|
|
81
|
+
if (auditedOf(deps).length > 0) return formatList(deps);
|
|
82
|
+
const state = await ensureAudited(deps.versionRepair);
|
|
83
|
+
if (state.busy === true) return { kind: 'success', text: BUSY_TEXT };
|
|
84
|
+
return formatList(deps);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 列出所有被审计过的插件:不先知道包名也能看现状。
|
|
88
|
+
//
|
|
89
|
+
// 按判定分组输出:需要处理的排在前面。混在一起时,扫一眼看不出问题在哪。
|
|
90
|
+
export function formatList(deps) {
|
|
91
|
+
const audited = auditedOf(deps);
|
|
92
|
+
if (audited.length === 0) return { kind: 'success', text: '还没有扫描结果,先执行 /doctor 或等下一轮扫描' };
|
|
93
|
+
|
|
94
|
+
const broken = audited.filter((item) => item.verdict !== 'compatible');
|
|
95
|
+
const fine = audited.filter((item) => item.verdict === 'compatible');
|
|
96
|
+
const width = Math.max(...audited.map((item) => item.name.length));
|
|
97
|
+
|
|
98
|
+
const lines = [`已审计 ${audited.length} 个插件:`];
|
|
99
|
+
if (broken.length > 0) pushGroup(lines, `需要处理 ${broken.length} 个`, broken, width);
|
|
100
|
+
if (fine.length > 0) pushGroup(lines, `可以直接用 ${fine.length} 个`, fine, width);
|
|
101
|
+
|
|
102
|
+
// 几个插件被同一个符号卡住时,修一次能全解开,值得单独点出来。
|
|
103
|
+
if (broken.length > 0) {
|
|
104
|
+
const gap = gapSummary(broken);
|
|
105
|
+
if (gap !== undefined) lines.push('', `先修这个:${gap}`);
|
|
106
|
+
lines.push('', NEXT_STEP);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 审计说兼容、管理器仍拒装:两边结论冲突,必须单独说,并给出授权动作。
|
|
110
|
+
const conflict = blockedButCompatible(audited, deps.refusedRows ?? []);
|
|
111
|
+
if (conflict.length > 0) {
|
|
112
|
+
lines.push('', `审计认为可用但被拒装 ${conflict.length} 个:${conflict.join(', ')}`);
|
|
113
|
+
lines.push(' 这些插件本身没问题,是版本检查没过。用 /doctor allow <包名@版本> 授权后再装。');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lines.push('', '用 /doctor why <包名> 看某个插件的完整理由。');
|
|
117
|
+
return { kind: 'success', text: linesText(lines) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 一个小标题加若干行。
|
|
121
|
+
function pushGroup(lines, title, items, width) {
|
|
122
|
+
lines.push('', `${title}:`);
|
|
123
|
+
for (const item of items) lines.push(` ${rowOf(item, width)}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 一行:名字对齐,后面跟判定和环境提示。
|
|
127
|
+
function rowOf(item, width) {
|
|
128
|
+
const name = item.name.padEnd(width);
|
|
129
|
+
const where = item.environment === undefined ? '' : ` ${shortEnvironment(item.environment)}`;
|
|
130
|
+
return `${name} ${item.verdict}${where}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 哪些符号是多个插件共同缺的。
|
|
134
|
+
//
|
|
135
|
+
// 一个符号被两个以上插件需要、又都不在运行时里,这是最值得先处理的事:
|
|
136
|
+
// 修一次能同时解开好几个插件,用户不必逐个 /doctor why 去拼出这件事。
|
|
137
|
+
export function commonGaps(audited) {
|
|
138
|
+
const owners = new Map();
|
|
139
|
+
for (const item of audited ?? []) {
|
|
140
|
+
for (const symbol of item.missing ?? []) {
|
|
141
|
+
const list = owners.get(symbol) ?? [];
|
|
142
|
+
list.push(item.name);
|
|
143
|
+
owners.set(symbol, list);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const gaps = [];
|
|
148
|
+
for (const [symbol, names] of owners) {
|
|
149
|
+
if (names.length >= 2) gaps.push({ symbol, plugins: names });
|
|
150
|
+
}
|
|
151
|
+
return gaps.sort((a, b) => b.plugins.length - a.plugins.length);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 把共同缺口说成一句话;没有时返回 undefined。
|
|
155
|
+
export function gapSummary(audited) {
|
|
156
|
+
const gaps = commonGaps(audited);
|
|
157
|
+
if (gaps.length === 0) return undefined;
|
|
158
|
+
const first = gaps[0];
|
|
159
|
+
return `${first.plugins.length} 个插件都缺 ${first.symbol}(${first.plugins.join(', ')})`;
|
|
160
|
+
}
|
|
161
|
+
// 列表里只用环境的摘要:把包名列表整段丢掉,超长再截。
|
|
162
|
+
//
|
|
163
|
+
// 「真实旧版本 3 个(需重启):@deepseek…」把冒号和半截包名挤在一起,读起来别扭;
|
|
164
|
+
// 包名列表留给 /doctor why 输出。
|
|
165
|
+
export function shortEnvironment(text) {
|
|
166
|
+
const head = text.split(':')[0].split(';')[0];
|
|
167
|
+
return head.length > 30 ? `${head.slice(0, 30)}…` : head;
|
|
168
|
+
}
|