Files
2026Technology-Competition/docs/superpowers/specs/2026-07-31-import-error-rule-edit-design.md
T
范智鹏 247f19fd44 feat: 方法级代码审查 + 模板导入/预览增强 + SQLFluff 方言 + AI 空响应报错修复
- 方法级审查:CodeLens 触发 + 单次 AI 调用(规则匹配 + 6 维度深度审查),新增 method-extractor / status-cache / codeLensProvider
- 模板导入:severity 保留原始值 + 占位 id、去重对照统一 known-rules、重复提示条双语翻译、箭头展开/折叠 UI、520 条静态规则补 zh/ja 翻译
- SQL:sql-lint 重命名 sqlfluff + sqlfluff.dialect 方言可配置 + 默认方言调整
- ESLint:v9 flat config 接线修复(overrideConfigFile)+ legacy 迁移提示
- AI:空响应 EmptyContentError + 重试一次 + max_tokens 截断专用报错
- JSP:整文件检查走 PMD JSP 规则集 + scriptlet 包装解析 + 行号映射
- 诊断按 severity + 行号排序
2026-08-03 22:53:20 +08:00

17 KiB
Raw Blame History

导入预览 · 错误规则可编辑与「添加」设计书

版本:v1.0 日期:2026-07-31 适用项目:vscode-code-reviewer 参考文档:2026-07-30-export-template-design-v2.md(错误规则组雏形)、2026-07-25-import-preview-edit-design.md(预览编辑)


一、方案概览

1.1 目标

模板导入(勾选「使用模板文件导入」)进入预览后,错误规则不再是纯只读展示,而是:

  • 卡片提供完整编辑表单(与有效规则一致:id / severity / description / message / languages / excludeLanguages
  • 每张错误卡片有独立「添加」按钮,点击后:
    1. 重新校验格式id/description/message 非空、severity 合法)
    2. 校验通过后单条 AI 去重(复用 buildDedupOnlyPrompt),失败先重试一次,仍失败降级为 none
    3. 按去重结果(exact/overlap/none)把该规则移入对应分区,变为正常可编辑规则(带保留/注释切换)
  • 添加时若 id 与预览中已有规则重复 → 阻止并提示修改 id
  • 未添加的错误规则维持现状:确认导入时自动丢弃、不参与校验
  • 确认写盘必须包含已添加的规则(避免走 raw yaml 路径丢失)

1.2 数据流

现状(仅展示):
  parseTemplate → errorRules(带 validationIssues)→ 预览只读展示 → 确认时自动丢弃

改造后:
  parseTemplate → errorRules → 预览可编辑错误卡片
      ├─ 用户编辑字段 → 点击「添加」
      │     ├─ [扩展端] 格式校验 → 失败 → postMessage addError(卡片内提示)
      │     ├─ [扩展端] id 冲突检查(对照预览已有有效规则)→ 冲突 → addError
      │     ├─ [扩展端] AI 单条去重(重试 1 次 → 降级 none)→ postMessage ruleAdded
      │     └─ [前端] DOM 手术:错误卡片移入 exact/overlap/none 分区,
      │                   去除 data-error、换保留/注释按钮、更新计数
      └─ 确认导入
            ├─ 已添加或已编辑 → 前端回传 editedRules(含已添加规则)→ renderRulesToYaml 写盘
            └─ 无编辑无添加 → 走原 buildFinalYamlFromRaw(零回归)

1.3 范围

类型 内容
错误卡片完整编辑表单;「添加」按钮(校验 + 单条 AI 去重 + 移入分区);id 冲突拦截;去重失败重试/降级;确认写盘含已添加规则;计数动态更新
不含 非模板导入路径(AI 链路无 validationIssues,不受影响);批量「全部添加」按钮;错误规则的本地重复检查
不触碰 parseTemplate 校验逻辑、buildDedupOnlyPromptapplyConversion、其余转换器

二、架构设计

2.1 模块划分

src/rules/
├── import-service.ts    ← 修改: 新增 export async function dedupSingleRule()
├── import-preview.ts    ← 修改: 错误卡片可编辑 + 添加流程(前端 JS + 扩展消息处理)
└── import-types.ts      ← 不改(复用现有类型)

src/i18n/messages.ts     ← 修改: 新增 key,调整 import.cannotImport 文案

2.2 关键接口

// import-service.ts 新增导出函数
export interface DedupResult {
  duplicateLevel: 'exact' | 'overlap' | 'none';
  duplicateOf?: string;
  duplicateReason?: string;
}

// 单条规则 AI 去重:失败重试 1 次,仍失败返回 null(调用方降级为 none)
export async function dedupSingleRule(
  rule: ImportableRule,
  context: vscode.ExtensionContext,
): Promise<DedupResult | null>
// import-preview.ts — Webview 消息协议扩展

