diff --git a/.env.development b/.env.development index 13a36dcc7..3411011bd 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,5 @@ VITE_BASE_URL=/api -VITE_PROXY_URL=https://dev.eigent.ai +VITE_PROXY_URL=http://localhost:3001 -VITE_USE_LOCAL_PROXY=false \ No newline at end of file +VITE_USE_LOCAL_PROXY=true diff --git a/.gitignore b/.gitignore index 7fdb72658..7d652a0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ yarn.lock .env .env.local .env.production +.env.development .cursor diff --git a/electron/main/index.ts b/electron/main/index.ts index a7efe8cb8..096671fc0 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -4,7 +4,7 @@ import path from 'node:path' import os, { homedir } from 'node:os' import log from 'electron-log' import { update, registerUpdateIpcHandlers } from './update' -import { checkToolInstalled, installDependencies, startBackend } from './init' +import { checkToolInstalled, installDependencies, killProcessOnPort, startBackend } from './init' import { WebViewManager } from './webview' import { FileReader } from './fileReader' import { ChildProcessWithoutNullStreams } from 'node:child_process' @@ -15,6 +15,9 @@ import { getEnvPath, updateEnvBlock, removeEnvKey, getEmailFolderPath } from './ import { copyBrowserData } from './copy' import { findAvailablePort } from './init' import kill from 'tree-kill'; +import { zipFolder } from './utils/log' +import axios from 'axios'; +import FormData from 'form-data'; const userData = app.getPath('userData'); const versionFile = path.join(userData, 'version.txt'); @@ -318,7 +321,7 @@ function registerIpcHandlers() { }); ipcMain.handle('execute-command', async (event, command: string, email: string) => { log.info("execute-command", command); - const {MCP_REMOTE_CONFIG_DIR} = getEmailFolderPath(email); + const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); try { const { spawn } = await import('child_process'); @@ -440,6 +443,72 @@ function registerIpcHandlers() { } }); + ipcMain.handle('upload-log', async (event, email: string, taskId: string, baseUrl: string, token: string) => { + let zipPath: string | null = null; + + try { + // Validate required parameters + if (!email || !taskId || !baseUrl || !token) { + return { success: false, error: 'Missing required parameters' }; + } + + // Sanitize taskId to prevent path traversal attacks + const sanitizedTaskId = taskId.replace(/[^a-zA-Z0-9_-]/g, ''); + if (!sanitizedTaskId) { + return { success: false, error: 'Invalid task ID' }; + } + + const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); + const logFolderName = `task_${sanitizedTaskId}`; + const logFolderPath = path.join(MCP_REMOTE_CONFIG_DIR, logFolderName); + + // Check if log folder exists + if (!fs.existsSync(logFolderPath)) { + return { success: false, error: 'Log folder not found' }; + } + + zipPath = path.join(MCP_REMOTE_CONFIG_DIR, `${logFolderName}.zip`); + await zipFolder(logFolderPath, zipPath); + + // Create form data with file stream + const formData = new FormData(); + const fileStream = fs.createReadStream(zipPath); + formData.append('file', fileStream); + formData.append('task_id', sanitizedTaskId); + + // Upload with timeout + const response = await axios.post(baseUrl + '/api/chat/logs', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': `Bearer ${token}` + }, + timeout: 60000, // 60 second timeout + maxContentLength: Infinity, + maxBodyLength: Infinity + }); + + fileStream.destroy(); + + if (response.status === 200) { + return { success: true, data: response.data }; + } else { + return { success: false, error: response.data }; + } + } catch (error: any) { + log.error('Failed to upload log:', error); + return { success: false, error: error.message || 'Upload failed' }; + } finally { + // Clean up zip file + if (zipPath && fs.existsSync(zipPath)) { + try { + fs.unlinkSync(zipPath); + } catch (cleanupError) { + log.error('Failed to clean up zip file:', cleanupError); + } + } + } + }); + // ==================== MCP manage handler ==================== ipcMain.handle('mcp-install', async (event, name, mcp) => { addMcp(name, mcp); @@ -609,7 +678,7 @@ function registerIpcHandlers() { // ==================== delete folder handler ==================== ipcMain.handle('delete-folder', async (event, email: string) => { - const {MCP_REMOTE_CONFIG_DIR} = getEmailFolderPath(email); + const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); try { log.info('Deleting folder:', MCP_REMOTE_CONFIG_DIR); @@ -646,7 +715,7 @@ function registerIpcHandlers() { // ==================== get MCP config path handler ==================== ipcMain.handle('get-mcp-config-path', async (event, email: string) => { try { - const {MCP_REMOTE_CONFIG_DIR,tempEmail} = getEmailFolderPath(email); + const { MCP_REMOTE_CONFIG_DIR, tempEmail } = getEmailFolderPath(email); log.info('Getting MCP config path for email:', email); log.info('MCP config path:', MCP_REMOTE_CONFIG_DIR); return { @@ -664,7 +733,7 @@ function registerIpcHandlers() { }); // ==================== env handler ==================== - + ipcMain.handle('get-env-path', async (_event, email) => { return getEnvPath(email); }); @@ -957,6 +1026,7 @@ const checkAndStartBackend = async () => { }); python_process?.on('exit', (code, signal) => { + log.info('Python process exited', { code, signal }); }); } else { @@ -965,20 +1035,41 @@ const checkAndStartBackend = async () => { }; // ==================== process cleanup ==================== -const cleanupPythonProcess = () => { +const cleanupPythonProcess = async () => { try { + // First attempt: Try to kill using PID if (python_process?.pid) { - log.info('Cleaning up Python process', { pid: python_process.pid }); - kill(python_process.pid, 'SIGINT', (err) => { - if (err) { - log.error('Failed to clean up process tree:', err); - } else { - log.info('Successfully cleaned up Python process tree'); - } + const pid = python_process.pid; + log.info('Cleaning up Python process', { pid }); + + await new Promise((resolve) => { + kill(pid, 'SIGINT', (err) => { + if (err) { + log.error('Failed to clean up process tree:', err); + } else { + log.info('Successfully cleaned up Python process tree'); + } + resolve(); + }); }); - } else { - log.info('No Python process to clean up'); } + + // Second attempt: Use port-based cleanup as fallback + const portFile = path.join(userData, 'port.txt'); + if (fs.existsSync(portFile)) { + try { + const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10); + if (!isNaN(port) && port > 0 && port < 65536) { + log.info(`Attempting to kill process on port: ${port}`); + await killProcessOnPort(port); + } + fs.unlinkSync(portFile); + } catch (error) { + log.error('Error handling port file:', error); + } + } + + python_process = null; } catch (error) { log.error('Error occurred while cleaning up process:', error); } diff --git a/electron/main/init.ts b/electron/main/init.ts index bf54b8923..70dbb7824 100644 --- a/electron/main/init.ts +++ b/electron/main/init.ts @@ -423,7 +423,7 @@ function checkPortAvailable(port: number): Promise { }); } -async function killProcessOnPort(port: number): Promise { +export async function killProcessOnPort(port: number): Promise { try { const platform = process.platform; let command: string; diff --git a/electron/main/utils/envUtil.ts b/electron/main/utils/envUtil.ts index 82e2bc531..850c40cbc 100644 --- a/electron/main/utils/envUtil.ts +++ b/electron/main/utils/envUtil.ts @@ -90,9 +90,9 @@ export function getEmailFolderPath(email: string) { hasToken = false; } } catch (error) { - console.log("error", error); hasToken = false; } return { MCP_REMOTE_CONFIG_DIR, MCP_CONFIG_DIR, tempEmail, hasToken }; -} \ No newline at end of file +} + diff --git a/electron/main/utils/log.ts b/electron/main/utils/log.ts new file mode 100644 index 000000000..8d8e03bfd --- /dev/null +++ b/electron/main/utils/log.ts @@ -0,0 +1,25 @@ +import fs from 'node:fs' +// @ts-ignore +import archiver from 'archiver' +import log from 'electron-log' + +export function zipFolder(folderPath: string, outputZipPath: string): Promise { + return new Promise((resolve, reject) => { + const output = fs.createWriteStream(outputZipPath) + const archive = archiver('zip', { zlib: { level: 9 } }) + + output.on('close', () => resolve(outputZipPath)) + + archive.on('error', (err: any) => { + log.error('Archive error:', err); + reject(err); + }) + + archive.pipe(output) + archive.directory(folderPath, false) + archive.finalize() + }) +} + + + diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 5c91e46ca..17525d191 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -44,6 +44,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getShowWebview: () => ipcRenderer.invoke('get-show-webview'), webviewDestroy: (webviewId: string) => ipcRenderer.invoke('webview-destroy', webviewId), exportLog: () => ipcRenderer.invoke('export-log'), + uploadLog: (email: string, taskId: string, baseUrl: string, token: string) => ipcRenderer.invoke('upload-log', email, taskId, baseUrl, token), // mcp mcpInstall: (name: string, mcp: any) => ipcRenderer.invoke('mcp-install', name, mcp), mcpRemove: (name: string) => ipcRenderer.invoke('mcp-remove', name), diff --git a/package.json b/package.json index 1911f73df..4bc699bb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eigent", - "version": "0.0.46", + "version": "0.0.47", "main": "dist-electron/main/index.js", "description": "Eigent", "author": "Eigent.AI", @@ -49,6 +49,7 @@ "@xterm/xterm": "^5.5.0", "@xyflow/react": "^12.6.4", "adm-zip": "^0.5.16", + "archiver": "^7.0.1", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -82,6 +83,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.2", + "@types/archiver": "^6.0.3", "@types/lodash-es": "^4.17.12", "@types/papaparse": "^5.3.16", "@types/react": "^18.3.12", diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index 6e887a466..53b5b1fb7 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -1,5 +1,14 @@ import { useState, useRef, useEffect, useMemo } from "react"; -import { Settings, Minus, Square, X, FileDown, Menu, Plus } from "lucide-react"; +import { + Settings, + Minus, + Square, + X, + FileDown, + Menu, + Plus, + Import, +} from "lucide-react"; import "./index.css"; import folderIcon from "@/assets/Folder.svg"; import { Button } from "@/components/ui/button"; @@ -7,6 +16,7 @@ import { useLocation, useNavigate } from "react-router-dom"; import { useChatStore } from "@/store/chatStore"; import { useSidebarStore } from "@/store/sidebarStore"; import chevron_left from "@/assets/chevron_left.svg"; +import { getAuthStore } from "@/store/authStore"; function HeaderWin() { const titlebarRef = useRef(null); const controlsRef = useRef(null); @@ -16,6 +26,7 @@ function HeaderWin() { const chatStore = useChatStore(); const { toggle } = useSidebarStore(); const [isFullscreen, setIsFullscreen] = useState(false); + const { token } = getAuthStore(); useEffect(() => { const p = window.electronAPI.getPlatform(); setPlatform(p); @@ -140,20 +151,20 @@ function HeaderWin() { <> {activeTaskTitle === "New Project" ? ( + variant="ghost" + size="sm" + className="font-bold text-base no-drag" + onClick={createNewProject} + > + {activeTaskTitle} + ) : ( -
{activeTaskTitle}
+
+ {activeTaskTitle} +
)} )} - -
{/* right */} diff --git a/src/lib/index.ts b/src/lib/index.ts index 2977cf91a..0e4d34a0a 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,5 @@ +import { getAuthStore } from "@/store/authStore" + export function getProxyBaseURL() { const isDev = import.meta.env.DEV @@ -56,4 +58,22 @@ export function hasStackKeys() { return import.meta.env.VITE_STACK_PROJECT_ID && import.meta.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY && import.meta.env.VITE_STACK_SECRET_SERVER_KEY; +} + +export async function uploadLog(taskId: string, type?: string | undefined) { + if (import.meta.env.VITE_USE_LOCAL_PROXY !== "true" && !type) { + try { + const { email, token } = getAuthStore() + const baseUrl = import.meta.env.DEV ? import.meta.env.VITE_PROXY_URL : import.meta.env.VITE_BASE_URL + + await window.electronAPI.uploadLog( + email, + taskId, + baseUrl, + token + ); + } catch (error) { + console.error('Failed to upload log:', error); + } + } } \ No newline at end of file diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 2273deae2..b4a97a9ea 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -1,11 +1,10 @@ import { fetchPost, fetchPut, getBaseURL, proxyFetchPost, proxyFetchPut, proxyFetchGet, uploadFile } from '@/api/http'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { create } from 'zustand'; -import { generateUniqueId } from "@/lib"; +import { generateUniqueId, uploadLog } from "@/lib"; import { FileText } from 'lucide-react'; import { getAuthStore, useWorkerList } from './authStore'; import { showCreditsToast } from '@/components/Toast/creditsToast'; -import { OAuth } from '@/lib/oauth'; import { showStorageToast } from '@/components/Toast/storageToast'; @@ -184,8 +183,7 @@ const chatStore = create()( } const base_Url = import.meta.env.DEV ? import.meta.env.VITE_PROXY_URL : import.meta.env.VITE_BASE_URL const api = type == 'share' ? `${base_Url}/api/chat/share/playback/${shareToken}?delay_time=${delayTime}` : type == 'replay' ? `${base_Url}/api/chat/steps/playback/${taskId}?delay_time=${delayTime}` : `${baseURL}/chat` - const isInChina = await getIsInChina(systemLanguage) - console.log("isInChina", isInChina); + const { tasks } = get() let historyId: string | null = null; let snapshots: any = []; @@ -265,7 +263,7 @@ const chatStore = create()( } catch (error) { console.log('get-env-path error', error) } - + // create history if (!type) { const authStore = getAuthStore(); @@ -766,7 +764,7 @@ const chatStore = create()( addFileList(taskId, agentMessages.data.process_task_id as string, fileInfo); // Async file upload - if (!type && file_path && import.meta.env.VITE_USE_LOCAL_PROXY!=='true') { + if (!type && file_path && import.meta.env.VITE_USE_LOCAL_PROXY !== 'true') { (async () => { try { // Read file content using Electron API @@ -805,13 +803,14 @@ const chatStore = create()( console.log('error', agentMessages.data) showCreditsToast() setStatus(taskId, 'pause'); + uploadLog(taskId, type) return } if (agentMessages.step === "error") { console.error('Model error:', agentMessages.data) const errorMessage = agentMessages.data.message || 'An error occurred while processing your request'; - + // Create a new task to avoid "Task already exists" error // and completely reset the interface const newTaskId = create(); @@ -825,7 +824,7 @@ const chatStore = create()( role: "agent", content: `❌ **Error**: ${errorMessage}`, }); - + uploadLog(taskId, type) return } @@ -846,6 +845,8 @@ const chatStore = create()( } proxyFetchPut(`/api/chat/history/${historyId}`, obj) } + uploadLog(taskId, type) + let taskRunning = [...tasks[taskId].taskRunning]; let taskAssigning = [...tasks[taskId].taskAssigning]; @@ -1561,47 +1562,14 @@ const chatStore = create()( }) ); -// const filterMessage = (message: string, method_name: string = '') => { -// if (!message!.includes("=======================") && !message?.includes("Original Query") && !message?.startsWith('You need to process one given task') && method_name !== 'browser_take_screenshot' && message !== '{}' && !message?.startsWith('{"query"') && !message?.startsWith('{"entity"') && message !== '' && !message?.startsWith("{'warning':") && !message?.startsWith("{'results':") && !message?.startsWith(`{"index"`) && !message?.startsWith('- Ran Playwright code')) { -// if (message?.includes(`{"content"`)) { -// message = JSON.parse(message)?.content || '' -// } -// if (message?.startsWith('{"element"')) { -// message = JSON.parse(message)?.element || '' -// } -// if (message?.startsWith('{"url"')) { -// message = 'Open URL: ' + JSON.parse(message)?.url || '' -// } -// if (message?.startsWith('{"filename"')) { -// message = JSON.parse(message)?.filename || '' -// } -// if (method_name === 'browser_click' && message?.startsWith('{"element"')) { -// message = 'Click Element: ' + JSON.parse(message)?.element || '' -// } -// if (message?.startsWith('{"query"')) { -// message = 'Search: ' + JSON.parse(message)?.query || '' -// } -// if (message?.startsWith('{"result"')) { -// message = JSON.parse(message)?.result || '' -// } - -// // && !message?.startsWith("{'error':") -// if (message?.startsWith("{'error':")) { -// message = JSON.parse(message.replace(/'error'/g, '"error"'))?.error || '' -// } -// console.log(message) -// return message -// } -// return '' -// } const filterMessage = (message: AgentMessage) => { if (message.data.toolkit_name?.includes('Search ')) { - message.data.toolkit_name='Search Toolkit' + message.data.toolkit_name = 'Search Toolkit' } if (message.data.method_name?.includes('search')) { - message.data.method_name='search' + message.data.method_name = 'search' } - + if (message.data.toolkit_name === 'Note Taking Toolkit') { message.data.message = message.data.message!.replace(/content='/g, '').replace(/', update=False/g, '').replace(/', update=True/g, '') } @@ -1610,45 +1578,8 @@ const filterMessage = (message: AgentMessage) => { } return message } -let isInChinaCache: boolean | null = null; -const getIsInChina = async (systemLanguage: string): Promise => { - if (isInChinaCache !== null) { - return isInChinaCache; - } - const fetchWithTimeout = (url: string, timeout = 3000): Promise => { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new Error('Timeout')); - }, timeout); - - fetch(url) - .then((response) => { - clearTimeout(timer); - resolve(response); - }) - .catch((err) => { - clearTimeout(timer); - reject(err); - }); - }); - }; - - try { - const response = await fetchWithTimeout('https://ipinfo.io/json', 3000); - if (!response.ok) throw new Error('Network response was not ok'); - - const info = await response.json(); - console.log('country', info?.country) - isInChinaCache = info?.country === 'CN'; - return isInChinaCache; - } catch (error) { - console.warn('IP Timeout', error); - isInChinaCache = systemLanguage === 'zh-cn'; - return isInChinaCache; - } -}; export const useChatStore = chatStore; diff --git a/vite.config.ts b/vite.config.ts index b97c02c22..442d027c6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -96,3 +96,4 @@ process.on('SIGINT', () => { console.log(e) } }) +