mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-10 04:00:24 +00:00
commit
5588eca97e
11 changed files with 943 additions and 509 deletions
|
|
@ -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, killProcessOnPort, startBackend } from './init'
|
||||
import { checkToolInstalled, killProcessOnPort, startBackend } from './init'
|
||||
import { WebViewManager } from './webview'
|
||||
import { FileReader } from './fileReader'
|
||||
import { ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
|
|
@ -18,9 +18,9 @@ import kill from 'tree-kill';
|
|||
import { zipFolder } from './utils/log'
|
||||
import axios from 'axios';
|
||||
import FormData from 'form-data';
|
||||
import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps'
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
const versionFile = path.join(userData, 'version.txt');
|
||||
|
||||
// ==================== constants ====================
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -50,69 +50,6 @@ findAvailablePort(browser_port).then(port => {
|
|||
app.commandLine.appendSwitch('remote-debugging-port', port + '');
|
||||
});
|
||||
|
||||
// Read last run version and install dependencies on update
|
||||
async function checkAndInstallDepsOnUpdate(): Promise<boolean> {
|
||||
const currentVersion = app.getVersion();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
log.info(' start check version', { currentVersion });
|
||||
|
||||
// Check if version file exists
|
||||
const versionExists = fs.existsSync(versionFile);
|
||||
let savedVersion = '';
|
||||
|
||||
if (versionExists) {
|
||||
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||
log.info(' read saved version', { savedVersion });
|
||||
} else {
|
||||
log.info(' version file not exist, will create new file');
|
||||
}
|
||||
|
||||
// If version file does not exist or version does not match, reinstall dependencies
|
||||
if (!versionExists || savedVersion !== currentVersion) {
|
||||
log.info(' version changed, prepare to reinstall uv dependencies...', {
|
||||
currentVersion,
|
||||
savedVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
|
||||
// Notify frontend to update
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-notification', {
|
||||
type: 'version-update',
|
||||
currentVersion,
|
||||
previousVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
}
|
||||
|
||||
// Update version file
|
||||
fs.writeFileSync(versionFile, currentVersion);
|
||||
log.info(' version file updated', { currentVersion });
|
||||
|
||||
// Install dependencies
|
||||
const result = await installDependencies();
|
||||
if (!result) {
|
||||
log.error(' install dependencies failed');
|
||||
resolve(false);
|
||||
return
|
||||
}
|
||||
resolve(true);
|
||||
log.info(' install dependencies complete');
|
||||
return
|
||||
} else {
|
||||
log.info(' version not changed, skip install dependencies', { currentVersion });
|
||||
resolve(true);
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(' check version and install dependencies error:', error);
|
||||
resolve(false);
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== app config ====================
|
||||
process.env.APP_ROOT = MAIN_DIST;
|
||||
process.env.VITE_PUBLIC = VITE_PUBLIC;
|
||||
|
|
@ -259,51 +196,6 @@ const checkManagerInstance = (manager: any, name: string) => {
|
|||
return manager;
|
||||
};
|
||||
|
||||
export const handleDependencyInstallation = async () => {
|
||||
try {
|
||||
log.info(' start install dependencies...');
|
||||
|
||||
const isSuccess = await installDependencies();
|
||||
if (!isSuccess) {
|
||||
log.error(' install dependencies failed');
|
||||
return { success: false, error: 'install dependencies failed' };
|
||||
}
|
||||
|
||||
log.info(' install dependencies success, check tool installed status...');
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
log.info('isToolInstalled && !python_process', isToolInstalled && !python_process);
|
||||
if (isToolInstalled && !python_process) {
|
||||
log.info(' tool installed, start backend service...');
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info(' backend service start success', { port });
|
||||
});
|
||||
|
||||
// Notify frontend to install success
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
|
||||
}
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
log.info(' python process exit', { code, signal });
|
||||
});
|
||||
} else if (!isToolInstalled) {
|
||||
log.warn(' tool not installed, skip backend start');
|
||||
} else {
|
||||
log.info(' backend process already exist, skip start');
|
||||
}
|
||||
|
||||
log.info(' install dependencies complete');
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
log.error(' install dependencies error:', error);
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
function registerIpcHandlers() {
|
||||
// ==================== basic info handler ====================
|
||||
ipcMain.handle('get-browser-port', () => {
|
||||
|
|
@ -934,13 +826,35 @@ function registerIpcHandlers() {
|
|||
});
|
||||
|
||||
// ==================== dependency install handler ====================
|
||||
ipcMain.handle('install-dependencies', handleDependencyInstallation);
|
||||
ipcMain.handle('frontend-ready', handleDependencyInstallation);
|
||||
ipcMain.handle('install-dependencies', async () => {
|
||||
try {
|
||||
if(win === null) throw new Error("Window is null");
|
||||
//Force installation even if versionFile exists
|
||||
const isInstalled = await checkAndInstallDepsOnUpdate({win, forceInstall: true});
|
||||
return { success: true, isInstalled };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('check-tool-installed', async () => {
|
||||
try {
|
||||
const isInstalled = await checkToolInstalled();
|
||||
return { success: true, isInstalled };
|
||||
return { success: true, isInstalled: isInstalled.success };
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-installation-status', async () => {
|
||||
try {
|
||||
const { isInstalling, hasLockFile } = await getInstallationStatus();
|
||||
return {
|
||||
success: true,
|
||||
isInstalling,
|
||||
hasLockFile,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
|
|
@ -1006,12 +920,13 @@ async function createWindow() {
|
|||
update(win);
|
||||
|
||||
// ==================== check tool installed ====================
|
||||
let res = await checkAndInstallDepsOnUpdate();
|
||||
if (!res) {
|
||||
log.info('checkAndInstallDepsOnUpdate,install dependencies failed');
|
||||
win.webContents.send('install-dependencies-complete', { success: false, code: 2 });
|
||||
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 });
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.info("[DEPS INSTALL] Dependency Success: ", res.message);
|
||||
await checkAndStartBackend();
|
||||
}
|
||||
|
||||
|
|
@ -1069,27 +984,30 @@ const setupExternalLinkHandling = () => {
|
|||
// ==================== check and start backend ====================
|
||||
const checkAndStartBackend = async () => {
|
||||
log.info('Checking and starting backend service...');
|
||||
try {
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
if (isToolInstalled.success) {
|
||||
log.info('Tool installed, starting backend service...');
|
||||
|
||||
const isToolInstalled = await checkToolInstalled();
|
||||
if (isToolInstalled) {
|
||||
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 });
|
||||
}
|
||||
|
||||
// Notify frontend installation success
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('install-dependencies-complete', { success: true, code: 0 });
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info('Backend service started successfully', { port });
|
||||
});
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
|
||||
log.info('Python process exited', { code, signal });
|
||||
});
|
||||
} else {
|
||||
log.warn('Tool not installed, cannot start backend service');
|
||||
}
|
||||
|
||||
python_process = await startBackend((port) => {
|
||||
backendPort = port;
|
||||
log.info('Backend service started successfully', { port });
|
||||
});
|
||||
|
||||
python_process?.on('exit', (code, signal) => {
|
||||
|
||||
log.info('Python process exited', { code, signal });
|
||||
});
|
||||
} else {
|
||||
log.warn('Tool not installed, cannot start backend service');
|
||||
} catch (error) {
|
||||
log.debug("Cannot Start Backend due to ", error)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,82 +6,30 @@ import path from 'path'
|
|||
import * as net from "net";
|
||||
import { ipcMain, BrowserWindow, app } from 'electron'
|
||||
import { promisify } from 'util'
|
||||
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// helper function to get main window
|
||||
function getMainWindow(): BrowserWindow | null {
|
||||
export function getMainWindow(): BrowserWindow | null {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
return windows.length > 0 ? windows[0] : null;
|
||||
}
|
||||
|
||||
|
||||
export async function checkToolInstalled() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise<PromiseReturnType>(async (resolve, reject) => {
|
||||
if (!(await isBinaryExists('uv'))) {
|
||||
resolve(false)
|
||||
resolve({success: false, message: "uv doesn't exist"})
|
||||
return
|
||||
}
|
||||
|
||||
if (!(await isBinaryExists('bun'))) {
|
||||
resolve(false)
|
||||
resolve({success: false, message: "Bun doesn't exist"})
|
||||
return
|
||||
}
|
||||
|
||||
resolve(true)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command line tools are installed, install if not
|
||||
*/
|
||||
export async function installCommandTool() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<boolean> => {
|
||||
if (await isBinaryExists(toolName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`start install ${toolName}`);
|
||||
const isSuccess = await runInstallScript(scriptName);
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed() && !isSuccess) {
|
||||
mainWindow.webContents.send('install-dependencies-complete', {
|
||||
success: false,
|
||||
code: 2,
|
||||
error: `${toolName} installation failed`,
|
||||
});
|
||||
return false
|
||||
}
|
||||
|
||||
const installed = await isBinaryExists(toolName);
|
||||
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (installed) {
|
||||
mainWindow.webContents.send('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: `${toolName} installed successfully`,
|
||||
});
|
||||
} else {
|
||||
mainWindow.webContents.send('install-dependencies-complete', {
|
||||
success: false,
|
||||
code: 2,
|
||||
error: `${toolName} installation failed`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return installed;
|
||||
};
|
||||
|
||||
if (!(await ensureInstalled('uv', 'install-uv.js'))) {
|
||||
return reject("uv install failed");
|
||||
}
|
||||
if (!(await ensureInstalled('bun', 'install-bun.js'))) {
|
||||
return reject("bun install failed");
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
resolve({success: true, message: "Tools exist already"})
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -167,124 +115,6 @@ export async function installCommandTool() {
|
|||
// })
|
||||
// })
|
||||
// }
|
||||
export async function installDependencies() {
|
||||
return new Promise<boolean>(async (resolve, reject) => {
|
||||
console.log('start install dependencies')
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-start');
|
||||
}
|
||||
|
||||
const isInstalCommandTool = await installCommandTool()
|
||||
if (!isInstalCommandTool) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
const uv_path = await getBinaryPath('uv')
|
||||
const backendPath = getBackendPath()
|
||||
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
fs.mkdirSync(backendPath, { recursive: true })
|
||||
}
|
||||
|
||||
const installingLockPath = path.join(backendPath, 'uv_installing.lock')
|
||||
fs.writeFileSync(installingLockPath, '')
|
||||
|
||||
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/']
|
||||
const runInstall = (extraArgs: string[]) => {
|
||||
return new Promise<boolean>((resolveInner, rejectInner) => {
|
||||
try {
|
||||
const node_process = spawn(uv_path, [
|
||||
'sync',
|
||||
'--no-dev',
|
||||
'--cache-dir', getCachePath('uv_cache'),
|
||||
...extraArgs], {
|
||||
cwd: backendPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
}
|
||||
})
|
||||
console.log('start install dependencies', extraArgs)
|
||||
node_process.stdout.on('data', (data) => {
|
||||
|
||||
log.info(`Script output: ${data}`)
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
}
|
||||
})
|
||||
|
||||
node_process.stderr.on('data', (data) => {
|
||||
log.error(`Script error: ${data}`)
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-log', { type: 'stderr', data: data.toString() });
|
||||
}
|
||||
})
|
||||
|
||||
node_process.on('close', (code) => {
|
||||
console.log('install dependencies end', code === 0)
|
||||
resolveInner(code === 0)
|
||||
})
|
||||
} catch (err) {
|
||||
log.error('run install failed', err)
|
||||
// Clean up uv_installing.lock file if installation fails
|
||||
if (fs.existsSync(installingLockPath)) {
|
||||
fs.unlinkSync(installingLockPath);
|
||||
}
|
||||
rejectInner(err)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// try default install
|
||||
const installSuccess = await runInstall([])
|
||||
|
||||
if (installSuccess) {
|
||||
fs.unlinkSync(installingLockPath)
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('Script completed successfully')
|
||||
console.log('end install dependencies')
|
||||
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
// try mirror install
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
let mirrorInstallSuccess = false
|
||||
|
||||
if (timezone === 'Asia/Shanghai') {
|
||||
mirrorInstallSuccess = await runInstall(proxyArgs)
|
||||
} else {
|
||||
mirrorInstallSuccess = await runInstall([])
|
||||
}
|
||||
|
||||
|
||||
fs.existsSync(installingLockPath) && fs.unlinkSync(installingLockPath)
|
||||
|
||||
if (mirrorInstallSuccess) {
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('Mirror script completed successfully')
|
||||
console.log('end install dependencies (mirror)')
|
||||
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
|
||||
resolve(true)
|
||||
} else {
|
||||
log.error('Both default and mirror install failed')
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('install-dependencies-complete', { success: false, error: 'Both default and mirror install failed' });
|
||||
}
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function startBackend(setPort?: (port: number) => void): Promise<any> {
|
||||
console.log('start fastapi')
|
||||
|
|
@ -330,6 +160,9 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
const displayFilteredLogs = (data: String) => {
|
||||
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")) {
|
||||
|
|
@ -343,6 +176,7 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
|
|||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
//Implicitly runs uv sync
|
||||
const node_process = spawn(
|
||||
uv_path,
|
||||
["run", "uvicorn", "main:api", "--port", port.toString(), "--loop", "asyncio"],
|
||||
|
|
|
|||
424
electron/main/install-deps.ts
Normal file
424
electron/main/install-deps.ts
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
import { app, BrowserWindow } from 'electron'
|
||||
import path from 'node:path'
|
||||
import log from 'electron-log'
|
||||
import { getMainWindow } from './init'
|
||||
import fs from 'node:fs'
|
||||
import { getBackendPath, getBinaryPath, getCachePath, isBinaryExists, runInstallScript } from './utils/process'
|
||||
import { spawn } from 'child_process'
|
||||
import { safeMainWindowSend } from './utils/safeWebContentsSend'
|
||||
|
||||
const userData = app.getPath('userData');
|
||||
const versionFile = path.join(userData, 'version.txt');
|
||||
|
||||
export type PromiseReturnType = {
|
||||
message: string;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
interface checkInstallProps {
|
||||
win:BrowserWindow|null;
|
||||
forceInstall?:boolean
|
||||
}
|
||||
// Read last run version and install dependencies on update
|
||||
export const checkAndInstallDepsOnUpdate = async ({win, forceInstall=false}:checkInstallProps):
|
||||
Promise<PromiseReturnType> => {
|
||||
const currentVersion = app.getVersion();
|
||||
let savedVersion = '';
|
||||
const checkInstallOperations = {
|
||||
getSavedVersion: ():boolean => {
|
||||
// Check if version file exists
|
||||
const versionExists = fs.existsSync(versionFile);
|
||||
if (versionExists) {
|
||||
log.info('[DEPS INSTALL] start check version', { currentVersion });
|
||||
savedVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
||||
log.info('[DEPS INSTALL] read saved version', { savedVersion });
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] version file not exist, will create new file');
|
||||
}
|
||||
return versionExists;
|
||||
},
|
||||
handleUpdateNotification: (versionExists:boolean) => {
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.webContents.send('update-notification', {
|
||||
type: 'version-update',
|
||||
currentVersion,
|
||||
previousVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
} else {
|
||||
log.warn('[DEPS INSTALL] Cannot send update notification - window not available');
|
||||
}
|
||||
},
|
||||
createVersionFile: () => {
|
||||
fs.writeFileSync(versionFile, currentVersion);
|
||||
log.info('[DEPS INSTALL] version file updated', { currentVersion });
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const versionExists:boolean = checkInstallOperations.getSavedVersion();
|
||||
|
||||
// If version file does not exist or version does not match, reinstall dependencies
|
||||
if (forceInstall || !versionExists || savedVersion !== currentVersion) {
|
||||
log.info('[DEPS INSTALL] version changed, prepare to reinstall uv dependencies...', {
|
||||
currentVersion,
|
||||
savedVersion: versionExists ? savedVersion : 'none',
|
||||
reason: !versionExists ? 'version file not exist' : 'version not match'
|
||||
});
|
||||
|
||||
// Notify frontend to update
|
||||
checkInstallOperations.handleUpdateNotification(versionExists);
|
||||
|
||||
// Update version file
|
||||
checkInstallOperations.createVersionFile();
|
||||
|
||||
// Install dependencies
|
||||
const result = await installDependencies();
|
||||
if (!result.success) {
|
||||
log.error(' install dependencies failed');
|
||||
resolve({ message: `Install dependencies failed, msg ${result.message}`, success: false });
|
||||
return
|
||||
}
|
||||
resolve({ message: "Dependencies installed successfully after update", success: true });
|
||||
log.info('[DEPS INSTALL] install dependencies complete');
|
||||
return
|
||||
} else {
|
||||
log.info('[DEPS INSTALL] version not changed, skip install dependencies', { currentVersion });
|
||||
resolve({ message: "Version not changed, skipped installation", success: true });
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(' check version and install dependencies error:', error);
|
||||
resolve({ message: `Error checking version: ${error}`, success: false });
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if command line tools are installed, install if not
|
||||
*/
|
||||
export async function installCommandTool(): Promise<PromiseReturnType> {
|
||||
try {
|
||||
const ensureInstalled = async (toolName: 'uv' | 'bun', scriptName: string): Promise<PromiseReturnType> => {
|
||||
if (await isBinaryExists(toolName)) {
|
||||
return { message: `${toolName} already installed`, success: true };
|
||||
}
|
||||
|
||||
console.log(`start install ${toolName}`);
|
||||
await runInstallScript(scriptName);
|
||||
const installed = await isBinaryExists(toolName);
|
||||
|
||||
if (installed) {
|
||||
safeMainWindowSend('install-dependencies-log', {
|
||||
type: 'stdout',
|
||||
data: `${toolName} installed successfully`,
|
||||
});
|
||||
} else {
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: false,
|
||||
code: 2,
|
||||
error: `${toolName} installation failed (script exit code 2)`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message: installed ? `${toolName} installed successfully` : `${toolName} installation failed`,
|
||||
success: installed
|
||||
};
|
||||
};
|
||||
|
||||
const uvResult = await ensureInstalled('uv', 'install-uv.js');
|
||||
if (!uvResult.success) {
|
||||
return { message: uvResult.message, success: false };
|
||||
}
|
||||
|
||||
const bunResult = await ensureInstalled('bun', 'install-bun.js');
|
||||
if (!bunResult.success) {
|
||||
return { message: bunResult.message, success: false };
|
||||
}
|
||||
|
||||
return { message: "Command tools installed successfully", success: true };
|
||||
} catch (error) {
|
||||
return { message: `Command tool installation failed: ${error}`, success: false };
|
||||
}
|
||||
}
|
||||
|
||||
let uv_path:string;
|
||||
const mainWindow = getMainWindow();
|
||||
const backendPath = getBackendPath();
|
||||
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
|
||||
* @returns Object with installation status information
|
||||
*/
|
||||
export async function getInstallationStatus(): Promise<{
|
||||
isInstalling: boolean;
|
||||
hasLockFile: boolean;
|
||||
installedExists: boolean;
|
||||
}> {
|
||||
try {
|
||||
const installingExists = fs.existsSync(installingLockPath);
|
||||
const installedExists = fs.existsSync(installedLockPath);
|
||||
|
||||
// If installing lock exists, installation is in progress
|
||||
// If installed lock exists, installation completed previously
|
||||
return {
|
||||
isInstalling: installingExists,
|
||||
hasLockFile: installingExists || installedExists,
|
||||
installedExists: installedExists
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[getInstallationStatus] Error checking installation status:', error);
|
||||
return {
|
||||
isInstalling: false,
|
||||
hasLockFile: false,
|
||||
installedExists: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class InstallLogs {
|
||||
private node_process;
|
||||
|
||||
constructor(extraArgs:string[]) {
|
||||
console.log('start install dependencies', extraArgs)
|
||||
this.node_process = spawn(uv_path, [
|
||||
'sync',
|
||||
'--no-dev',
|
||||
'--cache-dir', getCachePath('uv_cache'),
|
||||
...extraArgs], {
|
||||
cwd: backendPath,
|
||||
env: {
|
||||
...process.env,
|
||||
UV_TOOL_DIR: getCachePath('uv_tool'),
|
||||
UV_PYTHON_INSTALL_DIR: getCachePath('uv_python'),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**Display filtered logs based on severity */
|
||||
displayFilteredLogs(data:String) {
|
||||
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: [DEPS INSTALL] ${msg}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stderr', data: data.toString() });
|
||||
} else {
|
||||
log.info(`BACKEND: [DEPS INSTALL] ${msg}`);
|
||||
safeMainWindowSend('install-dependencies-log', { type: 'stdout', data: data.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
/**Handle stdout data */
|
||||
onStdout() {
|
||||
this.node_process.stdout.on('data', (data:any) => {
|
||||
this.displayFilteredLogs(data);
|
||||
})
|
||||
}
|
||||
|
||||
/**Handle stderr data */
|
||||
onStderr() {
|
||||
this.node_process.stderr.on('data', (data:any) => {
|
||||
this.displayFilteredLogs(data);
|
||||
})
|
||||
}
|
||||
|
||||
/**Handle process close event */
|
||||
onClose(resolveInner:(code: number | null) => void) {
|
||||
this.node_process.on('close', resolveInner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set installing Lock Path
|
||||
* Creates uv_installing.lock file to indicate installation in progress
|
||||
* Creates backend directory if not exists
|
||||
*/
|
||||
static setLockPath() {
|
||||
if (!fs.existsSync(backendPath)) {
|
||||
fs.mkdirSync(backendPath, { recursive: true })
|
||||
}
|
||||
fs.writeFileSync(installingLockPath, '')
|
||||
}
|
||||
|
||||
/**Clean installing Lock Path */
|
||||
static cleanLockPath() {
|
||||
if (fs.existsSync(installingLockPath)) {
|
||||
fs.unlinkSync(installingLockPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const runInstall = (extraArgs: string[]) => {
|
||||
const installLogs = new InstallLogs(extraArgs);
|
||||
return new Promise<PromiseReturnType>((resolveInner, rejectInner) => {
|
||||
try {
|
||||
installLogs.onStdout();
|
||||
installLogs.onStderr();
|
||||
installLogs.onClose((code) => {
|
||||
console.log('install dependencies end', code === 0)
|
||||
InstallLogs.cleanLockPath()
|
||||
resolveInner({
|
||||
message: code === 0 ? "Installation completed successfully" : `Installation failed with code ${code}`,
|
||||
success: code === 0
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
log.error('run install failed', err)
|
||||
// Clean up uv_installing.lock file if installation fails
|
||||
InstallLogs.cleanLockPath();
|
||||
rejectInner({ message: `Installation failed: ${err}`, success: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function installDependencies(): Promise<PromiseReturnType> {
|
||||
uv_path = await getBinaryPath('uv');
|
||||
const handleInstallOperations = {
|
||||
spawnBabel: (message:"mirror"|"main"="main") => {
|
||||
fs.writeFileSync(installedLockPath, '')
|
||||
log.info('[DEPS INSTALL] Script completed successfully')
|
||||
console.log(`Install Dependencies completed ${message}`)
|
||||
spawn(uv_path, ['run', 'task', 'babel'], { cwd: backendPath })
|
||||
},
|
||||
notifyInstallDependenciesPage: ():boolean => {
|
||||
const success = safeMainWindowSend('install-dependencies-start');
|
||||
if (!success) {
|
||||
log.warn('[DEPS INSTALL] Main window not available, continuing installation without UI updates');
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<PromiseReturnType>(async (resolve, reject) => {
|
||||
console.log('start install dependencies')
|
||||
const mainWindowAvailable = handleInstallOperations.notifyInstallDependenciesPage();
|
||||
|
||||
if (!mainWindowAvailable) {
|
||||
log.info('[DEPS INSTALL] Proceeding with installation without UI notifications');
|
||||
}
|
||||
|
||||
const isInstalCommandTool = await installCommandTool()
|
||||
if (!isInstalCommandTool.success) {
|
||||
resolve({ message: "Command tool installation failed", success: false })
|
||||
return
|
||||
}
|
||||
|
||||
// Set Installing Lock Files
|
||||
InstallLogs.setLockPath();
|
||||
|
||||
// try default install
|
||||
const installSuccess = await runInstall([])
|
||||
if (installSuccess.success) {
|
||||
handleInstallOperations.spawnBabel()
|
||||
resolve({ message: "Dependencies installed successfully", success: true })
|
||||
return
|
||||
}
|
||||
|
||||
// try mirror install
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
let mirrorInstallSuccess: PromiseReturnType = { message: "", success: false }
|
||||
mirrorInstallSuccess = (timezone === 'Asia/Shanghai')? await runInstall(proxyArgs) :await runInstall([])
|
||||
|
||||
if (mirrorInstallSuccess.success) {
|
||||
handleInstallOperations.spawnBabel("mirror")
|
||||
resolve({ message: "Dependencies installed successfully with mirror", success: true })
|
||||
} else {
|
||||
log.error('Both default and mirror install failed')
|
||||
safeMainWindowSend('install-dependencies-complete', {
|
||||
success: false,
|
||||
error: 'Both default and mirror install failed'
|
||||
});
|
||||
resolve({ message: "Both default and mirror install failed", success: false })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let dependencyInstallationDetected = false;
|
||||
let installationNotificationSent = false;
|
||||
export function detectInstallationLogs(msg:string) {
|
||||
// Check for UV dependency installation patterns
|
||||
const installPatterns = [
|
||||
"Resolved", // UV resolving dependencies
|
||||
"Downloaded", // UV downloading packages
|
||||
"Installing", // UV installing packages
|
||||
"Built", // UV building packages
|
||||
"Prepared", // UV preparing virtual environment
|
||||
"Syncing", // UV sync process
|
||||
"Creating virtualenv", // Virtual environment creation
|
||||
"Updating", // UV updating packages
|
||||
"× No solution found when resolving dependencies", // Dependency resolution issues
|
||||
"Audited" // UV auditing dependencies
|
||||
];
|
||||
|
||||
// Detect if UV is installing dependencies
|
||||
if (!dependencyInstallationDetected && installPatterns.some(pattern =>
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
20
electron/main/utils/safeWebContentsSend.ts
Normal file
20
electron/main/utils/safeWebContentsSend.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import log from 'electron-log'
|
||||
import { getMainWindow } from "../init";
|
||||
|
||||
/**
|
||||
* Safely send message to main window if it exists and is not destroyed
|
||||
* @param channel - The IPC channel to send message to
|
||||
* @param data - The data to send
|
||||
*/
|
||||
function safeMainWindowSend(channel: string, data?: any) {
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(channel, data);
|
||||
return true;
|
||||
} else {
|
||||
log.warn(`[WEBCONTENTS SEND] Cannot send message to main window: ${channel}`, data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export {safeMainWindowSend}
|
||||
|
|
@ -62,9 +62,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
deleteFolder: (email: string) => ipcRenderer.invoke('delete-folder', email),
|
||||
getMcpConfigPath: (email: string) => ipcRenderer.invoke('get-mcp-config-path', email),
|
||||
// install dependencies related API
|
||||
installDependencies: () => ipcRenderer.invoke('install-dependencies'),
|
||||
frontendReady: () => ipcRenderer.invoke('frontend-ready'),
|
||||
checkAndInstallDepsOnUpdate: () => ipcRenderer.invoke('install-dependencies'),
|
||||
checkInstallBrowser: () => ipcRenderer.invoke('check-install-browser'),
|
||||
getInstallationStatus: () => ipcRenderer.invoke('get-installation-status'),
|
||||
onInstallDependenciesStart: (callback: () => void) => {
|
||||
ipcRenderer.on('install-dependencies-start', callback);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { ProgressInstall } from "@/components/ui/progress-install";
|
||||
import { FileDown, RefreshCcw } from "lucide-react";
|
||||
|
|
@ -14,158 +14,21 @@ import {
|
|||
DialogFooter,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useInstallationUI } from "@/store/installationStore";
|
||||
import { TooltipSimple } from "../ui/tooltip";
|
||||
|
||||
interface InstallLog {
|
||||
type: "stdout" | "stderr";
|
||||
data: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export const InstallDependencies: React.FC<{
|
||||
isInstalling: boolean;
|
||||
setIsInstalling: (isInstalling: boolean) => void;
|
||||
}> = ({ isInstalling, setIsInstalling }) => {
|
||||
const { initState, setInitState } = useAuthStore();
|
||||
const {t} = useTranslation()
|
||||
const [logs, setLogs] = useState<InstallLog[]>([]);
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "installing" | "success" | "error"
|
||||
>("idle");
|
||||
const [showInstallScreen, setShowInstallScreen] = useState(true);
|
||||
const [progress, setProgress] = useState(20);
|
||||
useEffect(() => {
|
||||
// listen to install start event
|
||||
window.electronAPI.onInstallDependenciesStart(() => {
|
||||
setIsInstalling(true);
|
||||
setStatus("installing");
|
||||
setShowInstallScreen(true);
|
||||
setLogs([]);
|
||||
console.log("start installing dependencies...");
|
||||
setProgress(20);
|
||||
});
|
||||
|
||||
// listen to install log
|
||||
window.electronAPI.onInstallDependenciesLog(
|
||||
(data: { type: string; data: string }) => {
|
||||
console.log("data", data);
|
||||
const newLog: InstallLog = {
|
||||
type: data?.type as "stdout" | "stderr",
|
||||
data: data?.data,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setProgress((prev) => {
|
||||
const progress = prev + 5;
|
||||
if (progress >= 90) {
|
||||
return 90;
|
||||
}
|
||||
return progress;
|
||||
});
|
||||
console.log(`install log [${data?.type}]:`, data?.data);
|
||||
setLogs((prev) => [...prev, newLog]);
|
||||
}
|
||||
);
|
||||
|
||||
// listen to install complete event
|
||||
window.electronAPI.onInstallDependenciesComplete(
|
||||
(data: { success: boolean; code?: number; error?: string }) => {
|
||||
setIsInstalling(false);
|
||||
if (data?.success) {
|
||||
setStatus("success");
|
||||
console.log("dependencies installed successfully!");
|
||||
setProgress(100);
|
||||
setInitState("done");
|
||||
} else {
|
||||
setStatus("error");
|
||||
console.error("dependencies installation failed:", data?.code);
|
||||
console.error("dependencies installation failed:", data?.error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// after component mounted, notify main process frontend is ready
|
||||
const notifyFrontendReady = async () => {
|
||||
try {
|
||||
// check if there is frontend-ready API
|
||||
if (window.electronAPI.frontendReady) {
|
||||
await window.electronAPI.frontendReady();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"frontend ready notification failed, maybe manual install mode:",
|
||||
error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// delay notification, ensure component is fully initialized
|
||||
setTimeout(notifyFrontendReady, 500);
|
||||
|
||||
// clean up listeners
|
||||
return () => {
|
||||
window.electronAPI.removeAllListeners("install-dependencies-start");
|
||||
window.electronAPI.removeAllListeners("install-dependencies-log");
|
||||
window.electronAPI.removeAllListeners("install-dependencies-complete");
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
setStatus("installing");
|
||||
setIsInstalling(true);
|
||||
setLogs([]);
|
||||
setShowInstallScreen(true);
|
||||
|
||||
const result = await window.electronAPI.installDependencies();
|
||||
console.log("result", result);
|
||||
if (!result.success) {
|
||||
setStatus("error");
|
||||
setIsInstalling(false);
|
||||
return;
|
||||
}
|
||||
setStatus("success");
|
||||
setProgress(100);
|
||||
setIsInstalling(false);
|
||||
setInitState("done");
|
||||
} catch (error) {
|
||||
console.error("install start failed:", error);
|
||||
setStatus("error");
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportLog = async () => {
|
||||
try {
|
||||
const response = await window.electronAPI.exportLog();
|
||||
|
||||
if (!response.success) {
|
||||
alert("Export cancelled:" + response.error);
|
||||
return;
|
||||
}
|
||||
if (response.savedPath) {
|
||||
window.location.href =
|
||||
"https://github.com/eigent-ai/eigent/issues/new/choose";
|
||||
alert("log saved:" + response.savedPath);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert("export error:" + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
// if not show install interface, return null
|
||||
if (initState === "done" && !isInstalling) {
|
||||
return (
|
||||
<Dialog open={status === "error"}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleInstall}>{t("layout.retry")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
export const InstallDependencies: React.FC = () => {
|
||||
const { initState } = useAuthStore();
|
||||
const {t} = useTranslation();
|
||||
|
||||
const {
|
||||
progress,
|
||||
latestLog,
|
||||
error,
|
||||
isInstalling,
|
||||
retryInstallation,
|
||||
exportLog,
|
||||
} = useInstallationUI();
|
||||
|
||||
return (
|
||||
<div className="fixed !z-[100] inset-0 !bg-bg-page bg-opacity-80 h-full w-full flex items-center justify-center backdrop-blur-sm">
|
||||
|
|
@ -180,16 +43,18 @@ export const InstallDependencies: React.FC<{
|
|||
<div className="flex items-center gap-2 justify-between">
|
||||
<div className="text-text-label text-xs font-normal leading-tight ">
|
||||
{isInstalling ? "System Installing ..." : ""}
|
||||
<span className="pl-2">{logs.at(-1)?.data}</span>
|
||||
<span className="pl-2">{latestLog?.data}</span>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="mt-1"
|
||||
onClick={handleInstall}
|
||||
>
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
<TooltipSimple content={`Cannot retry because state is ${error}`} hidden={true}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="mt-1"
|
||||
onClick={retryInstallation}
|
||||
>
|
||||
<RefreshCcw className="w-4 h-4" />
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -214,7 +79,7 @@ export const InstallDependencies: React.FC<{
|
|||
<FileDown className="w-4 h-4" />
|
||||
{t("layout.report-bug")}
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleInstall}>
|
||||
<Button size="sm" onClick={retryInstallation}>
|
||||
{t("layout.retry")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { error } from "electron-log";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
|
||||
interface InstallationErrorDialogProps {
|
||||
error: string;
|
||||
installationState: string;
|
||||
latestLog: any;
|
||||
retryInstallation: () => void;
|
||||
}
|
||||
|
||||
const InstallationErrorDialog = ({
|
||||
error,
|
||||
installationState,
|
||||
latestLog,
|
||||
retryInstallation,
|
||||
}:InstallationErrorDialogProps) => {
|
||||
return (
|
||||
<Dialog open={installationState == "error"}>
|
||||
<DialogContent className="bg-white-100%">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("layout.installation-failed")}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="text-text-label text-xs font-normal leading-tight mb-4">
|
||||
{
|
||||
<div className="mb-1">
|
||||
<span className="text-text-label/60">
|
||||
Error: {error} <br />
|
||||
Log: {latestLog?.data}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={retryInstallation}>{t("layout.retry")}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default InstallationErrorDialog;
|
||||
|
|
@ -8,13 +8,25 @@ import { AnimationJson } from "@/components/AnimationJson";
|
|||
import animationData from "@/assets/animation/onboarding_success.json";
|
||||
import CloseNoticeDialog from "../Dialog/CloseNotice";
|
||||
import { useChatStore } from "@/store/chatStore";
|
||||
import { useInstallationUI } from "@/store/installationStore";
|
||||
import { useInstallationSetup } from "@/hooks/useInstallationSetup";
|
||||
import InstallationErrorDialog from "../InstallStep/InstallationErrorDialog/InstallationErrorDialog";
|
||||
const Layout = () => {
|
||||
const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
|
||||
useAuthStore();
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const { initState, isFirstLaunch, setIsFirstLaunch } = useAuthStore();
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const chatStore = useChatStore();
|
||||
const {
|
||||
installationState,
|
||||
latestLog,
|
||||
error,
|
||||
isInstalling,
|
||||
shouldShowInstallScreen,
|
||||
retryInstallation,
|
||||
} = useInstallationUI();
|
||||
|
||||
// Setup installation IPC listeners and state synchronization
|
||||
useInstallationSetup();
|
||||
|
||||
useEffect(() => {
|
||||
const handleBeforeClose = () => {
|
||||
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
|
||||
|
|
@ -32,43 +44,42 @@ const Layout = () => {
|
|||
};
|
||||
}, [chatStore.tasks, chatStore.activeTaskId]);
|
||||
|
||||
useEffect(() => {
|
||||
const checkToolInstalled = async () => {
|
||||
// in render process
|
||||
const result = await window.ipcRenderer.invoke("check-tool-installed");
|
||||
if (result.success) {
|
||||
if (initState === "done" && !result.isInstalled) {
|
||||
setInitState("carousel");
|
||||
}
|
||||
console.log("tool is installed:");
|
||||
} else {
|
||||
console.error("check failed:", result.error);
|
||||
}
|
||||
};
|
||||
checkToolInstalled();
|
||||
}, []);
|
||||
// Determine what to show based on states
|
||||
const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling;
|
||||
const shouldShowMainContent = !shouldShowInstallScreen;
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
|
||||
<TopBar />
|
||||
<div className="flex-1 h-full p-2">
|
||||
{initState === "done" && isFirstLaunch && !isInstalling && (
|
||||
{/* Onboarding animation */}
|
||||
{shouldShowOnboarding && (
|
||||
<AnimationJson
|
||||
onComplete={() => {
|
||||
setIsFirstLaunch(false);
|
||||
}}
|
||||
onComplete={() => setIsFirstLaunch(false)}
|
||||
animationData={animationData}
|
||||
/>
|
||||
)}
|
||||
{(initState !== "done" || isInstalling) && (
|
||||
<InstallDependencies
|
||||
isInstalling={isInstalling}
|
||||
setIsInstalling={setIsInstalling}
|
||||
/>
|
||||
|
||||
{/* Installation screen */}
|
||||
{shouldShowInstallScreen && <InstallDependencies />}
|
||||
|
||||
{/* Main app content */}
|
||||
{shouldShowMainContent && (
|
||||
<>
|
||||
<Outlet />
|
||||
<HistorySidebar />
|
||||
</>
|
||||
)}
|
||||
<Outlet />
|
||||
<HistorySidebar />
|
||||
|
||||
{(error != "" && error !=undefined) &&
|
||||
<InstallationErrorDialog
|
||||
error={error}
|
||||
installationState={installationState}
|
||||
latestLog={latestLog}
|
||||
retryInstallation={retryInstallation}/>
|
||||
}
|
||||
|
||||
<CloseNoticeDialog
|
||||
onOpenChange={setNoticeOpen}
|
||||
open={noticeOpen}
|
||||
|
|
|
|||
89
src/hooks/useInstallationSetup.ts
Normal file
89
src/hooks/useInstallationSetup.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useInstallationStore } from '@/store/installationStore';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
|
||||
/**
|
||||
* Hook that sets up Electron IPC listeners and handles installation state synchronization
|
||||
* This should be called once in your App component or Layout component
|
||||
*/
|
||||
export const useInstallationSetup = () => {
|
||||
const { initState, setInitState } = useAuthStore();
|
||||
|
||||
// Extract only the functions we need to avoid dependency issues
|
||||
const startInstallation = useInstallationStore(state => state.startInstallation);
|
||||
const addLog = useInstallationStore(state => state.addLog);
|
||||
const setSuccess = useInstallationStore(state => state.setSuccess);
|
||||
const setError = useInstallationStore(state => state.setError);
|
||||
|
||||
// Check tool installation status on mount
|
||||
useEffect(() => {
|
||||
const checkToolInstalled = async () => {
|
||||
try {
|
||||
console.log('[useInstallationSetup] Checking tool installation status...');
|
||||
const result = await window.ipcRenderer.invoke("check-tool-installed");
|
||||
|
||||
if (result.success && initState === "done" && !result.isInstalled) {
|
||||
console.log('[useInstallationSetup] Tool not installed, setting initState to carousel');
|
||||
setInitState("carousel");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[useInstallationSetup] Tool installation check failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const checkBackendStatus = async() => {
|
||||
// Also check if installation is currently in progress
|
||||
const installationStatus = await window.electronAPI.getInstallationStatus();
|
||||
console.log('[useInstallationSetup] Installation status check:', installationStatus);
|
||||
|
||||
if (installationStatus.success && installationStatus.isInstalling) {
|
||||
console.log('[useInstallationSetup] Installation in progress, starting frontend state');
|
||||
startInstallation();
|
||||
}
|
||||
}
|
||||
|
||||
checkToolInstalled();
|
||||
checkBackendStatus();
|
||||
}, [initState, setInitState, startInstallation]);
|
||||
|
||||
// Setup Electron IPC listeners (only once)
|
||||
useEffect(() => {
|
||||
// Electron IPC event handlers
|
||||
const handleInstallStart = () => {
|
||||
startInstallation();
|
||||
};
|
||||
|
||||
const handleInstallLog = (data: { type: string; data: string }) => {
|
||||
addLog({
|
||||
type: data.type as 'stdout' | 'stderr',
|
||||
data: data.data,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleInstallComplete = (data: { success: boolean; code?: number; error?: string }) => {
|
||||
console.log('[useInstallationSetup] Install complete event received:', data);
|
||||
|
||||
if (data.success) {
|
||||
setSuccess();
|
||||
setInitState('done');
|
||||
} else {
|
||||
setError(data.error || 'Installation failed');
|
||||
}
|
||||
};
|
||||
|
||||
// Register Electron IPC listeners
|
||||
window.electronAPI.onInstallDependenciesStart(handleInstallStart);
|
||||
window.electronAPI.onInstallDependenciesLog(handleInstallLog);
|
||||
window.electronAPI.onInstallDependenciesComplete(handleInstallComplete);
|
||||
|
||||
console.log('[useInstallationSetup] Installation listeners registered');
|
||||
|
||||
// Cleanup listeners on unmount
|
||||
return () => {
|
||||
window.electronAPI.removeAllListeners('install-dependencies-start');
|
||||
window.electronAPI.removeAllListeners('install-dependencies-log');
|
||||
window.electronAPI.removeAllListeners('install-dependencies-complete');
|
||||
};
|
||||
}, [startInstallation, addLog, setSuccess, setError, setInitState]);
|
||||
};
|
||||
216
src/store/installationStore.ts
Normal file
216
src/store/installationStore.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
// Define all possible installation states
|
||||
export type InstallationState =
|
||||
| 'idle'
|
||||
| 'checking-permissions'
|
||||
| 'showing-carousel'
|
||||
| 'installing'
|
||||
| 'error'
|
||||
| 'completed';
|
||||
|
||||
// Installation log entry
|
||||
export interface InstallationLog {
|
||||
type: 'stdout' | 'stderr';
|
||||
data: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Installation store state
|
||||
interface InstallationStoreState {
|
||||
// Core state
|
||||
state: InstallationState;
|
||||
progress: number;
|
||||
logs: InstallationLog[];
|
||||
error?: string;
|
||||
isVisible: boolean;
|
||||
|
||||
// Actions
|
||||
startInstallation: () => void;
|
||||
addLog: (log: InstallationLog) => void;
|
||||
setSuccess: () => void;
|
||||
setError: (error: string) => void;
|
||||
retryInstallation: () => void;
|
||||
completeSetup: () => void;
|
||||
updateProgress: (progress: number) => void;
|
||||
setVisible: (visible: boolean) => void;
|
||||
reset: () => void;
|
||||
|
||||
// Async actions
|
||||
performInstallation: () => Promise<void>;
|
||||
exportLog: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState = {
|
||||
state: 'idle' as InstallationState,
|
||||
progress: 20,
|
||||
logs: [] as InstallationLog[],
|
||||
error: undefined,
|
||||
isVisible: false,
|
||||
};
|
||||
|
||||
// Create the installation store
|
||||
export const useInstallationStore = create<InstallationStoreState>()(
|
||||
subscribeWithSelector(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
...initialState,
|
||||
|
||||
// Basic actions
|
||||
startInstallation: () =>
|
||||
set({
|
||||
state: 'installing',
|
||||
progress: 20,
|
||||
logs: [],
|
||||
error: undefined,
|
||||
isVisible: true,
|
||||
}),
|
||||
|
||||
addLog: (log: InstallationLog) =>
|
||||
set((state) => {
|
||||
const newProgress = Math.min(state.progress + 5, 90);
|
||||
return {
|
||||
logs: [...state.logs, log],
|
||||
progress: newProgress,
|
||||
};
|
||||
}),
|
||||
|
||||
setSuccess: () =>
|
||||
set({
|
||||
state: 'completed',
|
||||
progress: 100,
|
||||
}),
|
||||
|
||||
setError: (error: string) =>
|
||||
set((state) => ({
|
||||
state: 'error',
|
||||
error,
|
||||
logs: [
|
||||
...state.logs,
|
||||
{
|
||||
type: 'stderr',
|
||||
data: error,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
],
|
||||
})),
|
||||
|
||||
retryInstallation: () => {
|
||||
set({
|
||||
...initialState,
|
||||
isVisible: true,
|
||||
state: 'installing',
|
||||
});
|
||||
get().performInstallation();
|
||||
},
|
||||
|
||||
completeSetup: () =>
|
||||
set({
|
||||
state: 'completed',
|
||||
isVisible: false,
|
||||
}),
|
||||
|
||||
updateProgress: (progress: number) =>
|
||||
set({ progress }),
|
||||
|
||||
setVisible: (visible: boolean) =>
|
||||
set({ isVisible: visible }),
|
||||
|
||||
reset: () =>
|
||||
set(initialState),
|
||||
|
||||
// Async actions
|
||||
performInstallation: async () => {
|
||||
const { startInstallation, setSuccess, setError } = get();
|
||||
|
||||
try {
|
||||
startInstallation();
|
||||
const result = await window.electronAPI.checkAndInstallDepsOnUpdate();
|
||||
|
||||
if (result.success) {
|
||||
setSuccess();
|
||||
// Update auth store
|
||||
const { useAuthStore } = await import('./authStore');
|
||||
useAuthStore.getState().setInitState('done');
|
||||
} else {
|
||||
setError('Installation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Unknown error');
|
||||
}
|
||||
},
|
||||
|
||||
exportLog: async () => {
|
||||
try {
|
||||
const response = await window.electronAPI.exportLog();
|
||||
|
||||
if (!response.success) {
|
||||
alert('Export cancelled: ' + response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.savedPath) {
|
||||
window.location.href = 'https://github.com/eigent-ai/eigent/issues/new/choose';
|
||||
alert('Log saved: ' + response.savedPath);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert('Export error: ' + e.message);
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Computed selectors
|
||||
export const useLatestLog = () => useInstallationStore(state =>
|
||||
state.logs[state.logs.length - 1]
|
||||
);
|
||||
|
||||
export const useInstallationActions = () => useInstallationStore(state => ({
|
||||
startInstallation: state.startInstallation,
|
||||
retryInstallation: state.retryInstallation,
|
||||
completeSetup: state.completeSetup,
|
||||
performInstallation: state.performInstallation,
|
||||
exportLog: state.exportLog,
|
||||
}));
|
||||
|
||||
// Combined hook for components that need multiple pieces of state
|
||||
export const useInstallationStatus = () => {
|
||||
const state = useInstallationStore(state => state.state);
|
||||
const isVisible = useInstallationStore(state => state.isVisible);
|
||||
|
||||
return {
|
||||
isInstalling: state === 'installing',
|
||||
installationState: state,
|
||||
shouldShowInstallScreen: isVisible && state !== 'completed',
|
||||
isInstallationComplete: state === 'completed',
|
||||
canRetry: state === 'error',
|
||||
};
|
||||
};
|
||||
|
||||
// Hook for the main installation UI component
|
||||
export const useInstallationUI = () => {
|
||||
const state = useInstallationStore(state => state.state);
|
||||
const progress = useInstallationStore(state => state.progress);
|
||||
const logs = useInstallationStore(state => state.logs);
|
||||
const error = useInstallationStore(state => state.error);
|
||||
const isVisible = useInstallationStore(state => state.isVisible);
|
||||
const performInstallation = useInstallationStore(state => state.performInstallation);
|
||||
const retryInstallation = useInstallationStore(state => state.retryInstallation);
|
||||
const exportLog = useInstallationStore(state => state.exportLog);
|
||||
|
||||
return {
|
||||
installationState: state,
|
||||
progress,
|
||||
latestLog: logs[logs.length - 1],
|
||||
error,
|
||||
isInstalling: state === 'installing',
|
||||
shouldShowInstallScreen: isVisible && state !== 'completed',
|
||||
canRetry: state === 'error',
|
||||
performInstallation,
|
||||
retryInstallation,
|
||||
exportLog,
|
||||
};
|
||||
};
|
||||
11
src/types/electron.d.ts
vendored
11
src/types/electron.d.ts
vendored
|
|
@ -45,9 +45,16 @@ interface ElectronAPI {
|
|||
envRemove: (email: string, key: string) => Promise<any>;
|
||||
getEnvPath: (email: string) => Promise<string>;
|
||||
executeCommand: (command: string,email:string) => Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }>;
|
||||
installDependencies: () => Promise<{ success: boolean; error?: string }>;
|
||||
frontendReady: () => Promise<{ success: boolean; error?: string }>;
|
||||
checkAndInstallDepsOnUpdate: () => Promise<{ success: boolean; error?: string }>;
|
||||
checkInstallBrowser: () => Promise<{ data:any[] }>;
|
||||
getInstallationStatus: () => Promise<{
|
||||
success: boolean;
|
||||
isInstalling?: boolean;
|
||||
hasLockFile?: boolean;
|
||||
installedExists?: boolean;
|
||||
timestamp?: number;
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue