From 214345884a82f22a7644c0cae94ff8af15332753 Mon Sep 17 00:00:00 2001 From: 4pmtong Date: Tue, 13 Jan 2026 23:15:11 +0800 Subject: [PATCH 1/6] :memo: docs: optimize local development setup --- backend/.python-version | 2 +- electron/main/index.ts | 802 ++++++++++------- server/.env.example | 4 +- server/README_CN.md | 16 +- server/README_EN.md | 13 +- .../app/controller/user/login_controller.py | 122 ++- server/docker-compose.dev.yml | 30 + src/pages/Login.tsx | 808 +++++++++--------- 8 files changed, 1072 insertions(+), 725 deletions(-) create mode 100644 server/docker-compose.dev.yml diff --git a/backend/.python-version b/backend/.python-version index 59002f8f6..8cc1b46f5 100644 --- a/backend/.python-version +++ b/backend/.python-version @@ -1 +1 @@ -3.10.16 \ No newline at end of file +3.10.15 diff --git a/electron/main/index.ts b/electron/main/index.ts index fa62d7fa3..d6a6b5eae 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,26 +1,48 @@ -import { app, BrowserWindow, shell, ipcMain, Menu, dialog, nativeTheme, protocol, session } from 'electron' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import os, { homedir } from 'node:os' -import log from 'electron-log' -import { update, registerUpdateIpcHandlers } from './update' -import { checkToolInstalled, killProcessOnPort, startBackend } from './init' -import { WebViewManager } from './webview' -import { FileReader } from './fileReader' -import { ChildProcessWithoutNullStreams } from 'node:child_process' -import fs, { existsSync, readFileSync } from 'node:fs' -import fsp from 'fs/promises' -import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig' -import { getEnvPath, updateEnvBlock, removeEnvKey, getEmailFolderPath } from './utils/envUtil' -import { copyBrowserData } from './copy' -import { findAvailablePort } from './init' +import { + app, + BrowserWindow, + shell, + ipcMain, + Menu, + dialog, + nativeTheme, + protocol, + session, +} from 'electron'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import os, { homedir } from 'node:os'; +import log from 'electron-log'; +import installExtension, { + REACT_DEVELOPER_TOOLS, +} from 'electron-devtools-installer'; +import { update, registerUpdateIpcHandlers } from './update'; +import { checkToolInstalled, killProcessOnPort, startBackend } from './init'; +import { WebViewManager } from './webview'; +import { FileReader } from './fileReader'; +import { ChildProcessWithoutNullStreams } from 'node:child_process'; +import fs, { existsSync, readFileSync } from 'node:fs'; +import fsp from 'fs/promises'; +import { addMcp, removeMcp, updateMcp, readMcpConfig } from './utils/mcpConfig'; +import { + getEnvPath, + updateEnvBlock, + removeEnvKey, + getEmailFolderPath, +} from './utils/envUtil'; +import { copyBrowserData } from './copy'; +import { findAvailablePort } from './init'; import kill from 'tree-kill'; -import { zipFolder } from './utils/log' -import mime from "mime"; +import { zipFolder } from './utils/log'; +import mime from 'mime'; import axios from 'axios'; import FormData from 'form-data'; -import { checkAndInstallDepsOnUpdate, PromiseReturnType, getInstallationStatus } from './install-deps' -import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process' +import { + checkAndInstallDepsOnUpdate, + PromiseReturnType, + getInstallationStatus, +} from './install-deps'; +import { isBinaryExists, getBackendPath, getVenvPath } from './utils/process'; const userData = app.getPath('userData'); @@ -59,12 +81,16 @@ let profileInitPromise: Promise; // 2. WebView: partition 'persist:user_login' in app userData → will import cookies from tool_controller via session API // 3. tool_controller: ~/.eigent/browser_profiles/profile_user_login → source of truth for login cookies // 4. CDP browser: uses separate profile (doesn't share with main app) -profileInitPromise = findAvailablePort(browser_port).then(async port => { +profileInitPromise = findAvailablePort(browser_port).then(async (port) => { browser_port = port; app.commandLine.appendSwitch('remote-debugging-port', port + ''); // Create isolated profile for CDP browser only - const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles'); + const browserProfilesBase = path.join( + os.homedir(), + '.eigent', + 'browser_profiles' + ); const cdpProfile = path.join(browserProfilesBase, `cdp_profile_${port}`); try { @@ -118,22 +144,26 @@ log.transports.console.format = '[{level}]{text}'; log.transports.file.format = '[{level}]{text}'; // Disable GPU Acceleration for Windows 7 -if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() +if (os.release().startsWith('6.1')) app.disableHardwareAcceleration(); // Set application name for Windows 10+ notifications -if (process.platform === 'win32') app.setAppUserModelId(app.getName()) +if (process.platform === 'win32') app.setAppUserModelId(app.getName()); if (!app.requestSingleInstanceLock()) { - app.quit() - process.exit(0) + app.quit(); + process.exit(0); } // ==================== protocol config ==================== const setupProtocolHandlers = () => { if (process.env.NODE_ENV === 'development') { - const isDefault = app.isDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]); + const isDefault = app.isDefaultProtocolClient('eigent', process.execPath, [ + path.resolve(process.argv[1]), + ]); if (!isDefault) { - app.setAsDefaultProtocolClient('eigent', process.execPath, [path.resolve(process.argv[1])]); + app.setAsDefaultProtocolClient('eigent', process.execPath, [ + path.resolve(process.argv[1]), + ]); } } else { app.setAsDefaultProtocolClient('eigent'); @@ -143,7 +173,7 @@ const setupProtocolHandlers = () => { // ==================== protocol url handle ==================== function handleProtocolUrl(url: string) { log.info('enter handleProtocolUrl', url); - + // If window is not ready, queue the URL if (!isWindowReady || !win || win.isDestroyed()) { log.info('Window not ready, queuing protocol URL:', url); @@ -171,7 +201,7 @@ function processProtocolUrl(url: string) { log.info('oauth'); const provider = urlObj.searchParams.get('provider'); const code = urlObj.searchParams.get('code'); - log.info("protocol oauth", provider, code); + log.info('protocol oauth', provider, code); win.webContents.send('oauth-authorized', { provider, code }); return; } @@ -196,14 +226,16 @@ function processQueuedProtocolUrls() { // Verify window is ready before processing if (!win || win.isDestroyed() || !isWindowReady) { - log.warn('Window not ready for processing queued URLs, keeping URLs in queue'); + log.warn( + 'Window not ready for processing queued URLs, keeping URLs in queue' + ); return; } const urls = [...protocolUrlQueue]; protocolUrlQueue = []; - urls.forEach(url => { + urls.forEach((url) => { processProtocolUrl(url); }); } @@ -213,18 +245,18 @@ function processQueuedProtocolUrls() { const setupSingleInstanceLock = () => { const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { - log.info("no-lock"); + log.info('no-lock'); app.quit(); } else { app.on('second-instance', (event, argv) => { - log.info("second-instance", argv); - const url = argv.find(arg => arg.startsWith('eigent://')); + log.info('second-instance', argv); + const url = argv.find((arg) => arg.startsWith('eigent://')); if (url) handleProtocolUrl(url); if (win) win.show(); }); app.on('open-url', (event, url) => { - log.info("open-url"); + log.info('open-url'); event.preventDefault(); handleProtocolUrl(url); }); @@ -244,9 +276,9 @@ const initializeApp = () => { */ // Get backup log path const getBackupLogPath = () => { - const userDataPath = app.getPath('userData') - return path.join(userDataPath, 'logs', 'main.log') -} + const userDataPath = app.getPath('userData'); + return path.join(userDataPath, 'logs', 'main.log'); +}; // Constants define const BROWSER_PATHS = { win32: { @@ -254,10 +286,16 @@ const BROWSER_PATHS = { edge: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', firefox: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', qq: 'C:\\Program Files\\Tencent\\QQBrowser\\QQBrowser.exe', - '360': path.join(homedir(), 'AppData\\Local\\360Chrome\\Chrome\\Application\\360chrome.exe'), + '360': path.join( + homedir(), + 'AppData\\Local\\360Chrome\\Chrome\\Application\\360chrome.exe' + ), arc: path.join(homedir(), 'AppData\\Local\\Arc\\User Data\\Arc.exe'), dia: path.join(homedir(), 'AppData\\Local\\Dia\\Application\\dia.exe'), - fellou: path.join(homedir(), 'AppData\\Local\\Fellou\\Application\\fellou.exe'), + fellou: path.join( + homedir(), + 'AppData\\Local\\Fellou\\Application\\fellou.exe' + ), }, darwin: { chrome: '/Applications/Google Chrome.app', @@ -286,26 +324,26 @@ const checkManagerInstance = (manager: any, name: string) => { function registerIpcHandlers() { // ==================== basic info handler ==================== ipcMain.handle('get-browser-port', () => { - log.info('Getting browser port') - return browser_port + log.info('Getting browser port'); + return browser_port; }); ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-backend-port', () => backendPort); - + // ==================== restart app handler ==================== ipcMain.handle('restart-app', async () => { log.info('[RESTART] Restarting app to apply user profile changes'); - + // Clean up Python process first await cleanupPythonProcess(); - + // Schedule relaunch after a short delay setTimeout(() => { app.relaunch(); app.quit(); }, 100); }); - + ipcMain.handle('restart-backend', async () => { try { if (backendPort) { @@ -331,94 +369,98 @@ function registerIpcHandlers() { return platform === 'win32' ? process.env.USERPROFILE : process.env.HOME; }); - // ==================== command execution handler ==================== ipcMain.handle('get-email-folder-path', async (event, email: string) => { return getEmailFolderPath(email); }); - ipcMain.handle('execute-command', async (event, command: string, email: string) => { - log.info("execute-command", command); - const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); + ipcMain.handle( + 'execute-command', + async (event, command: string, email: string) => { + log.info('execute-command', command); + const { MCP_REMOTE_CONFIG_DIR } = getEmailFolderPath(email); - try { - const { spawn } = await import('child_process'); + try { + const { spawn } = await import('child_process'); - // Add --host parameter - const commandWithHost = `${command} --debug --host dev.eigent.ai/api/oauth/notion/callback?code=1`; - // const commandWithHost = `${command}`; + // Add --host parameter + const commandWithHost = `${command} --debug --host dev.eigent.ai/api/oauth/notion/callback?code=1`; + // const commandWithHost = `${command}`; - log.info(' start execute command:', commandWithHost); + log.info(' start execute command:', commandWithHost); - // Parse command and arguments - const [cmd, ...args] = commandWithHost.split(' '); - log.info('start execute command:', commandWithHost.split(' ')); - console.log(cmd, args) - return new Promise((resolve) => { - const child = spawn(cmd, args, { - cwd: process.cwd(), - env: { ...process.env, MCP_REMOTE_CONFIG_DIR }, - stdio: ['pipe', 'pipe', 'pipe'] - }); + // Parse command and arguments + const [cmd, ...args] = commandWithHost.split(' '); + log.info('start execute command:', commandWithHost.split(' ')); + console.log(cmd, args); + return new Promise((resolve) => { + const child = spawn(cmd, args, { + cwd: process.cwd(), + env: { ...process.env, MCP_REMOTE_CONFIG_DIR }, + stdio: ['pipe', 'pipe', 'pipe'], + }); - let stdout = ''; - let stderr = ''; + let stdout = ''; + let stderr = ''; - // Realtime listen standard output - child.stdout.on('data', (data) => { - const output = data.toString(); - stdout += output; - log.info('Real-time output:', output.trim()); - }); + // Realtime listen standard output + child.stdout.on('data', (data) => { + const output = data.toString(); + stdout += output; + log.info('Real-time output:', output.trim()); + }); - // Realtime listen error output - child.stderr.on('data', (data) => { - const output = data.toString(); - stderr += output; - if (output.includes('OAuth callback server running at')) { - const url = output.split('OAuth callback server running at')[1].trim(); - log.info('detect OAuth callback URL:', url); - - // Notify frontend to callback URL - if (win && !win.isDestroyed()) { - const match = url.match(/^https?:\/\/[^:\n]+:\d+/); - const cleanedUrl = match ? match[0] : null; - log.info('cleanedUrl', cleanedUrl); - win.webContents.send('oauth-callback-url', { - url: cleanedUrl, - provider: 'notion' // TODO: can be set dynamically according to actual situation - }); + // Realtime listen error output + child.stderr.on('data', (data) => { + const output = data.toString(); + stderr += output; + if (output.includes('OAuth callback server running at')) { + const url = output + .split('OAuth callback server running at')[1] + .trim(); + log.info('detect OAuth callback URL:', url); + // Notify frontend to callback URL + if (win && !win.isDestroyed()) { + const match = url.match(/^https?:\/\/[^:\n]+:\d+/); + const cleanedUrl = match ? match[0] : null; + log.info('cleanedUrl', cleanedUrl); + win.webContents.send('oauth-callback-url', { + url: cleanedUrl, + provider: 'notion', // TODO: can be set dynamically according to actual situation + }); + } } - } - if (output.includes('Press Ctrl+C to exit')) { - child.kill(); - } - log.info(' real-time error output:', output.trim()); - }); + if (output.includes('Press Ctrl+C to exit')) { + child.kill(); + } + log.info(' real-time error output:', output.trim()); + }); - // Listen process exit - child.on('close', (code) => { - log.info(` command execute complete, exit code: ${code}`); - resolve({ success: code === null, stdout, stderr }); - }); + // Listen process exit + child.on('close', (code) => { + log.info(` command execute complete, exit code: ${code}`); + resolve({ success: code === null, stdout, stderr }); + }); - // Listen process error - child.on('error', (error) => { - log.error(' command execute error:', error); - resolve({ success: false, error: error.message }); + // Listen process error + child.on('error', (error) => { + log.error(' command execute error:', error); + resolve({ success: false, error: error.message }); + }); }); - }); - } catch (error: any) { - log.error(' command execute failed:', error); - return { success: false, error: error.message }; + } catch (error: any) { + log.error(' command execute failed:', error); + return { success: false, error: error.message }; + } } - }); + ); - ipcMain.handle("read-file-dataurl", async (event, filePath) => { + ipcMain.handle('read-file-dataurl', async (event, filePath) => { try { const file = fs.readFileSync(filePath); - const mimeType = mime.getType(path.extname(filePath)) || "application/octet-stream"; - return `data:${mimeType};base64,${file.toString("base64")}`; + const mimeType = + mime.getType(path.extname(filePath)) || 'application/octet-stream'; + return `data:${mimeType};base64,${file.toString('base64')}`; } catch (error: any) { log.error('Failed to read file as data URL:', filePath, error); throw new Error(`Failed to read file: ${error.message}`); @@ -457,7 +499,7 @@ function registerIpcHandlers() { const { canceled, filePath } = await dialog.showSaveDialog({ title: 'save log file', defaultPath: defaultFileName, - filters: [{ name: 'log file', extensions: ['log', 'txt'] }] + filters: [{ name: 'log file', extensions: ['log', 'txt'] }], }); if (canceled || !filePath) { @@ -471,71 +513,84 @@ function registerIpcHandlers() { } }); - ipcMain.handle('upload-log', async (event, email: string, taskId: string, baseUrl: string, token: string) => { - let zipPath: string | null = null; + 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' }; - } + 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' }; - } + // 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); + 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' }; - } + // 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); + 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); + // 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 - }); + // 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(); + 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); + 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) => { @@ -545,7 +600,10 @@ function registerIpcHandlers() { mcp.args = JSON.parse(mcp.args); } catch (e) { // If parsing fails, split by comma as fallback - mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + mcp.args = mcp.args + .split(',') + .map((arg: string) => arg.trim()) + .filter((arg: string) => arg !== ''); } } addMcp(name, mcp); @@ -564,7 +622,10 @@ function registerIpcHandlers() { mcp.args = JSON.parse(mcp.args); } catch (e) { // If parsing fails, split by comma as fallback - mcp.args = mcp.args.split(',').map((arg: string) => arg.trim()).filter((arg: string) => arg !== ''); + mcp.args = mcp.args + .split(',') + .map((arg: string) => arg.trim()) + .filter((arg: string) => arg !== ''); } } updateMcp(name, mcp); @@ -650,10 +711,10 @@ function registerIpcHandlers() { // ==================== window control handler ==================== ipcMain.on('window-close', (_, data) => { - if(data.isForceQuit) { - return app?.quit() + if (data.isForceQuit) { + return app?.quit(); } - return win?.close() + return win?.close(); }); ipcMain.on('window-minimize', () => win?.minimize()); ipcMain.on('window-toggle-maximize', () => { @@ -668,38 +729,40 @@ function registerIpcHandlers() { ipcMain.handle('select-file', async (event, options = {}) => { const result = await dialog.showOpenDialog(win!, { properties: ['openFile', 'multiSelections'], - ...options + ...options, }); if (!result.canceled && result.filePaths.length > 0) { - const files = result.filePaths.map(filePath => ({ + const files = result.filePaths.map((filePath) => ({ filePath, - fileName: filePath.split(/[/\\]/).pop() || '' + fileName: filePath.split(/[/\\]/).pop() || '', })); return { success: true, files, - fileCount: files.length + fileCount: files.length, }; } return { success: false, - canceled: result.canceled + canceled: result.canceled, }; }); - ipcMain.handle("reveal-in-folder", async (event, filePath: string) => { + ipcMain.handle('reveal-in-folder', async (event, filePath: string) => { try { - const stats = await fs.promises.stat(filePath.replace(/\/$/, '')).catch(() => null); + const stats = await fs.promises + .stat(filePath.replace(/\/$/, '')) + .catch(() => null); if (stats && stats.isDirectory()) { shell.openPath(filePath); } else { shell.showItemInFolder(filePath); } } catch (e) { - log.error("reveal in folder failed", e); + log.error('reveal in folder failed', e); } }); @@ -728,13 +791,13 @@ function registerIpcHandlers() { return { success: true, data: fileContent, - size: fileContent.length + size: fileContent.length, }; } catch (error: any) { log.error('Failed to read file:', filePath, error); return { success: false, - error: error.message || 'Failed to read file' + error: error.message || 'Failed to read file', }; } }); @@ -764,13 +827,13 @@ function registerIpcHandlers() { return { success: true, - message: 'Folder deleted successfully' + message: 'Folder deleted successfully', }; } catch (error: any) { log.error('Failed to delete folder:', MCP_REMOTE_CONFIG_DIR, error); return { success: false, - error: error.message || 'Failed to delete folder' + error: error.message || 'Failed to delete folder', }; } }); @@ -790,7 +853,7 @@ function registerIpcHandlers() { log.error('Failed to get MCP config path:', error); return { success: false, - error: error.message || 'Failed to get MCP config path' + error: error.message || 'Failed to get MCP config path', }; } }); @@ -805,33 +868,39 @@ function registerIpcHandlers() { const ENV_PATH = getEnvPath(email); let content = ''; try { - content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; + content = fs.existsSync(ENV_PATH) + ? fs.readFileSync(ENV_PATH, 'utf-8') + : ''; } catch (error) { - log.error("env-remove error:", error); + log.error('env-remove error:', error); } let lines = content.split(/\r?\n/); - return { success: lines.some(line => line.startsWith(key + '=')) }; + return { success: lines.some((line) => line.startsWith(key + '=')) }; }); ipcMain.handle('env-write', async (_event, email, { key, value }) => { const ENV_PATH = getEnvPath(email); let content = ''; try { - content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; + content = fs.existsSync(ENV_PATH) + ? fs.readFileSync(ENV_PATH, 'utf-8') + : ''; } catch (error) { - log.error("env-write error:", error); + log.error('env-write error:', error); } let lines = content.split(/\r?\n/); lines = updateEnvBlock(lines, { [key]: value }); fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); - + // Also write to global .env file for backend process to read const GLOBAL_ENV_PATH = path.join(os.homedir(), '.eigent', '.env'); let globalContent = ''; try { - globalContent = fs.existsSync(GLOBAL_ENV_PATH) ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8') : ''; + globalContent = fs.existsSync(GLOBAL_ENV_PATH) + ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8') + : ''; } catch (error) { - log.error("global env-write read error:", error); + log.error('global env-write read error:', error); } let globalLines = globalContent.split(/\r?\n/); globalLines = updateEnvBlock(globalLines, { [key]: value }); @@ -839,38 +908,44 @@ function registerIpcHandlers() { fs.writeFileSync(GLOBAL_ENV_PATH, globalLines.join('\n'), 'utf-8'); log.info(`env-write: wrote ${key} to both user and global .env files`); } catch (error) { - log.error("global env-write error:", error); + log.error('global env-write error:', error); } - + return { success: true }; }); ipcMain.handle('env-remove', async (_event, email, key) => { - log.info("env-remove", key); + log.info('env-remove', key); const ENV_PATH = getEnvPath(email); let content = ''; try { - content = fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, 'utf-8') : ''; + content = fs.existsSync(ENV_PATH) + ? fs.readFileSync(ENV_PATH, 'utf-8') + : ''; } catch (error) { - log.error("env-remove error:", error); + log.error('env-remove error:', error); } let lines = content.split(/\r?\n/); lines = removeEnvKey(lines, key); fs.writeFileSync(ENV_PATH, lines.join('\n'), 'utf-8'); - log.info("env-remove success", ENV_PATH); - + log.info('env-remove success', ENV_PATH); + // Also remove from global .env file const GLOBAL_ENV_PATH = path.join(os.homedir(), '.eigent', '.env'); try { - let globalContent = fs.existsSync(GLOBAL_ENV_PATH) ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8') : ''; + let globalContent = fs.existsSync(GLOBAL_ENV_PATH) + ? fs.readFileSync(GLOBAL_ENV_PATH, 'utf-8') + : ''; let globalLines = globalContent.split(/\r?\n/); globalLines = removeEnvKey(globalLines, key); fs.writeFileSync(GLOBAL_ENV_PATH, globalLines.join('\n'), 'utf-8'); - log.info(`env-remove: removed ${key} from both user and global .env files`); + log.info( + `env-remove: removed ${key} from both user and global .env files` + ); } catch (error) { - log.error("global env-remove error:", error); + log.error('global env-remove error:', error); } - + return { success: true }; }); @@ -892,10 +967,13 @@ function registerIpcHandlers() { }); // ==================== FileReader handler ==================== - ipcMain.handle('open-file', async (_, type: string, filePath: string, isShowSourceCode: boolean) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.openFile(type, filePath, isShowSourceCode); - }); + ipcMain.handle( + 'open-file', + async (_, type: string, filePath: string, isShowSourceCode: boolean) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.openFile(type, filePath, isShowSourceCode); + } + ); ipcMain.handle('download-file', async (_, url: string) => { try { @@ -945,41 +1023,59 @@ function registerIpcHandlers() { } }); - ipcMain.handle('get-file-list', async (_, email: string, taskId: string, projectId?: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.getFileList(email, taskId, projectId); - }); + ipcMain.handle( + 'get-file-list', + async (_, email: string, taskId: string, projectId?: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.getFileList(email, taskId, projectId); + } + ); - ipcMain.handle('delete-task-files', async (_, email: string, taskId: string, projectId?: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.deleteTaskFiles(email, taskId, projectId); - }); + ipcMain.handle( + 'delete-task-files', + async (_, email: string, taskId: string, projectId?: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.deleteTaskFiles(email, taskId, projectId); + } + ); // New project management handlers - ipcMain.handle('create-project-structure', async (_, email: string, projectId: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.createProjectStructure(email, projectId); - }); + ipcMain.handle( + 'create-project-structure', + async (_, email: string, projectId: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.createProjectStructure(email, projectId); + } + ); ipcMain.handle('get-project-list', async (_, email: string) => { const manager = checkManagerInstance(fileReader, 'FileReader'); return manager.getProjectList(email); }); - ipcMain.handle('get-tasks-in-project', async (_, email: string, projectId: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.getTasksInProject(email, projectId); - }); + ipcMain.handle( + 'get-tasks-in-project', + async (_, email: string, projectId: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.getTasksInProject(email, projectId); + } + ); - ipcMain.handle('move-task-to-project', async (_, email: string, taskId: string, projectId: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.moveTaskToProject(email, taskId, projectId); - }); + ipcMain.handle( + 'move-task-to-project', + async (_, email: string, taskId: string, projectId: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.moveTaskToProject(email, taskId, projectId); + } + ); - ipcMain.handle('get-project-file-list', async (_, email: string, projectId: string) => { - const manager = checkManagerInstance(fileReader, 'FileReader'); - return manager.getProjectFileList(email, projectId); - }); + ipcMain.handle( + 'get-project-file-list', + async (_, email: string, projectId: string) => { + const manager = checkManagerInstance(fileReader, 'FileReader'); + return manager.getProjectFileList(email, projectId); + } + ); ipcMain.handle('get-log-folder', async (_, email: string) => { const manager = checkManagerInstance(fileReader, 'FileReader'); @@ -1010,23 +1106,28 @@ function registerIpcHandlers() { // ==================== dependency install handler ==================== ipcMain.handle('install-dependencies', async () => { try { - if(win === null) throw new Error("Window is null"); + if (win === null) throw new Error('Window is null'); // Prevent concurrent installations if (isInstallationInProgress) { log.info('[DEPS INSTALL] Installation already in progress, waiting...'); await installationLock; - return { success: true, message: 'Installation completed by another process' }; + return { + success: true, + message: 'Installation completed by another process', + }; } log.info('[DEPS INSTALL] Manual installation/retry triggered'); // Set lock isInstallationInProgress = true; - installationLock = checkAndInstallDepsOnUpdate({win, forceInstall: true}) - .finally(() => { - isInstallationInProgress = false; - }); + installationLock = checkAndInstallDepsOnUpdate({ + win, + forceInstall: true, + }).finally(() => { + isInstallationInProgress = false; + }); const result = await installationLock; @@ -1040,8 +1141,13 @@ function registerIpcHandlers() { // IMPORTANT: Send install-dependencies-complete success event if (!win.isDestroyed()) { - win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); - log.info('[DEPS INSTALL] Sent install-dependencies-complete event after retry'); + win.webContents.send('install-dependencies-complete', { + success: true, + code: 0, + }); + log.info( + '[DEPS INSTALL] Sent install-dependencies-complete event after retry' + ); } // Start backend after retry with cleanup @@ -1066,11 +1172,11 @@ function registerIpcHandlers() { ipcMain.handle('get-installation-status', async () => { try { const { isInstalling, hasLockFile } = await getInstallationStatus(); - return { - success: true, - isInstalling, + return { + success: true, + isInstalling, hasLockFile, - timestamp: Date.now() + timestamp: Date.now(), }; } catch (error) { return { success: false, error: (error as Error).message }; @@ -1109,14 +1215,17 @@ const startBackendAfterInstall = async () => { log.info('[DEPS INSTALL] Starting backend...'); // Add a small delay to ensure any previous processes are fully cleaned up - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); await checkAndStartBackend(); }; // ==================== installation lock ==================== let isInstallationInProgress = false; -let installationLock: Promise = Promise.resolve({ message: "No installation needed", success: true }); +let installationLock: Promise = Promise.resolve({ + message: 'No installation needed', + success: true, +}); // ==================== window create ==================== async function createWindow() { @@ -1125,10 +1234,20 @@ async function createWindow() { // Ensure .eigent directories exist before anything else ensureEigentDirectories(); - log.info(`[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}`); - log.info(`[PROJECT BROWSER WINDOW] Current user data path: ${app.getPath('userData')}`); - log.info(`[PROJECT BROWSER WINDOW] Command line switch user-data-dir: ${app.commandLine.getSwitchValue('user-data-dir')}`); - + log.info( + `[PROJECT BROWSER WINDOW] Creating BrowserWindow which will start Chrome with CDP on port ${browser_port}` + ); + log.info( + `[PROJECT BROWSER WINDOW] Current user data path: ${app.getPath( + 'userData' + )}` + ); + log.info( + `[PROJECT BROWSER WINDOW] Command line switch user-data-dir: ${app.commandLine.getSwitchValue( + 'user-data-dir' + )}` + ); + win = new BrowserWindow({ title: 'Eigent', width: 1200, @@ -1163,14 +1282,31 @@ async function createWindow() { // ==================== Import cookies from tool_controller to WebView BEFORE creating WebViews ==================== // Copy partition data files before any session accesses them try { - const browserProfilesBase = path.join(os.homedir(), '.eigent', 'browser_profiles'); - const toolControllerProfile = path.join(browserProfilesBase, 'profile_user_login'); - const toolControllerPartitionPath = path.join(toolControllerProfile, 'Partitions', 'user_login'); + const browserProfilesBase = path.join( + os.homedir(), + '.eigent', + 'browser_profiles' + ); + const toolControllerProfile = path.join( + browserProfilesBase, + 'profile_user_login' + ); + const toolControllerPartitionPath = path.join( + toolControllerProfile, + 'Partitions', + 'user_login' + ); if (fs.existsSync(toolControllerPartitionPath)) { - log.info('[COOKIE SYNC] Found tool_controller partition, copying to WebView partition...'); + log.info( + '[COOKIE SYNC] Found tool_controller partition, copying to WebView partition...' + ); - const targetPartitionPath = path.join(app.getPath('userData'), 'Partitions', 'user_login'); + const targetPartitionPath = path.join( + app.getPath('userData'), + 'Partitions', + 'user_login' + ); log.info('[COOKIE SYNC] From:', toolControllerPartitionPath); log.info('[COOKIE SYNC] To:', targetPartitionPath); @@ -1182,7 +1318,7 @@ async function createWindow() { // Copy the entire partition directory fs.cpSync(toolControllerPartitionPath, targetPartitionPath, { recursive: true, - force: true + force: true, }); log.info('[COOKIE SYNC] Successfully copied partition data to WebView'); @@ -1193,7 +1329,9 @@ async function createWindow() { log.info(`[COOKIE SYNC] Cookies file size: ${stats.size} bytes`); } } else { - log.info('[COOKIE SYNC] No tool_controller partition found, WebView will start fresh'); + log.info( + '[COOKIE SYNC] No tool_controller partition found, WebView will start fresh' + ); } } catch (error) { log.error('[COOKIE SYNC] Failed to sync partition data:', error); @@ -1204,7 +1342,9 @@ async function createWindow() { webViewManager = new WebViewManager(win); // create multiple webviews - log.info(`[PROJECT BROWSER] Creating WebViews with partition: persist:user_login`); + log.info( + `[PROJECT BROWSER] Creating WebViews with partition: persist:user_login` + ); for (let i = 1; i <= 8; i++) { webViewManager.createWebview(i === 1 ? undefined : i.toString()); } @@ -1243,7 +1383,13 @@ async function createWindow() { const venvPath = getVenvPath(currentVersion); const venvExists = fs.existsSync(venvPath); - const needsInstallation = !versionExists || savedVersion !== currentVersion || !uvExists || !bunExists || !installationCompleted || !venvExists; + const needsInstallation = + !versionExists || + savedVersion !== currentVersion || + !uvExists || + !bunExists || + !installationCompleted || + !venvExists; log.info('Installation check result:', { needsInstallation, @@ -1253,23 +1399,31 @@ async function createWindow() { bunExists, installationCompleted, venvExists, - venvPath + venvPath, }); // Handle localStorage based on installation state if (needsInstallation) { - log.info('Installation needed - resetting initState to carousel while preserving auth data'); + log.info( + 'Installation needed - resetting initState to carousel while preserving auth data' + ); // Instead of deleting the entire localStorage, we'll update only the initState // This preserves login information while resetting the initialization flow // Set up the injection for when page loads win.webContents.once('dom-ready', () => { if (!win || win.isDestroyed()) { - log.warn('Window destroyed before DOM ready - skipping localStorage injection'); + log.warn( + 'Window destroyed before DOM ready - skipping localStorage injection' + ); return; } - log.info('DOM ready - updating initState to carousel while preserving auth data'); - win.webContents.executeJavaScript(` + log.info( + 'DOM ready - updating initState to carousel while preserving auth data' + ); + win.webContents + .executeJavaScript( + ` (function() { try { const authStorage = localStorage.getItem('auth-storage'); @@ -1311,9 +1465,11 @@ async function createWindow() { console.error('[ELECTRON PRE-INJECT] Failed to update storage:', e); } })(); - `).catch(err => { - log.error('Failed to inject script:', err); - }); + ` + ) + .catch((err) => { + log.error('Failed to inject script:', err); + }); }); } else { // The proper flow is now handled by useInstallationSetup.ts with dual-check mechanism: @@ -1322,7 +1478,9 @@ async function createWindow() { // 3. Only when BOTH are true → setInitState('done') // // This ensures frontend never shows before backend is ready. - log.info('Installation already complete - letting useInstallationSetup handle state transitions'); + log.info( + 'Installation already complete - letting useInstallationSetup handle state transitions' + ); } // Load content @@ -1334,9 +1492,11 @@ async function createWindow() { } // Wait for window to be ready - await new Promise(resolve => { + await new Promise((resolve) => { win!.webContents.once('did-finish-load', () => { - log.info('Window content loaded, starting dependency check immediately...'); + log.info( + 'Window content loaded, starting dependency check immediately...' + ); resolve(); }); }); @@ -1347,28 +1507,33 @@ async function createWindow() { processQueuedProtocolUrls(); // Wait for React components to mount and register event listeners - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // Now check and install dependencies - let res:PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); + let res: PromiseReturnType = await checkAndInstallDepsOnUpdate({ win }); if (!res.success) { - log.info("[DEPS INSTALL] Dependency Error: ", res.message); + log.info('[DEPS INSTALL] Dependency Error: ', res.message); // Note: install-dependencies-complete failure event is already sent by installDependencies function // in install-deps.ts, so we don't send it again here to avoid duplicate events return; } - log.info("[DEPS INSTALL] Dependency Success: ", res.message); + log.info('[DEPS INSTALL] Dependency Success: ', res.message); // IMPORTANT: Wait a bit to ensure React components have mounted and registered event listeners // This prevents race condition where events are sent before listeners are ready - await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise((resolve) => setTimeout(resolve, 500)); // IMPORTANT: Always send install-dependencies-complete event when installation check succeeds // This includes both cases: actual installation completed AND installation was skipped (already installed) // The frontend needs this event to properly transition from installation screen to main app if (!win.isDestroyed()) { - win.webContents.send('install-dependencies-complete', { success: true, code: 0 }); - log.info("[DEPS INSTALL] Sent install-dependencies-complete event to frontend"); + win.webContents.send('install-dependencies-complete', { + success: true, + code: 0, + }); + log.info( + '[DEPS INSTALL] Sent install-dependencies-complete event to frontend' + ); } // Start backend after dependencies are ready @@ -1396,12 +1561,22 @@ const setupDevToolsShortcuts = () => { } // Ctrl+Shift+I (Windows/Linux) or Cmd+Shift+I (Mac) - if (input.control && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') { + if ( + input.control && + input.shift && + input.key.toLowerCase() === 'i' && + input.type === 'keyDown' + ) { toggleDevTools(); } // Mac Cmd+Shift+I - if (input.meta && input.shift && input.key.toLowerCase() === 'i' && input.type === 'keyDown') { + if ( + input.meta && + input.shift && + input.key.toLowerCase() === 'i' && + input.type === 'keyDown' + ) { toggleDevTools(); } }); @@ -1477,7 +1652,7 @@ const checkAndStartBackend = async () => { log.info('Backend is ready, notifying frontend...'); win.webContents.send('backend-ready', { success: true, - port: backendPort + port: backendPort, }); } @@ -1490,17 +1665,17 @@ const checkAndStartBackend = async () => { if (win && !win.isDestroyed()) { win.webContents.send('backend-ready', { success: false, - error: 'Tools not installed' + error: 'Tools not installed', }); } } } catch (error) { - log.error("Failed to start backend:", error); + log.error('Failed to start backend:', error); // Notify frontend of backend startup failure if (win && !win.isDestroyed()) { win.webContents.send('backend-ready', { success: false, - error: String(error) + error: String(error), }); } } @@ -1579,19 +1754,19 @@ const cleanupPythonProcess = async () => { // before close const handleBeforeClose = () => { - let isQuitting = false; - - app.on('before-quit', () => { - isQuitting = true; - }); - - win?.on("close", (event) => { - if (!isQuitting) { - event.preventDefault(); - win?.webContents.send("before-close"); - } - }) -} + let isQuitting = false; + + app.on('before-quit', () => { + isQuitting = true; + }); + + win?.on('close', (event) => { + if (!isQuitting) { + event.preventDefault(); + win?.webContents.send('before-close'); + } + }); +}; // ==================== app event handle ==================== app.whenReady().then(async () => { @@ -1604,6 +1779,21 @@ app.whenReady().then(async () => { log.error('[MAIN] Profile initialization failed:', error); } + // ==================== install React DevTools ==================== + // Only install in development mode + if (VITE_DEV_SERVER_URL) { + try { + log.info('[DEVTOOLS] Installing React DevTools extension...'); + const name = await installExtension(REACT_DEVELOPER_TOOLS, { + loadExtensionOptions: { allowFileAccess: true }, + }); + log.info(`[DEVTOOLS] Successfully installed extension: ${name}`); + } catch (err) { + log.error('[DEVTOOLS] Failed to install React DevTools:', err); + // Don't throw - allow app to continue even if extension installation fails + } + } + // ==================== download handle ==================== session.defaultSession.on('will-download', (event, item, webContents) => { item.once('done', (event, state) => { @@ -1622,7 +1812,10 @@ app.whenReady().then(async () => { try { // Check if file exists - const fileExists = await fsp.access(filePath).then(() => true).catch(() => false); + const fileExists = await fsp + .access(filePath) + .then(() => true) + .catch(() => false); if (!fileExists) { log.error(`[PROTOCOL] File not found: ${filePath}`); return new Response('File Not Found', { status: 404 }); @@ -1682,7 +1875,9 @@ app.whenReady().then(async () => { const mainSession = session.fromPartition('persist:main_window'); mainSession.protocol.handle('localfile', protocolHandler); - log.info('[PROTOCOL] Registered localfile protocol on both default and main_window sessions'); + log.info( + '[PROTOCOL] Registered localfile protocol on both default and main_window sessions' + ); // ==================== initialize app ==================== initializeApp(); @@ -1693,18 +1888,18 @@ app.whenReady().then(async () => { // ==================== window close event ==================== app.on('window-all-closed', () => { log.info('window-all-closed'); - + // Clean up WebView manager if (webViewManager) { webViewManager.destroy(); webViewManager = null; } - + // Reset window state win = null; isWindowReady = false; protocolUrlQueue = []; - + if (process.platform !== 'darwin') { app.quit(); } @@ -1771,4 +1966,3 @@ app.on('before-quit', async (event) => { app.exit(0); } }); - diff --git a/server/.env.example b/server/.env.example index d58015f48..ff40468c0 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,8 +1,8 @@ debug=false url_prefix=/api secret_key=postgres -database_url=postgresql://postgres:postgres@localhost:5432/postgres -# Chat Share Secret Key +database_url=postgresql://postgres:123456@localhost:5432/postgres +# Chat Share Secret Key CHAT_SHARE_SECRET_KEY=put-your-secret-key-here CHAT_SHARE_SALT=put-your-encode-salt-here diff --git a/server/README_CN.md b/server/README_CN.md index 7bd08f671..d2feb387e 100644 --- a/server/README_CN.md +++ b/server/README_CN.md @@ -21,10 +21,15 @@ --- ### 快速开始(Docker 推荐) -前置要求:已安装 Docker Desktop。 + +#### 前置要求 +- **Docker Desktop**:已安装并运行 +- **Python**:3.10.*(推荐使用 3.10.15) +- **Node.js**:>=18.0.0 <23.0.0 + +#### 启动步骤 1) 启动服务 -- ```bash cd server # 复制 .env.example 为 .env(或者按照.env.example的格式创建.env) @@ -83,8 +88,11 @@ docker logs -f eigent_postgres | cat # 1) 停止容器中的 API 服务,仅保留数据库 docker stop eigent_api -# 2) 本地启动(需提供数据库连接串) +# 2) 初始化数据库(首次或数据库结构变更时) cd server + uv run alembic upgrade head + +# 3) 本地启动(需提供数据库连接串) # 方式 A:在当前 shell 导出环境变量 export database_url=postgresql://postgres:123456@localhost:5432/eigent uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0 @@ -107,4 +115,4 @@ uv run pybabel init -i messages.pot -d lang -l zh_CN uv run pybabel compile -d lang -l zh_CN ``` -如需完全离线环境,请仅使用本地模型与本地 MCP 服务器,并避免配置任何外部 Provider 与远程 MCP 地址。 \ No newline at end of file +如需完全离线环境,请仅使用本地模型与本地 MCP 服务器,并避免配置任何外部 Provider 与远程 MCP 地址。 diff --git a/server/README_EN.md b/server/README_EN.md index 5c772ef38..712acda3c 100644 --- a/server/README_EN.md +++ b/server/README_EN.md @@ -21,7 +21,13 @@ Note: All the above data is stored in the local PostgreSQL volume in Docker (see --- ### Quick Start (Docker) -Prerequisite: Docker Desktop installed. + +#### Prerequisites +- **Docker Desktop**: Installed and running +- **Python**: 3.10.* (3.10.15 recommended) +- **Node.js**: >=18.0.0 <23.0.0 + +#### Setup Steps 1) Start services ```bash @@ -81,8 +87,11 @@ You can run the API locally with hot-reload while keeping the database in Docker # Stop API in container, keep DB docker stop eigent_api -# Run locally (provide DB connection string) +# Initialize database (first-time or when DB schema changes) cd server +uv run alembic upgrade head + +# Run locally (provide DB connection string) export database_url=postgresql://postgres:123456@localhost:5432/eigent uv run uvicorn main:api --reload --port 3001 --host 0.0.0.0 ``` diff --git a/server/app/controller/user/login_controller.py b/server/app/controller/user/login_controller.py index b0d943eb3..61c9b429b 100644 --- a/server/app/controller/user/login_controller.py +++ b/server/app/controller/user/login_controller.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Form from fastapi_babel import _ from sqlmodel import Session from app.component import code @@ -7,7 +7,13 @@ from app.component.database import session from app.component.encrypt import password_verify from app.component.stack_auth import StackAuth from app.exception.exception import UserException -from app.model.user.user import LoginByPasswordIn, LoginResponse, Status, User, RegisterIn +from app.model.user.user import ( + LoginByPasswordIn, + LoginResponse, + Status, + User, + RegisterIn, +) from app.component.environment import env from utils import traceroot_wrapper as traceroot @@ -19,25 +25,62 @@ router = APIRouter(tags=["Login/Registration"]) @router.post("/login", name="login by email or password") @traceroot.trace() -async def by_password(data: LoginByPasswordIn, session: Session = Depends(session)) -> LoginResponse: +async def by_password( + data: LoginByPasswordIn, session: Session = Depends(session) +) -> LoginResponse: """ User login with email and password """ email = data.email user = User.by(User.email == email, s=session).one_or_none() - + if not user: logger.warning("Login failed: user not found", extra={"email": email}) raise UserException(code.password, _("Account or password error")) - + if not password_verify(data.password, user.password): - logger.warning("Login failed: invalid password", extra={"user_id": user.id, "email": email}) + logger.warning( + "Login failed: invalid password", extra={"user_id": user.id, "email": email} + ) raise UserException(code.password, _("Account or password error")) - + logger.info("User login successful", extra={"user_id": user.id, "email": email}) return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) +@router.post("/dev_login", name="OAuth2 password flow login (for Swagger UI)") +@traceroot.trace() +async def dev_login( + username: str = Form(...), # OAuth2 uses 'username' but we accept email + password: str = Form(...), + session: Session = Depends(session), +) -> dict: + """ + OAuth2 password flow compatible login endpoint for Swagger UI. + This endpoint accepts form data (username/password) and returns an access token. + """ + # Use username as email (OAuth2 standard uses 'username' field) + email = username + user = User.by(User.email == email, s=session).one_or_none() + + if not user: + logger.warning("OAuth2 login failed: user not found", extra={"email": email}) + raise HTTPException(status_code=401, detail="Incorrect username or password") + + if not password_verify(password, user.password): + logger.warning( + "OAuth2 login failed: invalid password", + extra={"user_id": user.id, "email": email}, + ) + raise HTTPException(status_code=401, detail="Incorrect username or password") + + token = Auth.create_access_token(user.id) + logger.info("OAuth2 login successful", extra={"user_id": user.id, "email": email}) + + # Return OAuth2 compatible response + return {"access_token": token, "token_type": "bearer"} + + @router.post("/login-by_stack", name="login by stack") @traceroot.trace() async def by_stack_auth( @@ -50,16 +93,21 @@ async def by_stack_auth( stack_id = await StackAuth.user_id(token) info = await StackAuth.user_info(token) except Exception as e: - logger.error("Stack auth failed", extra={"type": type, "error": str(e)}, exc_info=True) + logger.error( + "Stack auth failed", extra={"type": type, "error": str(e)}, exc_info=True + ) raise HTTPException(500, detail=_("Authentication failed")) - + user = User.by(User.stack_id == stack_id, s=session).one_or_none() if not user: if type != "signup": - logger.warning("Stack auth signup blocked: user not found", extra={"stack_id": stack_id, "type": type}) + logger.warning( + "Stack auth signup blocked: user not found", + extra={"stack_id": stack_id, "type": type}, + ) raise UserException(code.error, _("User not found")) - + with session as s: try: user = User( @@ -72,18 +120,37 @@ async def by_stack_auth( s.add(user) s.commit() s.refresh(user) - logger.info("New user registered via stack", extra={"user_id": user.id, "email": user.email, "stack_id": stack_id}) - return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) + logger.info( + "New user registered via stack", + extra={ + "user_id": user.id, + "email": user.email, + "stack_id": stack_id, + }, + ) + return LoginResponse( + token=Auth.create_access_token(user.id), email=user.email + ) except Exception as e: s.rollback() - logger.error("Stack auth registration failed", extra={"stack_id": stack_id, "error": str(e)}, exc_info=True) + logger.error( + "Stack auth registration failed", + extra={"stack_id": stack_id, "error": str(e)}, + exc_info=True, + ) raise UserException(code.error, _("Failed to register")) else: if user.status == Status.Block: - logger.warning("Blocked user login attempt", extra={"user_id": user.id, "stack_id": stack_id}) + logger.warning( + "Blocked user login attempt", + extra={"user_id": user.id, "stack_id": stack_id}, + ) raise UserException(code.error, _("Your account has been blocked.")) - - logger.info("User login via stack successful", extra={"user_id": user.id, "email": user.email, "stack_id": stack_id}) + + logger.info( + "User login via stack successful", + extra={"user_id": user.id, "email": user.email, "stack_id": stack_id}, + ) return LoginResponse(token=Auth.create_access_token(user.id), email=user.email) @@ -91,9 +158,11 @@ async def by_stack_auth( @traceroot.trace() async def register(data: RegisterIn, session: Session = Depends(session)): email = data.email - + if User.by(User.email == email, s=session).one_or_none(): - logger.warning("Registration failed: email already exists", extra={"email": email}) + logger.warning( + "Registration failed: email already exists", extra={"email": email} + ) raise UserException(code.error, _("Email already registered")) with session as s: @@ -105,10 +174,17 @@ async def register(data: RegisterIn, session: Session = Depends(session)): s.add(user) s.commit() s.refresh(user) - logger.info("User registered successfully", extra={"user_id": user.id, "email": email}) + logger.info( + "User registered successfully", + extra={"user_id": user.id, "email": email}, + ) except Exception as e: s.rollback() - logger.error("User registration failed", extra={"email": email, "error": str(e)}, exc_info=True) + logger.error( + "User registration failed", + extra={"email": email, "error": str(e)}, + exc_info=True, + ) raise UserException(code.error, _("Failed to register")) - - return {"status": "success"} \ No newline at end of file + + return {"status": "success"} diff --git a/server/docker-compose.dev.yml b/server/docker-compose.dev.yml new file mode 100644 index 000000000..19fd97753 --- /dev/null +++ b/server/docker-compose.dev.yml @@ -0,0 +1,30 @@ +services: + # PostgreSQL Database Only + postgres: + image: postgres:15 + container_name: eigent_postgres + restart: unless-stopped + environment: + POSTGRES_DB: eigent + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 123456 + POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - eigent_network + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres -d eigent" ] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local + +networks: + eigent_network: + driver: bridge diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index ca8a09a2c..fd6bcbf22 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,432 +1,462 @@ -import { useAuthStore } from "@/store/authStore"; -import { useNavigate, useLocation } from "react-router-dom"; -import { useCallback, useEffect, useState, useRef } from "react"; -import { useStackApp } from "@stackframe/react"; -import loginGif from "@/assets/login.gif"; -import { Button } from "@/components/ui/button"; +import { useAuthStore } from '@/store/authStore'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { useCallback, useEffect, useState, useRef } from 'react'; +import { useStackApp } from '@stackframe/react'; +import loginGif from '@/assets/login.gif'; +import { Button } from '@/components/ui/button'; -import { Input } from "@/components/ui/input"; +import { Input } from '@/components/ui/input'; -import github2 from "@/assets/github2.svg"; -import google from "@/assets/google.svg"; -import eye from "@/assets/eye.svg"; -import eyeOff from "@/assets/eye-off.svg"; -import { proxyFetchPost } from "@/api/http"; -import { hasStackKeys } from "@/lib"; -import { useTranslation } from "react-i18next"; -import WindowControls from "@/components/WindowControls"; +import github2 from '@/assets/github2.svg'; +import google from '@/assets/google.svg'; +import eye from '@/assets/eye.svg'; +import eyeOff from '@/assets/eye-off.svg'; +import { proxyFetchPost } from '@/api/http'; +import { hasStackKeys } from '@/lib'; +import { useTranslation } from 'react-i18next'; +import WindowControls from '@/components/WindowControls'; const HAS_STACK_KEYS = hasStackKeys(); let lock = false; export default function Login() { - const app = HAS_STACK_KEYS ? useStackApp() : null; - const { setAuth, setModelType, setLocalProxyValue } = useAuthStore(); - const navigate = useNavigate(); - const location = useLocation(); - const [hidePassword, setHidePassword] = useState(true); - const { t } = useTranslation(); - const [formData, setFormData] = useState({ - email: "", - password: "", - }); - const [errors, setErrors] = useState({ - email: "", - password: "", - }); - const [isLoading, setIsLoading] = useState(false); - const [generalError, setGeneralError] = useState(""); - const titlebarRef = useRef(null); - const [platform, setPlatform] = useState(""); + const app = HAS_STACK_KEYS ? useStackApp() : null; + const { setAuth, setModelType, setLocalProxyValue } = useAuthStore(); + const navigate = useNavigate(); + const location = useLocation(); + const [hidePassword, setHidePassword] = useState(true); + const { t } = useTranslation(); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [errors, setErrors] = useState({ + email: '', + password: '', + }); + const [isLoading, setIsLoading] = useState(false); + const [generalError, setGeneralError] = useState(''); + const titlebarRef = useRef(null); + const [platform, setPlatform] = useState(''); - const validateEmail = (email: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - }; + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; - const validateForm = () => { - const newErrors = { - email: "", - password: "", - }; + const validateForm = () => { + const newErrors = { + email: '', + password: '', + }; - if (!formData.email) { - newErrors.email = t("layout.please-enter-email-address"); - } else if (!validateEmail(formData.email)) { - newErrors.email = t("layout.please-enter-a-valid-email-address"); - } + if (!formData.email) { + newErrors.email = t('layout.please-enter-email-address'); + } else if (!validateEmail(formData.email)) { + newErrors.email = t('layout.please-enter-a-valid-email-address'); + } - if (!formData.password) { - newErrors.password = t("layout.please-enter-password"); - } else if (formData.password.length < 6) { - newErrors.password = t("layout.password-must-be-at-least-8-characters"); - } + if (!formData.password) { + newErrors.password = t('layout.please-enter-password'); + } else if (formData.password.length < 6) { + newErrors.password = t('layout.password-must-be-at-least-8-characters'); + } - setErrors(newErrors); - return !newErrors.email && !newErrors.password; - }; + setErrors(newErrors); + return !newErrors.email && !newErrors.password; + }; - const getLoginErrorMessage = (data: any) => { - if (!data || typeof data !== "object" || typeof data.code !== "number") { - return ""; - } + const getLoginErrorMessage = (data: any) => { + if (!data || typeof data !== 'object' || typeof data.code !== 'number') { + return ''; + } - if (data.code === 0) { - return ""; - } + if (data.code === 0) { + return ''; + } - if (data.code === 10) { - return data.text || t("layout.login-failed-please-check-your-email-and-password"); - } + if (data.code === 10) { + return ( + data.text || + t('layout.login-failed-please-check-your-email-and-password') + ); + } - if (data.code === 1 && Array.isArray(data.error) && data.error.length > 0) { - const firstError = data.error[0]; - if (typeof firstError === "string") { - return firstError; - } - if (typeof firstError?.msg === "string") { - return firstError.msg; - } - if (typeof firstError?.message === "string") { - return firstError.message; - } - } + if (data.code === 1 && Array.isArray(data.error) && data.error.length > 0) { + const firstError = data.error[0]; + if (typeof firstError === 'string') { + return firstError; + } + if (typeof firstError?.msg === 'string') { + return firstError.msg; + } + if (typeof firstError?.message === 'string') { + return firstError.message; + } + } - return data.text || t("layout.login-failed-please-try-again"); - }; + return data.text || t('layout.login-failed-please-try-again'); + }; - const handleInputChange = (field: string, value: string) => { - setFormData((prev) => ({ - ...prev, - [field]: value, - })); + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ + ...prev, + [field]: value, + })); - if (errors[field as keyof typeof errors]) { - setErrors((prev) => ({ - ...prev, - [field]: "", - })); - } + if (errors[field as keyof typeof errors]) { + setErrors((prev) => ({ + ...prev, + [field]: '', + })); + } - if (generalError) { - setGeneralError(""); - } - }; + if (generalError) { + setGeneralError(''); + } + }; - // - const handleLogin = async () => { - if (!validateForm()) { - return; - } + // + const handleLogin = async () => { + if (!validateForm()) { + return; + } - setGeneralError(""); - setIsLoading(true); - try { - const data = await proxyFetchPost("/api/login", { - email: formData.email, - password: formData.password, - }); + setGeneralError(''); + setIsLoading(true); + try { + const data = await proxyFetchPost('/api/login', { + email: formData.email, + password: formData.password, + }); - const errorMessage = getLoginErrorMessage(data); - if (errorMessage) { - setGeneralError(errorMessage); - return; - } + const errorMessage = getLoginErrorMessage(data); + if (errorMessage) { + setGeneralError(errorMessage); + return; + } - setAuth({ email: formData.email, ...data }); - setModelType('cloud'); - // Record VITE_USE_LOCAL_PROXY value at login - const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null; - setLocalProxyValue(localProxyValue); - navigate("/"); - } catch (error: any) { - console.error("Login failed:", error); - setGeneralError(t("layout.login-failed-please-check-your-email-and-password")); - } finally { - setIsLoading(false); - } - }; + setAuth({ email: formData.email, ...data }); + setModelType('cloud'); + // Record VITE_USE_LOCAL_PROXY value at login + const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null; + setLocalProxyValue(localProxyValue); + navigate('/'); + } catch (error: any) { + console.error('Login failed:', error); + setGeneralError( + t('layout.login-failed-please-check-your-email-and-password') + ); + } finally { + setIsLoading(false); + } + }; - const handleLoginByStack = async (token: string) => { - try { - const data = await proxyFetchPost("/api/login-by_stack?token=" + token, { - token: token, - }); + const handleLoginByStack = async (token: string) => { + try { + const data = await proxyFetchPost('/api/login-by_stack?token=' + token, { + token: token, + }); - const errorMessage = getLoginErrorMessage(data); - if (errorMessage) { - setGeneralError(errorMessage); - return; - } - console.log("data", data); - setModelType('cloud'); - setAuth({ email: formData.email, ...data }); - // Record VITE_USE_LOCAL_PROXY value at login - const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null; - setLocalProxyValue(localProxyValue); - navigate("/"); - } catch (error: any) { - console.error("Login failed:", error); - setGeneralError(t("layout.login-failed-please-check-your-email-and-password")); - } finally { - setIsLoading(false); - } - }; + const errorMessage = getLoginErrorMessage(data); + if (errorMessage) { + setGeneralError(errorMessage); + return; + } + console.log('data', data); + setModelType('cloud'); + setAuth({ email: formData.email, ...data }); + // Record VITE_USE_LOCAL_PROXY value at login + const localProxyValue = import.meta.env.VITE_USE_LOCAL_PROXY || null; + setLocalProxyValue(localProxyValue); + navigate('/'); + } catch (error: any) { + console.error('Login failed:', error); + setGeneralError( + t('layout.login-failed-please-check-your-email-and-password') + ); + } finally { + setIsLoading(false); + } + }; - const handleReloadBtn = async (type: string) => { - console.log("handleReloadBtn1", type); - const cookies = document.cookie.split("; "); - cookies.forEach((cookie) => { - const [name] = cookie.split("="); - if (name.startsWith("stack-oauth-outer-")) { - document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; - } - }); - console.log("handleReloadBtn2", type); - await app.signInWithOAuth(type); - }; + const handleReloadBtn = async (type: string) => { + console.log('handleReloadBtn1', type); + const cookies = document.cookie.split('; '); + cookies.forEach((cookie) => { + const [name] = cookie.split('='); + if (name.startsWith('stack-oauth-outer-')) { + document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`; + } + }); + console.log('handleReloadBtn2', type); + await app.signInWithOAuth(type); + }; - const handleGetToken = async (code: string) => { - const code_verifier = localStorage.getItem("stack-oauth-outer-"); - const formData = new URLSearchParams(); - console.log( - "import.meta.env.PROD", - import.meta.env.PROD - ? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback` - : `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback` - ); - formData.append( - "redirect_uri", - import.meta.env.PROD - ? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback` - : `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback` - ); - formData.append("code_verifier", code_verifier || ""); - formData.append("code", code); - formData.append("grant_type", "authorization_code"); - formData.append("client_id", "aa49cdd0-318e-46bd-a540-0f1e5f2b391f"); - formData.append( - "client_secret", - "pck_t13egrd9ve57tz52kfcd2s4h1zwya5502z43kr5xv5cx8" - ); + const handleGetToken = async (code: string) => { + const code_verifier = localStorage.getItem('stack-oauth-outer-'); + const formData = new URLSearchParams(); + console.log( + 'import.meta.env.PROD', + import.meta.env.PROD + ? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback` + : `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback` + ); + formData.append( + 'redirect_uri', + import.meta.env.PROD + ? `${import.meta.env.VITE_BASE_URL}/api/redirect/callback` + : `${import.meta.env.VITE_PROXY_URL}/api/redirect/callback` + ); + formData.append('code_verifier', code_verifier || ''); + formData.append('code', code); + formData.append('grant_type', 'authorization_code'); + formData.append('client_id', 'aa49cdd0-318e-46bd-a540-0f1e5f2b391f'); + formData.append( + 'client_secret', + 'pck_t13egrd9ve57tz52kfcd2s4h1zwya5502z43kr5xv5cx8' + ); - try { - const res = await fetch( - "https://api.stack-auth.com/api/v1/auth/oauth/token", - { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", - }, - body: formData, - } - ); - const data = await res.json(); // parse response data - return data.access_token; - } catch (error) { - console.error(error); - setIsLoading(false); - } - }; + try { + const res = await fetch( + 'https://api.stack-auth.com/api/v1/auth/oauth/token', + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + body: formData, + } + ); + const data = await res.json(); // parse response data + return data.access_token; + } catch (error) { + console.error(error); + setIsLoading(false); + } + }; - const handleAuthCode = useCallback( - async (event: any, code: string) => { - if (lock || location.pathname !== "/login") return; + const handleAuthCode = useCallback( + async (event: any, code: string) => { + if (lock || location.pathname !== '/login') return; - lock = true; - setIsLoading(true); - let accessToken = await handleGetToken(code); - handleLoginByStack(accessToken); - setTimeout(() => { - lock = false; - }, 1500); - }, - [location.pathname] - ); + lock = true; + setIsLoading(true); + let accessToken = await handleGetToken(code); + handleLoginByStack(accessToken); + setTimeout(() => { + lock = false; + }, 1500); + }, + [location.pathname] + ); - useEffect(() => { - window.ipcRenderer?.on("auth-code-received", handleAuthCode); + useEffect(() => { + window.ipcRenderer?.on('auth-code-received', handleAuthCode); - return () => { - window.ipcRenderer?.off("auth-code-received", handleAuthCode); - }; - }, []); + return () => { + window.ipcRenderer?.off('auth-code-received', handleAuthCode); + }; + }, []); - useEffect(() => { - const p = window.electronAPI.getPlatform(); - setPlatform(p); + useEffect(() => { + const p = window.electronAPI.getPlatform(); + setPlatform(p); - if (platform === "darwin") { - titlebarRef.current?.classList.add("mac"); - } - }, [platform]); + if (platform === 'darwin') { + titlebarRef.current?.classList.add('mac'); + } + }, [platform]); - // Handle before-close event for login page - useEffect(() => { - const handleBeforeClose = () => { - // On login page, always close directly without confirmation - window.electronAPI.closeWindow(true); - }; + // Handle before-close event for login page + useEffect(() => { + const handleBeforeClose = () => { + // On login page, always close directly without confirmation + window.electronAPI.closeWindow(true); + }; - window.ipcRenderer?.on("before-close", handleBeforeClose); + window.ipcRenderer?.on('before-close', handleBeforeClose); - return () => { - window.ipcRenderer?.off("before-close", handleBeforeClose); - }; - }, []); + return () => { + window.ipcRenderer?.off('before-close', handleBeforeClose); + }; + }, []); - return ( -
- {/* Titlebar with drag region and window controls */} -
- {/* Left spacer for macOS */} -
- {platform === "darwin" && Eigent} -
+ return ( +
+ {/* Titlebar with drag region and window controls */} +
+ {/* Left spacer for macOS */} +
+ {platform === 'darwin' && ( + + Eigent + + )} +
- {/* Center drag region */} -
-
-
+ {/* Center drag region */} +
+
+
- {/* Right window controls */} -
e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - > - -
-
+ {/* Right window controls */} +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + +
+
- {/* Main content - image extends to top, form has padding */} -
-
- -
-
-
-
-
- {t("layout.login")} -
- -
- {HAS_STACK_KEYS && ( -
- - -
- )} - {HAS_STACK_KEYS && ( -
- {t("layout.or")} -
- )} -
- {generalError && ( -

- {generalError} -

- )} -
- handleInputChange("email", e.target.value)} - state={errors.email ? "error" : undefined} - note={errors.email} - onEnter={handleLogin} - /> + {/* Main content - image extends to top, form has padding */} +
+
+ +
+
+
+
+
+ {t('layout.login')} +
+ +
+ {HAS_STACK_KEYS && ( +
+ + +
+ )} + {HAS_STACK_KEYS && ( +
+ {t('layout.or')} +
+ )} +
+ {generalError && ( +

+ {generalError} +

+ )} +
+ handleInputChange('email', e.target.value)} + state={errors.email ? 'error' : undefined} + note={errors.email} + onEnter={handleLogin} + /> - - handleInputChange("password", e.target.value) - } - state={errors.password ? "error" : undefined} - note={errors.password} - backIcon={} - onBackIconClick={() => setHidePassword(!hidePassword)} - onEnter={handleLogin} - /> -
-
- -
- -
-
-
- ); + + handleInputChange('password', e.target.value) + } + state={errors.password ? 'error' : undefined} + note={errors.password} + backIcon={} + onBackIconClick={() => setHidePassword(!hidePassword)} + onEnter={handleLogin} + /> +
+
+ +
+ +
+
+
+ ); } From bf0d9c536936ca8dc16ec505f74e64cfa9b685d9 Mon Sep 17 00:00:00 2001 From: 4pmtong Date: Tue, 13 Jan 2026 23:30:38 +0800 Subject: [PATCH 2/6] :memo: dev guideline --- .npmrc | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.npmrc b/.npmrc index 45c8cd74a..c88c011b2 100644 --- a/.npmrc +++ b/.npmrc @@ -3,4 +3,5 @@ shamefully-hoist=true # For China 🇨🇳 developers +# registry=https://registry.npmmirror.com # electron_mirror=https://npmmirror.com/mirrors/electron/ diff --git a/package.json b/package.json index 956dee537..5c8d04109 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "autoprefixer": "^10.4.20", "electron": "^33.2.0", "electron-builder": "^24.13.3", + "electron-devtools-installer": "^4.0.0", "i18next": "^25.4.2", "jsdom": "^26.1.0", "postcss": "^8.4.49", @@ -133,4 +134,4 @@ "engines": { "node": ">=18.0.0 <23.0.0" } -} \ No newline at end of file +} From 07c2f8bdb5448d5d828e8657b36a95d54d4f8954 Mon Sep 17 00:00:00 2001 From: 4pmtong Date: Tue, 13 Jan 2026 23:57:18 +0800 Subject: [PATCH 3/6] :bug: fix middleware for auth --- server/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/main.py b/server/main.py index 3ea80ecd3..105b1c2da 100644 --- a/server/main.py +++ b/server/main.py @@ -12,22 +12,33 @@ from app import api from app.component.environment import auto_include_routers, env from fastapi.staticfiles import StaticFiles +# Import middleware to register BabelMiddleware +import app.middleware # noqa: F401 + +# Import exception handlers to register them +import app.exception.handler # noqa: F401 + # Only initialize traceroot if enabled if traceroot.is_enabled(): from traceroot.integrations.fastapi import connect_fastapi + connect_fastapi(api) logger = traceroot.get_logger("server_main") prefix = env("url_prefix", "") auto_include_routers(api, prefix, "app/controller") -public_dir = os.environ.get("PUBLIC_DIR") or os.path.join(os.path.dirname(__file__), "app", "public") +public_dir = os.environ.get("PUBLIC_DIR") or os.path.join( + os.path.dirname(__file__), "app", "public" +) if not os.path.isdir(public_dir): try: os.makedirs(public_dir, exist_ok=True) logger.warning(f"Public directory did not exist. Created: {public_dir}") except Exception as e: - logger.error(f"Public directory missing and could not be created: {public_dir}. Error: {e}") + logger.error( + f"Public directory missing and could not be created: {public_dir}. Error: {e}" + ) public_dir = None if public_dir and os.path.isdir(public_dir): From 382db262a16fa7fe232aa00870c9f537558cb0f8 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Wed, 14 Jan 2026 00:33:01 +0800 Subject: [PATCH 4/6] minor update --- electron/main/index.ts | 7 ++++--- src/pages/Login.tsx | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/electron/main/index.ts b/electron/main/index.ts index d6a6b5eae..7e84d4ab5 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -13,9 +13,6 @@ import { fileURLToPath } from 'node:url'; import path from 'node:path'; import os, { homedir } from 'node:os'; import log from 'electron-log'; -import installExtension, { - REACT_DEVELOPER_TOOLS, -} from 'electron-devtools-installer'; import { update, registerUpdateIpcHandlers } from './update'; import { checkToolInstalled, killProcessOnPort, startBackend } from './init'; import { WebViewManager } from './webview'; @@ -1784,6 +1781,10 @@ app.whenReady().then(async () => { if (VITE_DEV_SERVER_URL) { try { log.info('[DEVTOOLS] Installing React DevTools extension...'); + // Dynamic import to avoid bundling in production + const { default: installExtension, REACT_DEVELOPER_TOOLS } = await import( + 'electron-devtools-installer' + ); const name = await installExtension(REACT_DEVELOPER_TOOLS, { loadExtensionOptions: { allowFileAccess: true }, }); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fd6bcbf22..a6bfb57c0 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -57,7 +57,7 @@ export default function Login() { if (!formData.password) { newErrors.password = t('layout.please-enter-password'); - } else if (formData.password.length < 6) { + } else if (formData.password.length < 8) { newErrors.password = t('layout.password-must-be-at-least-8-characters'); } @@ -180,6 +180,10 @@ export default function Login() { }; const handleReloadBtn = async (type: string) => { + if (!app) { + console.error('Stack app not initialized'); + return; + } console.log('handleReloadBtn1', type); const cookies = document.cookie.split('; '); cookies.forEach((cookie) => { From 2b779e5459bb8b85a71d649d2d5adb6aa761f6ae Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Wed, 14 Jan 2026 00:42:58 +0800 Subject: [PATCH 5/6] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8297d605c..58c5023a7 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Built on [CAMEL-AI][camel-site]'s acclaimed open-source project, our system intr
+[![][image-join-us]][join-us] +
Table of contents From b9d24686fc422d200efb2799b9b084091e0bcad2 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Wed, 14 Jan 2026 01:34:28 +0800 Subject: [PATCH 6/6] fix: model platform naming --- src/lib/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/llm.ts b/src/lib/llm.ts index 9f6d3d0b6..0d351a128 100644 --- a/src/lib/llm.ts +++ b/src/lib/llm.ts @@ -56,7 +56,7 @@ export const INIT_PROVODERS: Provider[] = [ model_type: "" }, { - id: 'bedrock', + id: 'aws-bedrock', name: 'AWS Bedrock', apiKey: '', apiHost: '',