update at 2025-10-16 12:45:05

This commit is contained in:
douboer
2025-10-16 12:45:05 +08:00
parent 4e670ad5f6
commit 298b5aa931
18 changed files with 3311 additions and 1176 deletions

View 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()