Ver código fonte

Merge pull request #1915 from danding5/main

add submodel.ai
Seefs 3 meses atrás
pai
commit
83b2b071fd
40 arquivos alterados com 3598 adições e 465 exclusões
  1. 1 1
      README.en.md
  2. 216 0
      README.fr.md
  3. 1 1
      README.md
  4. 2 0
      common/api_type.go
  5. 32 1
      common/sys_log.go
  6. 72 0
      common/utils.go
  7. 3 2
      constant/api_type.go
  8. 3 0
      constant/channel.go
  9. 3 0
      controller/channel.go
  10. 8 0
      dto/channel_settings.go
  11. 7 1
      main.go
  12. 1 1
      model/task.go
  13. 1 1
      model/user.go
  14. 1 0
      relay/channel/api_request.go
  15. 18 34
      relay/channel/ollama/adaptor.go
  16. 57 36
      relay/channel/ollama/dto.go
  17. 157 101
      relay/channel/ollama/relay-ollama.go
  18. 210 0
      relay/channel/ollama/stream.go
  19. 18 0
      relay/channel/openai/relay-openai.go
  20. 7 0
      relay/channel/openrouter/dto.go
  21. 82 0
      relay/channel/submodel/adaptor.go
  22. 16 0
      relay/channel/submodel/constants.go
  23. 13 6
      relay/channel/volcengine/adaptor.go
  24. 5 0
      relay/channel/volcengine/constants.go
  25. 3 4
      relay/channel/xunfei/relay-xunfei.go
  26. 3 1
      relay/relay_adaptor.go
  27. 40 6
      service/http_client.go
  28. 11 4
      setting/ratio_setting/model_ratio.go
  29. 9 2
      web/src/components/layout/headerbar/LanguageSelector.jsx
  30. 39 10
      web/src/components/settings/PersonalSetting.jsx
  31. 53 21
      web/src/components/settings/personal/cards/AccountManagement.jsx
  32. 84 1
      web/src/components/table/channels/modals/EditChannelModal.jsx
  33. 3 0
      web/src/components/table/channels/modals/EditTagModal.jsx
  34. 5 0
      web/src/constants/channel.constants.js
  35. 68 68
      web/src/helpers/render.jsx
  36. 195 162
      web/src/hooks/channels/useChannelsData.jsx
  37. 4 0
      web/src/i18n/i18n.js
  38. 4 1
      web/src/i18n/locales/en.json
  39. 2140 0
      web/src/i18n/locales/fr.json
  40. 3 0
      web/src/i18n/locales/zh.json

+ 1 - 1
README.en.md

@@ -1,5 +1,5 @@
 <p align="right">
-   <a href="./README.md">中文</a> | <strong>English</strong>
+   <a href="./README.md">中文</a> | <strong>English</strong> | <a href="./README.fr.md">Français</a>
 </p>
 <div align="center">
 

+ 216 - 0
README.fr.md

@@ -0,0 +1,216 @@
+<p align="right">
+   <a href="./README.md">中文</a> | <a href="./README.en.md">English</a> | <strong>Français</strong>
+</p>
+<div align="center">
+
+![new-api](/web/public/logo.png)
+
+# New API
+
+🍥 Passerelle de modèles étendus de nouvelle génération et système de gestion d'actifs d'IA
+
+<a href="https://trendshift.io/repositories/8227" target="_blank"><img src="https://trendshift.io/api/badge/repositories/8227" alt="Calcium-Ion%2Fnew-api | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
+
+<p align="center">
+  <a href="https://raw.githubusercontent.com/Calcium-Ion/new-api/main/LICENSE">
+    <img src="https://img.shields.io/github/license/Calcium-Ion/new-api?color=brightgreen" alt="licence">
+  </a>
+  <a href="https://github.com/Calcium-Ion/new-api/releases/latest">
+    <img src="https://img.shields.io/github/v/release/Calcium-Ion/new-api?color=brightgreen&include_prereleases" alt="version">
+  </a>
+  <a href="https://github.com/users/Calcium-Ion/packages/container/package/new-api">
+    <img src="https://img.shields.io/badge/docker-ghcr.io-blue" alt="docker">
+  </a>
+  <a href="https://hub.docker.com/r/CalciumIon/new-api">
+    <img src="https://img.shields.io/badge/docker-dockerHub-blue" alt="docker">
+  </a>
+  <a href="https://goreportcard.com/report/github.com/Calcium-Ion/new-api">
+    <img src="https://goreportcard.com/badge/github.com/Calcium-Ion/new-api" alt="GoReportCard">
+  </a>
+</p>
+</div>
+
+## 📝 Description du projet
+
+> [!NOTE]
+> Il s'agit d'un projet open-source développé sur la base de [One API](https://github.com/songquanpeng/one-api)
+
+> [!IMPORTANT]
+> - Ce projet est uniquement destiné à des fins d'apprentissage personnel, sans garantie de stabilité ni de support technique.
+> - Les utilisateurs doivent se conformer aux [Conditions d'utilisation](https://openai.com/policies/terms-of-use) d'OpenAI et aux **lois et réglementations applicables**, et ne doivent pas l'utiliser à des fins illégales.
+> - Conformément aux [《Mesures provisoires pour la gestion des services d'intelligence artificielle générative》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm), veuillez ne fournir aucun service d'IA générative non enregistré au public en Chine.
+
+<h2>🤝 Partenaires de confiance</h2>
+<p id="premium-sponsors">&nbsp;</p>
+<p align="center"><strong>Sans ordre particulier</strong></p>
+<p align="center">
+  <a href="https://www.cherry-ai.com/" target=_blank><img
+    src="./docs/images/cherry-studio.png" alt="Cherry Studio" height="120"
+  /></a>
+  <a href="https://bda.pku.edu.cn/" target=_blank><img
+    src="./docs/images/pku.png" alt="Université de Pékin" height="120"
+  /></a>
+  <a href="https://www.compshare.cn/?ytag=GPU_yy_gh_newapi" target=_blank><img
+    src="./docs/images/ucloud.png" alt="UCloud" height="120"
+  /></a>
+  <a href="https://www.aliyun.com/" target=_blank><img
+    src="./docs/images/aliyun.png" alt="Alibaba Cloud" height="120"
+  /></a>
+  <a href="https://io.net/" target=_blank><img
+    src="./docs/images/io-net.png" alt="IO.NET" height="120"
+  /></a>
+</p>
+<p>&nbsp;</p>
+
+## 📚 Documentation
+
+Pour une documentation détaillée, veuillez consulter notre Wiki officiel : [https://docs.newapi.pro/](https://docs.newapi.pro/)
+
+Vous pouvez également accéder au DeepWiki généré par l'IA :
+[![Demander à DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/QuantumNous/new-api)
+
+## ✨ Fonctionnalités clés
+
+New API offre un large éventail de fonctionnalités, veuillez vous référer à [Présentation des fonctionnalités](https://docs.newapi.pro/wiki/features-introduction) pour plus de détails :
+
+1. 🎨 Nouvelle interface utilisateur
+2. 🌍 Prise en charge multilingue
+3. 💰 Fonctionnalité de recharge en ligne (YiPay)
+4. 🔍 Prise en charge de la recherche de quotas d'utilisation avec des clés (fonctionne avec [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
+5. 🔄 Compatible avec la base de données originale de One API
+6. 💵 Prise en charge de la tarification des modèles de paiement à l'utilisation
+7. ⚖️ Prise en charge de la sélection aléatoire pondérée des canaux
+8. 📈 Tableau de bord des données (console)
+9. 🔒 Regroupement de jetons et restrictions de modèles
+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 :
+    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. 💰 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 :
+        - [x] OpenAI
+        - [x] Azure
+        - [x] DeepSeek
+        - [x] Claude
+
+## Prise en charge des modèles
+
+Cette version prend en charge plusieurs modèles, veuillez vous référer à [Documentation de l'API-Interface de relais](https://docs.newapi.pro/api) pour plus de détails :
+
+1. Modèles tiers **gpts** (gpt-4-gizmo-*)
+2. Canal tiers [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy), [Documentation de l'API](https://docs.newapi.pro/api/midjourney-proxy-image)
+3. Canal tiers [Suno API](https://github.com/Suno-API/Suno-API), [Documentation de l'API](https://docs.newapi.pro/api/suno-music)
+4. Canaux personnalisés, prenant en charge la saisie complète de l'adresse d'appel
+5. Modèles Rerank ([Cohere](https://cohere.ai/) et [Jina](https://jina.ai/)), [Documentation de l'API](https://docs.newapi.pro/api/jinaai-rerank)
+6. Format de messages Claude, [Documentation de l'API](https://docs.newapi.pro/api/anthropic-chat)
+7. Dify, ne prend actuellement en charge que chatflow
+
+## Configuration des variables d'environnement
+
+Pour des instructions de configuration détaillées, veuillez vous référer à [Guide d'installation-Configuration des variables d'environnement](https://docs.newapi.pro/installation/environment-variables) :
+
+- `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
+- `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`
+- `ERROR_LOG_ENABLED=true` : S'il faut enregistrer et afficher les journaux d'erreurs, la valeur par défaut est `false`
+
+## Déploiement
+
+Pour des guides de déploiement détaillés, veuillez vous référer à [Guide d'installation-Méthodes de déploiement](https://docs.newapi.pro/installation) :
+
+> [!TIP]
+> Dernière image Docker : `calciumion/new-api:latest`
+
+### Considérations sur le déploiement multi-machines
+- La variable d'environnement `SESSION_SECRET` doit être définie, sinon l'état de connexion sera incohérent sur plusieurs machines
+- Si vous partagez Redis, `CRYPTO_SECRET` doit être défini, sinon le contenu de Redis ne pourra pas être consulté sur plusieurs machines
+
+### Exigences de déploiement
+- Base de données locale (par défaut) : SQLite (le déploiement Docker doit monter le répertoire `/data`)
+- Base de données distante : MySQL version >= 5.7.8, PgSQL version >= 9.6
+
+### Méthodes de déploiement
+
+#### Utilisation de la fonctionnalité Docker du panneau BaoTa
+Installez le panneau BaoTa (version **9.2.0** ou supérieure), recherchez **New-API** dans le magasin d'applications et installez-le.
+[Tutoriel avec des images](./docs/BT.md)
+
+#### Utilisation de Docker Compose (recommandé)
+```shell
+# Télécharger le projet
+git clone https://github.com/Calcium-Ion/new-api.git
+cd new-api
+# Modifier docker-compose.yml si nécessaire
+# Démarrer
+docker-compose up -d
+```
+
+#### Utilisation directe de l'image Docker
+```shell
+# Utilisation de SQLite
+docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+
+# Utilisation de MySQL
+docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
+```
+
+## 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**.
+
+### Méthode de configuration du cache
+1. `REDIS_CONN_STRING` : Définir Redis comme cache
+2. `MEMORY_CACHE_ENABLED` : Activer le cache mémoire (pas besoin de le définir manuellement si Redis est défini)
+
+## Documentation de l'API
+
+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 Claude (messages)](https://docs.newapi.pro/api/anthropic-chat)
+
+## 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 :
+- [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
+
+Si vous avez des questions, veuillez vous référer à [Aide et support](https://docs.newapi.pro/support) :
+- [Interaction avec la communauté](https://docs.newapi.pro/support/community-interaction)
+- [Commentaires sur les problèmes](https://docs.newapi.pro/support/feedback-issues)
+- [FAQ](https://docs.newapi.pro/support/faq)
+
+## 🌟 Historique des étoiles
+
+[![Graphique de l'historique des étoiles](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date)

+ 1 - 1
README.md

@@ -1,5 +1,5 @@
 <p align="right">
-   <strong>中文</strong> | <a href="./README.en.md">English</a>
+   <strong>中文</strong> | <a href="./README.en.md">English</a> | <a href="./README.fr.md">Français</a>
 </p>
 <div align="center">
 

+ 2 - 0
common/api_type.go

@@ -67,6 +67,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
 		apiType = constant.APITypeJimeng
 	case constant.ChannelTypeMoonshot:
 		apiType = constant.APITypeMoonshot
+	case constant.ChannelTypeSubmodel:
+		apiType = constant.APITypeSubmodel
 	}
 	if apiType == -1 {
 		return constant.APITypeOpenAI, false

+ 32 - 1
common/sys_log.go

@@ -2,9 +2,10 @@ package common
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"os"
 	"time"
+
+	"github.com/gin-gonic/gin"
 )
 
 func SysLog(s string) {
@@ -22,3 +23,33 @@ func FatalLog(v ...any) {
 	_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
 	os.Exit(1)
 }
+
+func LogStartupSuccess(startTime time.Time, port string) {
+
+	duration := time.Since(startTime)
+	durationMs := duration.Milliseconds()
+
+	// Get network IPs
+	networkIps := GetNetworkIps()
+
+	// Print blank line for spacing
+	fmt.Fprintf(gin.DefaultWriter, "\n")
+
+	// Print the main success message
+	fmt.Fprintf(gin.DefaultWriter, "  \033[32m%s %s\033[0m  ready in %d ms\n", SystemName, Version, durationMs)
+	fmt.Fprintf(gin.DefaultWriter, "\n")
+
+	// Skip fancy startup message in container environments
+	if !IsRunningInContainer() {
+		// Print local URL
+		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mLocal:\033[0m   http://localhost:%s/\n", port)
+	}
+
+	// Print network URLs
+	for _, ip := range networkIps {
+		fmt.Fprintf(gin.DefaultWriter, "  ➜  \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
+	}
+
+	// Print blank line for spacing
+	fmt.Fprintf(gin.DefaultWriter, "\n")
+}

+ 72 - 0
common/utils.go

@@ -68,6 +68,78 @@ func GetIp() (ip string) {
 	return
 }
 
+func GetNetworkIps() []string {
+	var networkIps []string
+	ips, err := net.InterfaceAddrs()
+	if err != nil {
+		log.Println(err)
+		return networkIps
+	}
+
+	for _, a := range ips {
+		if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
+			if ipNet.IP.To4() != nil {
+				ip := ipNet.IP.String()
+				// Include common private network ranges
+				if strings.HasPrefix(ip, "10.") ||
+					strings.HasPrefix(ip, "172.") ||
+					strings.HasPrefix(ip, "192.168.") {
+					networkIps = append(networkIps, ip)
+				}
+			}
+		}
+	}
+	return networkIps
+}
+
+// IsRunningInContainer detects if the application is running inside a container
+func IsRunningInContainer() bool {
+	// Method 1: Check for .dockerenv file (Docker containers)
+	if _, err := os.Stat("/.dockerenv"); err == nil {
+		return true
+	}
+
+	// Method 2: Check cgroup for container indicators
+	if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
+		content := string(data)
+		if strings.Contains(content, "docker") ||
+			strings.Contains(content, "containerd") ||
+			strings.Contains(content, "kubepods") ||
+			strings.Contains(content, "/lxc/") {
+			return true
+		}
+	}
+
+	// Method 3: Check environment variables commonly set by container runtimes
+	containerEnvVars := []string{
+		"KUBERNETES_SERVICE_HOST",
+		"DOCKER_CONTAINER",
+		"container",
+	}
+
+	for _, envVar := range containerEnvVars {
+		if os.Getenv(envVar) != "" {
+			return true
+		}
+	}
+
+	// Method 4: Check if init process is not the traditional init
+	if data, err := os.ReadFile("/proc/1/comm"); err == nil {
+		comm := strings.TrimSpace(string(data))
+		// In containers, process 1 is often not "init" or "systemd"
+		if comm != "init" && comm != "systemd" {
+			// Additional check: if it's a common container entrypoint
+			if strings.Contains(comm, "docker") ||
+				strings.Contains(comm, "containerd") ||
+				strings.Contains(comm, "runc") {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
 var sizeKB = 1024
 var sizeMB = sizeKB * 1024
 var sizeGB = sizeMB * 1024

+ 3 - 2
constant/api_type.go

@@ -31,6 +31,7 @@ const (
 	APITypeXai
 	APITypeCoze
 	APITypeJimeng
-	APITypeMoonshot // this one is only for count, do not add any channel after this
-	APITypeDummy    // this one is only for count, do not add any channel after this
+     APITypeMoonshot
+     APITypeSubmodel
+     APITypeDummy    // this one is only for count, do not add any channel after this
 )

+ 3 - 0
constant/channel.go

@@ -50,8 +50,10 @@ const (
 	ChannelTypeKling          = 50
 	ChannelTypeJimeng         = 51
 	ChannelTypeVidu           = 52
+	ChannelTypeSubmodel       = 53
 	ChannelTypeDummy          // this one is only for count, do not add any channel after this
 
+
 )
 
 var ChannelBaseURLs = []string{
@@ -108,4 +110,5 @@ var ChannelBaseURLs = []string{
 	"https://api.klingai.com",                   //50
 	"https://visual.volcengineapi.com",          //51
 	"https://api.vidu.cn",                       //52
+	"https://llm.submodel.ai",                   //53
 }

+ 3 - 0
controller/channel.go

@@ -8,6 +8,7 @@ import (
 	"one-api/constant"
 	"one-api/dto"
 	"one-api/model"
+	"one-api/service"
 	"strconv"
 	"strings"
 
@@ -633,6 +634,7 @@ func AddChannel(c *gin.Context) {
 		common.ApiError(c, err)
 		return
 	}
+	service.ResetProxyClientCache()
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
@@ -894,6 +896,7 @@ func UpdateChannel(c *gin.Context) {
 		return
 	}
 	model.InitChannelCache()
+	service.ResetProxyClientCache()
 	channel.Key = ""
 	clearChannelInfo(&channel.Channel)
 	c.JSON(http.StatusOK, gin.H{

+ 8 - 0
dto/channel_settings.go

@@ -19,4 +19,12 @@ const (
 type ChannelOtherSettings struct {
 	AzureResponsesVersion string        `json:"azure_responses_version,omitempty"`
 	VertexKeyType         VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key"
+	OpenRouterEnterprise  *bool         `json:"openrouter_enterprise,omitempty"`
+}
+
+func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool {
+	if s == nil || s.OpenRouterEnterprise == nil {
+		return false
+	}
+	return *s.OpenRouterEnterprise
 }

+ 7 - 1
main.go

@@ -18,6 +18,7 @@ import (
 	"os"
 	"strconv"
 	"strings"
+	"time"
 
 	"github.com/bytedance/gopkg/util/gopool"
 	"github.com/gin-contrib/sessions"
@@ -35,6 +36,7 @@ var buildFS embed.FS
 var indexPage []byte
 
 func main() {
+	startTime := time.Now()
 
 	err := InitResources()
 	if err != nil {
@@ -168,6 +170,10 @@ func main() {
 	if port == "" {
 		port = strconv.Itoa(*common.Port)
 	}
+
+	// Log startup success message
+	common.LogStartupSuccess(startTime, port)
+
 	err = server.Run(":" + port)
 	if err != nil {
 		common.FatalLog("failed to start HTTP server: " + err.Error())
@@ -222,4 +228,4 @@ func InitResources() error {
 		return err
 	}
 	return nil
-}
+}

+ 1 - 1
model/task.go

@@ -24,7 +24,7 @@ type Task struct {
 	ID         int64                 `json:"id" gorm:"primary_key;AUTO_INCREMENT"`
 	CreatedAt  int64                 `json:"created_at" gorm:"index"`
 	UpdatedAt  int64                 `json:"updated_at"`
-	TaskID     string                `json:"task_id" gorm:"type:varchar(50);index"`  // 第三方id,不一定有/ song id\ Task id
+	TaskID     string                `json:"task_id" gorm:"type:varchar(191);index"` // 第三方id,不一定有/ song id\ Task id
 	Platform   constant.TaskPlatform `json:"platform" gorm:"type:varchar(30);index"` // 平台
 	UserId     int                   `json:"user_id" gorm:"index"`
 	ChannelId  int                   `json:"channel_id" gorm:"index"`

+ 1 - 1
model/user.go

@@ -18,7 +18,7 @@ import (
 // Otherwise, the sensitive information will be saved on local storage in plain text!
 type User struct {
 	Id               int            `json:"id"`
-	Username         string         `json:"username" gorm:"unique;index" validate:"max=12"`
+	Username         string         `json:"username" gorm:"unique;index" validate:"max=20"`
 	Password         string         `json:"password" gorm:"not null;" validate:"min=8,max=20"`
 	OriginalPassword string         `json:"original_password" gorm:"-:all"` // this field is only for Password change verification, don't save it to database!
 	DisplayName      string         `json:"display_name" gorm:"index" validate:"max=20"`

+ 1 - 0
relay/channel/api_request.go

@@ -265,6 +265,7 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
 
 	resp, err := client.Do(req)
 	if err != nil {
+		logger.LogError(c, "do request failed: "+err.Error())
 		return nil, types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithHideErrMsg("upstream error: do request failed"))
 	}
 	if resp == nil {

+ 18 - 34
relay/channel/ollama/adaptor.go

@@ -10,6 +10,7 @@ import (
 	relaycommon "one-api/relay/common"
 	relayconstant "one-api/relay/constant"
 	"one-api/types"
+	"strings"
 
 	"github.com/gin-gonic/gin"
 )
@@ -17,10 +18,7 @@ import (
 type Adaptor struct {
 }
 
-func (a *Adaptor) ConvertGeminiRequest(*gin.Context, *relaycommon.RelayInfo, *dto.GeminiChatRequest) (any, error) {
-	//TODO implement me
-	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{}
@@ -31,32 +29,21 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
 	openaiRequest.(*dto.GeneralOpenAIRequest).StreamOptions = &dto.StreamOptions{
 		IncludeUsage: true,
 	}
-	return requestOpenAI2Ollama(c, openaiRequest.(*dto.GeneralOpenAIRequest))
+	// map to ollama chat request (Claude -> OpenAI -> Ollama chat)
+	return openAIChatToOllamaChat(c, openaiRequest.(*dto.GeneralOpenAIRequest))
 }
 
-func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
-	//TODO implement me
-	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) {
-	//TODO implement me
-	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.RelayFormat == types.RelayFormatClaude {
-		return info.ChannelBaseUrl + "/v1/chat/completions", nil
-	}
-	switch info.RelayMode {
-	case relayconstant.RelayModeEmbeddings:
-		return info.ChannelBaseUrl + "/api/embed", nil
-	default:
-		return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), 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 {
@@ -66,10 +53,12 @@ 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)
 	}
-	return requestOpenAI2Ollama(c, request)
+	return openAIChatToOllamaChat(c, request)
 }
 
 func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
@@ -80,10 +69,7 @@ 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) {
-	// TODO implement me
-	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)
@@ -92,15 +78,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request
 func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
 	switch info.RelayMode {
 	case relayconstant.RelayModeEmbeddings:
-		usage, err = ollamaEmbeddingHandler(c, info, resp)
+		return ollamaEmbeddingHandler(c, info, resp)
 	default:
 		if info.IsStream {
-			usage, err = openai.OaiStreamHandler(c, info, resp)
-		} else {
-			usage, err = openai.OpenaiHandler(c, info, resp)
+			return ollamaStreamHandler(c, info, resp)
 		}
+		return ollamaChatHandler(c, info, resp)
 	}
-	return
 }
 
 func (a *Adaptor) GetModelList() []string {

+ 57 - 36
relay/channel/ollama/dto.go

@@ -2,48 +2,69 @@ package ollama
 
 import (
 	"encoding/json"
-	"one-api/dto"
 )
 
-type OllamaRequest struct {
-	Model            string                `json:"model,omitempty"`
-	Messages         []dto.Message         `json:"messages,omitempty"`
-	Stream           bool                  `json:"stream,omitempty"`
-	Temperature      *float64              `json:"temperature,omitempty"`
-	Seed             float64               `json:"seed,omitempty"`
-	Topp             float64               `json:"top_p,omitempty"`
-	TopK             int                   `json:"top_k,omitempty"`
-	Stop             any                   `json:"stop,omitempty"`
-	MaxTokens        uint                  `json:"max_tokens,omitempty"`
-	Tools            []dto.ToolCallRequest `json:"tools,omitempty"`
-	ResponseFormat   any                   `json:"response_format,omitempty"`
-	FrequencyPenalty float64               `json:"frequency_penalty,omitempty"`
-	PresencePenalty  float64               `json:"presence_penalty,omitempty"`
-	Suffix           any                   `json:"suffix,omitempty"`
-	StreamOptions    *dto.StreamOptions    `json:"stream_options,omitempty"`
-	Prompt           any                   `json:"prompt,omitempty"`
-	Think            json.RawMessage       `json:"think,omitempty"`
-}
-
-type Options struct {
-	Seed             int      `json:"seed,omitempty"`
-	Temperature      *float64 `json:"temperature,omitempty"`
-	TopK             int      `json:"top_k,omitempty"`
-	TopP             float64  `json:"top_p,omitempty"`
-	FrequencyPenalty float64  `json:"frequency_penalty,omitempty"`
-	PresencePenalty  float64  `json:"presence_penalty,omitempty"`
-	NumPredict       int      `json:"num_predict,omitempty"`
-	NumCtx           int      `json:"num_ctx,omitempty"`
+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"`
+}
+
+type OllamaToolFunction struct {
+	Name        string      `json:"name"`
+	Description string      `json:"description,omitempty"`
+	Parameters  interface{} `json:"parameters,omitempty"`
+}
+
+type OllamaTool struct {
+	Type     string            `json:"type"`
+	Function OllamaToolFunction `json:"function"`
+}
+
+type OllamaToolCall struct {
+	Function struct {
+		Name      string      `json:"name"`
+		Arguments interface{} `json:"arguments"`
+	} `json:"function"`
+}
+
+type OllamaChatRequest struct {
+	Model     string              `json:"model"`
+	Messages  []OllamaChatMessage `json:"messages"`
+	Tools     interface{}         `json:"tools,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 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"`
+	Think     json.RawMessage `json:"think,omitempty"`
 }
 
 type OllamaEmbeddingRequest struct {
-	Model   string   `json:"model,omitempty"`
-	Input   []string `json:"input"`
-	Options *Options `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"`
-	Embedding [][]float64 `json:"embeddings,omitempty"`
+	Error           string        `json:"error,omitempty"`
+	Model           string        `json:"model"`
+	Embeddings      [][]float64   `json:"embeddings"`
+	PromptEvalCount int           `json:"prompt_eval_count,omitempty"`
 }
+

+ 157 - 101
relay/channel/ollama/relay-ollama.go

@@ -1,6 +1,7 @@
 package ollama
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
@@ -14,121 +15,176 @@ import (
 	"github.com/gin-gonic/gin"
 )
 
-func requestOpenAI2Ollama(c *gin.Context, request *dto.GeneralOpenAIRequest) (*OllamaRequest, error) {
-	messages := make([]dto.Message, 0, len(request.Messages))
-	for _, message := range request.Messages {
-		if !message.IsStringContent() {
-			mediaMessages := message.ParseContent()
-			for j, mediaMessage := range mediaMessages {
-				if mediaMessage.Type == dto.ContentTypeImageURL {
-					imageUrl := mediaMessage.GetImageMedia()
-					// check if not base64
-					if strings.HasPrefix(imageUrl.Url, "http") {
-						fileData, err := service.GetFileBase64FromUrl(c, imageUrl.Url, "formatting image for Ollama")
-						if err != nil {
-							return nil, err
+func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaChatRequest, error) {
+	chatReq := &OllamaChatRequest{
+		Model:   r.Model,
+		Stream:  r.Stream,
+		Options: map[string]any{},
+		Think:   r.Think,
+	}
+	if r.ResponseFormat != nil {
+		if r.ResponseFormat.Type == "json" {
+			chatReq.Format = "json"
+		} else if r.ResponseFormat.Type == "json_schema" {
+			if len(r.ResponseFormat.JsonSchema) > 0 {
+				var schema any
+				_ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema)
+				chatReq.Format = schema
+			}
+		}
+	}
+
+	// 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.Stop != nil {
+		switch v := r.Stop.(type) {
+		case string:
+			chatReq.Options["stop"] = []string{v}
+		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 }
+		}
+	}
+
+	if len(r.Tools) > 0 {
+		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))
+	for _, m := range r.Messages {
+		var textBuilder strings.Builder
+		var images []string
+		if m.IsStringContent() {
+			textBuilder.WriteString(m.StringContent())
+		} else {
+			parts := m.ParseContent()
+			for _, part := range parts {
+				if part.Type == dto.ContentTypeImageURL {
+					img := part.GetImageMedia()
+					if img != nil && img.Url != "" {
+						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 }
+							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:] }
+						} else {
+							base64Data = img.Url
 						}
-						imageUrl.Url = fmt.Sprintf("data:%s;base64,%s", fileData.MimeType, fileData.Base64Data)
+						if base64Data != "" { images = append(images, base64Data) }
 					}
-					mediaMessage.ImageUrl = imageUrl
-					mediaMessages[j] = mediaMessage
+				} else if part.Type == dto.ContentTypeText {
+					textBuilder.WriteString(part.Text)
+				}
+			}
+		}
+		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 m.ToolCalls != nil && len(m.ToolCalls) > 0 {
+			parsed := m.ParseToolCalls()
+			if len(parsed) > 0 {
+				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{} }
+					oc := OllamaToolCall{}
+					oc.Function.Name = tc.Function.Name
+					oc.Function.Arguments = args
+					calls = append(calls, oc)
 				}
+				cm.ToolCalls = calls
 			}
-			message.SetMediaContent(mediaMessages)
 		}
-		messages = append(messages, dto.Message{
-			Role:       message.Role,
-			Content:    message.Content,
-			ToolCalls:  message.ToolCalls,
-			ToolCallId: message.ToolCallId,
-		})
+		chatReq.Messages = append(chatReq.Messages, cm)
 	}
-	str, ok := request.Stop.(string)
-	var Stop []string
-	if ok {
-		Stop = []string{str}
-	} else {
-		Stop, _ = request.Stop.([]string)
+	return chatReq, nil
+}
+
+// openAIToGenerate converts OpenAI completions request to Ollama generate
+func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGenerateRequest, error) {
+	gen := &OllamaGenerateRequest{
+		Model:   r.Model,
+		Stream:  r.Stream,
+		Options: map[string]any{},
+		Think:   r.Think,
+	}
+	// Prompt may be in r.Prompt (string or []any)
+	if r.Prompt != nil {
+		switch v := r.Prompt.(type) {
+		case string:
+			gen.Prompt = v
+		case []any:
+			var sb strings.Builder
+			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)
+		}
 	}
-	ollamaRequest := &OllamaRequest{
-		Model:            request.Model,
-		Messages:         messages,
-		Stream:           request.Stream,
-		Temperature:      request.Temperature,
-		Seed:             request.Seed,
-		Topp:             request.TopP,
-		TopK:             request.TopK,
-		Stop:             Stop,
-		Tools:            request.Tools,
-		MaxTokens:        request.GetMaxTokens(),
-		ResponseFormat:   request.ResponseFormat,
-		FrequencyPenalty: request.FrequencyPenalty,
-		PresencePenalty:  request.PresencePenalty,
-		Prompt:           request.Prompt,
-		StreamOptions:    request.StreamOptions,
-		Suffix:           request.Suffix,
+	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 }
 	}
-	ollamaRequest.Think = request.Think
-	return ollamaRequest, nil
+	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 }
+		}
+	}
+	return gen, nil
 }
 
-func requestOpenAI2Embeddings(request dto.EmbeddingRequest) *OllamaEmbeddingRequest {
-	return &OllamaEmbeddingRequest{
-		Model: request.Model,
-		Input: request.ParseInput(),
-		Options: &Options{
-			Seed:             int(request.Seed),
-			Temperature:      request.Temperature,
-			TopP:             request.TopP,
-			FrequencyPenalty: request.FrequencyPenalty,
-			PresencePenalty:  request.PresencePenalty,
-		},
-	}
+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 }
+	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}
 }
 
 func ollamaEmbeddingHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) {
-	var ollamaEmbeddingResponse OllamaEmbeddingResponse
-	responseBody, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
-	}
+	var oResp OllamaEmbeddingResponse
+	body, err := io.ReadAll(resp.Body)
+	if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) }
 	service.CloseResponseBodyGracefully(resp)
