devassist-agent 1.0.3 → 1.0.5
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/devassist.js +5 -3
- package/package.json +1 -1
- package/src/cli/commands/report.js +104 -0
- package/src/cli/commands/start.js +9 -1
- package/src/cli/commands/watch.js +322 -68
package/bin/devassist.js
CHANGED
|
@@ -166,12 +166,13 @@ function printHelp() {
|
|
|
166
166
|
printHeader('开发守护 CLI');
|
|
167
167
|
console.log(`${c.bold('用法:')} devassist <命令> [选项]\n`);
|
|
168
168
|
console.log(`${c.bold('命令:')}`);
|
|
169
|
-
console.log(` ${c.green('start')} 一键启动:自动初始化 + AI
|
|
169
|
+
console.log(` ${c.green('start')} 一键启动:自动初始化 + AI双引擎实时监控 + 自动修复`);
|
|
170
170
|
console.log(` ${c.green('init')} 初始化项目——创建 .devassist/ 配置与基础代码`);
|
|
171
171
|
console.log(` ${c.green('check')} 运行全部开发时检查(代码质量 + 约定 + 模式 + 架构)`);
|
|
172
172
|
console.log(` ${c.green('watch')} 实时监控文件变更并运行检查`);
|
|
173
173
|
console.log(` ${c.green('diff')} ${c.gray('--staged|--base <分支>')} 仅检查变更文件(CI 模式)`);
|
|
174
174
|
console.log(` ${c.green('report')} ${c.gray('[--open]')} 生成 HTML 检查报告`);
|
|
175
|
+
console.log(` ${c.gray(' --watch-history [n]')} 查看最近 n 条监控复盘报告(默认 20)`);
|
|
175
176
|
console.log(` ${c.green('deploy')} ${c.gray('--check')} 部署前验证(端口 + 路由 + MIME + 清单)`);
|
|
176
177
|
console.log(` ${c.green('debt')} ${c.gray('--scan')} 查看技术债务看板(按优先级排序)`);
|
|
177
178
|
console.log(` ${c.green('convention')} ${c.gray('add|list|remove')} 管理项目约定`);
|
|
@@ -189,18 +190,19 @@ function printHelp() {
|
|
|
189
190
|
console.log(` ${c.gray('--ai')} 启用 AI 深度分析(需配置)`);
|
|
190
191
|
console.log(` ${c.gray('--compare')} 多引擎对比分析(所有引擎并行)`);
|
|
191
192
|
console.log(` ${c.gray('--json')} 输出 JSON 格式(用于 CI 集成)`);
|
|
193
|
+
console.log(` ${c.gray('--fix')} 自动修复安全项(watch 模式下生效)`);
|
|
192
194
|
console.log(` ${c.gray('--fail-on <级别>')} 在以下级别非零退出:block(默认)或 warn`);
|
|
193
195
|
console.log(` ${c.gray('--base-code')} 安装运行时基础设施(Layer 2)`);
|
|
194
196
|
console.log(` ${c.gray('--force')} 覆盖已有配置`);
|
|
195
197
|
console.log(` ${c.gray('-v, --version')} 显示版本`);
|
|
196
198
|
console.log(` ${c.gray('-h, --help')} 显示帮助\n`);
|
|
197
199
|
console.log(`${c.bold('示例:')}`);
|
|
198
|
-
console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI
|
|
200
|
+
console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI双引擎+自动修复)`);
|
|
199
201
|
console.log(` ${c.gray('$')} devassist start --check # 一键检查(不启动监控)`);
|
|
200
202
|
console.log(` ${c.gray('$')} devassist init`);
|
|
201
203
|
console.log(` ${c.gray('$')} devassist check --ai`);
|
|
202
204
|
console.log(` ${c.gray('$')} devassist watch # 开发时实时监控`);
|
|
203
|
-
console.log(` ${c.gray('$')} devassist watch --ai --compare #
|
|
205
|
+
console.log(` ${c.gray('$')} devassist watch --ai --compare --fix # 全自动:检测+AI+修复`);
|
|
204
206
|
console.log(` ${c.gray('$')} devassist diff --staged # 提交前检查暂存文件`);
|
|
205
207
|
console.log(` ${c.gray('$')} devassist report --open # 生成并查看 HTML 报告`);
|
|
206
208
|
console.log(` ${c.gray('$')} devassist convention add --id naming-list --rule "使用 data.list 而非 data.items" --severity block`);
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* devassist report # Generate report
|
|
13
13
|
* devassist report --ai # Include AI analysis
|
|
14
14
|
* devassist report --open # Open in browser after generating
|
|
15
|
+
* devassist report --watch-history [n] # Show recent watch reports (default 20)
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
const fs = require('fs');
|
|
@@ -38,6 +39,11 @@ module.exports = async function report(projectRoot, args, ui) {
|
|
|
38
39
|
const useAI = args.includes('--ai');
|
|
39
40
|
const openBrowser = args.includes('--open');
|
|
40
41
|
|
|
42
|
+
// Watch history mode
|
|
43
|
+
if (args.includes('--watch-history')) {
|
|
44
|
+
return showWatchHistory(projectRoot, args, ui);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
printHeader('生成报告');
|
|
42
48
|
|
|
43
49
|
// Collect files using shared utility
|
|
@@ -308,3 +314,101 @@ function findingHTML(f) {
|
|
|
308
314
|
function escapeHTML(str) {
|
|
309
315
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
310
316
|
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Show watch history for retrospective.
|
|
320
|
+
* Usage: devassist report --watch-history [n]
|
|
321
|
+
*/
|
|
322
|
+
function showWatchHistory(projectRoot, args, ui) {
|
|
323
|
+
const { printHeader, c } = ui;
|
|
324
|
+
printHeader('监控复盘报告');
|
|
325
|
+
|
|
326
|
+
const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
|
|
327
|
+
const logPath = path.join(historyDir, 'watch-log.jsonl');
|
|
328
|
+
|
|
329
|
+
if (!fs.existsSync(logPath)) {
|
|
330
|
+
console.log(` ${c.yellow('暂无监控历史记录。')}`);
|
|
331
|
+
console.log(` ${c.gray('运行 devassist start 后,文件变更的检查/分析/修复记录会自动保存到这里。')}\n`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Parse limit from args
|
|
336
|
+
let limit = 20;
|
|
337
|
+
const idx = args.indexOf('--watch-history');
|
|
338
|
+
if (idx >= 0 && args[idx + 1] && /^\d+$/.test(args[idx + 1])) {
|
|
339
|
+
limit = parseInt(args[idx + 1]);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Read JSONL log
|
|
343
|
+
const lines = fs.readFileSync(logPath, 'utf-8').trim().split('\n').filter(Boolean);
|
|
344
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
|
|
345
|
+
|
|
346
|
+
if (entries.length === 0) {
|
|
347
|
+
console.log(` ${c.yellow('暂无监控历史记录。')}\n`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Show recent entries (newest first)
|
|
352
|
+
const recent = entries.slice(-limit).reverse();
|
|
353
|
+
console.log(` ${c.gray(`共 ${entries.length} 条记录,显示最近 ${recent.length} 条:`)}\n`);
|
|
354
|
+
|
|
355
|
+
// Summary stats
|
|
356
|
+
const totalEvents = entries.length;
|
|
357
|
+
const totalFindings = entries.reduce((s, e) => s + (e.total || 0), 0);
|
|
358
|
+
const totalBlocks = entries.reduce((s, e) => s + (e.blocks || 0), 0);
|
|
359
|
+
const totalFixed = entries.reduce((s, e) => s + (e.fixed || 0), 0);
|
|
360
|
+
const aiEvents = entries.filter(e => e.aiUsed).length;
|
|
361
|
+
|
|
362
|
+
console.log(` ${c.bold('汇总统计:')}`);
|
|
363
|
+
console.log(` 监控事件:${totalEvents} 次`);
|
|
364
|
+
console.log(` 发现问题:${totalFindings} 个(其中 ${c.red(totalBlocks + ' 阻断')})`);
|
|
365
|
+
console.log(` 自动修复:${c.green(totalFixed + ' 处')}`);
|
|
366
|
+
console.log(` AI 分析:${aiEvents} 次\n`);
|
|
367
|
+
|
|
368
|
+
// Detail table
|
|
369
|
+
console.log(` ${c.bold('最近事件:')}\n`);
|
|
370
|
+
console.log(` ${c.gray('时间'.padEnd(20))} ${c.gray('文件'.padEnd(30))} ${c.gray('问题'.padEnd(8))} ${c.gray('修复'.padEnd(6))} ${c.gray('引擎')}`);
|
|
371
|
+
console.log(` ${'─'.repeat(90)}`);
|
|
372
|
+
|
|
373
|
+
for (const e of recent) {
|
|
374
|
+
const time = new Date(e.timestamp).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
375
|
+
const file = (e.file || '').length > 28 ? '...' + (e.file || '').slice(-25) : (e.file || '');
|
|
376
|
+
const issues = e.total > 0 ? c.yellow(String(e.total)) : c.green('0');
|
|
377
|
+
const blocks = e.blocks > 0 ? c.red(`(${e.blocks}阻断)`) : '';
|
|
378
|
+
const fixed = e.fixed > 0 ? c.green(String(e.fixed)) : '-';
|
|
379
|
+
const engines = e.engines || '-';
|
|
380
|
+
|
|
381
|
+
console.log(` ${time.padEnd(20)} ${file.padEnd(30)} ${issues} ${blocks}`.padEnd(58) + ` ${fixed.toString().padEnd(6)} ${engines}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Latest report detail
|
|
385
|
+
const latestPath = path.join(projectRoot, '.devassist', 'reports', 'latest.json');
|
|
386
|
+
if (fs.existsSync(latestPath)) {
|
|
387
|
+
console.log(`\n ${c.gray('最近一次完整报告:')} ${latestPath}`);
|
|
388
|
+
console.log(` ${c.gray('历史报告目录:')} ${historyDir}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Watch files with most issues
|
|
392
|
+
const fileStats = {};
|
|
393
|
+
for (const e of entries) {
|
|
394
|
+
if (!fileStats[e.file]) fileStats[e.file] = { count: 0, issues: 0, blocks: 0, fixed: 0 };
|
|
395
|
+
fileStats[e.file].count++;
|
|
396
|
+
fileStats[e.file].issues += e.total || 0;
|
|
397
|
+
fileStats[e.file].blocks += e.blocks || 0;
|
|
398
|
+
fileStats[e.file].fixed += e.fixed || 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const hotFiles = Object.entries(fileStats)
|
|
402
|
+
.sort((a, b) => b[1].issues - a[1].issues)
|
|
403
|
+
.filter(([, s]) => s.issues > 0)
|
|
404
|
+
.slice(0, 5);
|
|
405
|
+
|
|
406
|
+
if (hotFiles.length > 0) {
|
|
407
|
+
console.log(`\n ${c.bold('问题热点文件 TOP 5:')}\n`);
|
|
408
|
+
for (const [file, s] of hotFiles) {
|
|
409
|
+
console.log(` ${file} — ${s.issues} 个问题,${s.blocks} 阻断,${s.fixed} 已修复(${s.count} 次变更)`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
console.log('');
|
|
414
|
+
}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* 3. Start watch mode with --ai --compare
|
|
8
8
|
*
|
|
9
9
|
* Usage:
|
|
10
|
-
* devassist start # Full auto: init + watch --ai --compare
|
|
10
|
+
* devassist start # Full auto: init + watch --ai --compare --fix
|
|
11
11
|
* devassist start --no-ai # Init + watch (rules only, no AI)
|
|
12
|
+
* devassist start --no-fix # Init + watch --ai --compare (no auto-fix)
|
|
12
13
|
* devassist start --check # Init + one-time check --ai --compare (no watch)
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -20,6 +21,7 @@ module.exports = async function start(projectRoot, args, ui) {
|
|
|
20
21
|
const { printHeader, printFinding, printStats, c } = ui;
|
|
21
22
|
|
|
22
23
|
const noAI = args.includes('--no-ai');
|
|
24
|
+
const noFix = args.includes('--no-fix');
|
|
23
25
|
const checkOnly = args.includes('--check');
|
|
24
26
|
|
|
25
27
|
printHeader('一键启动');
|
|
@@ -51,6 +53,9 @@ module.exports = async function start(projectRoot, args, ui) {
|
|
|
51
53
|
if (aiReady && !noAI) {
|
|
52
54
|
const engineNames = readyEngines.map(e => e.name || e.provider).join(' + ');
|
|
53
55
|
console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${engineNames})`);
|
|
56
|
+
if (!noFix) {
|
|
57
|
+
console.log(` ${c.gray('自动修复已启用(--fix)')}`);
|
|
58
|
+
}
|
|
54
59
|
} else if (readyEngines.length === 1 && !noAI) {
|
|
55
60
|
console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${readyEngines[0].name},仅单引擎)`);
|
|
56
61
|
console.log(` ${c.gray('提示:配置第二个引擎可启用对比模式')}`);
|
|
@@ -79,6 +84,9 @@ module.exports = async function start(projectRoot, args, ui) {
|
|
|
79
84
|
if (!noAI) {
|
|
80
85
|
watchArgs.push('--ai', '--compare');
|
|
81
86
|
}
|
|
87
|
+
if (!noFix) {
|
|
88
|
+
watchArgs.push('--fix');
|
|
89
|
+
}
|
|
82
90
|
await watchCmd(projectRoot, watchArgs, ui);
|
|
83
91
|
}
|
|
84
92
|
};
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* devassist watch - Real-time file monitoring
|
|
2
|
+
* devassist watch - Real-time file monitoring with full automation
|
|
3
3
|
*
|
|
4
4
|
* Watches project files for changes and runs checks on modified files.
|
|
5
|
-
*
|
|
5
|
+
* With --ai flag, automatically runs AI analysis on findings.
|
|
6
|
+
* With --compare flag, runs multi-engine comparison.
|
|
7
|
+
* With --fix flag, automatically applies safe fixes.
|
|
6
8
|
*
|
|
7
9
|
* Usage:
|
|
8
|
-
* devassist watch
|
|
9
|
-
* devassist watch --ai
|
|
10
|
-
* devassist watch --
|
|
10
|
+
* devassist watch # Watch all source files (rules only)
|
|
11
|
+
* devassist watch --ai # Watch + AI analysis on findings
|
|
12
|
+
* devassist watch --ai --compare # Watch + multi-engine comparison
|
|
13
|
+
* devassist watch --ai --compare --fix # Full auto: detect + AI + fix
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
const fs = require('fs');
|
|
14
17
|
const path = require('path');
|
|
15
|
-
const { execSync } = require('child_process');
|
|
16
18
|
|
|
17
19
|
// Core
|
|
18
20
|
const { engine, RuleEngine } = require('../../core/rule-engine');
|
|
@@ -25,14 +27,29 @@ const { ArchRiskAssessor } = require('../../modules/dev-time/arch-risk-assessor'
|
|
|
25
27
|
// Shared utilities
|
|
26
28
|
const { loadConfig } = require('../shared/config-loader');
|
|
27
29
|
const { filterIgnored } = require('../shared/inline-ignore');
|
|
30
|
+
const { enrichWithContext } = require('../shared/code-context');
|
|
31
|
+
|
|
32
|
+
// AI
|
|
33
|
+
const ai = require('../../ai/adapter');
|
|
34
|
+
const qwen = require('../../ai/providers/qwen');
|
|
35
|
+
const openai = require('../../ai/providers/openai');
|
|
36
|
+
const claude = require('../../ai/providers/claude');
|
|
37
|
+
const hunyuan = require('../../ai/providers/hunyuan');
|
|
38
|
+
|
|
39
|
+
// Register AI providers
|
|
40
|
+
ai.registerProvider('qwen', qwen);
|
|
41
|
+
ai.registerProvider('openai', openai);
|
|
42
|
+
ai.registerProvider('claude', claude);
|
|
43
|
+
ai.registerProvider('hunyuan', hunyuan);
|
|
28
44
|
|
|
29
45
|
const CODE_EXTENSIONS = /\.(js|ts|jsx|tsx|py|java)$/;
|
|
30
46
|
const SKIP_DIRS = ['node_modules', '.git', '.devassist', 'dist', 'build', 'vendor', '.gradle', '__pycache__'];
|
|
31
47
|
|
|
32
48
|
module.exports = async function watch(projectRoot, args, ui) {
|
|
33
|
-
const { printHeader, printFinding, c } = ui;
|
|
49
|
+
const { printHeader, printFinding, printStats, c } = ui;
|
|
34
50
|
const useAI = args.includes('--ai');
|
|
35
51
|
const useCompare = args.includes('--compare');
|
|
52
|
+
const autoFix = args.includes('--fix');
|
|
36
53
|
const watchDir = args.includes('--dir') ? args[args.indexOf('--dir') + 1] : null;
|
|
37
54
|
|
|
38
55
|
printHeader('监控模式');
|
|
@@ -40,6 +57,7 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
40
57
|
console.log(` ${c.gray('监控目录:')} ${watchDir || '整个项目'}`);
|
|
41
58
|
console.log(` ${c.gray('AI 分析:')} ${useAI ? '已启用' : '已禁用'}`);
|
|
42
59
|
console.log(` ${c.gray('多引擎对比:')} ${useCompare ? '已启用' : '已禁用'}`);
|
|
60
|
+
console.log(` ${c.gray('自动修复:')} ${autoFix ? '已启用' : '已禁用'}`);
|
|
43
61
|
console.log(` ${c.gray('按 Ctrl+C 停止')}\n`);
|
|
44
62
|
|
|
45
63
|
// Initialize convention store
|
|
@@ -62,9 +80,28 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
62
80
|
}
|
|
63
81
|
});
|
|
64
82
|
|
|
83
|
+
// Load AI config (global or project-level)
|
|
84
|
+
let aiConfig = null;
|
|
85
|
+
let readyEngines = [];
|
|
86
|
+
if (useAI) {
|
|
87
|
+
const { loadAIConfig } = require('./ai');
|
|
88
|
+
aiConfig = loadAIConfig(projectRoot);
|
|
89
|
+
if (aiConfig && aiConfig.engines) {
|
|
90
|
+
readyEngines = aiConfig.engines.filter(e => e.enabled && e.apiKey);
|
|
91
|
+
}
|
|
92
|
+
if (readyEngines.length === 0) {
|
|
93
|
+
console.log(` ${c.yellow('警告:')} 未检测到可用的 AI 引擎,将仅使用规则引擎\n`);
|
|
94
|
+
} else if (useCompare && readyEngines.length < 2) {
|
|
95
|
+
console.log(` ${c.yellow('提示:')} 对比模式需要 2+ 引擎,当前仅 ${readyEngines.length} 个,将使用单引擎\n`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` ${c.green('AI 引擎就绪:')} ${readyEngines.map(e => e.name || e.provider).join(' + ')}\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
// Track watchers for cleanup
|
|
66
102
|
const watchers = [];
|
|
67
103
|
const debounceTimers = new Map();
|
|
104
|
+
const analyzingFiles = new Set(); // Lock: prevent overlapping AI analysis on same file
|
|
68
105
|
|
|
69
106
|
// Watch a single file
|
|
70
107
|
function watchFile(filePath) {
|
|
@@ -72,14 +109,17 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
72
109
|
const watcher = fs.watch(filePath, { persistent: true }, (eventType) => {
|
|
73
110
|
if (eventType !== 'change') return;
|
|
74
111
|
|
|
75
|
-
// Debounce: wait
|
|
112
|
+
// Debounce: wait 500ms after last change before checking (longer with AI)
|
|
113
|
+
const debounceMs = useAI ? 500 : 300;
|
|
76
114
|
if (debounceTimers.has(filePath)) {
|
|
77
115
|
clearTimeout(debounceTimers.get(filePath));
|
|
78
116
|
}
|
|
79
117
|
debounceTimers.set(filePath, setTimeout(() => {
|
|
80
118
|
debounceTimers.delete(filePath);
|
|
81
|
-
checkFile(filePath)
|
|
82
|
-
|
|
119
|
+
checkFile(filePath).catch(err => {
|
|
120
|
+
console.log(` ${c.red('检查出错:')} ${err.message}`);
|
|
121
|
+
});
|
|
122
|
+
}, debounceMs));
|
|
83
123
|
});
|
|
84
124
|
watcher.on('error', () => {});
|
|
85
125
|
watchers.push(watcher);
|
|
@@ -102,61 +142,165 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
102
142
|
}
|
|
103
143
|
}
|
|
104
144
|
|
|
105
|
-
// Check a single file
|
|
106
|
-
function checkFile(filePath) {
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
// Check a single file — async for AI analysis
|
|
146
|
+
async function checkFile(filePath) {
|
|
147
|
+
// Skip if already analyzing this file (AI takes time)
|
|
148
|
+
if (analyzingFiles.has(filePath)) return;
|
|
149
|
+
analyzingFiles.add(filePath);
|
|
109
150
|
|
|
110
|
-
|
|
111
|
-
|
|
151
|
+
try {
|
|
152
|
+
const relPath = path.relative(projectRoot, filePath);
|
|
153
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
112
154
|
|
|
113
|
-
|
|
114
|
-
|
|
155
|
+
let content = '';
|
|
156
|
+
try { content = fs.readFileSync(filePath, 'utf-8'); } catch (_) { return; }
|
|
115
157
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
projectRoot,
|
|
119
|
-
config: loadConfig(projectRoot),
|
|
120
|
-
};
|
|
158
|
+
// Quick skip for HTML without script tags
|
|
159
|
+
if (/\.html?$/.test(filePath) && !/<script[\s>]/i.test(content)) return;
|
|
121
160
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
161
|
+
const ctx = {
|
|
162
|
+
files: [{ path: relPath, content, fullPath: filePath }],
|
|
163
|
+
projectRoot,
|
|
164
|
+
config: loadConfig(projectRoot),
|
|
165
|
+
};
|
|
125
166
|
|
|
126
|
-
|
|
127
|
-
|
|
167
|
+
const { findings: ruleFindings } = engine.run(ctx);
|
|
168
|
+
const archFindings = archAssessor.analyzeFile(relPath, content);
|
|
169
|
+
let allFindings = [...ruleFindings, ...archFindings];
|
|
128
170
|
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
// Apply inline ignores
|
|
172
|
+
allFindings = filterIgnored(allFindings, [{ path: relPath, content }]);
|
|
131
173
|
|
|
132
|
-
|
|
133
|
-
|
|
174
|
+
// Print header
|
|
175
|
+
console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
|
|
134
176
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
if (allFindings.length === 0) {
|
|
178
|
+
console.log(` ${c.green('OK')} — 未发现问题`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
139
181
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
// --- AI Analysis (if enabled and there are warn/block findings) ---
|
|
183
|
+
let finalFindings = allFindings;
|
|
184
|
+
const suspectFindings = allFindings.filter(f => f.severity === 'block' || f.severity === 'warn');
|
|
185
|
+
|
|
186
|
+
if (useAI && readyEngines.length > 0 && suspectFindings.length > 0) {
|
|
187
|
+
console.log(` ${c.cyan('AI 分析中...')} (${suspectFindings.length} 个疑似问题)`);
|
|
188
|
+
|
|
189
|
+
// Enrich with code context
|
|
190
|
+
const fileContents = [{ path: relPath, content }];
|
|
191
|
+
finalFindings = enrichWithContext(allFindings, fileContents);
|
|
192
|
+
|
|
193
|
+
const aiContext = {
|
|
194
|
+
conventions: conventionStore.getRelevant({}),
|
|
195
|
+
projectInfo: { root: projectRoot },
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
if (useCompare && readyEngines.length >= 2) {
|
|
200
|
+
finalFindings = await ai.multiAnalyze(readyEngines, finalFindings, aiContext);
|
|
201
|
+
} else {
|
|
202
|
+
finalFindings = await ai.analyze(readyEngines[0], finalFindings, aiContext);
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.log(` ${c.yellow('AI 分析失败:')} ${err.message}`);
|
|
206
|
+
console.log(` ${c.gray('已回退到规则引擎结果')}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
153
209
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
210
|
+
// --- Print findings ---
|
|
211
|
+
const blocks = finalFindings.filter(f => f.severity === 'block');
|
|
212
|
+
const warns = finalFindings.filter(f => f.severity === 'warn');
|
|
213
|
+
const infos = finalFindings.filter(f => f.severity === 'info');
|
|
214
|
+
|
|
215
|
+
for (const f of finalFindings) {
|
|
216
|
+
const icon = f.severity === 'block' ? c.red('[阻断]') :
|
|
217
|
+
f.severity === 'warn' ? c.yellow('[警告]') :
|
|
218
|
+
c.gray('[提示]');
|
|
219
|
+
console.log(` ${icon} ${f.message}`);
|
|
220
|
+
if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
|
|
221
|
+
if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
|
|
222
|
+
|
|
223
|
+
// Print AI analysis if available
|
|
224
|
+
if (f.aiAnalysis) {
|
|
225
|
+
const a = f.aiAnalysis;
|
|
226
|
+
if (a.falsePositive) {
|
|
227
|
+
console.log(` ${c.gray('AI 判定:误报 — ' + (a.falsePositiveReason || '不适用'))}`);
|
|
228
|
+
} else {
|
|
229
|
+
const rootCause = a.rootCause || a.root_cause || '';
|
|
230
|
+
const fixSuggestion = a.fixSuggestion || a.fix_suggestion || '';
|
|
231
|
+
const confidence = a.confidence || '';
|
|
232
|
+
if (rootCause) console.log(` ${c.cyan('AI 根因:')} ${rootCause}`);
|
|
233
|
+
if (fixSuggestion) console.log(` ${c.cyan('AI 建议:')} ${fixSuggestion}`);
|
|
234
|
+
if (confidence) console.log(` ${c.gray(`置信度:${confidence}`)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Print multi-engine comparison
|
|
239
|
+
if (f.aiAnalyses && f.aiAnalyses.length >= 2) {
|
|
240
|
+
for (const ea of f.aiAnalyses) {
|
|
241
|
+
if (ea.analysis && !ea.failed) {
|
|
242
|
+
const engineName = ea.engineName || ea.provider;
|
|
243
|
+
const a = ea.analysis;
|
|
244
|
+
if (a.falsePositive) {
|
|
245
|
+
console.log(` ${c.gray(`[${engineName}] 误报 — ${a.falsePositiveReason || ''}`)}`);
|
|
246
|
+
} else {
|
|
247
|
+
const rootCause = a.rootCause || a.root_cause || '';
|
|
248
|
+
if (rootCause) console.log(` ${c.cyan(`[${engineName}]`)} ${rootCause}`);
|
|
249
|
+
}
|
|
250
|
+
} else if (ea.failed) {
|
|
251
|
+
console.log(` ${c.gray(`[${ea.engineName || ea.provider}] 分析失败`)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Agreement indicator
|
|
255
|
+
if (f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && !ea.analysis.falsePositive).length === 0
|
|
256
|
+
&& f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && ea.analysis.falsePositive).length >= 2) {
|
|
257
|
+
console.log(` ${c.green('引擎一致:均判定为误报')}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Summary ---
|
|
263
|
+
const parts = [];
|
|
264
|
+
if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
|
|
265
|
+
if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
|
|
266
|
+
if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
|
|
267
|
+
console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
|
|
268
|
+
|
|
269
|
+
// --- Auto Fix (if enabled) ---
|
|
270
|
+
let fixResult = null;
|
|
271
|
+
if (autoFix && suspectFindings.length > 0) {
|
|
272
|
+
fixResult = await autoFixFile(filePath, content, projectRoot, c);
|
|
273
|
+
if (fixResult.count > 0) {
|
|
274
|
+
console.log(` ${c.green('自动修复')} ${fixResult.count} 处问题`);
|
|
275
|
+
for (const d of fixResult.details) {
|
|
276
|
+
const typeLabel = d.type === 'fetch-catch' ? 'fetch 添加 .catch()' : '添加 await';
|
|
277
|
+
console.log(` ${c.gray(`第${d.line}行`)} ${typeLabel}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- Save comprehensive report for retrospective ---
|
|
283
|
+
saveWatchReport(projectRoot, {
|
|
284
|
+
timestamp: new Date().toISOString(),
|
|
285
|
+
file: relPath,
|
|
286
|
+
findings: finalFindings,
|
|
287
|
+
aiAnalysisUsed: useAI && readyEngines.length > 0 && suspectFindings.length > 0,
|
|
288
|
+
compareMode: useCompare && readyEngines.length >= 2,
|
|
289
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
290
|
+
fixes: fixResult ? fixResult.details : [],
|
|
291
|
+
fixBackupPath: fixResult ? fixResult.backupPath : null,
|
|
292
|
+
stats: {
|
|
293
|
+
total: finalFindings.length,
|
|
294
|
+
blocks: blocks.length,
|
|
295
|
+
warnings: warns.length,
|
|
296
|
+
infos: infos.length,
|
|
297
|
+
fixed: fixResult ? fixResult.count : 0,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
} finally {
|
|
302
|
+
analyzingFiles.delete(filePath);
|
|
303
|
+
}
|
|
160
304
|
}
|
|
161
305
|
|
|
162
306
|
// Start watching
|
|
@@ -199,19 +343,129 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
199
343
|
});
|
|
200
344
|
};
|
|
201
345
|
|
|
202
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Auto-fix a single file in-place (safe fixes only).
|
|
348
|
+
* Returns the number of fixes applied.
|
|
349
|
+
*/
|
|
350
|
+
async function autoFixFile(filePath, content, projectRoot, c) {
|
|
351
|
+
const lines = content.split('\n');
|
|
352
|
+
let fixCount = 0;
|
|
353
|
+
const changes = [];
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < lines.length; i++) {
|
|
356
|
+
const line = lines[i];
|
|
357
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
|
|
358
|
+
|
|
359
|
+
// Fix 1: fetch() without .catch()
|
|
360
|
+
if (/\bfetch\s*\(/.test(line)) {
|
|
361
|
+
const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
362
|
+
if (/\.catch\s*\(/.test(context)) continue;
|
|
363
|
+
const hasAwait = /\bawait\b/.test(line);
|
|
364
|
+
const hasTryCatch = /try\s*\{/.test(lines.slice(Math.max(0, i - 5), i + 1).join('\n'));
|
|
365
|
+
if (hasAwait && hasTryCatch) continue;
|
|
366
|
+
|
|
367
|
+
const trimmed = line.trimEnd();
|
|
368
|
+
if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
|
|
369
|
+
const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
|
|
370
|
+
changes.push({ line: i + 1, type: 'fetch-catch', before: line, after: fixedLine });
|
|
371
|
+
fixCount++;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Fix 2: Add await to async calls (fetch, axios, etc.)
|
|
377
|
+
if (!/\bawait\b/.test(line)) {
|
|
378
|
+
const asyncMatch = line.match(/(?<![\w.])(fetch|axios\.(get|post|put|delete|patch)|\.save\(|\.findOne\(|\.findById\(|\.create\()\s*/);
|
|
379
|
+
if (asyncMatch) {
|
|
380
|
+
const trimmed = line.trim();
|
|
381
|
+
if (/^\s*(const|let|var|if|while|return|throw)/.test(trimmed)) continue;
|
|
382
|
+
if (/^\s*(async\s+)?function\s+/.test(line)) continue;
|
|
383
|
+
if (/\.then\s*\(|\.catch\s*\(|\.finally\s*\(/.test(line)) continue;
|
|
384
|
+
|
|
385
|
+
const fixedLine = line.replace(
|
|
386
|
+
new RegExp('(?<![\\w.])(' + asyncMatch[1] + '\\w+)\\s*\\('),
|
|
387
|
+
'await $1('
|
|
388
|
+
);
|
|
389
|
+
if (fixedLine !== line) {
|
|
390
|
+
changes.push({ line: i + 1, type: 'add-await', before: line, after: fixedLine });
|
|
391
|
+
fixCount++;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (fixCount === 0) return { count: 0, details: [] };
|
|
399
|
+
|
|
400
|
+
// Apply changes
|
|
401
|
+
for (const ch of changes) {
|
|
402
|
+
lines[ch.line - 1] = ch.after;
|
|
403
|
+
}
|
|
404
|
+
const newContent = lines.join('\n');
|
|
405
|
+
|
|
406
|
+
// Backup original
|
|
407
|
+
const backupDir = path.join(projectRoot, '.devassist', 'backups', `watch-fix-${Date.now()}`);
|
|
408
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
409
|
+
const backupPath = path.join(backupDir, path.basename(filePath));
|
|
410
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
411
|
+
|
|
412
|
+
// Write fixed content
|
|
413
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
414
|
+
|
|
415
|
+
return { count: fixCount, details: changes, backupPath };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Save comprehensive watch report for retrospective.
|
|
420
|
+
* - latest.json: always overwritten with the latest event
|
|
421
|
+
* - history/watch-<timestamp>.json: per-event detail, kept for 90 days
|
|
422
|
+
* - history/watch-log.jsonl: append-only summary log for quick querying
|
|
423
|
+
*/
|
|
424
|
+
function saveWatchReport(projectRoot, report) {
|
|
203
425
|
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
204
426
|
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
427
|
+
|
|
428
|
+
// 1. Save latest.json (full detail, always latest)
|
|
429
|
+
const latestPath = path.join(reportDir, 'latest.json');
|
|
430
|
+
fs.writeFileSync(latestPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
431
|
+
|
|
432
|
+
// 2. Save timestamped history
|
|
433
|
+
const historyDir = path.join(projectRoot, '.devassist', 'history', 'watch');
|
|
434
|
+
if (!fs.existsSync(historyDir)) fs.mkdirSync(historyDir, { recursive: true });
|
|
435
|
+
|
|
436
|
+
const ts = new Date();
|
|
437
|
+
const tsStr = ts.toISOString().replace(/[:.]/g, '-');
|
|
438
|
+
const historyPath = path.join(historyDir, `watch-${tsStr}.json`);
|
|
439
|
+
fs.writeFileSync(historyPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
440
|
+
|
|
441
|
+
// 3. Append summary to JSONL log
|
|
442
|
+
const logPath = path.join(historyDir, 'watch-log.jsonl');
|
|
443
|
+
const summary = {
|
|
444
|
+
timestamp: report.timestamp,
|
|
445
|
+
file: report.file,
|
|
446
|
+
total: report.stats.total,
|
|
447
|
+
blocks: report.stats.blocks,
|
|
448
|
+
warnings: report.stats.warnings,
|
|
449
|
+
infos: report.stats.infos,
|
|
450
|
+
fixed: report.stats.fixed,
|
|
451
|
+
aiUsed: report.aiAnalysisUsed,
|
|
452
|
+
compare: report.compareMode,
|
|
453
|
+
engines: report.engines.map(e => e.provider).join('+'),
|
|
454
|
+
};
|
|
455
|
+
fs.appendFileSync(logPath, JSON.stringify(summary) + '\n', 'utf-8');
|
|
456
|
+
|
|
457
|
+
// 4. Auto-cleanup: remove history files older than 90 days
|
|
458
|
+
try {
|
|
459
|
+
const files = fs.readdirSync(historyDir);
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
const maxAge = 90 * 24 * 60 * 60 * 1000; // 90 days in ms
|
|
462
|
+
for (const f of files) {
|
|
463
|
+
if (!f.startsWith('watch-') || !f.endsWith('.json')) continue;
|
|
464
|
+
const filePath = path.join(historyDir, f);
|
|
465
|
+
const stat = fs.statSync(filePath);
|
|
466
|
+
if (now - stat.mtime.getTime() > maxAge) {
|
|
467
|
+
fs.unlinkSync(filePath);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (_) {}
|
|
217
471
|
}
|