package controller import ( "fmt" "net/http" "strconv" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" ) type tokenAutoGroupsInput struct { Set bool Groups []string } func (input *tokenAutoGroupsInput) UnmarshalJSON(data []byte) error { input.Set = true if strings.TrimSpace(string(data)) == "null" { input.Groups = nil return nil } return common.Unmarshal(data, &input.Groups) } type tokenGroupPoolInput struct { Set bool Entries []model.TokenGroupEntry } func (input *tokenGroupPoolInput) UnmarshalJSON(data []byte) error { input.Set = true if strings.TrimSpace(string(data)) == "null" { input.Entries = nil return nil } return common.Unmarshal(data, &input.Entries) } type tokenRequest struct { model.Token AstraDetectionEnabled *bool `json:"astra_detection_enabled"` AstraAutoSwitch *bool `json:"astra_auto_switch"` RoutingPolicy *model.TokenRoutingPolicy `json:"routing_policy"` AutoGroups tokenAutoGroupsInput `json:"auto_groups"` GroupPool tokenGroupPoolInput `json:"group_pool"` } type tokenResponse struct { *model.Token RoutingPolicy *model.TokenRoutingPolicy `json:"routing_policy,omitempty"` AutoGroups []string `json:"auto_groups"` GroupPool []model.TokenGroupEntry `json:"group_pool,omitempty"` } func maxTokenQuota() int { quota, err := common.WalletQuotaFromDecimalStrict( decimal.NewFromInt(1_000_000_000).Mul(decimal.NewFromFloat(common.QuotaPerUnit)), ) if err != nil { return common.MaxWalletQuota } return quota } func buildMaskedTokenResponse(token *model.Token) *tokenResponse { if token == nil { return nil } maskedToken := *token maskedToken.Key = token.GetMaskedKey() groupPool, err := token.GetGroupPool() if err != nil { common.SysError(fmt.Sprintf("failed to parse auto groups for token %d: %v", token.Id, err)) groupPool = nil } autoGroups := make([]string, 0, len(groupPool)) for _, entry := range groupPool { autoGroups = append(autoGroups, entry.Group) } if len(autoGroups) == 0 { autoGroups = nil } policy, _ := token.GetRoutingPolicy() return &tokenResponse{Token: &maskedToken, AutoGroups: autoGroups, GroupPool: groupPool, RoutingPolicy: policy} } func buildMaskedTokenResponses(tokens []*model.Token) []*tokenResponse { maskedTokens := make([]*tokenResponse, 0, len(tokens)) for _, token := range tokens { maskedTokens = append(maskedTokens, buildMaskedTokenResponse(token)) } return maskedTokens } func getTokenRequestUserGroup(c *gin.Context) (string, error) { if userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup); userGroup != "" { return userGroup, nil } if userGroup := c.GetString("group"); userGroup != "" { return userGroup, nil } return model.GetUserGroup(c.GetInt("id"), false) } func setTokenAutoGroups(c *gin.Context, token *model.Token, groups []string) bool { if len(groups) == 0 { if err := token.SetAutoGroups(nil); err != nil { common.ApiError(c, err) return false } return true } maxCount := setting.GetMaxTokenAutoGroups() if len(groups) > maxCount { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsTooMany, map[string]any{"Max": maxCount}) return false } userGroup, err := getTokenRequestUserGroup(c) if err != nil { common.ApiError(c, err) return false } seen := make(map[string]struct{}, len(groups)) for _, group := range groups { if _, ok := seen[group]; ok { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsDuplicate, map[string]any{"Group": group}) return false } seen[group] = struct{}{} if !service.IsUserSelectableGroup(userGroup, group) { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsInvalid, map[string]any{"Group": group}) return false } } if err := token.SetAutoGroups(groups); err != nil { common.ApiError(c, err) return false } return true } func setTokenBackupGroup(c *gin.Context, token *model.Token, groups []string) bool { maxCount := setting.GetMaxTokenAutoGroups() if len(groups) > maxCount { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsTooMany, map[string]any{"Max": maxCount}) return false } for _, group := range groups { if group == token.Group { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsDuplicate, map[string]any{"Group": group}) return false } } if !setTokenAutoGroups(c, token, groups) { return false } token.CrossGroupRetry = len(groups) > 0 return true } func setTokenGroupPool(c *gin.Context, token *model.Token, entries []model.TokenGroupEntry) bool { maxCount := setting.GetMaxTokenAutoGroups() if len(entries) > maxCount { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsTooMany, map[string]any{"Max": maxCount}) return false } userGroup, err := getTokenRequestUserGroup(c) if err != nil { common.ApiError(c, err) return false } seen := make(map[string]struct{}, len(entries)) for _, entry := range entries { if entry.Group == token.Group { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsDuplicate, map[string]any{"Group": entry.Group}) return false } if _, ok := seen[entry.Group]; ok { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsDuplicate, map[string]any{"Group": entry.Group}) return false } seen[entry.Group] = struct{}{} if !service.IsUserSelectableGroup(userGroup, entry.Group) { common.ApiErrorI18n(c, i18n.MsgTokenAutoGroupsInvalid, map[string]any{"Group": entry.Group}) return false } } if err := token.SetGroupPool(entries); err != nil { common.ApiError(c, err) return false } token.CrossGroupRetry = len(entries) > 0 return true } func GetAllTokens(c *gin.Context) { userId := c.GetInt("id") pageInfo := common.GetPageQuery(c) tokens, err := model.GetAllUserTokens(userId, pageInfo.GetStartIdx(), pageInfo.GetPageSize()) if err != nil { common.ApiError(c, err) return } total, _ := model.CountUserTokens(userId) pageInfo.SetTotal(int(total)) pageInfo.SetItems(buildMaskedTokenResponses(tokens)) common.ApiSuccess(c, pageInfo) } func SearchTokens(c *gin.Context) { userId := c.GetInt("id") keyword := c.Query("keyword") token := c.Query("token") pageInfo := common.GetPageQuery(c) tokens, total, err := model.SearchUserTokens(userId, keyword, token, pageInfo.GetStartIdx(), pageInfo.GetPageSize()) if err != nil { common.ApiError(c, err) return } pageInfo.SetTotal(int(total)) pageInfo.SetItems(buildMaskedTokenResponses(tokens)) common.ApiSuccess(c, pageInfo) } func GetToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) userId := c.GetInt("id") if err != nil { common.ApiError(c, err) return } token, err := model.GetTokenByIds(id, userId) if err != nil { common.ApiError(c, err) return } common.ApiSuccess(c, buildMaskedTokenResponse(token)) } func GetTokenAutoGroups(c *gin.Context) { userGroup, err := getTokenRequestUserGroup(c) if err != nil { common.ApiError(c, err) return } common.ApiSuccess(c, gin.H{ "groups": service.GetUserAutoGroup(userGroup), "max_count": setting.GetMaxTokenAutoGroups(), }) } func GetTokenKey(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) userId := c.GetInt("id") if err != nil { common.ApiError(c, err) return } token, err := model.GetTokenByIds(id, userId) if err != nil { common.ApiError(c, err) return } params := tokenAuditParams(c) params["id"], params["name"] = token.Id, token.Name common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) common.ApiSuccess(c, gin.H{ "key": token.GetFullKey(), }) } func GetTokenStatus(c *gin.Context) { tokenId := c.GetInt("token_id") userId := c.GetInt("id") token, err := model.GetTokenByIds(tokenId, userId) if err != nil { common.ApiError(c, err) return } expiredAt := token.ExpiredTime if expiredAt == -1 { expiredAt = 0 } c.JSON(http.StatusOK, gin.H{ "object": "credit_summary", "total_granted": token.RemainQuota, "total_used": 0, // not supported currently "total_available": token.RemainQuota, "expires_at": expiredAt * 1000, }) } func GetTokenUsage(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "No Authorization header", }) return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { c.JSON(http.StatusUnauthorized, gin.H{ "success": false, "message": "Invalid Bearer token", }) return } tokenKey := parts[1] token, err := model.GetTokenByKey(strings.TrimPrefix(tokenKey, "sk-"), false) if err != nil { common.SysError("failed to get token by key: " + err.Error()) common.ApiErrorI18n(c, i18n.MsgTokenGetInfoFailed) return } expiredAt := token.ExpiredTime if expiredAt == -1 { expiredAt = 0 } c.JSON(http.StatusOK, gin.H{ "code": true, "message": "ok", "data": gin.H{ "object": "token_usage", "name": token.Name, "total_granted": token.RemainQuota + token.UsedQuota, "total_used": token.UsedQuota, "total_available": token.RemainQuota, "unlimited_quota": token.UnlimitedQuota, "model_limits": token.GetModelLimitsMap(), "model_limits_enabled": token.ModelLimitsEnabled, "expires_at": expiredAt, }, }) } func AddToken(c *gin.Context) { request := tokenRequest{} err := c.ShouldBindJSON(&request) if err != nil { common.ApiError(c, err) return } token := request.Token token.AstraDetectionEnabled = true if request.AstraDetectionEnabled != nil { token.AstraDetectionEnabled = *request.AstraDetectionEnabled } if request.AstraAutoSwitch != nil { token.AstraAutoSwitch = *request.AstraAutoSwitch } if !token.AstraDetectionEnabled { token.AstraAutoSwitch = false } if len(token.Name) > 50 { common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong) return } params := tokenAuditParams(c) params["name"] = token.Name // 非无限额度时,检查额度值是否超出有效范围 if !token.UnlimitedQuota { if token.RemainQuota < 0 { common.ApiErrorI18n(c, i18n.MsgTokenQuotaNegative) return } maxQuotaValue := maxTokenQuota() if token.RemainQuota > maxQuotaValue { common.ApiErrorI18n(c, i18n.MsgTokenQuotaExceedMax, map[string]any{"Max": maxQuotaValue}) return } } // 检查用户令牌数量是否已达上限 maxTokens := operation_setting.GetMaxUserTokens() count, err := model.CountUserTokens(c.GetInt("id")) if err != nil { common.ApiError(c, err) return } if int(count) >= maxTokens { c.JSON(http.StatusOK, gin.H{ "success": false, "message": fmt.Sprintf("已达到最大令牌数量限制 (%d)", maxTokens), }) return } if request.RoutingPolicy != nil { if !setTokenRoutingPolicy(c, &token, request.RoutingPolicy) { return } } else if token.Group == "auto" { if !setTokenAutoGroups(c, &token, request.AutoGroups.Groups) { return } } else if request.GroupPool.Set { if !setTokenGroupPool(c, &token, request.GroupPool.Entries) { return } } else { if !setTokenBackupGroup(c, &token, request.AutoGroups.Groups) { return } } if token.RoutingPolicy == "" { policy, err := token.LegacyRoutingPolicy() if err != nil { common.ApiError(c, err) return } body, err := common.Marshal(policy) if err != nil { common.ApiError(c, err) return } token.RoutingPolicy = string(body) } key, err := common.GenerateKey() if err != nil { common.ApiErrorI18n(c, i18n.MsgTokenGenerateFailed) common.SysLog("failed to generate token key: " + err.Error()) return } cleanToken := model.Token{ UserId: c.GetInt("id"), Name: token.Name, Key: key, CreatedTime: common.GetTimestamp(), AccessedTime: common.GetTimestamp(), ExpiredTime: token.ExpiredTime, RemainQuota: token.RemainQuota, UnlimitedQuota: token.UnlimitedQuota, ModelLimitsEnabled: token.ModelLimitsEnabled, ModelLimits: token.ModelLimits, AllowIps: token.AllowIps, Group: token.Group, CrossGroupRetry: token.CrossGroupRetry, AstraAutoSwitch: token.AstraAutoSwitch, AstraDetectionEnabled: token.AstraDetectionEnabled, AutoGroups: token.AutoGroups, RoutingPolicy: token.RoutingPolicy, } err = cleanToken.Insert() if err != nil { common.ApiError(c, err) return } params["id"] = cleanToken.Id common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) } func DeleteToken(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) userId := c.GetInt("id") token, err := model.GetTokenByIds(id, userId) if err != nil { common.ApiError(c, err) return } params := tokenAuditParams(c) params["id"], params["name"] = token.Id, token.Name err = token.Delete() if err != nil { common.ApiError(c, err) return } common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", }) } func UpdateToken(c *gin.Context) { userId := c.GetInt("id") statusOnly := c.Query("status_only") request := tokenRequest{} err := c.ShouldBindJSON(&request) if err != nil { common.ApiError(c, err) return } token := request.Token params := tokenAuditParams(c) if token.Id > 0 { params["id"] = token.Id } if len(token.Name) > 50 { common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong) return } if !token.UnlimitedQuota { if token.RemainQuota < 0 { common.ApiErrorI18n(c, i18n.MsgTokenQuotaNegative) return } maxQuotaValue := maxTokenQuota() if token.RemainQuota > maxQuotaValue { common.ApiErrorI18n(c, i18n.MsgTokenQuotaExceedMax, map[string]any{"Max": maxQuotaValue}) return } } cleanToken, err := model.GetTokenByIds(token.Id, userId) if err != nil { common.ApiError(c, err) return } params["name"] = cleanToken.Name previous := *cleanToken if token.Status == common.TokenStatusEnabled { if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 { common.ApiErrorI18n(c, i18n.MsgTokenExpiredCannotEnable) return } if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota { common.ApiErrorI18n(c, i18n.MsgTokenExhaustedCannotEable) return } } if statusOnly != "" { cleanToken.Status = token.Status } else { // If you add more fields, please also update token.Update() cleanToken.Name = token.Name cleanToken.ExpiredTime = token.ExpiredTime cleanToken.RemainQuota = token.RemainQuota cleanToken.UnlimitedQuota = token.UnlimitedQuota cleanToken.ModelLimitsEnabled = token.ModelLimitsEnabled cleanToken.ModelLimits = token.ModelLimits cleanToken.AllowIps = token.AllowIps if request.AstraDetectionEnabled != nil { cleanToken.AstraDetectionEnabled = *request.AstraDetectionEnabled } if request.AstraAutoSwitch != nil { cleanToken.AstraAutoSwitch = *request.AstraAutoSwitch } if !cleanToken.AstraDetectionEnabled { cleanToken.AstraAutoSwitch = false } if request.RoutingPolicy != nil { if !setTokenRoutingPolicy(c, cleanToken, request.RoutingPolicy) { return } } else if cleanToken.RoutingPolicy == "" { cleanToken.Group = token.Group cleanToken.CrossGroupRetry = token.CrossGroupRetry if token.Group != "auto" { if request.GroupPool.Set { if !setTokenGroupPool(c, cleanToken, request.GroupPool.Entries) { return } } else if !setTokenBackupGroup(c, cleanToken, request.AutoGroups.Groups) { return } } else if request.AutoGroups.Set { if !setTokenAutoGroups(c, cleanToken, request.AutoGroups.Groups) { return } } } } err = cleanToken.Update() if err != nil { common.ApiError(c, err) return } params["name"] = cleanToken.Name if statusOnly != "" { params["from"], params["to"] = previous.Status, cleanToken.Status } else { changedFields := []string{} for _, field := range []struct { name string changed bool }{ {"name", previous.Name != cleanToken.Name}, {"expired_time", previous.ExpiredTime != cleanToken.ExpiredTime}, {"remain_quota", previous.RemainQuota != cleanToken.RemainQuota}, {"unlimited_quota", previous.UnlimitedQuota != cleanToken.UnlimitedQuota}, {"model_limits_enabled", previous.ModelLimitsEnabled != cleanToken.ModelLimitsEnabled}, {"model_limits", previous.ModelLimits != cleanToken.ModelLimits}, {"allow_ips", (previous.AllowIps == nil) != (cleanToken.AllowIps == nil) || (previous.AllowIps != nil && cleanToken.AllowIps != nil && *previous.AllowIps != *cleanToken.AllowIps)}, {"group", previous.Group != cleanToken.Group}, {"cross_group_retry", previous.CrossGroupRetry != cleanToken.CrossGroupRetry}, {"astra_auto_switch", previous.AstraAutoSwitch != cleanToken.AstraAutoSwitch}, {"astra_detection_enabled", previous.AstraDetectionEnabled != cleanToken.AstraDetectionEnabled}, {"auto_groups", previous.AutoGroups != cleanToken.AutoGroups}, {"routing_policy", previous.RoutingPolicy != cleanToken.RoutingPolicy}, } { if field.changed { changedFields = append(changedFields, field.name) } } params["changed_fields"] = changedFields } common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": buildMaskedTokenResponse(cleanToken), }) } type TokenBatch struct { Ids []int `json:"ids"` } func DeleteTokenBatch(c *gin.Context) { tokenBatch := TokenBatch{} if err := c.ShouldBindJSON(&tokenBatch); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } params := tokenBatchAuditParams(c, tokenBatch.Ids) if len(tokenBatch.Ids) == 0 { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } userId := c.GetInt("id") count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId) if err != nil { common.ApiError(c, err) return } params["count"] = count common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", "data": count, }) } func GetTokenKeysBatch(c *gin.Context) { tokenBatch := TokenBatch{} if err := c.ShouldBindJSON(&tokenBatch); err != nil { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } params := tokenBatchAuditParams(c, tokenBatch.Ids) if len(tokenBatch.Ids) == 0 { common.ApiErrorI18n(c, i18n.MsgInvalidParams) return } if len(tokenBatch.Ids) > 100 { common.ApiErrorI18n(c, i18n.MsgBatchTooMany, map[string]any{"Max": 100}) return } userId := c.GetInt("id") tokens, err := model.GetTokenKeysByIds(tokenBatch.Ids, userId) if err != nil { common.ApiError(c, err) return } keysMap := make(map[int]string) returnedIDs := make([]int, 0, len(tokens)) for _, t := range tokens { keysMap[t.Id] = t.GetFullKey() returnedIDs = append(returnedIDs, t.Id) } params["count"] = len(tokens) params["returned_ids"] = returnedIDs common.SetContextKey(c, constant.ContextKeyTokenAuditSucceeded, true) common.ApiSuccess(c, gin.H{"keys": keysMap}) }