-	err = common.Unmarshal(responseBody, &ollamaEmbeddingResponse)
-	if err != nil {
-		return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
-	}
-	if ollamaEmbeddingResponse.Error != "" {
-		return nil, types.NewOpenAIError(fmt.Errorf("ollama error: %s", ollamaEmbeddingResponse.Error), types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
-	}
-	flattenedEmbeddings := flattenEmbeddings(ollamaEmbeddingResponse.Embedding)
-	data := make([]dto.OpenAIEmbeddingResponseItem, 0, 1)
-	data = append(data, dto.OpenAIEmbeddingResponseItem{
-		Embedding: flattenedEmbeddings,
-		Object:    "embedding",
-	})
-	usage := &dto.Usage{
-		TotalTokens:      info.PromptTokens,
-		CompletionTokens: 0,
-		PromptTokens:     info.PromptTokens,
-	}
-	embeddingResponse := &dto.OpenAIEmbeddingResponse{
-		Object: "list",
-		Data:   data,
-		Model:  info.UpstreamModelName,
-		Usage:  *usage,
-	}
-	doResponseBody, err := common.Marshal(embeddingResponse)
-	if err != nil {
-		return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
-	}
-	service.IOCopyBytesGracefully(c, resp, doResponseBody)
+	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
 }
 
-func flattenEmbeddings(embeddings [][]float64) []float64 {
-	flattened := []float64{}
-	for _, row := range embeddings {
-		flattened = append(flattened, row...)
-	}
-	return flattened
-}

+ 210 - 0
relay/channel/ollama/stream.go

@@ -0,0 +1,210 @@
+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"
+
+    "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"`
+}
+
+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()
+}
+
+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)
+
+    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)
+
+        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) }
+
+    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) }
+    }
+
+    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
+}
+
+func contentPtr(s string) *string { if s=="" { return nil }; return &s }

+ 18 - 0
relay/channel/openai/relay-openai.go

@@ -12,6 +12,7 @@ import (
 	"one-api/constant"
 	"one-api/dto"
 	"one-api/logger"
+	"one-api/relay/channel/openrouter"
 	relaycommon "one-api/relay/common"
 	"one-api/relay/helper"
 	"one-api/service"
@@ -185,10 +186,27 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo
 	if common.DebugEnabled {
 		println("upstream response body:", string(responseBody))
 	}
+	// Unmarshal to simpleResponse
+	if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.IsOpenRouterEnterprise() {
+		// 尝试解析为 openrouter enterprise
+		var enterpriseResponse openrouter.OpenRouterEnterpriseResponse
+		err = common.Unmarshal(responseBody, &enterpriseResponse)
+		if err != nil {
+			return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
+		}
+		if enterpriseResponse.Success {
+			responseBody = enterpriseResponse.Data
+		} else {
+			logger.LogError(c, fmt.Sprintf("openrouter enterprise response success=false, data: %s", enterpriseResponse.Data))
+			return nil, types.NewOpenAIError(fmt.Errorf("openrouter response success=false"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
+		}
+	}
+
 	err = common.Unmarshal(responseBody, &simpleResponse)
 	if err != nil {
 		return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError)
 	}
+
 	if oaiError := simpleResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" {
 		return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
 	}

+ 7 - 0
relay/channel/openrouter/dto.go

@@ -1,5 +1,7 @@
 package openrouter
 
+import "encoding/json"
+
 type RequestReasoning struct {
 	// One of the following (not both):
 	Effort    string `json:"effort,omitempty"`     // Can be "high", "medium", or "low" (OpenAI-style)
@@ -7,3 +9,8 @@ type RequestReasoning struct {
 	// Optional: Default is false. All models support this.
 	Exclude bool `json:"exclude,omitempty"` // Set to true to exclude reasoning tokens from response
 }
+
+type OpenRouterEnterpriseResponse struct {
+	Data    json.RawMessage `json:"data"`
+	Success bool            `json:"success"`
+}

+ 82 - 0
relay/channel/submodel/adaptor.go

@@ -0,0 +1,82 @@
+package submodel
+
+import (
+	"errors"
+	"io"
+	"net/http"
+	"one-api/dto"
+	"one-api/relay/channel"
+	"one-api/relay/channel/openai"
+	relaycommon "one-api/relay/common"
+	"one-api/types"
+
+	"github.com/gin-gonic/gin"
+)
+
+type Adaptor struct {
+}
+
+func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+}
+
+func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+	return relaycommon.GetFullRequestURL(info.BaseUrl, info.RequestURLPath, info.ChannelType), nil
+}
+
+func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
+	channel.SetupApiRequestHeader(info, c, req)
+	req.Set("Authorization", "Bearer "+info.ApiKey)
+	return nil
+}
+
+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")
+	}
+	return request, nil
+}
+
+func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.OpenAIResponsesRequest) (any, error) {
+	return nil, errors.New("submodel channel: endpoint not supported")
+}
+
+func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
+	return channel.DoApiRequest(a, c, info, requestBody)
+}
+
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *types.NewAPIError) {
+	if info.IsStream {
+		usage, err = openai.OaiStreamHandler(c, info, resp)
+	} else {
+		usage, err = openai.OpenaiHandler(c, info, resp)
+	}
+	return
+}
+
+func (a *Adaptor) GetModelList() []string {
+	return ModelList
+}
+
+func (a *Adaptor) GetChannelName() string {
+	return ChannelName
+}

+ 16 - 0
relay/channel/submodel/constants.go

@@ -0,0 +1,16 @@
+package submodel
+
+var ModelList = []string{
+	"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",
+}
+
+const ChannelName = "submodel"

+ 13 - 6
relay/channel/volcengine/adaptor.go

@@ -9,6 +9,7 @@ import (
 	"mime/multipart"
 	"net/http"
 	"net/textproto"
+	channelconstant "one-api/constant"
 	"one-api/dto"
 	"one-api/relay/channel"
 	"one-api/relay/channel/openai"
@@ -188,20 +189,26 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
 }
 
 func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+	// 支持自定义域名,如果未设置则使用默认域名
+	baseUrl := info.ChannelBaseUrl
+	if baseUrl == "" {
+		baseUrl = channelconstant.ChannelBaseURLs[channelconstant.ChannelTypeVolcEngine]
+	}
+
 	switch info.RelayMode {
 	case constant.RelayModeChatCompletions:
 		if strings.HasPrefix(info.UpstreamModelName, "bot") {
-			return fmt.Sprintf("%s/api/v3/bots/chat/completions", info.ChannelBaseUrl), nil
+			return fmt.Sprintf("%s/api/v3/bots/chat/completions", baseUrl), nil
 		}
-		return fmt.Sprintf("%s/api/v3/chat/completions", info.ChannelBaseUrl), nil
+		return fmt.Sprintf("%s/api/v3/chat/completions", baseUrl), nil
 	case constant.RelayModeEmbeddings:
-		return fmt.Sprintf("%s/api/v3/embeddings", info.ChannelBaseUrl), nil
+		return fmt.Sprintf("%s/api/v3/embeddings", baseUrl), nil
 	case constant.RelayModeImagesGenerations:
-		return fmt.Sprintf("%s/api/v3/images/generations", info.ChannelBaseUrl), nil
+		return fmt.Sprintf("%s/api/v3/images/generations", baseUrl), nil
 	case constant.RelayModeImagesEdits:
-		return fmt.Sprintf("%s/api/v3/images/edits", info.ChannelBaseUrl), nil
+		return fmt.Sprintf("%s/api/v3/images/edits", baseUrl), nil
 	case constant.RelayModeRerank:
-		return fmt.Sprintf("%s/api/v3/rerank", info.ChannelBaseUrl), nil
+		return fmt.Sprintf("%s/api/v3/rerank", baseUrl), nil
 	default:
 	}
 	return "", fmt.Errorf("unsupported relay mode: %d", info.RelayMode)

+ 5 - 0
relay/channel/volcengine/constants.go

@@ -9,6 +9,11 @@ var ModelList = []string{
 	"Doubao-lite-4k",
 	"Doubao-embedding",
 	"doubao-seedream-4-0-250828",
+	"seedream-4-0-250828",
+	"doubao-seedance-1-0-pro-250528",
+	"seedance-1-0-pro-250528",
+	"doubao-seed-1-6-thinking-250715",
+	"seed-1-6-thinking-250715",
 }
 
 var ChannelName = "volcengine"

+ 3 - 4
relay/channel/xunfei/relay-xunfei.go

@@ -207,10 +207,6 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
 		return nil, nil, err
 	}
 
-	defer func() {
-		conn.Close()
-	}()
-
 	data := requestOpenAI2Xunfei(textRequest, appId, domain)
 	err = conn.WriteJSON(data)
 	if err != nil {
@@ -220,6 +216,9 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
 	dataChan := make(chan XunfeiChatResponse)
 	stopChan := make(chan bool)
 	go func() {
+		defer func() {
+			conn.Close()
+		}()
 		for {
 			_, msg, err := conn.ReadMessage()
 			if err != nil {

+ 3 - 1
relay/relay_adaptor.go

@@ -37,7 +37,7 @@ import (
 	"one-api/relay/channel/zhipu"
 	"one-api/relay/channel/zhipu_4v"
 	"strconv"
-
+    "one-api/relay/channel/submodel"
 	"github.com/gin-gonic/gin"
 )
 
@@ -103,6 +103,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
 		return &jimeng.Adaptor{}
 	case constant.APITypeMoonshot:
 		return &moonshot.Adaptor{} // Moonshot uses Claude API
+	case constant.APITypeSubmodel:
+		return &submodel.Adaptor{}
 	}
 	return nil
 }

+ 40 - 6
service/http_client.go

@@ -7,12 +7,17 @@ import (
 	"net/http"
 	"net/url"
 	"one-api/common"
+	"sync"
 	"time"
 
 	"golang.org/x/net/proxy"
 )
 
-var httpClient *http.Client
+var (
+	httpClient      *http.Client
+	proxyClientLock sync.Mutex
+	proxyClients    = make(map[string]*http.Client)
+)
 
 func InitHttpClient() {
 	if common.RelayTimeout == 0 {
@@ -28,12 +33,31 @@ func GetHttpClient() *http.Client {
 	return httpClient
 }
 
+// ResetProxyClientCache 清空代理客户端缓存,确保下次使用时重新初始化
+func ResetProxyClientCache() {
+	proxyClientLock.Lock()
+	defer proxyClientLock.Unlock()
+	for _, client := range proxyClients {
+		if transport, ok := client.Transport.(*http.Transport); ok && transport != nil {
+			transport.CloseIdleConnections()
+		}
+	}
+	proxyClients = make(map[string]*http.Client)
+}
+
 // NewProxyHttpClient 创建支持代理的 HTTP 客户端
 func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
 	if proxyURL == "" {
 		return http.DefaultClient, nil
 	}
 
+	proxyClientLock.Lock()
+	if client, ok := proxyClients[proxyURL]; ok {
+		proxyClientLock.Unlock()
+		return client, nil
+	}
+	proxyClientLock.Unlock()
+
 	parsedURL, err := url.Parse(proxyURL)
 	if err != nil {
 		return nil, err
@@ -41,11 +65,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
 
 	switch parsedURL.Scheme {
 	case "http", "https":
-		return &http.Client{
+		client := &http.Client{
 			Transport: &http.Transport{
 				Proxy: http.ProxyURL(parsedURL),
 			},
-		}, nil
+		}
+		client.Timeout = time.Duration(common.RelayTimeout) * time.Second
+		proxyClientLock.Lock()
+		proxyClients[proxyURL] = client
+		proxyClientLock.Unlock()
+		return client, nil
 
 	case "socks5", "socks5h":
 		// 获取认证信息
@@ -67,15 +96,20 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
 			return nil, err
 		}
 
-		return &http.Client{
+		client := &http.Client{
 			Transport: &http.Transport{
 				DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 					return dialer.Dial(network, addr)
 				},
 			},
-		}, nil
+		}
+		client.Timeout = time.Duration(common.RelayTimeout) * time.Second
+		proxyClientLock.Lock()
+		proxyClients[proxyURL] = client
+		proxyClientLock.Unlock()
+		return client, nil
 
 	default:
-		return nil, fmt.Errorf("unsupported proxy scheme: %s", parsedURL.Scheme)
+		return nil, fmt.Errorf("unsupported proxy scheme: %s, must be http, https, socks5 or socks5h", parsedURL.Scheme)
 	}
 }

+ 11 - 4
setting/ratio_setting/model_ratio.go

@@ -251,6 +251,17 @@ var defaultModelRatio = map[string]float64{
 	"grok-vision-beta":      2.5,
 	"grok-3-fast-beta":      2.5,
 	"grok-3-mini-fast-beta": 0.3,
+    // submodel
+	"NousResearch/Hermes-4-405B-FP8":               0.8,
+	"Qwen/Qwen3-235B-A22B-Thinking-2507":           0.6,
+	"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":      0.8,
+	"Qwen/Qwen3-235B-A22B-Instruct-2507":           0.3,
+	"zai-org/GLM-4.5-FP8":                          0.8,
+	"openai/gpt-oss-120b":                          0.5,
+	"deepseek-ai/DeepSeek-R1-0528":                 0.8,
+	"deepseek-ai/DeepSeek-R1":                      0.8,
+	"deepseek-ai/DeepSeek-V3-0324":                 0.8,
+	"deepseek-ai/DeepSeek-V3.1":                    0.8,
 }
 
 var defaultModelPrice = map[string]float64{
@@ -501,7 +512,6 @@ func GetCompletionRatio(name string) float64 {
 }
 
 func getHardcodedCompletionModelRatio(name string) (float64, bool) {
-	lowercaseName := strings.ToLower(name)
 
 	isReservedModel := strings.HasSuffix(name, "-all") || strings.HasSuffix(name, "-gizmo-*")
 	if isReservedModel {
@@ -594,9 +604,6 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
 		}
 	}
 	// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
-	if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
-		return 4, true
-	}
 	if strings.HasPrefix(name, "ERNIE-Speed-") {
 		return 2, true
 	} else if strings.HasPrefix(name, "ERNIE-Lite-") {

+ 9 - 2
web/src/components/layout/headerbar/LanguageSelector.jsx

@@ -20,7 +20,7 @@ For commercial licensing, please contact [email protected]
 import React from 'react';
 import { Button, Dropdown } from '@douyinfe/semi-ui';
 import { Languages } from 'lucide-react';
-import { CN, GB } from 'country-flag-icons/react/3x2';
+import { CN, GB, FR } from 'country-flag-icons/react/3x2';
 
 const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
   return (
@@ -42,12 +42,19 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
             <GB title='English' className='!w-5 !h-auto' />
             <span>English</span>
           </Dropdown.Item>
+          <Dropdown.Item
+            onClick={() => onLanguageChange('fr')}
+            className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'fr' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
+          >
+            <FR title='Français' className='!w-5 !h-auto' />
+            <span>Français</span>
+          </Dropdown.Item>
         </Dropdown.Menu>
       }
     >
       <Button
         icon={<Languages size={18} />}
-        aria-label={t('切换语言')}
+        aria-label={t('common.changeLanguage')}
         theme='borderless'
         type='tertiary'
         className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 dark:focus:!bg-gray-700 !rounded-full !bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-semi-color-fill-2'

+ 39 - 10
web/src/components/settings/PersonalSetting.jsx

@@ -19,7 +19,14 @@ For commercial licensing, please contact [email protected]
 
 import React, { useContext, useEffect, useState } from 'react';
 import { useNavigate } from 'react-router-dom';
-import { API, copy, showError, showInfo, showSuccess } from '../../helpers';
+import {
+  API,
+  copy,
+  showError,
+  showInfo,
+  showSuccess,
+  setStatusData,
+} from '../../helpers';
 import { UserContext } from '../../context/User';
 import { Modal } from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
@@ -71,18 +78,40 @@ const PersonalSetting = () => {
   });
 
   useEffect(() => {
-    let status = localStorage.getItem('status');
-    if (status) {
-      status = JSON.parse(status);
-      setStatus(status);
-      if (status.turnstile_check) {
+    let saved = localStorage.getItem('status');
+    if (saved) {
+      const parsed = JSON.parse(saved);
+      setStatus(parsed);
+      if (parsed.turnstile_check) {
         setTurnstileEnabled(true);
-        setTurnstileSiteKey(status.turnstile_site_key);
+        setTurnstileSiteKey(parsed.turnstile_site_key);
+      } else {
+        setTurnstileEnabled(false);
+        setTurnstileSiteKey('');
       }
     }
-    getUserData().then((res) => {
-      console.log(userState);
-    });
+    // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
+    (async () => {
+      try {
+        const res = await API.get('/api/status');
+        const { success, data } = res.data;
+        if (success && data) {
+          setStatus(data);
+          setStatusData(data);
+          if (data.turnstile_check) {
+            setTurnstileEnabled(true);
+            setTurnstileSiteKey(data.turnstile_site_key);
+          } else {
+            setTurnstileEnabled(false);
+            setTurnstileSiteKey('');
+          }
+        }
+      } catch (e) {
+        // ignore and keep local status
+      }
+    })();
+
+    getUserData();
   }, []);
 
   useEffect(() => {

+ 53 - 21
web/src/components/settings/personal/cards/AccountManagement.jsx

@@ -28,6 +28,7 @@ import {
   Tabs,
   TabPane,
   Popover,
+  Modal,
 } from '@douyinfe/semi-ui';
 import {
   IconMail,
@@ -83,6 +84,9 @@ const AccountManagement = ({
       </Popover>
     );
   };
+  const isBound = (accountId) => Boolean(accountId);
+  const [showTelegramBindModal, setShowTelegramBindModal] = React.useState(false);
+
   return (
     <Card className='!rounded-2xl'>
       {/* 卡片头部 */}
@@ -142,7 +146,7 @@ const AccountManagement = ({
                       size='small'
                       onClick={() => setShowEmailBindModal(true)}
                     >
-                      {userState.user && userState.user.email !== ''
+                      {isBound(userState.user?.email)
                         ? t('修改绑定')
                         : t('绑定')}
                     </Button>
@@ -165,10 +169,11 @@ const AccountManagement = ({
                         {t('微信')}
                       </div>
                       <div className='text-sm text-gray-500 truncate'>
-                        {renderAccountInfo(
-                          userState.user?.wechat_id,
-                          t('微信 ID'),
-                        )}
+                        {!status.wechat_login
+                          ? t('未启用')
+                          : isBound(userState.user?.wechat_id)
+                            ? t('已绑定')
+                            : t('未绑定')}
                       </div>
                     </div>
                   </div>
@@ -180,7 +185,7 @@ const AccountManagement = ({
                       disabled={!status.wechat_login}
                       onClick={() => setShowWeChatBindModal(true)}
                     >
-                      {userState.user && userState.user?.wechat_id
+                      {isBound(userState.user?.wechat_id)
                         ? t('修改绑定')
                         : status.wechat_login
                           ? t('绑定')
@@ -221,8 +226,7 @@ const AccountManagement = ({
                         onGitHubOAuthClicked(status.github_client_id)
                       }
                       disabled={
-                        (userState.user && userState.user.github_id !== '') ||
-                        !status.github_oauth
+                        isBound(userState.user?.github_id) || !status.github_oauth
                       }
                     >
                       {status.github_oauth ? t('绑定') : t('未启用')}
@@ -265,8 +269,7 @@ const AccountManagement = ({
                         )
                       }
                       disabled={
-                        (userState.user && userState.user.oidc_id !== '') ||
-                        !status.oidc_enabled
+                        isBound(userState.user?.oidc_id) || !status.oidc_enabled
                       }
                     >
                       {status.oidc_enabled ? t('绑定') : t('未启用')}
@@ -299,26 +302,56 @@ const AccountManagement = ({
                   </div>
                   <div className='flex-shrink-0'>
                     {status.telegram_oauth ? (
-                      userState.user?.telegram_id ? (
-                        <Button disabled={true} size='small'>
+                      isBound(userState.user?.telegram_id) ? (
+                        <Button
+                          disabled
+                          size='small'
+                          type='primary'
+                          theme='outline'
+                        >
                           {t('已绑定')}
                         </Button>
                       ) : (
-                        <div className='scale-75'>
-                          <TelegramLoginButton
-                            dataAuthUrl='/api/oauth/telegram/bind'
-                            botName={status.telegram_bot_name}
-                          />
-                        </div>
+                        <Button
+                          type='primary'
+                          theme='outline'
+                          size='small'
+                          onClick={() => setShowTelegramBindModal(true)}
+                        >
+                          {t('绑定')}
+                        </Button>
                       )
                     ) : (
-                      <Button disabled={true} size='small'>
+                      <Button
+                        disabled
+                        size='small'
+                        type='primary'
+                        theme='outline'
+                      >
                         {t('未启用')}
                       </Button>
                     )}
                   </div>
                 </div>
               </Card>
+              <Modal
+                title={t('绑定 Telegram')}
+                visible={showTelegramBindModal}
+                onCancel={() => setShowTelegramBindModal(false)}
+                footer={null}
+              >
+                <div className='my-3 text-sm text-gray-600'>
+                  {t('点击下方按钮通过 Telegram 完成绑定')}
+                </div>
+                <div className='flex justify-center'>
+                  <div className='scale-90'>
+                    <TelegramLoginButton
+                      dataAuthUrl='/api/oauth/telegram/bind'
+                      botName={status.telegram_bot_name}
+                    />
+                  </div>
+                </div>
+              </Modal>
 
               {/* LinuxDO绑定 */}
               <Card className='!rounded-xl'>
@@ -351,8 +384,7 @@ const AccountManagement = ({
                         onLinuxDOOAuthClicked(status.linuxdo_client_id)
                       }
                       disabled={
-                        (userState.user && userState.user.linux_do_id !== '') ||
-                        !status.linuxdo_oauth
+                        isBound(userState.user?.linux_do_id) || !status.linuxdo_oauth
                       }
                     >
                       {status.linuxdo_oauth ? t('绑定') : t('未启用')}

+ 84 - 1
web/src/components/table/channels/modals/EditChannelModal.jsx

@@ -103,6 +103,7 @@ const MODEL_FETCHABLE_TYPES = new Set([
   40,
   42,
   48,
+  43,
 ]);
 
 function type2secretPrompt(type) {
@@ -164,6 +165,8 @@ const EditChannelModal = (props) => {
     settings: '',
     // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type)
     vertex_key_type: 'json',
+    // 企业账户设置
+    is_enterprise_account: false,
   };
   const [batch, setBatch] = useState(false);
   const [multiToSingle, setMultiToSingle] = useState(false);
@@ -189,6 +192,7 @@ const EditChannelModal = (props) => {
   const [channelSearchValue, setChannelSearchValue] = useState('');
   const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
   const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
+  const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
 
   // 2FA验证查看密钥相关状态
   const [twoFAState, setTwoFAState] = useState({
@@ -235,7 +239,7 @@ const EditChannelModal = (props) => {
     pass_through_body_enabled: false,
     system_prompt: '',
   });
-  const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示)
+  const showApiConfigCard = true; // 控制是否显示 API 配置卡片
   const getInitValues = () => ({ ...originInputs });
 
   // 处理渠道额外设置的更新
@@ -342,6 +346,10 @@ const EditChannelModal = (props) => {
         case 36:
           localModels = ['suno_music', 'suno_lyrics'];
           break;
+        case 45:
+          localModels = getChannelModels(value);
+          setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' }));
+          break;
         default:
           localModels = getChannelModels(value);
           break;
@@ -433,15 +441,27 @@ const EditChannelModal = (props) => {
             parsedSettings.azure_responses_version || '';
           // 读取 Vertex 密钥格式
           data.vertex_key_type = parsedSettings.vertex_key_type || 'json';
+          // 读取企业账户设置
+          data.is_enterprise_account = parsedSettings.openrouter_enterprise === true;
         } catch (error) {
           console.error('解析其他设置失败:', error);
           data.azure_responses_version = '';
           data.region = '';
           data.vertex_key_type = 'json';
+          data.is_enterprise_account = false;
         }
       } else {
         // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
         data.vertex_key_type = 'json';
+        data.is_enterprise_account = false;
+      }
+
+      if (
+        data.type === 45 &&
+        (!data.base_url ||
+          (typeof data.base_url === 'string' && data.base_url.trim() === ''))
+      ) {
+        data.base_url = 'https://ark.cn-beijing.volces.com';
       }
 
       setInputs(data);
@@ -453,6 +473,8 @@ const EditChannelModal = (props) => {
       } else {
         setAutoBan(true);
       }
+      // 同步企业账户状态
+      setIsEnterpriseAccount(data.is_enterprise_account || false);
       setBasicModels(getChannelModels(data.type));
       // 同步更新channelSettings状态显示
       setChannelSettings({
@@ -712,6 +734,8 @@ const EditChannelModal = (props) => {
     });
     // 重置密钥模式状态
     setKeyMode('append');
+    // 重置企业账户状态
+    setIsEnterpriseAccount(false);
     // 清空表单中的key_mode字段
     if (formApiRef.current) {
       formApiRef.current.setValue('key_mode', undefined);
@@ -844,6 +868,10 @@ const EditChannelModal = (props) => {
       showInfo(t('请至少选择一个模型!'));
       return;
     }
+    if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) {
+      showInfo(t('请输入API地址!'));
+      return;
+    }
     if (
       localInputs.model_mapping &&
       localInputs.model_mapping !== '' &&
@@ -873,6 +901,21 @@ const EditChannelModal = (props) => {
     };
     localInputs.setting = JSON.stringify(channelExtraSettings);
 
+    // 处理type === 20的企业账户设置
+    if (localInputs.type === 20) {
+      let settings = {};
+      if (localInputs.settings) {
+        try {
+          settings = JSON.parse(localInputs.settings);
+        } catch (error) {
+          console.error('解析settings失败:', error);
+        }
+      }
+      // 设置企业账户标识,无论是true还是false都要传到后端
+      settings.openrouter_enterprise = localInputs.is_enterprise_account === true;
+      localInputs.settings = JSON.stringify(settings);
+    }
+
     // 清理不需要发送到后端的字段
     delete localInputs.force_format;
     delete localInputs.thinking_to_content;
@@ -880,6 +923,7 @@ const EditChannelModal = (props) => {
     delete localInputs.pass_through_body_enabled;
     delete localInputs.system_prompt;
     delete localInputs.system_prompt_override;
+    delete localInputs.is_enterprise_account;
     // 顶层的 vertex_key_type 不应发送给后端
     delete localInputs.vertex_key_type;
 
@@ -1264,6 +1308,21 @@ const EditChannelModal = (props) => {
                     onChange={(value) => handleInputChange('type', value)}
                   />
 
+                  {inputs.type === 20 && (
+                    <Form.Switch
+                      field='is_enterprise_account'
+                      label={t('是否为企业账户')}
+                      checkedText={t('是')}
+                      uncheckedText={t('否')}
+                      onChange={(value) => {
+                        setIsEnterpriseAccount(value);
+                        handleInputChange('is_enterprise_account', value);
+                      }}
+                      extraText={t('企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选')}
+                      initValue={inputs.is_enterprise_account}
+                    />
+                  )}
+
                   <Form.Input
                     field='name'
                     label={t('名称')}
@@ -1883,6 +1942,30 @@ const EditChannelModal = (props) => {
                         />
                       </div>
                     )}
+
+                    {inputs.type === 45 && (
+                        <div>
+                          <Form.Select
+                              field='base_url'
+                              label={t('API地址')}
+                              placeholder={t('请选择API地址')}
+                              onChange={(value) =>
+                                  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'
+                          />
+                        </div>
+                    )}
                   </Card>
                 )}
 

+ 3 - 0
web/src/components/table/channels/modals/EditTagModal.jsx

@@ -118,6 +118,9 @@ const EditTagModal = (props) => {
         case 36:
           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; 
         default:
           localModels = getChannelModels(value);
           break;

+ 5 - 0
web/src/constants/channel.constants.js

@@ -159,6 +159,11 @@ export const CHANNEL_OPTIONS = [
     color: 'purple',
     label: 'Vidu',
   },
+   {
+    value: 53,
+    color: 'blue',
+    label: 'SubModel',
+  },
 ];
 
 export const MODEL_TABLE_PAGE_SIZE = 10;

+ 68 - 68
web/src/helpers/render.jsx

@@ -1200,25 +1200,25 @@ export function renderModelPrice(
               const extraServices = [
                 webSearch && webSearchCallCount > 0
                   ? i18next.t(
-                      ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
-                      {
-                        count: webSearchCallCount,
-                        price: webSearchPrice,
-                        ratio: groupRatio,
-                        ratioType: ratioLabel,
-                      },
-                    )
+                    ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+                    {
+                      count: webSearchCallCount,
+                      price: webSearchPrice,
+                      ratio: groupRatio,
+                      ratioType: ratioLabel,
+                    },
+                  )
                   : '',
                 fileSearch && fileSearchCallCount > 0
                   ? i18next.t(
-                      ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
-                      {
-                        count: fileSearchCallCount,
-                        price: fileSearchPrice,
-                        ratio: groupRatio,
-                        ratioType: ratioLabel,
-                      },
-                    )
+                    ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * {{ratioType}} {{ratio}}',
+                    {
+                      count: fileSearchCallCount,
+                      price: fileSearchPrice,
+                      ratio: groupRatio,
+                      ratioType: ratioLabel,
+                    },
+                  )
                   : '',
                 imageGenerationCall && imageGenerationCallPrice > 0
                   ? i18next.t(
@@ -1398,10 +1398,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 (
       <>
@@ -1457,27 +1457,27 @@ export function renderAudioModelPrice(
           <p>
             {cacheTokens > 0
               ? i18next.t(
-                  '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                  {
-                    nonCacheInput: inputTokens - cacheTokens,
-                    cacheInput: cacheTokens,
-                    cachePrice: inputRatioPrice * cacheRatio,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    total: textPrice.toFixed(6),
-                  },
-                )
+                '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                {
+                  nonCacheInput: inputTokens - cacheTokens,
+                  cacheInput: cacheTokens,
+                  cachePrice: inputRatioPrice * cacheRatio,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  total: textPrice.toFixed(6),
+                },
+              )
               : i18next.t(
-                  '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
-                  {
-                    input: inputTokens,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    total: textPrice.toFixed(6),
-                  },
-                )}
+                '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
+                {
+                  input: inputTokens,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  total: textPrice.toFixed(6),
+                },
+              )}
           </p>
           <p>
             {i18next.t(
@@ -1617,35 +1617,35 @@ export function renderClaudeModelPrice(
           <p>
             {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: nonCachedTokens,
-                    cacheInput: cacheTokens,
-                    cacheRatio: cacheRatio,
-                    cacheCreationInput: cacheCreationTokens,
-                    cacheCreationRatio: cacheCreationRatio,
-                    cachePrice: cacheRatioPrice,
-                    cacheCreationPrice: cacheCreationRatioPrice,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    ratio: groupRatio,
-                    ratioType: ratioLabel,
-                    total: price.toFixed(6),
-                  },
-                )
+                '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+                {
+                  nonCacheInput: nonCachedTokens,
+                  cacheInput: cacheTokens,
+                  cacheRatio: cacheRatio,
+                  cacheCreationInput: cacheCreationTokens,
+                  cacheCreationRatio: cacheCreationRatio,
+                  cachePrice: cacheRatioPrice,
+                  cacheCreationPrice: cacheCreationRatioPrice,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  ratio: groupRatio,
+                  ratioType: ratioLabel,
+                  total: price.toFixed(6),
+                },
+              )
               : i18next.t(
-                  '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
-                  {
-                    input: inputTokens,
-                    price: inputRatioPrice,
-                    completion: completionTokens,
-                    compPrice: completionRatioPrice,
-                    ratio: groupRatio,
-                    ratioType: ratioLabel,
-                    total: price.toFixed(6),
-                  },
-                )}
+                '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * {{ratioType}} {{ratio}} = ${{total}}',
+                {
+                  input: inputTokens,
+                  price: inputRatioPrice,
+                  completion: completionTokens,
+                  compPrice: completionRatioPrice,
+                  ratio: groupRatio,
+                  ratioType: ratioLabel,
+                  total: price.toFixed(6),
+                },
+              )}
           </p>
           <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
         </article>

+ 195 - 162
web/src/hooks/channels/useChannelsData.jsx

@@ -25,13 +25,9 @@ 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';
@@ -68,7 +64,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
@@ -83,9 +79,10 @@ export const useChannelsData = () => {
   const [testingModels, setTestingModels] = useState(new Set());
   const [selectedModelKeys, setSelectedModelKeys] = useState([]);
   const [isBatchTesting, setIsBatchTesting] = useState(false);
-  const [testQueue, setTestQueue] = useState([]);
-  const [isProcessingQueue, setIsProcessingQueue] = useState(false);
   const [modelTablePage, setModelTablePage] = useState(1);
+  
+  // 使用 ref 来避免闭包问题,类似旧版实现
+  const shouldStopBatchTestingRef = useRef(false);
 
   // Multi-key management states
   const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false);
@@ -119,12 +116,9 @@ 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);
@@ -182,10 +176,7 @@ 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]);
 
@@ -299,21 +290,14 @@ 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}`,
@@ -327,10 +311,7 @@ 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);
@@ -354,18 +335,11 @@ 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}`,
@@ -373,10 +347,7 @@ 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);
@@ -395,14 +366,7 @@ 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);
     }
   };
 
@@ -488,16 +452,9 @@ 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);
     }
   };
 
@@ -513,14 +470,7 @@ export const useChannelsData = () => {
           showError(reason);
         });
     } else {
-      searchChannels(
-        enableTagMode,
-        activeTypeKey,
-        statusFilter,
-        1,
-        size,
-        idSort,
-      );
+      searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort);
     }
   };
 
@@ -551,10 +501,7 @@ 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));
     }
   };
 
@@ -593,11 +540,7 @@ 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;
         }
@@ -740,136 +683,226 @@ 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);
     }
   };
 
-  // Test channel
+  // Test channel - 单个模型测试,参考旧版实现
   const testChannel = async (record, model) => {
-    setTestQueue((prev) => [...prev, { channel: record, model }]);
-    if (!isProcessingQueue) {
-      setIsProcessingQueue(true);
+    const testKey = `${record.id}-${model}`;
+
+    // 检查是否应该停止批量测试
+    if (shouldStopBatchTestingRef.current && isBatchTesting) {
+      return Promise.resolve();
     }
-  };
 
-  // Process test queue
-  const processTestQueue = async () => {
-    if (!isProcessingQueue || testQueue.length === 0) return;
+    // 添加到正在测试的模型集合
+    setTestingModels(prev => new Set([...prev, model]));
 
-    const { channel, model, indexInFiltered } = testQueue[0];
+    try {
+      const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
 
-    if (currentTestChannel && currentTestChannel.id === channel.id) {
-      let pageNo;
-      if (indexInFiltered !== undefined) {
-        pageNo = Math.floor(indexInFiltered / MODEL_TABLE_PAGE_SIZE) + 1;
-      } else {
-        const filteredModelsList = currentTestChannel.models
-          .split(',')
-          .filter((m) =>
-            m.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
-          );
-        const modelIdx = filteredModelsList.indexOf(model);
-        pageNo =
-          modelIdx !== -1
-            ? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1
-            : 1;
+      // 检查是否在请求期间被停止
+      if (shouldStopBatchTestingRef.current && isBatchTesting) {
+        return Promise.resolve();
       }
-      setModelTablePage(pageNo);
-    }
 
-    try {
-      setTestingModels((prev) => new Set([...prev, model]));
-      const res = await API.get(
-        `/api/channel/test/${channel.id}?model=${model}`,
-      );
       const { success, message, time } = res.data;
 
-      setModelTestResults((prev) => ({
+      // 更新测试结果
+      setModelTestResults(prev => ({
         ...prev,
-        [`${channel.id}-${model}`]: { success, time },
+        [testKey]: {
+          success,
+          message,
+          time: time || 0,
+          timestamp: Date.now()
+        }
       }));
 
       if (success) {
-        updateChannelProperty(channel.id, (ch) => {
-          ch.response_time = time * 1000;
-          ch.test_time = Date.now() / 1000;
+        // 更新渠道响应时间
+        updateChannelProperty(record.id, (channel) => {
+          channel.response_time = time * 1000;
+          channel.test_time = Date.now() / 1000;
         });
-        if (!model) {
+
+        if (!model || model === '') {
           showInfo(
             t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。')
-              .replace('${name}', channel.name)
+              .replace('${name}', record.name)
+              .replace('${time.toFixed(2)}', time.toFixed(2)),
+          );
+        } else {
+          showInfo(
+            t('通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。')
+              .replace('${name}', record.name)
+              .replace('${model}', model)
               .replace('${time.toFixed(2)}', time.toFixed(2)),
           );
         }
       } else {
-        showError(message);
+        showError(`${t('模型')} ${model}: ${message}`);
       }
     } catch (error) {
-      showError(error.message);
+      // 处理网络错误
+      const testKey = `${record.id}-${model}`;
+      setModelTestResults(prev => ({
+        ...prev,
+        [testKey]: {
+          success: false,
+          message: error.message || t('网络错误'),
+          time: 0,
+          timestamp: Date.now()
+        }
+      }));
+      showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`);
     } finally {
-      setTestingModels((prev) => {
+      // 从正在测试的模型集合中移除
+      setTestingModels(prev => {
         const newSet = new Set(prev);
         newSet.delete(model);
         return newSet;
       });
     }
-
-    setTestQueue((prev) => prev.slice(1));
   };
 
-  // Monitor queue changes
-  useEffect(() => {
-    if (testQueue.length > 0 && isProcessingQueue) {
-      processTestQueue();
-    } else if (testQueue.length === 0 && isProcessingQueue) {
-      setIsProcessingQueue(false);
-      setIsBatchTesting(false);
+  // 批量测试单个渠道的所有模型,参考旧版实现
+  const batchTestModels = async () => {
+    if (!currentTestChannel || !currentTestChannel.models) {
+      showError(t('渠道模型信息不完整'));
+      return;
     }
-  }, [testQueue, isProcessingQueue]);
 
-  // Batch test models
-  const batchTestModels = async () => {
-    if (!currentTestChannel) return;
+    const models = currentTestChannel.models.split(',').filter(model =>
+      model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
+    );
+
+    if (models.length === 0) {
+      showError(t('没有找到匹配的模型'));
+      return;
+    }
 
     setIsBatchTesting(true);
-    setModelTablePage(1);
+    shouldStopBatchTestingRef.current = false; // 重置停止标志
+
+    // 清空该渠道之前的测试结果
+    setModelTestResults(prev => {
+      const newResults = { ...prev };
+      models.forEach(model => {
+        const testKey = `${currentTestChannel.id}-${model}`;
+        delete newResults[testKey];
+      });
+      return newResults;
+    });
 
-    const filteredModels = currentTestChannel.models
-      .split(',')
-      .filter((model) =>
-        model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
-      );
+    try {
+      showInfo(t('开始批量测试 ${count} 个模型,已清空上次结果...').replace('${count}', models.length));
 
-    setTestQueue(
-      filteredModels.map((model, idx) => ({
-        channel: currentTestChannel,
-        model,
-        indexInFiltered: idx,
-      })),
-    );
-    setIsProcessingQueue(true);
+      // 提高并发数量以加快测试速度,参考旧版的并发限制
+      const concurrencyLimit = 5;
+      const results = [];
+
+      for (let i = 0; i < models.length; i += concurrencyLimit) {
+        // 检查是否应该停止
+        if (shouldStopBatchTestingRef.current) {
+          showInfo(t('批量测试已停止'));
+          break;
+        }
+
+        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)
+        );
+
+        const batchPromises = batch.map(model => testChannel(currentTestChannel, model));
+        const batchResults = await Promise.allSettled(batchPromises);
+        results.push(...batchResults);
+
+        // 再次检查是否应该停止
+        if (shouldStopBatchTestingRef.current) {
+          showInfo(t('批量测试已停止'));
+          break;
+        }
+
+        // 短暂延迟避免过于频繁的请求
+        if (i + concurrencyLimit < models.length) {
+          await new Promise(resolve => setTimeout(resolve, 100));
+        }
+      }
+
+      if (!shouldStopBatchTestingRef.current) {
+        // 等待一小段时间确保所有结果都已更新
+        await new Promise(resolve => setTimeout(resolve, 300));
+
+        // 使用当前状态重新计算结果统计
+        setModelTestResults(currentResults => {
+          let successCount = 0;
+          let failCount = 0;
+
+          models.forEach(model => {
+            const testKey = `${currentTestChannel.id}-${model}`;
+            const result = currentResults[testKey];
+            if (result && result.success) {
+              successCount++;
+            } else {
+              failCount++;
+            }
+          });
+
+          // 显示完成消息
+          setTimeout(() => {
+            showSuccess(t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}')
+              .replace('${success}', successCount)
+              .replace('${fail}', failCount)
+              .replace('${total}', models.length)
+            );
+          }, 100);
+
+          return currentResults; // 不修改状态,只是为了获取最新值
+        });
+      }
+    } catch (error) {
+      showError(t('批量测试过程中发生错误: ') + error.message);
+    } finally {
+      setIsBatchTesting(false);
+    }
+  };
+
+  // 停止批量测试
+  const stopBatchTesting = () => {
+    shouldStopBatchTestingRef.current = true;
+    setIsBatchTesting(false);
+    setTestingModels(new Set());
+    showInfo(t('已停止批量测试'));
+  };
+
+  // 清空测试结果
+  const clearTestResults = () => {
+    setModelTestResults({});
+    showInfo(t('已清空测试结果'));
   };
 
   // Handle close modal
   const handleCloseModal = () => {
+    // 如果正在批量测试,先停止测试
     if (isBatchTesting) {
-      setTestQueue([]);
-      setIsProcessingQueue(false);
-      setIsBatchTesting(false);
-      showSuccess(t('已停止测试'));
-    } else {
-      setShowModelTestModal(false);
-      setModelSearchKeyword('');
-      setSelectedModelKeys([]);
-      setModelTablePage(1);
+      shouldStopBatchTestingRef.current = true;
+      showInfo(t('关闭弹窗,已停止批量测试'));
     }
+
+    setShowModelTestModal(false);
+    setModelSearchKeyword('');
+    setIsBatchTesting(false);
+    setTestingModels(new Set());
+    setSelectedModelKeys([]);
+    setModelTablePage(1);
+    // 可选择性保留测试结果,这里不清空以便用户查看
   };
 
   // Type counts
