diff --git a/electron/main.js b/electron/main.js index 82f954f00..3cebd34f8 100644 --- a/electron/main.js +++ b/electron/main.js @@ -9,6 +9,7 @@ let serverProcess; let tray = null; let serverErrorLogs = []; const PORT = 3000; +const DEV_FRONTEND_PORT = 5173; // Vite dev server port // 保存日志到文件并打开 function saveAndOpenErrorLog() { @@ -79,10 +80,10 @@ function analyzeError(errorLogs) { if (allLogs.includes('database is locked') || allLogs.includes('unable to open database')) { return { - type: '数据库错误', - title: '数据库访问失败', - message: '无法访问或锁定数据库文件', - solution: '可能的解决方案:\n\n1. 确保没有其他 New API 实例正在运行\n2. 检查数据库文件权限\n3. 尝试删除数据库锁文件(.db-shm 和 .db-wal)\n4. 重启应用程序' + type: '数据文件被占用', + title: '无法访问数据文件', + message: '应用的数据文件正被其他程序占用', + solution: '可能的解决方案:\n\n1. 检查是否已经打开了另一个 New API 窗口\n - 查看任务栏/Dock 中是否有其他 New API 图标\n - 查看系统托盘(Windows)或菜单栏(Mac)中是否有 New API 图标\n\n2. 如果刚刚关闭过应用,请等待 10 秒后再试\n\n3. 重启电脑以释放被占用的文件\n\n4. 如果问题持续,可以尝试:\n - 退出所有 New API 实例\n - 删除数据目录中的临时文件(.db-shm 和 .db-wal)\n - 重新启动应用' }; } @@ -173,32 +174,101 @@ function getBinaryPath() { return path.join(process.resourcesPath, 'bin', binaryName); } +// Check if a server is available with retry logic +function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) { + return new Promise((resolve, reject) => { + let currentAttempt = 0; + + const tryConnect = () => { + currentAttempt++; + + if (currentAttempt % 5 === 1 && currentAttempt > 1) { + console.log(`Attempting to connect to port ${port}... (attempt ${currentAttempt}/${maxRetries})`); + } + + const req = http.get({ + hostname: '127.0.0.1', // Use IPv4 explicitly instead of 'localhost' to avoid IPv6 issues + port: port, + timeout: 10000 + }, (res) => { + // Server responded, connection successful + req.destroy(); + console.log(`✓ Successfully connected to port ${port} (status: ${res.statusCode})`); + resolve(); + }); + + req.on('error', (err) => { + if (currentAttempt >= maxRetries) { + reject(new Error(`Failed to connect to port ${port} after ${maxRetries} attempts: ${err.message}`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); + + req.on('timeout', () => { + req.destroy(); + if (currentAttempt >= maxRetries) { + reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); + }; + + tryConnect(); + }); +} + function startServer() { return new Promise((resolve, reject) => { - const binaryPath = getBinaryPath(); const isDev = process.env.NODE_ENV === 'development'; - - console.log('Starting server from:', binaryPath); - - const env = { ...process.env, PORT: PORT.toString() }; - - let dataDir; + if (isDev) { - dataDir = path.join(__dirname, '..', 'data'); - } else { - const userDataPath = app.getPath('userData'); - dataDir = path.join(userDataPath, 'data'); + // 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器 + // 只需要等待前端开发服务器就绪 + console.log('Development mode: skipping server startup'); + console.log('Please make sure you have started:'); + console.log(' 1. Go backend: go run main.go (port 3000)'); + console.log(' 2. Frontend dev server: cd web && bun dev (port 5173)'); + console.log(''); + console.log('Checking if servers are running...'); + + // First check if both servers are accessible + checkServerAvailability(DEV_FRONTEND_PORT) + .then(() => { + console.log('✓ Frontend dev server is accessible on port 5173'); + resolve(); + }) + .catch((err) => { + console.error(`✗ Cannot connect to frontend dev server on port ${DEV_FRONTEND_PORT}`); + console.error('Please make sure the frontend dev server is running:'); + console.error(' cd web && bun dev'); + reject(err); + }); + return; } + // 生产模式:启动二进制服务器 + const env = { ...process.env, PORT: PORT.toString() }; + const userDataPath = app.getPath('userData'); + const dataDir = path.join(userDataPath, 'data'); + if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } env.SQLITE_PATH = path.join(dataDir, 'new-api.db'); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📁 您的数据存储位置:'); + console.log(' ' + dataDir); + console.log(' 💡 备份提示:复制此目录即可备份所有数据'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - const workingDir = isDev - ? path.join(__dirname, '..') - : process.resourcesPath; + const binaryPath = getBinaryPath(); + const workingDir = process.resourcesPath; + + console.log('Starting server from:', binaryPath); serverProcess = spawn(binaryPath, [], { env, @@ -299,32 +369,25 @@ function startServer() { } }); - waitForServer(resolve, reject); + checkServerAvailability(PORT) + .then(() => { + console.log('✓ Backend server is accessible on port 3000'); + resolve(); + }) + .catch((err) => { + console.error('✗ Failed to connect to backend server'); + reject(err); + }); }); } -function waitForServer(resolve, reject, retries = 30) { - if (retries === 0) { - reject(new Error('Server failed to start within timeout')); - return; - } - - const req = http.get(`http://localhost:${PORT}`, (res) => { - console.log('Server is ready'); - resolve(); - }); - - req.on('error', () => { - setTimeout(() => waitForServer(resolve, reject, retries - 1), 1000); - }); - - req.end(); -} - function createWindow() { + const isDev = process.env.NODE_ENV === 'development'; + const loadPort = isDev ? DEV_FRONTEND_PORT : PORT; + mainWindow = new BrowserWindow({ - width: 1400, - height: 900, + width: 1080, + height: 720, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, @@ -334,9 +397,11 @@ function createWindow() { icon: path.join(__dirname, 'icon.png') }); - mainWindow.loadURL(`http://localhost:${PORT}`); + mainWindow.loadURL(`http://127.0.0.1:${loadPort}`); + + console.log(`Loading from: http://127.0.0.1:${loadPort}`); - if (process.env.NODE_ENV === 'development') { + if (isDev) { mainWindow.webContents.openDevTools(); } diff --git a/electron/package.json b/electron/package.json index 21c933560..4b5880ea0 100644 --- a/electron/package.json +++ b/electron/package.json @@ -4,8 +4,8 @@ "description": "New API - AI Model Gateway Desktop Application", "main": "main.js", "scripts": { - "start": "electron .", - "dev": "cross-env NODE_ENV=development electron .", + "start-app": "electron .", + "dev-app": "cross-env NODE_ENV=development electron .", "build": "electron-builder", "build:mac": "electron-builder --mac", "build:win": "electron-builder --win", diff --git a/electron/preload.js b/electron/preload.js index 217f5a410..e089bbb5a 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,8 +1,28 @@ const { contextBridge } = require('electron'); +// 获取数据目录路径(用于显示给用户) +// 使用字符串拼接而不是 path.join 避免模块依赖问题 +function getDataDirPath() { + const platform = process.platform; + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + + switch (platform) { + case 'darwin': + return `${homeDir}/Library/Application Support/New API/data`; + case 'win32': + const appData = process.env.APPDATA || `${homeDir}\\AppData\\Roaming`; + return `${appData}\\New API\\data`; + case 'linux': + return `${homeDir}/.config/New API/data`; + default: + return `${homeDir}/.new-api/data`; + } +} + contextBridge.exposeInMainWorld('electron', { isElectron: true, version: process.versions.electron, platform: process.platform, - versions: process.versions + versions: process.versions, + dataDir: getDataDirPath() }); \ No newline at end of file diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 04dd76a3c..66923f445 100644 --- a/web/src/components/setup/components/steps/DatabaseStep.jsx +++ b/web/src/components/setup/components/steps/DatabaseStep.jsx @@ -25,29 +25,54 @@ import { Banner } from '@douyinfe/semi-ui'; * 显示当前数据库类型和相关警告信息 */ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => { + // 检测是否在 Electron 环境中运行 + const isElectron = typeof window !== 'undefined' && window.electron?.isElectron; + return ( <> {/* 数据库警告 */} {setupStatus.database_type === 'sqlite' && ( -

- {t( - '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', - )} -

-

- + isElectron ? ( +

+

{t( - '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + '您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。', )} - -

-
+

+ {window.electron?.dataDir && ( +

+ {t('数据存储位置:')} +
+ + {window.electron.dataDir} + +

+ )} +

+ 💡 {t('提示:如需备份数据,只需复制上述目录即可')} +

+ + ) : ( +
+

+ {t( + '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', + )} +

+

+ + {t( + '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + )} + +

+
+ ) } className='!rounded-lg' fullMode={false}