Эх сурвалжийг харах

[Bugfix 0000]修复智能助手在对话过程中切换任务时显示错误的问题

wangkeyi 13 цаг өмнө
parent
commit
de84dc2694

+ 6 - 0
src/assets/images/ventAI/chatModalBlue/4-7.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="31.185" height="34.438" viewBox="0 0 31.185 34.438">
+  <g id="待审批" transform="translate(-256.546 -576.183)">
+    <path id="路径_58195" data-name="路径 58195" d="M61.9,14.775a4.894,4.894,0,0,1,4.883,4.594l.008.3v6.291a1.334,1.334,0,0,1-2.655.178l-.013-.178V19.666a2.223,2.223,0,0,0-1.995-2.21l-.229-.013H44.242a2.223,2.223,0,0,0-2.21,2l-.013.229V44.322a2.222,2.222,0,0,0,2,2.21l.229.013h5.729a1.334,1.334,0,0,1,.183,2.655l-.183.013H44.242a4.892,4.892,0,0,1-4.881-4.594l-.01-.3V19.666a4.892,4.892,0,0,1,4.594-4.881l.3-.01H61.9Zm-9.373,13.5a1.334,1.334,0,0,1,.18,2.655l-.178.013H46.877a1.334,1.334,0,0,1-.183-2.655l.183-.013h5.648Zm6.194-5.559a1.334,1.334,0,0,1,.178,2.655l-.178.013H46.877a1.334,1.334,0,0,1-.183-2.655l.183-.013H58.722Z" transform="translate(217.195 561.408)" fill="#fff"/>
+    <path id="路径_58196" data-name="路径 58196" d="M143.718,138.188h-4.075l-.688-2.063a4.366,4.366,0,1,0-4.446,0l-.688,2.063h-4.075A1.626,1.626,0,0,0,128,139.643v4.366a1.626,1.626,0,0,0,1.746,1.455h13.972a1.626,1.626,0,0,0,1.746-1.455v-4.366A1.626,1.626,0,0,0,143.718,138.188Z" transform="translate(142.267 465.156)" fill="#fff"/>
+  </g>
+</svg>

+ 1 - 0
src/design/themify/default.less

