From 723eefe9d83c36fbe369e33534fd1eec847460e1 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 11:08:52 +0800 Subject: [PATCH] electron --- .gitignore | 6 +- electron/README.md | 172 ++++++++++++++++++++++++++++++++++++++++ electron/build.sh | 41 ++++++++++ electron/main.js | 178 ++++++++++++++++++++++++++++++++++++++++++ electron/package.json | 108 +++++++++++++++++++++++++ electron/preload.js | 6 ++ web/package.json | 1 + 7 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 electron/README.md create mode 100755 electron/build.sh create mode 100644 electron/main.js create mode 100644 electron/package.json create mode 100644 electron/preload.js diff --git a/.gitignore b/.gitignore index 1382829fd..570a4385b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ web/dist one-api .DS_Store tiktoken_cache -.eslintcache \ No newline at end of file +.eslintcache + +electron/node_modules +electron/dist +electron/package-lock.json \ No newline at end of file diff --git a/electron/README.md b/electron/README.md new file mode 100644 index 000000000..5122a1ba1 --- /dev/null +++ b/electron/README.md @@ -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 \ No newline at end of file diff --git a/electron/build.sh b/electron/build.sh new file mode 100755 index 000000000..cef714328 --- /dev/null +++ b/electron/build.sh @@ -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." \ No newline at end of file diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 000000000..9fb954b88 --- /dev/null +++ b/electron/main.js @@ -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(); + }); + } +}); \ No newline at end of file diff --git a/electron/package.json b/electron/package.json new file mode 100644 index 000000000..d7fd66100 --- /dev/null +++ b/electron/package.json @@ -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 + } + } +} \ No newline at end of file diff --git a/electron/preload.js b/electron/preload.js new file mode 100644 index 000000000..6d8b6daa0 --- /dev/null +++ b/electron/preload.js @@ -0,0 +1,6 @@ +const { contextBridge } = require('electron'); + +contextBridge.exposeInMainWorld('electron', { + version: process.versions.electron, + platform: process.platform +}); \ No newline at end of file diff --git a/web/package.json b/web/package.json index f014d84b9..b94445f3c 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,7 @@ "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", + "antd": "^5.27.4", "axios": "^0.27.2", "clsx": "^2.1.1", "country-flag-icons": "^1.5.19",