Przeglądaj źródła

feat: enhance Electron environment detection and improve database warnings

CaIon 2 miesięcy temu
rodzic
commit
ff77ba1157

+ 105 - 40
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);
 }
 
-function startServer() {
+// Check if a server is available with retry logic
+function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) {
   return new Promise((resolve, reject) => {
-    const binaryPath = getBinaryPath();
-    const isDev = process.env.NODE_ENV === 'development';
+    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();
+      });
 
-    console.log('Starting server from:', binaryPath);
+      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);
+        }
+      });
 
-    const env = { ...process.env, PORT: PORT.toString() };
+      req.on('timeout', () => {
+        req.destroy();
+        if (currentAttempt >= maxRetries) {
+          reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`));
+        } else {
+          setTimeout(tryConnect, retryDelay);
+        }
+      });
+    };
+    
+    tryConnect();
+  });
+}
 
-    let dataDir;
+function startServer() {
+  return new Promise((resolve, reject) => {
+    const isDev = process.env.NODE_ENV === 'development';
+    
     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);
-  });
-}
-
-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);
+    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);
+      });
   });
-
-  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();
   }
 

+ 2 - 2
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",

+ 21 - 1
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()
 });

+ 38 - 13
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' && (
         <Banner
-          type='warning'
+          type={isElectron ? 'info' : 'warning'}
           closeIcon={null}
-          title={t('数据库警告')}
+          title={isElectron ? t('本地数据存储') : t('数据库警告')}
           description={
-            <div>
-              <p>
-                {t(
-                  '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
+            isElectron ? (
+              <div>
+                <p>
+                  {t(
+                    '您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。',
+                  )}
+                </p>
+                {window.electron?.dataDir && (
+                  <p className='mt-2 text-sm opacity-80'>
+                    <strong>{t('数据存储位置:')}</strong>
+                    <br />
+                    <code className='bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded'>
+                      {window.electron.dataDir}
+                    </code>
+                  </p>
                 )}
-              </p>
-              <p className='mt-1'>
-                <strong>
+                <p className='mt-2 text-sm opacity-70'>
+                  💡 {t('提示:如需备份数据,只需复制上述目录即可')}
+                </p>
+              </div>
+            ) : (
+              <div>
+                <p>
                   {t(
-                    '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
+                    '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
                   )}
-                </strong>
-              </p>
-            </div>
+                </p>
+                <p className='mt-1'>
+                  <strong>
+                    {t(
+                      '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
+                    )}
+                  </strong>
+                </p>
+              </div>
+            )
           }
           className='!rounded-lg'
           fullMode={false}