@@ -107,6 +107,7 @@ html {
   --img-chat-close-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-4.svg');
   --img-chat-download-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-5.svg');
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
+  --img-chat-session-approve-icon: url('/src/assets/images/ventAI/chatModalBlue/4-7.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
   --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
   --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');

+ 211 - 80
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -73,6 +73,15 @@
           @reject="handleReject"
         />
 
+        <!-- 下载确认条 -->
+        <div v-if="downloadConfirm" class="download-confirm-bar">
+          <span class="download-confirm-text">是否下载 {{ downloadConfirm.filename }} 到本地?</span>
+          <div class="download-confirm-actions">
+            <button class="download-confirm-btn download-btn" @click="doDownload">下载</button>
+            <button class="download-confirm-btn cancel-btn" @click="downloadConfirm = null">取消</button>
+          </div>
+        </div>
+
         <!-- 输入区域 -->
         <ChatInputArea
           ref="chatInputRef"
@@ -200,6 +209,7 @@
   const editMode = ref('full');
   const pendingApprovalSessionId = ref('');
   const pendingApprovalThreadId = ref('');
+  const downloadConfirm = ref<{ url: string; filename: string } | null>(null);
   const contextBreakdown = ref({ messages: 0, mcp: 0, skills: 0, system_prompt: 0, other: 0 });
 
   const panelCollapsed = ref(false);
@@ -309,10 +319,6 @@
     const newTask = taskList.value.find((t) => t.id === taskId);
     if (!newTask) return;
 
-    if (newTask.sessionId) {
-      taskList.value = taskList.value.filter((t) => t.sessionId);
-    }
-
     inputMessage.value = '';
     pendingFile.value = null;
     previewFile.value = null;
@@ -374,6 +380,10 @@
   };
 
   const handlePinTask = async (task: Task) => {
+    if (!task.sessionId) {
+      message.warning('请先发送消息后再置顶');
+      return;
+    }
     try {
       await pinSession(task.sessionId);
       await fetchSessionList();
@@ -395,11 +405,11 @@
   };
 
   const handleEditModeChange = async (modeKey: string) => {
+    editMode.value = modeKey;
     const sessionId = getCurrentSessionId();
     if (!sessionId) return;
     try {
       await putSessionMode(sessionId, modeKey);
-      editMode.value = modeKey;
     } catch {
       message.error('切换模式失败');
     }
@@ -409,8 +419,12 @@
     const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
     if (!sessionId) return;
 
-    // 清除当前消息的 pendingApproval 状态,获取中断前累计时长和思考内容
-    const lastMsg = messages.value[messages.value.length - 1];
+    const approveTask = taskList.value.find((t) => t.sessionId === sessionId);
+    if (!approveTask) return;
+
+    // 清除中断消息的 pendingApproval 状态,获取中断前累计时长和思考内容
+    const taskMessages = approveTask.messages;
+    const lastMsg = taskMessages[taskMessages.length - 1];
     const prevElapsedMs = lastMsg?.pendingApprovalData?.elapsedMs || 0;
     const prevThinkingContent = lastMsg?.pendingApprovalData?.prevThinkingContent || lastMsg?.thinkingContent || '';
     if (lastMsg && lastMsg.isPendingApproval) {
@@ -418,7 +432,7 @@
     }
 
     const now = dayjs();
-    const aiMsgIndex = messages.value.length;
+    const aiMsgIndex = taskMessages.length;
     const aiMsg: Message = {
       type: 'ai',
       content: '',
@@ -430,10 +444,15 @@
       generateStartTime: Date.now(),
       baseDurationMs: prevElapsedMs,
     };
-    messages.value.push(aiMsg);
-    await scrollToBottom();
+    taskMessages.push(aiMsg);
+    approveTask.messages = [...taskMessages];
+    if (currentTaskId.value === approveTask.id) {
+      messages.value = approveTask.messages;
+      await scrollToBottom();
+    }
 
     loading.value = true;
+    approveTask.isStreaming = true;
 
     try {
       const result = await chatResumeStream(
@@ -443,8 +462,8 @@
           action: 'approve',
         },
         (chunk: string) => {
-          parseSSEData(chunk, aiMsgIndex);
-          scrollToBottom();
+          parseSSEData(chunk, aiMsgIndex, approveTask.id);
+          if (currentTaskId.value === approveTask.id) scrollToBottom();
         }
       );
 
@@ -454,16 +473,18 @@
     } catch (error) {
       console.error('审批继续执行失败:', error);
       message.error('审批失败,请重试');
-      const errorMsg = messages.value[aiMsgIndex];
-      if (errorMsg && errorMsg.isLoading) {
-        errorMsg.isLoading = false;
-        errorMsg.content = '抱歉,审批请求失败,请稍后重试。';
+      if (aiMsg.isLoading) {
+        aiMsg.isLoading = false;
+        aiMsg.content = '抱歉,审批请求失败,请稍后重试。';
       }
     } finally {
       loading.value = false;
+      approveTask.isStreaming = false;
       pendingApprovalSessionId.value = '';
       pendingApprovalThreadId.value = '';
-      await scrollToBottom();
+      if (currentTaskId.value === approveTask.id) {
+        await scrollToBottom();
+      }
     }
   };
 
@@ -471,8 +492,12 @@
     const sessionId = pendingApprovalSessionId.value || getCurrentSessionId();
     if (!sessionId) return;
 
-    // 清除当前消息的 pendingApproval 状态
-    const lastMsg = messages.value[messages.value.length - 1];
+    const rejectTask = taskList.value.find((t) => t.sessionId === sessionId);
+    if (!rejectTask) return;
+
+    // 清除中断消息的 pendingApproval 状态
+    const taskMessages = rejectTask.messages;
+    const lastMsg = taskMessages[taskMessages.length - 1];
     if (lastMsg && lastMsg.isPendingApproval) {
       lastMsg.isPendingApproval = false;
     }
@@ -497,17 +522,16 @@
       time: now.format('HH:mm'),
       createdAt: now.toISOString(),
     };
-    messages.value.push(rejectMsg);
+    taskMessages.push(rejectMsg);
+    rejectTask.messages = [...taskMessages];
 
     pendingApprovalSessionId.value = '';
     pendingApprovalThreadId.value = '';
 
-    const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
-    if (currentTask) {
-      currentTask.messages = [...messages.value];
+    if (currentTaskId.value === rejectTask.id) {
+      messages.value = rejectTask.messages;
+      await scrollToBottom();
     }
-
-    await scrollToBottom();
   };
 
   const handleDeleteTask = (task: Task, _index: number) => {
@@ -681,6 +705,17 @@
     document.body.removeChild(a);
   };
 
+  const doDownload = () => {
+    if (!downloadConfirm.value) return;
+    const a = document.createElement('a');
+    a.href = downloadConfirm.value.url;
+    a.download = downloadConfirm.value.filename;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    downloadConfirm.value = null;
+  };
+
   const downloadWordFile = (url: string) => {
     if (!url) return;
     const a = document.createElement('a');
@@ -820,8 +855,17 @@
     }
   };
 
