Files
2026Technology-Competition/docs/superpowers/specs/2026-07-31-import-error-rule-edit-design.md
T
范智鹏 247f19fd44 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 + 行号排序
2026-08-03 22:53:20 +08:00

418 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 导入预览 · 错误规则可编辑与「添加」设计书
> 版本: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. 未添加的错误规则 → 确认后自动丢弃