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:
范智鹏
2026-08-03 22:53:20 +08:00
parent 5848eaa82a
commit 247f19fd44
46 changed files with 6143 additions and 937 deletions
@@ -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. 追加 duplicateInfoexact/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` 返回 nullquiet),重试后降级 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 2AI 引擎
| 步骤 | 文件 | 验证方式 |
|------|------|----------|
| 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 4i18n 与打磨
| 步骤 | 文件 | 验证方式 |
|------|------|----------|
| 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 维度深度审查