feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复
- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider - 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译 - SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整 - ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示 - AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错 - JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射 - 诊断按 severity + 行号排序
This commit is contained in:
@@ -0,0 +1,417 @@
|
||||
# 导入预览 · 错误规则可编辑与「添加」设计书
|
||||
|
||||
> 版本:v1.0
|
||||
> 日期:2026-07-31
|
||||
> 适用项目:vscode-code-reviewer
|
||||
> 参考文档:`2026-07-30-export-template-design-v2.md`(错误规则组雏形)、`2026-07-25-import-preview-edit-design.md`(预览编辑)
|
||||
|
||||
---
|
||||
|
||||
## 一、方案概览
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
模板导入(勾选「使用模板文件导入」)进入预览后,**错误规则**不再是纯只读展示,而是:
|
||||
|
||||
- 卡片提供**完整编辑表单**(与有效规则一致:id / severity / description / message / languages / excludeLanguages)
|
||||
- 每张错误卡片有独立「添加」按钮,点击后:
|
||||
1. **重新校验格式**(id/description/message 非空、severity 合法)
|
||||
2. 校验通过后**单条 AI 去重**(复用 `buildDedupOnlyPrompt`),失败先重试一次,仍失败降级为 `none`
|
||||
3. 按去重结果(exact/overlap/none)把该规则**移入对应分区**,变为正常可编辑规则(带保留/注释切换)
|
||||
- 添加时若 id 与预览中已有规则重复 → **阻止并提示修改 id**
|
||||
- 未添加的错误规则维持现状:确认导入时自动丢弃、不参与校验
|
||||
- 确认写盘必须包含已添加的规则(避免走 raw yaml 路径丢失)
|
||||
|
||||
### 1.2 数据流
|
||||
|
||||
```
|
||||
现状(仅展示):
|
||||
parseTemplate → errorRules(带 validationIssues)→ 预览只读展示 → 确认时自动丢弃
|
||||
|
||||
改造后:
|
||||
parseTemplate → errorRules → 预览可编辑错误卡片
|
||||
├─ 用户编辑字段 → 点击「添加」
|
||||
│ ├─ [扩展端] 格式校验 → 失败 → postMessage addError(卡片内提示)
|
||||
│ ├─ [扩展端] id 冲突检查(对照预览已有有效规则)→ 冲突 → addError
|
||||
│ ├─ [扩展端] AI 单条去重(重试 1 次 → 降级 none)→ postMessage ruleAdded
|
||||
│ └─ [前端] DOM 手术:错误卡片移入 exact/overlap/none 分区,
|
||||
│ 去除 data-error、换保留/注释按钮、更新计数
|
||||
└─ 确认导入
|
||||
├─ 已添加或已编辑 → 前端回传 editedRules(含已添加规则)→ renderRulesToYaml 写盘
|
||||
└─ 无编辑无添加 → 走原 buildFinalYamlFromRaw(零回归)
|
||||
```
|
||||
|
||||
### 1.3 范围
|
||||
|
||||
| 类型 | 内容 |
|
||||
|------|------|
|
||||
| 含 | 错误卡片完整编辑表单;「添加」按钮(校验 + 单条 AI 去重 + 移入分区);id 冲突拦截;去重失败重试/降级;确认写盘含已添加规则;计数动态更新 |
|
||||
| 不含 | 非模板导入路径(AI 链路无 validationIssues,不受影响);批量「全部添加」按钮;错误规则的本地重复检查 |
|
||||
| 不触碰 | `parseTemplate` 校验逻辑、`buildDedupOnlyPrompt`、`applyConversion`、其余转换器 |
|
||||
|
||||
---
|
||||
|
||||
## 二、架构设计
|
||||
|
||||
### 2.1 模块划分
|
||||
|
||||
```
|
||||
src/rules/
|
||||
├── import-service.ts ← 修改: 新增 export async function dedupSingleRule()
|
||||
├── import-preview.ts ← 修改: 错误卡片可编辑 + 添加流程(前端 JS + 扩展消息处理)
|
||||
└── import-types.ts ← 不改(复用现有类型)
|
||||
|
||||
src/i18n/messages.ts ← 修改: 新增 key,调整 import.cannotImport 文案
|
||||
```
|
||||
|
||||
### 2.2 关键接口
|
||||
|
||||
```ts
|
||||
// import-service.ts 新增导出函数
|
||||
export interface DedupResult {
|
||||
duplicateLevel: 'exact' | 'overlap' | 'none';
|
||||
duplicateOf?: string;
|
||||
duplicateReason?: string;
|
||||
}
|
||||
|
||||
// 单条规则 AI 去重:失败重试 1 次,仍失败返回 null(调用方降级为 none)
|
||||
export async function dedupSingleRule(
|
||||
rule: ImportableRule,
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<DedupResult | null>
|
||||
```
|
||||
|
||||
```ts
|
||||
// import-preview.ts — Webview 消息协议扩展
|
||||
|
||||
// 前端 → 扩展
|
||||
interface AddErrorRuleMessage {
|
||||
type: 'addErrorRule';
|
||||
ruleId: string; // 原始卡片 id(用于在 result.rules 中定位)
|
||||
rule: { // 当前卡片全部字段(含用户编辑)
|
||||
id: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
message: string;
|
||||
languages?: string[];
|
||||
excludeLanguages?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// 扩展 → 前端
|
||||
interface AddErrorMessage {
|
||||
type: 'addError';
|
||||
ruleId: string;
|
||||
message: string;
|
||||
}
|
||||
interface RuleAddedMessage {
|
||||
type: 'ruleAdded';
|
||||
ruleId: string; // 原始卡片 id(前端按此定位 DOM)
|
||||
id: string; // 去重后的最终 id(可能被用户编辑过)
|
||||
duplicateLevel: 'exact' | 'overlap' | 'none';
|
||||
duplicateOf?: string;
|
||||
duplicateReason?: string;
|
||||
dedupFailed: boolean; // true 表示降级为 none
|
||||
}
|
||||
```
|
||||
|
||||
`convertContentWithAI` 增加可选第 4 参 `quiet?: boolean`(去重失败时抑制内置 error toast,改由前端降级提示)。现有调用点均传 3 参,向后兼容。
|
||||
|
||||
---
|
||||
|
||||
## 三、详细实现
|
||||
|
||||
### 3.1 `import-service.ts`:新增 `dedupSingleRule`
|
||||
|
||||
```ts
|
||||
export interface DedupResult {
|
||||
duplicateLevel: 'exact' | 'overlap' | 'none';
|
||||
duplicateOf?: string;
|
||||
duplicateReason?: string;
|
||||
}
|
||||
|
||||
export async function dedupSingleRule(
|
||||
rule: ImportableRule,
|
||||
context: vscode.ExtensionContext,
|
||||
): Promise<DedupResult | null> {
|
||||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
|
||||
|
||||
const singleYaml = [
|
||||
`- id: ${rule.id}`,
|
||||
` severity: ${rule.severity}`,
|
||||
` description: ${rule.description}`,
|
||||
` message: ${rule.message}`,
|
||||
...(rule.languages?.length ? [` languages: [${rule.languages.join(', ')}]`] : []),
|
||||
...(rule.excludeLanguages?.length ? [` excludeLanguages: [${rule.excludeLanguages.join(', ')}]`] : []),
|
||||
].join('\n');
|
||||
|
||||
const { system, user } = buildDedupOnlyPrompt(singleYaml, existingRules);
|
||||
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
const out = await convertContentWithAI(user, context, system, true); // quiet
|
||||
if (!out) continue;
|
||||
const parsed = parseImportableYaml(out);
|
||||
if (parsed.length === 0) continue;
|
||||
const r = parsed[0];
|
||||
return {
|
||||
duplicateLevel: r.duplicateLevel ?? 'none',
|
||||
duplicateOf: r.duplicateOf,
|
||||
duplicateReason: r.duplicateReason,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 `import-service.ts`:`convertContentWithAI` 增加 quiet 参数
|
||||
|
||||
```ts
|
||||
export async function convertContentWithAI(
|
||||
content: string,
|
||||
context: vscode.ExtensionContext,
|
||||
systemPrompt?: string,
|
||||
quiet?: boolean,
|
||||
): Promise<string | null> {
|
||||
// ... getApiKey 失败:quiet 时仅返回 null,不弹 toast
|
||||
// ... provider.chat 异常:quiet 时仅返回 null,不弹 toast
|
||||
// ... 其余逻辑不变
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 `import-preview.ts`:错误卡片渲染改造
|
||||
|
||||
**废弃 `renderErrorCard` / `renderErrorSection` 的只读版**,统一由 `renderRuleCard` 承担,新增 `isError` 分支:
|
||||
|
||||
```
|
||||
renderRuleCard(rule, { isError })
|
||||
├─ 普通卡片:现状逻辑不变
|
||||
└─ 错误卡片:
|
||||
├─ 卡片属性 data-ruleid + data-error="true" + 错误边框(保留现 opacity/border 样式)
|
||||
├─ 折叠态摘要:id(可编辑)+ 错误徽标(新 i18n,如「需修复后添加」)
|
||||
├─ 展开态表单:与普通卡片完全一致(id 可改、severity 下拉、
|
||||
│ description/message textarea、languages/excludeLanguages 标签)
|
||||
├─ 表单顶部:错误原因列表(复用现 issues 渲染)
|
||||
├─ 顶部操作区:用「添加」按钮替代「保留/注释」toggle(新 i18n)
|
||||
└─ 无 duplicateInfo(尚未去重)
|
||||
```
|
||||
|
||||
**默认展开**错误卡片(`body-<id>` display:block),让用户立即看到错误原因。
|
||||
|
||||
**分区容器加 `data-section` 属性**(前端 DOM 手术定位用):
|
||||
|
||||
- 错误分区容器:`data-section="error"`(原 `renderErrorSection`,标题/图标不变)
|
||||
- `renderSection` 三个分区容器分别加 `data-section="exact" | "overlap" | "none"`
|
||||
|
||||
**顶部 summary 计数加 id**(前端更新用):
|
||||
|
||||
- `⛔ 完全重复 N 条` → `<span id="count-exact">`
|
||||
- `⚠️ 部分重叠 N 条` → `<span id="count-overlap">`
|
||||
- `✅ 无重复 N 条` → `<span id="count-none">`
|
||||
- 错误分区标题计数 → `<span id="count-error">`(放在 `renderErrorSection` 的 section-title 内)
|
||||
|
||||
**确认按钮不再静态禁用**:改为 JS 动态控制。初始无有效规则时保持禁用 + 显示 `emptyValidHint`;一旦「添加」成功移入有效分区,JS 启用按钮并隐藏提示。
|
||||
|
||||
### 3.4 `import-preview.ts`:扩展端消息处理
|
||||
|
||||
`showImportPreview` 的 `onDidReceiveMessage` 增加分支:
|
||||
|
||||
```ts
|
||||
panel.webview.onDidReceiveMessage(async (msg) => {
|
||||
if (msg.type === 'toggleRule') {
|
||||
keepRule[msg.ruleId] = msg.keep;
|
||||
} else if (msg.type === 'addErrorRule') {
|
||||
await handleAddErrorRule(msg, result, keepRule, context, panel);
|
||||
} else if (msg.type === 'confirm') {
|
||||
resolve({ keepRule, confirmed: true, editedRules: msg.editedRules });
|
||||
panel.dispose();
|
||||
} else if (msg.type === 'cancel') {
|
||||
resolve(null);
|
||||
panel.dispose();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
`handleAddErrorRule` 逻辑(顺序严格):
|
||||
|
||||
```ts
|
||||
async function handleAddErrorRule(msg, result, keepRule, context, panel) {
|
||||
const rule = msg.rule;
|
||||
|
||||
// [1] 格式校验(复用前端同一套规则)
|
||||
const err = validateRule(rule); // id/severity/description/message
|
||||
if (err) {
|
||||
panel.webview.postMessage({ type: 'addError', ruleId: msg.ruleId, message: err });
|
||||
return;
|
||||
}
|
||||
|
||||
// [2] id 冲突检查(对照 result.rules 中非错误规则,忽略大小写)
|
||||
const conflict = result.rules.some(r =>
|
||||
!r.validationIssues?.length && r.id.toLowerCase() === rule.id.toLowerCase()
|
||||
);
|
||||
if (conflict) {
|
||||
panel.webview.postMessage({
|
||||
type: 'addError', ruleId: msg.ruleId,
|
||||
message: t('import.idConflict', { 0: rule.id }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// [3] AI 单条去重(内部已重试 1 次),失败降级 none
|
||||
const dedup = await dedupSingleRule(rule, context);
|
||||
const level = dedup?.duplicateLevel ?? 'none';
|
||||
const dedupFailed = !dedup;
|
||||
|
||||
// [4] 更新 result.rules 中该条规则(按原始 id 定位)
|
||||
const idx = result.rules.findIndex(r => r.id === msg.ruleId);
|
||||
if (idx >= 0) {
|
||||
result.rules[idx] = {
|
||||
...result.rules[idx],
|
||||
id: rule.id,
|
||||
severity: rule.severity,
|
||||
description: rule.description,
|
||||
message: rule.message,
|
||||
languages: rule.languages,
|
||||
excludeLanguages: rule.excludeLanguages,
|
||||
duplicateLevel: level,
|
||||
duplicateOf: dedup?.duplicateOf,
|
||||
duplicateReason: dedup?.duplicateReason,
|
||||
validationIssues: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// [5] keepRule 按新 id 记录(exact → false)
|
||||
keepRule[rule.id] = level !== 'exact';
|
||||
|
||||
// [6] 通知前端移动卡片
|
||||
panel.webview.postMessage({
|
||||
type: 'ruleAdded',
|
||||
ruleId: msg.ruleId, // 原始 id,前端定位 DOM
|
||||
id: rule.id, // 新 id
|
||||
duplicateLevel: level,
|
||||
duplicateOf: dedup?.duplicateOf,
|
||||
duplicateReason: dedup?.duplicateReason,
|
||||
dedupFailed,
|
||||
});
|
||||
}
|
||||
|
||||
function validateRule(rule): string | null {
|
||||
if (!rule.id || !rule.id.trim()) return t('import.validationIdEmpty');
|
||||
if (!['error', 'warning', 'info'].includes(rule.severity)) return t('import.validationSeverityInvalid');
|
||||
if (!rule.description || !rule.description.trim()) return t('import.validationDescEmpty', { 0: rule.id });
|
||||
if (!rule.message || !rule.message.trim()) return t('import.validationMsgEmpty', { 0: rule.id });
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
> `context` 需传入 `showImportPreview`(新增参数)或在模块内暂存——采用**新增参数**:`showImportPreview(result, context)`。
|
||||
|
||||
### 3.5 `import-preview.ts`:前端 JS 改造
|
||||
|
||||
新增 / 修改函数:
|
||||
|
||||
```js
|
||||
// 从单张卡片提取当前字段(供添加与校验复用;从 collectEditedRules 抽取公共逻辑)
|
||||
function collectCardRule(ruleId) { /* 读 id-display-input / select / textareas / tag-lists */ }
|
||||
|
||||
let addedRules = 0; // 已成功添加的错误规则数
|
||||
|
||||
// 点击「添加」
|
||||
function addErrorRule(ruleId) {
|
||||
const btn = document.querySelector(`[data-addbtn="${ruleId}"]`);
|
||||
btn.disabled = true; btn.textContent = ADDING_TEXT;
|
||||
const rule = collectCardRule(ruleId);
|
||||
vscode.postMessage({ type: 'addErrorRule', ruleId, rule });
|
||||
}
|
||||
|
||||
// 接收扩展消息
|
||||
window.addEventListener('message', e => {
|
||||
const msg = e.data;
|
||||
if (msg.type === 'addError') {
|
||||
// 卡片内展示 msg.message,恢复添加按钮可点击
|
||||
} else if (msg.type === 'ruleAdded') {
|
||||
moveCardToSection(msg);
|
||||
}
|
||||
});
|
||||
|
||||
function moveCardToSection(msg) {
|
||||
const card = document.querySelector(`.rule-card[data-ruleid="${msg.ruleId}"]`);
|
||||
// 1. 更新 id 相关:card.dataset.ruleid = msg.id;两个 id input 值为 msg.id
|
||||
// 2. 移除 data-error 与错误边框样式
|
||||
// 3. 还原「添加」按钮为保留/注释 toggle(按 keepRule 状态)—— 需从扩展同步 keep 状态:
|
||||
// msg.duplicateLevel === 'exact' 时默认注释态,否则保留态
|
||||
// 4. 更新徽标:exact →「将注释」;overlap/none →「保留」(badge-exact/overlap/none)
|
||||
// 5. 追加 duplicateInfo(exact/overlap 文案,复用现有拼接逻辑)
|
||||
// 6. 移入对应分区:document.querySelector(`[data-section="${section}"]`).appendChild(card)
|
||||
// 7. addedRules++;更新计数与确认按钮状态
|
||||
}
|
||||
|
||||
// 分区标题计数(N 条)与顶部 summary 计数统一重算
|
||||
function updateSectionCounts() {
|
||||
// 按 data-section 遍历,重算 4 个分区标题计数 + count-exact/overlap/none
|
||||
// 错误分区为空 → 隐藏整个分区容器
|
||||
// 无任何有效规则 → 禁用确认按钮 + 显示 emptyValidHint;否则启用
|
||||
}
|
||||
|
||||
// 确认:hasEdits 或 addedRules>0 时必带 editedRules
|
||||
function doConfirm() {
|
||||
const err = validate();
|
||||
if (err) { /* 现逻辑 */ return; }
|
||||
const edited = collectEditedRules();
|
||||
const hasEdits = Object.keys(editedRules).length > 0;
|
||||
const withData = (hasEdits || addedRules > 0) ? edited : undefined;
|
||||
vscode.postMessage({ type: 'confirm', editedRules: withData });
|
||||
}
|
||||
```
|
||||
|
||||
`updateSummary()`(保留/注释计数)已按 `data-error` 跳过错误卡片,无需改动;`moveCardToSection` 移除 `data-error` 后该卡片自动纳入统计。
|
||||
|
||||
### 3.6 关键边界
|
||||
|
||||
| 边界 | 处理 |
|
||||
|------|------|
|
||||
| 添加时 id 冲突(预览内已有有效规则) | `addError` 提示改 id,卡片留在错误分区 |
|
||||
| AI 去重首次失败 | 自动重试 1 次(共 2 次尝试,quiet 模式不弹错) |
|
||||
| 重试仍失败 | 降级 `none` 移入无重复分区,前端提示「AI 去重失败,已以无重复方式添加」 |
|
||||
| 添加后 id 被修改导致与后续规则重复 | 后续添加时冲突检查覆盖全量有效规则,会拦截 |
|
||||
| 只添加未编辑字段 | `addedRules>0` 仍回传 `editedRules`,走 `renderRulesToYaml`,已添加规则不会丢失 |
|
||||
| 错误规则未添加即确认 | 维持现状:`data-error` 卡片被 `collectEditedRules`/`validate` 跳过,自动丢弃 |
|
||||
| 初始无有效规则 | 确认按钮禁用;「添加」成功第一条后 JS 启用 |
|
||||
| 未配置 API Key | `convertContentWithAI` 返回 null(quiet),重试后降级 none,流程不中断 |
|
||||
|
||||
### 3.7 i18n 新增 / 调整
|
||||
|
||||
| key | zh-CN | en | ja |
|
||||
|-----|-------|----|----|
|
||||
| `import.add`(新增) | 添加 | Add | 追加 |
|
||||
| `import.adding`(新增) | 校验并去重中... | Validating & deduping... | 検証・重複排除中... |
|
||||
| `import.idConflict`(新增) | id {0} 与已有规则重复,请修改 id | id {0} conflicts with an existing rule, change the id | id {0} が既存ルールと重複、id を変更してください |
|
||||
| `import.addDedupFallback`(新增) | AI 去重失败,已以无重复方式添加 | AI dedup failed, added as no-duplicate | AI 重複排除失敗、重複なしとして追加 |
|
||||
| `import.validationSeverityInvalid`(新增) | severity 非法 | Invalid severity | severity が不正です |
|
||||
| `import.cannotImport`(调整) | 需修复后点击添加 | Fix then click Add | 修正して「追加」をクリック |
|
||||
|
||||
---
|
||||
|
||||
## 四、文件变更清单
|
||||
|
||||
| 文件 | 操作 | 内容 |
|
||||
|------|------|------|
|
||||
| `src/rules/import-service.ts` | 修改 | 新增 `dedupSingleRule`(含 `DedupResult` 接口);`convertContentWithAI` 增加 `quiet?` 参数 |
|
||||
| `src/rules/import-preview.ts` | 修改 | 错误卡片完整表单 + 添加流程;`showImportPreview(result, context)`;`handleAddErrorRule`;前端 `addErrorRule`/`moveCardToSection`/`updateSectionCounts`/`doConfirm`;分区 `data-section` 与计数 id;确认按钮动态控制 |
|
||||
| `src/i18n/messages.ts` | 修改 | 新增 5 个 key,调整 `import.cannotImport` 三语文案 |
|
||||
| `src/rules/import-types.ts` | 不改 | — |
|
||||
| `src/rules/converters/template-converter.ts` | 不改 | 校验逻辑不动 |
|
||||
|
||||
## 五、验证
|
||||
|
||||
验证顺序 `lint → compile`(当前仓库无测试文件):
|
||||
|
||||
- `npm run lint`(ESLint `src/`)
|
||||
- `npm run compile`(tsc)
|
||||
- 手动验证(Extension Dev Host):
|
||||
1. 导出模板 → 填一行错误数据(如空 description、拼错 severity)→ 模板导入 → 预览中错误卡片可展开编辑
|
||||
2. 修复后点「添加」→ 卡片移入对应分区、计数更新、可切换保留/注释
|
||||
3. 不改任何字段再添加一条 → 确认导入 → 写盘 YAML 含已添加规则
|
||||
4. id 冲突 → 添加被拦截并提示
|
||||
5. 断网/无 Key → 重试后降级无重复分区,卡片有降级提示
|
||||
6. 未添加的错误规则 → 确认后自动丢弃
|
||||
@@ -0,0 +1,244 @@
|
||||
# 导入预览 · 错误规则字段级报红设计书
|
||||
|
||||
> 版本:v1.0
|
||||
> 日期:2026-07-31
|
||||
> 适用项目:vscode-code-reviewer
|
||||
> 参考文档:`2026-07-31-import-error-rule-edit-design.md`(错误规则可编辑 + 添加流程)
|
||||
|
||||
---
|
||||
|
||||
## 一、方案概览
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
导入预览中的错误规则卡片,报红从「整卡红边 + 半透明 + 顶部 issues 横幅」改为**字段级报红**:
|
||||
|
||||
- 整卡恢复普通样式
|
||||
- 仅出错的输入框加红框高亮,错误原因文字显示在该字段下方
|
||||
- 用户修复字段时**实时清除**该字段的红框与提示
|
||||
- 「添加」失败与 id 冲突同样精确定位到具体字段
|
||||
|
||||
### 1.2 现状 → 改造
|
||||
|
||||
```
|
||||
现状:
|
||||
┌─ 规则卡片(opacity:0.7 + 红边框)─────────────┐
|
||||
│ [需修复后点击添加] │
|
||||
│ ⚠ description 为空 │ ← 顶部 issues 横幅
|
||||
│ ⚠ message 为空 │
|
||||
│ [id] [severity] [description] [message] ... │ ← 全部无高亮
|
||||
└────────────────────────────────────────────────┘
|
||||
|
||||
改造后:
|
||||
┌─ 规则卡片(普通样式)─────────────────────────┐
|
||||
│ [需修复后点击添加] │
|
||||
│ [id] │
|
||||
│ [severity] │
|
||||
│ [description] ← 红框 │
|
||||
│ ⚠ description 为空 │ ← 字段下方提示
|
||||
│ [message] ← 红框 │
|
||||
│ ⚠ message 为空 │
|
||||
└────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.3 范围
|
||||
|
||||
| 类型 | 内容 |
|
||||
|------|------|
|
||||
| 含 | 初始错误字段红框 + 字段下方原因;添加失败/id 冲突定位到字段;实时清除;卡片样式还原 |
|
||||
| 不含 | 新增 languages/excludeLanguages 校验(维持现状);错误卡片以外的样式改动 |
|
||||
| 不触碰 | `import-service.ts`、`import-types.ts`、i18n 结构、扩展端去重流程 |
|
||||
|
||||
---
|
||||
|
||||
## 二、详细实现
|
||||
|
||||
改动文件**仅 `src/rules/import-preview.ts`**(前端渲染 + JS + 少量 CSS,扩展端消息协议兼容扩展)。
|
||||
|
||||
### 2.1 字段 → 表单元素映射
|
||||
|
||||
| field | 表单元素 |
|
||||
|-------|---------|
|
||||
| `id` | `.id-display-input` |
|
||||
| `severity` | `.edit-field select` |
|
||||
| `description` | `.edit-field textarea`(第 1 个) |
|
||||
| `message` | `.edit-field textarea`(第 2 个) |
|
||||
|
||||
### 2.2 `renderRuleCard` 错误分支改造
|
||||
|
||||
1. **卡片样式还原**:去掉 `opacity:0.7` 与红边框,`cardStyle` 仅保留错误徽标。
|
||||
2. **去掉顶部 issues 横幅**:删除 `issuesHtml` 变量及其渲染。
|
||||
3. **字段级渲染**:渲染各 `edit-field` 时,若 `validationIssues` 中存在对应 `field`,给该 `edit-field` 追加:
|
||||
|
||||
```html
|
||||
<div class="edit-field field-error">
|
||||
<label>description(规则描述)</label>
|
||||
<textarea rows="2" ...>...</textarea>
|
||||
<div class="field-error-msg">⚠ description 为空</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- `edit-field` 加 `field-error` 类;输入控件本身加 `field-error-input` 类
|
||||
- 提示文字 `<div class="field-error-msg">⚠ {message}</div>` 插在输入控件之后、`edit-field` 内部末尾
|
||||
|
||||
实现方式:渲染前构建 `const issueByField = new Map((rule.validationIssues||[]).map(i => [i.field, i]))`;渲染 severity/description/message 三个字段时按 map 命中追加。
|
||||
|
||||
### 2.3 `validateRule` 返回字段
|
||||
|
||||
```ts
|
||||
function validateRule(rule: ImportableRule): {
|
||||
field: 'id' | 'severity' | 'description' | 'message';
|
||||
message: string;
|
||||
} | null {
|
||||
if (!rule.id || !rule.id.trim()) {
|
||||
return { field: 'id', message: t('import.validationIdEmpty') };
|
||||
}
|
||||
if (!['error', 'warning', 'info'].includes(rule.severity)) {
|
||||
return { field: 'severity', message: t('import.validationSeverityInvalid') };
|
||||
}
|
||||
if (!rule.description || !rule.description.trim()) {
|
||||
return { field: 'description', message: t('import.validationDescEmpty', { 0: rule.id }) };
|
||||
}
|
||||
if (!rule.message || !rule.message.trim()) {
|
||||
return { field: 'message', message: t('import.validationMsgEmpty', { 0: rule.id }) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
`handleAddErrorRule` 中:
|
||||
- `validateRule` 失败 → `postMessage({ type:'addError', ruleId, field, message })`
|
||||
- id 冲突 → `postMessage({ type:'addError', ruleId, field:'id', message: t('import.idConflict', ...) })`
|
||||
|
||||
### 2.4 前端消息处理改造
|
||||
|
||||
`showCardError(ruleId, message)` → `showCardError(ruleId, field, message)`:
|
||||
|
||||
```js
|
||||
function setFieldError(card, field, message) {
|
||||
const el = fieldElement(card, field);
|
||||
if (!el) return;
|
||||
el.classList.add('field-error-input');
|
||||
const wrap = el.closest('.edit-field');
|
||||
if (!wrap) return;
|
||||
wrap.classList.add('field-error');
|
||||
let msg = wrap.querySelector('.field-error-msg');
|
||||
if (!msg) {
|
||||
msg = document.createElement('div');
|
||||
msg.className = 'field-error-msg';
|
||||
wrap.appendChild(msg);
|
||||
}
|
||||
msg.textContent = '⚠ ' + message;
|
||||
}
|
||||
|
||||
function clearFieldError(card, field) {
|
||||
const el = fieldElement(card, field);
|
||||
if (!el) return;
|
||||
el.classList.remove('field-error-input');
|
||||
const wrap = el.closest('.edit-field');
|
||||
if (wrap) {
|
||||
wrap.classList.remove('field-error');
|
||||
const msg = wrap.querySelector('.field-error-msg');
|
||||
if (msg) msg.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function fieldElement(card, field) {
|
||||
if (field === 'id') return card.querySelector('.id-display-input');
|
||||
if (field === 'severity') return card.querySelector('.edit-field select');
|
||||
const tas = card.querySelectorAll('.edit-field textarea');
|
||||
return field === 'description' ? (tas[0] || null) : (tas[1] || null);
|
||||
}
|
||||
```
|
||||
|
||||
`window.addEventListener('message')` 中 `addError` 分支改为透传 `field`。
|
||||
|
||||
### 2.5 实时清除(事件委托)
|
||||
|
||||
在 `document` 上委托监听 `input` 与 `change`:
|
||||
|
||||
```js
|
||||
function liveClear(event) {
|
||||
const card = event.target.closest('.rule-card');
|
||||
if (!card || !card.hasAttribute('data-error')) return;
|
||||
const target = event.target;
|
||||
if (target.classList.contains('id-display-input') || target.classList.contains('rule-id-input')) {
|
||||
if (target.value.trim()) clearFieldError(card, 'id');
|
||||
} else if (target.tagName === 'SELECT') {
|
||||
clearFieldError(card, 'severity');
|
||||
} else if (target.tagName === 'TEXTAREA') {
|
||||
const tas = card.querySelectorAll('.edit-field textarea');
|
||||
const field = tas[0] === target ? 'description' : (tas[1] === target ? 'message' : null);
|
||||
if (field && target.value.trim()) clearFieldError(card, field);
|
||||
}
|
||||
}
|
||||
document.addEventListener('input', liveClear);
|
||||
document.addEventListener('change', liveClear);
|
||||
```
|
||||
|
||||
> severity 下拉天然只会给出合法值,故 `change` 即清除;id/description/message 以非空 trim 判定。
|
||||
|
||||
### 2.6 `moveCardToSection` 清理
|
||||
|
||||
卡片移入有效分区后,清除该卡全部字段级错误:
|
||||
|
||||
```js
|
||||
function clearCardFieldErrors(card) {
|
||||
card.querySelectorAll('.field-error-input').forEach(el => {
|
||||
el.classList.remove('field-error-input');
|
||||
});
|
||||
card.querySelectorAll('.field-error').forEach(wrap => {
|
||||
wrap.classList.remove('field-error');
|
||||
const msg = wrap.querySelector('.field-error-msg');
|
||||
if (msg) msg.remove();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
在移除 `data-error` 后调用。
|
||||
|
||||
### 2.7 CSS
|
||||
|
||||
```css
|
||||
.field-error-input {
|
||||
border-color: rgba(248,81,73,0.7) !important;
|
||||
box-shadow: 0 0 0 1px rgba(248,81,73,0.25);
|
||||
}
|
||||
.field-error-msg {
|
||||
color: #f48771; font-size: 11px; margin-top: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
删除不再使用的 `.error-issues` 规则(或保留无引用,推荐删除)。
|
||||
|
||||
---
|
||||
|
||||
## 三、边界与影响
|
||||
|
||||
| 边界 | 处理 |
|
||||
|------|------|
|
||||
| 同一卡片多字段出错 | 每个字段独立红框 + 独立提示,互不影响 |
|
||||
| 字段修复后再点「添加」 | 校验通过即入区;实时清除逻辑保证先显示绿色状态 |
|
||||
| 添加失败(未修复) | 红框/提示重新命中对应字段 |
|
||||
| id 冲突 | `field:'id'` 定位到 id 输入框 |
|
||||
| severity 原始非法但默认值为合法 | 红框+提示展示原始问题,下拉 change 即清除 |
|
||||
| 确认导入校验(`validate()`) | 仍跳过 `data-error` 卡片,行为不变 |
|
||||
| 非模板导入路径 | 无 `validationIssues`,不受影响 |
|
||||
|
||||
## 四、文件变更清单
|
||||
|
||||
| 文件 | 操作 | 内容 |
|
||||
|------|------|------|
|
||||
| `src/rules/import-preview.ts` | 修改 | 卡片样式还原;字段级错误渲染;`validateRule` 返回字段;`addError` 消息带 field;`setFieldError`/`clearFieldError`/`clearCardFieldErrors`/`fieldElement`;input/change 委托实时清除;`moveCardToSection` 清理;CSS `.field-error-*` |
|
||||
|
||||
i18n、import-service、import-types 均不改动。
|
||||
|
||||
## 五、验证
|
||||
|
||||
- `npm run lint` + `npm run compile`
|
||||
- 复用既有 harness 思路,mock vscode 渲染 webview 脚本并校验语法
|
||||
- 扩展端消息流验证:
|
||||
- 初始错误卡片:description/message 空 → 对应文本域带 `field-error-input`,无整卡红边、无顶部横幅
|
||||
- 添加失败(description 空)→ `addError` 消息带 `field:'description'`
|
||||
- id 冲突 → `addError` 消息带 `field:'id'`
|
||||
- 模拟 input 事件 → 修复后红框清除
|
||||
@@ -0,0 +1,1000 @@
|
||||
# 方法级代码审查功能实施计划书
|
||||
|
||||
> 面向 AI 编码 agent 的技术实施文档。本文档包含完整的架构设计、文件清单、代码模板和验收标准,可直接据此编码。
|
||||
|
||||
## 项目上下文
|
||||
|
||||
- 仓库:`sdjndaq/2026-ai-b3`(Gitee 私有仓库)
|
||||
- 分支:`vscode-code-reviewer`
|
||||
- 项目名:**代码审查官 · Code Purifier**(`vscode-code-reviewer`)
|
||||
- 版本:1.2.0
|
||||
- 类型:VS Code 扩展插件(TypeScript)
|
||||
- 技术栈:TypeScript + VS Code Extension API + DeepSeek AI
|
||||
|
||||
## 功能目标
|
||||
|
||||
为现有代码审查插件新增**方法级触发**能力。用户在每个函数声明行上方看到 CodeLens 按钮,点击后对该方法执行**自定义规则审查 + AI 深度审查**。方法级审查不执行静态分析(IDE 已实时提供 linter 诊断),但加载自定义规则与 AI 协同工作:AI 先按自定义规则做匹配,再以 6 维度增强策略做深度审查,单次调用同时产出两类结果。
|
||||
|
||||
### 核心设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 审查流程 | 自定义规则 + AI(不走静态分析) | IDE 已实时提供 linter 诊断,静态分析冗余;自定义规则保留是因为团队规则仍需在方法级生效 |
|
||||
| AI 调用模式 | 单次调用合并规则匹配 + 深度审查 | 方法代码短,token 预算充足,合并为单次调用减少延迟 |
|
||||
| 触发方式 | CodeLens 行内按钮 | 最直观的交互,用户无需记忆命令或快捷键 |
|
||||
| 方法提取 | VS Code Symbol API | 原生支持,覆盖 TS/JS/Java/Python 等主流语言 |
|
||||
| 调用链 | 同文件粗匹配 | 首版限制在同文件内,控制实现复杂度 |
|
||||
| AI 审查范围 | 6 维度增强 | AI 同时承担规则匹配和深度审查,维度覆盖比全文件更广 |
|
||||
|
||||
## 现有架构摘要
|
||||
|
||||
编码前必须理解以下现有代码结构,所有新代码需与这些模式保持一致。
|
||||
|
||||
### 入口与注册
|
||||
|
||||
`src/extension.ts` 的 `activate(context)` 函数完成三件事:
|
||||
|
||||
1. 创建 `Orchestrator` 实例
|
||||
2. 注册 `SetupViewProvider`(Webview View)
|
||||
3. 调用 `registerCommands(context, orchestrator)` 注册所有命令
|
||||
4. 监听 `onDidSaveTextDocument` 执行防抖静态分析
|
||||
5. 监听 `onDidChangeConfiguration` 切换 i18n 语言
|
||||
|
||||
### 命令注册
|
||||
|
||||
`src/activation/commands.ts` 的 `registerCommands(context, orchestrator)` 注册命令。现有命令通过 `vscode.window.withProgress` 包裹执行,使用 `t()` 函数做 i18n。核心命令 `codeReviewer.review` 的执行流程:
|
||||
|
||||
```
|
||||
获取 document + workingDir
|
||||
→ withProgress:
|
||||
→ orchestrator.runStaticAnalysis(document, workingDir)
|
||||
→ loadActiveRules(workspaceRoot) + filterAndSummarize
|
||||
→ runAIReview(context, code, staticDiagnostics, relevantRules)
|
||||
→ mergeResults(...)
|
||||
→ ReviewPanel.createOrShow + panel.update(currentReport)
|
||||
```
|
||||
|
||||
### AI 引擎
|
||||
|
||||
`src/ai/engine.ts` 导出 `runAIReview(context, code, staticDiagnostics, customRules)` 函数。内部流程:
|
||||
|
||||
1. `getApiKey(context)` 获取 API Key
|
||||
2. `createProvider(providerId, apiKey, baseUrl, extensionUri)` 创建 AI Provider
|
||||
3. 并行发起两个请求:
|
||||
- `requestA`:自定义规则匹配(`buildCustomRuleSystemPrompt` + `buildUserPromptCustomRules`)
|
||||
- `requestB`:深度审查(`buildDeepReviewSystemPrompt` + `buildUserPromptDeepReview`)
|
||||
4. `parseJsonResponse` 解析 JSON
|
||||
5. 返回 `AIEngineResult`
|
||||
|
||||
现有 `buildDeepReviewSystemPrompt()` 的定位是"补充静态分析的盲区",任务包含翻译静态诊断和发现额外问题。方法级审查不复用这个 prompt。
|
||||
|
||||
### 类型定义
|
||||
|
||||
`src/types.ts` 定义核心类型:
|
||||
|
||||
```typescript
|
||||
interface CustomRule {
|
||||
id: string;
|
||||
severity: Severity;
|
||||
description: string;
|
||||
message: string;
|
||||
languages?: string[];
|
||||
excludeLanguages?: string[];
|
||||
}
|
||||
|
||||
interface LinterDiagnostic {
|
||||
severity: Severity;
|
||||
ruleId: string;
|
||||
message: string;
|
||||
range: vscode.Range;
|
||||
suggestion?: string;
|
||||
}
|
||||
|
||||
interface LinterAdapter {
|
||||
id: string;
|
||||
supportedLanguages: string[];
|
||||
check(document: vscode.TextDocument, workingDir: string): Promise<AdapterResult>;
|
||||
isAvailable(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
`src/ai/schema.ts` 定义 AI 输出类型:
|
||||
|
||||
```typescript
|
||||
interface AIFinding {
|
||||
ruleId: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
category: 'bug' | 'performance' | 'security' | 'style' | 'design';
|
||||
title: string;
|
||||
description: string;
|
||||
suggestion: string;
|
||||
codeDiff?: string;
|
||||
line: number;
|
||||
}
|
||||
|
||||
interface AIEngineResult {
|
||||
customRuleResults: CustomRuleResult[];
|
||||
translatedDiagnostics: TranslatedDiagnostic[];
|
||||
findings: AIFinding[];
|
||||
degraded: boolean;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 结果合并与展示
|
||||
|
||||
`src/merger/merger.ts` 的 `mergeResults(...)` 将静态诊断、AI 发现等聚合成 `MergedReport`。`src/panel/webview.ts` 的 `ReviewPanel` 类负责 Webview 面板的创建和更新。
|
||||
|
||||
### i18n
|
||||
|
||||
`src/i18n/messages.ts` 导出 `t(key, params?)` 和 `setLanguage(lang)` 函数。所有用户可见文案必须通过 `t()` 获取。
|
||||
|
||||
## 改动总览
|
||||
|
||||
### 新增文件(3 个)
|
||||
|
||||
| 文件路径 | 职责 |
|
||||
|----------|------|
|
||||
| `src/scope/method-extractor.ts` | 从文档中提取方法符号、代码、签名、调用链 |
|
||||
| `src/views/codeLensProvider.ts` | CodeLens Provider,在函数声明上方渲染审查按钮 |
|
||||
| `src/scope/status-cache.ts` | 方法审查状态缓存,驱动 CodeLens 按钮文案刷新 |
|
||||
|
||||
### 修改文件(4 个 + package.json)
|
||||
|
||||
| 文件路径 | 改动内容 |
|
||||
|----------|----------|
|
||||
| `src/activation/commands.ts` | 新增 `codeReviewer.reviewMethod` 命令 |
|
||||
| `src/ai/engine.ts` | 新增 `runMethodReview` 函数 + 方法级 prompt builder |
|
||||
| `src/ai/schema.ts` | 新增 `MethodFinding` 类型 + 扩展 category |
|
||||
| `src/extension.ts` | 注册 CodeLensProvider |
|
||||
| `package.json` | 新增 command、codelens 配置项 |
|
||||
|
||||
### 不修改的文件
|
||||
|
||||
`src/orchestrator/orchestrator.ts`、`src/merger/merger.ts`、`src/panel/webview.ts`、`src/ai/providers/` 目录、`src/i18n/` 目录均不需要修改。方法级审查的执行路径不经过 Orchestrator,直接从命令层调用 engine.ts。自定义规则的加载复用现有 `src/rules/rule-loader.ts` 的 `loadActiveRules` 函数,过滤复用 `src/rules/rule-filter.ts` 的 `filterForDocument` 函数,均无需修改。
|
||||
|
||||
---
|
||||
|
||||
## 新增文件详细设计
|
||||
|
||||
### 1. `src/scope/method-extractor.ts`
|
||||
|
||||
#### 职责
|
||||
|
||||
从 VS Code 文档中提取方法级别的信息,包括方法代码、签名和同文件内的调用链。
|
||||
|
||||
#### 导出类型
|
||||
|
||||
```typescript
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface MethodScope {
|
||||
/** 方法名称 */
|
||||
name: string;
|
||||
/** 方法在文档中的完整范围 */
|
||||
range: vscode.Range;
|
||||
/** 方法代码文本(带行号) */
|
||||
code: string;
|
||||
/** 方法签名(一行摘要,如 "processOrder(order: Order): Promise<Result>") */
|
||||
signature: string;
|
||||
/** 调用者方法名列表(同文件内引用了本方法的其他方法) */
|
||||
callers: string[];
|
||||
/** 被调用方法名列表(本方法体内调用的其他方法) */
|
||||
callees: string[];
|
||||
/** 方法在业务流中的角色描述(由符号层级推断) */
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface MethodSymbol {
|
||||
/** 方法名称 */
|
||||
name: string;
|
||||
/** 方法范围 */
|
||||
range: vscode.Range;
|
||||
/** 方法所在类/模块名(如果有) */
|
||||
containerName?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 导出函数
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 获取文档中所有方法/函数符号
|
||||
* 使用 VS Code Symbol API,无 Provider 时回退到正则匹配
|
||||
*/
|
||||
export async function getMethodSymbols(
|
||||
document: vscode.TextDocument
|
||||
): Promise<MethodSymbol[]>;
|
||||
|
||||
/**
|
||||
* 提取指定位置所在方法的完整 MethodScope
|
||||
* @param document 文档
|
||||
* @param position 光标位置或 Symbol 范围
|
||||
*/
|
||||
export async function extractMethodScope(
|
||||
document: vscode.TextDocument,
|
||||
range: vscode.Range
|
||||
): Promise<MethodScope | null>;
|
||||
```
|
||||
|
||||
#### 实现要点
|
||||
|
||||
**Symbol API 获取符号树:**
|
||||
|
||||
```typescript
|
||||
const symbols = await vscode.commands.executeCommand<vscode.DocumentSymbol[]>(
|
||||
'vscode.executeDocumentSymbolProvider',
|
||||
document.uri
|
||||
);
|
||||
```
|
||||
|
||||
过滤 `SymbolKind.Function`、`SymbolKind.Method`、`SymbolKind.Constructor` 类型的节点。递归遍历子符号(类内的方法)。
|
||||
|
||||
**正则回退(无 Symbol Provider 时):**
|
||||
|
||||
针对 JavaScript/TypeScript 使用正则 `/(?:async\s+)?function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(/g` 匹配函数声明。针对 Java 使用 `/((?:public|private|protected|static)\s+)*\w+(?:<[^>]+>)?\s+(\w+)\s*\(/g`。
|
||||
|
||||
**调用链粗匹配:**
|
||||
|
||||
获取同文件内所有方法符号后,对每个方法:
|
||||
- **被调用者**:在本方法代码文本中搜索其他方法名是否出现(正则 `\b方法名\s*\(`)
|
||||
- **调用者**:遍历其他所有方法的代码,检查本方法名是否出现
|
||||
|
||||
**签名提取:**
|
||||
|
||||
取方法代码的第一行(到 `{` 或 `)` 结束),去除注释和多余空白。
|
||||
|
||||
**role 字段推断:**
|
||||
|
||||
根据 `containerName` 和方法名粗略推断,例如:
|
||||
- `containerName` 包含 "Controller" → "HTTP 请求处理入口"
|
||||
- `containerName` 包含 "Service" → "业务逻辑处理"
|
||||
- 方法名以 "get/set/is" 开头 → "属性访问器"
|
||||
- 默认 → "通用方法"
|
||||
|
||||
### 2. `src/views/codeLensProvider.ts`
|
||||
|
||||
#### 职责
|
||||
|
||||
实现 `vscode.CodeLensProvider`,在每个函数声明行上方渲染审查按钮。
|
||||
|
||||
#### 类定义
|
||||
|
||||
```typescript
|
||||
import * as vscode from 'vscode';
|
||||
import { getMethodSymbols } from '../scope/method-extractor';
|
||||
import { ReviewStatusCache } from '../scope/status-cache';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
export class MethodCodeLensProvider implements vscode.CodeLensProvider {
|
||||
private _onDidChangeCodeLenses: vscode.EventEmitter<void> =
|
||||
new vscode.EventEmitter<void>();
|
||||
readonly onDidChangeCodeLenses: vscode.Event<void> =
|
||||
this._onDidChangeCodeLenses.event;
|
||||
|
||||
constructor(private statusCache: ReviewStatusCache) {}
|
||||
|
||||
/** 触发 CodeLens 刷新 */
|
||||
refresh(): void {
|
||||
this._onDidChangeCodeLenses.fire();
|
||||
}
|
||||
|
||||
async provideCodeLenses(
|
||||
document: vscode.TextDocument,
|
||||
token: vscode.CancellationToken
|
||||
): Promise<vscode.CodeLens[]> {
|
||||
// 1. 检查配置是否启用
|
||||
const config = vscode.workspace.getConfiguration('vscode-code-reviewer');
|
||||
const enabled = config.get<boolean>('codelens.enabled', true);
|
||||
if (!enabled) return [];
|
||||
|
||||
// 2. 检查语言是否在允许列表内
|
||||
const languages = config.get<string[]>('codelens.languages', [
|
||||
'typescript', 'javascript', 'java', 'python'
|
||||
]);
|
||||
if (!languages.includes(document.languageId)) return [];
|
||||
|
||||
// 3. 获取方法符号
|
||||
const symbols = await getMethodSymbols(document);
|
||||
if (symbols.length === 0) return [];
|
||||
|
||||
// 4. 方法数量上限保护
|
||||
if (symbols.length > 50) return [];
|
||||
|
||||
// 5. 为每个方法生成 CodeLens
|
||||
const lenses: vscode.CodeLens[] = [];
|
||||
for (const symbol of symbols) {
|
||||
const status = this.statusCache.get(document.uri, symbol.name);
|
||||
const title = this.buildLensTitle(status);
|
||||
lenses.push(new vscode.CodeLens(symbol.range.start, {
|
||||
command: 'codeReviewer.reviewMethod',
|
||||
title: title,
|
||||
arguments: [symbol.range],
|
||||
}));
|
||||
}
|
||||
return lenses;
|
||||
}
|
||||
|
||||
private buildLensTitle(status: ReviewStatus | null): string {
|
||||
if (!status) {
|
||||
return t('codelens.reviewMethod');
|
||||
}
|
||||
if (status.issueCount === 0) {
|
||||
return t('codelens.reviewedClean');
|
||||
}
|
||||
return t('codelens.reviewedWithIssues', { 0: String(status.issueCount) });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### i18n key 约定
|
||||
|
||||
需要在 `src/i18n/messages.ts` 中新增以下 key(中英文):
|
||||
|
||||
| key | 中文 | English |
|
||||
|-----|------|---------|
|
||||
| `codelens.reviewMethod` | `🔍 Code Purifier: 审查此方法` | `🔍 Code Purifier: Review This Method` |
|
||||
| `codelens.reviewedClean` | `✓ Code Purifier: 已审查(无问题)` | `✓ Code Purifier: Reviewed (No Issues)` |
|
||||
| `codelens.reviewedWithIssues` | `✓ Code Purifier: 已审查({0} 个问题)` | `✓ Code Purifier: Reviewed ({0} Issues)` |
|
||||
| `methodReview.running` | `Code Purifier 正在审查方法:{0}` | `Code Purifier: Reviewing method: {0}` |
|
||||
| `methodReview.noMethod` | `当前位置未检测到方法` | `No method detected at current position` |
|
||||
| `methodReview.complete` | `方法审查完成,发现 {0} 个问题` | `Method review complete, {0} issues found` |
|
||||
|
||||
### 3. `src/scope/status-cache.ts`
|
||||
|
||||
#### 职责
|
||||
|
||||
内存缓存,记录每个方法最近一次的审查结果,驱动 CodeLens 按钮文案刷新。
|
||||
|
||||
#### 类定义
|
||||
|
||||
```typescript
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export interface ReviewStatus {
|
||||
/** 发现的问题数量 */
|
||||
issueCount: number;
|
||||
/** 审查时间戳 */
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export class ReviewStatusCache {
|
||||
/** key 格式:documentUri.toString() + '::' + methodName */
|
||||
private cache = new Map<string, ReviewStatus>();
|
||||
|
||||
get(uri: vscode.Uri, methodName: string): ReviewStatus | null {
|
||||
const key = this.buildKey(uri, methodName);
|
||||
return this.cache.get(key) ?? null;
|
||||
}
|
||||
|
||||
set(uri: vscode.Uri, methodName: string, issueCount: number): void {
|
||||
const key = this.buildKey(uri, methodName);
|
||||
this.cache.set(key, {
|
||||
issueCount,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/** 清除指定文档的所有缓存(文档关闭时调用) */
|
||||
clearDocument(uri: vscode.Uri): void {
|
||||
const prefix = uri.toString() + '::';
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildKey(uri: vscode.Uri, methodName: string): string {
|
||||
return uri.toString() + '::' + methodName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 修改文件详细设计
|
||||
|
||||
### 4. `src/ai/schema.ts`
|
||||
|
||||
在现有类型基础上新增方法级审查的类型定义,不修改现有类型。
|
||||
|
||||
#### 新增内容
|
||||
|
||||
```typescript
|
||||
/** 方法级审查的 category 类型(6 维度) */
|
||||
export type MethodFindingCategory =
|
||||
| 'correctness' // 正确性:分支覆盖、边界条件、异常路径
|
||||
| 'security' // 安全性:输入校验、注入风险、权限检查
|
||||
| 'design' // 设计:职责单一、参数合理性、调用链适配
|
||||
| 'convention' // 规范:命名、复杂度、魔法数字、注释
|
||||
| 'performance' // 性能:时间/空间复杂度、资源泄漏
|
||||
| 'testability'; // 可测试性:副作用隔离、依赖可 Mock 性
|
||||
|
||||
/** 方法级审查的 finding,扩展 AIFinding */
|
||||
export interface MethodFinding {
|
||||
ruleId: string;
|
||||
severity: 'error' | 'warning' | 'info';
|
||||
category: MethodFindingCategory;
|
||||
title: string;
|
||||
description: string;
|
||||
suggestion: string;
|
||||
codeDiff?: string;
|
||||
line: number;
|
||||
/** 触发路径描述,如 "if(order == null) → NPE on .getId()" */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/** 方法级审查的 AI 返回结构 */
|
||||
export interface MethodReviewResult {
|
||||
/** 自定义规则匹配结果(AI 按规则做语义判断) */
|
||||
customRuleResults: CustomRuleResult[];
|
||||
/** AI 深度审查发现 */
|
||||
findings: MethodFinding[];
|
||||
degraded: boolean;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. `src/ai/engine.ts`
|
||||
|
||||
新增方法级审查的入口函数和专用 prompt builder。不修改现有 `runAIReview` 及其相关函数。
|
||||
|
||||
#### 新增函数
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 方法级 AI 审查
|
||||
* 不依赖静态诊断,但加载自定义规则与 AI 协同工作
|
||||
* 单次 AI 调用同时完成规则匹配和 6 维度深度审查
|
||||
*/
|
||||
export async function runMethodReview(
|
||||
context: vscode.ExtensionContext,
|
||||
scope: MethodScope,
|
||||
customRules: CustomRule[],
|
||||
): Promise<MethodReviewResult>;
|
||||
```
|
||||
|
||||
#### 内部实现流程
|
||||
|
||||
```typescript
|
||||
export async function runMethodReview(
|
||||
context: vscode.ExtensionContext,
|
||||
scope: MethodScope,
|
||||
customRules: CustomRule[],
|
||||
): Promise<MethodReviewResult> {
|
||||
// 1. 获取 API Key
|
||||
const apiKey = await getApiKey(context);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
customRuleResults: [],
|
||||
findings: [],
|
||||
degraded: true,
|
||||
error: t('adapter.noApiKey'),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 创建 Provider(复用现有工厂)
|
||||
const providerId = getAIProvider();
|
||||
const baseUrl = getAIBaseUrl();
|
||||
let provider: AIProvider;
|
||||
try {
|
||||
provider = createProvider(providerId, apiKey, baseUrl, context.extensionUri);
|
||||
} catch (err) {
|
||||
return {
|
||||
customRuleResults: [],
|
||||
findings: [],
|
||||
degraded: true,
|
||||
error: t('adapter.createProviderFail', { 0: err instanceof Error ? err.message : String(err) }),
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 构建请求
|
||||
const options = {
|
||||
model: getAIModel(),
|
||||
temperature: getAITemperature(),
|
||||
maxTokens: getAIMaxTokens(),
|
||||
timeoutMs: getAITimeout() * 1000,
|
||||
};
|
||||
|
||||
const numberedCode = addLineNumbers(scope.code);
|
||||
|
||||
// 4. 单次 AI 调用(规则匹配 + 深度审查合并为一次)
|
||||
// 有规则时 prompt 包含规则匹配任务,无规则时只做深度审查
|
||||
const hasRules = customRules.length > 0;
|
||||
const result = await provider.chat(
|
||||
buildMethodReviewSystemPrompt(hasRules),
|
||||
buildMethodUserPrompt(scope, numberedCode, customRules),
|
||||
options,
|
||||
);
|
||||
|
||||
// 5. 解析结果
|
||||
const errors: string[] = [];
|
||||
let customRuleResults: CustomRuleResult[] = [];
|
||||
let findings: MethodFinding[] = [];
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
try {
|
||||
const parsed = parseJsonResponse(result.value) as {
|
||||
customRuleResults?: CustomRuleResult[];
|
||||
findings?: MethodFinding[];
|
||||
};
|
||||
customRuleResults = (parsed.customRuleResults ?? []).map(r => ({
|
||||
...r,
|
||||
ruleId: r.ruleId.startsWith('custom:') ? r.ruleId : `custom:${r.ruleId}`,
|
||||
}));
|
||||
findings = (parsed.findings ?? []).map(f => ({
|
||||
...f,
|
||||
ruleId: f.ruleId.startsWith('method:') ? f.ruleId : `method:${f.ruleId}`,
|
||||
}));
|
||||
} catch (e) {
|
||||
errors.push(t('adapter.aiReviewParseFail', { 0: e instanceof Error ? e.message : String(e) }));
|
||||
}
|
||||
} else {
|
||||
errors.push(t('adapter.aiReviewRequestFail', { 0: result.reason }));
|
||||
}
|
||||
|
||||
return {
|
||||
customRuleResults,
|
||||
findings,
|
||||
degraded: errors.length > 0,
|
||||
error: errors.join('; ') || undefined,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增 prompt builder
|
||||
|
||||
```typescript
|
||||
function buildMethodReviewSystemPrompt(hasRules: boolean): string {
|
||||
const lang = getLanguage();
|
||||
if (lang === 'en') {
|
||||
const ruleSection = hasRules
|
||||
? `## Task 1: Custom Rule Matching
|
||||
Evaluate whether the method violates any of the provided custom rules.
|
||||
Understand semantics, not text matching.
|
||||
Report violations in "customRuleResults".\n\n`
|
||||
: '';
|
||||
const ruleOutput = hasRules
|
||||
? ` "customRuleResults": [
|
||||
{
|
||||
"ruleId": "original rule id",
|
||||
"line": line_number,
|
||||
"severity": "error|warning|info",
|
||||
"message": "violation description"
|
||||
}
|
||||
],\n`
|
||||
: '';
|
||||
return `You are a senior code review expert reviewing a single method.
|
||||
There is no static analysis before you — you handle rule matching AND deep review.
|
||||
|
||||
${ruleSection}## Review Strategy: Path Enumeration
|
||||
- Walk through every if/else/switch branch, note coverage and gaps
|
||||
- Enumerate boundary values for every parameter (null, empty collection, extreme values, wrong types)
|
||||
- Check every throw/catch path for proper fallback strategy
|
||||
- Trace the method's role in its call chain
|
||||
|
||||
## Required Dimensions (do not skip any)
|
||||
A. Correctness: branch coverage, boundary conditions, exception path completeness
|
||||
B. Security: input validation, injection risk, permission check, sensitive data leakage
|
||||
C. Design: single responsibility, parameter design, return value contract, call chain adaptation
|
||||
D. Convention: naming, cyclomatic complexity, magic numbers, missing comments
|
||||
E. Performance: time/space complexity, resource leaks, unnecessary computation
|
||||
F. Testability: side effect isolation, dependency mockability, deterministic output
|
||||
|
||||
## Call Chain Analysis
|
||||
- Check whether callers' arguments match this method's expectations
|
||||
- Check whether this method's return value is correctly handled by callers
|
||||
- Check whether exceptions are caught or declared by callers
|
||||
|
||||
Output JSON only. Double quotes in strings must be escaped with \\".
|
||||
Format:
|
||||
{
|
||||
${ruleOutput} "findings": [
|
||||
{
|
||||
"ruleId": "method-boundary-null",
|
||||
"severity": "error|warning|info",
|
||||
"category": "correctness|security|design|convention|performance|testability",
|
||||
"title": "issue title",
|
||||
"description": "detailed description",
|
||||
"suggestion": "fix suggestion",
|
||||
"codeDiff": "optional fix diff",
|
||||
"line": line_number,
|
||||
"path": "trigger path description, e.g. if(order==null) -> NPE on .getId()"
|
||||
}
|
||||
]
|
||||
}
|
||||
If no issues found, return empty arrays.
|
||||
|
||||
Output language: en`;
|
||||
}
|
||||
// 中文 prompt(默认)
|
||||
const ruleSection = hasRules
|
||||
? `## 任务一:自定义规则匹配
|
||||
评估方法是否违反了提供的自定义规则。
|
||||
理解语义,而非文本匹配。
|
||||
在 "customRuleResults" 中报告违规。\n\n`
|
||||
: '';
|
||||
const ruleOutput = hasRules
|
||||
? ` "customRuleResults": [
|
||||
{
|
||||
"ruleId": "原始规则 ID",
|
||||
"line": 行号,
|
||||
"severity": "error|warning|info",
|
||||
"message": "违规描述"
|
||||
}
|
||||
],\n`
|
||||
: '';
|
||||
return `你是资深代码审查专家,正在审查单个方法。
|
||||
没有静态分析的前置过滤——你同时负责规则匹配和深度审查。
|
||||
|
||||
${ruleSection}## 审查策略:逐路径枚举
|
||||
- 遍历每个 if/else/switch 分支,标注覆盖与遗漏
|
||||
- 枚举每个入参的边界值(null、空集合、极值、错误类型)
|
||||
- 检查每个 throw/catch 路径的降级策略
|
||||
- 追踪方法在调用链中的角色
|
||||
|
||||
## 必须覆盖的维度(不可跳过)
|
||||
A. 正确性:分支覆盖、边界条件、异常路径完整性
|
||||
B. 安全性:输入校验、注入风险、权限检查、敏感信息泄露
|
||||
C. 设计:职责单一性、参数设计合理性、返回值契约、调用链适配
|
||||
D. 规范:命名、圈复杂度、魔法数字、注释缺失
|
||||
E. 性能:时间/空间复杂度、资源泄漏、不必要的计算
|
||||
F. 可测试性:副作用隔离、依赖可 Mock 性、确定性输出
|
||||
|
||||
## 调用链分析
|
||||
- 检查调用者传入的参数是否符合本方法预期
|
||||
- 检查本方法的返回值是否被调用者正确处理
|
||||
- 检查异常是否被调用者捕获或声明
|
||||
|
||||
输出 JSON,字符串中的双引号必须用 \\" 转义。
|
||||
格式:
|
||||
{
|
||||
${ruleOutput} "findings": [
|
||||
{
|
||||
"ruleId": "method-boundary-null",
|
||||
"severity": "error|warning|info",
|
||||
"category": "correctness|security|design|convention|performance|testability",
|
||||
"title": "问题标题",
|
||||
"description": "详细描述",
|
||||
"suggestion": "修复建议",
|
||||
"codeDiff": "可选的修复 diff",
|
||||
"line": 行号,
|
||||
"path": "触发路径描述,如 if(order==null) -> NPE on .getId()"
|
||||
}
|
||||
]
|
||||
}
|
||||
如果未发现问题,返回空数组。
|
||||
|
||||
输出语言:zh-CN`;
|
||||
}
|
||||
|
||||
function buildMethodUserPrompt(
|
||||
scope: MethodScope,
|
||||
numberedCode: string,
|
||||
customRules: CustomRule[],
|
||||
): string {
|
||||
const lang = getLanguage();
|
||||
const codeLabel = lang === 'en' ? 'Method Code (with line numbers)' : '方法代码(带行号)';
|
||||
const sigLabel = lang === 'en' ? 'Method Signature' : '方法签名';
|
||||
const chainLabel = lang === 'en' ? 'Call Chain Context' : '调用链上下文';
|
||||
const roleLabel = lang === 'en' ? 'Role in Business Flow' : '业务流中的角色';
|
||||
const callersLabel = lang === 'en' ? 'Callers' : '调用者';
|
||||
const calleesLabel = lang === 'en' ? 'Callees' : '被调用者';
|
||||
const noneLabel = lang === 'en' ? '(none)' : '(无)';
|
||||
|
||||
// 自定义规则块:有规则时注入,无规则时省略
|
||||
let ruleBlock = '';
|
||||
if (customRules.length > 0) {
|
||||
const ruleLabel = lang === 'en' ? 'Custom Rules to Match' : '需匹配的自定义规则';
|
||||
const ruleLines = customRules
|
||||
.map((r, i) => `${i + 1}. [${r.id}] (${r.severity}) ${r.description}\n ${r.message}`)
|
||||
.join('\n');
|
||||
ruleBlock = `\n## ${ruleLabel}\n${ruleLines}\n`;
|
||||
}
|
||||
|
||||
return `## ${sigLabel}
|
||||
${scope.signature}
|
||||
|
||||
## ${codeLabel}
|
||||
${numberedCode}
|
||||
${ruleBlock}
|
||||
## ${chainLabel}
|
||||
${roleLabel}: ${scope.role}
|
||||
${callersLabel}: ${scope.callers.length > 0 ? scope.callers.join(', ') : noneLabel}
|
||||
${calleesLabel}: ${scope.callees.length > 0 ? scope.callees.join(', ') : noneLabel}`;
|
||||
}
|
||||
```
|
||||
|
||||
#### 与现有 `runAIReview` 的关键差异
|
||||
|
||||
| 对比项 | 现有 `runAIReview` | 新增 `runMethodReview` |
|
||||
|--------|--------------------|-----------------------|
|
||||
| 输入参数 | `code, staticDiagnostics, customRules` | `scope: MethodScope, customRules` |
|
||||
| AI 调用次数 | 2 次(requestA + requestB 并行) | 1 次(规则匹配 + 深度审查合并) |
|
||||
| prompt 定位 | 补充静态分析盲区 | 唯一审查者,全覆盖 |
|
||||
| 输出结构 | `customRuleResults + translatedDiagnostics + findings` | `customRuleResults + findings` |
|
||||
| category | 5 类 | 6 类(新增 correctness、testability) |
|
||||
| path 字段 | 无 | 有(触发路径描述) |
|
||||
| 规则匹配方式 | 独立 prompt + 独立请求 | 与深度审查合并为单次 prompt |
|
||||
|
||||
### 6. `src/activation/commands.ts`
|
||||
|
||||
新增 `codeReviewer.reviewMethod` 命令。不修改现有命令。
|
||||
|
||||
#### 新增命令注册
|
||||
|
||||
在 `registerCommands` 函数内追加:
|
||||
|
||||
```typescript
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
'codeReviewer.reviewMethod',
|
||||
async (symbolRange: vscode.Range) => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor) return;
|
||||
|
||||
const document = editor.document;
|
||||
const workspaceRoot = vscode.workspace.getWorkspaceFolder(document.uri)?.uri.fsPath;
|
||||
|
||||
// 1. 提取方法
|
||||
const scope = await extractMethodScope(document, symbolRange);
|
||||
if (!scope) {
|
||||
vscode.window.showWarningMessage(t('methodReview.noMethod'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 加载并过滤自定义规则(复用现有 rule-filter)
|
||||
let customRules: CustomRule[] = [];
|
||||
if (workspaceRoot) {
|
||||
const allRules = await loadActiveRules(workspaceRoot);
|
||||
customRules = filterForDocument(allRules, document);
|
||||
}
|
||||
|
||||
// 3. 执行 AI 审查(传入自定义规则)
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: t('methodReview.running', { 0: scope.name }),
|
||||
cancellable: false,
|
||||
},
|
||||
async () => {
|
||||
const result = await runMethodReview(context, scope, customRules);
|
||||
|
||||
// 4. 构造 MergedReport(复用现有 merger)
|
||||
// customRuleResults 和 findings 均来自 AI 单次调用
|
||||
const totalIssues = result.customRuleResults.length + result.findings.length;
|
||||
currentReport = mergeResults({
|
||||
staticDiagnostics: [],
|
||||
customRuleResults: result.customRuleResults,
|
||||
translatedDiagnostics: [],
|
||||
aiFindings: result.findings, // MethodFinding[] 兼容 AIFinding[]
|
||||
errors: result.error ? [result.error] : [],
|
||||
degraded: result.degraded,
|
||||
startTime: Date.now(),
|
||||
filePath: document.uri.fsPath,
|
||||
language: document.languageId,
|
||||
adapterIds: [],
|
||||
customRuleFilterInfo: undefined,
|
||||
});
|
||||
|
||||
// 5. 更新状态缓存并刷新 CodeLens
|
||||
statusCache.set(document.uri, scope.name, totalIssues);
|
||||
codeLensProvider.refresh();
|
||||
|
||||
// 6. 面板展示
|
||||
const panel = ReviewPanel.createOrShow(context.extensionUri);
|
||||
panel.update(currentReport);
|
||||
|
||||
// 7. 提示
|
||||
vscode.window.showInformationMessage(
|
||||
t('methodReview.complete', { 0: String(totalIssues) })
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
#### 函数签名变更
|
||||
|
||||
`registerCommands` 需要新增两个参数:
|
||||
|
||||
```typescript
|
||||
export function registerCommands(
|
||||
context: vscode.ExtensionContext,
|
||||
orchestrator: Orchestrator,
|
||||
codeLensProvider: MethodCodeLensProvider, // 新增
|
||||
statusCache: ReviewStatusCache, // 新增
|
||||
): void {
|
||||
```
|
||||
|
||||
#### 新增 import
|
||||
|
||||
`commands.ts` 顶部需追加以下 import(现有 import 不变):
|
||||
|
||||
```typescript
|
||||
import { loadActiveRules } from '../rules/rule-loader';
|
||||
import { filterForDocument } from '../rules/rule-filter';
|
||||
import { CustomRule } from '../types';
|
||||
```
|
||||
|
||||
### 7. `src/extension.ts`
|
||||
|
||||
注册 CodeLensProvider 和 StatusCache。
|
||||
|
||||
#### 修改内容
|
||||
|
||||
在 `activate` 函数中,`registerCommands` 调用之前新增:
|
||||
|
||||
```typescript
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const lang = getAIOutputLanguage() as Language;
|
||||
setLanguage(lang);
|
||||
console.log(t('extension.activated'));
|
||||
|
||||
orchestrator = new Orchestrator();
|
||||
|
||||
const setupProvider = new SetupViewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider('codeReviewer.setupView', setupProvider)
|
||||
);
|
||||
|
||||
// ===== 新增:方法级审查基础设施 =====
|
||||
const statusCache = new ReviewStatusCache();
|
||||
const codeLensProvider = new MethodCodeLensProvider(statusCache);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCodeLensProvider(
|
||||
{ scheme: 'file' },
|
||||
codeLensProvider
|
||||
)
|
||||
);
|
||||
|
||||
// 文档关闭时清理状态缓存
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidCloseTextDocument((document) => {
|
||||
statusCache.clearDocument(document.uri);
|
||||
})
|
||||
);
|
||||
// ===== 新增结束 =====
|
||||
|
||||
registerCommands(context, orchestrator, codeLensProvider, statusCache);
|
||||
|
||||
// ... 保留现有的 onDidSaveTextDocument 和 onDidChangeConfiguration 逻辑不变 ...
|
||||
}
|
||||
```
|
||||
|
||||
### 8. `package.json`
|
||||
|
||||
#### 新增 command
|
||||
|
||||
在 `contributes.commands` 数组中追加:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "codeReviewer.reviewMethod",
|
||||
"title": "Code Purifier: 审查此方法"
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增 configuration
|
||||
|
||||
在 `contributes.configuration.properties` 中追加:
|
||||
|
||||
```json
|
||||
"vscode-code-reviewer.codelens.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "在函数声明上方显示方法级审查按钮"
|
||||
},
|
||||
"vscode-code-reviewer.codelens.languages": {
|
||||
"type": "array",
|
||||
"default": ["typescript", "javascript", "java", "python"],
|
||||
"description": "启用方法级 CodeLens 的语言列表"
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增 menu(可选)
|
||||
|
||||
在 `contributes.menus` 中追加右键菜单入口(作为 CodeLens 的补充):
|
||||
|
||||
```json
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "codeReviewer.reviewMethod",
|
||||
"when": "editorTextFocus",
|
||||
"group": "navigation"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
注意:右键菜单触发时 `symbolRange` 参数为 `undefined`,命令内需要处理此情况——使用 `editor.selection.active` 作为 fallback 位置传入 `extractMethodScope`。
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
按以下顺序实施,每步完成后可独立验证:
|
||||
|
||||
### Phase 1:基础设施
|
||||
|
||||
| 步骤 | 文件 | 验证方式 |
|
||||
|------|------|----------|
|
||||
| 1.1 | `src/scope/status-cache.ts` | 单元测试:set/get/clearDocument 行为正确 |
|
||||
| 1.2 | `src/scope/method-extractor.ts` | 手动测试:在 TS 文件中调用 `getMethodSymbols` 和 `extractMethodScope`,确认能提取方法名、代码、调用链 |
|
||||
| 1.3 | `src/ai/schema.ts` | TypeScript 编译通过,类型无冲突 |
|
||||
|
||||
### Phase 2:AI 引擎
|
||||
|
||||
| 步骤 | 文件 | 验证方式 |
|
||||
|------|------|----------|
|
||||
| 2.1 | `src/ai/engine.ts` 新增 `buildMethodReviewSystemPrompt` | 确认 prompt 文本包含 6 维度、逐路径枚举策略,且有规则时包含 Task 1 规则匹配任务 |
|
||||
| 2.2 | `src/ai/engine.ts` 新增 `buildMethodUserPrompt` | 确认输出包含签名、代码、调用链,且传入 `customRules` 时包含规则列表块 |
|
||||
| 2.3 | `src/ai/engine.ts` 新增 `runMethodReview` | 配置 API Key + 自定义规则后手动调用,确认返回 `customRuleResults` 和 `findings` 两类结果 |
|
||||
|
||||
### Phase 3:命令与集成
|
||||
|
||||
| 步骤 | 文件 | 验证方式 |
|
||||
|------|------|----------|
|
||||
| 3.1 | `src/activation/commands.ts` 新增 `reviewMethod` 命令 | 通过命令面板触发,确认加载自定义规则并传入 `runMethodReview`,面板展示审查结果 |
|
||||
| 3.2 | `src/extension.ts` 注册 CodeLensProvider | 打开 TS 文件,确认函数上方出现 CodeLens 按钮 |
|
||||
| 3.3 | `src/views/codeLensProvider.ts` | 点击 CodeLens 按钮,确认触发方法级审查 |
|
||||
| 3.4 | `package.json` 新增配置项 | 在设置中修改 `codelens.enabled`,确认 CodeLens 消失/出现 |
|
||||
|
||||
### Phase 4:i18n 与打磨
|
||||
|
||||
| 步骤 | 文件 | 验证方式 |
|
||||
|------|------|----------|
|
||||
| 4.1 | `src/i18n/messages.ts` 新增 key | 切换输出语言为 en,确认 CodeLens 文案变为英文 |
|
||||
| 4.2 | 右键菜单 fallback 处理 | 右键触发 `reviewMethod`,确认能用光标位置定位方法 |
|
||||
| 4.3 | 状态缓存刷新 | 审查完成后确认 CodeLens 文案变为 `✓ 已审查(N 个问题)` |
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 功能验收
|
||||
|
||||
1. 打开任意 `.ts` / `.js` / `.java` / `.py` 文件,每个函数声明上方出现 `🔍 Code Purifier: 审查此方法` CodeLens 按钮
|
||||
2. 点击按钮后,右下角弹出进度通知 `Code Purifier 正在审查方法:xxx`
|
||||
3. 审查完成后,CodeLens 按钮文案变为 `✓ Code Purifier: 已审查(N 个问题)` 或 `✓ Code Purifier: 已审查(无问题)`,其中 N = 自定义规则违规数 + AI 深度审查发现数
|
||||
4. 审查面板自动打开,展示方法级的 AI 审查结果
|
||||
5. 审查结果中,自定义规则违规的 `ruleId` 以 `custom:` 前缀标识,AI 深度审查发现的 `ruleId` 以 `method:` 前缀标识
|
||||
6. 审查结果覆盖 6 个维度(correctness / security / design / convention / performance / testability)
|
||||
7. 审查结果中的 `path` 字段描述了触发路径
|
||||
8. 当工作区配置了自定义规则时,审查结果中包含 `customRuleResults`,且违规内容与规则语义相关(非文本匹配)
|
||||
9. 当工作区无自定义规则时,审查结果中 `customRuleResults` 为空数组,AI 仅做 6 维度深度审查
|
||||
|
||||
### 边界验收
|
||||
|
||||
1. 在没有 Symbol Provider 的纯文本文件中,CodeLens 不出现(不报错)
|
||||
2. 文件内方法数超过 50 个时,CodeLens 不出现(性能保护)
|
||||
3. 在设置中关闭 `codelens.enabled` 后,CodeLens 消失
|
||||
4. 在设置中将某语言从 `codelens.languages` 中移除后,该语言文件不显示 CodeLens
|
||||
5. 未配置 API Key 时,提示需要 API Key(不崩溃)
|
||||
6. AI 返回非 JSON 时,降级提示解析失败(不崩溃)
|
||||
7. 文档关闭后重新打开,CodeLens 恢复为初始状态 `🔍 Code Purifier: 审查此方法`(缓存已清理)
|
||||
|
||||
### 架构验收
|
||||
|
||||
1. `src/orchestrator/orchestrator.ts` 未被修改
|
||||
2. `src/rules/rule-filter.ts` 未被修改
|
||||
3. 现有 `codeReviewer.review` 命令行为不变(文档级三阶段审查正常工作)
|
||||
4. 现有 `codeReviewer.reviewSelection` 命令行为不变
|
||||
5. TypeScript 编译无错误(`npm run compile`)
|
||||
6. ESLint 检查无错误(`npm run lint`)
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有用户可见文案必须通过 `t()` 函数获取,支持中英文切换
|
||||
- `runMethodReview` 内复用现有的 `getApiKey`、`createProvider`、`parseJsonResponse`、`addLineNumbers` 等工具函数,不重复实现
|
||||
- `MethodFinding` 的字段与 `AIFinding` 高度重叠,`mergeResults` 可直接接收 `MethodFinding[]` 作为 `aiFindings` 参数(结构兼容)
|
||||
- CodeLens 的 `refresh()` 通过 `_onDidChangeCodeLenses.fire()` 触发,VS Code 会重新调用 `provideCodeLenses`
|
||||
- `package.json` 中的 `engines.vscode` 字段为 `^1.120.0`,CodeLens API 和 DocumentSymbol API 在该版本完全支持
|
||||
- 自定义规则通过 `loadActiveRules` + `filterForDocument` 加载,与现有 `codeReviewer.review` 命令使用同一加载路径,保证规则一致性
|
||||
- AI 对自定义规则做语义匹配而非文本匹配——prompt 中明确要求"理解语义,而非文本匹配",避免简单关键词命中导致的误报
|
||||
- 当 `customRules` 为空数组时,system prompt 中的 Task 1 规则匹配段和 `customRuleResults` 输出段自动省略,AI 仅执行 6 维度深度审查
|
||||
Reference in New Issue
Block a user