-  const handleSSEMessage = (data: any, aiMsgIndex: number) => {
-    const aiMsg = messages.value[aiMsgIndex];
+  const handleSSEMessage = (data: any, aiMsgIndex: number, taskId: string) => {
+    const streamingTask = taskList.value.find((t) => t.id === taskId);
+    if (!streamingTask) return;
+
+    // 当前正在查看流式任务时,新消息被 push 到 messages.value 而非 task.messages
+    // 所以需要从 messages.value 读取;否则从任务存储的 messages 读取
+    const isViewingTask = currentTaskId.value === taskId;
+    const sourceArr = isViewingTask ? messages.value : streamingTask.messages;
+    const aiMsg = sourceArr[aiMsgIndex];
+    if (!aiMsg) return;
+
     const normalizedContent = normalizeLineBreaks(data.content || '');
 
     // 初始化 thinkingSteps 数组
@@ -911,6 +955,9 @@
           message: data.message || '执行完成',
           timestamp: Date.now(),
         });
+        if (data.download_url) {
+          downloadConfirm.value = { url: data.download_url, filename: data.filename || '文件' };
+        }
         break;
       }
 
@@ -964,8 +1011,7 @@
       case 'word_download':
         if (normalizedContent) {
           aiMsg.wordDownloadUrl = normalizedContent;
-          const wordTask = taskList.value.find((t) => t.id === currentTaskId.value);
-          if (wordTask) wordTask.wordUrl = normalizedContent;
+          streamingTask.wordUrl = normalizedContent;
         }
         break;
 
@@ -991,27 +1037,30 @@
         const elapsedMs = aiMsg.baseDurationMs || (aiMsg.generateStartTime ? Date.now() - aiMsg.generateStartTime : 0);
         aiMsg.isLoading = false;
         aiMsg.isPendingApproval = true;
+        const interruptSessionId = data.session_id || streamingTask.sessionId;
         aiMsg.pendingApprovalData = {
           message: data.message || '智能体制定了执行计划,等待您的审批...',
-          sessionId: data.session_id || getCurrentSessionId(),
+          sessionId: interruptSessionId,
           threadId: data.thread_id,
           interrupts: data.interrupts,
           elapsedMs,
           prevThinkingContent: aiMsg.thinkingContent,
         };
-        pendingApprovalSessionId.value = data.session_id || getCurrentSessionId();
+        pendingApprovalSessionId.value = interruptSessionId;
         pendingApprovalThreadId.value = data.thread_id || '';
         break;
       }
     }
 
-    const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
-    if (currentTask) {
-      currentTask.messages = [...messages.value];
+    // 保存到流式任务的 messages,并同步 UI
+    if (isViewingTask) {
+      streamingTask.messages = [...messages.value];
+    } else {
+      streamingTask.messages = [...streamingTask.messages];
     }
   };
 
