Files
2026Technology-Competition/docs/superpowers/specs/2026-07-30-export-template-design.md
T
范智鹏 5848eaa82a feat: Linter 规则精细化增强 + 模板导入/导出闭环
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
2026-07-30 23:17:20 +08:00

41 KiB
Raw Blame History

自定义规则 · 模板导出与导入功能设计书

项目: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 链路的校验补齐(本次不动)。
  • 不触碰:现有 ExcelConverterprompt-builder.tsaddRule 原 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: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 行):

<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) 流程的一部分(填名 → 选文件 → 自动转换)。
  • 没有任何 checkboxtype="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 校验策略(两层:准入 + 数据行)

校验分两层,准入校验失败硬报错,数据行问题软提示进预览。

第一层:准入校验(硬报错,不进预览)

第一关:文件格式校验

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" 记录 issueseverity 临时填 '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 + 导出按钮;addRuleuseTemplateMode 分流;新增 case 'exportTemplate'
修改 src/activation/commands.ts 注册 codeReviewer.exportTemplate 命令
修改 package.json contributes.commands 增 1 条
修改 src/i18n/messages.ts checkbox/校验/去重 prompt/导出 文案(中英)

不动ExcelConverterprompt-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: 重复的规则 IDlinter 如 eslint/no-console;自定义如 custom/my-rule',
      '- duplicateLevel: exact(完全相同)/ overlap(部分重叠)/ none',
      '- duplicateReason: overlap 时必填',
    ],
    rulesTitle: '## 判定规则',
    rulesLines: [
      '1. exactid 相同,或 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.tsUI + 分流)

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 导出模板

  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/<name>.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 链路被误改 评审约束:不动 ExcelConverterprompt-builder.tsimport-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) 做纯本地比对。