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
907 lines
41 KiB
Markdown
907 lines
41 KiB
Markdown
# 自定义规则 · 模板导出与导入功能设计书
|
||
|
||
> 项目: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/<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 行):
|
||
|
||
```html
|
||
<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 | 排除的语言 | 逗号分隔,可留空 | |
|
||
|
||
**填写说明:**
|
||
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<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` 扩展**:
|
||
|
||
```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<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`(程序解析 + 准入校验 + 数据行校验)
|
||
|
||
```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)
|
||
|
||
```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<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)
|
||
|
||
```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<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 改动**(「自定义规则」区块):
|
||
|
||
```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 状态):
|
||
|
||
```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<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`(注册导出命令)
|
||
|
||
```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/<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 链路被误改 | 中 | 评审约束:不动 `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)` 做纯本地比对。
|