Feat/auto group (#6590)

* feat(token): support custom auto group order

* feat(keys): enhance auto group presentation

* fix(keys): rework Auto flow border and compact inherited order

The Auto group highlight previously tinted the whole control surface
with a gradient and animated only a 1px top sweep, which read as a
background color rather than a flowing border. Replace it with a
border-only effect: an aria-hidden, pointer-events-none overlay whose
conic gradient is masked down to a thin ring hugging the rounded
perimeter, so the highlight travels around all four edges and corners
every 3.2s. The interior stays neutral with a restrained static
primary border and glow; prefers-reduced-motion hides the moving
layer while keeping the static emphasis.

The inherited global Auto order also rendered as spacious two-line
rows with circular sequence markers, wasting drawer space. Render it
as a compact wrapping strip of one-line chips (index, name, ratio
badge) with descriptions kept accessible via title and sr-only text,
scrolling only past a much smaller max height.

Custom add/remove/reorder editing, empty-array inheritance semantics,
and the submit payload are unchanged.

* fix(keys): preserve Auto inheritance and unify effects

* refactor(keys): temporarily disable AutoGroupBadge in api-key-group-cell
This commit is contained in:
Calcium-Ion
2026-08-01 23:19:01 +08:00
committed by GitHub
parent bd585d78ef
commit 0ab0202060
57 changed files with 3922 additions and 210 deletions
+124 -8
View File
@@ -7,30 +7,115 @@ import (
"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"
)
func buildMaskedTokenResponse(token *model.Token) *model.Token {
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 tokenRequest struct {
model.Token
AutoGroups tokenAutoGroupsInput `json:"auto_groups"`
}
type tokenResponse struct {
*model.Token
AutoGroups []string `json:"auto_groups"`
}
func buildMaskedTokenResponse(token *model.Token) *tokenResponse {
if token == nil {
return nil
}
maskedToken := *token
maskedToken.Key = token.GetMaskedKey()
return &maskedToken
autoGroups, err := token.GetAutoGroups()
if err != nil {
common.SysError(fmt.Sprintf("failed to parse auto groups for token %d: %v", token.Id, err))
autoGroups = nil
}
if len(autoGroups) == 0 {
autoGroups = nil
}
return &tokenResponse{Token: &maskedToken, AutoGroups: autoGroups}
}
func buildMaskedTokenResponses(tokens []*model.Token) []*model.Token {
maskedTokens := make([]*model.Token, 0, len(tokens))
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 GetAllTokens(c *gin.Context) {
userId := c.GetInt("id")
pageInfo := common.GetPageQuery(c)
@@ -77,6 +162,18 @@ func GetToken(c *gin.Context) {
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")
@@ -165,12 +262,13 @@ func GetTokenUsage(c *gin.Context) {
}
func AddToken(c *gin.Context) {
token := model.Token{}
err := c.ShouldBindJSON(&token)
request := tokenRequest{}
err := c.ShouldBindJSON(&request)
if err != nil {
common.ApiError(c, err)
return
}
token := request.Token
if len(token.Name) > 50 {
common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong)
return
@@ -201,6 +299,14 @@ func AddToken(c *gin.Context) {
})
return
}
if token.Group == "auto" {
if !setTokenAutoGroups(c, &token, request.AutoGroups.Groups) {
return
}
} else {
token.CrossGroupRetry = false
_ = token.SetAutoGroups(nil)
}
key, err := common.GenerateKey()
if err != nil {
common.ApiErrorI18n(c, i18n.MsgTokenGenerateFailed)
@@ -221,6 +327,7 @@ func AddToken(c *gin.Context) {
AllowIps: token.AllowIps,
Group: token.Group,
CrossGroupRetry: token.CrossGroupRetry,
AutoGroups: token.AutoGroups,
}
err = cleanToken.Insert()
if err != nil {
@@ -250,12 +357,13 @@ func DeleteToken(c *gin.Context) {
func UpdateToken(c *gin.Context) {
userId := c.GetInt("id")
statusOnly := c.Query("status_only")
token := model.Token{}
err := c.ShouldBindJSON(&token)
request := tokenRequest{}
err := c.ShouldBindJSON(&request)
if err != nil {
common.ApiError(c, err)
return
}
token := request.Token
if len(token.Name) > 50 {
common.ApiErrorI18n(c, i18n.MsgTokenNameTooLong)
return
@@ -299,6 +407,14 @@ func UpdateToken(c *gin.Context) {
cleanToken.AllowIps = token.AllowIps
cleanToken.Group = token.Group
cleanToken.CrossGroupRetry = token.CrossGroupRetry
if token.Group != "auto" {
cleanToken.CrossGroupRetry = false
_ = cleanToken.SetAutoGroups(nil)
} else if request.AutoGroups.Set {
if !setTokenAutoGroups(c, cleanToken, request.AutoGroups.Groups) {
return
}
}
}
err = cleanToken.Update()
if err != nil {