From 2488e6ab66dfaae716fc15b4cb5d6d567ccc3733 Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Sat, 19 Jul 2025 21:18:38 +0800 Subject: [PATCH 01/79] feat: add ali qwen channel autoDisabled --- service/channel.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/channel.go b/service/channel.go index 4d38e6edc7ea..fe76fc21e743 100644 --- a/service/channel.go +++ b/service/channel.go @@ -67,6 +67,8 @@ func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool { return true case "pre_consume_token_quota_failed": return true + case "Arrearage": + return true } switch oaiErr.Type { case "insufficient_quota": From 723eefe9d83c36fbe369e33534fd1eec847460e1 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 11:08:52 +0800 Subject: [PATCH 02/79] 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 1382829fd7d4..570a4385baa8 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 000000000000..5122a1ba1a6a --- /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 000000000000..cef714328d5a --- /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 000000000000..9fb954b880f9 --- /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 000000000000..d7fd66100ba7 --- /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 000000000000..6d8b6daa0649 --- /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 f014d84b9e0f..b94445f3ca16 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", From 0046282fb8d968c001a1c63864c17b39ce4d87c8 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 13:01:07 +0800 Subject: [PATCH 03/79] stash --- electron/create-tray-icon.js | 60 +++++++++++++++++++++++++++++++ electron/entitlements.mac.plist | 18 ++++++++++ electron/main.js | 63 ++++++++++++++++++++++++++++++--- electron/package.json | 5 +++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 electron/create-tray-icon.js create mode 100644 electron/entitlements.mac.plist diff --git a/electron/create-tray-icon.js b/electron/create-tray-icon.js new file mode 100644 index 000000000000..517393b2e1d6 --- /dev/null +++ b/electron/create-tray-icon.js @@ -0,0 +1,60 @@ +// Create a simple tray icon for macOS +// Run: node create-tray-icon.js + +const fs = require('fs'); +const { createCanvas } = require('canvas'); + +function createTrayIcon() { + // For macOS, we'll use a Template image (black and white) + // Size should be 22x22 for Retina displays (@2x would be 44x44) + const canvas = createCanvas(22, 22); + const ctx = canvas.getContext('2d'); + + // Clear canvas + ctx.clearRect(0, 0, 22, 22); + + // Draw a simple "API" icon + ctx.fillStyle = '#000000'; + ctx.font = 'bold 10px system-ui'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('API', 11, 11); + + // Save as PNG + const buffer = canvas.toBuffer('image/png'); + fs.writeFileSync('tray-icon.png', buffer); + + // For Template images on macOS (will adapt to menu bar theme) + fs.writeFileSync('tray-iconTemplate.png', buffer); + fs.writeFileSync('tray-iconTemplate@2x.png', buffer); + + console.log('Tray icon created successfully!'); +} + +// Check if canvas is installed +try { + createTrayIcon(); +} catch (err) { + console.log('Canvas module not installed.'); + console.log('For now, creating a placeholder. Install canvas with: npm install canvas'); + + // Create a minimal 1x1 transparent PNG as placeholder + const minimalPNG = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xDB, 0x56, + 0xCA, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4C, 0x54, + 0x45, 0x00, 0x00, 0x00, 0xA7, 0x7A, 0x3D, 0xDA, + 0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4E, 0x53, + 0x00, 0x40, 0xE6, 0xD8, 0x66, 0x00, 0x00, 0x00, + 0x0A, 0x49, 0x44, 0x41, 0x54, 0x08, 0x1D, 0x62, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x0A, 0x2D, 0xCB, 0x59, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, + 0x60, 0x82 + ]); + + fs.writeFileSync('tray-icon.png', minimalPNG); + console.log('Created placeholder tray icon.'); +} \ No newline at end of file diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist new file mode 100644 index 000000000000..a00aebcd06ae --- /dev/null +++ b/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-jit + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/electron/main.js b/electron/main.js index 9fb954b880f9..50307eac29ee 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, dialog } = require('electron'); +const { app, BrowserWindow, dialog, Tray, Menu } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const http = require('http'); @@ -6,6 +6,7 @@ const fs = require('fs'); let mainWindow; let serverProcess; +let tray = null; const PORT = 3000; function getBinaryPath() { @@ -109,6 +110,49 @@ function waitForServer(resolve, reject, retries = 30) { req.end(); } +function createTray() { + tray = new Tray(path.join(__dirname, 'tray-icon.png')); + + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show New API', + click: () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.show(); + if (process.platform === 'darwin') { + app.dock.show(); + } + } + } + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + app.isQuitting = true; + app.quit(); + } + } + ]); + + tray.setToolTip('New API'); + tray.setContextMenu(contextMenu); + + // On macOS, clicking the tray icon shows the menu + tray.on('click', () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); + if (mainWindow.isVisible() && process.platform === 'darwin') { + app.dock.show(); + } + } + }); +} + function createWindow() { mainWindow = new BrowserWindow({ width: 1400, @@ -128,6 +172,17 @@ function createWindow() { mainWindow.webContents.openDevTools(); } + // Close to tray instead of quitting + mainWindow.on('close', (event) => { + if (!app.isQuitting) { + event.preventDefault(); + mainWindow.hide(); + if (process.platform === 'darwin') { + app.dock.hide(); + } + } + }); + mainWindow.on('closed', () => { mainWindow = null; }); @@ -136,6 +191,7 @@ function createWindow() { app.whenReady().then(async () => { try { await startServer(); + createTray(); createWindow(); } catch (err) { console.error('Failed to start application:', err); @@ -145,9 +201,8 @@ app.whenReady().then(async () => { }); app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit(); - } + // Don't quit when window is closed, keep running in tray + // Only quit when explicitly choosing Quit from tray menu }); app.on('activate', () => { diff --git a/electron/package.json b/electron/package.json index d7fd66100ba7..a480b5da063f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -58,6 +58,11 @@ "mac": { "category": "public.app-category.developer-tools", "icon": "icon.png", + "identity": null, + "hardenedRuntime": false, + "gatekeeperAssess": false, + "entitlements": "entitlements.mac.plist", + "entitlementsInherit": "entitlements.mac.plist", "target": [ "dmg", "zip" From a57a36a739244878d00bf04acf250d6f4382f046 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 13:25:22 +0800 Subject: [PATCH 04/79] tray --- electron/main.js | 74 +++++++++++++++--------------- electron/tray-iconTemplate.png | Bin 0 -> 459 bytes electron/tray-iconTemplate@2x.png | Bin 0 -> 754 bytes 3 files changed, 37 insertions(+), 37 deletions(-) create mode 100644 electron/tray-iconTemplate.png create mode 100644 electron/tray-iconTemplate@2x.png diff --git a/electron/main.js b/electron/main.js index 50307eac29ee..bbe6e7cc89e4 100644 --- a/electron/main.js +++ b/electron/main.js @@ -110,8 +110,43 @@ function waitForServer(resolve, reject, retries = 30) { 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(); + } + + // Close to tray instead of quitting + mainWindow.on('close', (event) => { + if (!app.isQuitting) { + event.preventDefault(); + mainWindow.hide(); + if (process.platform === 'darwin') { + app.dock.hide(); + } + } + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + function createTray() { - tray = new Tray(path.join(__dirname, 'tray-icon.png')); + tray = new Tray(path.join(__dirname, 'tray-iconTemplate.png')); const contextMenu = Menu.buildFromTemplate([ { @@ -140,7 +175,7 @@ function createTray() { tray.setToolTip('New API'); tray.setContextMenu(contextMenu); - // On macOS, clicking the tray icon shows the menu + // On macOS, clicking the tray icon shows the window tray.on('click', () => { if (mainWindow === null) { createWindow(); @@ -153,41 +188,6 @@ function createTray() { }); } -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(); - } - - // Close to tray instead of quitting - mainWindow.on('close', (event) => { - if (!app.isQuitting) { - event.preventDefault(); - mainWindow.hide(); - if (process.platform === 'darwin') { - app.dock.hide(); - } - } - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); -} - app.whenReady().then(async () => { try { await startServer(); diff --git a/electron/tray-iconTemplate.png b/electron/tray-iconTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..187741e10d7ad49cae1e1624c3dfb5253355d15c GIT binary patch literal 459 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6jKx9jPK-BC>eK@{oCO|{#S9GG z!XV7ZFl&wkP_Qw;C&U#sR%fZ>$7zmYc*&kIant>eck|4iehX2gQcmEuz z{F=Cqqha|&@g1y&Q9jxs3q=B%)Lf^Ze_X8e;U;hDvn_g?JpUEm5dvyk4UX$+uyx4kfBSaIWU^~T@O3fm^-Kre+=ySF ztIN7`AEV68qdOf$gj<*MZ%*T%kkyu}&oDVnIL!Bma2BgTrd;A1okE#&RSFl@D1GU% z_B&L@y!aW1fUfGC726WB=3QBK{+fx4&*Zy3b6A)2uj2#x-P6_2Wt~$(69B(4fSCXQ literal 0 HcmV?d00001 diff --git a/electron/tray-iconTemplate@2x.png b/electron/tray-iconTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d5666a04e55b0233bb3c3981a361d1f61ea575e8 GIT binary patch literal 754 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv7~+#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWND7Z1eC&U#<(-tr?Qf@vs7dImtJ1-9(0~3?yBj?9J6Rk^v{DK+&OR<)| z@c6UW_#^+~Ut2k5J`mR9I#O^({^GUsYC#Th;(3R59yh-|afM>FwWe=t+Uu`f%O0zo zbTf6`a%|?!3}7Hl_H=O!shIP2%JoTy40v4Ms`xd2VN?8a&-(Vi|A()MO1#-%ZT-7L z&gk0Jjf*_W7%Fbs&svwcU*)n&dHZdVBlh_#_`UekvNvo}p7QkU@->g6FIclOTv6Eg zc|qLSGC?(`GfXGWeA4cIDOd1kwf{89m3iFz%O1O3*>V5TQ~94gb*}vMbBs2Q zW&ExqberRQQt>Xci8r^XOuV)>`mf@0_o7!vlo`M4P1sWLY8%UQlLUj~CMP!^4xTL} z6I6R#ra6!68IPUarl{su-p>{?-C4R#e)sXjpv{>g0!qyLYYdkrJuWnmnWx9mvpiTJ zqWo&XncKG(#x>5^6>|8&i3gPhza1FLLb#Xa&G?}A^|$(auBkkNHAU=&arfnkhefSI<14qid^b4TcP^#>`ia1e z6$Z(U>06Q>$?eJ#yS#4&i#4MrQ|P8A+Y8g&B{r+`$G_{6+F|;c$MqZ68sisJ_p0yb t4fNr;^!CD8Vb}Vr(k55u*4>={R(g)PNNv{CrnR7Svd$@?2>^`i{gwa# literal 0 HcmV?d00001 From 050e0221c75576f4e7ce0f84046d60748957d5c6 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 13:33:41 +0800 Subject: [PATCH 05/79] README --- electron/README.md | 167 +++++++++------------------------------------ 1 file changed, 34 insertions(+), 133 deletions(-) diff --git a/electron/README.md b/electron/README.md index 5122a1ba1a6a..88463b8aefd9 100644 --- a/electron/README.md +++ b/electron/README.md @@ -1,172 +1,73 @@ # 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. +This directory contains the Electron wrapper for New API, providing a native desktop application with system tray support for Windows, macOS, and Linux. -## Architecture +## Prerequisites -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 +### 1. Go Binary (Required) +The Electron app requires the compiled Go binary to function. You have two options: -## Development - -### Prerequisites - -1. Build the Go backend first: +**Option A: Use existing binary (without Go installed)** ```bash -cd .. -go build -o new-api +# If you have a pre-built binary (e.g., new-api-macos) +cp ../new-api-macos ../new-api ``` -2. Install Electron dependencies: +**Option B: Build from source (requires Go)** +TODO + +### 3. Electron Dependencies ```bash cd electron npm install ``` -### Running in Development Mode +## Development +Run the app 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 +- Open an Electron window with DevTools enabled +- Create a system tray icon (menu bar on macOS) +- Store database in `../data/new-api.db` ## Building for Production -### Quick Build (Current Platform) - -Use the provided build script: +### Quick Build ```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 -``` +# Ensure Go binary exists in parent directory +ls ../new-api # Should exist -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 +# Build for current platform 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 +# Platform-specific builds +npm run build:mac # Creates .dmg and .zip +npm run build:win # Creates .exe installer +npm run build:linux # Creates .AppImage and .deb ``` -Note: Building macOS apps requires macOS. Building Windows apps with code signing requires Windows. +### Build Output +- Built applications are in `electron/dist/` +- macOS: `.dmg` (installer) and `.zip` (portable) +- Windows: `.exe` (installer) and portable exe +- Linux: `.AppImage` and `.deb` ## Configuration ### Port - -The app uses port 3000 by default. To change: - -Edit `electron/main.js`: +Default port is 3000. To change, edit `main.js`: ```javascript -const PORT = 3000; // Change to your desired port +const PORT = 3000; // Change to desired port ``` -### Data Directory - -- **Development**: Uses `data/` in the project root -- **Production**: Uses Electron's `userData` directory: +### Database Location +- **Development**: `../data/new-api.db` (project directory) +- **Production**: - 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 From 7c7f9abd04762da9bf49541c57f56c0eb2f539c0 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Mon, 29 Sep 2025 13:41:17 +0800 Subject: [PATCH 06/79] icon --- electron/icon.png | Bin 0 -> 31262 bytes electron/package.json | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 electron/icon.png diff --git a/electron/icon.png b/electron/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c63ac2d77598af361f898c561ed40938719c8391 GIT binary patch literal 31262 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 Q5b%+eRFbIrXcGMY0IP Date: Mon, 29 Sep 2025 23:23:31 +0800 Subject: [PATCH 07/79] =?UTF-8?q?=F0=9F=92=B1=20feat(settings):=20introduc?= =?UTF-8?q?e=20site-wide=20quota=20display=20type=20(USD/CNY/TOKENS/CUSTOM?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe configuration `general_setting.quota_display_type`, and wire it through the backend and frontend. Backend - Add `QuotaDisplayType` to `operation_setting.GeneralSetting` with injected registration via `config.GlobalConfig.Register("general_setting", ...)`. Helpers: `IsCurrencyDisplay()`, `IsCNYDisplay()`, `GetQuotaDisplayType()`. - Expose `quota_display_type` in `/api/status` and keep legacy `display_in_currency` for backward compatibility. - Logger: update `LogQuota` and `FormatQuota` to support USD/CNY/TOKENS. When CNY is selected, convert using `operation_setting.USDExchangeRate`. - Controllers: - `billing`: compute subscription/usage amounts based on the selected type (USD: divide by `QuotaPerUnit`; CNY: USD→CNY; TOKENS: keep raw tokens). - `topup` / `topup_stripe`: treat inputs as “amount” for USD/CNY and as token-count for TOKENS; adjust min topup and pay money accordingly. - `misc`: include `quota_display_type` in status payload. - Compatibility: in `model/option.UpdateOption`, map updates to `DisplayInCurrencyEnabled` → `general_setting.quota_display_type` (true→USD, false→TOKENS). Keep exporting the legacy key in `OptionMap`. Frontend - Settings: replace the “display in currency” switch with a Select (`general_setting.quota_display_type`) offering USD / CNY / Tokens. Provide fallback mapping from legacy `DisplayInCurrencyEnabled`. - Persist `quota_display_type` to localStorage (keep `display_in_currency` for legacy components). - Rendering helpers: base all quota/price rendering on `quota_display_type`; use `usd_exchange_rate` for CNY symbol/values. - Pricing page: default view currency follows site display type (USD/CNY), while TOKENS mode still allows per-view currency toggling when needed. Notes - No database migrations required. - Legacy clients remain functional via compatibility fields. --- common/constants.go | 1 + common/database.go | 2 +- controller/billing.go | 24 +- controller/misc.go | 28 +- controller/setup.go | 2 +- controller/topup.go | 9 +- controller/topup_stripe.go | 4 +- logger/logger.go | 50 +- model/option.go | 10 +- relay/channel/ollama/adaptor.go | 30 +- relay/channel/ollama/dto.go | 45 +- relay/channel/ollama/relay-ollama.go | 194 ++++++-- relay/channel/ollama/stream.go | 436 ++++++++++-------- relay/channel/vertex/adaptor.go | 2 +- setting/operation_setting/general_setting.go | 72 ++- web/index.html | 2 +- web/jsconfig.json | 2 +- .../common/modals/TwoFactorAuthModal.jsx | 4 +- .../components/settings/OperationSetting.jsx | 2 +- web/src/components/settings/SystemSetting.jsx | 74 ++- .../personal/cards/AccountManagement.jsx | 9 +- .../channels/modals/EditChannelModal.jsx | 80 ++-- .../table/mj-logs/MjLogsFilters.jsx | 4 +- .../filter/PricingDisplaySettings.jsx | 1 + .../layout/header/SearchActions.jsx | 1 + .../table/task-logs/TaskLogsColumnDefs.jsx | 5 +- .../table/task-logs/TaskLogsFilters.jsx | 4 +- .../table/task-logs/modals/ContentModal.jsx | 78 ++-- .../table/usage-logs/UsageLogsFilters.jsx | 4 +- web/src/components/topup/RechargeCard.jsx | 82 +++- web/src/components/topup/index.jsx | 31 +- .../topup/modals/PaymentConfirmModal.jsx | 7 +- web/src/constants/console.constants.js | 10 +- web/src/helpers/api.js | 2 - web/src/helpers/data.js | 2 + web/src/helpers/render.jsx | 98 ++-- web/src/helpers/utils.jsx | 20 +- web/src/hooks/channels/useChannelsData.jsx | 179 +++++-- web/src/hooks/common/useSidebar.js | 7 +- .../model-pricing/useModelPricingData.jsx | 25 + web/src/i18n/locales/en.json | 5 +- web/src/i18n/locales/fr.json | 7 +- .../Setting/Operation/SettingsGeneral.jsx | 142 ++++-- .../Setting/Operation/SettingsMonitoring.jsx | 3 +- .../Payment/SettingsPaymentGateway.jsx | 42 +- .../Setting/Ratio/ModelRatioSettings.jsx | 28 +- 46 files changed, 1268 insertions(+), 601 deletions(-) diff --git a/common/constants.go b/common/constants.go index e6d59d101eed..2ef2b7df2f92 100644 --- a/common/constants.go +++ b/common/constants.go @@ -19,6 +19,7 @@ var TopUpLink = "" // var ChatLink = "" // var ChatLink2 = "" var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens +// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制 var DisplayInCurrencyEnabled = true var DisplayTokenStatEnabled = true var DrawingEnabled = true diff --git a/common/database.go b/common/database.go index 38a54d5e6f27..71dbd94d588c 100644 --- a/common/database.go +++ b/common/database.go @@ -12,4 +12,4 @@ var LogSqlType = DatabaseTypeSQLite // Default to SQLite for logging SQL queries var UsingMySQL = false var UsingClickHouse = false -var SQLitePath = "one-api.db?_busy_timeout=30000" \ No newline at end of file +var SQLitePath = "one-api.db?_busy_timeout=30000" diff --git a/controller/billing.go b/controller/billing.go index 1fb83633e4da..db3b62b1eafe 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -5,6 +5,7 @@ import ( "one-api/common" "one-api/dto" "one-api/model" + "one-api/setting/operation_setting" ) func GetSubscription(c *gin.Context) { @@ -39,8 +40,18 @@ func GetSubscription(c *gin.Context) { } quota := remainQuota + usedQuota amount := float64(quota) - if common.DisplayInCurrencyEnabled { - amount /= common.QuotaPerUnit + // OpenAI 兼容接口中的 *_USD 字段含义保持“额度单位”对应值: + // 我们将其解释为以“站点展示类型”为准: + // - USD: 直接除以 QuotaPerUnit + // - CNY: 先转 USD 再乘汇率 + // - TOKENS: 直接使用 tokens 数量 + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate + case operation_setting.QuotaDisplayTypeTokens: + // amount 保持 tokens 数值 + default: + amount = amount / common.QuotaPerUnit } if token != nil && token.UnlimitedQuota { amount = 100000000 @@ -80,8 +91,13 @@ func GetUsage(c *gin.Context) { return } amount := float64(quota) - if common.DisplayInCurrencyEnabled { - amount /= common.QuotaPerUnit + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + amount = amount / common.QuotaPerUnit * operation_setting.USDExchangeRate + case operation_setting.QuotaDisplayTypeTokens: + // tokens 保持原值 + default: + amount = amount / common.QuotaPerUnit } usage := OpenAIUsageResponse{ Object: "list", diff --git a/controller/misc.go b/controller/misc.go index 875142ffbf04..8d78e3358c26 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -64,18 +64,22 @@ func GetStatus(c *gin.Context) { "top_up_link": common.TopUpLink, "docs_link": operation_setting.GetGeneralSetting().DocsLink, "quota_per_unit": common.QuotaPerUnit, - "display_in_currency": common.DisplayInCurrencyEnabled, - "enable_batch_update": common.BatchUpdateEnabled, - "enable_drawing": common.DrawingEnabled, - "enable_task": common.TaskEnabled, - "enable_data_export": common.DataExportEnabled, - "data_export_default_time": common.DataExportDefaultTime, - "default_collapse_sidebar": common.DefaultCollapseSidebar, - "mj_notify_enabled": setting.MjNotifyEnabled, - "chats": setting.Chats, - "demo_site_enabled": operation_setting.DemoSiteEnabled, - "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, - "default_use_auto_group": setting.DefaultUseAutoGroup, + // 兼容旧前端:保留 display_in_currency,同时提供新的 quota_display_type + "display_in_currency": operation_setting.IsCurrencyDisplay(), + "quota_display_type": operation_setting.GetQuotaDisplayType(), + "custom_currency_symbol": operation_setting.GetGeneralSetting().CustomCurrencySymbol, + "custom_currency_exchange_rate": operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate, + "enable_batch_update": common.BatchUpdateEnabled, + "enable_drawing": common.DrawingEnabled, + "enable_task": common.TaskEnabled, + "enable_data_export": common.DataExportEnabled, + "data_export_default_time": common.DataExportDefaultTime, + "default_collapse_sidebar": common.DefaultCollapseSidebar, + "mj_notify_enabled": setting.MjNotifyEnabled, + "chats": setting.Chats, + "demo_site_enabled": operation_setting.DemoSiteEnabled, + "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, + "default_use_auto_group": setting.DefaultUseAutoGroup, "usd_exchange_rate": operation_setting.USDExchangeRate, "price": operation_setting.Price, diff --git a/controller/setup.go b/controller/setup.go index 3ae255e94b50..8b7fa3e7a405 100644 --- a/controller/setup.go +++ b/controller/setup.go @@ -178,4 +178,4 @@ func boolToString(b bool) string { return "true" } return "false" -} \ No newline at end of file +} diff --git a/controller/topup.go b/controller/topup.go index 243e67940d98..7e2cadf1d2a0 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -86,8 +86,9 @@ func GetEpayClient() *epay.Client { func getPayMoney(amount int64, group string) float64 { dAmount := decimal.NewFromInt(amount) - - if !common.DisplayInCurrencyEnabled { + // 充值金额以“展示类型”为准: + // - USD/CNY: 前端传 amount 为金额单位;TOKENS: 前端传 tokens,需要换成 USD 金额 + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) dAmount = dAmount.Div(dQuotaPerUnit) } @@ -115,7 +116,7 @@ func getPayMoney(amount int64, group string) float64 { func getMinTopup() int64 { minTopup := operation_setting.MinTopUp - if !common.DisplayInCurrencyEnabled { + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { dMinTopup := decimal.NewFromInt(int64(minTopup)) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart()) @@ -176,7 +177,7 @@ func RequestEpay(c *gin.Context) { return } amount := req.Amount - if !common.DisplayInCurrencyEnabled { + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { dAmount := decimal.NewFromInt(int64(amount)) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) amount = dAmount.Div(dQuotaPerUnit).IntPart() diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index 9a568d857e42..628a3fea58f9 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -258,7 +258,7 @@ func GetChargedAmount(count float64, user model.User) float64 { func getStripePayMoney(amount float64, group string) float64 { originalAmount := amount - if !common.DisplayInCurrencyEnabled { + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { amount = amount / common.QuotaPerUnit } // Using float64 for monetary calculations is acceptable here due to the small amounts involved @@ -279,7 +279,7 @@ func getStripePayMoney(amount float64, group string) float64 { func getStripeMinTopup() int64 { minTopup := setting.StripeMinTopUp - if !common.DisplayInCurrencyEnabled { + if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { minTopup = minTopup * int(common.QuotaPerUnit) } return int64(minTopup) diff --git a/logger/logger.go b/logger/logger.go index d59e51cb8de9..68f564370e9f 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -7,6 +7,7 @@ import ( "io" "log" "one-api/common" + "one-api/setting/operation_setting" "os" "path/filepath" "sync" @@ -92,18 +93,55 @@ func logHelper(ctx context.Context, level string, msg string) { } func LogQuota(quota int) string { - if common.DisplayInCurrencyEnabled { - return fmt.Sprintf("$%.6f 额度", float64(quota)/common.QuotaPerUnit) - } else { + // 新逻辑:根据额度展示类型输出 + q := float64(quota) + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + usd := q / common.QuotaPerUnit + cny := usd * operation_setting.USDExchangeRate + return fmt.Sprintf("¥%.6f 额度", cny) + case operation_setting.QuotaDisplayTypeCustom: + usd := q / common.QuotaPerUnit + rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate + symbol := operation_setting.GetGeneralSetting().CustomCurrencySymbol + if symbol == "" { + symbol = "¤" + } + if rate <= 0 { + rate = 1 + } + v := usd * rate + return fmt.Sprintf("%s%.6f 额度", symbol, v) + case operation_setting.QuotaDisplayTypeTokens: return fmt.Sprintf("%d 点额度", quota) + default: // USD + return fmt.Sprintf("$%.6f 额度", q/common.QuotaPerUnit) } } func FormatQuota(quota int) string { - if common.DisplayInCurrencyEnabled { - return fmt.Sprintf("$%.6f", float64(quota)/common.QuotaPerUnit) - } else { + q := float64(quota) + switch operation_setting.GetQuotaDisplayType() { + case operation_setting.QuotaDisplayTypeCNY: + usd := q / common.QuotaPerUnit + cny := usd * operation_setting.USDExchangeRate + return fmt.Sprintf("¥%.6f", cny) + case operation_setting.QuotaDisplayTypeCustom: + usd := q / common.QuotaPerUnit + rate := operation_setting.GetGeneralSetting().CustomCurrencyExchangeRate + symbol := operation_setting.GetGeneralSetting().CustomCurrencySymbol + if symbol == "" { + symbol = "¤" + } + if rate <= 0 { + rate = 1 + } + v := usd * rate + return fmt.Sprintf("%s%.6f", symbol, v) + case operation_setting.QuotaDisplayTypeTokens: return fmt.Sprintf("%d", quota) + default: + return fmt.Sprintf("$%.6f", q/common.QuotaPerUnit) } } diff --git a/model/option.go b/model/option.go index 9ace8fece1c7..77525ea25239 100644 --- a/model/option.go +++ b/model/option.go @@ -240,7 +240,15 @@ func updateOptionMap(key string, value string) (err error) { case "LogConsumeEnabled": common.LogConsumeEnabled = boolValue case "DisplayInCurrencyEnabled": - common.DisplayInCurrencyEnabled = boolValue + // 兼容旧字段:同步到新配置 general_setting.quota_display_type(运行时生效) + // true -> USD, false -> TOKENS + newVal := "USD" + if !boolValue { + newVal = "TOKENS" + } + if cfg := config.GlobalConfig.Get("general_setting"); cfg != nil { + _ = config.UpdateConfigFromMap(cfg, map[string]string{"quota_display_type": newVal}) + } case "DisplayTokenStatEnabled": common.DisplayTokenStatEnabled = boolValue case "DrawingEnabled": diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index bafe73b92bd1..d88e04d6feb6 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -18,7 +18,9 @@ import ( type Adaptor struct { } -func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) { + return nil, errors.New("not implemented") +} func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { openaiAdaptor := openai.Adaptor{} @@ -33,17 +35,25 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn return openAIChatToOllamaChat(c, openaiRequest.(*dto.GeneralOpenAIRequest)) } -func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + return nil, errors.New("not implemented") +} -func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + return nil, errors.New("not implemented") +} func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - if info.RelayMode == relayconstant.RelayModeEmbeddings { return info.ChannelBaseUrl + "/api/embed", nil } - if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { return info.ChannelBaseUrl + "/api/generate", nil } - return info.ChannelBaseUrl + "/api/chat", nil + if info.RelayMode == relayconstant.RelayModeEmbeddings { + return info.ChannelBaseUrl + "/api/embed", nil + } + if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { + return info.ChannelBaseUrl + "/api/generate", nil + } + return info.ChannelBaseUrl + "/api/chat", nil } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -53,7 +63,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { - if request == nil { return nil, errors.New("request is nil") } + if request == nil { + return nil, errors.New("request is nil") + } // decide generate or chat if strings.Contains(info.RequestURLPath, "/v1/completions") || info.RelayMode == relayconstant.RelayModeCompletions { return openAIToGenerate(c, request) @@ -69,7 +81,9 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return requestOpenAI2Embeddings(request), nil } -func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { return nil, errors.New("not implemented") } +func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) { + return nil, errors.New("not implemented") +} func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) diff --git a/relay/channel/ollama/dto.go b/relay/channel/ollama/dto.go index 45e49ab431b7..2434a4cbc08a 100644 --- a/relay/channel/ollama/dto.go +++ b/relay/channel/ollama/dto.go @@ -5,12 +5,12 @@ import ( ) type OllamaChatMessage struct { - Role string `json:"role"` - Content string `json:"content,omitempty"` - Images []string `json:"images,omitempty"` - ToolCalls []OllamaToolCall `json:"tool_calls,omitempty"` - ToolName string `json:"tool_name,omitempty"` - Thinking json.RawMessage `json:"thinking,omitempty"` + Role string `json:"role"` + Content string `json:"content,omitempty"` + Images []string `json:"images,omitempty"` + ToolCalls []OllamaToolCall `json:"tool_calls,omitempty"` + ToolName string `json:"tool_name,omitempty"` + Thinking json.RawMessage `json:"thinking,omitempty"` } type OllamaToolFunction struct { @@ -20,7 +20,7 @@ type OllamaToolFunction struct { } type OllamaTool struct { - Type string `json:"type"` + Type string `json:"type"` Function OllamaToolFunction `json:"function"` } @@ -43,28 +43,27 @@ type OllamaChatRequest struct { } type OllamaGenerateRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt,omitempty"` - Suffix string `json:"suffix,omitempty"` - Images []string `json:"images,omitempty"` - Format interface{} `json:"format,omitempty"` - Stream bool `json:"stream,omitempty"` - Options map[string]any `json:"options,omitempty"` - KeepAlive interface{} `json:"keep_alive,omitempty"` + Model string `json:"model"` + Prompt string `json:"prompt,omitempty"` + Suffix string `json:"suffix,omitempty"` + Images []string `json:"images,omitempty"` + Format interface{} `json:"format,omitempty"` + Stream bool `json:"stream,omitempty"` + Options map[string]any `json:"options,omitempty"` + KeepAlive interface{} `json:"keep_alive,omitempty"` Think json.RawMessage `json:"think,omitempty"` } type OllamaEmbeddingRequest struct { - Model string `json:"model"` - Input interface{} `json:"input"` - Options map[string]any `json:"options,omitempty"` + Model string `json:"model"` + Input interface{} `json:"input"` + Options map[string]any `json:"options,omitempty"` Dimensions int `json:"dimensions,omitempty"` } type OllamaEmbeddingResponse struct { - Error string `json:"error,omitempty"` - Model string `json:"model"` - Embeddings [][]float64 `json:"embeddings"` - PromptEvalCount int `json:"prompt_eval_count,omitempty"` + Error string `json:"error,omitempty"` + Model string `json:"model"` + Embeddings [][]float64 `json:"embeddings"` + PromptEvalCount int `json:"prompt_eval_count,omitempty"` } - diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go index 3b67f952521c..f94a654c74a5 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -35,13 +35,27 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam } // options mapping - if r.Temperature != nil { chatReq.Options["temperature"] = r.Temperature } - if r.TopP != 0 { chatReq.Options["top_p"] = r.TopP } - if r.TopK != 0 { chatReq.Options["top_k"] = r.TopK } - if r.FrequencyPenalty != 0 { chatReq.Options["frequency_penalty"] = r.FrequencyPenalty } - if r.PresencePenalty != 0 { chatReq.Options["presence_penalty"] = r.PresencePenalty } - if r.Seed != 0 { chatReq.Options["seed"] = int(r.Seed) } - if mt := r.GetMaxTokens(); mt != 0 { chatReq.Options["num_predict"] = int(mt) } + if r.Temperature != nil { + chatReq.Options["temperature"] = r.Temperature + } + if r.TopP != 0 { + chatReq.Options["top_p"] = r.TopP + } + if r.TopK != 0 { + chatReq.Options["top_k"] = r.TopK + } + if r.FrequencyPenalty != 0 { + chatReq.Options["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + chatReq.Options["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + chatReq.Options["seed"] = int(r.Seed) + } + if mt := r.GetMaxTokens(); mt != 0 { + chatReq.Options["num_predict"] = int(mt) + } if r.Stop != nil { switch v := r.Stop.(type) { @@ -50,21 +64,27 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam case []string: chatReq.Options["stop"] = v case []any: - arr := make([]string,0,len(v)) - for _, i := range v { if s,ok:=i.(string); ok { arr = append(arr,s) } } - if len(arr)>0 { chatReq.Options["stop"] = arr } + arr := make([]string, 0, len(v)) + for _, i := range v { + if s, ok := i.(string); ok { + arr = append(arr, s) + } + } + if len(arr) > 0 { + chatReq.Options["stop"] = arr + } } } if len(r.Tools) > 0 { - tools := make([]OllamaTool,0,len(r.Tools)) + tools := make([]OllamaTool, 0, len(r.Tools)) for _, t := range r.Tools { tools = append(tools, OllamaTool{Type: "function", Function: OllamaToolFunction{Name: t.Function.Name, Description: t.Function.Description, Parameters: t.Function.Parameters}}) } chatReq.Tools = tools } - chatReq.Messages = make([]OllamaChatMessage,0,len(r.Messages)) + chatReq.Messages = make([]OllamaChatMessage, 0, len(r.Messages)) for _, m := range r.Messages { var textBuilder strings.Builder var images []string @@ -79,14 +99,20 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam var base64Data string if strings.HasPrefix(img.Url, "http") { fileData, err := service.GetFileBase64FromUrl(c, img.Url, "fetch image for ollama chat") - if err != nil { return nil, err } + if err != nil { + return nil, err + } base64Data = fileData.Base64Data } else if strings.HasPrefix(img.Url, "data:") { - if idx := strings.Index(img.Url, ","); idx != -1 && idx+1 < len(img.Url) { base64Data = img.Url[idx+1:] } + if idx := strings.Index(img.Url, ","); idx != -1 && idx+1 < len(img.Url) { + base64Data = img.Url[idx+1:] + } } else { base64Data = img.Url } - if base64Data != "" { images = append(images, base64Data) } + if base64Data != "" { + images = append(images, base64Data) + } } } else if part.Type == dto.ContentTypeText { textBuilder.WriteString(part.Text) @@ -94,16 +120,24 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam } } cm := OllamaChatMessage{Role: m.Role, Content: textBuilder.String()} - if len(images)>0 { cm.Images = images } - if m.Role == "tool" && m.Name != nil { cm.ToolName = *m.Name } + if len(images) > 0 { + cm.Images = images + } + if m.Role == "tool" && m.Name != nil { + cm.ToolName = *m.Name + } if m.ToolCalls != nil && len(m.ToolCalls) > 0 { parsed := m.ParseToolCalls() if len(parsed) > 0 { - calls := make([]OllamaToolCall,0,len(parsed)) + calls := make([]OllamaToolCall, 0, len(parsed)) for _, tc := range parsed { var args interface{} - if tc.Function.Arguments != "" { _ = json.Unmarshal([]byte(tc.Function.Arguments), &args) } - if args==nil { args = map[string]any{} } + if tc.Function.Arguments != "" { + _ = json.Unmarshal([]byte(tc.Function.Arguments), &args) + } + if args == nil { + args = map[string]any{} + } oc := OllamaToolCall{} oc.Function.Name = tc.Function.Name oc.Function.Arguments = args @@ -132,28 +166,67 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener gen.Prompt = v case []any: var sb strings.Builder - for _, it := range v { if s,ok:=it.(string); ok { sb.WriteString(s) } } + for _, it := range v { + if s, ok := it.(string); ok { + sb.WriteString(s) + } + } gen.Prompt = sb.String() default: gen.Prompt = fmt.Sprintf("%v", r.Prompt) } } - if r.Suffix != nil { if s,ok:=r.Suffix.(string); ok { gen.Suffix = s } } + if r.Suffix != nil { + if s, ok := r.Suffix.(string); ok { + gen.Suffix = s + } + } if r.ResponseFormat != nil { - if r.ResponseFormat.Type == "json" { gen.Format = "json" } else if r.ResponseFormat.Type == "json_schema" { var schema any; _ = json.Unmarshal(r.ResponseFormat.JsonSchema,&schema); gen.Format=schema } - } - if r.Temperature != nil { gen.Options["temperature"] = r.Temperature } - if r.TopP != 0 { gen.Options["top_p"] = r.TopP } - if r.TopK != 0 { gen.Options["top_k"] = r.TopK } - if r.FrequencyPenalty != 0 { gen.Options["frequency_penalty"] = r.FrequencyPenalty } - if r.PresencePenalty != 0 { gen.Options["presence_penalty"] = r.PresencePenalty } - if r.Seed != 0 { gen.Options["seed"] = int(r.Seed) } - if mt := r.GetMaxTokens(); mt != 0 { gen.Options["num_predict"] = int(mt) } + if r.ResponseFormat.Type == "json" { + gen.Format = "json" + } else if r.ResponseFormat.Type == "json_schema" { + var schema any + _ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema) + gen.Format = schema + } + } + if r.Temperature != nil { + gen.Options["temperature"] = r.Temperature + } + if r.TopP != 0 { + gen.Options["top_p"] = r.TopP + } + if r.TopK != 0 { + gen.Options["top_k"] = r.TopK + } + if r.FrequencyPenalty != 0 { + gen.Options["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + gen.Options["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + gen.Options["seed"] = int(r.Seed) + } + if mt := r.GetMaxTokens(); mt != 0 { + gen.Options["num_predict"] = int(mt) + } if r.Stop != nil { switch v := r.Stop.(type) { - case string: gen.Options["stop"] = []string{v} - case []string: gen.Options["stop"] = v - case []any: arr:=make([]string,0,len(v)); for _,i:= range v { if s,ok:=i.(string); ok { arr=append(arr,s) } }; if len(arr)>0 { gen.Options["stop"]=arr } + case string: + gen.Options["stop"] = []string{v} + case []string: + gen.Options["stop"] = v + case []any: + arr := make([]string, 0, len(v)) + for _, i := range v { + if s, ok := i.(string); ok { + arr = append(arr, s) + } + } + if len(arr) > 0 { + gen.Options["stop"] = arr + } } } return gen, nil @@ -161,30 +234,51 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener func requestOpenAI2Embeddings(r dto.EmbeddingRequest) *OllamaEmbeddingRequest { opts := map[string]any{} - if r.Temperature != nil { opts["temperature"] = r.Temperature } - if r.TopP != 0 { opts["top_p"] = r.TopP } - if r.FrequencyPenalty != 0 { opts["frequency_penalty"] = r.FrequencyPenalty } - if r.PresencePenalty != 0 { opts["presence_penalty"] = r.PresencePenalty } - if r.Seed != 0 { opts["seed"] = int(r.Seed) } - if r.Dimensions != 0 { opts["dimensions"] = r.Dimensions } + if r.Temperature != nil { + opts["temperature"] = r.Temperature + } + if r.TopP != 0 { + opts["top_p"] = r.TopP + } + if r.FrequencyPenalty != 0 { + opts["frequency_penalty"] = r.FrequencyPenalty + } + if r.PresencePenalty != 0 { + opts["presence_penalty"] = r.PresencePenalty + } + if r.Seed != 0 { + opts["seed"] = int(r.Seed) + } + if r.Dimensions != 0 { + opts["dimensions"] = r.Dimensions + } input := r.ParseInput() - if len(input)==1 { return &OllamaEmbeddingRequest{Model:r.Model, Input: input[0], Options: opts, Dimensions:r.Dimensions} } - return &OllamaEmbeddingRequest{Model:r.Model, Input: input, Options: opts, Dimensions:r.Dimensions} + if len(input) == 1 { + return &OllamaEmbeddingRequest{Model: r.Model, Input: input[0], Options: opts, Dimensions: r.Dimensions} + } + return &OllamaEmbeddingRequest{Model: r.Model, Input: input, Options: opts, Dimensions: r.Dimensions} } func ollamaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { var oResp OllamaEmbeddingResponse body, err := io.ReadAll(resp.Body) - if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } service.CloseResponseBodyGracefully(resp) - if err = common.Unmarshal(body, &oResp); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - if oResp.Error != "" { return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", oResp.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - data := make([]dto.OpenAIEmbeddingResponseItem,0,len(oResp.Embeddings)) - for i, emb := range oResp.Embeddings { data = append(data, dto.OpenAIEmbeddingResponseItem{Index:i,Object:"embedding",Embedding:emb}) } - usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens:0, TotalTokens: oResp.PromptEvalCount} - embResp := &dto.OpenAIEmbeddingResponse{Object:"list", Data:data, Model: info.UpstreamModelName, Usage:*usage} + if err = common.Unmarshal(body, &oResp); err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if oResp.Error != "" { + return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", oResp.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + data := make([]dto.OpenAIEmbeddingResponseItem, 0, len(oResp.Embeddings)) + for i, emb := range oResp.Embeddings { + data = append(data, dto.OpenAIEmbeddingResponseItem{Index: i, Object: "embedding", Embedding: emb}) + } + usage := &dto.Usage{PromptTokens: oResp.PromptEvalCount, CompletionTokens: 0, TotalTokens: oResp.PromptEvalCount} + embResp := &dto.OpenAIEmbeddingResponse{Object: "list", Data: data, Model: info.UpstreamModelName, Usage: *usage} out, _ := common.Marshal(embResp) service.IOCopyBytesGracefully(c, resp, out) return usage, nil } - diff --git a/relay/channel/ollama/stream.go b/relay/channel/ollama/stream.go index 964f11d90a78..9a98f7b69960 100644 --- a/relay/channel/ollama/stream.go +++ b/relay/channel/ollama/stream.go @@ -1,210 +1,278 @@ package ollama import ( - "bufio" - "encoding/json" - "fmt" - "io" - "net/http" - "one-api/common" - "one-api/dto" - "one-api/logger" - relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" - "one-api/types" - "strings" - "time" + "bufio" + "encoding/json" + "fmt" + "io" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/logger" + relaycommon "one-api/relay/common" + "one-api/relay/helper" + "one-api/service" + "one-api/types" + "strings" + "time" - "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin" ) type ollamaChatStreamChunk struct { - Model string `json:"model"` - CreatedAt string `json:"created_at"` - // chat - Message *struct { - Role string `json:"role"` - Content string `json:"content"` - Thinking json.RawMessage `json:"thinking"` - ToolCalls []struct { - Function struct { - Name string `json:"name"` - Arguments interface{} `json:"arguments"` - } `json:"function"` - } `json:"tool_calls"` - } `json:"message"` - // generate - Response string `json:"response"` - Done bool `json:"done"` - DoneReason string `json:"done_reason"` - TotalDuration int64 `json:"total_duration"` - LoadDuration int64 `json:"load_duration"` - PromptEvalCount int `json:"prompt_eval_count"` - EvalCount int `json:"eval_count"` - PromptEvalDuration int64 `json:"prompt_eval_duration"` - EvalDuration int64 `json:"eval_duration"` + Model string `json:"model"` + CreatedAt string `json:"created_at"` + // chat + Message *struct { + Role string `json:"role"` + Content string `json:"content"` + Thinking json.RawMessage `json:"thinking"` + ToolCalls []struct { + Function struct { + Name string `json:"name"` + Arguments interface{} `json:"arguments"` + } `json:"function"` + } `json:"tool_calls"` + } `json:"message"` + // generate + Response string `json:"response"` + Done bool `json:"done"` + DoneReason string `json:"done_reason"` + TotalDuration int64 `json:"total_duration"` + LoadDuration int64 `json:"load_duration"` + PromptEvalCount int `json:"prompt_eval_count"` + EvalCount int `json:"eval_count"` + PromptEvalDuration int64 `json:"prompt_eval_duration"` + EvalDuration int64 `json:"eval_duration"` } func toUnix(ts string) int64 { - if ts == "" { return time.Now().Unix() } - // try time.RFC3339 or with nanoseconds - t, err := time.Parse(time.RFC3339Nano, ts) - if err != nil { t2, err2 := time.Parse(time.RFC3339, ts); if err2==nil { return t2.Unix() }; return time.Now().Unix() } - return t.Unix() + if ts == "" { + return time.Now().Unix() + } + // try time.RFC3339 or with nanoseconds + t, err := time.Parse(time.RFC3339Nano, ts) + if err != nil { + t2, err2 := time.Parse(time.RFC3339, ts) + if err2 == nil { + return t2.Unix() + } + return time.Now().Unix() + } + return t.Unix() } func ollamaStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { - if resp == nil || resp.Body == nil { return nil, types.NewOpenAIError(fmt.Errorf("empty response"), types.ErrorCodeBadResponse, http.StatusBadRequest) } - defer service.CloseResponseBodyGracefully(resp) + if resp == nil || resp.Body == nil { + return nil, types.NewOpenAIError(fmt.Errorf("empty response"), types.ErrorCodeBadResponse, http.StatusBadRequest) + } + defer service.CloseResponseBodyGracefully(resp) - helper.SetEventStreamHeaders(c) - scanner := bufio.NewScanner(resp.Body) - usage := &dto.Usage{} - var model = info.UpstreamModelName - var responseId = common.GetUUID() - var created = time.Now().Unix() - var toolCallIndex int - start := helper.GenerateStartEmptyResponse(responseId, created, model, nil) - if data, err := common.Marshal(start); err == nil { _ = helper.StringData(c, string(data)) } + helper.SetEventStreamHeaders(c) + scanner := bufio.NewScanner(resp.Body) + usage := &dto.Usage{} + var model = info.UpstreamModelName + var responseId = common.GetUUID() + var created = time.Now().Unix() + var toolCallIndex int + start := helper.GenerateStartEmptyResponse(responseId, created, model, nil) + if data, err := common.Marshal(start); err == nil { + _ = helper.StringData(c, string(data)) + } - for scanner.Scan() { - line := scanner.Text() - line = strings.TrimSpace(line) - if line == "" { continue } - var chunk ollamaChatStreamChunk - if err := json.Unmarshal([]byte(line), &chunk); err != nil { - logger.LogError(c, "ollama stream json decode error: "+err.Error()+" line="+line) - return usage, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) - } - if chunk.Model != "" { model = chunk.Model } - created = toUnix(chunk.CreatedAt) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + if line == "" { + continue + } + var chunk ollamaChatStreamChunk + if err := json.Unmarshal([]byte(line), &chunk); err != nil { + logger.LogError(c, "ollama stream json decode error: "+err.Error()+" line="+line) + return usage, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if chunk.Model != "" { + model = chunk.Model + } + created = toUnix(chunk.CreatedAt) - if !chunk.Done { - // delta content - var content string - if chunk.Message != nil { content = chunk.Message.Content } else { content = chunk.Response } - delta := dto.ChatCompletionsStreamResponse{ - Id: responseId, - Object: "chat.completion.chunk", - Created: created, - Model: model, - Choices: []dto.ChatCompletionsStreamResponseChoice{ { - Index: 0, - Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ Role: "assistant" }, - } }, - } - if content != "" { delta.Choices[0].Delta.SetContentString(content) } - if chunk.Message != nil && len(chunk.Message.Thinking) > 0 { - raw := strings.TrimSpace(string(chunk.Message.Thinking)) - if raw != "" && raw != "null" { delta.Choices[0].Delta.SetReasoningContent(raw) } - } - // tool calls - if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 { - delta.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse,0,len(chunk.Message.ToolCalls)) - for _, tc := range chunk.Message.ToolCalls { - // arguments -> string - argBytes, _ := json.Marshal(tc.Function.Arguments) - toolId := fmt.Sprintf("call_%d", toolCallIndex) - tr := dto.ToolCallResponse{ID:toolId, Type:"function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}} - tr.SetIndex(toolCallIndex) - toolCallIndex++ - delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr) - } - } - if data, err := common.Marshal(delta); err == nil { _ = helper.StringData(c, string(data)) } - continue - } - // done frame - // finalize once and break loop - usage.PromptTokens = chunk.PromptEvalCount - usage.CompletionTokens = chunk.EvalCount - usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens - finishReason := chunk.DoneReason - if finishReason == "" { finishReason = "stop" } - // emit stop delta - if stop := helper.GenerateStopResponse(responseId, created, model, finishReason); stop != nil { - if data, err := common.Marshal(stop); err == nil { _ = helper.StringData(c, string(data)) } - } - // emit usage frame - if final := helper.GenerateFinalUsageResponse(responseId, created, model, *usage); final != nil { - if data, err := common.Marshal(final); err == nil { _ = helper.StringData(c, string(data)) } - } - // send [DONE] - helper.Done(c) - break - } - if err := scanner.Err(); err != nil && err != io.EOF { logger.LogError(c, "ollama stream scan error: "+err.Error()) } - return usage, nil + if !chunk.Done { + // delta content + var content string + if chunk.Message != nil { + content = chunk.Message.Content + } else { + content = chunk.Response + } + delta := dto.ChatCompletionsStreamResponse{ + Id: responseId, + Object: "chat.completion.chunk", + Created: created, + Model: model, + Choices: []dto.ChatCompletionsStreamResponseChoice{{ + Index: 0, + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{Role: "assistant"}, + }}, + } + if content != "" { + delta.Choices[0].Delta.SetContentString(content) + } + if chunk.Message != nil && len(chunk.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(chunk.Message.Thinking)) + if raw != "" && raw != "null" { + delta.Choices[0].Delta.SetReasoningContent(raw) + } + } + // tool calls + if chunk.Message != nil && len(chunk.Message.ToolCalls) > 0 { + delta.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 0, len(chunk.Message.ToolCalls)) + for _, tc := range chunk.Message.ToolCalls { + // arguments -> string + argBytes, _ := json.Marshal(tc.Function.Arguments) + toolId := fmt.Sprintf("call_%d", toolCallIndex) + tr := dto.ToolCallResponse{ID: toolId, Type: "function", Function: dto.FunctionResponse{Name: tc.Function.Name, Arguments: string(argBytes)}} + tr.SetIndex(toolCallIndex) + toolCallIndex++ + delta.Choices[0].Delta.ToolCalls = append(delta.Choices[0].Delta.ToolCalls, tr) + } + } + if data, err := common.Marshal(delta); err == nil { + _ = helper.StringData(c, string(data)) + } + continue + } + // done frame + // finalize once and break loop + usage.PromptTokens = chunk.PromptEvalCount + usage.CompletionTokens = chunk.EvalCount + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + finishReason := chunk.DoneReason + if finishReason == "" { + finishReason = "stop" + } + // emit stop delta + if stop := helper.GenerateStopResponse(responseId, created, model, finishReason); stop != nil { + if data, err := common.Marshal(stop); err == nil { + _ = helper.StringData(c, string(data)) + } + } + // emit usage frame + if final := helper.GenerateFinalUsageResponse(responseId, created, model, *usage); final != nil { + if data, err := common.Marshal(final); err == nil { + _ = helper.StringData(c, string(data)) + } + } + // send [DONE] + helper.Done(c) + break + } + if err := scanner.Err(); err != nil && err != io.EOF { + logger.LogError(c, "ollama stream scan error: "+err.Error()) + } + return usage, nil } // non-stream handler for chat/generate func ollamaChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { - body, err := io.ReadAll(resp.Body) - if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError) } - service.CloseResponseBodyGracefully(resp) - raw := string(body) - if common.DebugEnabled { println("ollama non-stream raw resp:", raw) } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeReadResponseBodyFailed, http.StatusInternalServerError) + } + service.CloseResponseBodyGracefully(resp) + raw := string(body) + if common.DebugEnabled { + println("ollama non-stream raw resp:", raw) + } - lines := strings.Split(raw, "\n") - var ( - aggContent strings.Builder - reasoningBuilder strings.Builder - lastChunk ollamaChatStreamChunk - parsedAny bool - ) - for _, ln := range lines { - ln = strings.TrimSpace(ln) - if ln == "" { continue } - var ck ollamaChatStreamChunk - if err := json.Unmarshal([]byte(ln), &ck); err != nil { - if len(lines) == 1 { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - continue - } - parsedAny = true - lastChunk = ck - if ck.Message != nil && len(ck.Message.Thinking) > 0 { - raw := strings.TrimSpace(string(ck.Message.Thinking)) - if raw != "" && raw != "null" { reasoningBuilder.WriteString(raw) } - } - if ck.Message != nil && ck.Message.Content != "" { aggContent.WriteString(ck.Message.Content) } else if ck.Response != "" { aggContent.WriteString(ck.Response) } - } + lines := strings.Split(raw, "\n") + var ( + aggContent strings.Builder + reasoningBuilder strings.Builder + lastChunk ollamaChatStreamChunk + parsedAny bool + ) + for _, ln := range lines { + ln = strings.TrimSpace(ln) + if ln == "" { + continue + } + var ck ollamaChatStreamChunk + if err := json.Unmarshal([]byte(ln), &ck); err != nil { + if len(lines) == 1 { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + continue + } + parsedAny = true + lastChunk = ck + if ck.Message != nil && len(ck.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(ck.Message.Thinking)) + if raw != "" && raw != "null" { + reasoningBuilder.WriteString(raw) + } + } + if ck.Message != nil && ck.Message.Content != "" { + aggContent.WriteString(ck.Message.Content) + } else if ck.Response != "" { + aggContent.WriteString(ck.Response) + } + } - if !parsedAny { - var single ollamaChatStreamChunk - if err := json.Unmarshal(body, &single); err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } - lastChunk = single - if single.Message != nil { - if len(single.Message.Thinking) > 0 { raw := strings.TrimSpace(string(single.Message.Thinking)); if raw != "" && raw != "null" { reasoningBuilder.WriteString(raw) } } - aggContent.WriteString(single.Message.Content) - } else { aggContent.WriteString(single.Response) } - } + if !parsedAny { + var single ollamaChatStreamChunk + if err := json.Unmarshal(body, &single); err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + lastChunk = single + if single.Message != nil { + if len(single.Message.Thinking) > 0 { + raw := strings.TrimSpace(string(single.Message.Thinking)) + if raw != "" && raw != "null" { + reasoningBuilder.WriteString(raw) + } + } + aggContent.WriteString(single.Message.Content) + } else { + aggContent.WriteString(single.Response) + } + } - model := lastChunk.Model - if model == "" { model = info.UpstreamModelName } - created := toUnix(lastChunk.CreatedAt) - usage := &dto.Usage{PromptTokens: lastChunk.PromptEvalCount, CompletionTokens: lastChunk.EvalCount, TotalTokens: lastChunk.PromptEvalCount + lastChunk.EvalCount} - content := aggContent.String() - finishReason := lastChunk.DoneReason - if finishReason == "" { finishReason = "stop" } + model := lastChunk.Model + if model == "" { + model = info.UpstreamModelName + } + created := toUnix(lastChunk.CreatedAt) + usage := &dto.Usage{PromptTokens: lastChunk.PromptEvalCount, CompletionTokens: lastChunk.EvalCount, TotalTokens: lastChunk.PromptEvalCount + lastChunk.EvalCount} + content := aggContent.String() + finishReason := lastChunk.DoneReason + if finishReason == "" { + finishReason = "stop" + } - msg := dto.Message{Role: "assistant", Content: contentPtr(content)} - if rc := reasoningBuilder.String(); rc != "" { msg.ReasoningContent = rc } - full := dto.OpenAITextResponse{ - Id: common.GetUUID(), - Model: model, - Object: "chat.completion", - Created: created, - Choices: []dto.OpenAITextResponseChoice{ { - Index: 0, - Message: msg, - FinishReason: finishReason, - } }, - Usage: *usage, - } - out, _ := common.Marshal(full) - service.IOCopyBytesGracefully(c, resp, out) - return usage, nil + msg := dto.Message{Role: "assistant", Content: contentPtr(content)} + if rc := reasoningBuilder.String(); rc != "" { + msg.ReasoningContent = rc + } + full := dto.OpenAITextResponse{ + Id: common.GetUUID(), + Model: model, + Object: "chat.completion", + Created: created, + Choices: []dto.OpenAITextResponseChoice{{ + Index: 0, + Message: msg, + FinishReason: finishReason, + }}, + Usage: *usage, + } + out, _ := common.Marshal(full) + service.IOCopyBytesGracefully(c, resp, out) + return usage, nil } -func contentPtr(s string) *string { if s=="" { return nil }; return &s } +func contentPtr(s string) *string { + if s == "" { + return nil + } + return &s +} diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index a424cb1a4610..834ed45db67c 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -187,7 +187,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } req.Set("Authorization", "Bearer "+accessToken) } - if a.AccountCredentials.ProjectID != "" { + if a.AccountCredentials.ProjectID != "" { req.Set("x-goog-user-project", a.AccountCredentials.ProjectID) } return nil diff --git a/setting/operation_setting/general_setting.go b/setting/operation_setting/general_setting.go index ae0c436ecefd..c47ff64faadc 100644 --- a/setting/operation_setting/general_setting.go +++ b/setting/operation_setting/general_setting.go @@ -2,17 +2,34 @@ package operation_setting import "one-api/setting/config" +// 额度展示类型 +const ( + QuotaDisplayTypeUSD = "USD" + QuotaDisplayTypeCNY = "CNY" + QuotaDisplayTypeTokens = "TOKENS" + QuotaDisplayTypeCustom = "CUSTOM" +) + type GeneralSetting struct { DocsLink string `json:"docs_link"` PingIntervalEnabled bool `json:"ping_interval_enabled"` PingIntervalSeconds int `json:"ping_interval_seconds"` + // 当前站点额度展示类型:USD / CNY / TOKENS + QuotaDisplayType string `json:"quota_display_type"` + // 自定义货币符号,用于 CUSTOM 展示类型 + CustomCurrencySymbol string `json:"custom_currency_symbol"` + // 自定义货币与美元汇率(1 USD = X Custom) + CustomCurrencyExchangeRate float64 `json:"custom_currency_exchange_rate"` } // 默认配置 var generalSetting = GeneralSetting{ - DocsLink: "https://docs.newapi.pro", - PingIntervalEnabled: false, - PingIntervalSeconds: 60, + DocsLink: "https://docs.newapi.pro", + PingIntervalEnabled: false, + PingIntervalSeconds: 60, + QuotaDisplayType: QuotaDisplayTypeUSD, + CustomCurrencySymbol: "¤", + CustomCurrencyExchangeRate: 1.0, } func init() { @@ -23,3 +40,52 @@ func init() { func GetGeneralSetting() *GeneralSetting { return &generalSetting } + +// IsCurrencyDisplay 是否以货币形式展示(美元或人民币) +func IsCurrencyDisplay() bool { + return generalSetting.QuotaDisplayType != QuotaDisplayTypeTokens +} + +// IsCNYDisplay 是否以人民币展示 +func IsCNYDisplay() bool { + return generalSetting.QuotaDisplayType == QuotaDisplayTypeCNY +} + +// GetQuotaDisplayType 返回额度展示类型 +func GetQuotaDisplayType() string { + return generalSetting.QuotaDisplayType +} + +// GetCurrencySymbol 返回当前展示类型对应符号 +func GetCurrencySymbol() string { + switch generalSetting.QuotaDisplayType { + case QuotaDisplayTypeUSD: + return "$" + case QuotaDisplayTypeCNY: + return "¥" + case QuotaDisplayTypeCustom: + if generalSetting.CustomCurrencySymbol != "" { + return generalSetting.CustomCurrencySymbol + } + return "¤" + default: + return "" + } +} + +// GetUsdToCurrencyRate 返回 1 USD = X 的 X(TOKENS 不适用) +func GetUsdToCurrencyRate(usdToCny float64) float64 { + switch generalSetting.QuotaDisplayType { + case QuotaDisplayTypeUSD: + return 1 + case QuotaDisplayTypeCNY: + return usdToCny + case QuotaDisplayTypeCustom: + if generalSetting.CustomCurrencyExchangeRate > 0 { + return generalSetting.CustomCurrencyExchangeRate + } + return 1 + default: + return 1 + } +} diff --git a/web/index.html b/web/index.html index df6b0e398900..a9df87f5d335 100644 --- a/web/index.html +++ b/web/index.html @@ -10,7 +10,7 @@ content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> New API - + diff --git a/web/jsconfig.json b/web/jsconfig.json index ced4d054344a..170a7cb4cb56 100644 --- a/web/jsconfig.json +++ b/web/jsconfig.json @@ -6,4 +6,4 @@ } }, "include": ["src/**/*"] -} \ No newline at end of file +} diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx index 2a9a8b25b022..082e63d797c4 100644 --- a/web/src/components/common/modals/TwoFactorAuthModal.jsx +++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx @@ -135,7 +135,9 @@ const TwoFactorAuthModal = ({ autoFocus /> - {t('支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。')} + {t( + '支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。', + )} diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx index 0d6e441071e5..51cdb1de6630 100644 --- a/web/src/components/settings/OperationSetting.jsx +++ b/web/src/components/settings/OperationSetting.jsx @@ -42,7 +42,7 @@ const OperationSetting = () => { QuotaPerUnit: 0, USDExchangeRate: 0, RetryTimes: 0, - DisplayInCurrencyEnabled: false, + 'general_setting.quota_display_type': 'USD', DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, DemoSiteEnabled: false, diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index f9a2c019d112..6a567d227589 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -45,7 +45,6 @@ import { useTranslation } from 'react-i18next'; const SystemSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ - PasswordLoginEnabled: '', PasswordRegisterEnabled: '', EmailVerificationEnabled: '', @@ -188,7 +187,9 @@ const SystemSetting = () => { setInputs(newInputs); setOriginInputs(newInputs); // 同步模式布尔到本地状态 - if (typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined') { + if ( + typeof newInputs['fetch_setting.domain_filter_mode'] !== 'undefined' + ) { setDomainFilterMode(!!newInputs['fetch_setting.domain_filter_mode']); } if (typeof newInputs['fetch_setting.ip_filter_mode'] !== 'undefined') { @@ -695,14 +696,17 @@ const SystemSetting = () => { noLabel extraText={t('SSRF防护开关详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.enable_ssrf_protection', e) + handleCheckboxChange( + 'fetch_setting.enable_ssrf_protection', + e, + ) } > {t('启用SSRF防护(推荐开启以保护服务器安全)')} - + { noLabel extraText={t('私有IP访问详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.allow_private_ip', e) + handleCheckboxChange( + 'fetch_setting.allow_private_ip', + e, + ) } > - {t('允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)')} + {t( + '允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)', + )} - + { noLabel extraText={t('域名IP过滤详细说明')} onChange={(e) => - handleCheckboxChange('fetch_setting.apply_ip_filter_for_domain', e) + handleCheckboxChange( + 'fetch_setting.apply_ip_filter_for_domain', + e, + ) } style={{ marginBottom: 8 }} > @@ -740,17 +752,23 @@ const SystemSetting = () => { {t(domainFilterMode ? '域名白名单' : '域名黑名单')} - - {t('支持通配符格式,如:example.com, *.api.example.com')} + + {t( + '支持通配符格式,如:example.com, *.api.example.com', + )} { - const selected = val && val.target ? val.target.value : val; + const selected = + val && val.target ? val.target.value : val; const isWhitelist = selected === 'whitelist'; setDomainFilterMode(isWhitelist); - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, 'fetch_setting.domain_filter_mode': isWhitelist, })); @@ -765,9 +783,9 @@ const SystemSetting = () => { onChange={(value) => { setDomainList(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.domain_list': value + 'fetch_setting.domain_list': value, })); }} placeholder={t('输入域名后回车,如:example.com')} @@ -784,17 +802,21 @@ const SystemSetting = () => { {t(ipFilterMode ? 'IP白名单' : 'IP黑名单')} - + {t('支持CIDR格式,如:8.8.8.8, 192.168.1.0/24')} { - const selected = val && val.target ? val.target.value : val; + const selected = + val && val.target ? val.target.value : val; const isWhitelist = selected === 'whitelist'; setIpFilterMode(isWhitelist); - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, 'fetch_setting.ip_filter_mode': isWhitelist, })); @@ -809,9 +831,9 @@ const SystemSetting = () => { onChange={(value) => { setIpList(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.ip_list': value + 'fetch_setting.ip_list': value, })); }} placeholder={t('输入IP地址后回车,如:8.8.8.8')} @@ -826,7 +848,10 @@ const SystemSetting = () => { > {t('允许的端口')} - + {t('支持单个端口和端口范围,如:80, 443, 8000-8999')} { onChange={(value) => { setAllowedPorts(value); // 触发Form的onChange事件 - setInputs(prev => ({ + setInputs((prev) => ({ ...prev, - 'fetch_setting.allowed_ports': value + 'fetch_setting.allowed_ports': value, })); }} placeholder={t('输入端口后回车,如:80 或 8000-8999')} style={{ width: '100%' }} /> - + {t('端口配置详细说明')} diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index 017e7c1e61f1..01f2fcdd39a3 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -85,7 +85,8 @@ const AccountManagement = ({ ); }; const isBound = (accountId) => Boolean(accountId); - const [showTelegramBindModal, setShowTelegramBindModal] = React.useState(false); + const [showTelegramBindModal, setShowTelegramBindModal] = + React.useState(false); return ( @@ -226,7 +227,8 @@ const AccountManagement = ({ onGitHubOAuthClicked(status.github_client_id) } disabled={ - isBound(userState.user?.github_id) || !status.github_oauth + isBound(userState.user?.github_id) || + !status.github_oauth } > {status.github_oauth ? t('绑定') : t('未启用')} @@ -384,7 +386,8 @@ const AccountManagement = ({ onLinuxDOOAuthClicked(status.linuxdo_client_id) } disabled={ - isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth + isBound(userState.user?.linux_do_id) || + !status.linuxdo_oauth } > {status.linuxdo_oauth ? t('绑定') : t('未启用')} diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 6f5f665b4b22..841373889969 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -87,23 +87,7 @@ const REGION_EXAMPLE = { // 支持并且已适配通过接口获取模型列表的渠道类型 const MODEL_FETCHABLE_TYPES = new Set([ - 1, - 4, - 14, - 34, - 17, - 26, - 24, - 47, - 25, - 20, - 23, - 31, - 35, - 40, - 42, - 48, - 43, + 1, 4, 14, 34, 17, 26, 24, 47, 25, 20, 23, 31, 35, 40, 42, 48, 43, ]); function type2secretPrompt(type) { @@ -348,7 +332,10 @@ const EditChannelModal = (props) => { break; case 45: localModels = getChannelModels(value); - setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' })); + setInputs((prevInputs) => ({ + ...prevInputs, + base_url: 'https://ark.cn-beijing.volces.com', + })); break; default: localModels = getChannelModels(value); @@ -442,7 +429,8 @@ const EditChannelModal = (props) => { // 读取 Vertex 密钥格式 data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; // 读取企业账户设置 - data.is_enterprise_account = parsedSettings.openrouter_enterprise === true; + data.is_enterprise_account = + parsedSettings.openrouter_enterprise === true; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; @@ -868,7 +856,10 @@ const EditChannelModal = (props) => { showInfo(t('请至少选择一个模型!')); return; } - if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) { + if ( + localInputs.type === 45 && + (!localInputs.base_url || localInputs.base_url.trim() === '') + ) { showInfo(t('请输入API地址!')); return; } @@ -912,7 +903,8 @@ const EditChannelModal = (props) => { } } // 设置企业账户标识,无论是true还是false都要传到后端 - settings.openrouter_enterprise = localInputs.is_enterprise_account === true; + settings.openrouter_enterprise = + localInputs.is_enterprise_account === true; localInputs.settings = JSON.stringify(settings); } @@ -1318,7 +1310,9 @@ const EditChannelModal = (props) => { setIsEnterpriseAccount(value); handleInputChange('is_enterprise_account', value); }} - extraText={t('企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选')} + extraText={t( + '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', + )} initValue={inputs.is_enterprise_account} /> )} @@ -1944,27 +1938,27 @@ const EditChannelModal = (props) => { )} {inputs.type === 45 && ( -
- - handleInputChange('base_url', value) - } - optionList={[ - { - value: 'https://ark.cn-beijing.volces.com', - label: 'https://ark.cn-beijing.volces.com' - }, - { - value: 'https://ark.ap-southeast.bytepluses.com', - label: 'https://ark.ap-southeast.bytepluses.com' - } - ]} - defaultValue='https://ark.cn-beijing.volces.com' - /> -
+
+ + handleInputChange('base_url', value) + } + optionList={[ + { + value: 'https://ark.cn-beijing.volces.com', + label: 'https://ark.cn-beijing.volces.com', + }, + { + value: 'https://ark.ap-southeast.bytepluses.com', + label: 'https://ark.ap-southeast.bytepluses.com', + }, + ]} + defaultValue='https://ark.cn-beijing.volces.com' + /> +
)}
)} diff --git a/web/src/components/table/mj-logs/MjLogsFilters.jsx b/web/src/components/table/mj-logs/MjLogsFilters.jsx index 6db96e791cf8..7c61454e0d59 100644 --- a/web/src/components/table/mj-logs/MjLogsFilters.jsx +++ b/web/src/components/table/mj-logs/MjLogsFilters.jsx @@ -56,10 +56,10 @@ const MjLogsFilters = ({ showClear pure size='small' - presets={DATE_RANGE_PRESETS.map(preset => ({ + presets={DATE_RANGE_PRESETS.map((preset) => ({ text: t(preset.text), start: preset.start(), - end: preset.end() + end: preset.end(), }))} /> diff --git a/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx b/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx index 4423ce39cd17..71dbd200054b 100644 --- a/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx +++ b/web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx @@ -56,6 +56,7 @@ const PricingDisplaySettings = ({ const currencyItems = [ { value: 'USD', label: 'USD ($)' }, { value: 'CNY', label: 'CNY (¥)' }, + { value: 'CUSTOM', label: t('自定义货币') }, ]; const handleChange = (value) => { diff --git a/web/src/components/table/model-pricing/layout/header/SearchActions.jsx b/web/src/components/table/model-pricing/layout/header/SearchActions.jsx index 646ffe023165..c961b8dc1e70 100644 --- a/web/src/components/table/model-pricing/layout/header/SearchActions.jsx +++ b/web/src/components/table/model-pricing/layout/header/SearchActions.jsx @@ -107,6 +107,7 @@ const SearchActions = memo( optionList={[ { value: 'USD', label: 'USD' }, { value: 'CNY', label: 'CNY' }, + { value: 'CUSTOM', label: t('自定义货币') }, ]} /> )} diff --git a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx index b63c7dd4fb8a..1f097b2b7c2c 100644 --- a/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx +++ b/web/src/components/table/task-logs/TaskLogsColumnDefs.jsx @@ -36,8 +36,9 @@ import { } from 'lucide-react'; import { TASK_ACTION_FIRST_TAIL_GENERATE, - TASK_ACTION_GENERATE, TASK_ACTION_REFERENCE_GENERATE, - TASK_ACTION_TEXT_GENERATE + TASK_ACTION_GENERATE, + TASK_ACTION_REFERENCE_GENERATE, + TASK_ACTION_TEXT_GENERATE, } from '../../../constants/common.constant'; import { CHANNEL_OPTIONS } from '../../../constants/channel.constants'; diff --git a/web/src/components/table/task-logs/TaskLogsFilters.jsx b/web/src/components/table/task-logs/TaskLogsFilters.jsx index e27cea867c8c..3bfae77a4a45 100644 --- a/web/src/components/table/task-logs/TaskLogsFilters.jsx +++ b/web/src/components/table/task-logs/TaskLogsFilters.jsx @@ -56,10 +56,10 @@ const TaskLogsFilters = ({ showClear pure size='small' - presets={DATE_RANGE_PRESETS.map(preset => ({ + presets={DATE_RANGE_PRESETS.map((preset) => ({ text: t(preset.text), start: preset.start(), - end: preset.end() + end: preset.end(), }))} /> diff --git a/web/src/components/table/task-logs/modals/ContentModal.jsx b/web/src/components/table/task-logs/modals/ContentModal.jsx index 3bfba37b178a..1c7d9664193c 100644 --- a/web/src/components/table/task-logs/modals/ContentModal.jsx +++ b/web/src/components/table/task-logs/modals/ContentModal.jsx @@ -60,38 +60,54 @@ const ContentModal = ({ if (videoError) { return (
- + 视频无法在当前浏览器中播放,这可能是由于: - + • 视频服务商的跨域限制 - + • 需要特定的请求头或认证 - + • 防盗链保护机制 - +
- -
- -
- + {modalContent} @@ -104,22 +120,24 @@ const ContentModal = ({ return (
{isLoading && ( -
- +
+
)} -
diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index 03ea2b31e376..0a299ffa26f7 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -30,7 +30,8 @@ import { Space, Row, Col, - Spin, Tooltip + Spin, + Tooltip, } from '@douyinfe/semi-ui'; import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react'; @@ -266,7 +267,8 @@ const RechargeCard = ({ {payMethods && payMethods.length > 0 ? ( {payMethods.map((payMethod) => { - const minTopupVal = Number(payMethod.min_topup) || 0; + const minTopupVal = + Number(payMethod.min_topup) || 0; const isStripe = payMethod.type === 'stripe'; const disabled = (!enableOnlineTopUp && !isStripe) || @@ -280,7 +282,9 @@ const RechargeCard = ({ type='tertiary' onClick={() => preTopUp(payMethod.type)} disabled={disabled} - loading={paymentLoading && payWay === payMethod.type} + loading={ + paymentLoading && payWay === payMethod.type + } icon={ payMethod.type === 'alipay' ? ( @@ -291,7 +295,10 @@ const RechargeCard = ({ ) : ( ) } @@ -301,12 +308,22 @@ const RechargeCard = ({ ); - return disabled && minTopupVal > Number(topUpCount || 0) ? ( - + return disabled && + minTopupVal > Number(topUpCount || 0) ? ( + {buttonEl} ) : ( - {buttonEl} + + {buttonEl} + ); })} @@ -324,23 +341,27 @@ const RechargeCard = ({
{presetAmounts.map((preset, index) => { - const discount = preset.discount || topupInfo?.discount?.[preset.value] || 1.0; + const discount = + preset.discount || + topupInfo?.discount?.[preset.value] || + 1.0; const originalPrice = preset.value * priceRatio; const discountedPrice = originalPrice * discount; const hasDiscount = discount < 1.0; const actualPay = discountedPrice; const save = originalPrice - discountedPrice; - + return ( { @@ -352,24 +373,35 @@ const RechargeCard = ({ }} >
- + {formatLargeNumber(preset.value)} {hasDiscount && ( - - {t('折').includes('off') ? - ((1 - parseFloat(discount)) * 100).toFixed(1) : - (discount * 10).toFixed(1)}{t('折')} - + + {t('折').includes('off') + ? ( + (1 - parseFloat(discount)) * + 100 + ).toFixed(1) + : (discount * 10).toFixed(1)} + {t('折')} + )} -
+
{t('实付')} {actualPay.toFixed(2)}, - {hasDiscount ? `${t('节省')} ${save.toFixed(2)}` : `${t('节省')} 0.00`} + {hasDiscount + ? `${t('节省')} ${save.toFixed(2)}` + : `${t('节省')} 0.00`}
diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 929a47e39b04..558c6705009e 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -80,11 +80,11 @@ const TopUp = () => { // 预设充值额度选项 const [presetAmounts, setPresetAmounts] = useState([]); const [selectedPreset, setSelectedPreset] = useState(null); - + // 充值配置信息 const [topupInfo, setTopupInfo] = useState({ amount_options: [], - discount: {} + discount: {}, }); const topUp = async () => { @@ -262,9 +262,9 @@ const TopUp = () => { if (success) { setTopupInfo({ amount_options: data.amount_options || [], - discount: data.discount || {} + discount: data.discount || {}, }); - + // 处理支付方式 let payMethods = data.pay_methods || []; try { @@ -280,10 +280,15 @@ const TopUp = () => { payMethods = payMethods.map((method) => { // 规范化最小充值数 const normalizedMinTopup = Number(method.min_topup); - method.min_topup = Number.isFinite(normalizedMinTopup) ? normalizedMinTopup : 0; + method.min_topup = Number.isFinite(normalizedMinTopup) + ? normalizedMinTopup + : 0; // Stripe 的最小充值从后端字段回填 - if (method.type === 'stripe' && (!method.min_topup || method.min_topup <= 0)) { + if ( + method.type === 'stripe' && + (!method.min_topup || method.min_topup <= 0) + ) { const stripeMin = Number(data.stripe_min_topup); if (Number.isFinite(stripeMin)) { method.min_topup = stripeMin; @@ -313,7 +318,11 @@ const TopUp = () => { setPayMethods(payMethods); const enableStripeTopUp = data.enable_stripe_topup || false; const enableOnlineTopUp = data.enable_online_topup || false; - const minTopUpValue = enableOnlineTopUp? data.min_topup : enableStripeTopUp? data.stripe_min_topup : 1; + const minTopUpValue = enableOnlineTopUp + ? data.min_topup + : enableStripeTopUp + ? data.stripe_min_topup + : 1; setEnableOnlineTopUp(enableOnlineTopUp); setEnableStripeTopUp(enableStripeTopUp); setMinTopUp(minTopUpValue); @@ -330,12 +339,12 @@ const TopUp = () => { console.log('解析支付方式失败:', e); setPayMethods([]); } - + // 如果有自定义充值数量选项,使用它们替换默认的预设选项 if (data.amount_options && data.amount_options.length > 0) { - const customPresets = data.amount_options.map(amount => ({ + const customPresets = data.amount_options.map((amount) => ({ value: amount, - discount: data.discount[amount] || 1.0 + discount: data.discount[amount] || 1.0, })); setPresetAmounts(customPresets); } @@ -483,7 +492,7 @@ const TopUp = () => { const selectPresetAmount = (preset) => { setTopUpCount(preset.value); setSelectedPreset(preset.value); - + // 计算实际支付金额,考虑折扣 const discount = preset.discount || topupInfo.discount[preset.value] || 1.0; const discountedAmount = preset.value * priceRatio * discount; diff --git a/web/src/components/topup/modals/PaymentConfirmModal.jsx b/web/src/components/topup/modals/PaymentConfirmModal.jsx index 1bffbfed1d4a..8bd5455c7f84 100644 --- a/web/src/components/topup/modals/PaymentConfirmModal.jsx +++ b/web/src/components/topup/modals/PaymentConfirmModal.jsx @@ -40,9 +40,10 @@ const PaymentConfirmModal = ({ amountNumber, discountRate, }) => { - const hasDiscount = discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0; - const originalAmount = hasDiscount ? (amountNumber / discountRate) : 0; - const discountAmount = hasDiscount ? (originalAmount - amountNumber) : 0; + const hasDiscount = + discountRate && discountRate > 0 && discountRate < 1 && amountNumber > 0; + const originalAmount = hasDiscount ? amountNumber / discountRate : 0; + const discountAmount = hasDiscount ? originalAmount - amountNumber : 0; return ( dayjs().startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '近 7 天', start: () => dayjs().subtract(6, 'day').startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '本周', start: () => dayjs().startOf('week').toDate(), - end: () => dayjs().endOf('week').toDate() + end: () => dayjs().endOf('week').toDate(), }, { text: '近 30 天', start: () => dayjs().subtract(29, 'day').startOf('day').toDate(), - end: () => dayjs().endOf('day').toDate() + end: () => dayjs().endOf('day').toDate(), }, { text: '本月', start: () => dayjs().startOf('month').toDate(), - end: () => dayjs().endOf('month').toDate() + end: () => dayjs().endOf('month').toDate(), }, ]; diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index bc389b2e8a9d..1ccfffaf25da 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -131,13 +131,11 @@ export const buildApiPayload = ( seed: 'seed', }; - Object.entries(parameterMappings).forEach(([key, param]) => { const enabled = parameterEnabled[key]; const value = inputs[param]; const hasValue = value !== undefined && value !== null; - if (enabled && hasValue) { payload[param] = value; } diff --git a/web/src/helpers/data.js b/web/src/helpers/data.js index b894a953c318..e45aac3e9941 100644 --- a/web/src/helpers/data.js +++ b/web/src/helpers/data.js @@ -23,7 +23,9 @@ export function setStatusData(data) { localStorage.setItem('logo', data.logo); localStorage.setItem('footer_html', data.footer_html); localStorage.setItem('quota_per_unit', data.quota_per_unit); + // 兼容:保留旧字段,同时写入新的额度展示类型 localStorage.setItem('display_in_currency', data.display_in_currency); + localStorage.setItem('quota_display_type', data.quota_display_type || 'USD'); localStorage.setItem('enable_drawing', data.enable_drawing); localStorage.setItem('enable_task', data.enable_task); localStorage.setItem('enable_data_export', data.enable_data_export); diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index c19e2849d918..19e10ce31a6a 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -830,12 +830,25 @@ export function renderQuotaNumberWithDigit(num, digits = 2) { if (typeof num !== 'number' || isNaN(num)) { return 0; } - let displayInCurrency = localStorage.getItem('display_in_currency'); + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; num = num.toFixed(digits); - if (displayInCurrency) { + if (quotaDisplayType === 'CNY') { + return '¥' + num; + } else if (quotaDisplayType === 'USD') { return '$' + num; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbol = '¤'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || symbol; + } + } catch (e) {} + return symbol + num; + } else { + return num; } - return num; } export function renderNumberWithPoint(num) { @@ -887,33 +900,67 @@ export function getQuotaWithUnit(quota, digits = 6) { } export function renderQuotaWithAmount(amount) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - return '$' + amount; - } else { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + if (quotaDisplayType === 'TOKENS') { return renderNumber(renderUnitWithQuota(amount)); } + if (quotaDisplayType === 'CNY') { + return '¥' + amount; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbol = '¤'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || symbol; + } + } catch (e) {} + return symbol + amount; + } + return '$' + amount; } export function renderQuota(quota, digits = 2) { let quotaPerUnit = localStorage.getItem('quota_per_unit'); - let displayInCurrency = localStorage.getItem('display_in_currency'); + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; quotaPerUnit = parseFloat(quotaPerUnit); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { - const result = quota / quotaPerUnit; - const fixedResult = result.toFixed(digits); - - // 如果 toFixed 后结果为 0 但原始值不为 0,显示最小值 - if (parseFloat(fixedResult) === 0 && quota > 0 && result > 0) { - const minValue = Math.pow(10, -digits); - return '$' + minValue.toFixed(digits); - } - - return '$' + fixedResult; + if (quotaDisplayType === 'TOKENS') { + return renderNumber(quota); + } + const resultUSD = quota / quotaPerUnit; + let symbol = '$'; + let value = resultUSD; + if (quotaDisplayType === 'CNY') { + const statusStr = localStorage.getItem('status'); + let usdRate = 1; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + usdRate = s?.usd_exchange_rate || 1; + } + } catch (e) {} + value = resultUSD * usdRate; + symbol = '¥'; + } else if (quotaDisplayType === 'CUSTOM') { + const statusStr = localStorage.getItem('status'); + let symbolCustom = '¤'; + let rate = 1; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbolCustom = s?.custom_currency_symbol || symbolCustom; + rate = s?.custom_currency_exchange_rate || rate; + } + } catch (e) {} + value = resultUSD * rate; + symbol = symbolCustom; + } + const fixedResult = value.toFixed(digits); + if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) { + const minValue = Math.pow(10, -digits); + return symbol + minValue.toFixed(digits); } - return renderNumber(quota); + return symbol + fixedResult; } function isValidGroupRatio(ratio) { @@ -1072,7 +1119,7 @@ export function renderModelPrice( (completionTokens / 1000000) * completionRatioPrice * groupRatio + (webSearchCallCount / 1000) * webSearchPrice * groupRatio + (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + - (imageGenerationCallPrice * groupRatio); + imageGenerationCallPrice * groupRatio; return ( <> @@ -1510,9 +1557,8 @@ export function renderAudioModelPrice( } export function renderQuotaWithPrompt(quota, digits) { - let displayInCurrency = localStorage.getItem('display_in_currency'); - displayInCurrency = displayInCurrency === 'true'; - if (displayInCurrency) { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + if (quotaDisplayType !== 'TOKENS') { return i18next.t('等价金额:') + renderQuota(quota, digits); } return ''; diff --git a/web/src/helpers/utils.jsx b/web/src/helpers/utils.jsx index bcd13230e6f6..0cbdebd15497 100644 --- a/web/src/helpers/utils.jsx +++ b/web/src/helpers/utils.jsx @@ -646,9 +646,25 @@ export const calculateModelPrice = ({ const numCompletion = parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor; + let symbol = '$'; + if (currency === 'CNY') { + symbol = '¥'; + } else if (currency === 'CUSTOM') { + try { + const statusStr = localStorage.getItem('status'); + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || '¤'; + } else { + symbol = '¤'; + } + } catch (e) { + symbol = '¤'; + } + } return { - inputPrice: `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(precision)}`, - completionPrice: `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(precision)}`, + inputPrice: `${symbol}${numInput.toFixed(precision)}`, + completionPrice: `${symbol}${numCompletion.toFixed(precision)}`, unitLabel, isPerToken: true, usedGroup, diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 7d09d4dfd40f..2d1f4c1e34b5 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -25,9 +25,13 @@ import { showInfo, showSuccess, loadChannelModels, - copy + copy, } from '../../helpers'; -import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants'; +import { + CHANNEL_OPTIONS, + ITEMS_PER_PAGE, + MODEL_TABLE_PAGE_SIZE, +} from '../../constants'; import { useIsMobile } from '../common/useIsMobile'; import { useTableCompactMode } from '../common/useTableCompactMode'; import { Modal } from '@douyinfe/semi-ui'; @@ -64,7 +68,7 @@ export const useChannelsData = () => { // Status filter const [statusFilter, setStatusFilter] = useState( - localStorage.getItem('channel-status-filter') || 'all' + localStorage.getItem('channel-status-filter') || 'all', ); // Type tabs states @@ -80,7 +84,7 @@ export const useChannelsData = () => { const [selectedModelKeys, setSelectedModelKeys] = useState([]); const [isBatchTesting, setIsBatchTesting] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); - + // 使用 ref 来避免闭包问题,类似旧版实现 const shouldStopBatchTestingRef = useRef(false); @@ -116,9 +120,12 @@ export const useChannelsData = () => { // Initialize from localStorage useEffect(() => { const localIdSort = localStorage.getItem('id-sort') === 'true'; - const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; - const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true'; - const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true'; + const localPageSize = + parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + const localEnableTagMode = + localStorage.getItem('enable-tag-mode') === 'true'; + const localEnableBatchDelete = + localStorage.getItem('enable-batch-delete') === 'true'; setIdSort(localIdSort); setPageSize(localPageSize); @@ -176,7 +183,10 @@ export const useChannelsData = () => { // Save column preferences useEffect(() => { if (Object.keys(visibleColumns).length > 0) { - localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns)); + localStorage.setItem( + 'channels-table-columns', + JSON.stringify(visibleColumns), + ); } }, [visibleColumns]); @@ -290,14 +300,21 @@ export const useChannelsData = () => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') { setLoading(true); - await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort); + await searchChannels( + enableTagMode, + typeKey, + statusF, + page, + pageSize, + idSort, + ); setLoading(false); return; } const reqId = ++requestCounter.current; setLoading(true); - const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; + const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`, @@ -311,7 +328,10 @@ export const useChannelsData = () => { if (success) { const { items, total, type_counts } = data; if (type_counts) { - const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + const sumAll = Object.values(type_counts).reduce( + (acc, v) => acc + v, + 0, + ); setTypeCounts({ ...type_counts, all: sumAll }); } setChannelFormat(items, enableTagMode); @@ -335,11 +355,18 @@ export const useChannelsData = () => { setSearching(true); try { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF); + await loadChannels( + page, + pageSz, + sortFlag, + enableTagMode, + typeKey, + statusF, + ); return; } - const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : ''; + const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : ''; const statusParam = statusF !== 'all' ? `&status=${statusF}` : ''; const res = await API.get( `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`, @@ -347,7 +374,10 @@ export const useChannelsData = () => { const { success, message, data } = res.data; if (success) { const { items = [], total = 0, type_counts = {} } = data; - const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + const sumAll = Object.values(type_counts).reduce( + (acc, v) => acc + v, + 0, + ); setTypeCounts({ ...type_counts, all: sumAll }); setChannelFormat(items, enableTagMode); setChannelCount(total); @@ -366,7 +396,14 @@ export const useChannelsData = () => { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(page, pageSize, idSort, enableTagMode); } else { - await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort); + await searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + page, + pageSize, + idSort, + ); } }; @@ -452,9 +489,16 @@ export const useChannelsData = () => { const { searchKeyword, searchGroup, searchModel } = getFormValues(); setActivePage(page); if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - loadChannels(page, pageSize, idSort, enableTagMode).then(() => { }); + loadChannels(page, pageSize, idSort, enableTagMode).then(() => {}); } else { - searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort); + searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + page, + pageSize, + idSort, + ); } }; @@ -470,7 +514,14 @@ export const useChannelsData = () => { showError(reason); }); } else { - searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort); + searchChannels( + enableTagMode, + activeTypeKey, + statusFilter, + 1, + size, + idSort, + ); } }; @@ -501,7 +552,10 @@ export const useChannelsData = () => { showError(res?.data?.message || t('渠道复制失败')); } } catch (error) { - showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error)); + showError( + t('渠道复制失败: ') + + (error?.response?.data?.message || error?.message || error), + ); } }; @@ -540,7 +594,11 @@ export const useChannelsData = () => { data.priority = parseInt(data.priority); break; case 'weight': - if (data.weight === undefined || data.weight < 0 || data.weight === '') { + if ( + data.weight === undefined || + data.weight < 0 || + data.weight === '' + ) { showInfo('权重必须是非负整数!'); return; } @@ -683,7 +741,11 @@ export const useChannelsData = () => { const res = await API.post(`/api/channel/fix`); const { success, message, data } = res.data; if (success) { - showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails)); + showSuccess( + t('已修复 ${success} 个通道,失败 ${fails} 个通道。') + .replace('${success}', data.success) + .replace('${fails}', data.fails), + ); await refresh(); } else { showError(message); @@ -700,10 +762,12 @@ export const useChannelsData = () => { } // 添加到正在测试的模型集合 - setTestingModels(prev => new Set([...prev, model])); + setTestingModels((prev) => new Set([...prev, model])); try { - const res = await API.get(`/api/channel/test/${record.id}?model=${model}`); + const res = await API.get( + `/api/channel/test/${record.id}?model=${model}`, + ); // 检查是否在请求期间被停止 if (shouldStopBatchTestingRef.current && isBatchTesting) { @@ -713,14 +777,14 @@ export const useChannelsData = () => { const { success, message, time } = res.data; // 更新测试结果 - setModelTestResults(prev => ({ + setModelTestResults((prev) => ({ ...prev, [testKey]: { success, message, time: time || 0, - timestamp: Date.now() - } + timestamp: Date.now(), + }, })); if (success) { @@ -738,7 +802,9 @@ export const useChannelsData = () => { ); } else { showInfo( - t('通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。') + t( + '通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。', + ) .replace('${name}', record.name) .replace('${model}', model) .replace('${time.toFixed(2)}', time.toFixed(2)), @@ -750,19 +816,19 @@ export const useChannelsData = () => { } catch (error) { // 处理网络错误 const testKey = `${record.id}-${model}`; - setModelTestResults(prev => ({ + setModelTestResults((prev) => ({ ...prev, [testKey]: { success: false, message: error.message || t('网络错误'), time: 0, - timestamp: Date.now() - } + timestamp: Date.now(), + }, })); showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`); } finally { // 从正在测试的模型集合中移除 - setTestingModels(prev => { + setTestingModels((prev) => { const newSet = new Set(prev); newSet.delete(model); return newSet; @@ -777,9 +843,11 @@ export const useChannelsData = () => { return; } - const models = currentTestChannel.models.split(',').filter(model => - model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) - ); + const models = currentTestChannel.models + .split(',') + .filter((model) => + model.toLowerCase().includes(modelSearchKeyword.toLowerCase()), + ); if (models.length === 0) { showError(t('没有找到匹配的模型')); @@ -790,9 +858,9 @@ export const useChannelsData = () => { shouldStopBatchTestingRef.current = false; // 重置停止标志 // 清空该渠道之前的测试结果 - setModelTestResults(prev => { + setModelTestResults((prev) => { const newResults = { ...prev }; - models.forEach(model => { + models.forEach((model) => { const testKey = `${currentTestChannel.id}-${model}`; delete newResults[testKey]; }); @@ -800,7 +868,12 @@ export const useChannelsData = () => { }); try { - showInfo(t('开始批量测试 ${count} 个模型,已清空上次结果...').replace('${count}', models.length)); + showInfo( + t('开始批量测试 ${count} 个模型,已清空上次结果...').replace( + '${count}', + models.length, + ), + ); // 提高并发数量以加快测试速度,参考旧版的并发限制 const concurrencyLimit = 5; @@ -814,13 +887,16 @@ export const useChannelsData = () => { } const batch = models.slice(i, i + concurrencyLimit); - showInfo(t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)') - .replace('${current}', i + 1) - .replace('${end}', Math.min(i + concurrencyLimit, models.length)) - .replace('${total}', models.length) + showInfo( + t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)') + .replace('${current}', i + 1) + .replace('${end}', Math.min(i + concurrencyLimit, models.length)) + .replace('${total}', models.length), ); - const batchPromises = batch.map(model => testChannel(currentTestChannel, model)); + const batchPromises = batch.map((model) => + testChannel(currentTestChannel, model), + ); const batchResults = await Promise.allSettled(batchPromises); results.push(...batchResults); @@ -832,20 +908,20 @@ export const useChannelsData = () => { // 短暂延迟避免过于频繁的请求 if (i + concurrencyLimit < models.length) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } } if (!shouldStopBatchTestingRef.current) { // 等待一小段时间确保所有结果都已更新 - await new Promise(resolve => setTimeout(resolve, 300)); + await new Promise((resolve) => setTimeout(resolve, 300)); // 使用当前状态重新计算结果统计 - setModelTestResults(currentResults => { + setModelTestResults((currentResults) => { let successCount = 0; let failCount = 0; - models.forEach(model => { + models.forEach((model) => { const testKey = `${currentTestChannel.id}-${model}`; const result = currentResults[testKey]; if (result && result.success) { @@ -857,10 +933,11 @@ export const useChannelsData = () => { // 显示完成消息 setTimeout(() => { - showSuccess(t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') - .replace('${success}', successCount) - .replace('${fail}', failCount) - .replace('${total}', models.length) + showSuccess( + t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}') + .replace('${success}', successCount) + .replace('${fail}', failCount) + .replace('${total}', models.length), ); }, 100); @@ -1045,4 +1122,4 @@ export const useChannelsData = () => { setCompactMode, setActivePage, }; -}; \ No newline at end of file +}; diff --git a/web/src/hooks/common/useSidebar.js b/web/src/hooks/common/useSidebar.js index 13d76fd86281..0b8eb3d8f204 100644 --- a/web/src/hooks/common/useSidebar.js +++ b/web/src/hooks/common/useSidebar.js @@ -128,7 +128,7 @@ export const useSidebar = () => { // 刷新用户配置的方法(供外部调用) const refreshUserConfig = async () => { - if (Object.keys(adminConfig).length > 0) { + if (Object.keys(adminConfig).length > 0) { await loadUserConfig(); } @@ -155,7 +155,10 @@ export const useSidebar = () => { sidebarEventTarget.addEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); return () => { - sidebarEventTarget.removeEventListener(SIDEBAR_REFRESH_EVENT, handleRefresh); + sidebarEventTarget.removeEventListener( + SIDEBAR_REFRESH_EVENT, + handleRefresh, + ); }; }, [adminConfig]); diff --git a/web/src/hooks/model-pricing/useModelPricingData.jsx b/web/src/hooks/model-pricing/useModelPricingData.jsx index 799cdc1367df..0a407236ddd8 100644 --- a/web/src/hooks/model-pricing/useModelPricingData.jsx +++ b/web/src/hooks/model-pricing/useModelPricingData.jsx @@ -64,6 +64,29 @@ export const useModelPricingData = () => { () => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate], ); + const customExchangeRate = useMemo( + () => statusState?.status?.custom_currency_exchange_rate ?? 1, + [statusState], + ); + const customCurrencySymbol = useMemo( + () => statusState?.status?.custom_currency_symbol ?? '¤', + [statusState], + ); + + // 默认货币与站点展示类型同步(USD/CNY),TOKENS 时仍允许切换视图内货币 + const siteDisplayType = useMemo( + () => statusState?.status?.quota_display_type || 'USD', + [statusState], + ); + useEffect(() => { + if ( + siteDisplayType === 'USD' || + siteDisplayType === 'CNY' || + siteDisplayType === 'CUSTOM' + ) { + setCurrency(siteDisplayType); + } + }, [siteDisplayType]); const filteredModels = useMemo(() => { let result = models; @@ -156,6 +179,8 @@ export const useModelPricingData = () => { if (currency === 'CNY') { return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`; + } else if (currency === 'CUSTOM') { + return `${customCurrencySymbol}${(priceInUSD * customExchangeRate).toFixed(3)}`; } return `$${priceInUSD.toFixed(3)}`; }; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 8a9d32f21999..6f26d670b388 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1773,7 +1773,10 @@ "自定义模型名称": "Custom model name", "启用全部密钥": "Enable all keys", "充值价格显示": "Recharge price", - "美元汇率(非充值汇率,仅用于定价页面换算)": "USD exchange rate (not recharge rate, only used for pricing page conversion)", + "自定义货币": "Custom currency", + "自定义货币符号": "Custom currency symbol", + "例如 €, £, Rp, ₩, ₹...": "For example, €, £, Rp, ₩, ₹...", + "站点额度展示类型及汇率": "Site quota display type and exchange rate", "美元汇率": "USD exchange rate", "隐藏操作项": "Hide actions", "显示操作项": "Show actions", diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index 35a635bdd6ef..6d601ef2b69d 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -1773,7 +1773,10 @@ "自定义模型名称": "Nom de modèle personnalisé", "启用全部密钥": "Activer toutes les clés", "充值价格显示": "Prix de recharge", - "美元汇率(非充值汇率,仅用于定价页面换算)": "Taux de change USD (pas de taux de recharge, uniquement utilisé pour la conversion de la page de tarification)", + "站点额度展示类型及汇率": "Type d'affichage du quota du site et taux de change", + "自定义货币": "Devise personnalisée", + "自定义货币符号": "Symbole de devise personnalisé", + "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", "美元汇率": "Taux de change USD", "隐藏操作项": "Masquer les actions", "显示操作项": "Afficher les actions", @@ -2137,4 +2140,4 @@ "common": { "changeLanguage": "Changer de langue" } -} \ No newline at end of file +} diff --git a/web/src/pages/Setting/Operation/SettingsGeneral.jsx b/web/src/pages/Setting/Operation/SettingsGeneral.jsx index 5af750ec39f0..fbfa0ed9995d 100644 --- a/web/src/pages/Setting/Operation/SettingsGeneral.jsx +++ b/web/src/pages/Setting/Operation/SettingsGeneral.jsx @@ -17,8 +17,19 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import React, { useEffect, useState, useRef } from 'react'; -import { Banner, Button, Col, Form, Row, Spin, Modal } from '@douyinfe/semi-ui'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; +import { + Banner, + Button, + Col, + Form, + Row, + Spin, + Modal, + Select, + InputGroup, + Input, +} from '@douyinfe/semi-ui'; import { compareObjects, API, @@ -35,10 +46,12 @@ export default function GeneralSettings(props) { const [inputs, setInputs] = useState({ TopUpLink: '', 'general_setting.docs_link': '', + 'general_setting.quota_display_type': 'USD', + 'general_setting.custom_currency_symbol': '¤', + 'general_setting.custom_currency_exchange_rate': '', QuotaPerUnit: '', RetryTimes: '', USDExchangeRate: '', - DisplayInCurrencyEnabled: false, DisplayTokenStatEnabled: false, DefaultCollapseSidebar: false, DemoSiteEnabled: false, @@ -88,6 +101,30 @@ export default function GeneralSettings(props) { }); } + // 计算展示在输入框中的“1 USD = X ”中的 X + const combinedRate = useMemo(() => { + const type = inputs['general_setting.quota_display_type']; + if (type === 'USD') return '1'; + if (type === 'CNY') return String(inputs['USDExchangeRate'] || ''); + if (type === 'TOKENS') return String(inputs['QuotaPerUnit'] || ''); + if (type === 'CUSTOM') + return String( + inputs['general_setting.custom_currency_exchange_rate'] || '', + ); + return ''; + }, [inputs]); + + const onCombinedRateChange = (val) => { + const type = inputs['general_setting.quota_display_type']; + if (type === 'CNY') { + handleFieldChange('USDExchangeRate')(val); + } else if (type === 'TOKENS') { + handleFieldChange('QuotaPerUnit')(val); + } else if (type === 'CUSTOM') { + handleFieldChange('general_setting.custom_currency_exchange_rate')(val); + } + }; + useEffect(() => { const currentInputs = {}; for (let key in props.options) { @@ -95,6 +132,28 @@ export default function GeneralSettings(props) { currentInputs[key] = props.options[key]; } } + // 若旧字段存在且新字段缺失,则做一次兜底映射 + if ( + currentInputs['general_setting.quota_display_type'] === undefined && + props.options?.DisplayInCurrencyEnabled !== undefined + ) { + currentInputs['general_setting.quota_display_type'] = props.options + .DisplayInCurrencyEnabled + ? 'USD' + : 'TOKENS'; + } + // 回填自定义货币相关字段(如果后端已存在) + if (props.options['general_setting.custom_currency_symbol'] !== undefined) { + currentInputs['general_setting.custom_currency_symbol'] = + props.options['general_setting.custom_currency_symbol']; + } + if ( + props.options['general_setting.custom_currency_exchange_rate'] !== + undefined + ) { + currentInputs['general_setting.custom_currency_exchange_rate'] = + props.options['general_setting.custom_currency_exchange_rate']; + } setInputs(currentInputs); setInputsRow(structuredClone(currentInputs)); refForm.current.setValues(currentInputs); @@ -130,29 +189,7 @@ export default function GeneralSettings(props) { showClear /> - {inputs.QuotaPerUnit !== '500000' && inputs.QuotaPerUnit !== 500000 && ( - - setShowQuotaWarning(true)} - /> - - )} - - - + {/* 单位美元额度已合入汇率组合控件(TOKENS 模式下编辑),不再单独展示 */} - - - + + + + + + + + + + - - setInputs({ ...inputs, - 'monitor_setting.auto_test_channel_minutes': parseInt(value), + 'monitor_setting.auto_test_channel_minutes': + parseInt(value), }) } /> diff --git a/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx b/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx index d681b6a27d12..a4f1029a1589 100644 --- a/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx +++ b/web/src/pages/Setting/Payment/SettingsPaymentGateway.jsx @@ -118,14 +118,20 @@ export default function SettingsPaymentGateway(props) { } } - if (originInputs['AmountOptions'] !== inputs.AmountOptions && inputs.AmountOptions.trim() !== '') { + if ( + originInputs['AmountOptions'] !== inputs.AmountOptions && + inputs.AmountOptions.trim() !== '' + ) { if (!verifyJSON(inputs.AmountOptions)) { showError(t('自定义充值数量选项不是合法的 JSON 数组')); return; } } - if (originInputs['AmountDiscount'] !== inputs.AmountDiscount && inputs.AmountDiscount.trim() !== '') { + if ( + originInputs['AmountDiscount'] !== inputs.AmountDiscount && + inputs.AmountDiscount.trim() !== '' + ) { if (!verifyJSON(inputs.AmountDiscount)) { showError(t('充值金额折扣配置不是合法的 JSON 对象')); return; @@ -163,10 +169,16 @@ export default function SettingsPaymentGateway(props) { options.push({ key: 'PayMethods', value: inputs.PayMethods }); } if (originInputs['AmountOptions'] !== inputs.AmountOptions) { - options.push({ key: 'payment_setting.amount_options', value: inputs.AmountOptions }); + options.push({ + key: 'payment_setting.amount_options', + value: inputs.AmountOptions, + }); } if (originInputs['AmountDiscount'] !== inputs.AmountDiscount) { - options.push({ key: 'payment_setting.amount_discount', value: inputs.AmountDiscount }); + options.push({ + key: 'payment_setting.amount_discount', + value: inputs.AmountDiscount, + }); } // 发送请求 @@ -273,7 +285,7 @@ export default function SettingsPaymentGateway(props) { placeholder={t('为一个 JSON 文本')} autosize /> - + - + - + diff --git a/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx b/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx index ed982edcfb8f..b298cc787a15 100644 --- a/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx +++ b/web/src/pages/Setting/Ratio/ModelRatioSettings.jsx @@ -226,8 +226,12 @@ export default function ModelRatioSettings(props) { - setInputs({ ...inputs, ImageRatio: value }) - } + onChange={(value) => setInputs({ ...inputs, ImageRatio: value })} /> @@ -249,7 +251,9 @@ export default function ModelRatioSettings(props) { - setInputs({ ...inputs, AudioRatio: value }) - } + onChange={(value) => setInputs({ ...inputs, AudioRatio: value })} /> @@ -270,8 +272,12 @@ export default function ModelRatioSettings(props) { Date: Tue, 30 Sep 2025 15:55:31 +0800 Subject: [PATCH 08/79] fix build on windows --- electron/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/electron/package.json b/electron/package.json index 21c56d31506c..cd1d1c46b774 100644 --- a/electron/package.json +++ b/electron/package.json @@ -4,7 +4,7 @@ "description": "New API - AI Model Gateway Desktop Application", "main": "main.js", "scripts": { - "start": "NODE_ENV=development electron .", + "start": "set NODE_ENV=development&& electron .", "build": "electron-builder", "build:mac": "electron-builder --mac", "build:win": "electron-builder --win", From 922ecef31e2aca2b8eff365a5f07c309f335dcf0 Mon Sep 17 00:00:00 2001 From: bubblepipe Date: Tue, 30 Sep 2025 16:41:41 +0800 Subject: [PATCH 09/79] fix wn --- electron/package.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/electron/package.json b/electron/package.json index cd1d1c46b774..0eb07afd95e0 100644 --- a/electron/package.json +++ b/electron/package.json @@ -41,22 +41,6 @@ "tray-iconTemplate.png", "tray-iconTemplate@2x.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", From 15b21c075f1007bfce7ccfb514ff694cf288762c Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Thu, 2 Oct 2025 14:55:06 +0800 Subject: [PATCH 10/79] windows tray icon --- electron/main.js | 8 +++++++- electron/package.json | 3 ++- electron/tray-icon-windows.png | Bin 0 -> 1203 bytes 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 electron/tray-icon-windows.png diff --git a/electron/main.js b/electron/main.js index bbe6e7cc89e4..9a8534f3afc0 100644 --- a/electron/main.js +++ b/electron/main.js @@ -146,7 +146,13 @@ function createWindow() { } function createTray() { - tray = new Tray(path.join(__dirname, 'tray-iconTemplate.png')); + // Use template icon for macOS (black with transparency, auto-adapts to theme) + // Use colored icon for Windows + const trayIconPath = process.platform === 'darwin' + ? path.join(__dirname, 'tray-iconTemplate.png') + : path.join(__dirname, 'tray-icon-windows.png'); + + tray = new Tray(trayIconPath); const contextMenu = Menu.buildFromTemplate([ { diff --git a/electron/package.json b/electron/package.json index 0eb07afd95e0..9cdf3d12fbed 100644 --- a/electron/package.json +++ b/electron/package.json @@ -39,7 +39,8 @@ "preload.js", "icon.png", "tray-iconTemplate.png", - "tray-iconTemplate@2x.png" + "tray-iconTemplate@2x.png", + "tray-icon-windows.png" ], "mac": { "category": "public.app-category.developer-tools", diff --git a/electron/tray-icon-windows.png b/electron/tray-icon-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..57df8ead031450e7787bb01bec1c0be0e906f0f6 GIT binary patch literal 1203 zcmZWoX;f236n;rvfI{xH-x6O3{njggI5v=Wc$#U=e%R68bWBN8VMHfzbW1tZ*yONWT$X`dbxkNYM3rj|%}VT7BC zju^x>I%DJ=G98V%s;iP<%|iUxDZSIIx_b|ieoi0Ip~))Kf2u-R3u-c5ZIY8o6}p5< z-JMVz6Wt?I=17^YJlS~ixZ%V}17{rB-G_o`(#yJ50S**iFKUEa<)yj<_ceHIzkjDD zUts`tw=o24cp2aF*o(+5(m(egou(G;(Iq13PaYl51sxuz3U1aq zEU1l#n*gtravdiADZD{T%Jeq6B`l`b4qf_e040+D0(xyEHpwS%gkc!KmX3to(t18a z&tw?Mw0|S&wSWa4`y{dsClTLn zlt_66Zt?CxiTDvfkTtJ9@KCdT7$FiTK(R!%+G-dB_JTaauiHtlpHaaB4T!&E2q9Dd z0qM#*z05K62dzD_6!fD8pf$+}02NgBe5!%{Qhim7^?8w?5V&;EBsWDgXhD5TvW>Qy z#btJIv-6Ca%y@G!^R?`(-ahxtb?{d1%Yp+BLe&3T*NQi`*Ka+mfiS&Qofu876Fph< zFNQ}BafQFFxH!V0p@MT^Gj>R*KD%9v)xq!A*tDimtIvnhG1}P3(cD7;sT|v4LAAev ziEYDnKityNL$eP~p+33a*gzk&9z3=sg)+=i;_1iPW5>-X>U-W)6h-bTj0&Ly^Fbdf z7-Ue5^r8cuqUNx`r^B!5#}nF0OxLKp%1f2tW5E0Ucp=$UVwjQ4juT68ugj#_?P47W(#k5m*34;x)Qo3<`Uja=OtMc=kLnFp3iS9+q2e8;1>k<4h)RH7iGtRY`dO$ zTbEVCWw#TjEUle}Ec~5YAjO8_{I6LomSzJ(*8Y{Mlg?oLjvz=*wM*Re+}z;-<=Gf! z_%{{ZN})=n>SXe}oJAs!roHSMDfiR@hs}OoTJ3$9&kq}2^YqJCt7f<{63LakYNc;1 zk?HBVo^kJNVc`>VzM|;6+S<0v#+3!@I|Cv*ohs*i;A zH%2+!N1NzR?Mj0}1pG@9xPn!8)74RA6}>V(F45+3h!PfVcJCa#$djf$NMNp$TDyE3 kyV0{HeVn Date: Fri, 3 Oct 2025 02:27:02 +0800 Subject: [PATCH 11/79] feat: Allow FIM chat requests without messages --- relay/channel/siliconflow/adaptor.go | 10 ++++++++++ relay/helper/valid_request.go | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index 4c176c0887c7..41ab763277f0 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -61,6 +61,16 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel } func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + // SiliconFlow requires messages array for FIM requests, even if client doesn't send it + if (request.Prefix != nil || request.Suffix != nil) && len(request.Messages) == 0 { + // Add an empty user message to satisfy SiliconFlow's requirement + request.Messages = []dto.Message{ + { + Role: "user", + Content: "", + }, + } + } return request, nil } diff --git a/relay/helper/valid_request.go b/relay/helper/valid_request.go index f4a290ec6496..c9d3ed0b7d2e 100644 --- a/relay/helper/valid_request.go +++ b/relay/helper/valid_request.go @@ -275,7 +275,9 @@ func GetAndValidateTextRequest(c *gin.Context, relayMode int) (*dto.GeneralOpenA return nil, errors.New("field prompt is required") } case relayconstant.RelayModeChatCompletions: - if len(textRequest.Messages) == 0 { + // For FIM (Fill-in-the-middle) requests with prefix/suffix, messages is optional + // It will be filled by provider-specific adaptors if needed (e.g., SiliconFlow)。Or it is allowed by model vendor(s) (e.g., DeepSeek) + if len(textRequest.Messages) == 0 && textRequest.Prefix == nil && textRequest.Suffix == nil { return nil, errors.New("field messages is required") } case relayconstant.RelayModeEmbeddings: From 937d93144230892556336e0dfd8e97764d0fd62e Mon Sep 17 00:00:00 2001 From: feitianbubu Date: Fri, 3 Oct 2025 12:29:31 +0800 Subject: [PATCH 12/79] refactor: simplify unsupported test channel types --- constant/channel.go | 61 ++++++++++++++++++++++++++++++++++++++ controller/channel-test.go | 52 ++++++++------------------------ 2 files changed, 74 insertions(+), 39 deletions(-) diff --git a/constant/channel.go b/constant/channel.go index 7d8893c1d0dc..1b5c2724ba3e 100644 --- a/constant/channel.go +++ b/constant/channel.go @@ -113,3 +113,64 @@ var ChannelBaseURLs = []string{ "https://llm.submodel.ai", //53 "https://ark.cn-beijing.volces.com", //54 } + +var ChannelTypeNames = map[int]string{ + ChannelTypeUnknown: "Unknown", + ChannelTypeOpenAI: "OpenAI", + ChannelTypeMidjourney: "Midjourney", + ChannelTypeAzure: "Azure", + ChannelTypeOllama: "Ollama", + ChannelTypeMidjourneyPlus: "MidjourneyPlus", + ChannelTypeOpenAIMax: "OpenAIMax", + ChannelTypeOhMyGPT: "OhMyGPT", + ChannelTypeCustom: "Custom", + ChannelTypeAILS: "AILS", + ChannelTypeAIProxy: "AIProxy", + ChannelTypePaLM: "PaLM", + ChannelTypeAPI2GPT: "API2GPT", + ChannelTypeAIGC2D: "AIGC2D", + ChannelTypeAnthropic: "Anthropic", + ChannelTypeBaidu: "Baidu", + ChannelTypeZhipu: "Zhipu", + ChannelTypeAli: "Ali", + ChannelTypeXunfei: "Xunfei", + ChannelType360: "360", + ChannelTypeOpenRouter: "OpenRouter", + ChannelTypeAIProxyLibrary: "AIProxyLibrary", + ChannelTypeFastGPT: "FastGPT", + ChannelTypeTencent: "Tencent", + ChannelTypeGemini: "Gemini", + ChannelTypeMoonshot: "Moonshot", + ChannelTypeZhipu_v4: "ZhipuV4", + ChannelTypePerplexity: "Perplexity", + ChannelTypeLingYiWanWu: "LingYiWanWu", + ChannelTypeAws: "AWS", + ChannelTypeCohere: "Cohere", + ChannelTypeMiniMax: "MiniMax", + ChannelTypeSunoAPI: "SunoAPI", + ChannelTypeDify: "Dify", + ChannelTypeJina: "Jina", + ChannelCloudflare: "Cloudflare", + ChannelTypeSiliconFlow: "SiliconFlow", + ChannelTypeVertexAi: "VertexAI", + ChannelTypeMistral: "Mistral", + ChannelTypeDeepSeek: "DeepSeek", + ChannelTypeMokaAI: "MokaAI", + ChannelTypeVolcEngine: "VolcEngine", + ChannelTypeBaiduV2: "BaiduV2", + ChannelTypeXinference: "Xinference", + ChannelTypeXai: "xAI", + ChannelTypeCoze: "Coze", + ChannelTypeKling: "Kling", + ChannelTypeJimeng: "Jimeng", + ChannelTypeVidu: "Vidu", + ChannelTypeSubmodel: "Submodel", + ChannelTypeDoubaoVideo: "DoubaoVideo", +} + +func GetChannelTypeName(channelType int) string { + if name, ok := ChannelTypeNames[channelType]; ok { + return name + } + return "Unknown" +} diff --git a/controller/channel-test.go b/controller/channel-test.go index ff1e8cef480c..8b0e37daea99 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -28,6 +28,7 @@ import ( "time" "github.com/bytedance/gopkg/util/gopool" + "github.com/samber/lo" "github.com/gin-gonic/gin" ) @@ -40,46 +41,19 @@ type testResult struct { func testChannel(channel *model.Channel, testModel string, endpointType string) testResult { tik := time.Now() - if channel.Type == constant.ChannelTypeMidjourney { + var unsupportedTestChannelTypes = []int{ + constant.ChannelTypeMidjourney, + constant.ChannelTypeMidjourneyPlus, + constant.ChannelTypeSunoAPI, + constant.ChannelTypeKling, + constant.ChannelTypeJimeng, + constant.ChannelTypeDoubaoVideo, + constant.ChannelTypeVidu, + } + if lo.Contains(unsupportedTestChannelTypes, channel.Type) { + channelTypeName := constant.GetChannelTypeName(channel.Type) return testResult{ - localErr: errors.New("midjourney channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeMidjourneyPlus { - return testResult{ - localErr: errors.New("midjourney plus channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeSunoAPI { - return testResult{ - localErr: errors.New("suno channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeKling { - return testResult{ - localErr: errors.New("kling channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeJimeng { - return testResult{ - localErr: errors.New("jimeng channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeDoubaoVideo { - return testResult{ - localErr: errors.New("doubao video channel test is not supported"), - newAPIError: nil, - } - } - if channel.Type == constant.ChannelTypeVidu { - return testResult{ - localErr: errors.New("vidu channel test is not supported"), - newAPIError: nil, + localErr: fmt.Errorf("%s channel test is not supported", channelTypeName), } } w := httptest.NewRecorder() From 6b75bc001637b1faae47843374076a8d24c6a720 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 12:44:33 +0800 Subject: [PATCH 13/79] refactor(openai_image): replace json.Marshal with common.Marshal for improved serialization #1961 --- dto/openai_image.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/dto/openai_image.go b/dto/openai_image.go index 5aece25f206f..16e9a175d982 100644 --- a/dto/openai_image.go +++ b/dto/openai_image.go @@ -74,14 +74,15 @@ func (r ImageRequest) MarshalJSON() ([]byte, error) { return nil, err } + // 不能合并ExtraFields!!!!!!!! // 合并 ExtraFields - for k, v := range r.Extra { - if _, exists := baseMap[k]; !exists { - baseMap[k] = v - } - } + //for k, v := range r.Extra { + // if _, exists := baseMap[k]; !exists { + // baseMap[k] = v + // } + //} - return json.Marshal(baseMap) + return common.Marshal(baseMap) } func GetJSONFieldNames(t reflect.Type) map[string]struct{} { From b39885be1e8c8175bf12c3222c43e77b63643c4a Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Fri, 3 Oct 2025 13:23:56 +0800 Subject: [PATCH 14/79] action --- .github/workflows/electron-build.yml | 103 +++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/electron-build.yml diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml new file mode 100644 index 000000000000..c7b562931769 --- /dev/null +++ b/.github/workflows/electron-build.yml @@ -0,0 +1,103 @@ +name: Build Electron App + +on: + push: + tags: + - 'v*.*.*' # Triggers on version tags like v1.0.0 + workflow_dispatch: # Allows manual triggering + +jobs: + build: + strategy: + matrix: + os: [macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build frontend + run: | + cd web + npm install --legacy-peer-deps + npm run build + env: + DISABLE_ESLINT_PLUGIN: 'true' + + - name: Build Go binary (darwin/Linux) + if: runner.os != 'Windows' + run: | + go build -ldflags="-s -w" -o new-api + + - name: Build Go binary (Windows) + if: runner.os == 'Windows' + run: | + go build -ldflags="-s -w" -o new-api.exe + + - name: Install Electron dependencies + run: | + cd electron + npm install + + - name: Build Electron app (macOS) + if: runner.os == 'macOS' + run: | + cd electron + npm run build:mac + env: + CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing + + - name: Build Electron app (Windows) + if: runner.os == 'Windows' + run: | + cd electron + npm run build:win + + - name: Upload artifacts (macOS) + if: runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: darwin-build + path: | + electron/dist/*.dmg + electron/dist/*.zip + + - name: Upload artifacts (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: | + electron/dist/*.exe + + release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + darwin-build/* + windows-build/* + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 93e30703d4a19a54a38373a47665b8573058bd4c Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Fri, 3 Oct 2025 13:55:19 +0800 Subject: [PATCH 15/79] action --- .github/workflows/electron-build.yml | 104 ++++++++++++ .gitignore | 6 +- electron/README.md | 73 ++++++++ electron/build.sh | 41 +++++ electron/create-tray-icon.js | 60 +++++++ electron/entitlements.mac.plist | 18 ++ electron/icon.png | Bin 0 -> 31262 bytes electron/main.js | 239 +++++++++++++++++++++++++++ electron/package.json | 100 +++++++++++ electron/preload.js | 6 + electron/tray-icon-windows.png | Bin 0 -> 1203 bytes electron/tray-iconTemplate.png | Bin 0 -> 459 bytes electron/tray-iconTemplate@2x.png | Bin 0 -> 754 bytes web/package.json | 1 + 14 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/electron-build.yml create mode 100644 electron/README.md create mode 100755 electron/build.sh create mode 100644 electron/create-tray-icon.js create mode 100644 electron/entitlements.mac.plist create mode 100644 electron/icon.png create mode 100644 electron/main.js create mode 100644 electron/package.json create mode 100644 electron/preload.js create mode 100644 electron/tray-icon-windows.png create mode 100644 electron/tray-iconTemplate.png create mode 100644 electron/tray-iconTemplate@2x.png diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml new file mode 100644 index 000000000000..b274db85755e --- /dev/null +++ b/.github/workflows/electron-build.yml @@ -0,0 +1,104 @@ +name: Build Electron App + +on: + push: + tags: + - 'v*.*.*' # Triggers on version tags like v1.0.0 + workflow_dispatch: # Allows manual triggering + +jobs: + build: + strategy: + matrix: + os: [macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Build frontend + run: | + cd web + npm install --legacy-peer-deps + npm run build + env: + DISABLE_ESLINT_PLUGIN: 'true' + NODE_OPTIONS: '--max_old_space_size=4096' + + - name: Build Go binary (macos/Linux) + if: runner.os != 'Windows' + run: | + go build -ldflags="-s -w" -o new-api + + - name: Build Go binary (Windows) + if: runner.os == 'Windows' + run: | + go build -ldflags="-s -w" -o new-api.exe + + - name: Install Electron dependencies + run: | + cd electron + npm install + + - name: Build Electron app (macOS) + if: runner.os == 'macOS' + run: | + cd electron + npm run build:mac + env: + CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing + + - name: Build Electron app (Windows) + if: runner.os == 'Windows' + run: | + cd electron + npm run build:win + + - name: Upload artifacts (macOS) + if: runner.os == 'macOS' + uses: actions/upload-artifact@v4 + with: + name: macos-build + path: | + electron/dist/*.dmg + electron/dist/*.zip + + - name: Upload artifacts (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: | + electron/dist/*.exe + + release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + macos-build/* + windows-build/* + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1382829fd7d4..570a4385baa8 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 000000000000..88463b8aefd9 --- /dev/null +++ b/electron/README.md @@ -0,0 +1,73 @@ +# New API Electron Desktop App + +This directory contains the Electron wrapper for New API, providing a native desktop application with system tray support for Windows, macOS, and Linux. + +## Prerequisites + +### 1. Go Binary (Required) +The Electron app requires the compiled Go binary to function. You have two options: + +**Option A: Use existing binary (without Go installed)** +```bash +# If you have a pre-built binary (e.g., new-api-macos) +cp ../new-api-macos ../new-api +``` + +**Option B: Build from source (requires Go)** +TODO + +### 3. Electron Dependencies +```bash +cd electron +npm install +``` + +## Development + +Run the app in development mode: +```bash +npm start +``` + +This will: +- Start the Go backend on port 3000 +- Open an Electron window with DevTools enabled +- Create a system tray icon (menu bar on macOS) +- Store database in `../data/new-api.db` + +## Building for Production + +### Quick Build +```bash +# Ensure Go binary exists in parent directory +ls ../new-api # Should exist + +# Build for current platform +npm run build + +# Platform-specific builds +npm run build:mac # Creates .dmg and .zip +npm run build:win # Creates .exe installer +npm run build:linux # Creates .AppImage and .deb +``` + +### Build Output +- Built applications are in `electron/dist/` +- macOS: `.dmg` (installer) and `.zip` (portable) +- Windows: `.exe` (installer) and portable exe +- Linux: `.AppImage` and `.deb` + +## Configuration + +### Port +Default port is 3000. To change, edit `main.js`: +```javascript +const PORT = 3000; // Change to desired port +``` + +### Database Location +- **Development**: `../data/new-api.db` (project directory) +- **Production**: + - macOS: `~/Library/Application Support/New API/data/` + - Windows: `%APPDATA%/New API/data/` + - Linux: `~/.config/New API/data/` diff --git a/electron/build.sh b/electron/build.sh new file mode 100755 index 000000000000..cef714328d5a --- /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/create-tray-icon.js b/electron/create-tray-icon.js new file mode 100644 index 000000000000..517393b2e1d6 --- /dev/null +++ b/electron/create-tray-icon.js @@ -0,0 +1,60 @@ +// Create a simple tray icon for macOS +// Run: node create-tray-icon.js + +const fs = require('fs'); +const { createCanvas } = require('canvas'); + +function createTrayIcon() { + // For macOS, we'll use a Template image (black and white) + // Size should be 22x22 for Retina displays (@2x would be 44x44) + const canvas = createCanvas(22, 22); + const ctx = canvas.getContext('2d'); + + // Clear canvas + ctx.clearRect(0, 0, 22, 22); + + // Draw a simple "API" icon + ctx.fillStyle = '#000000'; + ctx.font = 'bold 10px system-ui'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('API', 11, 11); + + // Save as PNG + const buffer = canvas.toBuffer('image/png'); + fs.writeFileSync('tray-icon.png', buffer); + + // For Template images on macOS (will adapt to menu bar theme) + fs.writeFileSync('tray-iconTemplate.png', buffer); + fs.writeFileSync('tray-iconTemplate@2x.png', buffer); + + console.log('Tray icon created successfully!'); +} + +// Check if canvas is installed +try { + createTrayIcon(); +} catch (err) { + console.log('Canvas module not installed.'); + console.log('For now, creating a placeholder. Install canvas with: npm install canvas'); + + // Create a minimal 1x1 transparent PNG as placeholder + const minimalPNG = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x03, 0x00, 0x00, 0x00, 0x25, 0xDB, 0x56, + 0xCA, 0x00, 0x00, 0x00, 0x03, 0x50, 0x4C, 0x54, + 0x45, 0x00, 0x00, 0x00, 0xA7, 0x7A, 0x3D, 0xDA, + 0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4E, 0x53, + 0x00, 0x40, 0xE6, 0xD8, 0x66, 0x00, 0x00, 0x00, + 0x0A, 0x49, 0x44, 0x41, 0x54, 0x08, 0x1D, 0x62, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x01, 0x0A, 0x2D, 0xCB, 0x59, 0x00, 0x00, + 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, + 0x60, 0x82 + ]); + + fs.writeFileSync('tray-icon.png', minimalPNG); + console.log('Created placeholder tray icon.'); +} \ No newline at end of file diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist new file mode 100644 index 000000000000..a00aebcd06ae --- /dev/null +++ b/electron/entitlements.mac.plist @@ -0,0 +1,18 @@ + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-jit + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.network.client + + com.apple.security.network.server + + + \ No newline at end of file diff --git a/electron/icon.png b/electron/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c63ac2d77598af361f898c561ed40938719c8391 GIT binary patch literal 31262 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 Q5b%+eRFbIrXcGMY0IP { + 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(); + } + + // Close to tray instead of quitting + mainWindow.on('close', (event) => { + if (!app.isQuitting) { + event.preventDefault(); + mainWindow.hide(); + if (process.platform === 'darwin') { + app.dock.hide(); + } + } + }); + + mainWindow.on('closed', () => { + mainWindow = null; + }); +} + +function createTray() { + // Use template icon for macOS (black with transparency, auto-adapts to theme) + // Use colored icon for Windows + const trayIconPath = process.platform === 'darwin' + ? path.join(__dirname, 'tray-iconTemplate.png') + : path.join(__dirname, 'tray-icon-windows.png'); + + tray = new Tray(trayIconPath); + + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show New API', + click: () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.show(); + if (process.platform === 'darwin') { + app.dock.show(); + } + } + } + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + app.isQuitting = true; + app.quit(); + } + } + ]); + + tray.setToolTip('New API'); + tray.setContextMenu(contextMenu); + + // On macOS, clicking the tray icon shows the window + tray.on('click', () => { + if (mainWindow === null) { + createWindow(); + } else { + mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show(); + if (mainWindow.isVisible() && process.platform === 'darwin') { + app.dock.show(); + } + } + }); +} + +app.whenReady().then(async () => { + try { + await startServer(); + createTray(); + 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', () => { + // Don't quit when window is closed, keep running in tray + // Only quit when explicitly choosing Quit from tray menu +}); + +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 000000000000..9cdf3d12fbed --- /dev/null +++ b/electron/package.json @@ -0,0 +1,100 @@ +{ + "name": "new-api-electron", + "version": "1.0.0", + "description": "New API - AI Model Gateway Desktop Application", + "main": "main.js", + "scripts": { + "start": "set 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", + "tray-iconTemplate.png", + "tray-iconTemplate@2x.png", + "tray-icon-windows.png" + ], + "mac": { + "category": "public.app-category.developer-tools", + "icon": "icon.png", + "identity": null, + "hardenedRuntime": false, + "gatekeeperAssess": false, + "entitlements": "entitlements.mac.plist", + "entitlementsInherit": "entitlements.mac.plist", + "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 000000000000..6d8b6daa0649 --- /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/electron/tray-icon-windows.png b/electron/tray-icon-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..57df8ead031450e7787bb01bec1c0be0e906f0f6 GIT binary patch literal 1203 zcmZWoX;f236n;rvfI{xH-x6O3{njggI5v=Wc$#U=e%R68bWBN8VMHfzbW1tZ*yONWT$X`dbxkNYM3rj|%}VT7BC zju^x>I%DJ=G98V%s;iP<%|iUxDZSIIx_b|ieoi0Ip~))Kf2u-R3u-c5ZIY8o6}p5< z-JMVz6Wt?I=17^YJlS~ixZ%V}17{rB-G_o`(#yJ50S**iFKUEa<)yj<_ceHIzkjDD zUts`tw=o24cp2aF*o(+5(m(egou(G;(Iq13PaYl51sxuz3U1aq zEU1l#n*gtravdiADZD{T%Jeq6B`l`b4qf_e040+D0(xyEHpwS%gkc!KmX3to(t18a z&tw?Mw0|S&wSWa4`y{dsClTLn zlt_66Zt?CxiTDvfkTtJ9@KCdT7$FiTK(R!%+G-dB_JTaauiHtlpHaaB4T!&E2q9Dd z0qM#*z05K62dzD_6!fD8pf$+}02NgBe5!%{Qhim7^?8w?5V&;EBsWDgXhD5TvW>Qy z#btJIv-6Ca%y@G!^R?`(-ahxtb?{d1%Yp+BLe&3T*NQi`*Ka+mfiS&Qofu876Fph< zFNQ}BafQFFxH!V0p@MT^Gj>R*KD%9v)xq!A*tDimtIvnhG1}P3(cD7;sT|v4LAAev ziEYDnKityNL$eP~p+33a*gzk&9z3=sg)+=i;_1iPW5>-X>U-W)6h-bTj0&Ly^Fbdf z7-Ue5^r8cuqUNx`r^B!5#}nF0OxLKp%1f2tW5E0Ucp=$UVwjQ4juT68ugj#_?P47W(#k5m*34;x)Qo3<`Uja=OtMc=kLnFp3iS9+q2e8;1>k<4h)RH7iGtRY`dO$ zTbEVCWw#TjEUle}Ec~5YAjO8_{I6LomSzJ(*8Y{Mlg?oLjvz=*wM*Re+}z;-<=Gf! z_%{{ZN})=n>SXe}oJAs!roHSMDfiR@hs}OoTJ3$9&kq}2^YqJCt7f<{63LakYNc;1 zk?HBVo^kJNVc`>VzM|;6+S<0v#+3!@I|Cv*ohs*i;A zH%2+!N1NzR?Mj0}1pG@9xPn!8)74RA6}>V(F45+3h!PfVcJCa#$djf$NMNp$TDyE3 kyV0{HeVneK@{oCO|{#S9GG z!XV7ZFl&wkP_Qw;C&U#sR%fZ>$7zmYc*&kIant>eck|4iehX2gQcmEuz z{F=Cqqha|&@g1y&Q9jxs3q=B%)Lf^Ze_X8e;U;hDvn_g?JpUEm5dvyk4UX$+uyx4kfBSaIWU^~T@O3fm^-Kre+=ySF ztIN7`AEV68qdOf$gj<*MZ%*T%kkyu}&oDVnIL!Bma2BgTrd;A1okE#&RSFl@D1GU% z_B&L@y!aW1fUfGC726WB=3QBK{+fx4&*Zy3b6A)2uj2#x-P6_2Wt~$(69B(4fSCXQ literal 0 HcmV?d00001 diff --git a/electron/tray-iconTemplate@2x.png b/electron/tray-iconTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d5666a04e55b0233bb3c3981a361d1f61ea575e8 GIT binary patch literal 754 zcmeAS@N?(olHy`uVBq!ia0vp^Iv~u!3?wz9Rv7~+#^NA%C&rs6b?Si}&H|6fVg?3o zVGw3ym^DWND7Z1eC&U#<(-tr?Qf@vs7dImtJ1-9(0~3?yBj?9J6Rk^v{DK+&OR<)| z@c6UW_#^+~Ut2k5J`mR9I#O^({^GUsYC#Th;(3R59yh-|afM>FwWe=t+Uu`f%O0zo zbTf6`a%|?!3}7Hl_H=O!shIP2%JoTy40v4Ms`xd2VN?8a&-(Vi|A()MO1#-%ZT-7L z&gk0Jjf*_W7%Fbs&svwcU*)n&dHZdVBlh_#_`UekvNvo}p7QkU@->g6FIclOTv6Eg zc|qLSGC?(`GfXGWeA4cIDOd1kwf{89m3iFz%O1O3*>V5TQ~94gb*}vMbBs2Q zW&ExqberRQQt>Xci8r^XOuV)>`mf@0_o7!vlo`M4P1sWLY8%UQlLUj~CMP!^4xTL} z6I6R#ra6!68IPUarl{su-p>{?-C4R#e)sXjpv{>g0!qyLYYdkrJuWnmnWx9mvpiTJ zqWo&XncKG(#x>5^6>|8&i3gPhza1FLLb#Xa&G?}A^|$(auBkkNHAU=&arfnkhefSI<14qid^b4TcP^#>`ia1e z6$Z(U>06Q>$?eJ#yS#4&i#4MrQ|P8A+Y8g&B{r+`$G_{6+F|;c$MqZ68sisJ_p0yb t4fNr;^!CD8Vb}Vr(k55u*4>={R(g)PNNv{CrnR7Svd$@?2>^`i{gwa# literal 0 HcmV?d00001 diff --git a/web/package.json b/web/package.json index f014d84b9e0f..b94445f3ca16 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", From a8bfa7ad29d794a4a41671dc3db835fd3dd6c3da Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 13:59:01 +0800 Subject: [PATCH 16/79] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20English=20bug?= =?UTF-8?q?=20report=20template=20to=20GitHub=20issue=20tracker=20for=20im?= =?UTF-8?q?proved=20issue=20reporting=20#1960?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report_en.md | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report_en.md diff --git a/.github/ISSUE_TEMPLATE/bug_report_en.md b/.github/ISSUE_TEMPLATE/bug_report_en.md new file mode 100644 index 000000000000..5c2506180e04 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_en.md @@ -0,0 +1,26 @@ +--- +name: Bug Report +about: Describe the issue you encountered with clear and detailed language +title: '' +labels: bug +assignees: '' + +--- + +**Routine Checks** + +[//]: # (Remove the space in the box and fill with an x) ++ [ ] I have confirmed there are no similar issues currently ++ [ ] I have confirmed I have upgraded to the latest version ++ [ ] I have thoroughly read the project README, especially the FAQ section ++ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback ++ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly** + +**Issue Description** + +**Steps to Reproduce** + +**Expected Result** + +**Related Screenshots** +If none, please delete this section. \ No newline at end of file From c9abe1d769a5e360732720606112a2b0b263acfc Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 14:01:16 +0800 Subject: [PATCH 17/79] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20English=20featu?= =?UTF-8?q?re=20request=20template=20to=20GitHub=20issue=20tracker=20for?= =?UTF-8?q?=20enhanced=20feature=20proposal=20submissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/feature_request_en.md | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request_en.md diff --git a/.github/ISSUE_TEMPLATE/feature_request_en.md b/.github/ISSUE_TEMPLATE/feature_request_en.md new file mode 100644 index 000000000000..cdfc43f0d7c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request_en.md @@ -0,0 +1,22 @@ +--- +name: Feature Request +about: Describe the new feature you would like to add with clear and detailed language +title: '' +labels: enhancement +assignees: '' + +--- + +**Routine Checks** + +[//]: # (Remove the space in the box and fill with an x) ++ [ ] I have confirmed there are no similar issues currently ++ [ ] I have confirmed I have upgraded to the latest version ++ [ ] I have thoroughly read the project README and confirmed the current version cannot meet my needs ++ [ ] I understand and am willing to follow up on this issue, assist with testing and provide feedback ++ [ ] I understand and acknowledge the above, and understand that project maintainers have limited time and energy, **issues that do not follow the rules may be ignored or closed directly** + +**Feature Description** + +**Use Case** + From d2492d2af963b391faf6b61f128bb6242f6c4ea4 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Fri, 3 Oct 2025 14:28:29 +0800 Subject: [PATCH 18/79] fix deps --- web/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/web/package.json b/web/package.json index b94445f3ca16..f014d84b9e0f 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,6 @@ "@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", From 8026e5142ba9178f866d369dd6715adc98db0c43 Mon Sep 17 00:00:00 2001 From: bubblepipe42 Date: Fri, 3 Oct 2025 14:30:48 +0800 Subject: [PATCH 19/79] fix deps --- web/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/web/package.json b/web/package.json index b94445f3ca16..f014d84b9e0f 100644 --- a/web/package.json +++ b/web/package.json @@ -10,7 +10,6 @@ "@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", From 51d71a6e1aa818784ea9d57dd2a2213ec601c2ea Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 14:45:39 +0800 Subject: [PATCH 20/79] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20Spanish=20featu?= =?UTF-8?q?re=20request=20template=20to=20GitHub=20issue=20tracker=20for?= =?UTF-8?q?=20improved=20feature=20proposal=20submissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/translation-glossary.md | 72 ++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/translation-glossary.md diff --git a/docs/translation-glossary.md b/docs/translation-glossary.md new file mode 100644 index 000000000000..2a617072a5e0 --- /dev/null +++ b/docs/translation-glossary.md @@ -0,0 +1,72 @@ +# 翻译术语表 (Translation Glossary) + +本文档为翻译贡献者提供项目中关键术语的标准翻译参考,以确保翻译的一致性和准确性。 + +This document provides standard translation references for key terminology in the project to ensure consistency and accuracy for translation contributors. + +## 核心概念 (Core Concepts) + +| 中文 | English | 说明 | Description | +|------|---------|------|-------------| +| 倍率 | Ratio | 用于计算价格的乘数因子 | Multiplier factor used for price calculation | +| 令牌 | Token | API访问凭证,也指模型处理的文本单元 | API access credentials or text units processed by models | +| 渠道 | Channel | API服务提供商的接入通道 | Access channel for API service providers | +| 分组 | Group | 用户或令牌的分类,影响价格倍率 | Classification of users or tokens, affecting price ratios | +| 额度 | Quota | 用户可用的服务额度 | Available service quota for users | + +## 模型相关 (Model Related) + +| 中文 | English | 说明 | Description | +|------|---------|------|-------------| +| 提示 | Prompt | 模型输入内容 | Model input content | +| 补全 | Completion | 模型输出内容 | Model output content | +| 输入 | Input/Prompt | 发送给模型的内容 | Content sent to the model | +| 输出 | Output/Completion | 模型返回的内容 | Content returned by the model | +| 模型倍率 | Model Ratio | 不同模型的计费倍率 | Billing ratio for different models | +| 补全倍率 | Completion Ratio | 输出内容的额外计费倍率 | Additional billing ratio for output content | +| 固定价格 | Price per call | 按次计费的价格 | Fixed price per call | +| 按量计费 | Pay-as-you-go | 根据使用量计费 | Billing based on usage | +| 按次计费 | Pay-per-view | 每次调用固定价格 | Fixed price per invocation | + +## 用户管理 (User Management) + +| 中文 | English | 说明 | Description | +|------|---------|------|-------------| +| 超级管理员 | Root User | 最高权限管理员 | Administrator with highest privileges | +| 管理员 | Admin User | 系统管理员 | System administrator | +| 普通用户 | Normal User | 普通权限用户 | Regular user with standard privileges | + +## 充值与兑换 (Recharge & Redemption) + +| 中文 | English | 说明 | Description | +|------|---------|------|-------------| +| 充值 | Top Up | 为账户增加额度 | Add quota to account | +| 兑换码 | Redemption Code | 可兑换额度的代码 | Code that can be redeemed for quota | + +## 渠道管理 (Channel Management) + +| 中文 | English | 说明 | Description | +|------|---------|------|-------------| +| 渠道 | Channel | API服务提供通道 | API service provider channel | +| 密钥 | Key | API访问密钥 | API access key | +| 优先级 | Priority | 渠道选择优先级 | Channel selection priority | +| 权重 | Weight | 负载均衡权重 | Load balancing weight | +| 代理 | Proxy | 代理服务器地址 | Proxy server address | +| 模型重定向 | Model Mapping | 请求体中模型名称替换 | Model name replacement in request body | + +## 翻译注意事项 (Translation Guidelines) + +- **提示 (Prompt)** = 模型输入内容 / Model input content +- **补全 (Completion)** = 模型输出内容 / Model output content +- **倍率 (Ratio)** = 价格计算的乘数因子 / Multiplier factor for price calculation +- **额度 (Quota)** = 可用的用户服务额度,有时也翻译为 Credit / Available service quota for users, sometimes also translated as Credit +- **Token** = 根据上下文可能指 / Depending on context, may refer to: + - API访问令牌 (API Token) + - 模型处理的文本单元 (Text Token) + - 系统访问令牌 (Access Token) + +--- + +**贡献说明**: 如发现术语翻译不一致或有更好的翻译建议,欢迎提交 Issue 或 Pull Request。 + +**Contribution Note**: If you find any inconsistencies in terminology translations or have better translation suggestions, please feel free to submit an Issue or Pull Request. From 0adfcf9d271de988962008e92aeeaec2a86ed346 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 20:27:34 +0800 Subject: [PATCH 21/79] chore: update README files --- README.en.md | 1 - README.fr.md | 1 - 2 files changed, 2 deletions(-) diff --git a/README.en.md b/README.en.md index 60d4f6a02862..b107fc78e8ec 100644 --- a/README.en.md +++ b/README.en.md @@ -213,7 +213,6 @@ For detailed API documentation, please refer to [API Documentation](https://docs Other projects based on New API: - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon): High-performance optimized version of New API -- [VoAPI](https://github.com/VoAPI/VoAPI): Frontend beautified version based on New API ## Help and Support diff --git a/README.fr.md b/README.fr.md index d069800533e9..6e4b707ed4e7 100644 --- a/README.fr.md +++ b/README.fr.md @@ -213,7 +213,6 @@ Pour une documentation détaillée de l'API, veuillez vous référer à [Documen Autres projets basés sur New API : - [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon) : Version optimisée hautes performances de New API -- [VoAPI](https://github.com/VoAPI/VoAPI) : Version embellie du frontend basée sur New API ## Aide et support From 72a12e3747d528aee7f5bb5e9af0e6d68351e9c0 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 20:28:26 +0800 Subject: [PATCH 22/79] chore: update README files --- README.en.md | 1 - README.fr.md | 1 - 2 files changed, 2 deletions(-) diff --git a/README.en.md b/README.en.md index b107fc78e8ec..6648a733309a 100644 --- a/README.en.md +++ b/README.en.md @@ -208,7 +208,6 @@ For detailed API documentation, please refer to [API Documentation](https://docs ## Related Projects - [One API](https://github.com/songquanpeng/one-api): Original project - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy): Midjourney interface support -- [chatnio](https://github.com/Deeptrain-Community/chatnio): Next-generation AI one-stop B/C-end solution - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool): Query usage quota with key Other projects based on New API: diff --git a/README.fr.md b/README.fr.md index 6e4b707ed4e7..65dd458d6684 100644 --- a/README.fr.md +++ b/README.fr.md @@ -208,7 +208,6 @@ Pour une documentation détaillée de l'API, veuillez vous référer à [Documen ## Projets connexes - [One API](https://github.com/songquanpeng/one-api) : Projet original - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy) : Prise en charge de l'interface Midjourney -- [chatnio](https://github.com/Deeptrain-Community/chatnio) : Solution B/C unique d'IA de nouvelle génération - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool) : Interroger le quota d'utilisation avec une clé Autres projets basés sur New API : From 731e9f4ca905c3f1a611c29aafc69f26410161a3 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 21:39:28 +0800 Subject: [PATCH 23/79] =?UTF-8?q?=F0=9F=92=B1=20feat(RechargeCard):=20enha?= =?UTF-8?q?nce=20currency=20display=20logic=20for=20top-up=20amounts=20bas?= =?UTF-8?q?ed=20on=20user=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/topup/RechargeCard.jsx | 91 +++++++++++++++++++++-- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index 0a299ffa26f7..ee251a0b54bf 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -338,7 +338,40 @@ const RechargeCard = ({ )} {(enableOnlineTopUp || enableStripeTopUp) && ( - + + {t('选择充值额度')} + {(() => { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + if (quotaDisplayType === 'USD') return null; + + const statusStr = localStorage.getItem('status'); + let symbol = '¥'; + let rate = 7; + + try { + if (statusStr) { + const s = JSON.parse(statusStr); + if (quotaDisplayType === 'CNY') { + rate = s?.usd_exchange_rate || 7; + symbol = '¥'; + } else if (quotaDisplayType === 'CUSTOM') { + rate = s?.custom_currency_exchange_rate || 1; + symbol = s?.custom_currency_symbol || '¤'; + } + } + } catch (e) {} + + return ( + + (1 $ = {rate.toFixed(2)} {symbol}) + + ); + })()} +
+ } + >
{presetAmounts.map((preset, index) => { const discount = @@ -351,6 +384,54 @@ const RechargeCard = ({ const actualPay = discountedPrice; const save = originalPrice - discountedPrice; + // 根据当前货币类型换算显示金额和数量 + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + let symbol = '¥'; + let displayValue = preset.value; // 显示的数量 + let displayActualPay = actualPay; + let displaySave = save; + + // 获取汇率 + const statusStr = localStorage.getItem('status'); + let usdRate = 7; // 默认汇率 1 USD = 7 CNY + try { + if (statusStr) { + const s = JSON.parse(statusStr); + usdRate = s?.usd_exchange_rate || 7; + } + } catch (e) {} + + if (quotaDisplayType === 'USD') { + symbol = '$'; + // 数量和价格都保持美元(系统默认) + displayValue = preset.value; + displayActualPay = actualPay / usdRate; + displaySave = save / usdRate; + } else if (quotaDisplayType === 'CNY') { + symbol = '¥'; + // 数量需要换算为人民币 + displayValue = preset.value * usdRate; + // 价格已经是人民币,保持不变 + displayActualPay = actualPay; + displaySave = save; + } else if (quotaDisplayType === 'CUSTOM') { + let customSymbol = '¤'; + let customRate = 1; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + customSymbol = s?.custom_currency_symbol || '¤'; + customRate = s?.custom_currency_exchange_rate || 1; + } + } catch (e) {} + symbol = customSymbol; + // 数量从USD转为自定义货币 + displayValue = preset.value * customRate; + // 价格从CNY先转USD再转自定义货币 + displayActualPay = (actualPay / usdRate) * customRate; + displaySave = (save / usdRate) * customRate; + } + return ( - {formatLargeNumber(preset.value)} + {formatLargeNumber(preset.value)} $ {hasDiscount && ( {t('折').includes('off') @@ -398,10 +479,10 @@ const RechargeCard = ({ margin: '4px 0', }} > - {t('实付')} {actualPay.toFixed(2)}, + {t('实付')} {symbol}{displayActualPay.toFixed(2)}, {hasDiscount - ? `${t('节省')} ${save.toFixed(2)}` - : `${t('节省')} 0.00`} + ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` + : `${t('节省')} ${symbol}0.00`}
From 55d19df029f470da25acf77e884dad685fd54856 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 21:40:07 +0800 Subject: [PATCH 24/79] chore: remove outdated instructions from pull request template --- .github/PULL_REQUEST_TEMPLATE/pull_request_template.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 4f6e41ac2b9f..7403f6c0020c 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -13,7 +13,3 @@ ### PR 描述 **请在下方详细描述您的 PR,包括目的、实现细节等。** - -### **重要提示** - -**所有 PR 都必须提交到 `alpha` 分支。请确保您的 PR 目标分支是 `alpha`。** From 7437b671efb6b994eae3d8d721e3cbe215e5abc9 Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 3 Oct 2025 21:49:24 +0800 Subject: [PATCH 25/79] =?UTF-8?q?=F0=9F=92=B1=20feat:=20implement=20curren?= =?UTF-8?q?cy=20configuration=20helper=20and=20update=20currency=20display?= =?UTF-8?q?=20logic=20in=20RechargeCard=20and=20render=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table/usage-logs/UsageLogsColumnDefs.jsx | 4 +- web/src/components/topup/RechargeCard.jsx | 71 ++-- web/src/helpers/render.jsx | 304 ++++++++++++------ 3 files changed, 221 insertions(+), 158 deletions(-) diff --git a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx index 59ec89d60dd7..9af30226aba3 100644 --- a/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx +++ b/web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx @@ -419,7 +419,7 @@ export const getLogsColumns = ({ }, { key: COLUMN_KEYS.PROMPT, - title: t('提示'), + title: t('输入'), dataIndex: 'prompt_tokens', render: (text, record, index) => { return record.type === 0 || record.type === 2 || record.type === 5 ? ( @@ -431,7 +431,7 @@ export const getLogsColumns = ({ }, { key: COLUMN_KEYS.COMPLETION, - title: t('补全'), + title: t('输出'), dataIndex: 'completion_tokens', render: (text, record, index) => { return parseInt(text) > 0 && diff --git a/web/src/components/topup/RechargeCard.jsx b/web/src/components/topup/RechargeCard.jsx index ee251a0b54bf..85a4220e2284 100644 --- a/web/src/components/topup/RechargeCard.jsx +++ b/web/src/components/topup/RechargeCard.jsx @@ -37,6 +37,7 @@ import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { CreditCard, Coins, Wallet, BarChart2, TrendingUp } from 'lucide-react'; import { IconGift } from '@douyinfe/semi-icons'; import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime'; +import { getCurrencyConfig } from '../../helpers/render'; const { Text } = Typography; @@ -343,25 +344,8 @@ const RechargeCard = ({
{t('选择充值额度')} {(() => { - const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; - if (quotaDisplayType === 'USD') return null; - - const statusStr = localStorage.getItem('status'); - let symbol = '¥'; - let rate = 7; - - try { - if (statusStr) { - const s = JSON.parse(statusStr); - if (quotaDisplayType === 'CNY') { - rate = s?.usd_exchange_rate || 7; - symbol = '¥'; - } else if (quotaDisplayType === 'CUSTOM') { - rate = s?.custom_currency_exchange_rate || 1; - symbol = s?.custom_currency_symbol || '¤'; - } - } - } catch (e) {} + const { symbol, rate, type } = getCurrencyConfig(); + if (type === 'USD') return null; return ( @@ -385,15 +369,9 @@ const RechargeCard = ({ const save = originalPrice - discountedPrice; // 根据当前货币类型换算显示金额和数量 - const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; - let symbol = '¥'; - let displayValue = preset.value; // 显示的数量 - let displayActualPay = actualPay; - let displaySave = save; - - // 获取汇率 + const { symbol, rate, type } = getCurrencyConfig(); const statusStr = localStorage.getItem('status'); - let usdRate = 7; // 默认汇率 1 USD = 7 CNY + let usdRate = 7; // 默认CNY汇率 try { if (statusStr) { const s = JSON.parse(statusStr); @@ -401,35 +379,22 @@ const RechargeCard = ({ } } catch (e) {} - if (quotaDisplayType === 'USD') { - symbol = '$'; - // 数量和价格都保持美元(系统默认) - displayValue = preset.value; + let displayValue = preset.value; // 显示的数量 + let displayActualPay = actualPay; + let displaySave = save; + + if (type === 'USD') { + // 数量保持USD,价格从CNY转USD displayActualPay = actualPay / usdRate; displaySave = save / usdRate; - } else if (quotaDisplayType === 'CNY') { - symbol = '¥'; - // 数量需要换算为人民币 + } else if (type === 'CNY') { + // 数量转CNY,价格已是CNY displayValue = preset.value * usdRate; - // 价格已经是人民币,保持不变 - displayActualPay = actualPay; - displaySave = save; - } else if (quotaDisplayType === 'CUSTOM') { - let customSymbol = '¤'; - let customRate = 1; - try { - if (statusStr) { - const s = JSON.parse(statusStr); - customSymbol = s?.custom_currency_symbol || '¤'; - customRate = s?.custom_currency_exchange_rate || 1; - } - } catch (e) {} - symbol = customSymbol; - // 数量从USD转为自定义货币 - displayValue = preset.value * customRate; - // 价格从CNY先转USD再转自定义货币 - displayActualPay = (actualPay / usdRate) * customRate; - displaySave = (save / usdRate) * customRate; + } else if (type === 'CUSTOM') { + // 数量和价格都转自定义货币 + displayValue = preset.value * rate; + displayActualPay = (actualPay / usdRate) * rate; + displaySave = (save / usdRate) * rate; } return ( diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index 6559b1b670e1..b5fcb4d6262c 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -922,6 +922,50 @@ export function renderQuotaWithAmount(amount) { return '$' + amount; } +/** + * 获取当前货币配置信息 + * @returns {Object} - { symbol, rate, type } + */ +export function getCurrencyConfig() { + const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; + const statusStr = localStorage.getItem('status'); + + let symbol = '$'; + let rate = 1; + + if (quotaDisplayType === 'CNY') { + symbol = '¥'; + try { + if (statusStr) { + const s = JSON.parse(statusStr); + rate = s?.usd_exchange_rate || 7; + } + } catch (e) {} + } else if (quotaDisplayType === 'CUSTOM') { + try { + if (statusStr) { + const s = JSON.parse(statusStr); + symbol = s?.custom_currency_symbol || '¤'; + rate = s?.custom_currency_exchange_rate || 1; + } + } catch (e) {} + } + + return { symbol, rate, type: quotaDisplayType }; +} + +/** + * 将美元金额转换为当前选择的货币 + * @param {number} usdAmount - 美元金额 + * @param {number} digits - 小数位数 + * @returns {string} - 格式化后的货币字符串 + */ +export function convertUSDToCurrency(usdAmount, digits = 2) { + const { symbol, rate } = getCurrencyConfig(); + const convertedAmount = usdAmount * rate; + return symbol + convertedAmount.toFixed(digits); +} + export function renderQuota(quota, digits = 2) { let quotaPerUnit = localStorage.getItem('quota_per_unit'); const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; @@ -1084,14 +1128,20 @@ export function renderModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); if (modelPrice !== -1) { + const displayPrice = (modelPrice * rate).toFixed(6); + const displayTotal = (modelPrice * groupRatio * rate).toFixed(6); return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: displayPrice, ratio: groupRatio, - total: modelPrice * groupRatio, + total: displayTotal, ratioType: ratioLabel, }, ); @@ -1127,19 +1177,21 @@ export function renderModelPrice( <>

- {i18next.t('输入价格:${{price}} / 1M tokens{{audioPrice}}', { - price: inputRatioPrice, + {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), audioPrice: audioInputSeperatePrice - ? `,音频 $${audioInputPrice} / 1M tokens` + ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` : '', })}

{i18next.t( - '输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + '输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (completionRatioPrice * rate).toFixed(6), completionRatio: completionRatio, }, )} @@ -1147,10 +1199,11 @@ export function renderModelPrice( {cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * cacheRatio * rate).toFixed(6), cacheRatio: cacheRatio, }, )} @@ -1159,11 +1212,12 @@ export function renderModelPrice( {image && imageOutputTokens > 0 && (

{i18next.t( - '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})', + '图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})', { - price: imageRatioPrice, + symbol: symbol, + price: (imageRatioPrice * rate).toFixed(6), ratio: groupRatio, - total: imageRatioPrice * groupRatio, + total: (imageRatioPrice * groupRatio * rate).toFixed(6), imageRatio: imageRatio, }, )} @@ -1171,22 +1225,25 @@ export function renderModelPrice( )} {webSearch && webSearchCallCount > 0 && (

- {i18next.t('Web搜索价格:${{price}} / 1K 次', { - price: webSearchPrice, + {i18next.t('Web搜索价格:{{symbol}}{{price}} / 1K 次', { + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), })}

)} {fileSearch && fileSearchCallCount > 0 && (

- {i18next.t('文件搜索价格:${{price}} / 1K 次', { - price: fileSearchPrice, + {i18next.t('文件搜索价格:{{symbol}}{{price}} / 1K 次', { + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), })}

)} {imageGenerationCall && imageGenerationCallPrice > 0 && (

- {i18next.t('图片生成调用:${{price}} / 1次', { - price: imageGenerationCallPrice, + {i18next.t('图片生成调用:{{symbol}}{{price}} / 1次', { + symbol: symbol, + price: (imageGenerationCallPrice * rate).toFixed(6), })}

)} @@ -1196,50 +1253,55 @@ export function renderModelPrice( let inputDesc = ''; if (image && imageOutputTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}', + '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}', { nonImageInput: inputTokens - imageOutputTokens, imageInput: imageOutputTokens, imageRatio: imageRatio, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), }, ); } else if (cacheTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}}', + '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}', { nonCacheInput: inputTokens - cacheTokens, cacheInput: cacheTokens, - price: inputRatioPrice, - cachePrice: cacheRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + cachePrice: (cacheRatioPrice * rate).toFixed(6), }, ); } else if (audioInputSeperatePrice && audioInputTokens > 0) { inputDesc = i18next.t( - '(输入 {{nonAudioInput}} tokens / 1M tokens * ${{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * ${{audioPrice}}', + '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}', { nonAudioInput: inputTokens - audioInputTokens, audioInput: audioInputTokens, - price: inputRatioPrice, - audioPrice: audioInputPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + audioPrice: (audioInputPrice * rate).toFixed(6), }, ); } else { inputDesc = i18next.t( - '(输入 {{input}} tokens / 1M tokens * ${{price}}', + '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), }, ); } // 构建输出部分描述 const outputDesc = i18next.t( - '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * {{ratioType}} {{ratio}}', + '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', { completion: completionTokens, - compPrice: completionRatioPrice, + symbol: symbol, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1249,10 +1311,11 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', { count: webSearchCallCount, - price: webSearchPrice, + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1260,10 +1323,11 @@ export function renderModelPrice( : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}', + ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', { count: fileSearchCallCount, - price: fileSearchPrice, + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1271,9 +1335,10 @@ export function renderModelPrice( : '', imageGenerationCall && imageGenerationCallPrice > 0 ? i18next.t( - ' + 图片生成调用 ${{price}} / 1次 * {{ratioType}} {{ratio}}', + ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}', { - price: imageGenerationCallPrice, + symbol: symbol, + price: (imageGenerationCallPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, }, @@ -1282,12 +1347,13 @@ export function renderModelPrice( ].join(''); return i18next.t( - '{{inputDesc}} + {{outputDesc}}{{extraServices}} = ${{total}}', + '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}', { inputDesc, outputDesc, extraServices, - total: price.toFixed(6), + symbol: symbol, + total: (price * rate).toFixed(6), }, ); })()} @@ -1318,10 +1384,14 @@ export function renderLogContent( label: ratioLabel, useUserGroupRatio: useUserGroupRatio, } = getEffectiveRatio(groupRatio, user_group_ratio); + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); if (modelPrice !== -1) { - return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { - price: modelPrice, + return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio, }); @@ -1414,14 +1484,19 @@ export function renderAudioModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); + // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratio: groupRatio, - total: modelPrice * groupRatio, + total: (modelPrice * groupRatio * rate).toFixed(6), ratioType: ratioLabel, }, ); @@ -1456,16 +1531,18 @@ export function renderAudioModelPrice( <>

- {i18next.t('提示价格:${{price}} / 1M tokens', { - price: inputRatioPrice, + {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), })}

{i18next.t( - '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + '补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (completionRatioPrice * rate).toFixed(6), completionRatio: completionRatio, }, )} @@ -1473,10 +1550,11 @@ export function renderAudioModelPrice( {cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * cacheRatio * rate).toFixed(6), cacheRatio: cacheRatio, }, )} @@ -1484,20 +1562,22 @@ export function renderAudioModelPrice( )}

{i18next.t( - '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', + '音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * audioRatio * rate).toFixed(6), audioRatio: audioRatio, }, )}

{i18next.t( - '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', + '音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio * audioCompletionRatio, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + total: (inputRatioPrice * audioRatio * audioCompletionRatio * rate).toFixed(6), audioRatio: audioRatio, audioCompRatio: audioCompletionRatio, }, @@ -1506,48 +1586,52 @@ export function renderAudioModelPrice(

{cacheTokens > 0 ? i18next.t( - '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', { nonCacheInput: inputTokens - cacheTokens, cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, - price: inputRatioPrice, + symbol: symbol, + cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), }, ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6), + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), }, )}

{i18next.t( - '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', + '音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}', { input: audioInputTokens, completion: audioCompletionTokens, - audioInputPrice: audioRatio * inputRatioPrice, + symbol: symbol, + audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(6), audioCompPrice: - audioRatio * audioCompletionRatio * inputRatioPrice, - total: audioPrice.toFixed(6), + (audioRatio * audioCompletionRatio * inputRatioPrice * rate).toFixed(6), + total: (audioPrice * rate).toFixed(6), }, )}

{i18next.t( - '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', + '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}', { - total: price.toFixed(6), - textPrice: textPrice.toFixed(6), - audioPrice: audioPrice.toFixed(6), + symbol: symbol, + total: (price * rate).toFixed(6), + textPrice: (textPrice * rate).toFixed(6), + audioPrice: (audioPrice * rate).toFixed(6), }, )}

@@ -1584,15 +1668,19 @@ export function renderClaudeModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); if (modelPrice !== -1) { return i18next.t( - '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', { - price: modelPrice, + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio: groupRatio, - total: modelPrice * groupRatio, + total: (modelPrice * groupRatio * rate).toFixed(6), }, ); } else { @@ -1621,28 +1709,31 @@ export function renderClaudeModelPrice( <>

- {i18next.t('提示价格:${{price}} / 1M tokens', { - price: inputRatioPrice, + {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), })}

{i18next.t( - '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', + '补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: completionRatio, - total: completionRatioPrice, + total: (completionRatioPrice * rate).toFixed(6), }, )}

{cacheTokens > 0 && (

{i18next.t( - '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + '缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: cacheRatio, - total: cacheRatioPrice, + total: (cacheRatioPrice * rate).toFixed(2), cacheRatio: cacheRatio, }, )} @@ -1651,11 +1742,12 @@ export function renderClaudeModelPrice( {cacheCreationTokens > 0 && (

{i18next.t( - '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', + '缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', { - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), ratio: cacheCreationRatio, - total: cacheCreationRatioPrice, + total: (cacheCreationRatioPrice * rate).toFixed(6), cacheCreationRatio: cacheCreationRatio, }, )} @@ -1665,33 +1757,35 @@ export function renderClaudeModelPrice(

{cacheTokens > 0 || cacheCreationTokens > 0 ? i18next.t( - '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', { nonCacheInput: nonCachedTokens, cacheInput: cacheTokens, cacheRatio: cacheRatio, cacheCreationInput: cacheCreationTokens, cacheCreationRatio: cacheCreationRatio, - cachePrice: cacheRatioPrice, - cacheCreationPrice: cacheCreationRatioPrice, - price: inputRatioPrice, + symbol: symbol, + cachePrice: (cacheRatioPrice * rate).toFixed(2), + cacheCreationPrice: (cacheCreationRatioPrice * rate).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, - total: price.toFixed(6), + total: (price * rate).toFixed(6), }, ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}', + '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', { input: inputTokens, - price: inputRatioPrice, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), completion: completionTokens, - compPrice: completionRatioPrice, + compPrice: (completionRatioPrice * rate).toFixed(6), ratio: groupRatio, ratioType: ratioLabel, - total: price.toFixed(6), + total: (price * rate).toFixed(6), }, )}

@@ -1716,10 +1810,14 @@ export function renderClaudeLogContent( user_group_ratio, ); groupRatio = effectiveGroupRatio; + + // 获取货币配置 + const { symbol, rate } = getCurrencyConfig(); if (modelPrice !== -1) { - return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { - price: modelPrice, + return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { + symbol: symbol, + price: (modelPrice * rate).toFixed(6), ratioType: ratioLabel, ratio: groupRatio, }); From 7763f11da7616c56500afdbbc82fa3067ef6b337 Mon Sep 17 00:00:00 2001 From: Moment Date: Sat, 4 Oct 2025 13:02:35 +0800 Subject: [PATCH 26/79] fix: Gemini missing func name for multi-streaming tool calls (except the first). --- relay/channel/gemini/relay-gemini.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index c8e9c7579556..fa932e51ea7a 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -961,9 +961,15 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * // send first response emptyResponse := helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil) if response.IsToolCall() { - emptyResponse.Choices[0].Delta.ToolCalls = make([]dto.ToolCallResponse, 1) - emptyResponse.Choices[0].Delta.ToolCalls[0] = *response.GetFirstToolCall() - emptyResponse.Choices[0].Delta.ToolCalls[0].Function.Arguments = "" + if len(emptyResponse.Choices) > 0 && len(response.Choices) > 0 { + toolCalls := response.Choices[0].Delta.ToolCalls + copiedToolCalls := make([]dto.ToolCallResponse, len(toolCalls)) + for idx := range toolCalls { + copiedToolCalls[idx] = toolCalls[idx] + copiedToolCalls[idx].Function.Arguments = "" + } + emptyResponse.Choices[0].Delta.ToolCalls = copiedToolCalls + } finishReason = constant.FinishReasonToolCalls err = handleStream(c, info, emptyResponse) if err != nil { From 3da7cebec67b50c392a09fb7b7a3554af53fc2a0 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 15:44:00 +0800 Subject: [PATCH 27/79] =?UTF-8?q?feat:=20=E9=98=B2=E5=91=86=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + electron/main.js | 294 +++++++++++++++++++++++++++++++++++++++++- electron/package.json | 11 +- electron/preload.js | 4 +- 4 files changed, 298 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 570a4385baa8..bc0b14579eca 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ logs web/dist .env one-api +new-api .DS_Store tiktoken_cache .eslintcache diff --git a/electron/main.js b/electron/main.js index 9a8534f3afc0..82f954f00c70 100644 --- a/electron/main.js +++ b/electron/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, dialog, Tray, Menu } = require('electron'); +const { app, BrowserWindow, dialog, Tray, Menu, shell } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const http = require('http'); @@ -7,8 +7,145 @@ const fs = require('fs'); let mainWindow; let serverProcess; let tray = null; +let serverErrorLogs = []; const PORT = 3000; +// 保存日志到文件并打开 +function saveAndOpenErrorLog() { + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const logFileName = `new-api-crash-${timestamp}.log`; + const logDir = app.getPath('logs'); + const logFilePath = path.join(logDir, logFileName); + + // 确保日志目录存在 + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + // 写入日志 + const logContent = `New API 崩溃日志 +生成时间: ${new Date().toLocaleString('zh-CN')} +平台: ${process.platform} +架构: ${process.arch} +应用版本: ${app.getVersion()} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +完整错误日志: + +${serverErrorLogs.join('\n')} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +日志文件位置: ${logFilePath} +`; + + fs.writeFileSync(logFilePath, logContent, 'utf8'); + + // 打开日志文件 + shell.openPath(logFilePath).then((error) => { + if (error) { + console.error('Failed to open log file:', error); + // 如果打开文件失败,至少显示文件位置 + shell.showItemInFolder(logFilePath); + } + }); + + return logFilePath; + } catch (err) { + console.error('Failed to save error log:', err); + return null; + } +} + +// 分析错误日志,识别常见错误并提供解决方案 +function analyzeError(errorLogs) { + const allLogs = errorLogs.join('\n'); + + // 检测端口占用错误 + if (allLogs.includes('failed to start HTTP server') || + allLogs.includes('bind: address already in use') || + allLogs.includes('listen tcp') && allLogs.includes('bind: address already in use')) { + return { + type: '端口被占用', + title: '端口 ' + PORT + ' 被占用', + message: '无法启动服务器,端口已被其他程序占用', + solution: `可能的解决方案:\n\n1. 关闭占用端口 ${PORT} 的其他程序\n2. 检查是否已经运行了另一个 New API 实例\n3. 使用以下命令查找占用端口的进程:\n Mac/Linux: lsof -i :${PORT}\n Windows: netstat -ano | findstr :${PORT}\n4. 重启电脑以释放端口` + }; + } + + // 检测数据库错误 + if (allLogs.includes('database is locked') || + allLogs.includes('unable to open database')) { + return { + type: '数据库错误', + title: '数据库访问失败', + message: '无法访问或锁定数据库文件', + solution: '可能的解决方案:\n\n1. 确保没有其他 New API 实例正在运行\n2. 检查数据库文件权限\n3. 尝试删除数据库锁文件(.db-shm 和 .db-wal)\n4. 重启应用程序' + }; + } + + // 检测权限错误 + if (allLogs.includes('permission denied') || + allLogs.includes('access denied')) { + return { + type: '权限错误', + title: '权限不足', + message: '程序没有足够的权限执行操作', + solution: '可能的解决方案:\n\n1. 以管理员/root权限运行程序\n2. 检查数据目录的读写权限\n3. 检查可执行文件的权限\n4. 在 Mac 上,检查安全性与隐私设置' + }; + } + + // 检测网络错误 + if (allLogs.includes('network is unreachable') || + allLogs.includes('no such host') || + allLogs.includes('connection refused')) { + return { + type: '网络错误', + title: '网络连接失败', + message: '无法建立网络连接', + solution: '可能的解决方案:\n\n1. 检查网络连接是否正常\n2. 检查防火墙设置\n3. 检查代理配置\n4. 确认目标服务器地址正确' + }; + } + + // 检测配置文件错误 + if (allLogs.includes('invalid configuration') || + allLogs.includes('failed to parse config') || + allLogs.includes('yaml') || allLogs.includes('json') && allLogs.includes('parse')) { + return { + type: '配置错误', + title: '配置文件错误', + message: '配置文件格式不正确或包含无效配置', + solution: '可能的解决方案:\n\n1. 检查配置文件格式是否正确\n2. 恢复默认配置\n3. 删除配置文件让程序重新生成\n4. 查看文档了解正确的配置格式' + }; + } + + // 检测内存不足 + if (allLogs.includes('out of memory') || + allLogs.includes('cannot allocate memory')) { + return { + type: '内存不足', + title: '系统内存不足', + message: '程序运行时内存不足', + solution: '可能的解决方案:\n\n1. 关闭其他占用内存的程序\n2. 增加系统可用内存\n3. 重启电脑释放内存\n4. 检查是否存在内存泄漏' + }; + } + + // 检测文件不存在错误 + if (allLogs.includes('no such file or directory') || + allLogs.includes('cannot find the file')) { + return { + type: '文件缺失', + title: '找不到必需的文件', + message: '缺少程序运行所需的文件', + solution: '可能的解决方案:\n\n1. 重新安装应用程序\n2. 检查安装目录是否完整\n3. 确保所有依赖文件都存在\n4. 检查文件路径是否正确' + }; + } + + return null; +} + function getBinaryPath() { const isDev = process.env.NODE_ENV === 'development'; const platform = process.platform; @@ -73,7 +210,13 @@ function startServer() { }); serverProcess.stderr.on('data', (data) => { - console.error(`Server Error: ${data}`); + const errorMsg = data.toString(); + console.error(`Server Error: ${errorMsg}`); + serverErrorLogs.push(errorMsg); + // 只保留最近的100条错误日志 + if (serverErrorLogs.length > 100) { + serverErrorLogs.shift(); + } }); serverProcess.on('error', (err) => { @@ -83,8 +226,76 @@ function startServer() { serverProcess.on('close', (code) => { console.log(`Server process exited with code ${code}`); - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.close(); + + // 如果退出代码不是0,说明服务器异常退出 + if (code !== 0 && code !== null) { + const errorDetails = serverErrorLogs.length > 0 + ? serverErrorLogs.slice(-20).join('\n') + : '没有捕获到错误日志'; + + // 分析错误类型 + const knownError = analyzeError(serverErrorLogs); + + let dialogOptions; + if (knownError) { + // 识别到已知错误,显示友好的错误信息和解决方案 + dialogOptions = { + type: 'error', + title: knownError.title, + message: knownError.message, + detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n退出代码: ${code}\n\n错误类型: ${knownError.type}\n\n最近的错误日志:\n${errorDetails}`, + buttons: ['退出应用', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }; + } else { + // 未识别的错误,显示通用错误信息 + dialogOptions = { + type: 'error', + title: '服务器崩溃', + message: '服务器进程异常退出', + detail: `退出代码: ${code}\n\n最近的错误信息:\n${errorDetails}`, + buttons: ['退出应用', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }; + } + + dialog.showMessageBox(dialogOptions).then((result) => { + if (result.response === 1) { + // 用户选择查看详情,保存并打开日志文件 + const logPath = saveAndOpenErrorLog(); + + // 显示确认对话框 + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.isQuitting = true; + app.quit(); + }); + + // 同时在控制台输出 + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + // 用户选择直接退出 + app.isQuitting = true; + app.quit(); + } + }); + } else { + // 正常退出(code为0或null),直接关闭窗口 + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.close(); + } } }); @@ -201,8 +412,79 @@ app.whenReady().then(async () => { createWindow(); } catch (err) { console.error('Failed to start application:', err); - dialog.showErrorBox('Startup Error', `Failed to start server: ${err.message}`); - app.quit(); + + // 分析启动失败的错误 + const knownError = analyzeError(serverErrorLogs); + + if (knownError) { + dialog.showMessageBox({ + type: 'error', + title: knownError.title, + message: `启动失败: ${knownError.message}`, + detail: `${knownError.solution}\n\n━━━━━━━━━━━━━━━━━━━━━━\n\n错误信息: ${err.message}\n\n错误类型: ${knownError.type}`, + buttons: ['退出', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }).then((result) => { + if (result.response === 1) { + // 用户选择查看日志 + const logPath = saveAndOpenErrorLog(); + + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.quit(); + }); + + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + app.quit(); + } + }); + } else { + dialog.showMessageBox({ + type: 'error', + title: '启动失败', + message: '无法启动服务器', + detail: `错误信息: ${err.message}\n\n请检查日志获取更多信息。`, + buttons: ['退出', '查看完整日志'], + defaultId: 0, + cancelId: 0 + }).then((result) => { + if (result.response === 1) { + // 用户选择查看日志 + const logPath = saveAndOpenErrorLog(); + + const confirmMessage = logPath + ? `日志已保存到:\n${logPath}\n\n日志文件已在默认文本编辑器中打开。\n\n点击"退出"关闭应用程序。` + : '日志保存失败,但已在控制台输出。\n\n点击"退出"关闭应用程序。'; + + dialog.showMessageBox({ + type: 'info', + title: '日志已保存', + message: confirmMessage, + buttons: ['退出'], + defaultId: 0 + }).then(() => { + app.quit(); + }); + + console.log('=== 完整错误日志 ==='); + console.log(serverErrorLogs.join('\n')); + } else { + app.quit(); + } + }); + } } }); diff --git a/electron/package.json b/electron/package.json index 9cdf3d12fbed..21c93356018f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -4,7 +4,8 @@ "description": "New API - AI Model Gateway Desktop Application", "main": "main.js", "scripts": { - "start": "set NODE_ENV=development&& electron .", + "start": "electron .", + "dev": "cross-env NODE_ENV=development electron .", "build": "electron-builder", "build:mac": "electron-builder --mac", "build:win": "electron-builder --win", @@ -17,14 +18,14 @@ "openai", "claude" ], - "author": "", - "license": "MIT", + "author": "QuantumNous", "repository": { "type": "git", - "url": "https://github.com/Calcium-Ion/new-api" + "url": "https://github.com/QuantumNous/new-api" }, "devDependencies": { - "electron": "^28.0.0", + "cross-env": "^7.0.3", + "electron": "28.3.3", "electron-builder": "^24.9.1" }, "build": { diff --git a/electron/preload.js b/electron/preload.js index 6d8b6daa0649..217f5a410c54 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,6 +1,8 @@ const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('electron', { + isElectron: true, version: process.versions.electron, - platform: process.platform + platform: process.platform, + versions: process.versions }); \ No newline at end of file From ff77ba11577fba55f746472e7179a320ecb7d911 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 16:45:29 +0800 Subject: [PATCH 28/79] feat: enhance Electron environment detection and improve database warnings --- electron/main.js | 145 +++++++++++++----- electron/package.json | 4 +- electron/preload.js | 22 ++- .../setup/components/steps/DatabaseStep.jsx | 51 ++++-- 4 files changed, 166 insertions(+), 56 deletions(-) diff --git a/electron/main.js b/electron/main.js index 82f954f00c70..3cebd34f8909 100644 --- a/electron/main.js +++ b/electron/main.js @@ -9,6 +9,7 @@ let serverProcess; let tray = null; let serverErrorLogs = []; const PORT = 3000; +const DEV_FRONTEND_PORT = 5173; // Vite dev server port // 保存日志到文件并打开 function saveAndOpenErrorLog() { @@ -79,10 +80,10 @@ function analyzeError(errorLogs) { if (allLogs.includes('database is locked') || allLogs.includes('unable to open database')) { return { - type: '数据库错误', - title: '数据库访问失败', - message: '无法访问或锁定数据库文件', - solution: '可能的解决方案:\n\n1. 确保没有其他 New API 实例正在运行\n2. 检查数据库文件权限\n3. 尝试删除数据库锁文件(.db-shm 和 .db-wal)\n4. 重启应用程序' + type: '数据文件被占用', + title: '无法访问数据文件', + message: '应用的数据文件正被其他程序占用', + solution: '可能的解决方案:\n\n1. 检查是否已经打开了另一个 New API 窗口\n - 查看任务栏/Dock 中是否有其他 New API 图标\n - 查看系统托盘(Windows)或菜单栏(Mac)中是否有 New API 图标\n\n2. 如果刚刚关闭过应用,请等待 10 秒后再试\n\n3. 重启电脑以释放被占用的文件\n\n4. 如果问题持续,可以尝试:\n - 退出所有 New API 实例\n - 删除数据目录中的临时文件(.db-shm 和 .db-wal)\n - 重新启动应用' }; } @@ -173,32 +174,101 @@ function getBinaryPath() { return path.join(process.resourcesPath, 'bin', binaryName); } -function startServer() { +// Check if a server is available with retry logic +function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) { return new Promise((resolve, reject) => { - const binaryPath = getBinaryPath(); - const isDev = process.env.NODE_ENV === 'development'; + let currentAttempt = 0; + + const tryConnect = () => { + currentAttempt++; + + if (currentAttempt % 5 === 1 && currentAttempt > 1) { + console.log(`Attempting to connect to port ${port}... (attempt ${currentAttempt}/${maxRetries})`); + } + + const req = http.get({ + hostname: '127.0.0.1', // Use IPv4 explicitly instead of 'localhost' to avoid IPv6 issues + port: port, + timeout: 10000 + }, (res) => { + // Server responded, connection successful + req.destroy(); + console.log(`✓ Successfully connected to port ${port} (status: ${res.statusCode})`); + resolve(); + }); - console.log('Starting server from:', binaryPath); + req.on('error', (err) => { + if (currentAttempt >= maxRetries) { + reject(new Error(`Failed to connect to port ${port} after ${maxRetries} attempts: ${err.message}`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); - const env = { ...process.env, PORT: PORT.toString() }; + req.on('timeout', () => { + req.destroy(); + if (currentAttempt >= maxRetries) { + reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`)); + } else { + setTimeout(tryConnect, retryDelay); + } + }); + }; + + tryConnect(); + }); +} - let dataDir; +function startServer() { + return new Promise((resolve, reject) => { + const isDev = process.env.NODE_ENV === 'development'; + if (isDev) { - dataDir = path.join(__dirname, '..', 'data'); - } else { - const userDataPath = app.getPath('userData'); - dataDir = path.join(userDataPath, 'data'); + // 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器 + // 只需要等待前端开发服务器就绪 + console.log('Development mode: skipping server startup'); + console.log('Please make sure you have started:'); + console.log(' 1. Go backend: go run main.go (port 3000)'); + console.log(' 2. Frontend dev server: cd web && bun dev (port 5173)'); + console.log(''); + console.log('Checking if servers are running...'); + + // First check if both servers are accessible + checkServerAvailability(DEV_FRONTEND_PORT) + .then(() => { + console.log('✓ Frontend dev server is accessible on port 5173'); + resolve(); + }) + .catch((err) => { + console.error(`✗ Cannot connect to frontend dev server on port ${DEV_FRONTEND_PORT}`); + console.error('Please make sure the frontend dev server is running:'); + console.error(' cd web && bun dev'); + reject(err); + }); + return; } + // 生产模式:启动二进制服务器 + const env = { ...process.env, PORT: PORT.toString() }; + const userDataPath = app.getPath('userData'); + const dataDir = path.join(userDataPath, 'data'); + if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } env.SQLITE_PATH = path.join(dataDir, 'new-api.db'); + + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log('📁 您的数据存储位置:'); + console.log(' ' + dataDir); + console.log(' 💡 备份提示:复制此目录即可备份所有数据'); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); - const workingDir = isDev - ? path.join(__dirname, '..') - : process.resourcesPath; + const binaryPath = getBinaryPath(); + const workingDir = process.resourcesPath; + + console.log('Starting server from:', binaryPath); serverProcess = spawn(binaryPath, [], { env, @@ -299,32 +369,25 @@ function startServer() { } }); - 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); + checkServerAvailability(PORT) + .then(() => { + console.log('✓ Backend server is accessible on port 3000'); + resolve(); + }) + .catch((err) => { + console.error('✗ Failed to connect to backend server'); + reject(err); + }); }); - - req.end(); } function createWindow() { + const isDev = process.env.NODE_ENV === 'development'; + const loadPort = isDev ? DEV_FRONTEND_PORT : PORT; + mainWindow = new BrowserWindow({ - width: 1400, - height: 900, + width: 1080, + height: 720, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, @@ -334,9 +397,11 @@ function createWindow() { icon: path.join(__dirname, 'icon.png') }); - mainWindow.loadURL(`http://localhost:${PORT}`); + mainWindow.loadURL(`http://127.0.0.1:${loadPort}`); + + console.log(`Loading from: http://127.0.0.1:${loadPort}`); - if (process.env.NODE_ENV === 'development') { + if (isDev) { mainWindow.webContents.openDevTools(); } diff --git a/electron/package.json b/electron/package.json index 21c93356018f..4b5880ea0e44 100644 --- a/electron/package.json +++ b/electron/package.json @@ -4,8 +4,8 @@ "description": "New API - AI Model Gateway Desktop Application", "main": "main.js", "scripts": { - "start": "electron .", - "dev": "cross-env NODE_ENV=development electron .", + "start-app": "electron .", + "dev-app": "cross-env NODE_ENV=development electron .", "build": "electron-builder", "build:mac": "electron-builder --mac", "build:win": "electron-builder --win", diff --git a/electron/preload.js b/electron/preload.js index 217f5a410c54..e089bbb5ae89 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,8 +1,28 @@ const { contextBridge } = require('electron'); +// 获取数据目录路径(用于显示给用户) +// 使用字符串拼接而不是 path.join 避免模块依赖问题 +function getDataDirPath() { + const platform = process.platform; + const homeDir = process.env.HOME || process.env.USERPROFILE || ''; + + switch (platform) { + case 'darwin': + return `${homeDir}/Library/Application Support/New API/data`; + case 'win32': + const appData = process.env.APPDATA || `${homeDir}\\AppData\\Roaming`; + return `${appData}\\New API\\data`; + case 'linux': + return `${homeDir}/.config/New API/data`; + default: + return `${homeDir}/.new-api/data`; + } +} + contextBridge.exposeInMainWorld('electron', { isElectron: true, version: process.versions.electron, platform: process.platform, - versions: process.versions + versions: process.versions, + dataDir: getDataDirPath() }); \ No newline at end of file diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 04dd76a3cf0e..66923f4450da 100644 --- a/web/src/components/setup/components/steps/DatabaseStep.jsx +++ b/web/src/components/setup/components/steps/DatabaseStep.jsx @@ -25,29 +25,54 @@ import { Banner } from '@douyinfe/semi-ui'; * 显示当前数据库类型和相关警告信息 */ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => { + // 检测是否在 Electron 环境中运行 + const isElectron = typeof window !== 'undefined' && window.electron?.isElectron; + return ( <> {/* 数据库警告 */} {setupStatus.database_type === 'sqlite' && ( -

- {t( - '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', + isElectron ? ( +

+

+ {t( + '您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。', + )} +

+ {window.electron?.dataDir && ( +

+ {t('数据存储位置:')} +
+ + {window.electron.dataDir} + +

)} -

-

- +

+ 💡 {t('提示:如需备份数据,只需复制上述目录即可')} +

+
+ ) : ( +
+

{t( - '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + '您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!', )} - -

-
+

+

+ + {t( + '建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。', + )} + +

+
+ ) } className='!rounded-lg' fullMode={false} From c1137027e657e890b2ec6f1aa98b1b0911011d31 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 17:11:30 +0800 Subject: [PATCH 29/79] chore: update build workflows for Electron and Go, including version tagging and dependency management --- .github/workflows/electron-build.yml | 79 +++++++++++++++++---------- .github/workflows/linux-release.yml | 9 +-- .github/workflows/macos-release.yml | 5 +- .github/workflows/windows-release.yml | 5 +- 4 files changed, 61 insertions(+), 37 deletions(-) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index b274db85755e..c3be05bf5f72 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -3,20 +3,31 @@ name: Build Electron App on: push: tags: - - 'v*.*.*' # Triggers on version tags like v1.0.0 + - '*' # Triggers on version tags like v1.0.0 workflow_dispatch: # Allows manual triggering jobs: build: strategy: matrix: - os: [macos-latest, windows-latest] + # os: [macos-latest, windows-latest] + os: [windows-latest] runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - name: Setup Node.js uses: actions/setup-node@v4 @@ -26,39 +37,49 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '>=1.18.0' - name: Build frontend + env: + CI: "" + NODE_OPTIONS: "--max-old-space-size=4096" run: | cd web - npm install --legacy-peer-deps - npm run build - env: - DISABLE_ESLINT_PLUGIN: 'true' - NODE_OPTIONS: '--max_old_space_size=4096' + bun install + DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(git describe --tags) bun run build + cd .. - - name: Build Go binary (macos/Linux) - if: runner.os != 'Windows' - run: | - go build -ldflags="-s -w" -o new-api + # - name: Build Go binary (macos/Linux) + # if: runner.os != 'Windows' + # run: | + # go mod download + # go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api - name: Build Go binary (Windows) if: runner.os == 'Windows' run: | - go build -ldflags="-s -w" -o new-api.exe + go mod download + go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe - - name: Install Electron dependencies + - name: Update Electron version run: | cd electron - npm install + VERSION=$(git describe --tags) + VERSION=${VERSION#v} # Remove 'v' prefix if present + npm version $VERSION --no-git-tag-version --allow-same-version - - name: Build Electron app (macOS) - if: runner.os == 'macOS' + - name: Install Electron dependencies run: | cd electron - npm run build:mac - env: - CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing + npm install + + # - name: Build Electron app (macOS) + # if: runner.os == 'macOS' + # run: | + # cd electron + # npm run build:mac + # env: + # CSC_IDENTITY_AUTO_DISCOVERY: false # Skip code signing - name: Build Electron app (Windows) if: runner.os == 'Windows' @@ -66,14 +87,14 @@ jobs: cd electron npm run build:win - - name: Upload artifacts (macOS) - if: runner.os == 'macOS' - uses: actions/upload-artifact@v4 - with: - name: macos-build - path: | - electron/dist/*.dmg - electron/dist/*.zip + # - name: Upload artifacts (macOS) + # if: runner.os == 'macOS' + # uses: actions/upload-artifact@v4 + # with: + # name: macos-build + # path: | + # electron/dist/*.dmg + # electron/dist/*.zip - name: Upload artifacts (Windows) if: runner.os == 'Windows' @@ -96,7 +117,7 @@ jobs: uses: softprops/action-gh-release@v1 with: files: | - macos-build/* + # macos-build/* windows-build/* draft: false prerelease: false diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 3e3ddc53b11c..d2039dced85e 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -38,21 +38,22 @@ jobs: - name: Build Backend (amd64) run: | go mod download - go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api + VERSION=$(git describe --tags) + go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-$VERSION - name: Build Backend (arm64) run: | sudo apt-get update DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu - CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)' -extldflags '-static'" -o new-api-arm64 + VERSION=$(git describe --tags) + CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: files: | - new-api - new-api-arm64 + new-api-* draft: true generate_release_notes: true env: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 8eaf2d67a58b..135e4610278e 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -39,12 +39,13 @@ jobs: - name: Build Backend run: | go mod download - go build -ldflags "-X 'one-api/common.Version=$(git describe --tags)'" -o new-api-macos + VERSION=$(git describe --tags) + go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: - files: new-api-macos + files: new-api-macos-* draft: true generate_release_notes: true env: diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 30e864f34415..e6cc456caf2b 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -41,12 +41,13 @@ jobs: - name: Build Backend run: | go mod download - go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe + VERSION=$(git describe --tags) + go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe - name: Release uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: - files: new-api.exe + files: new-api-*.exe draft: true generate_release_notes: true env: From 414be64d33ce5bc3a7114e33a1ac18c057342431 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 17:15:10 +0800 Subject: [PATCH 30/79] fix: correct Windows path handling in preload.js and update .gitignore for consistency --- .gitignore | 3 +- electron/package-lock.json | 3677 ++++++++++++++++++++++++++++++++++++ electron/preload.js | 3 +- 3 files changed, 3680 insertions(+), 3 deletions(-) create mode 100644 electron/package-lock.json diff --git a/.gitignore b/.gitignore index bc0b14579eca..3e6020e83042 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,4 @@ tiktoken_cache .eslintcache electron/node_modules -electron/dist -electron/package-lock.json \ No newline at end of file +electron/dist \ No newline at end of file diff --git a/electron/package-lock.json b/electron/package-lock.json new file mode 100644 index 000000000000..7b6ff0586a89 --- /dev/null +++ b/electron/package-lock.json @@ -0,0 +1,3677 @@ +{ + "name": "new-api-electron", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "new-api-electron", + "version": "1.0.0", + "devDependencies": { + "cross-env": "^7.0.3", + "electron": "28.3.3", + "electron-builder": "^24.9.1" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.2.1.tgz", + "integrity": "sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.0.5.tgz", + "integrity": "sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/universal": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", + "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.1", + "@malept/cross-spawn-promise": "^1.1.0", + "debug": "^4.3.1", + "dir-compare": "^3.0.0", + "fs-extra": "^9.0.1", + "minimatch": "^3.0.4", + "plist": "^3.0.4" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.129", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", + "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-4.0.0.tgz", + "integrity": "sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-24.13.3.tgz", + "integrity": "sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/notarize": "2.2.1", + "@electron/osx-sign": "1.0.5", + "@electron/universal": "1.5.1", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "bluebird-lst": "^1.0.9", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chromium-pickle-js": "^0.2.0", + "debug": "^4.3.4", + "ejs": "^3.1.8", + "electron-publish": "24.13.1", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "is-ci": "^3.0.0", + "isbinaryfile": "^5.0.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "minimatch": "^5.1.1", + "read-config-file": "6.3.2", + "sanitize-filename": "^1.6.3", + "semver": "^7.3.8", + "tar": "^6.1.12", + "temp-file": "^3.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "24.13.3", + "electron-builder-squirrel-windows": "24.13.3" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bluebird-lst": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.9.tgz", + "integrity": "sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "^3.5.5" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", + "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-24.13.1.tgz", + "integrity": "sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "4.0.0", + "bluebird-lst": "^1.0.9", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-ci": "^3.0.0", + "js-yaml": "^4.1.0", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz", + "integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-file-ts": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.6.tgz", + "integrity": "sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.3.10", + "typescript": "^5.3.3" + } + }, + "node_modules/config-file-ts/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/config-file-ts/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", + "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "^1.0.0", + "minimatch": "^3.0.4" + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", + "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", + "integrity": "sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "28.3.3", + "resolved": "https://registry.npmjs.org/electron/-/electron-28.3.3.tgz", + "integrity": "sha512-ObKMLSPNhomtCOBAxFS8P2DW/4umkh72ouZUlUKzXGtYuPzgr1SYhskhFWgzAsPtUzhL2CzyV2sfbHcEW4CXqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^18.11.18", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-24.13.3.tgz", + "integrity": "sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "24.13.3", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "dmg-builder": "24.13.3", + "fs-extra": "^10.1.0", + "is-ci": "^3.0.0", + "lazy-val": "^1.0.5", + "read-config-file": "6.3.2", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "24.13.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", + "integrity": "sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "24.13.1", + "builder-util-runtime": "9.2.4", + "chalk": "^4.1.2", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.6.tgz", + "integrity": "sha512-I+NmIfBHUl+r2wcDd6JwE9yWje/PIVY/R5/CmV8dXLZd5K+L9X2klAOwfAHNnondLXkbHyTAleQAWonpTJBTtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-config-file": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", + "integrity": "sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-file-ts": "^0.2.4", + "dotenv": "^9.0.2", + "dotenv-expand": "^5.1.0", + "js-yaml": "^4.1.0", + "json5": "^2.2.0", + "lazy-val": "^1.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/electron/preload.js b/electron/preload.js index e089bbb5ae89..b0579172c5a9 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -9,9 +9,10 @@ function getDataDirPath() { switch (platform) { case 'darwin': return `${homeDir}/Library/Application Support/New API/data`; - case 'win32': + case 'win32': { const appData = process.env.APPDATA || `${homeDir}\\AppData\\Roaming`; return `${appData}\\New API\\data`; + } case 'linux': return `${homeDir}/.config/New API/data`; default: From 7074ea2ed6ae7ecfc25350e82882fe13af978391 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 17:19:52 +0800 Subject: [PATCH 31/79] chore: upgrade action-gh-release to v2 in build workflows --- .github/workflows/electron-build.yml | 2 +- .github/workflows/linux-release.yml | 2 +- .github/workflows/macos-release.yml | 2 +- .github/workflows/windows-release.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index c3be05bf5f72..a58ad964d113 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -114,7 +114,7 @@ jobs: uses: actions/download-artifact@v4 - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | # macos-build/* diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index d2039dced85e..9e270c2abe4d 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -49,7 +49,7 @@ jobs: CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION' -extldflags '-static'" -o new-api-arm64-$VERSION - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: | diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 135e4610278e..ecd3a67086fe 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -42,7 +42,7 @@ jobs: VERSION=$(git describe --tags) go build -ldflags "-X 'one-api/common.Version=$VERSION'" -o new-api-macos-$VERSION - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: new-api-macos-* diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index e6cc456caf2b..a08c3648d663 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -44,7 +44,7 @@ jobs: VERSION=$(git describe --tags) go build -ldflags "-s -w -X 'one-api/common.Version=$VERSION'" -o new-api-$VERSION.exe - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: new-api-*.exe From 1154077eea23bc76e4be9e7af31bd123a23d7b0f Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 17:31:01 +0800 Subject: [PATCH 32/79] feat: enhance versioning logic in electron-build.yml for semver compliance --- .github/workflows/electron-build.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index a58ad964d113..2de544538bfe 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -66,6 +66,29 @@ jobs: cd electron VERSION=$(git describe --tags) VERSION=${VERSION#v} # Remove 'v' prefix if present + # Convert to valid semver: take first 3 components and convert rest to prerelease format + # e.g., 0.9.0.9.1-50-g7074ea2e -> 0.9.0-dev.9.1.50.g7074ea2e + if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$ ]]; then + MAJOR=${BASH_REMATCH[1]} + MINOR=${BASH_REMATCH[2]} + PATCH=${BASH_REMATCH[3]} + REST=${BASH_REMATCH[4]} + + VERSION="$MAJOR.$MINOR.$PATCH" + + # If there's extra content, parse and convert to prerelease format + if [[ -n "$REST" ]]; then + if [[ $REST =~ ^(\..*)?(-[0-9]+-g[0-9a-f]+)$ ]]; then + EXTRA=${BASH_REMATCH[1]} + GIT_SUFFIX=${BASH_REMATCH[2]} + VERSION="$VERSION-dev" + [[ -n "$EXTRA" ]] && VERSION="$VERSION${EXTRA//./.}" + [[ -n "$GIT_SUFFIX" ]] && VERSION="$VERSION${GIT_SUFFIX//-/.}" + else + VERSION="$VERSION-dev${REST//./.}" + fi + fi + fi npm version $VERSION --no-git-tag-version --allow-same-version - name: Install Electron dependencies From 8a56795bd88cc8abef9d42f55f60423d31614003 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 17:50:35 +0800 Subject: [PATCH 33/79] chore: update electron-build.yml to add write permissions and enable file overwriting in artifact uploads --- .github/workflows/electron-build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index 2de544538bfe..9e53ff76d607 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -131,6 +131,8 @@ jobs: needs: build runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write steps: - name: Download all artifacts @@ -140,9 +142,9 @@ jobs: uses: softprops/action-gh-release@v2 with: files: | - # macos-build/* windows-build/* draft: false prerelease: false + overwrite_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From 9f44116260f1d3989bd7f49cf5a54b1905691f0b Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 18:20:43 +0800 Subject: [PATCH 34/79] fix: update versioning logic in electron-build.yml to correctly handle prerelease formats and modify product name in package.json --- .github/workflows/electron-build.yml | 14 +++----------- electron/package.json | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index 9e53ff76d607..cae38a0d5a0e 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -67,7 +67,7 @@ jobs: VERSION=$(git describe --tags) VERSION=${VERSION#v} # Remove 'v' prefix if present # Convert to valid semver: take first 3 components and convert rest to prerelease format - # e.g., 0.9.0.9.1-50-g7074ea2e -> 0.9.0-dev.9.1.50.g7074ea2e + # e.g., 0.9.3-patch.1 -> 0.9.3-patch.1 if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)(.*)$ ]]; then MAJOR=${BASH_REMATCH[1]} MINOR=${BASH_REMATCH[2]} @@ -76,17 +76,9 @@ jobs: VERSION="$MAJOR.$MINOR.$PATCH" - # If there's extra content, parse and convert to prerelease format + # If there's extra content, append it without adding -dev if [[ -n "$REST" ]]; then - if [[ $REST =~ ^(\..*)?(-[0-9]+-g[0-9a-f]+)$ ]]; then - EXTRA=${BASH_REMATCH[1]} - GIT_SUFFIX=${BASH_REMATCH[2]} - VERSION="$VERSION-dev" - [[ -n "$EXTRA" ]] && VERSION="$VERSION${EXTRA//./.}" - [[ -n "$GIT_SUFFIX" ]] && VERSION="$VERSION${GIT_SUFFIX//-/.}" - else - VERSION="$VERSION-dev${REST//./.}" - fi + VERSION="$VERSION$REST" fi fi npm version $VERSION --no-git-tag-version --allow-same-version diff --git a/electron/package.json b/electron/package.json index 4b5880ea0e44..d28ef73b2a10 100644 --- a/electron/package.json +++ b/electron/package.json @@ -30,7 +30,7 @@ }, "build": { "appId": "com.newapi.desktop", - "productName": "New API", + "productName": "New-API-App", "publish": null, "directories": { "output": "dist" From 021892b17d1d5a1b2c470a4a79c2eabc4070b9fd Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 18:38:25 +0800 Subject: [PATCH 35/79] feat: set data directory path in main process and update preload.js to use it --- electron/main.js | 8 ++++++-- electron/preload.js | 19 ++++--------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/electron/main.js b/electron/main.js index 3cebd34f8909..210a4565852d 100644 --- a/electron/main.js +++ b/electron/main.js @@ -222,6 +222,12 @@ function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) { function startServer() { return new Promise((resolve, reject) => { const isDev = process.env.NODE_ENV === 'development'; + + const userDataPath = app.getPath('userData'); + const dataDir = path.join(userDataPath, 'data'); + + // 设置环境变量供 preload.js 使用 + process.env.ELECTRON_DATA_DIR = dataDir; if (isDev) { // 开发模式:假设开发者手动启动了 Go 后端和前端开发服务器 @@ -250,8 +256,6 @@ function startServer() { // 生产模式:启动二进制服务器 const env = { ...process.env, PORT: PORT.toString() }; - const userDataPath = app.getPath('userData'); - const dataDir = path.join(userDataPath, 'data'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); diff --git a/electron/preload.js b/electron/preload.js index b0579172c5a9..ac971fd0a1c4 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -1,22 +1,11 @@ const { contextBridge } = require('electron'); // 获取数据目录路径(用于显示给用户) -// 使用字符串拼接而不是 path.join 避免模块依赖问题 +// 优先使用主进程设置的真实路径,如果没有则回退到手动拼接 function getDataDirPath() { - const platform = process.platform; - const homeDir = process.env.HOME || process.env.USERPROFILE || ''; - - switch (platform) { - case 'darwin': - return `${homeDir}/Library/Application Support/New API/data`; - case 'win32': { - const appData = process.env.APPDATA || `${homeDir}\\AppData\\Roaming`; - return `${appData}\\New API\\data`; - } - case 'linux': - return `${homeDir}/.config/New API/data`; - default: - return `${homeDir}/.new-api/data`; + // 如果主进程已设置真实路径,直接使用 + if (process.env.ELECTRON_DATA_DIR) { + return process.env.ELECTRON_DATA_DIR; } } From 74f93d41f3755dc3843d40dee1397599066f046b Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 19:33:47 +0800 Subject: [PATCH 36/79] feat: update Gemini API response handling to include block reason and improve error reporting --- dto/gemini.go | 7 ++++--- relay/channel/gemini/relay-gemini.go | 7 ++++++- service/convert.go | 6 ------ types/error.go | 1 + 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/dto/gemini.go b/dto/gemini.go index fdeb2793d1a8..feee333cb6c0 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -293,12 +293,13 @@ type GeminiChatSafetyRating struct { type GeminiChatPromptFeedback struct { SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"` + BlockReason *string `json:"blockReason,omitempty"` } type GeminiChatResponse struct { - Candidates []GeminiChatCandidate `json:"candidates"` - PromptFeedback GeminiChatPromptFeedback `json:"promptFeedback"` - UsageMetadata GeminiUsageMetadata `json:"usageMetadata"` + Candidates []GeminiChatCandidate `json:"candidates"` + PromptFeedback *GeminiChatPromptFeedback `json:"promptFeedback,omitempty"` + UsageMetadata GeminiUsageMetadata `json:"usageMetadata"` } type GeminiUsageMetadata struct { diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index fa932e51ea7a..a8247217b5a7 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -1050,7 +1050,12 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } if len(geminiResponse.Candidates) == 0 { - return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + //return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { + return nil, types.NewOpenAIError(errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest) + } else { + return nil, types.NewOpenAIError(errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + } } fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse) fullTextResponse.Model = info.UpstreamModelName diff --git a/service/convert.go b/service/convert.go index b232ca396120..1a39e537a8eb 100644 --- a/service/convert.go +++ b/service/convert.go @@ -636,9 +636,6 @@ func extractTextFromGeminiParts(parts []dto.GeminiPart) string { func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.GeminiChatResponse { geminiResponse := &dto.GeminiChatResponse{ Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)), - PromptFeedback: dto.GeminiChatPromptFeedback{ - SafetyRatings: []dto.GeminiChatSafetyRating{}, - }, UsageMetadata: dto.GeminiUsageMetadata{ PromptTokenCount: openAIResponse.PromptTokens, CandidatesTokenCount: openAIResponse.CompletionTokens, @@ -735,9 +732,6 @@ func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamRespon geminiResponse := &dto.GeminiChatResponse{ Candidates: make([]dto.GeminiChatCandidate, 0, len(openAIResponse.Choices)), - PromptFeedback: dto.GeminiChatPromptFeedback{ - SafetyRatings: []dto.GeminiChatSafetyRating{}, - }, UsageMetadata: dto.GeminiUsageMetadata{ PromptTokenCount: info.PromptTokens, CandidatesTokenCount: 0, // 流式响应中可能没有完整的 usage 信息 diff --git a/types/error.go b/types/error.go index a42e8438575c..72f26ff3070a 100644 --- a/types/error.go +++ b/types/error.go @@ -69,6 +69,7 @@ const ( ErrorCodeEmptyResponse ErrorCode = "empty_response" ErrorCodeAwsInvokeError ErrorCode = "aws_invoke_error" ErrorCodeModelNotFound ErrorCode = "model_not_found" + ErrorCodePromptBlocked ErrorCode = "prompt_blocked" // sql error ErrorCodeQueryDataError ErrorCode = "query_data_error" From 98261ec9fa14e2a0b7532ce18c207922636f7e45 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 19:40:31 +0800 Subject: [PATCH 37/79] chore: update README files --- README.en.md | 30 +++++++++++++++--------------- README.fr.md | 30 +++++++++++++++--------------- README.ja.md | 18 ++++++++++-------- README.md | 22 ++++++++++++---------- 4 files changed, 52 insertions(+), 48 deletions(-) diff --git a/README.en.md b/README.en.md index 6648a733309a..54f4274a57bb 100644 --- a/README.en.md +++ b/README.en.md @@ -89,22 +89,23 @@ New API offers a wide range of features, please refer to [Features Introduction] 10. 🤖 Support for more authorization login methods (LinuxDO, Telegram, OIDC) 11. 🔄 Support for Rerank models (Cohere and Jina), [API Documentation](https://docs.newapi.pro/api/jinaai-rerank) 12. ⚡ Support for OpenAI Realtime API (including Azure channels), [API Documentation](https://docs.newapi.pro/api/openai-realtime) -13. ⚡ Support for Claude Messages format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat) -14. Support for entering chat interface via /chat2link route -15. 🧠 Support for setting reasoning effort through model name suffixes: +13. ⚡ Support for **OpenAI Responses** format, [API Documentation](https://docs.newapi.pro/api/openai-responses) +14. ⚡ Support for **Claude Messages** format, [API Documentation](https://docs.newapi.pro/api/anthropic-chat) +15. ⚡ Support for **Google Gemini** format, [API Documentation](https://docs.newapi.pro/api/google-gemini-chat/) +16. 🧠 Support for setting reasoning effort through model name suffixes: 1. OpenAI o-series models - Add `-high` suffix for high reasoning effort (e.g.: `o3-mini-high`) - Add `-medium` suffix for medium reasoning effort (e.g.: `o3-mini-medium`) - Add `-low` suffix for low reasoning effort (e.g.: `o3-mini-low`) 2. Claude thinking models - Add `-thinking` suffix to enable thinking mode (e.g.: `claude-3-7-sonnet-20250219-thinking`) -16. 🔄 Thinking-to-content functionality -17. 🔄 Model rate limiting for users -18. 🔄 Request format conversion functionality, supporting the following three format conversions: +17. 🔄 Thinking-to-content functionality +18. 🔄 Model rate limiting for users +19. 🔄 Request format conversion functionality, supporting the following three format conversions: 1. OpenAI Chat Completions => Claude Messages 2. Claude Messages => OpenAI Chat Completions (can be used for Claude Code to call third-party models) 3. OpenAI Chat Completions => Gemini Chat -19. 💰 Cache billing support, which allows billing at a set ratio when cache is hit: +20. 💰 Cache billing support, which allows billing at a set ratio when cache is hit: 1. Set the `Prompt Cache Ratio` option in `System Settings-Operation Settings` 2. Set `Prompt Cache Ratio` in the channel, range 0-1, e.g., setting to 0.5 means billing at 50% when cache is hit 3. Supported channels: @@ -134,14 +135,12 @@ For detailed configuration instructions, please refer to [Installation Guide-Env - `GENERATE_DEFAULT_TOKEN`: Whether to generate initial tokens for newly registered users, default is `false` - `STREAMING_TIMEOUT`: Streaming response timeout, default is 300 seconds - `DIFY_DEBUG`: Whether to output workflow and node information for Dify channels, default is `true` -- `FORCE_STREAM_OPTION`: Whether to override client stream_options parameter, default is `true` - `GET_MEDIA_TOKEN`: Whether to count image tokens, default is `true` - `GET_MEDIA_TOKEN_NOT_STREAM`: Whether to count image tokens in non-streaming cases, default is `true` - `UPDATE_TASK`: Whether to update asynchronous tasks (Midjourney, Suno), default is `true` -- `COHERE_SAFETY_SETTING`: Cohere model safety settings, options are `NONE`, `CONTEXTUAL`, `STRICT`, default is `NONE` - `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16` - `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20` -- `CRYPTO_SECRET`: Encryption key used for encrypting database content +- `CRYPTO_SECRET`: Encryption key used for encrypting Redis database content - `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview` - `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes - `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2` @@ -188,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ``` ## Channel Retry and Cache -Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings`. It is **recommended to enable caching**. +Channel retry functionality has been implemented, you can set the number of retries in `Settings->Operation Settings->General Settings->Failure Retry Count`, **recommended to enable caching** functionality. ### Cache Configuration Method 1. `REDIS_CONN_STRING`: Set Redis as cache @@ -198,10 +197,11 @@ Channel retry functionality has been implemented, you can set the number of retr For detailed API documentation, please refer to [API Documentation](https://docs.newapi.pro/api): -- [Chat API](https://docs.newapi.pro/api/openai-chat) -- [Image API](https://docs.newapi.pro/api/openai-image) -- [Rerank API](https://docs.newapi.pro/api/jinaai-rerank) -- [Realtime API](https://docs.newapi.pro/api/openai-realtime) +- [Chat API (Chat Completions)](https://docs.newapi.pro/api/openai-chat) +- [Response API (Responses)](https://docs.newapi.pro/api/openai-responses) +- [Image API (Image)](https://docs.newapi.pro/api/openai-image) +- [Rerank API (Rerank)](https://docs.newapi.pro/api/jinaai-rerank) +- [Realtime Chat API (Realtime)](https://docs.newapi.pro/api/openai-realtime) - [Claude Chat API](https://docs.newapi.pro/api/anthropic-chat) - [Google Gemini Chat API](https://docs.newapi.pro/api/google-gemini-chat) diff --git a/README.fr.md b/README.fr.md index 65dd458d6684..b308660cf6ac 100644 --- a/README.fr.md +++ b/README.fr.md @@ -89,22 +89,23 @@ New API offre un large éventail de fonctionnalités, veuillez vous référer à 10. 🤖 Prise en charge de plus de méthodes de connexion par autorisation (LinuxDO, Telegram, OIDC) 11. 🔄 Prise en charge des modèles Rerank (Cohere et Jina), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank) 12. ⚡ Prise en charge de l'API OpenAI Realtime (y compris les canaux Azure), [Documentation de l'API](https://docs.newapi.pro/api/openai-realtime) -13. ⚡ Prise en charge du format Claude Messages, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat) -14. Prise en charge de l'accès à l'interface de discussion via la route /chat2link -15. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle : +13. ⚡ Prise en charge du format **OpenAI Responses**, [Documentation de l'API](https://docs.newapi.pro/api/openai-responses) +14. ⚡ Prise en charge du format **Claude Messages**, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat) +15. ⚡ Prise en charge du format **Google Gemini**, [Documentation de l'API](https://docs.newapi.pro/api/google-gemini-chat/) +16. 🧠 Prise en charge de la définition de l'effort de raisonnement via les suffixes de nom de modèle : 1. Modèles de la série o d'OpenAI - Ajouter le suffixe `-high` pour un effort de raisonnement élevé (par exemple : `o3-mini-high`) - Ajouter le suffixe `-medium` pour un effort de raisonnement moyen (par exemple : `o3-mini-medium`) - Ajouter le suffixe `-low` pour un effort de raisonnement faible (par exemple : `o3-mini-low`) 2. Modèles de pensée de Claude - Ajouter le suffixe `-thinking` pour activer le mode de pensée (par exemple : `claude-3-7-sonnet-20250219-thinking`) -16. 🔄 Fonctionnalité de la pensée au contenu -17. 🔄 Limitation du débit du modèle pour les utilisateurs -18. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes : +17. 🔄 Fonctionnalité de la pensée au contenu +18. 🔄 Limitation du débit du modèle pour les utilisateurs +19. 🔄 Fonctionnalité de conversion de format de requête, prenant en charge les trois conversions de format suivantes : 1. OpenAI Chat Completions => Claude Messages 2. Claude Messages => OpenAI Chat Completions (peut être utilisé pour Claude Code pour appeler des modèles tiers) 3. OpenAI Chat Completions => Gemini Chat -19. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint : +20. 💰 Prise en charge de la facturation du cache, qui permet de facturer à un ratio défini lorsque le cache est atteint : 1. Définir l'option `Ratio de cache d'invite` dans `Paramètres système->Paramètres de fonctionnement` 2. Définir le `Ratio de cache d'invite` dans le canal, plage de 0 à 1, par exemple, le définir sur 0,5 signifie facturer à 50 % lorsque le cache est atteint 3. Canaux pris en charge : @@ -134,14 +135,12 @@ Pour des instructions de configuration détaillées, veuillez vous référer à - `GENERATE_DEFAULT_TOKEN` : S'il faut générer des jetons initiaux pour les utilisateurs nouvellement enregistrés, la valeur par défaut est `false` - `STREAMING_TIMEOUT` : Délai d'expiration de la réponse en streaming, la valeur par défaut est de 300 secondes - `DIFY_DEBUG` : S'il faut afficher les informations sur le flux de travail et les nœuds pour les canaux Dify, la valeur par défaut est `true` -- `FORCE_STREAM_OPTION` : S'il faut remplacer le paramètre client stream_options, la valeur par défaut est `true` - `GET_MEDIA_TOKEN` : S'il faut compter les jetons d'image, la valeur par défaut est `true` - `GET_MEDIA_TOKEN_NOT_STREAM` : S'il faut compter les jetons d'image dans les cas sans streaming, la valeur par défaut est `true` - `UPDATE_TASK` : S'il faut mettre à jour les tâches asynchrones (Midjourney, Suno), la valeur par défaut est `true` -- `COHERE_SAFETY_SETTING` : Paramètres de sécurité du modèle Cohere, les options sont `NONE`, `CONTEXTUAL`, `STRICT`, la valeur par défaut est `NONE` - `GEMINI_VISION_MAX_IMAGE_NUM` : Nombre maximum d'images pour les modèles Gemini, la valeur par défaut est `16` - `MAX_FILE_DOWNLOAD_MB` : Taille maximale de téléchargement de fichier en Mo, la valeur par défaut est `20` -- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données +- `CRYPTO_SECRET` : Clé de chiffrement utilisée pour chiffrer le contenu de la base de données Redis - `AZURE_DEFAULT_API_VERSION` : Version de l'API par défaut du canal Azure, la valeur par défaut est `2025-04-01-preview` - `NOTIFICATION_LIMIT_DURATION_MINUTE` : Durée de la limite de notification, la valeur par défaut est de `10` minutes - `NOTIFY_LIMIT_COUNT` : Nombre maximal de notifications utilisateur dans la durée spécifiée, la valeur par défaut est `2` @@ -188,7 +187,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ``` ## Nouvelle tentative de canal et cache -La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux`. Il est **recommandé d'activer la mise en cache**. +La fonctionnalité de nouvelle tentative de canal a été implémentée, vous pouvez définir le nombre de tentatives dans `Paramètres->Paramètres de fonctionnement->Paramètres généraux->Nombre de tentatives en cas d'échec`, **recommandé d'activer la fonctionnalité de mise en cache**. ### Méthode de configuration du cache 1. `REDIS_CONN_STRING` : Définir Redis comme cache @@ -198,10 +197,11 @@ La fonctionnalité de nouvelle tentative de canal a été implémentée, vous po Pour une documentation détaillée de l'API, veuillez vous référer à [Documentation de l'API](https://docs.newapi.pro/api) : -- [API de discussion](https://docs.newapi.pro/api/openai-chat) -- [API d'image](https://docs.newapi.pro/api/openai-image) -- [API de rerank](https://docs.newapi.pro/api/jinaai-rerank) -- [API en temps réel](https://docs.newapi.pro/api/openai-realtime) +- [API de discussion (Chat Completions)](https://docs.newapi.pro/api/openai-chat) +- [API de réponse (Responses)](https://docs.newapi.pro/api/openai-responses) +- [API d'image (Image)](https://docs.newapi.pro/api/openai-image) +- [API de rerank (Rerank)](https://docs.newapi.pro/api/jinaai-rerank) +- [API de discussion en temps réel (Realtime)](https://docs.newapi.pro/api/openai-realtime) - [API de discussion Claude](https://docs.newapi.pro/api/anthropic-chat) - [API de discussion Google Gemini](https://docs.newapi.pro/api/google-gemini-chat) diff --git a/README.ja.md b/README.ja.md index 13049e86d7f8..315518c911ba 100644 --- a/README.ja.md +++ b/README.ja.md @@ -89,22 +89,23 @@ New APIは豊富な機能を提供しています。詳細な機能について 10. 🤖 より多くの認証ログイン方法をサポート(LinuxDO、Telegram、OIDC) 11. 🔄 Rerankモデルをサポート(CohereとJina)、[API ドキュメント](https://docs.newapi.pro/api/jinaai-rerank) 12. ⚡ OpenAI Realtime APIをサポート(Azureチャネルを含む)、[APIドキュメント](https://docs.newapi.pro/api/openai-realtime) -13. ⚡ Claude Messages形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat) -14. /chat2linkルートを使用してチャット画面に入ることをサポート -15. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート: +13. ⚡ **OpenAI Responses**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/openai-responses) +14. ⚡ **Claude Messages**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/anthropic-chat) +15. ⚡ **Google Gemini**形式をサポート、[APIドキュメント](https://docs.newapi.pro/api/google-gemini-chat/) +16. 🧠 モデル名のサフィックスを通じてreasoning effortを設定することをサポート: 1. OpenAI oシリーズモデル - `-high`サフィックスを追加してhigh reasoning effortに設定(例:`o3-mini-high`) - `-medium`サフィックスを追加してmedium reasoning effortに設定(例:`o3-mini-medium`) - `-low`サフィックスを追加してlow reasoning effortに設定(例:`o3-mini-low`) 2. Claude思考モデル - `-thinking`サフィックスを追加して思考モードを有効にする(例:`claude-3-7-sonnet-20250219-thinking`) -16. 🔄 思考からコンテンツへの機能 -17. 🔄 ユーザーに対するモデルレート制限機能 -18. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート: +17. 🔄 思考からコンテンツへの機能 +18. 🔄 ユーザーに対するモデルレート制限機能 +19. 🔄 リクエストフォーマット変換機能、以下の3つのフォーマット変換をサポート: 1. OpenAI Chat Completions => Claude Messages 2. Claude Messages => OpenAI Chat Completions(Claude Codeがサードパーティモデルを呼び出す際に使用可能) 3. OpenAI Chat Completions => Gemini Chat -19. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます: +20. 💰 キャッシュ課金サポート、有効にするとキャッシュがヒットした際に設定された比率で課金できます: 1. `システム設定-運営設定`で`プロンプトキャッシュ倍率`オプションを設定 2. チャネルで`プロンプトキャッシュ倍率`を設定、範囲は0-1、例えば0.5に設定するとキャッシュがヒットした際に50%で課金 3. サポートされているチャネル: @@ -196,7 +197,8 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 詳細なAPIドキュメントについては[APIドキュメント](https://docs.newapi.pro/api)を参照してください: -- [チャットインターフェース(Chat)](https://docs.newapi.pro/api/openai-chat) +- [チャットインターフェース(Chat Completions)](https://docs.newapi.pro/api/openai-chat) +- [レスポンスインターフェース(Responses)](https://docs.newapi.pro/api/openai-responses) - [画像インターフェース(Image)](https://docs.newapi.pro/api/openai-image) - [再ランク付けインターフェース(Rerank)](https://docs.newapi.pro/api/jinaai-rerank) - [リアルタイム対話インターフェース(Realtime)](https://docs.newapi.pro/api/openai-realtime) diff --git a/README.md b/README.md index af2b64b44427..feeff86f555a 100644 --- a/README.md +++ b/README.md @@ -85,22 +85,23 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do 10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC) 11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank) 12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime) -13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat) -14. 支持使用路由/chat2link进入聊天界面 -15. 🧠 支持通过模型名称后缀设置 reasoning effort: +13. ⚡ 支持 **OpenAI Responses** 格式,[接口文档](https://docs.newapi.pro/api/openai-responses) +14. ⚡ 支持 **Claude Messages** 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat) +15. ⚡ 支持 **Google Gemini** 格式,[接口文档](https://docs.newapi.pro/api/google-gemini-chat/) +16. 🧠 支持通过模型名称后缀设置 reasoning effort: 1. OpenAI o系列模型 - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`) - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`) - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`) 2. Claude 思考模型 - 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`) -16. 🔄 思考转内容功能 -17. 🔄 针对用户的模型限流功能 -18. 🔄 请求格式转换功能,支持以下三种格式转换: - 1. OpenAI Chat Completions => Claude Messages +17. 🔄 思考转内容功能 +18. 🔄 针对用户的模型限流功能 +19. 🔄 请求格式转换功能,支持以下三种格式转换: + 1. OpenAI Chat Completions => Claude Messages (OpenAI格式调用Claude模型) 2. Clade Messages => OpenAI Chat Completions (可用于Claude Code调用第三方模型) - 3. OpenAI Chat Completions => Gemini Chat -19. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费: + 3. OpenAI Chat Completions => Gemini Chat (OpenAI格式调用Gemini模型) +20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费: 1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项 2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费 3. 支持的渠道: @@ -192,7 +193,8 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 详细接口文档请参考[接口文档](https://docs.newapi.pro/api): -- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat) +- [聊天接口(Chat Completions)](https://docs.newapi.pro/api/openai-chat) +- [响应接口 (Responses)](https://docs.newapi.pro/api/openai-responses) - [图像接口(Image)](https://docs.newapi.pro/api/openai-image) - [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank) - [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime) From 407da544fe6927f3d728d42f9f858ca67174600c Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 22:51:28 +0800 Subject: [PATCH 38/79] feat: enhance SettingsLog component with confirmation modal for log deletion and improve user feedback --- .../pages/Setting/Operation/SettingsLog.jsx | 96 +++++++++++++++---- 1 file changed, 76 insertions(+), 20 deletions(-) diff --git a/web/src/pages/Setting/Operation/SettingsLog.jsx b/web/src/pages/Setting/Operation/SettingsLog.jsx index dcd17081fb91..067e968b60ab 100644 --- a/web/src/pages/Setting/Operation/SettingsLog.jsx +++ b/web/src/pages/Setting/Operation/SettingsLog.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; -import { Button, Col, Form, Row, Spin, DatePicker } from '@douyinfe/semi-ui'; +import { Button, Col, Form, Row, Spin, DatePicker, Typography, Modal } from '@douyinfe/semi-ui'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { @@ -29,6 +29,8 @@ import { showWarning, } from '../../../helpers'; +const { Text } = Typography; + export default function SettingsLog(props) { const { t } = useTranslation(); const [loading, setLoading] = useState(false); @@ -78,24 +80,75 @@ export default function SettingsLog(props) { }); } async function onCleanHistoryLog() { - try { - setLoadingCleanHistoryLog(true); - if (!inputs.historyTimestamp) throw new Error(t('请选择日志记录时间')); - const res = await API.delete( - `/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`, - ); - const { success, message, data } = res.data; - if (success) { - showSuccess(`${data} ${t('条日志已清理!')}`); - return; - } else { - throw new Error(t('日志清理失败:') + message); - } - } catch (error) { - showError(error.message); - } finally { - setLoadingCleanHistoryLog(false); + if (!inputs.historyTimestamp) { + showError(t('请选择日志记录时间')); + return; } + + const now = dayjs(); + const targetDate = dayjs(inputs.historyTimestamp); + const targetTime = targetDate.format('YYYY-MM-DD HH:mm:ss'); + const currentTime = now.format('YYYY-MM-DD HH:mm:ss'); + const daysDiff = now.diff(targetDate, 'day'); + + Modal.confirm({ + title: t('确认清除历史日志'), + content: ( +
+

+ {t('当前时间')}: + {currentTime} +

+

+ {t('选择时间')}: + {targetTime} + {daysDiff > 0 && ( + ({t('约')} {daysDiff} {t('天前')}) + )} +

+
+ ⚠️ {t('注意')}: + {t('将删除')} + {targetTime} + {daysDiff > 0 && ( + ({t('约')} {daysDiff} {t('天前')}) + )} + {t('之前的所有日志')} +
+

+ {t('此操作不可恢复,请仔细确认时间后再操作!')} +

+
+ ), + okText: t('确认删除'), + cancelText: t('取消'), + okType: 'danger', + onOk: async () => { + try { + setLoadingCleanHistoryLog(true); + const res = await API.delete( + `/api/log/?target_timestamp=${Date.parse(inputs.historyTimestamp) / 1000}`, + ); + const { success, message, data } = res.data; + if (success) { + showSuccess(`${data} ${t('条日志已清理!')}`); + return; + } else { + throw new Error(t('日志清理失败:') + message); + } + } catch (error) { + showError(error.message); + } finally { + setLoadingCleanHistoryLog(false); + } + }, + }); } useEffect(() => { @@ -138,7 +191,7 @@ export default function SettingsLog(props) { - From bb0ed4dddf1b898f34d0be4fa828ee40524d53a7 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Oct 2025 23:11:45 +0800 Subject: [PATCH 39/79] feat: implement user preferences management in SettingsLog component for improved customization --- .github/workflows/sync-to-gitee.yml | 91 +++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .github/workflows/sync-to-gitee.yml diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml new file mode 100644 index 000000000000..a5cd1a2967ad --- /dev/null +++ b/.github/workflows/sync-to-gitee.yml @@ -0,0 +1,91 @@ +name: Sync Release to Gitee + +permissions: + contents: read + +on: + workflow_dispatch: + inputs: + tag_name: + description: 'Release Tag to sync (e.g. v1.0.0)' + required: true + type: string + +# 配置你的 Gitee 仓库信息 +env: + GITEE_OWNER: 'QuantumNous' # 修改为你的 Gitee 用户名 + GITEE_REPO: 'new-api' # 修改为你的 Gitee 仓库名 + +jobs: + sync-to-gitee: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Get Release Info + id: release_info + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ github.event.inputs.tag_name }} + run: | + # 获取 release 信息 + RELEASE_INFO=$(gh release view "$TAG_NAME" --json name,body,tagName,targetCommitish) + + RELEASE_NAME=$(echo "$RELEASE_INFO" | jq -r '.name') + TARGET_COMMITISH=$(echo "$RELEASE_INFO" | jq -r '.targetCommitish') + + # 使用多行字符串输出 + { + echo "release_name=$RELEASE_NAME" + echo "target_commitish=$TARGET_COMMITISH" + echo "release_body<> $GITHUB_OUTPUT + + # 下载 release 的所有附件 + gh release download "$TAG_NAME" --dir ./release_assets || echo "No assets to download" + + # 列出下载的文件 + ls -la ./release_assets/ || echo "No assets directory" + + - name: Create Gitee Release + id: create_release + uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 + with: + gitee_action: create_release + gitee_owner: ${{ env.GITEE_OWNER }} + gitee_repo: ${{ env.GITEE_REPO }} + gitee_token: ${{ secrets.GITEE_TOKEN }} + gitee_tag_name: ${{ github.event.inputs.tag_name }} + gitee_release_name: ${{ steps.release_info.outputs.release_name }} + gitee_release_body: ${{ steps.release_info.outputs.release_body }} + gitee_target_commitish: ${{ steps.release_info.outputs.target_commitish }} + + - name: Upload Assets to Gitee + if: hashFiles('release_assets/*') != '' + uses: nICEnnnnnnnLee/action-gitee-release@v2.0.0 + with: + gitee_action: upload_asset + gitee_owner: ${{ env.GITEE_OWNER }} + gitee_repo: ${{ env.GITEE_REPO }} + gitee_token: ${{ secrets.GITEE_TOKEN }} + gitee_release_id: ${{ steps.create_release.outputs.release-id }} + gitee_upload_retry_times: 3 + gitee_files: | + release_assets/* + + - name: Cleanup + if: always() + run: | + rm -rf release_assets/ + + - name: Summary + if: success() + run: | + echo "✅ Successfully synced release ${{ github.event.inputs.tag_name }} to Gitee!" + echo "🔗 Gitee Release URL: https://gitee.com/${{ env.GITEE_OWNER }}/${{ env.GITEE_REPO }}/releases/tag/${{ github.event.inputs.tag_name }}" + From ca9ee54fba02a1a1671778cf9cde7b9c18b7c43a Mon Sep 17 00:00:00 2001 From: RedwindA Date: Sun, 5 Oct 2025 23:39:20 +0800 Subject: [PATCH 40/79] fix: improve text visibility in warning box for dark mode in SettingsLog --- .../pages/Setting/Operation/SettingsLog.jsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/web/src/pages/Setting/Operation/SettingsLog.jsx b/web/src/pages/Setting/Operation/SettingsLog.jsx index 067e968b60ab..a1f5a49fa792 100644 --- a/web/src/pages/Setting/Operation/SettingsLog.jsx +++ b/web/src/pages/Setting/Operation/SettingsLog.jsx @@ -106,20 +106,21 @@ export default function SettingsLog(props) { ({t('约')} {daysDiff} {t('天前')}) )}

-
- ⚠️ {t('注意')}: - {t('将删除')} - {targetTime} + ⚠️ {t('注意')}: + {t('将删除')} + {targetTime} {daysDiff > 0 && ( - ({t('约')} {daysDiff} {t('天前')}) + ({t('约')} {daysDiff} {t('天前')}) )} - {t('之前的所有日志')} + {t('之前的所有日志')}

{t('此操作不可恢复,请仔细确认时间后再操作!')} From c24608730bff157f50f89f17ff46839148f039f5 Mon Sep 17 00:00:00 2001 From: CaIon Date: Mon, 6 Oct 2025 14:33:48 +0800 Subject: [PATCH 41/79] CI --- .github/workflows/sync-to-gitee.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml index a5cd1a2967ad..4f515a188dbe 100644 --- a/.github/workflows/sync-to-gitee.yml +++ b/.github/workflows/sync-to-gitee.yml @@ -18,7 +18,7 @@ env: jobs: sync-to-gitee: - runs-on: ubuntu-latest + runs-on: sync steps: - name: Checkout uses: actions/checkout@v3 From 6ef95c97cc0b8f3a115e8bfddcd6d556575f4088 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 7 Oct 2025 00:22:45 +0800 Subject: [PATCH 42/79] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20topup=20billing?= =?UTF-8?q?=20history=20with=20admin=20manual=20completion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement comprehensive topup billing system with user history viewing and admin management capabilities. ## Features Added ### Frontend - Add topup history modal with paginated billing records - Display order details: trade number, payment method, amount, money, status, create time - Implement empty state with proper illustrations - Add payment method column with localized display (Stripe, Alipay, WeChat) - Add admin manual completion feature for pending orders - Add Coins icon for recharge amount display - Integrate "Bills" button in RechargeCard header - Optimize code quality by using shared utility functions (isAdmin) - Extract constants for status and payment method mappings - Use React.useMemo for performance optimization ### Backend - Create GET `/api/user/topup/self` endpoint for user topup history with pagination - Create POST `/api/user/topup/complete` endpoint for admin manual order completion - Add `payment_method` field to TopUp model for tracking payment types - Implement `GetUserTopUps` method with proper pagination and ordering - Implement `ManualCompleteTopUp` with transaction safety and row-level locking - Add application-level mutex locks to prevent concurrent order processing - Record payment method in Epay and Stripe payment flows - Ensure idempotency and data consistency with proper error handling ### Internationalization - Add i18n keys for Chinese (zh), English (en), and French (fr) - Support for billing-related UI text and status messages ## Technical Improvements - Use database transactions with FOR UPDATE row-level locking - Implement sync.Map-based mutex for order-level concurrency control - Proper error handling and user-friendly toast notifications - Follow existing codebase patterns for empty states and modals - Maintain code quality with extracted render functions and constants ## Files Changed - Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go - Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx - i18n: web/src/i18n/locales/{zh,en,fr}.json --- controller/topup.go | 51 +- controller/topup_stripe.go | 13 +- model/topup.go | 124 +- router/api-router.go | 2 + web/src/components/auth/LoginForm.jsx | 20 +- .../common/examples/ChannelKeyViewExample.jsx | 10 +- .../common/modals/SecureVerificationModal.jsx | 143 +- .../components/settings/PersonalSetting.jsx | 13 +- web/src/components/settings/SystemSetting.jsx | 50 +- .../personal/cards/AccountManagement.jsx | 10 +- .../personal/cards/NotificationSettings.jsx | 8 +- .../setup/components/steps/DatabaseStep.jsx | 5 +- .../channels/modals/EditChannelModal.jsx | 2333 +++++++++-------- .../table/channels/modals/EditTagModal.jsx | 15 +- .../table/channels/modals/ModelTestModal.jsx | 18 +- .../table/users/UsersColumnDefs.jsx | 12 +- .../table/users/modals/ResetPasskeyModal.jsx | 5 +- .../table/users/modals/ResetTwoFAModal.jsx | 9 +- web/src/components/topup/RechargeCard.jsx | 56 +- web/src/components/topup/index.jsx | 20 + .../topup/modals/TopupHistoryModal.jsx | 253 ++ web/src/constants/channel.constants.js | 2 +- web/src/helpers/passkey.js | 58 +- web/src/helpers/render.jsx | 206 +- web/src/helpers/secureApiCall.js | 6 +- web/src/hooks/channels/useChannelsData.jsx | 2 +- .../hooks/common/useSecureVerification.jsx | 222 +- web/src/hooks/users/useUsersData.jsx | 2 +- web/src/i18n/locales/en.json | 17 +- web/src/i18n/locales/fr.json | 15 +- web/src/i18n/locales/zh.json | 19 +- web/src/pages/Setting/Chat/SettingsChats.jsx | 2 +- .../pages/Setting/Operation/SettingsLog.jsx | 73 +- web/src/services/secureVerification.js | 105 +- 34 files changed, 2341 insertions(+), 1558 deletions(-) create mode 100644 web/src/components/topup/modals/TopupHistoryModal.jsx diff --git a/controller/topup.go b/controller/topup.go index 7e2cadf1d2a0..e6f6eef2ddd8 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -183,12 +183,13 @@ func RequestEpay(c *gin.Context) { amount = dAmount.Div(dQuotaPerUnit).IntPart() } topUp := &model.TopUp{ - UserId: id, - Amount: amount, - Money: payMoney, - TradeNo: tradeNo, - CreateTime: time.Now().Unix(), - Status: "pending", + UserId: id, + Amount: amount, + Money: payMoney, + TradeNo: tradeNo, + PaymentMethod: req.PaymentMethod, + CreateTime: time.Now().Unix(), + Status: "pending", } err = topUp.Insert() if err != nil { @@ -313,3 +314,41 @@ func RequestAmount(c *gin.Context) { } c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)}) } + +func GetUserTopUps(c *gin.Context) { + userId := c.GetInt("id") + pageInfo := common.GetPageQuery(c) + + topups, total, err := model.GetUserTopUps(userId, pageInfo) + if err != nil { + common.ApiError(c, err) + return + } + + pageInfo.SetTotal(int(total)) + pageInfo.SetItems(topups) + common.ApiSuccess(c, pageInfo) +} + +type AdminCompleteTopupRequest struct { + TradeNo string `json:"trade_no"` +} + +// AdminCompleteTopUp 管理员补单接口 +func AdminCompleteTopUp(c *gin.Context) { + var req AdminCompleteTopupRequest + if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" { + common.ApiErrorMsg(c, "参数错误") + return + } + + // 订单级互斥,防止并发补单 + LockOrder(req.TradeNo) + defer UnlockOrder(req.TradeNo) + + if err := model.ManualCompleteTopUp(req.TradeNo); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, nil) +} diff --git a/controller/topup_stripe.go b/controller/topup_stripe.go index 628a3fea58f9..a4bdf064d341 100644 --- a/controller/topup_stripe.go +++ b/controller/topup_stripe.go @@ -83,12 +83,13 @@ func (*StripeAdaptor) RequestPay(c *gin.Context, req *StripePayRequest) { } topUp := &model.TopUp{ - UserId: id, - Amount: req.Amount, - Money: chargedMoney, - TradeNo: referenceId, - CreateTime: time.Now().Unix(), - Status: common.TopUpStatusPending, + UserId: id, + Amount: req.Amount, + Money: chargedMoney, + TradeNo: referenceId, + PaymentMethod: PaymentMethodStripe, + CreateTime: time.Now().Unix(), + Status: common.TopUpStatusPending, } err = topUp.Insert() if err != nil { diff --git a/model/topup.go b/model/topup.go index 802c866f7718..c280db430931 100644 --- a/model/topup.go +++ b/model/topup.go @@ -6,18 +6,20 @@ import ( "one-api/common" "one-api/logger" + "github.com/shopspring/decimal" "gorm.io/gorm" ) type TopUp struct { - Id int `json:"id"` - UserId int `json:"user_id" gorm:"index"` - Amount int64 `json:"amount"` - Money float64 `json:"money"` - TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"` - CreateTime int64 `json:"create_time"` - CompleteTime int64 `json:"complete_time"` - Status string `json:"status"` + Id int `json:"id"` + UserId int `json:"user_id" gorm:"index"` + Amount int64 `json:"amount"` + Money float64 `json:"money"` + TradeNo string `json:"trade_no" gorm:"unique;type:varchar(255);index"` + PaymentMethod string `json:"payment_method" gorm:"type:varchar(50)"` + CreateTime int64 `json:"create_time"` + CompleteTime int64 `json:"complete_time"` + Status string `json:"status"` } func (topUp *TopUp) Insert() error { @@ -99,3 +101,109 @@ func Recharge(referenceId string, customerId string) (err error) { return nil } + +func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + // Start transaction + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // Get total count within transaction + err = tx.Model(&TopUp{}).Where("user_id = ?", userId).Count(&total).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Get paginated topups within same transaction + err = tx.Where("user_id = ?", userId).Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error + if err != nil { + tx.Rollback() + return nil, 0, err + } + + // Commit transaction + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + +// ManualCompleteTopUp 管理员手动完成订单并给用户充值 +func ManualCompleteTopUp(tradeNo string) error { + if tradeNo == "" { + return errors.New("未提供订单号") + } + + refCol := "`trade_no`" + if common.UsingPostgreSQL { + refCol = `"trade_no"` + } + + var userId int + var quotaToAdd int + var payMoney float64 + + err := DB.Transaction(func(tx *gorm.DB) error { + topUp := &TopUp{} + // 行级锁,避免并发补单 + if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil { + return errors.New("充值订单不存在") + } + + // 幂等处理:已成功直接返回 + if topUp.Status == common.TopUpStatusSuccess { + return nil + } + + if topUp.Status != common.TopUpStatusPending { + return errors.New("订单状态不是待支付,无法补单") + } + + // 计算应充值额度: + // - Stripe 订单:Money 代表经分组倍率换算后的美元数量,直接 * QuotaPerUnit + // - 其他订单(如易支付):Amount 为美元数量,* QuotaPerUnit + if topUp.PaymentMethod == "stripe" { + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + quotaToAdd = int(decimal.NewFromFloat(topUp.Money).Mul(dQuotaPerUnit).IntPart()) + } else { + dAmount := decimal.NewFromInt(topUp.Amount) + dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) + quotaToAdd = int(dAmount.Mul(dQuotaPerUnit).IntPart()) + } + if quotaToAdd <= 0 { + return errors.New("无效的充值额度") + } + + // 标记完成 + topUp.CompleteTime = common.GetTimestamp() + topUp.Status = common.TopUpStatusSuccess + if err := tx.Save(topUp).Error; err != nil { + return err + } + + // 增加用户额度(立即写库,保持一致性) + if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil { + return err + } + + userId = topUp.UserId + payMoney = topUp.Money + return nil + }) + + if err != nil { + return err + } + + // 事务外记录日志,避免阻塞 + RecordLog(userId, LogTypeTopup, fmt.Sprintf("管理员补单成功,充值金额: %v,支付金额:%f", logger.FormatQuota(quotaToAdd), payMoney)) + return nil +} diff --git a/router/api-router.go b/router/api-router.go index d296159146dd..49a42360270f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -73,12 +73,14 @@ func SetApiRouter(router *gin.Engine) { selfRoute.DELETE("/passkey", controller.PasskeyDelete) selfRoute.GET("/aff", controller.GetAffCode) selfRoute.GET("/topup/info", controller.GetTopUpInfo) + selfRoute.GET("/topup/self", controller.GetUserTopUps) selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp) selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay) selfRoute.POST("/amount", controller.RequestAmount) selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay) selfRoute.POST("/stripe/amount", controller.RequestStripeAmount) selfRoute.POST("/aff_transfer", controller.TransferAffQuota) + selfRoute.POST("/topup/complete", middleware.AdminAuth(), controller.AdminCompleteTopUp) selfRoute.PUT("/setting", controller.UpdateUserSetting) // 2FA routes diff --git a/web/src/components/auth/LoginForm.jsx b/web/src/components/auth/LoginForm.jsx index 828e7178651d..fea4e39ba2f6 100644 --- a/web/src/components/auth/LoginForm.jsx +++ b/web/src/components/auth/LoginForm.jsx @@ -42,7 +42,12 @@ import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import TelegramLoginButton from 'react-telegram-login'; -import { IconGithubLogo, IconMail, IconLock, IconKey } from '@douyinfe/semi-icons'; +import { + IconGithubLogo, + IconMail, + IconLock, + IconKey, +} from '@douyinfe/semi-icons'; import OIDCIcon from '../common/logo/OIDCIcon'; import WeChatIcon from '../common/logo/WeChatIcon'; import LinuxDoIcon from '../common/logo/LinuxDoIcon'; @@ -296,15 +301,22 @@ const LoginForm = () => { return; } - const publicKeyOptions = prepareCredentialRequestOptions(data?.options || data?.publicKey || data); - const assertion = await navigator.credentials.get({ publicKey: publicKeyOptions }); + const publicKeyOptions = prepareCredentialRequestOptions( + data?.options || data?.publicKey || data, + ); + const assertion = await navigator.credentials.get({ + publicKey: publicKeyOptions, + }); const payload = buildAssertionResult(assertion); if (!payload) { showError('Passkey 验证失败,请重试'); return; } - const finishRes = await API.post('/api/user/passkey/login/finish', payload); + const finishRes = await API.post( + '/api/user/passkey/login/finish', + payload, + ); const finish = finishRes.data; if (finish.success) { userDispatch({ type: 'login', payload: finish.data }); diff --git a/web/src/components/common/examples/ChannelKeyViewExample.jsx b/web/src/components/common/examples/ChannelKeyViewExample.jsx index cd38775274db..1bb2998b208b 100644 --- a/web/src/components/common/examples/ChannelKeyViewExample.jsx +++ b/web/src/components/common/examples/ChannelKeyViewExample.jsx @@ -58,7 +58,7 @@ const ChannelKeyViewExample = ({ channelId }) => { // 开始查看密钥流程 const handleViewKey = async () => { const apiCall = createApiCalls.viewChannelKey(channelId); - + await startVerification(apiCall, { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), @@ -69,11 +69,7 @@ const ChannelKeyViewExample = ({ channelId }) => { return ( <> {/* 查看密钥按钮 */} - @@ -114,4 +110,4 @@ const ChannelKeyViewExample = ({ channelId }) => { ); }; -export default ChannelKeyViewExample; \ No newline at end of file +export default ChannelKeyViewExample; diff --git a/web/src/components/common/modals/SecureVerificationModal.jsx b/web/src/components/common/modals/SecureVerificationModal.jsx index 06f18c7e676e..6c61c291d1d6 100644 --- a/web/src/components/common/modals/SecureVerificationModal.jsx +++ b/web/src/components/common/modals/SecureVerificationModal.jsx @@ -19,7 +19,16 @@ For commercial licensing, please contact support@quantumnous.com import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Modal, Button, Input, Typography, Tabs, TabPane, Space, Spin } from '@douyinfe/semi-ui'; +import { + Modal, + Button, + Input, + Typography, + Tabs, + TabPane, + Space, + Spin, +} from '@douyinfe/semi-ui'; /** * 通用安全验证模态框组件 @@ -78,9 +87,7 @@ const SecureVerificationModal = ({ title={title || t('安全验证')} visible={visible} onCancel={onCancel} - footer={ - - } + footer={} width={500} style={{ maxWidth: '90vw' }} > @@ -123,21 +130,21 @@ const SecureVerificationModal = ({ width={460} centered style={{ - maxWidth: 'calc(100vw - 32px)' + maxWidth: 'calc(100vw - 32px)', }} bodyStyle={{ - padding: '20px 24px' + padding: '20px 24px', }} >

{/* 描述信息 */} {description && ( {description} @@ -153,10 +160,7 @@ const SecureVerificationModal = ({ style={{ margin: 0 }} > {has2FA && ( - +
- + + } style={{ width: '100%' }} @@ -178,24 +195,26 @@ const SecureVerificationModal = ({
{t('从认证器应用中获取验证码,或使用备用码')} -
+
@@ -214,31 +233,47 @@ const SecureVerificationModal = ({ )} {hasPasskey && passkeySupported && ( - +
-
-
- - +
+
+ +
- + {t('使用 Passkey 验证')} {t('点击验证按钮,使用您的生物特征或安全密钥')}
-
+
@@ -282,4 +319,4 @@ const SecureVerificationModal = ({ ); }; -export default SecureVerificationModal; \ No newline at end of file +export default SecureVerificationModal; diff --git a/web/src/components/settings/PersonalSetting.jsx b/web/src/components/settings/PersonalSetting.jsx index c9934604c211..18d3748019ea 100644 --- a/web/src/components/settings/PersonalSetting.jsx +++ b/web/src/components/settings/PersonalSetting.jsx @@ -155,9 +155,7 @@ const PersonalSetting = () => { gotifyUrl: settings.gotify_url || '', gotifyToken: settings.gotify_token || '', gotifyPriority: - settings.gotify_priority !== undefined - ? settings.gotify_priority - : 5, + settings.gotify_priority !== undefined ? settings.gotify_priority : 5, acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false, recordIpLog: settings.record_ip_log || false, @@ -214,7 +212,9 @@ const PersonalSetting = () => { return; } - const publicKey = prepareCredentialCreationOptions(data?.options || data?.publicKey || data); + const publicKey = prepareCredentialCreationOptions( + data?.options || data?.publicKey || data, + ); const credential = await navigator.credentials.create({ publicKey }); const payload = buildRegistrationResult(credential); if (!payload) { @@ -222,7 +222,10 @@ const PersonalSetting = () => { return; } - const finishRes = await API.post('/api/user/passkey/register/finish', payload); + const finishRes = await API.post( + '/api/user/passkey/register/finish', + payload, + ); if (finishRes.data.success) { showSuccess(t('Passkey 注册成功')); await loadPasskeyStatus(); diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index 780e89fb1c35..2f0b892ff601 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -615,7 +615,10 @@ const SystemSetting = () => { options.push({ key: 'passkey.rp_display_name', - value: formValues['passkey.rp_display_name'] || inputs['passkey.rp_display_name'] || '', + value: + formValues['passkey.rp_display_name'] || + inputs['passkey.rp_display_name'] || + '', }); options.push({ key: 'passkey.rp_id', @@ -623,11 +626,17 @@ const SystemSetting = () => { }); options.push({ key: 'passkey.user_verification', - value: formValues['passkey.user_verification'] || inputs['passkey.user_verification'] || 'preferred', + value: + formValues['passkey.user_verification'] || + inputs['passkey.user_verification'] || + 'preferred', }); options.push({ key: 'passkey.attachment_preference', - value: formValues['passkey.attachment_preference'] || inputs['passkey.attachment_preference'] || '', + value: + formValues['passkey.attachment_preference'] || + inputs['passkey.attachment_preference'] || + '', }); options.push({ key: 'passkey.origins', @@ -1044,7 +1053,9 @@ const SystemSetting = () => { {t('用以支持基于 WebAuthn 的无密码登录注册')} { field="['passkey.rp_display_name']" label={t('服务显示名称')} placeholder={t('默认使用系统名称')} - extraText={t('用户注册时看到的网站名称,比如\'我的网站\'')} + extraText={t( + "用户注册时看到的网站名称,比如'我的网站'", + )} /> @@ -1078,7 +1091,9 @@ const SystemSetting = () => { field="['passkey.rp_id']" label={t('网站域名标识')} placeholder={t('例如:example.com')} - extraText={t('留空则默认使用服务器地址,注意不能携带http://或者https://')} + extraText={t( + '留空则默认使用服务器地址,注意不能携带http://或者https://', + )} /> @@ -1092,7 +1107,10 @@ const SystemSetting = () => { label={t('安全验证级别')} placeholder={t('是否要求指纹/面容等生物识别')} optionList={[ - { label: t('推荐使用(用户可选)'), value: 'preferred' }, + { + label: t('推荐使用(用户可选)'), + value: 'preferred', + }, { label: t('强制要求'), value: 'required' }, { label: t('不建议使用'), value: 'discouraged' }, ]} @@ -1109,7 +1127,9 @@ const SystemSetting = () => { { label: t('本设备内置'), value: 'platform' }, { label: t('外接设备'), value: 'cross-platform' }, ]} - extraText={t('本设备:手机指纹/面容,外接:USB安全密钥')} + extraText={t( + '本设备:手机指纹/面容,外接:USB安全密钥', + )} /> @@ -1123,7 +1143,10 @@ const SystemSetting = () => { noLabel extraText={t('仅用于开发环境,生产环境应使用 HTTPS')} onChange={(e) => - handleCheckboxChange('passkey.allow_insecure_origin', e) + handleCheckboxChange( + 'passkey.allow_insecure_origin', + e, + ) } > {t('允许不安全的 Origin(HTTP)')} @@ -1139,11 +1162,16 @@ const SystemSetting = () => { field="['passkey.origins']" label={t('允许的 Origins')} placeholder={t('填写带https的域名,逗号分隔')} - extraText={t('为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https')} + extraText={t( + '为空则默认使用服务器地址,多个 Origin 用逗号分隔,例如 https://newapi.pro,https://newapi.com ,注意不能携带[],需使用https', + )} /> - diff --git a/web/src/components/settings/personal/cards/AccountManagement.jsx b/web/src/components/settings/personal/cards/AccountManagement.jsx index ac2146c2788d..d54edb93a7c8 100644 --- a/web/src/components/settings/personal/cards/AccountManagement.jsx +++ b/web/src/components/settings/personal/cards/AccountManagement.jsx @@ -535,7 +535,9 @@ const AccountManagement = ({ ? () => { Modal.confirm({ title: t('确认解绑 Passkey'), - content: t('解绑后将无法使用 Passkey 登录,确定要继续吗?'), + content: t( + '解绑后将无法使用 Passkey 登录,确定要继续吗?', + ), okText: t('确认解绑'), cancelText: t('取消'), okType: 'danger', @@ -547,7 +549,11 @@ const AccountManagement = ({ className={`w-full sm:w-auto ${passkeyEnabled ? '!bg-slate-500 hover:!bg-slate-600' : ''}`} icon={} disabled={!passkeySupported && !passkeyEnabled} - loading={passkeyEnabled ? passkeyDeleteLoading : passkeyRegisterLoading} + loading={ + passkeyEnabled + ? passkeyDeleteLoading + : passkeyRegisterLoading + } > {passkeyEnabled ? t('解绑 Passkey') : t('注册 Passkey')} diff --git a/web/src/components/settings/personal/cards/NotificationSettings.jsx b/web/src/components/settings/personal/cards/NotificationSettings.jsx index 0c99e28551ab..c19084a51587 100644 --- a/web/src/components/settings/personal/cards/NotificationSettings.jsx +++ b/web/src/components/settings/personal/cards/NotificationSettings.jsx @@ -621,7 +621,9 @@ const NotificationSettings = ({ }, { pattern: /^https?:\/\/.+/, - message: t('Gotify服务器地址必须以http://或https://开头'), + message: t( + 'Gotify服务器地址必须以http://或https://开头', + ), }, ]} /> @@ -678,9 +680,7 @@ const NotificationSettings = ({ '复制应用的令牌(Token)并填写到上方的应用令牌字段', )}
-
- 3. {t('填写Gotify服务器的完整URL地址')} -
+
3. {t('填写Gotify服务器的完整URL地址')}
{t('更多信息请参考')} diff --git a/web/src/components/setup/components/steps/DatabaseStep.jsx b/web/src/components/setup/components/steps/DatabaseStep.jsx index 66923f4450da..d8d1d4f9f5ad 100644 --- a/web/src/components/setup/components/steps/DatabaseStep.jsx +++ b/web/src/components/setup/components/steps/DatabaseStep.jsx @@ -26,8 +26,9 @@ import { Banner } from '@douyinfe/semi-ui'; */ const DatabaseStep = ({ setupStatus, renderNavigationButtons, t }) => { // 检测是否在 Electron 环境中运行 - const isElectron = typeof window !== 'undefined' && window.electron?.isElectron; - + const isElectron = + typeof window !== 'undefined' && window.electron?.isElectron; + return ( <> {/* 数据库警告 */} diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index d5d29996985f..cce63340a77f 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -157,7 +157,7 @@ const EditChannelModal = (props) => { is_enterprise_account: false, // 字段透传控制默认值 allow_service_tier: false, - disable_store: false, // false = 允许透传(默认开启) + disable_store: false, // false = 允许透传(默认开启) allow_safety_identifier: false, }; const [batch, setBatch] = useState(false); @@ -206,7 +206,13 @@ const EditChannelModal = (props) => { channelExtraSettings: null, }); const [currentSectionIndex, setCurrentSectionIndex] = useState(0); - const formSections = ['basicInfo', 'apiConfig', 'modelConfig', 'advancedSettings', 'channelExtraSettings']; + const formSections = [ + 'basicInfo', + 'apiConfig', + 'modelConfig', + 'advancedSettings', + 'channelExtraSettings', + ]; const formContainerRef = useRef(null); // 2FA状态更新辅助函数 @@ -266,13 +272,13 @@ const EditChannelModal = (props) => { sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start', - inline: 'nearest' + inline: 'nearest', }); } }; const navigateToSection = (direction) => { - const availableSections = formSections.filter(section => { + const availableSections = formSections.filter((section) => { if (section === 'apiConfig') { return showApiConfigCard; } @@ -281,9 +287,15 @@ const EditChannelModal = (props) => { let newIndex; if (direction === 'up') { - newIndex = currentSectionIndex > 0 ? currentSectionIndex - 1 : availableSections.length - 1; + newIndex = + currentSectionIndex > 0 + ? currentSectionIndex - 1 + : availableSections.length - 1; } else { - newIndex = currentSectionIndex < availableSections.length - 1 ? currentSectionIndex + 1 : 0; + newIndex = + currentSectionIndex < availableSections.length - 1 + ? currentSectionIndex + 1 + : 0; } setCurrentSectionIndex(newIndex); @@ -509,7 +521,8 @@ const EditChannelModal = (props) => { // 读取字段透传控制设置 data.allow_service_tier = parsedSettings.allow_service_tier || false; data.disable_store = parsedSettings.disable_store || false; - data.allow_safety_identifier = parsedSettings.allow_safety_identifier || false; + data.allow_safety_identifier = + parsedSettings.allow_safety_identifier || false; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; @@ -686,7 +699,7 @@ const EditChannelModal = (props) => { title: t('查看渠道密钥'), description: t('为了保护账户安全,请验证您的身份。'), preferredMethod: 'passkey', // 优先使用 Passkey - } + }, ); // 如果直接返回了结果(已验证),显示密钥 @@ -990,7 +1003,8 @@ const EditChannelModal = (props) => { // 仅 OpenAI 渠道需要 store 和 safety_identifier if (localInputs.type === 1) { settings.disable_store = localInputs.disable_store === true; - settings.allow_safety_identifier = localInputs.allow_safety_identifier === true; + settings.allow_safety_identifier = + localInputs.allow_safety_identifier === true; } } @@ -1339,7 +1353,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('上一个表单块')} /> @@ -1355,7 +1369,7 @@ const EditChannelModal = (props) => { padding: 0, display: 'flex', alignItems: 'center', - justifyContent: 'center' + justifyContent: 'center', }} title={t('下一个表单块')} /> @@ -1390,336 +1404,149 @@ const EditChannelModal = (props) => { > {() => ( -
-
formSectionRefs.current.basicInfo = el}> +
+
(formSectionRefs.current.basicInfo = el)}> {/* Header: Basic Info */}
- - - -
- - {t('基本信息')} - -
- {t('渠道的基本配置信息')} + + + +
+ + {t('基本信息')} + +
+ {t('渠道的基本配置信息')} +
-
- setChannelSearchValue(value)} - renderOptionItem={renderChannelOption} - onChange={(value) => handleInputChange('type', value)} - /> - - {inputs.type === 20 && ( - { - setIsEnterpriseAccount(value); - handleInputChange('is_enterprise_account', value); - }} - extraText={t( - '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', - )} - initValue={inputs.is_enterprise_account} - /> - )} - - handleInputChange('name', value)} - autoComplete='new-password' - /> - - {inputs.type === 41 && ( { - // 更新设置中的 vertex_key_type - handleChannelOtherSettingsChange( - 'vertex_key_type', - value, - ); - // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 - if (value === 'api_key') { - setBatch(false); - setUseManualInput(false); - setVertexKeys([]); - setVertexFileList([]); - if (formApiRef.current) { - formApiRef.current.setValue('vertex_files', []); - } - } - }} - extraText={ - inputs.vertex_key_type === 'api_key' - ? t('API Key 模式下不支持批量创建') - : t('JSON 模式支持手动输入或上传服务账号 JSON') - } + filter={selectFilter} + autoClearSearchValue={false} + searchPosition='dropdown' + onSearch={(value) => setChannelSearchValue(value)} + renderOptionItem={renderChannelOption} + onChange={(value) => handleInputChange('type', value)} /> - )} - {batch ? ( - inputs.type === 41 && - (inputs.vertex_key_type || 'json') === 'json' ? ( - } - dragMainText={t('点击上传文件或拖拽文件到这里')} - dragSubText={t('仅支持 JSON 文件,支持多文件')} - style={{ marginTop: 10 }} - uploadTrigger='custom' - beforeUpload={() => false} - onChange={handleVertexUploadChange} - fileList={vertexFileList} - rules={ - isEdit - ? [] - : [{ required: true, message: t('请上传密钥文件') }] - } - extraText={batchExtra} + + {inputs.type === 20 && ( + { + setIsEnterpriseAccount(value); + handleInputChange('is_enterprise_account', value); + }} + extraText={t( + '企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选', + )} + initValue={inputs.is_enterprise_account} /> - ) : ( - handleInputChange('key', value)} + )} + + handleInputChange('name', value)} + autoComplete='new-password' + /> + + {inputs.type === 41 && ( + { + // 更新设置中的 vertex_key_type + handleChannelOtherSettingsChange( + 'vertex_key_type', + value, + ); + // 切换为 api_key 时,关闭批量与手动/文件切换,并清理已选文件 + if (value === 'api_key') { + setBatch(false); + setUseManualInput(false); + setVertexKeys([]); + setVertexFileList([]); + if (formApiRef.current) { + formApiRef.current.setValue('vertex_files', []); + } + } + }} extraText={ -
- {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
+ inputs.vertex_key_type === 'api_key' + ? t('API Key 模式下不支持批量创建') + : t('JSON 模式支持手动输入或上传服务账号 JSON') } - showClear /> - ) - ) : ( - <> - {inputs.type === 41 && + )} + {batch ? ( + inputs.type === 41 && (inputs.vertex_key_type || 'json') === 'json' ? ( - <> - {!batch && ( -
- - {t('密钥输入方式')} - - - - - -
- )} - - {batch && ( - - )} - - {useManualInput && !batch ? ( - - handleInputChange('key', value) - } - extraText={ -
- - {t('请输入完整的 JSON 格式密钥内容')} - - {isEdit && - isMultiKeyChannel && - keyMode === 'append' && ( - - {t( - '追加模式:新密钥将添加到现有密钥列表的末尾', - )} - - )} - {isEdit && ( - - )} - {batchExtra} -
- } - autosize - showClear - /> - ) : ( - } - dragMainText={t('点击上传文件或拖拽文件到这里')} - dragSubText={t('仅支持 JSON 文件')} - style={{ marginTop: 10 }} - uploadTrigger='custom' - beforeUpload={() => false} - onChange={handleVertexUploadChange} - fileList={vertexFileList} - rules={ - isEdit - ? [] - : [ - { - required: true, - message: t('请上传密钥文件'), - }, - ] - } - extraText={batchExtra} - /> - )} - - ) : ( - } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件,支持多文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ isEdit - ? t('密钥(编辑模式下,保存的密钥不会显示)') - : t('密钥') + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] } - placeholder={t(type2secretPrompt(inputs.type))} + extraText={batchExtra} + /> + ) : ( + handleInputChange('key', value)} extraText={ -
+
{isEdit && isMultiKeyChannel && keyMode === 'append' && ( @@ -1744,254 +1571,501 @@ const EditChannelModal = (props) => { } showClear /> - )} - - )} + ) + ) : ( + <> + {inputs.type === 41 && + (inputs.vertex_key_type || 'json') === 'json' ? ( + <> + {!batch && ( +
+ + {t('密钥输入方式')} + + + + + +
+ )} - {isEdit && isMultiKeyChannel && ( - setKeyMode(value)} - extraText={ - - {keyMode === 'replace' - ? t('覆盖模式:将完全替换现有的所有密钥') - : t('追加模式:将新密钥添加到现有密钥列表末尾')} - - } - /> - )} - {batch && multiToSingle && ( - <> + {batch && ( + + )} + + {useManualInput && !batch ? ( + + handleInputChange('key', value) + } + extraText={ +
+ + {t('请输入完整的 JSON 格式密钥内容')} + + {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + autosize + showClear + /> + ) : ( + } + dragMainText={t('点击上传文件或拖拽文件到这里')} + dragSubText={t('仅支持 JSON 文件')} + style={{ marginTop: 10 }} + uploadTrigger='custom' + beforeUpload={() => false} + onChange={handleVertexUploadChange} + fileList={vertexFileList} + rules={ + isEdit + ? [] + : [ + { + required: true, + message: t('请上传密钥文件'), + }, + ] + } + extraText={batchExtra} + /> + )} + + ) : ( + + handleInputChange('key', value) + } + extraText={ +
+ {isEdit && + isMultiKeyChannel && + keyMode === 'append' && ( + + {t( + '追加模式:新密钥将添加到现有密钥列表的末尾', + )} + + )} + {isEdit && ( + + )} + {batchExtra} +
+ } + showClear + /> + )} + + )} + + {isEdit && isMultiKeyChannel && ( { - setMultiKeyMode(value); - handleInputChange('multi_key_mode', value); - }} + value={keyMode} + onChange={(value) => setKeyMode(value)} + extraText={ + + {keyMode === 'replace' + ? t('覆盖模式:将完全替换现有的所有密钥') + : t('追加模式:将新密钥添加到现有密钥列表末尾')} + + } /> - {inputs.multi_key_mode === 'polling' && ( - + { + setMultiKeyMode(value); + handleInputChange('multi_key_mode', value); + }} /> - )} - - )} + {inputs.multi_key_mode === 'polling' && ( + + )} + + )} - {inputs.type === 18 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 18 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 41 && ( - handleInputChange('other', value)} - rules={[{ required: true, message: t('请填写部署地区') }]} - template={REGION_EXAMPLE} - templateLabel={t('填入模板')} - editorType='region' - formApi={formApiRef.current} - extraText={t('设置默认地区和特定模型的专用地区')} - /> - )} + {inputs.type === 41 && ( + handleInputChange('other', value)} + rules={[ + { required: true, message: t('请填写部署地区') }, + ]} + template={REGION_EXAMPLE} + templateLabel={t('填入模板')} + editorType='region' + formApi={formApiRef.current} + extraText={t('设置默认地区和特定模型的专用地区')} + /> + )} - {inputs.type === 21 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 21 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 39 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 39 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 49 && ( - handleInputChange('other', value)} - showClear - /> - )} + {inputs.type === 49 && ( + handleInputChange('other', value)} + showClear + /> + )} - {inputs.type === 1 && ( - - handleInputChange('openai_organization', value) - } - /> - )} + {inputs.type === 1 && ( + + handleInputChange('openai_organization', value) + } + /> + )}
{/* API Configuration Card */} {showApiConfigCard && ( -
formSectionRefs.current.apiConfig = el}> +
(formSectionRefs.current.apiConfig = el)}> {/* Header: API Config */}
- - - -
- - {t('API 配置')} - -
- {t('API 地址和相关配置')} + + + +
+ + {t('API 配置')} + +
+ {t('API 地址和相关配置')} +
-
- - {inputs.type === 40 && ( - - {t('邀请链接')}: - - window.open( - 'https://cloud.siliconflow.cn/i/hij0YNTZ', - ) - } - > - https://cloud.siliconflow.cn/i/hij0YNTZ - -
- } - className='!rounded-lg' - /> - )} - {inputs.type === 3 && ( - <> + {inputs.type === 40 && ( + {t('邀请链接')}: + + window.open( + 'https://cloud.siliconflow.cn/i/hij0YNTZ', + ) + } + > + https://cloud.siliconflow.cn/i/hij0YNTZ + +
+ } className='!rounded-lg' /> -
- + - handleInputChange('base_url', value) - } - showClear + className='!rounded-lg' /> -
-
- - handleInputChange('other', value) - } - showClear - /> -
-
- + + handleInputChange('base_url', value) + } + showClear + /> +
+
+ + handleInputChange('other', value) + } + showClear + /> +
+
+ + handleChannelOtherSettingsChange( + 'azure_responses_version', + value, + ) + } + showClear + /> +
+ + )} + + {inputs.type === 8 && ( + <> + - handleChannelOtherSettingsChange( - 'azure_responses_version', - value, - ) - } - showClear + className='!rounded-lg' /> -
- - )} +
+ + handleInputChange('base_url', value) + } + showClear + /> +
+ + )} - {inputs.type === 8 && ( - <> + {inputs.type === 37 && ( + )} + + {inputs.type !== 3 && + inputs.type !== 8 && + inputs.type !== 22 && + inputs.type !== 36 && + inputs.type !== 45 && ( +
+ + handleInputChange('base_url', value) + } + showClear + extraText={t( + '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', + )} + /> +
+ )} + + {inputs.type === 22 && (
handleInputChange('base_url', value) @@ -1999,488 +2073,410 @@ const EditChannelModal = (props) => { showClear />
- - )} - - {inputs.type === 37 && ( - - )} + )} - {inputs.type !== 3 && - inputs.type !== 8 && - inputs.type !== 22 && - inputs.type !== 36 && - inputs.type !== 45 && ( + {inputs.type === 36 && (
handleInputChange('base_url', value) } showClear - extraText={t( - '对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写', - )} />
)} - {inputs.type === 22 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 36 && ( -
- - handleInputChange('base_url', value) - } - showClear - /> -
- )} - - {inputs.type === 45 && ( -
- - handleInputChange('base_url', value) - } - optionList={[ - { - value: 'https://ark.cn-beijing.volces.com', - label: 'https://ark.cn-beijing.volces.com', - }, - { - value: 'https://ark.ap-southeast.bytepluses.com', - label: 'https://ark.ap-southeast.bytepluses.com', - }, - ]} - defaultValue='https://ark.cn-beijing.volces.com' - /> -
- )} + {inputs.type === 45 && ( +
+ + handleInputChange('base_url', value) + } + optionList={[ + { + value: 'https://ark.cn-beijing.volces.com', + label: 'https://ark.cn-beijing.volces.com', + }, + { + value: + 'https://ark.ap-southeast.bytepluses.com', + label: + 'https://ark.ap-southeast.bytepluses.com', + }, + ]} + defaultValue='https://ark.cn-beijing.volces.com' + /> +
+ )}
)} {/* Model Configuration Card */} -
formSectionRefs.current.modelConfig = el}> +
(formSectionRefs.current.modelConfig = el)}> {/* Header: Model Config */}
- - - -
- - {t('模型配置')} - -
- {t('模型选择和映射设置')} + + + +
+ + {t('模型配置')} + +
+ {t('模型选择和映射设置')} +
-
- handleInputChange('models', value)} - renderSelectedItem={(optionNode) => { - const modelName = String(optionNode?.value ?? ''); - return { - isRenderInTag: true, - content: ( - { - e.stopPropagation(); - const ok = await copy(modelName); - if (ok) { - showSuccess( - t('已复制:{{name}}', { name: modelName }), - ); - } else { - showError(t('复制失败')); - } - }} + handleInputChange('models', value)} + renderSelectedItem={(optionNode) => { + const modelName = String(optionNode?.value ?? ''); + return { + isRenderInTag: true, + content: ( + { + e.stopPropagation(); + const ok = await copy(modelName); + if (ok) { + showSuccess( + t('已复制:{{name}}', { name: modelName }), + ); + } else { + showError(t('复制失败')); + } + }} + > + {optionNode.label || modelName} + + ), + }; + }} + extraText={ + + - - {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + {t('填入相关模型')} + + + {MODEL_FETCHABLE_TYPES.has(inputs.type) && ( + + )} + - )} - + {modelGroups && + modelGroups.length > 0 && + modelGroups.map((group) => ( + + ))} + + } + /> + + setCustomModel(value.trim())} + value={customModel} + suffix={ - {modelGroups && - modelGroups.length > 0 && - modelGroups.map((group) => ( - - ))} - - } - /> - - setCustomModel(value.trim())} - value={customModel} - suffix={ - - } - /> - - handleInputChange('test_model', value)} - showClear - /> - - - handleInputChange('model_mapping', value) - } - template={MODEL_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t('键为请求中的模型名称,值为要替换的模型名称')} - /> + } + /> + + + handleInputChange('test_model', value) + } + showClear + /> + + + handleInputChange('model_mapping', value) + } + template={MODEL_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为请求中的模型名称,值为要替换的模型名称', + )} + />
{/* Advanced Settings Card */} -
formSectionRefs.current.advancedSettings = el}> +
(formSectionRefs.current.advancedSettings = el)} + > {/* Header: Advanced Settings */}
- - - -
- - {t('高级设置')} - -
- {t('渠道的高级配置选项')} + + + +
+ + {t('高级设置')} + +
+ {t('渠道的高级配置选项')} +
-
- handleInputChange('groups', value)} - /> - - handleInputChange('tag', value)} - /> - handleInputChange('remark', value)} - /> - - - - - handleInputChange('priority', value) - } - style={{ width: '100%' }} - /> - - - - handleInputChange('weight', value) - } - style={{ width: '100%' }} - /> - - - - setAutoBan(value)} - extraText={t( - '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', - )} - initValue={autoBan} - /> - - - handleInputChange('param_override', value) - } - extraText={ -
- - handleInputChange( - 'param_override', - JSON.stringify({ temperature: 0 }, null, 2), - ) + handleInputChange('groups', value)} + /> + + handleInputChange('tag', value)} + /> + handleInputChange('remark', value)} + /> + + + + + handleInputChange('priority', value) } - > - {t('旧格式模板')} - - - handleInputChange( - 'param_override', - JSON.stringify( - { - operations: [ - { - path: 'temperature', - mode: 'set', - value: 0.7, - conditions: [ - { - path: 'model', - mode: 'prefix', - value: 'gpt', - }, - ], - logic: 'AND', - }, - ], - }, - null, - 2, - ), - ) + style={{ width: '100%' }} + /> + + + + handleInputChange('weight', value) } - > - {t('新格式模板')} - -
- } - showClear - /> - - - handleInputChange('header_override', value) - } - extraText={ - -
-
+ style={{ width: '100%' }} + /> + + + + setAutoBan(value)} + extraText={t( + '仅当自动禁用开启时有效,关闭后不会自动禁用该渠道', + )} + initValue={autoBan} + /> + + + handleInputChange('param_override', value) + } + extraText={ +
handleInputChange( - 'header_override', + 'param_override', + JSON.stringify({ temperature: 0 }, null, 2), + ) + } + > + {t('旧格式模板')} + + + handleInputChange( + 'param_override', JSON.stringify( { - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', - 'Authorization': 'Bearer{api_key}', + operations: [ + { + path: 'temperature', + mode: 'set', + value: 0.7, + conditions: [ + { + path: 'model', + mode: 'prefix', + value: 'gpt', + }, + ], + logic: 'AND', + }, + ], }, null, 2, @@ -2488,220 +2484,281 @@ const EditChannelModal = (props) => { ) } > - {t('填入模板')} + {t('新格式模板')}
-
- - {t('支持变量:')} - -
-
{t('渠道密钥')}: {'{api_key}'}
+ } + showClear + /> + + + handleInputChange('header_override', value) + } + extraText={ +
+
+ + handleInputChange( + 'header_override', + JSON.stringify( + { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0', + Authorization: 'Bearer{api_key}', + }, + null, + 2, + ), + ) + } + > + {t('填入模板')} + +
+
+ + {t('支持变量:')} + +
+
+ {t('渠道密钥')}: {'{api_key}'} +
+
-
- } - showClear - /> - - - handleInputChange('status_code_mapping', value) - } - template={STATUS_CODE_MAPPING_EXAMPLE} - templateLabel={t('填入模板')} - editorType='keyValue' - formApi={formApiRef.current} - extraText={t( - '键为原状态码,值为要复写的状态码,仅影响本地判断', - )} - /> + } + showClear + /> - {/* 字段透传控制 - OpenAI 渠道 */} - {inputs.type === 1 && ( - <> -
- {t('字段透传控制')} -
+ + handleInputChange('status_code_mapping', value) + } + template={STATUS_CODE_MAPPING_EXAMPLE} + templateLabel={t('填入模板')} + editorType='keyValue' + formApi={formApiRef.current} + extraText={t( + '键为原状态码,值为要复写的状态码,仅影响本地判断', + )} + /> - - handleChannelOtherSettingsChange('allow_service_tier', value) - } - extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', - )} - /> + {/* 字段透传控制 - OpenAI 渠道 */} + {inputs.type === 1 && ( + <> +
+ {t('字段透传控制')} +
- - handleChannelOtherSettingsChange('disable_store', value) - } - extraText={t( - 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', - )} - /> + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> - - handleChannelOtherSettingsChange('allow_safety_identifier', value) - } - extraText={t( - 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', - )} - /> - - )} - - {/* 字段透传控制 - Claude 渠道 */} - {(inputs.type === 14) && ( - <> -
- {t('字段透传控制')} + + handleChannelOtherSettingsChange( + 'disable_store', + value, + ) + } + extraText={t( + 'store 字段用于授权 OpenAI 存储请求数据以评估和优化产品。默认关闭,开启后可能导致 Codex 无法正常使用', + )} + /> + + + handleChannelOtherSettingsChange( + 'allow_safety_identifier', + value, + ) + } + extraText={t( + 'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私', + )} + /> + + )} + + {/* 字段透传控制 - Claude 渠道 */} + {inputs.type === 14 && ( + <> +
+ {t('字段透传控制')} +
+ + + handleChannelOtherSettingsChange( + 'allow_service_tier', + value, + ) + } + extraText={t( + 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + )} + /> + + )} + +
+ + {/* Channel Extra Settings Card */} +
+ (formSectionRefs.current.channelExtraSettings = el) + } + > + + {/* Header: Channel Extra Settings */} +
+ + + +
+ + {t('渠道额外设置')} +
+
+ {inputs.type === 1 && ( - handleChannelOtherSettingsChange('allow_service_tier', value) + handleChannelSettingsChange('force_format', value) } extraText={t( - 'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用', + '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', )} /> - - )} -
-
+ )} - {/* Channel Extra Settings Card */} -
formSectionRefs.current.channelExtraSettings = el}> - - {/* Header: Channel Extra Settings */} -
- - - -
- - {t('渠道额外设置')} - -
-
+ + handleChannelSettingsChange( + 'thinking_to_content', + value, + ) + } + extraText={t( + '将 reasoning_content 转换为 标签拼接到内容中', + )} + /> - {inputs.type === 1 && ( - handleChannelSettingsChange('force_format', value) + handleChannelSettingsChange( + 'pass_through_body_enabled', + value, + ) } + extraText={t('启用请求体透传功能')} + /> + + + handleChannelSettingsChange('proxy', value) + } + showClear + extraText={t('用于配置网络代理,支持 socks5 协议')} + /> + + + handleChannelSettingsChange('system_prompt', value) + } + autosize + showClear extraText={t( - '强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)', + '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', + )} + /> + + handleChannelSettingsChange( + 'system_prompt_override', + value, + ) + } + extraText={t( + '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', )} /> - )} - - - handleChannelSettingsChange('thinking_to_content', value) - } - extraText={t( - '将 reasoning_content 转换为 标签拼接到内容中', - )} - /> - - - handleChannelSettingsChange( - 'pass_through_body_enabled', - value, - ) - } - extraText={t('启用请求体透传功能')} - /> - - - handleChannelSettingsChange('proxy', value) - } - showClear - extraText={t('用于配置网络代理,支持 socks5 协议')} - /> - - - handleChannelSettingsChange('system_prompt', value) - } - autosize - showClear - extraText={t( - '用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置', - )} - /> - - handleChannelSettingsChange( - 'system_prompt_override', - value, - ) - } - extraText={t( - '如果用户请求中包含系统提示词,则使用此设置拼接到用户的系统提示词前面', - )} - />
diff --git a/web/src/components/table/channels/modals/EditTagModal.jsx b/web/src/components/table/channels/modals/EditTagModal.jsx index 752ff3dc5641..0b2f17d90a83 100644 --- a/web/src/components/table/channels/modals/EditTagModal.jsx +++ b/web/src/components/table/channels/modals/EditTagModal.jsx @@ -119,8 +119,19 @@ const EditTagModal = (props) => { localModels = ['suno_music', 'suno_lyrics']; break; case 53: - localModels = ['NousResearch/Hermes-4-405B-FP8', 'Qwen/Qwen3-235B-A22B-Thinking-2507', 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8','Qwen/Qwen3-235B-A22B-Instruct-2507', 'zai-org/GLM-4.5-FP8', 'openai/gpt-oss-120b', 'deepseek-ai/DeepSeek-R1-0528', 'deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-V3-0324', 'deepseek-ai/DeepSeek-V3.1']; - break; + localModels = [ + 'NousResearch/Hermes-4-405B-FP8', + 'Qwen/Qwen3-235B-A22B-Thinking-2507', + 'Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8', + 'Qwen/Qwen3-235B-A22B-Instruct-2507', + 'zai-org/GLM-4.5-FP8', + 'openai/gpt-oss-120b', + 'deepseek-ai/DeepSeek-R1-0528', + 'deepseek-ai/DeepSeek-R1', + 'deepseek-ai/DeepSeek-V3-0324', + 'deepseek-ai/DeepSeek-V3.1', + ]; + break; default: localModels = getChannelModels(value); break; diff --git a/web/src/components/table/channels/modals/ModelTestModal.jsx b/web/src/components/table/channels/modals/ModelTestModal.jsx index 7cc56612d4be..1879cd574ae8 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -67,9 +67,15 @@ const ModelTestModal = ({ { value: 'openai', label: 'OpenAI (/v1/chat/completions)' }, { value: 'openai-response', label: 'OpenAI Response (/v1/responses)' }, { value: 'anthropic', label: 'Anthropic (/v1/messages)' }, - { value: 'gemini', label: 'Gemini (/v1beta/models/{model}:generateContent)' }, + { + value: 'gemini', + label: 'Gemini (/v1beta/models/{model}:generateContent)', + }, { value: 'jina-rerank', label: 'Jina Rerank (/rerank)' }, - { value: 'image-generation', label: t('图像生成') + ' (/v1/images/generations)' }, + { + value: 'image-generation', + label: t('图像生成') + ' (/v1/images/generations)', + }, { value: 'embeddings', label: 'Embeddings (/v1/embeddings)' }, ]; @@ -166,7 +172,13 @@ const ModelTestModal = ({ return ( - -
@@ -339,16 +356,22 @@ const RechargeCard = ({ )} {(enableOnlineTopUp || enableStripeTopUp) && ( - {t('选择充值额度')} {(() => { const { symbol, rate, type } = getCurrencyConfig(); if (type === 'USD') return null; - + return ( - + (1 $ = {rate.toFixed(2)} {symbol}) ); @@ -378,11 +401,11 @@ const RechargeCard = ({ usdRate = s?.usd_exchange_rate || 7; } } catch (e) {} - + let displayValue = preset.value; // 显示的数量 let displayActualPay = actualPay; let displaySave = save; - + if (type === 'USD') { // 数量保持USD,价格从CNY转USD displayActualPay = actualPay / usdRate; @@ -444,7 +467,8 @@ const RechargeCard = ({ margin: '4px 0', }} > - {t('实付')} {symbol}{displayActualPay.toFixed(2)}, + {t('实付')} {symbol} + {displayActualPay.toFixed(2)}, {hasDiscount ? `${t('节省')} ${symbol}${displaySave.toFixed(2)}` : `${t('节省')} ${symbol}0.00`} diff --git a/web/src/components/topup/index.jsx b/web/src/components/topup/index.jsx index 558c6705009e..9054da524d51 100644 --- a/web/src/components/topup/index.jsx +++ b/web/src/components/topup/index.jsx @@ -37,6 +37,7 @@ import RechargeCard from './RechargeCard'; import InvitationCard from './InvitationCard'; import TransferModal from './modals/TransferModal'; import PaymentConfirmModal from './modals/PaymentConfirmModal'; +import TopupHistoryModal from './modals/TopupHistoryModal'; const TopUp = () => { const { t } = useTranslation(); @@ -77,6 +78,9 @@ const TopUp = () => { const [openTransfer, setOpenTransfer] = useState(false); const [transferAmount, setTransferAmount] = useState(0); + // 账单Modal状态 + const [openHistory, setOpenHistory] = useState(false); + // 预设充值额度选项 const [presetAmounts, setPresetAmounts] = useState([]); const [selectedPreset, setSelectedPreset] = useState(null); @@ -488,6 +492,14 @@ const TopUp = () => { setOpenTransfer(false); }; + const handleOpenHistory = () => { + setOpenHistory(true); + }; + + const handleHistoryCancel = () => { + setOpenHistory(false); + }; + // 选择预设充值额度 const selectPresetAmount = (preset) => { setTopUpCount(preset.value); @@ -544,6 +556,13 @@ const TopUp = () => { discountRate={topupInfo?.discount?.[topUpCount] || 1.0} /> + {/* 充值账单模态框 */} + + {/* 用户信息头部 */}
@@ -580,6 +599,7 @@ const TopUp = () => { renderQuota={renderQuota} statusLoading={statusLoading} topupInfo={topupInfo} + onOpenHistory={handleOpenHistory} />
diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx new file mode 100644 index 000000000000..e2548f388e95 --- /dev/null +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -0,0 +1,253 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ +import React, { useState, useEffect, useMemo } from 'react'; +import { + Modal, + Table, + Badge, + Typography, + Toast, + Empty, + Button, +} from '@douyinfe/semi-ui'; +import { + IllustrationNoResult, + IllustrationNoResultDark, +} from '@douyinfe/semi-illustrations'; +import { Coins } from 'lucide-react'; +import { API, timestamp2string } from '../../../helpers'; +import { isAdmin } from '../../../helpers/utils'; +import { useIsMobile } from '../../../hooks/common/useIsMobile'; + +const { Text } = Typography; + +// 状态映射配置 +const STATUS_CONFIG = { + success: { type: 'success', key: '成功' }, + pending: { type: 'warning', key: '待支付' }, + expired: { type: 'danger', key: '已过期' }, +}; + +// 支付方式映射 +const PAYMENT_METHOD_MAP = { + stripe: 'Stripe', + alipay: '支付宝', + wxpay: '微信', +}; + +const TopupHistoryModal = ({ visible, onCancel, t }) => { + const [loading, setLoading] = useState(false); + const [topups, setTopups] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + const isMobile = useIsMobile(); + + const loadTopups = async (currentPage, currentPageSize) => { + setLoading(true); + try { + const res = await API.get( + `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`, + ); + const { success, message, data } = res.data; + if (success) { + setTopups(data.items || []); + setTotal(data.total || 0); + } else { + Toast.error({ content: message || t('加载失败') }); + } + } catch (error) { + console.error('Load topups error:', error); + Toast.error({ content: t('加载账单失败') }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (visible) { + loadTopups(page, pageSize); + } + }, [visible, page, pageSize]); + + const handlePageChange = (currentPage) => { + setPage(currentPage); + }; + + const handlePageSizeChange = (currentPageSize) => { + setPageSize(currentPageSize); + setPage(1); + }; + + // 管理员补单 + const handleAdminComplete = async (tradeNo) => { + try { + const res = await API.post('/api/user/topup/complete', { + trade_no: tradeNo, + }); + const { success, message } = res.data; + if (success) { + Toast.success({ content: t('补单成功') }); + await loadTopups(page, pageSize); + } else { + Toast.error({ content: message || t('补单失败') }); + } + } catch (e) { + Toast.error({ content: t('补单失败') }); + } + }; + + const confirmAdminComplete = (tradeNo) => { + Modal.confirm({ + title: t('确认补单'), + content: t('是否将该订单标记为成功并为用户入账?'), + onOk: () => handleAdminComplete(tradeNo), + }); + }; + + // 渲染状态徽章 + const renderStatusBadge = (status) => { + const config = STATUS_CONFIG[status] || { type: 'primary', key: status }; + return ( + + + {t(config.key)} + + ); + }; + + // 渲染支付方式 + const renderPaymentMethod = (pm) => { + const displayName = PAYMENT_METHOD_MAP[pm]; + return {displayName ? t(displayName) : pm || '-'}; + }; + + // 检查是否为管理员 + const userIsAdmin = useMemo(() => isAdmin(), []); + + const columns = useMemo(() => { + const baseColumns = [ + { + title: t('订单号'), + dataIndex: 'trade_no', + key: 'trade_no', + render: (text) => {text}, + }, + { + title: t('支付方式'), + dataIndex: 'payment_method', + key: 'payment_method', + render: renderPaymentMethod, + }, + { + title: t('充值额度'), + dataIndex: 'amount', + key: 'amount', + render: (amount) => ( + + + {amount} + + ), + }, + { + title: t('支付金额'), + dataIndex: 'money', + key: 'money', + render: (money) => ¥{money.toFixed(2)}, + }, + { + title: t('状态'), + dataIndex: 'status', + key: 'status', + render: renderStatusBadge, + }, + ]; + + // 管理员才显示操作列 + if (userIsAdmin) { + baseColumns.push({ + title: t('操作'), + key: 'action', + render: (_, record) => { + if (record.status !== 'pending') return null; + return ( + + ); + }, + }); + } + + baseColumns.push({ + title: t('创建时间'), + dataIndex: 'create_time', + key: 'create_time', + render: (time) => timestamp2string(time), + }); + + return baseColumns; + }, [t, userIsAdmin]); + + return ( + + } + darkModeImage={ + + } + description={t('暂无充值记录')} + style={{ padding: 30 }} + /> + } + /> + + ); +}; + +export default TopupHistoryModal; diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 3b376ed357cd..ad6999365dea 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -159,7 +159,7 @@ export const CHANNEL_OPTIONS = [ color: 'purple', label: 'Vidu', }, - { + { value: 53, color: 'blue', label: 'SubModel', diff --git a/web/src/helpers/passkey.js b/web/src/helpers/passkey.js index ae62775e892f..c3f3e927d6ef 100644 --- a/web/src/helpers/passkey.js +++ b/web/src/helpers/passkey.js @@ -1,3 +1,21 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ export function base64UrlToBuffer(base64url) { if (!base64url) return new ArrayBuffer(0); let padding = '='.repeat((4 - (base64url.length % 4)) % 4); @@ -26,7 +44,11 @@ export function bufferToBase64Url(buffer) { } export function prepareCredentialCreationOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 注册参数'); } @@ -46,7 +68,10 @@ export function prepareCredentialCreationOptions(payload) { })); } - if (Array.isArray(options.attestationFormats) && options.attestationFormats.length === 0) { + if ( + Array.isArray(options.attestationFormats) && + options.attestationFormats.length === 0 + ) { delete publicKey.attestationFormats; } @@ -54,7 +79,11 @@ export function prepareCredentialCreationOptions(payload) { } export function prepareCredentialRequestOptions(payload) { - const options = payload?.publicKey || payload?.PublicKey || payload?.response || payload?.Response; + const options = + payload?.publicKey || + payload?.PublicKey || + payload?.response || + payload?.Response; if (!options) { throw new Error('无法从服务端响应中解析 Passkey 登录参数'); } @@ -77,7 +106,10 @@ export function buildRegistrationResult(credential) { if (!credential) return null; const { response } = credential; - const transports = typeof response.getTransports === 'function' ? response.getTransports() : undefined; + const transports = + typeof response.getTransports === 'function' + ? response.getTransports() + : undefined; return { id: credential.id, @@ -107,7 +139,9 @@ export function buildAssertionResult(assertion) { authenticatorData: bufferToBase64Url(response.authenticatorData), clientDataJSON: bufferToBase64Url(response.clientDataJSON), signature: bufferToBase64Url(response.signature), - userHandle: response.userHandle ? bufferToBase64Url(response.userHandle) : null, + userHandle: response.userHandle + ? bufferToBase64Url(response.userHandle) + : null, }, clientExtensionResults: assertion.getClientExtensionResults?.() ?? {}, }; @@ -117,15 +151,22 @@ export async function isPasskeySupported() { if (typeof window === 'undefined' || !window.PublicKeyCredential) { return false; } - if (typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function') { + if ( + typeof window.PublicKeyCredential.isConditionalMediationAvailable === + 'function' + ) { try { - const available = await window.PublicKeyCredential.isConditionalMediationAvailable(); + const available = + await window.PublicKeyCredential.isConditionalMediationAvailable(); if (available) return true; } catch (error) { // ignore } } - if (typeof window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') { + if ( + typeof window.PublicKeyCredential + .isUserVerifyingPlatformAuthenticatorAvailable === 'function' + ) { try { return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); } catch (error) { @@ -134,4 +175,3 @@ export async function isPasskeySupported() { } return true; } - diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx index b5fcb4d6262c..c22bdd782234 100644 --- a/web/src/helpers/render.jsx +++ b/web/src/helpers/render.jsx @@ -929,10 +929,10 @@ export function renderQuotaWithAmount(amount) { export function getCurrencyConfig() { const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; const statusStr = localStorage.getItem('status'); - + let symbol = '$'; let rate = 1; - + if (quotaDisplayType === 'CNY') { symbol = '¥'; try { @@ -950,7 +950,7 @@ export function getCurrencyConfig() { } } catch (e) {} } - + return { symbol, rate, type: quotaDisplayType }; } @@ -1128,7 +1128,7 @@ export function renderModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1177,13 +1177,16 @@ export function renderModelPrice( <>

- {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', { - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - audioPrice: audioInputSeperatePrice - ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` - : '', - })} + {i18next.t( + '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', + { + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + audioPrice: audioInputSeperatePrice + ? `,音频 ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` + : '', + }, + )}

{i18next.t( @@ -1311,27 +1314,27 @@ export function renderModelPrice( const extraServices = [ webSearch && webSearchCallCount > 0 ? i18next.t( - ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', - { - count: webSearchCallCount, - symbol: symbol, - price: (webSearchPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: webSearchCallCount, + symbol: symbol, + price: (webSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', fileSearch && fileSearchCallCount > 0 ? i18next.t( - ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', - { - count: fileSearchCallCount, - symbol: symbol, - price: (fileSearchPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - }, - ) + ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', + { + count: fileSearchCallCount, + symbol: symbol, + price: (fileSearchPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + }, + ) : '', imageGenerationCall && imageGenerationCallPrice > 0 ? i18next.t( @@ -1384,7 +1387,7 @@ export function renderLogContent( label: ratioLabel, useUserGroupRatio: useUserGroupRatio, } = getEffectiveRatio(groupRatio, user_group_ratio); - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1484,10 +1487,10 @@ export function renderAudioModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); - + // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { return i18next.t( @@ -1522,10 +1525,10 @@ export function renderAudioModelPrice( let audioPrice = (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + (audioCompletionTokens / 1000000) * - inputRatioPrice * - audioRatio * - audioCompletionRatio * - groupRatio; + inputRatioPrice * + audioRatio * + audioCompletionRatio * + groupRatio; let price = textPrice + audioPrice; return ( <> @@ -1577,7 +1580,12 @@ export function renderAudioModelPrice( { symbol: symbol, price: (inputRatioPrice * rate).toFixed(6), - total: (inputRatioPrice * audioRatio * audioCompletionRatio * rate).toFixed(6), + total: ( + inputRatioPrice * + audioRatio * + audioCompletionRatio * + rate + ).toFixed(6), audioRatio: audioRatio, audioCompRatio: audioCompletionRatio, }, @@ -1586,29 +1594,31 @@ export function renderAudioModelPrice(

{cacheTokens > 0 ? i18next.t( - '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', - { - nonCacheInput: inputTokens - cacheTokens, - cacheInput: cacheTokens, - symbol: symbol, - cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(6), - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - total: (textPrice * rate).toFixed(6), - }, - ) + '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', + { + nonCacheInput: inputTokens - cacheTokens, + cacheInput: cacheTokens, + symbol: symbol, + cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed( + 6, + ), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), + }, + ) : i18next.t( - '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', - { - input: inputTokens, - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - total: (textPrice * rate).toFixed(6), - }, - )} + '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + total: (textPrice * rate).toFixed(6), + }, + )}

{i18next.t( @@ -1617,9 +1627,15 @@ export function renderAudioModelPrice( input: audioInputTokens, completion: audioCompletionTokens, symbol: symbol, - audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(6), - audioCompPrice: - (audioRatio * audioCompletionRatio * inputRatioPrice * rate).toFixed(6), + audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed( + 6, + ), + audioCompPrice: ( + audioRatio * + audioCompletionRatio * + inputRatioPrice * + rate + ).toFixed(6), total: (audioPrice * rate).toFixed(6), }, )} @@ -1668,7 +1684,7 @@ export function renderClaudeModelPrice( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); @@ -1757,37 +1773,39 @@ export function renderClaudeModelPrice(

{cacheTokens > 0 || cacheCreationTokens > 0 ? i18next.t( - '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', - { - nonCacheInput: nonCachedTokens, - cacheInput: cacheTokens, - cacheRatio: cacheRatio, - cacheCreationInput: cacheCreationTokens, - cacheCreationRatio: cacheCreationRatio, - symbol: symbol, - cachePrice: (cacheRatioPrice * rate).toFixed(2), - cacheCreationPrice: (cacheCreationRatioPrice * rate).toFixed(6), - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - total: (price * rate).toFixed(6), - }, - ) + '提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * {{symbol}}{{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', + { + nonCacheInput: nonCachedTokens, + cacheInput: cacheTokens, + cacheRatio: cacheRatio, + cacheCreationInput: cacheCreationTokens, + cacheCreationRatio: cacheCreationRatio, + symbol: symbol, + cachePrice: (cacheRatioPrice * rate).toFixed(2), + cacheCreationPrice: ( + cacheCreationRatioPrice * rate + ).toFixed(6), + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + ) : i18next.t( - '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', - { - input: inputTokens, - symbol: symbol, - price: (inputRatioPrice * rate).toFixed(6), - completion: completionTokens, - compPrice: (completionRatioPrice * rate).toFixed(6), - ratio: groupRatio, - ratioType: ratioLabel, - total: (price * rate).toFixed(6), - }, - )} + '提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', + { + input: inputTokens, + symbol: symbol, + price: (inputRatioPrice * rate).toFixed(6), + completion: completionTokens, + compPrice: (completionRatioPrice * rate).toFixed(6), + ratio: groupRatio, + ratioType: ratioLabel, + total: (price * rate).toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -1810,7 +1828,7 @@ export function renderClaudeLogContent( user_group_ratio, ); groupRatio = effectiveGroupRatio; - + // 获取货币配置 const { symbol, rate } = getCurrencyConfig(); diff --git a/web/src/helpers/secureApiCall.js b/web/src/helpers/secureApiCall.js index b82a6ae92454..0054e04aef6f 100644 --- a/web/src/helpers/secureApiCall.js +++ b/web/src/helpers/secureApiCall.js @@ -37,7 +37,7 @@ export function isVerificationRequiredError(error) { const verificationCodes = [ 'VERIFICATION_REQUIRED', 'VERIFICATION_EXPIRED', - 'VERIFICATION_INVALID' + 'VERIFICATION_INVALID', ]; return verificationCodes.includes(data.code); @@ -57,6 +57,6 @@ export function extractVerificationInfo(error) { return { code: data.code, message: data.message || '需要安全验证', - required: true + required: true, }; -} \ No newline at end of file +} diff --git a/web/src/hooks/channels/useChannelsData.jsx b/web/src/hooks/channels/useChannelsData.jsx index 109d2e0f8fc9..f3f99f01eb4b 100644 --- a/web/src/hooks/channels/useChannelsData.jsx +++ b/web/src/hooks/channels/useChannelsData.jsx @@ -84,7 +84,7 @@ export const useChannelsData = () => { const [selectedModelKeys, setSelectedModelKeys] = useState([]); const [isBatchTesting, setIsBatchTesting] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); -const [selectedEndpointType, setSelectedEndpointType] = useState(''); + const [selectedEndpointType, setSelectedEndpointType] = useState(''); // 使用 ref 来避免闭包问题,类似旧版实现 const shouldStopBatchTestingRef = useRef(false); diff --git a/web/src/hooks/common/useSecureVerification.jsx b/web/src/hooks/common/useSecureVerification.jsx index e60a104dbc50..9109ec7d7787 100644 --- a/web/src/hooks/common/useSecureVerification.jsx +++ b/web/src/hooks/common/useSecureVerification.jsx @@ -31,11 +31,11 @@ import { isVerificationRequiredError } from '../../helpers/secureApiCall'; * @param {string} options.successMessage - 成功提示消息 * @param {boolean} options.autoReset - 验证完成后是否自动重置状态,默认为 true */ -export const useSecureVerification = ({ - onSuccess, - onError, +export const useSecureVerification = ({ + onSuccess, + onError, successMessage, - autoReset = true + autoReset = true, } = {}) => { const { t } = useTranslation(); @@ -43,7 +43,7 @@ export const useSecureVerification = ({ const [verificationMethods, setVerificationMethods] = useState({ has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }); // 模态框状态 @@ -54,12 +54,13 @@ export const useSecureVerification = ({ method: null, // '2fa' | 'passkey' loading: false, code: '', - apiCall: null + apiCall: null, }); // 检查可用的验证方式 const checkVerificationMethods = useCallback(async () => { - const methods = await SecureVerificationService.checkAvailableVerificationMethods(); + const methods = + await SecureVerificationService.checkAvailableVerificationMethods(); setVerificationMethods(methods); return methods; }, []); @@ -75,94 +76,108 @@ export const useSecureVerification = ({ method: null, loading: false, code: '', - apiCall: null + apiCall: null, }); setIsModalVisible(false); }, []); // 开始验证流程 - const startVerification = useCallback(async (apiCall, options = {}) => { - const { preferredMethod, title, description } = options; + const startVerification = useCallback( + async (apiCall, options = {}) => { + const { preferredMethod, title, description } = options; - // 检查验证方式 - const methods = await checkVerificationMethods(); + // 检查验证方式 + const methods = await checkVerificationMethods(); - if (!methods.has2FA && !methods.hasPasskey) { - const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); - showError(errorMessage); - onError?.(new Error(errorMessage)); - return false; - } + if (!methods.has2FA && !methods.hasPasskey) { + const errorMessage = t('您需要先启用两步验证或 Passkey 才能执行此操作'); + showError(errorMessage); + onError?.(new Error(errorMessage)); + return false; + } - // 设置默认验证方式 - let defaultMethod = preferredMethod; - if (!defaultMethod) { - if (methods.hasPasskey && methods.passkeySupported) { - defaultMethod = 'passkey'; - } else if (methods.has2FA) { - defaultMethod = '2fa'; + // 设置默认验证方式 + let defaultMethod = preferredMethod; + if (!defaultMethod) { + if (methods.hasPasskey && methods.passkeySupported) { + defaultMethod = 'passkey'; + } else if (methods.has2FA) { + defaultMethod = '2fa'; + } } - } - setVerificationState(prev => ({ - ...prev, - method: defaultMethod, - apiCall, - title, - description - })); - setIsModalVisible(true); + setVerificationState((prev) => ({ + ...prev, + method: defaultMethod, + apiCall, + title, + description, + })); + setIsModalVisible(true); - return true; - }, [checkVerificationMethods, onError, t]); + return true; + }, + [checkVerificationMethods, onError, t], + ); // 执行验证 - const executeVerification = useCallback(async (method, code = '') => { - if (!verificationState.apiCall) { - showError(t('验证配置错误')); - return; - } + const executeVerification = useCallback( + async (method, code = '') => { + if (!verificationState.apiCall) { + showError(t('验证配置错误')); + return; + } - setVerificationState(prev => ({ ...prev, loading: true })); + setVerificationState((prev) => ({ ...prev, loading: true })); - try { - // 先调用验证 API,成功后后端会设置 session - await SecureVerificationService.verify(method, code); + try { + // 先调用验证 API,成功后后端会设置 session + await SecureVerificationService.verify(method, code); - // 验证成功,调用业务 API(此时中间件会通过) - const result = await verificationState.apiCall(); + // 验证成功,调用业务 API(此时中间件会通过) + const result = await verificationState.apiCall(); - // 显示成功消息 - if (successMessage) { - showSuccess(successMessage); - } + // 显示成功消息 + if (successMessage) { + showSuccess(successMessage); + } - // 调用成功回调 - onSuccess?.(result, method); + // 调用成功回调 + onSuccess?.(result, method); - // 自动重置状态 - if (autoReset) { - resetState(); - } + // 自动重置状态 + if (autoReset) { + resetState(); + } - return result; - } catch (error) { - showError(error.message || t('验证失败,请重试')); - onError?.(error); - throw error; - } finally { - setVerificationState(prev => ({ ...prev, loading: false })); - } - }, [verificationState.apiCall, successMessage, onSuccess, onError, autoReset, resetState, t]); + return result; + } catch (error) { + showError(error.message || t('验证失败,请重试')); + onError?.(error); + throw error; + } finally { + setVerificationState((prev) => ({ ...prev, loading: false })); + } + }, + [ + verificationState.apiCall, + successMessage, + onSuccess, + onError, + autoReset, + resetState, + t, + ], + ); // 设置验证码 const setVerificationCode = useCallback((code) => { - setVerificationState(prev => ({ ...prev, code })); + setVerificationState((prev) => ({ ...prev, code })); }, []); // 切换验证方式 const switchVerificationMethod = useCallback((method) => { - setVerificationState(prev => ({ ...prev, method, code: '' })); + setVerificationState((prev) => ({ ...prev, method, code: '' })); }, []); // 取消验证 @@ -171,20 +186,29 @@ export const useSecureVerification = ({ }, [resetState]); // 检查是否可以使用某种验证方式 - const canUseMethod = useCallback((method) => { - switch (method) { - case '2fa': - return verificationMethods.has2FA; - case 'passkey': - return verificationMethods.hasPasskey && verificationMethods.passkeySupported; - default: - return false; - } - }, [verificationMethods]); + const canUseMethod = useCallback( + (method) => { + switch (method) { + case '2fa': + return verificationMethods.has2FA; + case 'passkey': + return ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ); + default: + return false; + } + }, + [verificationMethods], + ); // 获取推荐的验证方式 const getRecommendedMethod = useCallback(() => { - if (verificationMethods.hasPasskey && verificationMethods.passkeySupported) { + if ( + verificationMethods.hasPasskey && + verificationMethods.passkeySupported + ) { return 'passkey'; } if (verificationMethods.has2FA) { @@ -200,22 +224,25 @@ export const useSecureVerification = ({ * @param {Object} options - 验证选项(同 startVerification) * @returns {Promise} */ - const withVerification = useCallback(async (apiCall, options = {}) => { - try { - // 直接尝试调用 API - return await apiCall(); - } catch (error) { - // 检查是否是需要验证的错误 - if (isVerificationRequiredError(error)) { - // 自动触发验证流程 - await startVerification(apiCall, options); - // 不抛出错误,让验证模态框处理 - return null; + const withVerification = useCallback( + async (apiCall, options = {}) => { + try { + // 直接尝试调用 API + return await apiCall(); + } catch (error) { + // 检查是否是需要验证的错误 + if (isVerificationRequiredError(error)) { + // 自动触发验证流程 + await startVerification(apiCall, options); + // 不抛出错误,让验证模态框处理 + return null; + } + // 其他错误继续抛出 + throw error; } - // 其他错误继续抛出 - throw error; - } - }, [startVerification]); + }, + [startVerification], + ); return { // 状态 @@ -238,9 +265,10 @@ export const useSecureVerification = ({ withVerification, // 新增:自动处理验证的包装函数 // 便捷属性 - hasAnyVerificationMethod: verificationMethods.has2FA || verificationMethods.hasPasskey, + hasAnyVerificationMethod: + verificationMethods.has2FA || verificationMethods.hasPasskey, isLoading: verificationState.loading, currentMethod: verificationState.method, - code: verificationState.code + code: verificationState.code, }; -}; \ No newline at end of file +}; diff --git a/web/src/hooks/users/useUsersData.jsx b/web/src/hooks/users/useUsersData.jsx index 38579c2fa940..f906be543930 100644 --- a/web/src/hooks/users/useUsersData.jsx +++ b/web/src/hooks/users/useUsersData.jsx @@ -86,7 +86,7 @@ export const useUsersData = () => { }; // Search users with keyword and group -const searchUsers = async ( + const searchUsers = async ( startIdx, pageSize, searchKeyword = null, diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 45f7181e4524..8b4f1b41111b 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1285,7 +1285,6 @@ "可视化倍率设置": "Visual model ratio settings", "确定重置模型倍率吗?": "Confirm to reset model ratio?", "模型固定价格": "Model price per call", - "模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)", "保存模型倍率设置": "Save model ratio settings", "重置模型倍率": "Reset model ratio", "一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio", @@ -2177,7 +2176,6 @@ "最后使用时间": "Last used time", "备份支持": "Backup support", "支持备份": "Supported", - "不支持": "Not supported", "备份状态": "Backup state", "已备份": "Backed up", "未备份": "Not backed up", @@ -2248,5 +2246,18 @@ "轮询模式必须搭配Redis和内存缓存功能使用,否则性能将大幅降低,并且无法实现轮询功能": "Polling mode must be used with Redis and memory cache functions, otherwise the performance will be significantly reduced and the polling function will not be implemented", "common": { "changeLanguage": "Change Language" - } + }, + "充值账单": "Recharge Bills", + "订单号": "Order No.", + "支付金额": "Payment Amount", + "待支付": "Pending", + "加载失败": "Load failed", + "加载账单失败": "Failed to load bills", + "暂无充值记录": "No recharge records", + "账单": "Bills", + "补单": "Complete Order", + "补单成功": "Order completed successfully", + "补单失败": "Failed to complete order", + "确认补单": "Confirm Order Completion", + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index ad60a6f59e9e..e5db1125bbae 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2238,5 +2238,18 @@ "配置 Passkey": "Configurer Passkey", "重置 2FA": "Réinitialiser 2FA", "重置 Passkey": "Réinitialiser le Passkey", - "默认使用系统名称": "Le nom du système est utilisé par défaut" + "默认使用系统名称": "Le nom du système est utilisé par défaut", + "充值账单": "Factures de recharge", + "订单号": "N° de commande", + "支付金额": "Montant payé", + "待支付": "En attente", + "加载失败": "Échec du chargement", + "加载账单失败": "Échec du chargement des factures", + "暂无充值记录": "Aucune recharge", + "账单": "Factures", + "补单": "Compléter la commande", + "补单成功": "Commande complétée avec succès", + "补单失败": "Échec de la complétion de la commande", + "确认补单": "Confirmer la complétion", + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?" } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index fb6fbf990dee..dcb693ecd862 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -94,5 +94,22 @@ "允许通过 Passkey 登录 & 认证": "允许通过 Passkey 登录 & 认证", "确认解绑 Passkey": "确认解绑 Passkey", "解绑后将无法使用 Passkey 登录,确定要继续吗?": "解绑后将无法使用 Passkey 登录,确定要继续吗?", - "确认解绑": "确认解绑" + "确认解绑": "确认解绑", + "充值账单": "充值账单", + "订单号": "订单号", + "支付金额": "支付金额", + "待支付": "待支付", + "加载失败": "加载失败", + "加载账单失败": "加载账单失败", + "暂无充值记录": "暂无充值记录", + "账单": "账单", + "支付方式": "支付方式", + "支付宝": "支付宝", + "微信": "微信", + "补单": "补单", + "补单成功": "补单成功", + "补单失败": "补单失败", + "确认补单": "确认补单", + "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", + "操作": "操作" } diff --git a/web/src/pages/Setting/Chat/SettingsChats.jsx b/web/src/pages/Setting/Chat/SettingsChats.jsx index 01591c782615..f7f309ac9868 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -227,7 +227,7 @@ export default function SettingsChats(props) { const isDuplicate = chatConfigs.some( (config) => config.name === values.name && - (!isEdit || config.id !== editingConfig.id) + (!isEdit || config.id !== editingConfig.id), ); if (isDuplicate) { diff --git a/web/src/pages/Setting/Operation/SettingsLog.jsx b/web/src/pages/Setting/Operation/SettingsLog.jsx index a1f5a49fa792..d309a12b0606 100644 --- a/web/src/pages/Setting/Operation/SettingsLog.jsx +++ b/web/src/pages/Setting/Operation/SettingsLog.jsx @@ -18,7 +18,16 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useEffect, useState, useRef } from 'react'; -import { Button, Col, Form, Row, Spin, DatePicker, Typography, Modal } from '@douyinfe/semi-ui'; +import { + Button, + Col, + Form, + Row, + Spin, + DatePicker, + Typography, + Modal, +} from '@douyinfe/semi-ui'; import dayjs from 'dayjs'; import { useTranslation } from 'react-i18next'; import { @@ -90,40 +99,58 @@ export default function SettingsLog(props) { const targetTime = targetDate.format('YYYY-MM-DD HH:mm:ss'); const currentTime = now.format('YYYY-MM-DD HH:mm:ss'); const daysDiff = now.diff(targetDate, 'day'); - + Modal.confirm({ title: t('确认清除历史日志'), content: (

{t('当前时间')}: - {currentTime} + + {currentTime} +

{t('选择时间')}: - {targetTime} + + {targetTime} + {daysDiff > 0 && ( - ({t('约')} {daysDiff} {t('天前')}) + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + )}

-
- ⚠️ {t('注意')}: +
+ + ⚠️ {t('注意')}: + {t('将删除')} - {targetTime} + + {targetTime} + {daysDiff > 0 && ( - ({t('约')} {daysDiff} {t('天前')}) + + {' '} + ({t('约')} {daysDiff} {t('天前')}) + )} {t('之前的所有日志')}

- {t('此操作不可恢复,请仔细确认时间后再操作!')} + + {t('此操作不可恢复,请仔细确认时间后再操作!')} +

), @@ -203,10 +230,18 @@ export default function SettingsLog(props) { }); }} /> - + {t('将清除选定时间之前的所有日志')} - diff --git a/web/src/services/secureVerification.js b/web/src/services/secureVerification.js index 93cdd0a4dc2d..51f871a96941 100644 --- a/web/src/services/secureVerification.js +++ b/web/src/services/secureVerification.js @@ -21,7 +21,7 @@ import { API, showError } from '../helpers'; import { prepareCredentialRequestOptions, buildAssertionResult, - isPasskeySupported + isPasskeySupported, } from '../helpers/passkey'; /** @@ -35,46 +35,54 @@ export class SecureVerificationService { */ static async checkAvailableVerificationMethods() { try { - const [twoFAResponse, passkeyResponse, passkeySupported] = await Promise.all([ - API.get('/api/user/2fa/status'), - API.get('/api/user/passkey'), - isPasskeySupported() - ]); + const [twoFAResponse, passkeyResponse, passkeySupported] = + await Promise.all([ + API.get('/api/user/2fa/status'), + API.get('/api/user/passkey'), + isPasskeySupported(), + ]); console.log('=== DEBUGGING VERIFICATION METHODS ==='); console.log('2FA Response:', JSON.stringify(twoFAResponse, null, 2)); - console.log('Passkey Response:', JSON.stringify(passkeyResponse, null, 2)); - - const has2FA = twoFAResponse.data?.success && twoFAResponse.data?.data?.enabled === true; - const hasPasskey = passkeyResponse.data?.success && passkeyResponse.data?.data?.enabled === true; - + console.log( + 'Passkey Response:', + JSON.stringify(passkeyResponse, null, 2), + ); + + const has2FA = + twoFAResponse.data?.success && + twoFAResponse.data?.data?.enabled === true; + const hasPasskey = + passkeyResponse.data?.success && + passkeyResponse.data?.data?.enabled === true; + console.log('has2FA calculation:', { success: twoFAResponse.data?.success, dataExists: !!twoFAResponse.data?.data, enabled: twoFAResponse.data?.data?.enabled, - result: has2FA + result: has2FA, }); - + console.log('hasPasskey calculation:', { success: passkeyResponse.data?.success, dataExists: !!passkeyResponse.data?.data, enabled: passkeyResponse.data?.data?.enabled, - result: hasPasskey + result: hasPasskey, }); const result = { has2FA, hasPasskey, - passkeySupported + passkeySupported, }; - + return result; } catch (error) { console.error('Failed to check verification methods:', error); return { has2FA: false, hasPasskey: false, - passkeySupported: false + passkeySupported: false, }; } } @@ -92,7 +100,7 @@ export class SecureVerificationService { // 调用通用验证 API,验证成功后后端会设置 session const verifyResponse = await API.post('/api/verify', { method: '2fa', - code: code.trim() + code: code.trim(), }); if (!verifyResponse.data?.success) { @@ -115,7 +123,9 @@ export class SecureVerificationService { } // 准备WebAuthn选项 - const publicKey = prepareCredentialRequestOptions(beginResponse.data.data.options); + const publicKey = prepareCredentialRequestOptions( + beginResponse.data.data.options, + ); // 执行WebAuthn验证 const credential = await navigator.credentials.get({ publicKey }); @@ -127,14 +137,17 @@ export class SecureVerificationService { const assertionResult = buildAssertionResult(credential); // 完成验证 - const finishResponse = await API.post('/api/user/passkey/verify/finish', assertionResult); + const finishResponse = await API.post( + '/api/user/passkey/verify/finish', + assertionResult, + ); if (!finishResponse.data?.success) { throw new Error(finishResponse.data?.message || '验证失败'); } // 调用通用验证 API 设置 session(Passkey 验证已完成) const verifyResponse = await API.post('/api/verify', { - method: 'passkey' + method: 'passkey', }); if (!verifyResponse.data?.success) { @@ -191,27 +204,29 @@ export const createApiCalls = { * @param {string} method - HTTP方法,默认为 'POST' * @param {Object} extraData - 额外的请求数据 */ - custom: (url, method = 'POST', extraData = {}) => async () => { - // 新系统中,验证已通过中间件处理 - const data = extraData; - - let response; - switch (method.toUpperCase()) { - case 'GET': - response = await API.get(url, { params: data }); - break; - case 'POST': - response = await API.post(url, data); - break; - case 'PUT': - response = await API.put(url, data); - break; - case 'DELETE': - response = await API.delete(url, { data }); - break; - default: - throw new Error(`不支持的HTTP方法: ${method}`); - } - return response.data; - } -}; \ No newline at end of file + custom: + (url, method = 'POST', extraData = {}) => + async () => { + // 新系统中,验证已通过中间件处理 + const data = extraData; + + let response; + switch (method.toUpperCase()) { + case 'GET': + response = await API.get(url, { params: data }); + break; + case 'POST': + response = await API.post(url, data); + break; + case 'PUT': + response = await API.put(url, data); + break; + case 'DELETE': + response = await API.delete(url, { data }); + break; + default: + throw new Error(`不支持的HTTP方法: ${method}`); + } + return response.data; + }, +}; From 2389dbafc5b7f134f678c3120616ae1c76d1f490 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 7 Oct 2025 00:46:47 +0800 Subject: [PATCH 43/79] =?UTF-8?q?=E2=9C=A8=20feat(topup):=20Admin-wide=20t?= =?UTF-8?q?opup=20listing=20and=20route=20reorganization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow administrators to view all platform topup orders and streamline admin-only routes. Frontend - TopupHistoryModal: dynamically switch endpoint by role - Admin → GET /api/user/topup (all orders) - Non-admin → GET /api/user/topup/self (own orders) - Use shared utils `isAdmin()`; keep logic centralized and DRY - Minor UI: set admin action button theme to outline for clarity Backend - model/topup.go: add GetAllTopUps(pageInfo) with pagination (ordered by id desc) - controller/topup.go: add GetAllTopUps handler returning PageInfo response - router/api-router.go: - Add admin route GET /api/user/topup (AdminAuth) - Move POST /api/user/topup/complete to adminRoute (keeps path stable, consolidates admin endpoints) Security/Behavior - Admin-only endpoints now reside under the admin route group with AdminAuth - No behavior change for regular users; no schema changes Affected files - model/topup.go - controller/topup.go - router/api-router.go - web/src/components/topup/modals/TopupHistoryModal.jsx --- controller/topup.go | 15 ++++++++++ model/topup.go | 29 +++++++++++++++++++ router/api-router.go | 3 +- .../topup/modals/TopupHistoryModal.jsx | 7 +++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/controller/topup.go b/controller/topup.go index e6f6eef2ddd8..8626695f354f 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -330,6 +330,21 @@ func GetUserTopUps(c *gin.Context) { common.ApiSuccess(c, pageInfo) } +// GetAllTopUps 管理员获取全平台充值记录 +func GetAllTopUps(c *gin.Context) { + pageInfo := common.GetPageQuery(c) + + topups, total, err := model.GetAllTopUps(pageInfo) + if err != nil { + common.ApiError(c, err) + return + } + + pageInfo.SetTotal(int(total)) + pageInfo.SetItems(topups) + common.ApiSuccess(c, pageInfo) +} + type AdminCompleteTopupRequest struct { TradeNo string `json:"trade_no"` } diff --git a/model/topup.go b/model/topup.go index c280db430931..e46a5a88f109 100644 --- a/model/topup.go +++ b/model/topup.go @@ -136,6 +136,35 @@ func GetUserTopUps(userId int, pageInfo *common.PageInfo) (topups []*TopUp, tota return topups, total, nil } +// GetAllTopUps 获取全平台的充值记录(管理员使用) +func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err = tx.Model(&TopUp{}).Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + + return topups, total, nil +} + // ManualCompleteTopUp 管理员手动完成订单并给用户充值 func ManualCompleteTopUp(tradeNo string) error { if tradeNo == "" { diff --git a/router/api-router.go b/router/api-router.go index 49a42360270f..963abd10583f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -80,7 +80,6 @@ func SetApiRouter(router *gin.Engine) { selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay) selfRoute.POST("/stripe/amount", controller.RequestStripeAmount) selfRoute.POST("/aff_transfer", controller.TransferAffQuota) - selfRoute.POST("/topup/complete", middleware.AdminAuth(), controller.AdminCompleteTopUp) selfRoute.PUT("/setting", controller.UpdateUserSetting) // 2FA routes @@ -95,6 +94,8 @@ func SetApiRouter(router *gin.Engine) { adminRoute.Use(middleware.AdminAuth()) { adminRoute.GET("/", controller.GetAllUsers) + adminRoute.GET("/topup", controller.GetAllTopUps) + adminRoute.POST("/topup/complete", controller.AdminCompleteTopUp) adminRoute.GET("/search", controller.SearchUsers) adminRoute.GET("/:id", controller.GetUser) adminRoute.POST("/", controller.CreateUser) diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index e2548f388e95..fe50c6864ba0 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -63,9 +63,10 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { const loadTopups = async (currentPage, currentPageSize) => { setLoading(true); try { - const res = await API.get( - `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`, - ); + const endpoint = isAdmin() + ? `/api/user/topup?p=${currentPage}&page_size=${currentPageSize}` + : `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`; + const res = await API.get(endpoint); const { success, message, data } = res.data; if (success) { setTopups(data.items || []); From a8c9b24c7eb71b755924a5793166ab5fe42d44a3 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 7 Oct 2025 00:55:01 +0800 Subject: [PATCH 44/79] =?UTF-8?q?=F0=9F=94=8E=20feat(topup):=20add=20order?= =?UTF-8?q?=20number=20search=20for=20billing=20history=20(admin=20and=20u?= =?UTF-8?q?ser)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable searching topup records by trade_no across both admin-wide and user-only views. Frontend - TopupHistoryModal.jsx: - Add search input with prefix icon (IconSearch) to filter by order number - Send `keyword` query param to backend; works with both endpoints: - Admin: GET /api/user/topup?p=1&page_size=10&keyword=... - User: GET /api/user/topup/self?p=1&page_size=10&keyword=... - Keep endpoint auto-switching based on role (isAdmin) - Minor UI polish: outlined admin action button; keep Coins icon for amount Backend - model/topup.go: - Add SearchUserTopUps(userId, keyword, pageInfo) - Add SearchAllTopUps(keyword, pageInfo) - Both support pagination and `trade_no LIKE %keyword%` filtering (ordered by id desc) - controller/topup.go: - GetUserTopUps / GetAllTopUps accept optional `keyword` and route to search functions when present Routes - No new endpoints; search is enabled via `keyword` on existing: - GET /api/user/topup - GET /api/user/topup/self Affected files - model/topup.go - controller/topup.go - web/src/components/topup/modals/TopupHistoryModal.jsx --- controller/topup.go | 28 ++++++-- model/topup.go | 68 +++++++++++++++++++ .../topup/modals/TopupHistoryModal.jsx | 22 ++++-- 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/controller/topup.go b/controller/topup.go index 8626695f354f..76a2521d9a72 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -318,8 +318,18 @@ func RequestAmount(c *gin.Context) { func GetUserTopUps(c *gin.Context) { userId := c.GetInt("id") pageInfo := common.GetPageQuery(c) - - topups, total, err := model.GetUserTopUps(userId, pageInfo) + keyword := c.Query("keyword") + + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo) + } else { + topups, total, err = model.GetUserTopUps(userId, pageInfo) + } if err != nil { common.ApiError(c, err) return @@ -333,8 +343,18 @@ func GetUserTopUps(c *gin.Context) { // GetAllTopUps 管理员获取全平台充值记录 func GetAllTopUps(c *gin.Context) { pageInfo := common.GetPageQuery(c) - - topups, total, err := model.GetAllTopUps(pageInfo) + keyword := c.Query("keyword") + + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchAllTopUps(keyword, pageInfo) + } else { + topups, total, err = model.GetAllTopUps(pageInfo) + } if err != nil { common.ApiError(c, err) return diff --git a/model/topup.go b/model/topup.go index e46a5a88f109..380f5851d9a3 100644 --- a/model/topup.go +++ b/model/topup.go @@ -165,6 +165,74 @@ func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err return topups, total, nil } +// SearchUserTopUps 按订单号搜索某用户的充值记录 +func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}).Where("user_id = ?", userId) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用) +func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + // ManualCompleteTopUp 管理员手动完成订单并给用户充值 func ManualCompleteTopUp(tradeNo string) error { if tradeNo == "" { diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index fe50c6864ba0..57916a9aa4e7 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -25,12 +25,14 @@ import { Toast, Empty, Button, + Input, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { Coins } from 'lucide-react'; +import { IconSearch } from '@douyinfe/semi-icons'; import { API, timestamp2string } from '../../../helpers'; import { isAdmin } from '../../../helpers/utils'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; @@ -57,15 +59,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(''); const isMobile = useIsMobile(); const loadTopups = async (currentPage, currentPageSize) => { setLoading(true); try { - const endpoint = isAdmin() - ? `/api/user/topup?p=${currentPage}&page_size=${currentPageSize}` - : `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`; + const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self'; + const qs = + `p=${currentPage}&page_size=${currentPageSize}` + + (keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''); + const endpoint = `${base}?${qs}`; const res = await API.get(endpoint); const { success, message, data } = res.data; if (success) { @@ -86,7 +91,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { if (visible) { loadTopups(page, pageSize); } - }, [visible, page, pageSize]); + }, [visible, page, pageSize, keyword]); const handlePageChange = (currentPage) => { setPage(currentPage); @@ -221,6 +226,15 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { footer={null} size={isMobile ? 'full-width' : 'large'} > +
+ } + placeholder={t('订单号')} + value={keyword} + onChange={setKeyword} + showClear + /> +
Date: Tue, 7 Oct 2025 14:13:14 +0800 Subject: [PATCH 45/79] =?UTF-8?q?=E2=9D=A4=20fix(topup):=20prevent=20nil-p?= =?UTF-8?q?ointer=20in=20Epay=20callback;=20reset=20page=20on=20search=20A?= =?UTF-8?q?dd=20early=20return=20when=20Epay=20client=20is=20missing=20in?= =?UTF-8?q?=20controller/topup.go=20to=20avoid=20panic=20Introduce=20handl?= =?UTF-8?q?eKeywordChange=20in=20TopupHistoryModal.jsx=20to=20reset=20page?= =?UTF-8?q?=20to=201=20when=20keyword=20updates=20Wire=20input=20onChange?= =?UTF-8?q?=20to=20new=20handler;=20minor=20UX=20improvement=20to=20avoid?= =?UTF-8?q?=20empty=20results=20on=20pagination=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/topup.go | 2 +- web/src/components/topup/modals/TopupHistoryModal.jsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/controller/topup.go b/controller/topup.go index 76a2521d9a72..62bc676dae62 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -237,8 +237,8 @@ func EpayNotify(c *gin.Context) { _, err := c.Writer.Write([]byte("fail")) if err != nil { log.Println("易支付回调写入失败") - return } + return } verifyInfo, err := client.Verify(params) if err == nil && verifyInfo.VerifyStatus { diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index 57916a9aa4e7..12abfbd79e05 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -102,6 +102,11 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { setPage(1); }; + const handleKeywordChange = (value) => { + setKeyword(value); + setPage(1); + }; + // 管理员补单 const handleAdminComplete = async (tradeNo) => { try { @@ -231,7 +236,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { prefix={} placeholder={t('订单号')} value={keyword} - onChange={setKeyword} + onChange={handleKeywordChange} showClear /> From 689105764738b9b2d0cefeb0eca2a333b68e5ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=93=E3=83=93=E3=82=A4?= Date: Wed, 8 Oct 2025 10:43:47 +0800 Subject: [PATCH 46/79] feat(web): add settings & pages of privacy policy & user agreement --- controller/misc.go | 22 ++ model/option.go | 2 + router/api-router.go | 2 + web/src/App.jsx | 18 ++ web/src/components/auth/RegisterForm.jsx | 64 +++++ web/src/components/settings/OtherSetting.jsx | 58 +++++ web/src/i18n/locales/en.json | 19 +- web/src/i18n/locales/fr.json | 21 +- web/src/i18n/locales/zh.json | 21 +- web/src/pages/PrivacyPolicy/index.jsx | 249 ++++++++++++++++++ web/src/pages/UserAgreement/index.jsx | 252 +++++++++++++++++++ web/src/utils/contentDetector.js | 61 +++++ 12 files changed, 786 insertions(+), 3 deletions(-) create mode 100644 web/src/pages/PrivacyPolicy/index.jsx create mode 100644 web/src/pages/UserAgreement/index.jsx create mode 100644 web/src/utils/contentDetector.js diff --git a/controller/misc.go b/controller/misc.go index a3e017f87f50..38e98fe9c5eb 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -151,6 +151,28 @@ func GetAbout(c *gin.Context) { return } +func GetUserAgreement(c *gin.Context) { + common.OptionMapRWMutex.RLock() + defer common.OptionMapRWMutex.RUnlock() + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": common.OptionMap["UserAgreement"], + }) + return +} + +func GetPrivacyPolicy(c *gin.Context) { + common.OptionMapRWMutex.RLock() + defer common.OptionMapRWMutex.RUnlock() + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": common.OptionMap["PrivacyPolicy"], + }) + return +} + func GetMidjourney(c *gin.Context) { common.OptionMapRWMutex.RLock() defer common.OptionMapRWMutex.RUnlock() diff --git a/model/option.go b/model/option.go index 77525ea25239..7f341ff33349 100644 --- a/model/option.go +++ b/model/option.go @@ -61,6 +61,8 @@ func InitOptionMap() { common.OptionMap["SMTPToken"] = "" common.OptionMap["SMTPSSLEnabled"] = strconv.FormatBool(common.SMTPSSLEnabled) common.OptionMap["Notice"] = "" + common.OptionMap["UserAgreement"] = "" + common.OptionMap["PrivacyPolicy"] = "" common.OptionMap["About"] = "" common.OptionMap["HomePageContent"] = "" common.OptionMap["Footer"] = common.Footer diff --git a/router/api-router.go b/router/api-router.go index 963abd10583f..7eefa936a557 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -20,6 +20,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/notice", controller.GetNotice) + apiRouter.GET("/user-agreement", controller.GetUserAgreement) + apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy) apiRouter.GET("/about", controller.GetAbout) //apiRouter.GET("/midjourney", controller.GetMidjourney) apiRouter.GET("/home_page_content", controller.GetHomePageContent) diff --git a/web/src/App.jsx b/web/src/App.jsx index 635742f9161e..06e364897cf6 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -51,6 +51,8 @@ import SetupCheck from './components/layout/SetupCheck'; const Home = lazy(() => import('./pages/Home')); const Dashboard = lazy(() => import('./pages/Dashboard')); const About = lazy(() => import('./pages/About')); +const UserAgreement = lazy(() => import('./pages/UserAgreement')); +const PrivacyPolicy = lazy(() => import('./pages/PrivacyPolicy')); function App() { const location = useLocation(); @@ -301,6 +303,22 @@ function App() { } /> + } key={location.pathname}> + + + } + /> + } key={location.pathname}> + + + } + /> { const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); + const [agreedToTerms, setAgreedToTerms] = useState(false); + const [hasUserAgreement, setHasUserAgreement] = useState(false); + const [hasPrivacyPolicy, setHasPrivacyPolicy] = useState(false); const logo = getLogo(); const systemName = getSystemName(); @@ -106,6 +109,28 @@ const RegisterForm = () => { setTurnstileEnabled(true); setTurnstileSiteKey(status.turnstile_site_key); } + + // 检查用户协议和隐私政策是否已设置 + const checkTermsAvailability = async () => { + try { + const [userAgreementRes, privacyPolicyRes] = await Promise.all([ + API.get('/api/user-agreement'), + API.get('/api/privacy-policy') + ]); + + if (userAgreementRes.data.success && userAgreementRes.data.data) { + setHasUserAgreement(true); + } + + if (privacyPolicyRes.data.success && privacyPolicyRes.data.data) { + setHasPrivacyPolicy(true); + } + } catch (error) { + console.error('检查用户协议和隐私政策失败:', error); + } + }; + + checkTermsAvailability(); }, [status]); useEffect(() => { @@ -505,6 +530,44 @@ const RegisterForm = () => { )} + {(hasUserAgreement || hasPrivacyPolicy) && ( +
+ setAgreedToTerms(checked)} + > + + {t('我已阅读并同意')} + {hasUserAgreement && ( + <> + + {t('用户协议')} + + + )} + {hasUserAgreement && hasPrivacyPolicy && t('和')} + {hasPrivacyPolicy && ( + <> + + {t('隐私政策')} + + + )} + + +
+ )} +
diff --git a/web/src/components/settings/OtherSetting.jsx b/web/src/components/settings/OtherSetting.jsx index 18119d2427db..6a159e813446 100644 --- a/web/src/components/settings/OtherSetting.jsx +++ b/web/src/components/settings/OtherSetting.jsx @@ -38,6 +38,8 @@ const OtherSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ Notice: '', + UserAgreement: '', + PrivacyPolicy: '', SystemName: '', Logo: '', Footer: '', @@ -69,6 +71,8 @@ const OtherSetting = () => { const [loadingInput, setLoadingInput] = useState({ Notice: false, + UserAgreement: false, + PrivacyPolicy: false, SystemName: false, Logo: false, HomePageContent: false, @@ -96,6 +100,32 @@ const OtherSetting = () => { setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); } }; + // 通用设置 - UserAgreement + const submitUserAgreement = async () => { + try { + setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: true })); + await updateOption('UserAgreement', inputs.UserAgreement); + showSuccess(t('用户协议已更新')); + } catch (error) { + console.error(t('用户协议更新失败'), error); + showError(t('用户协议更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ ...loadingInput, UserAgreement: false })); + } + }; + // 通用设置 - PrivacyPolicy + const submitPrivacyPolicy = async () => { + try { + setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: true })); + await updateOption('PrivacyPolicy', inputs.PrivacyPolicy); + showSuccess(t('隐私政策已更新')); + } catch (error) { + console.error(t('隐私政策更新失败'), error); + showError(t('隐私政策更新失败')); + } finally { + setLoadingInput((loadingInput) => ({ ...loadingInput, PrivacyPolicy: false })); + } + }; // 个性化设置 const formAPIPersonalization = useRef(); // 个性化设置 - SystemName @@ -324,6 +354,34 @@ const OtherSetting = () => { + + + + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 8b4f1b41111b..7b0f4793242a 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -244,6 +244,8 @@ "检查更新": "Check for updates", "公告": "Announcement", "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Enter user agreement content here, supports Markdown & HTML code", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Enter privacy policy content here, supports Markdown & HTML code", "保存公告": "Save Announcement", "个性化设置": "Personalization Settings", "系统名称": "System Name", @@ -1260,6 +1262,8 @@ "仅修改展示粒度,统计精确到小时": "Only modify display granularity, statistics accurate to the hour", "当运行通道全部测试时,超过此时间将自动禁用通道": "When running all channel tests, the channel will be automatically disabled when this time is exceeded", "设置公告": "Set notice", + "设置用户协议": "Set user agreement", + "设置隐私政策": "Set privacy policy", "设置 Logo": "Set Logo", "设置首页内容": "Set home page content", "设置关于": "Set about", @@ -2259,5 +2263,18 @@ "补单成功": "Order completed successfully", "补单失败": "Failed to complete order", "确认补单": "Confirm Order Completion", - "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?" + "是否将该订单标记为成功并为用户入账?": "Mark this order as successful and credit the user?", + "用户协议": "User Agreement", + "隐私政策": "Privacy Policy", + "用户协议更新失败": "Failed to update user agreement", + "隐私政策更新失败": "Failed to update privacy policy", + "管理员未设置用户协议内容": "Administrator has not set user agreement content", + "管理员未设置隐私政策内容": "Administrator has not set privacy policy content", + "加载用户协议内容失败...": "Failed to load user agreement content...", + "加载隐私政策内容失败...": "Failed to load privacy policy content...", + "我已阅读并同意": "I have read and agree to", + "和": " and ", + "请先阅读并同意用户协议和隐私政策": "Please read and agree to the user agreement and privacy policy first", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "After filling in the user agreement content, users will be required to check that they have read the user agreement when registering", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "After filling in the privacy policy content, users will be required to check that they have read the privacy policy when registering" } diff --git a/web/src/i18n/locales/fr.json b/web/src/i18n/locales/fr.json index e5db1125bbae..ee2a3c297cd0 100644 --- a/web/src/i18n/locales/fr.json +++ b/web/src/i18n/locales/fr.json @@ -2251,5 +2251,24 @@ "补单成功": "Commande complétée avec succès", "补单失败": "Échec de la complétion de la commande", "确认补单": "Confirmer la complétion", - "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?" + "是否将该订单标记为成功并为用户入账?": "Marquer cette commande comme réussie et créditer l'utilisateur ?", + "用户协议": "Accord utilisateur", + "隐私政策": "Politique de confidentialité", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de l'accord utilisateur, prend en charge le code Markdown et HTML", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "Saisissez ici le contenu de la politique de confidentialité, prend en charge le code Markdown et HTML", + "设置用户协议": "Définir l'accord utilisateur", + "设置隐私政策": "Définir la politique de confidentialité", + "用户协议已更新": "L'accord utilisateur a été mis à jour", + "隐私政策已更新": "La politique de confidentialité a été mise à jour", + "用户协议更新失败": "Échec de la mise à jour de l'accord utilisateur", + "隐私政策更新失败": "Échec de la mise à jour de la politique de confidentialité", + "管理员未设置用户协议内容": "L'administrateur n'a pas défini le contenu de l'accord utilisateur", + "管理员未设置隐私政策内容": "L'administrateur n'a pas défini le contenu de la politique de confidentialité", + "加载用户协议内容失败...": "Échec du chargement du contenu de l'accord utilisateur...", + "加载隐私政策内容失败...": "Échec du chargement du contenu de la politique de confidentialité...", + "我已阅读并同意": "J'ai lu et j'accepte", + "和": " et ", + "请先阅读并同意用户协议和隐私政策": "Veuillez d'abord lire et accepter l'accord utilisateur et la politique de confidentialité", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "Après avoir rempli le contenu de l'accord utilisateur, les utilisateurs devront cocher qu'ils ont lu l'accord utilisateur lors de l'inscription", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "Après avoir rempli le contenu de la politique de confidentialité, les utilisateurs devront cocher qu'ils ont lu la politique de confidentialité lors de l'inscription" } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index dcb693ecd862..661cbea92eb3 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -111,5 +111,24 @@ "补单失败": "补单失败", "确认补单": "确认补单", "是否将该订单标记为成功并为用户入账?": "是否将该订单标记为成功并为用户入账?", - "操作": "操作" + "操作": "操作", + "用户协议": "用户协议", + "隐私政策": "隐私政策", + "在此输入用户协议内容,支持 Markdown & HTML 代码": "在此输入用户协议内容,支持 Markdown & HTML 代码", + "在此输入隐私政策内容,支持 Markdown & HTML 代码": "在此输入隐私政策内容,支持 Markdown & HTML 代码", + "设置用户协议": "设置用户协议", + "设置隐私政策": "设置隐私政策", + "用户协议已更新": "用户协议已更新", + "隐私政策已更新": "隐私政策已更新", + "用户协议更新失败": "用户协议更新失败", + "隐私政策更新失败": "隐私政策更新失败", + "管理员未设置用户协议内容": "管理员未设置用户协议内容", + "管理员未设置隐私政策内容": "管理员未设置隐私政策内容", + "加载用户协议内容失败...": "加载用户协议内容失败...", + "加载隐私政策内容失败...": "加载隐私政策内容失败...", + "我已阅读并同意": "我已阅读并同意", + "和": "和", + "请先阅读并同意用户协议和隐私政策": "请先阅读并同意用户协议和隐私政策", + "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议": "填写用户协议内容后,用户注册时将被要求勾选已阅读用户协议", + "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策": "填写隐私政策内容后,用户注册时将被要求勾选已阅读隐私政策" } diff --git a/web/src/pages/PrivacyPolicy/index.jsx b/web/src/pages/PrivacyPolicy/index.jsx new file mode 100644 index 000000000000..373b8a282090 --- /dev/null +++ b/web/src/pages/PrivacyPolicy/index.jsx @@ -0,0 +1,249 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useEffect, useState } from 'react'; +import { API, showError } from '../../helpers'; +import { Empty } from '@douyinfe/semi-ui'; +import { + IllustrationConstruction, + IllustrationConstructionDark, +} from '@douyinfe/semi-illustrations'; +import { useTranslation } from 'react-i18next'; +import MarkdownRenderer from '../../components/common/markdown/MarkdownRenderer'; +import { getContentType } from '../../utils/contentDetector'; + +const PrivacyPolicy = () => { + const { t } = useTranslation(); + const [privacyPolicy, setPrivacyPolicy] = useState(''); + const [privacyPolicyLoaded, setPrivacyPolicyLoaded] = useState(false); + const [contentType, setContentType] = useState('empty'); + const [htmlBody, setHtmlBody] = useState(''); + const [htmlStyles, setHtmlStyles] = useState(''); + const [htmlLinks, setHtmlLinks] = useState([]); + // Height of the top navigation/header in pixels. Adjust if your header is a different height. + const HEADER_HEIGHT = 64; + + const displayPrivacyPolicy = async () => { + // 先从缓存中获取 + const cachedContent = localStorage.getItem('privacy_policy') || ''; + if (cachedContent) { + setPrivacyPolicy(cachedContent); + const ct = getContentType(cachedContent); + setContentType(ct); + if (ct === 'html') { + // parse cached HTML to extract body and inline styles + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(cachedContent, 'text/html'); + setHtmlBody(doc.body ? doc.body.innerHTML : cachedContent); + const styles = Array.from(doc.querySelectorAll('style')) + .map((s) => s.innerHTML) + .join('\n'); + setHtmlStyles(styles); + const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')) + .map((l) => l.getAttribute('href') || l.href) + .filter(Boolean); + setHtmlLinks(links); + } catch (e) { + setHtmlBody(cachedContent); + setHtmlStyles(''); + setHtmlLinks([]); + } + } + } + + try { + const res = await API.get('/api/privacy-policy'); + const { success, message, data } = res.data; + if (success && data) { + // 直接使用原始数据,不进行任何预处理 + setPrivacyPolicy(data); + const ct = getContentType(data); + setContentType(ct); + // 如果是完整 HTML 文档,解析 body 内容并提取内联样式放到 head + if (ct === 'html') { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(data, 'text/html'); + setHtmlBody(doc.body ? doc.body.innerHTML : data); + const styles = Array.from(doc.querySelectorAll('style')) + .map((s) => s.innerHTML) + .join('\n'); + setHtmlStyles(styles); + const links = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')) + .map((l) => l.getAttribute('href') || l.href) + .filter(Boolean); + setHtmlLinks(links); + } catch (e) { + setHtmlBody(data); + setHtmlStyles(''); + setHtmlLinks([]); + } + } else { + setHtmlBody(''); + setHtmlStyles(''); + setHtmlLinks([]); + } + localStorage.setItem('privacy_policy', data); + } else { + if (!cachedContent) { + showError(message || t('加载隐私政策内容失败...')); + setPrivacyPolicy(''); + setContentType('empty'); + } + } + } catch (error) { + if (!cachedContent) { + showError(t('加载隐私政策内容失败...')); + setPrivacyPolicy(''); + setContentType('empty'); + } + } + setPrivacyPolicyLoaded(true); + }; + + useEffect(() => { + displayPrivacyPolicy(); + }, []); + + // inject inline styles for parsed HTML content and cleanup on unmount or styles change + useEffect(() => { + const styleId = 'privacy-policy-inline-styles'; + const createdLinkIds = []; + + if (htmlStyles) { + let styleEl = document.getElementById(styleId); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.id = styleId; + styleEl.type = 'text/css'; + document.head.appendChild(styleEl); + } + styleEl.innerHTML = htmlStyles; + } else { + const el = document.getElementById(styleId); + if (el) el.remove(); + } + + if (htmlLinks && htmlLinks.length) { + htmlLinks.forEach((href, idx) => { + try { + const existing = document.querySelector(`link[rel="stylesheet"][href="${href}"]`); + if (existing) return; + const linkId = `${styleId}-link-${idx}`; + const linkEl = document.createElement('link'); + linkEl.id = linkId; + linkEl.rel = 'stylesheet'; + linkEl.href = href; + document.head.appendChild(linkEl); + createdLinkIds.push(linkId); + } catch (e) { + // ignore + } + }); + } + + return () => { + const el = document.getElementById(styleId); + if (el) el.remove(); + createdLinkIds.forEach((id) => { + const l = document.getElementById(id); + if (l) l.remove(); + }); + }; + }, [htmlStyles]); + + const renderContent = () => { + if (!privacyPolicyLoaded) { + return ( +
+ +
+ ); + } + + if (contentType === 'empty' || !privacyPolicy) { + return ( +
+ + } + darkModeImage={ + + } + description={t('管理员未设置隐私政策内容')} + /> +
+ ); + } + + if (contentType === 'url') { + return ( +