# 自定义规则 · 模板导出与导入功能设计书 > 项目: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 顺带完成去重判定。该模式存在三个痛点: 1. **无标准模板**:用户批量录入规则时无从获取标准格式,凭经验构造导致列名错乱、AI 解析失败率高。 2. **解析全靠 AI**:即使是结构化的 Excel,也要走 AI 解析,耗时长、消耗 token、字段映射可能误判。 3. **去重与解析耦合**:去重判定由 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`: ```ts interface CustomRule { id: string; severity: 'error' | 'warning' | 'info'; description: string; message: string; languages?: string[]; excludeLanguages?: string[]; } ``` `src/rules/import-types.ts` 扩展: ```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/.yaml ``` ### 2.3 去重机制:由 AI 完成(关键事实) **去重判定由 AI 在解析阶段一并完成,本地无独立去重函数。** 证据: - `prompt-builder.ts:83-85` 让 AI 输出 `duplicateOf/duplicateLevel/duplicateReason`。 - `prompt-builder.ts:463` `buildDedupPromptSection(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 行): ```html
``` - **没有独立「导入」按钮**:导入是 `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 | 排除的语言 | 逗号分隔,可留空 | | **填写说明:** 1. severity 仅接受 error / warning / info 三个值。 2. languages / excludeLanguages 多值用英文逗号分隔。 3. 示例行可删除,仅作填写参考。 4. 该模板可直接用于「从模板导入」功能回环校验。 ### 3.2 导出模板交互 1. 用户在侧边栏「自定义规则」区块点击「📤 导出模板」按钮。 2. 弹出系统保存对话框,默认文件名 `code-review-rules-template.xlsx`,过滤器 `Excel (*.xlsx)`。 3. 用户选定位置后写盘。 4. 右下角信息提示:`✓ 模板已导出`,附带「打开文件夹」按钮。 5. 用户填写模板 → 回到插件,勾选「从模板导入」→ 点「添加」→ 预览 → 确认写入。 ### 3.3 模板导入交互 ``` 侧边栏「自定义规则」区块: [规则名输入框] [+ 添加] ☐ 从模板导入(跳过 AI 解析,仅支持 Excel 模板) ← 新增 checkbox [📤 导出模板] ← 新增按钮 ``` - **不勾选**(默认):点「添加」→ 走现有 `addRule` AI 链路(支持 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 校验策略(两层:准入 + 数据行) 校验分两层,准入校验失败硬报错,数据行问题软提示进预览。 #### 第一层:准入校验(硬报错,不进预览) **第一关:文件格式校验** ```ts 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')); } ``` **第二关:数据结构校验(是不是模板文件)** ```ts const sheet = wb.Sheets[wb.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json>(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` 扩展**: ```ts 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`(导出模板) ```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 { 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`(程序解析 + 准入校验 + 数据行校验) ```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>(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) ```ts 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 = { '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) ```ts 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 { // [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 改动**(「自定义规则」区块): ```html
${t('setup.ruleNameRequired')}
${t('setup.ruleNameHint')}
``` **webview script**(获取 checkbox 状态): ```js 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` 分流**: ```ts 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` 方法分流**: ```ts private async addRule(name: string, useTemplateMode?: boolean): Promise { 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`(注册导出命令) ```ts import { exportTemplate } from '../rules/export-service'; // 在 registerCommands 中追加 context.subscriptions.push( vscode.commands.registerCommand('codeReviewer.exportTemplate', () => exportTemplate()), ); ``` ### 4.8 修改 `package.json` `contributes.commands` 追加: ```json { "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 导出模板 1. 侧边栏「自定义规则」区块出现「导出模板」按钮。 2. 点击弹出保存对话框,默认文件名 `code-review-rules-template.xlsx`。 3. 生成文件含 2 个 sheet:「规则」(表头+示例行)、「说明」(字段说明+填写规范)。 4. 列名依次为 `id, severity, description, message, languages, excludeLanguages`。 5. 导出成功后右下角提示,点击「打开文件夹」可定位文件。 6. 命令面板执行 `Code Purifier: 导出规则模板` 触发同样流程。 7. 中英文/日文切换下,按钮文案与提示信息正确。 8. 取消保存对话框时不报错、无残留。 ### 6.2 模板导入 1. 「自定义规则」区块出现 checkbox「从模板导入」。 2. 不勾选时点「添加」,走原 AI 链路,行为与现状完全一致(零回归)。 3. 勾选时点「添加」,文件选择对话框仅过滤 `xlsx/xls`。 4. 选非 Excel 文件(如 .txt)→ 提示"文件格式错误",不进入预览。 5. 选损坏的 .xlsx → 提示"文件格式错误",不进入预览。 6. 选表头非标准的 .xlsx → 提示"不是模板文件,缺少列: ...",不进入预览。 7. 选标准模板 → 进入预览,exact 重复默认不勾选。 8. 预览界面复用 `showImportPreview`,新增「错误规则」组(置顶,无 checkbox 只读),其余三组渲染逻辑不变。 9. 确认导入后写入 `.code-review/rules/.yaml`。 10. 导入速度明显快于 AI 链路(解析阶段无 AI 调用)。 11. 准入校验失败时右下角错误提示,无残留文件。 12. 数据行有 severity 拼错时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。 13. 数据行 description/message 为空时,该行进入"错误规则"组,标注错误原因,确认导入时自动丢弃。 14. 规则文件内多条 id 相同的规则,不校验表内重复,按普通规则进入有效规则组,交由 AI 去重环节处理。 15. 空行被跳过时,预览面板顶部提示"已跳过 N 行空数据"(N 来自 `skippedCount`)。 16. 错误规则不参与 AI 去重,无 duplicateLevel 标记。 17. 确认导入时只写入选中的有效规则,错误规则自动丢弃,导入流程正常完成(不阻塞)。 18. 文件全是错误规则(validRules 为空)时,预览面板禁用"确认导入"按钮,提示"无有效规则可导入,请修改文件后重新导入"。 ### 6.3 闭环验证 1. 导出模板 → 不修改直接勾选「从模板导入」导入 → 预览出现 `no-todo` 规则(示例行)。 2. 导出模板 → 删除示例行 → 填入 2 条新规则 → 导入 → 预览出现 2 条新规则。 3. 导出模板 → 填入与工作区已有规则相同的 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)` 做纯本地比对。