feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
/**
|
||||
* 检查点管理器
|
||||
* 管理检查点的创建、回滚、清理等操作
|
||||
*/
|
||||
|
||||
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,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* 检查点提交消息前缀
|
||||
*/
|
||||
const CHECKPOINT_PREFIX = 'checkpoint:';
|
||||
|
||||
/**
|
||||
* 检查点管理器
|
||||
*/
|
||||
export class CheckpointManager {
|
||||
private shadowGit: ShadowGit;
|
||||
private config: CheckpointConfig;
|
||||
private workDir: string;
|
||||
private checkpointsIndex: Map<string, CheckpointMetadata> = new Map();
|
||||
private initialized = false;
|
||||
private lastCheckpointTime = 0;
|
||||
private eventListeners: Set<CheckpointEventListener> = new Set();
|
||||
|
||||
// 防止重复创建检查点的最小间隔 (毫秒)
|
||||
private static readonly MIN_CHECKPOINT_INTERVAL = 1000;
|
||||
|
||||
constructor(workDir: string, config: Partial<CheckpointConfig> = {}) {
|
||||
this.workDir = path.resolve(workDir);
|
||||
this.config = {
|
||||
enabled: true,
|
||||
autoCheckpoint: {
|
||||
beforeWrite: true,
|
||||
beforeEdit: true,
|
||||
beforeDelete: true,
|
||||
beforeMove: true,
|
||||
beforeBash: false,
|
||||
},
|
||||
maxCheckpoints: 100,
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000,
|
||||
storageDir: path.join(os.homedir(), '.ai-assist', 'checkpoints'),
|
||||
...config,
|
||||
};
|
||||
|
||||
this.shadowGit = createShadowGit(this.workDir, this.config.storageDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化检查点管理器
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
|
||||
if (!this.config.enabled) {
|
||||
this.initialized = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化 Shadow Git
|
||||
await this.shadowGit.initialize();
|
||||
|
||||
// 加载检查点索引
|
||||
await this.loadCheckpointsIndex();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载检查点索引
|
||||
*/
|
||||
private async loadCheckpointsIndex(): Promise<void> {
|
||||
try {
|
||||
const commits = await this.shadowGit.getCommits(this.config.maxCheckpoints);
|
||||
|
||||
for (const commit of commits) {
|
||||
if (commit.message.startsWith(CHECKPOINT_PREFIX)) {
|
||||
try {
|
||||
const jsonStr = commit.message.slice(CHECKPOINT_PREFIX.length);
|
||||
const metadata = JSON.parse(jsonStr) as CheckpointMetadata;
|
||||
metadata.commitHash = commit.hash;
|
||||
this.checkpointsIndex.set(metadata.id, metadata);
|
||||
} catch {
|
||||
// 解析失败,跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 仓库可能是空的
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该为指定工具创建检查点
|
||||
*/
|
||||
shouldCreateCheckpoint(tool: string): boolean {
|
||||
if (!this.config.enabled) return false;
|
||||
|
||||
const { autoCheckpoint } = this.config;
|
||||
|
||||
switch (tool) {
|
||||
case 'write_file':
|
||||
return autoCheckpoint.beforeWrite;
|
||||
case 'edit_file':
|
||||
return autoCheckpoint.beforeEdit;
|
||||
case 'delete_file':
|
||||
return autoCheckpoint.beforeDelete;
|
||||
case 'move_file':
|
||||
case 'copy_file':
|
||||
return autoCheckpoint.beforeMove;
|
||||
case 'bash':
|
||||
return autoCheckpoint.beforeBash;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在工具执行前创建检查点
|
||||
*/
|
||||
async beforeToolExecution(
|
||||
tool: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<string | null> {
|
||||
if (!this.shouldCreateCheckpoint(tool)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 防止过于频繁的检查点创建
|
||||
const now = Date.now();
|
||||
if (now - this.lastCheckpointTime < CheckpointManager.MIN_CHECKPOINT_INTERVAL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const checkpoint = await this.createCheckpoint({
|
||||
trigger: `tool:${tool}` as CheckpointTrigger,
|
||||
toolCall: { tool, params },
|
||||
description: this.generateDescription(tool, params),
|
||||
});
|
||||
|
||||
this.lastCheckpointTime = now;
|
||||
return checkpoint.id;
|
||||
} catch (error) {
|
||||
console.warn('Failed to create checkpoint:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成检查点描述
|
||||
*/
|
||||
private generateDescription(
|
||||
tool: string,
|
||||
params: Record<string, unknown>
|
||||
): string {
|
||||
switch (tool) {
|
||||
case 'write_file':
|
||||
return `Write file: ${params.file_path || params.path}`;
|
||||
case 'edit_file':
|
||||
return `Edit file: ${params.file_path || params.path}`;
|
||||
case 'delete_file':
|
||||
return `Delete file: ${params.file_path || params.path}`;
|
||||
case 'move_file':
|
||||
return `Move: ${params.source} -> ${params.destination}`;
|
||||
case 'copy_file':
|
||||
return `Copy: ${params.source} -> ${params.destination}`;
|
||||
case 'bash':
|
||||
return `Bash: ${String(params.command).slice(0, 50)}`;
|
||||
default:
|
||||
return `Tool: ${tool}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建检查点
|
||||
*/
|
||||
async createCheckpoint(options: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
trigger?: CheckpointTrigger;
|
||||
toolCall?: { tool: string; params: Record<string, unknown> };
|
||||
}): Promise<CheckpointMetadata> {
|
||||
await this.initialize();
|
||||
|
||||
if (!this.config.enabled) {
|
||||
throw new Error('Checkpoint system is disabled');
|
||||
}
|
||||
|
||||
const id = nanoid(10);
|
||||
const timestamp = Date.now();
|
||||
|
||||
// 创建元数据
|
||||
const metadata: CheckpointMetadata = {
|
||||
id,
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
timestamp,
|
||||
trigger: options.trigger || 'manual',
|
||||
toolCall: options.toolCall,
|
||||
commitHash: '', // 待填充
|
||||
filesChanged: 0, // 待填充
|
||||
};
|
||||
|
||||
// 获取变更文件数
|
||||
try {
|
||||
const diff = await this.shadowGit.getWorkingDirDiff();
|
||||
metadata.filesChanged = diff.files.length;
|
||||
} catch {
|
||||
// 忽略
|
||||
}
|
||||
|
||||
// 创建 commit
|
||||
const commitMessage = CHECKPOINT_PREFIX + JSON.stringify(metadata);
|
||||
const commitHash = await this.shadowGit.createCommit(commitMessage);
|
||||
metadata.commitHash = commitHash;
|
||||
|
||||
// 更新索引
|
||||
this.checkpointsIndex.set(id, metadata);
|
||||
|
||||
// 触发事件
|
||||
this.emitEvent({
|
||||
type: 'created',
|
||||
checkpoint: metadata,
|
||||
timestamp,
|
||||
});
|
||||
|
||||
// 异步清理
|
||||
this.cleanupAsync();
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建命名检查点
|
||||
*/
|
||||
async createNamedCheckpoint(name: string, description?: string): Promise<CheckpointMetadata> {
|
||||
return this.createCheckpoint({
|
||||
name,
|
||||
description,
|
||||
trigger: 'manual',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有检查点
|
||||
*/
|
||||
async listCheckpoints(): Promise<CheckpointMetadata[]> {
|
||||
await this.initialize();
|
||||
|
||||
return Array.from(this.checkpointsIndex.values()).sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定检查点
|
||||
*/
|
||||
async getCheckpoint(idOrHash: string): Promise<CheckpointMetadata | null> {
|
||||
await this.initialize();
|
||||
|
||||
// 先按 ID 查找
|
||||
if (this.checkpointsIndex.has(idOrHash)) {
|
||||
return this.checkpointsIndex.get(idOrHash)!;
|
||||
}
|
||||
|
||||
// 再按 commit hash 查找
|
||||
for (const checkpoint of this.checkpointsIndex.values()) {
|
||||
if (checkpoint.commitHash.startsWith(idOrHash)) {
|
||||
return checkpoint;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的检查点
|
||||
*/
|
||||
async getLatestCheckpoint(): Promise<CheckpointMetadata | null> {
|
||||
const checkpoints = await this.listCheckpoints();
|
||||
return checkpoints[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查点与当前工作区的差异
|
||||
*/
|
||||
async getDiff(checkpointId: string): Promise<DiffInfo> {
|
||||
await this.initialize();
|
||||
|
||||
const checkpoint = await this.getCheckpoint(checkpointId);
|
||||
if (!checkpoint) {
|
||||
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
||||
}
|
||||
|
||||
return this.shadowGit.getDiffSummary(checkpoint.commitHash, 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个检查点之间的差异
|
||||
*/
|
||||
async getDiffBetween(fromId: string, toId: string): Promise<DiffInfo> {
|
||||
await this.initialize();
|
||||
|
||||
const fromCheckpoint = await this.getCheckpoint(fromId);
|
||||
const toCheckpoint = await this.getCheckpoint(toId);
|
||||
|
||||
if (!fromCheckpoint) {
|
||||
throw new Error(`Checkpoint not found: ${fromId}`);
|
||||
}
|
||||
if (!toCheckpoint) {
|
||||
throw new Error(`Checkpoint not found: ${toId}`);
|
||||
}
|
||||
|
||||
return this.shadowGit.getDiffSummary(
|
||||
fromCheckpoint.commitHash,
|
||||
toCheckpoint.commitHash
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件差异详情
|
||||
*/
|
||||
async getFileDiff(checkpointId: string, filePath: string): Promise<FileDiff> {
|
||||
await this.initialize();
|
||||
|
||||
const checkpoint = await this.getCheckpoint(checkpointId);
|
||||
if (!checkpoint) {
|
||||
throw new Error(`Checkpoint not found: ${checkpointId}`);
|
||||
}
|
||||
|
||||
const head = await this.shadowGit.getHead();
|
||||
return this.shadowGit.getFileDiff(checkpoint.commitHash, head, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚到检查点
|
||||
*/
|
||||
async rollback(options: RollbackOptions): Promise<RollbackResult> {
|
||||
await this.initialize();
|
||||
|
||||
const checkpoint = await this.getCheckpoint(options.target);
|
||||
if (!checkpoint) {
|
||||
throw new Error(`Checkpoint not found: ${options.target}`);
|
||||
}
|
||||
|
||||
// 获取当前 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,
|
||||
};
|
||||
}
|
||||
|
||||
const result: RollbackResult = {
|
||||
success: true,
|
||||
restoredFiles: [],
|
||||
errors: [],
|
||||
previousCommit,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// 获取恢复的文件列表
|
||||
const diff = await this.shadowGit.getDiffSummary(
|
||||
previousCommit,
|
||||
checkpoint.commitHash
|
||||
);
|
||||
result.restoredFiles = diff.files.map((f) => f.path);
|
||||
}
|
||||
|
||||
// 触发事件
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销操作 (回滚到上一个检查点)
|
||||
*/
|
||||
async undo(): Promise<RollbackResult> {
|
||||
const latest = await this.getLatestCheckpoint();
|
||||
if (!latest) {
|
||||
throw new Error('No checkpoints available');
|
||||
}
|
||||
|
||||
// 找到倒数第二个检查点
|
||||
const checkpoints = await this.listCheckpoints();
|
||||
if (checkpoints.length < 2) {
|
||||
// 只有一个检查点,回滚到它
|
||||
return this.rollback({ target: latest.id });
|
||||
}
|
||||
|
||||
// 回滚到倒数第二个检查点
|
||||
return this.rollback({ target: checkpoints[1].id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除检查点
|
||||
*/
|
||||
async deleteCheckpoint(checkpointId: string): Promise<boolean> {
|
||||
await this.initialize();
|
||||
|
||||
if (!this.checkpointsIndex.has(checkpointId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkpoint = this.checkpointsIndex.get(checkpointId)!;
|
||||
this.checkpointsIndex.delete(checkpointId);
|
||||
|
||||
// 触发事件
|
||||
this.emitEvent({
|
||||
type: 'deleted',
|
||||
checkpoint,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步清理过期检查点
|
||||
*/
|
||||
private async cleanupAsync(): Promise<void> {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.cleanup();
|
||||
} catch (error) {
|
||||
console.warn('Checkpoint cleanup failed:', error);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期检查点
|
||||
*/
|
||||
async cleanup(): Promise<number> {
|
||||
await this.initialize();
|
||||
|
||||
const checkpoints = await this.listCheckpoints();
|
||||
const now = Date.now();
|
||||
let deletedCount = 0;
|
||||
|
||||
// 按时间过期清理
|
||||
for (const checkpoint of checkpoints) {
|
||||
if (now - checkpoint.timestamp > this.config.maxAge) {
|
||||
await this.deleteCheckpoint(checkpoint.id);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 按数量限制清理
|
||||
const remaining = checkpoints.length - deletedCount;
|
||||
if (remaining > this.config.maxCheckpoints) {
|
||||
const toDelete = checkpoints.slice(this.config.maxCheckpoints);
|
||||
for (const checkpoint of toDelete) {
|
||||
if (this.checkpointsIndex.has(checkpoint.id)) {
|
||||
await this.deleteCheckpoint(checkpoint.id);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
// 触发清理事件
|
||||
this.emitEvent({
|
||||
type: 'cleanup',
|
||||
timestamp: now,
|
||||
details: { deletedCount },
|
||||
});
|
||||
|
||||
// 运行 git gc
|
||||
await this.shadowGit.cleanup(this.config.maxCheckpoints);
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取检查点存储统计
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
count: number;
|
||||
oldestTimestamp: number | null;
|
||||
newestTimestamp: number | null;
|
||||
}> {
|
||||
const checkpoints = await this.listCheckpoints();
|
||||
|
||||
return {
|
||||
count: checkpoints.length,
|
||||
oldestTimestamp: checkpoints.length > 0
|
||||
? checkpoints[checkpoints.length - 1].timestamp
|
||||
: null,
|
||||
newestTimestamp: checkpoints.length > 0 ? checkpoints[0].timestamp : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
*/
|
||||
addEventListener(listener: CheckpointEventListener): void {
|
||||
this.eventListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除事件监听器
|
||||
*/
|
||||
removeEventListener(listener: CheckpointEventListener): void {
|
||||
this.eventListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
*/
|
||||
private emitEvent(event: CheckpointEvent): void {
|
||||
for (const listener of this.eventListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
console.warn('Checkpoint event listener error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.config.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig(): CheckpointConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
}
|
||||
|
||||
// 全局检查点管理器实例
|
||||
let globalCheckpointManager: CheckpointManager | null = null;
|
||||
|
||||
/**
|
||||
* 获取全局检查点管理器实例
|
||||
*/
|
||||
export function getCheckpointManager(): CheckpointManager {
|
||||
if (!globalCheckpointManager) {
|
||||
globalCheckpointManager = new CheckpointManager(process.cwd());
|
||||
}
|
||||
return globalCheckpointManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局检查点管理器
|
||||
*/
|
||||
export async function initCheckpointManager(
|
||||
workDir: string,
|
||||
config?: Partial<CheckpointConfig>
|
||||
): Promise<CheckpointManager> {
|
||||
globalCheckpointManager = new CheckpointManager(workDir, config);
|
||||
await globalCheckpointManager.initialize();
|
||||
return globalCheckpointManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置全局检查点管理器 (用于测试)
|
||||
*/
|
||||
export function resetCheckpointManager(): void {
|
||||
globalCheckpointManager = null;
|
||||
}
|
||||
Reference in New Issue
Block a user