From 69cd29803c123a032ac971fe11ab7ab72e5d94f0 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Fri, 19 Dec 2025 04:04:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(AgentChat):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E8=BE=93=E5=85=A5=E5=8C=BA=E5=9F=9F=E5=92=8C=E6=B5=81=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/assets/css/animations.less | 85 +++ web/src/components/AgentChatComponent.vue | 597 +++--------------- web/src/components/AgentInputArea.vue | 184 ++++++ .../components/AttachmentOptionsComponent.vue | 2 +- web/src/composables/useAgentStreamHandler.js | 175 +++++ 5 files changed, 541 insertions(+), 502 deletions(-) create mode 100644 web/src/assets/css/animations.less create mode 100644 web/src/components/AgentInputArea.vue create mode 100644 web/src/composables/useAgentStreamHandler.js diff --git a/web/src/assets/css/animations.less b/web/src/assets/css/animations.less new file mode 100644 index 00000000..c1f6d8ca --- /dev/null +++ b/web/src/assets/css/animations.less @@ -0,0 +1,85 @@ +/* 动画样式集合 */ + +@keyframes dotPulse { + 0%, 80%, 100% { + transform: scale(0.8); + opacity: 0.5; + } + 40% { + transform: scale(1.1); + opacity: 1; + } +} + +@keyframes shimmer { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} + +@keyframes swing-in-top-fwd { + 0% { + transform: rotateX(-100deg); + transform-origin: top; + opacity: 0; + } + 100% { + transform: rotateX(0deg); + transform-origin: top; + opacity: 1; + } +} + +@keyframes slideInUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue index 88e8ad4e..2f8c4ac0 100644 --- a/web/src/components/AgentChatComponent.vue +++ b/web/src/components/AgentChatComponent.vue @@ -68,12 +68,6 @@ 状态 - - @@ -87,61 +81,6 @@

您好,我是{{ currentAgentName }}!

- - -
- - - - - - - -
-
或试试这些问题:
-
-
- {{ question.text }} -
-
-
-
@@ -175,7 +114,7 @@
-
+
-
- + - - - - -
+ /> + + +
+
+
+ {{ question.text }} +
+
+
+ +

请注意辨别内容的可靠性

@@ -234,11 +166,8 @@ diff --git a/web/src/components/AttachmentOptionsComponent.vue b/web/src/components/AttachmentOptionsComponent.vue index 3706f152..3cf100a4 100644 --- a/web/src/components/AttachmentOptionsComponent.vue +++ b/web/src/components/AttachmentOptionsComponent.vue @@ -118,7 +118,7 @@ const processImageUpload = async (file) => { width: result.width, height: result.height, format: result.format, - mimeType: result.mime_type, + mimeType: result.mime_type || file.type, sizeBytes: result.size_bytes, originalName: file.name }); diff --git a/web/src/composables/useAgentStreamHandler.js b/web/src/composables/useAgentStreamHandler.js new file mode 100644 index 00000000..4a78378e --- /dev/null +++ b/web/src/composables/useAgentStreamHandler.js @@ -0,0 +1,175 @@ +import { message } from 'ant-design-vue'; +import { handleChatError } from '@/utils/errorHandler'; +import { unref } from 'vue'; + +/** + * Process a streaming response from the server + * @param {Response} response - The fetch response object + * @param {Function} onChunk - Callback function for each parsed JSON chunk. Return true to stop processing. + */ +const processStreamResponse = async (response, onChunk) => { + if (!response || !response.body) { + console.warn('Invalid response or missing body for stream processing'); + return; + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let stopProcessing = false; + + try { + while (!stopProcessing) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine) { + try { + const chunk = JSON.parse(trimmedLine); + if (onChunk && onChunk(chunk)) { + stopProcessing = true; + break; + } + } catch (e) { + console.warn('Failed to parse stream chunk JSON:', e, 'Line:', trimmedLine); + } + } + } + } + + if (!stopProcessing && buffer.trim()) { + try { + const chunk = JSON.parse(buffer.trim()); + if (onChunk) { + onChunk(chunk); + } + } catch (e) { + console.warn('Failed to parse final stream chunk JSON:', e); + } + } + } finally { + try { + reader.releaseLock(); + } catch (e) { + // Ignore errors on releasing lock + } + } +}; + +export function useAgentStreamHandler({ + getThreadState, + processApprovalInStream, + currentAgentId, + supportsTodo, + supportsFiles +}) { + + /** + * Process a single stream chunk based on its status + * @param {Object} chunk - The parsed JSON chunk + * @param {String} threadId - The current thread ID + * @returns {Boolean} - Returns true if processing should stop (e.g. error, finished, interrupted) + */ + const handleStreamChunk = (chunk, threadId) => { + const { status, msg, request_id, message: chunkMessage } = chunk; + const threadState = getThreadState(threadId); + + if (!threadState) return false; + + switch (status) { + case 'init': + threadState.onGoingConv.msgChunks[request_id] = [msg]; + return false; + + case 'loading': + if (msg.id) { + if (!threadState.onGoingConv.msgChunks[msg.id]) { + threadState.onGoingConv.msgChunks[msg.id] = []; + } + threadState.onGoingConv.msgChunks[msg.id].push(msg); + } + return false; + + case 'error': + handleChatError({ message: chunkMessage }, 'stream'); + // Stop the loading indicator + if (threadState) { + threadState.isStreaming = false; + + // Abort the stream controller to stop processing further events + if (threadState.streamAbortController) { + threadState.streamAbortController.abort(); + threadState.streamAbortController = null; + } + } + return true; + + case 'human_approval_required': + // 使用审批 composable 处理审批请求 + return processApprovalInStream(chunk, threadId, unref(currentAgentId)); + + case 'agent_state': + if ((unref(supportsTodo) || unref(supportsFiles)) && chunk.agent_state) { + console.log('[AgentState]', { + threadId, + todos: chunk.agent_state?.todos || [], + files: chunk.agent_state?.files || [] + }); + threadState.agentState = chunk.agent_state; + } + return false; + + case 'finished': + // 先标记流式结束,但保持消息显示直到历史记录加载完成 + if (threadState) { + threadState.isStreaming = false; + if ((unref(supportsTodo) || unref(supportsFiles)) && threadState.agentState) { + console.log(`[AgentState|Final] ${new Date().toLocaleTimeString()}.${new Date().getMilliseconds()}`, { + threadId, + todos: threadState.agentState?.todos || [], + files: threadState.agentState?.files || [] + }); + } + } + return true; + + case 'interrupted': + // 中断状态,刷新消息历史 + console.warn("[Interrupted] case"); + if (threadState) { + threadState.isStreaming = false; + } + // 如果有 message 字段,显示提示(例如:敏感内容检测) + if (chunkMessage) { + message.info(chunkMessage); + } + return true; + } + + return false; + }; + + /** + * Process the full agent stream response + * @param {Response} response - The fetch response + * @param {String} threadId - The thread ID + * @param {Function} [onChunk] - Optional callback for each chunk (e.g. for logging) + */ + const handleAgentResponse = async (response, threadId, onChunk = null) => { + await processStreamResponse(response, (chunk) => { + if (onChunk) onChunk(chunk); + return handleStreamChunk(chunk, threadId); + }); + }; + + return { + handleStreamChunk, + handleAgentResponse + }; +}