Convert web app to Electron desktop application
- Add Electron main process with developer tools support - Create preload script for secure context bridge - Configure electron-builder for packaging - Update Vite config for Electron compatibility (base: './') - Add environment variable support for API host configuration - Fix i18n to use relative paths for Electron file protocol - Restore multi-port backend architecture (8081-8085) - Add enhanced backend startup script with port checking - Update package.json with Electron dependencies and build scripts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
84
electron-builder.json
Normal file
84
electron-builder.json
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.termix.app",
|
||||||
|
"productName": "Termix",
|
||||||
|
"directories": {
|
||||||
|
"output": "release",
|
||||||
|
"buildResources": "build"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/main-simple.cjs",
|
||||||
|
"electron/preload-simple.cjs",
|
||||||
|
"public/icon.*",
|
||||||
|
"package.json"
|
||||||
|
],
|
||||||
|
"asar": true,
|
||||||
|
"mac": {
|
||||||
|
"category": "public.app-category.developer-tools",
|
||||||
|
"icon": "public/icon.icns",
|
||||||
|
"hardenedRuntime": true,
|
||||||
|
"gatekeeperAssess": false,
|
||||||
|
"entitlements": "build/entitlements.mac.plist",
|
||||||
|
"entitlementsInherit": "build/entitlements.mac.plist",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "dmg",
|
||||||
|
"arch": ["x64", "arm64"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "zip",
|
||||||
|
"arch": ["x64", "arm64"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"icon": "public/icon.ico",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "nsis",
|
||||||
|
"arch": ["x64", "ia32"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "portable",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"category": "Development",
|
||||||
|
"target": [
|
||||||
|
{
|
||||||
|
"target": "AppImage",
|
||||||
|
"arch": ["x64"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"perMachine": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"deleteAppDataOnUninstall": false,
|
||||||
|
"createDesktopShortcut": true,
|
||||||
|
"createStartMenuShortcut": true
|
||||||
|
},
|
||||||
|
"dmg": {
|
||||||
|
"contents": [
|
||||||
|
{
|
||||||
|
"x": 410,
|
||||||
|
"y": 150,
|
||||||
|
"type": "link",
|
||||||
|
"path": "/Applications"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 130,
|
||||||
|
"y": 150,
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"publish": {
|
||||||
|
"provider": "github",
|
||||||
|
"owner": "LukeGus",
|
||||||
|
"repo": "Termix"
|
||||||
|
}
|
||||||
|
}
|
||||||
132
electron/main-simple.cjs
Normal file
132
electron/main-simple.cjs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let mainWindow = null;
|
||||||
|
|
||||||
|
// 开发环境检测
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
|
||||||
|
// 防止多开
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建主窗口
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600,
|
||||||
|
title: 'Termix',
|
||||||
|
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload-simple.cjs'),
|
||||||
|
webSecurity: !isDev
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建应用菜单(包含开发者工具快捷键)
|
||||||
|
const template = [
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Toggle Developer Tools',
|
||||||
|
accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.webContents.toggleDevTools();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reload',
|
||||||
|
accelerator: 'CmdOrCtrl+R',
|
||||||
|
click: () => {
|
||||||
|
mainWindow.webContents.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
|
||||||
|
// 加载应用
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:连接到 Vite 开发服务器
|
||||||
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
} else {
|
||||||
|
// 生产环境:加载构建后的文件
|
||||||
|
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||||||
|
// 生产环境也启用开发者工具以便调试
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口准备好后显示
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理窗口关闭事件
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理外部链接
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC 通信处理
|
||||||
|
ipcMain.handle('get-app-version', () => {
|
||||||
|
return app.getVersion();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-platform', () => {
|
||||||
|
return process.platform;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应用事件处理
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
createWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
} else if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理未捕获的异常
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught Exception:', error);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
467
electron/main.cjs
Normal file
467
electron/main.cjs
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
const { app, BrowserWindow, Menu, Tray, shell, ipcMain, dialog } = require('electron');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// 动态导入可能有 ESM 问题的模块
|
||||||
|
let portfinder;
|
||||||
|
let Store;
|
||||||
|
let autoUpdater;
|
||||||
|
|
||||||
|
try {
|
||||||
|
portfinder = require('portfinder');
|
||||||
|
Store = require('electron-store');
|
||||||
|
const updaterModule = require('electron-updater');
|
||||||
|
autoUpdater = updaterModule.autoUpdater;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading modules:', error);
|
||||||
|
// 提供后备方案
|
||||||
|
portfinder = {
|
||||||
|
getPortPromise: async () => 18080 + Math.floor(Math.random() * 100)
|
||||||
|
};
|
||||||
|
Store = class {
|
||||||
|
constructor() { this.data = {}; }
|
||||||
|
get(key, defaultValue) { return this.data[key] || defaultValue; }
|
||||||
|
set(key, value) { this.data[key] = value; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置存储
|
||||||
|
const store = new Store();
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let mainWindow = null;
|
||||||
|
let backendProcess = null;
|
||||||
|
let tray = null;
|
||||||
|
let backendPort = null;
|
||||||
|
let isQuitting = false;
|
||||||
|
|
||||||
|
// 开发环境检测
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
|
||||||
|
// 防止多开
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
// 如果用户试图运行第二个实例,我们应该聚焦我们的窗口
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端进程管理类
|
||||||
|
class BackendManager {
|
||||||
|
constructor() {
|
||||||
|
this.process = null;
|
||||||
|
this.port = null;
|
||||||
|
this.retryCount = 0;
|
||||||
|
this.maxRetries = 3;
|
||||||
|
this.isStarting = false;
|
||||||
|
this.healthCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAvailablePort() {
|
||||||
|
portfinder.basePort = store.get('backend.port', 18080);
|
||||||
|
try {
|
||||||
|
const port = await portfinder.getPortPromise();
|
||||||
|
this.port = port;
|
||||||
|
return port;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error finding available port:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
if (this.isStarting || this.process) {
|
||||||
|
console.log('Backend already starting or running');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStarting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找可用端口
|
||||||
|
await this.findAvailablePort();
|
||||||
|
console.log(`Starting backend on port ${this.port}`);
|
||||||
|
|
||||||
|
// 确定后端可执行文件路径
|
||||||
|
let backendPath;
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:使用 node 运行构建后的 JS
|
||||||
|
backendPath = path.join(__dirname, '..', 'dist', 'backend', 'starter.js');
|
||||||
|
} else {
|
||||||
|
// 生产环境:使用打包后的后端
|
||||||
|
backendPath = path.join(process.resourcesPath, 'backend', 'starter.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保后端文件存在
|
||||||
|
if (!fs.existsSync(backendPath)) {
|
||||||
|
throw new Error(`Backend file not found at ${backendPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置环境变量
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
PORT: this.port.toString(),
|
||||||
|
NODE_ENV: isDev ? 'development' : 'production',
|
||||||
|
DATA_PATH: app.getPath('userData'),
|
||||||
|
DB_PATH: path.join(app.getPath('userData'), 'database.db'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动后端进程
|
||||||
|
if (isDev) {
|
||||||
|
this.process = spawn('node', [backendPath], {
|
||||||
|
env,
|
||||||
|
cwd: path.join(__dirname, '..'),
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.process = spawn('node', [backendPath], {
|
||||||
|
env,
|
||||||
|
cwd: process.resourcesPath,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听后端输出
|
||||||
|
this.process.stdout.on('data', (data) => {
|
||||||
|
console.log(`Backend stdout: ${data}`);
|
||||||
|
// 向渲染进程发送日志
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('backend-log', data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process.stderr.on('data', (data) => {
|
||||||
|
console.error(`Backend stderr: ${data}`);
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('backend-error', data.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听后端进程退出
|
||||||
|
this.process.on('exit', (code) => {
|
||||||
|
console.log(`Backend process exited with code ${code}`);
|
||||||
|
this.process = null;
|
||||||
|
this.isStarting = false;
|
||||||
|
|
||||||
|
// 如果不是正在退出且退出码不为0,尝试重启
|
||||||
|
if (!isQuitting && code !== 0 && this.retryCount < this.maxRetries) {
|
||||||
|
this.retryCount++;
|
||||||
|
console.log(`Attempting to restart backend (retry ${this.retryCount}/${this.maxRetries})`);
|
||||||
|
setTimeout(() => this.start(), 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待后端启动
|
||||||
|
await this.waitForBackend();
|
||||||
|
|
||||||
|
// 启动健康检查
|
||||||
|
this.startHealthCheck();
|
||||||
|
|
||||||
|
// 更新全局端口变量
|
||||||
|
backendPort = this.port;
|
||||||
|
|
||||||
|
// 通知渲染进程
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('backend-started', { port: this.port });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isStarting = false;
|
||||||
|
this.retryCount = 0;
|
||||||
|
|
||||||
|
return this.port;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to start backend:', error);
|
||||||
|
this.isStarting = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForBackend() {
|
||||||
|
const maxWaitTime = 30000; // 30秒
|
||||||
|
const checkInterval = 500; // 每500ms检查一次
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
while (Date.now() - startTime < maxWaitTime) {
|
||||||
|
try {
|
||||||
|
// 尝试连接后端健康检查端点
|
||||||
|
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Backend is ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 继续等待
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Backend failed to start within timeout period');
|
||||||
|
}
|
||||||
|
|
||||||
|
startHealthCheck() {
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
|
clearInterval(this.healthCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.healthCheckInterval = setInterval(async () => {
|
||||||
|
if (!this.process) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Backend health check failed');
|
||||||
|
// 可以在这里触发重启逻辑
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Backend health check error:', error);
|
||||||
|
}
|
||||||
|
}, 10000); // 每10秒检查一次
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (this.healthCheckInterval) {
|
||||||
|
clearInterval(this.healthCheckInterval);
|
||||||
|
this.healthCheckInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.process) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Stopping backend process...');
|
||||||
|
|
||||||
|
// 设置超时强制杀死
|
||||||
|
const killTimeout = setTimeout(() => {
|
||||||
|
if (this.process) {
|
||||||
|
console.log('Force killing backend process');
|
||||||
|
this.process.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
this.process.on('exit', () => {
|
||||||
|
clearTimeout(killTimeout);
|
||||||
|
this.process = null;
|
||||||
|
console.log('Backend process stopped');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
|
this.process.kill('SIGTERM');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建后端管理器实例
|
||||||
|
const backendManager = new BackendManager();
|
||||||
|
|
||||||
|
// 创建主窗口
|
||||||
|
function createWindow() {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1200,
|
||||||
|
height: 800,
|
||||||
|
minWidth: 800,
|
||||||
|
minHeight: 600,
|
||||||
|
title: 'Termix',
|
||||||
|
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.cjs'),
|
||||||
|
webSecurity: !isDev
|
||||||
|
},
|
||||||
|
show: false, // 先不显示,等加载完成
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除默认菜单栏(Windows/Linux)
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
mainWindow.setMenuBarVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载应用
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:连接到 Vite 开发服务器
|
||||||
|
mainWindow.loadURL('http://localhost:5173');
|
||||||
|
mainWindow.webContents.openDevTools();
|
||||||
|
} else {
|
||||||
|
// 生产环境:加载构建后的文件
|
||||||
|
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 窗口准备好后显示
|
||||||
|
mainWindow.once('ready-to-show', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理窗口关闭事件
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
if (!isQuitting && process.platform === 'darwin') {
|
||||||
|
// macOS:隐藏窗口而不是退出
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
} else if (!isQuitting && store.get('minimizeToTray', true)) {
|
||||||
|
// Windows/Linux:最小化到托盘
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理外部链接
|
||||||
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
shell.openExternal(url);
|
||||||
|
return { action: 'deny' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建系统托盘
|
||||||
|
function createTray() {
|
||||||
|
if (process.platform === 'darwin') return; // macOS 不需要托盘
|
||||||
|
|
||||||
|
const iconPath = path.join(__dirname, '..', 'public', 'icon.png');
|
||||||
|
tray = new Tray(iconPath);
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: '显示',
|
||||||
|
click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: '退出',
|
||||||
|
click: () => {
|
||||||
|
isQuitting = true;
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
tray.setToolTip('Termix');
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
// 双击托盘图标显示窗口
|
||||||
|
tray.on('double-click', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC 通信处理
|
||||||
|
ipcMain.handle('get-backend-port', () => {
|
||||||
|
return backendPort;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-app-version', () => {
|
||||||
|
return app.getVersion();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('get-platform', () => {
|
||||||
|
return process.platform;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('restart-backend', async () => {
|
||||||
|
try {
|
||||||
|
await backendManager.stop();
|
||||||
|
await backendManager.start();
|
||||||
|
return { success: true, port: backendManager.port };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||||||
|
const result = await dialog.showSaveDialog(mainWindow, options);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('show-open-dialog', async (event, options) => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, options);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动更新
|
||||||
|
if (!isDev && autoUpdater) {
|
||||||
|
try {
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
|
||||||
|
autoUpdater.on('update-available', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('update-available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on('update-downloaded', () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.send('update-downloaded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Auto-updater not available:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用事件处理
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
try {
|
||||||
|
// 启动后端
|
||||||
|
await backendManager.start();
|
||||||
|
|
||||||
|
// 创建窗口
|
||||||
|
createWindow();
|
||||||
|
|
||||||
|
// 创建托盘
|
||||||
|
createTray();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize application:', error);
|
||||||
|
dialog.showErrorBox('启动失败', `无法启动应用: ${error.message}`);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
} else if (mainWindow) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', async () => {
|
||||||
|
isQuitting = true;
|
||||||
|
await backendManager.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理未捕获的异常
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('Uncaught Exception:', error);
|
||||||
|
dialog.showErrorBox('未捕获的异常', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
});
|
||||||
17
electron/preload-simple.cjs
Normal file
17
electron/preload-simple.cjs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 暴露简化的 API 给渲染进程
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// 获取应用版本
|
||||||
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
|
||||||
|
// 获取平台信息
|
||||||
|
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
||||||
|
|
||||||
|
// 环境检测
|
||||||
|
isElectron: true,
|
||||||
|
isDev: process.env.NODE_ENV === 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加一个标识,让渲染进程知道这是 Electron 环境
|
||||||
|
window.IS_ELECTRON = true;
|
||||||
54
electron/preload.cjs
Normal file
54
electron/preload.cjs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
// 暴露安全的 API 给渲染进程
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// 获取后端端口
|
||||||
|
getBackendPort: () => ipcRenderer.invoke('get-backend-port'),
|
||||||
|
|
||||||
|
// 获取应用版本
|
||||||
|
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||||
|
|
||||||
|
// 获取平台信息
|
||||||
|
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
||||||
|
|
||||||
|
// 重启后端
|
||||||
|
restartBackend: () => ipcRenderer.invoke('restart-backend'),
|
||||||
|
|
||||||
|
// 文件对话框
|
||||||
|
showSaveDialog: (options) => ipcRenderer.invoke('show-save-dialog', options),
|
||||||
|
showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),
|
||||||
|
|
||||||
|
// 监听后端事件
|
||||||
|
onBackendStarted: (callback) => {
|
||||||
|
ipcRenderer.on('backend-started', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
onBackendLog: (callback) => {
|
||||||
|
ipcRenderer.on('backend-log', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
onBackendError: (callback) => {
|
||||||
|
ipcRenderer.on('backend-error', (event, data) => callback(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 监听更新事件
|
||||||
|
onUpdateAvailable: (callback) => {
|
||||||
|
ipcRenderer.on('update-available', () => callback());
|
||||||
|
},
|
||||||
|
|
||||||
|
onUpdateDownloaded: (callback) => {
|
||||||
|
ipcRenderer.on('update-downloaded', () => callback());
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除事件监听器
|
||||||
|
removeAllListeners: (channel) => {
|
||||||
|
ipcRenderer.removeAllListeners(channel);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 环境检测
|
||||||
|
isElectron: true,
|
||||||
|
isDev: process.env.NODE_ENV === 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加一个标识,让渲染进程知道这是 Electron 环境
|
||||||
|
window.IS_ELECTRON = true;
|
||||||
4010
package-lock.json
generated
4010
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -1,15 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "termix",
|
"name": "termix",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.6.0",
|
||||||
|
"description": "Open-source server management platform with SSH terminal access, tunnel management, and file editing",
|
||||||
|
"author": {
|
||||||
|
"name": "LukeGus",
|
||||||
|
"email": "support@termix.site"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"main": "electron/main-simple.cjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:backend": "tsc -p tsconfig.node.json",
|
"build:backend": "tsc -p tsconfig.node.json",
|
||||||
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js",
|
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"electron": "electron .",
|
||||||
|
"electron:dev": "npm run build:backend && NODE_ENV=development electron .",
|
||||||
|
"electron:build": "npm run build && npm run build:backend && electron-builder",
|
||||||
|
"dist": "npm run build && npm run build:backend && electron-builder --publish=never",
|
||||||
|
"dist:win": "npm run dist -- --win",
|
||||||
|
"dist:mac": "npm run dist -- --mac",
|
||||||
|
"dist:linux": "npm run dist -- --linux",
|
||||||
|
"postinstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
@@ -85,6 +99,7 @@
|
|||||||
"zod": "^4.0.5"
|
"zod": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/rebuild": "^4.0.1",
|
||||||
"@eslint/js": "^9.34.0",
|
"@eslint/js": "^9.34.0",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
@@ -97,6 +112,8 @@
|
|||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"electron": "^31.7.0",
|
||||||
|
"electron-builder": "^24.13.3",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
|
|||||||
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 B |
@@ -235,6 +235,16 @@ app.get('/releases/rss', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Health check endpoint for Electron backend manager
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.status(200).json({
|
||||||
|
status: 'ok',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'database-api',
|
||||||
|
port: PORT
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app.use('/users', userRoutes);
|
app.use('/users', userRoutes);
|
||||||
app.use('/ssh', sshRoutes);
|
app.use('/ssh', sshRoutes);
|
||||||
app.use('/alerts', alertRoutes);
|
app.use('/alerts', alertRoutes);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Children, ReactElement, cloneElement, isValidElement } from 'react';
|
import { Children, ReactElement, cloneElement, isValidElement } from 'react';
|
||||||
|
|
||||||
import { ButtonProps } from '@/components/ui/button';
|
import { type ButtonProps } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ButtonGroupProps {
|
interface ButtonGroupProps {
|
||||||
|
|||||||
@@ -35,16 +35,19 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ComponentProps<"button">,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
function Button({
|
function Button({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
size,
|
size,
|
||||||
asChild = false,
|
asChild = false,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"button"> &
|
}: ButtonProps) {
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,4 +59,4 @@ function Button({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants, type ButtonProps }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import { cva, VariantProps } from "class-variance-authority"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
import { PanelLeftIcon } from "lucide-react"
|
import { PanelLeftIcon } from "lucide-react"
|
||||||
|
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
|
||||||
const Toaster = ({ ...props }: ToasterProps) => {
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme()
|
const { theme = "system" } = useTheme()
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ i18n
|
|||||||
|
|
||||||
// Backend options
|
// Backend options
|
||||||
backend: {
|
backend: {
|
||||||
loadPath: '/locales/{{lng}}/translation.json',
|
loadPath: './locales/{{lng}}/translation.json',
|
||||||
},
|
},
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
|
|||||||
21
src/types/electron.d.ts
vendored
Normal file
21
src/types/electron.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
interface ElectronAPI {
|
||||||
|
getBackendPort: () => Promise<number>;
|
||||||
|
getAppVersion: () => Promise<string>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
restartBackend: () => Promise<{ success: boolean; port?: number; error?: string }>;
|
||||||
|
showSaveDialog: (options: any) => Promise<any>;
|
||||||
|
showOpenDialog: (options: any) => Promise<any>;
|
||||||
|
onBackendStarted: (callback: (data: { port: number }) => void) => void;
|
||||||
|
onBackendLog: (callback: (data: string) => void) => void;
|
||||||
|
onBackendError: (callback: (data: string) => void) => void;
|
||||||
|
onUpdateAvailable: (callback: () => void) => void;
|
||||||
|
onUpdateDownloaded: (callback: () => void) => void;
|
||||||
|
removeAllListeners: (channel: string) => void;
|
||||||
|
isElectron: boolean;
|
||||||
|
isDev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
electronAPI?: ElectronAPI;
|
||||||
|
IS_ELECTRON?: boolean;
|
||||||
|
}
|
||||||
@@ -201,34 +201,72 @@ function createApiInstance(baseURL: string): AxiosInstance {
|
|||||||
// API INSTANCES
|
// API INSTANCES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
// Check if running in Electron
|
||||||
|
const isElectron = window.IS_ELECTRON === true;
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development' &&
|
const isDev = process.env.NODE_ENV === 'development' &&
|
||||||
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
(window.location.port === '3000' || window.location.port === '5173' || window.location.port === '');
|
||||||
|
|
||||||
|
// Get API host and port configuration
|
||||||
|
let apiHost = import.meta.env.VITE_API_HOST || 'localhost';
|
||||||
|
let apiPort = 8081;
|
||||||
|
|
||||||
|
// In Electron, use fixed port since backend runs independently
|
||||||
|
if (isElectron) {
|
||||||
|
// Backend runs independently, use default port
|
||||||
|
apiPort = 8081;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get the correct API URL
|
||||||
|
function getApiUrl(path: string, defaultPort: number): string {
|
||||||
|
if (isElectron) {
|
||||||
|
// In Electron, always use localhost with dynamic port
|
||||||
|
return `http://127.0.0.1:${apiPort}${path}`;
|
||||||
|
} else if (isDev) {
|
||||||
|
// In dev mode, use configured host
|
||||||
|
return `http://${apiHost}:${defaultPort}${path}`;
|
||||||
|
} else {
|
||||||
|
// In production web mode, use relative paths
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-port backend architecture (original design)
|
||||||
// SSH Host Management API (port 8081)
|
// SSH Host Management API (port 8081)
|
||||||
export const sshHostApi = createApiInstance(
|
export let sshHostApi = createApiInstance(
|
||||||
isDev ? 'http://localhost:8081/ssh' : '/ssh'
|
getApiUrl('/ssh', 8081)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tunnel Management API (port 8083)
|
// Tunnel Management API (port 8083)
|
||||||
export const tunnelApi = createApiInstance(
|
export let tunnelApi = createApiInstance(
|
||||||
isDev ? 'http://localhost:8083/ssh' : '/ssh'
|
getApiUrl('/ssh', 8083)
|
||||||
);
|
);
|
||||||
|
|
||||||
// File Manager Operations API (port 8084) - SSH file operations
|
// File Manager Operations API (port 8084) - SSH file operations
|
||||||
export const fileManagerApi = createApiInstance(
|
export let fileManagerApi = createApiInstance(
|
||||||
isDev ? 'http://localhost:8084/ssh/file_manager' : '/ssh/file_manager'
|
getApiUrl('/ssh/file_manager', 8084)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Server Statistics API (port 8085)
|
// Server Statistics API (port 8085)
|
||||||
export const statsApi = createApiInstance(
|
export let statsApi = createApiInstance(
|
||||||
isDev ? 'http://localhost:8085' : ''
|
getApiUrl('', 8085)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Authentication API (port 8081) - includes users, alerts, version, releases
|
// Authentication API (port 8081) - includes users, alerts, version, releases
|
||||||
export const authApi = createApiInstance(
|
export let authApi = createApiInstance(
|
||||||
isDev ? 'http://localhost:8081' : ''
|
getApiUrl('', 8081)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Function to update API instances with new port (for Electron)
|
||||||
|
function updateApiPorts(port: number) {
|
||||||
|
apiPort = port;
|
||||||
|
sshHostApi = createApiInstance(`http://127.0.0.1:${port}/ssh`);
|
||||||
|
tunnelApi = createApiInstance(`http://127.0.0.1:${port}/ssh`);
|
||||||
|
fileManagerApi = createApiInstance(`http://127.0.0.1:${port}/ssh/file_manager`);
|
||||||
|
statsApi = createApiInstance(`http://127.0.0.1:${port}`);
|
||||||
|
authApi = createApiInstance(`http://127.0.0.1:${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ERROR HANDLING
|
// ERROR HANDLING
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -945,7 +983,7 @@ export async function generateBackupCodes(password?: string, totp_code?: string)
|
|||||||
|
|
||||||
export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> {
|
export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> {
|
||||||
try {
|
try {
|
||||||
const apiInstance = createApiInstance(isDev ? 'http://localhost:8081' : '');
|
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
||||||
const response = await apiInstance.get(`/alerts/user/${userId}`);
|
const response = await apiInstance.get(`/alerts/user/${userId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -956,7 +994,7 @@ export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }>
|
|||||||
export async function dismissAlert(userId: string, alertId: string): Promise<any> {
|
export async function dismissAlert(userId: string, alertId: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Use the general API instance since alerts endpoint is at root level
|
// Use the general API instance since alerts endpoint is at root level
|
||||||
const apiInstance = createApiInstance(isDev ? 'http://localhost:8081' : '');
|
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
||||||
const response = await apiInstance.post('/alerts/dismiss', { userId, alertId });
|
const response = await apiInstance.post('/alerts/dismiss', { userId, alertId });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -971,7 +1009,7 @@ export async function dismissAlert(userId: string, alertId: string): Promise<any
|
|||||||
export async function getReleasesRSS(perPage: number = 100): Promise<any> {
|
export async function getReleasesRSS(perPage: number = 100): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Use the general API instance since releases endpoint is at root level
|
// Use the general API instance since releases endpoint is at root level
|
||||||
const apiInstance = createApiInstance(isDev ? 'http://localhost:8081' : '');
|
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
||||||
const response = await apiInstance.get(`/releases/rss?per_page=${perPage}`);
|
const response = await apiInstance.get(`/releases/rss?per_page=${perPage}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -982,7 +1020,7 @@ export async function getReleasesRSS(perPage: number = 100): Promise<any> {
|
|||||||
export async function getVersionInfo(): Promise<any> {
|
export async function getVersionInfo(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Use the general API instance since version endpoint is at root level
|
// Use the general API instance since version endpoint is at root level
|
||||||
const apiInstance = createApiInstance(isDev ? 'http://localhost:8081' : '');
|
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
|
||||||
const response = await apiInstance.get('/version/');
|
const response = await apiInstance.get('/version/');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
43
start-backend.sh
Executable file
43
start-backend.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "Starting Termix Backend Services..."
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
export NODE_ENV=${NODE_ENV:-production}
|
||||||
|
export DATA_PATH=${DATA_PATH:-./data}
|
||||||
|
|
||||||
|
# 检查是否已经构建
|
||||||
|
if [ ! -d "dist/backend" ]; then
|
||||||
|
echo "Building backend..."
|
||||||
|
npm run build:backend
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查端口是否被占用的函数
|
||||||
|
check_port() {
|
||||||
|
local port=$1
|
||||||
|
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
|
||||||
|
echo "Port $port is already in use"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动所有后端服务
|
||||||
|
echo "Starting backend services..."
|
||||||
|
echo "- Database API (8081)"
|
||||||
|
echo "- WebSocket Terminal (8082)"
|
||||||
|
echo "- Tunnel Management (8083)"
|
||||||
|
echo "- File Manager (8084)"
|
||||||
|
echo "- Server Statistics (8085)"
|
||||||
|
|
||||||
|
# 检查关键端口
|
||||||
|
for port in 8081 8082 8083 8084 8085; do
|
||||||
|
if ! check_port $port; then
|
||||||
|
echo "Error: Port $port is in use. Please stop the conflicting service."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 启动主服务(会自动启动所有其他服务)
|
||||||
|
echo "Starting all services via starter.js..."
|
||||||
|
node dist/backend/starter.js
|
||||||
@@ -11,4 +11,5 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
base: './', // 使用相对路径,适配 Electron
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user