This commit is contained in:
bubblepipe42
2025-09-29 11:08:52 +08:00
parent 74122e4175
commit 723eefe9d8
7 changed files with 511 additions and 1 deletions

6
.gitignore vendored
View File

@@ -11,4 +11,8 @@ web/dist
one-api one-api
.DS_Store .DS_Store
tiktoken_cache tiktoken_cache
.eslintcache .eslintcache
electron/node_modules
electron/dist
electron/package-lock.json

172
electron/README.md Normal file
View File

@@ -0,0 +1,172 @@
# New API Electron Desktop App
This directory contains the Electron wrapper for New API, allowing it to run as a native desktop application on Windows, macOS, and Linux.
## Architecture
The Electron app consists of:
- **main.js**: Main process that spawns the Go backend server and creates the application window
- **preload.js**: Preload script for secure context isolation
- **package.json**: Electron dependencies and build configuration
## Development
### Prerequisites
1. Build the Go backend first:
```bash
cd ..
go build -o new-api
```
2. Install Electron dependencies:
```bash
cd electron
npm install
```
### Running in Development Mode
```bash
npm start
```
This will:
- Start the Go backend on port 3000
- Open an Electron window pointing to `http://localhost:3000`
- Enable DevTools for debugging
## Building for Production
### Quick Build (Current Platform)
Use the provided build script:
```bash
./build.sh
```
This will:
1. Build the frontend (web/dist)
2. Build the Go binary for your platform
3. Package the Electron app
### Manual Build Steps
1. Build frontend:
```bash
cd ../web
DISABLE_ESLINT_PLUGIN='true' bun run build
```
2. Build backend:
```bash
cd ..
# macOS/Linux
go build -ldflags="-s -w" -o new-api
# Windows
go build -ldflags="-s -w" -o new-api.exe
```
3. Build Electron app:
```bash
cd electron
npm install
# All platforms
npm run build
# Or specific platforms
npm run build:mac # macOS (DMG, ZIP)
npm run build:win # Windows (NSIS installer, Portable)
npm run build:linux # Linux (AppImage, DEB)
```
### Output
Built apps are located in `electron/dist/`:
- **macOS**: `.dmg` and `.zip`
- **Windows**: `.exe` installer and portable `.exe`
- **Linux**: `.AppImage` and `.deb`
## Cross-Platform Building
To build for other platforms:
```bash
# From macOS, build Windows app
npm run build:win
# From macOS, build Linux app
npm run build:linux
```
Note: Building macOS apps requires macOS. Building Windows apps with code signing requires Windows.
## Configuration
### Port
The app uses port 3000 by default. To change:
Edit `electron/main.js`:
```javascript
const PORT = 3000; // Change to your desired port
```
### Data Directory
- **Development**: Uses `data/` in the project root
- **Production**: Uses Electron's `userData` directory:
- macOS: `~/Library/Application Support/New API/data/`
- Windows: `%APPDATA%/New API/data/`
- Linux: `~/.config/New API/data/`
### Window Size
Edit `electron/main.js` in the `createWindow()` function:
```javascript
mainWindow = new BrowserWindow({
width: 1400, // Change width
height: 900, // Change height
// ...
});
```
## Troubleshooting
### Server fails to start
Check the console logs in DevTools (Cmd/Ctrl+Shift+I). Common issues:
- Go binary not found (ensure it's built)
- Port 3000 already in use
- Database file permission issues
### Binary not found in production
Ensure the Go binary is built before running `electron-builder`:
```bash
go build -o new-api # macOS/Linux
go build -o new-api.exe # Windows
```
The binary must be in the project root, not inside `electron/`.
### Database issues
If you encounter database errors, delete the data directory and restart:
- Dev: `rm -rf data/`
- Prod: Clear Electron's userData folder (see "Data Directory" above)
## Icon
To add a custom icon:
1. Place a 512x512 PNG icon at `electron/icon.png`
2. Rebuild the app with `npm run build`
## Security
- Context isolation is enabled
- Node integration is disabled in renderer process
- Only safe APIs are exposed via preload script
- Backend runs as a local subprocess with no external network access by default

41
electron/build.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
echo "Building New API Electron App..."
echo "Step 1: Building frontend..."
cd ../web
DISABLE_ESLINT_PLUGIN='true' bun run build
cd ../electron
echo "Step 2: Building Go backend..."
cd ..
if [[ "$OSTYPE" == "darwin"* ]]; then
echo "Building for macOS..."
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
cd electron
npm install
npm run build:mac
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
echo "Building for Linux..."
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
cd electron
npm install
npm run build:linux
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
echo "Building for Windows..."
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api.exe
cd electron
npm install
npm run build:win
else
echo "Unknown OS, building for current platform..."
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
cd electron
npm install
npm run build
fi
echo "Build complete! Check electron/dist/ for output."

178
electron/main.js Normal file
View File

@@ -0,0 +1,178 @@
const { app, BrowserWindow, dialog } = require('electron');
const { spawn } = require('child_process');
const path = require('path');
const http = require('http');
const fs = require('fs');
let mainWindow;
let serverProcess;
const PORT = 3000;
function getBinaryPath() {
const isDev = process.env.NODE_ENV === 'development';
const platform = process.platform;
if (isDev) {
const binaryName = platform === 'win32' ? 'new-api.exe' : 'new-api';
return path.join(__dirname, '..', binaryName);
}
let binaryName;
switch (platform) {
case 'win32':
binaryName = 'new-api.exe';
break;
case 'darwin':
binaryName = 'new-api';
break;
case 'linux':
binaryName = 'new-api';
break;
default:
binaryName = 'new-api';
}
return path.join(process.resourcesPath, 'bin', binaryName);
}
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');
}
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
env.SQLITE_PATH = path.join(dataDir, 'new-api.db');
const workingDir = isDev
? path.join(__dirname, '..')
: process.resourcesPath;
serverProcess = spawn(binaryPath, [], {
env,
cwd: workingDir
});
serverProcess.stdout.on('data', (data) => {
console.log(`Server: ${data}`);
});
serverProcess.stderr.on('data', (data) => {
console.error(`Server Error: ${data}`);
});
serverProcess.on('error', (err) => {
console.error('Failed to start server:', err);
reject(err);
});
serverProcess.on('close', (code) => {
console.log(`Server process exited with code ${code}`);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
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);
});
req.end();
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true
},
title: 'New API',
icon: path.join(__dirname, 'icon.png')
});
mainWindow.loadURL(`http://localhost:${PORT}`);
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(async () => {
try {
await startServer();
createWindow();
} catch (err) {
console.error('Failed to start application:', err);
dialog.showErrorBox('Startup Error', `Failed to start server: ${err.message}`);
app.quit();
}
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
app.on('before-quit', (event) => {
if (serverProcess) {
event.preventDefault();
console.log('Shutting down server...');
serverProcess.kill('SIGTERM');
setTimeout(() => {
if (serverProcess) {
serverProcess.kill('SIGKILL');
}
app.exit();
}, 5000);
serverProcess.on('close', () => {
serverProcess = null;
app.exit();
});
}
});

108
electron/package.json Normal file
View File

@@ -0,0 +1,108 @@
{
"name": "new-api-electron",
"version": "1.0.0",
"description": "New API - AI Model Gateway Desktop Application",
"main": "main.js",
"scripts": {
"start": "NODE_ENV=development electron .",
"build": "electron-builder",
"build:mac": "electron-builder --mac",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux"
},
"keywords": [
"ai",
"api",
"gateway",
"openai",
"claude"
],
"author": "",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/Calcium-Ion/new-api"
},
"devDependencies": {
"electron": "^28.0.0",
"electron-builder": "^24.9.1"
},
"build": {
"appId": "com.newapi.desktop",
"productName": "New API",
"publish": null,
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"icon.png"
],
"extraResources": [
{
"from": "../new-api",
"to": "bin/new-api",
"filter": [
"**/*"
]
},
{
"from": "../new-api.exe",
"to": "bin/new-api.exe",
"filter": [
"**/*"
]
}
],
"mac": {
"category": "public.app-category.developer-tools",
"icon": "icon.png",
"target": [
"dmg",
"zip"
],
"extraResources": [
{
"from": "../new-api",
"to": "bin/new-api"
},
{
"from": "../web/dist",
"to": "web/dist"
}
]
},
"win": {
"icon": "icon.png",
"target": [
"nsis",
"portable"
],
"extraResources": [
{
"from": "../new-api.exe",
"to": "bin/new-api.exe"
}
]
},
"linux": {
"icon": "icon.png",
"target": [
"AppImage",
"deb"
],
"category": "Development",
"extraResources": [
{
"from": "../new-api",
"to": "bin/new-api"
}
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}

6
electron/preload.js Normal file
View File

@@ -0,0 +1,6 @@
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
version: process.versions.electron,
platform: process.platform
});

View File

@@ -10,6 +10,7 @@
"@visactor/react-vchart": "~1.8.8", "@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8", "@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8",
"antd": "^5.27.4",
"axios": "^0.27.2", "axios": "^0.27.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.5.19", "country-flag-icons": "^1.5.19",