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:
ZacharyZcR
2025-09-04 17:57:12 +08:00
parent 983cf7383e
commit 591309530d
19 changed files with 4919 additions and 31 deletions

3
.env
View File

@@ -1 +1,2 @@
VERSION=1.5.0
VERSION=1.5.0
VITE_API_HOST=10.31.2.21

84
electron-builder.json Normal file
View 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
View 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
View 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);
});

View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,29 @@
{
"name": "termix",
"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",
"main": "electron/main-simple.cjs",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:backend": "tsc -p tsconfig.node.json",
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/starter.js",
"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": {
"@hookform/resolvers": "^5.1.1",
@@ -85,6 +99,7 @@
"zod": "^4.0.5"
},
"devDependencies": {
"@electron/rebuild": "^4.0.1",
"@eslint/js": "^9.34.0",
"@types/better-sqlite3": "^7.6.13",
"@types/cors": "^2.8.19",
@@ -97,6 +112,8 @@
"@types/ws": "^8.18.1",
"@vitejs/plugin-react-swc": "^3.10.2",
"autoprefixer": "^10.4.21",
"electron": "^31.7.0",
"electron-builder": "^24.13.3",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

View File

@@ -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('/ssh', sshRoutes);
app.use('/alerts', alertRoutes);

View File

@@ -1,6 +1,6 @@
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';
interface ButtonGroupProps {

View File

@@ -35,16 +35,19 @@ const buttonVariants = cva(
}
)
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
}: ButtonProps) {
const Comp = asChild ? Slot : "button"
return (
@@ -56,4 +59,4 @@ function Button({
)
}
export { Button, buttonVariants }
export { Button, buttonVariants, type ButtonProps }

View File

@@ -1,6 +1,6 @@
import * as React from "react"
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 { useIsMobile } from "@/hooks/use-mobile"

View File

@@ -1,5 +1,5 @@
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 { theme = "system" } = useTheme()

View File

@@ -25,7 +25,7 @@ i18n
// Backend options
backend: {
loadPath: '/locales/{{lng}}/translation.json',
loadPath: './locales/{{lng}}/translation.json',
},
interpolation: {

21
src/types/electron.d.ts vendored Normal file
View 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;
}

View File

@@ -201,34 +201,72 @@ function createApiInstance(baseURL: string): AxiosInstance {
// API INSTANCES
// ============================================================================
// Check if running in Electron
const isElectron = window.IS_ELECTRON === true;
const isDev = process.env.NODE_ENV === 'development' &&
(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)
export const sshHostApi = createApiInstance(
isDev ? 'http://localhost:8081/ssh' : '/ssh'
export let sshHostApi = createApiInstance(
getApiUrl('/ssh', 8081)
);
// Tunnel Management API (port 8083)
export const tunnelApi = createApiInstance(
isDev ? 'http://localhost:8083/ssh' : '/ssh'
export let tunnelApi = createApiInstance(
getApiUrl('/ssh', 8083)
);
// File Manager Operations API (port 8084) - SSH file operations
export const fileManagerApi = createApiInstance(
isDev ? 'http://localhost:8084/ssh/file_manager' : '/ssh/file_manager'
export let fileManagerApi = createApiInstance(
getApiUrl('/ssh/file_manager', 8084)
);
// Server Statistics API (port 8085)
export const statsApi = createApiInstance(
isDev ? 'http://localhost:8085' : ''
export let statsApi = createApiInstance(
getApiUrl('', 8085)
);
// Authentication API (port 8081) - includes users, alerts, version, releases
export const authApi = createApiInstance(
isDev ? 'http://localhost:8081' : ''
export let authApi = createApiInstance(
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
// ============================================================================
@@ -945,7 +983,7 @@ export async function generateBackupCodes(password?: string, totp_code?: string)
export async function getUserAlerts(userId: string): Promise<{ alerts: any[] }> {
try {
const apiInstance = createApiInstance(isDev ? 'http://localhost:8081' : '');
const apiInstance = createApiInstance(isDev ? `http://${apiHost}:8081` : '');
const response = await apiInstance.get(`/alerts/user/${userId}`);
return response.data;
} 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> {
try {
// 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 });
return response.data;
} 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> {
try {
// 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}`);
return response.data;
} catch (error) {
@@ -982,7 +1020,7 @@ export async function getReleasesRSS(perPage: number = 100): Promise<any> {
export async function getVersionInfo(): Promise<any> {
try {
// 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/');
return response.data;
} catch (error) {

43
start-backend.sh Executable file
View 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

View File

@@ -11,4 +11,5 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
base: './', // 使用相对路径,适配 Electron
})