// 前端 → 扩展
interface AddErrorRuleMessage {
  type: 'addErrorRule';
  ruleId: string;                       // 原始卡片 id(用于在 result.rules 中定位)
  rule: {                               // 当前卡片全部字段(含用户编辑)
    id: string;
    severity: string;
    description: string;
    message: string;
    languages?: string[];
    excludeLanguages?: string[];
  };
}

// 扩展 → 前端
interface AddErrorMessage {
  type: 'addError';
  ruleId: string;
  message: string;
}
interface RuleAddedMessage {
  type: 'ruleAdded';
  ruleId: string;                       // 原始卡片 id(前端按此定位 DOM)
  id: string;                           // 去重后的最终 id(可能被用户编辑过)
  duplicateLevel: 'exact' | 'overlap' | 'none';
  duplicateOf?: string;
  duplicateReason?: string;
  dedupFailed: boolean;                 // true 表示降级为 none
}

convertContentWithAI 增加可选第 4 参 quiet?: boolean(去重失败时抑制内置 error toast,改由前端降级提示)。现有调用点均传 3 参,向后兼容。


三、详细实现

3.1 import-service.ts:新增 dedupSingleRule

export interface DedupResult {
  duplicateLevel: 'exact' | 'overlap' | 'none';
  duplicateOf?: string;
  duplicateReason?: string;
}

export async function dedupSingleRule(
  rule: ImportableRule,
  context: vscode.ExtensionContext,
): Promise<DedupResult | null> {
  const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
  const existingRules = workspaceRoot ? loadActiveRules(workspaceRoot) : [];

  const singleYaml = [
    `- id: ${rule.id}`,
    `  severity: ${rule.severity}`,
    `  description: ${rule.description}`,
    `  message: ${rule.message}`,
    ...(rule.languages?.length ? [`  languages: [${rule.languages.join(', ')}]`] : []),
    ...(rule.excludeLanguages?.length ? [`  excludeLanguages: [${rule.excludeLanguages.join(', ')}]`] : []),
  ].join('\n');

  const { system, user } = buildDedupOnlyPrompt(singleYaml, existingRules);

  for (let attempt = 0; attempt < 2; attempt++) {
    const out = await convertContentWithAI(user, context, system, true); // quiet
    if (!out) continue;
    const parsed = parseImportableYaml(out);
    if (parsed.length === 0) continue;
    const r = parsed[0];
    return {
      duplicateLevel: r.duplicateLevel ?? 'none',
      duplicateOf: r.duplicateOf,
      duplicateReason: r.duplicateReason,
    };
  }
  return null;
}

3.2 import-service.tsconvertContentWithAI 增加 quiet 参数

export async function convertContentWithAI(
  content: string,
  context: vscode.ExtensionContext,
  systemPrompt?: string,
  quiet?: boolean,
): Promise<string | null> {
  // ... getApiKey 失败:quiet 时仅返回 null,不弹 toast
  // ... provider.chat 异常:quiet 时仅返回 null,不弹 toast
  // ... 其余逻辑不变
}

3.3 import-preview.ts:错误卡片渲染改造

废弃 renderErrorCard / renderErrorSection 的只读版,统一由 renderRuleCard 承担,新增 isError 分支:

