refactor(core): 拆分大型单体文件为模块化子组件

将三个超过 700 行的大型文件重构为模块化架构:

Agent (1033 → ~400 行):
- agent-tool-executor: 工具获取、过滤和执行
- agent-message-handler: 消息构建、流式处理
- agent-mode-manager: 模式切换和权限检查
- agent-vision-handler: 视觉处理委托

CheckpointManager (1015 → ~620 行):
- checkpoint-store: 检查点 CRUD 操作
- checkpoint-rollback: 回滚和撤销操作
- checkpoint-session: 会话跟踪
- checkpoint-events: 事件发射系统

SessionManager (768 → 356 行):
- message-converter: Part ↔ ModelMessage 转换
- session-store: 会话 CRUD 操作
- project-manager: 项目管理
- session-auto-save: 自动保存功能

重构原则: 单一职责、编排器模式、向后兼容 API
This commit is contained in:
2025-12-16 22:07:13 +08:00
parent e53035ffc0
commit 66ad1a1ec9
17 changed files with 3154 additions and 1959 deletions
@@ -0,0 +1,110 @@
/**
* 检查点事件系统
* 负责事件的发布和订阅
*/
import type {
CheckpointEvent,
CheckpointEventListener,
CheckpointMetadata,
} from './types.js';
/**
* 检查点事件管理器
*/
export class CheckpointEvents {
private listeners: Set<CheckpointEventListener> = new Set();
/**
* 添加事件监听器
*/
addEventListener(listener: CheckpointEventListener): void {
this.listeners.add(listener);
}
/**
* 移除事件监听器
*/
removeEventListener(listener: CheckpointEventListener): void {
this.listeners.delete(listener);
}
/**
* 触发事件
*/
emit(event: CheckpointEvent): void {
for (const listener of this.listeners) {
try {
listener(event);
} catch (error) {
console.warn('Checkpoint event listener error:', error);
}
}
}
/**
* 触发创建事件
*/
emitCreated(checkpoint: CheckpointMetadata): void {
this.emit({
type: 'created',
checkpoint,
timestamp: Date.now(),
});
}
/**
* 触发恢复事件
*/
emitRestored(
checkpoint: CheckpointMetadata,
details: {
files: string[];
previousCommit: string;
mode: string;
}
): void {
this.emit({
type: 'restored',
checkpoint,
timestamp: Date.now(),
details,
});
}
/**
* 触发删除事件
*/
emitDeleted(checkpoint: CheckpointMetadata): void {
this.emit({
type: 'deleted',
checkpoint,
timestamp: Date.now(),
});
}
/**
* 触发清理事件
*/
emitCleanup(deletedCount: number): void {
this.emit({
type: 'cleanup',
timestamp: Date.now(),
details: { deletedCount },
});
}
/**
* 获取监听器数量
*/
get listenerCount(): number {
return this.listeners.size;
}
/**
* 清除所有监听器
*/
clearListeners(): void {
this.listeners.clear();
}
}
@@ -0,0 +1,289 @@
/**
* 检查点回滚
* 负责回滚相关操作
*/
import { nanoid } from 'nanoid';
import type { ShadowGit } from './shadow-git.js';
import type { CheckpointLock } from './lock.js';
import type { CheckpointSafetyChecker } from './safety.js';
import type { CheckpointStore } from './checkpoint-store.js';
import {
RestoreMode,
type CheckpointMetadata,
type RollbackOptions,
type RollbackResult,
type RollbackRecord,
type UnrevertResult,
type DiffInfo,
} from './types.js';
/**
* 检查点回滚
*/
export class CheckpointRollback {
private shadowGit: ShadowGit;
private lock: CheckpointLock;
private safetyChecker: CheckpointSafetyChecker;
private store: CheckpointStore;
private lastRollback: RollbackRecord | null = null;
constructor(
shadowGit: ShadowGit,
lock: CheckpointLock,
safetyChecker: CheckpointSafetyChecker,
store: CheckpointStore
) {
this.shadowGit = shadowGit;
this.lock = lock;
this.safetyChecker = safetyChecker;
this.store = store;
}
/**
* 回滚到指定检查点
*/
async rollback(
options: RollbackOptions,
onEvent?: (event: { type: string; checkpoint: CheckpointMetadata; details?: unknown }) => void
): Promise<RollbackResult> {
const checkpoint = this.store.get(options.target);
if (!checkpoint) {
throw new Error(`Checkpoint not found: ${options.target}`);
}
// 安全检查(除非明确跳过)
if (!options.skipSafetyCheck) {
const safetyResult = await this.safetyChecker.checkBeforeRollback(checkpoint, {
getCheckpoint: (id: string) => Promise.resolve(this.store.get(id)),
listCheckpoints: () => Promise.resolve(this.store.list()),
getDiff: (checkpointId: string) => this.getDiffById(checkpointId),
});
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('; '));
}
}
// 使用锁保护回滚操作
return this.lock.withLock(async () => {
const previousCommit = await this.shadowGit.getHead();
// 预览模式
if (options.dryRun) {
const diff = await this.getDiff(checkpoint);
return {
success: true,
restoredFiles: diff.files.map((f) => f.path),
errors: [],
previousCommit,
};
}
// 创建回滚前检查点(用于 unrevert)
let preRollbackCheckpoint: CheckpointMetadata | null = null;
try {
preRollbackCheckpoint = await this.store.createInternal({
trigger: 'pre_rollback',
description: `Before rollback to ${options.target}`,
});
} catch {
// 忽略创建失败
}
const result: RollbackResult = {
success: true,
restoredFiles: [],
errors: [],
previousCommit,
};
try {
const mode = options.mode || RestoreMode.FULL;
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);
}
// 记录回滚信息(用于 unrevert)
this.lastRollback = {
id: nanoid(10),
timestamp: Date.now(),
targetCheckpoint: checkpoint.id,
previousCommit: preRollbackCheckpoint?.commitHash || previousCommit,
restoredFiles: result.restoredFiles,
canUnrevert: true,
};
// 触发事件
onEvent?.({
type: 'restored',
checkpoint,
details: {
files: result.restoredFiles,
previousCommit,
mode,
},
});
} catch (error) {
result.success = false;
result.errors.push({
file: '*',
error: error instanceof Error ? error.message : String(error),
});
}
return result;
});
}
/**
* 撤销操作(回滚到上一个检查点)
*/
async undo(
onEvent?: (event: { type: string; checkpoint: CheckpointMetadata; details?: unknown }) => void
): Promise<RollbackResult> {
const latest = this.store.getLatest();
if (!latest) {
throw new Error('No checkpoints available');
}
const checkpoints = this.store.list();
if (checkpoints.length < 2) {
return this.rollback({ target: latest.id }, onEvent);
}
return this.rollback({ target: checkpoints[1].id }, onEvent);
}
/**
* 撤销最近一次回滚
*/
async unrevert(): Promise<UnrevertResult> {
if (!this.lastRollback || !this.lastRollback.canUnrevert) {
return {
success: false,
restoredCommit: '',
filesRestored: 0,
error: 'No rollback to unrevert',
};
}
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,
};
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;
}
/**
* 获取检查点与当前工作区的差异
*/
private async getDiff(checkpoint: CheckpointMetadata): Promise<DiffInfo> {
return this.shadowGit.getDiffSummary(checkpoint.commitHash, 'HEAD');
}
/**
* 根据 ID 获取检查点与当前工作区的差异
*/
private async getDiffById(checkpointId: string): Promise<DiffInfo> {
const checkpoint = this.store.get(checkpointId);
if (!checkpoint) {
return { from: '', to: 'HEAD', files: [], totalInsertions: 0, totalDeletions: 0 };
}
return this.getDiff(checkpoint);
}
/**
* 获取 AI 修改的文件列表
*/
private async getAiModifiedFiles(checkpoint: CheckpointMetadata): Promise<string[]> {
const files: string[] = [];
const checkpoints = this.store.list();
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);
const aiFiles = await this.getAiModifiedFiles(checkpoint);
return diff.files
.map((f) => f.path)
.filter((path) => !aiFiles.includes(path));
}
}
@@ -0,0 +1,178 @@
/**
* 检查点会话追踪
* 负责会话与检查点的关联
*/
import { nanoid } from 'nanoid';
import type { CheckpointStore } from './checkpoint-store.js';
import type { CheckpointMetadata, CheckpointTrigger } from './types.js';
/**
* 检查点会话追踪
*/
export class CheckpointSession {
private store: CheckpointStore;
private currentSessionId: string | null = null;
private sessionCheckpoints: Map<string, string[]> = new Map();
constructor(store: CheckpointStore) {
this.store = store;
}
/**
* 开始新会话
*/
async startSession(
sessionId?: string,
onEvent?: (event: { type: string; checkpoint?: CheckpointMetadata }) => void
): Promise<string> {
const id = sessionId || nanoid(10);
this.currentSessionId = id;
this.sessionCheckpoints.set(id, []);
// 创建会话开始检查点
try {
const checkpoint = await this.store.create(
{
trigger: 'session_start',
description: `Session started: ${id}`,
sessionId: id,
},
id,
(checkpointId) => {
this.recordCheckpoint(checkpointId);
}
);
onEvent?.({ type: 'created', checkpoint });
} catch {
// 忽略创建失败
}
return id;
}
/**
* 结束当前会话
*/
async endSession(
onEvent?: (event: { type: string; checkpoint?: CheckpointMetadata }) => void
): Promise<void> {
if (!this.currentSessionId) return;
// 创建会话结束检查点
try {
const checkpoint = await this.store.create(
{
trigger: 'session_end',
description: `Session ended: ${this.currentSessionId}`,
sessionId: this.currentSessionId,
},
this.currentSessionId,
(checkpointId) => {
this.recordCheckpoint(checkpointId);
}
);
onEvent?.({ type: 'created', checkpoint });
} catch {
// 忽略创建失败
}
this.currentSessionId = null;
}
/**
* 获取当前会话 ID
*/
getCurrentSessionId(): string | null {
return this.currentSessionId;
}
/**
* 设置当前会话 ID(用于外部恢复)
*/
setCurrentSessionId(sessionId: string | null): void {
this.currentSessionId = sessionId;
}
/**
* 获取会话的所有检查点
*/
getSessionCheckpoints(sessionId: string): CheckpointMetadata[] {
const checkpointIds = this.sessionCheckpoints.get(sessionId);
if (!checkpointIds) return [];
const checkpoints: CheckpointMetadata[] = [];
for (const id of checkpointIds) {
const checkpoint = this.store.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;
},
onEvent?: (event: { type: string; checkpoint: CheckpointMetadata }) => void
): Promise<CheckpointMetadata> {
const checkpoint = await this.store.create(
{
trigger: options?.trigger || 'auto',
description: options?.description || `Message checkpoint: ${messageId}`,
messageId,
sessionId: this.currentSessionId || undefined,
turnIndex,
},
this.currentSessionId,
(checkpointId) => {
this.recordCheckpoint(checkpointId);
}
);
onEvent?.({ type: 'created', checkpoint });
return checkpoint;
}
/**
* 获取与消息关联的检查点
*/
getMessageCheckpoints(messageId: string): CheckpointMetadata[] {
const checkpoints: CheckpointMetadata[] = [];
for (const checkpoint of this.store.list()) {
if (checkpoint.messageId === messageId) {
checkpoints.push(checkpoint);
}
}
return checkpoints.sort((a, b) => a.timestamp - b.timestamp);
}
/**
* 记录检查点到当前会话
*/
recordCheckpoint(checkpointId: string): void {
if (!this.currentSessionId) return;
const sessionCps = this.sessionCheckpoints.get(this.currentSessionId) || [];
sessionCps.push(checkpointId);
this.sessionCheckpoints.set(this.currentSessionId, sessionCps);
}
/**
* 获取会话开始的检查点
*/
getSessionStartCheckpoint(sessionId: string): CheckpointMetadata | null {
const checkpoints = this.getSessionCheckpoints(sessionId);
return checkpoints.find((cp) => cp.trigger === 'session_start') || null;
}
}
@@ -0,0 +1,263 @@
/**
* 检查点存储
* 负责检查点的 CRUD 操作
*/
import { nanoid } from 'nanoid';
import type { ShadowGit } from './shadow-git.js';
import type { CheckpointLock } from './lock.js';
import type { CommitMessageGenerator } from './commit-message.js';
import type {
CheckpointMetadata,
CheckpointConfig,
CheckpointTrigger,
} from './types.js';
/**
* 检查点提交消息前缀
*/
const CHECKPOINT_PREFIX = 'checkpoint:';
/**
* 创建检查点选项
*/
export interface CreateCheckpointOptions {
name?: string;
description?: string;
trigger?: CheckpointTrigger;
toolCall?: { tool: string; params: Record<string, unknown> };
messageId?: string;
sessionId?: string;
turnIndex?: number;
}
/**
* 检查点存储
*/
export class CheckpointStore {
private shadowGit: ShadowGit;
private lock: CheckpointLock;
private commitMessageGenerator: CommitMessageGenerator;
private config: CheckpointConfig;
private index: Map<string, CheckpointMetadata> = new Map();
constructor(
shadowGit: ShadowGit,
lock: CheckpointLock,
commitMessageGenerator: CommitMessageGenerator,
config: CheckpointConfig
) {
this.shadowGit = shadowGit;
this.lock = lock;
this.commitMessageGenerator = commitMessageGenerator;
this.config = config;
}
/**
* 从 Git 历史加载检查点索引
*/
async loadIndex(): 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.index.set(metadata.id, metadata);
} catch {
// 解析失败,跳过
}
}
}
} catch {
// 仓库可能是空的
}
}
/**
* 创建检查点
*/
async create(
options: CreateCheckpointOptions,
currentSessionId?: string | null,
onCheckpointCreated?: (id: string) => void
): Promise<CheckpointMetadata> {
return this.lock.withLock(async () => {
return this.createInternal(options, currentSessionId, onCheckpointCreated);
});
}
/**
* 创建内部检查点(不使用锁,供内部调用)
*/
async createInternal(
options: CreateCheckpointOptions,
currentSessionId?: string | null,
onCheckpointCreated?: (id: string) => void
): Promise<CheckpointMetadata> {
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,
toolCall: options.toolCall,
commitHash: '',
filesChanged: 0,
messageId: options.messageId,
sessionId: options.sessionId || currentSessionId || undefined,
turnIndex: options.turnIndex,
};
// 获取变更文件数
let filesChanged: Array<{ path: string; type: string }> = [];
try {
const diff = await this.shadowGit.getWorkingDirDiff();
metadata.filesChanged = diff.files.length;
filesChanged = diff.files;
} catch {
// 忽略
}
// 生成智能提交消息
const humanReadableMessage = this.commitMessageGenerator.generateMessage(
trigger,
options.toolCall,
filesChanged as Array<{ path: string; type: 'added' | 'modified' | 'deleted' | 'renamed' }>
);
// 创建 commit
const commitMessage = CHECKPOINT_PREFIX + JSON.stringify({
...metadata,
_readableMessage: humanReadableMessage,
});
const commitHash = await this.shadowGit.createCommit(commitMessage);
metadata.commitHash = commitHash;
// 更新索引
this.index.set(id, metadata);
// 通知回调
onCheckpointCreated?.(id);
return metadata;
}
/**
* 列出所有检查点
*/
list(): CheckpointMetadata[] {
return Array.from(this.index.values()).sort(
(a, b) => b.timestamp - a.timestamp
);
}
/**
* 获取检查点
*/
get(idOrHash: string): CheckpointMetadata | null {
// 先按 ID 查找
if (this.index.has(idOrHash)) {
return this.index.get(idOrHash)!;
}
// 再按 commit hash 查找
for (const checkpoint of this.index.values()) {
if (checkpoint.commitHash.startsWith(idOrHash)) {
return checkpoint;
}
}
return null;
}
/**
* 获取最新检查点
*/
getLatest(): CheckpointMetadata | null {
const checkpoints = this.list();
return checkpoints[0] || null;
}
/**
* 删除检查点
*/
delete(checkpointId: string): CheckpointMetadata | null {
const checkpoint = this.index.get(checkpointId);
if (!checkpoint) {
return null;
}
this.index.delete(checkpointId);
return checkpoint;
}
/**
* 判断是否应该为指定工具创建检查点
*/
shouldCreateForTool(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;
}
}
/**
* 生成检查点描述
*/
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}`;
}
}
/**
* 获取检查点数量
*/
get size(): number {
return this.index.size;
}
/**
* 获取配置
*/
getConfig(): CheckpointConfig {
return this.config;
}
}
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -5,11 +5,20 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import type { CheckpointManager } from './manager.js';
import type { SafetyCheckResult, CheckpointMetadata } from './types.js';
import type { SafetyCheckResult, CheckpointMetadata, DiffInfo } from './types.js';
const execAsync = promisify(exec);
/**
* 安全检查器所需的 CheckpointManager 接口
* 使用接口而非完整类以避免循环依赖
*/
export interface SafetyCheckManagerInterface {
listCheckpoints(): Promise<CheckpointMetadata[]>;
getDiff(checkpointId: string): Promise<DiffInfo>;
getCheckpoint(idOrHash: string): Promise<CheckpointMetadata | null>;
}
/**
* 检查点安全检查器
*/
@@ -25,7 +34,7 @@ export class CheckpointSafetyChecker {
*/
async checkBeforeRollback(
checkpoint: CheckpointMetadata,
manager: CheckpointManager
manager: SafetyCheckManagerInterface
): Promise<SafetyCheckResult> {
const result: SafetyCheckResult = {
safe: true,
@@ -87,7 +96,7 @@ export class CheckpointSafetyChecker {
/**
* 检查工作区是否有未保存的变更
*/
private async hasUnsavedChanges(manager: CheckpointManager): Promise<boolean> {
private async hasUnsavedChanges(manager: SafetyCheckManagerInterface): Promise<boolean> {
try {
// 通过 manager 检查
const checkpoints = await manager.listCheckpoints();
@@ -0,0 +1,406 @@
/**
* Agent 消息处理器
* 负责消息的构建、流式处理、压缩
*/
import {
generateText,
streamText,
stepCountIs,
type ModelMessage,
type Tool as AITool,
type LanguageModel,
} from 'ai';
import type { ToolResult, UserInput, ContentBlock, ChatResult } from '../types/index.js';
import {
CompressionManager,
CompressionStatus,
type TokenUsage,
} from '../context/index.js';
import {
createDoomLoopDetector,
type DoomLoopDetector,
DOOM_LOOP_WARNING,
} from './doom-loop.js';
import type { ToolStartInfo, ToolEndInfo } from './agent-tool-executor.js';
/**
* Doom Loop 检测事件信息
*/
export interface DoomLoopInfo {
toolName: string;
count: number;
}
/**
* 消息处理配置
*/
export interface MessageHandlerConfig {
model: LanguageModel;
systemPrompt: string;
maxTokens?: number;
maxSteps?: number;
}
/**
* 流式回调
*/
export interface StreamCallbacks {
onStream?: (text: string) => void;
onToolStart?: (info: ToolStartInfo) => void;
onToolEnd?: (info: ToolEndInfo) => void;
onDoomLoop?: (info: DoomLoopInfo) => void;
}
/**
* Agent 消息处理器
*/
export class AgentMessageHandler {
private compressionManager: CompressionManager;
private conversationHistory: ModelMessage[] = [];
private doomLoopDetector: DoomLoopDetector = createDoomLoopDetector();
constructor(compressionManager: CompressionManager) {
this.compressionManager = compressionManager;
}
/**
* 重置 Doom Loop 检测器
*/
resetDoomLoop(): void {
this.doomLoopDetector.reset();
}
/**
* 构建用户消息内容(处理文本和图片)
*/
buildUserMessageContent(input: string | UserInput): string | ContentBlock[] {
if (typeof input === 'string') {
return input;
}
const blocks: ContentBlock[] = [];
// 添加图片
if (input.images && input.images.length > 0) {
for (const img of input.images) {
blocks.push({
type: 'image',
image: img.data,
mimeType: img.mimeType,
});
}
}
// 添加文本
if (input.text) {
blocks.push({
type: 'text',
text: input.text,
});
}
// 如果只有一个文本块,直接返回文本
if (blocks.length === 1 && blocks[0].type === 'text') {
return blocks[0].text;
}
return blocks;
}
/**
* 添加用户消息到历史
*/
addUserMessage(content: string | ContentBlock[]): void {
this.conversationHistory.push({
role: 'user',
content,
} as ModelMessage);
}
/**
* 流式聊天
*/
async streamChat(
config: MessageHandlerConfig,
tools: Record<string, AITool>,
callbacks: StreamCallbacks,
abortSignal?: AbortSignal
): Promise<ChatResult> {
const { onStream, onToolStart, onToolEnd, onDoomLoop } = callbacks;
const maxSteps = config.maxSteps ?? 50;
// 工具调用时间跟踪
const toolStartTimes = new Map<string, number>();
// Doom loop 检测状态
let doomLoopTriggered = false;
let fullResponse = '';
let responseMessages: ModelMessage[] = [];
const result = streamText({
model: config.model,
system: config.systemPrompt,
messages: this.conversationHistory,
tools: doomLoopTriggered ? {} : tools, // doom loop 时禁用工具
maxOutputTokens: config.maxTokens,
stopWhen: stepCountIs(maxSteps),
abortSignal,
onChunk: ({ chunk }) => {
if (chunk.type === 'tool-call') {
this.handleToolCallChunk(
chunk,
toolStartTimes,
doomLoopTriggered,
onToolStart,
onDoomLoop,
onStream,
(triggered) => { doomLoopTriggered = triggered; }
);
} else if (chunk.type === 'tool-result') {
const toolResultChunk = chunk as unknown as { toolCallId: string; output?: unknown };
this.handleToolResultChunk(toolResultChunk, toolStartTimes, onToolEnd, onStream);
}
},
});
// 流式输出文本
let aborted = false;
try {
for await (const chunk of result.textStream) {
if (abortSignal?.aborted) {
aborted = true;
break;
}
fullResponse += chunk;
onStream?.(chunk);
}
if (aborted) {
onStream?.('\n[已取消]\n');
if (fullResponse) {
this.conversationHistory.push({
role: 'assistant',
content: fullResponse + '\n[已取消]',
} as ModelMessage);
}
return { text: fullResponse, messages: [] };
}
const response = await result.response;
responseMessages = response.messages as ModelMessage[];
} catch (error) {
if (error instanceof Error && (error.name === 'AbortError' || abortSignal?.aborted)) {
onStream?.('\n[已取消]\n');
if (fullResponse) {
this.conversationHistory.push({
role: 'assistant',
content: fullResponse + '\n[已取消]',
} as ModelMessage);
}
return { text: fullResponse, messages: [] };
}
throw error;
}
// 将完整的响应消息添加到历史
this.conversationHistory.push(...responseMessages);
return {
text: fullResponse,
messages: responseMessages,
};
}
/**
* 处理工具调用 chunk
*/
private handleToolCallChunk(
chunk: { toolCallId: string; toolName: string; input: unknown },
toolStartTimes: Map<string, number>,
doomLoopTriggered: boolean,
onToolStart?: (info: ToolStartInfo) => void,
onDoomLoop?: (info: DoomLoopInfo) => void,
onStream?: (text: string) => void,
setDoomLoopTriggered?: (triggered: boolean) => void
): void {
const toolCallId = chunk.toolCallId || `tool-${Date.now()}`;
// Doom Loop 检测
this.doomLoopDetector.record(chunk.toolName, chunk.input);
if (this.doomLoopDetector.isTriggered() && !doomLoopTriggered) {
setDoomLoopTriggered?.(true);
const toolName = this.doomLoopDetector.getLastToolName() || chunk.toolName;
onDoomLoop?.({ toolName, count: 3 });
onStream?.(`\n[警告: 检测到 Doom Loop - ${toolName} 被重复调用]\n`);
onStream?.(DOOM_LOOP_WARNING);
}
// 记录开始时间
toolStartTimes.set(toolCallId, Date.now());
// 调用回调
if (onToolStart) {
onToolStart({
id: toolCallId,
toolName: chunk.toolName,
args: (chunk.input as Record<string, unknown>) || {},
});
} else {
onStream?.(`\n[调用工具: ${chunk.toolName}]\n`);
}
}
/**
* 处理工具结果 chunk
*/
private handleToolResultChunk(
chunk: { toolCallId: string; output?: unknown },
toolStartTimes: Map<string, number>,
onToolEnd?: (info: ToolEndInfo) => void,
onStream?: (text: string) => void
): void {
const toolCallId = chunk.toolCallId || '';
const output = chunk.output as ToolResult | undefined;
// 计算执行时长
const startTime = toolStartTimes.get(toolCallId);
const duration = startTime ? Date.now() - startTime : undefined;
toolStartTimes.delete(toolCallId);
if (output && typeof output === 'object') {
if (onToolEnd) {
onToolEnd({
id: toolCallId,
status: output.success ? 'completed' : 'error',
result: output.success ? output.output : undefined,
error: output.success ? undefined : output.error,
duration,
});
} else {
if (output.success) {
const displayOutput =
output.output.length > 500
? output.output.substring(0, 500) + '...(截断)'
: output.output;
onStream?.(`[结果: ${displayOutput}]\n`);
} else {
onStream?.(`[错误: ${output.error}]\n`);
}
}
}
}
/**
* 非流式聊天
*/
async generateChat(
config: MessageHandlerConfig,
tools: Record<string, AITool>,
abortSignal?: AbortSignal
): Promise<ChatResult> {
const maxSteps = config.maxSteps ?? 50;
const result = await generateText({
model: config.model,
system: config.systemPrompt,
messages: this.conversationHistory,
tools,
maxOutputTokens: config.maxTokens,
stopWhen: stepCountIs(maxSteps),
abortSignal,
});
const fullResponse = result.text;
const responseMessages = result.response.messages as ModelMessage[];
// 将完整的响应消息添加到历史
this.conversationHistory.push(...responseMessages);
return {
text: fullResponse,
messages: responseMessages,
};
}
/**
* 检查并执行自动压缩
*/
async autoCompress(onStream?: (text: string) => void): Promise<void> {
if (!this.compressionManager.shouldCompress(this.conversationHistory)) {
return;
}
const result = await this.compressionManager.compress(this.conversationHistory);
if (result.status === CompressionStatus.SUCCESS && result.freedTokens > 0) {
this.conversationHistory = result.messages;
if (onStream) {
const typeLabel = result.type === 'both' ? 'prune+摘要' : result.type === 'compaction' ? '摘要' : 'prune';
onStream(`\n[自动压缩(${typeLabel}): 释放了 ${(result.freedTokens / 1000).toFixed(1)}k tokens]\n`);
}
} else if (result.status === CompressionStatus.FAILED_EMPTY_SUMMARY) {
onStream?.('\n[压缩失败: 摘要生成为空,已跳过]\n');
} else if (result.status === CompressionStatus.FAILED_TOKEN_INFLATED) {
onStream?.('\n[压缩失败: 摘要反而增加了 token,已跳过]\n');
} else if (result.status === CompressionStatus.FAILED_ERROR) {
onStream?.(`\n[压缩失败: ${result.error || '未知错误'}]\n`);
}
}
/**
* 手动压缩
*/
async forceCompress(): Promise<{ freedTokens: number; type: string }> {
const result = await this.compressionManager.forceCompress(this.conversationHistory);
if (result.freedTokens > 0) {
this.conversationHistory = result.messages;
}
return {
freedTokens: result.freedTokens,
type: result.type,
};
}
/**
* 获取对话历史
*/
getHistory(): ModelMessage[] {
return this.conversationHistory;
}
/**
* 设置对话历史
*/
setHistory(messages: ModelMessage[]): void {
this.conversationHistory = [...messages];
}
/**
* 清空对话历史
*/
clearHistory(): void {
this.conversationHistory = [];
}
/**
* 获取上下文使用情况
*/
getContextUsage(): TokenUsage {
return this.compressionManager.calculateUsage(this.conversationHistory);
}
/**
* 获取格式化的上下文使用情况
*/
getContextUsageFormatted(): string {
return this.compressionManager.formatUsage(this.conversationHistory);
}
/**
* 获取压缩管理器
*/
getCompressionManager(): CompressionManager {
return this.compressionManager;
}
}
@@ -0,0 +1,131 @@
/**
* Agent 模式管理器
* 负责 Agent 模式的切换和权限管理
*/
import type { AgentInfo } from '../agent/types.js';
import {
agentRegistry,
renderPromptTemplate,
createPlanContext,
} from '../agent/index.js';
/**
* Agent 模式管理器
*/
export class AgentModeManager {
private currentMode: AgentInfo | null = null;
private originalSystemPrompt: string;
private currentSystemPrompt: string;
constructor(originalSystemPrompt: string) {
this.originalSystemPrompt = originalSystemPrompt;
this.currentSystemPrompt = originalSystemPrompt;
}
/**
* 设置 Agent 模式
*/
setMode(mode: AgentInfo | 'build' | 'plan' | null): void {
// 如果是字符串模式,从 registry 获取预设
if (typeof mode === 'string') {
const presetAgent = agentRegistry.get(mode);
if (presetAgent) {
this.currentMode = presetAgent;
this.currentSystemPrompt = this.resolveSystemPrompt(presetAgent);
} else {
// 如果找不到预设,回退到默认模式
this.currentMode = null;
this.currentSystemPrompt = this.originalSystemPrompt;
}
return;
}
this.currentMode = mode;
if (mode) {
this.currentSystemPrompt = this.resolveSystemPrompt(mode);
} else {
this.currentSystemPrompt = this.originalSystemPrompt;
}
}
/**
* 解析系统提示词
*/
private resolveSystemPrompt(agent: AgentInfo): string {
if (!agent.prompt) {
return '';
}
// 如果启用了模板渲染,动态解析变量
if (agent.promptTemplate) {
const context = createPlanContext({
workdir: process.cwd(),
isSubagent: agent.mode === 'subagent',
});
return renderPromptTemplate(agent.prompt, context);
}
return agent.prompt;
}
/**
* 获取当前系统提示词
*/
getSystemPrompt(): string {
return this.currentSystemPrompt;
}
/**
* 获取原始系统提示词
*/
getOriginalSystemPrompt(): string {
return this.originalSystemPrompt;
}
/**
* 检查是否只读模式
*/
isReadOnlyMode(): boolean {
const permission = this.currentMode?.permission;
if (!permission) return false;
return permission.file?.write === 'deny' && permission.file?.edit === 'deny';
}
/**
* 获取当前模式信息
*/
getCurrentMode(): AgentInfo | null {
return this.currentMode;
}
/**
* 获取当前模式名称
*/
getModeName(): string {
return this.currentMode?.name ?? 'default';
}
/**
* 获取当前模式的 maxSteps 配置
*/
getMaxSteps(): number {
return this.currentMode?.maxSteps ?? 50;
}
/**
* 获取当前模式的权限配置
*/
getPermission(): AgentInfo['permission'] | undefined {
return this.currentMode?.permission;
}
/**
* 获取当前模式的工具配置
*/
getToolConfig(): AgentInfo['tools'] | undefined {
return this.currentMode?.tools;
}
}
@@ -0,0 +1,355 @@
/**
* Agent 工具执行器
* 负责工具的获取、过滤、转换和执行
*/
import type { Tool as AITool } from 'ai';
import type { Tool, ToolResult } from '../types/index.js';
import { buildZodSchema } from '../types/index.js';
import type { ToolRegistry } from '../tools/registry.js';
import type { AgentInfo } from '../agent/types.js';
import {
checkBashPermission,
renderPromptTemplate,
createToolDescriptionContext,
type PromptContext,
} from '../agent/index.js';
import { getHookManager } from '../hooks/index.js';
import { getGitManager } from '../git/index.js';
/**
* 工具调用开始事件信息
*/
export interface ToolStartInfo {
id: string;
toolName: string;
args: Record<string, unknown>;
}
/**
* 工具调用结束事件信息
*/
export interface ToolEndInfo {
id: string;
status: 'completed' | 'error';
result?: unknown;
error?: string;
duration?: number;
}
/**
* 工具执行上下文
*/
export interface ToolExecutionContext {
sessionId: string;
agentMode: AgentInfo | null;
onToolStart?: (info: ToolStartInfo) => void;
onToolEnd?: (info: ToolEndInfo) => void;
}
/**
* Agent 工具执行器
*/
export class AgentToolExecutor {
private registry: ToolRegistry;
private discoveredTools: Set<string> = new Set();
private toolDescriptionContext: PromptContext | null = null;
private currentAgentMode: AgentInfo | null = null;
constructor(registry: ToolRegistry) {
this.registry = registry;
}
/**
* 设置当前 Agent 模式
*/
setAgentMode(mode: AgentInfo | null): void {
this.currentAgentMode = mode;
// 清除工具描述上下文缓存
this.toolDescriptionContext = null;
}
/**
* 获取可用工具(核心 + 已发现)
*/
getAvailableTools(): Tool[] {
// 核心工具 + 已发现的工具
const coreTools = this.registry.getCoreTools();
const discoveredTools = this.registry.getTools([...this.discoveredTools]);
let tools = [...coreTools, ...discoveredTools];
// 应用 Agent 模式的工具过滤
if (this.currentAgentMode?.tools) {
tools = this.filterToolsByAgentConfig(tools);
}
return tools;
}
/**
* 根据 Agent 配置过滤工具
*/
private filterToolsByAgentConfig(tools: Tool[]): Tool[] {
const toolConfig = this.currentAgentMode?.tools;
if (!toolConfig) return tools;
let filteredTools = tools;
// 如果设置了 enabled 列表,只保留这些工具
if (toolConfig.enabled && toolConfig.enabled.length > 0) {
const enabledSet = new Set(toolConfig.enabled);
filteredTools = filteredTools.filter((t) => enabledSet.has(t.name));
}
// 如果设置了 disabled 列表,排除这些工具
if (toolConfig.disabled && toolConfig.disabled.length > 0) {
const disabledSet = new Set(toolConfig.disabled);
filteredTools = filteredTools.filter((t) => !disabledSet.has(t.name));
}
// 如果禁止嵌套 Task,移除 task 工具
if (toolConfig.noTask) {
filteredTools = filteredTools.filter((t) => t.name !== 'task');
}
return filteredTools;
}
/**
* 获取或创建工具描述渲染上下文
*/
private getToolDescriptionContext(): PromptContext {
if (!this.toolDescriptionContext) {
this.toolDescriptionContext = createToolDescriptionContext({
agent: {
name: this.currentAgentMode?.name ?? 'default',
mode: this.currentAgentMode?.mode ?? 'primary',
isSubagent: this.currentAgentMode?.mode === 'subagent',
},
});
}
return this.toolDescriptionContext;
}
/**
* 渲染工具描述中的模板变量
*/
private renderToolDescription(description: string): string {
const context = this.getToolDescriptionContext();
return renderPromptTemplate(description, context, {
throwOnUndefined: false,
undefinedValue: '',
});
}
/**
* 转换为 Vercel AI SDK 工具格式
*/
toVercelTools(context: ToolExecutionContext): Record<string, AITool> {
const vercelTools: Record<string, AITool> = {};
const availableTools = this.getAvailableTools();
const hookManager = getHookManager();
for (const tool of availableTools) {
const schema = buildZodSchema(tool.parameters);
const renderedDescription = this.renderToolDescription(tool.description);
vercelTools[tool.name] = {
description: renderedDescription,
inputSchema: schema,
execute: async (params) => {
return this.executeTool(tool, params as Record<string, unknown>, context, hookManager);
},
} as AITool;
}
return vercelTools;
}
/**
* 执行单个工具
*/
private async executeTool(
tool: Tool,
args: Record<string, unknown>,
context: ToolExecutionContext,
hookManager: ReturnType<typeof getHookManager>
): Promise<ToolResult> {
const callId = `${tool.name}-${Date.now()}`;
const { sessionId, onToolStart, onToolEnd } = context;
// 触发工具执行前 hook
let finalArgs = args;
if (hookManager) {
const beforeOutput = await hookManager.triggerToolExecuteBefore({
tool: tool.name,
sessionId,
callId,
args,
});
// 如果 hook 指定跳过,直接返回
if (beforeOutput.skip && beforeOutput.skipResult) {
return beforeOutput.skipResult;
}
finalArgs = beforeOutput.args;
}
// Agent 级别的 Bash 权限检查
if (tool.name === 'bash' && this.currentAgentMode?.permission?.bash) {
const command = finalArgs.command as string;
if (command) {
const action = checkBashPermission(command, this.currentAgentMode.permission.bash);
if (action === 'deny') {
return {
success: false,
output: '',
error: `[Agent 权限拒绝] 当前模式 (${this.currentAgentMode.name}) 禁止执行此命令: ${command}`,
};
}
}
}
// 通知工具开始
onToolStart?.({
id: callId,
toolName: tool.name,
args: finalArgs,
});
// 执行工具
const startTime = Date.now();
let result = await tool.execute(finalArgs);
const duration = Date.now() - startTime;
// 触发工具执行后 hook
if (hookManager) {
const afterOutput = await hookManager.triggerToolExecuteAfter(
{
tool: tool.name,
sessionId,
callId,
args: finalArgs,
duration,
},
result
);
result = afterOutput.result;
// 对于文件操作工具,触发相应的文件 hook 和 Git 自动提交
if (result.success) {
await this.triggerFileHooksIfNeeded(tool.name, finalArgs, sessionId, hookManager);
}
}
// 通知工具结束
onToolEnd?.({
id: callId,
status: result.success ? 'completed' : 'error',
result: result.success ? result.output : undefined,
error: result.success ? undefined : result.error,
duration,
});
// 如果是 tool_search 调用,解析结果并注入发现的工具
if (tool.name === 'tool_search' && result.success) {
this.handleToolSearchResult(result.output);
}
return result;
}
/**
* 触发文件相关的 Hooks
*/
private async triggerFileHooksIfNeeded(
toolName: string,
args: Record<string, unknown>,
sessionId: string,
hookManager: NonNullable<ReturnType<typeof getHookManager>>
): Promise<void> {
const filePath = args.path as string | undefined;
if (!filePath) return;
const gitManager = getGitManager();
if (toolName === 'write_file') {
await hookManager.triggerFileCreated({
path: filePath,
tool: toolName,
sessionId,
});
if (gitManager) {
await gitManager.onFileChanged(filePath, 'create');
}
} else if (toolName === 'edit_file') {
await hookManager.triggerFileEdited({
path: filePath,
tool: toolName,
sessionId,
});
if (gitManager) {
await gitManager.onFileChanged(filePath, 'modify');
}
} else if (toolName === 'delete_file') {
await hookManager.triggerFileDeleted({
path: filePath,
tool: toolName,
sessionId,
});
if (gitManager) {
await gitManager.onFileChanged(filePath, 'delete');
}
}
}
/**
* 处理 tool_search 的结果,将发现的工具添加到可用列表
*/
handleToolSearchResult(output: string): void {
// 解析输出,提取工具名称
// 格式: "- tool_name: description [category]"
const matches = output.matchAll(/^- (\w+):/gm);
for (const match of matches) {
const toolName = match[1];
if (this.registry.has(toolName)) {
this.discoveredTools.add(toolName);
}
}
}
/**
* 获取已发现的工具
*/
getDiscoveredTools(): string[] {
return [...this.discoveredTools];
}
/**
* 设置已发现的工具(用于会话恢复)
*/
setDiscoveredTools(tools: string[]): void {
this.discoveredTools = new Set(tools);
}
/**
* 清除已发现的工具
*/
clearDiscoveredTools(): void {
this.discoveredTools.clear();
}
/**
* 获取工具数量统计
*/
getToolCount(): { core: number; discovered: number; total: number } {
const coreCount = this.registry.getCoreTools().length;
const discoveredCount = this.discoveredTools.size;
return {
core: coreCount,
discovered: discoveredCount,
total: coreCount + discoveredCount,
};
}
}
@@ -0,0 +1,122 @@
/**
* Agent Vision 处理器
* 负责 Vision 处理的委托逻辑
*/
import type { AgentConfig } from '../types/index.js';
import type { ToolRegistry } from '../tools/registry.js';
import type { ImageData } from '../agent/types.js';
import { agentRegistry, AgentExecutor } from '../agent/index.js';
import { loadVisionConfig } from '../utils/config.js';
/**
* Agent Vision 处理器
*/
export class AgentVisionHandler {
private config: AgentConfig;
private registry: ToolRegistry | null = null;
constructor(config: AgentConfig) {
this.config = config;
}
/**
* 设置工具注册表
*/
setRegistry(registry: ToolRegistry): void {
this.registry = registry;
}
/**
* 检查当前模型是否支持 Vision(图片理解)
*/
supportsVision(): boolean {
const model = this.config.model.toLowerCase();
// Anthropic Claude 模型支持 vision
if (this.config.provider === 'anthropic') {
// Claude 3 及以上版本支持 vision
return model.includes('claude-3') || model.includes('claude-4');
}
// OpenAI GPT-4 系列支持 vision
if (this.config.provider === 'openai') {
// GPT-4o, GPT-4 Turbo, GPT-4 Vision 等支持
return model.includes('gpt-4');
}
// DeepSeek 目前不支持 vision
if (this.config.provider === 'deepseek') {
return false;
}
return false;
}
/**
* 使用 Vision Agent 处理图片
* 当主模型不支持 vision 时,委托给 Vision Agent 分析图片
* @returns 包含图片分析结果的文本消息,或 null 表示失败
*/
async processWithVisionAgent(
images: ImageData[],
userText?: string,
onStream?: (text: string) => void
): Promise<string | null> {
// 检查 Vision 配置是否可用
const visionConfig = loadVisionConfig();
if (!visionConfig) {
onStream?.('\n⚠ Vision 服务未配置,无法处理图片\n');
return null;
}
// 获取 Vision Agent
const visionAgent = agentRegistry.get('vision');
if (!visionAgent) {
onStream?.('\n⚠ Vision Agent 未注册\n');
return null;
}
// 确保有工具注册表
if (!this.registry) {
onStream?.('\n⚠ 工具注册表未初始化\n');
return null;
}
onStream?.(`\n[委托 Vision Agent (${visionConfig.model}) 分析图片...]\n`);
// 构建 Vision 配置
const visionAgentConfig: AgentConfig = {
...this.config,
provider: visionConfig.provider,
apiKey: visionConfig.apiKey,
model: visionConfig.model,
baseUrl: visionConfig.baseUrl,
};
// 创建 Vision Agent 执行器
const executor = new AgentExecutor(visionAgent, visionAgentConfig, this.registry);
// 构建提示词
const prompt = userText || '请详细描述这张图片的内容';
// 执行 Vision 分析
const result = await executor.execute(prompt, {
workdir: process.cwd(),
images,
onStream: undefined, // Vision Agent 不使用流式输出
});
if (!result.success) {
onStream?.(`\n⚠ Vision 分析失败: ${result.error}\n`);
return null;
}
onStream?.('\n[Vision 分析完成]\n');
// 构建带分析结果的文本消息
const combinedText = `[图片分析结果 - 由 ${visionConfig.model} 提供]\n${result.text}\n\n用户问题: ${userText || '(无附加问题)'}`;
return combinedText;
}
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
/**
* Core 模块导出
*/
// Agent 主类
export { Agent, type AgentChatOptions } from './agent.js';
// 子模块
export {
AgentToolExecutor,
type ToolStartInfo,
type ToolEndInfo,
type ToolExecutionContext,
} from './agent-tool-executor.js';
export {
AgentMessageHandler,
type DoomLoopInfo,
type MessageHandlerConfig,
type StreamCallbacks,
} from './agent-message-handler.js';
export { AgentModeManager } from './agent-mode-manager.js';
export { AgentVisionHandler } from './agent-vision-handler.js';
// Doom Loop
export {
createDoomLoopDetector,
DOOM_LOOP_WARNING,
type DoomLoopDetector,
} from './doom-loop.js';
+161 -573
View File
@@ -1,48 +1,19 @@
/**
* 会话管理器
* 作为编排器,委托具体工作给各个子模块
*/
import type { ModelMessage } from 'ai';
import * as storage from './storage/index.js';
import { SessionStorage, MessageStorage, PartStorage, TodoStorage } from './storage/index.js';
import type { SessionInfo, Part, TodoItem } from './storage/index.js';
import { generateSessionId } from './id.js';
import { getProjectId, isGitRepository } from './project.js';
import type { TodoItem } from './storage/index.js';
/**
* 会话摘要(用于列表展示)
*/
export interface SessionSummary {
id: string;
title: string;
workdir: string;
messageCount: number;
createdAt: string;
updatedAt: string;
}
// 子模块
import { SessionStore, type SessionData, type SessionSummary } from './session-store.js';
import { ProjectManager, type ProjectMetadata } from './project-manager.js';
import { SessionAutoSave } from './session-auto-save.js';
/**
* 运行时会话数据(兼容旧接口)
*/
export interface SessionData {
id: string;
projectId: string;
parentId?: string;
agentName?: string;
createdAt: string;
updatedAt: string;
workdir: string;
title?: string;
messages: ModelMessage[];
discoveredTools: string[];
todos: TodoItem[];
}
/**
* 项目元数据
*/
export interface ProjectMetadata {
id: string;
workdir: string;
createdAt: string;
isGitRepo: boolean;
}
// 重新导出类型
export type { SessionData, SessionSummary, ProjectMetadata };
/**
* 会话管理器
@@ -50,14 +21,24 @@ export interface ProjectMetadata {
*/
export class SessionManager {
private currentSession: SessionData | null = null;
private currentProject: ProjectMetadata | null = null;
private autoSaveInterval: ReturnType<typeof setInterval> | null = null;
private storageDir?: string;
// 子模块
private store: SessionStore;
private projectManager: ProjectManager;
private autoSave: SessionAutoSave;
constructor(storageDir?: string) {
this.storageDir = storageDir;
this.store = new SessionStore();
this.projectManager = new ProjectManager();
this.autoSave = new SessionAutoSave();
}
// ============================================================================
// 初始化
// ============================================================================
/**
* 初始化 - 尝试恢复或创建新会话
*/
@@ -66,13 +47,14 @@ export class SessionManager {
await storage.initStorage(this.storageDir);
// 获取或创建项目
this.currentProject = await this.getOrCreateProject(workdir);
await this.projectManager.getOrCreate(workdir);
// 尝试加载当前会话
const currentSessionId = await this.getCurrentSessionId();
if (currentSessionId) {
const existing = await this.loadSession(this.currentProject.id, currentSessionId);
const projectId = this.projectManager.getProjectId()!;
const existing = await this.store.load(projectId, currentSessionId);
if (existing && existing.workdir === workdir) {
this.currentSession = existing;
@@ -82,218 +64,18 @@ export class SessionManager {
}
// 创建新会话
this.currentSession = await this.createNewSession(workdir);
await this.saveSessionInfo();
const projectId = this.projectManager.getProjectId()!;
this.currentSession = await this.store.create(projectId, workdir);
await this.store.save(this.currentSession);
await this.setCurrentSessionPointer(this.currentSession.id);
this.startAutoSave();
return this.currentSession;
}
/**
* 获取或创建项目
*/
private async getOrCreateProject(workdir: string): Promise<ProjectMetadata> {
const projectId = await getProjectId(workdir);
try {
const existing = await storage.read<ProjectMetadata>(['project', projectId]);
return existing;
} catch (e) {
if (e instanceof storage.StorageNotFoundError) {
const isGitRepo = await isGitRepository(workdir);
const project: ProjectMetadata = {
id: projectId,
workdir,
createdAt: new Date().toISOString(),
isGitRepo,
};
await storage.write(['project', projectId], project);
return project;
}
throw e;
}
}
/**
* 创建新会话
*/
private async createNewSession(workdir: string): Promise<SessionData> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const sessionInfo = await SessionStorage.create(this.currentProject.id, workdir);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages: [],
discoveredTools: sessionInfo.discoveredTools,
todos: [],
};
}
/**
* 加载会话(从存储重建)
*/
private async loadSession(projectId: string, sessionId: string): Promise<SessionData | null> {
const sessionInfo = await SessionStorage.get(projectId, sessionId);
if (!sessionInfo) return null;
// 加载消息
const messages = await this.loadMessagesFromStorage(sessionId);
// 加载 todos
const todoList = await TodoStorage.get(sessionId);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
parentId: sessionInfo.parentId,
agentName: sessionInfo.agentName,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages,
discoveredTools: sessionInfo.discoveredTools,
todos: todoList?.items || [],
};
}
/**
* 从存储加载消息并转换为 AI SDK 格式
*/
private async loadMessagesFromStorage(sessionId: string): Promise<ModelMessage[]> {
const messageInfos = await MessageStorage.listBySession(sessionId);
const messages: ModelMessage[] = [];
for (const messageInfo of messageInfos) {
const parts = await PartStorage.getByIds(messageInfo.id, messageInfo.partIds);
const modelMessages = this.partsToModelMessages(messageInfo.role, parts);
messages.push(...modelMessages);
}
return messages;
}
/**
* 将 Parts 转换为 AI SDK ModelMessage(用于加载历史消息)
*
* 新逻辑:
* - user 消息:直接转换
* - assistant 消息:转换文本和工具调用,然后为已完成的工具生成 tool 消息
*/
private partsToModelMessages(role: string, parts: Part[]): ModelMessage[] {
if (parts.length === 0) return [];
const result: ModelMessage[] = [];
if (role === 'user') {
// User 消息:只有文本和文件
const content: unknown[] = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'file') {
content.push({
type: 'image',
image: part.data,
mimeType: part.mimeType,
});
}
}
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
result.push({
role: 'user',
content: (content[0] as { text: string }).text,
});
} else if (content.length > 0) {
result.push({
role: 'user',
content,
} as ModelMessage);
}
} else if (role === 'assistant') {
// Assistant 消息:文本 + 工具调用
const content: unknown[] = [];
// input 使用 unknown 类型以兼容 AI SDK(可能是对象、字符串等)
const completedTools: Array<{ toolCallId: string; toolName: string; input: unknown; output: unknown }> = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'tool') {
// 只有非 pending 状态的工具调用才添加到 AI SDK 消息
if (part.state.status !== 'pending') {
// AI SDK v5 使用 input 字段(不是 args
content.push({
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
});
// 收集已完成的工具结果
if (part.state.status === 'completed') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.output,
});
} else if (part.state.status === 'error') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.error,
});
}
}
} else if (part.type === 'reasoning') {
content.push({ type: 'text', text: `[Reasoning] ${part.text}` });
}
}
// 添加 assistant 消息
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
result.push({
role: 'assistant',
content: (content[0] as { text: string }).text,
});
} else if (content.length > 0) {
result.push({
role: 'assistant',
content,
} as ModelMessage);
}
// 添加 tool 消息(如果有已完成的工具)
// AI SDK v5 要求 tool-result 必须包含 input 和 output 字段
if (completedTools.length > 0) {
result.push({
role: 'tool',
content: completedTools.map((t) => ({
type: 'tool-result',
toolCallId: t.toolCallId,
toolName: t.toolName,
input: t.input,
output: t.output,
})),
} as unknown as ModelMessage);
}
}
return result;
}
// ============================================================================
// 会话获取
// ============================================================================
/**
* 获取当前会话
@@ -306,189 +88,108 @@ export class SessionManager {
* 获取当前项目
*/
getProject(): ProjectMetadata | null {
return this.currentProject;
return this.projectManager.getProject();
}
/**
* 保存会话信息
* 获取当前会话 ID
*/
private async saveSessionInfo(): Promise<void> {
if (!this.currentSession) return;
const sessionInfo: SessionInfo = {
id: this.currentSession.id,
projectId: this.currentSession.projectId,
parentId: this.currentSession.parentId,
agentName: this.currentSession.agentName,
createdAt: new Date(this.currentSession.createdAt).getTime(),
updatedAt: Date.now(),
workdir: this.currentSession.workdir,
title: this.currentSession.title,
discoveredTools: this.currentSession.discoveredTools,
stats: {
messageCount: this.currentSession.messages.length,
inputTokens: 0,
outputTokens: 0,
},
};
await SessionStorage.save(sessionInfo);
getSessionId(): string | undefined {
return this.currentSession?.id;
}
// ============================================================================
// 会话操作
// ============================================================================
/**
* 保存当前会话
*/
async save(): Promise<void> {
if (!this.currentSession) return;
await this.saveSessionInfo();
await this.store.save(this.currentSession);
}
/**
* 同步消息到存储(将 AI SDK 消息转换为 Message + Parts
*
* 新逻辑:只存储 user 和 assistant 消息
* - user 消息:直接存储
* - assistant 消息:合并后续的 tool 消息中的工具结果
* - tool 消息:跳过(结果合并到 assistant)
* 清空当前会话并创建新会话
*/
async syncMessages(messages: ModelMessage[]): Promise<void> {
if (!this.currentSession) return;
const sessionId = this.currentSession.id;
// 删除旧消息
await MessageStorage.removeBySession(sessionId);
// 用于跟踪当前 assistant 消息的工具调用
let currentAssistantMsgId: string | null = null;
let currentUserMsgId: string | null = null;
const toolCallPartIds = new Map<string, string>(); // toolCallId -> partId
for (let i = 0; i < messages.length; i++) {
const message = messages[i];
if (message.role === 'user') {
// User 消息
const messageInfo = await MessageStorage.create(sessionId, 'user');
currentUserMsgId = messageInfo.id;
const partIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageInfo.id, message.content);
partIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
partIds.push(part.id);
} else if (itemType === 'image') {
const img = item as unknown as { image: string; mimeType: string };
const part = await PartStorage.create(messageInfo.id, 'file', {
filename: 'image',
mimeType: img.mimeType,
data: typeof img.image === 'string' ? img.image : '',
});
partIds.push(part.id);
}
}
}
if (partIds.length > 0) {
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
}
// 重置工具调用追踪
currentAssistantMsgId = null;
toolCallPartIds.clear();
} else if (message.role === 'assistant') {
// Assistant 消息:如果当前轮次已有 assistant 消息,则追加 Parts
let messageId: string;
let existingPartIds: string[] = [];
if (currentAssistantMsgId) {
// 同一轮对话的后续 assistant 消息,追加到现有消息
messageId = currentAssistantMsgId;
const existingMsg = await MessageStorage.get(sessionId, messageId);
existingPartIds = existingMsg?.partIds ?? [];
} else {
// 新的 assistant 消息
const messageInfo = await MessageStorage.create(sessionId, 'assistant', {
parentId: currentUserMsgId ?? undefined,
});
messageId = messageInfo.id;
currentAssistantMsgId = messageId;
}
const newPartIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageId, message.content);
newPartIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageId, (item as { text: string }).text);
newPartIds.push(part.id);
} else if (itemType === 'tool-call') {
// AI SDK 的 tool-call 使用 input 字段存储参数(不是 args)
const toolCall = item as unknown as { toolCallId: string; toolName: string; input: Record<string, unknown> };
// 创建 running 状态的工具 Part
const part = await PartStorage.createToolRunning(
messageId,
toolCall.toolCallId,
toolCall.toolName,
(toolCall.input as Record<string, unknown>) ?? {}
);
newPartIds.push(part.id);
toolCallPartIds.set(toolCall.toolCallId, part.id);
}
}
}
if (newPartIds.length > 0) {
// 合并已有的和新的 partIds
const allPartIds = [...existingPartIds, ...newPartIds];
await MessageStorage.update(sessionId, messageId, { partIds: allPartIds });
}
} else if (message.role === 'tool' && currentAssistantMsgId) {
// Tool 消息:更新对应 assistant 消息中的工具 Part 状态
if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'tool-result') {
// AI SDK v5 使用 output 字段存储结果(不是 result)
const toolResult = item as unknown as { toolCallId: string; toolName: string; output: unknown };
const partId = toolCallPartIds.get(toolResult.toolCallId);
if (partId) {
// 更新工具状态为 completed
// 获取原始 start time
const part = await PartStorage.get(currentAssistantMsgId, partId);
const startTime = part?.type === 'tool' && part.state.status === 'running'
? part.state.time.start
: Date.now();
await PartStorage.setToolCompleted(currentAssistantMsgId, partId, toolResult.output, startTime);
}
}
}
}
// 不创建新消息,跳过 tool role
}
// 忽略 system 消息(system prompt 通过其他方式注入)
async newSession(workdir?: string): Promise<SessionData> {
if (!this.projectManager.isInitialized()) {
throw new Error('Project not initialized. Call init() first.');
}
const newWorkdir = workdir || this.currentSession?.workdir || process.cwd();
// 如果工作目录变化,需要切换项目
if (workdir && workdir !== this.projectManager.getProject()?.workdir) {
await this.projectManager.switchProject(workdir);
}
const projectId = this.projectManager.getProjectId()!;
this.currentSession = await this.store.create(projectId, newWorkdir);
await this.store.save(this.currentSession);
await this.setCurrentSessionPointer(this.currentSession.id);
return this.currentSession;
}
/**
* 恢复指定会话
*/
async restoreSession(sessionId: string): Promise<SessionData | null> {
if (!this.projectManager.isInitialized()) {
throw new Error('Project not initialized. Call init() first.');
}
const projectId = this.projectManager.getProjectId()!;
const session = await this.store.load(projectId, sessionId);
if (!session) return null;
this.currentSession = session;
await this.setCurrentSessionPointer(sessionId);
return session;
}
/**
* 列出当前项目的历史会话
*/
async listSessions(): Promise<SessionSummary[]> {
const projectId = this.projectManager.getProjectId();
if (!projectId) {
return this.listAllSessions();
}
return this.store.listByProject(projectId);
}
/**
* 列出所有项目的会话
*/
async listAllSessions(): Promise<SessionSummary[]> {
return this.store.listAll();
}
/**
* 删除历史会话
*/
async deleteSession(sessionId: string): Promise<boolean> {
const projectId = this.projectManager.getProjectId();
if (!projectId) return false;
return this.store.delete(projectId, sessionId);
}
// ============================================================================
// 消息操作
// ============================================================================
/**
* 批量设置消息(用于同步整个对话历史)
*/
async setMessages(messages: ModelMessage[]): Promise<void> {
if (!this.currentSession) return;
this.currentSession.messages = messages;
await this.syncMessages(messages);
await this.saveSessionInfo();
await this.store.syncMessages(this.currentSession.id, messages);
await this.store.save(this.currentSession);
}
/**
@@ -507,13 +208,17 @@ export class SessionManager {
return this.currentSession?.messages || [];
}
// ============================================================================
// 工具和待办操作
// ============================================================================
/**
* 设置已发现的工具
*/
async setDiscoveredTools(tools: string[]): Promise<void> {
if (!this.currentSession) return;
this.currentSession.discoveredTools = tools;
await this.saveSessionInfo();
await this.store.save(this.currentSession);
}
/**
@@ -526,10 +231,12 @@ export class SessionManager {
/**
* 更新待办事项
*/
async setTodos(todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>): Promise<void> {
async setTodos(
todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>
): Promise<void> {
if (!this.currentSession) return;
const todoList = await TodoStorage.replace(this.currentSession.id, todos);
this.currentSession.todos = todoList.items;
const items = await this.store.setTodos(this.currentSession.id, todos);
this.currentSession.todos = items;
}
/**
@@ -539,192 +246,46 @@ export class SessionManager {
return this.currentSession?.todos || [];
}
/**
* 清空当前会话并创建新会话
*/
async newSession(workdir?: string): Promise<SessionData> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const newWorkdir = workdir || this.currentSession?.workdir || process.cwd();
// 如果工作目录变化,需要切换项目
if (workdir && workdir !== this.currentProject.workdir) {
this.currentProject = await this.getOrCreateProject(workdir);
}
this.currentSession = await this.createNewSession(newWorkdir);
await this.saveSessionInfo();
await this.setCurrentSessionPointer(this.currentSession.id);
return this.currentSession;
}
// ============================================================================
// 子会话操作
// ============================================================================
/**
* 创建子会话(用于 Task 工具)
*/
createChildSession(parentId: string, agentName: string, title?: string): SessionData {
if (!this.currentProject) {
if (!this.projectManager.isInitialized()) {
throw new Error('Project not initialized. Call init() first.');
}
const projectId = this.projectManager.getProjectId()!;
const workdir = this.currentSession?.workdir || process.cwd();
return {
id: generateSessionId(),
projectId: this.currentProject.id,
parentId,
agentName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
workdir,
title: title || `子任务 (@${agentName})`,
messages: [],
discoveredTools: [],
todos: [],
};
return this.store.createChildSession(projectId, parentId, agentName, workdir, title);
}
/**
* 保存子会话
*/
async saveChildSession(session: SessionData): Promise<void> {
const sessionInfo: SessionInfo = {
id: session.id,
projectId: session.projectId,
parentId: session.parentId,
agentName: session.agentName,
createdAt: new Date(session.createdAt).getTime(),
updatedAt: Date.now(),
workdir: session.workdir,
title: session.title,
discoveredTools: session.discoveredTools,
};
await SessionStorage.save(sessionInfo);
await this.store.saveChildSession(session);
}
/**
* 获取当前会话 ID
*/
getSessionId(): string | undefined {
return this.currentSession?.id;
}
// ============================================================================
// 自动保存
// ============================================================================
/**
* 恢复指定会话
*/
async restoreSession(sessionId: string): Promise<SessionData | null> {
if (!this.currentProject) {
throw new Error('Project not initialized. Call init() first.');
}
const session = await this.loadSession(this.currentProject.id, sessionId);
if (!session) return null;
this.currentSession = session;
await this.setCurrentSessionPointer(sessionId);
return session;
}
/**
* 列出当前项目的历史会话
*/
async listSessions(): Promise<SessionSummary[]> {
if (!this.currentProject) {
return this.listAllSessions();
}
const sessions = await SessionStorage.listByProject(this.currentProject.id);
return sessions.map((s) => ({
id: s.id,
title: s.title || `会话 ${s.id}`,
workdir: s.workdir,
messageCount: s.stats?.messageCount || 0,
createdAt: new Date(s.createdAt).toISOString(),
updatedAt: new Date(s.updatedAt).toISOString(),
}));
}
/**
* 列出所有项目的会话
*/
async listAllSessions(): Promise<SessionSummary[]> {
const sessions = await SessionStorage.listAll();
return sessions.map((s) => ({
id: s.id,
title: s.title || `会话 ${s.id}`,
workdir: s.workdir,
messageCount: s.stats?.messageCount || 0,
createdAt: new Date(s.createdAt).toISOString(),
updatedAt: new Date(s.updatedAt).toISOString(),
}));
}
/**
* 删除历史会话
*/
async deleteSession(sessionId: string): Promise<boolean> {
if (!this.currentProject) return false;
try {
// 删除会话的消息和 Parts
const messageInfos = await MessageStorage.listBySession(sessionId);
for (const msg of messageInfos) {
await PartStorage.removeByMessage(msg.id);
}
await MessageStorage.removeBySession(sessionId);
// 删除 todos
await TodoStorage.removeBySession(sessionId);
// 删除会话信息
await SessionStorage.remove(this.currentProject.id, sessionId);
return true;
} catch {
return false;
}
}
/**
* 获取当前会话 ID(从存储)
*/
private async getCurrentSessionId(): Promise<string | null> {
try {
const pointer = await storage.read<{ sessionId: string }>(['current-session']);
return pointer.sessionId;
} catch {
return null;
}
}
/**
* 设置当前会话指针
*/
private async setCurrentSessionPointer(sessionId: string): Promise<void> {
await storage.write(['current-session'], { sessionId });
}
/**
* 启动自动保存(每 30 秒)
* 启动自动保存
*/
private startAutoSave(): void {
if (this.autoSaveInterval) return;
this.autoSaveInterval = setInterval(async () => {
await this.save();
}, 30000);
this.autoSave.start(() => this.save());
}
/**
* 停止自动保存
*/
stopAutoSave(): void {
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
this.autoSave.stop();
}
/**
@@ -735,6 +296,10 @@ export class SessionManager {
await this.save();
}
// ============================================================================
// 清理
// ============================================================================
/**
* 清理旧会话
*/
@@ -756,6 +321,29 @@ export class SessionManager {
return deletedCount;
}
// ============================================================================
// 辅助方法
// ============================================================================
/**
* 获取当前会话 ID(从存储)
*/
private async getCurrentSessionId(): Promise<string | null> {
try {
const pointer = await storage.read<{ sessionId: string }>(['current-session']);
return pointer.sessionId;
} catch {
return null;
}
}
/**
* 设置当前会话指针
*/
private async setCurrentSessionPointer(sessionId: string): Promise<void> {
await storage.write(['current-session'], { sessionId });
}
/**
* 获取存储目录
*/
@@ -0,0 +1,331 @@
/**
* 消息格式转换器
* 负责 Part ↔ ModelMessage 的转换
*/
import type { ModelMessage } from 'ai';
import { MessageStorage, PartStorage } from './storage/index.js';
import type { Part } from './storage/index.js';
/**
* 消息格式转换器
*/
export class MessageConverter {
/**
* 从存储加载消息并转换为 AI SDK 格式
*/
async loadFromStorage(sessionId: string): Promise<ModelMessage[]> {
const messageInfos = await MessageStorage.listBySession(sessionId);
const messages: ModelMessage[] = [];
for (const messageInfo of messageInfos) {
const parts = await PartStorage.getByIds(messageInfo.id, messageInfo.partIds);
const modelMessages = this.partsToModelMessages(messageInfo.role, parts);
messages.push(...modelMessages);
}
return messages;
}
/**
* 将 Parts 转换为 AI SDK ModelMessage(用于加载历史消息)
*
* 逻辑:
* - user 消息:直接转换
* - assistant 消息:转换文本和工具调用,然后为已完成的工具生成 tool 消息
*/
partsToModelMessages(role: string, parts: Part[]): ModelMessage[] {
if (parts.length === 0) return [];
const result: ModelMessage[] = [];
if (role === 'user') {
result.push(...this.convertUserParts(parts));
} else if (role === 'assistant') {
result.push(...this.convertAssistantParts(parts));
}
return result;
}
/**
* 转换用户消息 Parts
*/
private convertUserParts(parts: Part[]): ModelMessage[] {
const content: unknown[] = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'file') {
content.push({
type: 'image',
image: part.data,
mimeType: part.mimeType,
});
}
}
if (content.length === 0) return [];
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
return [{
role: 'user',
content: (content[0] as { text: string }).text,
}];
}
return [{
role: 'user',
content,
} as ModelMessage];
}
/**
* 转换助手消息 Parts
*/
private convertAssistantParts(parts: Part[]): ModelMessage[] {
const result: ModelMessage[] = [];
const content: unknown[] = [];
const completedTools: Array<{
toolCallId: string;
toolName: string;
input: unknown;
output: unknown;
}> = [];
for (const part of parts) {
if (part.type === 'text') {
content.push({ type: 'text', text: part.text });
} else if (part.type === 'tool') {
// 只有非 pending 状态的工具调用才添加到 AI SDK 消息
if (part.state.status !== 'pending') {
content.push({
type: 'tool-call',
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
});
// 收集已完成的工具结果
if (part.state.status === 'completed') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.output,
});
} else if (part.state.status === 'error') {
completedTools.push({
toolCallId: part.toolCallId,
toolName: part.toolName,
input: part.state.input,
output: part.state.error,
});
}
}
} else if (part.type === 'reasoning') {
content.push({ type: 'text', text: `[Reasoning] ${part.text}` });
}
}
// 添加 assistant 消息
if (content.length === 1 && (content[0] as { type: string }).type === 'text') {
result.push({
role: 'assistant',
content: (content[0] as { text: string }).text,
});
} else if (content.length > 0) {
result.push({
role: 'assistant',
content,
} as ModelMessage);
}
// 添加 tool 消息(如果有已完成的工具)
if (completedTools.length > 0) {
result.push({
role: 'tool',
content: completedTools.map((t) => ({
type: 'tool-result',
toolCallId: t.toolCallId,
toolName: t.toolName,
input: t.input,
output: t.output,
})),
} as unknown as ModelMessage);
}
return result;
}
/**
* 同步消息到存储(将 AI SDK 消息转换为 Message + Parts
*
* 逻辑:只存储 user 和 assistant 消息
* - user 消息:直接存储
* - assistant 消息:合并后续的 tool 消息中的工具结果
* - tool 消息:跳过(结果合并到 assistant)
*/
async syncToStorage(sessionId: string, messages: ModelMessage[]): Promise<void> {
// 删除旧消息
await MessageStorage.removeBySession(sessionId);
// 用于跟踪当前 assistant 消息的工具调用
let currentAssistantMsgId: string | null = null;
let currentUserMsgId: string | null = null;
const toolCallPartIds = new Map<string, string>(); // toolCallId -> partId
for (const message of messages) {
if (message.role === 'user') {
await this.syncUserMessage(sessionId, message);
currentUserMsgId = (await MessageStorage.listBySession(sessionId)).slice(-1)[0]?.id ?? null;
currentAssistantMsgId = null;
toolCallPartIds.clear();
} else if (message.role === 'assistant') {
const result = await this.syncAssistantMessage(
sessionId,
message,
currentAssistantMsgId,
currentUserMsgId,
toolCallPartIds
);
currentAssistantMsgId = result.messageId;
result.toolCallPartIds.forEach((v, k) => toolCallPartIds.set(k, v));
} else if (message.role === 'tool' && currentAssistantMsgId) {
await this.syncToolMessage(currentAssistantMsgId, message, toolCallPartIds);
}
}
}
/**
* 同步用户消息
*/
private async syncUserMessage(sessionId: string, message: ModelMessage): Promise<void> {
const messageInfo = await MessageStorage.create(sessionId, 'user');
const partIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageInfo.id, message.content);
partIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageInfo.id, (item as { text: string }).text);
partIds.push(part.id);
} else if (itemType === 'image') {
const img = item as unknown as { image: string; mimeType: string };
const part = await PartStorage.create(messageInfo.id, 'file', {
filename: 'image',
mimeType: img.mimeType,
data: typeof img.image === 'string' ? img.image : '',
});
partIds.push(part.id);
}
}
}
if (partIds.length > 0) {
await MessageStorage.update(sessionId, messageInfo.id, { partIds });
}
}
/**
* 同步助手消息
*/
private async syncAssistantMessage(
sessionId: string,
message: ModelMessage,
currentAssistantMsgId: string | null,
currentUserMsgId: string | null,
existingToolCallPartIds: Map<string, string>
): Promise<{ messageId: string; toolCallPartIds: Map<string, string> }> {
let messageId: string;
let existingPartIds: string[] = [];
const newToolCallPartIds = new Map<string, string>();
if (currentAssistantMsgId) {
// 同一轮对话的后续 assistant 消息,追加到现有消息
messageId = currentAssistantMsgId;
const existingMsg = await MessageStorage.get(sessionId, messageId);
existingPartIds = existingMsg?.partIds ?? [];
} else {
// 新的 assistant 消息
const messageInfo = await MessageStorage.create(sessionId, 'assistant', {
parentId: currentUserMsgId ?? undefined,
});
messageId = messageInfo.id;
}
const newPartIds: string[] = [];
if (typeof message.content === 'string') {
const part = await PartStorage.createText(messageId, message.content);
newPartIds.push(part.id);
} else if (Array.isArray(message.content)) {
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'text') {
const part = await PartStorage.createText(messageId, (item as { text: string }).text);
newPartIds.push(part.id);
} else if (itemType === 'tool-call') {
const toolCall = item as unknown as {
toolCallId: string;
toolName: string;
input: Record<string, unknown>;
};
const part = await PartStorage.createToolRunning(
messageId,
toolCall.toolCallId,
toolCall.toolName,
(toolCall.input as Record<string, unknown>) ?? {}
);
newPartIds.push(part.id);
newToolCallPartIds.set(toolCall.toolCallId, part.id);
}
}
}
if (newPartIds.length > 0) {
const allPartIds = [...existingPartIds, ...newPartIds];
await MessageStorage.update(sessionId, messageId, { partIds: allPartIds });
}
// 合并工具调用 ID
existingToolCallPartIds.forEach((v, k) => newToolCallPartIds.set(k, v));
return { messageId, toolCallPartIds: newToolCallPartIds };
}
/**
* 同步工具消息(更新工具状态)
*/
private async syncToolMessage(
assistantMsgId: string,
message: ModelMessage,
toolCallPartIds: Map<string, string>
): Promise<void> {
if (!Array.isArray(message.content)) return;
for (const item of message.content) {
const itemType = (item as { type: string }).type;
if (itemType === 'tool-result') {
const toolResult = item as unknown as {
toolCallId: string;
toolName: string;
output: unknown;
};
const partId = toolCallPartIds.get(toolResult.toolCallId);
if (partId) {
const part = await PartStorage.get(assistantMsgId, partId);
const startTime =
part?.type === 'tool' && part.state.status === 'running'
? part.state.time.start
: Date.now();
await PartStorage.setToolCompleted(assistantMsgId, partId, toolResult.output, startTime);
}
}
}
}
}
@@ -0,0 +1,86 @@
/**
* 项目管理器
* 负责项目的创建和管理
*/
import * as storage from './storage/index.js';
import { getProjectId, isGitRepository } from './project.js';
/**
* 项目元数据
*/
export interface ProjectMetadata {
id: string;
workdir: string;
createdAt: string;
isGitRepo: boolean;
}
/**
* 项目管理器
*/
export class ProjectManager {
private currentProject: ProjectMetadata | null = null;
/**
* 获取当前项目
*/
getProject(): ProjectMetadata | null {
return this.currentProject;
}
/**
* 设置当前项目
*/
setProject(project: ProjectMetadata | null): void {
this.currentProject = project;
}
/**
* 获取或创建项目
*/
async getOrCreate(workdir: string): Promise<ProjectMetadata> {
const projectId = await getProjectId(workdir);
try {
const existing = await storage.read<ProjectMetadata>(['project', projectId]);
this.currentProject = existing;
return existing;
} catch (e) {
if (e instanceof storage.StorageNotFoundError) {
const isGitRepo = await isGitRepository(workdir);
const project: ProjectMetadata = {
id: projectId,
workdir,
createdAt: new Date().toISOString(),
isGitRepo,
};
await storage.write(['project', projectId], project);
this.currentProject = project;
return project;
}
throw e;
}
}
/**
* 切换项目
*/
async switchProject(workdir: string): Promise<ProjectMetadata> {
return this.getOrCreate(workdir);
}
/**
* 检查项目是否初始化
*/
isInitialized(): boolean {
return this.currentProject !== null;
}
/**
* 获取项目 ID
*/
getProjectId(): string | null {
return this.currentProject?.id ?? null;
}
}
@@ -0,0 +1,93 @@
/**
* 会话自动保存
* 负责定期保存会话
*/
/**
* 保存回调类型
*/
export type SaveCallback = () => Promise<void>;
/**
* 自动保存配置
*/
export interface AutoSaveConfig {
/** 保存间隔(毫秒),默认 30000 */
interval: number;
}
const DEFAULT_CONFIG: AutoSaveConfig = {
interval: 30000,
};
/**
* 会话自动保存管理
*/
export class SessionAutoSave {
private intervalId: ReturnType<typeof setInterval> | null = null;
private saveCallback: SaveCallback | null = null;
private config: AutoSaveConfig;
constructor(config?: Partial<AutoSaveConfig>) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
/**
* 启动自动保存
*/
start(saveCallback: SaveCallback): void {
if (this.intervalId) return;
this.saveCallback = saveCallback;
this.intervalId = setInterval(async () => {
try {
await this.saveCallback?.();
} catch (error) {
console.warn('Auto-save failed:', error);
}
}, this.config.interval);
}
/**
* 停止自动保存
*/
stop(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
this.saveCallback = null;
}
/**
* 检查是否正在运行
*/
isRunning(): boolean {
return this.intervalId !== null;
}
/**
* 立即触发保存(不影响定时器)
*/
async saveNow(): Promise<void> {
await this.saveCallback?.();
}
/**
* 更新配置
*/
setConfig(config: Partial<AutoSaveConfig>): void {
const wasRunning = this.isRunning();
const callback = this.saveCallback;
if (wasRunning) {
this.stop();
}
this.config = { ...this.config, ...config };
if (wasRunning && callback) {
this.start(callback);
}
}
}
+236
View File
@@ -0,0 +1,236 @@
/**
* 会话存储管理
* 负责会话的 CRUD 操作
*/
import type { ModelMessage } from 'ai';
import { SessionStorage, MessageStorage, PartStorage, TodoStorage } from './storage/index.js';
import type { SessionInfo, TodoItem } from './storage/index.js';
import { MessageConverter } from './message-converter.js';
import { generateSessionId } from './id.js';
/**
* 会话摘要(用于列表展示)
*/
export interface SessionSummary {
id: string;
title: string;
workdir: string;
messageCount: number;
createdAt: string;
updatedAt: string;
}
/**
* 运行时会话数据
*/
export interface SessionData {
id: string;
projectId: string;
parentId?: string;
agentName?: string;
createdAt: string;
updatedAt: string;
workdir: string;
title?: string;
messages: ModelMessage[];
discoveredTools: string[];
todos: TodoItem[];
}
/**
* 会话存储管理
*/
export class SessionStore {
private messageConverter: MessageConverter;
constructor() {
this.messageConverter = new MessageConverter();
}
/**
* 创建新会话
*/
async create(projectId: string, workdir: string): Promise<SessionData> {
const sessionInfo = await SessionStorage.create(projectId, workdir);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages: [],
discoveredTools: sessionInfo.discoveredTools,
todos: [],
};
}
/**
* 加载会话(从存储重建)
*/
async load(projectId: string, sessionId: string): Promise<SessionData | null> {
const sessionInfo = await SessionStorage.get(projectId, sessionId);
if (!sessionInfo) return null;
// 加载消息
const messages = await this.messageConverter.loadFromStorage(sessionId);
// 加载 todos
const todoList = await TodoStorage.get(sessionId);
return {
id: sessionInfo.id,
projectId: sessionInfo.projectId,
parentId: sessionInfo.parentId,
agentName: sessionInfo.agentName,
createdAt: new Date(sessionInfo.createdAt).toISOString(),
updatedAt: new Date(sessionInfo.updatedAt).toISOString(),
workdir: sessionInfo.workdir,
title: sessionInfo.title,
messages,
discoveredTools: sessionInfo.discoveredTools,
todos: todoList?.items || [],
};
}
/**
* 保存会话信息
*/
async save(session: SessionData): Promise<void> {
const sessionInfo: SessionInfo = {
id: session.id,
projectId: session.projectId,
parentId: session.parentId,
agentName: session.agentName,
createdAt: new Date(session.createdAt).getTime(),
updatedAt: Date.now(),
workdir: session.workdir,
title: session.title,
discoveredTools: session.discoveredTools,
stats: {
messageCount: session.messages.length,
inputTokens: 0,
outputTokens: 0,
},
};
await SessionStorage.save(sessionInfo);
}
/**
* 同步消息到存储
*/
async syncMessages(sessionId: string, messages: ModelMessage[]): Promise<void> {
await this.messageConverter.syncToStorage(sessionId, messages);
}
/**
* 更新待办事项
*/
async setTodos(
sessionId: string,
todos: Array<{ content: string; status: 'pending' | 'in_progress' | 'completed' }>
): Promise<TodoItem[]> {
const todoList = await TodoStorage.replace(sessionId, todos);
return todoList.items;
}
/**
* 列出项目的所有会话
*/
async listByProject(projectId: string): Promise<SessionSummary[]> {
const sessions = await SessionStorage.listByProject(projectId);
return this.toSummaries(sessions);
}
/**
* 列出所有会话
*/
async listAll(): Promise<SessionSummary[]> {
const sessions = await SessionStorage.listAll();
return this.toSummaries(sessions);
}
/**
* 删除会话
*/
async delete(projectId: string, sessionId: string): Promise<boolean> {
try {
// 删除会话的消息和 Parts
const messageInfos = await MessageStorage.listBySession(sessionId);
for (const msg of messageInfos) {
await PartStorage.removeByMessage(msg.id);
}
await MessageStorage.removeBySession(sessionId);
// 删除 todos
await TodoStorage.removeBySession(sessionId);
// 删除会话信息
await SessionStorage.remove(projectId, sessionId);
return true;
} catch {
return false;
}
}
/**
* 创建子会话(用于 Task 工具)
*/
createChildSession(
projectId: string,
parentId: string,
agentName: string,
workdir: string,
title?: string
): SessionData {
return {
id: generateSessionId(),
projectId,
parentId,
agentName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
workdir,
title: title || `子任务 (@${agentName})`,
messages: [],
discoveredTools: [],
todos: [],
};
}
/**
* 保存子会话
*/
async saveChildSession(session: SessionData): Promise<void> {
const sessionInfo: SessionInfo = {
id: session.id,
projectId: session.projectId,
parentId: session.parentId,
agentName: session.agentName,
createdAt: new Date(session.createdAt).getTime(),
updatedAt: Date.now(),
workdir: session.workdir,
title: session.title,
discoveredTools: session.discoveredTools,
};
await SessionStorage.save(sessionInfo);
}
/**
* 转换为摘要列表
*/
private toSummaries(sessions: SessionInfo[]): SessionSummary[] {
return sessions.map((s) => ({
id: s.id,
title: s.title || `会话 ${s.id}`,
workdir: s.workdir,
messageCount: s.stats?.messageCount || 0,
createdAt: new Date(s.createdAt).toISOString(),
updatedAt: new Date(s.updatedAt).toISOString(),
}));
}
}