mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-02 00:52:04 +00:00
Compare commits
28 Commits
coderabbit
...
v0.9.3-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
407da544fe | ||
|
|
98261ec9fa | ||
|
|
74f93d41f3 | ||
|
|
021892b17d | ||
|
|
9f44116260 | ||
|
|
8a56795bd8 | ||
|
|
1154077eea | ||
|
|
42861bc5fb | ||
|
|
7074ea2ed6 | ||
|
|
414be64d33 | ||
|
|
c1137027e6 | ||
|
|
ff77ba1157 | ||
|
|
3da7cebec6 | ||
|
|
7dc5f8c92d | ||
|
|
7763f11da7 | ||
|
|
8026e5142b | ||
|
|
9e33c83351 | ||
|
|
d2492d2af9 | ||
|
|
93e30703d4 | ||
|
|
b39885be1e | ||
|
|
15b21c075f | ||
|
|
922ecef31e | ||
|
|
573b5c3e3b | ||
|
|
7c7f9abd04 | ||
|
|
050e0221c7 | ||
|
|
a57a36a739 | ||
|
|
0046282fb8 | ||
|
|
723eefe9d8 |
142
.github/workflows/electron-build.yml
vendored
Normal file
142
.github/workflows/electron-build.yml
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
name: Build Electron App
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*' # Triggers on version tags like v1.0.0
|
||||
workflow_dispatch: # Allows manual triggering
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
# 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
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '>=1.18.0'
|
||||
|
||||
- name: Build frontend
|
||||
env:
|
||||
CI: ""
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
run: |
|
||||
cd web
|
||||
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 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 mod download
|
||||
go build -ldflags "-s -w -X 'one-api/common.Version=$(git describe --tags)'" -o new-api.exe
|
||||
|
||||
- name: Update Electron version
|
||||
run: |
|
||||
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.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]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
REST=${BASH_REMATCH[4]}
|
||||
|
||||
VERSION="$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
# If there's extra content, append it without adding -dev
|
||||
if [[ -n "$REST" ]]; then
|
||||
VERSION="$VERSION$REST"
|
||||
fi
|
||||
fi
|
||||
npm version $VERSION --no-git-tag-version --allow-same-version
|
||||
|
||||
- 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/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
windows-build/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
overwrite_files: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
11
.github/workflows/linux-release.yml
vendored
11
.github/workflows/linux-release.yml
vendored
@@ -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
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
new-api
|
||||
new-api-arm64
|
||||
new-api-*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
|
||||
7
.github/workflows/macos-release.yml
vendored
7
.github/workflows/macos-release.yml
vendored
@@ -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
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: new-api-macos
|
||||
files: new-api-macos-*
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
|
||||
7
.github/workflows/windows-release.yml
vendored
7
.github/workflows/windows-release.yml
vendored
@@ -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
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: new-api.exe
|
||||
files: new-api-*.exe
|
||||
draft: true
|
||||
generate_release_notes: true
|
||||
env:
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -9,6 +9,10 @@ logs
|
||||
web/dist
|
||||
.env
|
||||
one-api
|
||||
new-api
|
||||
.DS_Store
|
||||
tiktoken_cache
|
||||
.eslintcache
|
||||
.eslintcache
|
||||
|
||||
electron/node_modules
|
||||
electron/dist
|
||||
30
README.en.md
30
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)
|
||||
|
||||
|
||||
30
README.fr.md
30
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)
|
||||
|
||||
|
||||
18
README.ja.md
18
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)
|
||||
|
||||
22
README.md
22
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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
73
electron/README.md
Normal file
73
electron/README.md
Normal file
@@ -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/`
|
||||
41
electron/build.sh
Executable file
41
electron/build.sh
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building New API Electron App..."
|
||||
|
||||
echo "Step 1: Building frontend..."
|
||||
cd ../web
|
||||
DISABLE_ESLINT_PLUGIN='true' bun run build
|
||||
cd ../electron
|
||||
|
||||
echo "Step 2: Building Go backend..."
|
||||
cd ..
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "Building for macOS..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:mac
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
echo "Building for Linux..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:linux
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "win32" ]]; then
|
||||
echo "Building for Windows..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api.exe
|
||||
cd electron
|
||||
npm install
|
||||
npm run build:win
|
||||
else
|
||||
echo "Unknown OS, building for current platform..."
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o new-api
|
||||
cd electron
|
||||
npm install
|
||||
npm run build
|
||||
fi
|
||||
|
||||
echo "Build complete! Check electron/dist/ for output."
|
||||
60
electron/create-tray-icon.js
Normal file
60
electron/create-tray-icon.js
Normal file
@@ -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.');
|
||||
}
|
||||
18
electron/entitlements.mac.plist
Normal file
18
electron/entitlements.mac.plist
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
electron/icon.png
Normal file
BIN
electron/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
590
electron/main.js
Normal file
590
electron/main.js
Normal file
@@ -0,0 +1,590 @@
|
||||
const { app, BrowserWindow, dialog, Tray, Menu, shell } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
|
||||
let mainWindow;
|
||||
let serverProcess;
|
||||
let tray = null;
|
||||
let serverErrorLogs = [];
|
||||
const PORT = 3000;
|
||||
const DEV_FRONTEND_PORT = 5173; // Vite dev server port
|
||||
|
||||
// 保存日志到文件并打开
|
||||
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 窗口\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 - 重新启动应用'
|
||||
};
|
||||
}
|
||||
|
||||
// 检测权限错误
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Check if a server is available with retry logic
|
||||
function checkServerAvailability(port, maxRetries = 30, retryDelay = 1000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
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();
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
if (currentAttempt >= maxRetries) {
|
||||
reject(new Error(`Connection timeout on port ${port} after ${maxRetries} attempts`));
|
||||
} else {
|
||||
setTimeout(tryConnect, retryDelay);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
tryConnect();
|
||||
});
|
||||
}
|
||||
|
||||
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 后端和前端开发服务器
|
||||
// 只需要等待前端开发服务器就绪
|
||||
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() };
|
||||
|
||||
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 binaryPath = getBinaryPath();
|
||||
const workingDir = process.resourcesPath;
|
||||
|
||||
console.log('Starting server from:', binaryPath);
|
||||
|
||||
serverProcess = spawn(binaryPath, [], {
|
||||
env,
|
||||
cwd: workingDir
|
||||
});
|
||||
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
console.log(`Server: ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
const errorMsg = data.toString();
|
||||
console.error(`Server Error: ${errorMsg}`);
|
||||
serverErrorLogs.push(errorMsg);
|
||||
// 只保留最近的100条错误日志
|
||||
if (serverErrorLogs.length > 100) {
|
||||
serverErrorLogs.shift();
|
||||
}
|
||||
});
|
||||
|
||||
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}`);
|
||||
|
||||
// 如果退出代码不是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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const loadPort = isDev ? DEV_FRONTEND_PORT : PORT;
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1080,
|
||||
height: 720,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
},
|
||||
title: 'New API',
|
||||
icon: path.join(__dirname, 'icon.png')
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`http://127.0.0.1:${loadPort}`);
|
||||
|
||||
console.log(`Loading from: http://127.0.0.1:${loadPort}`);
|
||||
|
||||
if (isDev) {
|
||||
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);
|
||||
|
||||
// 分析启动失败的错误
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
});
|
||||
3677
electron/package-lock.json
generated
Normal file
3677
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
101
electron/package.json
Normal file
101
electron/package.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"name": "new-api-electron",
|
||||
"version": "1.0.0",
|
||||
"description": "New API - AI Model Gateway Desktop Application",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start-app": "electron .",
|
||||
"dev-app": "cross-env 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": "QuantumNous",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/QuantumNous/new-api"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "28.3.3",
|
||||
"electron-builder": "^24.9.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.newapi.desktop",
|
||||
"productName": "New-API-App",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
18
electron/preload.js
Normal file
18
electron/preload.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const { contextBridge } = require('electron');
|
||||
|
||||
// 获取数据目录路径(用于显示给用户)
|
||||
// 优先使用主进程设置的真实路径,如果没有则回退到手动拼接
|
||||
function getDataDirPath() {
|
||||
// 如果主进程已设置真实路径,直接使用
|
||||
if (process.env.ELECTRON_DATA_DIR) {
|
||||
return process.env.ELECTRON_DATA_DIR;
|
||||
}
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
isElectron: true,
|
||||
version: process.versions.electron,
|
||||
platform: process.platform,
|
||||
versions: process.versions,
|
||||
dataDir: getDataDirPath()
|
||||
});
|
||||
BIN
electron/tray-icon-windows.png
Normal file
BIN
electron/tray-icon-windows.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
electron/tray-iconTemplate.png
Normal file
BIN
electron/tray-iconTemplate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 459 B |
BIN
electron/tray-iconTemplate@2x.png
Normal file
BIN
electron/tray-iconTemplate@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 754 B |
@@ -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 {
|
||||
@@ -1044,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
|
||||
|
||||
@@ -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 信息
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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' && (
|
||||
<Banner
|
||||
type='warning'
|
||||
type={isElectron ? 'info' : 'warning'}
|
||||
closeIcon={null}
|
||||
title={t('数据库警告')}
|
||||
title={isElectron ? t('本地数据存储') : t('数据库警告')}
|
||||
description={
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||
)}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
<strong>
|
||||
isElectron ? (
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||
'您的数据将安全地存储在本地计算机上。所有配置、用户信息和使用记录都会自动保存,关闭应用后不会丢失。',
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
{window.electron?.dataDir && (
|
||||
<p className='mt-2 text-sm opacity-80'>
|
||||
<strong>{t('数据存储位置:')}</strong>
|
||||
<br />
|
||||
<code className='bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded'>
|
||||
{window.electron.dataDir}
|
||||
</code>
|
||||
</p>
|
||||
)}
|
||||
<p className='mt-2 text-sm opacity-70'>
|
||||
💡 {t('提示:如需备份数据,只需复制上述目录即可')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>
|
||||
{t(
|
||||
'您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!',
|
||||
)}
|
||||
</p>
|
||||
<p className='mt-1'>
|
||||
<strong>
|
||||
{t(
|
||||
'建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。',
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
className='!rounded-lg'
|
||||
fullMode={false}
|
||||
|
||||
@@ -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: (
|
||||
<div style={{ lineHeight: '1.8' }}>
|
||||
<p>
|
||||
<Text>{t('当前时间')}:</Text>
|
||||
<Text strong style={{ color: '#52c41a' }}>{currentTime}</Text>
|
||||
</p>
|
||||
<p>
|
||||
<Text>{t('选择时间')}:</Text>
|
||||
<Text strong type="danger">{targetTime}</Text>
|
||||
{daysDiff > 0 && (
|
||||
<Text type="tertiary"> ({t('约')} {daysDiff} {t('天前')})</Text>
|
||||
)}
|
||||
</p>
|
||||
<div style={{
|
||||
background: '#fff7e6',
|
||||
border: '1px solid #ffd591',
|
||||
padding: '12px',
|
||||
borderRadius: '4px',
|
||||
marginTop: '12px'
|
||||
}}>
|
||||
<Text type="warning" strong>⚠️ {t('注意')}:</Text>
|
||||
<Text>{t('将删除')} </Text>
|
||||
<Text strong type="danger">{targetTime}</Text>
|
||||
{daysDiff > 0 && (
|
||||
<Text type="tertiary"> ({t('约')} {daysDiff} {t('天前')})</Text>
|
||||
)}
|
||||
<Text> {t('之前的所有日志')}</Text>
|
||||
</div>
|
||||
<p style={{ marginTop: '12px' }}>
|
||||
<Text type="danger">{t('此操作不可恢复,请仔细确认时间后再操作!')}</Text>
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
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) {
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
<Spin spinning={loadingCleanHistoryLog}>
|
||||
<Form.DatePicker
|
||||
label={t('日志记录时间')}
|
||||
label={t('清除历史日志')}
|
||||
field={'historyTimestamp'}
|
||||
type='dateTime'
|
||||
inputReadOnly={true}
|
||||
@@ -149,7 +202,10 @@ export default function SettingsLog(props) {
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Button size='default' onClick={onCleanHistoryLog}>
|
||||
<Text type="tertiary" size="small" style={{ display: 'block', marginTop: 4, marginBottom: 8 }}>
|
||||
{t('将清除选定时间之前的所有日志')}
|
||||
</Text>
|
||||
<Button size='default' type='danger' onClick={onCleanHistoryLog}>
|
||||
{t('清除历史日志')}
|
||||
</Button>
|
||||
</Spin>
|
||||
|
||||
Reference in New Issue
Block a user