/** * Agent Adapter * * 将 core 模块的 Agent 适配到 Server 环境 * 处理流式输出、事件推送等 * * 使用接口定义避免直接依赖 @ai-assistant/core 类型 */ import type { SessionStatus } from '../types.js'; import { getSessionManager } from '../session/manager.js'; import { broadcastToSession } from '../ws.js'; import { emitStatusEvent, emitLogEvent } from '../sse.js'; import { createServerPermissionCallback } from '../permission/handler.js'; // ============================================================================ // Core 模块接口定义(避免直接依赖 @ai-assistant/core 类型) // ============================================================================ /** * Token 使用情况接口 */ export interface TokenUsage { input: number; contextLimit: number; available: number; usagePercent: number; } /** * 压缩结果接口 */ export interface CompressionResult { type: 'prune' | 'compaction' | 'both' | 'none'; status: 'success' | 'noop' | 'failed_empty_summary' | 'failed_token_inflated' | 'failed_error'; freedTokens: number; error?: string; originalTokens?: number; summaryTokens?: number; } /** * Chat 返回结果 */ interface ChatResult { text: string; messages: unknown[]; } /** * Agent 实例接口 */ interface AgentInstance { setRegistry(registry: unknown): void; chat(message: string, onStream?: (chunk: string) => void): Promise; getToolCount(): { core: number; discovered: number; total: number }; getContextUsageFormatted(): string; getContextUsage(): TokenUsage; compactHistory(): Promise<{ freedTokens: number; type: string }>; getCompressionManager(): { shouldCompress(messages: unknown[]): boolean; }; getHistory(): unknown[]; } /** * Agent 构造函数接口 */ interface AgentConstructor { new (config: unknown): AgentInstance; } /** * Tool Registry 接口 */ interface ToolRegistry { getCoreTools(): unknown[]; getAllTools(): unknown[]; } /** * Permission Manager 接口 */ interface PermissionManager { setAskCallback(callback: (ctx: unknown) => Promise<{ allow: boolean; remember?: boolean }>): void; } /** * Provider Registry 接口 */ interface ProviderRegistryInterface { init(workdir?: string): Promise; isInitialized(): boolean; } /** * Agent Registry 接口 */ interface AgentRegistryInterface { init(workdir: string): Promise; isInitialized(): boolean; } /** * Core 模块接口 */ interface CoreModule { Agent: AgentConstructor; toolRegistry: ToolRegistry; loadConfig: () => unknown; saveConfig: (config: Record) => void; getPermissionManager: (projectRoot?: string) => PermissionManager; getProviderRegistry: () => ProviderRegistryInterface; agentRegistry: AgentRegistryInterface; } // ============================================================================ // 模块状态 // ============================================================================ // Core 模块引用 let coreModule: CoreModule | null = null; // Agent 实例缓存(每个 session 一个) const agentCache: Map = new Map(); // 配置错误缓存(用于向客户端返回友好错误) let lastConfigError: { provider: string; message: string } | null = null; // ============================================================================ // 公共 API // ============================================================================ /** * 初始化 core 模块 */ export async function initCore(): Promise { try { // 使用变量避免 TypeScript 静态分析 import 路径 const corePath = '@ai-assistant/core'; // eslint-disable-next-line @typescript-eslint/no-require-imports const core = (await import(/* webpackIgnore: true */ corePath)) as unknown as CoreModule; // 验证模块结构 if (!core.Agent || !core.toolRegistry || !core.loadConfig) { console.warn('[Agent] Core module missing required exports'); return false; } // 初始化 ProviderRegistry(加载用户配置) const providerRegistry = core.getProviderRegistry(); if (!providerRegistry.isInitialized()) { await providerRegistry.init(); console.log('[Agent] ProviderRegistry initialized'); } // 初始化 AgentRegistry(加载用户自定义 Agent 配置) const agentRegistry = core.agentRegistry; if (!agentRegistry.isInitialized()) { await agentRegistry.init(process.cwd()); console.log('[Agent] AgentRegistry initialized'); } coreModule = core; console.log('[Agent] Core module loaded'); return true; } catch (error) { console.warn('[Agent] Core module not available:', error); return false; } } /** * 检查 core 模块是否可用 */ export function isCoreAvailable(): boolean { return coreModule !== null; } /** * 获取或创建 Agent 实例 * * @returns Agent 实例,或 null(Core 不可用或配置错误) */ export function getOrCreateAgent(sessionId: string): AgentInstance | null { // 清除之前的配置错误 lastConfigError = null; if (!coreModule) { return null; } // 检查缓存 if (agentCache.has(sessionId)) { return agentCache.get(sessionId)!; } try { // 创建新 Agent(可能抛出 ConfigurationError) const config = coreModule.loadConfig(); const agent = new coreModule.Agent(config); agent.setRegistry(coreModule.toolRegistry); // 设置权限回调,通过 WebSocket 请求用户确认 const permissionManager = coreModule.getPermissionManager(); permissionManager.setAskCallback(createServerPermissionCallback(sessionId)); agentCache.set(sessionId, agent); return agent; } catch (error) { // 检测配置错误(通过 error.name 识别,避免直接依赖 Core 类型) if (error instanceof Error && error.name === 'ConfigurationError') { const configError = error as Error & { provider?: string }; lastConfigError = { provider: configError.provider || 'unknown', message: error.message, }; console.warn(`[Agent] Configuration error: ${error.message}`); return null; } // 其他错误继续抛出 throw error; } } /** * 销毁 Agent 实例 */ export function destroyAgent(sessionId: string): void { agentCache.delete(sessionId); } /** * 处理用户消息并流式返回响应 */ export async function processMessage(sessionId: string, content: string): Promise { const sessionManager = getSessionManager(); // 更新状态 sessionManager.updateStatus(sessionId, 'busy' as SessionStatus); emitStatusEvent(sessionId, 'processing', { message: '正在处理...' }); // 获取 Agent const agent = getOrCreateAgent(sessionId); if (!agent) { // 检查是否为配置错误 if (lastConfigError) { // 返回配置错误,引导用户配置 Provider broadcastToSession(sessionId, { type: 'error', sessionId, payload: { type: 'config_error', message: lastConfigError.message, provider: lastConfigError.provider, action: 'open_providers_panel', }, }); emitLogEvent(sessionId, 'error', `配置错误: ${lastConfigError.message}`); sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); emitStatusEvent(sessionId, 'idle'); return; } // Core 模块不可用,返回占位响应 const errorContent = 'Agent core module not available. Please build @ai-assistant/core first.'; broadcastToSession(sessionId, { type: 'chunk', sessionId, payload: { content: errorContent }, }); broadcastToSession(sessionId, { type: 'done', sessionId, payload: { text: errorContent, hasToolCalls: false, messageCount: 0 }, }); sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); emitStatusEvent(sessionId, 'idle'); return; } try { // 调用 Agent 的 chat 方法,使用流式回调 const result = await agent.chat(content, (chunk: string) => { // 推送流式内容 broadcastToSession(sessionId, { type: 'chunk', sessionId, payload: { content: chunk }, }); // 检测工具调用 if (chunk.includes('[调用工具:')) { const match = chunk.match(/\[调用工具: (.+?)\]/); if (match) { emitLogEvent(sessionId, 'info', `调用工具: ${match[1]}`); } } }); // 消息已由 Core Agent 自动持久化,这里只更新 Server 端的会话计数 const session = sessionManager.get(sessionId); if (session) { // 从 Agent 获取实际消息数 const history = agent.getHistory(); session.messageCount = history.length; session.updatedAt = new Date().toISOString(); } // 检查是否有工具调用 const hasToolCalls = result.messages.some((m: unknown) => { const msg = m as { content?: unknown }; return Array.isArray(msg.content) && msg.content.some((c: unknown) => { const block = c as { type?: string }; return block.type === 'tool-call'; }); }); // 发送完成消息 broadcastToSession(sessionId, { type: 'done', sessionId, payload: { text: result.text, hasToolCalls, messageCount: result.messages.length, }, }); // 检查是否需要生成会话标题(首次对话完成后) if (session && !session.name) { // 异步生成标题,不阻塞响应 generateSessionTitle(sessionId, content, result.text).catch(err => { console.error('[Agent] Failed to generate session title:', err); }); } emitStatusEvent(sessionId, 'idle'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // 发送错误 broadcastToSession(sessionId, { type: 'error', sessionId, payload: { message: errorMessage }, }); emitLogEvent(sessionId, 'error', errorMessage); } finally { sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); } } /** * 取消正在进行的处理 */ export function cancelProcessing(sessionId: string): void { // TODO: 实现取消逻辑 // 目前 AI SDK 的 streamText 不支持取消 const sessionManager = getSessionManager(); sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); emitStatusEvent(sessionId, 'cancelled'); } /** * 获取 Agent 统计信息 */ export function getAgentStats(sessionId: string): { available: boolean; toolCount?: { core: number; discovered: number; total: number }; contextUsage?: string; } { if (!coreModule) { return { available: false }; } const agent = agentCache.get(sessionId); if (!agent) { return { available: true }; } return { available: true, toolCount: agent.getToolCount(), contextUsage: agent.getContextUsageFormatted(), }; } // ============================================================================ // 会话标题生成 // ============================================================================ /** * 从用户消息中提取简短标题 * 使用简单的启发式方法,不依赖 LLM */ function extractTitleFromMessage(userMessage: string): string { // 移除多余空白 const cleaned = userMessage.trim().replace(/\s+/g, ' '); // 如果消息很短,直接使用 if (cleaned.length <= 30) { return cleaned; } // 尝试提取第一句话 const firstSentence = cleaned.match(/^[^。!?.!?]+[。!?.!?]?/)?.[0] || cleaned; // 如果第一句话也很长,截断 if (firstSentence.length > 40) { return firstSentence.slice(0, 37) + '...'; } return firstSentence; } /** * 生成会话标题并更新 */ async function generateSessionTitle( sessionId: string, userMessage: string, _assistantResponse: string ): Promise { const sessionManager = getSessionManager(); // 使用简单提取方式生成标题 const title = extractTitleFromMessage(userMessage); // 更新会话标题 const updatedSession = await sessionManager.updateSessionName(sessionId, title); if (updatedSession) { // 广播标题更新事件 broadcastToSession(sessionId, { type: 'session_updated', sessionId, payload: { id: updatedSession.id, name: updatedSession.name, }, }); console.log(`[Agent] Session title generated: "${title}"`); } } // ============================================================================ // 上下文压缩 API // ============================================================================ /** * 上下文使用情况(带额外字段) */ export interface ContextUsageInfo extends TokenUsage { formatted: string; shouldCompress: boolean; } /** * 获取会话的上下文使用情况 */ export function getContextUsage(sessionId: string): ContextUsageInfo | null { if (!coreModule) { return null; } const agent = agentCache.get(sessionId); if (!agent) { return null; } const usage = agent.getContextUsage(); const formatted = agent.getContextUsageFormatted(); return { ...usage, formatted, shouldCompress: usage.usagePercent >= 80, // 80% 阈值建议压缩 }; } /** * 执行上下文压缩 */ export async function compressContext( sessionId: string, force: boolean = false ): Promise { if (!coreModule) { return null; } const agent = agentCache.get(sessionId); if (!agent) { return null; } try { // 使用强制压缩或普通压缩 const result = await agent.compactHistory(); return { type: result.type as CompressionResult['type'], status: result.freedTokens > 0 ? 'success' : 'noop', freedTokens: result.freedTokens, }; } catch (error) { return { type: 'none', status: 'failed_error', freedTokens: 0, error: error instanceof Error ? error.message : String(error), }; } }