renderRuleCard(rule, { isError })
  ├─ 普通卡片:现状逻辑不变
  └─ 错误卡片:
       ├─ 卡片属性 data-ruleid + data-error="true" + 错误边框(保留现 opacity/border 样式)
       ├─ 折叠态摘要:id(可编辑)+ 错误徽标(新 i18n,如「需修复后添加」)
       ├─ 展开态表单:与普通卡片完全一致(id 可改、severity 下拉、
       │             description/message textarea、languages/excludeLanguages 标签)
       ├─ 表单顶部:错误原因列表(复用现 issues 渲染)
       ├─ 顶部操作区:用「添加」按钮替代「保留/注释」toggle(新 i18n
       └─ 无 duplicateInfo(尚未去重)

默认展开错误卡片(body-<id> display:block),让用户立即看到错误原因。

分区容器加 data-section 属性(前端 DOM 手术定位用):

  • 错误分区容器:data-section="error"(原 renderErrorSection,标题/图标不变)
  • renderSection 三个分区容器分别加 data-section="exact" | "overlap" | "none"

顶部 summary 计数加 id(前端更新用):

  • ⛔ 完全重复 N 条<span id="count-exact">
  • ⚠️ 部分重叠 N 条<span id="count-overlap">
  • ✅ 无重复 N 条<span id="count-none">
  • 错误分区标题计数 → <span id="count-error">(放在 renderErrorSection 的 section-title 内)

确认按钮不再静态禁用:改为 JS 动态控制。初始无有效规则时保持禁用 + 显示 emptyValidHint;一旦「添加」成功移入有效分区,JS 启用按钮并隐藏提示。

3.4 import-preview.ts:扩展端消息处理

showImportPreviewonDidReceiveMessage 增加分支:

panel.webview.onDidReceiveMessage(async (msg) => {
  if (msg.type === 'toggleRule') {
    keepRule[msg.ruleId] = msg.keep;
  } else if (msg.type === 'addErrorRule') {
    await handleAddErrorRule(msg, result, keepRule, context, panel);
  } else if (msg.type === 'confirm') {
    resolve({ keepRule, confirmed: true, editedRules: msg.editedRules });
    panel.dispose();
  } else if (msg.type === 'cancel') {
    resolve(null);
    panel.dispose();
  }
});

handleAddErrorRule 逻辑(顺序严格):

async function handleAddErrorRule(msg, result, keepRule, context, panel) {
  const rule = msg.rule;

  // [1] 格式校验(复用前端同一套规则)
  const err = validateRule(rule);                       // id/severity/description/message
  if (err) {
    panel.webview.postMessage({ type: 'addError', ruleId: msg.ruleId, message: err });
    return;
  }

  // [2] id 冲突检查(对照 result.rules 中非错误规则,忽略大小写)
  const conflict = result.rules.some(r =>
    !r.validationIssues?.length && r.id.toLowerCase() === rule.id.toLowerCase()
  );
  if (conflict) {
    panel.webview.postMessage({
      type: 'addError', ruleId: msg.ruleId,
      message: t('import.idConflict', { 0: rule.id }),
    });
    return;
  }

  // [3] AI 单条去重(内部已重试 1 次),失败降级 none
  const dedup = await dedupSingleRule(rule, context);
  const level = dedup?.duplicateLevel ?? 'none';
  const dedupFailed = !dedup;

  // [4] 更新 result.rules 中该条规则(按原始 id 定位)
  const idx = result.rules.findIndex(r => r.id === msg.ruleId);
  if (idx >= 0) {
    result.rules[idx] = {
      ...result.rules[idx],
      id: rule.id,
      severity: rule.severity,
      description: rule.description,
      message: rule.message,
      languages: rule.languages,
      excludeLanguages: rule.excludeLanguages,
      duplicateLevel: level,
      duplicateOf: dedup?.duplicateOf,
      duplicateReason: dedup?.duplicateReason,
      validationIssues: undefined,
    };
  }

  // [5] keepRule 按新 id 记录(exact → false
  keepRule[rule.id] = level !== 'exact';

  // [6] 通知前端移动卡片
  panel.webview.postMessage({
    type: 'ruleAdded',
    ruleId: msg.ruleId,            // 原始 id,前端定位 DOM
    id: rule.id,                   // 新 id
    duplicateLevel: level,
    duplicateOf: dedup?.duplicateOf,
    duplicateReason: dedup?.duplicateReason,
    dedupFailed,
  });
}

function validateRule(rule): string | null {
  if (!rule.id || !rule.id.trim()) return t('import.validationIdEmpty');
  if (!['error', 'warning', 'info'].includes(rule.severity)) return t('import.validationSeverityInvalid');
  if (!rule.description || !rule.description.trim()) return t('import.validationDescEmpty', { 0: rule.id });
  if (!rule.message || !rule.message.trim()) return t('import.validationMsgEmpty', { 0: rule.id });
  return null;
}

context 需传入 showImportPreview(新增参数)或在模块内暂存——采用新增参数showImportPreview(result, context)

3.5 import-preview.ts:前端 JS 改造

新增 / 修改函数:

// 从单张卡片提取当前字段(供添加与校验复用;从 collectEditedRules 抽取公共逻辑)
function collectCardRule(ruleId) { /* 读 id-display-input / select / textareas / tag-lists */ }

let addedRules = 0;                    // 已成功添加的错误规则数

// 点击「添加」
function addErrorRule(ruleId) {
  const btn = document.querySelector(`[data-addbtn="${ruleId}"]`);
  btn.disabled = true; btn.textContent = ADDING_TEXT;
  const rule = collectCardRule(ruleId);
  vscode.postMessage({ type: 'addErrorRule', ruleId, rule });
}

// 接收扩展消息
window.addEventListener('message', e => {
  const msg = e.data;
  if (msg.type === 'addError') {
    // 卡片内展示 msg.message,恢复添加按钮可点击
  } else if (msg.type === 'ruleAdded') {
    moveCardToSection(msg);
  }
});

function moveCardToSection(msg) {
  const card = document.querySelector(`.rule-card[data-ruleid="${msg.ruleId}"]`);
  // 1. 更新 id 相关:card.dataset.ruleid = msg.id;两个 id input 值为 msg.id
  // 2. 移除 data-error 与错误边框样式
  // 3. 还原「添加」按钮为保留/注释 toggle(按 keepRule 状态)—— 需从扩展同步 keep 状态:
  //    msg.duplicateLevel === 'exact' 时默认注释态,否则保留态
  // 4. 更新徽标:exact →「将注释」;overlap/none →「保留」(badge-exact/overlap/none
  // 5. 追加 duplicateInfoexact/overlap 文案,复用现有拼接逻辑)
  // 6. 移入对应分区:document.querySelector(`[data-section="${section}"]`).appendChild(card)
  // 7. addedRules++;更新计数与确认按钮状态
}

// 分区标题计数(N 条)与顶部 summary 计数统一重算
function updateSectionCounts() {
  // 按 data-section 遍历,重算 4 个分区标题计数 + count-exact/overlap/none
  // 错误分区为空 → 隐藏整个分区容器
  // 无任何有效规则 → 禁用确认按钮 + 显示 emptyValidHint;否则启用
}

// 确认:hasEdits 或 addedRules>0 时必带 editedRules
function doConfirm() {
  const err = validate();
  if (err) { /* 现逻辑 */ return; }
  const edited = collectEditedRules();
  const hasEdits = Object.keys(editedRules).length > 0;
  const withData = (hasEdits || addedRules > 0) ? edited : undefined;
  vscode.postMessage({ type: 'confirm', editedRules: withData });
}

updateSummary()(保留/注释计数)已按 data-error 跳过错误卡片,无需改动;moveCardToSection 移除 data-error 后该卡片自动纳入统计。

3.6 关键边界

边界 处理
添加时 id 冲突(预览内已有有效规则) addError 提示改 id,卡片留在错误分区
AI 去重首次失败 自动重试 1 次(共 2 次尝试,quiet 模式不弹错)
重试仍失败 降级 none 移入无重复分区,前端提示「AI 去重失败,已以无重复方式添加」
添加后 id 被修改导致与后续规则重复 后续添加时冲突检查覆盖全量有效规则,会拦截
只添加未编辑字段 addedRules>0 仍回传 editedRules,走 renderRulesToYaml,已添加规则不会丢失
错误规则未添加即确认 维持现状:data-error 卡片被 collectEditedRules/validate 跳过,自动丢弃
初始无有效规则 确认按钮禁用;「添加」成功第一条后 JS 启用
未配置 API Key convertContentWithAI 返回 nullquiet),重试后降级 none,流程不中断

