From c3e7e8ef42019cac3264ea251239db29b4e724e0 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Thu, 20 Nov 2025 16:25:35 +0800 Subject: [PATCH 1/6] fix: stop task not working as expected --- src/components/ChatBox/index.tsx | 22 +++--- src/store/chatStore.ts | 112 +++++++++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 41c66f352..1343e2fff 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -416,26 +416,30 @@ export default function ChatBox(): JSX.Element { const handleSkip = async () => { const taskId = chatStore.activeTaskId as string; setIsPauseResumeLoading(true); - + try { - // Skip the current task + // First, stop the SSE connection and update local state + chatStore.stopTask(taskId); + + // Then notify backend to skip the task await fetchPost(`/chat/${projectStore.activeProjectId}/skip-task`, { project_id: projectStore.activeProjectId }); - // Update task status to finished - chatStore.setStatus(taskId, 'finished'); + // Ensure pending state is cleared chatStore.setIsPending(taskId, false); - - // toast.success("Task skipped successfully", { - // closeButton: true, - // }); + toast.success("Task stopped successfully", { closeButton: true, }); } catch (error) { console.error("Failed to skip task:", error); - toast.error("Failed to skip task", { + + // If backend call failed, just ensure pending state is cleared + // Don't call stopTask again since it was already called above + chatStore.setIsPending(taskId, false); + + toast.error("Task stopped locally, but backend notification failed", { closeButton: true, }); } finally { diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 296a15bdf..1e5d000a9 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -51,6 +51,7 @@ export interface ChatStore { tasks: { [key: string]: Task }; create: (id?: string, type?: any) => string; removeTask: (taskId: string) => void; + stopTask: (taskId: string) => void; setStatus: (taskId: string, status: 'running' | 'finished' | 'pending' | 'pause') => void; setActiveTaskId: (taskId: string) => void; replay: (taskId: string, question: string, time: number) => Promise; @@ -114,6 +115,9 @@ export type VanillaChatStore = { // Track auto-confirm timers per task to avoid reusing stale timers across rounds const autoConfirmTimers: Record> = {}; +// Track active SSE connections for proper cleanup +const activeSSEControllers: Record = {}; + const chatStore = (initial?: Partial) => createStore()( (set, get) => ({ activeTaskId: null, @@ -189,6 +193,16 @@ const chatStore = (initial?: Partial) => createStore()( console.warn('Error clearing auto-confirm timer in removeTask:', error); } + // Clean up SSE connection if it exists + try { + if (activeSSEControllers[taskId]) { + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn('Error aborting SSE connection in removeTask:', error); + } + set((state) => { delete state.tasks[taskId]; return ({ @@ -198,6 +212,40 @@ const chatStore = (initial?: Partial) => createStore()( }) }) }, + stopTask(taskId: string) { + // Abort the SSE connection for this task + try { + if (activeSSEControllers[taskId]) { + console.log(`Stopping SSE connection for task ${taskId}`); + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn('Error aborting SSE connection in stopTask:', error); + } + + // Clean up any pending auto-confirm timers + try { + if (autoConfirmTimers[taskId]) { + clearTimeout(autoConfirmTimers[taskId]); + delete autoConfirmTimers[taskId]; + } + } catch (error) { + console.warn('Error clearing auto-confirm timer in stopTask:', error); + } + + // Update task status to finished + set((state) => ({ + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + status: 'finished' + }, + }, + })) + }, startTask: async (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => { const { token, language, modelType, cloud_model_type, email } = getAuthStore() const workerList = useWorkerList(); @@ -403,26 +451,31 @@ const chatStore = (initial?: Partial) => createStore()( // during active message processing let lockedChatStore = targetChatStore; let lockedTaskId = newTaskId; - + + // Create AbortController for this task's SSE connection + const abortController = new AbortController(); + activeSSEControllers[newTaskId] = abortController; + // Getter functions that use the locked references instead of dynamic ones const getCurrentChatStore = () => { return lockedChatStore.getState(); }; - + // Get the locked task ID - this won't change during the SSE session const getCurrentTaskId = () => { return lockedTaskId; }; - + // Function to update locked references (only for special cases like replay) const updateLockedReferences = (newChatStore: VanillaChatStore, newTaskId: string) => { lockedChatStore = newChatStore; lockedTaskId = newTaskId; }; - + fetchEventSource(api, { method: !type ? "POST" : "GET", openWhenHidden: true, + signal: abortController.signal, // Add abort signal for proper cleanup headers: { "Content-Type": "application/json", "Authorization": type == 'replay' ? `Bearer ${token}` : undefined as unknown as string }, body: !type ? JSON.stringify({ project_id: project_id, @@ -469,6 +522,21 @@ const chatStore = (initial?: Partial) => createStore()( return; } + // Check if this task has been stopped before processing any message + // But allow messages that switch to new tasks (like confirmed events) + const lockedTaskId = getCurrentTaskId(); + const currentTask = getCurrentChatStore().tasks[lockedTaskId]; + + // Only ignore messages if: + // 1. The task doesn't exist, OR + // 2. The task is finished AND it's not a task-switching event (confirmed, new_task_state) + const isTaskSwitchingEvent = agentMessages.step === "confirmed" || agentMessages.step === "new_task_state"; + if (!currentTask || (currentTask.status === 'finished' && !isTaskSwitchingEvent)) { + // Task was stopped, ignore any incoming messages + console.log(`Ignoring SSE message for stopped task ${lockedTaskId}, step: ${agentMessages.step}`); + return; + } + console.log("agentMessages", agentMessages); const agentNameMap = { developer_agent: "Developer Agent", @@ -1598,13 +1666,29 @@ const chatStore = (initial?: Partial) => createStore()( }, onerror(err) { - console.error("Error:", err); + console.error("SSE Error:", err); + // Clean up AbortController on error + try { + if (activeSSEControllers[newTaskId]) { + delete activeSSEControllers[newTaskId]; + } + } catch (error) { + console.warn('Error cleaning up AbortController on SSE error:', error); + } throw err; }, // Server closes connection onclose() { - console.log("server closed"); + console.log("SSE connection closed"); + // Clean up AbortController when connection closes + try { + if (activeSSEControllers[newTaskId]) { + delete activeSSEControllers[newTaskId]; + } + } catch (error) { + console.warn('Error cleaning up AbortController on SSE close:', error); + } }, }); @@ -2223,6 +2307,22 @@ const chatStore = (initial?: Partial) => createStore()( console.error('Error during timer cleanup in clearTasks:', error); } + // Clean up all active SSE connections + try { + Object.keys(activeSSEControllers).forEach(taskId => { + try { + if (activeSSEControllers[taskId]) { + activeSSEControllers[taskId].abort(); + delete activeSSEControllers[taskId]; + } + } catch (error) { + console.warn(`Error aborting SSE connection for task ${taskId}:`, error); + } + }); + } catch (error) { + console.error('Error during SSE cleanup in clearTasks:', error); + } + window.ipcRenderer.invoke('restart-backend') .then((res) => { console.log('restart-backend', res) From 9635ad22aefc267b174f0015d53a8f1dd0999d38 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 01:14:53 +0800 Subject: [PATCH 2/6] update --- src/components/ChatBox/index.tsx | 30 ++++++----- src/store/chatStore.ts | 86 ++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 1343e2fff..34ffc021c 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -418,15 +418,13 @@ export default function ChatBox(): JSX.Element { setIsPauseResumeLoading(true); try { - // First, stop the SSE connection and update local state - chatStore.stopTask(taskId); - - // Then notify backend to skip the task + // First, try to notify backend to skip the task await fetchPost(`/chat/${projectStore.activeProjectId}/skip-task`, { project_id: projectStore.activeProjectId }); - // Ensure pending state is cleared + // Only stop local task if backend call succeeds + chatStore.stopTask(taskId); chatStore.setIsPending(taskId, false); toast.success("Task stopped successfully", { @@ -435,13 +433,21 @@ export default function ChatBox(): JSX.Element { } catch (error) { console.error("Failed to skip task:", error); - // If backend call failed, just ensure pending state is cleared - // Don't call stopTask again since it was already called above - chatStore.setIsPending(taskId, false); - - toast.error("Task stopped locally, but backend notification failed", { - closeButton: true, - }); + // If backend call failed, still try to stop local task as fallback + // but with different messaging to user + try { + chatStore.stopTask(taskId); + chatStore.setIsPending(taskId, false); + toast.warning("Task stopped locally, but backend notification failed. Backend task may continue running.", { + closeButton: true, + duration: 5000, + }); + } catch (localError) { + console.error("Failed to stop task locally:", localError); + toast.error("Failed to stop task completely. Please refresh the page.", { + closeButton: true, + }); + } } finally { setIsPauseResumeLoading(false); } diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 1e5d000a9..d689195ff 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -222,6 +222,12 @@ const chatStore = (initial?: Partial) => createStore()( } } catch (error) { console.warn('Error aborting SSE connection in stopTask:', error); + // Even if abort fails, still clean up the reference + try { + delete activeSSEControllers[taskId]; + } catch (cleanupError) { + console.warn('Error cleaning up SSE controller reference:', cleanupError); + } } // Clean up any pending auto-confirm timers @@ -234,17 +240,29 @@ const chatStore = (initial?: Partial) => createStore()( console.warn('Error clearing auto-confirm timer in stopTask:', error); } - // Update task status to finished - set((state) => ({ - ...state, - tasks: { - ...state.tasks, - [taskId]: { - ...state.tasks[taskId], - status: 'finished' - }, - }, - })) + // Update task status to finished - ensure this happens even if cleanup fails + try { + set((state) => { + // Check if task exists before updating + if (!state.tasks[taskId]) { + console.warn(`Task ${taskId} not found when trying to stop it`); + return state; + } + + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + status: 'finished' + }, + }, + }; + }); + } catch (error) { + console.error('Error updating task status to finished in stopTask:', error); + } }, startTask: async (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => { const { token, language, modelType, cloud_model_type, email } = getAuthStore() @@ -453,6 +471,17 @@ const chatStore = (initial?: Partial) => createStore()( let lockedTaskId = newTaskId; // Create AbortController for this task's SSE connection + // First check if there's already an active SSE connection for this task + if (activeSSEControllers[newTaskId]) { + console.warn(`Task ${newTaskId} already has an active SSE connection, aborting old one`); + try { + activeSSEControllers[newTaskId].abort(); + } catch (error) { + console.warn('Error aborting existing SSE connection:', error); + } + delete activeSSEControllers[newTaskId]; + } + const abortController = new AbortController(); activeSSEControllers[newTaskId] = abortController; @@ -529,11 +558,22 @@ const chatStore = (initial?: Partial) => createStore()( // Only ignore messages if: // 1. The task doesn't exist, OR - // 2. The task is finished AND it's not a task-switching event (confirmed, new_task_state) - const isTaskSwitchingEvent = agentMessages.step === "confirmed" || agentMessages.step === "new_task_state"; - if (!currentTask || (currentTask.status === 'finished' && !isTaskSwitchingEvent)) { - // Task was stopped, ignore any incoming messages - console.log(`Ignoring SSE message for stopped task ${lockedTaskId}, step: ${agentMessages.step}`); + // 2. The task is finished AND it's not a task-switching event + const isTaskSwitchingEvent = agentMessages.step === "confirmed" || + agentMessages.step === "new_task_state" || + agentMessages.step === "end"; + + // More robust check - only ignore if task doesn't exist OR + // task is finished and it's not a legitimate flow-control event + if (!currentTask) { + console.log(`Task ${lockedTaskId} not found, ignoring SSE message for step: ${agentMessages.step}`); + return; + } + + if (currentTask.status === 'finished' && !isTaskSwitchingEvent) { + // Only ignore non-essential messages for finished tasks + // Allow flow control messages through even for finished tasks + console.log(`Ignoring SSE message for finished task ${lockedTaskId}, step: ${agentMessages.step}`); return; } @@ -1667,13 +1707,14 @@ const chatStore = (initial?: Partial) => createStore()( onerror(err) { console.error("SSE Error:", err); - // Clean up AbortController on error + // Clean up AbortController on error with robust error handling try { if (activeSSEControllers[newTaskId]) { delete activeSSEControllers[newTaskId]; + console.log(`Cleaned up SSE controller for task ${newTaskId} after error`); } - } catch (error) { - console.warn('Error cleaning up AbortController on SSE error:', error); + } catch (cleanupError) { + console.warn('Error cleaning up AbortController on SSE error:', cleanupError); } throw err; }, @@ -1681,13 +1722,14 @@ const chatStore = (initial?: Partial) => createStore()( // Server closes connection onclose() { console.log("SSE connection closed"); - // Clean up AbortController when connection closes + // Clean up AbortController when connection closes with robust error handling try { if (activeSSEControllers[newTaskId]) { delete activeSSEControllers[newTaskId]; + console.log(`Cleaned up SSE controller for task ${newTaskId} after connection close`); } - } catch (error) { - console.warn('Error cleaning up AbortController on SSE close:', error); + } catch (cleanupError) { + console.warn('Error cleaning up AbortController on SSE close:', cleanupError); } }, }); From 22d8776b7f987f16fccbc18fb69e7436338d4273 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Fri, 21 Nov 2025 01:27:13 +0800 Subject: [PATCH 3/6] update --- electron/main/index.ts | 62 +++++++++++++++++++++++++++++++-- index.html | 2 +- src/components/Folder/index.tsx | 23 +++++++++--- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index a720720db..02cedbb87 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -89,6 +89,21 @@ app.commandLine.appendSwitch('max_old_space_size', '4096'); app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction'); app.commandLine.appendSwitch('renderer-process-limit', '8'); +// ==================== protocol privileges ==================== +// Register custom protocol privileges before app ready +protocol.registerSchemesAsPrivileged([ + { + scheme: 'localfile', + privileges: { + standard: true, + secure: true, + supportFetchAPI: true, + corsEnabled: false, + bypassCSP: false, + }, + }, +]); + // ==================== app config ==================== process.env.APP_ROOT = MAIN_DIST; process.env.VITE_PUBLIC = VITE_PUBLIC; @@ -1577,12 +1592,24 @@ app.whenReady().then(async () => { }); // ==================== protocol handle ==================== - protocol.handle('localfile', async (request) => { + // Register protocol handler for both default session and main window session + const protocolHandler = async (request: Request) => { const url = decodeURIComponent(request.url.replace('localfile://', '')); const filePath = path.normalize(url); + log.info(`[PROTOCOL] Handling localfile request: ${request.url}`); + log.info(`[PROTOCOL] Decoded path: ${filePath}`); + try { + // Check if file exists + const fileExists = await fsp.access(filePath).then(() => true).catch(() => false); + if (!fileExists) { + log.error(`[PROTOCOL] File not found: ${filePath}`); + return new Response('File Not Found', { status: 404 }); + } + const data = await fsp.readFile(filePath); + log.info(`[PROTOCOL] Successfully read file, size: ${data.length} bytes`); // set correct Content-Type according to file extension const ext = path.extname(filePath).toLowerCase(); @@ -1596,17 +1623,46 @@ app.whenReady().then(async () => { case '.htm': contentType = 'text/html'; break; + case '.png': + contentType = 'image/png'; + break; + case '.jpg': + case '.jpeg': + contentType = 'image/jpeg'; + break; + case '.gif': + contentType = 'image/gif'; + break; + case '.svg': + contentType = 'image/svg+xml'; + break; + case '.webp': + contentType = 'image/webp'; + break; } + log.info(`[PROTOCOL] Returning file with Content-Type: ${contentType}`); + return new Response(new Uint8Array(data), { headers: { 'Content-Type': contentType, + 'Content-Length': data.length.toString(), }, }); } catch (err) { - return new Response('Not Found', { status: 404 }); + log.error(`[PROTOCOL] Error reading file: ${err}`); + return new Response('Internal Server Error', { status: 500 }); } - }); + }; + + // Register on default session + protocol.handle('localfile', protocolHandler); + + // Also register on main window session + const mainSession = session.fromPartition('persist:main_window'); + mainSession.protocol.handle('localfile', protocolHandler); + + log.info('[PROTOCOL] Registered localfile protocol on both default and main_window sessions'); // ==================== initialize app ==================== initializeApp(); diff --git a/index.html b/index.html index 9c2d1457f..5c3481fab 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ Eigent diff --git a/src/components/Folder/index.tsx b/src/components/Folder/index.tsx index 7834bfc4e..b12816221 100644 --- a/src/components/Folder/index.tsx +++ b/src/components/Folder/index.tsx @@ -183,7 +183,23 @@ export default function Folder({ data }: { data?: Agent }) { setLoading(true); console.log("file", JSON.parse(JSON.stringify(file))); - // all files call open-file interface, the backend handles download and parsing + // For PDF files, use data URL instead of custom protocol + if (file.type === "pdf") { + window.ipcRenderer + .invoke("read-file-dataurl", file.path) + .then((dataUrl: string) => { + setSelectedFile({ ...file, content: dataUrl }); + chatStore.setSelectedFile(chatStore.activeTaskId as string, file); + setLoading(false); + }) + .catch((error) => { + console.error("read-file-dataurl error:", error); + setLoading(false); + }); + return; + } + + // all other files call open-file interface, the backend handles download and parsing window.ipcRenderer .invoke("open-file", file.type, file.path, isShowSourceCode) .then((res) => { @@ -539,10 +555,7 @@ export default function Folder({ data }: { data?: Agent }) { ) : selectedFile.type === "pdf" ? (