ESLint: +29 条 P1/P2 规则 + 12 条 TS 专属规则(含 no-shadow/no-array-constructor 冲突处理) Stylelint: 集成 stylelint-config-recommended + 27 条额外规则 PMD: 排除 20 弃用 + 17 噪音规则,补启 Security/Multithreading,274+12 条精选 SQL-lint: 内置精选 57 条规则配置 + 按 tier 分级 severity + 无项目配置时自动注入临时配置 模板导出: export-service.ts 导出 2-sheet xlsx(复用 xlsx 零新依赖) 模板导入: template-converter.ts 固定列映射解析 + dedup-prompt.ts AI 语义去重 导入预览增强: 错误规则分组置顶只读、跳过行提示、空数据提示 i18n: 新增 18 条模板导入/导出相关翻译 WebView: 静态分析/自定义规则项默认可展开显示 suggestion
41 KiB
自定义规则 · 模板导出与导入功能设计书
项目:vscode-code-reviewer(净码特工 · Code Purifier) 仓库:sdjndaq/2026-ai-b3 分支:vscode-code-reviewer 版本:v2.0(整合导出 + 模板导入) 日期:2026-07-30
一、概述
1.1 背景
当前插件的自定义规则导入是「不确定规格 + AI 兜底」模式:任意格式的文件(md/txt/xlsx/docx/pptx)经 converter 转成 markdown,由 AI 自由理解列含义并输出 YAML,再由 AI 顺带完成去重判定。该模式存在三个痛点:
- 无标准模板:用户批量录入规则时无从获取标准格式,凭经验构造导致列名错乱、AI 解析失败率高。
- 解析全靠 AI:即使是结构化的 Excel,也要走 AI 解析,耗时长、消耗 token、字段映射可能误判。
- 去重与解析耦合:去重判定由 AI 在解析阶段一并完成,无法对「已是标准格式的输入」单独做去重。
1.2 目标
形成「导出模板 → 填写 → 模板导入」的闭环:
- 导出模板:在侧边栏设置面板新增「导出模板」按钮,生成标准格式
.xlsx模板。 - 模板导入:新增 checkbox「从模板导入」,勾选后走「程序解析 + AI 去重」链路——解析阶段跳过 AI(快、确定、零 token),去重阶段复用 AI 的语义比对能力。
1.3 范围
- 含:导出模板功能;模板导入功能(程序解析 + 准入校验 + AI 去重 + 预览)。
- 不含:导出当前规则;YAML 直通链路与 AI 链路的校验补齐(本次不动)。
- 不触碰:现有
ExcelConverter、prompt-builder.ts、addRule原 AI 流程,保证零回归。
注:
import-preview.ts需改动(新增错误规则组渲染),但仅做增量扩展,不改动现有三组的渲染逻辑。
二、现状分析
2.1 规则数据模型
src/types.ts:
interface CustomRule {
id: string;
severity: 'error' | 'warning' | 'info';
description: string;
message: string;
languages?: string[];
excludeLanguages?: string[];
}
src/rules/import-types.ts 扩展:
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
}
2.2 现有导入链路(AI 模式,本次不改)
addRule(name)
→ showOpenDialog(过滤器:yaml/yml/md/txt/xlsx/xls/docx/pptx)
→ 分流:
├─ .yaml/.yml → fs.copyFileSync(零校验直通)
└─ 其他 → ImportService.importRules()
→ 各 Converter 转成 markdown
→ convertContentWithAI()
├─ buildSystemPrompt('spreadsheet'|'freeform', existingRules)
│ └─ buildDedupPromptSection(existingRules) ← 把现有规则喂给 AI
└─ AI 输出 YAML(含 duplicateLevel 等去重字段)
→ parseImportableYaml() 统计 exactCount/overlapCount
→ showImportPreview() 预览
→ 用户确认 → 写入 .code-review/rules/<name>.yaml
2.3 去重机制:由 AI 完成(关键事实)
去重判定由 AI 在解析阶段一并完成,本地无独立去重函数。
证据:
prompt-builder.ts:83-85让 AI 输出duplicateOf/duplicateLevel/duplicateReason。prompt-builder.ts:463buildDedupPromptSection(existingRules, lang)把现有规则(loadActiveRules()+static-rules.json)拼进 system prompt。import-service.ts:274-275只统计 AI 给的duplicateLevel,无本地比对。- 搜索
markDuplicates/detectDuplicates/findDuplicates零命中。
含义:若模板导入跳过 AI,去重也随之消失——即使与已有规则完全相同也不会标记。因此模板导入需保留 AI 去重环节。
2.4 现有 UI(关键事实:无 checkbox、无独立导入按钮)
src/views/setupView.ts 的「自定义规则」区块(第 985-993 行):
<div class="field">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="规则名称">
<button onclick="addRule()">添加</button>
</div>
<div class="error-hint" id="ruleNameError">…</div>
<div class="field-hint">…</div>
</div>
- 没有独立「导入」按钮:导入是
addRule(name)流程的一部分(填名 → 选文件 → 自动转换)。 - 没有任何 checkbox:
type="checkbox"仅出现在docs/superpowers/specs/setup-panel-preview.html(那是规则项的 enable/disable 开关)。
2.5 已有依赖
xlsx库已由excel-converter.ts引入,导出/导入均可复用,零新增依赖。
三、功能设计
3.1 模板格式(导出/导入共用)
生成 .xlsx 文件,含 2 个 sheet。
Sheet 1「规则」(表头 + 1 行示例)
| id | severity | description | message | languages | excludeLanguages |
|---|---|---|---|---|---|
| no-todo | warning | 禁止提交 TODO 注释 | 发现 TODO 注释,请清理后提交 | javascript,typescript |
- 第 1 行:表头,固定 6 列,与
CustomRule字段一一对应。 - 第 2 行:示例行,用户可删除或覆盖。
- 第 3 行起:用户自行追加规则。
Sheet 2「说明」(字段含义 + 填写规范)
| 字段 | 含义 | 取值/格式 | 示例 |
|---|---|---|---|
| id | 规则唯一标识 | 小写字母、数字、连字符,全局唯一 | no-todo |
| severity | 严重级别 | error / warning / info | warning |
| description | 规则简述(给人看) | 自由文本 | 禁止提交 TODO 注释 |
| message | 命中时展示给开发者的提示语 | 自由文本 | 发现 TODO 注释,请清理后提交 |
| languages | 生效的语言 | 逗号分隔,留空表示对所有语言生效 | javascript,typescript |
| excludeLanguages | 排除的语言 | 逗号分隔,可留空 |
填写说明:
- severity 仅接受 error / warning / info 三个值。
- languages / excludeLanguages 多值用英文逗号分隔。
- 示例行可删除,仅作填写参考。
- 该模板可直接用于「从模板导入」功能回环校验。
3.2 导出模板交互
- 用户在侧边栏「自定义规则」区块点击「📤 导出模板」按钮。
- 弹出系统保存对话框,默认文件名
code-review-rules-template.xlsx,过滤器Excel (*.xlsx)。 - 用户选定位置后写盘。
- 右下角信息提示:
✓ 模板已导出,附带「打开文件夹」按钮。 - 用户填写模板 → 回到插件,勾选「从模板导入」→ 点「添加」→ 预览 → 确认写入。
3.3 模板导入交互
侧边栏「自定义规则」区块:
[规则名输入框] [+ 添加]
☐ 从模板导入(跳过 AI 解析,仅支持 Excel 模板) ← 新增 checkbox
[📤 导出模板] ← 新增按钮
- 不勾选(默认):点「添加」→ 走现有
addRuleAI 链路(支持 yaml/md/txt/xlsx/docx/pptx),行为不变。 - 勾选:点「添加」→ 走模板直通链路(仅 xlsx/xls,含两道准入校验,程序解析 + AI 去重)。
3.4 模板导入完整流程
勾选 checkbox + 填规则名 + 点「添加」
↓
[1] 选文件(showOpenDialog,过滤器仅 xlsx/xls)
↓
[2] 程序解析 + 数据行校验(TemplateConverter,无 AI)
├─ 准入校验1:文件格式(必须 xlsx/xls 且可读) → 失败则报错终止
├─ 准入校验2:表头结构(含 id/severity/description/message)→ 失败则报错终止
├─ 逐行解析 + 校验 → ImportableRule[](错误规则带 validationIssues)
├─ 分离:validRules(无 issue)+ errorRules(有 issue)
└─ yamlContent ← 仅 validRules 拼接(标准 YAML,无 duplicateLevel)
↓
[3] AI 去重(仅对 validRules,新增专用 prompt)
├─ loadActiveRules() 取工作区已有规则
├─ buildDedupOnlyPrompt(yamlContent, existingRules) ← 新增
│ 告诉 AI:"以下 YAML 已是标准格式,只需对照现有规则标 duplicateLevel,不要改字段"
├─ AI 返回带 duplicateOf/duplicateLevel/duplicateReason 的 YAML
└─ parseImportableYaml() 解析(复用现有)
(注:errorRules 不送 AI,无 duplicateLevel)
↓
[4] 预览(showImportPreview,复用 + 新增错误规则组)
├─ 顶部提示"已跳过 N 行空数据"(若有)
├─ 🚫 错误规则组(置顶,无 checkbox,只读,标注"将自动丢弃")
├─ ⛔ 完全重复组(默认不勾选)
├─ ⚠️ 部分重叠组
├─ ✅ 新规则组
└─ 用户确认 → 写入选中的有效规则 → 错误规则自动丢弃
(边界:validRules 全空时禁用确认按钮,提示"无有效规则可导入")
3.5 校验策略(两层:准入 + 数据行)
校验分两层,准入校验失败硬报错,数据行问题软提示进预览。
第一层:准入校验(硬报错,不进预览)
第一关:文件格式校验
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat')); // "文件格式错误,请使用 Excel 模板"
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
第二关:数据结构校验(是不是模板文件)
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty')); // "文件为空"
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const REQUIRED = ['id', 'severity', 'description', 'message'];
const missing = REQUIRED.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
| 场景 | 处理 |
|---|---|
| 选了 .txt/.docx/.md | 第一关拒,提示"文件格式错误" |
| 选了损坏的 .xlsx | 第一关拒,同上 |
| .xlsx 表头非标准(如中文列名) | 第二关拒,提示"缺少列: ..." |
| .xlsx 表头标准 | 通过准入,进入数据行校验 |
第二层:数据行校验(软提示,进预览,自动丢弃)
核心思路:数据行的问题不阻塞导入流程,而是带上 validationIssues 字段,和有效规则一起进入预览面板,作为独立的"错误规则"分组展示。错误规则不参与去重、不写入 YAML,确认导入时自动丢弃;有效规则正常导入。用户可查看错误原因后修改文件重新导入。
ImportableRule 扩展:
interface ValidationIssue {
field: string; // 'severity' | 'description' | 'message'
severity: 'error' | 'warning';
message: string; // 如 'severity 非法: "warn"'、'description 为空'
}
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[]; // 新增:非空则为错误规则
rowNumber?: number; // 新增:原始行号
}
校验规则(只记录错误,不修补;不校验表内重复):
| 问题 | 处理 | 是否导入 |
|---|---|---|
| severity 为空或拼错(如 "warn") | 记录 issue,severity 临时填 'warning' 仅用于结构完整 | ❌ 自动丢弃 |
| description 为空 | 记录 issue | ❌ 自动丢弃 |
| message 为空 | 记录 issue | ❌ 自动丢弃 |
| 空行/空 id | 静默丢弃(无法标识) | — |
关键:错误规则不进 yamlContent、不送 AI 去重、确认导入时自动丢弃。只有有效规则(无
validationIssues)才参与 AI 去重和写入 YAML。不校验表内 id 重复:本次不考虑规则文件内多条规则 id 相同的情况,交由 AI 去重环节按现有逻辑处理(与已有规则的比对)。
预览面板分组(复用现有 showImportPreview,错误规则组置顶):
┌─ 规则导入预览 ──────────────────────────┐
│ [🚫 错误规则:2] [⛔ 完全重复:1] [⚠️ 部分重叠:2] [✅ 新规则:3] │
│ │
│ 🚫 错误规则 (2) [只读] │
│ ⚠ 第3行 severity 非法: "warn" │
│ id: no-magic-numbers │
│ description: 禁止魔法数字 │
│ message: 请用常量替代 │
│ (将自动丢弃,请修改文件后重新导入)│
│ │
│ ⚠ 第5行 description 为空 │
│ id: check-null │
│ message: 检查空值 │
│ (将自动丢弃,请修改文件后重新导入)│
│ │
│ ⛔ 完全重复 (1) ...(现有逻辑) │
│ ⚠️ 部分重叠 (2) ...(现有逻辑) │
│ ✅ 新规则 (3) ...(现有逻辑) │
│ │
│ [ 确认导入 ] [ 取消 ] │
└──────────────────────────────────────────┘
- 错误规则组置顶,无 checkbox,纯只读展示
- 错误规则不参与 AI 去重(不送 AI),无
duplicateLevel - 仅有效规则(无
validationIssues)参与 AI 去重,按 exact/overlap/none 分组 - 确认导入时写入选中的有效规则,错误规则自动丢弃,导入流程正常完成
3.6 示例行处理
模板里的 no-todo 示例行按用户确认"不做特殊处理"——当普通规则解析,交由 AI 去重环节判定。若工作区已有同名规则,会被标记为 exact 重复,预览面板默认不勾选;若无冲突则正常进入预览,由用户自行决定保留或删除。
四、技术方案
4.1 改动文件清单
| 类型 | 文件 | 改动 |
|---|---|---|
| 新增 | src/rules/export-service.ts |
模板生成服务,复用 xlsx 库 |
| 新增 | src/rules/converters/template-converter.ts |
程序解析 + 两道准入校验 + 生成 yamlContent |
| 新增 | src/rules/converters/dedup-prompt.ts |
buildDedupOnlyPrompt(yamlContent, existingRules) 专用去重 prompt(中英) |
| 修改 | src/rules/import-types.ts |
ImportableRule 增加 validationIssues / rowNumber 字段;新增 ValidationIssue 接口;ConversionResult 增加 skippedCount?: number 字段 |
| 修改 | src/rules/import-service.ts |
新增 importTemplate(srcPath, name, existingRules):调 TemplateConverter → AI 去重 → 返回 ConversionResult |
| 修改 | src/rules/import-preview.ts |
renderPreviewHtml 增加错误规则组(置于现有三组之上,无 checkbox 只读);keepRule 默认值考虑 validationIssues(错误规则不进入 keepRule);renderRuleItem 展示 issue 详情 + "将自动丢弃"提示;顶部展示"已跳过 N 行空数据"(来自 skippedCount);validRules 为空时禁用"确认导入"按钮并提示"无有效规则可导入" |
| 修改 | src/views/setupView.ts |
加 checkbox + 导出按钮;addRule 据 useTemplateMode 分流;新增 case 'exportTemplate' |
| 修改 | src/activation/commands.ts |
注册 codeReviewer.exportTemplate 命令 |
| 修改 | package.json |
contributes.commands 增 1 条 |
| 修改 | src/i18n/messages.ts |
checkbox/校验/去重 prompt/导出 文案(中英) |
不动:ExcelConverter、prompt-builder.ts、现有 addRule 原 AI 流程。import-preview.ts 仅做增量扩展(新增错误规则组),不改动现有三组渲染逻辑。
4.2 新增 src/rules/export-service.ts(导出模板)
import * as XLSX from 'xlsx';
import * as vscode from 'vscode';
import { t } from '../i18n/messages';
const RULES_HEADER = ['id', 'severity', 'description', 'message', 'languages', 'excludeLanguages'];
const RULES_EXAMPLE = [
'no-todo', 'warning', '禁止提交 TODO 注释',
'发现 TODO 注释,请清理后提交', 'javascript,typescript', '',
];
const GUIDE_AOA: string[][] = [
['字段', '含义', '取值/格式', '示例'],
['id', '规则唯一标识', '小写字母、数字、连字符,全局唯一', 'no-todo'],
['severity', '严重级别', 'error / warning / info', 'warning'],
['description', '规则简述(给人看)', '自由文本', '禁止提交 TODO 注释'],
['message', '命中时展示给开发者的提示语', '自由文本', '发现 TODO 注释,请清理后提交'],
['languages', '生效的语言', '逗号分隔,留空表示对所有语言生效', 'javascript,typescript'],
['excludeLanguages', '排除的语言', '逗号分隔,可留空', ''],
['', '', '', ''],
['填写说明', '', '', ''],
['1. severity 仅接受 error / warning / info 三个值', '', '', ''],
['2. languages / excludeLanguages 多值用英文逗号分隔', '', '', ''],
['3. 示例行可删除,仅作填写参考', '', '', ''],
['4. 该模板可直接用于「从模板导入」功能回环校验', '', '', ''],
];
export async function exportTemplate(): Promise<void> {
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file('code-review-rules-template.xlsx'),
filters: { 'Excel': ['xlsx'] },
saveLabel: t('exportTemplate.saveLabel'),
});
if (!uri) { return; }
const wb = XLSX.utils.book_new();
const wsRules = XLSX.utils.aoa_to_sheet([RULES_HEADER, RULES_EXAMPLE]);
wsRules['!cols'] = [
{ wch: 16 }, { wch: 10 }, { wch: 32 }, { wch: 40 }, { wch: 24 }, { wch: 20 },
];
XLSX.utils.book_append_sheet(wb, wsRules, '规则');
const wsGuide = XLSX.utils.aoa_to_sheet(GUIDE_AOA);
wsGuide['!cols'] = [{ wch: 22 }, { wch: 28 }, { wch: 42 }, { wch: 36 }];
XLSX.utils.book_append_sheet(wb, wsGuide, '说明');
try {
XLSX.writeFile(wb, uri.fsPath);
const openFolder = t('exportTemplate.openFolder');
const choice = await vscode.window.showInformationMessage(
t('exportTemplate.success'), openFolder,
);
if (choice === openFolder) {
vscode.commands.executeCommand('revealFileInOS', uri);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(t('exportTemplate.fail', { 0: msg }));
}
}
4.3 新增 src/rules/converters/template-converter.ts(程序解析 + 准入校验 + 数据行校验)
import * as path from 'path';
import * as XLSX from 'xlsx';
import type { ImportableRule, ValidationIssue } from '../import-types';
import type { Severity } from '../../types';
import { t } from '../../i18n/messages';
const REQUIRED_HEADERS = ['id', 'severity', 'description', 'message'];
const VALID_SEVERITY = ['error', 'warning', 'info'];
function splitList(v: unknown): string[] {
const s = String(v ?? '').trim();
if (!s) { return []; }
// 兼容逗号、分号、顿号、换行
return s.split(/[,;、\n]/).map(x => x.trim()).filter(Boolean);
}
export interface TemplateParseResult {
rules: ImportableRule[]; // 所有规则(含错误的)
validRules: ImportableRule[]; // 仅有效规则(无 validationIssues)
yamlContent: string; // 仅有效规则的 YAML(送 AI 去重)
skippedCount: number; // 被丢弃的空行数
}
export function parseTemplate(srcPath: string): TemplateParseResult {
// 第一关:文件格式校验
const ext = path.extname(srcPath).toLowerCase();
if (!['.xlsx', '.xls'].includes(ext)) {
throw new Error(t('import.template.badFormat'));
}
let wb: XLSX.WorkBook;
try { wb = XLSX.readFile(srcPath); }
catch { throw new Error(t('import.template.badFormat')); }
// 第二关:数据结构校验
const sheet = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<Record<string, string>>(sheet, { defval: '' });
if (rows.length === 0) {
throw new Error(t('import.template.empty'));
}
const header = Object.keys(rows[0]).map(k => k.trim().toLowerCase());
const missing = REQUIRED_HEADERS.filter(h => !header.includes(h));
if (missing.length > 0) {
throw new Error(t('import.template.notTemplate', { 0: missing.join(', ') }));
}
// 数据行解析 + 校验(只记录错误,不修补、不丢弃有 id 的行)
const allRows = rows.length;
const rules: ImportableRule[] = rows
.map((r, idx) => ({ r, rowNo: idx + 2 })) // 先记原始行号(表头占第1行,数据从第2行起)
.filter(({ r }) => String(r.id ?? '').trim()) // 再 filter 空行/空 id
.map(({ r, rowNo }) => {
const issues: ValidationIssue[] = [];
// severity 校验(不修补,仅记录;临时填 warning 保证结构完整)
const sevRaw = String(r.severity ?? '').trim().toLowerCase();
const severity: Severity = VALID_SEVERITY.includes(sevRaw) ? (sevRaw as Severity) : 'warning';
if (!VALID_SEVERITY.includes(sevRaw)) {
issues.push({ field: 'severity', severity: 'warning', message: `severity 非法: "${r.severity ?? ''}"` });
}
// description 校验(不修补,仅记录)
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
// message 校验(不修补,仅记录)
const message = String(r.message ?? '').trim();
if (!message) {
issues.push({ field: 'message', severity: 'error', message: 'message 为空' });
}
return {
id: String(r.id).trim(),
severity,
description,
message,
languages: splitList(r.languages),
excludeLanguages: splitList(r.excludeLanguages),
rowNumber: rowNo,
validationIssues: issues.length > 0 ? issues : undefined,
};
});
// 仅有效规则进 yamlContent(错误规则不送 AI 去重)
// 注:本次不校验表内 id 重复,交由 AI 去重环节处理
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
return { rules, validRules, yamlContent, skippedCount: allRows - rules.length };
}
function buildYaml(rules: ImportableRule[]): string {
const lines: string[] = [];
for (const r of rules) {
lines.push(`- id: ${r.id}`);
lines.push(` severity: ${r.severity}`);
lines.push(` description: ${r.description}`);
lines.push(` message: ${r.message}`);
if (r.languages?.length) {
lines.push(` languages: [${r.languages.join(', ')}]`);
}
if (r.excludeLanguages?.length) {
lines.push(` excludeLanguages: [${r.excludeLanguages.join(', ')}]`);
}
}
return lines.join('\n');
}
4.4 新增 src/rules/converters/dedup-prompt.ts(专用去重 prompt)
import type { CustomRule } from '../../types';
import { getLanguage, type Language } from '../../i18n/messages';
export function buildDedupOnlyPrompt(
yamlContent: string,
existingRules: CustomRule[],
): { system: string; user: string } {
const lang = getLanguage();
const s = PROMPTS[lang];
const existingList = existingRules.length === 0
? s.noExisting
: existingRules.map(r =>
`- id: ${r.id} | severity: ${r.severity} | description: ${r.description} | message: ${r.message}`
).join('\n');
const system = [
s.role,
s.taskTitle,
s.taskLines.join('\n'),
s.rulesTitle,
s.rulesLines.join('\n'),
s.existingTitle,
existingList,
].join('\n\n');
const user = s.userPrefix + '\n\n' + yamlContent;
return { system, user };
}
const PROMPTS: Record<Language, {
role: string;
taskTitle: string;
taskLines: string[];
rulesTitle: string;
rulesLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重助手。',
taskTitle: '## 任务',
taskLines: [
'下面 USER 部分是已标准化的规则 YAML,只需对照现有规则标 duplicateLevel,不要修改任何原有字段。',
'为每条规则补充以下字段:',
'- duplicateOf: 重复的规则 ID(linter 如 eslint/no-console;自定义如 custom/my-rule)',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none',
'- duplicateReason: overlap 时必填',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exact:id 相同,或 description+message 语义完全一致',
'2. overlap:检测目标/场景部分重叠',
'3. 不要修改原有字段(id/severity/description/message/languages/excludeLanguages)',
'4. 只补充去重字段',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant.',
taskTitle: '## Task',
taskLines: [
'The USER section below is standardized rule YAML. Only mark duplicateLevel against existing rules; do NOT modify any original fields.',
'For each rule, add:',
'- duplicateOf: the duplicated rule ID (e.g. eslint/no-console; custom/my-rule)',
'- duplicateLevel: exact (identical) / overlap (partial) / none',
'- duplicateReason: required when overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: same id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. Do NOT modify original fields (id/severity/description/message/languages/excludeLanguages)',
'4. Only add dedup fields',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複排除アシスタントです。',
taskTitle: '## タスク',
taskLines: [
'下記 USER 部分は標準化されたルール YAML です。既存ルールと照合して duplicateLevel を付与するのみ、元フィールドは変更しないでください。',
'各ルールに以下を追加:',
'- duplicateOf: 重複ルール ID(例: eslint/no-console; custom/my-rule)',
'- duplicateLevel: exact(完全一致)/ overlap(部分重複)/ none',
'- duplicateReason: overlap 時必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: id 同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. 元フィールドを変更しない(id/severity/description/message/languages/excludeLanguages)',
'4. 重複フィールドのみ追加',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
4.5 修改 src/rules/import-service.ts(新增 importTemplate)
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
import { loadActiveRules } from './yaml-parser';
// convertContentWithAI / parseImportableYaml / normalizeRuleIds 均为本文件已有函数,直接使用,无需 import
export class ImportService {
// 原有 importRules 不动
async importTemplate(
srcPath: string,
name: string,
workspaceRoot: string,
): Promise<ConversionResult> {
// [1] 程序解析 + 准入校验 + 数据行校验(失败即 throw)
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
// [2] AI 去重(仅对有效规则;错误规则不送 AI)
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const existingRules = loadActiveRules(workspaceRoot);
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(system, user);
dedupedValidRules = parseImportableYaml(dedupedYaml);
}
// [3] 合并:有效规则(带去重标记)+ 错误规则(无去重标记,仅展示)
const errorRules = rules.filter(r => r.validationIssues);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = allRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = allRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount, // 传递给预览面板用于顶部提示
};
}
}
注意:错误规则(
validationIssues非空)不参与 AI 去重,无duplicateLevel,在预览中归入"错误规则"组。确认导入时只写入选中的有效规则。边界:validRules 全空。若文件全是错误规则(
validRules.length === 0),跳过 AI 去重,allRules全是错误规则。预览面板进入后应:
- 顶部提示"无有效规则可导入,请修改文件后重新导入"
- 禁用"确认导入"按钮,仅允许"取消"
此边界由
import-preview.ts在收到validRules数量为 0 时处理(见 4.1 改动描述)。ConversionResult 需扩展
skippedCount?: number字段,用于预览面板顶部提示"已跳过 N 行空数据"。
4.6 修改 src/views/setupView.ts(UI + 分流)
HTML 改动(「自定义规则」区块):
<div class="field">
<div class="input-group">
<input type="text" id="newRuleInput" placeholder="${t('setup.ruleNamePlaceholder')}">
<button class="btn btn-sm" style="background:#7c3aed;color:#fff;border-color:#7c3aed;"
onclick="addRule()">${t('setup.add')}</button>
</div>
<div class="error-hint" id="ruleNameError">${t('setup.ruleNameRequired')}</div>
<div class="field-hint">${t('setup.ruleNameHint')}</div>
<!-- 新增 checkbox -->
<label style="display:flex;align-items:center;gap:6px;margin-top:8px;font-size:12px;">
<input type="checkbox" id="useTemplateMode">
${t('setup.fromTemplate')}
</label>
<!-- 新增导出按钮 -->
<button class="btn btn-sm" style="margin-top:6px;"
onclick="exportTemplate()">${t('setup.exportTemplate')}</button>
</div>
webview script(获取 checkbox 状态):
function addRule() {
const name = document.getElementById('newRuleInput').value;
const useTemplateMode = document.getElementById('useTemplateMode').checked;
vscode.postMessage({ type: 'addRule', name, useTemplateMode });
}
function exportTemplate() {
vscode.postMessage({ type: 'exportTemplate' });
}
onDidReceiveMessage 分流:
import { exportTemplate } from '../rules/export-service';
// 在 SetupViewProvider 的 onDidReceiveMessage 中:
case 'addRule':
await this.addRule(msg.name, msg.useTemplateMode); // 传入 useTemplateMode
await this.pushConfig();
break;
case 'exportTemplate':
await exportTemplate();
break;
addRule 方法分流:
private async addRule(name: string, useTemplateMode?: boolean): Promise<void> {
if (!name.trim()) { return; }
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) { return; }
if (useTemplateMode) {
// 模板直通链路
const result = await vscode.window.showOpenDialog({
canSelectMany: false,
openLabel: t('setup.selectTemplateFile'),
filters: { 'Excel': ['xlsx', 'xls'] }, // 仅 Excel
});
if (!result || result.length === 0) { return; }
try {
const conversion = await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: t('setup.importingTemplate'),
}, async () => {
return await this.importService.importTemplate(result[0].fsPath, name, workspaceRoot);
});
const decision = await showImportPreview(conversion);
if (decision?.confirmed) {
await this.saveImportedRules(name, conversion, decision, workspaceRoot);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg); // 准入校验失败直接提示
}
} else {
// 原 AI 链路(不动)
// ... 现有 addRule 逻辑
}
}
4.7 修改 src/activation/commands.ts(注册导出命令)
import { exportTemplate } from '../rules/export-service';
// 在 registerCommands 中追加
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate()),
);
4.8 修改 package.json
contributes.commands 追加:
{
"command": "codeReviewer.exportTemplate",
"title": "Code Purifier: 导出规则模板"
}
4.9 修改 src/i18n/messages.ts(新增文案)
| key | 中文 | 英文 | 日文 |
|---|---|---|---|
setup.fromTemplate |
从模板导入(跳过 AI 解析,仅支持 Excel 模板) | Import from template (skip AI parsing, Excel only) | テンプレートからインポート(AI 解析スキップ、Excel のみ) |
setup.exportTemplate |
📤 导出模板 | 📤 Export Template | 📤 テンプレートをエクスポート |
setup.selectTemplateFile |
选择模板文件 | Select template file | テンプレートファイルを選択 |
setup.importingTemplate |
正在导入模板... | Importing template... | テンプレートをインポート中... |
import.template.badFormat |
文件格式错误,请使用 Excel 模板(.xlsx/.xls) | Bad file format, please use Excel template (.xlsx/.xls) | ファイル形式エラー、Excel テンプレートを使用してください |
import.template.empty |
文件为空 | File is empty | ファイルが空です |
import.template.notTemplate |
不是模板文件,缺少列: {0} | Not a template file, missing columns: {0} | テンプレートファイルではありません、欠損列: {0} |
exportTemplate.title |
导出模板 | Export Template | テンプレートをエクスポート |
exportTemplate.saveLabel |
导出模板 | Export Template | エクスポート |
exportTemplate.success |
模板已导出 | Template exported | テンプレートをエクスポートしました |
exportTemplate.fail |
导出失败:{0} | Export failed: {0} | エクスポート失敗: {0} |
exportTemplate.openFolder |
打开文件夹 | Reveal in Folder | フォルダを開く |
import.sectionInvalid |
错误规则 | Error Rules | エラー行 |
import.cannotImport |
将自动丢弃,请修改文件后重新导入 | Will be auto-discarded, please fix the file and retry | 自動破棄されます、ファイルを修正して再インポートしてください |
import.template.skipped |
已跳过 {0} 行空数据 | Skipped {0} empty rows | {0} 行の空データをスキップしました |
五、关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 是否新增依赖 | 否,复用 xlsx |
导入侧已引入,零额外体积 |
| 模板内容 | 表头 + 1 行示例 + 说明 sheet | 贴合「模板」语义,降低首次使用门槛 |
| 是否同时导出当前规则 | 否 | 用户确认暂不需要,保持最小化 |
| 导出按钮位置 | setupView「自定义规则」工具栏 | 紧邻「导入」,成对出现 |
| 模板列名 | 与 CustomRule 字段完全一致 |
保证导入回环自洽 |
| 模板导入触发方式 | checkbox「从模板导入」 | 不加独立按钮,用户显式选择模式 |
| 模板导入解析方式 | 程序解析(无 AI) | 快、确定、零 token 用于解析 |
| 模板导入去重方式 | AI 去重(新增专用 prompt) | 复用 AI 语义比对能力,去重与解析解耦 |
| 准入校验失败 | 硬报错阻止 | 文件格式错或结构错直接拒绝,不进预览 |
| severity 拼错 | 记录 issue + 进错误规则组,自动丢弃 | 错误规则不导入不阻塞流程,用户改文件重导 |
| description/message 空 | 记录 issue + 进错误规则组,自动丢弃 | 同上,不修补不导入 |
| 表内 id 重复 | 不校验,交由 AI 去重环节处理 | 本次不处理规则文件内重复,简化实现 |
| 错误规则是否参与去重 | 否,不送 AI | 错误规则不导入,无需标 duplicateLevel |
| 错误规则是否阻塞导入 | 否,有效规则正常导入 | 自动丢弃错误规则,流程不中断 |
| 空行/空 id | 静默丢弃 + 顶部提示跳过数 | 无法标识的行无法修复 |
| 数据行校验展示 | 复用现有预览,新增「错误规则」组(置于现有三组之上) | 不新建面板,与去重分组正交 |
| 示例行处理 | 不做特殊处理 | 当普通规则,交由 AI 去重判定 |
| 是否触碰导入侧 AI 链路 | 否 | 零回归 |
| 是否触碰现有去重 prompt | 否,新增专用 prompt | 任务聚焦,避免影响原 AI 解析链路 |
六、验收标准
6.1 导出模板
- 侧边栏「自定义规则」区块出现「导出模板」按钮。
- 点击弹出保存对话框,默认文件名
code-review-rules-template.xlsx。 - 生成文件含 2 个 sheet:「规则」(表头+示例行)、「说明」(字段说明+填写规范)。
- 列名依次为
id, severity, description, message, languages, excludeLanguages。 - 导出成功后右下角提示,点击「打开文件夹」可定位文件。
- 命令面板执行
Code Purifier: 导出规则模板触发同样流程。 - 中英文/日文切换下,按钮文案与提示信息正确。
- 取消保存对话框时不报错、无残留。
6.2 模板导入
- 「自定义规则」区块出现 checkbox「从模板导入」。
- 不勾选时点「添加」,走原 AI 链路,行为与现状完全一致(零回归)。
- 勾选时点「添加」,文件选择对话框仅过滤
xlsx/xls。 - 选非 Excel 文件(如 .txt)→ 提示"文件格式错误",不进入预览。
- 选损坏的 .xlsx → 提示"文件格式错误",不进入预览。
- 选表头非标准的 .xlsx → 提示"不是模板文件,缺少列: ...",不进入预览。
- 选标准模板 → 进入预览,exact 重复默认不勾选。
- 预览界面复用
showImportPreview,新增「错误规则」组(置顶,无 checkbox 只读),其余三组渲染逻辑不变。 - 确认导入后写入
.code-review/rules/<name>.yaml。 - 导入速度明显快于 AI 链路(解析阶段无 AI 调用)。
- 准入校验失败时右下角错误提示,无残留文件。
- 数据行有 severity 拼错时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。
- 数据行 description/message 为空时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。
- 规则文件内多条 id 相同的规则,不校验表内重复,按普通规则进入有效规则组,交由 AI 去重环节处理。
- 空行被跳过时,预览面板顶部提示"已跳过 N 行空数据"(N 来自
skippedCount)。 - 错误规则不参与 AI 去重,无 duplicateLevel 标记。
- 确认导入时只写入选中的有效规则,错误规则自动丢弃,导入流程正常完成(不阻塞)。
- 文件全是错误规则(validRules 为空)时,预览面板禁用"确认导入"按钮,提示"无有效规则可导入,请修改文件后重新导入"。
6.3 闭环验证
- 导出模板 → 不修改直接勾选「从模板导入」导入 → 预览出现
no-todo规则(示例行)。 - 导出模板 → 删除示例行 → 填入 2 条新规则 → 导入 → 预览出现 2 条新规则。
- 导出模板 → 填入与工作区已有规则相同的 id → 导入 → 预览标记为 exact 重复。
七、风险与回归
| 风险 | 等级 | 应对 |
|---|---|---|
| 导入侧 AI 链路被误改 | 中 | 评审约束:不动 ExcelConverter、prompt-builder.ts、import-preview.ts、原 addRule AI 分支 |
| 专用去重 prompt 与原解析 prompt 行为不一致 | 中 | 去重 prompt 明确"不要修改原有字段",且预览环节人工确认兜底 |
xlsx 写文件在无写权限目录失败 |
低 | try/catch 捕获并友好提示 |
| 跨平台路径(Win/Mac/Linux) | 低 | 使用 vscode.Uri.fsPath,不硬编码分隔符 |
| 国际化漏键 | 低 | 中英日三语同步增补,走现有 t() 机制 |
| AI 去重环节失败(网络/限流) | 中 | 复用现有 convertContentWithAI 的错误处理,提示用户重试 |
| validRules 全空(文件全是错误规则) | 低 | 预览禁用确认按钮,提示"无有效规则可导入",不写空文件 |
八、后续可扩展点(本次不做)
- 导出当前规则:复用
loadActiveRules(),把工作区规则写入 Excel(备份/迁移)。 - 模板多语言:根据
getAIOutputLanguage()切换示例与说明语言。 - 模板版本号:在「说明」sheet 增加
version字段,便于未来字段演进时的兼容判断。 - YAML 直通链路校验补齐:当前
.yaml/.yml直接copyFileSync零校验,可补结构化校验。 - AI 链路校验补齐:AI 解析结果也可过一遍
validateRules()硬校验。 - 本地去重函数:若未来想完全脱离 AI,可实现
detectDuplicates(rules, existing)做纯本地比对。