-
-
+
-
-
-
-
-
-
-
-
-
-
-
+ />
+
+
+
+
+
+ {{ 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
+ };
+}