feat(checkpoint): 添加 Checkpoint 可视化管理功能

Core 层增强:
- 添加 safety.ts: 7点安全检查机制
- 添加 session-tracker.ts: 会话级检查点跟踪
- 添加 lock.ts: 并发控制文件锁
- 添加 lfs.ts: Git LFS 大文件支持
- 添加 path-validator.ts: 路径验证
- 添加 commit-message.ts: 智能提交消息生成
- 增强 manager.ts: 支持三种恢复模式、unrevert 撤销回滚

Server 层:
- 添加 checkpoints.ts: 16个 REST API 端点
  - GET/POST /checkpoints: 列表/创建检查点
  - GET/DELETE /checkpoints/🆔 获取/删除检查点
  - GET /checkpoints/:id/diff: 获取差异
  - POST /checkpoints/:id/restore: 恢复到检查点
  - POST /checkpoints/unrevert: 撤销回滚
  - GET /checkpoints/:id/safety-check: 安全检查

UI 层:
- 添加 CheckpointPanel.tsx: 检查点列表面板
- 添加 CheckpointDiffViewer.tsx: 差异查看器
- 添加 RestoreDialog.tsx: 恢复确认对话框
- 添加 16 个 API 客户端函数
- 添加完整的 TypeScript 类型定义

Web/Desktop 集成:
- 添加 History 按钮到工具栏
- 集成 CheckpointPanel 组件
This commit is contained in:
2025-12-12 22:52:27 +08:00
parent a225e66ad7
commit cb554c65b4
23 changed files with 4970 additions and 116 deletions
+495 -93
View File
@@ -3,22 +3,29 @@
* 管理检查点的创建、回滚、清理等操作
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as os from 'os';
import { nanoid } from 'nanoid';
import { ShadowGit, createShadowGit } from './shadow-git.js';
import type {
CheckpointMetadata,
CheckpointConfig,
CheckpointTrigger,
RollbackOptions,
RollbackResult,
DiffInfo,
FileDiff,
CheckpointEvent,
CheckpointEventListener,
DEFAULT_CHECKPOINT_CONFIG,
import { CheckpointLock } from './lock.js';
import { CheckpointSafetyChecker } from './safety.js';
import { WorkspacePathValidator } from './path-validator.js';
import { CommitMessageGenerator } from './commit-message.js';
import { LFSPatternLoader } from './lfs.js';
import {
RestoreMode,
type CheckpointMetadata,
type CheckpointConfig,
type CheckpointTrigger,
type RollbackOptions,
type RollbackResult,
type DiffInfo,
type FileDiff,
type CheckpointEvent,
type CheckpointEventListener,
type RollbackRecord,
type UnrevertResult,
type SafetyCheckResult,
} from './types.js';
/**
@@ -38,6 +45,20 @@ export class CheckpointManager {
private lastCheckpointTime = 0;
private eventListeners: Set<CheckpointEventListener> = new Set();
// 新增:增强功能组件
private lock: CheckpointLock;
private safetyChecker: CheckpointSafetyChecker;
private pathValidator: WorkspacePathValidator;
private commitMessageGenerator: CommitMessageGenerator;
private lfsLoader: LFSPatternLoader;
// 新增:Unrevert 支持
private lastRollback: RollbackRecord | null = null;
// 新增:会话跟踪
private currentSessionId: string | null = null;
private sessionCheckpoints: Map<string, string[]> = new Map();
// 防止重复创建检查点的最小间隔 (毫秒)
private static readonly MIN_CHECKPOINT_INTERVAL = 1000;
@@ -59,6 +80,13 @@ export class CheckpointManager {
};
this.shadowGit = createShadowGit(this.workDir, this.config.storageDir);
// 初始化增强组件
this.lock = new CheckpointLock(this.shadowGit.getShadowGitDir());
this.safetyChecker = new CheckpointSafetyChecker(this.workDir);
this.pathValidator = new WorkspacePathValidator();
this.commitMessageGenerator = new CommitMessageGenerator();
this.lfsLoader = new LFSPatternLoader();
}
/**
@@ -72,6 +100,15 @@ export class CheckpointManager {
return;
}
// 验证工作目录路径
const pathValidation = this.pathValidator.validate(this.workDir);
if (!pathValidation.valid) {
throw new Error(`Invalid workspace path: ${pathValidation.reason}`);
}
// 加载 LFS 模式
await this.lfsLoader.loadPatterns(this.workDir);
// 初始化 Shadow Git
await this.shadowGit.initialize();
@@ -195,6 +232,9 @@ export class CheckpointManager {
description?: string;
trigger?: CheckpointTrigger;
toolCall?: { tool: string; params: Record<string, unknown> };
messageId?: string;
sessionId?: string;
turnIndex?: number;
}): Promise<CheckpointMetadata> {
await this.initialize();
@@ -202,48 +242,75 @@ export class CheckpointManager {
throw new Error('Checkpoint system is disabled');
}
const id = nanoid(10);
const timestamp = Date.now();
// 使用锁保护检查点创建
return this.lock.withLock(async () => {
const id = nanoid(10);
const timestamp = Date.now();
const trigger = options.trigger || 'manual';
// 创建元数据
const metadata: CheckpointMetadata = {
id,
name: options.name,
description: options.description,
timestamp,
trigger: options.trigger || 'manual',
toolCall: options.toolCall,
commitHash: '', // 待填充
filesChanged: 0, // 待填充
};
// 创建元数据
const metadata: CheckpointMetadata = {
id,
name: options.name,
description: options.description,
timestamp,
trigger,
toolCall: options.toolCall,
commitHash: '', // 待填充
filesChanged: 0, // 待填充
// 新增:消息和会话关联
messageId: options.messageId,
sessionId: options.sessionId || this.currentSessionId || undefined,
turnIndex: options.turnIndex,
};
// 获取变更文件数
try {
const diff = await this.shadowGit.getWorkingDirDiff();
metadata.filesChanged = diff.files.length;
} catch {
// 忽略
}
// 获取变更文件数
let filesChanged: Array<{ path: string; type: string }> = [];
try {
const diff = await this.shadowGit.getWorkingDirDiff();
metadata.filesChanged = diff.files.length;
filesChanged = diff.files;
} catch {
// 忽略
}
// 创建 commit
const commitMessage = CHECKPOINT_PREFIX + JSON.stringify(metadata);
const commitHash = await this.shadowGit.createCommit(commitMessage);
metadata.commitHash = commitHash;
// 生成智能提交消息
const humanReadableMessage = this.commitMessageGenerator.generateMessage(
trigger,
options.toolCall,
filesChanged as any
);
// 更新索引
this.checkpointsIndex.set(id, metadata);
// 创建 commit(使用 JSON 元数据作为 commit message,但包含可读描述)
const commitMessage = CHECKPOINT_PREFIX + JSON.stringify({
...metadata,
_readableMessage: humanReadableMessage,
});
const commitHash = await this.shadowGit.createCommit(commitMessage);
metadata.commitHash = commitHash;
// 触发事件
this.emitEvent({
type: 'created',
checkpoint: metadata,
timestamp,
// 更新索引
this.checkpointsIndex.set(id, metadata);
// 记录到当前会话
if (this.currentSessionId) {
const sessionCps = this.sessionCheckpoints.get(this.currentSessionId) || [];
sessionCps.push(id);
this.sessionCheckpoints.set(this.currentSessionId, sessionCps);
}
// 触发事件
this.emitEvent({
type: 'created',
checkpoint: metadata,
timestamp,
});
// 异步清理
this.cleanupAsync();
return metadata;
});
// 异步清理
this.cleanupAsync();
return metadata;
}
/**
@@ -359,63 +426,253 @@ export class CheckpointManager {
throw new Error(`Checkpoint not found: ${options.target}`);
}
// 获取当前 HEAD 用于可能的撤销
const previousCommit = await this.shadowGit.getHead();
// 安全检查(除非明确跳过)
if (!options.skipSafetyCheck) {
const safetyResult = await this.safetyChecker.checkBeforeRollback(checkpoint, this);
if (!safetyResult.safe) {
const errorMsg = safetyResult.errors.join('; ');
throw new Error(`Safety check failed: ${errorMsg}`);
}
// 警告仍然记录,但不阻止操作
if (safetyResult.warnings.length > 0) {
console.warn('Rollback warnings:', safetyResult.warnings.join('; '));
}
}
// 预览模式
if (options.dryRun) {
const diff = await this.getDiff(checkpoint.id);
return {
// 使用锁保护回滚操作
return this.lock.withLock(async () => {
// 获取当前 HEAD 用于可能的撤销
const previousCommit = await this.shadowGit.getHead();
// 预览模式
if (options.dryRun) {
const diff = await this.getDiff(checkpoint.id);
return {
success: true,
restoredFiles: diff.files.map((f) => f.path),
errors: [],
previousCommit,
};
}
// 创建回滚前检查点(用于 unrevert)
let preRollbackCheckpoint: CheckpointMetadata | null = null;
try {
preRollbackCheckpoint = await this.createCheckpointInternal({
trigger: 'pre_rollback',
description: `Before rollback to ${options.target}`,
});
} catch {
// 忽略创建失败
}
const result: RollbackResult = {
success: true,
restoredFiles: diff.files.map((f) => f.path),
restoredFiles: [],
errors: [],
previousCommit,
};
}
const result: RollbackResult = {
success: true,
restoredFiles: [],
errors: [],
previousCommit,
};
try {
const mode = options.mode || RestoreMode.FULL;
try {
if (options.files && options.files.length > 0) {
// 选择性回滚
await this.shadowGit.checkoutFiles(checkpoint.commitHash, options.files);
result.restoredFiles = options.files;
} else {
// 完整回滚
await this.shadowGit.resetHard(checkpoint.commitHash);
if (options.files && options.files.length > 0) {
// 选择性回滚(指定文件)
await this.shadowGit.checkoutFiles(checkpoint.commitHash, options.files);
result.restoredFiles = options.files;
} else if (mode === RestoreMode.AI_CHANGES_ONLY) {
// 仅恢复 AI 修改的文件
const aiFiles = await this.getAiModifiedFiles(checkpoint);
if (aiFiles.length > 0) {
await this.shadowGit.checkoutFiles(checkpoint.commitHash, aiFiles);
result.restoredFiles = aiFiles;
}
} else if (mode === RestoreMode.WORKSPACE_ONLY) {
// 仅恢复工作区变更(不包括 AI 修改)
const workspaceFiles = await this.getWorkspaceOnlyFiles(checkpoint);
if (workspaceFiles.length > 0) {
await this.shadowGit.checkoutFiles(checkpoint.commitHash, workspaceFiles);
result.restoredFiles = workspaceFiles;
}
} else {
// 完整回滚
await this.shadowGit.resetHard(checkpoint.commitHash);
// 获取恢复的文件列表
const diff = await this.shadowGit.getDiffSummary(
previousCommit,
checkpoint.commitHash
);
result.restoredFiles = diff.files.map((f) => f.path);
// 获取恢复的文件列表
const diff = await this.shadowGit.getDiffSummary(
previousCommit,
checkpoint.commitHash
);
result.restoredFiles = diff.files.map((f) => f.path);
}
// 记录回滚信息(用于 unrevert)
this.lastRollback = {
id: nanoid(10),
timestamp: Date.now(),
targetCheckpoint: checkpoint.id,
previousCommit: preRollbackCheckpoint?.commitHash || previousCommit,
restoredFiles: result.restoredFiles,
canUnrevert: true,
};
// 触发事件
this.emitEvent({
type: 'restored',
checkpoint,
timestamp: Date.now(),
details: {
files: result.restoredFiles,
previousCommit,
mode,
},
});
} catch (error) {
result.success = false;
result.errors.push({
file: '*',
error: error instanceof Error ? error.message : String(error),
});
}
// 触发事件
this.emitEvent({
type: 'restored',
checkpoint,
timestamp: Date.now(),
details: {
files: result.restoredFiles,
previousCommit,
},
});
} catch (error) {
result.success = false;
result.errors.push({
file: '*',
error: error instanceof Error ? error.message : String(error),
});
return result;
});
}
/**
* 撤销最近一次回滚(Unrevert)
*/
async unrevert(): Promise<UnrevertResult> {
await this.initialize();
if (!this.lastRollback || !this.lastRollback.canUnrevert) {
return {
success: false,
restoredCommit: '',
filesRestored: 0,
error: 'No rollback to unrevert',
};
}
return result;
return this.lock.withLock(async () => {
try {
// 恢复到回滚前的状态
await this.shadowGit.resetHard(this.lastRollback!.previousCommit);
const result: UnrevertResult = {
success: true,
restoredCommit: this.lastRollback!.previousCommit,
filesRestored: this.lastRollback!.restoredFiles.length,
};
// 清除 unrevert 记录
this.lastRollback = null;
return result;
} catch (error) {
return {
success: false,
restoredCommit: '',
filesRestored: 0,
error: error instanceof Error ? error.message : String(error),
};
}
});
}
/**
* 检查是否可以执行 unrevert
*/
canUnrevert(): boolean {
return this.lastRollback !== null && this.lastRollback.canUnrevert;
}
/**
* 获取最后一次回滚记录
*/
getLastRollback(): RollbackRecord | null {
return this.lastRollback;
}
/**
* 执行安全检查
*/
async checkSafety(checkpointId: string): Promise<SafetyCheckResult> {
await this.initialize();
const checkpoint = await this.getCheckpoint(checkpointId);
if (!checkpoint) {
return {
safe: false,
warnings: [],
errors: ['Checkpoint not found'],
};
}
return this.safetyChecker.checkBeforeRollback(checkpoint, this);
}
/**
* 内部创建检查点(不使用锁,供 rollback 内部调用)
*/
private async createCheckpointInternal(options: {
trigger: CheckpointTrigger;
description?: string;
}): Promise<CheckpointMetadata> {
const id = nanoid(10);
const timestamp = Date.now();
const metadata: CheckpointMetadata = {
id,
description: options.description,
timestamp,
trigger: options.trigger,
commitHash: '',
filesChanged: 0,
};
const commitMessage = CHECKPOINT_PREFIX + JSON.stringify(metadata);
const commitHash = await this.shadowGit.createCommit(commitMessage);
metadata.commitHash = commitHash;
this.checkpointsIndex.set(id, metadata);
return metadata;
}
/**
* 获取 AI 修改的文件列表
*/
private async getAiModifiedFiles(checkpoint: CheckpointMetadata): Promise<string[]> {
const files: string[] = [];
const checkpoints = await this.listCheckpoints();
// 找到该检查点之后的所有检查点
for (const cp of checkpoints) {
if (cp.timestamp > checkpoint.timestamp && cp.toolCall) {
const filePath =
(cp.toolCall.params.file_path as string) ||
(cp.toolCall.params.path as string);
if (filePath && !files.includes(filePath)) {
files.push(filePath);
}
}
}
return files;
}
/**
* 获取仅工作区变更的文件(不包括 AI 修改)
*/
private async getWorkspaceOnlyFiles(checkpoint: CheckpointMetadata): Promise<string[]> {
const diff = await this.getDiff(checkpoint.id);
const aiFiles = await this.getAiModifiedFiles(checkpoint);
// 返回不在 AI 修改列表中的文件
return diff.files
.map((f) => f.path)
.filter((path) => !aiFiles.includes(path));
}
/**
@@ -578,6 +835,151 @@ export class CheckpointManager {
getConfig(): CheckpointConfig {
return { ...this.config };
}
// ==================== 会话管理方法 ====================
/**
* 开始新会话
*/
async startSession(sessionId?: string): Promise<string> {
await this.initialize();
const id = sessionId || nanoid(10);
this.currentSessionId = id;
this.sessionCheckpoints.set(id, []);
// 创建会话开始检查点
try {
await this.createCheckpoint({
trigger: 'session_start',
description: `Session started: ${id}`,
sessionId: id,
});
} catch {
// 忽略创建失败
}
return id;
}
/**
* 结束当前会话
*/
async endSession(): Promise<void> {
if (!this.currentSessionId) return;
// 创建会话结束检查点
try {
await this.createCheckpoint({
trigger: 'session_end',
description: `Session ended: ${this.currentSessionId}`,
sessionId: this.currentSessionId,
});
} catch {
// 忽略创建失败
}
this.currentSessionId = null;
}
/**
* 获取当前会话 ID
*/
getCurrentSessionId(): string | null {
return this.currentSessionId;
}
/**
* 获取会话的所有检查点
*/
async getSessionCheckpoints(sessionId: string): Promise<CheckpointMetadata[]> {
await this.initialize();
const checkpointIds = this.sessionCheckpoints.get(sessionId);
if (!checkpointIds) return [];
const checkpoints: CheckpointMetadata[] = [];
for (const id of checkpointIds) {
const checkpoint = this.checkpointsIndex.get(id);
if (checkpoint) {
checkpoints.push(checkpoint);
}
}
return checkpoints.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 创建与消息关联的检查点
*/
async createMessageCheckpoint(
messageId: string,
turnIndex?: number,
options?: {
trigger?: CheckpointTrigger;
description?: string;
}
): Promise<CheckpointMetadata> {
return this.createCheckpoint({
trigger: options?.trigger || 'auto',
description: options?.description || `Message checkpoint: ${messageId}`,
messageId,
sessionId: this.currentSessionId || undefined,
turnIndex,
});
}
/**
* 获取与消息关联的检查点
*/
async getMessageCheckpoints(messageId: string): Promise<CheckpointMetadata[]> {
await this.initialize();
const checkpoints: CheckpointMetadata[] = [];
for (const checkpoint of this.checkpointsIndex.values()) {
if (checkpoint.messageId === messageId) {
checkpoints.push(checkpoint);
}
}
return checkpoints.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 撤销整个会话的修改
*/
async undoSession(sessionId: string): Promise<RollbackResult> {
const sessionCheckpoints = await this.getSessionCheckpoints(sessionId);
if (sessionCheckpoints.length === 0) {
throw new Error(`No checkpoints found for session: ${sessionId}`);
}
// 找到会话开始的检查点
const startCheckpoint = sessionCheckpoints.find(
(cp) => cp.trigger === 'session_start'
);
if (!startCheckpoint) {
// 如果没有明确的开始检查点,使用第一个检查点
return this.rollback({ target: sessionCheckpoints[0].id });
}
return this.rollback({ target: startCheckpoint.id });
}
/**
* 获取 LFS 模式加载器
*/
getLfsLoader(): LFSPatternLoader {
return this.lfsLoader;
}
/**
* 检查文件是否由 LFS 管理
*/
isLfsFile(filePath: string): boolean {
return this.lfsLoader.isLfsFile(filePath);
}
}
// 全局检查点管理器实例