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. 未添加的错误规则 → 确认后自动丢弃