openyida 1.0.0-beta.2 → 1.0.0-beta.3
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/bin/yida.js +59 -0
- package/lib/doctor.js +1503 -0
- package/package.json +1 -1
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.js - 宜搭 CLI 应用自动诊断模块
|
|
3
|
+
*
|
|
4
|
+
* 提供环境检查、应用诊断、智能修复、报告生成、健康监控等功能。
|
|
5
|
+
*
|
|
6
|
+
* 导出类:
|
|
7
|
+
* DiagnosticEngine - 诊断引擎核心调度器(三层架构)
|
|
8
|
+
* EnvironmentChecker - 环境诊断(Node/Python/Playwright/gh/config/Skills/登录态/网络)
|
|
9
|
+
* ApplicationChecker - 应用诊断(PRD/页面源码/Schema/React Hooks 检测)
|
|
10
|
+
* FixEngine - 智能修复引擎(自动修复/手动提示/命令执行)
|
|
11
|
+
* ReportGenerator - 诊断报告生成(JSON/Markdown/HTML)
|
|
12
|
+
* PreChecker - 预检查(发布前/创建前自动检查)
|
|
13
|
+
* HealthMonitor - 持续健康度监控与趋势分析
|
|
14
|
+
* ProductionErrorCollector - 线上错误诊断与智能分析
|
|
15
|
+
* TicketCreator - 工单创建(集成 GitHub Issues)
|
|
16
|
+
* VOCCreator - VOC 创建(业务价值分析/优先级建议)
|
|
17
|
+
* SubmissionDecider - 智能提交决策(自动判断工单/VOC)
|
|
18
|
+
*
|
|
19
|
+
* 导出函数:
|
|
20
|
+
* run(args) - CLI 入口,解析参数并执行诊断
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
const fs = require("fs");
|
|
26
|
+
const path = require("path");
|
|
27
|
+
const { execSync } = require("child_process");
|
|
28
|
+
const { findProjectRoot, loadCookieData, extractInfoFromCookies } = require("./utils");
|
|
29
|
+
|
|
30
|
+
// ── 诊断结果常量 ──────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
const Severity = {
|
|
33
|
+
ERROR: "error",
|
|
34
|
+
WARNING: "warning",
|
|
35
|
+
INFO: "info",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const FixType = {
|
|
39
|
+
AUTO: "auto",
|
|
40
|
+
MANUAL: "manual",
|
|
41
|
+
COMMAND: "command",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ── DiagnosticEngine ──────────────────────────────────
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 诊断引擎核心调度器。
|
|
48
|
+
* 三层架构:注册 Checker → 执行诊断 → 汇总结果。
|
|
49
|
+
*/
|
|
50
|
+
class DiagnosticEngine {
|
|
51
|
+
constructor({ projectRoot } = {}) {
|
|
52
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
53
|
+
this.checkers = [];
|
|
54
|
+
this.results = [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 注册诊断检查器。
|
|
59
|
+
* @param {object} checker - 需实现 check() 方法,返回诊断结果数组
|
|
60
|
+
*/
|
|
61
|
+
registerChecker(checker) {
|
|
62
|
+
this.checkers.push(checker);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 执行所有已注册的检查器。
|
|
67
|
+
* @returns {Promise<Array>} 所有诊断结果
|
|
68
|
+
*/
|
|
69
|
+
async runAll() {
|
|
70
|
+
this.results = [];
|
|
71
|
+
for (const checker of this.checkers) {
|
|
72
|
+
const checkerResults = await checker.check();
|
|
73
|
+
this.results.push(...checkerResults);
|
|
74
|
+
}
|
|
75
|
+
return this.results;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取可自动修复的问题列表。
|
|
80
|
+
* @returns {Array}
|
|
81
|
+
*/
|
|
82
|
+
getAutoFixableIssues() {
|
|
83
|
+
return this.results.filter(
|
|
84
|
+
(result) => result.fixType === FixType.AUTO && result.severity === Severity.ERROR
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取诊断汇总信息。
|
|
90
|
+
* @returns {object}
|
|
91
|
+
*/
|
|
92
|
+
getSummary() {
|
|
93
|
+
const errorCount = this.results.filter((r) => r.severity === Severity.ERROR).length;
|
|
94
|
+
const warningCount = this.results.filter((r) => r.severity === Severity.WARNING).length;
|
|
95
|
+
const infoCount = this.results.filter((r) => r.severity === Severity.INFO).length;
|
|
96
|
+
const autoFixable = this.getAutoFixableIssues().length;
|
|
97
|
+
const passed = this.results.filter((r) => r.passed).length;
|
|
98
|
+
const total = this.results.length;
|
|
99
|
+
|
|
100
|
+
return { total, passed, errorCount, warningCount, infoCount, autoFixable };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 格式化控制台输出。
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
formatConsoleOutput() {
|
|
108
|
+
const lines = [];
|
|
109
|
+
for (const result of this.results) {
|
|
110
|
+
const icon = result.passed ? "✅" : result.severity === Severity.ERROR ? "❌" : "⚠️ ";
|
|
111
|
+
lines.push(`${icon} ${result.label}`);
|
|
112
|
+
if (!result.passed && result.message) {
|
|
113
|
+
lines.push(` ${result.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const summary = this.getSummary();
|
|
118
|
+
lines.push("");
|
|
119
|
+
if (summary.errorCount === 0 && summary.warningCount === 0) {
|
|
120
|
+
lines.push("🎉 所有检查通过,环境配置完整!");
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(
|
|
123
|
+
`发现 ${summary.total - summary.passed} 个问题(${summary.errorCount} 个错误,${summary.warningCount} 个警告)`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return lines.join("\n");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── EnvironmentChecker ────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 环境诊断检查器。
|
|
135
|
+
* 检查 Node.js、Python、Playwright、gh CLI、config.json、Skills、登录态、网络连通性。
|
|
136
|
+
*/
|
|
137
|
+
class EnvironmentChecker {
|
|
138
|
+
constructor({ projectRoot } = {}) {
|
|
139
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 执行所有环境检查。
|
|
144
|
+
* @returns {Promise<Array>}
|
|
145
|
+
*/
|
|
146
|
+
async check() {
|
|
147
|
+
return [
|
|
148
|
+
this.checkNodeVersion(),
|
|
149
|
+
this.checkPythonVersion(),
|
|
150
|
+
this.checkPlaywrightInstalled(),
|
|
151
|
+
this.checkPlaywrightChromium(),
|
|
152
|
+
this.checkGhCli(),
|
|
153
|
+
this.checkGhAuth(),
|
|
154
|
+
this.checkConfig(),
|
|
155
|
+
this.checkSkills(),
|
|
156
|
+
this.checkLoginStatus(),
|
|
157
|
+
await this.checkNetwork(),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
checkNodeVersion() {
|
|
162
|
+
const nodeVersion = process.versions.node;
|
|
163
|
+
const major = parseInt(nodeVersion.split(".")[0], 10);
|
|
164
|
+
const passed = major >= 16;
|
|
165
|
+
return {
|
|
166
|
+
id: "env-node",
|
|
167
|
+
label: `Node.js v${nodeVersion}(要求 ≥ 16)`,
|
|
168
|
+
passed,
|
|
169
|
+
severity: passed ? Severity.INFO : Severity.ERROR,
|
|
170
|
+
message: passed ? null : `Node.js 版本过低(${nodeVersion}),请升级到 v16+`,
|
|
171
|
+
fixType: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
checkPythonVersion() {
|
|
176
|
+
try {
|
|
177
|
+
const pythonVersion = execSync("python3 --version 2>&1", { encoding: "utf-8" }).trim();
|
|
178
|
+
const versionMatch = pythonVersion.match(/Python (\d+)\.(\d+)/);
|
|
179
|
+
if (versionMatch) {
|
|
180
|
+
const major = parseInt(versionMatch[1], 10);
|
|
181
|
+
const minor = parseInt(versionMatch[2], 10);
|
|
182
|
+
const passed = major > 3 || (major === 3 && minor >= 10);
|
|
183
|
+
return {
|
|
184
|
+
id: "env-python",
|
|
185
|
+
label: `${pythonVersion}(要求 ≥ 3.10)`,
|
|
186
|
+
passed,
|
|
187
|
+
severity: passed ? Severity.INFO : Severity.ERROR,
|
|
188
|
+
message: passed ? null : `Python 版本过低(${pythonVersion}),请升级到 3.10+`,
|
|
189
|
+
fixType: null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id: "env-python",
|
|
194
|
+
label: "Python 版本检测",
|
|
195
|
+
passed: false,
|
|
196
|
+
severity: Severity.ERROR,
|
|
197
|
+
message: "无法解析 Python 版本",
|
|
198
|
+
fixType: null,
|
|
199
|
+
};
|
|
200
|
+
} catch {
|
|
201
|
+
return {
|
|
202
|
+
id: "env-python",
|
|
203
|
+
label: "Python 3 安装检测",
|
|
204
|
+
passed: false,
|
|
205
|
+
severity: Severity.ERROR,
|
|
206
|
+
message: "未找到 python3,请安装:https://www.python.org/",
|
|
207
|
+
fixType: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
checkPlaywrightInstalled() {
|
|
213
|
+
try {
|
|
214
|
+
execSync('python3 -c "import playwright"', { encoding: "utf-8", stdio: "pipe" });
|
|
215
|
+
return {
|
|
216
|
+
id: "env-playwright",
|
|
217
|
+
label: "Playwright 已安装",
|
|
218
|
+
passed: true,
|
|
219
|
+
severity: Severity.INFO,
|
|
220
|
+
fixType: null,
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
id: "env-playwright",
|
|
225
|
+
label: "Playwright 安装检测",
|
|
226
|
+
passed: false,
|
|
227
|
+
severity: Severity.ERROR,
|
|
228
|
+
message: "Playwright 未安装",
|
|
229
|
+
fixType: FixType.COMMAND,
|
|
230
|
+
fixCommand: "pip install playwright",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
checkPlaywrightChromium() {
|
|
236
|
+
try {
|
|
237
|
+
execSync(
|
|
238
|
+
'python3 -c "from playwright.sync_api import sync_playwright; p = sync_playwright().start(); p.stop()"',
|
|
239
|
+
{ encoding: "utf-8", stdio: "pipe", timeout: 10_000 }
|
|
240
|
+
);
|
|
241
|
+
return {
|
|
242
|
+
id: "env-playwright-chromium",
|
|
243
|
+
label: "Playwright Chromium 已安装",
|
|
244
|
+
passed: true,
|
|
245
|
+
severity: Severity.INFO,
|
|
246
|
+
fixType: null,
|
|
247
|
+
};
|
|
248
|
+
} catch {
|
|
249
|
+
return {
|
|
250
|
+
id: "env-playwright-chromium",
|
|
251
|
+
label: "Playwright Chromium 安装检测",
|
|
252
|
+
passed: false,
|
|
253
|
+
severity: Severity.WARNING,
|
|
254
|
+
message: "Playwright Chromium 可能未安装",
|
|
255
|
+
fixType: FixType.COMMAND,
|
|
256
|
+
fixCommand: "playwright install chromium",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
checkGhCli() {
|
|
262
|
+
try {
|
|
263
|
+
const ghVersion = execSync("gh --version 2>&1", { encoding: "utf-8" }).split("\n")[0].trim();
|
|
264
|
+
return {
|
|
265
|
+
id: "env-gh",
|
|
266
|
+
label: ghVersion,
|
|
267
|
+
passed: true,
|
|
268
|
+
severity: Severity.INFO,
|
|
269
|
+
fixType: null,
|
|
270
|
+
};
|
|
271
|
+
} catch {
|
|
272
|
+
return {
|
|
273
|
+
id: "env-gh",
|
|
274
|
+
label: "gh CLI 安装检测",
|
|
275
|
+
passed: false,
|
|
276
|
+
severity: Severity.ERROR,
|
|
277
|
+
message: "gh CLI 未安装,请安装:https://cli.github.com/",
|
|
278
|
+
fixType: null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
checkGhAuth() {
|
|
284
|
+
try {
|
|
285
|
+
execSync("gh auth status 2>&1", { encoding: "utf-8", stdio: "pipe" });
|
|
286
|
+
return {
|
|
287
|
+
id: "env-gh-auth",
|
|
288
|
+
label: "gh CLI 已登录",
|
|
289
|
+
passed: true,
|
|
290
|
+
severity: Severity.INFO,
|
|
291
|
+
fixType: null,
|
|
292
|
+
};
|
|
293
|
+
} catch {
|
|
294
|
+
return {
|
|
295
|
+
id: "env-gh-auth",
|
|
296
|
+
label: "gh CLI 登录状态",
|
|
297
|
+
passed: false,
|
|
298
|
+
severity: Severity.WARNING,
|
|
299
|
+
message: "gh CLI 未登录",
|
|
300
|
+
fixType: FixType.COMMAND,
|
|
301
|
+
fixCommand: "gh auth login",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
checkConfig() {
|
|
307
|
+
const configPath = path.join(this.projectRoot, "config.json");
|
|
308
|
+
if (!fs.existsSync(configPath)) {
|
|
309
|
+
return {
|
|
310
|
+
id: "env-config",
|
|
311
|
+
label: "config.json 检测",
|
|
312
|
+
passed: false,
|
|
313
|
+
severity: Severity.WARNING,
|
|
314
|
+
message: "config.json 不存在",
|
|
315
|
+
fixType: FixType.AUTO,
|
|
316
|
+
fixAction: "create-config",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
322
|
+
JSON.parse(content);
|
|
323
|
+
return {
|
|
324
|
+
id: "env-config",
|
|
325
|
+
label: "config.json 存在且格式正确",
|
|
326
|
+
passed: true,
|
|
327
|
+
severity: Severity.INFO,
|
|
328
|
+
fixType: null,
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return {
|
|
332
|
+
id: "env-config",
|
|
333
|
+
label: "config.json 检测",
|
|
334
|
+
passed: false,
|
|
335
|
+
severity: Severity.ERROR,
|
|
336
|
+
message: "config.json 格式错误,请检查 JSON 语法",
|
|
337
|
+
fixType: null,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
checkSkills() {
|
|
343
|
+
const skillsPath = path.join(this.projectRoot, ".claude", "skills", "skills");
|
|
344
|
+
if (fs.existsSync(skillsPath)) {
|
|
345
|
+
const skills = fs.readdirSync(skillsPath).filter((name) =>
|
|
346
|
+
fs.statSync(path.join(skillsPath, name)).isDirectory()
|
|
347
|
+
);
|
|
348
|
+
return {
|
|
349
|
+
id: "env-skills",
|
|
350
|
+
label: `Skills 已安装(${skills.length} 个)`,
|
|
351
|
+
passed: true,
|
|
352
|
+
severity: Severity.INFO,
|
|
353
|
+
fixType: null,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
id: "env-skills",
|
|
358
|
+
label: "Skills 安装检测",
|
|
359
|
+
passed: false,
|
|
360
|
+
severity: Severity.WARNING,
|
|
361
|
+
message: "Skills 未安装,运行 bash install-skills.sh 安装",
|
|
362
|
+
fixType: FixType.MANUAL,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
checkLoginStatus() {
|
|
367
|
+
const cookiePath = path.join(this.projectRoot, ".cache", "cookies.json");
|
|
368
|
+
if (!fs.existsSync(cookiePath)) {
|
|
369
|
+
return {
|
|
370
|
+
id: "env-login",
|
|
371
|
+
label: "宜搭登录态",
|
|
372
|
+
passed: false,
|
|
373
|
+
severity: Severity.WARNING,
|
|
374
|
+
message: "未登录(运行 yida login 登录)",
|
|
375
|
+
fixType: FixType.COMMAND,
|
|
376
|
+
fixCommand: "yida login",
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const cookieData = JSON.parse(fs.readFileSync(cookiePath, "utf-8"));
|
|
382
|
+
const cookies = Array.isArray(cookieData) ? cookieData : cookieData.cookies || [];
|
|
383
|
+
const hasToken = cookies.some((c) => c.name === "tianshu_csrf_token");
|
|
384
|
+
const passed = hasToken;
|
|
385
|
+
return {
|
|
386
|
+
id: "env-login",
|
|
387
|
+
label: `宜搭登录态:${passed ? "已登录" : "Cookie 存在但可能已过期"}`,
|
|
388
|
+
passed,
|
|
389
|
+
severity: passed ? Severity.INFO : Severity.WARNING,
|
|
390
|
+
message: passed ? null : "Cookie 可能已过期,运行 yida login 重新登录",
|
|
391
|
+
fixType: passed ? null : FixType.COMMAND,
|
|
392
|
+
fixCommand: passed ? null : "yida login",
|
|
393
|
+
};
|
|
394
|
+
} catch {
|
|
395
|
+
return {
|
|
396
|
+
id: "env-login",
|
|
397
|
+
label: "宜搭登录态",
|
|
398
|
+
passed: false,
|
|
399
|
+
severity: Severity.WARNING,
|
|
400
|
+
message: "Cookie 文件损坏",
|
|
401
|
+
fixType: FixType.COMMAND,
|
|
402
|
+
fixCommand: "yida login",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async checkNetwork() {
|
|
408
|
+
try {
|
|
409
|
+
const https = require("https");
|
|
410
|
+
await new Promise((resolve, reject) => {
|
|
411
|
+
const request = https.get("https://www.aliwork.com", { timeout: 5000 }, (response) => {
|
|
412
|
+
resolve(response.statusCode);
|
|
413
|
+
});
|
|
414
|
+
request.on("error", reject);
|
|
415
|
+
request.on("timeout", () => {
|
|
416
|
+
request.destroy();
|
|
417
|
+
reject(new Error("timeout"));
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
id: "env-network",
|
|
422
|
+
label: "网络连通性(aliwork.com)",
|
|
423
|
+
passed: true,
|
|
424
|
+
severity: Severity.INFO,
|
|
425
|
+
fixType: null,
|
|
426
|
+
};
|
|
427
|
+
} catch {
|
|
428
|
+
return {
|
|
429
|
+
id: "env-network",
|
|
430
|
+
label: "网络连通性检测",
|
|
431
|
+
passed: false,
|
|
432
|
+
severity: Severity.WARNING,
|
|
433
|
+
message: "无法连接 aliwork.com,请检查网络",
|
|
434
|
+
fixType: null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── ApplicationChecker ────────────────────────────────
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 应用诊断检查器。
|
|
444
|
+
* 检查 PRD 文件、页面源码、Schema 缓存、React Hooks 使用规范。
|
|
445
|
+
*/
|
|
446
|
+
class ApplicationChecker {
|
|
447
|
+
constructor({ projectRoot, appId } = {}) {
|
|
448
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
449
|
+
this.appId = appId || null;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 执行所有应用检查。
|
|
454
|
+
* @returns {Promise<Array>}
|
|
455
|
+
*/
|
|
456
|
+
async check() {
|
|
457
|
+
return [
|
|
458
|
+
this.checkPrdFiles(),
|
|
459
|
+
this.checkPageSources(),
|
|
460
|
+
this.checkSchemaCache(),
|
|
461
|
+
this.checkReactHooks(),
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
checkPrdFiles() {
|
|
466
|
+
const prdDir = path.join(this.projectRoot, "prd");
|
|
467
|
+
if (!fs.existsSync(prdDir)) {
|
|
468
|
+
return {
|
|
469
|
+
id: "app-prd",
|
|
470
|
+
label: "PRD 文件检测",
|
|
471
|
+
passed: false,
|
|
472
|
+
severity: Severity.WARNING,
|
|
473
|
+
message: "prd/ 目录不存在,建议创建 PRD 文档描述应用需求",
|
|
474
|
+
fixType: FixType.MANUAL,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const prdFiles = fs.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
|
|
479
|
+
const passed = prdFiles.length > 0;
|
|
480
|
+
return {
|
|
481
|
+
id: "app-prd",
|
|
482
|
+
label: `PRD 文件(${prdFiles.length} 个)`,
|
|
483
|
+
passed,
|
|
484
|
+
severity: passed ? Severity.INFO : Severity.WARNING,
|
|
485
|
+
message: passed ? null : "prd/ 目录为空,建议添加 PRD 文档",
|
|
486
|
+
fixType: null,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
checkPageSources() {
|
|
491
|
+
const srcDir = path.join(this.projectRoot, "pages", "src");
|
|
492
|
+
if (!fs.existsSync(srcDir)) {
|
|
493
|
+
return {
|
|
494
|
+
id: "app-pages",
|
|
495
|
+
label: "页面源码检测",
|
|
496
|
+
passed: false,
|
|
497
|
+
severity: Severity.WARNING,
|
|
498
|
+
message: "pages/src/ 目录不存在",
|
|
499
|
+
fixType: FixType.MANUAL,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const sourceFiles = fs.readdirSync(srcDir).filter((f) => /\.(js|jsx|ts|tsx)$/.test(f));
|
|
504
|
+
const issues = [];
|
|
505
|
+
|
|
506
|
+
for (const file of sourceFiles) {
|
|
507
|
+
const filePath = path.join(srcDir, file);
|
|
508
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
509
|
+
|
|
510
|
+
if (content.length === 0) {
|
|
511
|
+
issues.push(`${file}: 文件为空`);
|
|
512
|
+
}
|
|
513
|
+
if (content.includes("console.log") && !content.includes("// eslint-disable")) {
|
|
514
|
+
issues.push(`${file}: 包含 console.log 调试语句`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const passed = issues.length === 0 && sourceFiles.length > 0;
|
|
519
|
+
return {
|
|
520
|
+
id: "app-pages",
|
|
521
|
+
label: `页面源码(${sourceFiles.length} 个文件${issues.length > 0 ? `,${issues.length} 个问题` : ""})`,
|
|
522
|
+
passed,
|
|
523
|
+
severity: issues.length > 0 ? Severity.WARNING : Severity.INFO,
|
|
524
|
+
message: issues.length > 0 ? issues.join(";") : null,
|
|
525
|
+
fixType: null,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
checkSchemaCache() {
|
|
530
|
+
const cacheDir = path.join(this.projectRoot, ".cache");
|
|
531
|
+
if (!fs.existsSync(cacheDir)) {
|
|
532
|
+
return {
|
|
533
|
+
id: "app-schema",
|
|
534
|
+
label: "Schema 缓存检测",
|
|
535
|
+
passed: true,
|
|
536
|
+
severity: Severity.INFO,
|
|
537
|
+
message: null,
|
|
538
|
+
fixType: null,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const schemaFiles = fs.readdirSync(cacheDir).filter((f) => f.endsWith("-schema.json"));
|
|
543
|
+
for (const file of schemaFiles) {
|
|
544
|
+
try {
|
|
545
|
+
const content = fs.readFileSync(path.join(cacheDir, file), "utf-8");
|
|
546
|
+
JSON.parse(content);
|
|
547
|
+
} catch {
|
|
548
|
+
return {
|
|
549
|
+
id: "app-schema",
|
|
550
|
+
label: "Schema 缓存检测",
|
|
551
|
+
passed: false,
|
|
552
|
+
severity: Severity.WARNING,
|
|
553
|
+
message: `Schema 缓存文件 ${file} 格式错误`,
|
|
554
|
+
fixType: FixType.AUTO,
|
|
555
|
+
fixAction: "delete-invalid-schema",
|
|
556
|
+
fixTarget: file,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
id: "app-schema",
|
|
563
|
+
label: `Schema 缓存(${schemaFiles.length} 个)`,
|
|
564
|
+
passed: true,
|
|
565
|
+
severity: Severity.INFO,
|
|
566
|
+
fixType: null,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
checkReactHooks() {
|
|
571
|
+
const srcDir = path.join(this.projectRoot, "pages", "src");
|
|
572
|
+
if (!fs.existsSync(srcDir)) {
|
|
573
|
+
return {
|
|
574
|
+
id: "app-hooks",
|
|
575
|
+
label: "React Hooks 检测",
|
|
576
|
+
passed: true,
|
|
577
|
+
severity: Severity.INFO,
|
|
578
|
+
message: "无页面源码,跳过检测",
|
|
579
|
+
fixType: null,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const sourceFiles = fs.readdirSync(srcDir).filter((f) => /\.(js|jsx)$/.test(f));
|
|
584
|
+
const hookIssues = [];
|
|
585
|
+
|
|
586
|
+
for (const file of sourceFiles) {
|
|
587
|
+
const content = fs.readFileSync(path.join(srcDir, file), "utf-8");
|
|
588
|
+
const lines = content.split("\n");
|
|
589
|
+
|
|
590
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
591
|
+
const line = lines[lineIndex];
|
|
592
|
+
// 检测条件语句中使用 Hooks
|
|
593
|
+
if (/if\s*\(.*\)\s*\{/.test(line)) {
|
|
594
|
+
const blockEnd = findBlockEnd(lines, lineIndex);
|
|
595
|
+
const blockContent = lines.slice(lineIndex, blockEnd + 1).join("\n");
|
|
596
|
+
if (/\buse[A-Z]\w*\s*\(/.test(blockContent)) {
|
|
597
|
+
hookIssues.push(`${file}:${lineIndex + 1}: 条件语句中使用了 React Hook`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const passed = hookIssues.length === 0;
|
|
604
|
+
return {
|
|
605
|
+
id: "app-hooks",
|
|
606
|
+
label: `React Hooks 规范${hookIssues.length > 0 ? `(${hookIssues.length} 个问题)` : ""}`,
|
|
607
|
+
passed,
|
|
608
|
+
severity: passed ? Severity.INFO : Severity.WARNING,
|
|
609
|
+
message: passed ? null : hookIssues.join(";"),
|
|
610
|
+
fixType: null,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 查找代码块的结束行号。
|
|
617
|
+
* @param {string[]} lines
|
|
618
|
+
* @param {number} startLine
|
|
619
|
+
* @returns {number}
|
|
620
|
+
*/
|
|
621
|
+
function findBlockEnd(lines, startLine) {
|
|
622
|
+
let depth = 0;
|
|
623
|
+
for (let index = startLine; index < lines.length; index++) {
|
|
624
|
+
for (const char of lines[index]) {
|
|
625
|
+
if (char === "{") depth++;
|
|
626
|
+
if (char === "}") depth--;
|
|
627
|
+
if (depth === 0 && index > startLine) return index;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return lines.length - 1;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ── FixEngine ─────────────────────────────────────────
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 智能修复引擎。
|
|
637
|
+
* 支持自动修复、手动提示、命令执行三种修复方式。
|
|
638
|
+
*/
|
|
639
|
+
class FixEngine {
|
|
640
|
+
constructor({ projectRoot } = {}) {
|
|
641
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
642
|
+
this.fixResults = [];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 自动修复所有可修复的问题。
|
|
647
|
+
* @param {Array} issues - 可修复的诊断结果列表
|
|
648
|
+
* @returns {Promise<Array>}
|
|
649
|
+
*/
|
|
650
|
+
async autoFix(issues) {
|
|
651
|
+
this.fixResults = [];
|
|
652
|
+
|
|
653
|
+
for (const issue of issues) {
|
|
654
|
+
if (issue.fixType === FixType.AUTO) {
|
|
655
|
+
const result = this.applyAutoFix(issue);
|
|
656
|
+
this.fixResults.push(result);
|
|
657
|
+
} else if (issue.fixType === FixType.COMMAND) {
|
|
658
|
+
this.fixResults.push({
|
|
659
|
+
id: issue.id,
|
|
660
|
+
fixed: false,
|
|
661
|
+
message: `请手动运行:${issue.fixCommand}`,
|
|
662
|
+
});
|
|
663
|
+
} else if (issue.fixType === FixType.MANUAL) {
|
|
664
|
+
this.fixResults.push({
|
|
665
|
+
id: issue.id,
|
|
666
|
+
fixed: false,
|
|
667
|
+
message: issue.message,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return this.fixResults;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 执行自动修复动作。
|
|
677
|
+
* @param {object} issue
|
|
678
|
+
* @returns {object}
|
|
679
|
+
*/
|
|
680
|
+
applyAutoFix(issue) {
|
|
681
|
+
switch (issue.fixAction) {
|
|
682
|
+
case "create-config": {
|
|
683
|
+
const configPath = path.join(this.projectRoot, "config.json");
|
|
684
|
+
const template = {
|
|
685
|
+
loginUrl: "https://www.aliwork.com/workPlatform",
|
|
686
|
+
defaultBaseUrl: "https://www.aliwork.com",
|
|
687
|
+
};
|
|
688
|
+
fs.writeFileSync(configPath, JSON.stringify(template, null, 2), "utf-8");
|
|
689
|
+
return {
|
|
690
|
+
id: issue.id,
|
|
691
|
+
fixed: true,
|
|
692
|
+
message: "已创建 config.json 模板,请根据实际情况修改 loginUrl",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
case "delete-invalid-schema": {
|
|
697
|
+
const schemaPath = path.join(this.projectRoot, ".cache", issue.fixTarget);
|
|
698
|
+
if (fs.existsSync(schemaPath)) {
|
|
699
|
+
fs.unlinkSync(schemaPath);
|
|
700
|
+
}
|
|
701
|
+
return {
|
|
702
|
+
id: issue.id,
|
|
703
|
+
fixed: true,
|
|
704
|
+
message: `已删除损坏的 Schema 缓存文件:${issue.fixTarget}`,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
default:
|
|
709
|
+
return {
|
|
710
|
+
id: issue.id,
|
|
711
|
+
fixed: false,
|
|
712
|
+
message: `未知的修复动作:${issue.fixAction}`,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* 格式化修复结果输出。
|
|
719
|
+
* @returns {string}
|
|
720
|
+
*/
|
|
721
|
+
formatFixOutput() {
|
|
722
|
+
const lines = [];
|
|
723
|
+
for (const result of this.fixResults) {
|
|
724
|
+
const icon = result.fixed ? "✅" : "💡";
|
|
725
|
+
lines.push(`${icon} ${result.message}`);
|
|
726
|
+
}
|
|
727
|
+
const fixedCount = this.fixResults.filter((r) => r.fixed).length;
|
|
728
|
+
if (fixedCount > 0) {
|
|
729
|
+
lines.push(`\n✅ 自动修复了 ${fixedCount} 个问题`);
|
|
730
|
+
}
|
|
731
|
+
return lines.join("\n");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── ReportGenerator ───────────────────────────────────
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 诊断报告生成器。
|
|
739
|
+
* 支持 JSON、Markdown、HTML 三种格式。
|
|
740
|
+
*/
|
|
741
|
+
class ReportGenerator {
|
|
742
|
+
constructor({ projectRoot } = {}) {
|
|
743
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* 生成诊断报告。
|
|
748
|
+
* @param {Array} results - 诊断结果
|
|
749
|
+
* @param {object} summary - 汇总信息
|
|
750
|
+
* @param {string} format - 报告格式(json | markdown | html)
|
|
751
|
+
* @returns {string} 报告文件路径
|
|
752
|
+
*/
|
|
753
|
+
generate(results, summary, format) {
|
|
754
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
755
|
+
const reportDir = path.join(this.projectRoot, ".cache", "reports");
|
|
756
|
+
|
|
757
|
+
if (!fs.existsSync(reportDir)) {
|
|
758
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
switch (format) {
|
|
762
|
+
case "json":
|
|
763
|
+
return this.generateJson(results, summary, reportDir, timestamp);
|
|
764
|
+
case "markdown":
|
|
765
|
+
return this.generateMarkdown(results, summary, reportDir, timestamp);
|
|
766
|
+
case "html":
|
|
767
|
+
return this.generateHtml(results, summary, reportDir, timestamp);
|
|
768
|
+
default:
|
|
769
|
+
console.error(`未知报告格式:${format},使用 markdown`);
|
|
770
|
+
return this.generateMarkdown(results, summary, reportDir, timestamp);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
generateJson(results, summary, reportDir, timestamp) {
|
|
775
|
+
const reportPath = path.join(reportDir, `doctor-${timestamp}.json`);
|
|
776
|
+
const report = {
|
|
777
|
+
timestamp: new Date().toISOString(),
|
|
778
|
+
summary,
|
|
779
|
+
results,
|
|
780
|
+
};
|
|
781
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf-8");
|
|
782
|
+
return reportPath;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
generateMarkdown(results, summary, reportDir, timestamp) {
|
|
786
|
+
const reportPath = path.join(reportDir, `doctor-${timestamp}.md`);
|
|
787
|
+
const lines = [
|
|
788
|
+
"# OpenYida 诊断报告",
|
|
789
|
+
"",
|
|
790
|
+
`生成时间:${new Date().toLocaleString()}`,
|
|
791
|
+
"",
|
|
792
|
+
"## 汇总",
|
|
793
|
+
"",
|
|
794
|
+
`| 指标 | 数量 |`,
|
|
795
|
+
`|------|------|`,
|
|
796
|
+
`| 总检查项 | ${summary.total} |`,
|
|
797
|
+
`| 通过 | ${summary.passed} |`,
|
|
798
|
+
`| 错误 | ${summary.errorCount} |`,
|
|
799
|
+
`| 警告 | ${summary.warningCount} |`,
|
|
800
|
+
`| 可自动修复 | ${summary.autoFixable} |`,
|
|
801
|
+
"",
|
|
802
|
+
"## 详细结果",
|
|
803
|
+
"",
|
|
804
|
+
];
|
|
805
|
+
|
|
806
|
+
for (const result of results) {
|
|
807
|
+
const icon = result.passed ? "✅" : result.severity === Severity.ERROR ? "❌" : "⚠️";
|
|
808
|
+
lines.push(`- ${icon} **${result.label}**`);
|
|
809
|
+
if (!result.passed && result.message) {
|
|
810
|
+
lines.push(` - ${result.message}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
fs.writeFileSync(reportPath, lines.join("\n"), "utf-8");
|
|
815
|
+
return reportPath;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
generateHtml(results, summary, reportDir, timestamp) {
|
|
819
|
+
const reportPath = path.join(reportDir, `doctor-${timestamp}.html`);
|
|
820
|
+
const resultRows = results
|
|
821
|
+
.map((result) => {
|
|
822
|
+
const icon = result.passed ? "✅" : result.severity === Severity.ERROR ? "❌" : "⚠️";
|
|
823
|
+
const message = result.message || "-";
|
|
824
|
+
return `<tr><td>${icon}</td><td>${result.label}</td><td>${message}</td></tr>`;
|
|
825
|
+
})
|
|
826
|
+
.join("\n");
|
|
827
|
+
|
|
828
|
+
const html = `<!DOCTYPE html>
|
|
829
|
+
<html lang="zh-CN">
|
|
830
|
+
<head>
|
|
831
|
+
<meta charset="UTF-8">
|
|
832
|
+
<title>OpenYida 诊断报告</title>
|
|
833
|
+
<style>
|
|
834
|
+
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
|
|
835
|
+
h1 { color: #333; }
|
|
836
|
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
837
|
+
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
|
838
|
+
th { background: #f5f5f5; }
|
|
839
|
+
.summary { display: flex; gap: 20px; margin: 20px 0; }
|
|
840
|
+
.summary-card { background: #f9f9f9; padding: 16px; border-radius: 8px; flex: 1; text-align: center; }
|
|
841
|
+
.summary-card .number { font-size: 24px; font-weight: bold; }
|
|
842
|
+
</style>
|
|
843
|
+
</head>
|
|
844
|
+
<body>
|
|
845
|
+
<h1>🔍 OpenYida 诊断报告</h1>
|
|
846
|
+
<p>生成时间:${new Date().toLocaleString()}</p>
|
|
847
|
+
<div class="summary">
|
|
848
|
+
<div class="summary-card"><div class="number">${summary.total}</div><div>总检查项</div></div>
|
|
849
|
+
<div class="summary-card"><div class="number">${summary.passed}</div><div>通过</div></div>
|
|
850
|
+
<div class="summary-card"><div class="number">${summary.errorCount}</div><div>错误</div></div>
|
|
851
|
+
<div class="summary-card"><div class="number">${summary.warningCount}</div><div>警告</div></div>
|
|
852
|
+
</div>
|
|
853
|
+
<table>
|
|
854
|
+
<thead><tr><th>状态</th><th>检查项</th><th>详情</th></tr></thead>
|
|
855
|
+
<tbody>${resultRows}</tbody>
|
|
856
|
+
</table>
|
|
857
|
+
</body>
|
|
858
|
+
</html>`;
|
|
859
|
+
|
|
860
|
+
fs.writeFileSync(reportPath, html, "utf-8");
|
|
861
|
+
return reportPath;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── PreChecker ────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* 预检查器。
|
|
869
|
+
* 在发布前或创建前自动执行检查,确保环境和应用状态正常。
|
|
870
|
+
*/
|
|
871
|
+
class PreChecker {
|
|
872
|
+
constructor({ projectRoot } = {}) {
|
|
873
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* 执行发布前预检查。
|
|
878
|
+
* @returns {Promise<{ passed: boolean, results: Array }>}
|
|
879
|
+
*/
|
|
880
|
+
async prePublishCheck() {
|
|
881
|
+
const engine = new DiagnosticEngine({ projectRoot: this.projectRoot });
|
|
882
|
+
engine.registerChecker(new EnvironmentChecker({ projectRoot: this.projectRoot }));
|
|
883
|
+
engine.registerChecker(new ApplicationChecker({ projectRoot: this.projectRoot }));
|
|
884
|
+
|
|
885
|
+
const results = await engine.runAll();
|
|
886
|
+
const criticalIssues = results.filter(
|
|
887
|
+
(r) => !r.passed && r.severity === Severity.ERROR
|
|
888
|
+
);
|
|
889
|
+
|
|
890
|
+
return {
|
|
891
|
+
passed: criticalIssues.length === 0,
|
|
892
|
+
results,
|
|
893
|
+
criticalIssues,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* 执行创建前预检查(仅环境检查)。
|
|
899
|
+
* @returns {Promise<{ passed: boolean, results: Array }>}
|
|
900
|
+
*/
|
|
901
|
+
async preCreateCheck() {
|
|
902
|
+
const engine = new DiagnosticEngine({ projectRoot: this.projectRoot });
|
|
903
|
+
engine.registerChecker(new EnvironmentChecker({ projectRoot: this.projectRoot }));
|
|
904
|
+
|
|
905
|
+
const results = await engine.runAll();
|
|
906
|
+
const criticalIssues = results.filter(
|
|
907
|
+
(r) => !r.passed && r.severity === Severity.ERROR
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
return {
|
|
911
|
+
passed: criticalIssues.length === 0,
|
|
912
|
+
results,
|
|
913
|
+
criticalIssues,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ── HealthMonitor ─────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* 持续健康度监控。
|
|
922
|
+
* 定时执行诊断并记录趋势数据。
|
|
923
|
+
*/
|
|
924
|
+
class HealthMonitor {
|
|
925
|
+
constructor({ projectRoot, intervalMs, onResult } = {}) {
|
|
926
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
927
|
+
this.intervalMs = intervalMs || 60_000;
|
|
928
|
+
this.onResult = onResult || null;
|
|
929
|
+
this.timer = null;
|
|
930
|
+
this.history = [];
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* 启动监控。
|
|
935
|
+
*/
|
|
936
|
+
start() {
|
|
937
|
+
this.runOnce();
|
|
938
|
+
this.timer = setInterval(() => this.runOnce(), this.intervalMs);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* 停止监控。
|
|
943
|
+
*/
|
|
944
|
+
stop() {
|
|
945
|
+
if (this.timer) {
|
|
946
|
+
clearInterval(this.timer);
|
|
947
|
+
this.timer = null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* 执行一次诊断快照。
|
|
953
|
+
*/
|
|
954
|
+
async runOnce() {
|
|
955
|
+
const engine = new DiagnosticEngine({ projectRoot: this.projectRoot });
|
|
956
|
+
engine.registerChecker(new EnvironmentChecker({ projectRoot: this.projectRoot }));
|
|
957
|
+
engine.registerChecker(new ApplicationChecker({ projectRoot: this.projectRoot }));
|
|
958
|
+
|
|
959
|
+
await engine.runAll();
|
|
960
|
+
const summary = engine.getSummary();
|
|
961
|
+
const snapshot = {
|
|
962
|
+
timestamp: new Date().toISOString(),
|
|
963
|
+
...summary,
|
|
964
|
+
healthScore: this.calculateHealthScore(summary),
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
this.history.push(snapshot);
|
|
968
|
+
if (this.onResult) {
|
|
969
|
+
this.onResult(snapshot);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* 计算健康度分数(0-100)。
|
|
975
|
+
* @param {object} summary
|
|
976
|
+
* @returns {number}
|
|
977
|
+
*/
|
|
978
|
+
calculateHealthScore(summary) {
|
|
979
|
+
if (summary.total === 0) return 100;
|
|
980
|
+
const passRate = summary.passed / summary.total;
|
|
981
|
+
const errorPenalty = summary.errorCount * 10;
|
|
982
|
+
const warningPenalty = summary.warningCount * 3;
|
|
983
|
+
return Math.max(0, Math.round(passRate * 100 - errorPenalty - warningPenalty));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* 格式化监控输出。
|
|
988
|
+
* @param {object} snapshot
|
|
989
|
+
* @returns {string}
|
|
990
|
+
*/
|
|
991
|
+
formatMonitorOutput(snapshot) {
|
|
992
|
+
const time = new Date(snapshot.timestamp).toLocaleTimeString();
|
|
993
|
+
const trend = this.history.length >= 2
|
|
994
|
+
? this.history[this.history.length - 1].healthScore - this.history[this.history.length - 2].healthScore
|
|
995
|
+
: 0;
|
|
996
|
+
const trendIcon = trend > 0 ? "📈" : trend < 0 ? "📉" : "➡️";
|
|
997
|
+
|
|
998
|
+
return [
|
|
999
|
+
`[${time}] 健康度: ${snapshot.healthScore}/100 ${trendIcon}`,
|
|
1000
|
+
` 通过: ${snapshot.passed}/${snapshot.total} | 错误: ${snapshot.errorCount} | 警告: ${snapshot.warningCount}`,
|
|
1001
|
+
].join("\n");
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ── ProductionErrorCollector ──────────────────────────
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* 线上错误诊断与智能分析。
|
|
1009
|
+
* 收集线上应用的错误日志并进行分类分析。
|
|
1010
|
+
*/
|
|
1011
|
+
class ProductionErrorCollector {
|
|
1012
|
+
constructor({ projectRoot, appId } = {}) {
|
|
1013
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
1014
|
+
this.appId = appId || null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* 执行线上错误检查。
|
|
1019
|
+
* @returns {Promise<Array>}
|
|
1020
|
+
*/
|
|
1021
|
+
async check() {
|
|
1022
|
+
const results = [];
|
|
1023
|
+
|
|
1024
|
+
// 检查应用 ID 是否提供
|
|
1025
|
+
if (!this.appId) {
|
|
1026
|
+
results.push({
|
|
1027
|
+
id: "prod-app-id",
|
|
1028
|
+
label: "线上诊断:应用 ID",
|
|
1029
|
+
passed: false,
|
|
1030
|
+
severity: Severity.ERROR,
|
|
1031
|
+
message: "未指定应用 ID,请使用 --app <appId> 参数",
|
|
1032
|
+
fixType: null,
|
|
1033
|
+
});
|
|
1034
|
+
return results;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// 检查本地错误日志
|
|
1038
|
+
const errorLogPath = path.join(this.projectRoot, ".cache", "error-logs", `${this.appId}.json`);
|
|
1039
|
+
if (fs.existsSync(errorLogPath)) {
|
|
1040
|
+
try {
|
|
1041
|
+
const errorLog = JSON.parse(fs.readFileSync(errorLogPath, "utf-8"));
|
|
1042
|
+
const errorCount = Array.isArray(errorLog) ? errorLog.length : 0;
|
|
1043
|
+
results.push({
|
|
1044
|
+
id: "prod-errors",
|
|
1045
|
+
label: `线上错误日志(${errorCount} 条)`,
|
|
1046
|
+
passed: errorCount === 0,
|
|
1047
|
+
severity: errorCount > 0 ? Severity.WARNING : Severity.INFO,
|
|
1048
|
+
message: errorCount > 0 ? `发现 ${errorCount} 条错误日志,建议排查` : null,
|
|
1049
|
+
fixType: null,
|
|
1050
|
+
});
|
|
1051
|
+
} catch {
|
|
1052
|
+
results.push({
|
|
1053
|
+
id: "prod-errors",
|
|
1054
|
+
label: "线上错误日志",
|
|
1055
|
+
passed: false,
|
|
1056
|
+
severity: Severity.WARNING,
|
|
1057
|
+
message: "错误日志文件格式异常",
|
|
1058
|
+
fixType: null,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
results.push({
|
|
1063
|
+
id: "prod-errors",
|
|
1064
|
+
label: "线上错误日志",
|
|
1065
|
+
passed: true,
|
|
1066
|
+
severity: Severity.INFO,
|
|
1067
|
+
message: "无本地错误日志缓存",
|
|
1068
|
+
fixType: null,
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return results;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// ── TicketCreator ─────────────────────────────────────
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* 工单创建器。
|
|
1080
|
+
* 集成 GitHub Issues,支持从诊断结果创建工单。
|
|
1081
|
+
*/
|
|
1082
|
+
class TicketCreator {
|
|
1083
|
+
constructor({ projectRoot } = {}) {
|
|
1084
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* 创建工单。
|
|
1089
|
+
* @param {object} options - { title, description, type, labels }
|
|
1090
|
+
* @returns {Promise<object>}
|
|
1091
|
+
*/
|
|
1092
|
+
async createTicket({ title, description, type = "bug", labels = [] }) {
|
|
1093
|
+
const ticket = {
|
|
1094
|
+
id: `TICKET-${Date.now()}`,
|
|
1095
|
+
title,
|
|
1096
|
+
description,
|
|
1097
|
+
type,
|
|
1098
|
+
labels: [...labels, type],
|
|
1099
|
+
createdAt: new Date().toISOString(),
|
|
1100
|
+
status: "draft",
|
|
1101
|
+
remoteUrl: null,
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// 尝试通过 gh CLI 创建 GitHub Issue
|
|
1105
|
+
try {
|
|
1106
|
+
const labelArgs = ticket.labels.map((label) => `-l "${label}"`).join(" ");
|
|
1107
|
+
const result = execSync(
|
|
1108
|
+
`gh issue create --title "${title}" --body "${description}" ${labelArgs} 2>&1`,
|
|
1109
|
+
{ encoding: "utf-8", cwd: this.projectRoot, timeout: 15_000 }
|
|
1110
|
+
);
|
|
1111
|
+
const urlMatch = result.match(/https:\/\/github\.com\/\S+/);
|
|
1112
|
+
if (urlMatch) {
|
|
1113
|
+
ticket.status = "submitted";
|
|
1114
|
+
ticket.remoteUrl = urlMatch[0];
|
|
1115
|
+
}
|
|
1116
|
+
} catch {
|
|
1117
|
+
// gh CLI 不可用时保存到本地
|
|
1118
|
+
ticket.status = "local";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// 保存到本地
|
|
1122
|
+
this.saveTicketLocally(ticket);
|
|
1123
|
+
return ticket;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* 保存工单到本地文件。
|
|
1128
|
+
* @param {object} ticket
|
|
1129
|
+
*/
|
|
1130
|
+
saveTicketLocally(ticket) {
|
|
1131
|
+
const ticketDir = path.join(this.projectRoot, ".cache", "tickets");
|
|
1132
|
+
if (!fs.existsSync(ticketDir)) {
|
|
1133
|
+
fs.mkdirSync(ticketDir, { recursive: true });
|
|
1134
|
+
}
|
|
1135
|
+
const ticketPath = path.join(ticketDir, `${ticket.id}.json`);
|
|
1136
|
+
fs.writeFileSync(ticketPath, JSON.stringify(ticket, null, 2), "utf-8");
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// ── VOCCreator ────────────────────────────────────────
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* VOC(Voice of Customer)创建器。
|
|
1144
|
+
* 业务价值分析与优先级建议。
|
|
1145
|
+
*/
|
|
1146
|
+
class VOCCreator {
|
|
1147
|
+
constructor({ projectRoot } = {}) {
|
|
1148
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* 创建 VOC。
|
|
1153
|
+
* @param {object} options - { title, description, priority, businessValue }
|
|
1154
|
+
* @returns {Promise<object>}
|
|
1155
|
+
*/
|
|
1156
|
+
async createVOC({ title, description, priority, businessValue } = {}) {
|
|
1157
|
+
const analysis = this.analyzeBusinessValue(description || "");
|
|
1158
|
+
const voc = {
|
|
1159
|
+
id: `VOC-${Date.now()}`,
|
|
1160
|
+
title,
|
|
1161
|
+
description,
|
|
1162
|
+
priority: priority || analysis.suggestedPriority,
|
|
1163
|
+
businessValue: businessValue || analysis.businessValue,
|
|
1164
|
+
analysis,
|
|
1165
|
+
createdAt: new Date().toISOString(),
|
|
1166
|
+
status: "draft",
|
|
1167
|
+
remoteUrl: null,
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
// 尝试通过 gh CLI 创建 GitHub Issue(带 VOC 标签)
|
|
1171
|
+
try {
|
|
1172
|
+
const result = execSync(
|
|
1173
|
+
`gh issue create --title "[VOC] ${title}" --body "${description}" -l "voc" -l "enhancement" 2>&1`,
|
|
1174
|
+
{ encoding: "utf-8", cwd: this.projectRoot, timeout: 15_000 }
|
|
1175
|
+
);
|
|
1176
|
+
const urlMatch = result.match(/https:\/\/github\.com\/\S+/);
|
|
1177
|
+
if (urlMatch) {
|
|
1178
|
+
voc.status = "submitted";
|
|
1179
|
+
voc.remoteUrl = urlMatch[0];
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
voc.status = "local";
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
this.saveVOCLocally(voc);
|
|
1186
|
+
return voc;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* 分析业务价值。
|
|
1191
|
+
* @param {string} description
|
|
1192
|
+
* @returns {object}
|
|
1193
|
+
*/
|
|
1194
|
+
analyzeBusinessValue(description) {
|
|
1195
|
+
const keywords = {
|
|
1196
|
+
high: ["紧急", "严重", "阻塞", "线上", "生产", "崩溃", "数据丢失"],
|
|
1197
|
+
medium: ["影响", "用户", "体验", "性能", "优化", "改进"],
|
|
1198
|
+
low: ["建议", "希望", "可以", "美化", "文档"],
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const lowerDescription = description.toLowerCase();
|
|
1202
|
+
let suggestedPriority = "medium";
|
|
1203
|
+
let businessValue = "medium";
|
|
1204
|
+
|
|
1205
|
+
if (keywords.high.some((keyword) => lowerDescription.includes(keyword))) {
|
|
1206
|
+
suggestedPriority = "high";
|
|
1207
|
+
businessValue = "high";
|
|
1208
|
+
} else if (keywords.low.some((keyword) => lowerDescription.includes(keyword))) {
|
|
1209
|
+
suggestedPriority = "low";
|
|
1210
|
+
businessValue = "low";
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return { suggestedPriority, businessValue };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* 保存 VOC 到本地文件。
|
|
1218
|
+
* @param {object} voc
|
|
1219
|
+
*/
|
|
1220
|
+
saveVOCLocally(voc) {
|
|
1221
|
+
const vocDir = path.join(this.projectRoot, ".cache", "voc");
|
|
1222
|
+
if (!fs.existsSync(vocDir)) {
|
|
1223
|
+
fs.mkdirSync(vocDir, { recursive: true });
|
|
1224
|
+
}
|
|
1225
|
+
const vocPath = path.join(vocDir, `${voc.id}.json`);
|
|
1226
|
+
fs.writeFileSync(vocPath, JSON.stringify(voc, null, 2), "utf-8");
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// ── SubmissionDecider ─────────────────────────────────
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* 智能提交决策器。
|
|
1234
|
+
* 自动判断应该创建工单还是 VOC。
|
|
1235
|
+
*/
|
|
1236
|
+
class SubmissionDecider {
|
|
1237
|
+
constructor({ projectRoot } = {}) {
|
|
1238
|
+
this.projectRoot = projectRoot || findProjectRoot();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* 自动提交决策。
|
|
1243
|
+
* @param {object} options - { title, description }
|
|
1244
|
+
* @returns {Promise<object>}
|
|
1245
|
+
*/
|
|
1246
|
+
async autoSubmit({ title, description }) {
|
|
1247
|
+
const decision = this.decide(title, description);
|
|
1248
|
+
|
|
1249
|
+
let result;
|
|
1250
|
+
if (decision.type === "ticket") {
|
|
1251
|
+
const creator = new TicketCreator({ projectRoot: this.projectRoot });
|
|
1252
|
+
const ticket = await creator.createTicket({ title, description, type: "bug" });
|
|
1253
|
+
result = {
|
|
1254
|
+
decision,
|
|
1255
|
+
type: "ticket",
|
|
1256
|
+
data: ticket,
|
|
1257
|
+
message: ticket.status === "submitted"
|
|
1258
|
+
? `工单已提交:${ticket.remoteUrl}`
|
|
1259
|
+
: `工单已保存到本地(ID: ${ticket.id})`,
|
|
1260
|
+
};
|
|
1261
|
+
} else {
|
|
1262
|
+
const creator = new VOCCreator({ projectRoot: this.projectRoot });
|
|
1263
|
+
const voc = await creator.createVOC({ title, description });
|
|
1264
|
+
result = {
|
|
1265
|
+
decision,
|
|
1266
|
+
type: "voc",
|
|
1267
|
+
data: voc,
|
|
1268
|
+
message: voc.status === "submitted"
|
|
1269
|
+
? `VOC 已提交:${voc.remoteUrl}`
|
|
1270
|
+
: `VOC 已保存到本地(ID: ${voc.id})`,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
return result;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* 决策逻辑:判断是工单还是 VOC。
|
|
1279
|
+
* @param {string} title
|
|
1280
|
+
* @param {string} description
|
|
1281
|
+
* @returns {object}
|
|
1282
|
+
*/
|
|
1283
|
+
decide(title, description) {
|
|
1284
|
+
const combined = `${title} ${description}`.toLowerCase();
|
|
1285
|
+
const bugKeywords = ["bug", "错误", "异常", "崩溃", "失败", "报错", "无法", "不能", "修复"];
|
|
1286
|
+
const featureKeywords = ["需求", "功能", "建议", "希望", "优化", "新增", "改进", "支持"];
|
|
1287
|
+
|
|
1288
|
+
const bugScore = bugKeywords.filter((keyword) => combined.includes(keyword)).length;
|
|
1289
|
+
const featureScore = featureKeywords.filter((keyword) => combined.includes(keyword)).length;
|
|
1290
|
+
|
|
1291
|
+
if (bugScore > featureScore) {
|
|
1292
|
+
return {
|
|
1293
|
+
type: "ticket",
|
|
1294
|
+
reason: "检测到问题/缺陷相关描述,建议创建工单",
|
|
1295
|
+
confidence: Math.min(0.95, 0.5 + bugScore * 0.1),
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return {
|
|
1300
|
+
type: "voc",
|
|
1301
|
+
reason: "检测到需求/建议相关描述,建议创建 VOC",
|
|
1302
|
+
confidence: Math.min(0.95, 0.5 + featureScore * 0.1),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ── CLI 入口 ──────────────────────────────────────────
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* 解析 doctor 命令的参数。
|
|
1311
|
+
* @param {string[]} args - CLI 参数列表
|
|
1312
|
+
* @returns {object}
|
|
1313
|
+
*/
|
|
1314
|
+
function parseArgs(args) {
|
|
1315
|
+
const options = {
|
|
1316
|
+
fix: false,
|
|
1317
|
+
repair: false,
|
|
1318
|
+
production: false,
|
|
1319
|
+
app: null,
|
|
1320
|
+
monitor: false,
|
|
1321
|
+
report: null,
|
|
1322
|
+
createTicket: false,
|
|
1323
|
+
createVoc: false,
|
|
1324
|
+
autoSubmit: false,
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
for (let index = 0; index < args.length; index++) {
|
|
1328
|
+
switch (args[index]) {
|
|
1329
|
+
case "--fix":
|
|
1330
|
+
options.fix = true;
|
|
1331
|
+
break;
|
|
1332
|
+
case "--repair":
|
|
1333
|
+
options.repair = true;
|
|
1334
|
+
break;
|
|
1335
|
+
case "--production":
|
|
1336
|
+
options.production = true;
|
|
1337
|
+
break;
|
|
1338
|
+
case "--app":
|
|
1339
|
+
options.app = args[++index] || null;
|
|
1340
|
+
break;
|
|
1341
|
+
case "--monitor":
|
|
1342
|
+
options.monitor = true;
|
|
1343
|
+
break;
|
|
1344
|
+
case "--report":
|
|
1345
|
+
options.report = args[++index] || "markdown";
|
|
1346
|
+
break;
|
|
1347
|
+
case "--create-ticket":
|
|
1348
|
+
options.createTicket = true;
|
|
1349
|
+
break;
|
|
1350
|
+
case "--create-voc":
|
|
1351
|
+
options.createVoc = true;
|
|
1352
|
+
break;
|
|
1353
|
+
case "--auto-submit":
|
|
1354
|
+
options.autoSubmit = true;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
return options;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* 执行 doctor 命令。
|
|
1364
|
+
* @param {string[]} args - CLI 参数列表
|
|
1365
|
+
*/
|
|
1366
|
+
async function run(args) {
|
|
1367
|
+
const options = parseArgs(args);
|
|
1368
|
+
const projectRoot = findProjectRoot();
|
|
1369
|
+
const doFix = options.repair || options.fix;
|
|
1370
|
+
|
|
1371
|
+
// ── 监控模式 ──
|
|
1372
|
+
if (options.monitor) {
|
|
1373
|
+
console.log("📊 启动健康度实时监控...\n");
|
|
1374
|
+
const monitor = new HealthMonitor({
|
|
1375
|
+
projectRoot,
|
|
1376
|
+
intervalMs: 60_000,
|
|
1377
|
+
onResult: (snapshot) => {
|
|
1378
|
+
console.log(monitor.formatMonitorOutput(snapshot));
|
|
1379
|
+
console.log("");
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
monitor.start();
|
|
1383
|
+
console.log("按 Ctrl+C 停止监控\n");
|
|
1384
|
+
process.on("SIGINT", () => {
|
|
1385
|
+
monitor.stop();
|
|
1386
|
+
console.log("\n👋 监控已停止");
|
|
1387
|
+
process.exit(0);
|
|
1388
|
+
});
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// ── 创建工单 ──
|
|
1393
|
+
if (options.createTicket) {
|
|
1394
|
+
const readline = require("readline");
|
|
1395
|
+
const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1396
|
+
readlineInterface.question("工单标题:", (title) => {
|
|
1397
|
+
readlineInterface.question("问题描述:", async (description) => {
|
|
1398
|
+
readlineInterface.close();
|
|
1399
|
+
const creator = new TicketCreator({ projectRoot });
|
|
1400
|
+
const ticket = await creator.createTicket({ title, description, type: "bug" });
|
|
1401
|
+
if (ticket.status === "submitted") {
|
|
1402
|
+
console.log("\n✅ 工单已提交:" + ticket.remoteUrl);
|
|
1403
|
+
} else {
|
|
1404
|
+
console.log("\n✅ 工单已保存到本地(ID: " + ticket.id + ")");
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// ── 创建 VOC ──
|
|
1412
|
+
if (options.createVoc) {
|
|
1413
|
+
const readline = require("readline");
|
|
1414
|
+
const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1415
|
+
readlineInterface.question("需求标题:", (title) => {
|
|
1416
|
+
readlineInterface.question("需求描述:", async (description) => {
|
|
1417
|
+
readlineInterface.close();
|
|
1418
|
+
const creator = new VOCCreator({ projectRoot });
|
|
1419
|
+
const voc = await creator.createVOC({ title, description });
|
|
1420
|
+
if (voc.status === "submitted") {
|
|
1421
|
+
console.log("\n✅ VOC 已提交:" + voc.remoteUrl);
|
|
1422
|
+
} else {
|
|
1423
|
+
console.log("\n✅ VOC 已保存到本地(ID: " + voc.id + ")");
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
});
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// ── 自动提交 ──
|
|
1431
|
+
if (options.autoSubmit) {
|
|
1432
|
+
const readline = require("readline");
|
|
1433
|
+
const readlineInterface = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1434
|
+
readlineInterface.question("标题:", (title) => {
|
|
1435
|
+
readlineInterface.question("描述:", async (description) => {
|
|
1436
|
+
readlineInterface.close();
|
|
1437
|
+
const decider = new SubmissionDecider({ projectRoot });
|
|
1438
|
+
const result = await decider.autoSubmit({ title, description });
|
|
1439
|
+
console.log(
|
|
1440
|
+
"\n🤖 智能判断:" + result.decision.reason +
|
|
1441
|
+
"(置信度 " + Math.round(result.decision.confidence * 100) + "%)"
|
|
1442
|
+
);
|
|
1443
|
+
console.log("✅ " + result.message);
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// ── 主诊断流程 ──
|
|
1450
|
+
const engine = new DiagnosticEngine({ projectRoot });
|
|
1451
|
+
engine.registerChecker(new EnvironmentChecker({ projectRoot }));
|
|
1452
|
+
engine.registerChecker(new ApplicationChecker({ projectRoot, appId: options.app }));
|
|
1453
|
+
|
|
1454
|
+
if (options.production && options.app) {
|
|
1455
|
+
engine.registerChecker(new ProductionErrorCollector({ projectRoot, appId: options.app }));
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
console.log("🔍 检查 OpenYida 环境依赖...\n");
|
|
1459
|
+
|
|
1460
|
+
const results = await engine.runAll();
|
|
1461
|
+
console.log(engine.formatConsoleOutput());
|
|
1462
|
+
|
|
1463
|
+
// 自动修复
|
|
1464
|
+
if (doFix) {
|
|
1465
|
+
const fixableIssues = engine.getAutoFixableIssues();
|
|
1466
|
+
if (fixableIssues.length > 0) {
|
|
1467
|
+
console.log("\n🔧 正在自动修复...\n");
|
|
1468
|
+
const fixEngine = new FixEngine({ projectRoot });
|
|
1469
|
+
await fixEngine.autoFix(fixableIssues);
|
|
1470
|
+
console.log(fixEngine.formatFixOutput());
|
|
1471
|
+
} else {
|
|
1472
|
+
console.log("\n✅ 没有可自动修复的问题");
|
|
1473
|
+
}
|
|
1474
|
+
} else {
|
|
1475
|
+
const summary = engine.getSummary();
|
|
1476
|
+
if (summary.autoFixable > 0) {
|
|
1477
|
+
console.log("\n运行 yida doctor --fix 自动修复可修复的问题");
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// 生成报告
|
|
1482
|
+
if (options.report) {
|
|
1483
|
+
const reporter = new ReportGenerator({ projectRoot });
|
|
1484
|
+
const summary = engine.getSummary();
|
|
1485
|
+
const reportPath = reporter.generate(results, summary, options.report);
|
|
1486
|
+
console.log("\n📄 诊断报告已生成:" + reportPath);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
module.exports = {
|
|
1491
|
+
DiagnosticEngine,
|
|
1492
|
+
EnvironmentChecker,
|
|
1493
|
+
ApplicationChecker,
|
|
1494
|
+
FixEngine,
|
|
1495
|
+
ReportGenerator,
|
|
1496
|
+
PreChecker,
|
|
1497
|
+
HealthMonitor,
|
|
1498
|
+
ProductionErrorCollector,
|
|
1499
|
+
TicketCreator,
|
|
1500
|
+
VOCCreator,
|
|
1501
|
+
SubmissionDecider,
|
|
1502
|
+
run,
|
|
1503
|
+
};
|