/** * ChatOrchestrator * 聊天服务协调器 - 协调所有聊天相关的服务 * * 职责: * - 协调 MessageService、ConversationService、StreamProcessor、ToolExecutor * - 提供高级聊天操作(发送消息、流式发送、重新生成等) * - 管理话题和对话 * - 处理持久化 * - 统一错误处理 */ import type { Topic, Conversation, Message, SendMessageOptions, TopicFilter } from '../../types/chat' import { MessageService } from './MessageService' import { ConversationService } from './ConversationService' import { StreamProcessor } from './StreamProcessor' import { ToolExecutor } from './ToolExecutor' import { logger } from '../../utils/logger' import { ValidationError, ServiceError, ErrorCode } from '../../utils/error' const log = logger.namespace('ChatOrchestrator') export class ChatOrchestrator { private static instance: ChatOrchestrator // 数据存储 private topics: Map = new Map() private conversations: Map = new Map() // 服务实例 private messageService: MessageService private conversationService: ConversationService private streamProcessor: StreamProcessor private toolExecutor: ToolExecutor private constructor() { // 初始化服务 this.messageService = new MessageService(this.conversations) this.conversationService = new ConversationService(this.conversations) this.streamProcessor = new StreamProcessor() this.toolExecutor = new ToolExecutor() log.info('ChatOrchestrator 初始化完成') } static getInstance(): ChatOrchestrator { if (!ChatOrchestrator.instance) { ChatOrchestrator.instance = new ChatOrchestrator() } return ChatOrchestrator.instance } // ==================== 初始化 ==================== /** * 初始化 */ initialize(): void { log.info('开始初始化') this.loadTopics() this.loadConversations() // 如果没有话题,创建默认话题 if (this.topics.size === 0) { this.createTopic('欢迎使用', { description: '开始你的第一次对话' }) } log.info('初始化完成', { topicCount: this.topics.size, conversationCount: this.conversations.size }) } // ==================== 话题管理 ==================== /** * 创建新话题 */ createTopic(name: string, options?: { description?: string modelId?: string }): Topic { const topic: Topic = { id: this.generateId(), name: name || '新对话', description: options?.description, createdAt: new Date(), updatedAt: new Date(), messageCount: 0, pinned: false, archived: false, favorite: false, model: options?.modelId } this.topics.set(topic.id, topic) this.saveTopics() // 创建对应的对话 this.conversationService.createConversation({ topicId: topic.id, model: options?.modelId }) this.saveConversations() log.info('创建话题', { topicId: topic.id, name: topic.name }) return topic } /** * 获取所有话题 */ getTopics(filter?: TopicFilter): Topic[] { let topics = Array.from(this.topics.values()) if (filter) { if (filter.search) { const search = filter.search.toLowerCase() topics = topics.filter(t => t.name.toLowerCase().includes(search) || t.description?.toLowerCase().includes(search) || t.lastMessage?.toLowerCase().includes(search) ) } if (filter.pinned !== undefined) { topics = topics.filter(t => t.pinned === filter.pinned) } if (filter.archived !== undefined) { topics = topics.filter(t => t.archived === filter.archived) } if (filter.favorite !== undefined) { topics = topics.filter(t => t.favorite === filter.favorite) } } // 排序:置顶 > 更新时间 return topics.sort((a, b) => { if (a.pinned !== b.pinned) return a.pinned ? -1 : 1 return b.updatedAt.getTime() - a.updatedAt.getTime() }) } /** * 获取话题 */ getTopic(topicId: string): Topic | undefined { return this.topics.get(topicId) } /** * 更新话题 */ updateTopic(topicId: string, updates: Partial): boolean { const topic = this.topics.get(topicId) if (!topic) return false Object.assign(topic, updates) topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() log.debug('更新话题', { topicId, updates }) return true } /** * 删除话题 */ deleteTopic(topicId: string): boolean { const topic = this.topics.get(topicId) if (!topic) return false // 删除对话 this.conversationService.deleteConversationByTopicId(topicId) // 删除话题 this.topics.delete(topicId) this.saveTopics() this.saveConversations() log.info('删除话题', { topicId }) return true } /** * 切换话题置顶状态 */ toggleTopicPin(topicId: string): boolean { const topic = this.topics.get(topicId) if (!topic) return false topic.pinned = !topic.pinned topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() log.debug('切换置顶', { topicId, pinned: topic.pinned }) return true } /** * 切换话题收藏状态 */ toggleTopicFavorite(topicId: string): boolean { const topic = this.topics.get(topicId) if (!topic) return false topic.favorite = !topic.favorite topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() log.debug('切换收藏', { topicId, favorite: topic.favorite }) return true } /** * 归档话题 */ archiveTopic(topicId: string): boolean { const topic = this.topics.get(topicId) if (!topic) return false topic.archived = !topic.archived topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() log.debug('切换归档', { topicId, archived: topic.archived }) return true } // ==================== 消息管理 ==================== /** * 获取话题的消息列表 */ getMessages(topicId: string): Message[] { return this.messageService.getMessagesByTopicId(topicId) } /** * 删除消息 */ deleteMessage(topicId: string, messageId: string): boolean { const conversation = this.conversationService.getConversationByTopicId(topicId) if (!conversation) return false const success = this.messageService.deleteMessage(conversation.id, messageId) if (success) { this.updateTopicAfterMessageChange(topicId, conversation) this.saveConversations() } return success } // ==================== 发送消息 ==================== /** * 发送消息(非流式) */ async sendMessage(options: SendMessageOptions): Promise { const { topicId, content, role = 'user', model } = options // 验证 if (!content.trim()) { throw new ValidationError('消息内容不能为空') } // 获取对话 const conversation = this.conversationService.getConversationByTopicId(topicId) if (!conversation) { throw new ValidationError('对话不存在', { topicId }) } log.info('发送消息', { topicId, role, contentLength: content.length }) // 创建用户消息 const userMessage = this.messageService.createMessage(conversation.id, { role, content, status: 'success' }) // 更新话题 this.updateTopicAfterNewMessage(topicId, conversation, content) this.saveConversations() // 如果不是用户消息,直接返回 if (role !== 'user') { return userMessage } // 创建助手消息占位符 const assistantMessage = this.messageService.createMessage(conversation.id, { role: 'assistant', content: '', status: 'sending', model: model || conversation.metadata?.model }) this.saveConversations() try { // TODO: 调用非流式 API // 这里需要实现非流式调用逻辑 throw new ServiceError('非流式发送暂未实现,请使用流式发送', ErrorCode.SERVICE_ERROR) } catch (error) { this.messageService.updateMessageStatus(conversation.id, assistantMessage.id, 'error') this.saveConversations() throw error } } /** * 发送消息(流式) */ async sendMessageStream( options: SendMessageOptions, onChunk: (event: any) => void, mcpServerId?: string, signal?: AbortSignal ): Promise { const { topicId, content, role = 'user', model } = options // 验证 if (!content.trim()) { throw new ValidationError('消息内容不能为空') } // 获取对话 const conversation = this.conversationService.getConversationByTopicId(topicId) if (!conversation) { throw new ValidationError('对话不存在', { topicId }) } log.info('流式发送消息', { topicId, contentLength: content.length, hasMcpServer: !!mcpServerId }) // 创建用户消息 this.messageService.createMessage(conversation.id, { role, content, status: 'success' }) // 更新话题(用户消息) this.updateTopicAfterNewMessage(topicId, conversation, content) this.saveConversations() // 创建助手消息 const assistantMessage = this.messageService.createMessage(conversation.id, { role: 'assistant', content: '', status: 'sending', model: model || conversation.metadata?.model }) // 更新话题计数 const topic = this.topics.get(topicId) if (topic) { topic.messageCount = conversation.messages.length this.topics.set(topicId, topic) this.saveTopics() } this.saveConversations() onChunk({ type: 'start', messageId: assistantMessage.id }) try { // 处理流式响应 const result = await this.streamProcessor.processStream({ conversation, model, mcpServerId, signal, onChunk: (chunk) => { this.messageService.appendMessageContent(conversation.id, assistantMessage.id, chunk) this.saveConversations() onChunk({ type: 'delta', content: chunk, messageId: assistantMessage.id }) } }) // 处理工具调用 if (result.toolCalls && result.toolCalls.length > 0 && mcpServerId) { log.info('处理工具调用', { toolCount: result.toolCalls.length }) await this.toolExecutor.executeToolCalls({ conversation, toolCalls: result.toolCalls, mcpServerId, model, onChunk: (chunk) => { this.messageService.appendMessageContent(conversation.id, assistantMessage.id, chunk) this.saveConversations() onChunk({ type: 'delta', content: chunk, messageId: assistantMessage.id }) }, tools: undefined // TODO: 传递工具列表 }) } // 完成 this.messageService.updateMessageStatus(conversation.id, assistantMessage.id, 'success') this.saveConversations() onChunk({ type: 'end', messageId: assistantMessage.id }) // 更新话题(完成) const lastMessage = this.messageService.getLastMessage(conversation.id) if (topic && lastMessage) { topic.messageCount = conversation.messages.length topic.lastMessage = this.messageService.getMessagePreview(lastMessage.content) topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() } log.info('流式发送完成', { messageId: assistantMessage.id }) } catch (error) { log.error('流式发送失败', error) this.messageService.updateMessageStatus(conversation.id, assistantMessage.id, 'error') this.saveConversations() onChunk({ type: 'error', error: error instanceof Error ? error.message : '发送失败' }) throw error } } /** * 重新生成消息 */ async regenerateMessage(topicId: string, messageId: string): Promise { const conversation = this.conversationService.getConversationByTopicId(topicId) if (!conversation) { throw new ValidationError('对话不存在', { topicId }) } // 删除该消息之后的所有消息 const success = this.messageService.deleteMessagesAfter(conversation.id, messageId) if (!success) { throw new ValidationError('消息不存在', { messageId }) } // 获取最后一条用户消息 const lastUserMessage = this.messageService.getLastUserMessage(conversation.id) if (!lastUserMessage) { throw new ValidationError('没有找到用户消息') } log.info('重新生成消息', { topicId, messageId }) // TODO: 实现重新生成逻辑 throw new ServiceError('重新生成功能暂未实现', ErrorCode.SERVICE_ERROR) } // ==================== 辅助方法 ==================== /** * 更新话题(新消息) */ private updateTopicAfterNewMessage( topicId: string, conversation: Conversation, content: string ): void { const topic = this.topics.get(topicId) if (!topic) return topic.messageCount = conversation.messages.length topic.lastMessage = this.messageService.getMessagePreview(content) topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() } /** * 更新话题(消息变更) */ private updateTopicAfterMessageChange( topicId: string, conversation: Conversation ): void { const topic = this.topics.get(topicId) if (!topic) return topic.messageCount = conversation.messages.length const lastMessage = this.messageService.getLastMessage(conversation.id) topic.lastMessage = lastMessage ? this.messageService.getMessagePreview(lastMessage.content) : undefined topic.updatedAt = new Date() this.topics.set(topicId, topic) this.saveTopics() } /** * 生成唯一 ID */ private generateId(): string { return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } // ==================== 持久化 ==================== private saveTopics(): void { try { const data = Array.from(this.topics.values()) localStorage.setItem('chat-topics', JSON.stringify(data)) } catch (error) { log.error('保存话题失败', error) } } private loadTopics(): void { try { const data = localStorage.getItem('chat-topics') if (data) { const topics = JSON.parse(data) as Topic[] topics.forEach(topic => { topic.createdAt = new Date(topic.createdAt) topic.updatedAt = new Date(topic.updatedAt) this.topics.set(topic.id, topic) }) } } catch (error) { log.error('加载话题失败', error) } } private saveConversations(): void { try { const data = Array.from(this.conversations.values()) localStorage.setItem('chat-conversations', JSON.stringify(data)) } catch (error) { log.error('保存对话失败', error) } } private loadConversations(): void { try { const data = localStorage.getItem('chat-conversations') if (data) { const conversations = JSON.parse(data) as Conversation[] conversations.forEach(conv => { conv.createdAt = new Date(conv.createdAt) conv.updatedAt = new Date(conv.updatedAt) conv.messages.forEach(msg => { msg.timestamp = new Date(msg.timestamp) }) this.conversations.set(conv.id, conv) }) } } catch (error) { log.error('加载对话失败', error) } } } // 导出单例 export const chatOrchestrator = ChatOrchestrator.getInstance()