@@ -1012,4 +1045,4 @@ export const useChannelsData = () => {
     setCompactMode,
     setActivePage,
   };
-};
+}; 

+ 4 - 0
web/src/i18n/i18n.js

@@ -22,6 +22,7 @@ import { initReactI18next } from 'react-i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
 
 import enTranslation from './locales/en.json';
+import frTranslation from './locales/fr.json';
 import zhTranslation from './locales/zh.json';
 
 i18n
@@ -36,6 +37,9 @@ i18n
       zh: {
         translation: zhTranslation,
       },
+      fr: {
+        translation: frTranslation,
+      },
     },
     fallbackLng: 'zh',
     interpolation: {

+ 4 - 1
web/src/i18n/locales/en.json

@@ -2130,5 +2130,8 @@
   "域名IP过滤详细说明": "⚠️ This is an experimental option. A domain may resolve to multiple IPv4/IPv6 addresses. If enabled, ensure the IP filter list covers these addresses, otherwise access may fail.",
   "域名黑名单": "Domain Blacklist",
   "白名单": "Whitelist",
-  "黑名单": "Blacklist"
+  "黑名单": "Blacklist",
+  "common": {
+    "changeLanguage": "Change Language"
+  }
 }

+ 2140 - 0
web/src/i18n/locales/fr.json

