Chat.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. import AgentApi, { type AgentChatModel } from "@/api/agent/Agent";
  2. import AgentWorkApi from "@/api/agent/AgentWorks";
  3. import { nextTick, onBeforeUnmount, onMounted, ref, type Ref } from "vue";
  4. import { ChatUtils } from "../utils/ChatUtils";
  5. import { ChatMessage as ChatMessageModel } from "../model/Message";
  6. import { useMessages, LocalMessageIdPool, mergeSystemMessages } from "./Messages";
  7. import { useToolCalls } from "./ToolCall";
  8. import { useChatStaticMessages } from "./StaticMessages";
  9. import { useChatContext, type ContextMemoryConfig, type ContextMemorySetting } from "./Context";
  10. import { useChatTools, type ChatToolsManager } from "./Tools";
  11. import type { ChatSessionManager } from "../composables/useChatSession";
  12. import type { SSE } from "../../../../api/agent/ssemp";
  13. import type OpenAI from "openai";
  14. import { requireNotNull } from "@imengyu/imengyu-utils";
  15. export type ChatModelOptionValue = {
  16. temperature: number;
  17. top_p: number;
  18. top_k: number;
  19. presence_penalty: number;
  20. };
  21. export type ChatAttachmentStatus = 'uploading' | 'success' | 'error';
  22. export type ChatAttachmentType = 'image' | 'audio' | 'video' | 'text' | 'document' | 'unknown';
  23. export interface ChatAttachmentItem {
  24. id?: number;
  25. localId: string;
  26. name: string;
  27. size: number;
  28. status: ChatAttachmentStatus;
  29. path?: string;
  30. url?: string;
  31. type: ChatAttachmentType;
  32. errorMessage?: string;
  33. file: File;
  34. }
  35. export type ChatInterfaceManager = {
  36. messages: Ref<ChatMessageModel[]>;
  37. focusInput?: () => void;
  38. setInputValue?: (value: string) => void;
  39. scrollToBottom?: () => void;
  40. stopMessageEditing?: () => void;
  41. uploadAttachment?: () => void;
  42. getAttachmentList?: () => Promise<ChatAttachmentItem[]>;
  43. };
  44. export interface ChatConfig {
  45. /**
  46. * 默认系统提示词
  47. * @default ''
  48. */
  49. defaultSystemPrompt?: string;
  50. /**
  51. * 上下文记忆设置
  52. * @default 'short'
  53. */
  54. contextMemorySetting?: ContextMemorySetting;
  55. /**
  56. * 上下文记忆参数配置
  57. */
  58. contextMemoryConfig?: ContextMemoryConfig;
  59. /**
  60. * 构建欢迎消息
  61. */
  62. onBuildWelcome?: () => {
  63. /**
  64. * 欢迎消息
  65. * @default ''
  66. */
  67. welcomeMessage?: string;
  68. /**
  69. * 欢迎动作
  70. * @default []
  71. */
  72. welcomeActions?: string[];
  73. };
  74. /**
  75. * 获取发送选项
  76. * @returns
  77. */
  78. onGetSendOptions: () => ChatSendOptions;
  79. /**
  80. * 新对话
  81. * @param isNewChat - 是否是新对话
  82. */
  83. onNewChat?: (isNewChat: boolean) => void;
  84. /**
  85. * 消息合并前
  86. * @param message - 消息
  87. */
  88. onBeforeMessageMerge?: (userMessages: ChatMessageModel[], currentUserMessageIds: number[]) => void;
  89. /**
  90. * 发送消息前
  91. * @param message - 消息
  92. */
  93. onBeforeSend?: (userMessages: ChatMessageModel[], streamOptions: any) => Promise<void>;
  94. /**
  95. * 程序追加系统提示词
  96. * @returns
  97. */
  98. onGetAppendSystemMessages?: () => string[];
  99. /**
  100. * 程序追加消息,不保存到数据库
  101. * @returns
  102. */
  103. onGetAppendMessages?: () => ChatMessageModel[];
  104. /**
  105. * 对话消息结束
  106. * @param message - 消息
  107. * @param currentUserMessageIds - 当前用户消息ID列表
  108. */
  109. onAiMessageFinish?: (message: ChatMessageModel, currentUserMessageIds: number[], finishReason: string) => void;
  110. /**
  111. * 是否在ai回答后自动生成可能的问题
  112. * @default false
  113. */
  114. autoGeneratePossibleQuestions?: boolean;
  115. /**
  116. * 初始化工具
  117. * @param toolsManager - 工具管理器
  118. * @returns
  119. */
  120. onInitTools?: (toolsManager: ChatToolsManager) => void;
  121. }
  122. export type ChatSendOptions = {
  123. enableSearch: boolean;
  124. enableThinking: boolean;
  125. model: string;
  126. modelInfo: AgentChatModel ;
  127. customSystemPrompt?: string|null;
  128. chatOptions: ChatModelOptionValue;
  129. };
  130. export function useChat(options: {
  131. config: ChatConfig;
  132. interfaceManager: ChatInterfaceManager;
  133. sessionManager: ChatSessionManager;
  134. }) {
  135. const isLoading = ref(false);
  136. const config = options.config;
  137. let streamingAiMessageId: number | null = null;
  138. let eventSource: SSE | null = null;
  139. let startTime = 0;
  140. const messages = options.interfaceManager.messages;
  141. const referenceMessage = ref('');
  142. const messagesManager = useMessages(messages);
  143. const toolsManager = useChatTools(); options.config?.onInitTools?.(toolsManager);
  144. const staticMessagesManager = useChatStaticMessages(config);
  145. const sessionManager = options.sessionManager;
  146. const interfaceManager = options.interfaceManager;
  147. const contextManager = useChatContext({
  148. message: messages,
  149. contextMemorySetting: config.contextMemorySetting,
  150. contextMemoryConfig: config.contextMemoryConfig,
  151. sessionManager: sessionManager,
  152. onGetAppendMessages: config.onGetAppendMessages,
  153. });
  154. const toolCallsManager = useToolCalls(messagesManager, toolsManager, interfaceManager, sessionManager);
  155. //新建会话消息构建
  156. sessionManager.events.on('session-newed', () => {
  157. messages.value = [staticMessagesManager.getWelcomeMessage()];
  158. config.onNewChat?.(true);
  159. contextManager.estimateTokenUseage(config.onGetSendOptions());
  160. });
  161. //加载会话消息构建
  162. sessionManager.events.on('session-loaded', (session) => {
  163. config.onNewChat?.(false);
  164. contextManager.estimateTokenUseage(config.onGetSendOptions());
  165. });
  166. async function buildStreamOptions(
  167. sendOptions: ChatSendOptions,
  168. currentUserMessageIds: number[],
  169. limitHistoryStartAtMessageId: number|undefined,
  170. openAiTools: OpenAI.Chat.Completions.ChatCompletionTool[],
  171. ) {
  172. const finalMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [];
  173. /**
  174. * 构建系统消息
  175. */
  176. const systemMessages = [] as OpenAI.Chat.ChatCompletionSystemMessageParam[];
  177. if (sendOptions?.customSystemPrompt)
  178. systemMessages.push({ role: 'system', content: sendOptions.customSystemPrompt.trim() });
  179. else if (config.defaultSystemPrompt)
  180. systemMessages.push({ role: 'system', content: config.defaultSystemPrompt.trim() });
  181. if (config.onGetAppendSystemMessages)
  182. config.onGetAppendSystemMessages()
  183. .map(s => ({ role: 'system', content: s.trim() }))
  184. .forEach(s => systemMessages.push(s as OpenAI.Chat.ChatCompletionSystemMessageParam));
  185. /**
  186. * 构建用户消息
  187. */
  188. const {
  189. userMessages,
  190. systemMessages: contextSystemMessages
  191. } = await contextManager.convertMessagesToAi(sendOptions, currentUserMessageIds, limitHistoryStartAtMessageId);
  192. const referenceSystemMessages: OpenAI.Chat.ChatCompletionSystemMessageParam[] = [];
  193. if (referenceMessage.value)
  194. referenceSystemMessages.push({ role: 'system', content: `[用户引用消息] ${referenceMessage.value.trim()}` });
  195. finalMessages.push(
  196. //合并系统消息
  197. ...mergeSystemMessages([
  198. ...systemMessages,
  199. ...referenceSystemMessages,
  200. ...(contextSystemMessages) as OpenAI.Chat.ChatCompletionSystemMessageParam[],
  201. ]),
  202. ...userMessages,
  203. );
  204. if (finalMessages.length === 0)
  205. throw new Error('消息不能为空');
  206. /**
  207. * 构建额外选项
  208. */
  209. const extraOptions = {
  210. enable_thinking: sendOptions.enableThinking ?? undefined,
  211. enable_search: sendOptions.enableSearch ?? undefined,
  212. };
  213. const chartOptions: OpenAI.Chat.Completions.ChatCompletionCreateParams = {
  214. stream: true,
  215. model: sendOptions.model,
  216. ...sendOptions.chatOptions || {},
  217. messages: finalMessages,
  218. tools: openAiTools.length ? openAiTools : undefined,
  219. tool_choice: openAiTools.length ? 'auto' : undefined,
  220. };
  221. return {
  222. ...chartOptions,
  223. ...extraOptions,
  224. };
  225. }
  226. /**
  227. * 对话逻辑
  228. */
  229. async function startStreamForUserMessages(
  230. currentUserMessageIds: number[],
  231. aiMessageId?: number,
  232. limitHistoryStartAtMessageId?: number
  233. ) {
  234. const sendOptions = config.onGetSendOptions();
  235. if (!sendOptions.model)
  236. throw new Error('模型不能为空');
  237. let aiMessage: ChatMessageModel;
  238. if (!aiMessageId) {
  239. aiMessageId = LocalMessageIdPool.getNextId();
  240. aiMessage = messagesManager.addMessage(ChatMessageModel.createAssistant("", aiMessageId, "loading"));
  241. } else {
  242. aiMessage = requireNotNull(messagesManager.findMessage(aiMessageId));
  243. }
  244. aiMessage.parentId = currentUserMessageIds[0];
  245. aiMessage.state = "loading";
  246. aiMessage.name = sendOptions.modelInfo.name ?? '';
  247. //持久化消息
  248. if (!aiMessage.isPersisted)
  249. await sessionManager.persistMessages([aiMessage]);
  250. await nextTick();
  251. await interfaceManager.scrollToBottom?.();
  252. streamingAiMessageId = aiMessage.id;
  253. startTime = Date.now();
  254. const toolCallBuffers = new Map<number, { id?: string; name?: string; arguments: string }>();
  255. let streamOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming;
  256. try {
  257. config.onBeforeMessageMerge?.(messages.value.filter(m => currentUserMessageIds.includes(m.id)), currentUserMessageIds);
  258. // 用于拼接 tool_calls 的 arguments(流式会分片)
  259. streamOptions = await buildStreamOptions(
  260. sendOptions,
  261. currentUserMessageIds,
  262. limitHistoryStartAtMessageId,
  263. toolsManager.openAiTools.value
  264. );
  265. await config.onBeforeSend?.(messages.value.filter(m => currentUserMessageIds.includes(m.id)), streamOptions);
  266. } catch (error) {
  267. console.error("构建流式选项失败:", error);
  268. failedAndSetInfo("处理消息失败", error);
  269. return;
  270. }
  271. eventSource = AgentApi.chatStream(streamOptions as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming);
  272. eventSource.onmessage = async (event) => {
  273. try {
  274. const data = JSON.parse(event.data);
  275. // 后端通过 SSE 下发错误(含超时等)
  276. if (data?.error) {
  277. eventSource?.close();
  278. isLoading.value = false;
  279. streamingAiMessageId = null;
  280. const errorType = String(data.errorType || '');
  281. const errorText = typeof data.error === 'string' ? data.error : '请求失败';
  282. const title =
  283. errorType === 'timeout_connect' || errorType === 'timeout_idle'
  284. ? '请求超时'
  285. : '请求错误';
  286. aiMessage.content = errorType.startsWith('timeout')
  287. ? '请求超时,请检查模型地址/网络后重试。'
  288. : '失败,请稍后重试。';
  289. aiMessage.setError(title, errorText);
  290. await sessionManager.persistMessages([aiMessage]);
  291. return;
  292. }
  293. if (!data.chunk) return;
  294. const chunk = data.chunk as OpenAI.Chat.ChatCompletionChunk;
  295. const choice = chunk.choices[0];
  296. if (!choice) return;
  297. // 结束处理
  298. if (choice.finish_reason) {
  299. //结束状态
  300. isLoading.value = false;
  301. streamingAiMessageId = null;
  302. const isAiMessageEmpty = aiMessage.content === '' && aiMessage.reasoningContent === '';
  303. const finishReason = choice.finish_reason;
  304. aiMessage.state = "success";
  305. aiMessage.replyTime = Date.now() - startTime;
  306. switch (finishReason) {
  307. case "tool_calls": {
  308. // 如果内容与思考内容都为空,则说明ai进行了工具调用
  309. if (isAiMessageEmpty) {
  310. aiMessage.content = '准备执行工具调用...';
  311. }
  312. break;
  313. }
  314. }
  315. //持久化消息
  316. sessionManager.persistAssistantAfterStream(aiMessage, currentUserMessageIds);
  317. config.onAiMessageFinish?.(aiMessage, currentUserMessageIds, finishReason);
  318. contextManager.estimateTokenUseage(sendOptions);
  319. switch (finishReason) {
  320. case "stop": {
  321. //如果需要自动生成可能的问题,则生成可能的问题
  322. if (config.autoGeneratePossibleQuestions && !isAiMessageEmpty) {
  323. const userMessage = messages.value.find(m => currentUserMessageIds.includes(m.id))?.content;
  324. const possibleQuestions = await AgentWorkApi.autoGeneratePossibleQuestions(userMessage ?? '', aiMessage.content);
  325. aiMessage.actions = possibleQuestions;
  326. }
  327. break;
  328. }
  329. case "tool_calls": {
  330. //工具调用处理
  331. const toolCalls: OpenAI.Chat.ChatCompletionMessageToolCall[] = [...toolCallBuffers.entries()]
  332. .sort((a, b) => a[0] - b[0])
  333. .map(([, b]) => ({
  334. id: b.id || `call_${Math.random().toString(16).slice(2)}`,
  335. type: "function",
  336. function: {
  337. name: b.name || "unknown",
  338. arguments: b.arguments || "",
  339. },
  340. }));
  341. // 把 tool_calls 挂在 assistant 消息上,便于下一轮历史回放(如果后端严格校验)
  342. aiMessage.toolCalls = toolCalls;
  343. // 执行工具 -> 追加 tool 消息 -> 再发起一轮 stream 获取最终回答
  344. await toolCallsManager.executeToolCalls(toolCalls, aiMessage.parentId);
  345. // 继续一轮对话(同一批 user messages)
  346. isLoading.value = true;
  347. await startStreamForUserMessages(currentUserMessageIds);
  348. return;
  349. }
  350. }
  351. }
  352. // 结束后关闭
  353. if (data.finished) {
  354. closeStream();
  355. return;
  356. }
  357. // tool_calls 拼接
  358. const delta: any = choice.delta as any;
  359. if (Array.isArray(delta?.tool_calls)) {
  360. for (const tc of delta.tool_calls as any[]) {
  361. const index = tc.index ?? 0;
  362. const buf = toolCallBuffers.get(index) ?? { arguments: "" };
  363. if (tc.id) buf.id = tc.id;
  364. if (tc.function?.name) buf.name = tc.function.name;
  365. if (typeof tc.function?.arguments === "string") buf.arguments += tc.function.arguments;
  366. toolCallBuffers.set(index, buf);
  367. }
  368. }
  369. // 内容/思考内容
  370. if (choice?.delta?.content) {
  371. aiMessage.content += choice.delta.content;
  372. //如果思考内容不为空,则计算推理时间
  373. if (aiMessage.reasoningContent && aiMessage.reasoningTime === 0)
  374. aiMessage.reasoningTime = Date.now() - startTime;
  375. } else if (delta?.reasoning_content) {
  376. aiMessage.reasoningContent += delta.reasoning_content;
  377. }
  378. } catch (error) {
  379. // 解析失败不应打断整个对话
  380. console.error("解析SSE数据失败:", error);
  381. }
  382. };
  383. eventSource.onerror = (error) => {
  384. console.error("SSE连接错误:", error);
  385. eventSource?.close();
  386. isLoading.value = false;
  387. streamingAiMessageId = null;
  388. if (('' +error).includes('402')) {
  389. aiMessage.content = "您今日的对话次数已用完,请明日再来吧。";
  390. aiMessage.state = "error";
  391. } else {
  392. aiMessage.content = "失败,请稍后重试。";
  393. aiMessage.setError("请求错误", ChatUtils.formatError(error));
  394. }
  395. sessionManager.persistMessages([aiMessage]);
  396. };
  397. eventSource.stream();
  398. }
  399. /**
  400. * 发送消息
  401. * @param inputMessage - 输入消息
  402. */
  403. async function send(inputMessage: string) {
  404. if (isLoading.value) return;
  405. const userMessage = inputMessage.trim();
  406. if (!userMessage) {
  407. messagesManager.addMessage(staticMessagesManager.getEmptyMessage());
  408. return;
  409. }
  410. interfaceManager.stopMessageEditing?.();
  411. const newMessages = [
  412. messagesManager.addMessage(ChatMessageModel.createUser(userMessage, LocalMessageIdPool.getNextId()))
  413. ];
  414. const attachmentItems = await interfaceManager.getAttachmentList?.() || [];
  415. for (const item of attachmentItems) {
  416. const m = await ChatMessageModel.createAttachment(item, LocalMessageIdPool.getNextId());
  417. m.name = '你';
  418. newMessages.push(messagesManager.addMessage(m));
  419. }
  420. // 保存用户消息与会话
  421. try {
  422. await sessionManager.saveUserMessageAndPersistSession(newMessages);
  423. } catch (error) {
  424. newMessages.forEach((m) => {
  425. m.setError("保存会话失败,请稍后重试。", ChatUtils.formatError(error));
  426. });
  427. return;
  428. }
  429. await nextTick();
  430. try {
  431. isLoading.value = true;
  432. await startStreamForUserMessages(newMessages.map((m) => m.id));
  433. } catch (error) {
  434. console.error("失败:", error);
  435. isLoading.value = false;
  436. streamingAiMessageId = null;
  437. const message = messages.value[messages.value.length - 1];
  438. if (message && !message.isUser) {
  439. message.content = "失败,请稍后重试。";
  440. message.setError("请求错误", ChatUtils.formatError(error));
  441. }
  442. }
  443. }
  444. /**
  445. * 停止对话
  446. */
  447. function stop() {
  448. failedAndSetInfo("手动停止对话", null);
  449. closeStream();
  450. isLoading.value = false;
  451. }
  452. function failedAndSetInfo(message: string, error: any) {
  453. isLoading.value = false;
  454. if (streamingAiMessageId) {
  455. const aiMessage = messagesManager.findMessage(streamingAiMessageId);
  456. if (aiMessage) {
  457. aiMessage.content = message;
  458. aiMessage.setError(message, ChatUtils.formatError(error));
  459. sessionManager.persistMessages([aiMessage]);
  460. }
  461. }
  462. streamingAiMessageId = null;
  463. }
  464. function closeStream() {
  465. if (eventSource) {
  466. eventSource.close();
  467. eventSource = null;
  468. }
  469. }
  470. function prefindUserMessageIds(messageId: number) {
  471. let limitHistoryStartAtMessageId: number | undefined;
  472. const currentUserMessageIds = [] as number[];
  473. for (let i = messages.value.findIndex(m => m.id === messageId); i >= 0; i--) {
  474. const message = messages.value[i];
  475. if (message.isUser)
  476. currentUserMessageIds.push(message.id);
  477. }
  478. return {
  479. currentUserMessageIds,
  480. limitHistoryStartAtMessageId,
  481. };
  482. }
  483. /**
  484. * 用户编辑消息,重新发送
  485. * @param messageId
  486. */
  487. async function editMessage(messageId: number, newContent: string) {
  488. const message = messagesManager.findMessage(messageId);
  489. if (!message)
  490. return;
  491. message.content = newContent;
  492. if (message.isPersisted)
  493. await sessionManager.persistMessages([message]);
  494. //向上查找,找到本轮次所有用户消息ID
  495. const { currentUserMessageIds, limitHistoryStartAtMessageId } = prefindUserMessageIds(messageId);
  496. if (message.replyItemId) {
  497. //如果已有回复,则清空回复内容
  498. const replyMessage = messagesManager.findMessage(message.replyItemId);
  499. if (replyMessage) {
  500. replyMessage.content = '';
  501. replyMessage.reasoningContent = '';
  502. replyMessage.resetError();
  503. }
  504. await startStreamForUserMessages(currentUserMessageIds, message.replyItemId, limitHistoryStartAtMessageId);
  505. }
  506. else
  507. await startStreamForUserMessages(currentUserMessageIds, undefined, limitHistoryStartAtMessageId);
  508. }
  509. /**
  510. * 重新生成AI消息
  511. * @param messageId
  512. */
  513. async function regenerateMessage(messageId: number) {
  514. const message = messagesManager.findMessage(messageId);
  515. if (!message)
  516. return;
  517. if (!message.isAssistant)
  518. return;
  519. message.content = '';
  520. message.reasoningContent = '';
  521. message.resetError();
  522. //向上查找,找到本轮次所有用户消息ID
  523. const { currentUserMessageIds, limitHistoryStartAtMessageId } = prefindUserMessageIds(messageId);
  524. await startStreamForUserMessages(currentUserMessageIds, message.id, limitHistoryStartAtMessageId);
  525. }
  526. /**
  527. * 设置参考消息并等待用户输入
  528. * @param message - 参考消息
  529. */
  530. async function setReferenceAndWaitUser(message: string) {
  531. referenceMessage.value = message;
  532. interfaceManager.focusInput?.();
  533. }
  534. /**
  535. * 清空参考消息
  536. */
  537. function clearReferenceMessage() {
  538. referenceMessage.value = '';
  539. }
  540. onMounted(async () => {
  541. if (sessionManager.enableSession) {
  542. try {
  543. await sessionManager.loadSessions();
  544. sessionManager.onSelectNew();
  545. } catch (error) {
  546. console.error("加载会话失败:", error);
  547. sessionManager.onSelectLocal();
  548. }
  549. } else {
  550. sessionManager.onSelectLocal();
  551. }
  552. });
  553. onBeforeUnmount(() => {
  554. stop();
  555. });
  556. return {
  557. send,
  558. stop,
  559. editMessage,
  560. regenerateMessage,
  561. setReferenceAndWaitUser,
  562. clearReferenceMessage,
  563. config,
  564. isLoading,
  565. messages,
  566. referenceMessage,
  567. staticMessagesManager,
  568. toolsManager,
  569. messagesManager,
  570. sessionManager,
  571. interfaceManager,
  572. toolCallsManager,
  573. contextManager,
  574. }
  575. }
  576. export type ChatManager = ReturnType<typeof useChat>;