| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import { computed, ref } from 'vue';
- import dayjs from 'dayjs';
- import { message } from 'ant-design-vue';
- import { buildReportDownloadUrl, editSessionTitle, getHistoryList, pinSession } from '/@/views/ventAI/manageAssistent/api';
- import type { Message, Task } from '/@/views/ventAI/manageAssistent/types/index';
- import { normalizeLineBreaks } from '/@/views/ventAI/manageAssistent/lib/index';
- /**
- * 会话(任务)状态与列表:taskList / currentTaskId / 当前会话消息,以及列表拉取、置顶、标题、
- * 会话 id 迁移与历史消息转换。
- *
- * messages 是唯一数据源:直接读写 currentTask.messages,不再维护两份拷贝;
- * 切换任务、创建任务这类会牵动面板/额度/滚动的编排逻辑仍留在组件里,此处只提供状态与纯函数。
- */
- export const useChatTasks = () => {
- const taskList = ref<Task[]>([]);
- // 已从接口拉取过的会话 id,用于识别流式期间后端新建的会话
- const knownApiSessionIds = new Set<string>();
- // 从接口获取会话列表
- const fetchSessionList = async () => {
- try {
- const res = await getHistoryList({});
- const sessions = res?.sessions || res?.data || [];
- if (Array.isArray(sessions)) {
- const apiTasks: Task[] = sessions.map((item: { session_id: string; title: string; updated_at: string; sort_order?: number }) => ({
- id: item.session_id,
- name: item.title || '未命名会话',
- sessionId: item.session_id,
- messages: [],
- attachedFiles: [],
- updatedAt: item.updated_at,
- sortOrder: item.sort_order,
- }));
- // Build lookup maps for O(1) access
- const existingTaskMap = new Map(taskList.value.map((t) => [t.id, t]));
- const apiTaskIds = new Set(apiTasks.map((t) => t.id));
- // 流式中的空白任务:后端已为其创建会话,但前端要等流结束才拿到 session_id。
- // 该会话在接口里是一条全新记录,归并回空白任务而不是单独插入,避免列表出现重复条目
- let blankStreamingTask: Task | null = taskList.value.find((t) => !t.sessionId && t.isStreaming) || null;
- const mergedTasks: Task[] = [];
- for (const apiTask of apiTasks) {
- if (blankStreamingTask && (blankStreamingTask.pendingSessionId === apiTask.id || !knownApiSessionIds.has(apiTask.id))) {
- blankStreamingTask.pendingSessionId = apiTask.id;
- blankStreamingTask.name = apiTask.name;
- blankStreamingTask.updatedAt = apiTask.updatedAt;
- blankStreamingTask.sortOrder = apiTask.sortOrder;
- blankStreamingTask = null;
- continue;
- }
- const existing = existingTaskMap.get(apiTask.id);
- if (existing) {
- // 原地更新以保持对象引用,避免流式回调持有的任务引用失效(isStreaming 等状态丢失)
- existing.name = apiTask.name;
- existing.updatedAt = apiTask.updatedAt;
- existing.sortOrder = apiTask.sortOrder;
- mergedTasks.push(existing);
- } else {
- mergedTasks.push(apiTask);
- }
- }
- apiTasks.forEach((apiTask) => knownApiSessionIds.add(apiTask.id));
- const localOnlyTasks = taskList.value.filter((t) => !t.sessionId && !apiTaskIds.has(t.id));
- taskList.value = [...localOnlyTasks, ...mergedTasks];
- return mergedTasks;
- }
- } catch (error) {
- console.error('获取会话列表失败:', error);
- }
- return [];
- };
- const currentTaskId = ref('');
- const currentTask = computed(() => taskList.value.find((t) => t.id === currentTaskId.value));
- // 当前会话的消息。单一数据源:直接读写 currentTask.messages,不再维护
- // 「视图数组 + 任务数组」两份拷贝——历史实现需要在每次流式事件后手工同步两者,
- // 是任务切换/流式并存时视图错乱的根源。
- // fallbackMessages 仅用于「任务已全部删除」这类没有当前任务的瞬时状态,行为与旧 ref([]) 一致。
- const fallbackMessages: Message[] = [];
- const messages = computed<Message[]>({
- get: () => currentTask.value?.messages ?? fallbackMessages,
- set: (value: Message[]) => {
- const task = currentTask.value;
- if (task) task.messages = value;
- },
- });
- // Non-reactive lookup cache - Vue 3 does not track Set mutations on ref<Set>
- const historyLoadedTasks = new Set<string>();
- const setCurrentSessionId = (sessionId: string) => {
- const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
- if (currentTask && currentTask.id !== sessionId) {
- taskList.value = taskList.value.filter((t) => t.id !== sessionId);
- currentTask.id = sessionId;
- currentTask.sessionId = sessionId;
- currentTaskId.value = sessionId;
- historyLoadedTasks.add(sessionId);
- }
- };
- const handlePinTask = async (task: Task) => {
- if (!task.sessionId) {
- message.warning('请先发送消息后再置顶');
- return;
- }
- try {
- await pinSession(task.sessionId);
- await fetchSessionList();
- } catch {
- message.error('置顶操作失败');
- }
- };
- // 标题保存(由 ChatHeader 子组件触发,父组件负责 API 调用和状态更新)
- const handleSaveTitle = async (newTitle: string) => {
- if (!currentTask.value?.sessionId) return;
- try {
- await editSessionTitle(currentTask.value.sessionId, newTitle);
- currentTask.value.name = newTitle;
- message.success('标题修改成功');
- } catch {
- message.error('标题修改失败');
- }
- };
- const getDefaultMessages = (): Message[] => {
- return [];
- };
- const transformHistoryToMessages = (
- data: Array<{
- id?: number;
- role?: string;
- type?: string;
- content: string;
- created_at?: string;
- report_url?: string;
- report_filename?: string;
- citations?: any;
- }>
- ): Message[] => {
- const messages: Message[] = [];
- for (const item of data) {
- const normalizedContent = normalizeLineBreaks(item.content);
- const time = item.created_at ? dayjs(item.created_at).format('HH:mm') : '';
- const createdAt = item.created_at || '';
- // 新格式:role 字段 (user/assistant)
- if (item.role === 'user') {
- messages.push({ id: item.id, type: 'user', content: normalizedContent, time, createdAt });
- } else if (item.role === 'assistant') {
- const msg: Message = { id: item.id, type: 'ai', content: normalizedContent, time, createdAt };
- if (item.report_url && item.report_filename) {
- // 后端返回的报告地址前缀不稳定(绝对地址 / 缺 /ventAI 的相对路径),入前端状态前统一归一
- msg.downloadFiles = [{ url: buildReportDownloadUrl(item.report_url), filename: item.report_filename }];
- }
- if (item.citations) {
- // 历史接口返回 citations 为扁平数组,SSE 事件返回 { items, model_only },统一归一化
- const raw = item.citations;
- if (Array.isArray(raw)) {
- msg.citations = { items: raw, model_only: raw.length === 0 };
- } else {
- msg.citations = raw;
- }
- }
- messages.push(msg);
- }
- // 兼容旧格式:type 字段
- else if (item.type === 'user_message') {
- messages.push({ id: item.id, type: 'user', content: normalizedContent, time, createdAt });
- } else if (item.type === 'model_thinking') {
- const lastAiMsg = messages[messages.length - 1];
- if (lastAiMsg && lastAiMsg.type === 'ai') {
- lastAiMsg.content += normalizedContent;
- lastAiMsg.content = lastAiMsg.content.replace(/\|\|\|SPLIT_CONTENT\|\|\|/g, '');
- } else {
- messages.push({ type: 'ai', content: normalizedContent, time, createdAt });
- }
- }
- }
- return messages;
- };
- return {
- taskList,
- currentTaskId,
- currentTask,
- messages,
- historyLoadedTasks,
- getDefaultMessages,
- transformHistoryToMessages,
- fetchSessionList,
- handlePinTask,
- handleSaveTitle,
- setCurrentSessionId,
- };
- };
|