update at 2025-10-16 12:45:05
This commit is contained in:
@@ -165,7 +165,9 @@
|
||||
</n-dropdown>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<span class="toolbar-divider">|</span>
|
||||
-->
|
||||
|
||||
<div class="toolbar-right">
|
||||
|
||||
@@ -204,11 +206,6 @@
|
||||
@keydown.enter="handleKeyDown"
|
||||
class="message-input"
|
||||
/>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="input-footer">
|
||||
<span class="hint-text">ESC 关闭 | ▲▼ 选择 | ⌘ + ▲▼ 翻页 | ↩ 确认</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
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()
|
||||
140
web/src/services/chat/ConversationService.ts
Normal file
140
web/src/services/chat/ConversationService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* ConversationService
|
||||
* 负责对话的管理
|
||||
*
|
||||
* 职责:
|
||||
* - 对话的创建、读取、删除
|
||||
* - 对话与话题的关联
|
||||
* - 对话元数据管理
|
||||
*/
|
||||
|
||||
import type { Conversation } from '../../types/chat'
|
||||
|
||||
export interface CreateConversationOptions {
|
||||
topicId: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
systemPrompt?: string
|
||||
}
|
||||
|
||||
export class ConversationService {
|
||||
private conversations: Map<string, Conversation>
|
||||
|
||||
constructor(conversations: Map<string, Conversation>) {
|
||||
this.conversations = conversations
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新对话
|
||||
*/
|
||||
createConversation(options: CreateConversationOptions): Conversation {
|
||||
const conversation: Conversation = {
|
||||
id: this.generateId(),
|
||||
topicId: options.topicId,
|
||||
messages: [],
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
metadata: {
|
||||
model: options.model,
|
||||
temperature: options.temperature,
|
||||
maxTokens: options.maxTokens,
|
||||
systemPrompt: options.systemPrompt
|
||||
}
|
||||
}
|
||||
|
||||
this.conversations.set(conversation.id, conversation)
|
||||
return conversation
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话
|
||||
*/
|
||||
getConversation(conversationId: string): Conversation | undefined {
|
||||
return this.conversations.get(conversationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 topicId 获取对话
|
||||
*/
|
||||
getConversationByTopicId(topicId: string): Conversation | undefined {
|
||||
for (const conv of this.conversations.values()) {
|
||||
if (conv.topicId === topicId) {
|
||||
return conv
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*/
|
||||
getAllConversations(): Conversation[] {
|
||||
return Array.from(this.conversations.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对话
|
||||
*/
|
||||
deleteConversation(conversationId: string): boolean {
|
||||
return this.conversations.delete(conversationId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除话题对应的对话
|
||||
*/
|
||||
deleteConversationByTopicId(topicId: string): boolean {
|
||||
for (const [id, conv] of this.conversations.entries()) {
|
||||
if (conv.topicId === topicId) {
|
||||
this.conversations.delete(id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话元数据
|
||||
*/
|
||||
updateMetadata(
|
||||
conversationId: string,
|
||||
metadata: Partial<Conversation['metadata']>
|
||||
): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
conversation.metadata = {
|
||||
...conversation.metadata,
|
||||
...metadata
|
||||
}
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空对话消息
|
||||
*/
|
||||
clearMessages(conversationId: string): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
conversation.messages = []
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话消息数量
|
||||
*/
|
||||
getMessageCount(conversationId: string): number {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
return conversation?.messages.length || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
}
|
||||
251
web/src/services/chat/MessageService.ts
Normal file
251
web/src/services/chat/MessageService.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* MessageService
|
||||
* 负责消息的 CRUD 操作
|
||||
*
|
||||
* 职责:
|
||||
* - 消息的创建、读取、更新、删除
|
||||
* - 消息状态管理
|
||||
* - 消息查询和过滤
|
||||
*/
|
||||
|
||||
import type { Message, Conversation } from '../../types/chat'
|
||||
import type { CreateMessageOptions, UpdateMessageOptions, MessageQueryResult } from './types'
|
||||
|
||||
export class MessageService {
|
||||
private conversations: Map<string, Conversation>
|
||||
|
||||
constructor(conversations: Map<string, Conversation>) {
|
||||
this.conversations = conversations
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定对话中创建新消息
|
||||
*/
|
||||
createMessage(conversationId: string, options: CreateMessageOptions): Message {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) {
|
||||
throw new Error(`Conversation not found: ${conversationId}`)
|
||||
}
|
||||
|
||||
const message: Message = {
|
||||
id: this.generateId(),
|
||||
role: options.role,
|
||||
content: options.content,
|
||||
status: options.status || 'success',
|
||||
timestamp: new Date(),
|
||||
model: options.model,
|
||||
tokens: options.tokens
|
||||
}
|
||||
|
||||
conversation.messages.push(message)
|
||||
conversation.updatedAt = new Date()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定对话的所有消息
|
||||
*/
|
||||
getMessages(conversationId: string): Message[] {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
return conversation?.messages || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 topicId 获取消息
|
||||
*/
|
||||
getMessagesByTopicId(topicId: string): Message[] {
|
||||
for (const conv of this.conversations.values()) {
|
||||
if (conv.topicId === topicId) {
|
||||
return conv.messages
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定消息
|
||||
*/
|
||||
getMessage(conversationId: string, messageId: string): Message | undefined {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return undefined
|
||||
|
||||
return conversation.messages.find(m => m.id === messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找消息及其位置信息
|
||||
*/
|
||||
findMessage(messageId: string, topicId?: string): MessageQueryResult {
|
||||
for (const conv of this.conversations.values()) {
|
||||
if (topicId && conv.topicId !== topicId) continue
|
||||
|
||||
const index = conv.messages.findIndex(m => m.id === messageId)
|
||||
if (index !== -1) {
|
||||
return {
|
||||
message: conv.messages[index],
|
||||
conversation: conv,
|
||||
index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: undefined,
|
||||
conversation: undefined,
|
||||
index: -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
*/
|
||||
updateMessage(
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
options: UpdateMessageOptions
|
||||
): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
const message = conversation.messages.find(m => m.id === messageId)
|
||||
if (!message) return false
|
||||
|
||||
// 更新消息字段
|
||||
if (options.content !== undefined) {
|
||||
message.content = options.content
|
||||
}
|
||||
if (options.status !== undefined) {
|
||||
message.status = options.status
|
||||
}
|
||||
if (options.tokens !== undefined) {
|
||||
message.tokens = options.tokens
|
||||
}
|
||||
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息状态
|
||||
*/
|
||||
updateMessageStatus(
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
status: 'sending' | 'success' | 'error' | 'paused'
|
||||
): boolean {
|
||||
return this.updateMessage(conversationId, messageId, { status })
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加消息内容(用于流式响应)
|
||||
*/
|
||||
appendMessageContent(
|
||||
conversationId: string,
|
||||
messageId: string,
|
||||
content: string
|
||||
): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
const message = conversation.messages.find(m => m.id === messageId)
|
||||
if (!message) return false
|
||||
|
||||
message.content += content
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
deleteMessage(conversationId: string, messageId: string): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
const index = conversation.messages.findIndex(m => m.id === messageId)
|
||||
if (index === -1) return false
|
||||
|
||||
conversation.messages.splice(index, 1)
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定消息之后的所有消息(包括该消息)
|
||||
*/
|
||||
deleteMessagesAfter(conversationId: string, messageId: string): boolean {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return false
|
||||
|
||||
const index = conversation.messages.findIndex(m => m.id === messageId)
|
||||
if (index === -1) return false
|
||||
|
||||
conversation.messages.splice(index)
|
||||
conversation.updatedAt = new Date()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话中最后一条用户消息
|
||||
*/
|
||||
getLastUserMessage(conversationId: string): Message | undefined {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return undefined
|
||||
|
||||
for (let i = conversation.messages.length - 1; i >= 0; i--) {
|
||||
if (conversation.messages[i].role === 'user') {
|
||||
return conversation.messages[i]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话中最后一条消息
|
||||
*/
|
||||
getLastMessage(conversationId: string): Message | undefined {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return undefined
|
||||
|
||||
const messages = conversation.messages
|
||||
return messages.length > 0 ? messages[messages.length - 1] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息预览文本
|
||||
*/
|
||||
getMessagePreview(content: string, maxLength = 50): string {
|
||||
if (!content) return ''
|
||||
const text = content.replace(/\n/g, ' ').trim()
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话中成功的消息(用于发送给 LLM)
|
||||
*/
|
||||
getSuccessMessages(conversationId: string): Message[] {
|
||||
const conversation = this.conversations.get(conversationId)
|
||||
if (!conversation) return []
|
||||
|
||||
return conversation.messages.filter(m =>
|
||||
m.status === 'success' || m.status === 'paused'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的 N 条成功消息
|
||||
*/
|
||||
getRecentSuccessMessages(conversationId: string, limit: number): Message[] {
|
||||
const successMessages = this.getSuccessMessages(conversationId)
|
||||
return successMessages.slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一 ID
|
||||
*/
|
||||
private generateId(): string {
|
||||
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
}
|
||||
335
web/src/services/chat/StreamProcessor.ts
Normal file
335
web/src/services/chat/StreamProcessor.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* StreamProcessor
|
||||
* 负责处理流式响应
|
||||
*
|
||||
* 职责:
|
||||
* - 管理流式请求
|
||||
* - 处理流式数据块
|
||||
* - 缓冲和批量输出
|
||||
* - 性能监控
|
||||
* - 错误处理和取消
|
||||
*/
|
||||
|
||||
import type { Conversation } from '../../types/chat'
|
||||
import { modelServiceManager } from '../modelServiceManager'
|
||||
import { mcpClientService } from '../MCPClientService'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { ServiceError, ErrorCode } from '../../utils/error'
|
||||
|
||||
const log = logger.namespace('StreamProcessor')
|
||||
|
||||
/**
|
||||
* 流式处理选项
|
||||
*/
|
||||
export interface StreamOptions {
|
||||
conversation: Conversation
|
||||
model?: string
|
||||
mcpServerId?: string
|
||||
signal?: AbortSignal
|
||||
onChunk: (chunk: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式处理结果
|
||||
*/
|
||||
export interface StreamResult {
|
||||
success: boolean
|
||||
error?: string
|
||||
toolCalls?: any[]
|
||||
metrics?: {
|
||||
totalTime: number
|
||||
firstChunkDelay: number
|
||||
chunkCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamProcessor {
|
||||
private static readonly MAX_CONTEXT_MESSAGES = 20
|
||||
private static readonly BATCH_SIZE = 3 // 每3个字符输出一次,增强流式效果
|
||||
|
||||
/**
|
||||
* 处理流式请求
|
||||
*/
|
||||
async processStream(options: StreamOptions): Promise<StreamResult> {
|
||||
const startTime = performance.now()
|
||||
log.info('开始流式处理')
|
||||
|
||||
try {
|
||||
// 获取工具列表
|
||||
const { tools, mcpServerName } = await this.prepareTools(options.mcpServerId)
|
||||
|
||||
// 准备消息列表
|
||||
const messages = this.prepareMessages(options.conversation, tools, mcpServerName)
|
||||
|
||||
// 选择服务和模型
|
||||
const { service, selectedModel } = this.selectServiceAndModel(options.model)
|
||||
|
||||
log.info('流式处理配置', {
|
||||
service: service.name,
|
||||
model: selectedModel,
|
||||
mcpServer: options.mcpServerId || '未选择',
|
||||
toolCount: tools.length,
|
||||
messageCount: messages.length
|
||||
})
|
||||
|
||||
// 执行流式请求
|
||||
const result = await this.executeStream(
|
||||
service.id,
|
||||
messages,
|
||||
selectedModel,
|
||||
options.onChunk,
|
||||
tools,
|
||||
options.signal,
|
||||
startTime
|
||||
)
|
||||
|
||||
const endTime = performance.now()
|
||||
log.info('流式处理完成', {
|
||||
totalTime: (endTime - startTime).toFixed(2) + 'ms',
|
||||
chunkCount: result.metrics?.chunkCount
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
log.error('流式处理失败', error)
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : '流式请求失败',
|
||||
ErrorCode.STREAMING_ERROR,
|
||||
{ conversation: options.conversation.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备工具列表
|
||||
*/
|
||||
private async prepareTools(mcpServerId?: string): Promise<{
|
||||
tools: any[]
|
||||
mcpServerName: string
|
||||
}> {
|
||||
let tools: any[] = []
|
||||
let mcpServerName = ''
|
||||
|
||||
if (mcpServerId) {
|
||||
log.debug('获取 MCP 工具', { mcpServerId })
|
||||
const mcpTools = mcpClientService.getTools(mcpServerId)
|
||||
const serverInfo = mcpClientService.getServerInfo(mcpServerId)
|
||||
mcpServerName = serverInfo?.name || 'mcp'
|
||||
|
||||
tools = this.convertToolsToOpenAIFormat(mcpTools, mcpServerName)
|
||||
log.info('MCP 工具已准备', {
|
||||
serverName: mcpServerName,
|
||||
toolCount: tools.length
|
||||
})
|
||||
} else {
|
||||
log.debug('未选择 MCP 服务器,不注入工具')
|
||||
}
|
||||
|
||||
return { tools, mcpServerName }
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备消息列表
|
||||
*/
|
||||
private prepareMessages(
|
||||
conversation: Conversation,
|
||||
tools: any[],
|
||||
mcpServerName: string
|
||||
): any[] {
|
||||
// 过滤成功的消息
|
||||
let messages = conversation.messages
|
||||
.filter(m => m.status === 'success' || m.status === 'paused')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
|
||||
// 限制上下文
|
||||
if (messages.length > StreamProcessor.MAX_CONTEXT_MESSAGES) {
|
||||
log.info('限制上下文', {
|
||||
from: messages.length,
|
||||
to: StreamProcessor.MAX_CONTEXT_MESSAGES
|
||||
})
|
||||
messages = messages.slice(-StreamProcessor.MAX_CONTEXT_MESSAGES)
|
||||
}
|
||||
|
||||
// 添加工具系统提示词
|
||||
if (tools.length > 0 && messages.length > 0 && messages[0].role !== 'system') {
|
||||
const systemPrompt = this.createSystemPromptWithTools(tools, mcpServerName)
|
||||
messages = [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
...messages
|
||||
]
|
||||
}
|
||||
|
||||
log.debug('消息列表已准备', {
|
||||
messageCount: messages.length,
|
||||
hasSystemPrompt: messages[0]?.role === 'system'
|
||||
})
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择服务和模型
|
||||
*/
|
||||
private selectServiceAndModel(requestedModel?: string): {
|
||||
service: any
|
||||
selectedModel: string
|
||||
} {
|
||||
const allServices = modelServiceManager.getAllServices()
|
||||
const services = allServices.filter(s => s.status === 'connected')
|
||||
|
||||
if (services.length === 0) {
|
||||
throw new ServiceError(
|
||||
'没有可用的模型服务,请先在"模型服务"中添加并连接服务',
|
||||
ErrorCode.MODEL_NOT_AVAILABLE
|
||||
)
|
||||
}
|
||||
|
||||
let service = services[0]
|
||||
let selectedModel = requestedModel || service.models?.[0] || 'default'
|
||||
|
||||
// 如果指定了模型,尝试找到拥有该模型的服务
|
||||
if (requestedModel) {
|
||||
const foundService = services.find(s =>
|
||||
s.models && s.models.includes(requestedModel)
|
||||
)
|
||||
if (foundService) {
|
||||
service = foundService
|
||||
selectedModel = requestedModel
|
||||
log.debug('找到匹配服务', { service: foundService.name })
|
||||
} else {
|
||||
log.warn('未找到包含该模型的服务,使用默认服务', {
|
||||
requestedModel
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { service, selectedModel }
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行流式请求
|
||||
*/
|
||||
private async executeStream(
|
||||
serviceId: string,
|
||||
messages: any[],
|
||||
model: string,
|
||||
onChunk: (chunk: string) => void,
|
||||
tools: any[],
|
||||
signal?: AbortSignal,
|
||||
startTime?: number
|
||||
): Promise<StreamResult> {
|
||||
const beforeStreamCall = performance.now()
|
||||
let chunkCount = 0
|
||||
let firstChunkDelay = 0
|
||||
let buffer = ''
|
||||
|
||||
log.info('开始流式请求')
|
||||
|
||||
const result = await modelServiceManager.sendChatRequestStream(
|
||||
serviceId,
|
||||
messages,
|
||||
model,
|
||||
(chunk) => {
|
||||
chunkCount++
|
||||
|
||||
// 记录首字延迟
|
||||
if (chunkCount === 1) {
|
||||
firstChunkDelay = performance.now() - beforeStreamCall
|
||||
log.debug('首字延迟', { delay: firstChunkDelay.toFixed(2) + 'ms' })
|
||||
}
|
||||
|
||||
// 批量输出,增强流式效果
|
||||
buffer += chunk
|
||||
if (buffer.length >= StreamProcessor.BATCH_SIZE) {
|
||||
const output = buffer
|
||||
buffer = ''
|
||||
onChunk(output)
|
||||
}
|
||||
},
|
||||
tools.length > 0 ? tools : undefined,
|
||||
signal
|
||||
)
|
||||
|
||||
// 输出剩余缓冲区内容
|
||||
if (buffer.length > 0) {
|
||||
onChunk(buffer)
|
||||
}
|
||||
|
||||
const afterStreamCall = performance.now()
|
||||
const totalTime = startTime ? afterStreamCall - startTime : afterStreamCall - beforeStreamCall
|
||||
|
||||
log.info('流式请求完成', {
|
||||
chunkCount,
|
||||
totalTime: totalTime.toFixed(2) + 'ms',
|
||||
firstChunkDelay: firstChunkDelay.toFixed(2) + 'ms'
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
throw new ServiceError(
|
||||
result.error || '流式请求失败',
|
||||
ErrorCode.STREAMING_ERROR
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
toolCalls: result.data?.toolCalls,
|
||||
metrics: {
|
||||
totalTime,
|
||||
firstChunkDelay,
|
||||
chunkCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换工具为 OpenAI 格式
|
||||
*/
|
||||
private convertToolsToOpenAIFormat(mcpTools: any[], serverName: string): any[] {
|
||||
if (!Array.isArray(mcpTools)) {
|
||||
log.warn('工具列表不是数组', { mcpTools })
|
||||
return []
|
||||
}
|
||||
|
||||
return mcpTools.map(tool => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: `${serverName}__${tool.name}`,
|
||||
description: tool.description || tool.name,
|
||||
parameters: tool.inputSchema || {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含工具信息的系统提示词
|
||||
*/
|
||||
private createSystemPromptWithTools(tools: any[], serverName: string): string {
|
||||
const toolList = tools.map(t => `- ${t.function.name}: ${t.function.description}`).join('\n')
|
||||
|
||||
return `你是一个智能助手,可以使用以下工具来帮助用户:
|
||||
|
||||
可用工具列表:
|
||||
${toolList}
|
||||
|
||||
当需要使用工具时,请按照 OpenAI 的 function calling 格式调用。工具名称格式为:${serverName}__工具名称
|
||||
|
||||
注意事项:
|
||||
1. 仔细阅读工具的描述,确保理解其功能
|
||||
2. 根据用户需求选择合适的工具
|
||||
3. 提供准确的参数
|
||||
4. 可以连续调用多个工具来完成复杂任务
|
||||
|
||||
现在,请根据用户的需求提供帮助。`
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const streamProcessor = new StreamProcessor()
|
||||
304
web/src/services/chat/ToolExecutor.ts
Normal file
304
web/src/services/chat/ToolExecutor.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* ToolExecutor
|
||||
* 负责工具调用的执行
|
||||
*
|
||||
* 职责:
|
||||
* - 解析工具调用请求
|
||||
* - 执行 MCP 工具
|
||||
* - 处理工具结果
|
||||
* - 支持递归工具调用链
|
||||
* - 错误处理和重试
|
||||
*/
|
||||
|
||||
import type { Conversation } from '../../types/chat'
|
||||
import { modelServiceManager } from '../modelServiceManager'
|
||||
import { mcpClientService } from '../MCPClientService'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { ServiceError, ErrorCode } from '../../utils/error'
|
||||
|
||||
const log = logger.namespace('ToolExecutor')
|
||||
|
||||
/**
|
||||
* 工具调用选项
|
||||
*/
|
||||
export interface ToolCallOptions {
|
||||
conversation: Conversation
|
||||
toolCalls: any[]
|
||||
mcpServerId: string
|
||||
model?: string
|
||||
onChunk: (chunk: string) => void
|
||||
tools?: any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具调用结果
|
||||
*/
|
||||
export interface ToolCallResult {
|
||||
toolCallId: string
|
||||
name: string
|
||||
result: any
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class ToolExecutor {
|
||||
/**
|
||||
* 执行工具调用
|
||||
*/
|
||||
async executeToolCalls(options: ToolCallOptions): Promise<void> {
|
||||
log.info('开始执行工具调用', {
|
||||
toolCount: options.toolCalls.length,
|
||||
mcpServerId: options.mcpServerId
|
||||
})
|
||||
|
||||
try {
|
||||
// 执行所有工具调用
|
||||
const toolResults = await this.executeTools(
|
||||
options.toolCalls,
|
||||
options.mcpServerId,
|
||||
options.onChunk
|
||||
)
|
||||
|
||||
// 将工具结果发送给 AI
|
||||
await this.sendToolResultsToAI(
|
||||
options.conversation,
|
||||
options.toolCalls,
|
||||
toolResults,
|
||||
options.model,
|
||||
options.mcpServerId,
|
||||
options.onChunk,
|
||||
options.tools
|
||||
)
|
||||
} catch (error) {
|
||||
log.error('工具调用执行失败', error)
|
||||
throw new ServiceError(
|
||||
error instanceof Error ? error.message : '工具调用执行失败',
|
||||
ErrorCode.TOOL_EXECUTION_ERROR,
|
||||
{ conversation: options.conversation.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行多个工具
|
||||
*/
|
||||
private async executeTools(
|
||||
toolCalls: any[],
|
||||
mcpServerId: string,
|
||||
onChunk: (chunk: string) => void
|
||||
): Promise<ToolCallResult[]> {
|
||||
const results: ToolCallResult[] = []
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
try {
|
||||
const result = await this.executeSingleTool(
|
||||
toolCall,
|
||||
mcpServerId,
|
||||
onChunk
|
||||
)
|
||||
results.push(result)
|
||||
} catch (error) {
|
||||
log.error('工具执行失败', error, {
|
||||
toolCall: toolCall.function.name
|
||||
})
|
||||
// 继续执行其他工具
|
||||
results.push({
|
||||
toolCallId: toolCall.id,
|
||||
name: toolCall.function.name,
|
||||
result: null,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个工具
|
||||
*/
|
||||
private async executeSingleTool(
|
||||
toolCall: any,
|
||||
mcpServerId: string,
|
||||
onChunk: (chunk: string) => void
|
||||
): Promise<ToolCallResult> {
|
||||
const fullFunctionName = toolCall.function.name
|
||||
const toolName = this.parseToolName(fullFunctionName)
|
||||
const functionArgs = JSON.parse(toolCall.function.arguments)
|
||||
|
||||
log.info('执行工具', {
|
||||
fullName: fullFunctionName,
|
||||
toolName,
|
||||
mcpServerId,
|
||||
args: functionArgs
|
||||
})
|
||||
|
||||
// 通知用户
|
||||
onChunk(`\n\n🔧 正在调用工具: ${toolName}...\n`)
|
||||
|
||||
// 调用 MCP 工具
|
||||
const result = await mcpClientService.callTool(
|
||||
mcpServerId,
|
||||
toolName,
|
||||
functionArgs
|
||||
)
|
||||
|
||||
log.info('工具执行成功', {
|
||||
toolName,
|
||||
resultSize: JSON.stringify(result).length
|
||||
})
|
||||
|
||||
onChunk(`✅ 工具执行完成\n`)
|
||||
|
||||
return {
|
||||
toolCallId: toolCall.id,
|
||||
name: fullFunctionName,
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将工具结果发送给 AI
|
||||
*/
|
||||
private async sendToolResultsToAI(
|
||||
conversation: Conversation,
|
||||
toolCalls: any[],
|
||||
toolResults: ToolCallResult[],
|
||||
model: string | undefined,
|
||||
mcpServerId: string,
|
||||
onChunk: (chunk: string) => void,
|
||||
tools?: any[]
|
||||
): Promise<void> {
|
||||
log.info('将工具结果发送给 AI')
|
||||
|
||||
// 构建消息历史
|
||||
const messages = this.buildMessagesWithToolResults(
|
||||
conversation,
|
||||
toolCalls,
|
||||
toolResults
|
||||
)
|
||||
|
||||
// 选择服务和模型
|
||||
const { service, selectedModel } = this.selectServiceAndModel(model)
|
||||
|
||||
log.info('发送工具结果', {
|
||||
service: service.name,
|
||||
model: selectedModel,
|
||||
messageCount: messages.length,
|
||||
toolCount: tools?.length || 0
|
||||
})
|
||||
|
||||
// 通知用户
|
||||
onChunk('\n\n🤖 正在生成回复...\n')
|
||||
|
||||
// 发送请求
|
||||
const result = await modelServiceManager.sendChatRequestStream(
|
||||
service.id,
|
||||
messages,
|
||||
selectedModel,
|
||||
onChunk,
|
||||
tools
|
||||
)
|
||||
|
||||
// 递归处理:如果 AI 再次调用工具
|
||||
if (result.data?.toolCalls && result.data.toolCalls.length > 0) {
|
||||
log.info('AI 再次调用工具,递归执行', {
|
||||
toolCount: result.data.toolCalls.length
|
||||
})
|
||||
|
||||
await this.executeToolCalls({
|
||||
conversation,
|
||||
toolCalls: result.data.toolCalls,
|
||||
mcpServerId,
|
||||
model,
|
||||
onChunk,
|
||||
tools
|
||||
})
|
||||
} else {
|
||||
log.info('工具调用链完成')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建包含工具结果的消息列表
|
||||
*/
|
||||
private buildMessagesWithToolResults(
|
||||
conversation: Conversation,
|
||||
toolCalls: any[],
|
||||
toolResults: ToolCallResult[]
|
||||
): any[] {
|
||||
// 获取成功的消息
|
||||
const messages = conversation.messages
|
||||
.filter(m => m.status === 'success' || m.status === 'paused')
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
|
||||
// 添加工具调用消息
|
||||
messages.push({
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
tool_calls: toolCalls
|
||||
} as any)
|
||||
|
||||
// 添加工具结果
|
||||
for (const result of toolResults) {
|
||||
messages.push({
|
||||
tool_call_id: result.toolCallId,
|
||||
role: 'tool',
|
||||
name: result.name,
|
||||
content: result.error
|
||||
? JSON.stringify({ error: result.error })
|
||||
: JSON.stringify(result.result)
|
||||
} as any)
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析工具名称
|
||||
*/
|
||||
private parseToolName(fullName: string): string {
|
||||
// 格式:serverName__toolName
|
||||
return fullName.includes('__')
|
||||
? fullName.split('__')[1]
|
||||
: fullName
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择服务和模型
|
||||
*/
|
||||
private selectServiceAndModel(requestedModel?: string): {
|
||||
service: any
|
||||
selectedModel: string
|
||||
} {
|
||||
const allServices = modelServiceManager.getAllServices()
|
||||
const services = allServices.filter(s => s.status === 'connected')
|
||||
|
||||
if (services.length === 0) {
|
||||
throw new ServiceError(
|
||||
'没有可用的模型服务',
|
||||
ErrorCode.MODEL_NOT_AVAILABLE
|
||||
)
|
||||
}
|
||||
|
||||
let service = services[0]
|
||||
let selectedModel = requestedModel || service.models?.[0] || 'default'
|
||||
|
||||
if (requestedModel) {
|
||||
const foundService = services.find(s =>
|
||||
s.models && s.models.includes(requestedModel)
|
||||
)
|
||||
if (foundService) {
|
||||
service = foundService
|
||||
selectedModel = requestedModel
|
||||
}
|
||||
}
|
||||
|
||||
return { service, selectedModel }
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const toolExecutor = new ToolExecutor()
|
||||
15
web/src/services/chat/index.ts
Normal file
15
web/src/services/chat/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Chat Services Index
|
||||
* 导出所有聊天服务
|
||||
*/
|
||||
|
||||
export { MessageService } from './MessageService'
|
||||
export { ConversationService, type CreateConversationOptions } from './ConversationService'
|
||||
export { StreamProcessor, streamProcessor, type StreamOptions, type StreamResult } from './StreamProcessor'
|
||||
export { ToolExecutor, toolExecutor, type ToolCallOptions, type ToolCallResult } from './ToolExecutor'
|
||||
export { ChatOrchestrator, chatOrchestrator } from './ChatOrchestrator'
|
||||
export type {
|
||||
CreateMessageOptions,
|
||||
UpdateMessageOptions,
|
||||
MessageQueryResult
|
||||
} from './types'
|
||||
43
web/src/services/chat/types.ts
Normal file
43
web/src/services/chat/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Chat Service Types
|
||||
* 聊天服务相关的类型定义
|
||||
*/
|
||||
|
||||
import type { Message, Conversation } from '../../types/chat'
|
||||
|
||||
/**
|
||||
* 消息创建选项
|
||||
*/
|
||||
export interface CreateMessageOptions {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
status?: 'sending' | 'success' | 'error' | 'paused'
|
||||
model?: string
|
||||
tokens?: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息更新选项
|
||||
*/
|
||||
export interface UpdateMessageOptions {
|
||||
content?: string
|
||||
status?: 'sending' | 'success' | 'error' | 'paused'
|
||||
tokens?: {
|
||||
prompt: number
|
||||
completion: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息查询结果
|
||||
*/
|
||||
export interface MessageQueryResult {
|
||||
message: Message | undefined
|
||||
conversation: Conversation | undefined
|
||||
index: number
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { reactive, computed } from 'vue'
|
||||
import { chatService } from '../services/chatService'
|
||||
import { chatOrchestrator } from '../services/chat'
|
||||
import type { Topic, Message, TopicFilter } from '../types/chat'
|
||||
|
||||
interface ChatState {
|
||||
@@ -30,7 +30,7 @@ export const useChatStore = () => {
|
||||
})
|
||||
|
||||
const filteredTopics = computed(() => {
|
||||
return chatService.getTopics(state.filter)
|
||||
return chatOrchestrator.getTopics(state.filter)
|
||||
})
|
||||
|
||||
const pinnedTopics = computed(() => {
|
||||
@@ -46,11 +46,11 @@ export const useChatStore = () => {
|
||||
|
||||
// Actions
|
||||
const loadTopics = () => {
|
||||
state.topics = chatService.getTopics()
|
||||
state.topics = chatOrchestrator.getTopics()
|
||||
}
|
||||
|
||||
const createTopic = (name: string) => {
|
||||
const topic = chatService.createTopic(name)
|
||||
const topic = chatOrchestrator.createTopic(name)
|
||||
loadTopics()
|
||||
setCurrentTopic(topic.id)
|
||||
return topic
|
||||
@@ -67,7 +67,7 @@ export const useChatStore = () => {
|
||||
|
||||
const loadMessages = (topicId: string) => {
|
||||
// 创建新数组以确保触发响应式更新
|
||||
state.messages = [...chatService.getMessages(topicId)]
|
||||
state.messages = [...chatOrchestrator.getMessages(topicId)]
|
||||
}
|
||||
|
||||
const sendMessage = async (content: string, model?: string) => {
|
||||
@@ -75,7 +75,7 @@ export const useChatStore = () => {
|
||||
|
||||
state.isSending = true
|
||||
try {
|
||||
await chatService.sendMessage({
|
||||
await chatOrchestrator.sendMessage({
|
||||
topicId: state.currentTopicId,
|
||||
content,
|
||||
model,
|
||||
@@ -105,18 +105,18 @@ export const useChatStore = () => {
|
||||
loadMessages(currentTopicId)
|
||||
|
||||
try {
|
||||
await chatService.sendMessageStream(
|
||||
await chatOrchestrator.sendMessageStream(
|
||||
{
|
||||
topicId: currentTopicId,
|
||||
content,
|
||||
model,
|
||||
stream: true
|
||||
},
|
||||
(event) => {
|
||||
(event: any) => {
|
||||
// 实时更新消息列表
|
||||
if (state.currentTopicId === currentTopicId) {
|
||||
// 强制创建新数组以触发响应式更新
|
||||
state.messages = [...chatService.getMessages(currentTopicId)]
|
||||
state.messages = [...chatOrchestrator.getMessages(currentTopicId)]
|
||||
}
|
||||
|
||||
if (event.type === 'delta' && event.content && onChunk) {
|
||||
@@ -129,7 +129,7 @@ export const useChatStore = () => {
|
||||
|
||||
// 最终更新
|
||||
if (state.currentTopicId === currentTopicId) {
|
||||
state.messages = [...chatService.getMessages(currentTopicId)]
|
||||
state.messages = [...chatOrchestrator.getMessages(currentTopicId)]
|
||||
}
|
||||
loadTopics()
|
||||
} catch (error: any) {
|
||||
@@ -137,7 +137,7 @@ export const useChatStore = () => {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('⏸️ [sendMessageStream] 用户中止,更新消息状态')
|
||||
if (state.currentTopicId === currentTopicId) {
|
||||
state.messages = [...chatService.getMessages(currentTopicId)]
|
||||
state.messages = [...chatOrchestrator.getMessages(currentTopicId)]
|
||||
}
|
||||
loadTopics()
|
||||
} else {
|
||||
@@ -159,7 +159,7 @@ export const useChatStore = () => {
|
||||
|
||||
const deleteMessage = (messageId: string) => {
|
||||
if (!state.currentTopicId) return
|
||||
chatService.deleteMessage(state.currentTopicId, messageId)
|
||||
chatOrchestrator.deleteMessage(state.currentTopicId, messageId)
|
||||
loadMessages(state.currentTopicId)
|
||||
loadTopics()
|
||||
}
|
||||
@@ -168,7 +168,7 @@ export const useChatStore = () => {
|
||||
if (!state.currentTopicId) return
|
||||
state.isSending = true
|
||||
try {
|
||||
await chatService.regenerateMessage(state.currentTopicId, messageId)
|
||||
await chatOrchestrator.regenerateMessage(state.currentTopicId, messageId)
|
||||
loadMessages(state.currentTopicId)
|
||||
loadTopics()
|
||||
} finally {
|
||||
@@ -177,16 +177,16 @@ export const useChatStore = () => {
|
||||
}
|
||||
|
||||
const updateTopic = (topicId: string, updates: Partial<Topic>) => {
|
||||
chatService.updateTopic(topicId, updates)
|
||||
chatOrchestrator.updateTopic(topicId, updates)
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
const deleteTopic = (topicId: string) => {
|
||||
chatService.deleteTopic(topicId)
|
||||
chatOrchestrator.deleteTopic(topicId)
|
||||
loadTopics()
|
||||
if (state.currentTopicId === topicId) {
|
||||
// 删除当前话题后,选择第一个话题
|
||||
const topics = chatService.getTopics()
|
||||
const topics = chatOrchestrator.getTopics()
|
||||
if (topics.length > 0) {
|
||||
setCurrentTopic(topics[0].id)
|
||||
} else {
|
||||
@@ -196,17 +196,17 @@ export const useChatStore = () => {
|
||||
}
|
||||
|
||||
const toggleTopicPin = (topicId: string) => {
|
||||
chatService.toggleTopicPin(topicId)
|
||||
chatOrchestrator.toggleTopicPin(topicId)
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
const toggleTopicFavorite = (topicId: string) => {
|
||||
chatService.toggleTopicFavorite(topicId)
|
||||
chatOrchestrator.toggleTopicFavorite(topicId)
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
const archiveTopic = (topicId: string) => {
|
||||
chatService.archiveTopic(topicId)
|
||||
chatOrchestrator.archiveTopic(topicId)
|
||||
loadTopics()
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ export const useChatStore = () => {
|
||||
}
|
||||
|
||||
const initialize = () => {
|
||||
chatService.initialize()
|
||||
chatOrchestrator.initialize()
|
||||
loadTopics()
|
||||
// 默认选中第一个话题
|
||||
if (state.topics.length > 0 && !state.currentTopicId) {
|
||||
|
||||
213
web/src/utils/error.ts
Normal file
213
web/src/utils/error.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* AppError - 统一错误处理
|
||||
*
|
||||
* 错误层次结构:
|
||||
* - AppError (基类)
|
||||
* - ValidationError (验证错误)
|
||||
* - NetworkError (网络错误)
|
||||
* - APIError (API 错误)
|
||||
* - ServiceError (服务错误)
|
||||
* - StorageError (存储错误)
|
||||
*/
|
||||
|
||||
import { logger } from './logger'
|
||||
|
||||
/**
|
||||
* 错误代码枚举
|
||||
*/
|
||||
export enum ErrorCode {
|
||||
// 通用错误 1xxx
|
||||
UNKNOWN = 1000,
|
||||
INVALID_ARGUMENT = 1001,
|
||||
NOT_FOUND = 1002,
|
||||
ALREADY_EXISTS = 1003,
|
||||
PERMISSION_DENIED = 1004,
|
||||
|
||||
// 网络错误 2xxx
|
||||
NETWORK_ERROR = 2000,
|
||||
TIMEOUT = 2001,
|
||||
CONNECTION_FAILED = 2002,
|
||||
|
||||
// API 错误 3xxx
|
||||
API_ERROR = 3000,
|
||||
API_UNAUTHORIZED = 3001,
|
||||
API_RATE_LIMIT = 3002,
|
||||
API_INVALID_RESPONSE = 3003,
|
||||
|
||||
// 服务错误 4xxx
|
||||
SERVICE_ERROR = 4000,
|
||||
MODEL_NOT_AVAILABLE = 4001,
|
||||
STREAMING_ERROR = 4002,
|
||||
TOOL_EXECUTION_ERROR = 4003,
|
||||
|
||||
// 存储错误 5xxx
|
||||
STORAGE_ERROR = 5000,
|
||||
STORAGE_QUOTA_EXCEEDED = 5001,
|
||||
STORAGE_READ_ERROR = 5002,
|
||||
STORAGE_WRITE_ERROR = 5003
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用错误基类
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
public readonly code: ErrorCode
|
||||
public readonly timestamp: Date
|
||||
public readonly context?: Record<string, any>
|
||||
public readonly isOperational: boolean
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.UNKNOWN,
|
||||
context?: Record<string, any>,
|
||||
isOperational = true
|
||||
) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
this.code = code
|
||||
this.timestamp = new Date()
|
||||
this.context = context
|
||||
this.isOperational = isOperational
|
||||
|
||||
// 捕获堆栈跟踪
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
|
||||
// 记录错误日志
|
||||
this.logError()
|
||||
}
|
||||
|
||||
private logError(): void {
|
||||
logger.error(
|
||||
`${this.name}: ${this.message}`,
|
||||
'AppError',
|
||||
this,
|
||||
{
|
||||
code: this.code,
|
||||
context: this.context
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
timestamp: this.timestamp,
|
||||
context: this.context,
|
||||
stack: this.stack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证错误
|
||||
*/
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, context?: Record<string, any>) {
|
||||
super(message, ErrorCode.INVALID_ARGUMENT, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络错误
|
||||
*/
|
||||
export class NetworkError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.NETWORK_ERROR,
|
||||
context?: Record<string, any>
|
||||
) {
|
||||
super(message, code, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 错误
|
||||
*/
|
||||
export class APIError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.API_ERROR,
|
||||
context?: Record<string, any>
|
||||
) {
|
||||
super(message, code, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务错误
|
||||
*/
|
||||
export class ServiceError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.SERVICE_ERROR,
|
||||
context?: Record<string, any>
|
||||
) {
|
||||
super(message, code, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储错误
|
||||
*/
|
||||
export class StorageError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
code: ErrorCode = ErrorCode.STORAGE_ERROR,
|
||||
context?: Record<string, any>
|
||||
) {
|
||||
super(message, code, context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理器
|
||||
*/
|
||||
export class ErrorHandler {
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
static handle(error: Error | AppError): void {
|
||||
if (error instanceof AppError) {
|
||||
// 应用错误
|
||||
if (error.isOperational) {
|
||||
// 可操作错误,显示给用户
|
||||
this.showErrorMessage(error.message)
|
||||
} else {
|
||||
// 程序错误,记录日志
|
||||
logger.error('Programmer error:', 'ErrorHandler', error)
|
||||
}
|
||||
} else {
|
||||
// 未知错误
|
||||
logger.error('Unknown error:', 'ErrorHandler', error)
|
||||
this.showErrorMessage('发生了未知错误,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误消息(可以集成到 UI 组件)
|
||||
*/
|
||||
private static showErrorMessage(message: string): void {
|
||||
// TODO: 集成到 Naive UI 的 Message 组件
|
||||
console.error('User Error:', message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装异步函数,自动处理错误
|
||||
*/
|
||||
static async wrap<T>(
|
||||
fn: () => Promise<T>,
|
||||
errorMessage?: string
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (errorMessage) {
|
||||
logger.error(errorMessage, 'ErrorHandler', error)
|
||||
}
|
||||
this.handle(error as Error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
16
web/src/utils/index.ts
Normal file
16
web/src/utils/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Utils Index
|
||||
* 导出所有工具类
|
||||
*/
|
||||
|
||||
export { logger, LogLevel, type LoggerConfig } from './logger'
|
||||
export {
|
||||
AppError,
|
||||
ValidationError,
|
||||
NetworkError,
|
||||
APIError,
|
||||
ServiceError,
|
||||
StorageError,
|
||||
ErrorHandler,
|
||||
ErrorCode
|
||||
} from './error'
|
||||
138
web/src/utils/logger.ts
Normal file
138
web/src/utils/logger.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Logger - 统一日志系统
|
||||
*
|
||||
* 特性:
|
||||
* - 支持多种日志级别(debug, info, warn, error)
|
||||
* - 可配置日志级别
|
||||
* - 格式化输出
|
||||
* - 支持日志分类(namespace)
|
||||
*/
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
NONE = 4
|
||||
}
|
||||
|
||||
export interface LoggerConfig {
|
||||
level: LogLevel
|
||||
enableTimestamp: boolean
|
||||
enableNamespace: boolean
|
||||
}
|
||||
|
||||
class Logger {
|
||||
private static instance: Logger
|
||||
private config: LoggerConfig = {
|
||||
level: LogLevel.INFO,
|
||||
enableTimestamp: true,
|
||||
enableNamespace: true
|
||||
}
|
||||
|
||||
static getInstance(): Logger {
|
||||
if (!Logger.instance) {
|
||||
Logger.instance = new Logger()
|
||||
}
|
||||
return Logger.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置日志系统
|
||||
*/
|
||||
configure(config: Partial<LoggerConfig>): void {
|
||||
this.config = { ...this.config, ...config }
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置日志级别
|
||||
*/
|
||||
setLevel(level: LogLevel): void {
|
||||
this.config.level = level
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志前缀
|
||||
*/
|
||||
private formatPrefix(level: string, namespace?: string): string {
|
||||
const parts: string[] = []
|
||||
|
||||
if (this.config.enableTimestamp) {
|
||||
const now = new Date()
|
||||
const time = now.toLocaleTimeString('zh-CN', { hour12: false })
|
||||
const ms = now.getMilliseconds().toString().padStart(3, '0')
|
||||
parts.push(`[${time}.${ms}]`)
|
||||
}
|
||||
|
||||
parts.push(`[${level}]`)
|
||||
|
||||
if (this.config.enableNamespace && namespace) {
|
||||
parts.push(`[${namespace}]`)
|
||||
}
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* DEBUG 级别日志
|
||||
*/
|
||||
debug(message: string, namespace?: string, ...args: any[]): void {
|
||||
if (this.config.level <= LogLevel.DEBUG) {
|
||||
const prefix = this.formatPrefix('DEBUG', namespace)
|
||||
console.debug(prefix, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INFO 级别日志
|
||||
*/
|
||||
info(message: string, namespace?: string, ...args: any[]): void {
|
||||
if (this.config.level <= LogLevel.INFO) {
|
||||
const prefix = this.formatPrefix('INFO', namespace)
|
||||
console.info(prefix, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WARN 级别日志
|
||||
*/
|
||||
warn(message: string, namespace?: string, ...args: any[]): void {
|
||||
if (this.config.level <= LogLevel.WARN) {
|
||||
const prefix = this.formatPrefix('WARN', namespace)
|
||||
console.warn(prefix, message, ...args)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ERROR 级别日志
|
||||
*/
|
||||
error(message: string, namespace?: string, error?: Error | unknown, ...args: any[]): void {
|
||||
if (this.config.level <= LogLevel.ERROR) {
|
||||
const prefix = this.formatPrefix('ERROR', namespace)
|
||||
if (error instanceof Error) {
|
||||
console.error(prefix, message, '\n', error.message, '\n', error.stack, ...args)
|
||||
} else {
|
||||
console.error(prefix, message, error, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带命名空间的 Logger 实例
|
||||
*/
|
||||
namespace(namespace: string) {
|
||||
return {
|
||||
debug: (message: string, ...args: any[]) => this.debug(message, namespace, ...args),
|
||||
info: (message: string, ...args: any[]) => this.info(message, namespace, ...args),
|
||||
warn: (message: string, ...args: any[]) => this.warn(message, namespace, ...args),
|
||||
error: (message: string, error?: Error | unknown, ...args: any[]) =>
|
||||
this.error(message, namespace, error, ...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const logger = Logger.getInstance()
|
||||
|
||||
// 导出便捷方法
|
||||
export default logger
|
||||
Reference in New Issue
Block a user