Files
2026Technology-Competition/docs/superpowers/specs/2026-08-19-fix-preview-design.md
T
范智鹏 d16d680f0f feat: AI 修复链路扩展 + 修复预览两步确认 + 自定义规则/AI 审查条目支持 AI 修复
- AI 修复:无原生 fix 的 linter 条目走 AI 修复(aiFixEngine/fixPrompt,AI 重检收敛),自定义规则与 AI 审查条目新增 AI 修复按钮(customFixEngine),自定义规则条目展开显示 AI 建议(suggestion 字段)

- 修复预览:面板触发修复先用内置 diff 预览,面板内「应用/取消」两步确认后才写入(fixPreview/fixPending,单条与分 tab 批量均支持)

- 修复面板交互:linter/custom/ai 分 tab「全部修复」、已修复+撤销、pending 按钮状态同步

- bug 修复:修复按钮失败后卡  不恢复;custom/ai 修复不稳定(空修复重试、AI 重检收敛判定放宽、降级接受最后一次有效修复)

- 移除 AI codeDiff 展示块
2026-08-20 22:26:30 +08:00

272 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 修复前 Diff 预览确认功能设计书
> 日期:2026-08-19
> 状态:已批准(Stage ④ 通过);2026-08-19 修订确认交互为「面板内两步确认」
> 流程:① 用户提出 → ② 需求澄清 → ③ 方案设计 → ④ 人类审批
## 1. 背景与目标
当前自动修复(`src/fix/`)在用户点击「修复 / AI 修复 / 全部修复」后**立即写盘**,用户无法在写入前确认改动内容,误修后只能依赖撤销功能回退。
本次功能目标:在面板触发的修复**应用前**,用 VSCode 内置 diff 编辑器展示「修改前 → 修改后」的代码对比,用户确认后才真正写入。
## 2. 需求澄清结论(Stage ② 共识)
| 决策项 | 结论 |
|--------|------|
| 触发时机 | 修复**前**预览确认,确认后才写入 |
| 展示方式 | VSCode 内置 diff 编辑器(`vscode.diff`,左旧右新) |
| 预览范围 | 单个修复 + 全部修复都走预览 |
| 全部修复 | 合并成一个 diff,确认后一次性应用 |
| AI 修复 | 原生 fix 与 AI fix 两条路径都预览 |
| 入口范围 | **仅面板按钮**预览;hover 快速修复保持直接应用(零行为变化) |
| 确认交互 | **面板内两步确认**:点「修复」→ 打开 diff + 面板按钮变「应用/取消」(常驻不消失);回面板点「应用」才写入。「全部修复」同理(「全部应用/取消」) |
> 修订说明:初版设计为 diff + `showInformationMessage` 确认对话框,实测发现非模态通知几秒后自动消失(返回值 undefined 被当取消并关闭 diff)。改为面板内两步确认——按钮翻转用 webview 轻量消息完成,不全量重建面板,滚动/标签页不丢失。
## 3. 现状关键点(已核实)
- `fixEngine.ts:123-129``aiFixEngine.ts:164-170`:多轮循环在**内存字符串**上计算最终 `currentText`,最后一步才用整文档 `WorkspaceEdit` 写入。
- 计算与写入天然可分,只需让引擎支持"只算不写"(`dryRun`)即可在写入前弹 diff。
- 引擎内部已用 `mockDocument` 对内存文本执行 `adapter.check`,支持对累积文本反复计算,无需改核心算法。
## 4. 架构概览
```
面板 fix/fixAll 消息 → codeReviewer.fixIssue / codeReviewer.fixAll
│ (origin === 'panel')
FixEngine/AiFixEngine (dryRun=true) → 返回最终 newText(不写盘)
fixPreview.openPreviewDiff() → vscode.diff 内置 diff 编辑器(左原文本 / 右新文本)
FixPendingStore.setSingle/setBatch + ReviewPanel.postMessage('pending'/'batchPending')
│ 面板条目按钮翻转:「修复」→「应用 / 取消」
codeReviewer.applyFixPreview / applyAllPreview ← 面板「应用」按钮
fixPreview.applyNewText() → 整文档 WorkspaceEdit 一次性写入 + recordFixes + save + refresh
codeReviewer.cancelFixPreview / cancelAllPreview → 关闭 diff + 还原按钮(不写入)
```
**数据流**
1. `fixIssue`panel/ `fixAll``dryRun=true` 调用修复引擎,拿到最终 `newText`**不写盘**。
2. `openPreviewDiff``TextDocumentContentProvider`scheme `codeReviewerPreview`)提供新文本,`vscode.diff` 打开内置 diff 编辑器。
3. 结果存入 `FixPendingStore`,向面板 `postMessage` 翻转按钮(`pending` 单条 / `batchPending` 全部)——「修复」隐藏、「应用/取消」显示,**常驻不消失**。
4. 用户回面板点「应用」→ `applyFixPreview`/`applyAllPreview` 整文档 `WorkspaceEdit` 写入、记录撤销会话、保存、刷新;点「取消」→ `cancelFixPreview`/`cancelAllPreview` 关闭 diff、还原按钮、不写入。
## 5. 文件变更清单
### 新建
| 文件 | 职责 |
|------|------|
| `src/fix/fixPreview.ts` | diff 内容提供者、`openPreviewDiff``closePreviewEditor``applyNewText` |
| `src/fix/fixPending.ts` | 待确认修复存储(PendingFix / PendingBatch / FixPendingStore |
| `docs/superpowers/specs/2026-08-19-fix-preview-design.md` | 本设计书 |
### 修改
| 文件 | 变更 |
|------|------|
| `src/fix/fixEngine.ts` | `FixResult``newText?: string``fixDiagnostic``dryRun` 参数,为真时跳过写盘 |
| `src/fix/aiFixEngine.ts` | `aiFixDiagnostic``dryRun` 参数,为真时跳过写盘并返回 `newText` |
| `src/activation/commands.ts` | `fixIssue` panel 路径两步确认;`fixAll` 内存累积 + 合并 diff + 存 pending;新增 `applyFixPreview`/`cancelFixPreview`/`applyAllPreview`/`cancelAllPreview` 命令 |
| `src/panel/webview.ts` | 消息类型增 `applyFix`/`cancelFix`/`applyAll`/`cancelAll`;条目与顶部渲染隐藏的应用/取消按钮;`postMessage` 公开方法 |
| `src/views/reviewPanel.js` | `window.addEventListener('message')` 翻转按钮显隐 |
| `src/extension.ts` | 创建/传递 `FixPendingStore`;文档关闭时清 pending |
| `src/i18n/messages.ts` | 新增 `report.fixAllApply`;复用 `fix.apply` / `fix.cancel` / `fix.previewTitle` |
## 6. 关键接口定义
### 6.1 `FixResult`fixEngine.ts
```typescript
export interface FixResult {
success: boolean;
attempts: number;
message?: string;
appliedFixes: AppliedFix[];
newText?: string; // 新增:最终整文档文本(dryRun 时返回)
}
```
### 6.2 引擎 dryRun
```typescript
export async function fixDiagnostic(
document: vscode.TextDocument,
workingDir: string,
adapter: LinterAdapter,
diag: LinterDiagnostic,
maxIterations: number,
dryRun?: boolean
): Promise<FixResult>;
export async function aiFixDiagnostic(
document, workingDir, adapter, diag, maxIterations,
provider, options,
dryRun?: boolean
): Promise<FixResult>;
```
- `dryRun=true`:跳过末尾 `WorkspaceEdit``applyEdit`,返回 `newText: currentText`
- `dryRun=false`(默认):保持现有写盘行为,**不返回** `newText`(或一并返回,调用方忽略)。
- hover 入口仍走默认路径,行为零变化。
### 6.3 `src/fix/fixPreview.ts`
```typescript
export class FixPreviewContentProvider implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string;
set(uri: vscode.Uri, text: string): void;
clear(uri: vscode.Uri): void;
}
export interface PreviewRequest {
originalText: string;
newText: string;
title: string;
fileName: string;
}
export async function openPreviewDiff(req: PreviewRequest): Promise<vscode.Uri | undefined>;
// 打开内置 diff 编辑器,返回右侧 newUri(供后续关闭)
export async function closePreviewEditor(uri: vscode.Uri): Promise<void>;
// 关闭对应 diff 编辑器标签并清理 provider 内容
export async function applyNewText(document: vscode.TextDocument, newText: string): Promise<boolean>;
// 整文档 WorkspaceEdit 一次性写入(recordFixes 由调用方完成)
```
### 6.4 `src/fix/fixPending.ts`
```typescript
export interface PendingFix {
key: string; // `${ruleId}@${line}`
ruleId: string;
line: number;
filePath: string;
originalText: string;
newText: string;
appliedFixes: AppliedFix[];
diffUri?: vscode.Uri;
}
export interface PendingBatch {
filePath: string;
originalText: string;
newText: string;
results: { ruleId: string; line: number; appliedFixes: AppliedFix[] }[];
diffUri?: vscode.Uri;
}
export class FixPendingStore {
setSingle(fix: PendingFix): void;
getSingle(filePath: string, key: string): PendingFix | undefined;
deleteSingle(filePath: string, key: string): void;
setBatch(batch: PendingBatch): void;
getBatch(filePath: string): PendingBatch | undefined;
deleteBatch(filePath: string): void;
clear(filePath: string): void;
}
```
### 6.5 面板消息协议
- 新增消息:`applyFix`/`cancelFix`(带 line/ruleId)→ `codeReviewer.applyFixPreview`/`cancelFixPreview``applyAll`/`cancelAll``applyAllPreview`/`cancelAllPreview`
- 扩展 → 面板:`{ type: 'pending', key, on: boolean }`(翻转单条按钮)、`{ type: 'batchPending', on: boolean }`(翻转顶部按钮)。
## 7. 核心逻辑
### 7.1 单个修复(fixIssuepanel origin
```
document = resolveFixDocument(...)
diag = 命中诊断
result = resolveFix(..., dryRun=true) // 计算,不写盘
if !result.success → 提示失败,return
key = `${ruleId}@${line}`
existing = pendingStore.getSingle(filePath, key) // 重复预览先关旧 diff
diffUri = openPreviewDiff({ originalText, newText, title, fileName })
pendingStore.setSingle({ key, ruleId, line, filePath, originalText, newText, appliedFixes, diffUri })
ReviewPanel.postMessage({ type:'pending', key, on:true }) // 面板按钮 → 「应用 / 取消」
return // 不写盘
```
用户回面板:
- 点「应用」→ `applyFixPreview``applyNewText(document, newText)``recordFixes` → 关 diff → 删 pending → `save``refreshAfterFix` → 提示成功。
- 点「取消」→ `cancelFixPreview`:关 diff → 删 pending → postMessage 还原按钮。
### 7.2 全部修复(fixAll
```
fixables = 可修复诊断列表(含 AI)
let currentText = document.getText()
results: { ruleId, line, appliedFixes }[] = []
for each fixable:
fresh = 对 currentText 构造 mock doc 重跑 check 得 freshDiag
result = fixDiagnostic / aiFixDiagnostic(dryRun=true)
if result.success: results.push(...); currentText = result.newText!
diffUri = openPreviewDiff({ originalText, newText: currentText, title, fileName })
pendingStore.setBatch({ filePath, originalText, newText, results, diffUri })
ReviewPanel.postMessage({ type:'batchPending', on:true }) // 顶部按钮 → 「全部应用 / 取消」
return // 不写盘
```
- 「全部应用」→ `applyAllPreview``applyNewText(document, currentText)` → 逐条 `recordFixes` → 关 diff → 删 batch → `save``refreshAfterFix` → 提示批量完成。
- 「取消全部」→ `cancelAllPreview`:关 diff → 删 batch → 还原按钮。
> 说明:多轮引擎已在内部对**传入文本**反复 `adapter.check`,因此 fixAll 只需把上一个 fix 的 `newText` 作为下一个的输入(用 mockDocument 包装为 TextDocument 再传引擎),即可得到合并后的最终文本;位置漂移由引擎内 `findClosestFixable` 按行号就近匹配处理。
### 7.3 diff 提供者
- 注册 `vscode.workspace.registerTextDocumentContentProvider('codeReviewerPreview', provider)`extension.ts 调 `registerFixPreviewProvider` 一次)。
- 新文本 URI`codeReviewerPreview://new/<fileName>-<timestamp>`,内容即 `newText`;原文本 URI 同样走 provider(避免脏文档时左屏显示磁盘旧内容与引擎计算不一致)。
### 7.4 面板按钮翻转(reviewPanel.js
```javascript
window.addEventListener('message', e => {
const msg = e.data;
if (msg.type === 'pending') {
// 按 data-fix-key 找到「修复 / 应用 / 取消」三个按钮,按 msg.on 显隐
} else if (msg.type === 'batchPending') {
// 按 data-fix-all-btn 找到「全部修复 / 全部应用 / 取消」三个按钮,按 msg.on 显隐
}
});
```
- 应用/取消按钮在 buildHtml 时默认 `display:none`,收到消息后显隐切换,**不全量重建面板**,滚动位置与标签页保持。
### 7.5 hover 入口保持不变
- `codeActionProvider.ts` 触发的 `fixIssue``origin: 'hover'`,命令内直接 `dryRun=false` 执行,不弹 diff、不弹确认框。
## 8. i18n 新增 key
| key | zh-CN | en | ja |
|-----|-------|----|----|
| `fix.previewTitle` | 修复预览 | Fix preview | 修正プレビュー |
| `fix.apply` | 应用 | Apply | 適用 |
| `fix.cancel` | 取消 | Cancel | キャンセル |
| `report.fixAllApply` | 全部应用 | Apply All | すべて適用 |
## 9. 配置变更
无新增配置。
## 10. 测试计划
- 现有 `npm test`106 tests)回归通过。
- 引擎 dryRun 路径回归:原有 fixEngine/aiFixEngine 测试走默认(写盘)路径不受影响。
## 11. 验证顺序
`lint → compile → test``npm run lint` / `npm run compile` / `npm test`