diff --git a/controller/channel.go b/controller/channel.go index 1cfb7906..13ed72b3 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -52,6 +52,14 @@ func GetAllChannels(c *gin.Context) { channelData := make([]*model.Channel, 0) idSort, _ := strconv.ParseBool(c.Query("id_sort")) enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode")) + // type filter + typeStr := c.Query("type") + typeFilter := -1 + if typeStr != "" { + if t, err := strconv.Atoi(typeStr); err == nil { + typeFilter = t + } + } var total int64 @@ -72,6 +80,14 @@ func GetAllChannels(c *gin.Context) { } // 计算 tag 总数用于分页 total, _ = model.CountAllTags() + } else if typeFilter >= 0 { + channels, err := model.GetChannelsByType((p-1)*pageSize, pageSize, idSort, typeFilter) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + channelData = channels + total, _ = model.CountChannelsByType(typeFilter) } else { channels, err := model.GetAllChannels((p-1)*pageSize, pageSize, false, idSort) if err != nil { @@ -82,14 +98,18 @@ func GetAllChannels(c *gin.Context) { total, _ = model.CountAllChannels() } + // calculate type counts + typeCounts, _ := model.CountChannelsGroupByType() + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": gin.H{ - "items": channelData, - "total": total, - "page": p, - "page_size": pageSize, + "items": channelData, + "total": total, + "page": p, + "page_size": pageSize, + "type_counts": typeCounts, }, }) return @@ -217,10 +237,20 @@ func SearchChannels(c *gin.Context) { } channelData = channels } + + // calculate type counts for search results + typeCounts := make(map[int64]int64) + for _, channel := range channelData { + typeCounts[int64(channel.Type)]++ + } + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", - "data": channelData, + "data": gin.H{ + "items": channelData, + "type_counts": typeCounts, + }, }) return } diff --git a/controller/group.go b/controller/group.go index 632b6cd5..2565b6ea 100644 --- a/controller/group.go +++ b/controller/group.go @@ -4,13 +4,14 @@ import ( "net/http" "one-api/model" "one-api/setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) func GetGroups(c *gin.Context) { groupNames := make([]string, 0) - for groupName, _ := range setting.GetGroupRatioCopy() { + for groupName := range ratio_setting.GetGroupRatioCopy() { groupNames = append(groupNames, groupName) } c.JSON(http.StatusOK, gin.H{ @@ -25,7 +26,7 @@ func GetUserGroups(c *gin.Context) { userGroup := "" userId := c.GetInt("id") userGroup, _ = model.GetUserGroup(userId, false) - for groupName, ratio := range setting.GetGroupRatioCopy() { + for groupName, ratio := range ratio_setting.GetGroupRatioCopy() { // UserUsableGroups contains the groups that the user can use userUsableGroups := setting.GetUserUsableGroups(userGroup) if desc, ok := userUsableGroups[groupName]; ok { diff --git a/controller/misc.go b/controller/misc.go index 1caaf640..4ffe86f4 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -76,6 +76,7 @@ func GetStatus(c *gin.Context) { "demo_site_enabled": operation_setting.DemoSiteEnabled, "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, "default_use_auto_group": setting.DefaultUseAutoGroup, + "pay_methods": setting.PayMethods, // 面板启用开关 "api_info_enabled": cs.ApiInfoEnabled, diff --git a/controller/option.go b/controller/option.go index 79ba2ffe..97bb6a5a 100644 --- a/controller/option.go +++ b/controller/option.go @@ -7,6 +7,7 @@ import ( "one-api/model" "one-api/setting" "one-api/setting/console_setting" + "one-api/setting/ratio_setting" "one-api/setting/system_setting" "strings" @@ -103,7 +104,7 @@ func UpdateOption(c *gin.Context) { return } case "GroupRatio": - err = setting.CheckGroupRatio(option.Value) + err = ratio_setting.CheckGroupRatio(option.Value) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/pricing.go b/controller/pricing.go index e6a3e57f..f27336b7 100644 --- a/controller/pricing.go +++ b/controller/pricing.go @@ -3,7 +3,7 @@ package controller import ( "one-api/model" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) @@ -13,7 +13,7 @@ func GetPricing(c *gin.Context) { userId, exists := c.Get("id") usableGroup := map[string]string{} groupRatio := map[string]float64{} - for s, f := range setting.GetGroupRatioCopy() { + for s, f := range ratio_setting.GetGroupRatioCopy() { groupRatio[s] = f } var group string @@ -22,7 +22,7 @@ func GetPricing(c *gin.Context) { if err == nil { group = user.Group for g := range groupRatio { - ratio, ok := setting.GetGroupGroupRatio(group, g) + ratio, ok := ratio_setting.GetGroupGroupRatio(group, g) if ok { groupRatio[g] = ratio } @@ -32,7 +32,7 @@ func GetPricing(c *gin.Context) { usableGroup = setting.GetUserUsableGroups(group) // check groupRatio contains usableGroup - for group := range setting.GetGroupRatioCopy() { + for group := range ratio_setting.GetGroupRatioCopy() { if _, ok := usableGroup[group]; !ok { delete(groupRatio, group) } @@ -47,7 +47,7 @@ func GetPricing(c *gin.Context) { } func ResetModelRatio(c *gin.Context) { - defaultStr := operation_setting.DefaultModelRatio2JSONString() + defaultStr := ratio_setting.DefaultModelRatio2JSONString() err := model.UpdateOption("ModelRatio", defaultStr) if err != nil { c.JSON(200, gin.H{ @@ -56,7 +56,7 @@ func ResetModelRatio(c *gin.Context) { }) return } - err = operation_setting.UpdateModelRatioByJSONString(defaultStr) + err = ratio_setting.UpdateModelRatioByJSONString(defaultStr) if err != nil { c.JSON(200, gin.H{ "success": false, diff --git a/dto/openai_request.go b/dto/openai_request.go index 299171ba..42c290ca 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -53,6 +53,7 @@ type GeneralOpenAIRequest struct { Modalities json.RawMessage `json:"modalities,omitempty"` Audio json.RawMessage `json:"audio,omitempty"` EnableThinking any `json:"enable_thinking,omitempty"` // ali + THINKING json.RawMessage `json:"thinking,omitempty"` // doubao ExtraBody json.RawMessage `json:"extra_body,omitempty"` WebSearchOptions *WebSearchOptions `json:"web_search_options,omitempty"` // OpenRouter Params diff --git a/main.go b/main.go index 30ba8092..cf593b57 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "one-api/model" "one-api/router" "one-api/service" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "os" "strconv" @@ -74,7 +74,7 @@ func main() { } // Initialize model settings - operation_setting.InitRatioSettings() + ratio_setting.InitRatioSettings() // Initialize constants constant.InitEnv() // Initialize options diff --git a/middleware/distributor.go b/middleware/distributor.go index 5d1c3641..84eb182e 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -11,6 +11,7 @@ import ( relayconstant "one-api/relay/constant" "one-api/service" "one-api/setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -48,7 +49,7 @@ func Distribute() func(c *gin.Context) { return } // check group in common.GroupRatio - if !setting.ContainsGroupRatio(tokenGroup) { + if !ratio_setting.ContainsGroupRatio(tokenGroup) { if tokenGroup != "auto" { abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup)) return diff --git a/model/channel.go b/model/channel.go index b5503eee..6cbd8adc 100644 --- a/model/channel.go +++ b/model/channel.go @@ -597,3 +597,39 @@ func CountAllTags() (int64, error) { err := DB.Model(&Channel{}).Where("tag is not null AND tag != ''").Distinct("tag").Count(&total).Error return total, err } + +// Get channels of specified type with pagination +func GetChannelsByType(startIdx int, num int, idSort bool, channelType int) ([]*Channel, error) { + var channels []*Channel + order := "priority desc" + if idSort { + order = "id desc" + } + err := DB.Where("type = ?", channelType).Order(order).Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error + return channels, err +} + +// Count channels of specific type +func CountChannelsByType(channelType int) (int64, error) { + var count int64 + err := DB.Model(&Channel{}).Where("type = ?", channelType).Count(&count).Error + return count, err +} + +// Return map[type]count for all channels +func CountChannelsGroupByType() (map[int64]int64, error) { + type result struct { + Type int64 `gorm:"column:type"` + Count int64 `gorm:"column:count"` + } + var results []result + err := DB.Model(&Channel{}).Select("type, count(*) as count").Group("type").Find(&results).Error + if err != nil { + return nil, err + } + counts := make(map[int64]int64) + for _, r := range results { + counts[r.Type] = r.Count + } + return counts, nil +} diff --git a/model/main.go b/model/main.go index 965bba93..d46a21cf 100644 --- a/model/main.go +++ b/model/main.go @@ -46,6 +46,15 @@ func initCol() { logGroupCol = commonGroupCol logKeyCol = commonKeyCol } + } else { + // LOG_SQL_DSN 为空时,日志数据库与主数据库相同 + if common.UsingPostgreSQL { + logGroupCol = `"group"` + logKeyCol = `"key"` + } else { + logGroupCol = commonGroupCol + logKeyCol = commonKeyCol + } } // log sql type and database type common.SysLog("Using Log SQL Type: " + common.LogSqlType) diff --git a/model/option.go b/model/option.go index 1391b203..ec386b29 100644 --- a/model/option.go +++ b/model/option.go @@ -5,6 +5,7 @@ import ( "one-api/setting" "one-api/setting/config" "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -78,6 +79,7 @@ func InitOptionMap() { common.OptionMap["Chats"] = setting.Chats2JsonString() common.OptionMap["AutoGroups"] = setting.AutoGroups2JsonString() common.OptionMap["DefaultUseAutoGroup"] = strconv.FormatBool(setting.DefaultUseAutoGroup) + common.OptionMap["PayMethods"] = setting.PayMethods2JsonString() common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientSecret"] = "" common.OptionMap["TelegramBotToken"] = "" @@ -96,13 +98,13 @@ func InitOptionMap() { common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes) common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount) common.OptionMap["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString() - common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString() - common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString() - common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString() - common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString() - common.OptionMap["GroupGroupRatio"] = setting.GroupGroupRatio2JSONString() + common.OptionMap["ModelRatio"] = ratio_setting.ModelRatio2JSONString() + common.OptionMap["ModelPrice"] = ratio_setting.ModelPrice2JSONString() + common.OptionMap["CacheRatio"] = ratio_setting.CacheRatio2JSONString() + common.OptionMap["GroupRatio"] = ratio_setting.GroupRatio2JSONString() + common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString() common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString() - common.OptionMap["CompletionRatio"] = operation_setting.CompletionRatio2JSONString() + common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString() common.OptionMap["TopUpLink"] = common.TopUpLink //common.OptionMap["ChatLink"] = common.ChatLink //common.OptionMap["ChatLink2"] = common.ChatLink2 @@ -358,19 +360,19 @@ func updateOptionMap(key string, value string) (err error) { case "DataExportDefaultTime": common.DataExportDefaultTime = value case "ModelRatio": - err = operation_setting.UpdateModelRatioByJSONString(value) + err = ratio_setting.UpdateModelRatioByJSONString(value) case "GroupRatio": - err = setting.UpdateGroupRatioByJSONString(value) + err = ratio_setting.UpdateGroupRatioByJSONString(value) case "GroupGroupRatio": - err = setting.UpdateGroupGroupRatioByJSONString(value) + err = ratio_setting.UpdateGroupGroupRatioByJSONString(value) case "UserUsableGroups": err = setting.UpdateUserUsableGroupsByJSONString(value) case "CompletionRatio": - err = operation_setting.UpdateCompletionRatioByJSONString(value) + err = ratio_setting.UpdateCompletionRatioByJSONString(value) case "ModelPrice": - err = operation_setting.UpdateModelPriceByJSONString(value) + err = ratio_setting.UpdateModelPriceByJSONString(value) case "CacheRatio": - err = operation_setting.UpdateCacheRatioByJSONString(value) + err = ratio_setting.UpdateCacheRatioByJSONString(value) case "TopUpLink": common.TopUpLink = value //case "ChatLink": @@ -387,6 +389,8 @@ func updateOptionMap(key string, value string) (err error) { operation_setting.AutomaticDisableKeywordsFromString(value) case "StreamCacheQueueLength": setting.StreamCacheQueueLength, _ = strconv.Atoi(value) + case "PayMethods": + err = setting.UpdatePayMethodsByJsonString(value) } return err } diff --git a/model/pricing.go b/model/pricing.go index ba1815e2..74a25f2d 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -2,7 +2,7 @@ package model import ( "one-api/common" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "sync" "time" ) @@ -65,14 +65,14 @@ func updatePricing() { ModelName: model, EnableGroup: groups, } - modelPrice, findPrice := operation_setting.GetModelPrice(model, false) + modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) if findPrice { pricing.ModelPrice = modelPrice pricing.QuotaType = 1 } else { - modelRatio, _ := operation_setting.GetModelRatio(model) + modelRatio, _ := ratio_setting.GetModelRatio(model) pricing.ModelRatio = modelRatio - pricing.CompletionRatio = operation_setting.GetCompletionRatio(model) + pricing.CompletionRatio = ratio_setting.GetCompletionRatio(model) pricing.QuotaType = 0 } pricingMap = append(pricingMap, pricing) diff --git a/model/utils.go b/model/utils.go index e6b09aa5..1f8a0963 100644 --- a/model/utils.go +++ b/model/utils.go @@ -2,11 +2,12 @@ package model import ( "errors" - "github.com/bytedance/gopkg/util/gopool" - "gorm.io/gorm" "one-api/common" "sync" "time" + + "github.com/bytedance/gopkg/util/gopool" + "gorm.io/gorm" ) const ( @@ -48,6 +49,22 @@ func addNewRecord(type_ int, id int, value int) { } func batchUpdate() { + // check if there's any data to update + hasData := false + for i := 0; i < BatchUpdateTypeCount; i++ { + batchUpdateLocks[i].Lock() + if len(batchUpdateStores[i]) > 0 { + hasData = true + batchUpdateLocks[i].Unlock() + break + } + batchUpdateLocks[i].Unlock() + } + + if !hasData { + return + } + common.SysLog("batch update started") for i := 0; i < BatchUpdateTypeCount; i++ { batchUpdateLocks[i].Lock() diff --git a/relay/channel/gemini/dto.go b/relay/channel/gemini/dto.go index fa9108df..b22e092a 100644 --- a/relay/channel/gemini/dto.go +++ b/relay/channel/gemini/dto.go @@ -140,6 +140,7 @@ type GeminiChatGenerationConfig struct { Seed int64 `json:"seed,omitempty"` ResponseModalities []string `json:"responseModalities,omitempty"` ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` + SpeechConfig json.RawMessage `json:"speechConfig,omitempty"` // RawMessage to allow flexible speech config } type GeminiChatCandidate struct { diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index d8d7db39..4475e9f8 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -373,7 +373,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon if content.Role == "assistant" { content.Role = "model" } - geminiRequest.Contents = append(geminiRequest.Contents, content) + if len(content.Parts) > 0 { + geminiRequest.Contents = append(geminiRequest.Contents, content) + } } if len(system_content) > 0 { diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 31f84abf..424234a8 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -123,14 +123,23 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if v, ok := claudeModelMap[info.UpstreamModelName]; ok { model = v } - return fmt.Sprintf( - "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s", - region, - adc.ProjectID, - region, - model, - suffix, - ), nil + if region == "global" { + return fmt.Sprintf( + "https://aiplatform.googleapis.com/v1/projects/%s/locations/global/publishers/anthropic/models/%s:%s", + adc.ProjectID, + model, + suffix, + ), nil + } else { + return fmt.Sprintf( + "https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/anthropic/models/%s:%s", + region, + adc.ProjectID, + region, + model, + suffix, + ), nil + } } else if a.RequestMode == RequestModeLlama { return fmt.Sprintf( "https://%s-aiplatform.googleapis.com/v1beta1/projects/%s/locations/%s/endpoints/openapi/chat/completions", diff --git a/relay/helper/price.go b/relay/helper/price.go index 326790b4..1ee2767e 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -5,8 +5,7 @@ import ( "one-api/common" constant2 "one-api/constant" relaycommon "one-api/relay/common" - "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "github.com/gin-gonic/gin" ) @@ -36,7 +35,7 @@ func (p PriceData) ToSetting() string { func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) GroupRatioInfo { groupRatioInfo := GroupRatioInfo{ GroupRatio: 1.0, // default ratio - GroupSpecialRatio: 1.0, // default user group ratio + GroupSpecialRatio: -1, } // check auto group @@ -49,21 +48,21 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) GroupR } // check user group special ratio - userGroupRatio, ok := setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) + userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) if ok { // user group special ratio groupRatioInfo.GroupSpecialRatio = userGroupRatio groupRatioInfo.GroupRatio = userGroupRatio } else { // normal group ratio - groupRatioInfo.GroupRatio = setting.GetGroupRatio(relayInfo.Group) + groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.Group) } return groupRatioInfo } func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) { - modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false) + modelPrice, usePrice := ratio_setting.GetModelPrice(info.OriginModelName, false) groupRatioInfo := HandleGroupRatio(c, info) @@ -79,7 +78,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens preConsumedTokens = promptTokens + maxTokens } var success bool - modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName) + modelRatio, success = ratio_setting.GetModelRatio(info.OriginModelName) if !success { acceptUnsetRatio := false if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok { @@ -92,10 +91,10 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName) } } - completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName) - cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName) - cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName) - imageRatio, _ = operation_setting.GetImageRatio(info.OriginModelName) + completionRatio = ratio_setting.GetCompletionRatio(info.OriginModelName) + cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) + cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) + imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName) ratio := modelRatio * groupRatioInfo.GroupRatio preConsumedQuota = int(float64(preConsumedTokens) * ratio) } else { @@ -122,11 +121,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens } func ContainPriceOrRatio(modelName string) bool { - _, ok := operation_setting.GetModelPrice(modelName, false) + _, ok := ratio_setting.GetModelPrice(modelName, false) if ok { return true } - _, ok = operation_setting.GetModelRatio(modelName) + _, ok = ratio_setting.GetModelRatio(modelName) if ok { return true } diff --git a/relay/relay-gemini.go b/relay/relay-gemini.go index 21cf5e12..9edbe5c2 100644 --- a/relay/relay-gemini.go +++ b/relay/relay-gemini.go @@ -155,6 +155,10 @@ func GeminiHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { return service.OpenAIErrorWrapperLocal(err, "marshal_text_request_failed", http.StatusInternalServerError) } + if common.DebugEnabled { + println("Gemini request body: %s", string(requestBody)) + } + resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody)) if err != nil { common.LogError(c, "Do gemini request failed: "+err.Error()) diff --git a/relay/relay-mj.go b/relay/relay-mj.go index 9d0a2077..ce4346b6 100644 --- a/relay/relay-mj.go +++ b/relay/relay-mj.go @@ -15,7 +15,7 @@ import ( relayconstant "one-api/relay/constant" "one-api/service" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strconv" "strings" "time" @@ -174,17 +174,17 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse { return service.MidjourneyErrorWrapper(constant.MjRequestError, "sour_base64_and_target_base64_is_required") } modelName := service.CoverActionToModelName(constant.MjActionSwapFace) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) // 如果没有配置价格,则使用默认价格 if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { modelPrice = defaultPrice } } - groupRatio := setting.GetGroupRatio(group) + groupRatio := ratio_setting.GetGroupRatio(group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(userId, false) if err != nil { @@ -480,17 +480,17 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) modelName := service.CoverActionToModelName(midjRequest.Action) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) // 如果没有配置价格,则使用默认价格 if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { modelPrice = defaultPrice } } - groupRatio := setting.GetGroupRatio(group) + groupRatio := ratio_setting.GetGroupRatio(group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(userId, false) if err != nil { diff --git a/relay/relay_task.go b/relay/relay_task.go index 26874ba6..3da9a20f 100644 --- a/relay/relay_task.go +++ b/relay/relay_task.go @@ -15,8 +15,7 @@ import ( relaycommon "one-api/relay/common" relayconstant "one-api/relay/constant" "one-api/service" - "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" ) /* @@ -38,9 +37,9 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) { } modelName := service.CoverTaskActionToModelName(platform, relayInfo.Action) - modelPrice, success := operation_setting.GetModelPrice(modelName, true) + modelPrice, success := ratio_setting.GetModelPrice(modelName, true) if !success { - defaultPrice, ok := operation_setting.GetDefaultModelRatioMap()[modelName] + defaultPrice, ok := ratio_setting.GetDefaultModelRatioMap()[modelName] if !ok { modelPrice = 0.1 } else { @@ -49,7 +48,7 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) { } // 预扣 - groupRatio := setting.GetGroupRatio(relayInfo.Group) + groupRatio := ratio_setting.GetGroupRatio(relayInfo.Group) ratio := modelPrice * groupRatio userQuota, err := model.GetUserQuota(relayInfo.UserId, false) if err != nil { diff --git a/service/quota.go b/service/quota.go index 0fb9e67c..973deba7 100644 --- a/service/quota.go +++ b/service/quota.go @@ -11,7 +11,7 @@ import ( relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/setting" - "one-api/setting/operation_setting" + "one-api/setting/ratio_setting" "strings" "time" @@ -46,9 +46,9 @@ func calculateAudioQuota(info QuotaInfo) int { return int(quota.IntPart()) } - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(info.ModelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(info.ModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(info.ModelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(info.ModelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(info.ModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(info.ModelName)) groupRatio := decimal.NewFromFloat(info.GroupRatio) modelRatio := decimal.NewFromFloat(info.ModelRatio) @@ -94,18 +94,18 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag textOutTokens := usage.OutputTokenDetails.TextTokens audioInputTokens := usage.InputTokenDetails.AudioTokens audioOutTokens := usage.OutputTokenDetails.AudioTokens - groupRatio := setting.GetGroupRatio(relayInfo.Group) - modelRatio, _ := operation_setting.GetModelRatio(modelName) + groupRatio := ratio_setting.GetGroupRatio(relayInfo.Group) + modelRatio, _ := ratio_setting.GetModelRatio(modelName) autoGroup, exists := ctx.Get("auto_group") if exists { - groupRatio = setting.GetGroupRatio(autoGroup.(string)) + groupRatio = ratio_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) + userGroupRatio, ok := ratio_setting.GetGroupGroupRatio(relayInfo.UserGroup, relayInfo.Group) if ok { actualGroupRatio = userGroupRatio } @@ -154,9 +154,9 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod audioOutTokens := usage.OutputTokenDetails.AudioTokens tokenName := ctx.GetString("token_name") - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(modelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(modelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(modelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(relayInfo.OriginModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(modelName)) modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatioInfo.GroupRatio @@ -289,9 +289,9 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, audioOutTokens := usage.CompletionTokenDetails.AudioTokens tokenName := ctx.GetString("token_name") - completionRatio := decimal.NewFromFloat(operation_setting.GetCompletionRatio(relayInfo.OriginModelName)) - audioRatio := decimal.NewFromFloat(operation_setting.GetAudioRatio(relayInfo.OriginModelName)) - audioCompletionRatio := decimal.NewFromFloat(operation_setting.GetAudioCompletionRatio(relayInfo.OriginModelName)) + completionRatio := decimal.NewFromFloat(ratio_setting.GetCompletionRatio(relayInfo.OriginModelName)) + audioRatio := decimal.NewFromFloat(ratio_setting.GetAudioRatio(relayInfo.OriginModelName)) + audioCompletionRatio := decimal.NewFromFloat(ratio_setting.GetAudioCompletionRatio(relayInfo.OriginModelName)) modelRatio := priceData.ModelRatio groupRatio := priceData.GroupRatioInfo.GroupRatio diff --git a/setting/payment.go b/setting/payment.go index f50723c3..4ffa4381 100644 --- a/setting/payment.go +++ b/setting/payment.go @@ -1,8 +1,45 @@ package setting +import "encoding/json" + var PayAddress = "" var CustomCallbackAddress = "" var EpayId = "" var EpayKey = "" var Price = 7.3 var MinTopUp = 1 + +var PayMethods = []map[string]string{ + { + "name": "支付宝", + "color": "rgba(var(--semi-blue-5), 1)", + "type": "zfb", + }, + { + "name": "微信", + "color": "rgba(var(--semi-green-5), 1)", + "type": "wx", + }, +} + +func UpdatePayMethodsByJsonString(jsonString string) error { + PayMethods = make([]map[string]string, 0) + return json.Unmarshal([]byte(jsonString), &PayMethods) +} + +func PayMethods2JsonString() string { + jsonBytes, err := json.Marshal(PayMethods) + if err != nil { + return "[]" + } + return string(jsonBytes) +} + +func ContainsPayMethod(method string) bool { + for _, payMethod := range PayMethods { + if payMethod["type"] == method { + return true + } + } + return false +} diff --git a/setting/operation_setting/cache_ratio.go b/setting/ratio_setting/cache_ratio.go similarity index 99% rename from setting/operation_setting/cache_ratio.go rename to setting/ratio_setting/cache_ratio.go index ec0c766d..aa934b22 100644 --- a/setting/operation_setting/cache_ratio.go +++ b/setting/ratio_setting/cache_ratio.go @@ -1,4 +1,4 @@ -package operation_setting +package ratio_setting import ( "encoding/json" diff --git a/setting/group_ratio.go b/setting/ratio_setting/group_ratio.go similarity index 99% rename from setting/group_ratio.go rename to setting/ratio_setting/group_ratio.go index 28dbd167..f600a7b5 100644 --- a/setting/group_ratio.go +++ b/setting/ratio_setting/group_ratio.go @@ -1,4 +1,4 @@ -package setting +package ratio_setting import ( "encoding/json" diff --git a/setting/operation_setting/model-ratio.go b/setting/ratio_setting/model_ratio.go similarity index 99% rename from setting/operation_setting/model-ratio.go rename to setting/ratio_setting/model_ratio.go index 175995ae..266c1e07 100644 --- a/setting/operation_setting/model-ratio.go +++ b/setting/ratio_setting/model_ratio.go @@ -1,8 +1,9 @@ -package operation_setting +package ratio_setting import ( "encoding/json" "one-api/common" + "one-api/setting/operation_setting" "strings" "sync" ) @@ -369,7 +370,7 @@ func GetModelRatio(name string) (float64, bool) { } ratio, ok := modelRatioMap[name] if !ok { - return 37.5, SelfUseModeEnabled + return 37.5, operation_setting.SelfUseModeEnabled } return ratio, true } diff --git a/web/src/components/settings/OperationSetting.js b/web/src/components/settings/OperationSetting.js index 7bd9bf62..f6786f95 100644 --- a/web/src/components/settings/OperationSetting.js +++ b/web/src/components/settings/OperationSetting.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; +import { Card, Spin } from '@douyinfe/semi-ui'; import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js'; import SettingsDrawing from '../../pages/Setting/Operation/SettingsDrawing.js'; import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js'; @@ -7,63 +7,58 @@ import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js'; import SettingsDataDashboard from '../../pages/Setting/Operation/SettingsDataDashboard.js'; import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js'; import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js'; -import ModelSettingsVisualEditor from '../../pages/Setting/Operation/ModelSettingsVisualEditor.js'; -import GroupRatioSettings from '../../pages/Setting/Operation/GroupRatioSettings.js'; -import ModelRatioSettings from '../../pages/Setting/Operation/ModelRatioSettings.js'; - -import { API, showError, showSuccess } from '../../helpers'; import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js'; -import { useTranslation } from 'react-i18next'; -import ModelRatioNotSetEditor from '../../pages/Setting/Operation/ModelRationNotSetEditor.js'; +import { API, showError } from '../../helpers'; const OperationSetting = () => { - const { t } = useTranslation(); let [inputs, setInputs] = useState({ + /* 额度相关 */ QuotaForNewUser: 0, + PreConsumedQuota: 0, QuotaForInviter: 0, QuotaForInvitee: 0, - QuotaRemindThreshold: 0, - PreConsumedQuota: 0, - StreamCacheQueueLength: 0, - ModelRatio: '', - CacheRatio: '', - CompletionRatio: '', - ModelPrice: '', - GroupRatio: '', - GroupGroupRatio: '', - AutoGroups: '', - DefaultUseAutoGroup: false, - UserUsableGroups: '', + + /* 通用设置 */ TopUpLink: '', 'general_setting.docs_link': '', - // ChatLink2: '', // 添加的新状态变量 QuotaPerUnit: 0, - AutomaticDisableChannelEnabled: false, - AutomaticEnableChannelEnabled: false, - ChannelDisableThreshold: 0, - LogConsumeEnabled: false, + RetryTimes: 0, DisplayInCurrencyEnabled: false, DisplayTokenStatEnabled: false, - CheckSensitiveEnabled: false, - CheckSensitiveOnPromptEnabled: false, - CheckSensitiveOnCompletionEnabled: '', - StopOnSensitiveEnabled: '', - SensitiveWords: '', + DefaultCollapseSidebar: false, + DemoSiteEnabled: false, + SelfUseModeEnabled: false, + + /* 绘图设置 */ + DrawingEnabled: false, MjNotifyEnabled: false, MjAccountFilterEnabled: false, - MjModeClearEnabled: false, MjForwardUrlEnabled: false, + MjModeClearEnabled: false, MjActionCheckSuccessEnabled: false, - DrawingEnabled: false, + + /* 敏感词设置 */ + CheckSensitiveEnabled: false, + CheckSensitiveOnPromptEnabled: false, + SensitiveWords: '', + + /* 日志设置 */ + LogConsumeEnabled: false, + + /* 数据看板 */ DataExportEnabled: false, DataExportDefaultTime: 'hour', DataExportInterval: 5, - DefaultCollapseSidebar: false, // 默认折叠侧边栏 - RetryTimes: 0, - Chats: '[]', - DemoSiteEnabled: false, - SelfUseModeEnabled: false, + + /* 监控设置 */ + ChannelDisableThreshold: 0, + QuotaRemindThreshold: 0, + AutomaticDisableChannelEnabled: false, + AutomaticEnableChannelEnabled: false, AutomaticDisableKeywords: '', + + /* 聊天设置 */ + Chats: '[]', }); let [loading, setLoading] = useState(false); @@ -74,22 +69,9 @@ const OperationSetting = () => { if (success) { let newInputs = {}; data.forEach((item) => { - if ( - item.key === 'ModelRatio' || - item.key === 'GroupRatio' || - item.key === 'GroupGroupRatio' || - item.key === 'AutoGroups' || - item.key === 'UserUsableGroups' || - item.key === 'CompletionRatio' || - item.key === 'ModelPrice' || - item.key === 'CacheRatio' - ) { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); - } if ( item.key.endsWith('Enabled') || - ['DefaultCollapseSidebar'].includes(item.key) || - ['DefaultUseAutoGroup'].includes(item.key) + ['DefaultCollapseSidebar'].includes(item.key) ) { newInputs[item.key] = item.value === 'true' ? true : false; } else { @@ -153,24 +135,6 @@ const OperationSetting = () => { - {/* 分组倍率设置 */} - - - - {/* 合并模型倍率设置和可视化倍率设置 */} - - - - - - - - - - - - - ); diff --git a/web/src/components/settings/RatioSetting.js b/web/src/components/settings/RatioSetting.js new file mode 100644 index 00000000..bf97282c --- /dev/null +++ b/web/src/components/settings/RatioSetting.js @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from 'react'; +import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; +import { useTranslation } from 'react-i18next'; + +import GroupRatioSettings from '../../pages/Setting/Ratio/GroupRatioSettings.js'; +import ModelRatioSettings from '../../pages/Setting/Ratio/ModelRatioSettings.js'; +import ModelSettingsVisualEditor from '../../pages/Setting/Ratio/ModelSettingsVisualEditor.js'; +import ModelRatioNotSetEditor from '../../pages/Setting/Ratio/ModelRationNotSetEditor.js'; + +import { API, showError } from '../../helpers'; + +const RatioSetting = () => { + const { t } = useTranslation(); + + let [inputs, setInputs] = useState({ + ModelPrice: '', + ModelRatio: '', + CacheRatio: '', + CompletionRatio: '', + GroupRatio: '', + GroupGroupRatio: '', + AutoGroups: '', + DefaultUseAutoGroup: false, + UserUsableGroups: '', + }); + + const [loading, setLoading] = useState(false); + + const getOptions = async () => { + const res = await API.get('/api/option/'); + const { success, message, data } = res.data; + if (success) { + let newInputs = {}; + data.forEach((item) => { + if ( + item.key === 'ModelRatio' || + item.key === 'GroupRatio' || + item.key === 'GroupGroupRatio' || + item.key === 'AutoGroups' || + item.key === 'UserUsableGroups' || + item.key === 'CompletionRatio' || + item.key === 'ModelPrice' || + item.key === 'CacheRatio' + ) { + try { + item.value = JSON.stringify(JSON.parse(item.value), null, 2); + } catch (e) { + // 如果后端返回的不是合法 JSON,直接展示 + } + } + if (['DefaultUseAutoGroup'].includes(item.key)) { + newInputs[item.key] = item.value === 'true' ? true : false; + } else { + newInputs[item.key] = item.value; + } + }); + setInputs(newInputs); + } else { + showError(message); + } + }; + + const onRefresh = async () => { + try { + setLoading(true); + await getOptions(); + } catch (error) { + showError('刷新失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + onRefresh(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {/* 分组倍率设置 */} + + + + {/* 模型倍率设置以及可视化编辑器 */} + + + + + + + + + + + + + + + ); +}; + +export default RatioSetting; \ No newline at end of file diff --git a/web/src/components/settings/SystemSetting.js b/web/src/components/settings/SystemSetting.js index 8219159b..1236ef2e 100644 --- a/web/src/components/settings/SystemSetting.js +++ b/web/src/components/settings/SystemSetting.js @@ -17,7 +17,7 @@ import { removeTrailingSlash, showError, showSuccess, - verifyJSON + verifyJSON, } from '../../helpers'; import axios from 'axios'; @@ -73,6 +73,7 @@ const SystemSetting = () => { LinuxDOOAuthEnabled: '', LinuxDOClientId: '', LinuxDOClientSecret: '', + PayMethods: '', }); const [originInputs, setOriginInputs] = useState({}); @@ -230,6 +231,12 @@ const SystemSetting = () => { return; } } + if (originInputs['PayMethods'] !== inputs.PayMethods) { + if (!verifyJSON(inputs.PayMethods)) { + showError('充值方式设置不是合法的 JSON 字符串'); + return; + } + } const options = [ { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }, @@ -256,6 +263,9 @@ const SystemSetting = () => { if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio }); } + if (originInputs['PayMethods'] !== inputs.PayMethods) { + options.push({ key: 'PayMethods', value: inputs.PayMethods }); + } await updateOptions(options); }; @@ -658,6 +668,12 @@ const SystemSetting = () => { placeholder='为一个 JSON 文本,键为组名称,值为倍率' autosize /> + diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index f5a78490..90921460 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo, useRef } from 'react'; import { API, showError, @@ -16,11 +16,6 @@ import { XCircle, AlertCircle, HelpCircle, - TestTube, - Zap, - Timer, - Clock, - AlertTriangle, Coins, Tags } from 'lucide-react'; @@ -43,7 +38,9 @@ import { Typography, Checkbox, Card, - Form + Form, + Tabs, + TabPane } from '@douyinfe/semi-ui'; import { IllustrationNoResult, @@ -141,31 +138,31 @@ const ChannelsTable = () => { time = time.toFixed(2) + t(' 秒'); if (responseTime === 0) { return ( - }> + {t('未测试')} ); } else if (responseTime <= 1000) { return ( - }> + {time} ); } else if (responseTime <= 3000) { return ( - }> + {time} ); } else if (responseTime <= 5000) { return ( - }> + {time} ); } else { return ( - }> + {time} ); @@ -682,11 +679,10 @@ const ChannelsTable = () => { const [isBatchTesting, setIsBatchTesting] = useState(false); const [testQueue, setTestQueue] = useState([]); const [isProcessingQueue, setIsProcessingQueue] = useState(false); - - // Form API 引用 + const [activeTypeKey, setActiveTypeKey] = useState('all'); + const [typeCounts, setTypeCounts] = useState({}); + const requestCounter = useRef(0); const [formApi, setFormApi] = useState(null); - - // Form 初始值 const formInitValues = { searchKeyword: '', searchGroup: '', @@ -868,17 +864,23 @@ const ChannelsTable = () => { setChannels(channelDates); }; - const loadChannels = async (page, pageSize, idSort, enableTagMode) => { + const loadChannels = async (page, pageSize, idSort, enableTagMode, typeKey = activeTypeKey) => { + const reqId = ++requestCounter.current; // 记录当前请求序号 setLoading(true); + const typeParam = (!enableTagMode && typeKey !== 'all') ? `&type=${typeKey}` : ''; const res = await API.get( - `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); - if (res === undefined) { + if (res === undefined || reqId !== requestCounter.current) { return; } const { success, message, data } = res.data; if (success) { - const { items, total } = data; + const { items, total, type_counts } = data; + if (type_counts) { + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + } setChannelFormat(items, enableTagMode); setChannelCount(total); } else { @@ -1044,12 +1046,16 @@ const ChannelsTable = () => { return; } + const typeParam = (!enableTagMode && activeTypeKey !== 'all') ? `&type=${activeTypeKey}` : ''; const res = await API.get( - `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`, + `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}`, ); const { success, message, data } = res.data; if (success) { - setChannelFormat(data, enableTagMode); + const { items = [], type_counts = {} } = data; + const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0); + setTypeCounts({ ...type_counts, all: sumAll }); + setChannelFormat(items, enableTagMode); setActivePage(1); } else { showError(message); @@ -1179,7 +1185,94 @@ const ChannelsTable = () => { } }; + const channelTypeCounts = useMemo(() => { + if (Object.keys(typeCounts).length > 0) return typeCounts; + // fallback 本地计算 + const counts = { all: channels.length }; + channels.forEach((channel) => { + const collect = (ch) => { + const type = ch.type; + counts[type] = (counts[type] || 0) + 1; + }; + if (channel.children !== undefined) { + channel.children.forEach(collect); + } else { + collect(channel); + } + }); + return counts; + }, [typeCounts, channels]); + + const availableTypeKeys = useMemo(() => { + const keys = ['all']; + Object.entries(channelTypeCounts).forEach(([k, v]) => { + if (k !== 'all' && v > 0) keys.push(String(k)); + }); + return keys; + }, [channelTypeCounts]); + + const renderTypeTabs = () => { + if (enableTagMode) return null; + + return ( + { + setActiveTypeKey(key); + setActivePage(1); + loadChannels(1, pageSize, idSort, enableTagMode, key); + }} + className="mb-4" + > + + {t('全部')} + + {channelTypeCounts['all'] || 0} + + + } + /> + + {CHANNEL_OPTIONS.filter((opt) => availableTypeKeys.includes(String(opt.value))).map((option) => { + const key = String(option.value); + const count = channelTypeCounts[option.value] || 0; + return ( + + {getChannelIcon(option.value)} + {option.label} + + {count} + + + } + /> + ); + })} + + ); + }; + let pageData = channels; + if (activeTypeKey !== 'all') { + const typeVal = parseInt(activeTypeKey); + if (!isNaN(typeVal)) { + pageData = pageData.filter((ch) => { + if (ch.children !== undefined) { + return ch.children.some((c) => c.type === typeVal); + } + return ch.type === typeVal; + }); + } + } const handlePageChange = (page) => { setActivePage(page); @@ -1371,6 +1464,7 @@ const ChannelsTable = () => { const renderHeader = () => (
+ {renderTypeTabs()}
@@ -1388,6 +1389,7 @@ const Detail = (props) => { ) : ( typeof item === 'string'); } catch (error) { diff --git a/web/src/pages/Setting/Operation/ModelRatioSettings.js b/web/src/pages/Setting/Ratio/ModelRatioSettings.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelRatioSettings.js rename to web/src/pages/Setting/Ratio/ModelRatioSettings.js diff --git a/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js b/web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelRationNotSetEditor.js rename to web/src/pages/Setting/Ratio/ModelRationNotSetEditor.js diff --git a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js b/web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js similarity index 100% rename from web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js rename to web/src/pages/Setting/Ratio/ModelSettingsVisualEditor.js diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js index dc48c8dc..5572e540 100644 --- a/web/src/pages/Setting/index.js +++ b/web/src/pages/Setting/index.js @@ -10,6 +10,7 @@ import OperationSetting from '../../components/settings/OperationSetting.js'; import RateLimitSetting from '../../components/settings/RateLimitSetting.js'; import ModelSetting from '../../components/settings/ModelSetting.js'; import DashboardSetting from '../../components/settings/DashboardSetting.js'; +import RatioSetting from '../../components/settings/RatioSetting.js'; const Setting = () => { const { t } = useTranslation(); @@ -24,6 +25,11 @@ const Setting = () => { content: , itemKey: 'operation', }); + panes.push({ + tab: t('倍率设置'), + content: , + itemKey: 'ratio', + }); panes.push({ tab: t('速率限制设置'), content: , @@ -45,7 +51,7 @@ const Setting = () => { itemKey: 'other', }); panes.push({ - tab: t('仪表盘配置'), + tab: t('仪表盘设置'), content: , itemKey: 'dashboard', }); diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index cb32bca2..e327178d 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -7,7 +7,7 @@ import { renderQuota, renderQuotaWithAmount, copy, - getQuotaPerUnit + getQuotaPerUnit, } from '../../helpers'; import { Avatar, @@ -34,7 +34,7 @@ import { Copy, Users, User, - Coins + Coins, } from 'lucide-react'; const { Text, Title } = Typography; @@ -49,9 +49,15 @@ const TopUp = () => { const [topUpCode, setTopUpCode] = useState(''); const [amount, setAmount] = useState(0.0); const [minTopUp, setMinTopUp] = useState(statusState?.status?.min_topup || 1); - const [topUpCount, setTopUpCount] = useState(statusState?.status?.min_topup || 1); - const [topUpLink, setTopUpLink] = useState(statusState?.status?.top_up_link || ''); - const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(statusState?.status?.enable_online_topup || false); + const [topUpCount, setTopUpCount] = useState( + statusState?.status?.min_topup || 1, + ); + const [topUpLink, setTopUpLink] = useState( + statusState?.status?.top_up_link || '', + ); + const [enableOnlineTopUp, setEnableOnlineTopUp] = useState( + statusState?.status?.enable_online_topup || false, + ); const [priceRatio, setPriceRatio] = useState(statusState?.status?.price || 1); const [userQuota, setUserQuota] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); @@ -61,6 +67,7 @@ const TopUp = () => { const [amountLoading, setAmountLoading] = useState(false); const [paymentLoading, setPaymentLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false); + const [payMethods, setPayMethods] = useState([]); // 邀请相关状态 const [affLink, setAffLink] = useState(''); @@ -76,7 +83,7 @@ const TopUp = () => { { value: 100 }, { value: 300 }, { value: 500 }, - { value: 1000 } + { value: 1000 }, ]); const [selectedPreset, setSelectedPreset] = useState(null); @@ -126,7 +133,7 @@ const TopUp = () => { if (userState.user) { const updatedUser = { ...userState.user, - quota: userState.user.quota + data + quota: userState.user.quota + data, }; userDispatch({ type: 'login', payload: updatedUser }); } @@ -283,6 +290,34 @@ const TopUp = () => { } getAffLink().then(); setTransferAmount(getQuotaPerUnit()); + + let payMethods = localStorage.getItem('pay_methods'); + try { + payMethods = JSON.parse(payMethods); + if (payMethods && payMethods.length > 0) { + // 检查name和type是否为空 + payMethods = payMethods.filter((method) => { + return method.name && method.type; + }); + // 如果没有color,则设置默认颜色 + payMethods = payMethods.map((method) => { + if (!method.color) { + if (method.type === 'zfb') { + method.color = 'rgba(var(--semi-blue-5), 1)'; + } else if (method.type === 'wx') { + method.color = 'rgba(var(--semi-green-5), 1)'; + } else { + method.color = 'rgba(var(--semi-primary-5), 1)'; + } + } + return method; + }); + setPayMethods(payMethods); + } + } catch (e) { + console.log(e); + showError(t('支付方式配置错误, 请联系管理员')); + } }, []); useEffect(() => { @@ -347,12 +382,12 @@ const TopUp = () => { }; return ( -
+
{/* 划转模态框 */} - +
+ {t('划转邀请额度')}
} @@ -360,22 +395,22 @@ const TopUp = () => { onOk={transfer} onCancel={handleTransferCancel} maskClosable={false} - size="small" + size='small' centered > -
+
- + {t('可用邀请额度')}
- + {t('划转额度')} ({t('最低') + renderQuota(getQuotaPerUnit())}) { max={userState?.user?.aff_quota || 0} value={transferAmount} onChange={(value) => setTransferAmount(value)} - size="large" - className="w-full" + size='large' + className='w-full' />
@@ -393,8 +428,8 @@ const TopUp = () => { {/* 充值确认模态框 */} - +
+ {t('充值确认')}
} @@ -402,57 +437,80 @@ const TopUp = () => { onOk={onlineTopUp} onCancel={handleCancel} maskClosable={false} - size="small" + size='small' centered confirmLoading={confirmLoading} > -
-
+
+
{t('充值数量')}: {renderQuotaWithAmount(topUpCount)}
-
+
{t('实付金额')}: {amountLoading ? ( ) : ( - {renderAmount()} + + {renderAmount()} + )}
-
+
{t('支付方式')}: - {payWay === 'zfb' ? ( -
- - {t('支付宝')} -
- ) : ( -
- - {t('微信')} -
- )} + {(() => { + const payMethod = payMethods.find( + (method) => method.type === payWay, + ); + if (payMethod) { + return ( +
+ {payMethod.type === 'zfb' ? ( + + ) : payMethod.type === 'wx' ? ( + + ) : ( + + )} + {payMethod.name} +
+ ); + } else { + // 默认充值方式 + return payWay === 'zfb' ? ( +
+ + {t('支付宝')} +
+ ) : ( +
+ + {t('微信')} +
+ ); + } + })()}
-
+
{/* 左侧充值区域 */} -
+
{/* 在线充值卡片 */} -
-
+
+
+
@@ -460,21 +518,23 @@ const TopUp = () => { {t('在线充值')} - + {t('快速方便的充值方式')}
-
+
{userDataLoading ? ( ) : ( - -
- - {getUsername()} ({getUserRole()}) - {getUsername()} + +
+ + + {getUsername()} ({getUserRole()}) + + {getUsername()}
)} @@ -483,29 +543,33 @@ const TopUp = () => {
} > -
+
{/* 账户余额信息 */} -
- - +
+ + {t('当前余额')} {userDataLoading ? ( - + ) : ( -
+
{renderQuota(userState?.user?.quota || userQuota)}
)} - - + + {t('历史消耗')} {userDataLoading ? ( - + ) : ( -
+
{renderQuota(userState?.user?.used_quota || 0)}
)} @@ -516,47 +580,59 @@ const TopUp = () => { <> {/* 预设充值额度卡片网格 */}
- {t('选择充值额度')} -
+ + {t('选择充值额度')} + +
{presetAmounts.map((preset, index) => ( selectPresetAmount(preset)} - className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${selectedPreset === preset.value - ? 'border-blue-500' - : 'border-gray-200 hover:border-gray-300' - }`} + className={`cursor-pointer !rounded-2xl transition-all hover:shadow-md ${ + selectedPreset === preset.value + ? 'border-blue-500' + : 'border-gray-200 hover:border-gray-300' + }`} bodyStyle={{ textAlign: 'center' }} > -
- +
+ {formatLargeNumber(preset.value)}
-
- {t('实付')} ¥{(preset.value * priceRatio).toFixed(2)} +
+ {t('实付')} ¥ + {(preset.value * priceRatio).toFixed(2)}
))}
{/* 桌面端显示的自定义金额和支付按钮 */} -
+
- {t('或输入自定义金额')} + + {t('或输入自定义金额')} +
-
+
{t('充值数量')} {amountLoading ? ( - + ) : ( - {t('实付金额:') + renderAmount()} + + {t('实付金额:') + renderAmount()} + )}
{ getAmount(1); } }} - size="large" - className="w-full" - formatter={(value) => value ? `${value}` : ''} - parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} + size='large' + className='w-full' + formatter={(value) => (value ? `${value}` : '')} + parser={(value) => + value ? parseInt(value.replace(/[^\d]/g, '')) : 0 + } />
-
- + {t('微信')} + */} + {payMethods.map((payMethod) => ( + + ))}
@@ -613,39 +716,41 @@ const TopUp = () => { {!enableOnlineTopUp && ( )} - {t('兑换码充值')} + {t('兑换码充值')} - -
- + +
+ {t('使用兑换码快速充值')}
-
+
setRedemptionCode(value)} - size="large" + size='large' />
-
+
{topUpLink && ( )}
{/* 右侧邀请信息卡片 */} -
+
-
-
+
+
+
@@ -689,7 +794,7 @@ const TopUp = () => { {t('邀请奖励')} - + {t('邀请好友获得额外奖励')}
@@ -698,53 +803,56 @@ const TopUp = () => {
} > -
-
- -
- {t('待使用收益')} +
+
+ +
+ {t('待使用收益')}
-
+
{renderQuota(userState?.user?.aff_quota || 0)}
-
- - {t('总收益')} -
+
+ + {t('总收益')} +
{renderQuota(userState?.user?.aff_history_quota || 0)}
- - {t('邀请人数')} -
- + + {t('邀请人数')} +
+ {userState?.user?.aff_count || 0}
-
+
{t('邀请链接')} } > @@ -753,24 +861,24 @@ const TopUp = () => { } /> -
- -
-
-
- +
+ +
+
+
+ {t('邀请好友注册,好友充值后您可获得相应奖励')}
-
-
- +
+
+ {t('通过划转功能将奖励额度转入到您的账户余额中')}
-
-
- +
+
+ {t('邀请的好友越多,获得的奖励越多')}
@@ -785,20 +893,27 @@ const TopUp = () => { {/* 移动端底部固定的自定义金额和支付区域 */} {enableOnlineTopUp && ( -
-
+
+
-
+
{t('充值数量')} {amountLoading ? ( ) : ( - {t('实付金额:') + renderAmount()} + + {t('实付金额:') + renderAmount()} + )}
{ getAmount(1); } }} - className="w-full" - formatter={(value) => value ? `${value}` : ''} - parser={(value) => value ? parseInt(value.replace(/[^\d]/g, '')) : 0} + className='w-full' + formatter={(value) => (value ? `${value}` : '')} + parser={(value) => + value ? parseInt(value.replace(/[^\d]/g, '')) : 0 + } />
-
- + {t('微信')} + */} + {payMethods.map((payMethod) => ( + + ))}