devassist-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +467 -0
- package/bin/devassist.js +220 -0
- package/package.json +44 -0
- package/src/ai/adapter.js +464 -0
- package/src/ai/providers/claude.js +80 -0
- package/src/ai/providers/hunyuan.js +87 -0
- package/src/ai/providers/openai.js +74 -0
- package/src/ai/providers/qwen.js +81 -0
- package/src/cli/commands/ai.js +944 -0
- package/src/cli/commands/ask.js +79 -0
- package/src/cli/commands/backup.js +30 -0
- package/src/cli/commands/check.js +327 -0
- package/src/cli/commands/clean.js +130 -0
- package/src/cli/commands/comparison-report.js +326 -0
- package/src/cli/commands/convention.js +91 -0
- package/src/cli/commands/debt.js +49 -0
- package/src/cli/commands/deploy.js +88 -0
- package/src/cli/commands/diff.js +193 -0
- package/src/cli/commands/doctor.js +186 -0
- package/src/cli/commands/fix.js +195 -0
- package/src/cli/commands/init.js +431 -0
- package/src/cli/commands/inject.js +254 -0
- package/src/cli/commands/report.js +310 -0
- package/src/cli/commands/restore.js +78 -0
- package/src/cli/commands/schema.js +93 -0
- package/src/cli/commands/watch.js +212 -0
- package/src/cli/shared/code-context.js +51 -0
- package/src/cli/shared/config-loader.js +89 -0
- package/src/cli/shared/file-collector.js +116 -0
- package/src/cli/shared/inline-ignore.js +142 -0
- package/src/cli/shared/watch-list.js +281 -0
- package/src/core/event-bus.js +83 -0
- package/src/core/fsm.js +103 -0
- package/src/core/logger.js +54 -0
- package/src/core/rule-engine.js +117 -0
- package/src/index.js +64 -0
- package/src/modules/dev-time/arch-risk-assessor.js +250 -0
- package/src/modules/dev-time/code-quality-guard.js +1340 -0
- package/src/modules/dev-time/convention-store.js +201 -0
- package/src/modules/dev-time/impact-analyzer.js +292 -0
- package/src/modules/dev-time/pre-deploy-guard.js +284 -0
- package/src/modules/dev-time/schema-registry.js +284 -0
- package/src/modules/dev-time/tech-debt-tracker.js +225 -0
- package/src/modules/dev-time/version-manager.js +280 -0
- package/src/modules/runtime/channel-agent.js +404 -0
- package/src/modules/runtime/channel-middleware.js +316 -0
- package/src/modules/runtime/index.js +64 -0
- package/src/modules/runtime/infrastructure-guard.js +582 -0
- package/src/modules/runtime/notification-center.js +443 -0
- package/src/modules/runtime/schema-gatekeeper.js +664 -0
- package/src/modules/runtime/tool-registry.js +329 -0
- package/templates/ci/github-actions.yml +60 -0
- package/templates/ci/gitlab-ci.yml +30 -0
- package/templates/ci/pre-commit-hook.sh +18 -0
- package/templates/git-hooks/pre-commit +61 -0
- package/tests/run-all.js +434 -0
- package/tests/test-layer2.js +461 -0
- package/tests/test-new-rules.js +157 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* devassist ai - AI 配置与多引擎管理
|
|
3
|
+
*
|
|
4
|
+
* devassist ai --status 查看当前 AI 配置状态
|
|
5
|
+
* devassist ai --setup --provider qwen --key <key> 配置主引擎
|
|
6
|
+
* devassist ai --add-engine --provider hunyuan --key <key> 添加引擎
|
|
7
|
+
* devassist ai --setup-secondary --provider hunyuan --key <key> (别名)添加引擎
|
|
8
|
+
* devassist ai --remove-engine <provider|index> 移除引擎
|
|
9
|
+
* devassist ai --list-engines 列出所有引擎
|
|
10
|
+
* devassist ai --test 测试主引擎连通性
|
|
11
|
+
* devassist ai --test-all 测试所有引擎连通性
|
|
12
|
+
* devassist ai --analyze 单引擎 AI 分析
|
|
13
|
+
* devassist ai --compare 多引擎对比分析(所有引擎并行)
|
|
14
|
+
*
|
|
15
|
+
* 支持的 provider: qwen(通义千问)/ openai / claude / hunyuan(腾讯混元)
|
|
16
|
+
*
|
|
17
|
+
* ai-config.json 格式(向后兼容):
|
|
18
|
+
* {
|
|
19
|
+
* "provider": "qwen", // 主引擎(引擎 0)
|
|
20
|
+
* "apiKey": "...",
|
|
21
|
+
* "model": "qwen-plus",
|
|
22
|
+
* "enabled": true,
|
|
23
|
+
* "engines": [ // 额外引擎(引擎 1, 2, ...)
|
|
24
|
+
* { "provider": "hunyuan", "apiKey": "...", "model": "hy3-preview", "enabled": true },
|
|
25
|
+
* { "provider": "openai", "apiKey": "...", "model": "gpt-4o-mini", "enabled": true }
|
|
26
|
+
* ],
|
|
27
|
+
* "secondary": { ... } // 旧格式副引擎,自动合并到 engines[]
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
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
|
+
ai.registerProvider('qwen', qwen);
|
|
40
|
+
ai.registerProvider('openai', openai);
|
|
41
|
+
ai.registerProvider('claude', claude);
|
|
42
|
+
ai.registerProvider('hunyuan', hunyuan);
|
|
43
|
+
|
|
44
|
+
const PROVIDER_INFO = {
|
|
45
|
+
qwen: { name: '通义千问', envVar: 'DASHSCOPE_API_KEY', defaultModel: 'qwen-plus', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
|
|
46
|
+
openai: { name: 'OpenAI', envVar: 'OPENAI_API_KEY', defaultModel: 'gpt-4o-mini', baseUrl: 'https://api.openai.com/v1' },
|
|
47
|
+
claude: { name: 'Claude', envVar: 'ANTHROPIC_API_KEY', defaultModel: 'claude-sonnet-4-20250514', baseUrl: 'https://api.anthropic.com' },
|
|
48
|
+
hunyuan: { name: '腾讯混元', envVar: 'HUNYUAN_API_KEY', defaultModel: 'hy3-preview', baseUrl: 'https://tokenhub.tencentmaas.com/v1' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const ENV_MAP = { qwen: 'DASHSCOPE_API_KEY', openai: 'OPENAI_API_KEY', claude: 'ANTHROPIC_API_KEY', hunyuan: 'HUNYUAN_API_KEY' };
|
|
52
|
+
|
|
53
|
+
module.exports = async function aiCmd(projectRoot, args, ui) {
|
|
54
|
+
const { printHeader, c } = ui;
|
|
55
|
+
|
|
56
|
+
const showStatus = args.includes('--status');
|
|
57
|
+
const doSetup = args.includes('--setup');
|
|
58
|
+
const doSetupSecondary = args.includes('--setup-secondary');
|
|
59
|
+
const doAddEngine = args.includes('--add-engine');
|
|
60
|
+
const doRemoveEngine = args.includes('--remove-engine');
|
|
61
|
+
const doListEngines = args.includes('--list-engines');
|
|
62
|
+
const doTest = args.includes('--test');
|
|
63
|
+
const doTestAll = args.includes('--test-all');
|
|
64
|
+
const doAnalyze = args.includes('--analyze');
|
|
65
|
+
const doCompare = args.includes('--compare');
|
|
66
|
+
const doWatchList = args.includes('--watch-list');
|
|
67
|
+
|
|
68
|
+
// Default: show help
|
|
69
|
+
if (!showStatus && !doSetup && !doSetupSecondary && !doAddEngine && !doRemoveEngine &&
|
|
70
|
+
!doListEngines && !doTest && !doTestAll && !doAnalyze && !doCompare && !doWatchList) {
|
|
71
|
+
printHelp(c);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (showStatus) { showAIStatus(projectRoot, c); return; }
|
|
76
|
+
if (doSetup) { await setupAI(projectRoot, args, c, 'primary'); return; }
|
|
77
|
+
if (doSetupSecondary){ await setupAI(projectRoot, args, c, 'add'); return; }
|
|
78
|
+
if (doAddEngine) { await setupAI(projectRoot, args, c, 'add'); return; }
|
|
79
|
+
if (doRemoveEngine) { removeEngine(projectRoot, args, c); return; }
|
|
80
|
+
if (doListEngines) { listEngines(projectRoot, c); return; }
|
|
81
|
+
if (doTest) { await testAI(projectRoot, c, false); return; }
|
|
82
|
+
if (doTestAll) { await testAI(projectRoot, c, true); return; }
|
|
83
|
+
if (doAnalyze) { await analyzeFindings(projectRoot, c); return; }
|
|
84
|
+
if (doCompare) { await compareFindings(projectRoot, c); return; }
|
|
85
|
+
if (doWatchList) { manageWatchList(projectRoot, args, c); return; }
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ─── Help ────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function printHelp(c) {
|
|
91
|
+
console.log(`\n ${c.bold('devassist ai')} — AI 配置与多引擎管理\n`);
|
|
92
|
+
console.log(` ${c.gray('用法:')}\n`);
|
|
93
|
+
console.log(` devassist ai --status 查看当前配置`);
|
|
94
|
+
console.log(` devassist ai --setup --provider qwen --key <密钥> 配置主引擎`);
|
|
95
|
+
console.log(` devassist ai --add-engine --provider hunyuan --key <密钥> 添加引擎`);
|
|
96
|
+
console.log(` devassist ai --setup-secondary --provider <p> --key <k> (别名)添加引擎`);
|
|
97
|
+
console.log(` devassist ai --remove-engine <provider|序号> 移除引擎`);
|
|
98
|
+
console.log(` devassist ai --list-engines 列出所有引擎`);
|
|
99
|
+
console.log(` devassist ai --test 测试主引擎连通性`);
|
|
100
|
+
console.log(` devassist ai --test-all 测试所有引擎`);
|
|
101
|
+
console.log(` devassist ai --analyze 单引擎 AI 分析`);
|
|
102
|
+
console.log(` devassist ai --compare 多引擎对比分析`);
|
|
103
|
+
console.log(` devassist ai --watch-list 查看待观察列表`);
|
|
104
|
+
console.log(` devassist ai --watch-list --resolve <序号> 标记待观察项为已修复`);
|
|
105
|
+
console.log(` devassist ai --watch-list --dismiss <序号> 标记待观察项为无问题\n`);
|
|
106
|
+
console.log(` ${c.gray('多引擎对比模式:')}`);
|
|
107
|
+
console.log(` 配置 2~N 个引擎后运行 --compare,所有引擎并行分析同一批发现`);
|
|
108
|
+
console.log(` 生成对比报告,按一致性自动分组\n`);
|
|
109
|
+
console.log(` ${c.gray('支持的 provider:')}`);
|
|
110
|
+
console.log(` qwen 通义千问(阿里云 DashScope)`);
|
|
111
|
+
console.log(` hunyuan 腾讯混元`);
|
|
112
|
+
console.log(` openai OpenAI GPT`);
|
|
113
|
+
console.log(` claude Anthropic Claude\n`);
|
|
114
|
+
console.log(` ${c.gray('也可通过环境变量配置:')}`);
|
|
115
|
+
console.log(` DASHSCOPE_API_KEY 通义千问`);
|
|
116
|
+
console.log(` HUNYUAN_API_KEY 腾讯混元`);
|
|
117
|
+
console.log(` OPENAI_API_KEY OpenAI`);
|
|
118
|
+
console.log(` ANTHROPIC_API_KEY Claude\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Config Loading (N-engine aware) ─────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load AI config and resolve all engine configs into a flat array.
|
|
125
|
+
* Returns: { ...rawConfig, engines: [{ provider, apiKey, model, baseUrl, enabled, name }] }
|
|
126
|
+
*
|
|
127
|
+
* Resolution order:
|
|
128
|
+
* 1. Primary engine from top-level fields (provider/apiKey/model)
|
|
129
|
+
* 2. Legacy `secondary` field (if present)
|
|
130
|
+
* 3. `engines` array (if present)
|
|
131
|
+
* 4. Env vars for API keys
|
|
132
|
+
* 5. Deduplicate by provider+model
|
|
133
|
+
*/
|
|
134
|
+
function loadAIConfig(projectRoot) {
|
|
135
|
+
const configPath = path.join(projectRoot, '.devassist', 'ai-config.json');
|
|
136
|
+
if (!fs.existsSync(configPath)) return null;
|
|
137
|
+
try {
|
|
138
|
+
const rawConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
139
|
+
|
|
140
|
+
// Build engines array
|
|
141
|
+
const engines = [];
|
|
142
|
+
|
|
143
|
+
// Engine 0: primary (top-level fields)
|
|
144
|
+
if (rawConfig.provider) {
|
|
145
|
+
const info = PROVIDER_INFO[rawConfig.provider];
|
|
146
|
+
let apiKey = rawConfig.apiKey;
|
|
147
|
+
if (!apiKey && info && process.env[info.envVar]) {
|
|
148
|
+
apiKey = process.env[info.envVar];
|
|
149
|
+
}
|
|
150
|
+
engines.push({
|
|
151
|
+
provider: rawConfig.provider,
|
|
152
|
+
apiKey,
|
|
153
|
+
model: rawConfig.model || info?.defaultModel,
|
|
154
|
+
baseUrl: rawConfig.baseUrl || info?.baseUrl,
|
|
155
|
+
enabled: rawConfig.enabled !== false,
|
|
156
|
+
name: info?.name || rawConfig.provider,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Legacy secondary engine
|
|
161
|
+
if (rawConfig.secondary && rawConfig.secondary.provider) {
|
|
162
|
+
const sInfo = PROVIDER_INFO[rawConfig.secondary.provider];
|
|
163
|
+
let sApiKey = rawConfig.secondary.apiKey;
|
|
164
|
+
if (!sApiKey && sInfo && process.env[sInfo.envVar]) {
|
|
165
|
+
sApiKey = process.env[sInfo.envVar];
|
|
166
|
+
}
|
|
167
|
+
engines.push({
|
|
168
|
+
provider: rawConfig.secondary.provider,
|
|
169
|
+
apiKey: sApiKey,
|
|
170
|
+
model: rawConfig.secondary.model || sInfo?.defaultModel,
|
|
171
|
+
baseUrl: rawConfig.secondary.baseUrl || sInfo?.baseUrl,
|
|
172
|
+
enabled: rawConfig.secondary.enabled !== false,
|
|
173
|
+
name: sInfo?.name || rawConfig.secondary.provider,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// New engines array
|
|
178
|
+
if (Array.isArray(rawConfig.engines)) {
|
|
179
|
+
for (const eng of rawConfig.engines) {
|
|
180
|
+
if (!eng || !eng.provider) continue;
|
|
181
|
+
const eInfo = PROVIDER_INFO[eng.provider];
|
|
182
|
+
let eApiKey = eng.apiKey;
|
|
183
|
+
if (!eApiKey && eInfo && process.env[eInfo.envVar]) {
|
|
184
|
+
eApiKey = process.env[eInfo.envVar];
|
|
185
|
+
}
|
|
186
|
+
engines.push({
|
|
187
|
+
provider: eng.provider,
|
|
188
|
+
apiKey: eApiKey,
|
|
189
|
+
model: eng.model || eInfo?.defaultModel,
|
|
190
|
+
baseUrl: eng.baseUrl || eInfo?.baseUrl,
|
|
191
|
+
enabled: eng.enabled !== false,
|
|
192
|
+
name: eng.name || eInfo?.name || eng.provider,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Deduplicate by provider+model
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
const uniqueEngines = engines.filter(e => {
|
|
200
|
+
const key = `${e.provider}:${e.model || ''}`;
|
|
201
|
+
if (seen.has(key)) return false;
|
|
202
|
+
seen.add(key);
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...rawConfig,
|
|
208
|
+
engines: uniqueEngines,
|
|
209
|
+
};
|
|
210
|
+
} catch (_) { return null; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Status ──────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
function showAIStatus(projectRoot, c) {
|
|
216
|
+
const config = loadAIConfig(projectRoot);
|
|
217
|
+
|
|
218
|
+
console.log(`\n ${c.bold('AI 配置状态')}\n`);
|
|
219
|
+
|
|
220
|
+
if (!config || config.engines.length === 0) {
|
|
221
|
+
console.log(` ${c.yellow('尚未配置。')} 运行 ${c.bold('devassist ai --setup --provider <name> --key <key>')} 来配置。\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const enabledEngines = config.engines.filter(e => e.enabled && e.apiKey);
|
|
226
|
+
|
|
227
|
+
config.engines.forEach((eng, i) => {
|
|
228
|
+
const info = PROVIDER_INFO[eng.provider];
|
|
229
|
+
const label = i === 0 ? '主引擎' : `引擎 ${i}`;
|
|
230
|
+
const status = eng.enabled ? (eng.apiKey ? c.green('就绪') : c.yellow('缺少 Key')) : c.gray('已禁用');
|
|
231
|
+
const envKey = info ? process.env[info.envVar] : null;
|
|
232
|
+
|
|
233
|
+
console.log(` ${c.bold(label)}`);
|
|
234
|
+
console.log(` Provider: ${c.bold(eng.provider)}${info ? ' (' + info.name + ')' : ''}`);
|
|
235
|
+
console.log(` 状态: ${status}`);
|
|
236
|
+
console.log(` API Key: ${eng.apiKey ? c.green('已设置') : c.yellow('未设置')}`);
|
|
237
|
+
if (envKey && !eng.apiKey) console.log(` 环境变量: ${c.green('已设置(' + info.envVar + ')')}`);
|
|
238
|
+
console.log(` 模型: ${eng.model || info?.defaultModel || c.gray('默认')}`);
|
|
239
|
+
if (i < config.engines.length - 1) console.log('');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (enabledEngines.length >= 2) {
|
|
243
|
+
console.log(`\n ${c.green(`${enabledEngines.length} 个引擎已就绪!`)} 可运行 ${c.bold('devassist ai --compare')} 进行多引擎对比分析。\n`);
|
|
244
|
+
} else if (enabledEngines.length === 1) {
|
|
245
|
+
console.log(`\n ${c.gray('仅 1 个引擎就绪。添加更多引擎启用对比模式:')}`);
|
|
246
|
+
console.log(` ${c.bold('devassist ai --add-engine --provider hunyuan --key <key>')}\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(` 配置文件: ${path.join(projectRoot, '.devassist', 'ai-config.json')}\n`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Setup / Add / Remove Engine ─────────────────────
|
|
253
|
+
|
|
254
|
+
async function setupAI(projectRoot, args, c, mode) {
|
|
255
|
+
// mode: 'primary' = replace main engine, 'add' = append to engines[]
|
|
256
|
+
const provider = getArg(args, '--provider');
|
|
257
|
+
const key = getArg(args, '--key');
|
|
258
|
+
const model = getArg(args, '--model');
|
|
259
|
+
const baseUrl = getArg(args, '--base-url');
|
|
260
|
+
|
|
261
|
+
if (!provider) {
|
|
262
|
+
console.log(`\n ${c.red('错误:')} 请指定 --provider\n`);
|
|
263
|
+
console.log(` 可选值:qwen / hunyuan / openai / claude\n`);
|
|
264
|
+
console.log(` 示例:devassist ai --${mode === 'primary' ? 'setup' : 'add-engine'} --provider qwen --key sk-xxxx\n`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const info = PROVIDER_INFO[provider];
|
|
269
|
+
if (!info) {
|
|
270
|
+
console.log(`\n ${c.red('错误:')} 不支持的 provider "${provider}"\n`);
|
|
271
|
+
console.log(` 可选值:qwen / hunyuan / openai / claude\n`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let apiKey = key || process.env[info.envVar] || '';
|
|
276
|
+
if (!apiKey) {
|
|
277
|
+
console.log(`\n ${c.red('错误:')} 未提供 API Key\n`);
|
|
278
|
+
console.log(` 请通过 --key 参数提供,或设置环境变量 ${info.envVar}\n`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const newEngine = {
|
|
283
|
+
provider,
|
|
284
|
+
apiKey,
|
|
285
|
+
model: model || info.defaultModel,
|
|
286
|
+
baseUrl: baseUrl || info.baseUrl,
|
|
287
|
+
enabled: true,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const configDir = path.join(projectRoot, '.devassist');
|
|
291
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
292
|
+
const configPath = path.join(configDir, 'ai-config.json');
|
|
293
|
+
|
|
294
|
+
// Load existing config
|
|
295
|
+
let existingConfig = {};
|
|
296
|
+
if (fs.existsSync(configPath)) {
|
|
297
|
+
try { existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (_) {}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (mode === 'primary') {
|
|
301
|
+
// Replace primary engine fields, preserve engines[]
|
|
302
|
+
existingConfig.provider = newEngine.provider;
|
|
303
|
+
existingConfig.apiKey = newEngine.apiKey;
|
|
304
|
+
existingConfig.model = newEngine.model;
|
|
305
|
+
existingConfig.baseUrl = newEngine.baseUrl;
|
|
306
|
+
existingConfig.enabled = newEngine.enabled;
|
|
307
|
+
// Preserve engines[] and legacy secondary
|
|
308
|
+
} else {
|
|
309
|
+
// Add to engines[] array
|
|
310
|
+
if (!Array.isArray(existingConfig.engines)) existingConfig.engines = [];
|
|
311
|
+
|
|
312
|
+
// Check for duplicate provider+model
|
|
313
|
+
const dup = existingConfig.engines.find(e => e.provider === provider && (e.model || PROVIDER_INFO[e.provider]?.defaultModel) === newEngine.model);
|
|
314
|
+
if (dup) {
|
|
315
|
+
// Update existing
|
|
316
|
+
Object.assign(dup, newEngine);
|
|
317
|
+
console.log(`\n ${c.yellow('已更新现有引擎配置。')}\n`);
|
|
318
|
+
} else {
|
|
319
|
+
existingConfig.engines.push(newEngine);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fs.writeFileSync(configPath, JSON.stringify(existingConfig, null, 2), 'utf-8');
|
|
324
|
+
|
|
325
|
+
const label = mode === 'primary' ? '主引擎' : '引擎';
|
|
326
|
+
console.log(`\n ${c.green(`${label}配置已保存!`)}\n`);
|
|
327
|
+
console.log(` Provider: ${provider} (${info.name})`);
|
|
328
|
+
console.log(` 模型: ${newEngine.model}`);
|
|
329
|
+
console.log(` API Key: ${c.gray('****' + apiKey.slice(-4))}`);
|
|
330
|
+
console.log(` 状态: ${c.green('已启用')}\n`);
|
|
331
|
+
|
|
332
|
+
// Show current engine count
|
|
333
|
+
const fullConfig = loadAIConfig(projectRoot);
|
|
334
|
+
const count = fullConfig ? fullConfig.engines.length : 1;
|
|
335
|
+
|
|
336
|
+
if (count >= 2) {
|
|
337
|
+
console.log(` ${c.green(`当前共 ${count} 个引擎已就绪。`)}`);
|
|
338
|
+
console.log(` ${c.bold('devassist ai --compare')} 多引擎对比分析`);
|
|
339
|
+
console.log(` ${c.bold('devassist ai --list-engines')} 查看所有引擎\n`);
|
|
340
|
+
} else {
|
|
341
|
+
console.log(` ${c.gray('提示:添加更多引擎可启用对比模式:')}`);
|
|
342
|
+
console.log(` ${c.bold('devassist ai --add-engine --provider hunyuan --key <key>')}\n`);
|
|
343
|
+
console.log(` ${c.gray('现在可以运行:')}`);
|
|
344
|
+
console.log(` ${c.bold('devassist ai --test')} 测试 AI 连通性`);
|
|
345
|
+
console.log(` ${c.bold('devassist check --ai')} 对代码检查结果做 AI 深度分析\n`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function removeEngine(projectRoot, args, c) {
|
|
350
|
+
// Find the target: either a provider name or a numeric index
|
|
351
|
+
const target = args[args.indexOf('--remove-engine') + 1];
|
|
352
|
+
|
|
353
|
+
if (!target) {
|
|
354
|
+
console.log(`\n ${c.red('错误:')} 请指定要移除的引擎(provider 名称或序号)\n`);
|
|
355
|
+
console.log(` 示例:devassist ai --remove-engine hunyuan`);
|
|
356
|
+
console.log(` devassist ai --remove-engine 2\n`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const configPath = path.join(projectRoot, '.devassist', 'ai-config.json');
|
|
361
|
+
if (!fs.existsSync(configPath)) {
|
|
362
|
+
console.log(`\n ${c.yellow('尚未配置 AI。')}\n`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let config;
|
|
367
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch (_) {
|
|
368
|
+
console.log(`\n ${c.red('配置文件无法读取。')}\n`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const fullConfig = loadAIConfig(projectRoot);
|
|
373
|
+
if (!fullConfig || fullConfig.engines.length === 0) {
|
|
374
|
+
console.log(`\n ${c.yellow('无引擎可移除。')}\n`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Try numeric index first
|
|
379
|
+
const idx = parseInt(target, 10);
|
|
380
|
+
if (!isNaN(idx) && idx >= 0 && idx < fullConfig.engines.length) {
|
|
381
|
+
const removed = fullConfig.engines[idx];
|
|
382
|
+
removeFromConfig(config, removed, idx);
|
|
383
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
384
|
+
console.log(`\n ${c.green('已移除引擎 ' + idx + ':')} ${removed.provider} (${removed.name})\n`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Try provider name
|
|
389
|
+
const matchIdx = fullConfig.engines.findIndex(e => e.provider === target);
|
|
390
|
+
if (matchIdx >= 0) {
|
|
391
|
+
const removed = fullConfig.engines[matchIdx];
|
|
392
|
+
removeFromConfig(config, removed, matchIdx);
|
|
393
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
394
|
+
console.log(`\n ${c.green('已移除引擎:')} ${removed.provider} (${removed.name})\n`);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
console.log(`\n ${c.red('未找到匹配的引擎:')} ${target}\n`);
|
|
399
|
+
listEngines(projectRoot, c);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function removeFromConfig(config, removed, index) {
|
|
403
|
+
// If it's the primary engine (index 0), clear top-level fields
|
|
404
|
+
if (index === 0) {
|
|
405
|
+
delete config.provider;
|
|
406
|
+
delete config.apiKey;
|
|
407
|
+
delete config.model;
|
|
408
|
+
delete config.baseUrl;
|
|
409
|
+
delete config.enabled;
|
|
410
|
+
// Promote first engine from engines[] to primary if available
|
|
411
|
+
if (Array.isArray(config.engines) && config.engines.length > 0) {
|
|
412
|
+
const promoted = config.engines.shift();
|
|
413
|
+
config.provider = promoted.provider;
|
|
414
|
+
config.apiKey = promoted.apiKey;
|
|
415
|
+
config.model = promoted.model;
|
|
416
|
+
config.baseUrl = promoted.baseUrl;
|
|
417
|
+
config.enabled = promoted.enabled;
|
|
418
|
+
}
|
|
419
|
+
} else if (index === 1 && config.secondary) {
|
|
420
|
+
// Legacy secondary
|
|
421
|
+
delete config.secondary;
|
|
422
|
+
} else {
|
|
423
|
+
// Remove from engines[]
|
|
424
|
+
if (Array.isArray(config.engines)) {
|
|
425
|
+
// Adjust index: subtract primary (1) and legacy secondary (1 if exists)
|
|
426
|
+
let adjIdx = index - 1;
|
|
427
|
+
if (config.secondary) adjIdx -= 1;
|
|
428
|
+
if (adjIdx >= 0 && adjIdx < config.engines.length) {
|
|
429
|
+
config.engines.splice(adjIdx, 1);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function listEngines(projectRoot, c) {
|
|
436
|
+
const config = loadAIConfig(projectRoot);
|
|
437
|
+
|
|
438
|
+
console.log(`\n ${c.bold('已配置的 AI 引擎')}\n`);
|
|
439
|
+
|
|
440
|
+
if (!config || config.engines.length === 0) {
|
|
441
|
+
console.log(` ${c.yellow('尚未配置任何引擎。')}\n`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
config.engines.forEach((eng, i) => {
|
|
446
|
+
const info = PROVIDER_INFO[eng.provider];
|
|
447
|
+
const label = i === 0 ? '主引擎' : `引擎 ${i}`;
|
|
448
|
+
const status = eng.enabled && eng.apiKey ? c.green('就绪') : c.yellow('未就绪');
|
|
449
|
+
|
|
450
|
+
console.log(` ${c.bold(`[${i}]`)} ${label}`);
|
|
451
|
+
console.log(` Provider: ${eng.provider} (${info?.name || eng.name || '?'})`);
|
|
452
|
+
console.log(` 模型: ${eng.model || info?.defaultModel || '默认'}`);
|
|
453
|
+
console.log(` 状态: ${status}`);
|
|
454
|
+
if (i < config.engines.length - 1) console.log('');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
console.log(`\n ${c.gray('共 ' + config.engines.length + ' 个引擎')}\n`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── Test ────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
async function testAI(projectRoot, c, testAll) {
|
|
463
|
+
const config = loadAIConfig(projectRoot);
|
|
464
|
+
|
|
465
|
+
if (!config || config.engines.length === 0) {
|
|
466
|
+
console.log(`\n ${c.yellow('AI 尚未配置。')}\n`);
|
|
467
|
+
console.log(` 运行 ${c.bold('devassist ai --setup --provider qwen --key <key>')} 来配置。\n`);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const enginesToTest = testAll ? config.engines : [config.engines[0]];
|
|
472
|
+
|
|
473
|
+
console.log(`\n ${c.bold(testAll ? '所有引擎连通性测试' : '主引擎连通性测试')}\n`);
|
|
474
|
+
|
|
475
|
+
let allPassed = true;
|
|
476
|
+
|
|
477
|
+
for (let i = 0; i < enginesToTest.length; i++) {
|
|
478
|
+
const eng = enginesToTest[i];
|
|
479
|
+
const info = PROVIDER_INFO[eng.provider];
|
|
480
|
+
const label = testAll ? `引擎 [${i}] ` : '';
|
|
481
|
+
|
|
482
|
+
console.log(` ${c.bold(label + eng.provider)} (${info?.name || '?'})`);
|
|
483
|
+
console.log(` 模型: ${eng.model || info?.defaultModel || '默认'}`);
|
|
484
|
+
console.log(` ${c.gray('正在发送测试请求...')}`);
|
|
485
|
+
|
|
486
|
+
const testConfig = { provider: eng.provider, apiKey: eng.apiKey, model: eng.model, baseUrl: eng.baseUrl };
|
|
487
|
+
const testQuestion = '请用一句话回答:1+1等于几?';
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const result = await ai.ask(testConfig, testQuestion, {});
|
|
491
|
+
if (result.error || result.unavailable) {
|
|
492
|
+
console.log(` ${c.red('失败:')} ${result.content}\n`);
|
|
493
|
+
allPassed = false;
|
|
494
|
+
} else {
|
|
495
|
+
console.log(` ${c.green('成功!')} ${c.gray('回复:')} ${result.content.trim()}`);
|
|
496
|
+
if (result.model) console.log(` ${c.gray('模型:')} ${result.model}\n`);
|
|
497
|
+
else console.log('');
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
console.log(` ${c.red('失败:')} ${err.message}\n`);
|
|
501
|
+
allPassed = false;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (testAll) {
|
|
506
|
+
if (allPassed) {
|
|
507
|
+
console.log(` ${c.green('所有引擎测试通过!')}\n`);
|
|
508
|
+
} else {
|
|
509
|
+
console.log(` ${c.yellow('部分引擎测试失败,请检查配置。')}\n`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (allPassed) {
|
|
514
|
+
console.log(` ${c.gray('AI 已就绪。现在可以运行:')}`);
|
|
515
|
+
console.log(` ${c.bold('devassist check --ai')} 对代码做 AI 深度分析`);
|
|
516
|
+
if (config.engines.length >= 2) {
|
|
517
|
+
console.log(` ${c.bold('devassist ai --compare')} 多引擎对比分析\n`);
|
|
518
|
+
} else {
|
|
519
|
+
console.log(` ${c.bold('devassist ai --analyze')} 分析最近一次检查结果\n`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ─── Single-Engine Analyze ───────────────────────────
|
|
525
|
+
|
|
526
|
+
async function analyzeFindings(projectRoot, c) {
|
|
527
|
+
const config = loadAIConfig(projectRoot);
|
|
528
|
+
|
|
529
|
+
if (!config || config.engines.length === 0) {
|
|
530
|
+
console.log(`\n ${c.yellow('AI 尚未配置。')}\n`);
|
|
531
|
+
console.log(` 运行 ${c.bold('devassist ai --setup --provider qwen --key <key>')} 来配置。\n`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const primaryEngine = config.engines[0];
|
|
536
|
+
|
|
537
|
+
// Load latest report findings
|
|
538
|
+
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
539
|
+
const latestJson = path.join(reportDir, 'latest.json');
|
|
540
|
+
|
|
541
|
+
let findings = [];
|
|
542
|
+
if (fs.existsSync(latestJson)) {
|
|
543
|
+
try {
|
|
544
|
+
const data = JSON.parse(fs.readFileSync(latestJson, 'utf-8'));
|
|
545
|
+
findings = data.findings || [];
|
|
546
|
+
} catch (_) {}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (findings.length === 0) {
|
|
550
|
+
console.log(`\n ${c.yellow('没有可分析的发现。')}\n`);
|
|
551
|
+
console.log(` 请先运行 ${c.bold('devassist check')} 生成检查结果。\n`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const suspectFindings = findings.filter(f => f.severity === 'block' || f.severity === 'warn');
|
|
556
|
+
|
|
557
|
+
if (suspectFindings.length === 0) {
|
|
558
|
+
console.log(`\n ${c.green('无需 AI 分析。')} 所有发现均为 info 级别。\n`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
console.log(`\n ${c.bold('AI 深度分析')}\n`);
|
|
563
|
+
console.log(` 发现数量: ${suspectFindings.length} 个(warn/block 级别)`);
|
|
564
|
+
console.log(` Provider: ${primaryEngine.provider}`);
|
|
565
|
+
console.log(` ${c.gray('正在分析...')}\n`);
|
|
566
|
+
|
|
567
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
568
|
+
const conventionStore = new ConventionStore(projectRoot);
|
|
569
|
+
const aiContext = {
|
|
570
|
+
conventions: conventionStore.getRelevant({}),
|
|
571
|
+
projectInfo: { root: projectRoot },
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const enrichedFindings = await ai.analyze(primaryEngine, suspectFindings, aiContext);
|
|
576
|
+
|
|
577
|
+
console.log(` ${c.green('分析完成!')}\n`);
|
|
578
|
+
|
|
579
|
+
for (const f of enrichedFindings) {
|
|
580
|
+
const sev = f.severity === 'block' ? c.red('[阻断]') : c.yellow('[警告]');
|
|
581
|
+
console.log(` ${sev} ${f.message}`);
|
|
582
|
+
console.log(` ${c.gray(f.file)}${f.line ? ':' + f.line : ''}`);
|
|
583
|
+
|
|
584
|
+
if (f.aiAnalysis && !f.aiAnalysisUnavailable) {
|
|
585
|
+
const a = f.aiAnalysis;
|
|
586
|
+
console.log(` ${c.cyan('── AI 根因分析 ──')}`);
|
|
587
|
+
if (a.rootCause) console.log(` 根因: ${a.rootCause}`);
|
|
588
|
+
if (a.isFalsePositive !== undefined) console.log(` 误报: ${a.isFalsePositive ? '是' : '否'}`);
|
|
589
|
+
if (a.fix) console.log(` 修复: ${a.fix}`);
|
|
590
|
+
if (a.codeExample) console.log(` ${c.gray('示例:')} ${a.codeExample}`);
|
|
591
|
+
if (a.confidence !== undefined) console.log(` 置信度: ${(a.confidence * 100).toFixed(0)}%`);
|
|
592
|
+
} else if (f.aiAnalysisUnavailable) {
|
|
593
|
+
console.log(` ${c.gray('(AI 分析不可用)')}`);
|
|
594
|
+
if (f.aiError) console.log(` ${c.gray('错误: ' + f.aiError)}`);
|
|
595
|
+
}
|
|
596
|
+
console.log();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Save AI analysis results
|
|
600
|
+
const analysisPath = path.join(projectRoot, '.devassist', 'reports', 'ai-analysis.json');
|
|
601
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
602
|
+
fs.writeFileSync(analysisPath, JSON.stringify({
|
|
603
|
+
analyzedAt: new Date().toISOString(),
|
|
604
|
+
provider: primaryEngine.provider,
|
|
605
|
+
model: primaryEngine.model,
|
|
606
|
+
findings: enrichedFindings,
|
|
607
|
+
}, null, 2), 'utf-8');
|
|
608
|
+
|
|
609
|
+
console.log(` ${c.gray('分析结果已保存到 .devassist/reports/ai-analysis.json')}\n`);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.log(` ${c.red('分析失败:')} ${err.message}\n`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ─── Multi-Engine Compare ────────────────────────────
|
|
616
|
+
|
|
617
|
+
async function compareFindings(projectRoot, c) {
|
|
618
|
+
const config = loadAIConfig(projectRoot);
|
|
619
|
+
|
|
620
|
+
if (!config || config.engines.length === 0) {
|
|
621
|
+
console.log(`\n ${c.yellow('尚未配置任何 AI 引擎。')}\n`);
|
|
622
|
+
console.log(` 运行 ${c.bold('devassist ai --setup --provider qwen --key <key>')} 来配置主引擎。\n`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const readyEngines = config.engines.filter(e => e.enabled && e.apiKey);
|
|
627
|
+
|
|
628
|
+
if (readyEngines.length < 2) {
|
|
629
|
+
console.log(`\n ${c.yellow('对比分析至少需要 2 个引擎就绪。')}\n`);
|
|
630
|
+
console.log(` 当前就绪引擎数:${readyEngines.length}`);
|
|
631
|
+
console.log(` 运行 ${c.bold('devassist ai --add-engine --provider hunyuan --key <key>')} 添加更多引擎。\n`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Load latest report findings
|
|
636
|
+
const reportDir = path.join(projectRoot, '.devassist', 'reports');
|
|
637
|
+
const latestJson = path.join(reportDir, 'latest.json');
|
|
638
|
+
|
|
639
|
+
let findings = [];
|
|
640
|
+
if (fs.existsSync(latestJson)) {
|
|
641
|
+
try {
|
|
642
|
+
const data = JSON.parse(fs.readFileSync(latestJson, 'utf-8'));
|
|
643
|
+
findings = data.findings || [];
|
|
644
|
+
} catch (_) {}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (findings.length === 0) {
|
|
648
|
+
console.log(`\n ${c.yellow('没有可分析的发现。')}\n`);
|
|
649
|
+
console.log(` 请先运行 ${c.bold('devassist check')} 生成检查结果。\n`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const suspectFindings = findings.filter(f => f.severity === 'block' || f.severity === 'warn');
|
|
654
|
+
|
|
655
|
+
if (suspectFindings.length === 0) {
|
|
656
|
+
console.log(`\n ${c.green('无需 AI 分析。')} 所有发现均为 info 级别。\n`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(`\n ${c.bold(`${readyEngines.length} 引擎对比分析`)}\n`);
|
|
661
|
+
console.log(` 发现数量: ${suspectFindings.length} 个(warn/block 级别)`);
|
|
662
|
+
readyEngines.forEach((eng, i) => {
|
|
663
|
+
console.log(` 引擎 [${i}]: ${eng.provider} (${eng.name}) → ${eng.model}`);
|
|
664
|
+
});
|
|
665
|
+
console.log(` ${c.gray(`正在并行分析(${readyEngines.length} 个引擎同时运行)...`)}\n`);
|
|
666
|
+
|
|
667
|
+
// Load conventions as context
|
|
668
|
+
const { ConventionStore } = require('../../modules/dev-time/convention-store');
|
|
669
|
+
const conventionStore = new ConventionStore(projectRoot);
|
|
670
|
+
const aiContext = {
|
|
671
|
+
conventions: conventionStore.getRelevant({}),
|
|
672
|
+
projectInfo: { root: projectRoot },
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const mergedFindings = await ai.multiAnalyze(readyEngines, suspectFindings, aiContext);
|
|
677
|
+
|
|
678
|
+
// Compute agreement stats
|
|
679
|
+
const stats = computeAgreementStats(mergedFindings, readyEngines.length);
|
|
680
|
+
|
|
681
|
+
console.log(` ${c.green('多引擎分析完成!')}\n`);
|
|
682
|
+
console.log(` ${c.bold('一致性统计:')}`);
|
|
683
|
+
console.log(` 全引擎一致: ${stats.allAgree} 个`);
|
|
684
|
+
console.log(` 部分一致: ${stats.partialAgree} 个`);
|
|
685
|
+
console.log(` 全引擎不一致: ${stats.allDisagree} 个`);
|
|
686
|
+
console.log(` 误报判断一致率: ${stats.agreementRate}\n`);
|
|
687
|
+
|
|
688
|
+
// Print comparison
|
|
689
|
+
for (const f of mergedFindings) {
|
|
690
|
+
const sev = f.severity === 'block' ? c.red('[阻断]') : c.yellow('[警告]');
|
|
691
|
+
console.log(` ${sev} ${f.message}`);
|
|
692
|
+
console.log(` ${c.gray(f.file)}${f.line ? ':' + f.line : ''}`);
|
|
693
|
+
|
|
694
|
+
if (f.aiAnalyses) {
|
|
695
|
+
for (const ea of f.aiAnalyses) {
|
|
696
|
+
const tag = `[${ea.engineName}]`;
|
|
697
|
+
if (ea.available && ea.analysis) {
|
|
698
|
+
const a = ea.analysis;
|
|
699
|
+
console.log(` ${c.cyan(`── ${tag} ──`)}`);
|
|
700
|
+
if (a.rootCause) console.log(` 根因: ${a.rootCause}`);
|
|
701
|
+
if (a.isFalsePositive !== undefined) console.log(` 误报: ${a.isFalsePositive ? '是' : '否'}`);
|
|
702
|
+
if (a.fix) console.log(` 修复: ${a.fix}`);
|
|
703
|
+
if (a.confidence !== undefined) console.log(` 置信度: ${(a.confidence * 100).toFixed(0)}%`);
|
|
704
|
+
} else {
|
|
705
|
+
console.log(` ${c.gray(`── ${tag} 不可用 ──`)}`);
|
|
706
|
+
if (ea.error) console.log(` ${c.gray('错误: ' + ea.error)}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Agreement indicator
|
|
712
|
+
if (f.aiAnalyses) {
|
|
713
|
+
const available = f.aiAnalyses.filter(ea => ea.available && ea.analysis);
|
|
714
|
+
if (available.length >= 2) {
|
|
715
|
+
const fpValues = available.map(ea => ea.analysis.isFalsePositive);
|
|
716
|
+
const allSame = fpValues.every(v => v === fpValues[0]);
|
|
717
|
+
console.log(` ${allSame ? c.green('✓ 全引擎判断一致 → 建议修复') : c.yellow('⏳ 引擎判断不一致 → 已标记待观察,暂不修复')}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
console.log();
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Auto-add disagreement findings to watch list
|
|
725
|
+
const watchList = require('../shared/watch-list');
|
|
726
|
+
const deferredCount = watchList.addDeferredFindings(projectRoot, mergedFindings,
|
|
727
|
+
'待观察:引擎判断不一致,等运行时验证');
|
|
728
|
+
|
|
729
|
+
// Save comparison results
|
|
730
|
+
const comparisonPath = path.join(projectRoot, '.devassist', 'reports', 'ai-comparison.json');
|
|
731
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
732
|
+
fs.writeFileSync(comparisonPath, JSON.stringify({
|
|
733
|
+
analyzedAt: new Date().toISOString(),
|
|
734
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
735
|
+
stats,
|
|
736
|
+
findings: mergedFindings,
|
|
737
|
+
}, null, 2), 'utf-8');
|
|
738
|
+
|
|
739
|
+
// Generate comparison HTML report
|
|
740
|
+
const { generateComparisonHTML } = require('./comparison-report');
|
|
741
|
+
const html = generateComparisonHTML({
|
|
742
|
+
projectRoot: path.basename(projectRoot),
|
|
743
|
+
timestamp: new Date().toISOString(),
|
|
744
|
+
engines: readyEngines.map(e => ({ provider: e.provider, model: e.model, name: e.name })),
|
|
745
|
+
stats,
|
|
746
|
+
findings: mergedFindings,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const htmlPath = path.join(reportDir, 'ai-comparison.html');
|
|
750
|
+
fs.writeFileSync(htmlPath, html, 'utf-8');
|
|
751
|
+
|
|
752
|
+
console.log(` ${c.gray('分析结果已保存:')}`);
|
|
753
|
+
console.log(` JSON: .devassist/reports/ai-comparison.json`);
|
|
754
|
+
console.log(` HTML: .devassist/reports/ai-comparison.html`);
|
|
755
|
+
|
|
756
|
+
if (deferredCount > 0) {
|
|
757
|
+
console.log(`\n ${c.yellow(`⏳ ${deferredCount} 个判断不一致的发现已标记为待观察`)}`);
|
|
758
|
+
console.log(` ${c.gray(' → 暂不修复,等实际运行验证后再处理')}`);
|
|
759
|
+
console.log(` ${c.gray(' → 查看待观察列表:devassist ai --watch-list')}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
console.log(`\n ${c.gray('修复策略:')}`);
|
|
763
|
+
console.log(` ${c.gray(' ✓ 全引擎一致 → 优先修复')}`);
|
|
764
|
+
console.log(` ${c.gray(' ⏳ 全引擎不一致 → 待观察,不修复')}\n`);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
console.log(` ${c.red('分析失败:')} ${err.message}\n`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Compute agreement statistics for N engines.
|
|
772
|
+
*/
|
|
773
|
+
function computeAgreementStats(findings, engineCount) {
|
|
774
|
+
let allAgree = 0; // All engines agree on isFalsePositive
|
|
775
|
+
let partialAgree = 0; // Some engines agree, some disagree
|
|
776
|
+
let allDisagree = 0; // All engines returned but no two agree
|
|
777
|
+
let partialAvailable = 0; // Not all engines returned
|
|
778
|
+
let falsePositiveAgreement = 0;
|
|
779
|
+
let totalCompared = 0;
|
|
780
|
+
|
|
781
|
+
for (const f of findings) {
|
|
782
|
+
if (!f.aiAnalyses) continue;
|
|
783
|
+
const available = f.aiAnalyses.filter(ea => ea.available && ea.analysis);
|
|
784
|
+
if (available.length < 2) {
|
|
785
|
+
partialAvailable++;
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
totalCompared++;
|
|
790
|
+
const fpValues = available.map(ea => ea.analysis.isFalsePositive);
|
|
791
|
+
const allSame = fpValues.every(v => v === fpValues[0]);
|
|
792
|
+
|
|
793
|
+
if (available.length === engineCount) {
|
|
794
|
+
if (allSame) {
|
|
795
|
+
allAgree++;
|
|
796
|
+
falsePositiveAgreement++;
|
|
797
|
+
} else {
|
|
798
|
+
// Check if majority agree
|
|
799
|
+
const trueCount = fpValues.filter(v => v === true).length;
|
|
800
|
+
const falseCount = fpValues.filter(v => v === false).length;
|
|
801
|
+
if (Math.max(trueCount, falseCount) >= 2) {
|
|
802
|
+
partialAgree++;
|
|
803
|
+
} else {
|
|
804
|
+
allDisagree++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} else {
|
|
808
|
+
partialAvailable++;
|
|
809
|
+
if (allSame) falsePositiveAgreement++;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const agreementRate = totalCompared > 0
|
|
814
|
+
? `${((falsePositiveAgreement / totalCompared) * 100).toFixed(0)}%(${falsePositiveAgreement}/${totalCompared})`
|
|
815
|
+
: '—';
|
|
816
|
+
|
|
817
|
+
return { allAgree, partialAgree, allDisagree, partialAvailable, falsePositiveAgreement, totalCompared, agreementRate };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ─── Watch List Management ───────────────────────────
|
|
821
|
+
|
|
822
|
+
function manageWatchList(projectRoot, args, c) {
|
|
823
|
+
const watchList = require('../shared/watch-list');
|
|
824
|
+
|
|
825
|
+
const doResolve = args.includes('--resolve');
|
|
826
|
+
const doDismiss = args.includes('--dismiss');
|
|
827
|
+
const doNote = args.includes('--note');
|
|
828
|
+
const doRemove = args.includes('--remove');
|
|
829
|
+
const doStats = args.includes('--stats');
|
|
830
|
+
|
|
831
|
+
// --resolve <index> — mark as fixed (runtime confirmed issue)
|
|
832
|
+
if (doResolve) {
|
|
833
|
+
const target = getArg(args, '--resolve');
|
|
834
|
+
const runtimeNotes = getArg(args, '--runtime-note');
|
|
835
|
+
const item = watchList.resolveItem(projectRoot, target, 'fixed', runtimeNotes);
|
|
836
|
+
if (item) {
|
|
837
|
+
console.log(`\n ${c.green('✓ 已标记为已修复:')} ${item.finding.file}:${item.finding.line}\n`);
|
|
838
|
+
} else {
|
|
839
|
+
console.log(`\n ${c.red('未找到对应的待观察项。')}\n`);
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// --dismiss <index> — mark as no-issue (runtime confirmed no problem)
|
|
845
|
+
if (doDismiss) {
|
|
846
|
+
const target = getArg(args, '--dismiss');
|
|
847
|
+
const runtimeNotes = getArg(args, '--runtime-note');
|
|
848
|
+
const item = watchList.dismissItem(projectRoot, target, runtimeNotes);
|
|
849
|
+
if (item) {
|
|
850
|
+
console.log(`\n ${c.green('✓ 已标记为无问题:')} ${item.finding.file}:${item.finding.line}\n`);
|
|
851
|
+
} else {
|
|
852
|
+
console.log(`\n ${c.red('未找到对应的待观察项。')}\n`);
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// --note <index> --text "备注内容"
|
|
858
|
+
if (doNote) {
|
|
859
|
+
const target = getArg(args, '--note');
|
|
860
|
+
const note = getArg(args, '--text') || '无备注';
|
|
861
|
+
const item = watchList.updateNote(projectRoot, target, note);
|
|
862
|
+
if (item) {
|
|
863
|
+
console.log(`\n ${c.green('✓ 备注已更新:')} ${item.finding.file}:${item.finding.line}`);
|
|
864
|
+
console.log(` ${c.gray('备注:' + item.note)}\n`);
|
|
865
|
+
} else {
|
|
866
|
+
console.log(`\n ${c.red('未找到对应的待观察项。')}\n`);
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// --remove <index>
|
|
872
|
+
if (doRemove) {
|
|
873
|
+
const target = getArg(args, '--remove');
|
|
874
|
+
const removed = watchList.removeItem(projectRoot, target);
|
|
875
|
+
if (removed) {
|
|
876
|
+
console.log(`\n ${c.green('✓ 已移除:')} ${removed.finding.file}:${removed.finding.line}\n`);
|
|
877
|
+
} else {
|
|
878
|
+
console.log(`\n ${c.red('未找到对应的待观察项。')}\n`);
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// --stats only
|
|
884
|
+
if (doStats) {
|
|
885
|
+
const stats = watchList.getStats(projectRoot);
|
|
886
|
+
console.log(`\n ${c.bold('待观察列表统计')}\n`);
|
|
887
|
+
console.log(` 总计: ${stats.total}`);
|
|
888
|
+
console.log(` 待观察: ${c.yellow(stats.pending)}`);
|
|
889
|
+
console.log(` 已修复: ${c.green(stats.resolved)}`);
|
|
890
|
+
console.log(` 已排除: ${c.gray(stats.dismissed)}\n`);
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Default: list all pending items
|
|
895
|
+
const stats = watchList.getStats(projectRoot);
|
|
896
|
+
const pending = watchList.getPending(projectRoot);
|
|
897
|
+
|
|
898
|
+
console.log(`\n ${c.bold('⏳ 待观察列表')}\n`);
|
|
899
|
+
|
|
900
|
+
if (pending.length === 0) {
|
|
901
|
+
console.log(` ${c.green('没有待观察的发现。')}\n`);
|
|
902
|
+
if (stats.total > 0) {
|
|
903
|
+
console.log(` ${c.gray(`(历史记录:${stats.resolved} 已修复,${stats.dismissed} 已排除)`)}\n`);
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
console.log(` 共 ${c.yellow(pending.length + ' 个')} 待观察的发现(暂不修复,等运行时验证)\n`);
|
|
909
|
+
|
|
910
|
+
pending.forEach((item, i) => {
|
|
911
|
+
const sev = item.finding.severity === 'block' ? c.red('[阻断]') : c.yellow('[警告]');
|
|
912
|
+
console.log(` ${c.bold(`[${i}]`)} ${sev} ${item.finding.message}`);
|
|
913
|
+
console.log(` 位置: ${c.gray(item.finding.file + ':' + item.finding.line)} | ${item.finding.ruleId}`);
|
|
914
|
+
console.log(` 备注: ${c.gray(item.note)}`);
|
|
915
|
+
console.log(` 标记于: ${c.gray(new Date(item.deferredAt).toLocaleString('zh-CN'))}`);
|
|
916
|
+
|
|
917
|
+
// Show engine disagreement summary
|
|
918
|
+
if (item.aiAnalyses && item.aiAnalyses.length >= 2) {
|
|
919
|
+
const fpYes = item.aiAnalyses.filter(a => a.isFalsePositive === true).map(a => a.engineName);
|
|
920
|
+
const fpNo = item.aiAnalyses.filter(a => a.isFalsePositive === false).map(a => a.engineName);
|
|
921
|
+
console.log(` 分歧: ${c.gray(fpNo.join('/') + ' 认为真问题 vs ' + fpYes.join('/') + ' 认为误报')}`);
|
|
922
|
+
}
|
|
923
|
+
console.log();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
console.log(` ${c.gray('操作:')}`);
|
|
927
|
+
console.log(` ${c.bold('devassist ai --watch-list --resolve <序号>')} 标记为已修复`);
|
|
928
|
+
console.log(` ${c.bold('devassist ai --watch-list --dismiss <序号>')} 标记为无问题`);
|
|
929
|
+
console.log(` ${c.bold('devassist ai --watch-list --note <序号> --text "备注"')} 更新备注`);
|
|
930
|
+
console.log(` ${c.bold('devassist ai --watch-list --remove <序号>')} 从列表移除\n`);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ─── Utils ───────────────────────────────────────────
|
|
934
|
+
|
|
935
|
+
function getArg(args, flag) {
|
|
936
|
+
const idx = args.indexOf(flag);
|
|
937
|
+
if (idx >= 0 && args[idx + 1]) return args[idx + 1];
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
module.exports.loadAIConfig = loadAIConfig;
|
|
942
|
+
module.exports.PROVIDER_INFO = PROVIDER_INFO;
|
|
943
|
+
module.exports.ENV_MAP = ENV_MAP;
|
|
944
|
+
module.exports.computeAgreementStats = computeAgreementStats;
|