feat: enhance Electron environment detection and improve database warnings
This commit is contained in:
145
electron/main.js
145
electron/main.js
@@ -9,6 +9,7 @@ let serverProcess;
|
|||||||
let tray = null;
|
let tray = null;
|
||||||
let serverErrorLogs = [];
|
let serverErrorLogs = [];
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
const DEV_FRONTEND_PORT = 5173; // Vite dev server port
|
||||||
|
|
||||||
// 保存日志到文件并打开
|
// 保存日志到文件并打开
|
||||||
function saveAndOpenErrorLog() {
|
function saveAndOpenErrorLog() {
|
||||||
@@ -79,10 +80,10 @@ function analyzeError(errorLogs) {
|
|||||||
if (allLogs.includes('database is locked') ||
|
if (allLogs.includes('database is locked') ||
|
||||||
allLogs.includes('unable to open database')) {
|
allLogs.includes('unable to open database')) {
|
||||||
return {
|
return {
|
||||||
type: '数据库错误',
|
type: '数据文件被占用',
|
||||||
title: '数据库访问失败',
|
title: '无法访问数据文件',
|
||||||
message: '无法访问或锁定数据库文件',
|
message: '应用的数据文件正被其他程序占用',
|
||||||
solution: '可能的解决方案:\n\n1. 确保没有其他 New API 实例正在运行\n2. 检查数据库文件权限\n3. 尝试删除数据库锁文件(.db-shm 和 .db-wal)\n4. 重启应用程序'
|
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);
|
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() {
|
function startServer() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const binaryPath = getBinaryPath();
|
|
||||||
const isDev = process.env.NODE_ENV === 'development';
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
console.log('Starting server from:', binaryPath);
|
|
||||||
|
|
||||||
const env = { ...process.env, PORT: PORT.toString() };
|
|
||||||
|
|
||||||
let dataDir;
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
dataDir = path.join(__dirname, '..', 'data');
|
// 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器
|
||||||
} else {
|
// 只需要等待前端开发服务器就绪
|
||||||
const userDataPath = app.getPath('userData');
|
console.log('Development mode: skipping server startup');
|
||||||
dataDir = path.join(userDataPath, 'data');
|
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)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
env.SQLITE_PATH = path.join(dataDir, 'new-api.db');
|
env.SQLITE_PATH = path.join(dataDir, 'new-api.db');
|
||||||
|
|
||||||
const workingDir = isDev
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
? path.join(__dirname, '..')
|
console.log('📁 您的数据存储位置:');
|
||||||
: process.resourcesPath;
|
console.log(' ' + dataDir);
|
||||||
|
console.log(' 💡 备份提示:复制此目录即可备份所有数据');
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||||
|
|
||||||
|
const binaryPath = getBinaryPath();
|
||||||
|
const workingDir = process.resourcesPath;
|
||||||
|
|
||||||
|
console.log('Starting server from:', binaryPath);
|
||||||
|
|
||||||
serverProcess = spawn(binaryPath, [], {
|
serverProcess = spawn(binaryPath, [], {
|
||||||
env,
|
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() {
|
function createWindow() {
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const loadPort = isDev ? DEV_FRONTEND_PORT : PORT;
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1080,
|
||||||
height: 900,
|
height: 720,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
@@ -334,9 +397,11 @@ function createWindow() {
|
|||||||
icon: path.join(__dirname, 'icon.png')
|
icon: path.join(__dirname, 'icon.png')
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadURL(`http://localhost:${PORT}`);
|
mainWindow.loadURL(`http://127.0.0.1:${loadPort}`);
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
console.log(`Loading from: http://127.0.0.1:${loadPort}`);
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"description": "New API - AI Model Gateway Desktop Application",
|
"description": "New API - AI Model Gateway Desktop Application",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start-app": "electron .",
|
||||||
"dev": "cross-env NODE_ENV=development electron .",
|
"dev-app": "cross-env NODE_ENV=development electron .",
|
||||||
"build": "electron-builder",
|
"build": "electron-builder",
|
||||||
"build:mac": "electron-builder --mac",
|
"build:mac": "electron-builder --mac",
|
||||||
"build:win": "electron-builder --win",
|
"build:win": "electron-builder --win",
|
||||||
|
|||||||
@@ -1,8 +1,28 @@
|
|||||||
const { contextBridge } = require('electron');
|
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', {
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
version: process.versions.electron,
|
version: process.versions.electron,
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
versions: process.versions
|
versions: process.versions,
|
||||||
|
dataDir: getDataDirPath()
|
||||||
});
|
});
|
||||||
@@ -25,29 +25,54 @@ import { Banner } from '@douyinfe/semi-ui';
|
|||||||
* 显示当前数据库类型和相关警告信息
|
* 显示当前数据库类型和相关警告信息
|
||||||
*/
|
*/
|
||||||
const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => {
|
||||||
|
// 检测是否在 Electron 环境中运行
|
||||||
|
const isElectron = typeof window !== 'undefined' && window.electron?.isElectron;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 数据库警告 */}
|
{/* 数据库警告 */}
|
||||||
{setupStatus.database_type === 'sqlite' && (
|
{setupStatus.database_type === 'sqlite' && (
|
||||||
<Banner
|
<Banner
|
||||||
type='warning'
|
type={isElectron ? 'info' : 'warning'}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
title={t('数据库警告')}
|
title={isElectron ? t('本地数据存储') : t('数据库警告')}
|
||||||
description={
|
description={
|
||||||
<div>
|
isElectron ? (
|
||||||
<p>
|
<div>
|
||||||
{t(
|
<p>
|
||||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className='mt-1'>
|
|
||||||
<strong>
|
|
||||||
{t(
|
{t(
|
||||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
'您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。',
|
||||||
)}
|
)}
|
||||||
</strong>
|
</p>
|
||||||
</p>
|
{window.electron?.dataDir && (
|
||||||
</div>
|
<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 className='mt-2 text-sm opacity-70'>
|
||||||
|
💡 {t('提示:如需备份数据,只需复制上述目录即可')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className='mt-1'>
|
||||||
|
<strong>
|
||||||
|
{t(
|
||||||
|
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||||
|
)}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className='!rounded-lg'
|
className='!rounded-lg'
|
||||||
fullMode={false}
|
fullMode={false}
|
||||||
|
|||||||
Reference in New Issue
Block a user