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
33 KiB
33 KiB
自定义规则 · 模板导出与导入功能设计书 v2.0(修正稿)
版本:v2.0(整合导出 + 模板导入) 日期:2026-07-30 修正:基于 Stage ② 需求澄清,修正 API 签名、错误处理、参数传递
一、方案概览
1.1 目标
形成「导出模板 → 填写 → 模板导入」闭环:
- 导出模板: 侧边栏新增按钮,生成标准
.xlsx模板(2 个 sheet) - 模板导入: 新增 checkbox「从模板导入」,程序解析 + AI 去重,预览确认
1.2 数据流
┌──────────────────────────────────────────────────────────────────┐
│ 导出模板 │
│ setupView 按钮 → exportService.ts → XLSX.writeFile → .xlsx 文件 │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ 模板导入(勾选 checkbox) │
│ │
│ setupView checkbox + 填名 + 点「添加」 │
│ → showOpenDialog(仅 xlsx/xls) │
│ → ImportService.importTemplate(srcPath, name, context) │
│ ├─ TemplateConverter.parseTemplate(srcPath) ← 纯程序,无 AI │
│ │ ├─ 准入校验-1: 文件格式 │
│ │ ├─ 准入校验-2: 表头结构 │
│ │ ├─ 数据行解析 + 校验 → ImportableRule[](带 validationIssues) │
│ │ └─ 拆分 validRules / errorRules,生成 yamlContent │
│ ├─ AI 去重(仅 validRules) ← 新增 dedup-prompt │
│ │ ├─ 成功: parseImportableYaml() → 带 duplicateLevel │
│ │ └─ 失败: 降级,validRules 无去重标记 + warning 提示 │
│ └─ 返回 ConversionResult(含 errorRules + skippedCount) │
│ → showImportPreview(预览面板) │
│ ├─ 顶部: 跳过 N 行空数据(若 skippedCount > 0) │
│ ├─ 🚫 错误规则组(置顶,无 checkbox,只读) │
│ ├─ ⛔ 完全重复 / ⚠️ 部分重叠 / ✅ 新规则(仅有效规则,现有逻辑) │
│ └─ 确认 → ImportService.applyConversion → 写 YAML │
│ │
│ 不勾选 checkbox → 走原有 addRule AI 链路,零改动 │
└──────────────────────────────────────────────────────────────────────┘
1.3 范围
| 类型 | 内容 |
|---|---|
| 含 | 导出模板;模板导入(程序解析 + 准入校验 + AI 去重 + 预览) |
| 不含 | 导出当前规则;YAML 直通链路校验补齐;AI 链路校验补齐 |
| 不触碰 | ExcelConverter、prompt-builder.ts、原 addRule AI 流程 |
1.4 Stage ② 修正项(相对初版设计书)
| 编号 | 修正点 | 内容 |
|---|---|---|
| ① | convertContentWithAI 调用签名 |
convertContentWithAI(yamlContent, context, system) — content 在前,context 为中,systemPrompt 在后 |
| ② | AI 去重失败降级 | convertContentWithAI 返回 null 时,有效规则不带 duplicateLevel 进预览,弹出 warning |
| ③ | importTemplate 参数 |
importTemplate(srcPath, name, context) — 增加 context,workspaceRoot 内部获取 |
| ⑤ | prompt 约束加强 | 去重 prompt 明确 "你只负责去重判定,禁止修改任何已有字段,禁止添加/删除规则" |
二、架构设计
2.1 模块划分
src/rules/
├── export-service.ts ← 新增: 模板导出(xlsx 写盘)
├── converters/
│ ├── template-converter.ts ← 新增: 程序解析 + 准入校验
│ └── dedup-prompt.ts ← 新增: 专用去重 prompt 构建
├── import-service.ts ← 修改: 新增 importTemplate 方法
├── import-preview.ts ← 修改: 新增错误规则组 + skippedCount 提示
├── import-types.ts ← 修改: 扩展接口字段
├── yaml-parser.ts ← 不改(loadActiveRules 已被 import)
└── converters/prompt-builder.ts ← 不碰
src/views/setupView.ts ← 修改: checkbox + 导出按钮 + 分流逻辑
src/activation/commands.ts ← 修改: 注册 exportTemplate 命令
src/i18n/messages.ts ← 修改: 新增 i18n key
package.json ← 修改: contributes.commands 追加 1 条
2.2 关键接口扩展
// src/rules/import-types.ts — 扩展
interface ValidationIssue {
field: string;
severity: 'error' | 'warning';
message: string;
}
interface ImportableRule extends CustomRule {
duplicateOf?: string;
duplicateLevel?: 'exact' | 'overlap' | 'none';
duplicateReason?: string;
validationIssues?: ValidationIssue[]; // 新增: 非空则为错误规则
rowNumber?: number; // 新增: 原始行号
}
interface ConversionResult {
rules: ImportableRule[];
yamlContent: string;
sourceFileName: string;
exactCount: number;
overlapCount: number;
skippedCount?: number; // 新增: 被跳过的空行数
errorCount?: number; // 新增: 错误规则数
}
三、详细实现
3.1 新增 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 }));
}
}
3.2 新增 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[];
yamlContent: string;
skippedCount: number;
}
export function parseTemplate(srcPath: string): TemplateParseResult {
// 准入校验-1: 文件格式
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')); }
// 准入校验-2: 表头结构
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(', ') }));
}
// 数据行解析 + 校验
const totalRows = rows.length;
const rules: ImportableRule[] = rows
.filter(r => String(r.id ?? '').trim() !== '')
.map((r, idx) => {
const rowNo = idx + 2;
const issues: ValidationIssue[] = [];
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 ?? ''}"` });
}
const description = String(r.description ?? '').trim();
if (!description) {
issues.push({ field: 'description', severity: 'error', message: 'description 为空' });
}
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,
};
});
const validRules = rules.filter(r => !r.validationIssues);
const yamlContent = buildYaml(validRules);
const skippedCount = totalRows - rules.length;
return { rules, validRules, yamlContent, skippedCount };
}
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');
}
3.3 新增 src/rules/converters/dedup-prompt.ts
约束:AI 只做去重判定,严禁修改已有字段、禁止添加/删除规则。若输入 YAML 无重复,原样返回即可。
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.constraintTitle,
s.constraintLines.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[];
constraintTitle: string;
constraintLines: string[];
existingTitle: string;
noExisting: string;
userPrefix: string;
}> = {
'zh-CN': {
role: '你是规则去重判定助手。你只输出 YAML,不输出任何解释。',
taskTitle: '## 任务',
taskLines: [
'下面是已标准化的规则 YAML。你只负责对照"现有规则"为每条规则标注去重字段。',
'为每条规则补充以下字段(如果无重复则标注 none):',
'- duplicateOf: 重复的规则 ID(如 eslint/no-console、custom/my-rule)',
'- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none(无重复)',
'- duplicateReason: 仅 overlap 时必填,简要说明重叠原因',
],
rulesTitle: '## 判定规则',
rulesLines: [
'1. exact: id 完全相同,或 description + message 语义完全一致',
'2. overlap: 检测目标/场景部分重叠,但并非完全相同',
'3. none: 与现有规则无冲突',
],
constraintTitle: '## ⚠️ 严格约束(必须遵守)',
constraintLines: [
'1. 严禁修改任何已有字段的值(id、severity、description、message、languages、excludeLanguages)',
'2. 严禁添加新规则,严禁删除或合并规则',
'3. 规则数量必须与输入完全一致,顺序必须与输入完全一致',
'4. 你只允许添加三个字段: duplicateOf、duplicateLevel、duplicateReason',
'5. 如果某条规则与现有规则无任何重复,设置 duplicateLevel: none 即可,不需要补充 duplicateOf',
'6. 输出纯 YAML,不要用 markdown 代码块包裹',
],
existingTitle: '## 现有规则',
noExisting: '(无)',
userPrefix: '## 待去重的规则 YAML',
},
'en': {
role: 'You are a rule deduplication assistant. Output YAML only, no explanations.',
taskTitle: '## Task',
taskLines: [
'Below is standardized rule YAML. Only annotate dedup fields against the "Existing Rules" list.',
'For each rule, add (mark none if no conflict):',
'- duplicateOf: duplicated rule ID (e.g. eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: required only for overlap',
],
rulesTitle: '## Judgement Rules',
rulesLines: [
'1. exact: identical id, or semantically identical description+message',
'2. overlap: partially overlapping target/scenario',
'3. none: no conflict with existing rules',
],
constraintTitle: '## ⚠️ Strict Constraints (MUST follow)',
constraintLines: [
'1. Do NOT modify any existing field values (id, severity, description, message, languages, excludeLanguages)',
'2. Do NOT add, delete, or merge rules',
'3. Rule count and order must exactly match the input',
'4. Only add three fields: duplicateOf, duplicateLevel, duplicateReason',
'5. If a rule has no duplication, set duplicateLevel: none without duplicateOf',
'6. Output pure YAML, do NOT wrap in markdown code fences',
],
existingTitle: '## Existing Rules',
noExisting: '(none)',
userPrefix: '## YAML to deduplicate',
},
'ja': {
role: 'あなたはルール重複判定アシスタントです。YAML のみ出力し、説明は不要です。',
taskTitle: '## タスク',
taskLines: [
'以下は標準化されたルール YAML です。既存ルールと照合し、重複フィールドのみ注釈してください。',
'各ルールに以下を追加(重複がない場合は none と表記):',
'- duplicateOf: 重複ルール ID(例: eslint/no-console, custom/my-rule)',
'- duplicateLevel: exact / overlap / none',
'- duplicateReason: overlap 時のみ必須',
],
rulesTitle: '## 判定ルール',
rulesLines: [
'1. exact: ID が同一、または description+message が意味的に完全一致',
'2. overlap: 検出対象/シナリオが部分重複',
'3. none: 既存ルールと競合なし',
],
constraintTitle: '## ⚠️ 厳格な制約(必ず遵守)',
constraintLines: [
'1. 既存フィールド(id, severity, description, message, languages, excludeLanguages)の値を一切変更しない',
'2. ルールの追加、削除、統合を一切行わない',
'3. ルールの数と順序は入力と完全に一致させる',
'4. 追加できるフィールドは duplicateOf, duplicateLevel, duplicateReason のみ',
'5. 重複がないルールは duplicateLevel: none とし、duplicateOf は付けない',
'6. 純粋な YAML を出力し、markdown コードブロックで囲まない',
],
existingTitle: '## 既存ルール',
noExisting: '(なし)',
userPrefix: '## 重複排除対象の YAML',
},
};
3.4 修改 src/rules/import-service.ts
新增 ImportService.importTemplate 方法:
import { parseTemplate } from './converters/template-converter';
import { buildDedupOnlyPrompt } from './converters/dedup-prompt';
export class ImportService {
// ... 现有 registerConverter、convert、applyConversion 不动
async importTemplate(
srcPath: string,
name: string,
context: vscode.ExtensionContext,
): Promise<ConversionResult> {
// [1] 程序解析 + 准入校验(失败即 throw,由调用方 catch 弹错误提示)
const { rules, validRules, yamlContent, skippedCount } = parseTemplate(srcPath);
// [2] AI 去重(仅有效规则;错误规则不送 AI)
let dedupedValidRules: ImportableRule[] = validRules;
if (validRules.length > 0) {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];
const { system, user } = buildDedupOnlyPrompt(yamlContent, existingRules);
const dedupedYaml = await convertContentWithAI(user, context, system);
if (dedupedYaml) {
dedupedValidRules = parseImportableYaml(dedupedYaml);
} else {
// AI 去重失败,降级:有效规则不带 duplicateLevel,用户自行判断
vscode.window.showWarningMessage(t('import.dedupFailed'));
}
}
// [3] 合并
const errorRules = rules.filter(r => r.validationIssues?.length);
const allRules = [...dedupedValidRules, ...errorRules];
const exactCount = dedupedValidRules.filter(r => r.duplicateLevel === 'exact').length;
const overlapCount = dedupedValidRules.filter(r => r.duplicateLevel === 'overlap').length;
return {
rules: allRules,
yamlContent,
sourceFileName: path.basename(srcPath),
exactCount,
overlapCount,
skippedCount,
errorCount: errorRules.length,
};
}
}
注意:
loadActiveRules、convertContentWithAI、parseImportableYaml均为本文件已有函数/导入,无需额外 import。
3.5 修改 src/rules/import-preview.ts
原则:增量扩展,仅新增错误规则组渲染 + skippedCount 提示。现有 exact/overlap/none 三组逻辑不动。UI 样式沿用现有
renderSection,不做独立设计。
3.5.1 showImportPreview 改动
keepRule 初始化只计算非错误规则:
const keepRule: Record<string, boolean> = {};
for (const rule of result.rules) {
if (!rule.validationIssues?.length) {
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
}
}
3.5.2 新增 renderErrorSection 辅助函数
function renderErrorSection(rules: ImportableRule[]): string {
if (rules.length === 0) { return ''; }
const sectionId = 'section-error';
return `
<div style="margin-bottom:12px;">
<div class="section-header" onclick="toggleSection('${sectionId}')">
<span style="font-size:14px;">🚫</span>
<span class="section-title">${t('import.sectionInvalid')}(${rules.length})</span>
<span class="section-arrow">▼</span>
</div>
<div id="${sectionId}">
${rules.map(renderErrorCard).join('')}
</div>
</div>
`;
}
function renderErrorCard(rule: ImportableRule): string {
const issues = (rule.validationIssues || []).map(i =>
`<div style="color:#f48771;font-size:12px;margin-bottom:4px;">⚠ ${i.message}</div>`
).join('');
return `
<div class="rule-card" style="opacity:0.7;border-color:rgba(248,81,73,0.3);">
<div class="rule-card-header" style="cursor:default;">
<div class="rule-card-summary">
<span style="font-family:monospace;font-size:13px;font-weight:600;">${rule.id}</span>
<span style="color:#f48771;font-size:11px;font-weight:600;">${t('import.cannotImport')}</span>
</div>
</div>
<div class="rule-card-body" style="border-top:1px solid rgba(248,81,73,0.15);padding-top:8px;">
${issues}
<div style="color:#8b949e;font-size:11px;margin-top:6px;">
severity: ${rule.severity} | description: ${rule.description} | message: ${rule.message}
</div>
</div>
</div>
`;
}
3.5.3 renderPreviewHtml 分组逻辑
const errorRules = result.rules.filter(r => r.validationIssues?.length);
const cleanRules = result.rules.filter(r => !r.validationIssues?.length);
const exactRules = cleanRules.filter(r => r.duplicateLevel === 'exact');
const overlapRules = cleanRules.filter(r => r.duplicateLevel === 'overlap');
const noneRules = cleanRules.filter(
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
);
3.5.4 顶部 skippedRows 提示
const skippedHint = result.skippedCount
? `<div class="summary-bar" style="border-color:rgba(88,166,255,0.3);color:#58a6ff;">${t('import.template.skipped', { 0: String(result.skippedCount) })}</div>`
: '';
3.5.5 分组渲染顺序
${skippedHint}
${renderErrorSection(errorRules)}
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
3.5.6 validRules 全空时的处理
当 cleanRules.length === 0(文件全是错误规则):
// 确认按钮禁用 + 提示
const confirmDisabled = cleanRules.length === 0;
// 按钮: <button class="btn btn-primary" ${confirmDisabled ? 'disabled style="opacity:0.5;cursor:not-allowed;"' : ''} onclick="doConfirm()">
// 状态栏: 替换为 "无有效规则可导入,请修改文件后重新导入"
const emptyValidHint = cleanRules.length === 0
? `<div class="validation-error" style="display:block;">${t('import.emptyValidRules')}</div>`
: '';
3.5.7 确认导入时只写入有效规则
doConfirm 中 collectEditedRules 排除错误卡片(或直接沿用现有 keepRule 过滤——错误规则不在 keepRule 中,applyConversion 自动忽略)。
3.6 修改 src/views/setupView.ts
3.6.1 HTML(「自定义规则」区块新增 checkbox + 按钮)
<div class="field" style="margin-top:8px;">
<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>
<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>
3.6.2 webview script
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' });
}
3.6.3 Provider: onDidReceiveMessage 分流
import { exportTemplate } from '../rules/export-service';
// message handler:
case 'addRule':
await this.addRule(msg.name, msg.useTemplateMode);
await this.pushConfig();
break;
case 'exportTemplate':
await exportTemplate();
break;
3.6.4 Provider: addRule 方法签名改为支持分流
注意:
addRule当前参数为(name: string),改为(name: string, useTemplateMode?: boolean),原有 AI 分支保持不变。
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'] },
});
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, this.context,
);
});
const decision = await showImportPreview(conversion);
if (decision?.confirmed) {
const rulesDir = path.join(workspaceRoot, '.code-review', 'rules');
if (!fs.existsSync(rulesDir)) { fs.mkdirSync(rulesDir, { recursive: true }); }
const yamlFileName = name.endsWith('.yaml') ? name : `${name}.yaml`;
const yamlPath = path.join(rulesDir, yamlFileName);
if (fs.existsSync(yamlPath)) {
vscode.window.showErrorMessage(t('setup.ruleFileExists', { 0: name }));
return;
}
this.importService.applyConversion(conversion, decision, yamlPath);
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
vscode.window.showErrorMessage(msg);
}
return;
}
// ── 原 AI 链路,以下不动 ──
// ... 现有 addRule 逻辑
}
3.7 修改 src/activation/commands.ts
import { exportTemplate } from '../rules/export-service';
// 在 registerCommands 中追加:
context.subscriptions.push(
vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate()),
);
3.8 修改 package.json
contributes.commands 追加:
{
"command": "codeReviewer.exportTemplate",
"title": "Code Purifier: 导出规则模板"
}
3.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} |
import.template.skipped |
已跳过 {0} 行空数据 | Skipped {0} empty rows | {0} 行の空データをスキップしました |
import.sectionInvalid |
错误规则 | Error Rules | エラー行 |
import.cannotImport |
将自动丢弃,请修改文件后重新导入 | Will be auto-discarded, please fix the file and retry | 自動破棄されます、ファイルを修正して再インポートしてください |
import.dedupFailed |
AI 去重失败,规则将不带去重标记导入 | AI dedup failed, rules imported without dedup marks | AI 重複排除失敗、重複マークなしでインポート |
import.emptyValidRules |
无有效规则可导入,请修改文件后重新导入 | No valid rules to import, please fix the file and retry | 有効なルールがありません、ファイルを修正して再インポートしてください |
exportTemplate.saveLabel |
导出模板 | Export Template | エクスポート |
exportTemplate.success |
模板已导出 | Template exported | テンプレートをエクスポートしました |
exportTemplate.fail |
导出失败:{0} | Export failed: {0} | エクスポート失敗: {0} |
exportTemplate.openFolder |
打开文件夹 | Reveal in Folder | フォルダを開く |
四、验收标准
4.1 导出模板
- 侧边栏「自定义规则」区块出现「📤 导出模板」按钮
- 点击弹出保存对话框,默认文件名
code-review-rules-template.xlsx - 生成文件含 2 个 sheet:「规则」(表头+示例行)、「说明」(字段说明+填写规范)
- 列名:
id, severity, description, message, languages, excludeLanguages - 导出成功右下角提示,点击「打开文件夹」可定位文件
- 命令面板
Code Purifier: 导出规则模板触发同样流程 - 中/英/日三语切换下,按钮与提示文案正确
- 取消保存对话框不报错
4.2 模板导入
- 「自定义规则」区块出现 checkbox「从模板导入」
- 不勾选 → 走原 AI 链路,行为零回归
- 勾选 → 文件选择仅过滤
xlsx/xls - 非 Excel → 提示"文件格式错误"
- 损坏 Excel → 提示"文件格式错误"
- 表头非标准 → 提示"缺少列: ..."
- 标准模板 → 进入预览,exact 重复默认不勾选
- 预览含错误规则组(置顶,无 checkbox 只读)、+ 原有 3 组
- 空行被跳过 → 顶部提示"已跳过 N 行空数据"
- 确认导入写入
.code-review/rules/<name>.yaml - 准入校验失败仅提示,无残留文件
- severity 拼错行 → 错误规则组,标注原因,确认时自动丢弃
- description/message 为空 → 同上
- 表内 id 重复 → 不校验,按普通有效规则进 AI 去重
- 错误规则不参与 AI 去重
- validRules 为空 → 确认按钮禁用,提示"无有效规则可导入"
- AI 去重失败 → 降级,有效规则不带去重标记进预览,弹出 warning
- 导入速度明显快于 AI 链路
4.3 闭环验证
- 导出模板 → 不修改直接导入 → 预览出现
no-todo规则 - 导出模板 → 删示例行 → 填 2 条新规则 → 导入 → 预览 2 条新规则
- 导出模板 → 填与已有规则相同 id → 导入 → 预览标记 exact 重复
五、风险与应对
| 风险 | 等级 | 应对 |
|---|---|---|
| 导入侧 AI 链路被误改 | 中 | 不碰 ExcelConverter、prompt-builder.ts、原 addRule 分支 |
| AI 去重 prompt 越权修改字段 | 中 | prompt 增加严格约束段 + 预览人工确认兜底 |
| 去重 AI 故障(网络/限流) | 中 | 降级处理:有效规则不带去重标记进预览 + warning 提示 |
xlsx 写盘无权限 |
低 | try/catch + 友好提示 |
| 跨平台路径 | 低 | vscode.Uri.fsPath 不硬编码分隔符 |
| 国际化漏键 | 低 | 中英日三语同步增补 |
六、后续可扩展点(本次不做)
- 导出当前规则(备份/迁移)
- 模板多语言(根据语言切换示例)
- 模板版本号(用于兼容判断)
- YAML 直通链路校验补齐
- AI 链路校验补齐
- 纯本地去重函数