Feature upload logs (#124)

This commit is contained in:
Wendong-Fan 2025-08-12 20:53:35 +08:00 committed by GitHub
commit b4b1d897f6
12 changed files with 196 additions and 113 deletions

View file

@ -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
VITE_USE_LOCAL_PROXY=true

1
.gitignore vendored
View file

@ -36,6 +36,7 @@ yarn.lock
.env
.env.local
.env.production
.env.development
.cursor

View file

@ -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<void>((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);
}

View file

@ -423,7 +423,7 @@ function checkPortAvailable(port: number): Promise<boolean> {
});
}
async function killProcessOnPort(port: number): Promise<boolean> {
export async function killProcessOnPort(port: number): Promise<boolean> {
try {
const platform = process.platform;
let command: string;

View file

@ -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 };
}
}

View file

@ -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<string> {
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()
})
}

View file

@ -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),

View file

@ -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",

View file

@ -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<HTMLDivElement>(null);
const controlsRef = useRef<HTMLDivElement>(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" ? (
<Button
variant="ghost"
size="sm"
className="font-bold text-base no-drag"
onClick={createNewProject}
>
{activeTaskTitle}
</Button>
variant="ghost"
size="sm"
className="font-bold text-base no-drag"
onClick={createNewProject}
>
{activeTaskTitle}
</Button>
) : (
<div className="font-bold leading-10 text-base ">{activeTaskTitle}</div>
<div className="font-bold leading-10 text-base ">
{activeTaskTitle}
</div>
)}
</>
)}
</div>
<div id="maximize-window" className="flex-1 h-10"></div>
{/* right */}

View file

@ -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);
}
}
}

View file

@ -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<ChatStore>()(
}
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<ChatStore>()(
} catch (error) {
console.log('get-env-path error', error)
}
// create history
if (!type) {
const authStore = getAuthStore();
@ -766,7 +764,7 @@ const chatStore = create<ChatStore>()(
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<ChatStore>()(
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<ChatStore>()(
role: "agent",
content: `❌ **Error**: ${errorMessage}`,
});
uploadLog(taskId, type)
return
}
@ -846,6 +845,8 @@ const chatStore = create<ChatStore>()(
}
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<ChatStore>()(
})
);
// 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<boolean> => {
if (isInChinaCache !== null) {
return isInChinaCache;
}
const fetchWithTimeout = (url: string, timeout = 3000): Promise<Response> => {
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;

View file

@ -96,3 +96,4 @@ process.on('SIGINT', () => {
console.log(e)
}
})