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
@@ -0,0 +1,275 @@
/**
* 智能提交消息生成模块
* 参考 Aider 的 AI 提交消息生成
*/
import * as path from 'path';
import type { CheckpointTrigger, FileChange } from './types.js';
/**
* 工具调用信息
*/
interface ToolCallInfo {
tool: string;
params: Record<string, unknown>;
}
/**
* 提交消息生成器
* 生成人类可读的检查点提交消息
*/
export class CommitMessageGenerator {
/**
* 生成提交消息
*/
generateMessage(
trigger: CheckpointTrigger,
toolCall?: ToolCallInfo,
filesChanged?: FileChange[]
): string {
const prefix = this.getTriggerPrefix(trigger);
const description = this.getDescription(trigger, toolCall, filesChanged);
const body = this.generateBody(trigger, toolCall, filesChanged);
if (body) {
return `${prefix}: ${description}\n\n${body}`;
}
return `${prefix}: ${description}`;
}
/**
* 获取触发类型的前缀
*/
private getTriggerPrefix(trigger: CheckpointTrigger): string {
const prefixes: Record<CheckpointTrigger, string> = {
auto: 'auto',
manual: 'checkpoint',
'tool:write_file': 'write',
'tool:edit_file': 'edit',
'tool:delete_file': 'delete',
'tool:move_file': 'move',
'tool:copy_file': 'copy',
'tool:bash': 'bash',
task_start: 'session-start',
task_complete: 'session-end',
pre_rollback: 'pre-rollback',
session_start: 'session-start',
session_end: 'session-end',
};
return prefixes[trigger] || 'checkpoint';
}
/**
* 生成描述
*/
private getDescription(
trigger: CheckpointTrigger,
toolCall?: ToolCallInfo,
filesChanged?: FileChange[]
): string {
// 从工具调用参数获取文件路径
if (toolCall) {
const filePath =
(toolCall.params.file_path as string) ||
(toolCall.params.path as string);
if (filePath) {
return this.formatFilePath(filePath);
}
// bash 命令
if (toolCall.tool === 'bash') {
const command = String(toolCall.params.command || '');
return this.formatCommand(command);
}
// 移动/复制操作
if (toolCall.params.source && toolCall.params.destination) {
const source = this.formatFilePath(String(toolCall.params.source));
const dest = this.formatFilePath(String(toolCall.params.destination));
return `${source} -> ${dest}`;
}
}
// 从文件变更列表获取描述
if (filesChanged && filesChanged.length > 0) {
if (filesChanged.length === 1) {
return this.formatFilePath(filesChanged[0].path);
}
return `${filesChanged.length} files`;
}
// 根据触发类型生成描述
switch (trigger) {
case 'task_start':
case 'session_start':
return 'begin session';
case 'task_complete':
case 'session_end':
return 'end session';
case 'pre_rollback':
return 'state before rollback';
case 'manual':
return 'manual checkpoint';
default:
return 'state snapshot';
}
}
/**
* 生成消息正文
*/
private generateBody(
trigger: CheckpointTrigger,
toolCall?: ToolCallInfo,
filesChanged?: FileChange[]
): string {
const lines: string[] = [];
// 时间戳
lines.push(`Time: ${new Date().toISOString()}`);
// 触发类型
lines.push(`Trigger: ${trigger}`);
// 工具信息
if (toolCall) {
lines.push(`Tool: ${toolCall.tool}`);
}
// 文件变更统计
if (filesChanged && filesChanged.length > 0) {
const stats = this.calculateStats(filesChanged);
lines.push(`Files: ${filesChanged.length}`);
if (stats.insertions > 0 || stats.deletions > 0) {
lines.push(`Changes: +${stats.insertions} -${stats.deletions}`);
}
}
return lines.join('\n');
}
/**
* 格式化文件路径(只保留文件名和父目录)
*/
private formatFilePath(filePath: string): string {
const parts = filePath.split(path.sep);
if (parts.length <= 2) {
return filePath;
}
// 返回 父目录/文件名 格式
return parts.slice(-2).join('/');
}
/**
* 格式化命令(截断过长的命令)
*/
private formatCommand(command: string): string {
const maxLength = 50;
const trimmed = command.trim();
if (trimmed.length <= maxLength) {
return `"${trimmed}"`;
}
return `"${trimmed.slice(0, maxLength - 3)}..."`;
}
/**
* 计算文件变更统计
*/
private calculateStats(filesChanged: FileChange[]): {
insertions: number;
deletions: number;
added: number;
modified: number;
deleted: number;
renamed: number;
} {
let insertions = 0;
let deletions = 0;
let added = 0;
let modified = 0;
let deleted = 0;
let renamed = 0;
for (const file of filesChanged) {
insertions += file.insertions || 0;
deletions += file.deletions || 0;
switch (file.type) {
case 'added':
added++;
break;
case 'modified':
modified++;
break;
case 'deleted':
deleted++;
break;
case 'renamed':
renamed++;
break;
}
}
return { insertions, deletions, added, modified, deleted, renamed };
}
/**
* 生成简短的提交消息(单行)
*/
generateShortMessage(
trigger: CheckpointTrigger,
toolCall?: ToolCallInfo
): string {
const prefix = this.getTriggerPrefix(trigger);
const description = this.getDescription(trigger, toolCall);
return `${prefix}: ${description}`;
}
/**
* 从提交消息解析元数据
*/
parseMessage(message: string): {
prefix: string;
description: string;
metadata: Record<string, string>;
} {
const lines = message.split('\n');
const firstLine = lines[0] || '';
// 解析第一行: prefix: description
const [prefix, ...descParts] = firstLine.split(':');
const description = descParts.join(':').trim();
// 解析元数据
const metadata: Record<string, string> = {};
for (let i = 2; i < lines.length; i++) {
const line = lines[i];
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.slice(0, colonIndex).trim();
const value = line.slice(colonIndex + 1).trim();
metadata[key] = value;
}
}
return {
prefix: prefix.trim(),
description,
metadata,
};
}
}
/**
* 创建提交消息生成器实例
*/
export function createCommitMessageGenerator(): CommitMessageGenerator {
return new CommitMessageGenerator();
}
+74 -17
View File
@@ -2,7 +2,16 @@
* 检查点系统模块
*
* 提供工作区快照和回滚功能,使用 Shadow Git 架构
* 参考 Cline 的实现
* 参考 Cline 的实现,并增强了以下功能:
* - Unrevert 撤销回滚
* - 7 点安全检查机制
* - 三种恢复模式
* - 消息级检查点关联
* - 会话级跟踪
* - 并发控制(文件锁)
* - LFS 大文件支持
* - 工作区路径验证
* - 智能提交消息生成
*/
// 检查点管理器
@@ -16,20 +25,68 @@ export {
// Shadow Git
export { ShadowGit, createShadowGit, hashWorkingDir } from './shadow-git.js';
// 类型
export type {
CheckpointMetadata,
CheckpointConfig,
CheckpointTrigger,
FileChange,
FileChangeType,
DiffInfo,
FileDiff,
RollbackOptions,
RollbackResult,
CheckpointEvent,
CheckpointEventType,
CheckpointEventListener,
} from './types.js';
// 安全检查器
export {
CheckpointSafetyChecker,
createSafetyChecker,
} from './safety.js';
export { DEFAULT_CHECKPOINT_CONFIG } from './types.js';
// 会话跟踪器
export {
SessionTracker,
createSessionTracker,
} from './session-tracker.js';
// 并发锁
export { CheckpointLock } from './lock.js';
// LFS 支持
export {
LFSPatternLoader,
createLFSPatternLoader,
isCommonLargeFile,
COMMON_LARGE_FILE_EXTENSIONS,
} from './lfs.js';
// 路径验证器
export {
WorkspacePathValidator,
createPathValidator,
} from './path-validator.js';
// 提交消息生成器
export {
CommitMessageGenerator,
createCommitMessageGenerator,
} from './commit-message.js';
// 类型
export {
// 基础类型
type CheckpointMetadata,
type CheckpointConfig,
type CheckpointTrigger,
type FileChange,
type FileChangeType,
type DiffInfo,
type FileDiff,
type RollbackOptions,
type RollbackResult,
type CheckpointEvent,
type CheckpointEventType,
type CheckpointEventListener,
// 恢复模式
RestoreMode,
// Unrevert 相关
type RollbackRecord,
type UnrevertResult,
// 安全检查
type SafetyCheckResult,
// 会话跟踪
type SessionState,
type SessionStats,
// 路径验证
type PathValidationResult,
// 默认配置
DEFAULT_CHECKPOINT_CONFIG,
} from './types.js';
+189
View File
@@ -0,0 +1,189 @@
/**
* Git LFS 大文件支持模块
* 参考 Cline 的 LFS 模式检测
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { minimatch } from 'minimatch';
/**
* LFS 模式加载器
* 从 .gitattributes 文件中加载 LFS 模式
*/
export class LFSPatternLoader {
private patterns: string[] = [];
private loaded = false;
/**
* 从工作目录加载 LFS 模式
*/
async loadPatterns(workDir: string): Promise<void> {
const gitattributesPath = path.join(workDir, '.gitattributes');
try {
const content = await fs.readFile(gitattributesPath, 'utf-8');
this.patterns = this.parseLfsPatterns(content);
this.loaded = true;
} catch {
// .gitattributes 不存在或无法读取
this.patterns = [];
this.loaded = true;
}
}
/**
* 解析 .gitattributes 内容,提取 LFS 模式
*/
private parseLfsPatterns(content: string): string[] {
const patterns: string[] = [];
for (const line of content.split('\n')) {
const trimmed = line.trim();
// 跳过空行和注释
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
// 匹配 LFS 配置行: pattern filter=lfs diff=lfs merge=lfs -text
// 或简单形式: pattern filter=lfs
if (trimmed.includes('filter=lfs')) {
// 提取模式(第一个空白字符前的部分)
const match = trimmed.match(/^(\S+)/);
if (match) {
patterns.push(match[1]);
}
}
}
return patterns;
}
/**
* 检查文件是否为 LFS 管理的文件
*/
isLfsFile(filePath: string): boolean {
if (!this.loaded) {
return false;
}
// 规范化路径(使用正斜杠)
const normalizedPath = filePath.replace(/\\/g, '/');
return this.patterns.some((pattern) =>
minimatch(normalizedPath, pattern, { matchBase: true })
);
}
/**
* 获取所有 LFS 模式
*/
getPatterns(): string[] {
return [...this.patterns];
}
/**
* 获取排除模式(用于 .gitignore
*/
getExcludePatterns(): string[] {
return this.patterns.map((p) => {
// 确保模式以 / 开头(相对于仓库根目录)
if (!p.startsWith('/') && !p.startsWith('*')) {
return `/${p}`;
}
return p;
});
}
/**
* 检查是否已加载
*/
isLoaded(): boolean {
return this.loaded;
}
/**
* 重置(清除已加载的模式)
*/
reset(): void {
this.patterns = [];
this.loaded = false;
}
/**
* 过滤文件列表,排除 LFS 文件
*/
filterNonLfsFiles(files: string[]): string[] {
return files.filter((file) => !this.isLfsFile(file));
}
/**
* 获取 LFS 文件列表
*/
filterLfsFiles(files: string[]): string[] {
return files.filter((file) => this.isLfsFile(file));
}
}
/**
* 常见的大文件扩展名(作为 LFS 候选)
*/
export const COMMON_LARGE_FILE_EXTENSIONS = [
// 图片
'.psd',
'.ai',
'.eps',
'.tiff',
'.raw',
'.cr2',
'.nef',
// 视频
'.mp4',
'.mov',
'.avi',
'.mkv',
'.wmv',
'.flv',
// 音频
'.mp3',
'.wav',
'.flac',
'.aac',
'.ogg',
// 压缩文件
'.zip',
'.tar',
'.gz',
'.rar',
'.7z',
// 二进制
'.exe',
'.dll',
'.so',
'.dylib',
// 数据文件
'.db',
'.sqlite',
'.mdb',
// 其他
'.pdf',
'.docx',
'.xlsx',
'.pptx',
];
/**
* 检查文件扩展名是否为常见大文件类型
*/
export function isCommonLargeFile(filePath: string): boolean {
const ext = path.extname(filePath).toLowerCase();
return COMMON_LARGE_FILE_EXTENSIONS.includes(ext);
}
/**
* 创建 LFS 模式加载器实例
*/
export function createLFSPatternLoader(): LFSPatternLoader {
return new LFSPatternLoader();
}
+229
View File
@@ -0,0 +1,229 @@
/**
* 检查点并发控制模块
* 参考 Cline 的 proper-lockfile 机制
*/
import * as fs from 'fs/promises';
import * as path from 'path';
/**
* 锁文件内容
*/
interface LockInfo {
pid: number;
hostname: string;
timestamp: number;
}
/**
* 锁配置
*/
interface LockOptions {
/** 锁过期时间(毫秒) */
stale?: number;
/** 重试次数 */
retries?: number;
/** 重试间隔(毫秒) */
retryDelay?: number;
}
const DEFAULT_OPTIONS: Required<LockOptions> = {
stale: 30000, // 30 秒过期
retries: 10,
retryDelay: 100,
};
/**
* 检查点锁管理器
* 防止多个实例同时操作同一工作区的检查点
*/
export class CheckpointLock {
private lockPath: string;
private locked = false;
private options: Required<LockOptions>;
constructor(shadowGitDir: string, options: LockOptions = {}) {
this.lockPath = path.join(shadowGitDir, '.checkpoint.lock');
this.options = { ...DEFAULT_OPTIONS, ...options };
}
/**
* 获取锁
*/
async acquire(): Promise<void> {
let lastError: Error | null = null;
for (let i = 0; i < this.options.retries; i++) {
try {
await this.tryAcquire();
this.locked = true;
return;
} catch (error) {
lastError = error as Error;
// 检查是否为过期锁
if (await this.isLockStale()) {
await this.forceRelease();
continue;
}
// 等待后重试
await this.sleep(this.options.retryDelay);
}
}
throw new Error(
`Failed to acquire checkpoint lock after ${this.options.retries} retries: ${lastError?.message}`
);
}
/**
* 尝试获取锁
*/
private async tryAcquire(): Promise<void> {
const lockInfo: LockInfo = {
pid: process.pid,
hostname: this.getHostname(),
timestamp: Date.now(),
};
// 确保目录存在
const lockDir = path.dirname(this.lockPath);
await fs.mkdir(lockDir, { recursive: true });
// 使用 exclusive 标志创建锁文件
const handle = await fs.open(this.lockPath, 'wx');
try {
await handle.writeFile(JSON.stringify(lockInfo, null, 2));
} finally {
await handle.close();
}
}
/**
* 释放锁
*/
async release(): Promise<void> {
if (!this.locked) {
return;
}
try {
// 验证锁是否属于当前进程
const lockInfo = await this.readLockInfo();
if (lockInfo && lockInfo.pid === process.pid) {
await fs.unlink(this.lockPath);
}
} catch {
// 忽略错误
} finally {
this.locked = false;
}
}
/**
* 强制释放锁(用于清理过期锁)
*/
private async forceRelease(): Promise<void> {
try {
await fs.unlink(this.lockPath);
} catch {
// 忽略错误
}
}
/**
* 检查锁是否过期
*/
private async isLockStale(): Promise<boolean> {
try {
const lockInfo = await this.readLockInfo();
if (!lockInfo) {
return true;
}
const age = Date.now() - lockInfo.timestamp;
return age > this.options.stale;
} catch {
return true;
}
}
/**
* 读取锁信息
*/
private async readLockInfo(): Promise<LockInfo | null> {
try {
const content = await fs.readFile(this.lockPath, 'utf-8');
return JSON.parse(content) as LockInfo;
} catch {
return null;
}
}
/**
* 获取主机名
*/
private getHostname(): string {
try {
return require('os').hostname();
} catch {
return 'unknown';
}
}
/**
* 等待
*/
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* 带锁执行操作
*/
async withLock<T>(fn: () => Promise<T>): Promise<T> {
await this.acquire();
try {
return await fn();
} finally {
await this.release();
}
}
/**
* 检查是否已锁定
*/
isLocked(): boolean {
return this.locked;
}
/**
* 检查锁文件是否存在
*/
async lockExists(): Promise<boolean> {
try {
await fs.access(this.lockPath);
return true;
} catch {
return false;
}
}
/**
* 获取锁持有者信息
*/
async getLockHolder(): Promise<LockInfo | null> {
return this.readLockInfo();
}
}
/**
* 创建检查点锁实例
*/
export function createCheckpointLock(
shadowGitDir: string,
options?: LockOptions
): CheckpointLock {
return new CheckpointLock(shadowGitDir, options);
}
+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);
}
}
// 全局检查点管理器实例
@@ -0,0 +1,254 @@
/**
* 工作区路径验证模块
* 参考 Cline 的路径安全检查机制
*/
import * as os from 'os';
import * as path from 'path';
import type { PathValidationResult } from './types.js';
/**
* 工作区路径验证器
* 阻止在敏感目录(home、desktop、documents 等)创建检查点
*/
export class WorkspacePathValidator {
private blockedPaths: string[];
private blockedPathsLower: string[];
constructor() {
const home = os.homedir();
this.blockedPaths = [
// Home 目录
home,
// 常见敏感目录
path.join(home, 'Desktop'),
path.join(home, 'Documents'),
path.join(home, 'Downloads'),
path.join(home, 'Pictures'),
path.join(home, 'Music'),
path.join(home, 'Videos'),
// 系统根目录
'/',
'/tmp',
'/var',
'/etc',
'/usr',
'/bin',
'/sbin',
'/opt',
];
// Windows 特殊路径
if (process.platform === 'win32') {
this.blockedPaths.push(
'C:\\',
'C:\\Windows',
'C:\\Windows\\System32',
'C:\\Program Files',
'C:\\Program Files (x86)',
'C:\\Users',
'C:\\ProgramData'
);
}
// macOS 特殊路径
if (process.platform === 'darwin') {
this.blockedPaths.push(
'/Applications',
'/System',
'/Library',
path.join(home, 'Library')
);
}
// 创建小写版本用于不区分大小写的比较
this.blockedPathsLower = this.blockedPaths.map((p) => p.toLowerCase());
}
/**
* 验证工作目录是否安全
*/
validate(workDir: string): PathValidationResult {
const normalizedPath = path.resolve(workDir);
const normalizedLower = normalizedPath.toLowerCase();
// 1. 检查是否为阻止的路径
for (let i = 0; i < this.blockedPaths.length; i++) {
const blocked = this.blockedPaths[i];
const blockedLower = this.blockedPathsLower[i];
// 精确匹配
if (
normalizedPath === blocked ||
normalizedLower === blockedLower
) {
return {
valid: false,
reason: `Cannot create checkpoints in "${blocked}" - this is a protected directory`,
};
}
}
// 2. 检查是否为系统目录的直接子目录
if (this.isDirectChildOfBlocked(normalizedPath)) {
return {
valid: false,
reason: `Cannot create checkpoints directly under system directories`,
};
}
// 3. 检查路径是否过短(可能是根目录或系统目录)
const pathDepth = this.getPathDepth(normalizedPath);
if (pathDepth < 2) {
return {
valid: false,
reason: 'Path is too shallow - please use a project subdirectory',
};
}
// 4. 检查是否包含危险字符
if (this.containsDangerousChars(normalizedPath)) {
return {
valid: false,
reason: 'Path contains invalid characters',
};
}
return { valid: true };
}
/**
* 检查是否为阻止路径的直接子目录
*/
private isDirectChildOfBlocked(targetPath: string): boolean {
const parent = path.dirname(targetPath);
const parentLower = parent.toLowerCase();
for (let i = 0; i < this.blockedPaths.length; i++) {
if (
parent === this.blockedPaths[i] ||
parentLower === this.blockedPathsLower[i]
) {
// 特殊情况:允许 home 目录下的项目目录
const home = os.homedir();
if (parent === home || parentLower === home.toLowerCase()) {
// 但不允许一些特定的目录名
const dirName = path.basename(targetPath).toLowerCase();
const protectedNames = [
'desktop',
'documents',
'downloads',
'pictures',
'music',
'videos',
'library',
'.trash',
'.cache',
];
if (!protectedNames.includes(dirName)) {
return false; // 允许 home 下的其他目录
}
}
return true;
}
}
return false;
}
/**
* 获取路径深度
*/
private getPathDepth(targetPath: string): number {
const normalized = path.normalize(targetPath);
const parts = normalized.split(path.sep).filter((p) => p.length > 0);
// Windows 盘符算一层
if (process.platform === 'win32' && /^[A-Za-z]:$/.test(parts[0] || '')) {
return parts.length - 1;
}
return parts.length;
}
/**
* 检查是否包含危险字符
*/
private containsDangerousChars(targetPath: string): boolean {
// 检查空字节和其他控制字符
if (/[\x00-\x1f]/.test(targetPath)) {
return true;
}
// 检查 .. 路径遍历
if (targetPath.includes('..')) {
const resolved = path.resolve(targetPath);
if (resolved !== targetPath) {
return true;
}
}
return false;
}
/**
* 获取阻止的路径列表
*/
getBlockedPaths(): string[] {
return [...this.blockedPaths];
}
/**
* 添加自定义阻止路径
*/
addBlockedPath(blockedPath: string): void {
const normalized = path.resolve(blockedPath);
if (!this.blockedPaths.includes(normalized)) {
this.blockedPaths.push(normalized);
this.blockedPathsLower.push(normalized.toLowerCase());
}
}
/**
* 检查路径是否为有效的项目目录
* 额外检查是否包含常见的项目标识文件
*/
async isProjectDirectory(workDir: string): Promise<boolean> {
const fs = await import('fs/promises');
const projectIndicators = [
'package.json',
'Cargo.toml',
'go.mod',
'pom.xml',
'build.gradle',
'requirements.txt',
'setup.py',
'pyproject.toml',
'Gemfile',
'composer.json',
'.git',
'Makefile',
'CMakeLists.txt',
];
for (const indicator of projectIndicators) {
try {
await fs.access(path.join(workDir, indicator));
return true;
} catch {
// 继续检查下一个
}
}
return false;
}
}
/**
* 创建路径验证器实例
*/
export function createPathValidator(): WorkspacePathValidator {
return new WorkspacePathValidator();
}
+214
View File
@@ -0,0 +1,214 @@
/**
* 检查点安全检查模块
* 参考 Aider 的 7 点安全检查机制
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import type { CheckpointManager } from './manager.js';
import type { SafetyCheckResult, CheckpointMetadata } from './types.js';
const execAsync = promisify(exec);
/**
* 检查点安全检查器
*/
export class CheckpointSafetyChecker {
private workDir: string;
constructor(workDir: string) {
this.workDir = workDir;
}
/**
* 在回滚前执行安全检查
*/
async checkBeforeRollback(
checkpoint: CheckpointMetadata,
manager: CheckpointManager
): Promise<SafetyCheckResult> {
const result: SafetyCheckResult = {
safe: true,
warnings: [],
errors: [],
};
// 1. 检查检查点是否存在
if (!checkpoint) {
result.safe = false;
result.errors.push('Checkpoint not found');
return result;
}
// 2. 检查工作区是否有未保存的变更
const hasUnsaved = await this.hasUnsavedChanges(manager);
if (hasUnsaved) {
result.warnings.push('Working directory has unsaved changes that will be lost');
}
// 3. 检查主仓库状态
const mainRepoStatus = await this.checkMainRepoStatus();
if (mainRepoStatus.hasChanges) {
result.warnings.push('Main repository has uncommitted changes');
}
// 4. 检查是否会影响已推送的文件
const pushedCheck = await this.checkPushedFiles(checkpoint);
if (pushedCheck.hasPushed) {
result.warnings.push(
`Some changes may have been pushed to remote. Files: ${pushedCheck.files.join(', ')}`
);
}
// 5. 检查时间跨度
const age = Date.now() - checkpoint.timestamp;
const oneDay = 24 * 60 * 60 * 1000;
if (age > oneDay) {
const days = Math.floor(age / oneDay);
result.warnings.push(`Checkpoint is ${days} day(s) old`);
}
// 6. 检查是否为 AI 创建的检查点
const isAiCreated = this.isAiCreatedCheckpoint(checkpoint);
if (!isAiCreated) {
result.warnings.push('This checkpoint was not created by AI tools');
}
// 7. 检查影响的文件数量
if (checkpoint.filesChanged > 50) {
result.warnings.push(
`This rollback will affect ${checkpoint.filesChanged} files`
);
}
return result;
}
/**
* 检查工作区是否有未保存的变更
*/
private async hasUnsavedChanges(manager: CheckpointManager): Promise<boolean> {
try {
// 通过 manager 检查
const checkpoints = await manager.listCheckpoints();
if (checkpoints.length === 0) return false;
const latest = checkpoints[0];
const diff = await manager.getDiff(latest.id);
return diff.files.length > 0;
} catch {
return false;
}
}
/**
* 检查主仓库状态
*/
private async checkMainRepoStatus(): Promise<{ hasChanges: boolean; isGitRepo: boolean }> {
try {
const { stdout } = await execAsync('git status --porcelain', {
cwd: this.workDir,
});
return {
hasChanges: stdout.trim().length > 0,
isGitRepo: true,
};
} catch {
return {
hasChanges: false,
isGitRepo: false,
};
}
}
/**
* 检查是否有文件已被推送到远程
*/
private async checkPushedFiles(
checkpoint: CheckpointMetadata
): Promise<{ hasPushed: boolean; files: string[] }> {
try {
// 检查主仓库中自检查点时间后是否有推送
const since = new Date(checkpoint.timestamp).toISOString();
const { stdout } = await execAsync(
`git log --oneline --since="${since}" --name-only origin/HEAD..HEAD 2>/dev/null || true`,
{ cwd: this.workDir }
);
if (!stdout.trim()) {
return { hasPushed: false, files: [] };
}
// 解析文件列表
const lines = stdout.trim().split('\n');
const files: string[] = [];
for (const line of lines) {
// 跳过 commit 行(包含空格)
if (!line.includes(' ') && line.length > 0) {
files.push(line);
}
}
return {
hasPushed: files.length > 0,
files: [...new Set(files)],
};
} catch {
return { hasPushed: false, files: [] };
}
}
/**
* 检查是否为 AI 创建的检查点
*/
private isAiCreatedCheckpoint(checkpoint: CheckpointMetadata): boolean {
// AI 创建的检查点通常有 toolCall 或特定的 trigger
const aiTriggers = [
'tool:write_file',
'tool:edit_file',
'tool:delete_file',
'tool:move_file',
'tool:copy_file',
'tool:bash',
'task_start',
'task_complete',
'auto',
];
return aiTriggers.includes(checkpoint.trigger) || !!checkpoint.toolCall;
}
/**
* 格式化安全检查结果为用户友好的消息
*/
formatResult(result: SafetyCheckResult): string {
const lines: string[] = [];
if (result.errors.length > 0) {
lines.push('❌ Errors:');
for (const error of result.errors) {
lines.push(` - ${error}`);
}
}
if (result.warnings.length > 0) {
lines.push('⚠️ Warnings:');
for (const warning of result.warnings) {
lines.push(` - ${warning}`);
}
}
if (result.safe && result.warnings.length === 0) {
lines.push('✅ All safety checks passed');
}
return lines.join('\n');
}
}
/**
* 创建安全检查器实例
*/
export function createSafetyChecker(workDir: string): CheckpointSafetyChecker {
return new CheckpointSafetyChecker(workDir);
}
@@ -0,0 +1,222 @@
/**
* 会话级检查点跟踪
* 参考 Aider 的 aider_commit_hashes 和 commit_before_message 机制
*/
import { nanoid } from 'nanoid';
import type { CheckpointManager } from './manager.js';
import type {
SessionState,
SessionStats,
CheckpointMetadata,
RollbackResult,
} from './types.js';
/**
* 会话跟踪器
* 跟踪当前会话的所有检查点和文件修改
*/
export class SessionTracker {
private currentSession: SessionState | null = null;
private checkpointManager: CheckpointManager;
constructor(checkpointManager: CheckpointManager) {
this.checkpointManager = checkpointManager;
}
/**
* 开始新会话
*/
async startSession(): Promise<string> {
// 创建会话开始检查点
const startCp = await this.checkpointManager.createCheckpoint({
trigger: 'session_start',
description: 'Session start',
});
const sessionId = nanoid(10);
this.currentSession = {
id: sessionId,
startTime: Date.now(),
startCheckpoint: startCp.id,
checkpoints: [startCp.id],
modifiedFiles: [],
};
return sessionId;
}
/**
* 结束当前会话
*/
async endSession(): Promise<void> {
if (!this.currentSession) return;
// 创建会话结束检查点
await this.checkpointManager.createCheckpoint({
trigger: 'session_end',
description: 'Session end',
});
this.currentSession = null;
}
/**
* 获取当前会话 ID
*/
getCurrentSessionId(): string | null {
return this.currentSession?.id ?? null;
}
/**
* 检查是否有活跃会话
*/
hasActiveSession(): boolean {
return this.currentSession !== null;
}
/**
* 记录检查点到当前会话
*/
recordCheckpoint(checkpointId: string): void {
if (this.currentSession) {
this.currentSession.checkpoints.push(checkpointId);
}
}
/**
* 记录文件修改
*/
recordFileChange(filePath: string): void {
if (this.currentSession) {
if (!this.currentSession.modifiedFiles.includes(filePath)) {
this.currentSession.modifiedFiles.push(filePath);
}
}
}
/**
* 记录多个文件修改
*/
recordFileChanges(filePaths: string[]): void {
for (const filePath of filePaths) {
this.recordFileChange(filePath);
}
}
/**
* 获取会话中修改的所有文件
*/
getModifiedFiles(): string[] {
return this.currentSession?.modifiedFiles ?? [];
}
/**
* 获取会话中的所有检查点
*/
async getSessionCheckpoints(): Promise<CheckpointMetadata[]> {
if (!this.currentSession) {
return [];
}
const checkpoints: CheckpointMetadata[] = [];
for (const id of this.currentSession.checkpoints) {
const cp = await this.checkpointManager.getCheckpoint(id);
if (cp) {
checkpoints.push(cp);
}
}
return checkpoints.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 撤销当前会话的所有修改
*/
async undoSession(): Promise<RollbackResult> {
if (!this.currentSession) {
throw new Error('No active session');
}
return this.checkpointManager.rollback({
target: this.currentSession.startCheckpoint,
skipSafetyCheck: true, // 会话级撤销跳过安全检查
});
}
/**
* 回滚到会话中的指定检查点
*/
async rollbackToCheckpoint(checkpointId: string): Promise<RollbackResult> {
if (!this.currentSession) {
throw new Error('No active session');
}
// 验证检查点属于当前会话
if (!this.currentSession.checkpoints.includes(checkpointId)) {
throw new Error('Checkpoint does not belong to current session');
}
return this.checkpointManager.rollback({
target: checkpointId,
});
}
/**
* 获取会话统计信息
*/
getSessionStats(): SessionStats {
if (!this.currentSession) {
throw new Error('No active session');
}
return {
duration: Date.now() - this.currentSession.startTime,
checkpointCount: this.currentSession.checkpoints.length,
modifiedFilesCount: this.currentSession.modifiedFiles.length,
modifiedFiles: [...this.currentSession.modifiedFiles],
};
}
/**
* 获取当前会话状态
*/
getSessionState(): SessionState | null {
if (!this.currentSession) {
return null;
}
return {
...this.currentSession,
modifiedFiles: [...this.currentSession.modifiedFiles],
checkpoints: [...this.currentSession.checkpoints],
};
}
/**
* 获取会话开始时的检查点
*/
getStartCheckpoint(): string | null {
return this.currentSession?.startCheckpoint ?? null;
}
/**
* 获取会话持续时间(毫秒)
*/
getSessionDuration(): number {
if (!this.currentSession) {
return 0;
}
return Date.now() - this.currentSession.startTime;
}
}
/**
* 创建会话跟踪器实例
*/
export function createSessionTracker(
checkpointManager: CheckpointManager
): SessionTracker {
return new SessionTracker(checkpointManager);
}
+110 -1
View File
@@ -16,7 +16,10 @@ export type CheckpointTrigger =
| 'tool:copy_file' // 复制文件前
| 'tool:bash' // bash 命令前
| 'task_start' // 任务开始
| 'task_complete'; // 任务完成
| 'task_complete' // 任务完成
| 'pre_rollback' // 回滚前(用于 unrevert
| 'session_start' // 会话开始
| 'session_end'; // 会话结束
/**
* 检查点元数据
@@ -41,6 +44,12 @@ export interface CheckpointMetadata {
commitHash: string;
/** 受影响的文件数 */
filesChanged: number;
/** 关联的消息 ID */
messageId?: string;
/** 会话 ID */
sessionId?: string;
/** 对话轮次 */
turnIndex?: number;
}
/**
@@ -140,6 +149,18 @@ export interface FileDiff {
patch?: string;
}
/**
* 恢复模式
*/
export enum RestoreMode {
/** 仅恢复 AI 修改的文件 */
AI_CHANGES_ONLY = 'ai_changes_only',
/** 仅恢复工作区变更 */
WORKSPACE_ONLY = 'workspace_only',
/** 完整恢复(AI + 工作区) */
FULL = 'full',
}
/**
* 回滚选项
*/
@@ -150,6 +171,10 @@ export interface RollbackOptions {
files?: string[];
/** 预览模式 (不实际执行) */
dryRun?: boolean;
/** 恢复模式 */
mode?: RestoreMode;
/** 跳过安全检查 */
skipSafetyCheck?: boolean;
}
/**
@@ -189,3 +214,87 @@ export interface CheckpointEvent {
* 检查点事件监听器
*/
export type CheckpointEventListener = (event: CheckpointEvent) => void;
/**
* 回滚记录(用于 unrevert
*/
export interface RollbackRecord {
/** 唯一标识 */
id: string;
/** 时间戳 */
timestamp: number;
/** 目标检查点 */
targetCheckpoint: string;
/** 回滚前的 commit hash */
previousCommit: string;
/** 恢复的文件列表 */
restoredFiles: string[];
/** 是否可撤销 */
canUnrevert: boolean;
}
/**
* 撤销回滚结果
*/
export interface UnrevertResult {
/** 是否成功 */
success: boolean;
/** 恢复的 commit */
restoredCommit: string;
/** 恢复的文件数 */
filesRestored: number;
/** 错误信息 */
error?: string;
}
/**
* 安全检查结果
*/
export interface SafetyCheckResult {
/** 是否安全 */
safe: boolean;
/** 警告列表 */
warnings: string[];
/** 错误列表 */
errors: string[];
}
/**
* 会话状态
*/
export interface SessionState {
/** 会话 ID */
id: string;
/** 开始时间 */
startTime: number;
/** 会话开始时的检查点 ID */
startCheckpoint: string;
/** 会话中创建的检查点 ID 列表 */
checkpoints: string[];
/** 会话中修改的文件 */
modifiedFiles: string[];
}
/**
* 会话统计
*/
export interface SessionStats {
/** 持续时间(毫秒) */
duration: number;
/** 检查点数量 */
checkpointCount: number;
/** 修改的文件数 */
modifiedFilesCount: number;
/** 修改的文件列表 */
modifiedFiles: string[];
}
/**
* 路径验证结果
*/
export interface PathValidationResult {
/** 是否有效 */
valid: boolean;
/** 原因 */
reason?: string;
}
+47
View File
@@ -44,6 +44,53 @@ export type {
CommandOperationResult,
} from './commands/index.js';
// Checkpoint
export {
CheckpointManager,
getCheckpointManager,
initCheckpointManager,
resetCheckpointManager,
ShadowGit,
createShadowGit,
hashWorkingDir,
CheckpointSafetyChecker,
createSafetyChecker,
SessionTracker,
createSessionTracker,
CheckpointLock,
LFSPatternLoader,
createLFSPatternLoader,
isCommonLargeFile,
COMMON_LARGE_FILE_EXTENSIONS,
WorkspacePathValidator,
createPathValidator,
CommitMessageGenerator,
createCommitMessageGenerator,
RestoreMode,
DEFAULT_CHECKPOINT_CONFIG,
} from './checkpoint/index.js';
export type {
CheckpointMetadata,
CheckpointConfig,
CheckpointTrigger,
FileChange,
FileChangeType,
DiffInfo,
FileDiff,
RollbackOptions,
RollbackResult,
CheckpointEvent,
CheckpointEventType,
CheckpointEventListener,
RollbackRecord,
UnrevertResult,
SafetyCheckResult,
SessionState,
SessionStats,
PathValidationResult,
} from './checkpoint/index.js';
const program = new Command();
// MCP 管理器实例