devassist-agent 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/devassist.js +4 -3
- package/package.json +1 -1
- package/src/cli/commands/start.js +28 -18
- package/src/cli/commands/watch.js +250 -55
package/bin/devassist.js
CHANGED
|
@@ -166,7 +166,7 @@ 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')} 实时监控文件变更并运行检查`);
|
|
@@ -189,18 +189,19 @@ function printHelp() {
|
|
|
189
189
|
console.log(` ${c.gray('--ai')} 启用 AI 深度分析(需配置)`);
|
|
190
190
|
console.log(` ${c.gray('--compare')} 多引擎对比分析(所有引擎并行)`);
|
|
191
191
|
console.log(` ${c.gray('--json')} 输出 JSON 格式(用于 CI 集成)`);
|
|
192
|
+
console.log(` ${c.gray('--fix')} 自动修复安全项(watch 模式下生效)`);
|
|
192
193
|
console.log(` ${c.gray('--fail-on <级别>')} 在以下级别非零退出:block(默认)或 warn`);
|
|
193
194
|
console.log(` ${c.gray('--base-code')} 安装运行时基础设施(Layer 2)`);
|
|
194
195
|
console.log(` ${c.gray('--force')} 覆盖已有配置`);
|
|
195
196
|
console.log(` ${c.gray('-v, --version')} 显示版本`);
|
|
196
197
|
console.log(` ${c.gray('-h, --help')} 显示帮助\n`);
|
|
197
198
|
console.log(`${c.bold('示例:')}`);
|
|
198
|
-
console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI
|
|
199
|
+
console.log(` ${c.gray('$')} devassist start # 一键启动(自动init+AI双引擎+自动修复)`);
|
|
199
200
|
console.log(` ${c.gray('$')} devassist start --check # 一键检查(不启动监控)`);
|
|
200
201
|
console.log(` ${c.gray('$')} devassist init`);
|
|
201
202
|
console.log(` ${c.gray('$')} devassist check --ai`);
|
|
202
203
|
console.log(` ${c.gray('$')} devassist watch # 开发时实时监控`);
|
|
203
|
-
console.log(` ${c.gray('$')} devassist watch --ai --compare #
|
|
204
|
+
console.log(` ${c.gray('$')} devassist watch --ai --compare --fix # 全自动:检测+AI+修复`);
|
|
204
205
|
console.log(` ${c.gray('$')} devassist diff --staged # 提交前检查暂存文件`);
|
|
205
206
|
console.log(` ${c.gray('$')} devassist report --open # 生成并查看 HTML 报告`);
|
|
206
207
|
console.log(` ${c.gray('$')} devassist convention add --id naming-list --rule "使用 data.list 而非 data.items" --severity block`);
|
package/package.json
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Automatically:
|
|
5
5
|
* 1. Initialize project (if not already initialized)
|
|
6
|
-
* 2. Verify AI config (global or project-level)
|
|
6
|
+
* 2. Verify AI config (global or project-level, with real API keys)
|
|
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('一键启动');
|
|
@@ -36,26 +38,31 @@ module.exports = async function start(projectRoot, args, ui) {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
// ─── Step 2: Verify AI config ─────────────────────
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
+
// Use loadAIConfig to check if real API keys are configured
|
|
42
|
+
const aiCmd = require('./ai');
|
|
43
|
+
const config = aiCmd.loadAIConfig(projectRoot);
|
|
44
|
+
const readyEngines = config ? config.engines.filter(e => e.enabled && e.apiKey) : [];
|
|
45
|
+
const aiReady = readyEngines.length >= 2;
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
configSource = '项目配置';
|
|
48
|
-
} else if (fs.existsSync(globalAIConfig)) {
|
|
49
|
-
aiReady = true;
|
|
50
|
-
configSource = '全局配置';
|
|
51
|
-
}
|
|
47
|
+
// Determine config source
|
|
48
|
+
const projectConfigPath = path.join(projectRoot, '.devassist', 'ai-config.json');
|
|
49
|
+
const globalConfigPath = path.join(os.homedir(), '.devassist', 'ai-config.json');
|
|
50
|
+
const configSource = fs.existsSync(projectConfigPath) ? '项目配置' :
|
|
51
|
+
(fs.existsSync(globalConfigPath) ? '全局配置' : '');
|
|
52
52
|
|
|
53
53
|
if (aiReady && !noAI) {
|
|
54
|
-
|
|
55
|
-
}
|
|
54
|
+
const engineNames = readyEngines.map(e => e.name || e.provider).join(' + ');
|
|
55
|
+
console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${engineNames})`);
|
|
56
|
+
if (!noFix) {
|
|
57
|
+
console.log(` ${c.gray('自动修复已启用(--fix)')}`);
|
|
58
|
+
}
|
|
59
|
+
} else if (readyEngines.length === 1 && !noAI) {
|
|
60
|
+
console.log(` ${c.green('2/3')} AI 引擎已就绪 ✓ (${configSource}:${readyEngines[0].name},仅单引擎)`);
|
|
61
|
+
console.log(` ${c.gray('提示:配置第二个引擎可启用对比模式')}`);
|
|
62
|
+
} else if (!noAI) {
|
|
56
63
|
console.log(` ${c.yellow('2/3')} 未检测到 AI 配置`);
|
|
57
|
-
console.log(` ${c.gray('将仅使用规则引擎检查(
|
|
58
|
-
console.log(` ${c.gray('配置 AI
|
|
64
|
+
console.log(` ${c.gray('将仅使用规则引擎检查(30条规则)')}`);
|
|
65
|
+
console.log(` ${c.gray('配置 AI:devassist ai --setup --provider qwen --key <key>')}`);
|
|
59
66
|
noAI = true; // fallback to rules-only
|
|
60
67
|
} else {
|
|
61
68
|
console.log(` ${c.green('2/3')} 规则引擎模式 ✓`);
|
|
@@ -77,6 +84,9 @@ module.exports = async function start(projectRoot, args, ui) {
|
|
|
77
84
|
if (!noAI) {
|
|
78
85
|
watchArgs.push('--ai', '--compare');
|
|
79
86
|
}
|
|
87
|
+
if (!noFix) {
|
|
88
|
+
watchArgs.push('--fix');
|
|
89
|
+
}
|
|
80
90
|
await watchCmd(projectRoot, watchArgs, ui);
|
|
81
91
|
}
|
|
82
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,144 @@ 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
|
+
// Save findings for ai --analyze
|
|
175
|
+
saveWatchFindings(projectRoot, allFindings);
|
|
134
176
|
|
|
135
|
-
|
|
136
|
-
console.log(
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
177
|
+
// Print header
|
|
178
|
+
console.log(`\n ${c.gray(`[${timestamp}]`)} ${c.bold(relPath)}`);
|
|
139
179
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
for (const f of allFindings) {
|
|
146
|
-
const icon = f.severity === 'block' ? c.red('[阻断]') :
|
|
147
|
-
f.severity === 'warn' ? c.yellow('[警告]') :
|
|
148
|
-
c.gray('[提示]');
|
|
149
|
-
console.log(` ${icon} ${f.message}`);
|
|
150
|
-
if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
|
|
151
|
-
if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
|
|
152
|
-
}
|
|
180
|
+
if (allFindings.length === 0) {
|
|
181
|
+
console.log(` ${c.green('OK')} — 未发现问题`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
153
184
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
// --- AI Analysis (if enabled and there are warn/block findings) ---
|
|
186
|
+
let finalFindings = allFindings;
|
|
187
|
+
const suspectFindings = allFindings.filter(f => f.severity === 'block' || f.severity === 'warn');
|
|
188
|
+
|
|
189
|
+
if (useAI && readyEngines.length > 0 && suspectFindings.length > 0) {
|
|
190
|
+
console.log(` ${c.cyan('AI 分析中...')} (${suspectFindings.length} 个疑似问题)`);
|
|
191
|
+
|
|
192
|
+
// Enrich with code context
|
|
193
|
+
const fileContents = [{ path: relPath, content }];
|
|
194
|
+
finalFindings = enrichWithContext(allFindings, fileContents);
|
|
195
|
+
|
|
196
|
+
const aiContext = {
|
|
197
|
+
conventions: conventionStore.getRelevant({}),
|
|
198
|
+
projectInfo: { root: projectRoot },
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (useCompare && readyEngines.length >= 2) {
|
|
203
|
+
finalFindings = await ai.multiAnalyze(readyEngines, finalFindings, aiContext);
|
|
204
|
+
} else {
|
|
205
|
+
finalFindings = await ai.analyze(readyEngines[0], finalFindings, aiContext);
|
|
206
|
+
}
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.log(` ${c.yellow('AI 分析失败:')} ${err.message}`);
|
|
209
|
+
console.log(` ${c.gray('已回退到规则引擎结果')}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Print findings ---
|
|
214
|
+
const blocks = finalFindings.filter(f => f.severity === 'block');
|
|
215
|
+
const warns = finalFindings.filter(f => f.severity === 'warn');
|
|
216
|
+
const infos = finalFindings.filter(f => f.severity === 'info');
|
|
217
|
+
|
|
218
|
+
for (const f of finalFindings) {
|
|
219
|
+
const icon = f.severity === 'block' ? c.red('[阻断]') :
|
|
220
|
+
f.severity === 'warn' ? c.yellow('[警告]') :
|
|
221
|
+
c.gray('[提示]');
|
|
222
|
+
console.log(` ${icon} ${f.message}`);
|
|
223
|
+
if (f.line) console.log(` ${c.gray(`第 ${f.line} 行`)}`);
|
|
224
|
+
if (f.suggestion) console.log(` ${c.gray(f.suggestion)}`);
|
|
225
|
+
|
|
226
|
+
// Print AI analysis if available
|
|
227
|
+
if (f.aiAnalysis) {
|
|
228
|
+
const a = f.aiAnalysis;
|
|
229
|
+
if (a.falsePositive) {
|
|
230
|
+
console.log(` ${c.gray('AI 判定:误报 — ' + (a.falsePositiveReason || '不适用'))}`);
|
|
231
|
+
} else {
|
|
232
|
+
const rootCause = a.rootCause || a.root_cause || '';
|
|
233
|
+
const fixSuggestion = a.fixSuggestion || a.fix_suggestion || '';
|
|
234
|
+
const confidence = a.confidence || '';
|
|
235
|
+
if (rootCause) console.log(` ${c.cyan('AI 根因:')} ${rootCause}`);
|
|
236
|
+
if (fixSuggestion) console.log(` ${c.cyan('AI 建议:')} ${fixSuggestion}`);
|
|
237
|
+
if (confidence) console.log(` ${c.gray(`置信度:${confidence}`)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Print multi-engine comparison
|
|
242
|
+
if (f.aiAnalyses && f.aiAnalyses.length >= 2) {
|
|
243
|
+
for (const ea of f.aiAnalyses) {
|
|
244
|
+
if (ea.analysis && !ea.failed) {
|
|
245
|
+
const engineName = ea.engineName || ea.provider;
|
|
246
|
+
const a = ea.analysis;
|
|
247
|
+
if (a.falsePositive) {
|
|
248
|
+
console.log(` ${c.gray(`[${engineName}] 误报 — ${a.falsePositiveReason || ''}`)}`);
|
|
249
|
+
} else {
|
|
250
|
+
const rootCause = a.rootCause || a.root_cause || '';
|
|
251
|
+
if (rootCause) console.log(` ${c.cyan(`[${engineName}]`)} ${rootCause}`);
|
|
252
|
+
}
|
|
253
|
+
} else if (ea.failed) {
|
|
254
|
+
console.log(` ${c.gray(`[${ea.engineName || ea.provider}] 分析失败`)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Agreement indicator
|
|
258
|
+
if (f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && !ea.analysis.falsePositive).length === 0
|
|
259
|
+
&& f.aiAnalyses.filter(ea => ea.analysis && !ea.failed && ea.analysis.falsePositive).length >= 2) {
|
|
260
|
+
console.log(` ${c.green('引擎一致:均判定为误报')}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Summary ---
|
|
266
|
+
const parts = [];
|
|
267
|
+
if (blocks.length > 0) parts.push(c.red(`${blocks.length} 阻断`));
|
|
268
|
+
if (warns.length > 0) parts.push(c.yellow(`${warns.length} 警告`));
|
|
269
|
+
if (infos.length > 0) parts.push(c.gray(`${infos.length} 提示`));
|
|
270
|
+
console.log(` ${c.gray('—')} ${parts.join(c.gray(' · '))}`);
|
|
271
|
+
|
|
272
|
+
// --- Auto Fix (if enabled) ---
|
|
273
|
+
if (autoFix && suspectFindings.length > 0) {
|
|
274
|
+
const fixedCount = await autoFixFile(filePath, content, projectRoot, c);
|
|
275
|
+
if (fixedCount > 0) {
|
|
276
|
+
console.log(` ${c.green('自动修复')} ${fixedCount} 处问题`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
} finally {
|
|
281
|
+
analyzingFiles.delete(filePath);
|
|
282
|
+
}
|
|
160
283
|
}
|
|
161
284
|
|
|
162
285
|
// Start watching
|
|
@@ -199,6 +322,78 @@ module.exports = async function watch(projectRoot, args, ui) {
|
|
|
199
322
|
});
|
|
200
323
|
};
|
|
201
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Auto-fix a single file in-place (safe fixes only).
|
|
327
|
+
* Returns the number of fixes applied.
|
|
328
|
+
*/
|
|
329
|
+
async function autoFixFile(filePath, content, projectRoot, c) {
|
|
330
|
+
const lines = content.split('\n');
|
|
331
|
+
let fixCount = 0;
|
|
332
|
+
const changes = [];
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < lines.length; i++) {
|
|
335
|
+
const line = lines[i];
|
|
336
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) continue;
|
|
337
|
+
|
|
338
|
+
// Fix 1: fetch() without .catch()
|
|
339
|
+
if (/\bfetch\s*\(/.test(line)) {
|
|
340
|
+
const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
341
|
+
if (/\.catch\s*\(/.test(context)) continue;
|
|
342
|
+
const hasAwait = /\bawait\b/.test(line);
|
|
343
|
+
const hasTryCatch = /try\s*\{/.test(lines.slice(Math.max(0, i - 5), i + 1).join('\n'));
|
|
344
|
+
if (hasAwait && hasTryCatch) continue;
|
|
345
|
+
|
|
346
|
+
const trimmed = line.trimEnd();
|
|
347
|
+
if (trimmed.endsWith(')') && !trimmed.endsWith('.catch()')) {
|
|
348
|
+
const fixedLine = trimmed.replace(/\)\s*$/, ").catch(err => console.error('fetch error:', err))");
|
|
349
|
+
changes.push({ line: i, newLine: fixedLine, type: 'fetch-catch' });
|
|
350
|
+
fixCount++;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Fix 2: Add await to async calls (fetch, axios, etc.)
|
|
356
|
+
if (!/\bawait\b/.test(line)) {
|
|
357
|
+
const asyncMatch = line.match(/(?<![\w.])(fetch|axios\.(get|post|put|delete|patch)|\.save\(|\.findOne\(|\.findById\(|\.create\()\s*/);
|
|
358
|
+
if (asyncMatch) {
|
|
359
|
+
const trimmed = line.trim();
|
|
360
|
+
if (/^\s*(const|let|var|if|while|return|throw)/.test(trimmed)) continue;
|
|
361
|
+
if (/^\s*(async\s+)?function\s+/.test(line)) continue;
|
|
362
|
+
if (/\.then\s*\(|\.catch\s*\(|\.finally\s*\(/.test(line)) continue;
|
|
363
|
+
|
|
364
|
+
const fixedLine = line.replace(
|
|
365
|
+
new RegExp('(?<![\\w.])(' + asyncMatch[1] + '\\w+)\\s*\\('),
|
|
366
|
+
'await $1('
|
|
367
|
+
);
|
|
368
|
+
if (fixedLine !== line) {
|
|
369
|
+
changes.push({ line: i, newLine: fixedLine, type: 'add-await' });
|
|
370
|
+
fixCount++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (fixCount === 0) return 0;
|
|
378
|
+
|
|
379
|
+
// Apply changes
|
|
380
|
+
for (const ch of changes) {
|
|
381
|
+
lines[ch.line] = ch.newLine;
|
|
382
|
+
}
|
|
383
|
+
const newContent = lines.join('\n');
|
|
384
|
+
|
|
385
|
+
// Backup original
|
|
386
|
+
const backupDir = path.join(projectRoot, '.devassist', 'backups', `watch-fix-${Date.now()}`);
|
|
387
|
+
if (!fs.existsSync(backupDir)) fs.mkdirSync(backupDir, { recursive: true });
|
|
388
|
+
const backupPath = path.join(backupDir, path.basename(filePath));
|
|
389
|
+
fs.writeFileSync(backupPath, content, 'utf-8');
|
|
390
|
+
|
|
391
|
+
// Write fixed content
|
|
392
|
+
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
393
|
+
|
|
394
|
+
return fixCount;
|
|
395
|
+
}
|
|
396
|
+
|
|
202
397
|
function saveWatchFindings(projectRoot, findings) {
|
|
203
398
|
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
204
399
|
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|