# 自动修复功能重构设计书 > 日期:2026-08-14 > 状态:待人类审批(Stage ④) > 流程:① 用户提出 → ② 需求澄清 → ③ 方案设计 ## 1. 背景与目标 现有 `src/fixer/fixer.ts` 基于 AI 生成修复文本,但未接线(`fixIssue`/`fixAll` 命令是空壳),且存在以下问题: - AI 修复一次调用一次修复,命中率低、上下文小、无法批量收敛 - 面板 webview 已发出 `fix`/`fixAll` 消息但无响应 - 单条修复按钮文案错用「全部修复」;AI tab 误显示「全部修复」按钮 本次重构目标:基于 linter 原生 `--fix` 提取**单条 fix 对象**,多轮循环验证收敛,支持 CodeAction 内联修复与面板修复,采用快照 + diff 反向撤销,保证一次撤销回到修复前状态。 ## 2. 需求澄清结论(Stage ② 共识) | 决策项 | 结论 | |--------|------| | 交互入口 | 编辑器 hover 悬浮框修复链接(CodeAction QuickFix)+ 审查面板修复按钮 + 一键修复全部 | | 修复来源 | 仅 linter 静态诊断;PMD/SQLFluff/JSP 无单条 fix 能力则不显示修复入口 | | 修复生成 | 优先 linter 原生 `--fix`,提取单条 fix 对象精确修复(ESLint/Stylelint) | | 修复策略 | 多轮循环:每轮修复后重跑 linter 验证,未消除继续修,轮次上限默认 3 可配置 | | 批量修复 | 保留面板「一键修复全部」,逐条循环,修不了的跳过计数 | | 面板撤销 | 每条修复独立记录 diff,撤销时当前文本定位后反向应用该条;可任意顺序撤销互不影响 | | Ctrl+Z | 每个修复会话独立一次编辑提交,多轮循环中间态不入 undo 栈,按会话后进先出逐步回退 | | 修复后刷新 | 自动重跑静态分析 → 波浪线更新/消失、CodeLens 重新获取、面板标记已修复(变绿+撤销按钮+不显示行号但记录行号)、未修复条目行号自动校正 | ## 3. 架构概览 废弃 AI 修复模块,改为 linter 原生 fix 驱动: ``` ┌─ UI 层 ──────────────────────────────────────────────┐ │ FixCodeActionProvider (hover 修复链接) ReviewPanel │ └───────────────┬───────────────────────┬──────────────┘ │ CodeAction / fix命令 │ fix/undo 消息 ┌───────────────▼───────────────────────▼──────────────┐ │ 核心层 FixEngine (src/fix/) │ │ fixDiagnostic() 多轮循环 内存模拟 → 单次提交 │ │ FixSessionManager 快照 + FixedEntry diff 栈 │ └──────┬───────────────────────────┬───────────────────┘ │ adapter.check(文本) │ linter fix 对象 ┌──────▼───────────────────────────▼───────────────────┐ │ 适配层 LinterAdapter.check() 提取 fix 到 LinterFix │ │ ESLint(msg.fix) / Stylelint(w.fix) / PMD/SQLFluff ✗ │ └──────────────────────┬───────────────────────────────┘ │ ┌──────────────────────▼───────────────────────────────┐ │ 基础层 Orchestrator(静态分析结果缓存) 配置 i18n │ └───────────────────────────────────────────────────────┘ ``` **数据流**: 1. `adapter.check()` 返回诊断 + `fix` 对象 → 存入 Orchestrator 静态分析结果缓存(按 uri) 2. 用户 hover 修复链接 / 面板点修复 → `codeReviewer.fixIssue` 命令 → `FixEngine.fixDiagnostic()` 3. 多轮循环在**内存字符串**上模拟(mockDocument),最终一次 `WorkspaceEdit` 全文替换提交 → undo 栈只有一次编辑 4. 成功记录 `FixedEntry` → 刷新:重跑静态分析 → markers 更新 → CodeLens 刷新 → 面板更新(已修复变绿/撤销按钮/无行号,未修复行号校正) ## 4. 文件变更清单 ### 新建 | 文件 | 职责 | |------|------| | `src/fix/fixEngine.ts` | 多轮修复循环核心 | | `src/fix/fixSession.ts` | 修复会话、FixedEntry diff 记录、撤销定位 | | `src/fix/codeActionProvider.ts` | 编辑器 QuickFix CodeAction 提供者 | | `src/utils/mockDocument.ts` | 从 jsp.ts 提取的 mockDocument(内存文本→TextDocument),供 fix 引擎复用 | | `src/test/fixEngine.test.ts` | 修复引擎测试 | | `src/test/fixSession.test.ts` | 会话撤销测试 | ### 修改 | 文件 | 变更 | |------|------| | `src/types.ts` | `LinterDiagnostic` 增 `fix?: LinterFix`;新增 `LinterFix` 接口 | | `src/adapters/eslint.ts` | 提取 `msg.fix` 填充 fix 字段 | | `src/adapters/stylelint.ts` | 提取 `w.fix` 填充 fix 字段 | | `src/orchestrator/orchestrator.ts` | 增静态分析结果缓存(uri→diagnostics/adapter/workingDir) | | `src/activation/commands.ts` | 实现 `fixIssue`/`fixAll`,新增 `undoFix`,review 更新缓存 | | `src/extension.ts` | 注册 FixCodeActionProvider | | `src/panel/webview.ts` | 修复状态渲染 + undo 消息 + 修正 fixAll 按钮 bug | | `src/config/fixer.ts` | `getFixMaxIterations()`(替代废弃的 contextLines) | | `src/i18n/messages.ts` | 新增 fix 相关 key,删除 fixNotAvailable | | `package.json` | 配置 `fixer.maxIterations`,移除 `fixer.contextLines` | ### 删除 | 文件 | 说明 | |------|------| | `src/fixer/fixer.ts` | 整个旧 AI 修复模块 | ## 5. 关键接口定义 ### 5.1 `src/types.ts` 新增 ```typescript export interface LinterFix { range: [number, number]; // 0-based 字符偏移,相对 lint 传入文本 text: string; } export interface LinterDiagnostic { severity: Severity; ruleId: string; message: string; range: vscode.Range; suggestion?: string; fix?: LinterFix; // 新增:可自动修复时存在 } ``` > ESLint 9 的 `LinterMessage.fix` 与 Stylelint 的 `Warning.fix?: EditInfo` 结构完全一致,均为 `{ range: [number, number], text: string }`,可直接透传。 ### 5.2 `src/fix/fixEngine.ts` ```typescript export interface FixResult { success: boolean; attempts: number; message?: string; } export async function fixDiagnostic( document: vscode.TextDocument, workingDir: string, adapter: LinterAdapter, diag: LinterDiagnostic, maxIterations: number ): Promise; ``` ### 5.3 `src/fix/fixSession.ts` ```typescript export interface FixedEntry { key: string; // `${ruleId}@${line}`(修复前原始行号) ruleId: string; line: number; // 修复前原始行号(撤销定位用,面板不显示) originalText: string; // 修复前该片段 newText: string; // 修复后该片段 source: 'linter'; } export class FixSessionManager { add(uri: vscode.Uri, entry: FixedEntry): void; get(uri: vscode.Uri, key: string): FixedEntry | undefined; undo(document: vscode.TextDocument, key: string): Promise; has(uri: vscode.Uri, key: string): boolean; clear(uri: vscode.Uri): void; } ``` ### 5.4 `src/fix/codeActionProvider.ts` ```typescript export class FixCodeActionProvider implements vscode.CodeActionProvider { constructor(orchestrator: Orchestrator); provideCodeActions(...): vscode.CodeAction[]; // QuickFix,command 指向 codeReviewer.fixIssue } ``` ### 5.5 Orchestrator 缓存 ```typescript setAnalysisResult(uri: vscode.Uri, result: { diagnostics: LinterDiagnostic[]; adapterId: string; workingDir: string; }): void; getAnalysisResult(uri: vscode.Uri): { diagnostics: LinterDiagnostic[]; adapterId: string; workingDir: string; } | undefined; ``` ### 5.6 命令协议 - 面板消息沿用现有 `{ type, line, ruleId, source }` - 新增 `{ type: 'undo', line, ruleId, source }` → `codeReviewer.undoFix` ## 6. 核心逻辑 ### 6.1 多轮循环(fixEngine.ts) ``` currentText = document.getText() for round in 1..maxIterations: mock = mockDocument(currentText, languageId, fileName) result = adapter.check(mock, workingDir) target = 找到 ruleId 匹配且行号最接近、带 fix 的诊断 if !target?.fix: success = false; message = '无法自动修复'; break newText = applyFix(currentText, target.fix) // 字符串级 offset 替换 currentText = newText // 预验证:重跑 check 若该问题消失 → success if 未收敛(达到轮次上限仍存在): success = false; message = '达到轮次上限仍未修复' 一次 WorkspaceEdit 将 document 全文替换为 currentText(单次提交) ``` ### 6.2 撤销(fixSession.ts) - 每次修复成功后,从最终提交前后提取片段记录 `FixedEntry` - 撤销:打开文档 → 行号优先匹配 `newText` 首行 → 全文搜索回退 → `WorkspaceEdit` 替换回 `originalText` - 先修 A 再修 B,撤销 A:在当前文本定位 A 的 newText 反向替换,B 保留 - 匹配失败(用户手动改动了相关区域):提示重新审查 ### 6.3 CodeAction - 从 Orchestrator 缓存取当前文档诊断,为每个带 `fix` 的生成 QuickFix CodeAction - 命令执行 `codeReviewer.fixIssue`(多轮 + 会话),不直接用 `action.edit` - hover 悬浮框自动显示修复链接(VSCode 标准行为) ### 6.4 面板状态 - 面板内部维护 `fixedEntries: Map` - linter tab = 新报告未修复条目(行号+修复按钮) + fixedEntries(变绿+撤销按钮+不显示行号) - 修复后自动刷新(重跑静态分析 → markers → CodeLens → 面板) - 顺带修复既有 bug:AI tab 误显示 fixAll 按钮、单条修复按钮文案错用「全部修复」 ### 6.5 适配器 fix 提取 | 适配器 | fix 来源 | 配置解析链 | |--------|----------|-----------| | ESLint | `msg.fix` | 全局 `linters.eslintConfigPath` → 项目 `eslint.config.*` → 内置规则 | | Stylelint | `warning.fix` | 全局 `linters.stylelintConfigPath` → 项目 `.stylelintrc` → 内置规则 | | PMD | ✗(无 --fix) | — | | SQLFluff | ✗(CLI 整文件操作) | — | | JSP | ✗(fix 偏移映射复杂,一期不支持) | — | ## 7. 技术选型与影响范围 - **mockDocument**:jsp.ts 已有完整实现,提取复用,零新增依赖 - **fix 对象结构**:ESLint `LinterMessage.fix` / Stylelint `EditInfo` 均为 `{range, text}`,已验证 - **影响面**:types.ts(契约)、eslint/stylelint 适配器(提取 fix)、orchestrator(缓存)、commands/extension/panel/i18n/config、删除旧 fixer 模块 - **撤销限制**:依赖当前文档文本匹配,修复后用户大幅改动相关区域会撤销失败并提示 ## 8. 配置变更 ```jsonc // package.json configuration.properties 新增 "vscode-code-reviewer.fixer.maxIterations": { "type": "number", "default": 3, "description": "单条问题自动修复的最大循环轮次" } // 移除 "vscode-code-reviewer.fixer.contextLines" // AI 修复遗留,废弃 ``` ## 9. 测试计划 - `fixEngine.test.ts`:单条可修问题多轮收敛、不可修问题返回失败、达到轮次上限 - `fixSession.test.ts`:顺序修复→撤销、交错撤销(先 A 后 B 撤销 A 保留 B) - 现有 `npm test`(84 tests)回归通过 ## 10. 验证顺序 `lint → compile → test`(`npm run lint` / `npm run compile` / `npm test`)