@@ -0,0 +1,2140 @@
+{
+  "主页": "Accueil",
+  "控制台": "Console",
+  "$%.6f 额度": "Quota de $%.6f",
+  "或": "ou",
+  "登 录": "Se connecter",
+  "注 册": "S'inscrire",
+  "使用 邮箱或用户名 登录": "Connectez-vous avec votre e-mail ou votre nom d'utilisateur",
+  "使用 GitHub 继续": "Continuer avec GitHub",
+  "使用 OIDC 继续": "Continuer avec OIDC",
+  "使用 微信 继续": "Continuer avec WeChat",
+  "使用 LinuxDO 继续": "Continuer avec LinuxDO",
+  "使用 用户名 注册": "S'inscrire avec un nom d'utilisateur",
+  "其他登录选项": "Autres options de connexion",
+  "其他注册选项": "Autres options d'inscription",
+  "请输入您的用户名或邮箱地址": "Veuillez saisir votre nom d'utilisateur ou votre adresse e-mail",
+  "请输入您的邮箱地址": "Veuillez saisir votre adresse e-mail",
+  "请输入您的密码": "Veuillez saisir votre mot de passe",
+  "继续": "Continuer",
+  "%d 点额度": "Quota de %d points",
+  "尚未实现": "Pas encore implémenté",
+  "余额不足": "Quota insuffisant",
+  "危险操作": "Opération dangereuse",
+  "输入你的账户名": "Saisissez le nom de votre compte",
+  "确认删除": "Confirmer la suppression",
+  "确认绑定": "Confirmer la liaison",
+  "您正在删除自己的帐户,将清空所有数据且不可恢复": "Vous êtes sur le point de supprimer votre compte. Toutes les données seront effacées et ne pourront pas être récupérées.",
+  "通道「%s」(#%d)已被禁用": "Le canal %s (#%d) a été désactivé",
+  "通道「%s」(#%d)已被禁用,原因:%s": "Le canal %s (#%d) a été désactivé pour la raison suivante : %s",
+  "测试已在运行中": "Le test est déjà en cours",
+  "响应时间 %.2fs 超过阈值 %.2fs": "Le temps de réponse de %.2fs secondes dépasse le seuil de %.2fs secondes",
+  "通道测试完成": "Test du canal terminé",
+  "通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Test du canal terminé. Si aucune notification de désactivation n'a été reçue, tous les canaux fonctionnent normalement",
+  "无法连接至 GitHub 服务器,请稍后重试!": "Impossible de se connecter au serveur GitHub. Veuillez réessayer plus tard !",
+  "返回值非法,用户字段为空,请稍后重试!": "Valeur de retour non valide, le champ utilisateur est vide. Veuillez réessayer plus tard !",
+  "管理员未开启通过 GitHub 登录以及注册": "L'administrateur n'a pas activé la connexion et l'inscription via GitHub",
+  "管理员关闭了新用户注册": "L'administrateur a désactivé l'inscription de nouveaux utilisateurs",
+  "用户已被封禁": "L'utilisateur a été banni",
+  "该 GitHub 账户已被绑定": "Ce compte GitHub est déjà lié",
+  "邮箱地址已被占用": "L'adresse e-mail est déjà utilisée",
+  "%s邮箱验证邮件": "E-mail de vérification de %s",
+  "<p>您好,你正在进行%s邮箱验证。</p>": "<p>Bonjour, vous êtes en train de vérifier votre adresse e-mail %s.</p>",
+  "<p>您的验证码为: <strong>%s</strong></p>": "<p>Votre code de vérification est : <strong>%s</strong></p>",
+  "<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>Le code de vérification est valide pendant %d minutes. Si vous n'êtes pas à l'origine de cette demande, veuillez l'ignorer.</p>",
+  "无效的参数": "Paramètre non valide",
+  "该邮箱地址未注册": "Cette adresse e-mail n'est pas enregistrée",
+  "%s密码重置": "Réinitialisation du mot de passe de %s",
+  "<p>您好,你正在进行%s密码重置。</p>": "<p>Bonjour, vous êtes en train de réinitialiser votre mot de passe %s.</p>",
+  "<p>点击<a href='%s'>此处</a>进行密码重置。</p>": "<p>Cliquez <a href='%s'>ici</a> pour réinitialiser votre mot de passe.</p>",
+  "<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>": "<p>Le lien de réinitialisation est valide pendant %d minutes. Si vous n'êtes pas à l'origine de cette demande, veuillez l'ignorer.</p>",
+  "重置链接非法或已过期": "Le lien de réinitialisation est non valide ou a expiré",
+  "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!": "Impossible d'activer GitHub OAuth. Veuillez d'abord saisir l'ID client et le secret client GitHub !",
+  "无法启用微信登录,请先填入微信登录相关配置信息!": "Impossible d'activer la connexion WeChat. Veuillez d'abord saisir les informations de configuration de la connexion WeChat !",
+  "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!": "Impossible d'activer la vérification Turnstile. Veuillez d'abord saisir les informations de configuration de la vérification Turnstile !",
+  "兑换码名称长度必须在1-20之间": "Le nom du code d'échange doit comporter entre 1 et 20 caractères",
+  "兑换码个数必须大于0": "Le nombre de codes d'échange doit être supérieur à 0",
+  "一次兑换码批量生成的个数不能大于 100": "Impossible de générer plus de 100 codes d'échange à la fois",
+  "当前分组上游负载已饱和,请稍后再试": "La charge en amont du groupe actuel est saturée. Veuillez réessayer plus tard",
+  "令牌名称过长": "Le nom du jeton est trop long",
+  "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "Le jeton a expiré et ne peut pas être activé. Veuillez modifier la date d'expiration du jeton ou le définir pour qu'il n'expire jamais",
+  "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "Le quota du jeton est épuisé et ne peut pas être activé. Veuillez modifier le quota restant ou le définir sur illimité",
+  "管理员关闭了密码登录": "L'administrateur a désactivé la connexion par mot de passe",
+  "无法保存会话信息,请重试": "Impossible d'enregistrer les informations de session. Veuillez réessayer",
+  "管理员关闭了通过密码进行注册,请使用第三方账户验证的形式进行注册": "L'administrateur a désactivé l'inscription par mot de passe. Veuillez vous inscrire en utilisant la vérification de compte tiers",
+  "输入不合法 ": "Entrée non valide ",
+  "管理员开启了邮箱验证,请输入邮箱地址和验证码": "L'administrateur a activé la vérification par e-mail. Veuillez saisir votre adresse e-mail et votre code de vérification",
+  "验证码错误或已过期": "Le code de vérification est incorrect ou a expiré",
+  "无权获取同级或更高等级用户的信息": "Aucune autorisation d'accéder aux informations des utilisateurs de même niveau ou de niveau supérieur",
+  "请重试,系统生成的 UUID 竟然重复了!": "Veuillez réessayer, l'UUID généré par le système est dupliqué !",
+  "输入不合法": "Entrée non valide",
+  "无权更新同权限等级或更高权限等级的用户信息": "Aucune autorisation de mettre à jour les informations des utilisateurs de même niveau de permission ou supérieur",
+  "管理员将用户额度从 %s修改为 %s": "L'administrateur a modifié le quota de l'utilisateur de %s à %s",
+  "无权删除同权限等级或更高权限等级的用户": "Aucune autorisation de supprimer les utilisateurs de même niveau de permission ou supérieur",
+  "无法创建权限大于等于自己的用户": "Impossible de créer des utilisateurs avec des autorisations supérieures ou égales aux vôtres",
+  "用户不存在": "L'utilisateur n'existe pas",
+  "无法禁用超级管理员用户": "Impossible de désactiver l'utilisateur super administrateur",
+  "无法删除超级管理员用户": "Impossible de supprimer l'utilisateur super administrateur",
+  "普通管理员用户无法提升其他用户为管理员": "Un administrateur ordinaire ne peut pas promouvoir d'autres utilisateurs au rang d'administrateur",
+  "该用户已经是管理员": "Cet utilisateur est déjà administrateur",
+  "无法降级超级管理员用户": "Impossible de rétrograder l'utilisateur super administrateur",
+  "该用户已经是普通用户": "Cet utilisateur est déjà un utilisateur ordinaire",
+  "管理员未开启通过微信登录以及注册": "L'administrateur n'a pas activé la connexion et l'inscription via WeChat",
+  "该微信账号已被绑定": "Ce compte WeChat est déjà lié",
+  "无权进行此操作,未登录且未提供 access token": "Aucune autorisation pour cette opération : non connecté et aucun jeton d'accès fourni",
+  "无权进行此操作,access token 无效": "Aucune autorisation pour cette opération : jeton d'accès non valide",
+  "无权进行此操作,权限不足": "Aucune autorisation pour cette opération : autorisations insuffisantes",
+  "普通用户不支持指定渠道": "Les utilisateurs ordinaires ne peuvent pas spécifier de canaux",
+  "无效的渠道 ID": "ID de canal non valide",
+  "该渠道已被禁用": "Ce canal a été désactivé",
+  "无效的请求": "Requête non valide",
+  "无可用渠道": "Aucun canal disponible",
+  "Turnstile token 为空": "Le jeton Turnstile est vide",
+  "Turnstile 校验失败,请刷新重试!": "La vérification Turnstile a échoué. Veuillez actualiser et réessayer !",
+  "id 为空!": "L'ID est vide !",
+  "未提供兑换码": "Aucun code d'échange fourni",
+  "无效的 user id": "ID utilisateur non valide",
+  "无效的兑换码": "Code d'échange non valide",
+  "该兑换码已被使用": "Ce code d'échange a déjà été utilisé",
+  "通过兑换码充值 %s": "Recharger %s via le code d'échange",
+  "未提供令牌": "Aucun jeton fourni",
+  "该令牌状态不可用": "Le statut de ce jeton n'est pas disponible",
+  "该令牌已过期": "Ce jeton a expiré",
+  "该令牌额度已用尽": "Le quota de ce jeton est épuisé",
+  "无效的令牌": "Jeton non valide",
+  "id 或 userId 为空!": "L'ID ou l'userID est vide !",
+  "quota 不能为负数!": "Le quota ne peut pas être négatif !",
+  "令牌额度不足": "Quota de jeton insuffisant",
+  "用户额度不足": "Quota utilisateur insuffisant",
+  "您的额度即将用尽": "Votre quota est presque épuisé",
+  "您的额度已用尽": "Votre quota est épuisé",
+  "%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>": "%s, le quota restant actuel est de %d. Pour éviter toute interruption de service, veuillez recharger rapidement.<br/>Lien de recharge : <a href='%s'>%s</a>",
+  "affCode 为空!": "Le code d'affiliation est vide !",
+  "新用户注册赠送 %s": "Bonus d'inscription pour nouvel utilisateur : %s",
+  "使用邀请码赠送 %s": "Bonus de code d'invitation : %s",
+  "邀请用户赠送 %s": "Bonus de parrainage : %s",
+  "用户名或密码为空": "Le nom d'utilisateur ou le mot de passe est vide",
+  "用户名或密码错误,或用户已被封禁": "Le nom d'utilisateur ou le mot de passe est incorrect, ou l'utilisateur a été banni",
+  "email 为空!": "L'e-mail est vide !",
+  "GitHub id 为空!": "L'ID GitHub est vide !",
+  "WeChat id 为空!": "L'ID WeChat est vide !",
+  "username 为空!": "Le nom d'utilisateur est vide !",
+  "邮箱地址或密码为空!": "L'adresse e-mail ou le mot de passe est vide !",
+  "OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用": "Système de gestion d'agrégation d'API OpenAI prenant en charge plusieurs canaux, y compris Azure. Peut être utilisé pour la gestion et la redistribution des clés. Fichier exécutable unique, image Docker pré-empaquetée, déploiement en un clic, prêt à l'emploi",
+  "未知类型": "Type inconnu",
+  "不支持": "Non pris en charge",
+  "操作成功完成!": "Opération terminée avec succès !",
+  "已启用": "Activé",
+  "已禁用": "Désactivé",
+  "未知状态": "Statut inconnu",
+  " 秒": "s",
+  " 分钟 ": "m",
+  " 小时 ": "h",
+  " 天 ": "j",
+  " 个月 ": "M",
+  " 年 ": "a",
+  "未测试": "Non testé",
+  "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Test du canal ${name} réussi, a pris ${time.toFixed(2)} secondes.",
+  "已成功开始测试所有已启用通道,请刷新页面查看结果。": "Le test de tous les canaux activés a démarré avec succès. Veuillez actualiser la page pour voir les résultats.",
+  "通道 ${name} 余额更新成功!": "Le quota du canal ${name} a été mis à jour avec succès !",
+  "已更新完毕所有已启用通道余额!": "Le quota de tous les canaux activés a été mis à jour !",
+  "渠道ID,名称,密钥,API地址": "ID du canal, nom, clé, URL de base",
+  "名称": "Nom",
+  "分组": "Groupe",
+  "类型": "Type",
+  "状态": "Statut",
+  "响应时间": "Temps de réponse",
+  "余额": "Solde",
+  "操作": "Actions",
+  "未更新": "Non mis à jour",
+  "测试": "Tester",
+  "更新余额": "Mettre à jour le solde",
+  "删除": "Supprimer",
+  "删除渠道 {channel.name}": "Supprimer le canal {channel.name}",
+  "禁用": "Désactiver",
+  "启用": "Activer",
+  "编辑": "Modifier",
+  "添加新的渠道": "Ajouter un nouveau canal",
+  "测试所有已启用通道": "Tester tous les canaux activés",
+  "更新所有已启用通道余额": "Mettre à jour le solde de tous les canaux activés",
+  "刷新": "Actualiser",
+  "绑定成功!": "Liaison réussie !",
+  "登录成功!": "Connexion réussie !",
+  "操作失败,重定向至登录界面中...": "L'opération a échoué, redirection vers la page de connexion...",
+  "出现错误,第 ${count} 次重试中...": "Une erreur s'est produite, tentative de relance ${count}...",
+  "首页": "Accueil",
+  "渠道": "Canal",
+  "令牌": "Jetons",
+  "兑换额度": "Utiliser",
+  "充值": "Recharger",
+  "用户": "Utilisateurs",
+  "日志": "Journaux",
+  "设置": "Paramètres",
+  "关于": "À propos",
+  "价格": "Tarifs",
+  "注销成功!": "Déconnexion réussie !",
+  "注销": "Se déconnecter",
+  "登录": "Se connecter",
+  "注册": "S'inscrire",
+  "未登录或登录已过期,请重新登录!": "Non connecté ou session expirée. Veuillez vous reconnecter !",
+  "用户登录": "Connexion utilisateur",
+  "密码": "Mot de passe",
+  "忘记密码?": "Mot de passe oublié ?",
+  "点击重置": "Cliquez pour réinitialiser",
+  "; 没有账户?": " ; Pas de compte ?",
+  "点击注册": "Cliquez pour vous inscrire",
+  "微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)": "Scannez le code QR WeChat pour suivre le compte officiel, entrez \"code de vérification\" pour obtenir le code (valide 3 minutes)",
+  "全部用户": "Tous les utilisateurs",
+  "当前用户": "Utilisateur actuel",
+  "全部'": "Tous'",
+  "充值'": "Recharger'",
+  "消费'": "Consommer'",
+  "管理'": "Gérer'",
+  "系统'": "Système'",
+  " 充值 ": " Recharger ",
+  " 消费 ": " Consommer ",
+  " 管理 ": " Gérer ",
+  " 系统 ": " Système ",
+  " 未知 ": " Inconnu ",
+  "时间": "Heure",
+  "详情": "Détails",
+  "选择模式": "Sélectionner le mode",
+  "选择明细分类": "Sélectionner la catégorie de détail",
+  "模型倍率不是合法的 JSON 字符串": "Le ratio de modèle n'est pas une chaîne JSON valide",
+  "通用设置": "Paramètres généraux",
+  "充值链接": "Lien de recharge",
+  "例如发卡网站的购买链接": "Par exemple, lien d'achat sur un site d'émission de cartes",
+  "文档地址": "Lien du document",
+  "例如 https://docs.newapi.pro": "Par exemple, https://docs.newapi.pro",
+  "聊天页面链接": "Lien de la page de discussion",
+  "例如 ChatGPT Next Web 的部署地址": "Par exemple, l'adresse de déploiement de ChatGPT Next Web",
+  "单位美元额度": "Quota par USD",
+  "一单位货币能兑换的额度": "Quota échangeable par unité monétaire",
+  "启用额度消费日志记录": "Activer la journalisation de la consommation de quota",
+  "以货币形式显示额度": "Afficher le quota en devise",
+  "相关 API 显示令牌额度而非用户额度": "Les API associées affichent le quota de jeton au lieu du quota utilisateur",
+  "保存通用设置": "Enregistrer les paramètres généraux",
+  "监控设置": "Paramètres de surveillance",
+  "测试所有渠道的最长响应时间": "Temps de réponse maximal pour tester tous les canaux",
+  "单位秒": "Unité : secondes",
+  "当运行通道全部测试时": "Lors de l'exécution de tous les tests de canaux",
+  "超过此时间将自动禁用通道": "Les canaux dépassant ce temps seront automatiquement désactivés",
+  "额度提醒阈值": "Seuil de rappel de quota",
+  "低于此额度时将发送邮件提醒用户": "Un rappel par e-mail sera envoyé lorsque le quota tombera en dessous de ce seuil",
+  "失败时自动禁用通道": "Désactiver automatiquement le canal en cas d'échec",
+  "保存监控设置": "Enregistrer les paramètres de surveillance",
+  "额度设置": "Paramètres de quota",
+  "新用户初始额度": "Quota initial pour les nouveaux utilisateurs",
+  "例如": "Par exemple",
+  "请求预扣费额度": "Quota de pré-déduction pour les demandes",
+  "请求结束后多退少补": "Ajuster après la fin de la demande",
+  "邀请新用户奖励额度": "Quota de bonus de parrainage",
+  "新用户使用邀请码奖励额度": "Quota de bonus de code d'invitation pour nouvel utilisateur",
+  "保存额度设置": "Enregistrer les paramètres de quota",
+  "倍率设置": "Paramètres de ratio",
+  "模型倍率": "Ratio de modèle",
+  "为一个 JSON 文本": "Est un texte JSON",
+  "键为模型名称": "La clé est le nom du modèle",
+  "值为倍率": "La valeur est le ratio",
+  "分组倍率": "Ratio de groupe",
+  "键为分组名称": "La clé est le nom du groupe",
+  "保存倍率设置": "Enregistrer les paramètres de ratio",
+  "已是最新版本": "Est la dernière version",
+  "检查更新": "Vérifier les mises à jour",
+  "公告": "Annonce",
+  "在此输入新的公告内容,支持 Markdown & HTML 代码": "Saisissez le nouveau contenu de l'annonce ici, prend en charge le code Markdown & HTML",
+  "保存公告": "Enregistrer l'annonce",
+  "个性化设置": "Paramètres de personnalisation",
+  "系统名称": "Nom du système",
+  "在此输入系统名称": "Saisissez le nom du système ici",
+  "设置系统名称": "Définir le nom du système",
+  "图片地址": "URL de l'image",
+  "在此输入 Logo 图片地址": "Saisissez l'URL de l'image du logo ici",
+  "首页内容": "Contenu de la page d'accueil",
+  "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页": "Saisissez le contenu de la page d'accueil ici, prend en charge Markdown",
+  "保存首页内容": "Enregistrer le contenu de la page d'accueil",
+  "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面": "Saisissez le nouveau contenu \"À propos\" ici, prend en charge Markdown",
+  "保存关于": "Enregistrer \"À propos\"",
+  "移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目": "La suppression de la marque de copyright de One API doit d'abord être autorisée. La maintenance du projet demande beaucoup d'efforts. Si ce projet a du sens pour vous, veuillez le soutenir activement.",
+  "页脚": "Pied de page",
+  "在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码": "Saisissez le nouveau pied de page ici, laissez vide pour utiliser le pied de page par défaut, prend en charge le code HTML.",
+  "设置页脚": "Définir le pied de page",
+  "新版本": "Nouvelle version",
+  "关闭": "Fermer",
+  "密码已重置并已复制到剪贴板:": "Le mot de passe a été réinitialisé et copié dans le presse-papiers : ",
+  "密码已复制到剪贴板:": "Le mot de passe a été copié dans le presse-papiers : ",
+  "密码重置确认": "Confirmation de la réinitialisation du mot de passe",
+  "邮箱地址": "Adresse e-mail",
+  "提交": "Soumettre",
+  "等待获取邮箱信息...": "En attente d'obtenir des informations par e-mail...",
+  "确认重置密码": "Confirmer la réinitialisation du mot de passe",
+  "无效的重置链接,请重新发起密码重置请求": "Lien de réinitialisation non valide, veuillez lancer une nouvelle demande de réinitialisation de mot de passe",
+  "请输入邮箱地址": "Veuillez saisir l'adresse e-mail",
+  "请稍后几秒重试": "Veuillez réessayer dans quelques secondes",
+  "正在检查用户环境": "Vérification de l'environnement utilisateur",
+  "重置邮件发送成功": "E-mail de réinitialisation envoyé avec succès",
+  "请检查邮箱": "Veuillez vérifier votre e-mail",
+  "密码重置": "Réinitialisation du mot de passe",
+  "令牌已重置并已复制到剪贴板": "Le jeton a été réinitialisé et copié dans le presse-papiers",
+  "邀请链接已复制到剪切板": "Le lien d'invitation a été copié dans le presse-papiers",
+  "微信账户绑定成功": "La liaison du compte WeChat a réussi",
+  "验证码发送成功": "Code de vérification envoyé avec succès",
+  "邮箱账户绑定成功": "La liaison du compte e-mail a réussi",
+  "注意": "Remarque",
+  "此处生成的令牌用于系统管理": "Le jeton généré ici est utilisé pour la gestion du système",
+  "而非用于请求 OpenAI 相关的服务": "Non pour demander des services liés à OpenAI",
+  "请知悉": "Veuillez en être conscient",
+  "更新个人信息": "Mettre à jour les informations personnelles",
+  "生成系统访问令牌": "Générer un jeton d'accès au système",
+  "复制邀请链接": "Copier le lien d'invitation",
+  "账号绑定": "Liaison de compte",
+  "绑定微信账号": "Lier le compte WeChat",
+  "微信扫码关注公众号": "Scannez le code QR avec WeChat pour suivre le compte officiel",
+  "输入": "Entrée",
+  "输出": "Sortie",
+  "验证码": "Code de vérification",
+  "获取验证码": "Obtenir le code de vérification",
+  "三分钟内有效": "Valide pendant trois minutes",
+  "绑定": "Lier",
+  "绑定 GitHub 账号": "Lier le compte GitHub",
+  "绑定邮箱地址": "Lier l'adresse e-mail",
+  "输入邮箱地址": "Saisir l'adresse e-mail",
+  "未使用": "Inutilisé",
+  "已使用": "Utilisé",
+  "操作成功完成": "Opération terminée avec succès",
+  "搜索兑换码的 ID 和名称": "Rechercher l'ID et le nom",
+  "额度": "Quota",
+  "创建时间": "Heure de création",
+  "兑换时间": "Heure d'échange",
+  "尚未兑换": "Pas encore échangé",
+  "已复制到剪贴板": "Copié dans le presse-papiers",
+  "无法复制到剪贴板": "Impossible de copier dans le presse-papiers",
+  "请手动复制": "Veuillez copier manuellement",
+  "已将兑换码填入搜索框": "Le code du bon a été renseigné dans le champ de recherche",
+  "复制": "Copier",
+  "添加新的兑换码": "Ajouter un nouveau bon",
+  "密码长度不得小于 8 位": "La longueur du mot de passe ne doit pas être inférieure à 8 caractères",
+  "两次输入的密码不一致": "Les deux mots de passe saisis ne correspondent pas",
+  "注册成功": "Inscription réussie",
+  "请稍后几秒重试,Turnstile 正在检查用户环境": "Veuillez réessayer dans quelques secondes, Turnstile vérifie l'environnement utilisateur",
+  "验证码发送成功,请检查你的邮箱": "Code de vérification envoyé avec succès, veuillez vérifier votre e-mail",
+  "新用户注册": "Inscription d'un nouvel utilisateur",
+  "输入用户名,最长 12 位": "Saisissez un nom d'utilisateur, jusqu'à 12 caractères",
+  "输入密码,最短 8 位,最长 20 位": "Saisissez un mot de passe, d'au moins 8 caractères et jusqu'à 20 caractères",
+  "输入验证码": "Saisir le code de vérification",
+  "已有账户": "Vous avez déjà un compte",
+  "点击登录": "Cliquez pour vous connecter",
+  "服务器地址": "Adresse du serveur",
+  "更新服务器地址": "Mettre à jour l'adresse du serveur",
+  "配置登录注册": "Configurer la connexion/l'inscription",
+  "允许通过密码进行登录": "Autoriser la connexion via mot de passe",
+  "允许通过密码进行注册": "Autoriser l'inscription via mot de passe",
+  "通过密码注册时需要进行邮箱验证": "La vérification par e-mail est requise lors de l'inscription via mot de passe",
+  "允许通过 GitHub 账户登录 & 注册": "Autoriser la connexion & l'inscription via le compte GitHub",
+  "允许通过微信登录 & 注册": "Autoriser la connexion & l'inscription via WeChat",
+  "允许新用户注册(此项为否时,新用户将无法以任何方式进行注册": "Autoriser l'inscription de nouveaux utilisateurs (si cette option est désactivée, les nouveaux utilisateurs ne pourront s'inscrire d'aucune manière)",
+  "启用 Turnstile 用户校验": "Activer la vérification utilisateur Turnstile",
+  "配置 SMTP": "Configurer SMTP",
+  "用以支持系统的邮件发送": "Pour prendre en charge l'envoi d'e-mails système",
+  "SMTP 服务器地址": "Adresse du serveur SMTP",
+  "例如:smtp.qq.com": "Par exemple : smtp.qq.com",
+  "SMTP 端口": "Port SMTP",
+  "默认: 587": "Par défaut : 587",
+  "SMTP 账户": "Compte SMTP",
+  "通常是邮箱地址": "Généralement une adresse e-mail",
+  "发送者邮箱": "E-mail de l'expéditeur",
+  "通常和邮箱地址保持一致": "Généralement identique à l'adresse e-mail",
+  "SMTP 访问凭证": "Informations d'identification d'accès SMTP",
+  "敏感信息不会发送到前端显示": "Les informations sensibles ne seront pas affichées dans le frontend",
+  "保存 SMTP 设置": "Enregistrer les paramètres SMTP",
+  "配置 GitHub OAuth App": "Configurer l'application GitHub OAuth",
+  "用以支持通过 GitHub 进行登录注册": "Pour prendre en charge la connexion & l'inscription via GitHub",
+  "点击此处": "cliquez ici",
+  "管理你的 GitHub OAuth App": "Gérer votre application GitHub OAuth",
+  "输入你注册的 GitHub OAuth APP 的 ID": "Saisissez votre ID d'application GitHub OAuth enregistré",
+  "保存 GitHub OAuth 设置": "Enregistrer les paramètres GitHub OAuth",
+  "配置 WeChat Server": "Configurer le serveur WeChat",
+  "用以支持通过微信进行登录注册": "Pour prendre en charge la connexion & l'inscription via WeChat",
+  "了解 WeChat Server": "En savoir plus sur le serveur WeChat",
+  "WeChat Server 访问凭证": "Informations d'identification d'accès au serveur WeChat",
+  "微信公众号二维码图片链接": "Lien de l'image du code QR du compte public WeChat",
+  "输入一个图片链接": "Saisir un lien d'image",
+  "保存 WeChat Server 设置": "Enregistrer les paramètres du serveur WeChat",
+  "配置 Turnstile": "Configurer Turnstile",
+  "用以支持用户校验": "Pour prendre en charge la vérification des utilisateurs",
+  "管理你的 Turnstile Sites,推荐选择 Invisible Widget Type": "Gérez vos sites Turnstile, nous vous recommandons de sélectionner le type de widget invisible",
+  "输入你注册的 Turnstile Site Key": "Saisissez votre clé de site Turnstile enregistrée",
+  "保存 Turnstile 设置": "Enregistrer les paramètres Turnstile",
+  "已过期": "Expiré",
+  "已耗尽": "Épuisé",
+  "搜索令牌的名称 ...": "Rechercher le nom du jeton...",
+  "已用额度": "Quota utilisé",
+  "剩余额度": "Quota restant",
+  "总额度": "Quota total",
+  "剩余额度/总额度": "Restant/Total",
+  "智能熔断": "Fallback intelligent",
+  "当前分组为 auto,会自动选择最优分组,当一个组不可用时自动降级到下一个组(熔断机制)": "Le groupe actuel est auto, il sélectionnera automatiquement le groupe optimal et passera automatiquement au groupe suivant lorsqu'un groupe n'est pas disponible (mécanisme de disjoncteur)",
+  "过期时间": "Date d'expiration",
+  "无": "Aucun",
+  "无限制": "Illimité",
+  "永不过期": "N'expire jamais",
+  "无法复制到剪贴板,请手动复制,已将令牌填入搜索框": "Impossible de copier dans le presse-papiers, veuillez copier manuellement, le jeton a été saisi dans le champ de recherche",
+  "删除令牌": "Supprimer le jeton",
+  "添加新的令牌": "Ajouter un nouveau jeton",
+  "普通用户": "Utilisateur normal",
+  "管理员": "Admin",
+  "超级管理员": "Super Admin",
+  "未知身份": "Identité inconnue",
+  "已激活": "Activé",
+  "已封禁": "Banni",
+  "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...": "Rechercher l'ID utilisateur, le nom d'utilisateur, le nom d'affichage et l'adresse e-mail...",
+  "用户名": "Nom d'utilisateur",
+  "用户角色": "Rôle de l'utilisateur",
+  "未绑定邮箱地址": "E-mail non lié",
+  "请求次数": "Nombre de demandes",
+  "提升": "Promouvoir",
+  "降级": "Rétrograder",
+  "删除用户": "Supprimer l'utilisateur",
+  "添加新的用户": "Ajouter un nouvel utilisateur",
+  "自定义": "Personnalisé",
+  "等价金额:": "Montant équivalent : ",
+  "未登录或登录已过期,请重新登录": "Non connecté ou la connexion a expiré, veuillez vous reconnecter",
+  "请求次数过多,请稍后再试": "Trop de demandes, veuillez réessayer plus tard",
+  "服务器内部错误,请联系管理员": "Erreur interne du serveur, veuillez contacter l'administrateur",
+  "本站仅作演示之用,无服务端": "Ce site est uniquement à des fins de démonstration, pas de serveur",
+  "超级管理员未设置充值链接!": "Le super administrateur n'a pas défini le lien de recharge !",
+  "错误:": "Erreur : ",
+  "新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面": "Nouvelle version disponible : ${data.version}, veuillez actualiser la page en utilisant le raccourci Maj + F5",
+  "无法正常连接至服务器": "Impossible de se connecter normalement au serveur",
+  "管理渠道": "Gérer les canaux",
+  "系统状况": "État du système",
+  "系统信息": "Informations système",
+  "系统信息总览": "Aperçu des informations système",
+  "版本": "Version",
+  "源码": "Code source",
+  "启动时间": "Heure de démarrage",
+  "系统配置": "Configuration du système",
+  "系统配置总览": "Aperçu de la configuration du système",
+  "邮箱验证": "Vérification de l'e-mail",
+  "未启用": "Non activé",
+  "GitHub 身份验证": "Authentification GitHub",
+  "微信身份验证": "Authentification WeChat",
+  "Turnstile 用户校验": "Vérification utilisateur Turnstile",
+  "创建新的渠道": "Créer un nouveau canal",
+  "是否自动禁用": "Désactiver automatiquement",
+  "仅当自动禁用开启时有效,关闭后不会自动禁用该渠道": "Efficace uniquement lorsque la désactivation automatique est activée, après la fermeture, le canal ne sera pas automatiquement désactivé",
+  "镜像": "Miroir",
+  "请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Veuillez saisir l'adresse du site miroir, le format est : https://domain.com, il peut être laissé vide, s'il est laissé vide, la valeur par défaut du canal sera utilisée",
+  "模型": "Modèle",
+  "请选择该通道所支持的模型": "Veuillez sélectionner le modèle pris en charge par le canal",
+  "填入基础模型": "Remplir le modèle de base",
+  "填入所有模型": "Remplir tous les modèles",
+  "清除所有模型": "Effacer tous les modèles",
+  "复制所有模型": "Copier tous les modèles",
+  "密钥": "Clé",
+  "请输入密钥": "Veuillez saisir la clé",
+  "批量创建": "Création par lots",
+  "更新渠道信息": "Mettre à jour les informations du canal",
+  "我的令牌": "Mes jetons",
+  "管理兑换码": "Gérer les codes d'échange",
+  "兑换码": "Code d'échange",
+  "管理用户": "Gérer les utilisateurs",
+  "额度明细": "Détails du quota",
+  "运营设置": "Paramètres de fonctionnement",
+  "其他设置": "Autres paramètres",
+  "项目仓库地址": "Adresse du référentiel du projet",
+  "可在设置页面设置关于内容,支持 HTML & Markdown": "Le contenu \"À propos\" peut être défini sur la page des paramètres, prenant en charge HTML & Markdown",
+  "由": "développé par",
+  "开发,基于": "basé sur",
+  "MIT 协议": "Licence MIT",
+  "充值额度": "Quota de recharge",
+  "获取兑换码": "Obtenir un code d'échange",
+  "一个月后过期": "Expire dans un mois",
+  "一天后过期": "Expire dans un jour",
+  "一小时后过期": "Expire dans une heure",
+  "一分钟后过期": "Expire dans une minute",
+  "创建新的令牌": "Créer un nouveau jeton",
+  "令牌分组,默认为用户的分组": "Groupe de jetons, par défaut le groupe de l'utilisateur",
+  "IP白名单": "Liste blanche d'adresses IP",
+  "令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制": "Le quota du jeton est uniquement utilisé pour limiter l'utilisation maximale du quota du jeton lui-même, et l'utilisation réelle est limitée par le quota restant du compte",
+  "无限额度": "Quota illimité",
+  "更新令牌信息": "Mettre à jour les informations du jeton",
+  "请输入充值码!": "Veuillez saisir le code de recharge !",
+  "请输入名称": "Veuillez saisir un nom",
+  "请输入密钥,一行一个": "Veuillez saisir la clé, une par ligne",
+  "请输入额度": "Veuillez saisir le quota",
+  "令牌创建成功": "Jeton créé avec succès",
+  "令牌更新成功": "Jeton mis à jour avec succès",
+  "充值成功!": "Recharge réussie !",
+  "更新用户信息": "Mettre à jour les informations de l'utilisateur",
+  "请输入新的用户名": "Veuillez saisir un nouveau nom d'utilisateur",
+  "请输入新的密码": "Veuillez saisir un nouveau mot de passe",
+  "显示名称": "Nom d'affichage",
+  "请输入新的显示名称": "Veuillez saisir un nouveau nom d'affichage",
+  "已绑定的 GITHUB 账户": "Compte GitHub lié",
+  "已绑定的 WECHAT 账户": "Compte WeChat lié",
+  "已绑定的 EMAIL 账户": "Compte EMAIL lié",
+  "已绑定的 TELEGRAM 账户": "Compte Telegram lié",
+  "此项只读,要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Cet élément est en lecture seule. Les utilisateurs doivent se lier via le bouton de liaison correspondant sur la page des paramètres personnels et ne peuvent pas être modifiés directement",
+  "用户信息更新成功!": "Informations utilisateur mises à jour avec succès !",
+  "使用明细(总消耗额度:{renderQuota(stat.quota)})": "Détails d'utilisation (Quota de consommation total : {renderQuota(stat.quota)})",
+  "用户名称": "Nom d'utilisateur",
+  "令牌名称": "Nom du jeton",
+  "留空则查询全部用户": "Laissez vide pour interroger tous les utilisateurs",
+  "留空则查询全部令牌": "Laissez vide pour interroger tous les jetons",
+  "模型名称": "Nom du modèle",
+  "留空则查询全部模型": "Laissez vide pour interroger tous les modèles",
+  "起始时间": "Heure de début",
+  "结束时间": "Heure de fin",
+  "查询": "Requête",
+  "提示": "Invite",
+  "补全": "Achèvement",
+  "消耗额度": "Quota utilisé",
+  "渠道不存在:%d": "Le canal n'existe pas : %d",
+  "数据库一致性已被破坏,请联系管理员": "La cohérence de la base de données a été rompue, veuillez contacter l'administrateur",
+  "使用近似的方式估算 token 数以减少计算量": "Estimer le nombre de jetons de manière approximative pour réduire la charge de calcul",
+  "请填写ChannelName和ChannelKey!": "Veuillez remplir le nom du canal et la clé du canal !",
+  "请至少选择一个Model!": "Veuillez sélectionner au moins un modèle !",
+  "加载关于内容失败": "Échec du chargement du contenu \"À propos\"",
+  "用户账户创建成功!": "Compte utilisateur créé avec succès !",
+  "生成数量": "Générer la quantité",
+  "请输入生成数量": "Veuillez saisir la quantité à générer",
+  "创建新用户账户": "Créer un nouveau compte utilisateur",
+  "渠道更新成功!": "Canal mis à jour avec succès !",
+  "渠道创建成功!": "Canal créé avec succès !",
+  "请选择分组": "Veuillez sélectionner un groupe",
+  "更新兑换码信息": "Mettre à jour les informations du code d'échange",
+  "创建新的兑换码": "Créer un nouveau code d'échange",
+  "未找到所请求的页面": "La page demandée n'a pas été trouvée",
+  "过期时间格式错误!": "Erreur de format de la date d'expiration !",
+  "过期时间不能早于当前时间!": "La date d'expiration ne peut pas être antérieure à l'heure actuelle !",
+  "请输入过期时间,格式为 yyyy-MM-dd HH:mm:ss,-1 表示无限制": "Veuillez saisir la date d'expiration, le format est aaaa-MM-jj HH:mm:ss, -1 signifie sans limite",
+  "此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:": "Ceci est facultatif, c'est un texte JSON, la clé est le nom du modèle demandé par l'utilisateur, et la valeur est le nom du modèle à remplacer, par exemple :",
+  "此项可选,输入镜像站地址,格式为:": "Ceci est facultatif, saisissez l'adresse du site miroir, le format est :",
+  "模型映射": "Mappage de modèles",
+  "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Veuillez saisir la version de l'API par défaut, par exemple : 2023-03-15-preview, cette configuration peut être remplacée par les paramètres de requête réels",
+  "默认": "Par défaut",
+  "图片演示": "Démonstration d'image",
+  "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Notez que le point dans le nom du modèle demandé par le système sera supprimé, par exemple : gpt-4.1 sera demandé comme gpt-41, donc lors du déploiement sur Azure, le nom du modèle de déploiement doit être manuellement changé en gpt-41",
+  "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "Après le 10 mai 2025, les canaux ajoutés n'ont plus besoin de supprimer le point dans le nom du modèle lors du déploiement",
+  "模型映射必须是合法的 JSON 格式!": "Le mappage de modèles doit être au format JSON valide !",
+  "取消": "Annuler",
+  "重置": "Réinitialiser",
+  "请输入新的剩余额度": "Veuillez saisir le nouveau quota restant",
+  "请输入单个兑换码中包含的额度": "Veuillez saisir le quota inclus dans un seul code d'échange",
+  "请输入用户名": "Veuillez saisir un nom d'utilisateur",
+  "请输入显示名称": "Veuillez saisir un nom d'affichage",
+  "请输入密码": "Veuillez saisir un mot de passe",
+  "注意,模型部署名称必须和模型名称保持一致": "Notez que le nom de déploiement du modèle doit être cohérent avec le nom du modèle",
+  "请输入 AZURE_OPENAI_ENDPOINT": "Veuillez saisir AZURE_OPENAI_ENDPOINT",
+  "请输入自定义渠道的 Base URL": "Veuillez saisir l'URL de base du canal personnalisé",
+  "Homepage URL 填": "Remplir l'URL de la page d'accueil",
+  "Authorization callback URL 填": "Remplir l'URL de rappel d'autorisation",
+  "请为通道命名": "Veuillez nommer le canal",
+  "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "Ceci est facultatif, utilisé pour modifier le nom du modèle dans le corps de la requête, c'est une chaîne JSON, la clé est le nom du modèle dans la requête, et la valeur est le nom du modèle à remplacer, par exemple :",
+  "模型重定向": "Redirection de modèle",
+  "请输入渠道对应的鉴权密钥": "Veuillez saisir la clé d'authentification correspondant au canal",
+  "注意,": "Notez que, ",
+  ",图片演示。": "démonstration d'image connexe.",
+  "令牌创建成功,请在列表页面点击复制获取令牌!": "Jeton créé avec succès, veuillez cliquer sur copier sur la page de liste pour obtenir le jeton !",
+  "代理": "Proxy",
+  "此项可选,用于通过自定义API地址来进行 API 调用,请输入API地址,格式为:https://domain.com": "Ceci est facultatif, utilisé pour effectuer des appels d'API via le site proxy, veuillez saisir l'adresse du site proxy, le format est : https://domain.com",
+  "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "L'annulation de la connexion par mot de passe empêchera tous les utilisateurs (y compris les administrateurs) qui n'ont pas lié d'autres méthodes de connexion de se connecter via un mot de passe, confirmer l'annulation ?",
+  "按照如下格式输入:": "Saisir dans le format suivant :",
+  "模型版本": "Version du modèle",
+  "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Veuillez saisir la version du modèle Starfire, notez qu'il s'agit du numéro de version dans l'adresse de l'interface, par exemple : v2.1",
+  "点击查看": "cliquez pour voir",
+  "请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!": "Veuillez vous assurer que le modèle gpt-35-turbo a été créé sur Azure et que l'apiVersion a été correctement renseignée !",
+  "建议收藏所有地址,以防失联。": "Il est recommandé de mettre en signet toutes les adresses pour éviter de perdre le contact.",
+  "无法正常请求API的用户,请联系管理员。": "Pour les utilisateurs qui ne peuvent pas demander l'API normalement, veuillez contacter l'administrateur.",
+  "温馨提示": "Conseils aimables",
+  "获取API URL列表时发生错误,请稍后重试。": "Une erreur s'est produite lors de la récupération de la liste des URL de l'API, veuillez réessayer plus tard.",
+  ",时间:": ",heure:",
+  "已用/剩余": "Utilisé/Restant",
+  ",点击更新": ", cliquez sur Mettre à jour",
+  "确定是否要清空此渠道记录额度?": "Êtes-vous sûr de vouloir effacer le quota d'enregistrement de ce canal ?",
+  "此修改将不可逆": "Cette modification sera irréversible",
+  "优先级": "Priorité",
+  "权重": "Poids",
+  "测试操作项目组": "Équipe de projet d'opération de test",
+  "确定是否要删除此渠道?": "Êtes-vous sûr de vouloir supprimer ce canal ?",
+  "确定是否要复制此渠道?": "Êtes-vous sûr de vouloir copier ce canal ?",
+  "复制渠道的所有信息": "Copier toutes les informations d'un canal",
+  "展开操作": "Développer l'opération",
+  "_复制": "_copier",
+  "渠道未找到,请刷新页面后重试。": "Canal non trouvé, veuillez actualiser la page et réessayer.",
+  "渠道复制成功": "Copie de canal réussie",
+  "渠道复制失败: ": "Échec de la copie du canal :",
+  "已成功开始测试所有通道,请刷新页面查看结果。": "Le test de tous les canaux a démarré avec succès, veuillez actualiser la page pour voir les résultats.",
+  "请先选择要删除的通道!": "Veuillez d'abord sélectionner le canal que vous souhaitez supprimer !",
+  "搜索渠道关键词": "Rechercher des mots-clés de canal",
+  "模型关键字": "mot-clé du modèle",
+  "选择分组": "Sélectionner un groupe",
+  "使用ID排序": "Trier par ID",
+  "是否用ID排序": "Trier par ID",
+  "确定?": "Sûr ?",
+  "确定是否要删除禁用通道?": "Êtes-vous sûr de vouloir supprimer le canal désactivé ?",
+  "开启批量操作": "Activer la sélection par lots",
+  "是否开启批量操作": "Activer la sélection par lots",
+  "确定是否要删除所选通道?": "Êtes-vous sûr de vouloir supprimer les canaux sélectionnés ?",
+  "确定是否要修复数据库一致性?": "Êtes-vous sûr de vouloir réparer la cohérence de la base de données ?",
+  "进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "Lors de cette opération, cela peut entraîner des erreurs d'accès au canal. Veuillez ne l'utiliser que lorsqu'il y a un problème avec la base de données.",
+  "当前没有可用的启用令牌,请确认是否有令牌处于启用状态!": "Aucun jeton d'activation n'est actuellement disponible, veuillez confirmer si l'un d'entre eux est activé !",
+  "Midjourney日志": "Midjourney",
+  "模型列表": "Liste des modèles",
+  "常见问题": "FAQ",
+  "免费体验": "Essai gratuit",
+  "新用户注册赠送$": "Gratuit $ pour l'inscription d'un nouvel utilisateur",
+  "测试金额": "Montant du test",
+  "请稍后几秒重试,Turnstile 正在检查用户环境!": "Veuillez réessayer dans quelques secondes, Turnstile vérifie l'environnement utilisateur !",
+  "您正在使用默认密码!": "Vous utilisez le mot de passe par défaut !",
+  "请立刻修改默认密码!": "Veuillez changer immédiatement le mot de passe par défaut !",
+  "请输入用户名和密码!": "Veuillez saisir le nom d'utilisateur et le mot de passe !",
+  "用户名或邮箱": "Nom d'utilisateur ou e-mail",
+  "微信扫码登录": "Scanner le code WeChat pour vous connecter",
+  "刷新成功": "Actualisation réussie",
+  "刷新失败": "Échec de l'actualisation",
+  "用时/首字": "Temps/premier mot",
+  "重试": "Réessayer",
+  "用户信息": "Informations utilisateur",
+  "无法复制到剪贴板,请手动复制": "Impossible de copier dans le presse-papiers, veuillez copier manuellement",
+  "消费": "Consommer",
+  "管理": "Gérer",
+  "系统": "Système",
+  "用时": "temps",
+  "首字时间": "Heure du premier mot",
+  "是否流式": "Flux ou non",
+  "非流": "pas de flux",
+  "渠道 ID": "ID du canal",
+  "用户ID": "ID de l'utilisateur",
+  "花费": "Dépenser",
+  "列设置": "Paramètres de colonne",
+  "补偿": "compenser",
+  "错误": "erreur",
+  "未知": "inconnu",
+  "全选": "Tout sélectionner",
+  "组名必须唯一": "Le nom du groupe doit être unique",
+  "解析 JSON 出错:": "Erreur d'analyse JSON :",
+  "解析 GroupModel 时发生错误: ": "Une erreur s'est produite lors de l'analyse de GroupModel :",
+  "GroupModel 未定义,无法更新分组": "GroupModel n'est pas défini, impossible de mettre à jour le regroupement",
+  "重置成功": "Réinitialisation réussie",
+  "加载数据出错:": "Erreur de chargement des données :",
+  "加载数据时发生错误: ": "Une erreur s'est produite lors du chargement des données :",
+  "部分保存失败,请重试": "Échec de l'enregistrement partiel, veuillez réessayer",
+  "请检查输入": "Veuillez vérifier votre saisie",
+  "如何区分不同分组不同模型的价格:供参考的配置方式": "Comment distinguer les prix des différents modèles dans différents groupes : méthode de configuration pour référence",
+  "获取价格顺序": "Obtenir l'ordre des prix",
+  "确定同步远程数据吗?": "Êtes-vous sûr de vouloir synchroniser les données distantes ?",
+  "此修改将不可逆!建议同步前先备份自己的设置!": "Cette modification sera irréversible ! Il est recommandé de sauvegarder vos paramètres avant la synchronisation !",
+  "模型固定价格(按次计费模型用)": "Prix fixe du modèle (pour les modèles de paiement à la séance)",
+  "模型倍率(按量计费模型用)": "Grossissement du modèle (pour le modèle de paiement à l'utilisation)",
+  "为一个 JSON 文本,键为模型名称,值为倍率": "est un texte JSON, la clé est le nom du modèle et la valeur est le grossissement",
+  "隐藏": "Masquer",
+  "分组名称": "Nom du groupe",
+  "提交结果": "Résultats",
+  "模式": "Mode",
+  "任务状态": "Statut",
+  "耗时": "Temps consommé",
+  "结果图片": "Résultat",
+  "失败原因": "Raison de l'échec",
+  "全部": "Tous",
+  "成功": "Succès",
+  "未启动": "Pas de démarrage",
+  "队列中": "En file d'attente",
+  "窗口等待": "attente de la fenêtre",
+  "失败": "Échoué",
+  "绘图": "Dessin",
+  "放大": "Upscalers",
+  "微妙放大": "Upscale (Subtil)",
+  "创造放大": "Upscale (Créatif)",
+  "强变换": "Faible variation",
+  "弱变换": "Forte variation",
+  "图生文": "Décrire",
+  "图混合": "Mélanger",
+  "重绘": "Varier",
+  "局部重绘-提交": "Varier la région",
+  "自定义变焦-提交": "Zoom personnalisé-Soumettre",
+  "窗口处理": "gestion des fenêtres",
+  "缩词后生图": "diagramme épigénétique de l'abréviation",
+  "图生文按钮生图": "Bouton image et texte",
+  "任务 ID": "ID de la tâche",
+  "速度模式": "mode vitesse",
+  "错误:未登录或登录已过期,请重新登录!": "Erreur : Non connecté ou votre connexion a expiré, veuillez vous reconnecter !",
+  "错误:请求次数过多,请稍后再试!": "Erreur : Trop de demandes, veuillez réessayer plus tard !",
+  "错误:服务器内部错误,请联系管理员!": "Erreur : Erreur interne du serveur, veuillez contacter l'administrateur !",
+  "本站仅作演示之用,无服务端!": "Ce site est uniquement à des fins de démonstration, pas de serveur !",
+  "已用额度:": "Montant utilisé :",
+  "请求次数:": "Nombre de demandes :",
+  "平移": "Panoramique",
+  "上传文件": "Télécharger",
+  "图生文后生图": "Les images donnent naissance à du texte et plus tard à des images",
+  "已提交": "Soumis",
+  "重复提交": "Soumission en double",
+  "未提交": "Non soumis",
+  "缩词": "Raccourcir",
+  "变焦": "zoom",
+  "按次计费": "Paiement à la séance",
+  "按量计费": "Paiement à l'utilisation",
+  "标签": "Étiquette",
+  "人民币": "RMB",
+  "说明": "illustrer",
+  "可用性": "Disponibilité",
+  "数据加载失败": "Échec du chargement des données",
+  "发生错误,请重试": "Une erreur s'est produite, veuillez réessayer",
+  "本站汇率1美金=": "Le taux de change de ce site est de 1 USD =",
+  "模糊搜索": "recherche floue",
+  "选择标签": "Sélectionner une étiquette",
+  "令牌分组": "Regroupement de jetons",
+  "隐": "caché",
+  "本站当前已启用模型": "Le modèle est actuellement activé sur ce site",
+  "个": " individuel",
+  "倍率是本站的计算方式,不同模型有着不同的倍率,并非官方价格的多少倍,请务必知晓。": "Le grossissement est la méthode de calcul de ce site Web. Différents modèles ont des grossissements différents, qui ne sont pas des multiples du prix officiel. Assurez-vous de le savoir.",
+  "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库<br/>Claude()Claude官方格式请求": "Veuillez utiliser la méthode OpenAI pour demander tous les modèles de discussion de chaque usine et prendre en charge la bibliothèque officielle OpenAI<br/>Demande de format officiel Claude()Claude",
+  "分组说明": "Description du groupe",
+  "倍率是为了方便换算不同价格的模型": "Le grossissement sert à faciliter la conversion de modèles à des prix différents.",
+  "点击查看倍率说明": "Cliquez pour voir la description du grossissement",
+  "显": "afficher",
+  "当前分组可用": "Disponible dans le groupe actuel",
+  "当前分组不可用": "Le groupe actuel n'est pas disponible",
+  "提示:": "entrée :",
+  "输入:": "entrée :",
+  "补全:": "sortie :",
+  "输出:": "sortie :",
+  "图片输出:": "Sortie d'image :",
+  "模型价格:": "Prix du modèle :",
+  "模型:": "Modèle :",
+  "分组:": "Regroupement :",
+  "最终价格": "prix final",
+  "计费类型": "Type de facturation",
+  "美元": "Dollar",
+  "倍率": "Ratio",
+  "常见问题不是合法的 JSON 字符串": "La FAQ n'est pas une chaîne JSON valide",
+  "常见问题更新失败": "Échec de la mise à jour de la FAQ",
+  "活动内容已更新": "Le contenu de l'événement a été mis à jour",
+  "活动内容更新失败": "Échec de la mise à jour du contenu de l'événement",
+  "页脚内容已更新": "Contenu du pied de page mis à jour",
+  "页脚内容更新失败": "Échec de la mise à jour du contenu du pied de page",
+  "Logo 图片地址": "Adresse de l'image du logo",
+  "在此输入图片地址": "Saisir l'adresse de l'image ici",
+  "在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。": "Saisissez le contenu de la page d'accueil ici, prend en charge Markdown",
+  "令牌分组说明": "Description du regroupement de jetons",
+  "在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。": "Saisissez le nouveau contenu \"À propos\" ici, prend en charge Markdown",
+  "API地址列表": "Liste d'adresses API",
+  "在此输入新的常见问题,json格式;键为问题,值为答案。": "Saisissez une nouvelle FAQ ici au format json ; la clé est la question et la valeur est la réponse.",
+  "活动内容": "Contenu de l'activité",
+  "在此输入新的活动内容。": "Saisissez le nouveau contenu de l'événement ici.",
+  "总计": "Total",
+  "无数据": "Aucune donnée",
+  "小时": "Heure",
+  "新密码": "Nouveau mot de passe",
+  "重置邮件发送成功,请检查邮箱!": "L'e-mail de réinitialisation a été envoyé avec succès, veuillez vérifier votre e-mail !",
+  "输入你的账户名{{username}}以确认删除": "Saisissez votre nom de compte{{username}}pour confirmer la suppression",
+  "账户已删除!": "Le compte a été supprimé !",
+  "微信账户绑定成功!": "Compte WeChat lié avec succès !",
+  "两次输入的密码不一致!": "Les mots de passe saisis deux fois sont incohérents !",
+  "密码修改成功!": "Mot de passe changé avec succès !",
+  "划转金额最低为": "Le montant minimum du virement est de",
+  "请输入邮箱!": "Veuillez saisir votre e-mail !",
+  "验证码发送成功,请检查邮箱!": "Le code de vérification a été envoyé avec succès, veuillez vérifier votre e-mail !",
+  "请输入邮箱验证码!": "Veuillez saisir le code de vérification de l'e-mail !",
+  "请输入要划转的数量": "Veuillez saisir le montant à transférer",
+  "当前余额": "Solde actuel",
+  "单独并发限制": "Limites de concurrence individuelles",
+  "未设置单独并发限制": "Aucune limite de concurrence individuelle n'est définie",
+  "无效的用户单独并发限制数据": "Données de limite de concurrence individuelle d'utilisateur non valides",
+  "未绑定": "Non lié",
+  "修改绑定": "Modifier la liaison",
+  "确认新密码": "Confirmer le nouveau mot de passe",
+  "历史消耗": "Consommation",
+  "查看": "Vérifier",
+  "修改密码": "Changer le mot de passe",
+  "删除个人账户": "Supprimer le compte personnel",
+  "已绑定": "Lié",
+  "获取二维码失败": "Échec de l'obtention du code QR",
+  "获取当前设置失败": "Échec de l'obtention des paramètres actuels",
+  "设置已更新": "Paramètres mis à jour",
+  "更新设置失败": "Échec de la mise à jour des paramètres",
+  "确认解绑": "Confirmer la dissociation",
+  "您确定要解绑WxPusher吗?": "Êtes-vous sûr de vouloir dissocier WxPusher ?",
+  "解绑失败": "Échec de la dissociation",
+  "订阅事件": "S'abonner aux événements",
+  "通知方式": "Méthode de notification",
+  "留空将通知到账号邮箱": "Laissez ce champ vide pour être averti par e-mail sur le compte",
+  "查看接入文档": "Afficher la documentation d'accès",
+  "企业微信机器人Key": "Clé du robot WeChat d'entreprise",
+  "您已绑定WxPusher,可以点击下方解绑": "Vous avez lié WxPusher, vous pouvez cliquer ci-dessous pour le délier",
+  "请扫描二维码绑定WxPusher": "Veuillez scanner le code QR pour lier WxPusher",
+  "预警额度(需订阅事件)": "Quota d'alerte (nécessite de s'abonner aux événements)",
+  " 时,将收到预警邮件(2小时最多1次)": "Quand, vous recevrez un e-mail d'alerte précoce (maximum une fois toutes les 2 heures)",
+  "兑换人ID": "ID du demandeur",
+  "确定是否要删除此兑换码?": "Êtes-vous sûr de vouloir supprimer ce code d'échange ?",
+  "已复制到剪贴板!": "Copié dans le presse-papiers !",
+  "搜索关键字": "Rechercher des mots-clés",
+  "关键字(id或者名称)": "Mot-clé (id ou nom)",
+  "复制所选兑换码": "Copier le code d'échange sélectionné",
+  "请至少选择一个兑换码!": "Veuillez sélectionner au moins un code d'échange !",
+  "密码长度不得小于 8 位!": "Le mot de passe doit comporter au moins 8 caractères !",
+  "注册成功!": "Inscription réussie !",
+  "验证码发送成功,请检查你的邮箱!": "Le code de vérification a été envoyé avec succès, veuillez vérifier votre e-mail !",
+  "确认密码": "Confirmer le mot de passe",
+  "邀请码": "Code d'invitation",
+  "输入邀请码": "Saisir le code d'invitation",
+  "账户": "Compte",
+  "邮箱": "E-mail",
+  "已有账户?": "Vous avez déjà un compte ?",
+  "创意任务": "Tâches",
+  "任务ID(点击查看详情)": "ID de la tâche (cliquez pour voir les détails)",
+  "进度": "calendrier",
+  "花费时间": "passer du temps",
+  "生成音乐": "générer de la musique",
+  "生成歌词": "Générer des paroles",
+  "歌曲拼接": "épissage de chansons",
+  "上传歌曲": "Télécharger des chansons",
+  "生成视频": "Générer une vidéo",
+  "扩展视频": "Vidéo étendue",
+  "获取无水印": "Obtenir sans filigrane",
+  "生成图片": "Générer des images",
+  "可灵": "Kling",
+  "即梦": "Jimeng",
+  "正在提交": "Envoi en cours",
+  "执行中": "en cours de traitement",
+  "平台": "plateforme",
+  "排队中": "En file d'attente",
+  "已启用:限制模型": "Activé : modèle restreint",
+  "AMA 问天": "AMA Wentian",
+  "项目操作按钮组": "Groupe de boutons d'action du projet",
+  "AMA 问天(BotGem)": "AMA Wentian (BotGem)",
+  "确定是否要删除此令牌?": "Êtes-vous sûr de vouloir supprimer ce jeton ?",
+  "管理员未设置聊天链接": "L'administrateur n'a pas configuré de lien de discussion",
+  "复制所选令牌": "Copier le jeton sélectionné",
+  "请至少选择一个令牌!": "Veuillez sélectionner au moins un jeton !",
+  "管理员未设置查询页链接": "L'administrateur n'a pas défini le lien de la page de requête",
+  "批量删除令牌": "Supprimer le jeton par lots",
+  "确定要删除所选的 {{count}} 个令牌吗?": "Êtes-vous sûr de vouloir supprimer les {{count}} jetons sélectionnés ?",
+  "删除所选令牌": "Supprimer le jeton sélectionné",
+  "请先选择要删除的令牌!": "Veuillez sélectionner le jeton à supprimer !",
+  "已删除 {{count}} 个令牌!": "Supprimé {{count}} jetons !",
+  "删除失败": "Échec de la suppression",
+  "复制令牌": "Copier le jeton",
+  "请选择你的复制方式": "Veuillez sélectionner votre méthode de copie",
+  "名称+密钥": "Nom + clé",
+  "仅密钥": "Clé uniquement",
+  "查看API地址": "Afficher l'adresse de l'API",
+  "打开查询页": "Ouvrir la page de requête",
+  "时间(仅显示近3天)": "Heure (affiche uniquement les 3 derniers jours)",
+  "请输入兑换码!": "Veuillez saisir le code d'échange !",
+  "兑换成功!": "Échange réussi !",
+  "成功兑换额度:": "Montant de l'échange réussi :",
+  "请求失败": "Échec de la demande",
+  "管理员未开启在线充值!": "L'administrateur n'a pas activé la recharge en ligne !",
+  "充值数量不能小于": "Le montant de la recharge ne peut pas être inférieur à",
+  "管理员未开启Stripe在线充值!": "L'administrateur n'a pas activé la recharge en ligne Stripe !",
+  "当前充值1美金=": "Recharge actuelle = 1 USD =",
+  "请选择充值方式!": "Veuillez choisir un mode de recharge !",
+  "元": "CNY",
+  "充值记录": "Enregistrement de recharge",
+  "返利记录": "Enregistrement de remise",
+  "确定要充值 $": "Confirmer la recharge de $",
+  "微信/支付宝 实付金额:": "Montant du paiement réel WeChat/Alipay :",
+  "Stripe 实付金额:": "Montant du paiement réel Stripe :",
+  "允许在 Stripe 支付中输入促销码": "Autoriser la saisie de codes promotionnels lors du paiement Stripe",
+  "支付中...": "Paiement en cours",
+  "支付宝": "Alipay",
+  "收益统计": "Statistiques sur les revenus",
+  "待使用收益": "Produits à utiliser",
+  "邀请人数": "Nombre de personnes invitées",
+  "兑换码充值": "Recharge par code d'échange",
+  "奖励说明": "Description de la récompense",
+  "选择支付方式": "Sélectionner le mode de paiement",
+  "在找兑换码?": "Vous cherchez un code d'échange ? ",
+  "购买兑换码": "Acheter un code d'échange",
+  "账户统计": "Statistiques du compte",
+  "账户充值": "Recharge de compte",
+  "多种充值方式,安全便捷": "Plusieurs méthodes de recharge, sûres et pratiques",
+  "支付方式": "Mode de paiement",
+  "邀请奖励": "Récompense d'invitation",
+  "或输入自定义金额": "Ou saisir un montant personnalisé",
+  "选择充值额度": "Sélectionner le montant de la recharge",
+  "实付": "Paiement réel",
+  "快速方便的充值方式": "Méthode de recharge rapide et pratique",
+  "邀请好友获得额外奖励": "Invitez des amis pour obtenir des récompenses supplémentaires",
+  "邀请好友注册,好友充值后您可获得相应奖励": "Invitez des amis à s'inscrire et vous pourrez obtenir la récompense correspondante après que l'ami ait rechargé",
+  "通过划转功能将奖励额度转入到您的账户余额中": "Transférez le montant de la récompense sur le solde de votre compte via la fonction de virement",
+  "邀请的好友越多,获得的奖励越多": "Plus vous invitez d'amis, plus vous obtiendrez de récompenses",
+  "在线充值": "Recharge en ligne",
+  "充值数量,最低 ": "Quantité de recharge, minimum ",
+  "请选择充值金额": "Veuillez sélectionner le montant de la recharge",
+  "微信": "WeChat",
+  "邀请返利": "Remise sur invitation",
+  "总收益": "revenu total",
+  "邀请信息": "Informations sur l'invitation",
+  "代理加盟": "Agent à rejoindre",
+  "代理商信息": "Informations sur l'agent",
+  "分红记录": "Enregistrements de dividendes",
+  "提现记录": "Enregistrements de retrait",
+  "代理商管理": "Gestion des agents",
+  "自定义输入": "entrée personnalisée",
+  "加载token失败": "Échec du chargement du jeton",
+  "配置聊天": "Configurer la discussion",
+  "模型消耗分布": "Distribution de la consommation des modèles",
+  "模型调用次数占比": "Ratio d'appels de modèles",
+  "用户消耗分布": "Distribution de la consommation des utilisateurs",
+  "时间粒度": "Granularité temporelle",
+  "天": "jour",
+  "模型概览": "Aperçu du modèle",
+  "用户概览": "Aperçu de l'utilisateur",
+  "正在策划中": "En cours de planification",
+  "请求首页内容失败": "Échec de la demande de contenu de la page d'accueil",
+  "返回首页": "Retour à la page d'accueil",
+  "获取用户数据时发生错误,请稍后重试。": "Une erreur s'est produite lors de la récupération des données utilisateur, veuillez réessayer plus tard.",
+  "无额度": "Pas de limite",
+  "累计消费": "Consommation cumulée",
+  "累计请求": "Demandes cumulées",
+  "你好,": "Bonjour,",
+  "线路监控": "surveillance de ligne",
+  "查看全部": "Voir tout",
+  "异常": "Anormal",
+  "的未命名令牌": "jeton sans nom",
+  "令牌更新成功!": "Jeton mis à jour avec succès !",
+  "(origin) Discord原链接": "(origine) Lien original Discord",
+  "请选择过期时间": "Veuillez sélectionner une date d'expiration",
+  "数量": "quantité",
+  "请选择或输入创建令牌的数量": "Veuillez sélectionner ou saisir le nombre de jetons à créer",
+  "请选择渠道": "Veuillez sélectionner un canal",
+  "允许的IP,一行一个,不填写则不限制": "Adresses IP autorisées, une par ligne, non remplies signifie aucune restriction",
+  "IP黑名单": "Liste noire d'adresses IP",
+  "不允许的IP,一行一个": "Adresses IP non autorisées, une par ligne",
+  "请选择该渠道所支持的模型": "Veuillez sélectionner le modèle pris en charge par ce canal",
+  "次": "fois",
+  "达到限速报错内容": "Contenu d'erreur lorsque la limite de vitesse est atteinte",
+  "不填则使用默认报错": "Si ce champ n'est pas rempli, l'erreur par défaut sera signalée.",
+  "Midjouney 设置 (可选)": "Paramètres Midjourney (facultatif)",
+  "令牌纬度控制 Midjouney 配置,设置优先级:令牌 {": "La latitude du jeton contrôle la configuration de Midjourney, en définissant la priorité : jeton {",
+  "图片代理地址最好用自己的,本站绘图量大,公用代理地址可能有时网速不佳": "Il est préférable d'utiliser votre propre adresse de proxy d'image. Ce site a une grande quantité de dessins, et les adresses de proxy publiques peuvent parfois avoir une mauvaise vitesse de réseau.",
+  "【突发备用号池】用于应对高强度风控情况,当普通号池全部重试失败,任务进入备用号池执行并额外计费。": "Le [pool de numéros de secours soudain] est utilisé pour faire face aux situations de contrôle des risques de haute intensité. Lorsque toutes les tentatives dans le pool de numéros ordinaire échouent, la tâche sera exécutée dans le pool de numéros de secours et des frais supplémentaires seront facturés.",
+  "绘图模式": "Mode dessin",
+  "请选择模式": "Veuillez sélectionner le mode",
+  "图片代理方式": "Méthode d'agence d'images",
+  "用于替换 https://cdn.discordapp.com 的域名": "Le nom de domaine utilisé pour remplacer https://cdn.discordapp.com",
+  "一个月": "Un mois",
+  "一天": "Un jour",
+  "令牌渠道分组选择": "Sélection du regroupement de canaux de jetons",
+  "只可使用对应分组包含的模型。": "Seuls les modèles contenus dans le groupe correspondant peuvent être utilisés.",
+  "渠道分组": "Regroupement de canaux",
+  "安全设置(可选)": "Paramètres de sécurité (facultatif)",
+  "IP 限制": "Restrictions d'IP",
+  "模型限制": "Restrictions de modèle",
+  "秒": "Seconde",
+  "更新令牌后需等待几分钟生效": "Il faudra quelques minutes pour prendre effet après la mise à jour du jeton.",
+  "一小时": "Une heure",
+  "新建数量": "Nouvelle quantité",
+  "未设置": "Non défini",
+  "API文档": "Documentation de l'API",
+  "不是合法的 JSON 字符串": "N'est pas une chaîne JSON valide",
+  "个人中心": "Centre personnel",
+  "代理商": "Agent",
+  "备注": "Remarque",
+  "工作台": "Établi",
+  "已复制:": "Copié :",
+  "提交时间": "Heure de soumission",
+  "无法正常连接至服务器!": "Impossible de se connecter correctement au serveur !",
+  "无记录": "Pas d'enregistrement",
+  "日间模式": "mode jour",
+  "活动福利": "Avantages de l'activité",
+  "聊天/绘画": "Discuter/Dessiner",
+  "跟随系统": "Suivre le système",
+  "黑夜模式": "Mode sombre",
+  "管理员设置": "Admin",
+  "待更新": "À mettre à jour",
+  "支付中..": "Paiement en cours",
+  "查看图片": "Voir les images",
+  "并发限制": "Limite de concurrence",
+  "正常": "Normal",
+  "周期": "cycle",
+  "同步频率10-20分钟": "Fréquence de synchronisation 10-20 minutes",
+  "模型调用占比": "Proportion d'appels de modèles",
+  "次,平均每天": "fois, en moyenne par jour",
+  ",平均每天": ", en moyenne chaque jour",
+  "启用突发备用号池(建议勾选,极大降低故障率)": "Activer le pool de numéros de secours en rafale (il est recommandé de cocher cette case pour réduire considérablement le taux d'échec)",
+  "查看说明": "Voir les instructions",
+  "添加令牌": "Créer un jeton",
+  "IP限制": "Restrictions d'IP",
+  "令牌纬度控制 Midjouney 配置,设置优先级:令牌 > 路径参数 > 系统默认": "La latitude du jeton contrôle la configuration de Midjourney, en définissant la priorité : jeton > paramètre de chemin > système par défaut",
+  "启用速率限制": "Activer la limitation de débit",
+  "复制BaseURL": "Copier l'URL de base",
+  "总消耗额度": "Montant total de la consommation",
+  "近一分钟内消耗Token数": "Nombre de jetons consommés au cours de la dernière minute",
+  "近一分钟内消耗额度": "Quota consommé au cours de la dernière minute",
+  "近一分钟内请求次数": "Nombre de demandes au cours de la dernière minute",
+  "预估一天消耗量": "Consommation journalière estimée",
+  "模型固定价格:": "Prix fixe du modèle :",
+  "仅供参考,以实际扣费为准": "Pour référence uniquement, la déduction réelle prévaudra",
+  "导出CSV": "Exporter CSV",
+  "流": "flux",
+  "任务ID": "ID de la tâche",
+  "周": "semaine",
+  "总计:": "Total :",
+  "划转到余额": "Transférer au solde",
+  "可用额度": "Crédit disponible",
+  "邀请码:": "Code d'invitation :",
+  "最低": "le plus bas",
+  "划转额度": "Montant du virement",
+  "邀请链接": "Lien d'invitation",
+  "划转邀请额度": "Quota d'invitation de transfert",
+  "可用邀请额度": "Quota d'invitation disponible",
+  "更多优惠": "Plus d'offres",
+  "企业微信": "WeChat d'entreprise",
+  "点击解绑WxPusher": "Cliquez pour délier WxPusher",
+  "点击显示二维码": "Cliquez pour afficher le code QR",
+  "二维码已过期,点击重新获取": "Le code QR a expiré, cliquez pour l'obtenir à nouveau",
+  "邮件": "Courrier",
+  "个人信息": "Informations personnelles",
+  "余额不足预警": "Avertissement de solde insuffisant",
+  "促销活动通知": "Notification de promotion",
+  "修改密码、邮箱、微信等": "Changer le mot de passe, l'e-mail, WeChat, etc.",
+  "更多选项": "Plus d'options",
+  "模型调价通知": "Notification d'ajustement du prix du modèle",
+  "系统公告通知": "Notification d'annonce système",
+  "订阅管理": "Gestion des abonnements",
+  "防失联-定期通知": "Prévenir la perte de contact - notifications régulières",
+  "订阅事件后,当事件触发时,您将会收到相应的通知": "Après vous être abonné à l'événement, vous recevrez la notification correspondante lorsque l'événement sera déclenché.",
+  "当余额低于 ": "Lorsque le solde est inférieur à",
+  "保存": "enregistrer",
+  "计费说明": "Instructions de facturation",
+  "高稳定性": "Haute stabilité",
+  "没有账号请先": "Si vous n'avez pas de compte, veuillez",
+  "注册账号": "Créer un compte",
+  "第三方登录": "Connexion tierce",
+  "欢迎回来": "bon retour",
+  "忘记密码": "oublier le mot de passe",
+  "想起来了?": "Vous vous souvenez ?",
+  "退出": "Quitter",
+  "确定": "OK",
+  "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2[1]": "Veuillez saisir la version du modèle Spark, notez qu'il s'agit du numéro de version dans l'adresse de l'interface, par exemple : v2.1",
+  "等待中": "En attente",
+  "所有各厂聊天模型请统一使用OpenAI方式请求,支持OpenAI官方库": "Veuillez utiliser la méthode OpenAI pour demander tous les modèles de discussion de chaque usine et prendre en charge la bibliothèque officielle OpenAI.",
+  "实付金额:": "Montant du paiement réel : ",
+  "金额": "Montant",
+  "充值金额": "Montant de la recharge",
+  "易支付 实付金额:": "Montant du paiement réel Easy Pay :",
+  "微信扫码关注公众号,输入 ": "Scannez le code QR sur WeChat pour suivre le compte officiel et entrez ",
+  " 获取验证码(三分钟内有效)": "Obtenir le code de vérification (valide dans les trois minutes)",
+  "不可用模型": "Modèle indisponible",
+  "关": "fermer",
+  "加载首页内容失败": "Échec du chargement du contenu de la page d'accueil",
+  "打开聊天": "Ouvrir la discussion",
+  "新窗口打开": "Une nouvelle fenêtre s'ouvre",
+  "禁用(仍可为用户单独开启)": "Désactivé (peut toujours être activé individuellement pour les utilisateurs)",
+  "重新配置": "Reconfigurer",
+  "隐藏不可用模型": "Masquer les modèles indisponibles",
+  " 时,将收到预警通知(2小时最多1次)": "Quand, vous recevrez une notification d'alerte précoce (maximum une fois toutes les 2 heures)",
+  "在iframe中加载": "Charger dans un iframe",
+  "补全倍率": "Ratio d'achèvement",
+  "保存分组数据失败": "Échec de l'enregistrement des données du groupe",
+  "没有可用的使用信息": "Aucune information d'utilisation disponible",
+  "使用详情": "Détails d'utilisation",
+  "收起": "Réduire",
+  "计费详情": "Détails de facturation",
+  "提示Token": "Jeton d'astuce",
+  "补全Token": "Jeton complet",
+  "提示Token详情": "Détails du jeton d'invite",
+  "补全Token详情": "Détails du jeton complet",
+  "输出Token详情": "Détails du jeton de sortie",
+  "缓存Token": "Jeton de cache",
+  "内部缓存Token": "Jeton de cache interne",
+  "图像Token": "Jeton d'image",
+  "音频Token": "Jeton audio",
+  "开": "ouvert",
+  "推理Token": "Jeton de raisonnement",
+  "文本Token": "Jeton de texte",
+  "显示禁用渠道": "Afficher les canaux désactivés",
+  "输入Token详情": "Saisir les détails du jeton",
+  "输出Token": "Jeton de sortie",
+  "隐藏禁用渠道": "Masquer les canaux désactivés",
+  "今日不再提醒": "Plus de rappels aujourd'hui",
+  "平台/类型": "Plateforme/Type",
+  "平台和类型": "Plateformes et types",
+  "当前选择分组": "Groupe actuellement sélectionné",
+  "表情迁移": "migration d'expression",
+  "音频输入:": "Entrée audio :",
+  "音频输出:": "Sortie audio :",
+  "风格重绘": "repeindre le style",
+  "发送测试通知失败": "Échec de l'envoi de la notification de test",
+  "开始时间": "heure de début",
+  "当前所选分组不可用": "Le groupe actuellement sélectionné n'est pas disponible",
+  "接口凭证": "Informations d'identification de l'interface",
+  "文字输入": "Saisie de texte",
+  "文字输出": "sortie de texte",
+  "日志详情": "Détails du journal",
+  "未完成": "Non terminé",
+  "测试单个渠道操作项目组": "Tester un seul groupe de projet d'opération de canal",
+  "测试通知": "Notification de test",
+  "测试通知发送成功": "Notification de test envoyée avec succès",
+  "点击此处查看接入文档": "Cliquez ici pour voir la documentation d'accès",
+  "类型1": "Type 1",
+  "类型1 (Imagine)": "Type 1 (Imagine)",
+  "类型1价格": "Prix du type 1",
+  "类型2": "Type 2",
+  "类型2 (Upscale)": "Type 2 (Upscale)",
+  "类型2价格": "Prix du type 2",
+  "类型3价格": "Prix du type 3",
+  "计费过程": "Processus de mise en lots",
+  "语音输入": "Entrée vocale",
+  "语音输出": "Sortie vocale",
+  "请在右侧切换到可用分组": "Veuillez passer aux groupes disponibles à droite",
+  "请联系管理员~": "Veuillez contacter l'administrateur~",
+  "调用消费": "Consommation d'appels",
+  "质量": "qualité",
+  "速度": "vitesse",
+  "钉钉机器人Key": "Clé du robot DingTalk",
+  "需要@的用户手机号": "Besoin du numéro de téléphone portable de l'utilisateur @",
+  "(提示": "(indice",
+  "下载文件": "Télécharger le fichier",
+  "https...xxx.com.webhook": "",
+  "搜索渠道的 ID,名称和密钥 ": "",
+  "搜索用户的 ID,用户名,显示名称,以及邮箱地址 ": "",
+  "操作失败,重定向至登录界面中": "",
+  "支付中": "",
+  "等级": "grade",
+  "钉钉": "DingTalk",
+  "模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}": "Prix du modèle : ${{price}} * Ratio de groupe : {{ratio}} = ${{total}}",
+  "输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Invite : ${{price}} * {{ratio}} = ${{total}} / 1M de jetons",
+  "输出:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Achèvement : ${{price}} * {{ratio}} = ${{total}} / 1M de jetons",
+  "图片输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})": "Entrée d'image : ${{price}} * {{ratio}} = ${{total}} / 1M de jetons (Ratio d'image : {{imageRatio}})",
+  "音频输入:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens": "Invite audio : ${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M de jetons",
+  "音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}": "Invite audio {{input}} jetons / 1M de jetons * ${{price}} * {{audioRatio}} + Achèvement audio {{completion}} jetons / 1M de jetons * ${{price}} * {{audioRatio}} * {{audioCompRatio}}",
+  "音频输出:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens": "Achèvement audio : ${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M de jetons",
+  "输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Entrée {{nonImageInput}} jetons + Entrée d'image {{imageInput}} jetons * {{imageRatio}} / 1M de jetons * ${{price}} + Sortie {{completion}} jetons / 1M de jetons * ${{compPrice}} * Groupe {{ratio}} = ${{total}}",
+  "(文字 + 音频)* 分组倍率 {{ratio}} = ${{total}}": "(Texte + Audio) * Ratio de groupe {{ratio}} = ${{total}}",
+  "文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +": "Invite de texte {{input}} jetons / 1M de jetons * ${{price}} + Achèvement de texte {{completion}} jetons / 1M de jetons * ${{compPrice}} +",
+  "输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Invite {{input}} jetons / 1M de jetons * ${{price}} + Achèvement {{completion}} jetons / 1M de jetons * ${{compPrice}} * Groupe {{ratio}} = ${{total}}",
+  "价格:${{price}} * 分组:{{ratio}}": "Prix : ${{price}} * Groupe : {{ratio}}",
+  "模型: {{ratio}} * 分组: {{groupRatio}}": "Modèle : {{ratio}} * Groupe : {{groupRatio}}",
+  "统计额度": "Quota statistique",
+  "统计Tokens": "Jetons statistiques",
+  "统计次数": "Nombre de statistiques",
+  "平均RPM": "RPM moyen",
+  "平均TPM": "TPM moyen",
+  "消耗分布": "Distribution de la consommation",
+  "调用次数分布": "Distribution des appels de modèles",
+  "消耗趋势": "Tendance de la consommation",
+  "模型消耗趋势": "Tendance de la consommation des modèles",
+  "调用次数排行": "Classement des appels de modèles",
+  "模型调用次数排行": "Classement des appels de modèles",
+  "添加渠道": "Ajouter un canal",
+  "测试所有通道": "Tester tous les canaux",
+  "删除禁用通道": "Supprimer les canaux désactivés",
+  "修复数据库一致性": "Réparer la cohérence de la base de données",
+  "删除所选通道": "Supprimer les canaux sélectionnés",
+  "标签聚合模式": "Activer le mode étiquette",
+  "没有账户?": "Pas de compte ? ",
+  "请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Veuillez saisir AZURE_OPENAI_ENDPOINT, par exemple : https://docs-test-001.openai.azure.com",
+  "默认 API 版本": "Version de l'API par défaut",
+  "请输入默认 API 版本,例如:2025-04-01-preview": "Veuillez saisir la version de l'API par défaut, par exemple : 2025-04-01-preview.",
+  "请为渠道命名": "Veuillez nommer le canal",
+  "请选择可以使用该渠道的分组": "Veuillez sélectionner les groupes qui peuvent utiliser ce canal",
+  "请在系统设置页面编辑分组倍率以添加新的分组:": "Veuillez modifier les ratios de groupe dans les paramètres système pour ajouter de nouveaux groupes :",
+  "部署地区": "Région de déploiement",
+  "请输入部署地区,例如:us-central1\n支持使用模型映射格式": "Veuillez saisir la région de déploiement, par exemple : us-central1\nPrend en charge le format de mappage de modèle",
+  "填入模板": "Remplir le modèle",
+  "鉴权json": "JSON d'authentification",
+  "请输入鉴权json": "Veuillez saisir le JSON d'authentification",
+  "组织": "Organisation",
+  "组织,不填则为默认组织": "Organisation, par défaut si vide",
+  "请输入组织org-xxx": "Veuillez saisir l'organisation org-xxx",
+  "默认测试模型": "Modèle de test par défaut",
+  "不填则为模型列表第一个": "Premier modèle de la liste si vide",
+  "是否自动禁用(仅当自动禁用开启时有效),关闭后不会自动禁用该渠道": "Désactivation automatique (uniquement efficace lorsque la désactivation automatique est activée). Une fois désactivé, ce canal ne sera pas automatiquement désactivé",
+  "状态码复写": "Remplacement du code d'état",
+  "此项可选,用于复写返回的状态码,仅影响本地判断,不修改返回到上游的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:": "Facultatif, utilisé pour remplacer les codes d'état renvoyés, affecte uniquement le jugement local, ne modifie pas le code d'état renvoyé en amont, par exemple, réécrire l'erreur 400 du canal Claude en 500 (pour une nouvelle tentative). N'abusez pas de cette fonctionnalité. Exemple :",
+  "渠道标签": "Étiquette du canal",
+  "渠道优先级": "Priorité du canal",
+  "渠道权重": "Poids du canal",
+  "仅支持 OpenAI 接口格式": "Seul le format d'interface OpenAI est pris en charge",
+  "请填写密钥": "Veuillez saisir la clé",
+  "获取模型列表成功": "La liste des modèles a été récupérée avec succès",
+  "获取模型列表失败": "Échec de la récupération de la liste des modèles",
+  "请填写渠道名称和渠道密钥!": "Veuillez saisir le nom et la clé du canal !",
+  "请至少选择一个模型!": "Veuillez sélectionner au moins un modèle !",
+  "提交失败,请勿重复提交!": "Échec de la soumission, veuillez ne pas soumettre à plusieurs reprises !",
+  "某些模型已存在!": "Certains modèles existent déjà !",
+  "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "Si vous vous connectez à des projets de redirection One API ou New API en amont, veuillez utiliser le type OpenAI. N'utilisez pas ce type, sauf si vous savez ce que vous faites.",
+  "完整的 Base URL,支持变量{model}": "URL de base complète, prend en charge la variable {model}",
+  "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Veuillez saisir l'URL complète, par exemple : https://api.openai.com/v1/chat/completions",
+  "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Facultatif pour les appels d'API via une adresse d'API personnalisée, n'ajoutez pas /v1 et / à la fin",
+  "私有部署地址": "Adresse de déploiement privée",
+  "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Veuillez saisir l'adresse de déploiement privée, format : https://fastgpt.run/api/openapi",
+  "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Remarque : Pour les API non-Chat, assurez-vous de saisir l'adresse API correcte, sinon elle pourrait ne pas fonctionner",
+  "请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com": "Veuillez saisir le chemin avant /suno, généralement le domaine, par exemple : https://api.example.com",
+  "填入相关模型": "Remplir les modèles associés",
+  "新建渠道时,请求通过当前浏览器发出;编辑已有渠道,请求通过后端服务器发出": "Lors de la création d'un nouveau canal, les demandes sont envoyées via le navigateur actuel ; lors de la modification d'un canal existant, les demandes sont envoyées via le serveur principal",
+  "获取模型列表": "Obtenir la liste des modèles",
+  "填入": "Remplir",
+  "输入自定义模型名称": "Saisir un nom de modèle personnalisé",
+  "知识库 ID": "ID de la base de connaissances",
+  "请输入知识库 ID,例如:123456": "Veuillez saisir l'ID de la base de connaissances, par exemple : 123456",
+  "可选值": "Valeur facultative",
+  "你好": "Bonjour",
+  "你好,请问有什么可以帮助您的吗?": "Bonjour, comment puis-je vous aider ?",
+  "用户分组": "Votre groupe par défaut",
+  "每页条数": "Éléments par page",
+  "令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。": "Les jetons ne peuvent pas contrôler l'utilisation avec précision, uniquement pour un usage personnel, veuillez ne pas distribuer les jetons directement à d'autres.",
+  "添加兑换码": "Ajouter un code d'échange",
+  "复制所选兑换码到剪贴板": "Copier les codes d'échange sélectionnés dans le presse-papiers",
+  "新建兑换码": "Code",
+  "兑换码更新成功!": "Code d'échange mis à jour avec succès !",
+  "兑换码创建成功!": "Code d'échange créé avec succès !",
+  "兑换码创建成功": "Code d'échange créé",
+  "兑换码创建成功,是否下载兑换码?": "Code d'échange créé avec succès. Voulez-vous le télécharger ?",
+  "兑换码将以文本文件的形式下载,文件名为兑换码的名称。": "Le code d'échange sera téléchargé sous forme de fichier texte, le nom de fichier étant le nom du code d'échange.",
+  "模型价格": "Prix du modèle",
+  "按K显示单位": "Afficher en K",
+  "可用分组": "Groupes disponibles",
+  "您的默认分组为:{{group}},分组倍率为:{{ratio}}": "Votre groupe par défaut est : {{group}}, ratio de groupe : {{ratio}}",
+  "按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)": "Le coût du paiement à l'utilisation = Ratio de groupe × Ratio de modèle × (Nombre de jetons d'invite + Nombre de jetons d'achèvement × Ratio d'achèvement) / 500000 (Unité : USD)",
+  "模糊搜索模型名称": "Recherche floue de nom de modèle",
+  "您还未登陆,显示的价格为默认分组倍率: {{ratio}}": "Vous n'êtes pas connecté, le prix affiché est le ratio de groupe par défaut : {{ratio}}",
+  "你的分组无权使用该模型": "Votre groupe n'est pas autorisé à utiliser ce modèle",
+  "您的分组可以使用该模型": "Votre groupe peut utiliser ce modèle",
+  "当前查看的分组为:{{group}},倍率为:{{ratio}}": "Groupe actuel : {{group}}, ratio : {{ratio}}",
+  "添加用户": "Ajouter un utilisateur",
+  "角色": "Rôle",
+  "已绑定的 Telegram 账户": "Compte Telegram lié",
+  "新额度:": "Nouveau quota : ",
+  "需要添加的额度(支持负数)": "Besoin d'ajouter un quota (prend en charge les nombres négatifs)",
+  "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Lecture seule, paramètres personnels de l'utilisateur, et ne peut pas être modifié directement",
+  "请输入新的密码,最短 8 位": "Veuillez saisir un nouveau mot de passe, d'au moins 8 caractères",
+  "添加额度": "Ajouter un quota",
+  "以下信息不可修改": "Les informations suivantes ne peuvent pas être modifiées",
+  "充值确认": "Confirmation de la recharge",
+  "充值数量": "Quantité de recharge",
+  "实付金额": "Montant du paiement réel",
+  "是否确认充值?": "Confirmer la recharge ?",
+  "默认聊天页面链接": "Lien de la page de discussion par défaut",
+  "聊天页面 2 链接": "Lien de la page de discussion 2",
+  "失败重试次数": "Nombre de tentatives en cas d'échec",
+  "额度查询接口返回令牌额度而非用户额度": "Affiche le quota de jetons au lieu du quota utilisateur",
+  "默认折叠侧边栏": "Réduire la barre latérale par défaut",
+  "聊天链接功能已经弃用,请使用下方聊天设置功能": "La fonction de lien de discussion a été dépréciée, veuillez utiliser la fonction de paramètres de discussion ci-dessous",
+  "你似乎并没有修改什么": "Vous ne semblez rien avoir modifié",
+  "聊天设置": "Paramètres de discussion",
+  "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Vous devez définir tous les liens de discussion ci-dessus sur vide pour utiliser la fonction de paramètres de discussion ci-dessous",
+  "链接中的{key}将自动替换为sk-xxxx,{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "Le {key} dans le lien sera automatiquement remplacé par sk-xxxx, le {address} sera automatiquement remplacé par l'adresse du serveur dans les paramètres système, et la fin n'aura pas / et /v1",
+  "聊天配置": "Configuration de la discussion",
+  "保存聊天设置": "Enregistrer les paramètres de discussion",
+  "绘图设置": "Paramètres de dessin",
+  "启用绘图功能": "Activer la fonction de dessin",
+  "允许回调(会泄露服务器 IP 地址)": "Autoriser le rappel (divulguera l'adresse IP du serveur)",
+  "允许 AccountFilter 参数": "Autoriser le paramètre AccountFilter",
+  "开启之后将上游地址替换为服务器地址": "Après l'activation, l'adresse en amont sera remplacée par l'adresse du serveur",
+  "开启之后会清除用户提示词中的": "Après l'activation, l'invite de l'utilisateur sera effacée",
+  "检测必须等待绘图成功才能进行放大等操作": "La détection doit attendre que le dessin réussisse avant d'effectuer un zoom et d'autres opérations",
+  "保存绘图设置": "Enregistrer les paramètres de dessin",
+  "以及": "et",
+  "参数": "paramètre",
+  "屏蔽词过滤设置": "Paramètres de filtrage des mots sensibles",
+  "启用屏蔽词过滤功能": "Activer la fonction de filtrage des mots sensibles",
+  "启用 Prompt 检查": "Activer la vérification de l'invite",
+  "屏蔽词列表": "Liste des mots sensibles",
+  "一行一个屏蔽词,不需要符号分割": "Un mot sensible par ligne, aucun symbole n'est requis",
+  "保存屏蔽词过滤设置": "Enregistrer les paramètres de filtrage des mots sensibles",
+  "日志设置": "Paramètres du journal",
+  "日志记录时间": "Heure d'enregistrement du journal",
+  "请选择日志记录时间": "Veuillez sélectionner l'heure d'enregistrement du journal",
+  "清除历史日志": "Effacer les journaux historiques",
+  "条日志已清理!": "les journaux ont été effacés !",
+  "保存日志设置": "Enregistrer les paramètres du journal",
+  "数据看板设置": "Paramètres du tableau de bord des données",
+  "启用数据看板(实验性)": "Activer le tableau de bord des données (expérimental)",
+  "数据看板更新间隔": "Intervalle de mise à jour du tableau de bord des données",
+  "数据看板默认时间粒度": "Granularité temporelle par défaut du tableau de bord des données",
+  "保存数据看板设置": "Enregistrer les paramètres du tableau de bord des données",
+  "请选择最长响应时间": "Veuillez sélectionner le temps de réponse le plus long",
+  "成功时自动启用通道": "Activer le canal en cas de succès",
+  "分钟": "minutes",
+  "设置过短会影响数据库性能": "Un réglage trop court affectera les performances de la base de données",
+  "仅修改展示粒度,统计精确到小时": "Modifier uniquement la granularité d'affichage, statistiques précises à l'heure près",
+  "当运行通道全部测试时,超过此时间将自动禁用通道": "Lors de l'exécution de tous les tests de canaux, le canal sera automatiquement désactivé lorsque ce temps sera dépassé",
+  "设置公告": "Définir un avis",
+  "设置 Logo": "Définir un logo",
+  "设置首页内容": "Définir le contenu de la page d'accueil",
+  "设置关于": "Définir \"À propos\"",
+  "公告已更新": "Avis mis à jour",
+  "系统名称已更新": "Nom du système mis à jour",
+  "Logo 已更新": "Logo mis à jour",
+  "首页内容已更新": "Contenu de la page d'accueil mis à jour",
+  "关于已更新": "À propos mis à jour",
+  "模型测试": "test de modèle",
+  "当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。": "Le rappel Midjourney actuel n'est pas activé, certains projets peuvent ne pas être en mesure d'obtenir des résultats de dessin, qui peuvent être activés dans les paramètres de fonctionnement.",
+  "Telegram 身份验证": "Authentification Telegram",
+  "Linux DO 身份验证": "Authentification Linux DO",
+  "协议": "Licence",
+  "修改子渠道权重": "Modifier le poids du sous-canal",
+  "确定要修改所有子渠道权重为 ": "Confirmer la modification de tous les poids des sous-canaux en ",
+  " 吗?": " ?",
+  "修改子渠道优先级": "Modifier la priorité du sous-canal",
+  "确定要修改所有子渠道优先级为 ": "Confirmer la modification de toutes les priorités des sous-canaux en ",
+  "分组倍率设置": "Paramètres de ratio de groupe",
+  "用户可选分组": "Groupes sélectionnables par l'utilisateur",
+  "保存分组倍率设置": "Enregistrer les paramètres de ratio de groupe",
+  "模型倍率设置": "Paramètres de ratio de modèle",
+  "可视化倍率设置": "Paramètres de ratio de modèle visuel",
+  "确定重置模型倍率吗?": "Confirmer la réinitialisation du ratio de modèle ?",
+  "模型固定价格": "Prix du modèle par appel",
+  "模型补全倍率(仅对自定义模型有效)": "Ratio d'achèvement de modèle (uniquement efficace pour les modèles personnalisés)",
+  "保存模型倍率设置": "Enregistrer les paramètres de ratio de modèle",
+  "重置模型倍率": "Réinitialiser le ratio de modèle",
+  "一次调用消耗多少刀,优先级大于模型倍率": "Combien de USD coûte un appel, priorité sur le ratio de modèle",
+  "仅对自定义模型有效": "Uniquement efficace pour les modèles personnalisés",
+  "添加模型": "Ajouter un modèle",
+  "应用更改": "Appliquer les modifications",
+  "更多": "Développer plus",
+  "个模型": "modèles",
+  "可用模型": "Modèles disponibles",
+  "时间范围": "Plage de temps",
+  "批量设置标签": "Définir l'étiquette par lots",
+  "请输入要设置的标签名称": "Veuillez saisir le nom de l'étiquette à définir",
+  "请输入标签名称": "Veuillez saisir le nom de l'étiquette",
+  "支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Prise en charge de la recherche par ID utilisateur, nom d'utilisateur, nom d'affichage et adresse e-mail",
+  "已注销": "Déconnecté",
+  "自动禁用关键词": "Mots-clés de désactivation automatique",
+  "一行一个,不区分大小写": "Un mot-clé par ligne, insensible à la casse",
+  "当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "Lorsque le canal en amont renvoie une erreur contenant ces mots-clés (insensible à la casse), désactivez automatiquement le canal",
+  "请求并计费模型": "Modèle de demande et de facturation",
+  "实际模型": "Modèle réel",
+  "渠道信息": "Informations sur le canal",
+  "通知设置": "Paramètres de notification",
+  "Webhook地址": "URL du Webhook",
+  "请输入Webhook地址,例如: https://example.com/webhook": "Veuillez saisir l'URL du Webhook, par exemple : https://example.com/webhook",
+  "邮件通知": "Notification par e-mail",
+  "Webhook通知": "Notification par Webhook",
+  "接口凭证(可选)": "Informations d'identification de l'interface (facultatif)",
+  "密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性": "Le secret sera ajouté à l'en-tête de la requête en tant que jeton Bearer pour vérifier la légitimité de la requête webhook",
+  "Authorization: Bearer your-secret-key": "Autorisation : Bearer votre-clé-secrète",
+  "额度预警阈值": "Seuil d'avertissement de quota",
+  "当剩余额度低于此数值时,系统将通过选择的方式发送通知": "Lorsque le quota restant est inférieur à cette valeur, le système enverra une notification via la méthode sélectionnée",
+  "Webhook请求结构": "Structure de la requête Webhook",
+  "只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求": "Seul https est pris en charge, le système enverra une notification via POST, veuillez vous assurer que l'adresse peut recevoir des requêtes POST",
+  "通知邮箱": "E-mail de notification",
+  "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Définissez l'adresse e-mail pour recevoir les notifications d'avertissement de quota, si elle n'est pas définie, l'adresse e-mail liée au compte sera utilisée",
+  "留空则使用账号绑定的邮箱": "Si ce champ est laissé vide, l'adresse e-mail liée au compte sera utilisée",
+  "API地址": "URL de base",
+  "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "Pour les canaux officiels, le new-api a une adresse intégrée. Sauf s'il s'agit d'un site proxy tiers ou d'une adresse d'accès Azure spéciale, il n'est pas nécessaire de la remplir",
+  "渠道额外设置": "Paramètres supplémentaires du canal",
+  "强制格式化": "Forcer le format",
+  "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Forcer le formatage des réponses au format standard OpenAI (uniquement pour les types de canaux OpenAI)",
+  "思考内容转换": "Conversion du contenu de la pensée",
+  "将 reasoning_content 转换为 <think> 标签拼接到内容中": "Convertir reasoning_content en balises <think> et les ajouter au contenu",
+  "透传请求体": "Corps de transmission",
+  "启用请求体透传功能": "Activer la fonctionnalité de transmission du corps de la requête",
+  "代理地址": "Adresse du proxy",
+  "例如: socks5://user:pass@host:port": "par exemple : socks5://user:pass@host:port",
+  "用于配置网络代理,支持 socks5 协议": "Utilisé pour configurer le proxy réseau, prend en charge le protocole socks5",
+  "系统提示词": "Invite système",
+  "输入系统提示词,用户的系统提示词将优先于此设置": "Saisissez l'invite système, l'invite système de l'utilisateur aura la priorité sur ce paramètre",
+  "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "Priorité de l'utilisateur : si l'utilisateur spécifie une invite système dans la requête, le paramètre de l'utilisateur sera utilisé en premier",
+  "参数覆盖": "Remplacement des paramètres",
+  "模型请求速率限制": "Limite de débit de requête de modèle",
+  "启用用户模型请求速率限制(可能会影响高并发性能)": "Activer la limite de débit de requête de modèle utilisateur (peut affecter les performances à haute concurrence)",
+  "限制周期": "Période de limite",
+  "用户每周期最多请求次数": "Nombre maximal de requêtes utilisateur par période",
+  "用户每周期最多请求完成次数": "Nombre maximal de requêtes utilisateur réussies par période",
+  "包括失败请求的次数,0代表不限制": "Y compris les tentatives de requête échouées, 0 signifie aucune limite",
+  "频率限制的周期(分钟)": "Période de limitation de débit (minutes)",
+  "只包括请求成功的次数": "N'inclure que les tentatives de requête réussies",
+  "保存模型速率限制": "Enregistrer les paramètres de limite de débit de modèle",
+  "速率限制设置": "Paramètres de limitation de débit",
+  "获取启用模型失败:": "Échec de l'obtention des modèles activés :",
+  "获取启用模型失败": "Échec de l'obtention des modèles activés",
+  "JSON解析错误:": "Erreur d'analyse JSON :",
+  "保存失败:": "Échec de l'enregistrement :",
+  "输入模型倍率": "Saisir le ratio de modèle",
+  "输入补全倍率": "Saisir le ratio d'achèvement",
+  "请输入数字": "Veuillez saisir un nombre",
+  "模型名称已存在": "Le nom du modèle existe déjà",
+  "添加成功": "Ajouté avec succès",
+  "请先选择需要批量设置的模型": "Veuillez d'abord sélectionner les modèles pour le paramétrage par lots",
+  "请输入模型倍率和补全倍率": "Veuillez saisir le ratio de modèle et le ratio d'achèvement",
+  "请输入有效的数字": "Veuillez saisir un nombre valide",
+  "请输入填充值": "Veuillez saisir une valeur",
+  "批量设置成功": "Paramétrage par lots réussi",
+  "已为 {{count}} 个模型设置{{type}}": "Définir {{type}} pour {{count}} modèles",
+  "固定价格": "Prix fixe",
+  "模型倍率和补全倍率": "Ratio de modèle et ratio d'achèvement",
+  "批量设置": "Paramétrage par lots",
+  "搜索模型名称": "Rechercher un nom de modèle",
+  "此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除": "Cette page n'affiche que les modèles sans prix ni ratio. Après le paramétrage, ils seront automatiquement supprimés de la liste",
+  "没有未设置的模型": "Aucun modèle non configuré",
+  "定价模式": "Mode de tarification",
+  "固定价格(每次)": "Prix fixe (par utilisation)",
+  "输入每次价格": "Saisir le prix par utilisation",
+  "批量设置模型参数": "Paramètres de modèle par lots",
+  "设置类型": "Type de paramètre",
+  "模型倍率值": "Valeur du ratio de modèle",
+  "补全倍率值": "Valeur du ratio d'achèvement",
+  "请输入模型倍率": "Saisir le ratio de modèle",
+  "请输入补全倍率": "Saisir le ratio d'achèvement",
+  "请输入数值": "Saisir une valeur",
+  "将为选中的 ": "Définira pour la sélection ",
+  " 个模型设置相同的值": " modèles avec la même valeur",
+  "当前设置类型: ": "Type de paramètre actuel : ",
+  "固定价格值": "Valeur de prix fixe",
+  "未设置倍率模型": "Modèles sans ratio",
+  "模型倍率和补全倍率同时设置": "Le ratio de modèle et le ratio d'achèvement sont définis simultanément",
+  "自用模式": "Mode auto-utilisation",
+  "开启后不限制:必须设置模型倍率": "Après l'activation, aucune limite : le ratio de modèle doit être défini",
+  "演示站点模式": "Mode site de démonstration",
+  "当前版本": "Version actuelle",
+  "Gemini设置": "Paramètres Gemini",
+  "Gemini安全设置": "Paramètres de sécurité Gemini",
+  "default为默认设置,可单独设置每个分类的安全等级": "\"default\" est le paramètre par défaut, et chaque catégorie peut être définie séparément",
+  "Gemini版本设置": "Paramètres de version Gemini",
+  "default为默认设置,可单独设置每个模型的版本": "\"default\" est le paramètre par défaut, et chaque modèle peut être défini séparément",
+  "Claude设置": "Paramètres Claude",
+  "Claude请求头覆盖": "Remplacement de l'en-tête de la requête Claude",
+  "示例": "Exemple",
+  "缺省 MaxTokens": "MaxTokens par défaut",
+  "启用Claude思考适配(-thinking后缀)": "Activer l'adaptation de la pensée Claude (suffixe -thinking)",
+  "和Claude不同,默认情况下Gemini的思考模型会自动决定要不要思考,就算不开启适配模型也可以正常使用,": "Contrairement à Claude, le modèle de pensée de Gemini décide automatiquement s'il doit penser par défaut, et peut être utilisé normalement même sans activer le modèle d'adaptation.",
+  "如果您需要计费,推荐设置无后缀模型价格按思考价格设置。": "Si vous avez besoin de facturer, il est recommandé de définir le prix du modèle sans suffixe en fonction du prix de la pensée.",
+  "支持使用 gemini-2.5-pro-preview-06-05-thinking-128 格式来精确传递思考预算。": "Prend en charge l'utilisation du format gemini-2.5-pro-preview-06-05-thinking-128 pour transmettre précisément le budget de la pensée.",
+  "启用Gemini思考后缀适配": "Activer l'adaptation du suffixe de la pensée Gemini",
+  "适配-thinking、-thinking-预算数字和-nothinking后缀": "Adapter les suffixes -thinking, -thinking-budgetNumber et -nothinking",
+  "思考预算占比": "Ratio du budget de la pensée",
+  "Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Adaptation de la pensée Claude BudgetTokens = MaxTokens * BudgetTokens pourcentage",
+  "思考适配 BudgetTokens 百分比": "Adaptation de la pensée BudgetTokens pourcentage",
+  "0.1-1之间的小数": "Décimal entre 0,1 et 1",
+  "模型相关设置": "Paramètres liés au modèle",
+  "收起侧边栏": "Réduire la barre latérale",
+  "展开侧边栏": "Développer la barre latérale",
+  "提示缓存倍率": "Ratio de cache d'invite",
+  "缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache : ${{price}} * {{ratio}} = ${{total}} / 1M de jetons (ratio de cache : {{cacheRatio}})",
+  "提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Invite {{nonCacheInput}} jetons + cache {{cacheInput}} jetons * {{cacheRatio}} / 1M de jetons * ${{price}} + achèvement {{completion}} jetons / 1M de jetons * ${{compPrice}} * groupe {{ratio}} = ${{total}}",
+  "缓存 Tokens": "Jetons de cache",
+  "系统初始化": "Initialisation du système",
+  "管理员账号已经初始化过,请继续设置其他参数": "Le compte administrateur a déjà été initialisé, veuillez continuer à définir d'autres paramètres",
+  "管理员账号": "Compte administrateur",
+  "请输入管理员用户名": "Veuillez saisir le nom d'utilisateur de l'administrateur",
+  "请输入管理员密码": "Veuillez saisir le mot de passe de l'administrateur",
+  "请确认管理员密码": "Veuillez confirmer le mot de passe de l'administrateur",
+  "请选择使用模式": "Veuillez sélectionner le mode d'utilisation",
+  "数据库警告": "Avertissement de la base de données",
+  "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "Vous utilisez la base de données SQLite. Si vous exécutez dans un environnement de conteneur, veuillez vous assurer que le mappage de persistance du fichier de base de données est correctement défini, sinon toutes les données seront perdues après le redémarrage du conteneur !",
+  "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "Il est recommandé d'utiliser les bases de données MySQL ou PostgreSQL dans les environnements de production, ou de s'assurer que le fichier de base de données SQLite est mappé sur le stockage persistant de la machine hôte.",
+  "使用模式": "Mode d'utilisation",
+  "对外运营模式": "Mode par défaut",
+  "密码长度至少为8个字符": "Le mot de passe doit comporter au moins 8 caractères",
+  "表单引用错误,请刷新页面重试": "Erreur de référence de formulaire, veuillez actualiser la page et réessayer",
+  "初始化系统": "Initialiser le système",
+  "支持众多的大模型供应商": "Prise en charge de divers fournisseurs de LLM",
+  "统一的大模型接口网关": "La passerelle d'API LLM unifiée",
+  "更好的价格,更好的稳定性,只需要将模型基址替换为:": "Meilleur prix, meilleure stabilité, aucun abonnement requis, il suffit de remplacer l'URL de BASE du modèle par : ",
+  "获取密钥": "Obtenir la clé",
+  "关于我们": "À propos de nous",
+  "关于项目": "À propos du projet",
+  "联系我们": "Contactez-nous",
+  "功能特性": "Fonctionnalités",
+  "快速开始": "Démarrage rapide",
+  "安装指南": "Guide d'installation",
+  "API 文档": "Documentation de l'API",
+  "相关项目": "Projets connexes",
+  "基于New API的项目": "Projets basés sur la nouvelle API",
+  "版权所有": "Tous droits réservés",
+  "设计与开发由": "Conçu et développé avec amour par",
+  "演示站点": "Site de démonstration",
+  "页面未找到,请检查您的浏览器地址是否正确": "Page non trouvée, veuillez vérifier si l'adresse de votre navigateur est correcte",
+  "您无权访问此页面,请联系管理员": "Vous n'êtes pas autorisé à accéder à cette page. Veuillez contacter l'administrateur.",
+  "New API项目仓库地址:": "Adresse du référentiel du projet New API : ",
+  "© {{currentYear}}": "© {{currentYear}}",
+  "| 基于": " | Basé sur ",
+  "MIT许可证": "Licence MIT",
+  "AGPL v3.0协议": "Licence AGPL v3.0",
+  "本项目根据": "Ce projet est sous licence ",
+  "授权,需在遵守": " et doit être utilisé conformément au ",
+  "的前提下使用。": ".",
+  "管理员暂时未设置任何关于内容": "L'administrateur n'a encore défini aucun contenu personnalisé \"À propos\".",
+  "早上好": "Bonjour",
+  "中午好": "Bon après-midi",
+  "下午好": "Bon après-midi",
+  "晚上好": "Bonsoir",
+  "更多提示信息": "Plus d'invites",
+  "新建": "Créer",
+  "更新": "Mettre à jour",
+  "基本信息": "Informations de base",
+  "设置令牌的基本信息": "Définir les informations de base du jeton",
+  "设置令牌可用额度和数量": "Définir le quota et la quantité disponibles du jeton",
+  "访问限制": "Restrictions d'accès",
+  "设置令牌的访问限制": "Définir les restrictions d'accès au jeton",
+  "请勿过度信任此功能,IP可能被伪造": "Ne faites pas trop confiance à cette fonctionnalité, l'IP peut être usurpée",
+  "模型限制列表": "Liste des restrictions de modèle",
+  "请选择该令牌支持的模型,留空支持所有模型": "Sélectionnez les modèles pris en charge par le jeton, laissez vide pour prendre en charge tous les modèles",
+  "非必要,不建议启用模型限制": "Non nécessaire, les restrictions de modèle ne sont pas recommandées",
+  "分组信息": "Informations sur le groupe",
+  "设置令牌的分组": "Définir le regroupement de jetons",
+  "管理员未设置用户可选分组": "L'administrateur n'a pas défini de groupes sélectionnables par l'utilisateur",
+  "10个": "10 éléments",
+  "20个": "20 éléments",
+  "30个": "30 éléments",
+  "100个": "100 éléments",
+  "Midjourney 任务记录": "Enregistrements de tâches Midjourney",
+  "任务记录": "Enregistrements de tâches",
+  "剩余": "Restant",
+  "已用": "Utilisé",
+  "调用": "Appels",
+  "调用次数": "Nombre d'appels",
+  "邀请": "Invitations",
+  "收益": "Gains",
+  "无邀请人": "Pas d'invitant",
+  "邀请人": "Inviteur",
+  "设置兑换码的基本信息": "Définir les informations de base du code d'échange",
+  "设置兑换码的额度和数量": "Définir le quota et la quantité du code d'échange",
+  "编辑用户": "Modifier l'utilisateur",
+  "权限设置": "Paramètres d'autorisation",
+  "用户的基本账户信息": "Informations de base du compte utilisateur",
+  "用户分组和额度管理": "Gestion des groupes d'utilisateurs et des quotas",
+  "绑定信息": "Informations de liaison",
+  "第三方账户绑定状态(只读)": "État de la liaison du compte tiers (lecture seule)",
+  "已绑定的 OIDC 账户": "Comptes OIDC liés",
+  "使用兑换码充值余额": "Recharger le solde avec un code d'échange",
+  "支持多种支付方式": "Prend en charge plusieurs méthodes de paiement",
+  "尊敬的": "Cher",
+  "请输入兑换码": "Veuillez saisir le code d'échange",
+  "在线充值功能未开启": "La fonction de recharge en ligne n'est pas activée",
+  "管理员未开启在线充值功能,请联系管理员开启或使用兑换码充值。": "L'administrateur n'a pas activé la fonction de recharge en ligne, veuillez contacter l'administrateur pour l'activer ou recharger avec un code d'échange.",
+  "点击模型名称可复制": "Cliquez sur le nom du modèle pour copier",
+  "管理您的邀请链接和收益": "Gérez votre lien d'invitation et vos gains",
+  "没有可用模型": "Aucun modèle disponible",
+  "账户绑定": "Liaison de compte",
+  "安全设置": "Paramètres de sécurité",
+  "系统访问令牌": "Jeton d'accès au système",
+  "用于API调用的身份验证令牌,请妥善保管": "Jeton d'authentification pour les appels d'API, veuillez le conserver en lieu sûr",
+  "密码管理": "Gestion des mots de passe",
+  "定期更改密码可以提高账户安全性": "Changer régulièrement votre mot de passe peut améliorer la sécurité de votre compte",
+  "删除账户": "Supprimer le compte",
+  "此操作不可逆,所有数据将被永久删除": "Cette opération est irréversible, toutes les données seront définitivement supprimées",
+  "生成令牌": "Générer un jeton",
+  "通过邮件接收通知": "Recevoir des notifications par e-mail",
+  "通过HTTP请求接收通知": "Recevoir des notifications via une requête HTTP",
+  "价格设置": "Paramètres de prix",
+  "重新生成": "Régénérer",
+  "绑定微信账户": "Lier le compte WeChat",
+  "原密码": "Mot de passe original",
+  "请输入原密码": "Veuillez saisir le mot de passe original",
+  "请输入新密码": "Veuillez saisir le nouveau mot de passe",
+  "请再次输入新密码": "Veuillez saisir à nouveau le nouveau mot de passe",
+  "删除账户确认": "Confirmation de la suppression du compte",
+  "请输入您的用户名以确认删除": "Veuillez saisir votre nom d'utilisateur pour confirmer la suppression",
+  "接受未设置价格模型": "Accepter les modèles sans prix défini",
+  "当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用": "Acceptez les appels même si le modèle n'a pas de prix défini, utilisez uniquement lorsque vous faites confiance au site Web, ce qui peut entraîner des coûts élevés",
+  "批量操作": "Opérations par lots",
+  "未开始": "Non démarré",
+  "测试中": "Test en cours",
+  "请求时长: ${time}s": "Durée de la requête : ${time}s",
+  "搜索模型...": "Rechercher des modèles...",
+  "批量测试${count}个模型": "Tester par lots ${count} modèles",
+  "测试中...": "Test en cours...",
+  "渠道的模型测试": "Test de modèle de canal",
+  "共": "Total",
+  "确定要测试所有通道吗?": "Êtes-vous sûr de vouloir tester tous les canaux ?",
+  "确定要更新所有已启用通道余额吗?": "Êtes-vous sûr de vouloir mettre à jour le solde de tous les canaux activés ?",
+  "已选择 ${count} 个渠道": "${count} canaux sélectionnés",
+  "渠道的基本配置信息": "Informations de configuration de base du canal",
+  "API 配置": "Configuration de l'API",
+  "API 地址和相关配置": "URL de l'API et configuration associée",
+  "模型配置": "Configuration du modèle",
+  "模型选择和映射设置": "Sélection de modèle et paramètres de mappage",
+  "高级设置": "Paramètres avancés",
+  "渠道的高级配置选项": "Options de configuration avancées du canal",
+  "设置说明": "Description du paramètre",
+  "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "Ceci est facultatif, utilisé pour configurer les paramètres spécifiques au canal, sous forme de chaîne JSON, par exemple :",
+  "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "Ceci est facultatif, utilisé pour remplacer les paramètres de la requête. Ne prend pas en charge le remplacement du paramètre de flux. Sous forme de chaîne JSON, par exemple :",
+  "编辑标签": "Modifier l'étiquette",
+  "标签信息": "Informations sur l'étiquette",
+  "标签的基本配置": "Configuration de base de l'étiquette",
+  "所有编辑均为覆盖操作,留空则不更改": "Toutes les modifications sont des opérations de remplacement, laisser vide ne changera rien",
+  "标签名称": "Nom de l'étiquette",
+  "请选择该渠道所支持的模型,留空则不更改": "Veuillez sélectionner les modèles pris en charge par le canal, laisser vide ne changera rien",
+  "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改": "Ceci est facultatif, utilisé pour modifier le nom du modèle dans le corps de la requête, sous forme de chaîne JSON, la clé est le nom du modèle dans la requête, la valeur est le nom du modèle à remplacer, laisser vide ne changera rien",
+  "清空重定向": "Effacer la redirection",
+  "不更改": "Ne pas changer",
+  "用户分组配置": "Configuration du groupe d'utilisateurs",
+  "请选择可以使用该渠道的分组,留空则不更改": "Veuillez sélectionner les groupes qui peuvent utiliser ce canal, laisser vide ne changera rien",
+  "启用全部": "Activer tout",
+  "禁用全部": "Désactiver tout",
+  "模型定价": "Tarification du modèle",
+  "当前分组": "Groupe actuel",
+  "全部模型": "Tous les modèles",
+  "智谱": "Zhipu AI",
+  "通义千问": "Qwen",
+  "文心一言": "ERNIE Bot",
+  "讯飞星火": "Spark Desk",
+  "腾讯混元": "Hunyuan",
+  "360智脑": "360 AI Brain",
+  "零一万物": "Yi",
+  "豆包": "Doubao",
+  "系统公告": "Avis système",
+  "今日关闭": "Fermer aujourd'hui",
+  "关闭公告": "Fermer l'avis",
+  "搜索条件": "Conditions de recherche",
+  "加载中...": "Chargement...",
+  "正在跳转...": "Redirection...",
+  "暂无公告": "Pas d'avis",
+  "欢迎使用,请完成以下设置以开始使用系统": "Bienvenue, veuillez compléter les paramètres suivants pour commencer à utiliser le système",
+  "数据库检查": "Vérification de la base de données",
+  "验证数据库连接状态": "Vérifier l'état de la connexion à la base de données",
+  "设置管理员登录信息": "Définir les informations de connexion de l'administrateur",
+  "选择系统运行模式": "Sélectionner le mode de fonctionnement du système",
+  "完成初始化": "Terminer l'initialisation",
+  "确认设置并完成初始化": "Confirmer les paramètres et terminer l'initialisation",
+  "数据库信息": "Informations sur la base de données",
+  "请填写完整的管理员账号信息": "Veuillez remplir les informations complètes du compte administrateur",
+  "准备完成初始化": "Prêt à terminer l'initialisation",
+  "请确认以下设置信息,点击\"初始化系统\"开始配置": "Veuillez confirmer les informations de configuration suivantes, cliquez sur \"Initialiser le système\" pour commencer la configuration",
+  "数据库类型": "Type de base de données",
+  "您正在使用 MySQL 数据库。MySQL 是一个可靠的关系型数据库管理系统,适合生产环境使用。": "Vous utilisez la base de données MySQL. MySQL est un système de gestion de base de données relationnelle fiable, adapté aux environnements de production.",
+  "您正在使用 PostgreSQL 数据库。PostgreSQL 是一个功能强大的开源关系型数据库系统,提供了出色的可靠性和数据完整性,适合生产环境使用。": "Vous utilisez la base de données PostgreSQL. PostgreSQL est un système de base de données relationnelle open-source puissant qui offre une excellente fiabilité et intégrité des données, adapté aux environnements de production.",
+  "选择适合您使用场景的模式": "Sélectionnez le mode adapté à votre scénario d'utilisation",
+  "适用于为多个用户提供服务的场景": "Adapté aux scénarios où plusieurs utilisateurs sont fournis.",
+  "适用于个人使用的场景,不需要设置模型价格": "Adapté à un usage personnel, pas besoin de définir le prix du modèle.",
+  "适用于展示系统功能的场景,提供基础功能演示": "Adapté aux scénarios où les fonctions du système sont affichées, fournissant des démonstrations de fonctionnalités de base.",
+  "账户数据": "Données du compte",
+  "使用统计": "Statistiques d'utilisation",
+  "资源消耗": "Consommation de ressources",
+  "性能指标": "Indicateurs de performance",
+  "模型数据分析": "Analyse des données du modèle",
+  "搜索无结果": "Aucun résultat trouvé",
+  "仪表盘设置": "Paramètres du tableau de bord",
+  "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "Gestion des informations de l'API, vous pouvez configurer plusieurs adresses d'API pour l'affichage de l'état et l'équilibrage de charge (maximum 50)",
+  "线路描述": "Description de l'itinéraire",
+  "颜色": "Couleur",
+  "标识颜色": "Couleur de l'identifiant",
+  "添加API": "Ajouter une API",
+  "API信息": "Informations sur l'API",
+  "暂无API信息": "Aucune information sur l'API",
+  "请输入API地址": "Veuillez saisir l'adresse de l'API",
+  "请输入线路描述": "Veuillez saisir la description de l'itinéraire",
+  "如:大带宽批量分析图片推荐": "par exemple, Recommandations d'analyse d'images par lots à large bande passante",
+  "请输入说明": "Veuillez saisir la description",
+  "如:香港线路": "par exemple, Ligne de Hong Kong",
+  "请联系管理员在系统设置中配置API信息": "Veuillez contacter l'administrateur pour configurer les informations de l'API dans les paramètres système.",
+  "请联系管理员在系统设置中配置公告信息": "Veuillez contacter l'administrateur pour configurer les informations d'avis dans les paramètres système.",
+  "请联系管理员在系统设置中配置常见问答": "Veuillez contacter l'administrateur pour configurer les informations de la FAQ dans les paramètres système.",
+  "请联系管理员在系统设置中配置Uptime": "Veuillez contacter l'administrateur pour configurer Uptime dans les paramètres système.",
+  "确定要删除此API信息吗?": "Êtes-vous sûr de vouloir supprimer ces informations d'API ?",
+  "测速": "Test de vitesse",
+  "跳转": "Sauter",
+  "批量删除": "Supprimer par lots",
+  "常见问答": "FAQ",
+  "进行中": "En cours",
+  "警告": "Avertissement",
+  "添加公告": "Ajouter un avis",
+  "编辑公告": "Modifier l'avis",
+  "公告内容": "Contenu de l'avis",
+  "请输入公告内容": "Veuillez saisir le contenu de l'avis",
+  "请输入公告内容(支持 Markdown/HTML)": "Veuillez saisir le contenu de l'avis (prend en charge Markdown/HTML)",
+  "发布日期": "Date de publication",
+  "请选择发布日期": "Veuillez sélectionner la date de publication",
+  "发布时间": "Heure de publication",
+  "公告类型": "Type d'avis",
+  "说明信息": "Description",
+  "可选,公告的补充说明": "Facultatif, informations supplémentaires pour l'avis",
+  "确定要删除此公告吗?": "Êtes-vous sûr de vouloir supprimer cet avis ?",
+  "系统公告管理,可以发布系统通知和重要消息": "Gestion des avis système, vous pouvez publier des avis système et des messages importants",
+  "暂无系统公告": "Pas d'avis système",
+  "添加问答": "Ajouter une FAQ",
+  "编辑问答": "Modifier la FAQ",
+  "问题标题": "Titre de la question",
+  "请输入问题标题": "Veuillez saisir le titre de la question",
+  "回答内容": "Contenu de la réponse",
+  "请输入回答内容": "Veuillez saisir le contenu de la réponse",
+  "请输入回答内容(支持 Markdown/HTML)": "Veuillez saisir le contenu de la réponse (prend en charge Markdown/HTML)",
+  "确定要删除此问答吗?": "Êtes-vous sûr de vouloir supprimer cette FAQ ?",
+  "系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "Gestion des avis système, vous pouvez publier des avis système et des messages importants (maximum 100, afficher les 20 derniers sur le front-end)",
+  "常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "Gestion de la FAQ, fournissant des réponses aux questions courantes des utilisateurs (maximum 50, afficher les 20 dernières sur le front-end)",
+  "暂无常见问答": "Pas de FAQ",
+  "显示最新20条": "Afficher les 20 dernières",
+  "Uptime Kuma监控分类管理,可以配置多个监控分类用于服务状态展示(最多20个)": "Gestion des catégories de surveillance Uptime Kuma, vous pouvez configurer plusieurs catégories de surveillance pour l'affichage de l'état du service (maximum 20)",
+  "添加分类": "Ajouter une catégorie",
+  "分类名称": "Nom de la catégorie",
+  "Uptime Kuma地址": "Adresse Uptime Kuma",
+  "状态页面Slug": "Slug de la page d'état",
+  "请输入分类名称,如:OpenAI、Claude等": "Veuillez saisir le nom de la catégorie, tel que : OpenAI, Claude, etc.",
+  "请输入Uptime Kuma服务地址,如:https://status.example.com": "Veuillez saisir l'adresse du service Uptime Kuma, telle que : https://status.example.com",
+  "请输入状态页面的Slug,如:my-status": "Veuillez saisir le slug de la page d'état, tel que : my-status",
+  "确定要删除此分类吗?": "Êtes-vous sûr de vouloir supprimer cette catégorie ?",
+  "配置": "Configurer",
+  "服务监控地址,用于展示服务状态信息": "adresse de surveillance du service pour afficher les informations d'état",
+  "服务可用性": "État du service",
+  "可用率": "Disponibilité",
+  "有异常": "Anormal",
+  "高延迟": "Latence élevée",
+  "维护中": "Maintenance",
+  "暂无监控数据": "Pas de données de surveillance",
+  "IP记录": "Enregistrement IP",
+  "记录请求与错误日志 IP": "Enregistrer l'adresse IP du journal des requêtes et des erreurs",
+  "开启后,仅\"消费\"和\"错误\"日志将记录您的客户端IP地址": "Après l'activation, seuls les journaux \"consommation\" et \"erreur\" enregistreront votre adresse IP client",
+  "只有当用户设置开启IP记录时,才会进行请求和错误类型日志的IP记录": "Ce n'est que lorsque l'utilisateur définit l'enregistrement IP que l'enregistrement IP des journaux de type requête et erreur sera effectué",
+  "设置保存成功": "Paramètres enregistrés avec succès",
+  "设置保存失败": "Échec de l'enregistrement des paramètres",
+  "已新增 {{count}} 个模型:{{list}}": "{{count}} modèles ajoutés : {{list}}",
+  "未发现新增模型": "Aucun nouveau modèle n'a été ajouté",
+  "清除失效兑换码": "Effacer les codes d'échange non valides",
+  "确定清除所有失效兑换码?": "Êtes-vous sûr de vouloir effacer tous les codes d'échange non valides ?",
+  "将删除已使用、已禁用及过期的兑换码,此操作不可撤销。": "Cela supprimera tous les codes d'échange utilisés, désactivés et expirés, cette opération ne peut pas être annulée.",
+  "选择过期时间(可选,留空为永久)": "Sélectionnez la date d'expiration (facultatif, laissez vide pour permanent)",
+  "请输入备注(仅管理员可见)": "Veuillez saisir une remarque (visible uniquement par les administrateurs)",
+  "上游倍率同步": "Synchronisation du ratio en amont",
+  "获取渠道失败:": "Échec de l'obtention des canaux : ",
+  "请至少选择一个渠道": "Veuillez sélectionner au moins un canal",
+  "获取倍率失败:": "Échec de l'obtention des ratios : ",
+  "后端请求失败": "Échec de la requête du backend",
+  "部分渠道测试失败:": "Certains canaux n'ont pas réussi le test : ",
+  "未找到差异化倍率,无需同步": "Aucun ratio différentiel trouvé, aucune synchronisation n'est requise",
+  "请求后端接口失败:": "Échec de la requête de l'interface backend : ",
+  "同步成功": "Synchronisation réussie",
+  "部分保存失败": "Certains paramètres n'ont pas pu être enregistrés",
+  "保存失败": "Échec de l'enregistrement",
+  "选择同步渠道": "Sélectionner le canal de synchronisation",
+  "应用同步": "Appliquer la synchronisation",
+  "倍率类型": "Type de ratio",
+  "当前值": "Valeur actuelle",
+  "上游值": "Valeur en amont",
+  "差异": "Différence",
+  "搜索渠道名称或地址": "Rechercher un nom ou une adresse de canal",
+  "缓存倍率": "Ratio de cache",
+  "暂无差异化倍率显示": "Aucun affichage de ratio différentiel",
+  "请先选择同步渠道": "Veuillez d'abord sélectionner le canal de synchronisation",
+  "与本地相同": "Identique au local",
+  "未找到匹配的模型": "Aucun modèle correspondant trouvé",
+  "暴露倍率接口": "Exposer l'API de ratio",
+  "支付设置": "Paramètres de paiement",
+  "(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)": "(Actuellement, seule l'interface Epay est prise en charge, l'adresse du serveur ci-dessus est utilisée par défaut comme adresse de rappel !)",
+  "支付地址": "Adresse de paiement",
+  "易支付商户ID": "ID marchand Epay",
+  "易支付商户密钥": "Clé marchand Epay",
+  "回调地址": "Adresse de rappel",
+  "充值价格(x元/美金)": "Prix de recharge (x yuans/dollar)",
+  "最低充值美元数量": "Montant minimum de recharge en dollars",
+  "充值分组倍率": "Ratio de groupe de recharge",
+  "充值方式设置": "Paramètres de la méthode de recharge",
+  "更新支付设置": "Mettre à jour les paramètres de paiement",
+  "通知": "Avis",
+  "源地址": "Adresse source",
+  "同步接口": "Interface de synchronisation",
+  "置信度": "Confiance",
+  "谨慎": "Prudent",
+  "该数据可能不可信,请谨慎使用": "Ces données peuvent ne pas être fiables, veuillez les utiliser avec prudence",
+  "可信": "Fiable",
+  "所有上游数据均可信": "Toutes les données en amont sont fiables",
+  "以下上游数据可能不可信:": "Les données en amont suivantes peuvent ne pas être fiables : ",
+  "按倍率类型筛选": "Filtrer par type de ratio",
+  "内容": "Contenu",
+  "放大编辑": "Développer l'éditeur",
+  "编辑公告内容": "Modifier le contenu de l'annonce",
+  "自适应列表": "Liste adaptative",
+  "紧凑列表": "Liste compacte",
+  "仅显示矛盾倍率": "Afficher uniquement les ratios contradictoires",
+  "矛盾": "Conflit",
+  "确认冲突项修改": "Confirmer la modification de l'élément de conflit",
+  "该模型存在固定价格与倍率计费方式冲突,请确认选择": "Le modèle a un conflit de méthode de facturation à prix fixe et à ratio, veuillez confirmer la sélection",
+  "当前计费": "Facturation actuelle",
+  "修改为": "Modifier en",
+  "状态筛选": "Filtre d'état",
+  "没有模型可以复制": "Aucun modèle à copier",
+  "模型列表已复制到剪贴板": "Liste des modèles copiée dans le presse-papiers",
+  "复制失败": "Échec de la copie",
+  "复制已选": "Copier la sélection",
+  "选择成功": "Sélection réussie",
+  "暂无成功模型": "Aucun modèle réussi",
+  "请先选择模型!": "Veuillez d'abord sélectionner un modèle !",
+  "已复制 ${count} 个模型": "${count} modèles copiés",
+  "复制失败,请手动复制": "Échec de la copie, veuillez copier manuellement",
+  "过期时间快捷设置": "Paramètres rapides de la date d'expiration",
+  "批量创建时会在名称后自动添加随机后缀": "Lors de la création par lots, un suffixe aléatoire sera automatiquement ajouté au nom",
+  "额度必须大于0": "Le quota doit être supérieur à 0",
+  "生成数量必须大于0": "La quantité de génération doit être supérieure à 0",
+  "可用端点类型": "Types de points de terminaison pris en charge",
+  "未登录,使用默认分组倍率:": "Non connecté, en utilisant le ratio de groupe par défaut : ",
+  "该服务器地址将影响支付回调地址以及默认首页展示的地址,请确保正确配置": "Cette adresse de serveur affectera l'adresse de rappel de paiement et l'adresse affichée sur la page d'accueil par défaut, veuillez vous assurer d'une configuration correcte",
+  "密钥聚合模式": "Mode d'agrégation de clés",
+  "随机": "Aléatoire",
+  "轮询": "Sondage",
+  "密钥文件 (.json)": "Fichier de clé (.json)",
+  "点击上传文件或拖拽文件到这里": "Cliquez pour télécharger un fichier ou faites glisser et déposez un fichier ici",
+  "仅支持 JSON 文件": "Seuls les fichiers JSON sont pris en charge",
+  "仅支持 JSON 文件,支持多文件": "Seuls les fichiers JSON sont pris en charge, plusieurs fichiers sont pris en charge",
+  "请上传密钥文件": "Veuillez télécharger le fichier de clé",
+  "请填写部署地区": "Veuillez remplir la région de déploiement",
+  "请输入部署地区,例如:us-central1\n支持使用模型映射格式\n{\n    \"default\": \"us-central1\",\n    \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}": "Veuillez saisir la région de déploiement, par exemple : us-central1\nPrend en charge l'utilisation du format de mappage de modèle\n{\n    \"default\": \"us-central1\",\n    \"claude-3-5-sonnet-20240620\": \"europe-west1\"\n}",
+  "其他": "Autre",
+  "未知渠道": "Canal inconnu",
+  "切换为单密钥模式": "Passer en mode clé unique",
+  "将仅保留第一个密钥文件,其余文件将被移除,是否继续?": "Seul le premier fichier de clé sera conservé, et les fichiers restants seront supprimés. Continuer ?",
+  "自定义模型名称": "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)",
+  "美元汇率": "Taux de change USD",
+  "隐藏操作项": "Masquer les actions",
+  "显示操作项": "Afficher les actions",
+  "用户组": "Groupe d'utilisateurs",
+  "邀请获得额度": "Quota d'invitation",
+  "显示第": "Affichage de",
+  "条 - 第": "à",
+  "条,共": "sur",
+  "条": "éléments",
+  "选择模型": "Sélectionner un modèle",
+  "已选择 {{selected}} / {{total}}": "{{selected}} / {{total}} sélectionnés",
+  "新获取的模型": "Nouveaux modèles",
+  "已有的模型": "Modèles existants",
+  "搜索模型": "Rechercher des modèles",
+  "缓存: {{cacheRatio}}": "Cache : {{cacheRatio}}",
+  "缓存创建: {{cacheCreationRatio}}": "Création de cache : {{cacheCreationRatio}}",
+  "图片输入: {{imageRatio}}": "Entrée d'image : {{imageRatio}}",
+  "系统提示覆盖": "Remplacement de l'invite système",
+  "模型: {{ratio}}": "Modèle : {{ratio}}",
+  "专属倍率": "Ratio de groupe exclusif",
+  "匹配类型": "Type de correspondance",
+  "描述": "Description",
+  "供应商": "Fournisseur",
+  "供应商介绍": "Présentation du fournisseur",
+  "端点": "Point de terminaison",
+  "已绑定渠道": "Canaux liés",
+  "更新时间": "Heure de mise à jour",
+  "未配置模型": "Aucun modèle configuré",
+  "预填组管理": "Groupe pré-rempli",
+  "搜索供应商": "Rechercher un fournisseur",
+  "新增供应商": "Ajouter un fournisseur",
+  "创建新的模型": "Créer un nouveau modèle",
+  "更新模型信息": "Mettre à jour les informations du modèle",
+  "请输入模型名称,如:gpt-4": "Veuillez saisir le nom du modèle, tel que : gpt-4",
+  "设置模型的基本信息": "Définir les informations de base du modèle",
+  "名称匹配类型": "Type de correspondance de nom",
+  "根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含": "Rechercher les métadonnées du modèle en fonction du nom du modèle et des règles de correspondance, priorité : exact > préfixe > suffixe > contient",
+  "请选择名称匹配类型": "Veuillez sélectionner le type de correspondance de nom",
+  "请输入模型描述": "Veuillez saisir la description du modèle",
+  "输入标签或使用\",\"分隔多个标签": "Saisissez des étiquettes ou utilisez \",\" pour séparer plusieurs étiquettes",
+  "选择模型供应商": "Sélectionner le fournisseur du modèle",
+  "端点映射": "Mappage de points de terminaison",
+  "可视化": "Visualisation",
+  "手动编辑": "Modification manuelle",
+  "暂无数据,点击下方按钮添加键值对": "Aucune donnée, cliquez sur le bouton ci-dessous pour ajouter des paires clé-valeur",
+  "添加键值对": "Ajouter une paire clé-valeur",
+  "留空则使用默认端点;支持 {path, method}": "Laissez vide pour utiliser le point de terminaison par défaut ; prend en charge {path, method}",
+  "未配置的模型列表": "Modèles non configurés",
+  "个未配置模型": "modèles non configurés",
+  "组列表": "Liste des groupes",
+  "管理模型、标签、端点等预填组": "Gérer les groupes pré-remplis de modèles, d'étiquettes, de points de terminaison, etc.",
+  "新建组": "Nouveau groupe",
+  "组名": "Nom du groupe",
+  "项目内容": "Contenu de l'élément",
+  "创建新的预填组": "Créer un nouveau groupe pré-rempli",
+  "更新预填组": "Mettre à jour le groupe pré-rempli",
+  "设置预填组的基本信息": "Définir les informations de base du groupe pré-rempli",
+  "请输入组名": "Veuillez saisir le nom du groupe",
+  "请输入组描述": "Veuillez saisir la description du groupe",
+  "项目": "Élément",
+  "输入项目名称,按回车添加": "Saisissez le nom de l'élément, appuyez sur Entrée pour ajouter",
+  "键为端点类型,值为路径和方法对象": "La clé est le type de point de terminaison, la valeur est le chemin et l'objet de la méthode",
+  "模型组": "Groupe de modèles",
+  "标签组": "Groupe d'étiquettes",
+  "端点组": "Groupe de points de terminaison",
+  "供应商名称": "Nom du fournisseur",
+  "请输入供应商名称,如:OpenAI": "Veuillez saisir le nom du fournisseur, tel que : OpenAI",
+  "请输入供应商描述": "Veuillez saisir la description du fournisseur",
+  "供应商图标": "Icône du fournisseur",
+  "请输入图标名称,如:OpenAI、Claude.Color": "Veuillez saisir le nom de l'icône, tel que : OpenAI, Claude.Color",
+  "图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={'platform'}、OpenRouter.Avatar.shape={'square'},查询所有可用图标请 ": "L'icône utilise la bibliothèque @lobehub/icons, telle que : OpenAI, Claude.Color, prend en charge les paramètres de chaîne : OpenAI.Avatar.type={'platform'}, OpenRouter.Avatar.shape={'square'}, interroger toutes les icônes disponibles s'il vous plaît ",
+  "请点击我": "Veuillez cliquer sur moi",
+  "精确": "Exact",
+  "前缀": "Préfixe",
+  "后缀": "Suffixe",
+  "包含": "Contient",
+  "全部供应商": "Tous les fournisseurs",
+  "筛选": "Filtre",
+  "显示设置": "Paramètres d'affichage",
+  "可用令牌分组": "Groupes de jetons disponibles",
+  "端点类型": "Type de point de terminaison",
+  "全部分组": "Tous les groupes",
+  "全部类型": "Tous les types",
+  "全部端点": "Tous les points de terminaison",
+  "全部标签": "Toutes les étiquettes",
+  "显示倍率": "Afficher le ratio",
+  "表格视图": "Vue tableau",
+  "模型的详细描述和基本特性": "Description détaillée et caractéristiques de base du modèle",
+  "API端点": "Points de terminaison de l'API",
+  "模型支持的接口端点信息": "Informations sur les points de terminaison de l'API pris en charge par le modèle",
+  "分组价格": "Prix de groupe",
+  "不同用户分组的价格信息": "Informations sur les prix pour différents groupes d'utilisateurs",
+  "auto分组调用链路": "Chaîne d'appels de groupe auto",
+  "查看所有可用的AI模型供应商,包括众多知名供应商的模型。": "Affichez tous les fournisseurs de modèles d'IA disponibles, y compris les modèles de nombreux fournisseurs bien connus.",
+  "包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。": "Comprend des modèles d'IA de fournisseurs inconnus ou non marqués, qui peuvent provenir de petits fournisseurs ou de projets open-source.",
+  "该供应商提供多种AI模型,适用于不同的应用场景。": "Ce fournisseur propose plusieurs modèles d'IA, adaptés à différents scénarios d'application.",
+  "未知供应商": "Inconnu",
+  "共 {{count}} 个模型": "{{count}} modèles",
+  "倍率信息": "Informations sur le ratio",
+  "多密钥管理": "Gestion multi-clés",
+  "总密钥数": "Nombre total de clés",
+  "随机模式": "Mode aléatoire",
+  "轮询模式": "Mode de sondage",
+  "手动禁用": "Désactivé manuellement",
+  "自动禁用": "Désactivé automatiquement",
+  "暂无密钥数据": "Aucune donnée de clé",
+  "请检查渠道配置或刷新重试": "Veuillez vérifier la configuration du canal ou actualiser et réessayer",
+  "全部状态": "Tous les statuts",
+  "索引": "Index",
+  "禁用原因": "Raison de la désactivation",
+  "禁用时间": "Heure de désactivation",
+  "确定要启用所有密钥吗?": "Êtes-vous sûr de vouloir activer toutes les clés ?",
+  "确定要禁用所有的密钥吗?": "Êtes-vous sûr de vouloir désactiver toutes les clés ?",
+  "确定要删除所有已自动禁用的密钥吗?": "Êtes-vous sûr de vouloir supprimer toutes les clés désactivées automatiquement ?",
+  "此操作不可撤销,将永久删除已自动禁用的密钥": "Cette opération ne peut pas être annulée et toutes les clés désactivées automatiquement seront définitivement supprimées.",
+  "删除自动禁用密钥": "Supprimer les clés désactivées automatiquement",
+  "图标": "Icône",
+  "模型图标": "Icône du modèle",
+  "请输入图标名称": "Veuillez saisir le nom de l'icône",
+  "精确名称匹配": "Correspondance de nom exacte",
+  "前缀名称匹配": "Correspondance de nom de préfixe",
+  "后缀名称匹配": "Correspondance de nom de suffixe",
+  "包含名称匹配": "Correspondance de nom contenant",
+  "展开更多": "Développer plus",
+  "已切换至最优倍率视图,每个模型使用其最低倍率分组": "Passé à la vue de ratio optimal, chaque modèle utilise son groupe de ratio le plus bas",
+  "两步验证设置": "Paramètres d'authentification à deux facteurs",
+  "两步验证(2FA)为您的账户提供额外的安全保护。启用后,登录时需要输入密码和验证器应用生成的验证码。": "L'authentification à deux facteurs (2FA) offre une protection de sécurité supplémentaire à votre compte. Après l'avoir activée, vous devez saisir votre mot de passe et le code de vérification généré par l'application d'authentification lorsque vous vous connectez.",
+  "启用两步验证": "Activer l'authentification à deux facteurs",
+  "禁用两步验证": "Désactiver l'authentification à deux facteurs",
+  "启用两步验证后,登录时需要输入密码和验证器应用生成的验证码": "Après avoir activé l'authentification à deux facteurs, vous devez saisir votre mot de passe et le code de vérification généré par l'application d'authentification lorsque vous vous connectez",
+  "禁用两步验证后,登录时只需要输入密码": "Après avoir désactivé l'authentification à deux facteurs, il vous suffit de saisir votre mot de passe lors de la connexion",
+  "验证身份": "Vérifier l'identité",
+  "为了保护您的账户安全,请输入认证器验证码来确认身份": "Pour protéger la sécurité de votre compte, veuillez saisir le code de vérification de l'authentificateur pour confirmer votre identité",
+  "输入认证器应用显示的6位数字验证码": "Saisissez le code de vérification à 6 chiffres affiché sur l'application d'authentification",
+  "新的备用恢复代码": "Nouveau code de récupération de sauvegarde",
+  "新的备用码已生成": "Un nouveau code de sauvegarde a été généré",
+  "我已了解禁用两步验证将永久删除所有相关设置和备用码,此操作不可撤销": "J'ai compris que la désactivation de l'authentification à deux facteurs supprimera définitivement tous les paramètres et codes de sauvegarde associés, cette opération ne peut pas être annulée",
+  "账户已锁定": "Compte verrouillé",
+  "剩余备用码:": "Codes de sauvegarde restants : ",
+  "启用验证": "Activer l'authentification",
+  "重新生成备用码": "Régénérer les codes de sauvegarde",
+  "设置两步验证": "Configurer l'authentification à deux facteurs",
+  "扫描二维码": "Scanner le code QR",
+  "使用认证器应用扫描二维码": "Scanner le code QR avec l'application d'authentification",
+  "保存备用码": "Enregistrer les codes de sauvegarde",
+  "保存备用码以备不时之需": "Enregistrez les codes de sauvegarde pour les urgences",
+  "验证设置": "Vérifier la configuration",
+  "输入验证码完成设置": "Saisissez le code de vérification pour terminer la configuration",
+  "使用认证器应用(如 Google Authenticator、Microsoft Authenticator)扫描下方二维码:": "Utilisez une application d'authentification (telle que Google Authenticator, Microsoft Authenticator) pour scanner le code QR ci-dessous :",
+  "或手动输入密钥:": "Ou saisissez manuellement le secret :",
+  "备用恢复代码": "Codes de récupération de sauvegarde",
+  "复制所有代码": "Copier tous les codes",
+  "上一步": "Précédent",
+  "下一步": "Suivant",
+  "完成设置并启用两步验证": "Terminer la configuration et activer l'authentification à deux facteurs",
+  "确认禁用": "Confirmer la désactivation",
+  "完成": "Terminé",
+  "生成新的备用码": "Générer de nouveaux codes de sauvegarde",
+  "警告:禁用两步验证将永久删除您的验证设置和所有备用码,此操作不可撤销!": "Avertissement : la désactivation de l'authentification à deux facteurs supprimera définitivement vos paramètres de vérification et tous les codes de sauvegarde. Cette action est irréversible !",
+  "禁用后的影响:": "Impact après la désactivation :",
+  "降低您账户的安全性": "Réduire la sécurité de votre compte",
+  "需要重新完整设置才能再次启用": "Nécessite une nouvelle configuration pour être réactivé",
+  "永久删除您的两步验证设置": "Supprimer définitivement vos paramètres d'authentification à deux facteurs",
+  "永久删除所有备用码(包括未使用的)": "Supprimer définitivement tous les codes de sauvegarde (y compris ceux non utilisés)",
+  "请输入认证器验证码或备用码": "Veuillez saisir le code de vérification de l'authentificateur ou le code de sauvegarde",
+  "重新生成备用码将使现有的备用码失效,请确保您已保存了当前的备用码。": "La régénération des codes de sauvegarde invalidera les codes de sauvegarde existants. Veuillez vous assurer que vous avez enregistré les codes de sauvegarde actuels.",
+  "请输入认证器验证码": "Veuillez saisir le code de vérification de l'authentificateur",
+  "旧的备用码已失效,请保存新的备用码": "Les anciens codes de sauvegarde ont été invalidés, veuillez enregistrer les nouveaux codes de sauvegarde",
+  "获取2FA状态失败": "Échec de l'obtention de l'état 2FA",
+  "设置2FA失败": "Échec de la configuration de 2FA",
+  "请输入验证码": "Veuillez saisir le code de vérification",
+  "两步验证启用成功!": "Authentification à deux facteurs activée avec succès !",
+  "启用2FA失败": "Échec de l'activation de 2FA",
+  "请输入验证码或备用码": "Veuillez saisir le code de vérification ou le code de sauvegarde",
+  "请确认您已了解禁用两步验证的后果": "Veuillez confirmer que vous comprenez les conséquences de la désactivation de l'authentification à deux facteurs",
+  "两步验证已禁用": "L'authentification à deux facteurs a été désactivée",
+  "禁用2FA失败": "Échec de la désactivation de 2FA",
+  "备用码重新生成成功": "Codes de sauvegarde régénérés avec succès",
+  "重新生成备用码失败": "Échec de la régénération des codes de sauvegarde",
+  "备用码已复制到剪贴板": "Codes de sauvegarde copiés dans le presse-papiers",
+  "账户管理": "Gestion de compte",
+  "账户绑定、安全设置和身份验证": "Liaison de compte, paramètres de sécurité et vérification d'identité",
+  "通知、价格和隐私相关设置": "Paramètres de notification, de prix et de confidentialité",
+  "通知配置": "Configuration des notifications",
+  "只支持HTTPS,系统将以POST方式发送通知,请确保地址可以接收POST请求": "Seul HTTPS est pris en charge, le système enverra des notifications via POST, veuillez vous assurer que l'adresse peut recevoir des requêtes POST",
+  "密钥将以Bearer方式添加到请求头中,用于验证webhook请求的合法性": "La clé sera ajoutée à l'en-tête de la requête en tant que Bearer pour vérifier la légitimité de la requête webhook",
+  "Webhook请求结构说明": "Description de la structure de la requête Webhook",
+  "通知类型 (quota_exceed: 额度预警)": "Type de notification (quota_exceed : avertissement de quota)",
+  "通知标题": "Titre de la notification",
+  "通知内容,支持 {{value}} 变量占位符": "Contenu de la notification, prend en charge les espaces réservés de variable {{value}}",
+  "按顺序替换content中的变量占位符": "Remplacer les espaces réservés de variable dans le contenu dans l'ordre",
+  "Unix时间戳": "Horodatage Unix",
+  "隐私设置": "Paramètres de confidentialité",
+  "记录请求与错误日志IP": "Enregistrer l'adresse IP du journal des requêtes et des erreurs",
+  "切换主题": "Changer de thème",
+  "浅色模式": "Mode clair",
+  "深色模式": "Mode sombre",
+  "自动模式": "Mode automatique",
+  "始终使用浅色主题": "Toujours utiliser le thème clair",
+  "始终使用深色主题": "Toujours utiliser le thème sombre",
+  "跟随系统主题设置": "Suivre le thème du système",
+  "当前跟随系统": "Suit actuellement le système",
+  "深色": "Sombre",
+  "浅色": "Clair",
+  "点击复制模型名称": "Cliquez pour copier le nom du modèle",
+  "已复制:{{name}}": "Copié : {{name}}",
+  "所有密钥已复制到剪贴板": "Toutes les clés ont été copiées dans le presse-papiers",
+  "密钥已复制到剪贴板": "Clé copiée dans le presse-papiers",
+  "验证成功": "Vérification réussie",
+  "渠道密钥列表": "Liste des clés de canal",
+  "渠道密钥": "Clé de canal",
+  "共 {{count}} 个密钥": "{{count}} clés au total",
+  "复制全部": "Tout copier",
+  "JSON格式密钥,请确保格式正确": "Clé au format JSON, veuillez vous assurer que le format est correct",
+  "检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Plusieurs clés détectées, vous pouvez copier chaque clé individuellement ou cliquer sur Tout copier pour obtenir le contenu complet.",
+  "安全提醒": "Rappel de sécurité",
+  "请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Conservez les informations de clé en lieu sûr, ne les divulguez pas à d'autres. En cas de problèmes de sécurité, veuillez changer la clé immédiatement.",
+  "安全验证": "Vérification de sécurité",
+  "验证": "Vérifier",
+  "为了保护账户安全,请验证您的两步验证码。": "Pour protéger la sécurité du compte, veuillez vérifier votre code d'authentification à deux facteurs.",
+  "支持6位TOTP验证码或8位备用码,可到`个人设置-安全设置-两步验证设置`配置或查看。": "Prend en charge le code de vérification TOTP à 6 chiffres ou le code de sauvegarde à 8 chiffres, peut être configuré ou consulté dans `Paramètres personnels - Paramètres de sécurité - Paramètres d'authentification à deux facteurs`.",
+  "获取密钥失败": "Échec de l'obtention de la clé",
+  "查看密钥": "Afficher la clé",
+  "查看渠道密钥": "Afficher la clé du canal",
+  "渠道密钥信息": "Informations sur la clé du canal",
+  "密钥获取成功": "Acquisition de la clé réussie",
+  "模型补全倍率(仅对自定义模型有效)": "Ratio d'achèvement de modèle (uniquement efficace pour les modèles personnalisés)",
+  "图片倍率": "Ratio d'image",
+  "音频倍率": "Ratio audio",
+  "音频补全倍率": "Ratio d'achèvement audio",
+  "图片输入相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'entrée d'image, la clé est le nom du modèle, la valeur est le ratio",
+  "音频输入相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'entrée audio, la clé est le nom du modèle, la valeur est le ratio",
+  "音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Paramètres de ratio liés à l'achèvement de la sortie audio, la clé est le nom du modèle, la valeur est le ratio",
+  "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-image-1\": 2}",
+  "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-4o-audio-preview\": 16}",
+  "为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "Un texte JSON avec le nom du modèle comme clé et le ratio comme valeur, par exemple : {\"gpt-4o-realtime\": 2}",
+  "顶栏管理": "Gestion de l'en-tête",
+  "控制顶栏模块显示状态,全局生效": "Contrôler l'état d'affichage du module d'en-tête, effet global",
+  "用户主页,展示系统信息": "Page d'accueil de l'utilisateur, affichant les informations système",
+  "用户控制面板,管理账户": "Panneau de configuration de l'utilisateur pour la gestion du compte",
+  "模型广场": "Place du marché des modèles",
+  "模型定价,需要登录访问": "Tarification du modèle, nécessite une connexion pour y accéder",
+  "文档": "Documentation",
+  "系统文档和帮助信息": "Documentation système et informations d'aide",
+  "关于系统的详细信息": "Informations détaillées sur le système",
+  "重置为默认": "Réinitialiser aux valeurs par défaut",
+  "保存设置": "Enregistrer les paramètres",
+  "已重置为默认配置": "Réinitialisé à la configuration par défaut",
+  "保存成功": "Enregistré avec succès",
+  "保存失败,请重试": "Échec de l'enregistrement, veuillez réessayer",
+  "侧边栏管理(全局控制)": "Gestion de la barre latérale (contrôle global)",
+  "全局控制侧边栏区域和功能显示,管理员隐藏的功能用户无法启用": "Contrôle global des zones et des fonctions de la barre latérale, les utilisateurs ne peuvent pas activer les fonctions masquées par les administrateurs",
+  "聊天区域": "Zone de discussion",
+  "操练场和聊天功能": "Terrain de jeu et fonctions de discussion",
+  "操练场": "Terrain de jeu",
+  "AI模型测试环境": "Environnement de test de modèle d'IA",
+  "聊天": "Discuter",
+  "聊天会话管理": "Gestion des sessions de discussion",
+  "控制台区域": "Zone de la console",
+  "数据管理和日志查看": "Gestion des données et affichage des journaux",
+  "数据看板": "Tableau de bord",
+  "系统数据统计": "Statistiques des données système",
+  "令牌管理": "Gestion des jetons",
+  "API令牌管理": "Gestion des jetons d'API",
+  "使用日志": "Journaux d'utilisation",
+  "API使用记录": "Enregistrements d'utilisation de l'API",
+  "绘图日志": "Journaux de dessin",
+  "绘图任务记录": "Enregistrements de tâches de dessin",
+  "任务日志": "Journaux de tâches",
+  "系统任务记录": "Enregistrements de tâches système",
+  "个人中心区域": "Zone du centre personnel",
+  "用户个人功能": "Fonctions personnelles de l'utilisateur",
+  "钱包管理": "Gestion du portefeuille",
+  "余额充值管理": "Gestion de la recharge du solde",
+  "个人设置": "Paramètres personnels",
+  "个人信息设置": "Paramètres des informations personnelles",
+  "管理员区域": "Zone administrateur",
+  "系统管理功能": "Fonctions de gestion du système",
+  "渠道管理": "Gestion des canaux",
+  "API渠道配置": "Configuration du canal de l'API",
+  "模型管理": "Gestion des modèles",
+  "AI模型配置": "Configuration du modèle d'IA",
+  "兑换码管理": "Gestion des codes d'échange",
+  "兑换码生成管理": "Gestion de la génération de codes d'échange",
+  "用户管理": "Gestion des utilisateurs",
+  "用户账户管理": "Gestion des comptes utilisateurs",
+  "系统设置": "Paramètres système",
+  "系统参数配置": "Configuration des paramètres système",
+  "边栏设置": "Paramètres de la barre latérale",
+  "您可以个性化设置侧边栏的要显示功能": "Vous pouvez personnaliser les fonctions de la barre latérale à afficher",
+  "保存边栏设置": "Enregistrer les paramètres de la barre latérale",
+  "侧边栏设置保存成功": "Paramètres de la barre latérale enregistrés avec succès",
+  "需要登录访问": "Nécessite une connexion",
+  "开启后未登录用户无法访问模型广场": "Lorsqu'il est activé, les utilisateurs non authentifiés ne peuvent pas accéder à la place du marché des modèles",
+  "参与官方同步": "Participer à la synchronisation officielle",
+  "关闭后,此模型将不会被\"同步官方\"自动覆盖或创建": "Lorsqu'il est désactivé, ce modèle sera ignoré par la synchronisation officielle (pas de création/remplacement automatique)",
+  "同步": "Synchroniser",
+  "同步向导": "Assistant de synchronisation",
+  "选择方式": "Sélectionner la méthode",
+  "选择同步来源": "Sélectionner la source de synchronisation",
+  "选择语言": "Sélectionner la langue",
+  "选择同步语言": "Sélectionner la langue de synchronisation",
+  "请选择同步语言": "Veuillez sélectionner la langue de synchronisation",
+  "从官方模型库同步": "Synchroniser depuis la bibliothèque de modèles officielle",
+  "官方模型同步": "Synchronisation des modèles officiels",
+  "从配置文件同步": "Synchroniser depuis un fichier de configuration",
+  "配置文件同步": "Synchronisation du fichier de configuration",
+  "开始同步": "Démarrer la synchronisation",
+  "选择要覆盖的冲突项": "Sélectionner les éléments en conflit à remplacer",
+  "点击查看差异": "Cliquez pour voir les différences",
+  "无冲突项": "Aucun élément en conflit",
+  "应用覆盖": "Appliquer le remplacement",
+  "仅会覆盖你勾选的字段,未勾选的字段保持本地不变。": "Seuls les champs sélectionnés seront remplacés, les champs non sélectionnés restent inchangés.",
+  "本地": "Local",
+  "官方": "Officiel",
+  "模型社区需要大家的共同维护,如发现数据有误或想贡献新的模型数据,请访问:": "La communauté des modèles a besoin de la contribution de tous. Si vous trouvez des données incorrectes ou si vous souhaitez contribuer à de nouvelles données de modèle, veuillez visiter :",
+  "是": "Oui",
+  "否": "Non",
+  "原价": "Prix original",
+  "优惠": "Remise",
+  "折": "% de réduction",
+  "节省": "Économiser",
+  "今天": "Aujourd'hui",
+  "近 7 天": "7 derniers jours",
+  "本周": "Cette semaine",
+  "本月": "Ce mois-ci",
+  "近 30 天": "30 derniers jours",
+  "代理设置": "Paramètres du proxy",
+  "更新Worker设置": "Mettre à jour les paramètres du worker",
+  "SSRF防护设置": "Paramètres de protection SSRF",
+  "配置服务器端请求伪造(SSRF)防护,用于保护内网资源安全": "Configurez la protection contre la falsification de requêtes côté serveur (SSRF) pour sécuriser les ressources du réseau interne",
+  "SSRF防护详细说明": "La protection SSRF empêche les utilisateurs malveillants d'utiliser votre serveur pour accéder aux ressources du réseau interne. Configurez des listes blanches pour les domaines/IP de confiance et limitez les ports autorisés. S'applique aux téléchargements de fichiers, aux webhooks et aux notifications.",
+  "启用SSRF防护(推荐开启以保护服务器安全)": "Activer la protection SSRF (recommandé pour la sécurité du serveur)",
+  "SSRF防护开关详细说明": "L'interrupteur principal contrôle si la protection SSRF est activée. Lorsqu'elle est désactivée, toutes les vérifications SSRF sont contournées, autorisant l'accès à n'importe quelle URL. ⚠️ Ne désactivez cette fonctionnalité que dans des environnements entièrement fiables.",
+  "允许访问私有IP地址(127.0.0.1、192.168.x.x等内网地址)": "Autoriser l'accès aux adresses IP privées (127.0.0.1, 192.168.x.x et autres adresses de réseau interne)",
+  "私有IP访问详细说明": "⚠️ Avertissement de sécurité : l'activation de cette option autorise l'accès aux ressources du réseau interne (localhost, réseaux privés). N'activez cette option que si vous devez accéder à des services internes et que vous comprenez les implications en matière de sécurité.",
+  "域名白名单": "Liste blanche de domaines",
+  "支持通配符格式,如:example.com, *.api.example.com": "Prend en charge le format générique, par exemple : example.com, *.api.example.com",
+  "域名白名单详细说明": "Les domaines sur liste blanche contournent toutes les vérifications SSRF et sont autorisés à y accéder directement. Prend en charge les domaines exacts (example.com) ou les caractères génériques (*.api.example.com) pour les sous-domaines. Lorsque la liste blanche est vide, tous les domaines passent par la validation SSRF.",
+  "输入域名后回车,如:example.com": "Saisissez le domaine et appuyez sur Entrée, par exemple : example.com",
+  "支持CIDR格式,如:8.8.8.8, 192.168.1.0/24": "Prend en charge le format CIDR, par exemple : 8.8.8.8, 192.168.1.0/24",
+  "IP白名单详细说明": "Contrôle les adresses IP autorisées à accéder. Utilisez des adresses IP uniques (8.8.8.8) ou la notation CIDR (192.168.1.0/24). Une liste blanche vide autorise toutes les adresses IP (sous réserve des paramètres d'adresses IP privées), une liste blanche non vide n'autorise que les adresses IP répertoriées.",
+  "输入IP地址后回车,如:8.8.8.8": "Saisissez l'adresse IP et appuyez sur Entrée, par exemple : 8.8.8.8",
+  "允许的端口": "Ports autorisés",
+  "支持单个端口和端口范围,如:80, 443, 8000-8999": "Prend en charge les ports uniques et les plages de ports, par exemple : 80, 443, 8000-8999",
+  "端口配置详细说明": "Limitez les requêtes externes à des ports spécifiques. Utilisez des ports uniques (80, 443) ou des plages (8000-8999). Une liste vide autorise tous les ports. La valeur par défaut inclut les ports Web courants.",
+  "输入端口后回车,如:80 或 8000-8999": "Saisissez le port et appuyez sur Entrée, par exemple : 80 ou 8000-8999",
+  "更新SSRF防护设置": "Mettre à jour les paramètres de protection SSRF",
+  "对域名启用 IP 过滤(实验性)": "Activer le filtrage IP pour les domaines (expérimental)",
+  "域名IP过滤详细说明": "⚠️ Il s'agit d'une option expérimentale. Un domaine peut se résoudre en plusieurs adresses IPv4/IPv6. Si cette option est activée, assurez-vous que la liste de filtres IP couvre ces adresses, sinon l'accès peut échouer.",
+  "域名黑名单": "Liste noire de domaines",
+  "白名单": "Liste blanche",
+  "黑名单": "Liste noire",
+  "切换语言": "Changer de langue",
+  "closeSidebar": "Fermer la barre latérale",
+  "pricing": "Tarification",
+  "language": "Langue",
+  "关闭侧边栏": "Fermer la barre latérale",
+  "定价": "Tarification",
+  "语言": "Langue",
+  "common": {
+    "changeLanguage": "Changer de langue"
+  }
+}

+ 3 - 0
web/src/i18n/locales/zh.json

@@ -33,5 +33,8 @@
   "输入端口后回车,如:80 或 8000-8999": "输入端口后回车,如:80 或 8000-8999",
   "更新SSRF防护设置": "更新SSRF防护设置",
   "域名IP过滤详细说明": "⚠️此功能为实验性选项,域名可能解析到多个 IPv4/IPv6 地址,若开启,请确保 IP 过滤列表覆盖这些地址,否则可能导致访问失败。",
+  "common": {
+    "changeLanguage": "切换语言"
+  },
   "允许在 Stripe 支付中输入促销码": "允许在 Stripe 支付中输入促销码"
 }