| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- 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<ChatMessageModel[]>;
- focusInput?: () => void;
- setInputValue?: (value: string) => void;
- scrollToBottom?: () => void;
- stopMessageEditing?: () => void;
- uploadAttachment?: () => void;
- getAttachmentList?: () => Promise<ChatAttachmentItem[]>;
- };
- 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<void>;
- /**
- * 程序追加系统提示词
- * @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<number, { id?: string; name?: string; arguments: string }>();
- 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<typeof useChat>;
|