1b7d55848d
- 删除 Server 中 60+ 个与 Core 重复的类型定义 - 将动态导入 (await import) 改为静态类型导入 (import type) - 保留必要的运行时静态导入 - 修复测试文件中的 mock 初始化问题 - 净删除约 960 行重复代码 重构文件: - routes/checkpoints.ts: 删除 155 行重复类型 - routes/agents.ts: 删除 93 行重复类型 - routes/commands.ts: 删除 83 行重复类型 - routes/mcp.ts: 修复类型窄化 - routes/hooks.ts: 已使用静态导入 - routes/providers.ts: 删除 63 行重复类型 - session/manager.ts: 删除 41 行重复类型 - routes/sessions.ts: 添加类型导入 - permission/handler.ts: 添加类型导入
733 lines
14 KiB
TypeScript
733 lines
14 KiB
TypeScript
/**
|
||
* Checkpoints API Routes
|
||
*
|
||
* 提供 Checkpoint 管理的 REST API
|
||
*/
|
||
|
||
import { Hono } from 'hono';
|
||
import { getConfig } from './config.js';
|
||
import type {
|
||
CheckpointMetadata,
|
||
CheckpointConfig,
|
||
CheckpointTrigger,
|
||
FileChange,
|
||
FileChangeType,
|
||
DiffInfo,
|
||
FileDiff,
|
||
RollbackOptions,
|
||
RollbackResult,
|
||
RollbackRecord,
|
||
SafetyCheckResult,
|
||
UnrevertResult,
|
||
} from '@ai-assistant/core';
|
||
import {
|
||
CheckpointManager,
|
||
getCheckpointManager,
|
||
initCheckpointManager,
|
||
RestoreMode,
|
||
} from '@ai-assistant/core';
|
||
|
||
interface CheckpointStats {
|
||
count: number;
|
||
oldestTimestamp: number | null;
|
||
newestTimestamp: number | null;
|
||
}
|
||
|
||
export const checkpointsRouter = new Hono();
|
||
|
||
// Manager 初始化状态
|
||
let managerInitialized = false;
|
||
|
||
/**
|
||
* 初始化 Checkpoint 模块
|
||
*/
|
||
async function ensureCheckpointManager(): Promise<CheckpointManager | null> {
|
||
if (managerInitialized) {
|
||
return getCheckpointManager();
|
||
}
|
||
|
||
try {
|
||
const config = getConfig();
|
||
await initCheckpointManager(config.workdir);
|
||
managerInitialized = true;
|
||
console.log('[Checkpoints] Checkpoint module initialized');
|
||
return getCheckpointManager();
|
||
} catch (error) {
|
||
console.warn('[Checkpoints] Failed to initialize Checkpoint module:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET /checkpoints - 获取所有检查点列表
|
||
*/
|
||
checkpointsRouter.get('/', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const checkpoints = await manager.listCheckpoints();
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoints.map((cp) => ({
|
||
id: cp.id,
|
||
name: cp.name,
|
||
description: cp.description,
|
||
timestamp: cp.timestamp,
|
||
trigger: cp.trigger,
|
||
filesChanged: cp.filesChanged,
|
||
commitHash: cp.commitHash,
|
||
messageId: cp.messageId,
|
||
sessionId: cp.sessionId,
|
||
})),
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to list checkpoints',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/stats - 获取检查点统计信息
|
||
*/
|
||
checkpointsRouter.get('/stats', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const stats = await manager.getStats();
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: stats,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get stats',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/latest - 获取最新检查点
|
||
*/
|
||
checkpointsRouter.get('/latest', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const checkpoint = await manager.getLatestCheckpoint();
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoint,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get latest checkpoint',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/unrevert/status - 检查是否可撤销回滚
|
||
*/
|
||
checkpointsRouter.get('/unrevert/status', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const canUnrevert = manager.canUnrevert();
|
||
const lastRollback = manager.getLastRollback();
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: {
|
||
canUnrevert,
|
||
lastRollback: lastRollback
|
||
? {
|
||
id: lastRollback.id,
|
||
timestamp: lastRollback.timestamp,
|
||
targetCheckpoint: lastRollback.targetCheckpoint,
|
||
restoredFiles: lastRollback.restoredFiles,
|
||
}
|
||
: null,
|
||
},
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get unrevert status',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/:id - 获取单个检查点详情
|
||
*/
|
||
checkpointsRouter.get('/:id', async (c) => {
|
||
const id = c.req.param('id');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const checkpoint = await manager.getCheckpoint(id);
|
||
|
||
if (!checkpoint) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: `Checkpoint not found: ${id}`,
|
||
},
|
||
404
|
||
);
|
||
}
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoint,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get checkpoint',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /checkpoints - 创建手动检查点
|
||
*/
|
||
checkpointsRouter.post('/', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const body = await c.req.json<{ name?: string; description?: string }>();
|
||
|
||
const checkpoint = await manager.createCheckpoint({
|
||
name: body.name,
|
||
description: body.description,
|
||
trigger: 'manual',
|
||
});
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoint,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to create checkpoint',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* DELETE /checkpoints/:id - 删除检查点
|
||
*/
|
||
checkpointsRouter.delete('/:id', async (c) => {
|
||
const id = c.req.param('id');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const deleted = await manager.deleteCheckpoint(id);
|
||
|
||
if (!deleted) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: `Checkpoint not found: ${id}`,
|
||
},
|
||
404
|
||
);
|
||
}
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: { deleted: true },
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to delete checkpoint',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/:id/diff - 获取检查点与当前工作区的差异
|
||
*/
|
||
checkpointsRouter.get('/:id/diff', async (c) => {
|
||
const id = c.req.param('id');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const diff = await manager.getDiff(id);
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: diff,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get diff',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/:id/file-diff - 获取单个文件的详细差异
|
||
*/
|
||
checkpointsRouter.get('/:id/file-diff', async (c) => {
|
||
const id = c.req.param('id');
|
||
const filePath = c.req.query('path');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
if (!filePath) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'File path is required',
|
||
},
|
||
400
|
||
);
|
||
}
|
||
|
||
try {
|
||
const fileDiff = await manager.getFileDiff(id, filePath);
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: fileDiff,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get file diff',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /checkpoints/:id/restore - 回滚到检查点
|
||
*/
|
||
checkpointsRouter.post('/:id/restore', async (c) => {
|
||
const id = c.req.param('id');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const body = await c.req.json<{
|
||
mode?: 'ai_changes_only' | 'workspace_only' | 'full';
|
||
files?: string[];
|
||
skipSafetyCheck?: boolean;
|
||
}>();
|
||
|
||
// 转换 mode 字符串为枚举值
|
||
let mode: RestoreMode | undefined;
|
||
if (body.mode) {
|
||
switch (body.mode) {
|
||
case 'ai_changes_only':
|
||
mode = RestoreMode.AI_CHANGES_ONLY;
|
||
break;
|
||
case 'workspace_only':
|
||
mode = RestoreMode.WORKSPACE_ONLY;
|
||
break;
|
||
case 'full':
|
||
mode = RestoreMode.FULL;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const result = await manager.rollback({
|
||
target: id,
|
||
mode,
|
||
files: body.files,
|
||
skipSafetyCheck: body.skipSafetyCheck,
|
||
});
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to restore checkpoint',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/:id/restore/preview - 预览回滚(dry run)
|
||
*/
|
||
checkpointsRouter.get('/:id/restore/preview', async (c) => {
|
||
const id = c.req.param('id');
|
||
const modeParam = c.req.query('mode');
|
||
const filesParam = c.req.query('files');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
// 转换 mode 字符串为枚举值
|
||
let mode: RestoreMode | undefined;
|
||
if (modeParam) {
|
||
switch (modeParam) {
|
||
case 'ai_changes_only':
|
||
mode = RestoreMode.AI_CHANGES_ONLY;
|
||
break;
|
||
case 'workspace_only':
|
||
mode = RestoreMode.WORKSPACE_ONLY;
|
||
break;
|
||
case 'full':
|
||
mode = RestoreMode.FULL;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const files = filesParam ? filesParam.split(',') : undefined;
|
||
|
||
const result = await manager.rollback({
|
||
target: id,
|
||
mode,
|
||
files,
|
||
dryRun: true,
|
||
});
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to preview restore',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /checkpoints/unrevert - 撤销最近一次回滚
|
||
*/
|
||
checkpointsRouter.post('/unrevert', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const result = await manager.unrevert();
|
||
|
||
if (!result.success) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: result.error || 'Failed to unrevert',
|
||
},
|
||
400
|
||
);
|
||
}
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to unrevert',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/:id/safety-check - 执行安全检查
|
||
*/
|
||
checkpointsRouter.get('/:id/safety-check', async (c) => {
|
||
const id = c.req.param('id');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const result = await manager.checkSafety(id);
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: result,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to check safety',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /checkpoints/cleanup - 清理过期检查点
|
||
*/
|
||
checkpointsRouter.post('/cleanup', async (c) => {
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const deleted = await manager.cleanup();
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: { deleted },
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to cleanup',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/sessions/:sessionId - 获取会话的所有检查点
|
||
*/
|
||
checkpointsRouter.get('/sessions/:sessionId', async (c) => {
|
||
const sessionId = c.req.param('sessionId');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const checkpoints = await manager.getSessionCheckpoints(sessionId);
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoints,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get session checkpoints',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /checkpoints/messages/:messageId - 获取消息关联的检查点
|
||
*/
|
||
checkpointsRouter.get('/messages/:messageId', async (c) => {
|
||
const messageId = c.req.param('messageId');
|
||
const manager = await ensureCheckpointManager();
|
||
|
||
if (!manager) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: 'Checkpoint module not available',
|
||
},
|
||
503
|
||
);
|
||
}
|
||
|
||
try {
|
||
const checkpoints = await manager.getMessageCheckpoints(messageId);
|
||
|
||
return c.json({
|
||
success: true,
|
||
data: checkpoints,
|
||
});
|
||
} catch (error) {
|
||
return c.json(
|
||
{
|
||
success: false,
|
||
error: error instanceof Error ? error.message : 'Failed to get message checkpoints',
|
||
},
|
||
500
|
||
);
|
||
}
|
||
});
|