- 设置面板:三步引导 / AI 配置 / API Key / 规则管理 / 连接测试 - Provider 重构:统一 OpenAICompatibleProvider 基类,新增 Gemini/Claude/混元/智谱等 - 审查面板 Webview:三 Tab、统计卡片、问题列表、postMessage 通信 - esbuild 构建脚本 + 生产打包 + PMD 下载 - 保存文件自动静态分析(500ms debounce)
254 lines
7.2 KiB
Markdown
254 lines
7.2 KiB
Markdown
# Step 10 — Phase 4.2: AI 引擎 + Schema
|
|
|
|
**依赖**: Step 09
|
|
**参考设计**: §5.3, §5.4, §5.5, §5.6
|
|
|
|
## 目标
|
|
|
|
实现 AI 审查引擎:两并行请求(自定义规则评估 + 翻译深度审查)、JSON 解析、错误降级。
|
|
|
|
## 新建文件
|
|
|
|
| # | 文件 | 说明 |
|
|
|---|------|------|
|
|
| 1 | `src/ai/schema.ts` | AI 响应结构类型定义 |
|
|
| 2 | `src/ai/engine.ts` | `runAIReview()` 主函数 |
|
|
|
|
---
|
|
|
|
## 1. `src/ai/schema.ts`
|
|
|
|
```typescript
|
|
export interface TranslatedDiagnostic {
|
|
originalRuleId: string;
|
|
translatedMessage: string;
|
|
translatedSuggestion: string;
|
|
codeDiff?: string;
|
|
}
|
|
|
|
export interface CustomRuleResult {
|
|
ruleId: string;
|
|
line: number;
|
|
severity: 'error' | 'warning' | 'info';
|
|
message: string;
|
|
}
|
|
|
|
export interface AIFinding {
|
|
ruleId: string;
|
|
severity: 'error' | 'warning' | 'info';
|
|
category: 'bug' | 'performance' | 'security' | 'style' | 'design';
|
|
title: string;
|
|
description: string;
|
|
suggestion: string;
|
|
codeDiff?: string;
|
|
line: number;
|
|
}
|
|
|
|
export interface AIResponse {
|
|
translatedDiagnostics: TranslatedDiagnostic[];
|
|
customRuleResults: CustomRuleResult[];
|
|
findings: AIFinding[];
|
|
}
|
|
|
|
export interface AIEngineResult {
|
|
customRuleResults: CustomRuleResult[];
|
|
translatedDiagnostics: TranslatedDiagnostic[];
|
|
findings: AIFinding[];
|
|
degraded: boolean;
|
|
error?: string;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. `src/ai/engine.ts`
|
|
|
|
```typescript
|
|
import * as vscode from 'vscode';
|
|
import { AIProvider } from './providers/base';
|
|
import { createProvider } from './factory';
|
|
import { getAIConfig, getApiKey } from '../config';
|
|
import { LinterDiagnostic } from '../types';
|
|
import { CustomRule } from '../rules/yaml-parser';
|
|
import {
|
|
AIEngineResult,
|
|
CustomRuleResult,
|
|
TranslatedDiagnostic,
|
|
AIFinding,
|
|
} from './schema';
|
|
|
|
function buildCustomRulePrompt(rules: CustomRule[]): string {
|
|
return rules.map(r =>
|
|
`- [${r.id}] (${r.severity}) ${r.description}`
|
|
).join('\n');
|
|
}
|
|
|
|
function buildLinterDiagnosticsPrompt(diagnostics: LinterDiagnostic[]): string {
|
|
return diagnostics.map(d =>
|
|
`- [${d.ruleId}] L${d.range.start.line + 1}: ${d.message}`
|
|
).join('\n');
|
|
}
|
|
|
|
function addLineNumbers(code: string): string {
|
|
return code.split('\n').map((line, i) => `${String(i + 1).padStart(4, ' ')}| ${line}`).join('\n');
|
|
}
|
|
|
|
function parseJsonResponse(raw: string): object {
|
|
const trimmed = raw.trim();
|
|
const start = trimmed.indexOf('{');
|
|
const end = trimmed.lastIndexOf('}');
|
|
if (start === -1 || end === -1) {
|
|
throw new Error('响应中未找到 JSON');
|
|
}
|
|
return JSON.parse(trimmed.substring(start, end + 1));
|
|
}
|
|
|
|
const CUSTOM_RULE_SYSTEM_PROMPT = `你是代码规则审查员,只评估以下自定义规则是否被违反。
|
|
理解语义而非文本匹配。
|
|
仅输出 JSON,格式:
|
|
{ "customRuleResults": [{ "ruleId": "规则ID", "line": 行号, "severity": "error|warning|info", "message": "触发描述" }] }
|
|
如果没有违反任何规则,返回空数组。`;
|
|
|
|
const DEEP_REVIEW_SYSTEM_PROMPT = `你是资深代码审查专家,完成两个任务:
|
|
1. 将英文静态分析结果翻译为输出语言,并补充修复建议
|
|
2. 深度审查代码,发现静态分析未覆盖的问题
|
|
重点:安全漏洞、逻辑错误、性能问题、设计缺陷
|
|
不要重复静态分析已报告的问题。
|
|
|
|
仅输出 JSON,格式:
|
|
{
|
|
"translatedDiagnostics": [{ "originalRuleId": "原始ID", "translatedMessage": "翻译", "translatedSuggestion": "建议", "codeDiff": "可选" }],
|
|
"findings": [{ "ruleId": "kebab-case", "severity": "error|warning|info", "category": "bug|performance|security|style|design", "title": "标题", "description": "描述", "suggestion": "建议", "codeDiff": "可选", "line": 行号 }]
|
|
}`;
|
|
|
|
export async function runAIReview(
|
|
context: vscode.ExtensionContext,
|
|
code: string,
|
|
staticDiagnostics: LinterDiagnostic[],
|
|
customRules: CustomRule[]
|
|
): Promise<AIEngineResult> {
|
|
const config = getAIConfig();
|
|
const apiKey = await getApiKey(context);
|
|
|
|
if (!apiKey) {
|
|
return {
|
|
customRuleResults: [],
|
|
translatedDiagnostics: [],
|
|
findings: [],
|
|
degraded: true,
|
|
error: '未配置 API Key',
|
|
};
|
|
}
|
|
|
|
let provider: AIProvider;
|
|
try {
|
|
provider = createProvider(config.provider, apiKey, config.baseUrl);
|
|
} catch (err) {
|
|
return {
|
|
customRuleResults: [],
|
|
translatedDiagnostics: [],
|
|
findings: [],
|
|
degraded: true,
|
|
error: `创建 Provider 失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
};
|
|
}
|
|
|
|
const options = {
|
|
model: config.model,
|
|
temperature: config.temperature,
|
|
timeoutMs: config.timeout * 1000,
|
|
};
|
|
|
|
const numberedCode = addLineNumbers(code);
|
|
|
|
const requestA =
|
|
customRules.length > 0
|
|
? provider.chat(
|
|
CUSTOM_RULE_SYSTEM_PROMPT,
|
|
`## 自定义规则\n${buildCustomRulePrompt(customRules)}\n\n## 代码(带行号)\n${numberedCode}`,
|
|
options
|
|
)
|
|
: Promise.resolve('{}');
|
|
|
|
const requestB = provider.chat(
|
|
`${DEEP_REVIEW_SYSTEM_PROMPT}\n输出语言:${config.outputLanguage}`,
|
|
`## 代码(带行号)\n${numberedCode}\n\n## 静态分析结果(英文)\n${buildLinterDiagnosticsPrompt(staticDiagnostics)}`,
|
|
options
|
|
);
|
|
|
|
const [resultA, resultB] = await Promise.allSettled([requestA, requestB]);
|
|
|
|
const errors: string[] = [];
|
|
|
|
let customRuleResults: CustomRuleResult[] = [];
|
|
if (resultA.status === 'fulfilled') {
|
|
try {
|
|
const parsed = parseJsonResponse(resultA.value) as { customRuleResults?: CustomRuleResult[] };
|
|
customRuleResults = (parsed.customRuleResults ?? []).map(r => ({
|
|
...r,
|
|
ruleId: `custom:${r.ruleId}`,
|
|
}));
|
|
} catch {
|
|
errors.push('自定义规则响应解析失败');
|
|
}
|
|
} else {
|
|
errors.push(`自定义规则请求失败: ${resultA.reason}`);
|
|
}
|
|
|
|
let translatedDiagnostics: TranslatedDiagnostic[] = [];
|
|
let findings: AIFinding[] = [];
|
|
if (resultB.status === 'fulfilled') {
|
|
try {
|
|
const parsed = parseJsonResponse(resultB.value) as {
|
|
translatedDiagnostics?: TranslatedDiagnostic[];
|
|
findings?: AIFinding[];
|
|
};
|
|
translatedDiagnostics = parsed.translatedDiagnostics ?? [];
|
|
findings = parsed.findings ?? [];
|
|
} catch {
|
|
errors.push('AI 审查响应解析失败');
|
|
}
|
|
} else {
|
|
errors.push(`AI 审查请求失败: ${resultB.reason}`);
|
|
}
|
|
|
|
const degraded = errors.length > 0;
|
|
return {
|
|
customRuleResults,
|
|
translatedDiagnostics,
|
|
findings,
|
|
degraded,
|
|
error: errors.join('; '),
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 关键逻辑
|
|
|
|
**两并行请求**:
|
|
|
|
| 请求 | 内容 | System Prompt |
|
|
|------|------|---------------|
|
|
| A | 自定义规则评估(需完整代码) | 只评估规则、语义理解、输出 JSON |
|
|
| B | 翻译 + 深度审查(需静态分析结果) | 翻译诊断 + 深度审查、不重复静态分析、输出 JSON |
|
|
|
|
**降级策略**:
|
|
1. `Promise.allSettled` 确保单请求失败不影响另一个
|
|
2. API Key 未配置 → 所有 AI 功能降级
|
|
3. JSON 解析失败 → 该请求降级,记录错误
|
|
4. Provider 创建失败 → 全部降级
|
|
|
|
**Prompt 设计**:
|
|
- 请求 A: 注入自定义规则的 description 列表
|
|
- 请求 B: 注入静态分析英文诊断列表 + 输出语言配置
|
|
|
|
---
|
|
|
|
## 验收
|
|
|
|
- [ ] 2 个文件创建完成
|
|
- [ ] `npm run compile` 通过
|
|
- [ ] `npm run lint` 通过
|