import AgentApi, { type AgentChatModel } from "@/api/agent/Agent"; import AgentWorkApi from "@/api/agent/AgentWorks"; import { nextTick, onBeforeUnmount, onMounted, ref, type Ref } from "vue"; import { ChatUtils } from "../utils/ChatUtils"; import { ChatMessage as ChatMessageModel } from "../model/Message"; import { useMessages, LocalMessageIdPool, mergeSystemMessages } from "./Messages"; import { useToolCalls } from "./ToolCall"; import { useChatStaticMessages } from "./StaticMessages"; import { useChatContext, type ContextMemoryConfig, type ContextMemorySetting } from "./Context"; import { useChatTools, type ChatToolsManager } from "./Tools"; import type { ChatSessionManager } from "../composables/useChatSession"; import type { SSE } from "../../../../api/agent/ssemp"; import type OpenAI from "openai"; import { requireNotNull } from "@imengyu/imengyu-utils"; export type ChatModelOptionValue = { temperature: number; top_p: number; top_k: number; presence_penalty: number; }; export type ChatAttachmentStatus = 'uploading' | 'success' | 'error'; export type ChatAttachmentType = 'image' | 'audio' | 'video' | 'text' | 'document' | 'unknown'; export interface ChatAttachmentItem { id?: number; localId: string; name: string; size: number; status: ChatAttachmentStatus; path?: string; url?: string; type: ChatAttachmentType; errorMessage?: string; file: File; } export type ChatInterfaceManager = { messages: Ref; focusInput?: () => void; setInputValue?: (value: string) => void; scrollToBottom?: () => void; stopMessageEditing?: () => void; uploadAttachment?: () => void; getAttachmentList?: () => Promise; }; export interface ChatConfig { /** * 默认系统提示词 * @default '' */ defaultSystemPrompt?: string; /** * 上下文记忆设置 * @default 'short' */ contextMemorySetting?: ContextMemorySetting; /** * 上下文记忆参数配置 */ contextMemoryConfig?: ContextMemoryConfig; /** * 构建欢迎消息 */ onBuildWelcome?: () => { /** * 欢迎消息 * @default '' */ welcomeMessage?: string; /** * 欢迎动作 * @default [] */ welcomeActions?: string[]; }; /** * 获取发送选项 * @returns */ onGetSendOptions: () => ChatSendOptions; /** * 新对话 * @param isNewChat - 是否是新对话 */ onNewChat?: (isNewChat: boolean) => void; /** * 消息合并前 * @param message - 消息 */ onBeforeMessageMerge?: (userMessages: ChatMessageModel[], currentUserMessageIds: number[]) => void; /** * 发送消息前 * @param message - 消息 */ onBeforeSend?: (userMessages: ChatMessageModel[], streamOptions: any) => Promise; /** * 程序追加系统提示词 * @returns */ onGetAppendSystemMessages?: () => string[]; /** * 程序追加消息,不保存到数据库 * @returns */ onGetAppendMessages?: () => ChatMessageModel[]; /** * 对话消息结束 * @param message - 消息 * @param currentUserMessageIds - 当前用户消息ID列表 */ onAiMessageFinish?: (message: ChatMessageModel, currentUserMessageIds: number[], finishReason: string) => void; /** * 是否在ai回答后自动生成可能的问题 * @default false */ autoGeneratePossibleQuestions?: boolean; /** * 初始化工具 * @param toolsManager - 工具管理器 * @returns */ onInitTools?: (toolsManager: ChatToolsManager) => void; } export type ChatSendOptions = { enableSearch: boolean; enableThinking: boolean; model: string; modelInfo: AgentChatModel ; customSystemPrompt?: string|null; chatOptions: ChatModelOptionValue; }; export function useChat(options: { config: ChatConfig; interfaceManager: ChatInterfaceManager; sessionManager: ChatSessionManager; }) { const isLoading = ref(false); const config = options.config; let streamingAiMessageId: number | null = null; let eventSource: SSE | null = null; let startTime = 0; const messages = options.interfaceManager.messages; const referenceMessage = ref(''); const messagesManager = useMessages(messages); const toolsManager = useChatTools(); options.config?.onInitTools?.(toolsManager); const staticMessagesManager = useChatStaticMessages(config); const sessionManager = options.sessionManager; const interfaceManager = options.interfaceManager; const contextManager = useChatContext({ message: messages, contextMemorySetting: config.contextMemorySetting, contextMemoryConfig: config.contextMemoryConfig, sessionManager: sessionManager, onGetAppendMessages: config.onGetAppendMessages, }); const toolCallsManager = useToolCalls(messagesManager, toolsManager, interfaceManager, sessionManager); //新建会话消息构建 sessionManager.events.on('session-newed', () => { messages.value = [staticMessagesManager.getWelcomeMessage()]; config.onNewChat?.(true); contextManager.estimateTokenUseage(config.onGetSendOptions()); }); //加载会话消息构建 sessionManager.events.on('session-loaded', (session) => { config.onNewChat?.(false); contextManager.estimateTokenUseage(config.onGetSendOptions()); }); async function buildStreamOptions( sendOptions: ChatSendOptions, currentUserMessageIds: number[], limitHistoryStartAtMessageId: number|undefined, openAiTools: OpenAI.Chat.Completions.ChatCompletionTool[], ) { const finalMessages: OpenAI.Chat.ChatCompletionMessageParam[] = []; /** * 构建系统消息 */ const systemMessages = [] as OpenAI.Chat.ChatCompletionSystemMessageParam[]; if (sendOptions?.customSystemPrompt) systemMessages.push({ role: 'system', content: sendOptions.customSystemPrompt.trim() }); else if (config.defaultSystemPrompt) systemMessages.push({ role: 'system', content: config.defaultSystemPrompt.trim() }); if (config.onGetAppendSystemMessages) config.onGetAppendSystemMessages() .map(s => ({ role: 'system', content: s.trim() })) .forEach(s => systemMessages.push(s as OpenAI.Chat.ChatCompletionSystemMessageParam)); /** * 构建用户消息 */ const { userMessages, systemMessages: contextSystemMessages } = await contextManager.convertMessagesToAi(sendOptions, currentUserMessageIds, limitHistoryStartAtMessageId); const referenceSystemMessages: OpenAI.Chat.ChatCompletionSystemMessageParam[] = []; if (referenceMessage.value) referenceSystemMessages.push({ role: 'system', content: `[用户引用消息] ${referenceMessage.value.trim()}` }); finalMessages.push( //合并系统消息 ...mergeSystemMessages([ ...systemMessages, ...referenceSystemMessages, ...(contextSystemMessages) as OpenAI.Chat.ChatCompletionSystemMessageParam[], ]), ...userMessages, ); if (finalMessages.length === 0) throw new Error('消息不能为空'); /** * 构建额外选项 */ const extraOptions = { enable_thinking: sendOptions.enableThinking ?? undefined, enable_search: sendOptions.enableSearch ?? undefined, }; const chartOptions: OpenAI.Chat.Completions.ChatCompletionCreateParams = { stream: true, model: sendOptions.model, ...sendOptions.chatOptions || {}, messages: finalMessages, tools: openAiTools.length ? openAiTools : undefined, tool_choice: openAiTools.length ? 'auto' : undefined, }; return { ...chartOptions, ...extraOptions, }; } /** * 对话逻辑 */ async function startStreamForUserMessages( currentUserMessageIds: number[], aiMessageId?: number, limitHistoryStartAtMessageId?: number ) { const sendOptions = config.onGetSendOptions(); if (!sendOptions.model) throw new Error('模型不能为空'); let aiMessage: ChatMessageModel; if (!aiMessageId) { aiMessageId = LocalMessageIdPool.getNextId(); aiMessage = messagesManager.addMessage(ChatMessageModel.createAssistant("", aiMessageId, "loading")); } else { aiMessage = requireNotNull(messagesManager.findMessage(aiMessageId)); } aiMessage.parentId = currentUserMessageIds[0]; aiMessage.state = "loading"; aiMessage.name = sendOptions.modelInfo.name ?? ''; //持久化消息 if (!aiMessage.isPersisted) await sessionManager.persistMessages([aiMessage]); await nextTick(); await interfaceManager.scrollToBottom?.(); streamingAiMessageId = aiMessage.id; startTime = Date.now(); const toolCallBuffers = new Map(); let streamOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming; try { config.onBeforeMessageMerge?.(messages.value.filter(m => currentUserMessageIds.includes(m.id)), currentUserMessageIds); // 用于拼接 tool_calls 的 arguments(流式会分片) streamOptions = await buildStreamOptions( sendOptions, currentUserMessageIds, limitHistoryStartAtMessageId, toolsManager.openAiTools.value ); await config.onBeforeSend?.(messages.value.filter(m => currentUserMessageIds.includes(m.id)), streamOptions); } catch (error) { console.error("构建流式选项失败:", error); failedAndSetInfo("处理消息失败", error); return; } eventSource = AgentApi.chatStream(streamOptions as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming); eventSource.onmessage = async (event) => { try { const data = JSON.parse(event.data); // 后端通过 SSE 下发错误(含超时等) if (data?.error) { eventSource?.close(); isLoading.value = false; streamingAiMessageId = null; const errorType = String(data.errorType || ''); const errorText = typeof data.error === 'string' ? data.error : '请求失败'; const title = errorType === 'timeout_connect' || errorType === 'timeout_idle' ? '请求超时' : '请求错误'; aiMessage.content = errorType.startsWith('timeout') ? '请求超时,请检查模型地址/网络后重试。' : '失败,请稍后重试。'; aiMessage.setError(title, errorText); await sessionManager.persistMessages([aiMessage]); return; } if (!data.chunk) return; const chunk = data.chunk as OpenAI.Chat.ChatCompletionChunk; const choice = chunk.choices[0]; if (!choice) return; // 结束处理 if (choice.finish_reason) { //结束状态 isLoading.value = false; streamingAiMessageId = null; const isAiMessageEmpty = aiMessage.content === '' && aiMessage.reasoningContent === ''; const finishReason = choice.finish_reason; aiMessage.state = "success"; aiMessage.replyTime = Date.now() - startTime; switch (finishReason) { case "tool_calls": { // 如果内容与思考内容都为空,则说明ai进行了工具调用 if (isAiMessageEmpty) { aiMessage.content = '准备执行工具调用...'; } break; } } //持久化消息 sessionManager.persistAssistantAfterStream(aiMessage, currentUserMessageIds); config.onAiMessageFinish?.(aiMessage, currentUserMessageIds, finishReason); contextManager.estimateTokenUseage(sendOptions); switch (finishReason) { case "stop": { //如果需要自动生成可能的问题,则生成可能的问题 if (config.autoGeneratePossibleQuestions && !isAiMessageEmpty) { const userMessage = messages.value.find(m => currentUserMessageIds.includes(m.id))?.content; const possibleQuestions = await AgentWorkApi.autoGeneratePossibleQuestions(userMessage ?? '', aiMessage.content); aiMessage.actions = possibleQuestions; } break; } case "tool_calls": { //工具调用处理 const toolCalls: OpenAI.Chat.ChatCompletionMessageToolCall[] = [...toolCallBuffers.entries()] .sort((a, b) => a[0] - b[0]) .map(([, b]) => ({ id: b.id || `call_${Math.random().toString(16).slice(2)}`, type: "function", function: { name: b.name || "unknown", arguments: b.arguments || "", }, })); // 把 tool_calls 挂在 assistant 消息上,便于下一轮历史回放(如果后端严格校验) aiMessage.toolCalls = toolCalls; // 执行工具 -> 追加 tool 消息 -> 再发起一轮 stream 获取最终回答 await toolCallsManager.executeToolCalls(toolCalls, aiMessage.parentId); // 继续一轮对话(同一批 user messages) isLoading.value = true; await startStreamForUserMessages(currentUserMessageIds); return; } } } // 结束后关闭 if (data.finished) { closeStream(); return; } // tool_calls 拼接 const delta: any = choice.delta as any; if (Array.isArray(delta?.tool_calls)) { for (const tc of delta.tool_calls as any[]) { const index = tc.index ?? 0; const buf = toolCallBuffers.get(index) ?? { arguments: "" }; if (tc.id) buf.id = tc.id; if (tc.function?.name) buf.name = tc.function.name; if (typeof tc.function?.arguments === "string") buf.arguments += tc.function.arguments; toolCallBuffers.set(index, buf); } } // 内容/思考内容 if (choice?.delta?.content) { aiMessage.content += choice.delta.content; //如果思考内容不为空,则计算推理时间 if (aiMessage.reasoningContent && aiMessage.reasoningTime === 0) aiMessage.reasoningTime = Date.now() - startTime; } else if (delta?.reasoning_content) { aiMessage.reasoningContent += delta.reasoning_content; } } catch (error) { // 解析失败不应打断整个对话 console.error("解析SSE数据失败:", error); } }; eventSource.onerror = (error) => { console.error("SSE连接错误:", error); eventSource?.close(); isLoading.value = false; streamingAiMessageId = null; if (('' +error).includes('402')) { aiMessage.content = "您今日的对话次数已用完,请明日再来吧。"; aiMessage.state = "error"; } else { aiMessage.content = "失败,请稍后重试。"; aiMessage.setError("请求错误", ChatUtils.formatError(error)); } sessionManager.persistMessages([aiMessage]); }; eventSource.stream(); } /** * 发送消息 * @param inputMessage - 输入消息 */ async function send(inputMessage: string) { if (isLoading.value) return; const userMessage = inputMessage.trim(); if (!userMessage) { messagesManager.addMessage(staticMessagesManager.getEmptyMessage()); return; } interfaceManager.stopMessageEditing?.(); const newMessages = [ messagesManager.addMessage(ChatMessageModel.createUser(userMessage, LocalMessageIdPool.getNextId())) ]; const attachmentItems = await interfaceManager.getAttachmentList?.() || []; for (const item of attachmentItems) { const m = await ChatMessageModel.createAttachment(item, LocalMessageIdPool.getNextId()); m.name = '你'; newMessages.push(messagesManager.addMessage(m)); } // 保存用户消息与会话 try { await sessionManager.saveUserMessageAndPersistSession(newMessages); } catch (error) { newMessages.forEach((m) => { m.setError("保存会话失败,请稍后重试。", ChatUtils.formatError(error)); }); return; } await nextTick(); try { isLoading.value = true; await startStreamForUserMessages(newMessages.map((m) => m.id)); } catch (error) { console.error("失败:", error); isLoading.value = false; streamingAiMessageId = null; const message = messages.value[messages.value.length - 1]; if (message && !message.isUser) { message.content = "失败,请稍后重试。"; message.setError("请求错误", ChatUtils.formatError(error)); } } } /** * 停止对话 */ function stop() { failedAndSetInfo("手动停止对话", null); closeStream(); isLoading.value = false; } function failedAndSetInfo(message: string, error: any) { isLoading.value = false; if (streamingAiMessageId) { const aiMessage = messagesManager.findMessage(streamingAiMessageId); if (aiMessage) { aiMessage.content = message; aiMessage.setError(message, ChatUtils.formatError(error)); sessionManager.persistMessages([aiMessage]); } } streamingAiMessageId = null; } function closeStream() { if (eventSource) { eventSource.close(); eventSource = null; } } function prefindUserMessageIds(messageId: number) { let limitHistoryStartAtMessageId: number | undefined; const currentUserMessageIds = [] as number[]; for (let i = messages.value.findIndex(m => m.id === messageId); i >= 0; i--) { const message = messages.value[i]; if (message.isUser) currentUserMessageIds.push(message.id); } return { currentUserMessageIds, limitHistoryStartAtMessageId, }; } /** * 用户编辑消息,重新发送 * @param messageId */ async function editMessage(messageId: number, newContent: string) { const message = messagesManager.findMessage(messageId); if (!message) return; message.content = newContent; if (message.isPersisted) await sessionManager.persistMessages([message]); //向上查找,找到本轮次所有用户消息ID const { currentUserMessageIds, limitHistoryStartAtMessageId } = prefindUserMessageIds(messageId); if (message.replyItemId) { //如果已有回复,则清空回复内容 const replyMessage = messagesManager.findMessage(message.replyItemId); if (replyMessage) { replyMessage.content = ''; replyMessage.reasoningContent = ''; replyMessage.resetError(); } await startStreamForUserMessages(currentUserMessageIds, message.replyItemId, limitHistoryStartAtMessageId); } else await startStreamForUserMessages(currentUserMessageIds, undefined, limitHistoryStartAtMessageId); } /** * 重新生成AI消息 * @param messageId */ async function regenerateMessage(messageId: number) { const message = messagesManager.findMessage(messageId); if (!message) return; if (!message.isAssistant) return; message.content = ''; message.reasoningContent = ''; message.resetError(); //向上查找,找到本轮次所有用户消息ID const { currentUserMessageIds, limitHistoryStartAtMessageId } = prefindUserMessageIds(messageId); await startStreamForUserMessages(currentUserMessageIds, message.id, limitHistoryStartAtMessageId); } /** * 设置参考消息并等待用户输入 * @param message - 参考消息 */ async function setReferenceAndWaitUser(message: string) { referenceMessage.value = message; interfaceManager.focusInput?.(); } /** * 清空参考消息 */ function clearReferenceMessage() { referenceMessage.value = ''; } onMounted(async () => { if (sessionManager.enableSession) { try { await sessionManager.loadSessions(); sessionManager.onSelectNew(); } catch (error) { console.error("加载会话失败:", error); sessionManager.onSelectLocal(); } } else { sessionManager.onSelectLocal(); } }); onBeforeUnmount(() => { stop(); }); return { send, stop, editMessage, regenerateMessage, setReferenceAndWaitUser, clearReferenceMessage, config, isLoading, messages, referenceMessage, staticMessagesManager, toolsManager, messagesManager, sessionManager, interfaceManager, toolCallsManager, contextManager, } } export type ChatManager = ReturnType;