2
0
bubblepipe42 3 сар өмнө
parent
commit
723eefe9d8

+ 5 - 1
.gitignore

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

+ 172 - 0
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

+ 41 - 0
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."

+ 178 - 0
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();
+    });
+  }
+});

+ 108 - 0
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
+    }
+  }
+}

+ 6 - 0
electron/preload.js

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

+ 1 - 0
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",