-  const parseSSEData = (chunk: string, aiMsgIndex: number) => {
+  const parseSSEData = (chunk: string, aiMsgIndex: number, taskId: string) => {
     const lines = chunk.split('\n');
 
     for (const lineRaw of lines) {
@@ -1022,7 +1071,7 @@
         try {
           const jsonStr = line.substring(5).trim();
           const data = JSON.parse(jsonStr);
-          handleSSEMessage(data, aiMsgIndex);
+          handleSSEMessage(data, aiMsgIndex, taskId);
         } catch (error) {
           console.error('解析 SSE data 行失败:', error, line);
         }
@@ -1031,17 +1080,19 @@
 
       try {
         const data = JSON.parse(line);
-        handleSSEMessage(data, aiMsgIndex);
+        handleSSEMessage(data, aiMsgIndex, taskId);
         continue;
       } catch {
         // not json
       }
 
-      handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex);
+      handleSSEMessage({ type: 'model_thinking', content: line }, aiMsgIndex, taskId);
     }
   };
 
   const sendTextOnly = async (userInput: string) => {
+    const originTaskId = currentTaskId.value;
+
     const now = dayjs();
     const userMsg: Message = {
       type: 'user',
@@ -1065,37 +1116,46 @@
     messages.value.push(aiMsg);
     await scrollToBottom();
 
-    const result = await unifiedStream(
-      {
-        message: userInput,
-        session_id: getCurrentSessionId() || undefined,
-        mode: editMode.value,
-      },
-      (chunk: string) => {
-        parseSSEData(chunk, aiMsgIndex);
-        scrollToBottom();
-      }
-    );
-
-    if (result.session_id) {
-      const taskBeforeUpdate = taskList.value.find((t) => t.id === currentTaskId.value);
-      const isNewTask = taskBeforeUpdate && !taskBeforeUpdate.sessionId;
+    const originTask = taskList.value.find((t) => t.id === originTaskId);
+    if (originTask) originTask.isStreaming = true;
 
-      setCurrentSessionId(result.session_id);
+    try {
+      const result = await unifiedStream(
+        {
+          message: userInput,
+          session_id: getCurrentSessionId() || undefined,
+          mode: editMode.value,
+        },
+        (chunk: string) => {
+          parseSSEData(chunk, aiMsgIndex, originTaskId);
+          if (currentTaskId.value === originTaskId) scrollToBottom();
+        }
+      );
 
-      if (isNewTask) {
-        taskBeforeUpdate.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
+      if (result.session_id) {
+        const originTask = taskList.value.find((t) => t.id === originTaskId);
+        if (originTask && !originTask.sessionId) {
+          originTask.sessionId = result.session_id;
+          originTask.name = userInput.substring(0, 20) + (userInput.length > 20 ? '...' : '');
+          if (originTask.id !== result.session_id) {
+            taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
+            originTask.id = result.session_id;
+            historyLoadedTasks.value.add(result.session_id);
+          }
+          if (currentTaskId.value === originTaskId) {
+            currentTaskId.value = result.session_id;
+          }
+        }
       }
-    }
-
-    const currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
-    if (currentTask) {
-      currentTask.messages = [...messages.value];
+    } finally {
+      const originTask = taskList.value.find((t) => t.id === originTaskId);
+      if (originTask) originTask.isStreaming = false;
     }
   };
 
   const sendWithAttachment = async (file: AttachedFile, userInput: string) => {
-    let currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
+    const originTaskId = currentTaskId.value;
+    let currentTask = taskList.value.find((t) => t.id === originTaskId);
     if (currentTask) {
       currentTask.attachedFiles.push(file);
     } else {
@@ -1130,26 +1190,42 @@
       throw new Error('缺少原始文件对象');
     }
 
-    const result = await reviewPdfStream(
-      {
-        file: file.originalFile!,
-        session_id: getCurrentSessionId() || undefined,
-        message: userInput || undefined,
-        mode: editMode.value,
-      },
-      (chunk: string) => {
-        parseSSEData(chunk, aiMsgIndex);
-        scrollToBottom();
-      }
-    );
+    const originTask = taskList.value.find((t) => t.id === originTaskId);
+    if (originTask) originTask.isStreaming = true;
 
-    if (result.session_id) {
-      setCurrentSessionId(result.session_id);
-    }
+    try {
+      const result = await reviewPdfStream(
+        {
+          file: file.originalFile!,
+          session_id: getCurrentSessionId() || undefined,
+          message: userInput || undefined,
+          mode: editMode.value,
+        },
+        (chunk: string) => {
+          parseSSEData(chunk, aiMsgIndex, originTaskId);
+          if (currentTaskId.value === originTaskId) scrollToBottom();
+        }
+      );
 
-    currentTask = taskList.value.find((t) => t.id === currentTaskId.value);
-    if (currentTask) {
-      currentTask.messages = [...messages.value];
+      if (result.session_id) {
+        const originTask = taskList.value.find((t) => t.id === originTaskId);
+        if (originTask && !originTask.sessionId) {
+          const taskName = userInput || `文件:${file.name}`;
+          originTask.sessionId = result.session_id;
+          originTask.name = taskName.substring(0, 20) + (taskName.length > 20 ? '...' : '');
+          if (originTask.id !== result.session_id) {
+            taskList.value = taskList.value.filter((t) => t.id !== result.session_id);
+            originTask.id = result.session_id;
+            historyLoadedTasks.value.add(result.session_id);
+          }
+          if (currentTaskId.value === originTaskId) {
+            currentTaskId.value = result.session_id;
+          }
+        }
+      }
+    } finally {
+      const originTask = taskList.value.find((t) => t.id === originTaskId);
+      if (originTask) originTask.isStreaming = false;
     }
   };
 
@@ -1395,6 +1471,61 @@
     overflow: hidden;
     min-width: 0;
 
+    .download-confirm-bar {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 10px 16px;
+      margin: 0 16px;
+      background: #0a2a3f;
+      border: 1px solid #1a4a6f;
+      border-radius: 8px;
+      flex-shrink: 0;
+
+      .download-confirm-text {
+        font-size: 13px;
+        color: #e0e6ed;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+        margin-right: 12px;
+      }
+
+      .download-confirm-actions {
+        display: flex;
+        gap: 8px;
+        flex-shrink: 0;
+      }
+
+      .download-confirm-btn {
+        padding: 4px 14px;
+        border-radius: 4px;
+        font-size: 12px;
+        cursor: pointer;
+        border: none;
+        transition: all 0.2s;
+
+        &.download-btn {
+          background: #0a84ff;
+          color: #fff;
+
+          &:hover {
+            background: #3a9bfd;
+          }
+        }
+
+        &.cancel-btn {
+          background: rgba(255, 255, 255, 0.08);
+          color: #e0e6ed;
+          border: 1px solid rgba(63, 80, 106, 0.5);
+
+          &:hover {
+            background: rgba(255, 255, 255, 0.15);
+          }
+        }
+      }
+    }
+
     :deep(.chat-header) {
       display: flex;
       padding: 12px 20px;

+ 3 - 3
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -930,14 +930,14 @@
           gap: 12px;
           padding: 14px 16px;
           margin-top: 12px;
-          background: rgba(10, 132, 255, 0.08);
-          border: 1px solid rgba(10, 132, 255, 0.25);
+          background: #002f4a;
+          border: 1px solid #005482;
           border-radius: 8px;
 
           .approval-icon {
             width: 36px;
             height: 36px;
-            background-image: var(--img-chat-popup-command-icon);
+            background-image: var(--img-chat-session-approve-icon);
             background-repeat: no-repeat;
             background-size: 100% 100%;
             flex-shrink: 0;

+ 22 - 3
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -31,7 +31,8 @@
         <div class="task-pin-icon pinned" @click.stop="togglePin(task)" title="取消置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
-          <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
+          <span v-if="task.isStreaming" class="task-loading"></span>
+          <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
           <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
@@ -47,7 +48,8 @@
         <div class="task-pin-icon" @click.stop="togglePin(task)" title="置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
-          <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
+          <span v-if="task.isStreaming" class="task-loading"></span>
+          <span v-else class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
           <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
@@ -258,6 +260,16 @@
           white-space: nowrap;
         }
 
+        .task-loading {
+          width: 14px;
+          height: 14px;
+          flex-shrink: 0;
+          border: 2px solid rgba(10, 132, 255, 0.2);
+          border-top-color: rgba(10, 132, 255, 0.8);
+          border-radius: 50%;
+          animation: task-spin 0.8s linear infinite;
+        }
+
         .task-delete {
           flex-shrink: 0;
           width: 28px;
@@ -275,7 +287,8 @@
           }
         }
 
-        &:hover .task-time {
+        &:hover .task-time,
+        &:hover .task-loading {
           display: none;
         }
 
@@ -292,6 +305,12 @@
       }
     }
 
+    @keyframes task-spin {
+      to {
+        transform: rotate(360deg);
+      }
+    }
+
     .options-area {
       display: flex;
       flex-direction: column;

+ 1 - 0
src/views/ventAI/manageAssistent/components/chatModal/types.ts

@@ -68,6 +68,7 @@ export interface Task {
   wordUrl?: string;
   updatedAt?: string;
   sortOrder?: number;
+  isStreaming?: boolean;
 }
 
 export type SectionType = 'select' | 'params' | 'result';