Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
129d1b08
authored
Jun 17, 2025
by
Calcium-Ion
Committed by
GitHub
Jun 17, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1239 from QuantumNous/auto_group
feat: auto分组
parents
4238068c
4ec70129
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
543 additions
and
263 deletions
+543
-263
controller/channel-test.go
+3
-3
controller/group.go
+8
-1
controller/misc.go
+40
-39
controller/model.go
+16
-2
controller/playground.go
+4
-3
controller/relay.go
+2
-2
controller/user.go
+3
-0
middleware/distributor.go
+11
-4
model/cache.go
+40
-1
model/option.go
+7
-1
relay/helper/price.go
+43
-12
relay/relay-image.go
+1
-1
relay/relay-text.go
+2
-3
relay/websocket.go
+6
-37
service/quota.go
+26
-24
setting/auto_group.go
+31
-0
setting/user_usable_group.go
+7
-0
web/src/components/settings/OperationSetting.js
+5
-1
web/src/pages/Setting/Operation/GroupRatioSettings.js
+55
-0
web/src/pages/Token/EditToken.js
+233
-129
No files found.
controller/channel-test.go
View file @
129d1b08
...
...
@@ -165,8 +165,8 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
tok
:=
time
.
Now
()
milliseconds
:=
tok
.
Sub
(
tik
)
.
Milliseconds
()
consumedTime
:=
float64
(
milliseconds
)
/
1000.0
other
:=
service
.
GenerateTextOtherInfo
(
c
,
info
,
priceData
.
ModelRatio
,
priceData
.
GroupRatio
,
priceData
.
CompletionRatio
,
usage
.
PromptTokensDetails
.
CachedTokens
,
priceData
.
CacheRatio
,
priceData
.
ModelPrice
,
priceData
.
UserGroup
Ratio
)
other
:=
service
.
GenerateTextOtherInfo
(
c
,
info
,
priceData
.
ModelRatio
,
priceData
.
GroupRatio
Info
.
GroupRatio
,
priceData
.
CompletionRatio
,
usage
.
PromptTokensDetails
.
CachedTokens
,
priceData
.
CacheRatio
,
priceData
.
ModelPrice
,
priceData
.
GroupRatioInfo
.
GroupSpecial
Ratio
)
model
.
RecordConsumeLog
(
c
,
1
,
channel
.
Id
,
usage
.
PromptTokens
,
usage
.
CompletionTokens
,
info
.
OriginModelName
,
"模型测试"
,
quota
,
"模型测试"
,
0
,
quota
,
int
(
consumedTime
),
false
,
info
.
Group
,
other
)
common
.
SysLog
(
fmt
.
Sprintf
(
"testing channel #%d, response:
\n
%s"
,
channel
.
Id
,
string
(
respBody
)))
...
...
@@ -312,7 +312,7 @@ func testAllChannels(notify bool) error {
channel
.
UpdateResponseTime
(
milliseconds
)
time
.
Sleep
(
common
.
RequestInterval
)
}
if
notify
{
service
.
NotifyRootUser
(
dto
.
NotifyTypeChannelTest
,
"通道测试完成"
,
"所有通道测试已完成"
)
}
...
...
controller/group.go
View file @
129d1b08
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"
:
""
,
...
...
controller/misc.go
View file @
129d1b08
...
...
@@ -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
,
...
...
controller/model.go
View file @
129d1b08
...
...
@@ -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
])
...
...
controller/playground.go
View file @
129d1b08
...
...
@@ -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 无可用渠道"
,
g
roup
,
playgroundRequest
.
Model
)
message
:=
fmt
.
Sprintf
(
"当前分组 %s 下对于模型 %s 无可用渠道"
,
finalG
roup
,
playgroundRequest
.
Model
)
openaiErr
=
service
.
OpenAIErrorWrapperLocal
(
errors
.
New
(
message
),
"get_playground_channel_failed"
,
http
.
StatusInternalServerError
)
return
}
...
...
controller/relay.go
View file @
129d1b08
...
...
@@ -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
...
...
controller/user.go
View file @
129d1b08
...
...
@@ -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
,
...
...
middleware/distributor.go
View file @
129d1b08
...
...
@@ -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
))
...
...
model/cache.go
View file @
129d1b08
...
...
@@ -5,10 +5,13 @@ import (
"fmt"
"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 +78,43 @@ 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
{
if
common
.
DebugEnabled
{
println
(
"autoGroup:"
,
autoGroup
)
}
channel
,
_
=
getRandomSatisfiedChannel
(
autoGroup
,
model
,
retry
)
if
channel
==
nil
{
continue
}
else
{
c
.
Set
(
"auto_group"
,
autoGroup
)
selectGroup
=
autoGroup
if
common
.
DebugEnabled
{
println
(
"selectGroup:"
,
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-*"
}
...
...
model/option.go
View file @
129d1b08
...
...
@@ -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"
]
=
""
...
...
@@ -192,7 +194,7 @@ func updateOptionMap(key string, value string) (err error) {
common
.
ImageDownloadPermission
=
intValue
}
}
if
strings
.
HasSuffix
(
key
,
"Enabled"
)
||
key
==
"DefaultCollapseSidebar"
{
if
strings
.
HasSuffix
(
key
,
"Enabled"
)
||
key
==
"DefaultCollapseSidebar"
||
key
==
"DefaultUseAutoGroup"
{
boolValue
:=
value
==
"true"
switch
key
{
case
"PasswordRegisterEnabled"
:
...
...
@@ -261,6 +263,8 @@ func updateOptionMap(key string, value string) (err error) {
common
.
SMTPSSLEnabled
=
boolValue
case
"WorkerAllowHttpImageRequestEnabled"
:
setting
.
WorkerAllowHttpImageRequestEnabled
=
boolValue
case
"DefaultUseAutoGroup"
:
setting
.
DefaultUseAutoGroup
=
boolValue
}
}
switch
key
{
...
...
@@ -287,6 +291,8 @@ func updateOptionMap(key string, value string) (err error) {
setting
.
PayAddress
=
value
case
"Chats"
:
err
=
setting
.
UpdateChatsByJsonString
(
value
)
case
"AutoGroups"
:
err
=
setting
.
UpdateAutoGroupsByJsonString
(
value
)
case
"CustomCallbackAddress"
:
setting
.
CustomCallbackAddress
=
value
case
"EpayId"
:
...
...
relay/helper/price.go
View file @
129d1b08
...
...
@@ -11,6 +11,11 @@ import (
"github.com/gin-gonic/gin"
)
type
GroupRatioInfo
struct
{
GroupRatio
float64
GroupSpecialRatio
float64
}
type
PriceData
struct
{
ModelPrice
float64
ModelRatio
float64
...
...
@@ -18,23 +23,50 @@ type PriceData struct {
CacheRatio
float64
CacheCreationRatio
float64
ImageRatio
float64
GroupRatio
float64
UserGroupRatio
float64
UsePrice
bool
ShouldPreConsumedQuota
int
GroupRatioInfo
GroupRatioInfo
}
func
(
p
PriceData
)
ToSetting
()
string
{
return
fmt
.
Sprintf
(
"ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f"
,
p
.
ModelPrice
,
p
.
ModelRatio
,
p
.
CompletionRatio
,
p
.
CacheRatio
,
p
.
GroupRatio
,
p
.
UsePrice
,
p
.
CacheCreationRatio
,
p
.
ShouldPreConsumedQuota
,
p
.
ImageRatio
)
return
fmt
.
Sprintf
(
"ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f"
,
p
.
ModelPrice
,
p
.
ModelRatio
,
p
.
CompletionRatio
,
p
.
CacheRatio
,
p
.
GroupRatio
Info
.
GroupRatio
,
p
.
UsePrice
,
p
.
CacheCreationRatio
,
p
.
ShouldPreConsumedQuota
,
p
.
ImageRatio
)
}
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
)
userGroupRatio
,
ok
:=
setting
.
GetGroupGroupRatio
(
info
.
UserGroup
,
info
.
Group
)
// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.Group if present
func
HandleGroupRatio
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
)
GroupRatioInfo
{
groupRatioInfo
:=
GroupRatioInfo
{
GroupRatio
:
1.0
,
// default ratio
GroupSpecialRatio
:
1.0
,
// default user group ratio
}
// check auto group
autoGroup
,
exists
:=
ctx
.
Get
(
"auto_group"
)
if
exists
{
if
common
.
DebugEnabled
{
println
(
fmt
.
Sprintf
(
"final group: %s"
,
autoGroup
))
}
relayInfo
.
Group
=
autoGroup
.
(
string
)
}
// check user group special ratio
userGroupRatio
,
ok
:=
setting
.
GetGroupGroupRatio
(
relayInfo
.
UserGroup
,
relayInfo
.
Group
)
if
ok
{
groupRatio
=
userGroupRatio
// user group special ratio
groupRatioInfo
.
GroupSpecialRatio
=
userGroupRatio
groupRatioInfo
.
GroupRatio
=
userGroupRatio
}
else
{
// normal group ratio
groupRatioInfo
.
GroupRatio
=
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
)
groupRatioInfo
:=
HandleGroupRatio
(
c
,
info
)
var
preConsumedQuota
int
var
modelRatio
float64
var
completionRatio
float64
...
...
@@ -64,18 +96,17 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
cacheRatio
,
_
=
operation_setting
.
GetCacheRatio
(
info
.
OriginModelName
)
cacheCreationRatio
,
_
=
operation_setting
.
GetCreateCacheRatio
(
info
.
OriginModelName
)
imageRatio
,
_
=
operation_setting
.
GetImageRatio
(
info
.
OriginModelName
)
ratio
:=
modelRatio
*
groupRatio
ratio
:=
modelRatio
*
groupRatio
Info
.
GroupRatio
preConsumedQuota
=
int
(
float64
(
preConsumedTokens
)
*
ratio
)
}
else
{
preConsumedQuota
=
int
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatio
)
preConsumedQuota
=
int
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatio
Info
.
GroupRatio
)
}
priceData
:=
PriceData
{
ModelPrice
:
modelPrice
,
ModelRatio
:
modelRatio
,
CompletionRatio
:
completionRatio
,
GroupRatio
:
groupRatio
,
UserGroupRatio
:
userGroupRatio
,
GroupRatioInfo
:
groupRatioInfo
,
UsePrice
:
usePrice
,
CacheRatio
:
cacheRatio
,
ImageRatio
:
imageRatio
,
...
...
relay/relay-image.go
View file @
129d1b08
...
...
@@ -162,7 +162,7 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode {
// reset model price
priceData
.
ModelPrice
*=
sizeRatio
*
qualityRatio
*
float64
(
imageRequest
.
N
)
quota
=
int
(
priceData
.
ModelPrice
*
priceData
.
GroupRatio
*
common
.
QuotaPerUnit
)
quota
=
int
(
priceData
.
ModelPrice
*
priceData
.
GroupRatio
Info
.
GroupRatio
*
common
.
QuotaPerUnit
)
userQuota
,
err
=
model
.
GetUserQuota
(
relayInfo
.
UserId
,
false
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapperLocal
(
err
,
"get_user_quota_failed"
,
http
.
StatusInternalServerError
)
...
...
relay/relay-text.go
View file @
129d1b08
...
...
@@ -361,9 +361,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
cacheRatio
:=
priceData
.
CacheRatio
imageRatio
:=
priceData
.
ImageRatio
modelRatio
:=
priceData
.
ModelRatio
groupRatio
:=
priceData
.
GroupRatio
groupRatio
:=
priceData
.
GroupRatio
Info
.
GroupRatio
modelPrice
:=
priceData
.
ModelPrice
userGroupRatio
:=
priceData
.
UserGroupRatio
// Convert values to decimal for precise calculation
dPromptTokens
:=
decimal
.
NewFromInt
(
int64
(
promptTokens
))
...
...
@@ -511,7 +510,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
if
extraContent
!=
""
{
logContent
+=
", "
+
extraContent
}
other
:=
service
.
GenerateTextOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
cacheTokens
,
cacheRatio
,
modelPrice
,
userGroup
Ratio
)
other
:=
service
.
GenerateTextOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
cacheTokens
,
cacheRatio
,
modelPrice
,
priceData
.
GroupRatioInfo
.
GroupSpecial
Ratio
)
if
imageTokens
!=
0
{
other
[
"image"
]
=
true
other
[
"image_ratio"
]
=
imageRatio
...
...
relay/websocket.go
View file @
129d1b08
...
...
@@ -6,12 +6,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"net/http"
"one-api/common"
"one-api/dto"
relaycommon
"one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
"one-api/setting/operation_setting"
)
func
WssHelper
(
c
*
gin
.
Context
,
ws
*
websocket
.
Conn
)
(
openaiErr
*
dto
.
OpenAIErrorWithStatusCode
)
{
...
...
@@ -39,43 +37,14 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
//isModelMapped = true
}
}
//relayInfo.UpstreamModelName = textRequest.Model
modelPrice
,
getModelPriceSuccess
:=
operation_setting
.
GetModelPrice
(
relayInfo
.
UpstreamModelName
,
false
)
groupRatio
:=
setting
.
GetGroupRatio
(
relayInfo
.
Group
)
var
preConsumedQuota
int
var
ratio
float64
var
modelRatio
float64
//err := service.SensitiveWordsCheck(textRequest)
//if constant.ShouldCheckPromptSensitive() {
// err = checkRequestSensitive(textRequest, relayInfo)
// if err != nil {
// return service.OpenAIErrorWrapperLocal(err, "sensitive_words_detected", http.StatusBadRequest)
// }
//}
//promptTokens, err := getWssPromptTokens(realtimeEvent, relayInfo)
//// count messages token error 计算promptTokens错误
//if err != nil {
// return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
//}
//
if
!
getModelPriceSuccess
{
preConsumedTokens
:=
common
.
PreConsumedQuota
//if realtimeEvent.Session.MaxResponseOutputTokens != 0 {
// preConsumedTokens = promptTokens + int(realtimeEvent.Session.MaxResponseOutputTokens)
//}
modelRatio
,
_
=
operation_setting
.
GetModelRatio
(
relayInfo
.
UpstreamModelName
)
ratio
=
modelRatio
*
groupRatio
preConsumedQuota
=
int
(
float64
(
preConsumedTokens
)
*
ratio
)
}
else
{
preConsumedQuota
=
int
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatio
)
relayInfo
.
UsePrice
=
true
priceData
,
err
:=
helper
.
ModelPriceHelper
(
c
,
relayInfo
,
0
,
0
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapperLocal
(
err
,
"model_price_error"
,
http
.
StatusInternalServerError
)
}
// pre-consume quota 预消耗配额
preConsumedQuota
,
userQuota
,
openaiErr
:=
preConsumeQuota
(
c
,
preConsumedQuota
,
relayInfo
)
preConsumedQuota
,
userQuota
,
openaiErr
:=
preConsumeQuota
(
c
,
pr
iceData
.
ShouldPr
eConsumedQuota
,
relayInfo
)
if
openaiErr
!=
nil
{
return
openaiErr
}
...
...
@@ -113,6 +82,6 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
return
openaiErr
}
service
.
PostWssConsumeQuota
(
c
,
relayInfo
,
relayInfo
.
UpstreamModelName
,
usage
.
(
*
dto
.
RealtimeUsage
),
preConsumedQuota
,
userQuota
,
modelRatio
,
groupRatio
,
modelPrice
,
getModelPriceSuccess
,
""
)
userQuota
,
priceData
,
""
)
return
nil
}
service/quota.go
View file @
129d1b08
...
...
@@ -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
{
g
roupRatio
=
userGroupRatio
actualG
roupRatio
=
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
:
g
roupRatio
,
GroupRatio
:
actualG
roupRatio
,
}
quota
:=
calculateAudioQuota
(
quotaInfo
)
...
...
@@ -134,8 +144,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
}
func
PostWssConsumeQuota
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
modelName
string
,
usage
*
dto
.
RealtimeUsage
,
preConsumedQuota
int
,
userQuota
int
,
modelRatio
float64
,
groupRatio
float64
,
modelPrice
float64
,
usePrice
bool
,
extraContent
string
)
{
usage
*
dto
.
RealtimeUsage
,
preConsumedQuota
int
,
userQuota
int
,
priceData
helper
.
PriceData
,
extraContent
string
)
{
useTimeSeconds
:=
time
.
Now
()
.
Unix
()
-
relayInfo
.
StartTime
.
Unix
()
textInputTokens
:=
usage
.
InputTokenDetails
.
TextTokens
...
...
@@ -149,11 +158,11 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
audioRatio
:=
decimal
.
NewFromFloat
(
operation_setting
.
GetAudioRatio
(
relayInfo
.
OriginModelName
))
audioCompletionRatio
:=
decimal
.
NewFromFloat
(
operation_setting
.
GetAudioCompletionRatio
(
modelName
))
actualGroupRatio
:=
group
Ratio
userGroupRatio
,
ok
:=
setting
.
GetGroupGroupRatio
(
relayInfo
.
UserGroup
,
relayInfo
.
Group
)
if
ok
{
actualGroupRatio
=
userGroupRatio
}
modelRatio
:=
priceData
.
Model
Ratio
groupRatio
:=
priceData
.
GroupRatioInfo
.
GroupRatio
modelPrice
:=
priceData
.
ModelPrice
usePrice
:=
priceData
.
UsePrice
quotaInfo
:=
QuotaInfo
{
InputDetails
:
TokenDetails
{
TextTokens
:
textInputTokens
,
...
...
@@ -166,7 +175,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
ModelName
:
modelName
,
UsePrice
:
usePrice
,
ModelRatio
:
modelRatio
,
GroupRatio
:
actualG
roupRatio
,
GroupRatio
:
g
roupRatio
,
}
quota
:=
calculateAudioQuota
(
quotaInfo
)
...
...
@@ -198,7 +207,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
logContent
+=
", "
+
extraContent
}
other
:=
GenerateWssOtherInfo
(
ctx
,
relayInfo
,
usage
,
modelRatio
,
groupRatio
,
completionRatio
.
InexactFloat64
(),
audioRatio
.
InexactFloat64
(),
audioCompletionRatio
.
InexactFloat64
(),
modelPrice
,
userGroup
Ratio
)
completionRatio
.
InexactFloat64
(),
audioRatio
.
InexactFloat64
(),
audioCompletionRatio
.
InexactFloat64
(),
modelPrice
,
priceData
.
GroupRatioInfo
.
GroupSpecial
Ratio
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
usage
.
InputTokens
,
usage
.
OutputTokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
}
...
...
@@ -214,9 +223,8 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
tokenName
:=
ctx
.
GetString
(
"token_name"
)
completionRatio
:=
priceData
.
CompletionRatio
modelRatio
:=
priceData
.
ModelRatio
groupRatio
:=
priceData
.
GroupRatio
groupRatio
:=
priceData
.
GroupRatio
Info
.
GroupRatio
modelPrice
:=
priceData
.
ModelPrice
userGroupRatio
:=
priceData
.
UserGroupRatio
cacheRatio
:=
priceData
.
CacheRatio
cacheTokens
:=
usage
.
PromptTokensDetails
.
CachedTokens
...
...
@@ -265,7 +273,7 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
}
other
:=
GenerateClaudeOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
cacheTokens
,
cacheRatio
,
cacheCreationTokens
,
cacheCreationRatio
,
modelPrice
,
userGroup
Ratio
)
cacheTokens
,
cacheRatio
,
cacheCreationTokens
,
cacheCreationRatio
,
modelPrice
,
priceData
.
GroupRatioInfo
.
GroupSpecial
Ratio
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
promptTokens
,
completionTokens
,
modelName
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
}
...
...
@@ -286,16 +294,10 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
audioCompletionRatio
:=
decimal
.
NewFromFloat
(
operation_setting
.
GetAudioCompletionRatio
(
relayInfo
.
OriginModelName
))
modelRatio
:=
priceData
.
ModelRatio
groupRatio
:=
priceData
.
GroupRatio
groupRatio
:=
priceData
.
GroupRatio
Info
.
GroupRatio
modelPrice
:=
priceData
.
ModelPrice
usePrice
:=
priceData
.
UsePrice
actualGroupRatio
:=
groupRatio
userGroupRatio
,
ok
:=
setting
.
GetGroupGroupRatio
(
relayInfo
.
UserGroup
,
relayInfo
.
Group
)
if
ok
{
actualGroupRatio
=
userGroupRatio
}
quotaInfo
:=
QuotaInfo
{
InputDetails
:
TokenDetails
{
TextTokens
:
textInputTokens
,
...
...
@@ -308,7 +310,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
ModelName
:
relayInfo
.
OriginModelName
,
UsePrice
:
usePrice
,
ModelRatio
:
modelRatio
,
GroupRatio
:
actualG
roupRatio
,
GroupRatio
:
g
roupRatio
,
}
quota
:=
calculateAudioQuota
(
quotaInfo
)
...
...
@@ -348,7 +350,7 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
logContent
+=
", "
+
extraContent
}
other
:=
GenerateAudioOtherInfo
(
ctx
,
relayInfo
,
usage
,
modelRatio
,
groupRatio
,
completionRatio
.
InexactFloat64
(),
audioRatio
.
InexactFloat64
(),
audioCompletionRatio
.
InexactFloat64
(),
modelPrice
,
userGroup
Ratio
)
completionRatio
.
InexactFloat64
(),
audioRatio
.
InexactFloat64
(),
audioCompletionRatio
.
InexactFloat64
(),
modelPrice
,
priceData
.
GroupRatioInfo
.
GroupSpecial
Ratio
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
usage
.
PromptTokens
,
usage
.
CompletionTokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
}
...
...
setting/auto_group.go
0 → 100644
View file @
129d1b08
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
)
}
setting/user_usable_group.go
View file @
129d1b08
...
...
@@ -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
}
web/src/components/settings/OperationSetting.js
View file @
129d1b08
...
...
@@ -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
{
...
...
web/src/pages/Setting/Operation/GroupRatioSettings.js
View file @
129d1b08
...
...
@@ -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,59 @@ 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
)
=>
{
if
(
!
value
||
value
.
trim
()
===
''
)
{
return
true
;
// Allow empty values
}
// First check if it's valid JSON
try
{
const
parsed
=
JSON
.
parse
(
value
);
// Check if it's an array
if
(
!
Array
.
isArray
(
parsed
))
{
return
false
;
}
// Check if every element is a string
return
parsed
.
every
(
item
=>
typeof
item
===
'string'
);
}
catch
(
error
)
{
return
false
;
}
},
message
:
t
(
'必须是有效的 JSON 字符串数组,例如:["g1","g2"]'
),
},
]}
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
>
...
...
web/src/pages/Token/EditToken.js
View file @
129d1b08
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
/>
}
/
>
)}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment