- 方法级审查: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 + 行号排序
418 lines
17 KiB
Markdown
418 lines
17 KiB
Markdown
# 导入预览 · 错误规则可编辑与「添加」设计书
|
||
|
||
> 版本: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. 未添加的错误规则 → 确认后自动丢弃
|