3.7 i18n 新增 / 调整

key zh-CN en ja
import.add(新增) 添加 Add 追加
import.adding(新增) 校验并去重中... Validating & deduping... 検証・重複排除中...
import.idConflict(新增) id {0} 与已有规则重复,请修改 id id {0} conflicts with an existing rule, change the id id {0} が既存ルールと重複、id を変更してください
import.addDedupFallback(新增) AI 去重失败,已以无重复方式添加 AI dedup failed, added as no-duplicate AI 重複排除失敗、重複なしとして追加
import.validationSeverityInvalid(新增) severity 非法 Invalid severity severity が不正です
import.cannotImport(调整) 需修复后点击添加 Fix then click Add 修正して「追加」をクリック

四、文件变更清单

文件 操作 内容
src/rules/import-service.ts 修改 新增 dedupSingleRule(含 DedupResult 接口);convertContentWithAI 增加 quiet? 参数
src/rules/import-preview.ts 修改 错误卡片完整表单 + 添加流程;showImportPreview(result, context)handleAddErrorRule;前端 addErrorRule/moveCardToSection/updateSectionCounts/doConfirm;分区 data-section 与计数 id;确认按钮动态控制
src/i18n/messages.ts 修改 新增 5 个 key,调整 import.cannotImport 三语文案
src/rules/import-types.ts 不改
src/rules/converters/template-converter.ts 不改 校验逻辑不动

五、验证

验证顺序 lint → compile(当前仓库无测试文件):

  • npm run lintESLint src/
  • npm run compiletsc
  • 手动验证(Extension Dev Host):
    1. 导出模板 → 填一行错误数据(如空 description、拼错 severity)→ 模板导入 → 预览中错误卡片可展开编辑
    2. 修复后点「添加」→ 卡片移入对应分区、计数更新、可切换保留/注释
    3. 不改任何字段再添加一条 → 确认导入 → 写盘 YAML 含已添加规则
    4. id 冲突 → 添加被拦截并提示
    5. 断网/无 Key → 重试后降级无重复分区,卡片有降级提示
    6. 未添加的错误规则 → 确认后自动丢弃