Browse Source

feat: auto分组

creamlike1024 6 months ago
parent
commit
7fa21ce95f

+ 8 - 1
controller/group.go

@@ -1,10 +1,11 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/model"
 	"one-api/setting"
+
+	"github.com/gin-gonic/gin"
 )
 
 func GetGroups(c *gin.Context) {
@@ -34,6 +35,12 @@ func GetUserGroups(c *gin.Context) {
 			}
 		}
 	}
+	if setting.GroupInUserUsableGroups("auto") {
+		usableGroups["auto"] = map[string]interface{}{
+			"ratio": "自动",
+			"desc":  setting.GetUsableGroupDescription("auto"),
+		}
+	}
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",

+ 40 - 39
controller/misc.go

@@ -9,9 +9,9 @@ import (
 	"one-api/middleware"
 	"one-api/model"
 	"one-api/setting"
+	"one-api/setting/console_setting"
 	"one-api/setting/operation_setting"
 	"one-api/setting/system_setting"
-	"one-api/setting/console_setting"
 	"strings"
 
 	"github.com/gin-gonic/gin"
@@ -41,46 +41,47 @@ func GetStatus(c *gin.Context) {
 	cs := console_setting.GetConsoleSetting()
 
 	data := gin.H{
-		"version":                     common.Version,
-		"start_time":                  common.StartTime,
-		"email_verification":          common.EmailVerificationEnabled,
-		"github_oauth":                common.GitHubOAuthEnabled,
-		"github_client_id":            common.GitHubClientId,
-		"linuxdo_oauth":               common.LinuxDOOAuthEnabled,
-		"linuxdo_client_id":           common.LinuxDOClientId,
-		"telegram_oauth":              common.TelegramOAuthEnabled,
-		"telegram_bot_name":           common.TelegramBotName,
-		"system_name":                 common.SystemName,
-		"logo":                        common.Logo,
-		"footer_html":                 common.Footer,
-		"wechat_qrcode":               common.WeChatAccountQRCodeImageURL,
-		"wechat_login":                common.WeChatAuthEnabled,
-		"server_address":              setting.ServerAddress,
-		"price":                       setting.Price,
-		"min_topup":                   setting.MinTopUp,
-		"turnstile_check":             common.TurnstileCheckEnabled,
-		"turnstile_site_key":          common.TurnstileSiteKey,
-		"top_up_link":                 common.TopUpLink,
-		"docs_link":                   operation_setting.GetGeneralSetting().DocsLink,
-		"quota_per_unit":              common.QuotaPerUnit,
-		"display_in_currency":         common.DisplayInCurrencyEnabled,
-		"enable_batch_update":         common.BatchUpdateEnabled,
-		"enable_drawing":              common.DrawingEnabled,
-		"enable_task":                 common.TaskEnabled,
-		"enable_data_export":          common.DataExportEnabled,
-		"data_export_default_time":    common.DataExportDefaultTime,
-		"default_collapse_sidebar":    common.DefaultCollapseSidebar,
-		"enable_online_topup":         setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
-		"mj_notify_enabled":           setting.MjNotifyEnabled,
-		"chats":                       setting.Chats,
-		"demo_site_enabled":           operation_setting.DemoSiteEnabled,
-		"self_use_mode_enabled":       operation_setting.SelfUseModeEnabled,
+		"version":                  common.Version,
+		"start_time":               common.StartTime,
+		"email_verification":       common.EmailVerificationEnabled,
+		"github_oauth":             common.GitHubOAuthEnabled,
+		"github_client_id":         common.GitHubClientId,
+		"linuxdo_oauth":            common.LinuxDOOAuthEnabled,
+		"linuxdo_client_id":        common.LinuxDOClientId,
+		"telegram_oauth":           common.TelegramOAuthEnabled,
+		"telegram_bot_name":        common.TelegramBotName,
+		"system_name":              common.SystemName,
+		"logo":                     common.Logo,
+		"footer_html":              common.Footer,
+		"wechat_qrcode":            common.WeChatAccountQRCodeImageURL,
+		"wechat_login":             common.WeChatAuthEnabled,
+		"server_address":           setting.ServerAddress,
+		"price":                    setting.Price,
+		"min_topup":                setting.MinTopUp,
+		"turnstile_check":          common.TurnstileCheckEnabled,
+		"turnstile_site_key":       common.TurnstileSiteKey,
+		"top_up_link":              common.TopUpLink,
+		"docs_link":                operation_setting.GetGeneralSetting().DocsLink,
+		"quota_per_unit":           common.QuotaPerUnit,
+		"display_in_currency":      common.DisplayInCurrencyEnabled,
+		"enable_batch_update":      common.BatchUpdateEnabled,
+		"enable_drawing":           common.DrawingEnabled,
+		"enable_task":              common.TaskEnabled,
+		"enable_data_export":       common.DataExportEnabled,
+		"data_export_default_time": common.DataExportDefaultTime,
+		"default_collapse_sidebar": common.DefaultCollapseSidebar,
+		"enable_online_topup":      setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
+		"mj_notify_enabled":        setting.MjNotifyEnabled,
+		"chats":                    setting.Chats,
+		"demo_site_enabled":        operation_setting.DemoSiteEnabled,
+		"self_use_mode_enabled":    operation_setting.SelfUseModeEnabled,
+		"default_use_auto_group":   setting.DefaultUseAutoGroup,
 
 		// 面板启用开关
-		"api_info_enabled":            cs.ApiInfoEnabled,
-		"uptime_kuma_enabled":         cs.UptimeKumaEnabled,
-		"announcements_enabled":       cs.AnnouncementsEnabled,
-		"faq_enabled":                 cs.FAQEnabled,
+		"api_info_enabled":      cs.ApiInfoEnabled,
+		"uptime_kuma_enabled":   cs.UptimeKumaEnabled,
+		"announcements_enabled": cs.AnnouncementsEnabled,
+		"faq_enabled":           cs.FAQEnabled,
 
 		"oidc_enabled":                system_setting.GetOIDCSettings().Enabled,
 		"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,

+ 16 - 2
controller/model.go

@@ -2,7 +2,6 @@ package controller
 
 import (
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/common"
 	"one-api/constant"
@@ -15,6 +14,9 @@ import (
 	"one-api/relay/channel/moonshot"
 	relaycommon "one-api/relay/common"
 	relayconstant "one-api/relay/constant"
+	"one-api/setting"
+
+	"github.com/gin-gonic/gin"
 )
 
 // https://platform.openai.com/docs/api-reference/models/list
@@ -179,7 +181,19 @@ func ListModels(c *gin.Context) {
 		if tokenGroup != "" {
 			group = tokenGroup
 		}
-		models := model.GetGroupModels(group)
+		var models []string
+		if tokenGroup == "auto" {
+			for _, autoGroup := range setting.AutoGroups {
+				groupModels := model.GetGroupModels(autoGroup)
+				for _, g := range groupModels {
+					if !common.StringsContains(models, g) {
+						models = append(models, g)
+					}
+				}
+			}
+		} else {
+			models = model.GetGroupModels(group)
+		}
 		for _, s := range models {
 			if _, ok := openAIModelsMap[s]; ok {
 				userOpenAiModels = append(userOpenAiModels, openAIModelsMap[s])

+ 4 - 3
controller/playground.go

@@ -3,7 +3,6 @@ package controller
 import (
 	"errors"
 	"fmt"
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/common"
 	"one-api/constant"
@@ -13,6 +12,8 @@ import (
 	"one-api/service"
 	"one-api/setting"
 	"time"
+
+	"github.com/gin-gonic/gin"
 )
 
 func Playground(c *gin.Context) {
@@ -57,9 +58,9 @@ func Playground(c *gin.Context) {
 		c.Set("group", group)
 	}
 	c.Set("token_name", "playground-"+group)
-	channel, err := model.CacheGetRandomSatisfiedChannel(group, playgroundRequest.Model, 0)
+	channel, finalGroup, err := model.CacheGetRandomSatisfiedChannel(c, group, playgroundRequest.Model, 0)
 	if err != nil {
-		message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", group, playgroundRequest.Model)
+		message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", finalGroup, playgroundRequest.Model)
 		openaiErr = service.OpenAIErrorWrapperLocal(errors.New(message), "get_playground_channel_failed", http.StatusInternalServerError)
 		return
 	}

+ 2 - 2
controller/relay.go

@@ -259,7 +259,7 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m
 			AutoBan: &autoBanInt,
 		}, nil
 	}
-	channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, retryCount)
+	channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, retryCount)
 	if err != nil {
 		return nil, errors.New(fmt.Sprintf("获取重试渠道失败: %s", err.Error()))
 	}
@@ -388,7 +388,7 @@ func RelayTask(c *gin.Context) {
 		retryTimes = 0
 	}
 	for i := 0; shouldRetryTaskRelay(c, channelId, taskErr, retryTimes) && i < retryTimes; i++ {
-		channel, err := model.CacheGetRandomSatisfiedChannel(group, originalModel, i)
+		channel, _, err := model.CacheGetRandomSatisfiedChannel(c, group, originalModel, i)
 		if err != nil {
 			common.LogError(c, fmt.Sprintf("CacheGetRandomSatisfiedChannel failed: %s", err.Error()))
 			break

+ 3 - 0
controller/user.go

@@ -226,6 +226,9 @@ func Register(c *gin.Context) {
 			UnlimitedQuota:     true,
 			ModelLimitsEnabled: false,
 		}
+		if setting.DefaultUseAutoGroup {
+			token.Group = "auto"
+		}
 		if err := token.Insert(); err != nil {
 			c.JSON(http.StatusOK, gin.H{
 				"success": false,

+ 11 - 4
middleware/distributor.go

@@ -49,8 +49,10 @@ func Distribute() func(c *gin.Context) {
 			}
 			// check group in common.GroupRatio
 			if !setting.ContainsGroupRatio(tokenGroup) {
-				abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
-				return
+				if tokenGroup != "auto" {
+					abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
+					return
+				}
 			}
 			userGroup = tokenGroup
 		}
@@ -95,9 +97,14 @@ func Distribute() func(c *gin.Context) {
 			}
 
 			if shouldSelectChannel {
-				channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, 0)
+				var selectGroup string
+				channel, selectGroup, err = model.CacheGetRandomSatisfiedChannel(c, userGroup, modelRequest.Model, 0)
 				if err != nil {
-					message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
+					showGroup := userGroup
+					if userGroup == "auto" {
+						showGroup = fmt.Sprintf("auto(%s)", selectGroup)
+					}
+					message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", showGroup, modelRequest.Model)
 					// 如果错误,但是渠道不为空,说明是数据库一致性问题
 					if channel != nil {
 						common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id))

+ 37 - 1
model/cache.go

@@ -3,12 +3,16 @@ package model
 import (
 	"errors"
 	"fmt"
+	"log"
 	"math/rand"
 	"one-api/common"
+	"one-api/setting"
 	"sort"
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/gin-gonic/gin"
 )
 
 var group2model2channels map[string]map[string][]*Channel
@@ -75,7 +79,39 @@ func SyncChannelCache(frequency int) {
 	}
 }
 
-func CacheGetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
+func CacheGetRandomSatisfiedChannel(c *gin.Context, group string, model string, retry int) (*Channel, string, error) {
+	var channel *Channel
+	var err error
+	selectGroup := group
+	if group == "auto" {
+		if len(setting.AutoGroups) == 0 {
+			return nil, selectGroup, errors.New("auto groups is not enabled")
+		}
+		for _, autoGroup := range setting.AutoGroups {
+			log.Printf("autoGroup: %s", autoGroup)
+			channel, _ = getRandomSatisfiedChannel(autoGroup, model, retry)
+			if channel == nil {
+				continue
+			} else {
+				c.Set("auto_group", autoGroup)
+				selectGroup = autoGroup
+				log.Printf("selectGroup: %s", selectGroup)
+				break
+			}
+		}
+	} else {
+		channel, err = getRandomSatisfiedChannel(group, model, retry)
+		if err != nil {
+			return nil, group, err
+		}
+	}
+	if channel == nil {
+		return nil, group, errors.New("channel not found")
+	}
+	return channel, selectGroup, nil
+}
+
+func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, error) {
 	if strings.HasPrefix(model, "gpt-4-gizmo") {
 		model = "gpt-4-gizmo-*"
 	}

+ 6 - 0
model/option.go

@@ -76,6 +76,8 @@ func InitOptionMap() {
 	common.OptionMap["MinTopUp"] = strconv.Itoa(setting.MinTopUp)
 	common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString()
 	common.OptionMap["Chats"] = setting.Chats2JsonString()
+	common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString()
+	common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup)
 	common.OptionMap["GitHubClientId"] = ""
 	common.OptionMap["GitHubClientSecret"] = ""
 	common.OptionMap["TelegramBotToken"] = ""
@@ -287,6 +289,10 @@ func updateOptionMap(key string, value string) (err error) {
 		setting.PayAddress = value
 	case "Chats":
 		err = setting.UpdateChatsByJsonString(value)
+	case "AutoGroups":
+		err = setting.UpdateAutoGroupsByJsonString(value)
+	case "DefaultUseAutoGroup":
+		setting.DefaultUseAutoGroup = value == "true"
 	case "CustomCallbackAddress":
 		setting.CustomCallbackAddress = value
 	case "EpayId":

+ 11 - 1
relay/helper/price.go

@@ -2,6 +2,7 @@ package helper
 
 import (
 	"fmt"
+	"log"
 	"one-api/common"
 	constant2 "one-api/constant"
 	relaycommon "one-api/relay/common"
@@ -31,10 +32,19 @@ func (p PriceData) ToSetting() string {
 func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
 	modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false)
 	groupRatio := setting.GetGroupRatio(info.Group)
+	var userGroupRatio float64
+	autoGroup, exists := c.Get("auto_group")
+	if exists {
+		groupRatio = setting.GetGroupRatio(autoGroup.(string))
+		log.Printf("final group ratio: %f", groupRatio)
+		info.Group = autoGroup.(string)
+	}
+	actualGroupRatio := groupRatio
 	userGroupRatio, ok := setting.GetGroupGroupRatio(info.UserGroup, info.Group)
 	if ok {
-		groupRatio = userGroupRatio
+		actualGroupRatio = userGroupRatio
 	}
+	groupRatio = actualGroupRatio
 	var preConsumedQuota int
 	var modelRatio float64
 	var completionRatio float64

+ 27 - 3
service/quota.go

@@ -3,6 +3,7 @@ package service
 import (
 	"errors"
 	"fmt"
+	"log"
 	"one-api/common"
 	constant2 "one-api/constant"
 	"one-api/dto"
@@ -94,11 +95,20 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
 	audioInputTokens := usage.InputTokenDetails.AudioTokens
 	audioOutTokens := usage.OutputTokenDetails.AudioTokens
 	groupRatio := setting.GetGroupRatio(relayInfo.Group)
+	modelRatio, _ := operation_setting.GetModelRatio(modelName)
+
+	autoGroup, exists := ctx.Get("auto_group")
+	if exists {
+		groupRatio = setting.GetGroupRatio(autoGroup.(string))
+		log.Printf("final group ratio: %f", groupRatio)
+		relayInfo.Group = autoGroup.(string)
+	}
+
+	actualGroupRatio := groupRatio
 	userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group)
 	if ok {
-		groupRatio = userGroupRatio
+		actualGroupRatio = userGroupRatio
 	}
-	modelRatio, _ := operation_setting.GetModelRatio(modelName)
 
 	quotaInfo := QuotaInfo{
 		InputDetails: TokenDetails{
@@ -112,7 +122,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
 		ModelName:  modelName,
 		UsePrice:   relayInfo.UsePrice,
 		ModelRatio: modelRatio,
-		GroupRatio: groupRatio,
+		GroupRatio: actualGroupRatio,
 	}
 
 	quota := calculateAudioQuota(quotaInfo)
@@ -149,6 +159,13 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
 	audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName))
 	audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(modelName))
 
+	autoGroup, exists := ctx.Get("auto_group")
+	if exists {
+		groupRatio = setting.GetGroupRatio(autoGroup.(string))
+		log.Printf("final group ratio: %f", groupRatio)
+		relayInfo.Group = autoGroup.(string)
+	}
+
 	actualGroupRatio := groupRatio
 	userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group)
 	if ok {
@@ -290,6 +307,13 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
 	modelPrice := priceData.ModelPrice
 	usePrice := priceData.UsePrice
 
+	autoGroup, exists := ctx.Get("auto_group")
+	if exists {
+		groupRatio = setting.GetGroupRatio(autoGroup.(string))
+		log.Printf("final group ratio: %f", groupRatio)
+		relayInfo.Group = autoGroup.(string)
+	}
+
 	actualGroupRatio := groupRatio
 	userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group)
 	if ok {

+ 31 - 0
setting/auto_group.go

@@ -0,0 +1,31 @@
+package setting
+
+import "encoding/json"
+
+var AutoGroups = []string{
+	"default",
+}
+
+var DefaultUseAutoGroup = false
+
+func ContainsAutoGroup(group string) bool {
+	for _, autoGroup := range AutoGroups {
+		if autoGroup == group {
+			return true
+		}
+	}
+	return false
+}
+
+func UpdateAutoGroupsByJsonString(jsonString string) error {
+	AutoGroups = make([]string, 0)
+	return json.Unmarshal([]byte(jsonString), &AutoGroups)
+}
+
+func AutoGroups2JsonString() string {
+	jsonBytes, err := json.Marshal(AutoGroups)
+	if err != nil {
+		return "[]"
+	}
+	return string(jsonBytes)
+}

+ 7 - 0
setting/user_usable_group.go

@@ -50,3 +50,10 @@ func GroupInUserUsableGroups(groupName string) bool {
 	_, ok := userUsableGroups[groupName]
 	return ok
 }
+
+func GetUsableGroupDescription(groupName string) string {
+	if desc, ok := userUsableGroups[groupName]; ok {
+		return desc
+	}
+	return groupName
+}

+ 5 - 1
web/src/components/settings/OperationSetting.js

@@ -31,6 +31,8 @@ const OperationSetting = () => {
     ModelPrice: '',
     GroupRatio: '',
     GroupGroupRatio: '',
+    AutoGroups: '',
+    DefaultUseAutoGroup: false,
     UserUsableGroups: '',
     TopUpLink: '',
     'general_setting.docs_link': '',
@@ -76,6 +78,7 @@ const OperationSetting = () => {
           item.key === 'ModelRatio' ||
           item.key === 'GroupRatio' ||
           item.key === 'GroupGroupRatio' ||
+          item.key === 'AutoGroups' ||
           item.key === 'UserUsableGroups' ||
           item.key === 'CompletionRatio' ||
           item.key === 'ModelPrice' ||
@@ -85,7 +88,8 @@ const OperationSetting = () => {
         }
         if (
           item.key.endsWith('Enabled') ||
-          ['DefaultCollapseSidebar'].includes(item.key)
+          ['DefaultCollapseSidebar'].includes(item.key) ||
+          ['DefaultUseAutoGroup'].includes(item.key)
         ) {
           newInputs[item.key] = item.value === 'true' ? true : false;
         } else {

+ 36 - 0
web/src/pages/Setting/Operation/GroupRatioSettings.js

@@ -17,6 +17,8 @@ export default function GroupRatioSettings(props) {
     GroupRatio: '',
     UserUsableGroups: '',
     GroupGroupRatio: '',
+    AutoGroups: '',
+    DefaultUseAutoGroup: false,
   });
   const refForm = useRef();
   const [inputsRow, setInputsRow] = useState(inputs);
@@ -167,6 +169,40 @@ export default function GroupRatioSettings(props) {
               />
             </Col>
           </Row>
+          <Row gutter={16}>
+            <Col xs={24} sm={16}>
+              <Form.TextArea
+                label={t('自动分组auto,从第一个开始选择')}
+                placeholder={t('为一个 JSON 文本')}
+                field={'AutoGroups'}
+                autosize={{ minRows: 6, maxRows: 12 }}
+                trigger='blur'
+                stopValidateWithError
+                rules={[
+                  {
+                    validator: (rule, value) => verifyJSON(value),
+                    message: t('不是合法的 JSON 字符串'),
+                  },
+                ]}
+                onChange={(value) =>
+                  setInputs({ ...inputs, AutoGroups: value })
+                }
+              />
+            </Col>
+          </Row>
+          <Row gutter={16}>
+            <Col span={16}>
+              <Form.Switch
+                label={t(
+                  '创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)',
+                )}
+                field={'DefaultUseAutoGroup'}
+                onChange={(value) =>
+                  setInputs({ ...inputs, DefaultUseAutoGroup: value })
+                }
+              />
+            </Col>
+          </Row>
         </Form.Section>
       </Form>
       <Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>

+ 233 - 129
web/src/pages/Token/EditToken.js

@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useContext } from 'react';
 import { useNavigate } from 'react-router-dom';
 import {
   API,
@@ -7,7 +7,7 @@ import {
   showSuccess,
   timestamp2string,
   renderGroupOption,
-  renderQuotaWithPrompt
+  renderQuotaWithPrompt,
 } from '../../helpers';
 import {
   AutoComplete,
@@ -37,11 +37,13 @@ import {
   IconPlusCircle,
 } from '@douyinfe/semi-icons';
 import { useTranslation } from 'react-i18next';
+import { StatusContext } from '../../context/Status';
 
 const { Text, Title } = Typography;
 
 const EditToken = (props) => {
   const { t } = useTranslation();
+  const [statusState, statusDispatch] = useContext(StatusContext);
   const [isEdit, setIsEdit] = useState(false);
   const [loading, setLoading] = useState(isEdit);
   const originInputs = {
@@ -119,7 +121,19 @@ const EditToken = (props) => {
         value: group,
         ratio: info.ratio,
       }));
+      if (statusState?.status?.default_use_auto_group) {
+        // if contain auto, add it to the first position
+        if (localGroupOptions.some((group) => group.value === 'auto')) {
+          // 排序
+          localGroupOptions.sort((a, b) => (a.value === 'auto' ? -1 : 1));
+        } else {
+          localGroupOptions.unshift({ label: t('自动选择'), value: 'auto' });
+        }
+      }
       setGroups(localGroupOptions);
+      if (statusState?.status?.default_use_auto_group) {
+        setInputs({ ...inputs, group: 'auto' });
+      }
     } else {
       showError(t(message));
     }
@@ -268,32 +282,37 @@ const EditToken = (props) => {
       placement={isEdit ? 'right' : 'left'}
       title={
         <Space>
-          {isEdit ?
-            <Tag color="blue" shape="circle">{t('更新')}</Tag> :
-            <Tag color="green" shape="circle">{t('新建')}</Tag>
-          }
-          <Title heading={4} className="m-0">
+          {isEdit ? (
+            <Tag color='blue' shape='circle'>
+              {t('更新')}
+            </Tag>
+          ) : (
+            <Tag color='green' shape='circle'>
+              {t('新建')}
+            </Tag>
+          )}
+          <Title heading={4} className='m-0'>
             {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
           </Title>
         </Space>
       }
       headerStyle={{
         borderBottom: '1px solid var(--semi-color-border)',
-        padding: '24px'
+        padding: '24px',
       }}
       bodyStyle={{
         backgroundColor: 'var(--semi-color-bg-0)',
-        padding: '0'
+        padding: '0',
       }}
       visible={props.visiable}
       width={isMobile() ? '100%' : 600}
       footer={
-        <div className="flex justify-end bg-white">
+        <div className='flex justify-end bg-white'>
           <Space>
             <Button
-              theme="solid"
-              size="large"
-              className="!rounded-full"
+              theme='solid'
+              size='large'
+              className='!rounded-full'
               onClick={submit}
               icon={<IconSave />}
               loading={loading}
@@ -301,10 +320,10 @@ const EditToken = (props) => {
               {t('提交')}
             </Button>
             <Button
-              theme="light"
-              size="large"
-              className="!rounded-full"
-              type="primary"
+              theme='light'
+              size='large'
+              className='!rounded-full'
+              type='primary'
               onClick={handleCancel}
               icon={<IconClose />}
             >
@@ -317,87 +336,107 @@ const EditToken = (props) => {
       onCancel={() => handleCancel()}
     >
       <Spin spinning={loading}>
-        <div className="p-6">
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+        <div className='p-6'>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #1e3a8a 0%, #2563eb 50%, #3b82f6 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconPlusCircle size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconPlusCircle size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('基本信息')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的基本信息')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('基本信息')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的基本信息')}
+                </div>
               </div>
             </div>
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <Text strong className="block mb-2">{t('名称')}</Text>
+                <Text strong className='block mb-2'>
+                  {t('名称')}
+                </Text>
                 <Input
                   placeholder={t('请输入名称')}
                   onChange={(value) => handleInputChange('name', value)}
                   value={name}
-                  autoComplete="new-password"
-                  size="large"
-                  className="!rounded-lg"
+                  autoComplete='new-password'
+                  size='large'
+                  className='!rounded-lg'
                   showClear
                   required
                 />
               </div>
 
               <div>
-                <Text strong className="block mb-2">{t('过期时间')}</Text>
-                <div className="mb-2">
+                <Text strong className='block mb-2'>
+                  {t('过期时间')}
+                </Text>
+                <div className='mb-2'>
                   <DatePicker
                     placeholder={t('请选择过期时间')}
-                    onChange={(value) => handleInputChange('expired_time', value)}
+                    onChange={(value) =>
+                      handleInputChange('expired_time', value)
+                    }
                     value={expired_time}
-                    autoComplete="new-password"
-                    type="dateTime"
-                    className="w-full !rounded-lg"
-                    size="large"
+                    autoComplete='new-password'
+                    type='dateTime'
+                    className='w-full !rounded-lg'
+                    size='large'
                     prefix={<IconCalendar />}
                   />
                 </div>
 
-                <div className="flex flex-wrap gap-2">
+                <div className='flex flex-wrap gap-2'>
                   <Button
-                    theme="light"
-                    type="primary"
+                    theme='light'
+                    type='primary'
                     onClick={() => setExpiredTime(0, 0, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                   >
                     {t('永不过期')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(0, 0, 1, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconClock />}
                   >
                     {t('一小时')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(0, 1, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconCalendar />}
                   >
                     {t('一天')}
                   </Button>
                   <Button
-                    theme="light"
-                    type="tertiary"
+                    theme='light'
+                    type='tertiary'
                     onClick={() => setExpiredTime(1, 0, 0, 0)}
-                    className="!rounded-full"
+                    className='!rounded-full'
                     icon={<IconCalendar />}
                   >
                     {t('一个月')}
@@ -407,44 +446,62 @@ const EditToken = (props) => {
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #065f46 0%, #059669 50%, #10b981 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconCreditCard size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconCreditCard size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('额度设置')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌可用额度和数量')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('额度设置')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌可用额度和数量')}
+                </div>
               </div>
             </div>
 
             <Banner
-              type="warning"
-              description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
-              className="mb-4 !rounded-lg"
+              type='warning'
+              description={t(
+                '注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
+              )}
+              className='mb-4 !rounded-lg'
             />
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <div className="flex justify-between mb-2">
+                <div className='flex justify-between mb-2'>
                   <Text strong>{t('额度')}</Text>
-                  <Text type="tertiary">{renderQuotaWithPrompt(remain_quota)}</Text>
+                  <Text type='tertiary'>
+                    {renderQuotaWithPrompt(remain_quota)}
+                  </Text>
                 </div>
                 <AutoComplete
                   placeholder={t('请输入额度')}
                   onChange={(value) => handleInputChange('remain_quota', value)}
                   value={remain_quota}
-                  autoComplete="new-password"
-                  type="number"
-                  size="large"
-                  className="w-full !rounded-lg"
+                  autoComplete='new-password'
+                  type='number'
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconCreditCard />}
                   data={[
                     { value: 500000, label: '1$' },
@@ -460,16 +517,18 @@ const EditToken = (props) => {
 
               {!isEdit && (
                 <div>
-                  <Text strong className="block mb-2">{t('新建数量')}</Text>
+                  <Text strong className='block mb-2'>
+                    {t('新建数量')}
+                  </Text>
                   <AutoComplete
                     placeholder={t('请选择或输入创建令牌的数量')}
                     onChange={(value) => handleTokenCountChange(value)}
                     onSelect={(value) => handleTokenCountChange(value)}
                     value={tokenCount.toString()}
-                    autoComplete="off"
-                    type="number"
-                    className="w-full !rounded-lg"
-                    size="large"
+                    autoComplete='off'
+                    type='number'
+                    className='w-full !rounded-lg'
+                    size='large'
                     prefix={<IconPlusCircle />}
                     data={[
                       { value: 10, label: t('10个') },
@@ -482,12 +541,12 @@ const EditToken = (props) => {
                 </div>
               )}
 
-              <div className="flex justify-end">
+              <div className='flex justify-end'>
                 <Button
-                  theme="light"
-                  type={unlimited_quota ? "danger" : "warning"}
+                  theme='light'
+                  type={unlimited_quota ? 'danger' : 'warning'}
                   onClick={setUnlimitedQuota}
-                  className="!rounded-full"
+                  className='!rounded-full'
                 >
                   {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
                 </Button>
@@ -495,92 +554,137 @@ const EditToken = (props) => {
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0 mb-6'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #4c1d95 0%, #6d28d9 50%, #7c3aed 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconLink size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconLink size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('访问限制')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的访问限制')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('访问限制')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的访问限制')}
+                </div>
               </div>
             </div>
 
-            <div className="space-y-4">
+            <div className='space-y-4'>
               <div>
-                <Text strong className="block mb-2">{t('IP白名单')}</Text>
+                <Text strong className='block mb-2'>
+                  {t('IP白名单')}
+                </Text>
                 <TextArea
                   placeholder={t('允许的IP,一行一个,不填写则不限制')}
                   onChange={(value) => handleInputChange('allow_ips', value)}
                   value={inputs.allow_ips}
                   style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-                  className="!rounded-lg"
+                  className='!rounded-lg'
                   rows={4}
                 />
-                <Text type="tertiary" className="mt-1 block text-xs">{t('请勿过度信任此功能,IP可能被伪造')}</Text>
+                <Text type='tertiary' className='mt-1 block text-xs'>
+                  {t('请勿过度信任此功能,IP可能被伪造')}
+                </Text>
               </div>
 
               <div>
-                <div className="flex items-center mb-2">
+                <div className='flex items-center mb-2'>
                   <Checkbox
                     checked={model_limits_enabled}
-                    onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
+                    onChange={(e) =>
+                      handleInputChange(
+                        'model_limits_enabled',
+                        e.target.checked,
+                      )
+                    }
                   >
                     <Text strong>{t('模型限制')}</Text>
                   </Checkbox>
                 </div>
                 <Select
-                  placeholder={model_limits_enabled ? t('请选择该渠道所支持的模型') : t('勾选启用模型限制后可选择')}
+                  placeholder={
+                    model_limits_enabled
+                      ? t('请选择该渠道所支持的模型')
+                      : t('勾选启用模型限制后可选择')
+                  }
                   onChange={(value) => handleInputChange('model_limits', value)}
                   value={inputs.model_limits}
                   multiple
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconServer />}
                   optionList={models}
                   disabled={!model_limits_enabled}
                   maxTagCount={3}
                 />
-                <Text type="tertiary" className="mt-1 block text-xs">{t('非必要,不建议启用模型限制')}</Text>
+                <Text type='tertiary' className='mt-1 block text-xs'>
+                  {t('非必要,不建议启用模型限制')}
+                </Text>
               </div>
             </div>
           </Card>
 
-          <Card className="!rounded-2xl shadow-sm border-0">
-            <div className="flex items-center mb-4 p-6 rounded-xl" style={{
-              background: 'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
-              position: 'relative'
-            }}>
-              <div className="absolute inset-0 overflow-hidden">
-                <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
-                <div className="absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full"></div>
+          <Card className='!rounded-2xl shadow-sm border-0'>
+            <div
+              className='flex items-center mb-4 p-6 rounded-xl'
+              style={{
+                background:
+                  'linear-gradient(135deg, #92400e 0%, #d97706 50%, #f59e0b 100%)',
+                position: 'relative',
+              }}
+            >
+              <div className='absolute inset-0 overflow-hidden'>
+                <div className='absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full'></div>
+                <div className='absolute -bottom-8 -left-8 w-24 h-24 bg-white opacity-10 rounded-full'></div>
               </div>
-              <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative">
-                <IconUserGroup size="large" style={{ color: '#ffffff' }} />
+              <div className='w-10 h-10 rounded-full bg-white/20 flex items-center justify-center mr-4 relative'>
+                <IconUserGroup size='large' style={{ color: '#ffffff' }} />
               </div>
-              <div className="relative">
-                <Text style={{ color: '#ffffff' }} className="text-lg font-medium">{t('分组信息')}</Text>
-                <div style={{ color: '#ffffff' }} className="text-sm opacity-80">{t('设置令牌的分组')}</div>
+              <div className='relative'>
+                <Text
+                  style={{ color: '#ffffff' }}
+                  className='text-lg font-medium'
+                >
+                  {t('分组信息')}
+                </Text>
+                <div
+                  style={{ color: '#ffffff' }}
+                  className='text-sm opacity-80'
+                >
+                  {t('设置令牌的分组')}
+                </div>
               </div>
             </div>
 
             <div>
-              <Text strong className="block mb-2">{t('令牌分组')}</Text>
+              <Text strong className='block mb-2'>
+                {t('令牌分组')}
+              </Text>
               {groups.length > 0 ? (
                 <Select
                   placeholder={t('令牌分组,默认为用户的分组')}
                   onChange={(value) => handleInputChange('group', value)}
                   renderOptionItem={renderGroupOption}
                   value={inputs.group}
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconUserGroup />}
                   optionList={groups}
                 />
@@ -588,8 +692,8 @@ const EditToken = (props) => {
                 <Select
                   placeholder={t('管理员未设置用户可选分组')}
                   disabled={true}
-                  size="large"
-                  className="w-full !rounded-lg"
+                  size='large'
+                  className='w-full !rounded-lg'
                   prefix={<IconUserGroup />}
                 />
               )}