@luolingp/request-doctor 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +16 -4
- 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 +2 -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 +10 -6
- 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 +34 -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 +9 -9
- package/docs/quality-report.json +10 -10
- package/index.js +484 -464
- package/lib/env-shape.js +13 -0
- package/lib/env-verify.js +248 -159
- package/lib/restart-notice.js +21 -0
- package/lib/verdict-report.js +37 -0
- package/lib/version-repair.js +499 -495
- package/package.json +1 -1
package/lib/env-shape.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 描述一个插件的兼容环境形态:薄垫片几个、真实旧版本几个。
|
|
2
|
+
//
|
|
3
|
+
// 单独成文件是为了让 version-repair 保持在行数上限内,也让这段逻辑能被直接测。
|
|
4
|
+
import { classifyPackages, formatShape } from './env-verify.js';
|
|
5
|
+
import { bareNameOf } from './compat.js';
|
|
6
|
+
|
|
7
|
+
// @param {string} pluginDir - 插件在 profile 里的目录。
|
|
8
|
+
// @param {object} entry - 审计条目,只需要它的 importMap。
|
|
9
|
+
// @returns {string} 一句话,例如「薄垫片 4 个」或「真实旧版本 2 个(需重启):…」。
|
|
10
|
+
export function describeEnvironmentShape(pluginDir, entry) {
|
|
11
|
+
const names = Object.keys(entry.importMap ?? {}).map((spec) => bareNameOf(spec));
|
|
12
|
+
return formatShape(classifyPackages(pluginDir, names));
|
|
13
|
+
}
|
package/lib/env-verify.js
CHANGED
|
@@ -1,160 +1,249 @@
|
|
|
1
|
-
// 验证兼容环境真的能把插件要的东西交出来。
|
|
2
|
-
//
|
|
3
|
-
// 建完环境就假定成功是不够的:垫片写错一个导出名、目标文件读不出来,插件在加载那一刻
|
|
4
|
-
// 就会崩,而修复流程还以为自己干得不错。这里把「插件要的每个符号,环境是否真能提供」
|
|
5
|
-
// 逐条核对一遍,让问题在修复阶段暴露,而不是甩给用户。
|
|
6
|
-
import { readFileSync, statSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { bareNameOf, parseExportedNames } from './compat.js';
|
|
10
|
-
|
|
11
|
-
// 一个插件图里最多缓存这么多个包的结果。运行时包不小,每次扫描都重新解析太费。
|
|
12
|
-
const MAX_CACHE = 256;
|
|
13
|
-
|
|
14
|
-
// 缓存键带文件签名,改动自然失效。键算错不会导致误判——只是多解析一次。
|
|
15
|
-
const cache = new Map();
|
|
16
|
-
|
|
17
|
-
// 文件签名,读不到用固定占位(表示"读不到"本身也是一种稳定状态)。
|
|
18
|
-
function signatureOf(path) {
|
|
19
|
-
try {
|
|
20
|
-
const stat = statSync(path);
|
|
21
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
22
|
-
} catch {
|
|
23
|
-
return 'unreadable';
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 从垫片里取出 `export * from "..."` / `export { ... } from "..."` 指向的 URL。
|
|
28
|
-
//
|
|
29
|
-
// 这里保留 URL 而不是立刻转成路径:转路径在有些平台上会失败(比如没有盘符的 Windows
|
|
30
|
-
// 路径),那不代表垫片有问题,只说明这次核对不了。把"转换失败"和"没有目标"分开,
|
|
31
|
-
// 才不会把能正常跑的环境误判成坏的。
|
|
32
|
-
function targetUrlsOf(source) {
|
|
33
|
-
const urls = [];
|
|
34
|
-
const pattern = /\bfrom\s+"(file:[^"]+)"/g;
|
|
35
|
-
let match;
|
|
36
|
-
while ((match = pattern.exec(source)) !== null) urls.push(match[1]);
|
|
37
|
-
return urls;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 读一个文件,读不到就返回 undefined。
|
|
41
|
-
function readText(path) {
|
|
42
|
-
try {
|
|
43
|
-
return readFileSync(path, 'utf8');
|
|
44
|
-
} catch {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
// 验证兼容环境真的能把插件要的东西交出来。
|
|
2
|
+
//
|
|
3
|
+
// 建完环境就假定成功是不够的:垫片写错一个导出名、目标文件读不出来,插件在加载那一刻
|
|
4
|
+
// 就会崩,而修复流程还以为自己干得不错。这里把「插件要的每个符号,环境是否真能提供」
|
|
5
|
+
// 逐条核对一遍,让问题在修复阶段暴露,而不是甩给用户。
|
|
6
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { bareNameOf, parseExportedNames } from './compat.js';
|
|
10
|
+
|
|
11
|
+
// 一个插件图里最多缓存这么多个包的结果。运行时包不小,每次扫描都重新解析太费。
|
|
12
|
+
const MAX_CACHE = 256;
|
|
13
|
+
|
|
14
|
+
// 缓存键带文件签名,改动自然失效。键算错不会导致误判——只是多解析一次。
|
|
15
|
+
const cache = new Map();
|
|
16
|
+
|
|
17
|
+
// 文件签名,读不到用固定占位(表示"读不到"本身也是一种稳定状态)。
|
|
18
|
+
function signatureOf(path) {
|
|
19
|
+
try {
|
|
20
|
+
const stat = statSync(path);
|
|
21
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
22
|
+
} catch {
|
|
23
|
+
return 'unreadable';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 从垫片里取出 `export * from "..."` / `export { ... } from "..."` 指向的 URL。
|
|
28
|
+
//
|
|
29
|
+
// 这里保留 URL 而不是立刻转成路径:转路径在有些平台上会失败(比如没有盘符的 Windows
|
|
30
|
+
// 路径),那不代表垫片有问题,只说明这次核对不了。把"转换失败"和"没有目标"分开,
|
|
31
|
+
// 才不会把能正常跑的环境误判成坏的。
|
|
32
|
+
function targetUrlsOf(source) {
|
|
33
|
+
const urls = [];
|
|
34
|
+
const pattern = /\bfrom\s+"(file:[^"]+)"/g;
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = pattern.exec(source)) !== null) urls.push(match[1]);
|
|
37
|
+
return urls;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 读一个文件,读不到就返回 undefined。
|
|
41
|
+
function readText(path) {
|
|
42
|
+
try {
|
|
43
|
+
return readFileSync(path, 'utf8');
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// 环境里装的是一整个真实旧版本(不是垫片)时怎么核对。
|
|
52
|
+
//
|
|
53
|
+
// 这种形态是给「当前运行时已经删掉的导出」准备的:垫片转发不过去,只能装旧版。
|
|
54
|
+
// 所以这里不读垫片,读这个包自己的入口;读不到入口才算无法核对。
|
|
55
|
+
function verifyRealPackage(dir, symbols) {
|
|
56
|
+
const manifest = readText(join(dir, 'package.json'));
|
|
57
|
+
if (manifest === undefined) return { status: 'missing', detail: '没有垫片,也没有包清单' };
|
|
58
|
+
|
|
59
|
+
let entry = join(dir, 'index.js');
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(manifest);
|
|
62
|
+
const dot = parsed.exports?.['.'];
|
|
63
|
+
const target = typeof dot === 'string' ? dot : dot?.default;
|
|
64
|
+
if (typeof target === 'string') entry = join(dir, target);
|
|
65
|
+
else if (typeof parsed.main === 'string') entry = join(dir, parsed.main);
|
|
66
|
+
} catch {
|
|
67
|
+
// 清单读坏了就按 index.js 试,下面读不到会如实报出来。
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const source = readText(entry);
|
|
71
|
+
if (source === undefined) return { status: 'unverifiable', detail: '入口读不到' };
|
|
72
|
+
|
|
73
|
+
const names = parseExportedNames(entry, (file) => readFileSync(file, 'utf8')) ?? [];
|
|
74
|
+
const info = {
|
|
75
|
+
shimNames: [],
|
|
76
|
+
targetNames: names,
|
|
77
|
+
// 导出清单读不出来(CommonJS 包)时退回搜源码,和建环境那一侧保持同一口径。
|
|
78
|
+
needles: names.length === 0 ? [source] : [],
|
|
79
|
+
readableTargets: 1,
|
|
80
|
+
hasTargets: true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const lacking = symbols.filter((symbol) => {
|
|
84
|
+
if (symbol === 'default') {
|
|
85
|
+
return !(names.includes('default') || /\bexport\s+default\b/.test(source));
|
|
86
|
+
}
|
|
87
|
+
return !provides(info, symbol);
|
|
88
|
+
});
|
|
89
|
+
if (lacking.length === 0) return { status: 'provided', detail: '真实旧版本提供全部所需符号' };
|
|
90
|
+
return { status: 'missing', detail: '旧版本缺少 ' + lacking.join(', ') };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 一个包的环境能否满足它需要的符号。
|
|
94
|
+
//
|
|
95
|
+
// 三种结果:provided(能提供)、missing(能读到目标但确实没有)、unverifiable(目标读不到,
|
|
96
|
+
// 无从判断)。最后一种不算失败——运行时包常常装在归档里,普通读文件读不到是正常的。
|
|
97
|
+
export function verifyPackage(pluginDir, name, symbols) {
|
|
98
|
+
const dir = join(pluginDir, 'node_modules', ...name.split('/'));
|
|
99
|
+
const shimPath = join(dir, 'index.js');
|
|
100
|
+
const shimSource = readText(shimPath);
|
|
101
|
+
if (shimSource === undefined) return verifyRealPackage(dir, symbols);
|
|
102
|
+
|
|
103
|
+
// 垫片本身和它的目标都参与签名。这样运行时升级、垫片被改写都会自然失效,
|
|
104
|
+
// 而重复扫描同一个环境时不必再解析那个几百 KB 的运行时包。
|
|
105
|
+
const targets = targetUrlsOf(shimSource);
|
|
106
|
+
const targetPaths = targets.map((url) => {
|
|
107
|
+
try {
|
|
108
|
+
return fileURLToPath(url);
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const key = [shimPath, signatureOf(shimPath), ...targetPaths.map((path) => (path === undefined ? 'bad-url' : signatureOf(path)))].join('|');
|
|
114
|
+
|
|
115
|
+
const cached = cache.get(shimPath);
|
|
116
|
+
if (cached !== undefined && cached.key === key) return judge(cached.info, symbols);
|
|
117
|
+
|
|
118
|
+
const info = describeEnvironment(shimPath, shimSource, targetPaths);
|
|
119
|
+
if (cache.size >= MAX_CACHE && !cache.has(shimPath)) {
|
|
120
|
+
const oldest = cache.keys().next().value;
|
|
121
|
+
if (oldest !== undefined) cache.delete(oldest);
|
|
122
|
+
}
|
|
123
|
+
cache.set(shimPath, { key, info });
|
|
124
|
+
return judge(info, symbols);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 把一个环境能提供的符号收齐。
|
|
128
|
+
//
|
|
129
|
+
// 有些包是 CommonJS 的(schemastery 就是),读不出 ESM 的导出清单。这时候退回按词边界
|
|
130
|
+
// 搜源码——建环境那一侧本来就是这么做的,两边必须一致,否则一边说齐了、另一边说缺了,
|
|
131
|
+
// 插件就会在「修好了」和「没修好」之间来回跳。
|
|
132
|
+
function describeEnvironment(shimPath, shimSource, targetPaths) {
|
|
133
|
+
const shimNames = parseExportedNames(shimPath, (source) => readFileSync(source, 'utf8')) ?? [];
|
|
134
|
+
const targetNames = [];
|
|
135
|
+
const needles = [];
|
|
136
|
+
let readableTargets = 0;
|
|
137
|
+
for (const path of targetPaths) {
|
|
138
|
+
if (path === undefined) continue;
|
|
139
|
+
const source = readText(path);
|
|
140
|
+
if (source === undefined) continue;
|
|
141
|
+
readableTargets += 1;
|
|
142
|
+
const names = parseExportedNames(path, (file) => readFileSync(file, 'utf8'));
|
|
143
|
+
// 空清单同样不可信:CommonJS 包读出来就是空的。
|
|
144
|
+
if (names === undefined || names.length === 0) needles.push(source);
|
|
145
|
+
else targetNames.push(...names);
|
|
146
|
+
}
|
|
147
|
+
return { shimNames, targetNames, needles, readableTargets, hasTargets: targetPaths.length > 0 };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 一个符号在环境里到底有没有。名字对不上时退回搜源码。
|
|
151
|
+
function provides(info, symbol) {
|
|
152
|
+
if (info.shimNames.includes(symbol) || info.targetNames.includes(symbol)) return true;
|
|
153
|
+
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
const pattern = new RegExp('\\b' + escaped + '\\b');
|
|
155
|
+
return info.needles.some((source) => pattern.test(source));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 拿插件要的符号去对照环境能提供的,给出结论。
|
|
159
|
+
function judge(info, symbols) {
|
|
160
|
+
const lacking = symbols.filter((symbol) => {
|
|
161
|
+
// default 必须由垫片自己写出来:`export *` 从不转发默认导出,
|
|
162
|
+
// 目标有 default 不等于插件拿得到。
|
|
163
|
+
if (symbol === 'default') return !info.shimNames.includes('default');
|
|
164
|
+
return !provides(info, symbol);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (lacking.length === 0) return { status: 'provided' };
|
|
168
|
+
// 垫片指着的东西一个都读不到,这个环境是坏的:插件加载时自己也要读那些文件,
|
|
169
|
+
// 读不到就会崩。所以这里不能当「无法核对」放过去,要如实算作问题。
|
|
170
|
+
if (info.readableTargets === 0 && info.hasTargets) {
|
|
171
|
+
return { status: 'missing', detail: '垫片指向的文件一个都读不到,环境不可用' };
|
|
172
|
+
}
|
|
173
|
+
return { status: 'missing', detail: `缺 ${lacking.join(', ')}` };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 兼容环境里每个包到底是哪种形态。
|
|
177
|
+
//
|
|
178
|
+
// 薄垫片:只有 index.js,转发到现行实现,改完立刻有效。
|
|
179
|
+
// 真实旧版本:一整个包,用于当前运行时已经删掉的导出,改完必须重启才重新解析。
|
|
180
|
+
// 两者后果不同,所以报告里要分开写,而不是笼统说「建了若干个环境」。
|
|
181
|
+
export function classifyPackages(pluginDir, names) {
|
|
182
|
+
const shims = [];
|
|
183
|
+
const realVersions = [];
|
|
184
|
+
for (const name of names ?? []) {
|
|
185
|
+
const dir = join(pluginDir, 'node_modules', ...name.split('/'));
|
|
186
|
+
const manifest = readText(join(dir, 'package.json'));
|
|
187
|
+
if (manifest === undefined) {
|
|
188
|
+
// 连清单都没有:只有 index.js 的话,只可能是我们写的薄垫片。
|
|
189
|
+
if (readText(join(dir, 'index.js')) !== undefined) shims.push(name);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// 垫片也是完整的包(有自己的 package.json),靠描述里的标记认出来;
|
|
193
|
+
// 真实旧版本没有这个标记。
|
|
194
|
+
const isShim = manifest.includes('request-doctor compatibility environment');
|
|
195
|
+
if (isShim) shims.push(name);
|
|
196
|
+
else realVersions.push(name);
|
|
197
|
+
}
|
|
198
|
+
return { shims, realVersions };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 报一句某个插件的兼容环境是怎么建的。
|
|
202
|
+
//
|
|
203
|
+
// 薄垫片转发到现行实现,立刻有效;真实旧版本用于已被删掉的导出,必须重启才重新解析。
|
|
204
|
+
// 两种后果不同,报告里要分开写。
|
|
205
|
+
export function describeCompat(pluginDir, item, packageVersion) {
|
|
206
|
+
const names = Object.keys(item.importMap ?? {}).map((spec) => bareNameOf(spec));
|
|
207
|
+
const shape = classifyPackages(pluginDir, names);
|
|
208
|
+
const parts = [];
|
|
209
|
+
if (shape.shims.length > 0) parts.push(`薄垫片 ${shape.shims.length} 个`);
|
|
210
|
+
if (shape.realVersions.length > 0) parts.push(`真实旧版本 ${shape.realVersions.length} 个(${shape.realVersions.join(', ')})`);
|
|
211
|
+
const built = parts.length === 0 ? '无需建环境' : parts.join(',');
|
|
212
|
+
return `${packageVersion} given a ${item.runtimeVersion} compatibility environment:${built};声明已对齐`;
|
|
213
|
+
}
|
|
214
|
+
// 把形态压成一行,给审计结论用。真实旧版本要重启才生效,所以必须标出来。
|
|
215
|
+
export function formatShape(shape) {
|
|
216
|
+
if (shape.shims.length === 0 && shape.realVersions.length === 0) return '未建兼容环境';
|
|
217
|
+
const notes = [];
|
|
218
|
+
if (shape.shims.length > 0) notes.push(`薄垫片 ${shape.shims.length} 个`);
|
|
219
|
+
if (shape.realVersions.length > 0) notes.push(`真实旧版本 ${shape.realVersions.length} 个(需重启):${shape.realVersions.join(', ')}`);
|
|
220
|
+
return notes.join(';');
|
|
160
221
|
}
|
|
222
|
+
// 核对整个环境。
|
|
223
|
+
//
|
|
224
|
+
// @param {object} input
|
|
225
|
+
// @param {string} input.pluginDir - 插件目录。
|
|
226
|
+
// @param {Map<string,string[]>} input.imports - 插件导入的包与符号。
|
|
227
|
+
// @returns {{ok: boolean, problems: object[], unverified: object[]}}
|
|
228
|
+
export function verifyEnvironment(input) {
|
|
229
|
+
const problems = [];
|
|
230
|
+
const unverified = [];
|
|
231
|
+
// 同一个包可能既从包根、也从子路径被导入。垫片只有一个,所以要按裸包名归组,
|
|
232
|
+
// 把两处的符号合起来核对,否则子路径那条会被当成一个没有垫片的包。
|
|
233
|
+
const groups = new Map();
|
|
234
|
+
for (const [specifier, symbols] of input.imports ?? new Map()) {
|
|
235
|
+
if (!specifier.startsWith('@deepseek-ai/')) continue;
|
|
236
|
+
const name = bareNameOf(specifier);
|
|
237
|
+
if (groups.has(name) === false) groups.set(name, new Set());
|
|
238
|
+
const set = groups.get(name);
|
|
239
|
+
for (const symbol of symbols ?? []) set.add(symbol);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const [name, symbols] of groups) {
|
|
243
|
+
if (symbols.size === 0) continue;
|
|
244
|
+
const result = verifyPackage(input.pluginDir, name, [...symbols]);
|
|
245
|
+
if (result.status === 'missing') problems.push({ name, detail: result.detail });
|
|
246
|
+
else if (result.status === 'unverifiable') unverified.push({ name, detail: result.detail });
|
|
247
|
+
}
|
|
248
|
+
return { ok: problems.length === 0, problems, unverified };
|
|
249
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 「需要重启」的判定。
|
|
2
|
+
//
|
|
3
|
+
// 兼容环境的两种形态后果不同:
|
|
4
|
+
// - 薄垫片:转发到现行实现,改完立刻有效;
|
|
5
|
+
// - 真实旧版本:文件已经换好,但这个进程多半缓存过那次失败的导入,ESM 不会重新解析。
|
|
6
|
+
//
|
|
7
|
+
// 第二种只能重启,所以要明确告诉用户,而不是反复重试或报成环境失败。
|
|
8
|
+
|
|
9
|
+
const MISSING_EXPORT = /does not provide an export named '([^']+)'/;
|
|
10
|
+
|
|
11
|
+
// 从失败明细里认出「缺导出」这种情况,并把它记成一条待重启提示。
|
|
12
|
+
export function noteRestart(deps, name, detail) {
|
|
13
|
+
const missing = MISSING_EXPORT.exec(detail);
|
|
14
|
+
if (missing === null) return false;
|
|
15
|
+
const symbol = missing[1];
|
|
16
|
+
const hint = `${name} 已经装好需要的旧版本,但这个进程不会重新解析 ${symbol},重启 DSH 后即可生效`;
|
|
17
|
+
const box = deps.diagnostics.diagnostics;
|
|
18
|
+
box.restartRequired = [...(box.restartRequired ?? []), { name, symbol, hint }];
|
|
19
|
+
deps.record('restart-required', hint);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// 讲清某个插件为什么被拦下。
|
|
2
|
+
//
|
|
3
|
+
// 审计把判定、理由、缺的符号都记在诊断里,平时只在整轮扫描的输出里一闪而过。
|
|
4
|
+
// 装不上的人真正想问的就是「为什么」,所以要能直接查。
|
|
5
|
+
|
|
6
|
+
// @param {string} name - 插件包名。
|
|
7
|
+
// @param {object} deps - 组合根传进来的依赖,只需要 diagnostics。
|
|
8
|
+
// @returns {{kind: string, text: string}} 命令结果。
|
|
9
|
+
export function explainVerdict(name, deps) {
|
|
10
|
+
if (name === '') return { kind: 'error', text: 'usage: /doctor why <包名>' };
|
|
11
|
+
const audited = deps.diagnostics?.versionScan?.audited ?? [];
|
|
12
|
+
const hit = audited.find((item) => item.name === name);
|
|
13
|
+
if (hit === undefined) return { kind: 'error', text: missingText(name, audited) };
|
|
14
|
+
return { kind: 'success', text: reportLines(hit).join('\n') };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 一条一条拼出结论:判定、理由、缺的符号、环境,最后给一句能照做的建议。
|
|
18
|
+
function reportLines(hit) {
|
|
19
|
+
const lines = [`${hit.name}@${hit.version}:${hit.verdict}`];
|
|
20
|
+
for (const reason of hit.reasons ?? []) lines.push(` ${reason}`);
|
|
21
|
+
if ((hit.missing ?? []).length > 0) lines.push(` 缺符号:${hit.missing.join(', ')}`);
|
|
22
|
+
if (hit.environment !== undefined) lines.push(` 环境:${hit.environment}`);
|
|
23
|
+
lines.push(advice(hit.verdict));
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 没查到这个插件时说清楚现状,顺便列出已经审计过谁。
|
|
28
|
+
function missingText(name, audited) {
|
|
29
|
+
if (audited.length === 0) return `没有 ${name} 的审计结论:还没有扫描结果,先执行 /doctor 或等下一轮扫描`;
|
|
30
|
+
return `没有 ${name} 的审计结论。已审计的插件:${audited.map((item) => item.name).join(', ')}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 结论后面跟一句能直接照做的话。
|
|
34
|
+
function advice(verdict) {
|
|
35
|
+
if (verdict === 'compatible') return ' 结论:可以装。装不上就是管理器那边的检查没过,用 /doctor install 走本插件的路径。';
|
|
36
|
+
return ' 结论:要修得先让它要的东西能被提供;缺符号时本插件会尝试装入插件声明的旧版本。';
|
|
37
|
+
}
|