update at 2025-10-16 12:45:05
This commit is contained in:
568
web/src/services/chat/ChatOrchestrator.ts
Normal file
568
web/src/services/chat/ChatOrchestrator.ts
Normal file
@@ -0,0 +1,568 @@
|
||||
/**
|
||||
* 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<string, Topic> = new Map()
|
||||
private conversations: Map<string, Conversation> = 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<Topic>): 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<Message> {
|
||||
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<void> {
|
||||
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<Message> {
|
||||
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()
|
||||
Reference in New Issue
Block a user