From a377e9faa15105b36902d21594b6866e19de0985 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 14:00:02 +0800 Subject: [PATCH 01/36] update --- electron/main/index.ts | 45 ++++++++++++++--- electron/main/install-deps.ts | 17 ++++++- electron/preload/index.ts | 13 +++-- package.json | 2 +- resources/scripts/download.js | 84 ++++++++++++++++++++++--------- src/api/http.ts | 68 +++++++++++++++++++++++++ src/components/Layout/index.tsx | 37 +++++++------- src/hooks/useInstallationSetup.ts | 60 +++++++++++++++++++--- src/store/chatStore.ts | 37 +++++++++++++- 9 files changed, 298 insertions(+), 65 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index 7916b0606..f6120266b 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1329,11 +1329,20 @@ async function createWindow() { let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { log.info("[DEPS INSTALL] Dependency Error: ", res.message); - win.webContents.send('install-dependencies-complete', { success: false, code: 2, error: res.message }); + // Note: install-dependencies-complete failure event is already sent by installDependencies function + // in install-deps.ts, so we don't send it again here to avoid duplicate events return; } log.info("[DEPS INSTALL] Dependency Success: ", res.message); + // IMPORTANT: Always send install-dependencies-complete event when installation check succeeds + // This includes both cases: actual installation completed AND installation was skipped (already installed) + // The frontend needs this event to properly transition from installation screen to main app + if (!win.isDestroyed()) { + win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); + log.info("[DEPS INSTALL] Sent install-dependencies-complete event to frontend"); + } + // Start backend after dependencies are ready await checkAndStartBackend(); } @@ -1397,25 +1406,45 @@ const checkAndStartBackend = async () => { if (isToolInstalled.success) { log.info('Tool installed, starting backend service...'); - // Notify frontend installation success - if (win && !win.isDestroyed()) { - win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); - } - + // Start backend and wait for health check to pass python_process = await startBackend((port) => { backendPort = port; log.info('Backend service started successfully', { port }); }); - python_process?.on('exit', (code, signal) => { + // ✅ Only notify frontend AFTER backend health check passes + if (win && !win.isDestroyed()) { + log.info('Backend is ready, notifying frontend...'); + win.webContents.send('backend-ready', { + success: true, + port: backendPort + }); + } + python_process?.on('exit', (code, signal) => { log.info('Python process exited', { code, signal }); }); } else { log.warn('Tool not installed, cannot start backend service'); + // Notify frontend that backend cannot start + if (win && !win.isDestroyed()) { + win.webContents.send('backend-ready', { + success: false, + error: 'Tools not installed' + }); + } } } catch (error) { - log.debug("Cannot Start Backend due to ", error) + log.error("Failed to start backend:", error); + // Notify frontend of backend startup failure + if (win && !win.isDestroyed()) { + win.webContents.send('backend-ready', { + success: false, + error: String(error) + }); + } + // Re-throw to let caller know about the failure + throw error; } }; diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 257fca38e..d9c595a49 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -163,7 +163,15 @@ export async function installCommandTool(): Promise { return { message: "Command tools installed successfully", success: true }; } catch (error) { - return { message: `Command tool installation failed: ${error}`, success: false }; + const errorMessage = `Command tool installation failed: ${error}`; + log.error('[DEPS INSTALL] Exception during command tool installation:', error); + // Send failure event to frontend + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: errorMessage + }); + return { message: errorMessage, success: false }; } } @@ -599,6 +607,13 @@ export async function installDependencies(version: string): Promise void) => { ipcRenderer.on('install-dependencies-complete', (event, data) => callback(data)); }, - onUpdateNotification: (callback: (data: { - type: string; - currentVersion: string; - previousVersion: string; - reason: string; + onUpdateNotification: (callback: (data: { + type: string; + currentVersion: string; + previousVersion: string; + reason: string; }) => void) => { ipcRenderer.on('update-notification', (event, data) => callback(data)); }, + onBackendReady: (callback: (data: { success: boolean, port?: number, error?: string }) => void) => { + ipcRenderer.on('backend-ready', (event, data) => callback(data)); + }, startBrowserImport: (args?: any) => ipcRenderer.invoke('start-browser-import', args), // remove listeners removeAllListeners: (channel: string) => { diff --git a/package.json b/package.json index b70ec9258..f84e7612f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "compile-babel": "cd backend && uv run pybabel compile -d lang", "clean-cache": "rimraf node_modules/.vite", - "dev": "npm run clean-cache && npm run compile-babel && vite", + "dev": "npm run clean-cache && vite", "build": "npm run compile-babel && tsc && vite build && electron-builder -- --publish always", "build:mac": "npm run compile-babel && tsc && vite build && electron-builder --mac", "build:win": "npm run compile-babel && tsc && vite build && electron-builder --win", diff --git a/resources/scripts/download.js b/resources/scripts/download.js index f77115815..124e7cc88 100644 --- a/resources/scripts/download.js +++ b/resources/scripts/download.js @@ -15,6 +15,25 @@ export async function downloadWithRedirects(url, destinationPath) { reject(new Error(`timeout(${timeoutMs / 1000} seconds)`)); }, timeoutMs); + // Use flag to prevent multiple resolve/reject calls + let settled = false; + + const safeReject = (error) => { + if (!settled) { + settled = true; + clearTimeout(timeout); + reject(error); + } + }; + + const safeResolve = () => { + if (!settled) { + settled = true; + clearTimeout(timeout); + resolve(); + } + }; + const request = (url) => { https .get(url, (response) => { @@ -23,53 +42,70 @@ export async function downloadWithRedirects(url, destinationPath) { return } if (response.statusCode !== 200) { - clearTimeout(timeout); - reject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) + safeReject(new Error(`Download failed: ${response.statusCode} ${response.statusMessage}`)) return } - + const file = fs.createWriteStream(destinationPath) let downloadedBytes = 0 const expectedBytes = parseInt(response.headers['content-length'] || '0') - + response.on('data', (chunk) => { downloadedBytes += chunk.length }) - + response.pipe(file) - + file.on('finish', () => { file.close(() => { - clearTimeout(timeout); - + // Don't proceed if already rejected (e.g., by error handler) + if (settled) return; + // Verify the download is complete if (expectedBytes > 0 && downloadedBytes !== expectedBytes) { - fs.unlinkSync(destinationPath) - reject(new Error(`Download incomplete: received ${downloadedBytes} bytes, expected ${expectedBytes}`)) + try { + if (fs.existsSync(destinationPath)) { + fs.unlinkSync(destinationPath) + } + } catch (err) { + console.error('Failed to delete incomplete file:', err); + } + safeReject(new Error(`Download incomplete: received ${downloadedBytes} bytes, expected ${expectedBytes}`)) return } - + // Check if file exists and has size > 0 - const stats = fs.statSync(destinationPath) - if (stats.size === 0) { - fs.unlinkSync(destinationPath) - reject(new Error('Downloaded file is empty')) - return + try { + if (fs.existsSync(destinationPath)) { + const stats = fs.statSync(destinationPath) + if (stats.size === 0) { + fs.unlinkSync(destinationPath) + safeReject(new Error('Downloaded file is empty')) + return + } + safeResolve() + } else { + safeReject(new Error('Downloaded file does not exist')) + } + } catch (err) { + safeReject(new Error(`Failed to verify download: ${err.message}`)) } - - resolve() }) }) - + file.on('error', (err) => { - clearTimeout(timeout); - fs.unlinkSync(destinationPath) - reject(err) + try { + if (fs.existsSync(destinationPath)) { + fs.unlinkSync(destinationPath) + } + } catch (deleteErr) { + console.error('Failed to delete file after error:', deleteErr); + } + safeReject(err) }) }) .on('error', (err) => { - clearTimeout(timeout); - reject(err) + safeReject(err) }) } request(url) diff --git a/src/api/http.ts b/src/api/http.ts index 1a60855ad..55318a741 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -2,6 +2,8 @@ import { getAuthStore } from '@/store/authStore' import { showCreditsToast } from '@/components/Toast/creditsToast'; import { showStorageToast } from '@/components/Toast/storageToast'; import { showTrafficToast } from '@/components/Toast/trafficToast'; +import { useBackendStore } from '@/store/backendStore'; + const defaultHeaders = { 'Content-Type': 'application/json', } @@ -250,3 +252,69 @@ export async function uploadFile(url: string, formData: FormData, headers?: Reco return handleResponse(fetch(fullUrl, options)) } + +// =============== Backend Health Check =============== + +/** + * Check if backend is ready by checking the health endpoint + * @returns Promise - true if backend is ready, false otherwise + */ +export async function checkBackendHealth(): Promise { + try { + const baseURL = await getBaseURL(); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 1000); + + const res = await fetch(`${baseURL}/health`, { + signal: controller.signal, + method: 'GET', + }); + + clearTimeout(timeoutId); + return res.ok; + } catch (error) { + console.log('[Backend Health Check] Not ready:', error); + return false; + } +} + +/** + * Wait for backend to be ready with retries + * @param maxWaitMs - Maximum time to wait in milliseconds (default: 10000ms) + * @param retryIntervalMs - Interval between retries in milliseconds (default: 500ms) + * @returns Promise - true if backend becomes ready, false if timeout + */ +export async function waitForBackendReady( + maxWaitMs: number = 10000, + retryIntervalMs: number = 500 +): Promise { + const backendStore = useBackendStore.getState(); + + // If backend is already marked as ready, do a quick health check + if (backendStore.isReady) { + const isHealthy = await checkBackendHealth(); + if (isHealthy) { + console.log('[Backend Health Check] Already ready and healthy'); + return true; + } + } + + // Wait for backend to become ready + const startTime = Date.now(); + console.log('[Backend Health Check] Waiting for backend to be ready...'); + + while (Date.now() - startTime < maxWaitMs) { + const isReady = await checkBackendHealth(); + + if (isReady) { + console.log(`[Backend Health Check] Backend is ready after ${Date.now() - startTime}ms`); + return true; + } + + console.log(`[Backend Health Check] Backend not ready, retrying... (${Date.now() - startTime}ms elapsed)`); + await new Promise(resolve => setTimeout(resolve, retryIntervalMs)); + } + + console.error(`[Backend Health Check] Backend failed to start within ${maxWaitMs}ms`); + return false; +} diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 541819302..d26fd6842 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -37,23 +37,26 @@ const Layout = () => { useInstallationSetup(); // Additional check: If initState is carousel but tools are installed, skip to done - useEffect(() => { - const checkAndSkipCarousel = async () => { - if (initState === 'carousel' && !isInstalling) { - try { - const result = await window.ipcRenderer.invoke("check-tool-installed"); - if (result.success && result.isInstalled) { - console.log('[Layout] Tools installed, skipping carousel and setting initState to done'); - setInitState('done'); - } - } catch (error) { - console.error('[Layout] Failed to check tool installation:', error); - } - } - }; - - checkAndSkipCarousel(); - }, [initState, isInstalling, setInitState]); + // REMOVED: This check is too aggressive and causes premature navigation to main content + // The proper flow should be: installation complete -> backend ready -> then set initState='done' + // This is now handled in useInstallationSetup.ts + // useEffect(() => { + // const checkAndSkipCarousel = async () => { + // if (initState === 'carousel' && !isInstalling) { + // try { + // const result = await window.ipcRenderer.invoke("check-tool-installed"); + // if (result.success && result.isInstalled) { + // console.log('[Layout] Tools installed, skipping carousel and setting initState to done'); + // setInitState('done'); + // } + // } catch (error) { + // console.error('[Layout] Failed to check tool installation:', error); + // } + // } + // }; + // + // checkAndSkipCarousel(); + // }, [initState, isInstalling, setInitState]); useEffect(() => { const handleBeforeClose = () => { diff --git a/src/hooks/useInstallationSetup.ts b/src/hooks/useInstallationSetup.ts index 018828232..a0fdc8d0d 100644 --- a/src/hooks/useInstallationSetup.ts +++ b/src/hooks/useInstallationSetup.ts @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react'; import { useInstallationStore } from '@/store/installationStore'; import { useAuthStore } from '@/store/authStore'; +import { useBackendStore } from '@/store/backendStore'; /** * Hook that sets up Electron IPC listeners and handles installation state synchronization @@ -12,6 +13,10 @@ export const useInstallationSetup = () => { // Use ref to track if initial check is done to prevent repeated checks const hasCheckedOnMount = useRef(false); + // Track installation and backend readiness states + const installationCompleted = useRef(false); + const backendReady = useRef(false); + // Extract only the functions we need to avoid dependency issues const startInstallation = useInstallationStore(state => state.startInstallation); const performInstallation = useInstallationStore(state => state.performInstallation); @@ -38,11 +43,14 @@ export const useInstallationSetup = () => { // This prevents unexpected navigation away from the main app if (initState !== 'done') { if (result.success) { - if (result.isInstalled && initState === "carousel") { - // If tools ARE installed and we're in carousel state, go to done - console.log('[useInstallationSetup] Tools installed but initState is carousel, setting to done'); - setInitState("done"); - } else if (!result.isInstalled && initState === "permissions") { + // REMOVED: Don't automatically set to 'done' even if tools are installed + // We need to wait for proper installation complete + backend ready events + // if (result.isInstalled && initState === "carousel") { + // console.log('[useInstallationSetup] Tools installed but initState is carousel, setting to done'); + // setInitState("done"); + // } + + if (!result.isInstalled && initState === "permissions") { // If tools are NOT installed and we're in permissions state, set to carousel console.log('[useInstallationSetup] Tools not installed and initState is permissions, setting to carousel'); setInitState("carousel"); @@ -81,8 +89,23 @@ export const useInstallationSetup = () => { // Setup Electron IPC listeners (only once) useEffect(() => { + const backendStore = useBackendStore.getState(); + + // Helper function to check if both installation and backend are ready + const checkAndSetDone = () => { + console.log('[useInstallationSetup] Checking readiness - Installation:', installationCompleted.current, 'Backend:', backendReady.current); + + if (installationCompleted.current && backendReady.current) { + console.log('[useInstallationSetup] Both installation and backend are ready, setting initState to done'); + setInitState('done'); + } + }; + // Electron IPC event handlers const handleInstallStart = () => { + // Reset flags when installation starts + installationCompleted.current = false; + backendReady.current = false; startInstallation(); }; @@ -95,19 +118,41 @@ export const useInstallationSetup = () => { }; const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => { - + console.log('[useInstallationSetup] Installation complete event received:', data); + if (data.success) { setSuccess(); - setInitState('done'); + installationCompleted.current = true; + console.log('[useInstallationSetup] Installation marked as completed'); + // Only set initState to done if backend is also ready + checkAndSetDone(); } else { setError(data.error || 'Installation failed'); } }; + const handleBackendReady = (data: { success: boolean; port?: number; error?: string }) => { + console.log('[useInstallationSetup] Backend ready event received:', data); + + if (data.success && data.port) { + console.log(`[useInstallationSetup] Backend is ready on port ${data.port}`); + backendStore.setReady(data.port); + backendReady.current = true; + console.log('[useInstallationSetup] Backend marked as ready'); + // Only set initState to done if installation is also completed + checkAndSetDone(); + } else { + console.error('[useInstallationSetup] Backend failed to start:', data.error); + backendStore.setError(data.error || 'Backend startup failed'); + setError(data.error || 'Backend startup failed'); + } + }; + // Register Electron IPC listeners window.electronAPI.onInstallDependenciesStart(handleInstallStart); window.electronAPI.onInstallDependenciesLog(handleInstallLog); window.electronAPI.onInstallDependenciesComplete(handleInstallComplete); + window.electronAPI.onBackendReady(handleBackendReady); // Cleanup listeners on unmount @@ -115,6 +160,7 @@ export const useInstallationSetup = () => { window.electronAPI.removeAllListeners('install-dependencies-start'); window.electronAPI.removeAllListeners('install-dependencies-log'); window.electronAPI.removeAllListeners('install-dependencies-complete'); + window.electronAPI.removeAllListeners('backend-ready'); }; }, [startInstallation, addLog, setSuccess, setError, setInitState]); }; \ No newline at end of file diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 810701015..9005ab694 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -1,4 +1,4 @@ -import { fetchPost, fetchPut, getBaseURL, proxyFetchPost, proxyFetchPut, proxyFetchGet, uploadFile, fetchDelete } from '@/api/http'; +import { fetchPost, fetchPut, getBaseURL, proxyFetchPost, proxyFetchPut, proxyFetchGet, uploadFile, fetchDelete, waitForBackendReady } from '@/api/http'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { createStore } from 'zustand'; import { generateUniqueId, uploadLog } from "@/lib"; @@ -8,6 +8,7 @@ import { useProjectStore } from './projectStore'; import { showCreditsToast } from '@/components/Toast/creditsToast'; import { showStorageToast } from '@/components/Toast/storageToast'; import { toast } from 'sonner'; +import { useBackendStore } from './backendStore'; interface Task { @@ -198,6 +199,24 @@ const chatStore = (initial?: Partial) => createStore()( }) }, startTask: async (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => { + // ✅ Wait for backend to be ready before starting task (except for replay/share) + if (!type || type === 'normal') { + console.log('[startTask] Checking if backend is ready...'); + const isBackendReady = await waitForBackendReady(15000, 500); // Wait up to 15 seconds + + if (!isBackendReady) { + console.error('[startTask] Backend is not ready, cannot start task'); + const { addMessages } = get(); + addMessages(taskId, { + id: generateUniqueId(), + role: 'system', + content: '❌ Backend service is not ready. Please wait a moment and try again, or restart the application if the problem persists.', + }); + return; + } + console.log('[startTask] Backend is ready, proceeding with task...'); + } + const { token, language, modelType, cloud_model_type, email } = getAuthStore() const workerList = useWorkerList(); const { getLastUserMessage, setDelayTime, setType } = get(); @@ -1507,7 +1526,21 @@ const chatStore = (initial?: Partial) => createStore()( }, onerror(err) { - console.error("Error:", err); + console.error("[fetchEventSource] Error:", err); + + // Allow automatic retry for connection errors + // TypeError usually means network/connection issues + if (err instanceof TypeError || + err?.message?.includes('Failed to fetch') || + err?.message?.includes('ECONNREFUSED') || + err?.message?.includes('NetworkError')) { + console.warn('[fetchEventSource] Connection error detected, will retry automatically...'); + // Don't throw - let fetchEventSource auto-retry + return; + } + + // For other errors, log and throw to stop retrying + console.error('[fetchEventSource] Fatal error, stopping connection:', err); throw err; }, From 422d77ece6ba420e823788425faf8c4b15c2167a Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 14:07:32 +0800 Subject: [PATCH 02/36] update --- src/store/backendStore.ts | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/store/backendStore.ts diff --git a/src/store/backendStore.ts b/src/store/backendStore.ts new file mode 100644 index 000000000..b2e976ce3 --- /dev/null +++ b/src/store/backendStore.ts @@ -0,0 +1,50 @@ +import { create } from 'zustand'; + +interface BackendState { + isReady: boolean; + port: number | null; + error: string | null; + isChecking: boolean; + + // Actions + setReady: (port: number) => void; + setError: (error: string) => void; + setChecking: (isChecking: boolean) => void; + reset: () => void; +} + +const initialState = { + isReady: false, + port: null, + error: null, + isChecking: false, +}; + +export const useBackendStore = create((set) => ({ + ...initialState, + + setReady: (port: number) => + set({ + isReady: true, + port, + error: null, + isChecking: false, + }), + + setError: (error: string) => + set({ + isReady: false, + error, + isChecking: false, + }), + + setChecking: (isChecking: boolean) => + set({ isChecking }), + + reset: () => set(initialState), +})); + +// Selector hooks for common patterns +export const useBackendReady = () => useBackendStore(state => state.isReady); +export const useBackendPort = () => useBackendStore(state => state.port); +export const useBackendError = () => useBackendStore(state => state.error); From bb1ddc7814a7719c3e7c30e7acf59ec2f5328503 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Thu, 20 Nov 2025 14:24:26 +0800 Subject: [PATCH 03/36] romove_proxy --- electron/main/install-deps.ts | 65 +++++++++++------------------------ src/store/chatStore.ts | 2 -- 2 files changed, 21 insertions(+), 46 deletions(-) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index d9c595a49..e9066d047 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -187,8 +187,6 @@ if (!fs.existsSync(backendPath)) { const installingLockPath = path.join(backendPath, 'uv_installing.lock') const installedLockPath = path.join(backendPath, 'uv_installed.lock') -// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple'] -const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/'] /** * Get current installation status by checking lock files @@ -352,10 +350,10 @@ export async function installDependencies(version: string): Promise { + spawnBabel: () => { fs.writeFileSync(installedLockPath, '') log.info('[DEPS INSTALL] Script completed successfully') - console.log(`Install Dependencies completed ${message} for version ${version}`) + console.log(`Install Dependencies completed for version ${version}`) console.log(`Virtual environment path: ${venvPath}`) spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath, @@ -597,7 +595,7 @@ export async function installDependencies(version: string): Promise(async (resolve, reject) => { + return new Promise(async (resolve) => { console.log('start install dependencies') const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage(); @@ -647,50 +645,29 @@ export async function installDependencies(version: string): Promise) => createStore()( language: systemLanguage, allow_local_system: true, attaches: (messageAttaches || targetChatStore.getState().tasks[newTaskId]?.attaches || []).map(f => f.filePath), - bun_mirror: systemLanguage === 'zh-cn' ? 'https://registry.npmmirror.com' : '', - uvx_mirror: systemLanguage === 'zh-cn' ? 'http://mirrors.aliyun.com/pypi/simple/' : '', summary_prompt: ``, new_agents: [...addWorkers], browser_port: browser_port, From af62fed7fa355bbb02ecb0a555ee219ab90d3c68 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 14:40:31 +0800 Subject: [PATCH 04/36] update --- electron/main/index.ts | 27 +++++++++++++++++++++++++-- src/store/installationStore.ts | 16 +++++++++++----- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index f6120266b..eff4a903f 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -996,10 +996,33 @@ function registerIpcHandlers() { ipcMain.handle('install-dependencies', async () => { try { if(win === null) throw new Error("Window is null"); + + log.info('[DEPS INSTALL] Manual installation/retry triggered'); + //Force installation even if versionFile exists - const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true}); - return { success: true, isInstalled }; + const result = await checkAndInstallDepsOnUpdate({win, forceInstall: true}); + + if (!result.success) { + log.error('[DEPS INSTALL] Manual installation failed:', result.message); + // Note: Failure event already sent by installDependencies function + return { success: false, error: result.message }; + } + + log.info('[DEPS INSTALL] Manual installation succeeded'); + + // IMPORTANT: Send install-dependencies-complete success event + if (!win.isDestroyed()) { + win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); + log.info('[DEPS INSTALL] Sent install-dependencies-complete event after manual installation'); + } + + // IMPORTANT: Start backend after successful installation + log.info('[DEPS INSTALL] Starting backend after manual installation...'); + await checkAndStartBackend(); + + return { success: true, isInstalled: result.success }; } catch (error) { + log.error('[DEPS INSTALL] Manual installation error:', error); return { success: false, error: (error as Error).message }; } }); diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts index 374d88965..1fd4ca6cb 100644 --- a/src/store/installationStore.ts +++ b/src/store/installationStore.ts @@ -124,16 +124,22 @@ export const useInstallationStore = create()( // Async actions performInstallation: async () => { const { startInstallation, setSuccess, setError } = get(); - + try { startInstallation(); const result = await window.electronAPI.checkAndInstallDepsOnUpdate(); - + if (result.success) { + // ✅ FIXED: Don't set initState='done' here! + // The proper flow is: + // 1. Installation completes → install-dependencies-complete event + // 2. Backend starts → backend-ready event + // 3. useInstallationSetup receives both events → sets initState='done' + // This ensures backend is ready before showing main page setSuccess(); - // Update auth store - const { useAuthStore } = await import('./authStore'); - useAuthStore.getState().setInitState('done'); + + // Note: initState will be set to 'done' by useInstallationSetup.ts + // after both installation complete AND backend ready events are received } else { setError('Installation failed'); } From cdb61aa8103e870e30ab6f444397668f9828a479 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 14:53:25 +0800 Subject: [PATCH 05/36] update --- electron/main/index.ts | 23 ++++++++++--- electron/main/install-deps.ts | 65 ++++++++++++++++++++++++----------- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index eff4a903f..b174492b6 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1013,12 +1013,12 @@ function registerIpcHandlers() { // IMPORTANT: Send install-dependencies-complete success event if (!win.isDestroyed()) { win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); - log.info('[DEPS INSTALL] Sent install-dependencies-complete event after manual installation'); + log.info('[DEPS INSTALL] Sent install-dependencies-complete event after retry'); } - // IMPORTANT: Start backend after successful installation - log.info('[DEPS INSTALL] Starting backend after manual installation...'); - await checkAndStartBackend(); + // Start backend if needed (only if not already running) + // Uses shared logic to prevent duplication + await startBackendIfNeeded(); return { success: true, isInstalled: result.success }; } catch (error) { @@ -1367,9 +1367,22 @@ async function createWindow() { } // Start backend after dependencies are ready - await checkAndStartBackend(); + // Uses shared logic that checks if backend is already running + await startBackendIfNeeded(); } +// ==================== Shared backend startup logic ==================== +// This function ensures backend is started only when needed +// Used by both initial startup and retry flows +const startBackendIfNeeded = async () => { + if (!backendPort) { + log.info('[DEPS INSTALL] Backend not running, starting now...'); + await checkAndStartBackend(); + } else { + log.info('[DEPS INSTALL] Backend already running on port', backendPort, ', skipping startup'); + } +}; + // ==================== window event listeners ==================== const setupWindowEventListeners = () => { if (!win) return; diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index e9066d047..d9c595a49 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -187,6 +187,8 @@ if (!fs.existsSync(backendPath)) { const installingLockPath = path.join(backendPath, 'uv_installing.lock') const installedLockPath = path.join(backendPath, 'uv_installed.lock') +// const proxyArgs = ['--default-index', 'https://pypi.tuna.tsinghua.edu.cn/simple'] +const proxyArgs = ['--default-index', 'https://mirrors.aliyun.com/pypi/simple/'] /** * Get current installation status by checking lock files @@ -350,10 +352,10 @@ export async function installDependencies(version: string): Promise { + spawnBabel: (message:"mirror"|"main"="main") => { fs.writeFileSync(installedLockPath, '') log.info('[DEPS INSTALL] Script completed successfully') - console.log(`Install Dependencies completed for version ${version}`) + console.log(`Install Dependencies completed ${message} for version ${version}`) console.log(`Virtual environment path: ${venvPath}`) spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath, @@ -595,7 +597,7 @@ export async function installDependencies(version: string): Promise(async (resolve) => { + return new Promise(async (resolve, reject) => { console.log('start install dependencies') const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage(); @@ -645,29 +647,50 @@ export async function installDependencies(version: string): Promise Date: Thu, 20 Nov 2025 15:26:48 +0800 Subject: [PATCH 06/36] update --- electron/main/index.ts | 31 +++++++++---------- .../InstallStep/InstallDependencies.tsx | 15 ++++++--- src/components/Layout/index.tsx | 17 +++++++--- src/hooks/useInstallationSetup.ts | 6 ++++ 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index b174492b6..c142900ca 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1016,9 +1016,9 @@ function registerIpcHandlers() { log.info('[DEPS INSTALL] Sent install-dependencies-complete event after retry'); } - // Start backend if needed (only if not already running) - // Uses shared logic to prevent duplication - await startBackendIfNeeded(); + // Start backend after retry + // Note: checkAndStartBackend() will handle cleanup of any existing backend process + await startBackendAfterInstall(); return { success: true, isInstalled: result.success }; } catch (error) { @@ -1075,6 +1075,16 @@ const ensureEigentDirectories = () => { log.info('.eigent directory structure ensured'); }; +// ==================== Shared backend startup logic ==================== +// Starts backend after installation completes +// Used by both initial startup and retry flows +// Note: No need to check if backend is running - on app startup it's never running, +// and checkAndStartBackend() handles cleanup of any existing process +const startBackendAfterInstall = async () => { + log.info('[DEPS INSTALL] Starting backend...'); + await checkAndStartBackend(); +}; + // ==================== window create ==================== async function createWindow() { const isMac = process.platform === 'darwin'; @@ -1367,22 +1377,9 @@ async function createWindow() { } // Start backend after dependencies are ready - // Uses shared logic that checks if backend is already running - await startBackendIfNeeded(); + await startBackendAfterInstall(); } -// ==================== Shared backend startup logic ==================== -// This function ensures backend is started only when needed -// Used by both initial startup and retry flows -const startBackendIfNeeded = async () => { - if (!backendPort) { - log.info('[DEPS INSTALL] Backend not running, starting now...'); - await checkAndStartBackend(); - } else { - log.info('[DEPS INSTALL] Backend already running on port', backendPort, ', skipping startup'); - } -}; - // ==================== window event listeners ==================== const setupWindowEventListeners = () => { if (!win) return; diff --git a/src/components/InstallStep/InstallDependencies.tsx b/src/components/InstallStep/InstallDependencies.tsx index 5b913ba46..7b1d15244 100644 --- a/src/components/InstallStep/InstallDependencies.tsx +++ b/src/components/InstallStep/InstallDependencies.tsx @@ -16,11 +16,13 @@ import { import { useTranslation } from "react-i18next"; import { useInstallationUI } from "@/store/installationStore"; import { TooltipSimple } from "../ui/tooltip"; +import { useBackendReady } from "@/store/backendStore"; export const InstallDependencies: React.FC = () => { const { initState } = useAuthStore(); const {t} = useTranslation(); - + const backendIsReady = useBackendReady(); + const { progress, latestLog, @@ -30,6 +32,10 @@ export const InstallDependencies: React.FC = () => { exportLog, } = useInstallationUI(); + // Determine the installation phase + const installationComplete = !isInstalling && initState === 'done'; + const waitingForBackend = installationComplete && !backendIsReady; + return (
@@ -37,13 +43,13 @@ export const InstallDependencies: React.FC = () => { {/* {isInstalling.toString()} */}
- {isInstalling ? "System Installing ..." : ""} - {latestLog?.data} + {waitingForBackend ? "Starting backend service..." : (isInstalling ? "System Installing ..." : "")} + {!waitingForBackend && latestLog?.data}
- {/* error dialog */} - - - - {t("layout.installation-failed")} - - - - - - -
); }; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 2cb79c5ea..0fc54fec5 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -9,6 +9,7 @@ import animationData from "@/assets/animation/onboarding_success.json"; import CloseNoticeDialog from "../Dialog/CloseNotice"; import { useInstallationUI } from "@/store/installationStore"; import { useInstallationSetup } from "@/hooks/useInstallationSetup"; +import InstallationErrorDialog from "../InstallStep/InstallationErrorDialog/InstallationErrorDialog"; import Halo from "../Halo"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; @@ -26,8 +27,11 @@ const Layout = () => { const { installationState, + latestLog, + error, isInstalling, shouldShowInstallScreen, + retryInstallation, } = useInstallationUI(); // Setup installation IPC listeners and state synchronization @@ -107,6 +111,14 @@ const Layout = () => { )} + {(error != "" && error !=undefined) && + + } + Date: Thu, 20 Nov 2025 21:52:07 +0800 Subject: [PATCH 19/36] fix/avoid_install --- electron/main/init.ts | 9 ++++++--- electron/main/install-deps.ts | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/electron/main/init.ts b/electron/main/init.ts index b09db6595..39dc641ca 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -7,7 +7,7 @@ import * as net from "net"; import * as http from "http"; import { ipcMain, BrowserWindow, app } from 'electron' import { promisify } from 'util' -import { detectInstallationLogs, PromiseReturnType } from "./install-deps"; +import { PromiseReturnType } from "./install-deps"; const execAsync = promisify(exec); @@ -171,8 +171,11 @@ export async function startBackend(setPort?: (port: number) => void): Promise { if (!data) return; const msg = data.toString().trimEnd(); - //Detect if uv sync is run - detectInstallationLogs(msg); + + // REMOVED: detectInstallationLogs(msg) + // Reason: Removed keyword-based detection to avoid false positives when backend + // outputs logs containing keywords like "Installing", "Updating", "Syncing" etc. + // Installation is now only handled through the explicit installation flow. if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) { log.error(`BACKEND: ${msg}`); diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index 257fca38e..cb10b8c9f 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -681,6 +681,18 @@ export async function installDependencies(version: string): Promise Date: Thu, 20 Nov 2025 22:25:46 +0800 Subject: [PATCH 20/36] update error popup --- electron/main/init.ts | 4 +- electron/main/install-deps.ts | 46 +++++++++++------ electron/main/utils/process.ts | 10 +++- electron/preload/index.ts | 1 + .../InstallationErrorDialog.tsx | 41 +++++++++++++--- src/components/Layout/index.tsx | 14 ++++-- src/hooks/useInstallationSetup.ts | 6 ++- src/i18n/locales/en-us/layout.json | 1 + src/i18n/locales/zh-Hans/layout.json | 1 + src/store/installationStore.ts | 49 +++++++++++++++++-- src/types/electron.d.ts | 9 ++-- 11 files changed, 142 insertions(+), 40 deletions(-) diff --git a/electron/main/init.ts b/electron/main/init.ts index 24308e674..36f34006b 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -246,7 +246,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise void): Promise { let attempts = 0; - const maxAttempts = 160; // 40 seconds total (160 * 250ms) - enough for first run + const maxAttempts = 240; // 60 seconds total (240 * 250ms) - increased for first run const intervalMs = 250; healthCheckInterval = setInterval(() => { diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index d9c595a49..36fe4394f 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -129,26 +129,44 @@ export async function installCommandTool(): Promise { } console.log(`start install ${toolName}`); - await runInstallScript(scriptName); - const installed = await isBinaryExists(toolName); + try { + await runInstallScript(scriptName); + const installed = await isBinaryExists(toolName); - if (installed) { - safeMainWindowSend('install-dependencies-log', { - type: 'stdout', - data: `${toolName} installed successfully`, - }); - } else { + if (installed) { + safeMainWindowSend('install-dependencies-log', { + type: 'stdout', + data: `${toolName} installed successfully`, + }); + return { + message: `${toolName} installed successfully`, + success: true + }; + } else { + const errorMsg = `${toolName} installation failed: binary not found after installation`; + safeMainWindowSend('install-dependencies-complete', { + success: false, + code: 2, + error: errorMsg, + }); + return { + message: errorMsg, + success: false + }; + } + } catch (scriptError) { + // Capture detailed error from runInstallScript + const errorMsg = `${toolName} installation failed: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}`; safeMainWindowSend('install-dependencies-complete', { success: false, code: 2, - error: `${toolName} installation failed (script exit code 2)`, + error: errorMsg, }); + return { + message: errorMsg, + success: false + }; } - - return { - message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`, - success: installed - }; }; const uvResult = await ensureInstalled('uv', 'install-uv.js'); diff --git a/electron/main/utils/process.ts b/electron/main/utils/process.ts index 232860a8e..9570ffd88 100644 --- a/electron/main/utils/process.ts +++ b/electron/main/utils/process.ts @@ -29,12 +29,16 @@ export function runInstallScript(scriptPath: string): Promise { env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' } }) + let stderrOutput = ''; + nodeProcess.stdout.on('data', (data) => { log.info(`Script output: ${data}`) }) nodeProcess.stderr.on('data', (data) => { - log.error(`Script error: ${data}`) + const errorMsg = data.toString(); + stderrOutput += errorMsg; + log.error(`Script error: ${errorMsg}`) }) nodeProcess.on('close', (code) => { @@ -43,7 +47,9 @@ export function runInstallScript(scriptPath: string): Promise { resolve(true) } else { log.error(`Script exited with code ${code}`) - reject(false) + // Reject with detailed error message instead of just false + const errorMessage = stderrOutput.trim() || `Script exited with code ${code}`; + reject(new Error(errorMessage)) } }) }) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ae81e2565..7ef37abf1 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld('electronAPI', { checkAndInstallDepsOnUpdate: () => ipcRenderer.invoke('install-dependencies'), checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'), getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'), + restartBackend: () => ipcRenderer.invoke('restart-backend'), onInstallDependenciesStart: (callback: () => void) => { ipcRenderer.on('install-dependencies-start', callback); }, diff --git a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx index bdfd2c1a0..3461f0147 100644 --- a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx +++ b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx @@ -12,17 +12,45 @@ import React from "react"; interface InstallationErrorDialogProps { error: string; + backendError?: string; installationState: string; latestLog: any; retryInstallation: () => void; + retryBackend?: () => void; } const InstallationErrorDialog = ({ error, + backendError, installationState, latestLog, retryInstallation, + retryBackend, }:InstallationErrorDialogProps) => { + // Show backend error dialog if backendError exists + if (backendError) { + return ( + + + + {t("layout.backend-startup-failed")} + +
+
+ + {backendError} + +
+
+ + + +
+
+ ); + } + + // Show installation error dialog if installation state is error return ( @@ -30,14 +58,11 @@ const InstallationErrorDialog = ({ {t("layout.installation-failed")}
- { -
- - Error: {error}
- Log: {latestLog?.data} -
-
- } +
+ + {error} + +
diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 0fc54fec5..a3fcd7629 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -29,9 +29,11 @@ const Layout = () => { installationState, latestLog, error, + backendError, isInstalling, shouldShowInstallScreen, retryInstallation, + retryBackend, } = useInstallationUI(); // Setup installation IPC listeners and state synchronization @@ -111,13 +113,17 @@ const Layout = () => { )} - {(error != "" && error !=undefined) && + {/* Backend error dialog */} + {(backendError || (error && installationState === "error")) && ( - } + retryInstallation={retryInstallation} + retryBackend={retryBackend} + /> + )} { const addLog = useInstallationStore(state => state.addLog); const setSuccess = useInstallationStore(state => state.setSuccess); const setError = useInstallationStore(state => state.setError); + const setBackendError = useInstallationStore(state => state.setBackendError); const setWaitingBackend = useInstallationStore(state => state.setWaitingBackend); // REMOVED: Don't reset initState from 'done' to 'carousel' @@ -162,7 +163,8 @@ export const useInstallationSetup = () => { checkAndSetDone(); } else { console.error('[useInstallationSetup] Backend failed to start:', data.error); - setError(data.error || 'Backend startup failed'); + // Use setBackendError instead of setError for backend startup failures + setBackendError(data.error || 'Backend startup failed'); } }; @@ -179,5 +181,5 @@ export const useInstallationSetup = () => { window.electronAPI.removeAllListeners('install-dependencies-complete'); window.electronAPI.removeAllListeners('backend-ready'); }; - }, [startInstallation, addLog, setSuccess, setError, setInitState]); + }, [startInstallation, addLog, setSuccess, setError, setBackendError, setInitState]); }; \ No newline at end of file diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index fb8b27fe4..453444a19 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -29,6 +29,7 @@ "continue-with-google-login": "Continue with Google", "continue-with-github-login": "Continue with Github", "installation-failed": "Installation Failed", + "backend-startup-failed": "Backend Startup Failed", "projects": "Projects", "mcp-tools": "MCP & Tools", "browser": "Browser", diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index ae6c384f2..c64b89a91 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -31,6 +31,7 @@ "continue-with-google-login": "使用 Google 登录", "continue-with-github-login": "使用 Github 登录", "installation-failed": "安装失败", + "backend-startup-failed": "后端启动失败", "projects": "项目", "mcp-tools": "MCP & 工具", "browser": "浏览器", diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts index 4ea996db3..fd88ffb30 100644 --- a/src/store/installationStore.ts +++ b/src/store/installationStore.ts @@ -25,20 +25,23 @@ interface InstallationStoreState { progress: number; logs: InstallationLog[]; error?: string; + backendError?: string; // Separate error for backend startup failures isVisible: boolean; - + // Actions startInstallation: () => void; addLog: (log: InstallationLog) => void; setSuccess: () => void; setError: (error: string) => void; + setBackendError: (error: string) => void; setWaitingBackend: () => void; retryInstallation: () => void; + retryBackend: () => Promise; completeSetup: () => void; updateProgress: (progress: number) => void; setVisible: (visible: boolean) => void; reset: () => void; - + // Async actions performInstallation: () => Promise; exportLog: () => Promise; @@ -50,6 +53,7 @@ const initialState = { progress: 20, logs: [] as InstallationLog[], error: undefined, + backendError: undefined, isVisible: false, }; @@ -106,6 +110,12 @@ export const useInstallationStore = create()( isVisible: true, }), + setBackendError: (error: string) => + set({ + backendError: error, + state: 'error', + }), + retryInstallation: () => { set({ ...initialState, @@ -114,7 +124,34 @@ export const useInstallationStore = create()( }); get().performInstallation(); }, - + + retryBackend: async () => { + try { + // Clear backend error and go back to waiting-backend state + set({ + backendError: undefined, + state: 'waiting-backend', + progress: 80, + }); + + // Call restart-backend via electronAPI + const result = await window.electronAPI.restartBackend(); + + if (!result.success) { + set({ + backendError: result.error || 'Failed to restart backend', + state: 'error', + }); + } + // If successful, backend-ready event will be triggered automatically + } catch (error) { + set({ + backendError: error instanceof Error ? error.message : 'Unknown error', + state: 'error', + }); + } + }, + completeSetup: () => set({ state: 'completed', @@ -211,21 +248,25 @@ export const useInstallationUI = () => { const progress = useInstallationStore(state => state.progress); const logs = useInstallationStore(state => state.logs); const error = useInstallationStore(state => state.error); + const backendError = useInstallationStore(state => state.backendError); const isVisible = useInstallationStore(state => state.isVisible); const performInstallation = useInstallationStore(state => state.performInstallation); const retryInstallation = useInstallationStore(state => state.retryInstallation); + const retryBackend = useInstallationStore(state => state.retryBackend); const exportLog = useInstallationStore(state => state.exportLog); - + return { installationState: state, progress, latestLog: logs[logs.length - 1], error, + backendError, isInstalling: state === 'installing', shouldShowInstallScreen: isVisible && state !== 'completed', canRetry: state === 'error', performInstallation, retryInstallation, + retryBackend, exportLog, }; }; \ No newline at end of file diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index e5bd5bb6a..116a06a8f 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -47,14 +47,15 @@ interface ElectronAPI { executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }>; checkAndInstallDepsOnUpdate: () => Promise<{ success: boolean; error?: string }>; checkInstallBrowser: () => Promise<{ data:any[] }>; - getInstallationStatus: () => Promise<{ - success: boolean; - isInstalling?: boolean; + getInstallationStatus: () => Promise<{ + success: boolean; + isInstalling?: boolean; hasLockFile?: boolean; installedExists?: boolean; timestamp?: number; - error?: string + error?: string }>; + restartBackend: () => Promise<{ success: boolean; error?: string }>; onInstallDependenciesStart: (callback: () => void) => void; onInstallDependenciesLog: (callback: (data: { type: string; data: string }) => void) => void; onInstallDependenciesComplete: (callback: (data: { success: boolean; code?: number; error?: string }) => void) => void; From 8d390026ea9d7d7599fde56cd3f6effc2b063dab Mon Sep 17 00:00:00 2001 From: a7m-1st Date: Thu, 20 Nov 2025 17:41:00 +0300 Subject: [PATCH 21/36] fix: edit task not deleting history --- src/components/ChatBox/index.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index 41c66f352..b193bd132 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -461,7 +461,13 @@ export default function ChatBox(): JSX.Element { chatStore.setAttaches(id, attachments); } chatStore.removeTask(taskId); - proxyFetchDelete(`/api/chat/history/${taskId}`); + const history_id = projectStore.getHistoryId(projectStore.activeProjectId); + try { + if(!history_id) throw new Error("No history ID found for the project"); + proxyFetchDelete(`/api/chat/history/${history_id}`); + } catch(error) { + console.error("Failed to delete chat history:", error); + } setMessage(question); }; From 17b92e7f33d545291c45443471bc3037cdaa91f0 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 22:53:26 +0800 Subject: [PATCH 22/36] update --- electron/main/init.ts | 41 +++++-------------- electron/main/install-deps.ts | 3 -- electron/main/utils/process.ts | 1 - .../InstallationErrorDialog.tsx | 2 - src/components/Layout/index.tsx | 30 -------------- src/hooks/useInstallationSetup.ts | 38 +---------------- src/store/installationStore.ts | 11 +---- 7 files changed, 13 insertions(+), 113 deletions(-) diff --git a/electron/main/init.ts b/electron/main/init.ts index 36f34006b..f1bfec165 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -163,27 +163,24 @@ export async function startBackend(setPort?: (port: number) => void): Promise { if (!data) return; const msg = data.toString().trimEnd(); - //Detect if uv sync is run detectInstallationLogs(msg); if (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback")) { log.error(`BACKEND: ${msg}`); } else if (msg.toLowerCase().includes("warn")) { - //Skip Warnings - // log.warn(`BACKEND: ${msg}`); + // Skip warnings } else if (msg.includes("DEBUG")) { log.debug(`BACKEND: ${msg}`); } else { - log.info(`BACKEND: ${msg}`); // treat uvicorn info logs as normal + log.info(`BACKEND: ${msg}`); } } @@ -192,7 +189,6 @@ export async function startBackend(setPort?: (port: number) => void): Promise void): Promise void): Promise { if (node_process.killed) { log.error('Backend process was killed immediately after spawn'); @@ -246,40 +239,31 @@ export async function startBackend(setPort?: (port: number) => void): Promise { if (!started) { log.info('Starting backend health check polling...'); pollHealthEndpoint(); } - }, 2000); // 2 second delay for process initialization + }, 2000); - // Helper function to kill backend process and all children const killBackendProcess = (proc: any) => { if (!proc || !proc.pid) return; log.info(`Killing backend process ${proc.pid} and its children...`); try { if (process.platform === 'win32') { - // Windows: use taskkill to kill process tree spawn('taskkill', ['/pid', proc.pid.toString(), '/T', '/F']); } else { - // Unix: kill the entire process group try { process.kill(-proc.pid, 'SIGTERM'); - // Give it 1 second, then SIGKILL setTimeout(() => { try { process.kill(-proc.pid, 'SIGKILL'); - } catch (e) { - // Process already dead, ignore - } + } catch (e) {} }, 1000); } catch (e) { - // If process group kill fails, kill the main process log.error(`Failed to kill process group: ${e}`); proc.kill('SIGKILL'); } @@ -289,10 +273,9 @@ export async function startBackend(setPort?: (port: number) => void): Promise { let attempts = 0; - const maxAttempts = 240; // 60 seconds total (240 * 250ms) - increased for first run + const maxAttempts = 240; const intervalMs = 250; healthCheckInterval = setInterval(() => { @@ -346,22 +329,19 @@ export async function startBackend(setPort?: (port: number) => void): Promise { log.debug(`Backend stdout received ${data.length} bytes`); displayFilteredLogs(data); }); - // Monitor stderr for logs and errors node_process.stderr.on('data', (data) => { log.debug(`Backend stderr received ${data.length} bytes`); displayFilteredLogs(data); - // Check for port binding errors - critical failure if (data.toString().includes("Address already in use") || data.toString().includes("bind() failed")) { if (!started) { - started = true; // Prevent multiple rejections + started = true; clearTimeout(startTimeout); clearTimeout(initialDelay); if (healthCheckInterval) clearInterval(healthCheckInterval); @@ -388,7 +368,6 @@ export async function startBackend(setPort?: (port: number) => void): Promise { }; } } catch (scriptError) { - // Capture detailed error from runInstallScript const errorMsg = `${toolName} installation failed: ${scriptError instanceof Error ? scriptError.message : String(scriptError)}`; safeMainWindowSend('install-dependencies-complete', { success: false, @@ -183,7 +182,6 @@ export async function installCommandTool(): Promise { } catch (error) { const errorMessage = `Command tool installation failed: ${error}`; log.error('[DEPS INSTALL] Exception during command tool installation:', error); - // Send failure event to frontend safeMainWindowSend('install-dependencies-complete', { success: false, code: 2, @@ -626,7 +624,6 @@ export async function installDependencies(version: string): Promise { resolve(true) } else { log.error(`Script exited with code ${code}`) - // Reject with detailed error message instead of just false const errorMessage = stderrOutput.trim() || `Script exited with code ${code}`; reject(new Error(errorMessage)) } diff --git a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx index 3461f0147..a27e9246e 100644 --- a/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx +++ b/src/components/InstallStep/InstallationErrorDialog/InstallationErrorDialog.tsx @@ -27,7 +27,6 @@ const InstallationErrorDialog = ({ retryInstallation, retryBackend, }:InstallationErrorDialogProps) => { - // Show backend error dialog if backendError exists if (backendError) { return ( @@ -50,7 +49,6 @@ const InstallationErrorDialog = ({ ); } - // Show installation error dialog if installation state is error return ( diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index a3fcd7629..8e0a78142 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -36,31 +36,8 @@ const Layout = () => { retryBackend, } = useInstallationUI(); - // Setup installation IPC listeners and state synchronization useInstallationSetup(); - // Additional check: If initState is carousel but tools are installed, skip to done - // REMOVED: This check is too aggressive and causes premature navigation to main content - // The proper flow should be: installation complete -> backend ready -> then set initState='done' - // This is now handled in useInstallationSetup.ts - // useEffect(() => { - // const checkAndSkipCarousel = async () => { - // if (initState === 'carousel' && !isInstalling) { - // try { - // const result = await window.ipcRenderer.invoke("check-tool-installed"); - // if (result.success && result.isInstalled) { - // console.log('[Layout] Tools installed, skipping carousel and setting initState to done'); - // setInitState('done'); - // } - // } catch (error) { - // console.error('[Layout] Failed to check tool installation:', error); - // } - // } - // }; - // - // checkAndSkipCarousel(); - // }, [initState, isInstalling, setInitState]); - useEffect(() => { const handleBeforeClose = () => { const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status; @@ -81,13 +58,7 @@ const Layout = () => { // Determine what to show based on states const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling; - // Show install screen if either: - // 1. The installation store says to show it (isVisible && not completed) - // 2. OR if initState is not 'done' (meaning permissions or carousel should show) - // 3. OR if waiting for backend (installationState === 'waiting-backend') const actualShouldShowInstallScreen = shouldShowInstallScreen || initState !== 'done' || installationState === 'waiting-backend'; - - // Only show main content when installation is complete (initState === 'done' AND not waiting for backend) const shouldShowMainContent = !actualShouldShowInstallScreen; return ( @@ -113,7 +84,6 @@ const Layout = () => { )} - {/* Backend error dialog */} {(backendError || (error && installationState === "error")) && ( { const { initState, setInitState } = useAuthStore(); - // Use ref to track if initial check is done to prevent repeated checks const hasCheckedOnMount = useRef(false); - - // Track installation and backend readiness states const installationCompleted = useRef(false); const backendReady = useRef(false); - - // Extract only the functions we need to avoid dependency issues const startInstallation = useInstallationStore(state => state.startInstallation); const performInstallation = useInstallationStore(state => state.performInstallation); const addLog = useInstallationStore(state => state.addLog); @@ -25,14 +20,7 @@ export const useInstallationSetup = () => { const setBackendError = useInstallationStore(state => state.setBackendError); const setWaitingBackend = useInstallationStore(state => state.setWaitingBackend); - // REMOVED: Don't reset initState from 'done' to 'carousel' - // Instead, we'll use installationState to control visibility in Layout component - // When tools are already installed, we set installationState to 'waiting-backend' - // which will show progress bar + text without showing carousel slides - - // Check tool installation status on mount - but only during setup phase useEffect(() => { - // Only run this check once on initial mount if (hasCheckedOnMount.current) { return; } @@ -44,19 +32,14 @@ export const useInstallationSetup = () => { const result = await window.ipcRenderer.invoke("check-tool-installed"); if (result.success) { - // If tools are already installed, mark installation as completed - // This handles the app restart scenario where tools were installed previously if (result.isInstalled) { console.log('[useInstallationSetup] Tools already installed, waiting for backend'); installationCompleted.current = true; - setWaitingBackend(); // Show "waiting for backend" state (progress bar + text, no carousel) + setWaitingBackend(); } - // Only perform state transitions during setup phase (permissions or carousel) - // Once user is in 'done' state (main app), don't change initState if (initState !== 'done') { if (!result.isInstalled && initState === "permissions") { - // If tools are NOT installed and we're in permissions state, set to carousel console.log('[useInstallationSetup] Tools not installed and initState is permissions, setting to carousel'); setInitState("carousel"); } @@ -71,13 +54,11 @@ export const useInstallationSetup = () => { const checkBackendStatus = async(toolResult?: any) => { try { - // Also check if installation is currently in progress const installationStatus = await window.electronAPI.getInstallationStatus(); if (installationStatus.success && installationStatus.isInstalling) { startInstallation(); } else if (initState !== 'done' && toolResult) { - // Use the tool result from the previous check to avoid duplicate API calls if (toolResult.success && !toolResult.isInstalled) { console.log('[useInstallationSetup] Tools missing and not installing. Starting installation...'); try { @@ -92,7 +73,6 @@ export const useInstallationSetup = () => { } } - // Run checks sequentially to avoid race conditions and duplicate API calls const runInitialChecks = async () => { const toolResult = await checkToolInstalled(); await checkBackendStatus(toolResult); @@ -102,9 +82,7 @@ export const useInstallationSetup = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Setup Electron IPC listeners (only once) useEffect(() => { - // Helper function to check if both installation and backend are ready const checkAndSetDone = () => { console.log('[useInstallationSetup] Checking readiness - Installation:', installationCompleted.current, 'Backend:', backendReady.current); @@ -114,9 +92,7 @@ export const useInstallationSetup = () => { } }; - // Electron IPC event handlers const handleInstallStart = () => { - // Reset flags when installation starts installationCompleted.current = false; backendReady.current = false; startInstallation(); @@ -137,11 +113,7 @@ export const useInstallationSetup = () => { installationCompleted.current = true; console.log('[useInstallationSetup] Installation marked as completed'); - // Don't call setSuccess() yet if we're still waiting for backend - // setSuccess() will be called in handleBackendReady when backend is ready - // This prevents installationState from changing from 'waiting-backend' to 'completed' prematurely - - // Only set initState to done if backend is also ready + // setSuccess() will be called in handleBackendReady to prevent premature state change checkAndSetDone(); } else { setError(data.error || 'Installation failed'); @@ -156,25 +128,19 @@ export const useInstallationSetup = () => { backendReady.current = true; console.log('[useInstallationSetup] Backend marked as ready'); - // Mark installation as completed (changes state from 'waiting-backend' to 'completed') setSuccess(); - - // Only set initState to done if installation is also completed checkAndSetDone(); } else { console.error('[useInstallationSetup] Backend failed to start:', data.error); - // Use setBackendError instead of setError for backend startup failures setBackendError(data.error || 'Backend startup failed'); } }; - // Register Electron IPC listeners window.electronAPI.onInstallDependenciesStart(handleInstallStart); window.electronAPI.onInstallDependenciesLog(handleInstallLog); window.electronAPI.onInstallDependenciesComplete(handleInstallComplete); window.electronAPI.onBackendReady(handleBackendReady); - // Cleanup listeners on unmount return () => { window.electronAPI.removeAllListeners('install-dependencies-start'); window.electronAPI.removeAllListeners('install-dependencies-log'); diff --git a/src/store/installationStore.ts b/src/store/installationStore.ts index fd88ffb30..d5e9e9d15 100644 --- a/src/store/installationStore.ts +++ b/src/store/installationStore.ts @@ -143,7 +143,6 @@ export const useInstallationStore = create()( state: 'error', }); } - // If successful, backend-ready event will be triggered automatically } catch (error) { set({ backendError: error instanceof Error ? error.message : 'Unknown error', @@ -176,16 +175,8 @@ export const useInstallationStore = create()( const result = await window.electronAPI.checkAndInstallDepsOnUpdate(); if (result.success) { - // ✅ FIXED: Don't set initState='done' here! - // The proper flow is: - // 1. Installation completes → install-dependencies-complete event - // 2. Backend starts → backend-ready event - // 3. useInstallationSetup receives both events → sets initState='done' - // This ensures backend is ready before showing main page + // initState will be set to 'done' by useInstallationSetup after both installation and backend are ready setSuccess(); - - // Note: initState will be set to 'done' by useInstallationSetup.ts - // after both installation complete AND backend ready events are received } else { setError('Installation failed'); } From bac56520823ee5d76ab95231dbac949bec579a88 Mon Sep 17 00:00:00 2001 From: puzhen <1303385763@qq.com> Date: Thu, 20 Nov 2025 23:17:37 +0800 Subject: [PATCH 23/36] update --- electron/main/install-deps.ts | 112 ---------------------------------- 1 file changed, 112 deletions(-) diff --git a/electron/main/install-deps.ts b/electron/main/install-deps.ts index cb10b8c9f..7156b1c1f 100644 --- a/electron/main/install-deps.ts +++ b/electron/main/install-deps.ts @@ -677,116 +677,4 @@ export async function installDependencies(version: string): Promise - msg.includes(pattern) && !msg.includes("Uvicorn running on") - )) { - dependencyInstallationDetected = true; - log.info('[BACKEND STARTUP] UV dependency installation detected during uvicorn startup'); - - // Create installing lock file to maintain consistency with install-deps.ts - InstallLogs.setLockPath(); - log.info('[BACKEND STARTUP] Created uv_installing.lock file'); - - // Notify frontend that installation has started (only once) - if (!installationNotificationSent) { - installationNotificationSent = true; - const notificationSent = safeMainWindowSend('install-dependencies-start'); - if (notificationSent) { - log.info('[BACKEND STARTUP] Notified frontend of dependency installation start'); - } else { - log.warn('[BACKEND STARTUP] Failed to notify frontend of dependency installation start'); - } - } - } - - // Send installation logs to frontend if installation was detected - if (dependencyInstallationDetected && !msg.includes("Uvicorn running on")) { - safeMainWindowSend('install-dependencies-log', { - type: msg.toLowerCase().includes("error") || msg.toLowerCase().includes("traceback") ? 'stderr' : 'stdout', - data: msg - }); - } - - // Check if installation is complete (uvicorn starts successfully) - if (dependencyInstallationDetected && msg.includes("Uvicorn running on")) { - log.info('[BACKEND STARTUP] UV dependency installation completed, uvicorn started successfully'); - - // Clean up installing lock and create installed lock - InstallLogs.cleanLockPath(); - fs.writeFileSync(installedLockPath, ''); - log.info('[BACKEND STARTUP] Created uv_installed.lock file'); - - safeMainWindowSend('install-dependencies-complete', { - success: true, - message: 'Dependencies installed successfully during backend startup' - }); - } - - // Handle installation failures - if (dependencyInstallationDetected && ( - msg.toLowerCase().includes("failed to resolve dependencies") || - msg.toLowerCase().includes("installation failed") || - msg.includes("× No solution found when resolving dependencies") - )) { - log.error('[BACKEND STARTUP] UV dependency installation failed'); - - // Clean up installing lock file - InstallLogs.cleanLockPath(); - log.info('[BACKEND STARTUP] Cleaned up uv_installing.lock file after failure'); - - safeMainWindowSend('install-dependencies-complete', { - success: false, - error: 'Dependency installation failed during backend startup' - }); - } } \ No newline at end of file From 973fc385fdbf6d2313361d2f9ace0b5bf44c722d Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 00:36:45 +0800 Subject: [PATCH 24/36] Update chat_controller.py --- backend/app/controller/chat_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index e85fbb275..068db9db2 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -58,7 +58,7 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI yield data except asyncio.TimeoutError: chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection") - yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"}) + # TODO: Temporary change: suppress error signal to frontend on timeout. Needs proper fix later. break except StopAsyncIteration: break From 1b00270a23f9d689be2c3494453d645825e33468 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Fri, 21 Nov 2025 00:38:34 +0800 Subject: [PATCH 25/36] Update chat_controller.py --- backend/app/controller/chat_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index 068db9db2..a983acfd2 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -58,6 +58,7 @@ async def timeout_stream_wrapper(stream_generator, timeout_seconds: int = SSE_TI yield data except asyncio.TimeoutError: chat_logger.warning(f"SSE timeout: No data received for {timeout_seconds} seconds, closing connection") + # yield sse_json("error", {"message": "Connection timeout: No data received for 10 minutes"}) # TODO: Temporary change: suppress error signal to frontend on timeout. Needs proper fix later. break except StopAsyncIteration: From da1cc7c23be4d422ae53c29243edc088ed05c825 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 01:12:44 +0800 Subject: [PATCH 26/36] enhance delete logic --- src/components/ChatBox/index.tsx | 41 +++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index b193bd132..78cec6aa9 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -444,9 +444,17 @@ export default function ChatBox(): JSX.Element { }; // Edit query handler - const handleEditQuery = () => { + const handleEditQuery = async () => { const taskId = chatStore.activeTaskId as string; - fetchDelete(`/chat/${taskId}`); + const projectId = projectStore.activeProjectId; + + // Early validation + if (!projectId) { + console.error("No active project ID found for edit operation"); + return; + } + + // Get question and attachments before any deletions const messageIndex = chatStore.tasks[taskId].messages.findLastIndex( (item) => item.step === "to_sub_tasks" ); @@ -454,6 +462,28 @@ export default function ChatBox(): JSX.Element { const question = questionMessage.content; // Get the file attachments from the original user message (not from task.attaches which gets cleared after sending) const attachments = questionMessage.attaches || []; + + // Delete task from backend first + try { + await fetchDelete(`/chat/${taskId}`); + } catch (error) { + console.error("Failed to delete task from backend:", error); + // Continue with local cleanup even if backend fails + } + + // Delete chat history + const history_id = projectStore.getHistoryId(projectId); + if (history_id) { + try { + await proxyFetchDelete(`/api/chat/history/${history_id}`); + } catch(error) { + console.error(`Failed to delete chat history (ID: ${history_id}) for project ${projectId}:`, error); + } + } else { + console.warn(`No history ID found for project ${projectId} during edit operation`); + } + + // Create new task and clean up locally let id = chatStore.create(); chatStore.setHasMessages(id, true); // Copy the file attachments to the new task @@ -461,13 +491,6 @@ export default function ChatBox(): JSX.Element { chatStore.setAttaches(id, attachments); } chatStore.removeTask(taskId); - const history_id = projectStore.getHistoryId(projectStore.activeProjectId); - try { - if(!history_id) throw new Error("No history ID found for the project"); - proxyFetchDelete(`/api/chat/history/${history_id}`); - } catch(error) { - console.error("Failed to delete chat history:", error); - } setMessage(question); }; From 9635ad22aefc267b174f0015d53a8f1dd0999d38 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Fri, 21 Nov 2025 01:14:53 +0800 Subject: [PATCH 27/36] 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 28/36] 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" ? (