25:04:03 19:20:32 v0.6.0.6
Browse files- VERSION +1 -1
- constant/setup.go +3 -0
- constant/user_setting.go +6 -5
- controller/channel-test.go +6 -4
- controller/misc.go +2 -0
- controller/option.go +12 -1
- controller/setup.go +173 -0
- controller/user.go +7 -5
- docs/channel/other_setting.md +2 -2
- middleware/distributor.go +1 -0
- model/channel.go +12 -0
- model/main.go +24 -1
- model/setup.go +16 -0
- model/user.go +9 -0
- relay/channel/dify/relay-dify.go +6 -0
- relay/common/relay_info.go +3 -0
- relay/helper/price.go +14 -4
- relay/relay-text.go +17 -0
- router/api-router.go +2 -0
- setting/operation_setting/cache_ratio.go +4 -0
- setting/operation_setting/model-ratio.go +44 -34
- web/pnpm-lock.yaml +0 -0
- web/src/App.js +9 -0
- web/src/components/LinuxDoIcon.js +16 -6
- web/src/components/PersonalSetting.js +1002 -971
- web/src/components/SystemSetting.js +734 -797
- web/src/context/Style/index.js +3 -0
- web/src/i18n/locales/en.json +1 -0
- web/src/pages/Channel/EditChannel.js +17 -0
- web/src/pages/Home/index.js +4 -0
- web/src/pages/Setting/Operation/SettingsGeneral.js +13 -37
- web/src/pages/Setup/index.js +252 -0
VERSION
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
v0.6.0.
|
|
|
|
| 1 |
+
v0.6.0.6
|
constant/setup.go
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package constant
|
| 2 |
+
|
| 3 |
+
var Setup = false
|
constant/user_setting.go
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
package constant
|
| 2 |
|
| 3 |
var (
|
| 4 |
-
UserSettingNotifyType = "notify_type"
|
| 5 |
-
UserSettingQuotaWarningThreshold = "quota_warning_threshold"
|
| 6 |
-
UserSettingWebhookUrl = "webhook_url"
|
| 7 |
-
UserSettingWebhookSecret = "webhook_secret"
|
| 8 |
-
UserSettingNotificationEmail = "notification_email"
|
|
|
|
| 9 |
)
|
| 10 |
|
| 11 |
var (
|
|
|
|
| 1 |
package constant
|
| 2 |
|
| 3 |
var (
|
| 4 |
+
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
|
| 5 |
+
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
|
| 6 |
+
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
|
| 7 |
+
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
|
| 8 |
+
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
|
| 9 |
+
UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
|
| 10 |
)
|
| 11 |
|
| 12 |
var (
|
controller/channel-test.go
CHANGED
|
@@ -105,6 +105,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|
| 105 |
request := buildTestRequest(testModel)
|
| 106 |
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
adaptor.Init(info)
|
| 109 |
|
| 110 |
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
|
|
@@ -143,10 +148,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
|
| 143 |
return err, nil
|
| 144 |
}
|
| 145 |
info.PromptTokens = usage.PromptTokens
|
| 146 |
-
|
| 147 |
-
if err != nil {
|
| 148 |
-
return err, nil
|
| 149 |
-
}
|
| 150 |
quota := 0
|
| 151 |
if !priceData.UsePrice {
|
| 152 |
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
|
|
|
|
| 105 |
request := buildTestRequest(testModel)
|
| 106 |
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
|
| 107 |
|
| 108 |
+
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
|
| 109 |
+
if err != nil {
|
| 110 |
+
return err, nil
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
adaptor.Init(info)
|
| 114 |
|
| 115 |
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
|
|
|
|
| 148 |
return err, nil
|
| 149 |
}
|
| 150 |
info.PromptTokens = usage.PromptTokens
|
| 151 |
+
|
|
|
|
|
|
|
|
|
|
| 152 |
quota := 0
|
| 153 |
if !priceData.UsePrice {
|
| 154 |
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
|
controller/misc.go
CHANGED
|
@@ -5,6 +5,7 @@ import (
|
|
| 5 |
"fmt"
|
| 6 |
"net/http"
|
| 7 |
"one-api/common"
|
|
|
|
| 8 |
"one-api/model"
|
| 9 |
"one-api/setting"
|
| 10 |
"one-api/setting/operation_setting"
|
|
@@ -72,6 +73,7 @@ func GetStatus(c *gin.Context) {
|
|
| 72 |
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
| 73 |
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
| 74 |
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
|
|
|
| 75 |
},
|
| 76 |
})
|
| 77 |
return
|
|
|
|
| 5 |
"fmt"
|
| 6 |
"net/http"
|
| 7 |
"one-api/common"
|
| 8 |
+
"one-api/constant"
|
| 9 |
"one-api/model"
|
| 10 |
"one-api/setting"
|
| 11 |
"one-api/setting/operation_setting"
|
|
|
|
| 73 |
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
| 74 |
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
| 75 |
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
| 76 |
+
"setup": constant.Setup,
|
| 77 |
},
|
| 78 |
})
|
| 79 |
return
|
controller/option.go
CHANGED
|
@@ -53,11 +53,12 @@ func UpdateOption(c *gin.Context) {
|
|
| 53 |
return
|
| 54 |
}
|
| 55 |
case "oidc.enabled":
|
| 56 |
-
if option.Value == "true" && system_setting.GetOIDCSettings().
|
| 57 |
c.JSON(http.StatusOK, gin.H{
|
| 58 |
"success": false,
|
| 59 |
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!",
|
| 60 |
})
|
|
|
|
| 61 |
}
|
| 62 |
case "LinuxDOOAuthEnabled":
|
| 63 |
if option.Value == "true" && common.LinuxDOClientId == "" {
|
|
@@ -89,6 +90,15 @@ func UpdateOption(c *gin.Context) {
|
|
| 89 |
"success": false,
|
| 90 |
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
| 91 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
return
|
| 93 |
}
|
| 94 |
case "GroupRatio":
|
|
@@ -100,6 +110,7 @@ func UpdateOption(c *gin.Context) {
|
|
| 100 |
})
|
| 101 |
return
|
| 102 |
}
|
|
|
|
| 103 |
}
|
| 104 |
err = model.UpdateOption(option.Key, option.Value)
|
| 105 |
if err != nil {
|
|
|
|
| 53 |
return
|
| 54 |
}
|
| 55 |
case "oidc.enabled":
|
| 56 |
+
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
|
| 57 |
c.JSON(http.StatusOK, gin.H{
|
| 58 |
"success": false,
|
| 59 |
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!",
|
| 60 |
})
|
| 61 |
+
return
|
| 62 |
}
|
| 63 |
case "LinuxDOOAuthEnabled":
|
| 64 |
if option.Value == "true" && common.LinuxDOClientId == "" {
|
|
|
|
| 90 |
"success": false,
|
| 91 |
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
| 92 |
})
|
| 93 |
+
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
case "TelegramOAuthEnabled":
|
| 97 |
+
if option.Value == "true" && common.TelegramBotToken == "" {
|
| 98 |
+
c.JSON(http.StatusOK, gin.H{
|
| 99 |
+
"success": false,
|
| 100 |
+
"message": "无法启用 Telegram OAuth,请先填入 Telegram Bot Token!",
|
| 101 |
+
})
|
| 102 |
return
|
| 103 |
}
|
| 104 |
case "GroupRatio":
|
|
|
|
| 110 |
})
|
| 111 |
return
|
| 112 |
}
|
| 113 |
+
|
| 114 |
}
|
| 115 |
err = model.UpdateOption(option.Key, option.Value)
|
| 116 |
if err != nil {
|
controller/setup.go
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package controller
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"github.com/gin-gonic/gin"
|
| 5 |
+
"one-api/common"
|
| 6 |
+
"one-api/constant"
|
| 7 |
+
"one-api/model"
|
| 8 |
+
"one-api/setting/operation_setting"
|
| 9 |
+
"time"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
type Setup struct {
|
| 13 |
+
Status bool `json:"status"`
|
| 14 |
+
RootInit bool `json:"root_init"`
|
| 15 |
+
DatabaseType string `json:"database_type"`
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
type SetupRequest struct {
|
| 19 |
+
Username string `json:"username"`
|
| 20 |
+
Password string `json:"password"`
|
| 21 |
+
ConfirmPassword string `json:"confirmPassword"`
|
| 22 |
+
SelfUseModeEnabled bool `json:"SelfUseModeEnabled"`
|
| 23 |
+
DemoSiteEnabled bool `json:"DemoSiteEnabled"`
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
func GetSetup(c *gin.Context) {
|
| 27 |
+
setup := Setup{
|
| 28 |
+
Status: constant.Setup,
|
| 29 |
+
}
|
| 30 |
+
if constant.Setup {
|
| 31 |
+
c.JSON(200, gin.H{
|
| 32 |
+
"success": true,
|
| 33 |
+
"data": setup,
|
| 34 |
+
})
|
| 35 |
+
return
|
| 36 |
+
}
|
| 37 |
+
setup.RootInit = model.RootUserExists()
|
| 38 |
+
if common.UsingMySQL {
|
| 39 |
+
setup.DatabaseType = "mysql"
|
| 40 |
+
}
|
| 41 |
+
if common.UsingPostgreSQL {
|
| 42 |
+
setup.DatabaseType = "postgres"
|
| 43 |
+
}
|
| 44 |
+
if common.UsingSQLite {
|
| 45 |
+
setup.DatabaseType = "sqlite"
|
| 46 |
+
}
|
| 47 |
+
c.JSON(200, gin.H{
|
| 48 |
+
"success": true,
|
| 49 |
+
"data": setup,
|
| 50 |
+
})
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
func PostSetup(c *gin.Context) {
|
| 54 |
+
// Check if setup is already completed
|
| 55 |
+
if constant.Setup {
|
| 56 |
+
c.JSON(400, gin.H{
|
| 57 |
+
"success": false,
|
| 58 |
+
"message": "系统已经初始化完成",
|
| 59 |
+
})
|
| 60 |
+
return
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Check if root user already exists
|
| 64 |
+
rootExists := model.RootUserExists()
|
| 65 |
+
|
| 66 |
+
var req SetupRequest
|
| 67 |
+
err := c.ShouldBindJSON(&req)
|
| 68 |
+
if err != nil {
|
| 69 |
+
c.JSON(400, gin.H{
|
| 70 |
+
"success": false,
|
| 71 |
+
"message": "请求参数有误",
|
| 72 |
+
})
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// If root doesn't exist, validate and create admin account
|
| 77 |
+
if !rootExists {
|
| 78 |
+
// Validate password
|
| 79 |
+
if req.Password != req.ConfirmPassword {
|
| 80 |
+
c.JSON(400, gin.H{
|
| 81 |
+
"success": false,
|
| 82 |
+
"message": "两次输入的密码不一致",
|
| 83 |
+
})
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
if len(req.Password) < 8 {
|
| 88 |
+
c.JSON(400, gin.H{
|
| 89 |
+
"success": false,
|
| 90 |
+
"message": "密码长度至少为8个字符",
|
| 91 |
+
})
|
| 92 |
+
return
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// Create root user
|
| 96 |
+
hashedPassword, err := common.Password2Hash(req.Password)
|
| 97 |
+
if err != nil {
|
| 98 |
+
c.JSON(500, gin.H{
|
| 99 |
+
"success": false,
|
| 100 |
+
"message": "系统错误: " + err.Error(),
|
| 101 |
+
})
|
| 102 |
+
return
|
| 103 |
+
}
|
| 104 |
+
rootUser := model.User{
|
| 105 |
+
Username: req.Username,
|
| 106 |
+
Password: hashedPassword,
|
| 107 |
+
Role: common.RoleRootUser,
|
| 108 |
+
Status: common.UserStatusEnabled,
|
| 109 |
+
DisplayName: "Root User",
|
| 110 |
+
AccessToken: nil,
|
| 111 |
+
Quota: 100000000,
|
| 112 |
+
}
|
| 113 |
+
err = model.DB.Create(&rootUser).Error
|
| 114 |
+
if err != nil {
|
| 115 |
+
c.JSON(500, gin.H{
|
| 116 |
+
"success": false,
|
| 117 |
+
"message": "创建管理员账号失败: " + err.Error(),
|
| 118 |
+
})
|
| 119 |
+
return
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// Set operation modes
|
| 124 |
+
operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled
|
| 125 |
+
operation_setting.DemoSiteEnabled = req.DemoSiteEnabled
|
| 126 |
+
|
| 127 |
+
// Save operation modes to database for persistence
|
| 128 |
+
err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.SelfUseModeEnabled))
|
| 129 |
+
if err != nil {
|
| 130 |
+
c.JSON(500, gin.H{
|
| 131 |
+
"success": false,
|
| 132 |
+
"message": "保存自用模式设置失败: " + err.Error(),
|
| 133 |
+
})
|
| 134 |
+
return
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.DemoSiteEnabled))
|
| 138 |
+
if err != nil {
|
| 139 |
+
c.JSON(500, gin.H{
|
| 140 |
+
"success": false,
|
| 141 |
+
"message": "保存演示站点模式设置失败: " + err.Error(),
|
| 142 |
+
})
|
| 143 |
+
return
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Update setup status
|
| 147 |
+
constant.Setup = true
|
| 148 |
+
|
| 149 |
+
setup := model.Setup{
|
| 150 |
+
Version: common.Version,
|
| 151 |
+
InitializedAt: time.Now().Unix(),
|
| 152 |
+
}
|
| 153 |
+
err = model.DB.Create(&setup).Error
|
| 154 |
+
if err != nil {
|
| 155 |
+
c.JSON(500, gin.H{
|
| 156 |
+
"success": false,
|
| 157 |
+
"message": "系统初始化失败: " + err.Error(),
|
| 158 |
+
})
|
| 159 |
+
return
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
c.JSON(200, gin.H{
|
| 163 |
+
"success": true,
|
| 164 |
+
"message": "系统初始化成功",
|
| 165 |
+
})
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
func boolToString(b bool) string {
|
| 169 |
+
if b {
|
| 170 |
+
return "true"
|
| 171 |
+
}
|
| 172 |
+
return "false"
|
| 173 |
+
}
|
controller/user.go
CHANGED
|
@@ -913,11 +913,12 @@ func TopUp(c *gin.Context) {
|
|
| 913 |
}
|
| 914 |
|
| 915 |
type UpdateUserSettingRequest struct {
|
| 916 |
-
QuotaWarningType
|
| 917 |
-
QuotaWarningThreshold
|
| 918 |
-
WebhookUrl
|
| 919 |
-
WebhookSecret
|
| 920 |
-
NotificationEmail
|
|
|
|
| 921 |
}
|
| 922 |
|
| 923 |
func UpdateUserSetting(c *gin.Context) {
|
|
@@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) {
|
|
| 993 |
settings := map[string]interface{}{
|
| 994 |
constant.UserSettingNotifyType: req.QuotaWarningType,
|
| 995 |
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
|
|
|
|
| 996 |
}
|
| 997 |
|
| 998 |
// 如果是webhook类型,添加webhook相关设置
|
|
|
|
| 913 |
}
|
| 914 |
|
| 915 |
type UpdateUserSettingRequest struct {
|
| 916 |
+
QuotaWarningType string `json:"notify_type"`
|
| 917 |
+
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
|
| 918 |
+
WebhookUrl string `json:"webhook_url,omitempty"`
|
| 919 |
+
WebhookSecret string `json:"webhook_secret,omitempty"`
|
| 920 |
+
NotificationEmail string `json:"notification_email,omitempty"`
|
| 921 |
+
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
| 922 |
}
|
| 923 |
|
| 924 |
func UpdateUserSetting(c *gin.Context) {
|
|
|
|
| 994 |
settings := map[string]interface{}{
|
| 995 |
constant.UserSettingNotifyType: req.QuotaWarningType,
|
| 996 |
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
|
| 997 |
+
"accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel,
|
| 998 |
}
|
| 999 |
|
| 1000 |
// 如果是webhook类型,添加webhook相关设置
|
docs/channel/other_setting.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
| 11 |
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
|
| 12 |
|
| 13 |
3. thinking_to_content
|
| 14 |
-
- 用于标识是否将思考内容`
|
| 15 |
- 类型为布尔值,设置为 true 时启用思考内容转换
|
| 16 |
|
| 17 |
--------------------------------------------------------------
|
|
@@ -30,4 +30,4 @@
|
|
| 30 |
|
| 31 |
--------------------------------------------------------------
|
| 32 |
|
| 33 |
-
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
|
|
|
| 11 |
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
|
| 12 |
|
| 13 |
3. thinking_to_content
|
| 14 |
+
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回
|
| 15 |
- 类型为布尔值,设置为 true 时启用思考内容转换
|
| 16 |
|
| 17 |
--------------------------------------------------------------
|
|
|
|
| 30 |
|
| 31 |
--------------------------------------------------------------
|
| 32 |
|
| 33 |
+
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
middleware/distributor.go
CHANGED
|
@@ -212,6 +212,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
|
| 212 |
c.Set("channel_name", channel.Name)
|
| 213 |
c.Set("channel_type", channel.Type)
|
| 214 |
c.Set("channel_setting", channel.GetSetting())
|
|
|
|
| 215 |
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
| 216 |
c.Set("channel_organization", *channel.OpenAIOrganization)
|
| 217 |
}
|
|
|
|
| 212 |
c.Set("channel_name", channel.Name)
|
| 213 |
c.Set("channel_type", channel.Type)
|
| 214 |
c.Set("channel_setting", channel.GetSetting())
|
| 215 |
+
c.Set("param_override", channel.GetParamOverride())
|
| 216 |
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
| 217 |
c.Set("channel_organization", *channel.OpenAIOrganization)
|
| 218 |
}
|
model/channel.go
CHANGED
|
@@ -36,6 +36,7 @@ type Channel struct {
|
|
| 36 |
OtherInfo string `json:"other_info"`
|
| 37 |
Tag *string `json:"tag" gorm:"index"`
|
| 38 |
Setting *string `json:"setting" gorm:"type:text"`
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
func (channel *Channel) GetModels() []string {
|
|
@@ -511,6 +512,17 @@ func (channel *Channel) SetSetting(setting map[string]interface{}) {
|
|
| 511 |
channel.Setting = common.GetPointer[string](string(settingBytes))
|
| 512 |
}
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
func GetChannelsByIds(ids []int) ([]*Channel, error) {
|
| 515 |
var channels []*Channel
|
| 516 |
err := DB.Where("id in (?)", ids).Find(&channels).Error
|
|
|
|
| 36 |
OtherInfo string `json:"other_info"`
|
| 37 |
Tag *string `json:"tag" gorm:"index"`
|
| 38 |
Setting *string `json:"setting" gorm:"type:text"`
|
| 39 |
+
ParamOverride *string `json:"param_override" gorm:"type:text"`
|
| 40 |
}
|
| 41 |
|
| 42 |
func (channel *Channel) GetModels() []string {
|
|
|
|
| 512 |
channel.Setting = common.GetPointer[string](string(settingBytes))
|
| 513 |
}
|
| 514 |
|
| 515 |
+
func (channel *Channel) GetParamOverride() map[string]interface{} {
|
| 516 |
+
paramOverride := make(map[string]interface{})
|
| 517 |
+
if channel.ParamOverride != nil && *channel.ParamOverride != "" {
|
| 518 |
+
err := json.Unmarshal([]byte(*channel.ParamOverride), ¶mOverride)
|
| 519 |
+
if err != nil {
|
| 520 |
+
common.SysError("failed to unmarshal param override: " + err.Error())
|
| 521 |
+
}
|
| 522 |
+
}
|
| 523 |
+
return paramOverride
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
func GetChannelsByIds(ids []int) ([]*Channel, error) {
|
| 527 |
var channels []*Channel
|
| 528 |
err := DB.Where("id in (?)", ids).Find(&channels).Error
|
model/main.go
CHANGED
|
@@ -3,6 +3,7 @@ package model
|
|
| 3 |
import (
|
| 4 |
"log"
|
| 5 |
"one-api/common"
|
|
|
|
| 6 |
"os"
|
| 7 |
"strings"
|
| 8 |
"sync"
|
|
@@ -55,6 +56,26 @@ func createRootAccountIfNeed() error {
|
|
| 55 |
return nil
|
| 56 |
}
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
func chooseDB(envName string) (*gorm.DB, error) {
|
| 59 |
defer func() {
|
| 60 |
initCol()
|
|
@@ -214,8 +235,10 @@ func migrateDB() error {
|
|
| 214 |
if err != nil {
|
| 215 |
return err
|
| 216 |
}
|
|
|
|
| 217 |
common.SysLog("database migrated")
|
| 218 |
-
|
|
|
|
| 219 |
return err
|
| 220 |
}
|
| 221 |
|
|
|
|
| 3 |
import (
|
| 4 |
"log"
|
| 5 |
"one-api/common"
|
| 6 |
+
"one-api/constant"
|
| 7 |
"os"
|
| 8 |
"strings"
|
| 9 |
"sync"
|
|
|
|
| 56 |
return nil
|
| 57 |
}
|
| 58 |
|
| 59 |
+
func checkSetup() {
|
| 60 |
+
if GetSetup() == nil {
|
| 61 |
+
if RootUserExists() {
|
| 62 |
+
common.SysLog("system is not initialized, but root user exists")
|
| 63 |
+
// Create setup record
|
| 64 |
+
setup := Setup{
|
| 65 |
+
Version: common.Version,
|
| 66 |
+
InitializedAt: time.Now().Unix(),
|
| 67 |
+
}
|
| 68 |
+
err := DB.Create(&setup).Error
|
| 69 |
+
if err != nil {
|
| 70 |
+
common.SysLog("failed to create setup record: " + err.Error())
|
| 71 |
+
}
|
| 72 |
+
constant.Setup = true
|
| 73 |
+
} else {
|
| 74 |
+
constant.Setup = false
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
func chooseDB(envName string) (*gorm.DB, error) {
|
| 80 |
defer func() {
|
| 81 |
initCol()
|
|
|
|
| 235 |
if err != nil {
|
| 236 |
return err
|
| 237 |
}
|
| 238 |
+
err = DB.AutoMigrate(&Setup{})
|
| 239 |
common.SysLog("database migrated")
|
| 240 |
+
checkSetup()
|
| 241 |
+
//err = createRootAccountIfNeed()
|
| 242 |
return err
|
| 243 |
}
|
| 244 |
|
model/setup.go
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package model
|
| 2 |
+
|
| 3 |
+
type Setup struct {
|
| 4 |
+
ID uint `json:"id" gorm:"primaryKey"`
|
| 5 |
+
Version string `json:"version" gorm:"type:varchar(50);not null"`
|
| 6 |
+
InitializedAt int64 `json:"initialized_at" gorm:"type:bigint;not null"`
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
func GetSetup() *Setup {
|
| 10 |
+
var setup Setup
|
| 11 |
+
err := DB.First(&setup).Error
|
| 12 |
+
if err != nil {
|
| 13 |
+
return nil
|
| 14 |
+
}
|
| 15 |
+
return &setup
|
| 16 |
+
}
|
model/user.go
CHANGED
|
@@ -808,3 +808,12 @@ func (user *User) FillUserByLinuxDOId() error {
|
|
| 808 |
err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error
|
| 809 |
return err
|
| 810 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error
|
| 809 |
return err
|
| 810 |
}
|
| 811 |
+
|
| 812 |
+
func RootUserExists() bool {
|
| 813 |
+
var user User
|
| 814 |
+
err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error
|
| 815 |
+
if err != nil {
|
| 816 |
+
return false
|
| 817 |
+
}
|
| 818 |
+
return true
|
| 819 |
+
}
|
relay/channel/dify/relay-dify.go
CHANGED
|
@@ -198,6 +198,12 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt
|
|
| 198 |
choice.Delta.SetReasoningContent(text + "\n")
|
| 199 |
}
|
| 200 |
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
choice.Delta.SetContentString(difyResponse.Answer)
|
| 202 |
}
|
| 203 |
response.Choices = append(response.Choices, choice)
|
|
|
|
| 198 |
choice.Delta.SetReasoningContent(text + "\n")
|
| 199 |
}
|
| 200 |
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
|
| 201 |
+
if difyResponse.Answer == "<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>\n" {
|
| 202 |
+
difyResponse.Answer = "<think>"
|
| 203 |
+
} else if difyResponse.Answer == "</details>" {
|
| 204 |
+
difyResponse.Answer = "</think>"
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
choice.Delta.SetContentString(difyResponse.Answer)
|
| 208 |
}
|
| 209 |
response.Choices = append(response.Choices, choice)
|
relay/common/relay_info.go
CHANGED
|
@@ -76,6 +76,7 @@ type RelayInfo struct {
|
|
| 76 |
AudioUsage bool
|
| 77 |
ReasoningEffort string
|
| 78 |
ChannelSetting map[string]interface{}
|
|
|
|
| 79 |
UserSetting map[string]interface{}
|
| 80 |
UserEmail string
|
| 81 |
UserQuota int
|
|
@@ -131,6 +132,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|
| 131 |
channelType := c.GetInt("channel_type")
|
| 132 |
channelId := c.GetInt("channel_id")
|
| 133 |
channelSetting := c.GetStringMap("channel_setting")
|
|
|
|
| 134 |
|
| 135 |
tokenId := c.GetInt("token_id")
|
| 136 |
tokenKey := c.GetString("token_key")
|
|
@@ -168,6 +170,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|
| 168 |
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
| 169 |
Organization: c.GetString("channel_organization"),
|
| 170 |
ChannelSetting: channelSetting,
|
|
|
|
| 171 |
RelayFormat: RelayFormatOpenAI,
|
| 172 |
ThinkingContentInfo: ThinkingContentInfo{
|
| 173 |
IsFirstThinkingContent: true,
|
|
|
|
| 76 |
AudioUsage bool
|
| 77 |
ReasoningEffort string
|
| 78 |
ChannelSetting map[string]interface{}
|
| 79 |
+
ParamOverride map[string]interface{}
|
| 80 |
UserSetting map[string]interface{}
|
| 81 |
UserEmail string
|
| 82 |
UserQuota int
|
|
|
|
| 132 |
channelType := c.GetInt("channel_type")
|
| 133 |
channelId := c.GetInt("channel_id")
|
| 134 |
channelSetting := c.GetStringMap("channel_setting")
|
| 135 |
+
paramOverride := c.GetStringMap("param_override")
|
| 136 |
|
| 137 |
tokenId := c.GetInt("token_id")
|
| 138 |
tokenKey := c.GetString("token_key")
|
|
|
|
| 170 |
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
| 171 |
Organization: c.GetString("channel_organization"),
|
| 172 |
ChannelSetting: channelSetting,
|
| 173 |
+
ParamOverride: paramOverride,
|
| 174 |
RelayFormat: RelayFormatOpenAI,
|
| 175 |
ThinkingContentInfo: ThinkingContentInfo{
|
| 176 |
IsFirstThinkingContent: true,
|
relay/helper/price.go
CHANGED
|
@@ -4,6 +4,7 @@ import (
|
|
| 4 |
"fmt"
|
| 5 |
"github.com/gin-gonic/gin"
|
| 6 |
"one-api/common"
|
|
|
|
| 7 |
relaycommon "one-api/relay/common"
|
| 8 |
"one-api/setting"
|
| 9 |
"one-api/setting/operation_setting"
|
|
@@ -40,10 +41,19 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
|
| 40 |
var success bool
|
| 41 |
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
|
| 42 |
if !success {
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
}
|
| 49 |
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
|
|
|
|
| 4 |
"fmt"
|
| 5 |
"github.com/gin-gonic/gin"
|
| 6 |
"one-api/common"
|
| 7 |
+
constant2 "one-api/constant"
|
| 8 |
relaycommon "one-api/relay/common"
|
| 9 |
"one-api/setting"
|
| 10 |
"one-api/setting/operation_setting"
|
|
|
|
| 41 |
var success bool
|
| 42 |
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
|
| 43 |
if !success {
|
| 44 |
+
acceptUnsetRatio := false
|
| 45 |
+
if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok {
|
| 46 |
+
b, ok := accept.(bool)
|
| 47 |
+
if ok {
|
| 48 |
+
acceptUnsetRatio = b
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
if !acceptUnsetRatio {
|
| 52 |
+
if info.UserId == 1 {
|
| 53 |
+
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
|
| 54 |
+
} else {
|
| 55 |
+
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
|
| 56 |
+
}
|
| 57 |
}
|
| 58 |
}
|
| 59 |
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
|
relay/relay-text.go
CHANGED
|
@@ -168,6 +168,23 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
|
| 168 |
if err != nil {
|
| 169 |
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
| 170 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
if common.DebugEnabled {
|
| 172 |
println("requestBody: ", string(jsonData))
|
| 173 |
}
|
|
|
|
| 168 |
if err != nil {
|
| 169 |
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
| 170 |
}
|
| 171 |
+
|
| 172 |
+
// apply param override
|
| 173 |
+
if len(relayInfo.ParamOverride) > 0 {
|
| 174 |
+
reqMap := make(map[string]interface{})
|
| 175 |
+
err = json.Unmarshal(jsonData, &reqMap)
|
| 176 |
+
if err != nil {
|
| 177 |
+
return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError)
|
| 178 |
+
}
|
| 179 |
+
for key, value := range relayInfo.ParamOverride {
|
| 180 |
+
reqMap[key] = value
|
| 181 |
+
}
|
| 182 |
+
jsonData, err = json.Marshal(reqMap)
|
| 183 |
+
if err != nil {
|
| 184 |
+
return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError)
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
if common.DebugEnabled {
|
| 189 |
println("requestBody: ", string(jsonData))
|
| 190 |
}
|
router/api-router.go
CHANGED
|
@@ -13,6 +13,8 @@ func SetApiRouter(router *gin.Engine) {
|
|
| 13 |
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
| 15 |
{
|
|
|
|
|
|
|
| 16 |
apiRouter.GET("/status", controller.GetStatus)
|
| 17 |
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
| 18 |
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
|
|
|
| 13 |
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
| 14 |
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
| 15 |
{
|
| 16 |
+
apiRouter.GET("/setup", controller.GetSetup)
|
| 17 |
+
apiRouter.POST("/setup", controller.PostSetup)
|
| 18 |
apiRouter.GET("/status", controller.GetStatus)
|
| 19 |
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
| 20 |
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
setting/operation_setting/cache_ratio.go
CHANGED
|
@@ -14,6 +14,8 @@ var defaultCacheRatio = map[string]float64{
|
|
| 14 |
"o1-preview": 0.5,
|
| 15 |
"o1-mini-2024-09-12": 0.5,
|
| 16 |
"o1-mini": 0.5,
|
|
|
|
|
|
|
| 17 |
"gpt-4o-2024-11-20": 0.5,
|
| 18 |
"gpt-4o-2024-08-06": 0.5,
|
| 19 |
"gpt-4o": 0.5,
|
|
@@ -21,6 +23,8 @@ var defaultCacheRatio = map[string]float64{
|
|
| 21 |
"gpt-4o-mini": 0.5,
|
| 22 |
"gpt-4o-realtime-preview": 0.5,
|
| 23 |
"gpt-4o-mini-realtime-preview": 0.5,
|
|
|
|
|
|
|
| 24 |
"deepseek-chat": 0.25,
|
| 25 |
"deepseek-reasoner": 0.25,
|
| 26 |
"deepseek-coder": 0.25,
|
|
|
|
| 14 |
"o1-preview": 0.5,
|
| 15 |
"o1-mini-2024-09-12": 0.5,
|
| 16 |
"o1-mini": 0.5,
|
| 17 |
+
"o3-mini": 0.5,
|
| 18 |
+
"o3-mini-2025-01-31": 0.5,
|
| 19 |
"gpt-4o-2024-11-20": 0.5,
|
| 20 |
"gpt-4o-2024-08-06": 0.5,
|
| 21 |
"gpt-4o": 0.5,
|
|
|
|
| 23 |
"gpt-4o-mini": 0.5,
|
| 24 |
"gpt-4o-realtime-preview": 0.5,
|
| 25 |
"gpt-4o-mini-realtime-preview": 0.5,
|
| 26 |
+
"gpt-4.5-preview": 0.5,
|
| 27 |
+
"gpt-4.5-preview-2025-02-27": 0.5,
|
| 28 |
"deepseek-chat": 0.25,
|
| 29 |
"deepseek-reasoner": 0.25,
|
| 30 |
"deepseek-coder": 0.25,
|
setting/operation_setting/model-ratio.go
CHANGED
|
@@ -375,6 +375,17 @@ func GetCompletionRatio(name string) float64 {
|
|
| 375 |
return ratio
|
| 376 |
}
|
| 377 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
lowercaseName := strings.ToLower(name)
|
| 379 |
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
| 380 |
name = "gpt-4-gizmo-*"
|
|
@@ -385,87 +396,86 @@ func GetCompletionRatio(name string) float64 {
|
|
| 385 |
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
| 386 |
if strings.HasPrefix(name, "gpt-4o") {
|
| 387 |
if name == "gpt-4o-2024-05-13" {
|
| 388 |
-
return 3
|
| 389 |
}
|
| 390 |
-
return 4
|
| 391 |
}
|
| 392 |
-
|
| 393 |
-
|
|
|
|
| 394 |
}
|
| 395 |
-
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "
|
| 396 |
-
return 3
|
| 397 |
}
|
| 398 |
-
|
|
|
|
| 399 |
}
|
| 400 |
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
|
| 401 |
-
return 4
|
| 402 |
}
|
| 403 |
if name == "chatgpt-4o-latest" {
|
| 404 |
-
return 3
|
| 405 |
}
|
| 406 |
if strings.Contains(name, "claude-instant-1") {
|
| 407 |
-
return 3
|
| 408 |
} else if strings.Contains(name, "claude-2") {
|
| 409 |
-
return 3
|
| 410 |
} else if strings.Contains(name, "claude-3") {
|
| 411 |
-
return 5
|
| 412 |
}
|
| 413 |
if strings.HasPrefix(name, "gpt-3.5") {
|
| 414 |
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
|
| 415 |
// https://openai.com/blog/new-embedding-models-and-api-updates
|
| 416 |
// Updated GPT-3.5 Turbo model and lower pricing
|
| 417 |
-
return 3
|
| 418 |
}
|
| 419 |
if strings.HasSuffix(name, "1106") {
|
| 420 |
-
return 2
|
| 421 |
}
|
| 422 |
-
return 4.0 / 3.0
|
| 423 |
}
|
| 424 |
if strings.HasPrefix(name, "mistral-") {
|
| 425 |
-
return 3
|
| 426 |
}
|
| 427 |
if strings.HasPrefix(name, "gemini-") {
|
| 428 |
-
return 4
|
| 429 |
}
|
| 430 |
if strings.HasPrefix(name, "command") {
|
| 431 |
switch name {
|
| 432 |
case "command-r":
|
| 433 |
-
return 3
|
| 434 |
case "command-r-plus":
|
| 435 |
-
return 5
|
| 436 |
case "command-r-08-2024":
|
| 437 |
-
return 4
|
| 438 |
case "command-r-plus-08-2024":
|
| 439 |
-
return 4
|
| 440 |
default:
|
| 441 |
-
return 4
|
| 442 |
}
|
| 443 |
}
|
| 444 |
// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
|
| 445 |
if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
|
| 446 |
-
return 4
|
| 447 |
}
|
| 448 |
if strings.HasPrefix(name, "ERNIE-Speed-") {
|
| 449 |
-
return 2
|
| 450 |
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
|
| 451 |
-
return 2
|
| 452 |
} else if strings.HasPrefix(name, "ERNIE-Character") {
|
| 453 |
-
return 2
|
| 454 |
} else if strings.HasPrefix(name, "ERNIE-Functions") {
|
| 455 |
-
return 2
|
| 456 |
}
|
| 457 |
switch name {
|
| 458 |
case "llama2-70b-4096":
|
| 459 |
-
return 0.8 / 0.64
|
| 460 |
case "llama3-8b-8192":
|
| 461 |
-
return 2
|
| 462 |
case "llama3-70b-8192":
|
| 463 |
-
return 0.79 / 0.59
|
| 464 |
-
}
|
| 465 |
-
if ratio, ok := CompletionRatio[name]; ok {
|
| 466 |
-
return ratio
|
| 467 |
}
|
| 468 |
-
return 1
|
| 469 |
}
|
| 470 |
|
| 471 |
func GetAudioRatio(name string) float64 {
|
|
|
|
| 375 |
return ratio
|
| 376 |
}
|
| 377 |
}
|
| 378 |
+
hardCodedRatio, contain := getHardcodedCompletionModelRatio(name)
|
| 379 |
+
if contain {
|
| 380 |
+
return hardCodedRatio
|
| 381 |
+
}
|
| 382 |
+
if ratio, ok := CompletionRatio[name]; ok {
|
| 383 |
+
return ratio
|
| 384 |
+
}
|
| 385 |
+
return hardCodedRatio
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
| 389 |
lowercaseName := strings.ToLower(name)
|
| 390 |
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
| 391 |
name = "gpt-4-gizmo-*"
|
|
|
|
| 396 |
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
| 397 |
if strings.HasPrefix(name, "gpt-4o") {
|
| 398 |
if name == "gpt-4o-2024-05-13" {
|
| 399 |
+
return 3, true
|
| 400 |
}
|
| 401 |
+
return 4, true
|
| 402 |
}
|
| 403 |
+
// gpt-4.5-preview匹配
|
| 404 |
+
if strings.HasPrefix(name, "gpt-4.5-preview") {
|
| 405 |
+
return 2, true
|
| 406 |
}
|
| 407 |
+
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "gpt-4-1106") || strings.HasSuffix(name, "gpt-4-1105") {
|
| 408 |
+
return 3, true
|
| 409 |
}
|
| 410 |
+
// 没有特殊标记的 gpt-4 模型默认倍率为 2
|
| 411 |
+
return 2, false
|
| 412 |
}
|
| 413 |
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
|
| 414 |
+
return 4, true
|
| 415 |
}
|
| 416 |
if name == "chatgpt-4o-latest" {
|
| 417 |
+
return 3, true
|
| 418 |
}
|
| 419 |
if strings.Contains(name, "claude-instant-1") {
|
| 420 |
+
return 3, true
|
| 421 |
} else if strings.Contains(name, "claude-2") {
|
| 422 |
+
return 3, true
|
| 423 |
} else if strings.Contains(name, "claude-3") {
|
| 424 |
+
return 5, true
|
| 425 |
}
|
| 426 |
if strings.HasPrefix(name, "gpt-3.5") {
|
| 427 |
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
|
| 428 |
// https://openai.com/blog/new-embedding-models-and-api-updates
|
| 429 |
// Updated GPT-3.5 Turbo model and lower pricing
|
| 430 |
+
return 3, true
|
| 431 |
}
|
| 432 |
if strings.HasSuffix(name, "1106") {
|
| 433 |
+
return 2, true
|
| 434 |
}
|
| 435 |
+
return 4.0 / 3.0, true
|
| 436 |
}
|
| 437 |
if strings.HasPrefix(name, "mistral-") {
|
| 438 |
+
return 3, true
|
| 439 |
}
|
| 440 |
if strings.HasPrefix(name, "gemini-") {
|
| 441 |
+
return 4, true
|
| 442 |
}
|
| 443 |
if strings.HasPrefix(name, "command") {
|
| 444 |
switch name {
|
| 445 |
case "command-r":
|
| 446 |
+
return 3, true
|
| 447 |
case "command-r-plus":
|
| 448 |
+
return 5, true
|
| 449 |
case "command-r-08-2024":
|
| 450 |
+
return 4, true
|
| 451 |
case "command-r-plus-08-2024":
|
| 452 |
+
return 4, true
|
| 453 |
default:
|
| 454 |
+
return 4, true
|
| 455 |
}
|
| 456 |
}
|
| 457 |
// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
|
| 458 |
if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
|
| 459 |
+
return 4, true
|
| 460 |
}
|
| 461 |
if strings.HasPrefix(name, "ERNIE-Speed-") {
|
| 462 |
+
return 2, true
|
| 463 |
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
|
| 464 |
+
return 2, true
|
| 465 |
} else if strings.HasPrefix(name, "ERNIE-Character") {
|
| 466 |
+
return 2, true
|
| 467 |
} else if strings.HasPrefix(name, "ERNIE-Functions") {
|
| 468 |
+
return 2, true
|
| 469 |
}
|
| 470 |
switch name {
|
| 471 |
case "llama2-70b-4096":
|
| 472 |
+
return 0.8 / 0.64, true
|
| 473 |
case "llama3-8b-8192":
|
| 474 |
+
return 2, true
|
| 475 |
case "llama3-70b-8192":
|
| 476 |
+
return 0.79 / 0.59, true
|
|
|
|
|
|
|
|
|
|
| 477 |
}
|
| 478 |
+
return 1, false
|
| 479 |
}
|
| 480 |
|
| 481 |
func GetAudioRatio(name string) float64 {
|
web/pnpm-lock.yaml
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web/src/App.js
CHANGED
|
@@ -25,6 +25,7 @@ import Task from "./pages/Task/index.js";
|
|
| 25 |
import Playground from './pages/Playground/Playground.js';
|
| 26 |
import OAuth2Callback from "./components/OAuth2Callback.js";
|
| 27 |
import PersonalSetting from './components/PersonalSetting.js';
|
|
|
|
| 28 |
|
| 29 |
const Home = lazy(() => import('./pages/Home'));
|
| 30 |
const Detail = lazy(() => import('./pages/Detail'));
|
|
@@ -44,6 +45,14 @@ function App() {
|
|
| 44 |
</Suspense>
|
| 45 |
}
|
| 46 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
<Route
|
| 48 |
path='/channel'
|
| 49 |
element={
|
|
|
|
| 25 |
import Playground from './pages/Playground/Playground.js';
|
| 26 |
import OAuth2Callback from "./components/OAuth2Callback.js";
|
| 27 |
import PersonalSetting from './components/PersonalSetting.js';
|
| 28 |
+
import Setup from './pages/Setup/index.js';
|
| 29 |
|
| 30 |
const Home = lazy(() => import('./pages/Home'));
|
| 31 |
const Detail = lazy(() => import('./pages/Detail'));
|
|
|
|
| 45 |
</Suspense>
|
| 46 |
}
|
| 47 |
/>
|
| 48 |
+
<Route
|
| 49 |
+
path='/setup'
|
| 50 |
+
element={
|
| 51 |
+
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
| 52 |
+
<Setup />
|
| 53 |
+
</Suspense>
|
| 54 |
+
}
|
| 55 |
+
/>
|
| 56 |
<Route
|
| 57 |
path='/channel'
|
| 58 |
element={
|
web/src/components/LinuxDoIcon.js
CHANGED
|
@@ -6,17 +6,27 @@ const LinuxDoIcon = (props) => {
|
|
| 6 |
return (
|
| 7 |
<svg
|
| 8 |
className='icon'
|
| 9 |
-
viewBox='0 0
|
| 10 |
version='1.1'
|
| 11 |
xmlns='http://www.w3.org/2000/svg'
|
| 12 |
width='1em'
|
| 13 |
height='1em'
|
| 14 |
{...props}
|
| 15 |
>
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
</svg>
|
| 21 |
);
|
| 22 |
}
|
|
@@ -24,4 +34,4 @@ const LinuxDoIcon = (props) => {
|
|
| 24 |
return <Icon svg={<CustomIcon />} />;
|
| 25 |
};
|
| 26 |
|
| 27 |
-
export default LinuxDoIcon;
|
|
|
|
| 6 |
return (
|
| 7 |
<svg
|
| 8 |
className='icon'
|
| 9 |
+
viewBox='0 0 16 16'
|
| 10 |
version='1.1'
|
| 11 |
xmlns='http://www.w3.org/2000/svg'
|
| 12 |
width='1em'
|
| 13 |
height='1em'
|
| 14 |
{...props}
|
| 15 |
>
|
| 16 |
+
<g id='linuxdo_icon' data-name='linuxdo_icon'>
|
| 17 |
+
<path
|
| 18 |
+
d='m7.44,0s.09,0,.13,0c.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0q.12,0,.25,0t.26.08c.15.03.29.06.44.08,1.97.38,3.78,1.47,4.95,3.11.04.06.09.12.13.18.67.96,1.15,2.11,1.3,3.28q0,.19.09.26c0,.15,0,.29,0,.44,0,.04,0,.09,0,.13,0,.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0,.08,0,.17,0,.25q0,.19-.08.26c-.03.15-.06.29-.08.44-.38,1.97-1.47,3.78-3.11,4.95-.06.04-.12.09-.18.13-.96.67-2.11,1.15-3.28,1.3q-.19,0-.26.09c-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25,0q-.19,0-.26-.08c-.15-.03-.29-.06-.44-.08-1.97-.38-3.78-1.47-4.95-3.11q-.07-.09-.13-.18c-.67-.96-1.15-2.11-1.3-3.28q0-.19-.09-.26c0-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25q0-.19.08-.26c.03-.15.06-.29.08-.44.38-1.97,1.47-3.78,3.11-4.95.06-.04.12-.09.18-.13C4.42.73,5.57.26,6.74.1,7,.07,7.15,0,7.44,0Z'
|
| 19 |
+
fill='#EFEFEF'
|
| 20 |
+
/>
|
| 21 |
+
<path
|
| 22 |
+
d='m1.27,11.33h13.45c-.94,1.89-2.51,3.21-4.51,3.88-1.99.59-3.96.37-5.8-.57-1.25-.7-2.67-1.9-3.14-3.3Z'
|
| 23 |
+
fill='#FEB005'
|
| 24 |
+
/>
|
| 25 |
+
<path
|
| 26 |
+
d='m12.54,1.99c.87.7,1.82,1.59,2.18,2.68H1.27c.87-1.74,2.33-3.13,4.2-3.78,2.44-.79,5-.47,7.07,1.1Z'
|
| 27 |
+
fill='#1D1D1F'
|
| 28 |
+
/>
|
| 29 |
+
</g>
|
| 30 |
</svg>
|
| 31 |
);
|
| 32 |
}
|
|
|
|
| 34 |
return <Icon svg={<CustomIcon />} />;
|
| 35 |
};
|
| 36 |
|
| 37 |
+
export default LinuxDoIcon;
|
web/src/components/PersonalSetting.js
CHANGED
|
@@ -1,824 +1,852 @@
|
|
| 1 |
-
import React, {useContext, useEffect, useState} from 'react';
|
| 2 |
-
import {useNavigate} from 'react-router-dom';
|
| 3 |
import {
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
} from '../helpers';
|
| 11 |
import Turnstile from 'react-turnstile';
|
| 12 |
-
import {UserContext} from '../context/User';
|
| 13 |
-
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
|
| 14 |
import {
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
| 33 |
} from '@douyinfe/semi-ui';
|
| 34 |
import {
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
} from '../helpers/render';
|
| 40 |
import TelegramLoginButton from 'react-telegram-login';
|
| 41 |
import { useTranslation } from 'react-i18next';
|
| 42 |
|
| 43 |
const PersonalSetting = () => {
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
});
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
});
|
| 100 |
-
loadModels().then();
|
| 101 |
-
getAffLink().then();
|
| 102 |
-
setTransferAmount(getQuotaPerUnit());
|
| 103 |
-
}, []);
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
}
|
|
|
|
| 117 |
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
warningType: settings.notify_type || 'email',
|
| 123 |
-
warningThreshold: settings.quota_warning_threshold || 500000,
|
| 124 |
-
webhookUrl: settings.webhook_url || '',
|
| 125 |
-
webhookSecret: settings.webhook_secret || '',
|
| 126 |
-
notificationEmail: settings.notification_email || ''
|
| 127 |
-
});
|
| 128 |
-
}
|
| 129 |
-
}, [userState?.user?.setting]);
|
| 130 |
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
}, [isModelsExpanded]);
|
| 135 |
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
};
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
};
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
setModels(data);
|
| 179 |
-
}
|
| 180 |
-
} else {
|
| 181 |
-
showError(message);
|
| 182 |
-
}
|
| 183 |
-
};
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
}
|
| 196 |
|
| 197 |
-
const
|
| 198 |
-
|
| 199 |
-
showError(t('请输入你的账户名以确认删除!'));
|
| 200 |
-
return;
|
| 201 |
-
}
|
| 202 |
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
}
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
}
|
| 287 |
-
setLoading(false);
|
| 288 |
-
};
|
| 289 |
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
|
| 298 |
-
);
|
| 299 |
-
const {success, message} = res.data;
|
| 300 |
-
if (success) {
|
| 301 |
-
showSuccess(t('邮箱账户绑定成功!'));
|
| 302 |
-
setShowEmailBindModal(false);
|
| 303 |
-
userState.user.email = inputs.email;
|
| 304 |
-
} else {
|
| 305 |
-
showError(message);
|
| 306 |
-
}
|
| 307 |
-
setLoading(false);
|
| 308 |
-
};
|
| 309 |
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
} else {
|
| 314 |
-
return 'null';
|
| 315 |
-
}
|
| 316 |
-
};
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
}
|
| 329 |
-
};
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
if (res.data.success) {
|
| 349 |
-
showSuccess(t('通知设置已更新'));
|
| 350 |
-
await getUserData();
|
| 351 |
-
} else {
|
| 352 |
-
showError(res.data.message);
|
| 353 |
-
}
|
| 354 |
-
} catch (error) {
|
| 355 |
-
showError(t('更新通知设置失败'));
|
| 356 |
-
}
|
| 357 |
-
};
|
| 358 |
|
| 359 |
-
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
>
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
onClick={() => setIsModelsExpanded(false)}
|
| 468 |
-
>
|
| 469 |
-
{t('收起')}
|
| 470 |
-
</Tag>
|
| 471 |
-
</Space>
|
| 472 |
-
</Collapsible>
|
| 473 |
-
{!isModelsExpanded && (
|
| 474 |
-
<Space wrap>
|
| 475 |
-
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
|
| 476 |
-
<Tag
|
| 477 |
-
key={model}
|
| 478 |
-
color='cyan'
|
| 479 |
-
onClick={() => {
|
| 480 |
-
copyText(model);
|
| 481 |
-
}}
|
| 482 |
-
>
|
| 483 |
-
{model}
|
| 484 |
-
</Tag>
|
| 485 |
-
))}
|
| 486 |
-
<Tag
|
| 487 |
-
color='blue'
|
| 488 |
-
type="light"
|
| 489 |
-
style={{ cursor: 'pointer' }}
|
| 490 |
-
onClick={() => setIsModelsExpanded(true)}
|
| 491 |
-
>
|
| 492 |
-
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
| 493 |
-
</Tag>
|
| 494 |
-
</Space>
|
| 495 |
-
)}
|
| 496 |
-
</>
|
| 497 |
-
)}
|
| 498 |
-
</div>
|
| 499 |
-
</>
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
|
| 534 |
{renderQuota(userState?.user?.aff_quota)}
|
| 535 |
</span>
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
</div>
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
onChange={val => handleNotificationSettingChange('webhookUrl', val)}
|
| 811 |
-
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
| 812 |
-
/>
|
| 813 |
-
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
|
| 814 |
-
{t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
|
| 815 |
-
</Typography.Text>
|
| 816 |
-
<Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
|
| 817 |
-
<div style={{cursor: 'pointer'}} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
|
| 818 |
-
{t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
|
| 819 |
-
</div>
|
| 820 |
-
<Collapsible isOpen={showWebhookDocs}>
|
| 821 |
-
<pre style={{marginTop: 4, background: 'var(--semi-color-fill-0)', padding: 8, borderRadius: 4}}>
|
| 822 |
{`{
|
| 823 |
"type": "quota_exceed", // 通知类型
|
| 824 |
"title": "标题", // 通知标题
|
|
@@ -835,202 +863,205 @@ const PersonalSetting = () => {
|
|
| 835 |
"values": ["$0.99"],
|
| 836 |
"timestamp": 1739950503
|
| 837 |
}`}
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
</div>
|
| 875 |
-
)}
|
| 876 |
-
<div style={{marginTop: 20}}>
|
| 877 |
-
<Typography.Text strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
|
| 878 |
-
<div style={{marginTop: 10}}>
|
| 879 |
-
<AutoComplete
|
| 880 |
-
value={notificationSettings.warningThreshold}
|
| 881 |
-
onChange={val => handleNotificationSettingChange('warningThreshold', val)}
|
| 882 |
-
style={{width: 200}}
|
| 883 |
-
placeholder={t('请输入预警额度')}
|
| 884 |
-
data={[
|
| 885 |
-
{ value: 100000, label: '0.2$' },
|
| 886 |
-
{ value: 500000, label: '1$' },
|
| 887 |
-
{ value: 1000000, label: '5$' },
|
| 888 |
-
{ value: 5000000, label: '10$' }
|
| 889 |
-
]}
|
| 890 |
-
/>
|
| 891 |
-
</div>
|
| 892 |
-
<Typography.Text type="secondary" style={{marginTop: 10, display: 'block'}}>
|
| 893 |
-
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
| 894 |
-
</Typography.Text>
|
| 895 |
-
</div>
|
| 896 |
-
<div style={{marginTop: 20}}>
|
| 897 |
-
<Button type="primary" onClick={saveNotificationSettings}>
|
| 898 |
-
{t('保存设置')}
|
| 899 |
-
</Button>
|
| 900 |
-
</div>
|
| 901 |
-
</Card>
|
| 902 |
-
<Modal
|
| 903 |
-
onCancel={() => setShowEmailBindModal(false)}
|
| 904 |
-
onOk={bindEmail}
|
| 905 |
-
visible={showEmailBindModal}
|
| 906 |
-
size={'small'}
|
| 907 |
-
centered={true}
|
| 908 |
-
maskClosable={false}
|
| 909 |
-
>
|
| 910 |
-
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
|
| 911 |
-
<div
|
| 912 |
-
style={{
|
| 913 |
-
marginTop: 20,
|
| 914 |
-
display: 'flex',
|
| 915 |
-
justifyContent: 'space-between',
|
| 916 |
-
}}
|
| 917 |
-
>
|
| 918 |
-
<Input
|
| 919 |
-
fluid
|
| 920 |
-
placeholder='输入邮箱地址'
|
| 921 |
-
onChange={(value) => handleInputChange('email', value)}
|
| 922 |
-
name='email'
|
| 923 |
-
type='email'
|
| 924 |
-
/>
|
| 925 |
-
<Button
|
| 926 |
-
onClick={sendVerificationCode}
|
| 927 |
-
disabled={disableButton || loading}
|
| 928 |
-
>
|
| 929 |
-
{disableButton ? `重新发送 (${countdown})` : '获取验证码'}
|
| 930 |
-
</Button>
|
| 931 |
-
</div>
|
| 932 |
-
<div style={{marginTop: 10}}>
|
| 933 |
-
<Input
|
| 934 |
-
fluid
|
| 935 |
-
placeholder='验证码'
|
| 936 |
-
name='email_verification_code'
|
| 937 |
-
value={inputs.email_verification_code}
|
| 938 |
-
onChange={(value) =>
|
| 939 |
-
handleInputChange('email_verification_code', value)
|
| 940 |
-
}
|
| 941 |
-
/>
|
| 942 |
-
</div>
|
| 943 |
-
{turnstileEnabled ? (
|
| 944 |
-
<Turnstile
|
| 945 |
-
sitekey={turnstileSiteKey}
|
| 946 |
-
onVerify={(token) => {
|
| 947 |
-
setTurnstileToken(token);
|
| 948 |
-
}}
|
| 949 |
-
/>
|
| 950 |
-
) : (
|
| 951 |
-
<></>
|
| 952 |
-
)}
|
| 953 |
-
</Modal>
|
| 954 |
-
<Modal
|
| 955 |
-
onCancel={() => setShowAccountDeleteModal(false)}
|
| 956 |
-
visible={showAccountDeleteModal}
|
| 957 |
-
size={'small'}
|
| 958 |
-
centered={true}
|
| 959 |
-
onOk={deleteAccount}
|
| 960 |
-
>
|
| 961 |
-
<div style={{marginTop: 20}}>
|
| 962 |
-
<Banner
|
| 963 |
-
type='danger'
|
| 964 |
-
description='您正在删除自己的帐户,将清空所有数据且不可恢复'
|
| 965 |
-
closeIcon={null}
|
| 966 |
-
/>
|
| 967 |
-
</div>
|
| 968 |
-
<div style={{marginTop: 20}}>
|
| 969 |
-
<Input
|
| 970 |
-
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
| 971 |
-
name='self_account_deletion_confirmation'
|
| 972 |
-
value={inputs.self_account_deletion_confirmation}
|
| 973 |
-
onChange={(value) =>
|
| 974 |
-
handleInputChange(
|
| 975 |
-
'self_account_deletion_confirmation',
|
| 976 |
-
value,
|
| 977 |
-
)
|
| 978 |
-
}
|
| 979 |
-
/>
|
| 980 |
-
{turnstileEnabled ? (
|
| 981 |
-
<Turnstile
|
| 982 |
-
sitekey={turnstileSiteKey}
|
| 983 |
-
onVerify={(token) => {
|
| 984 |
-
setTurnstileToken(token);
|
| 985 |
-
}}
|
| 986 |
-
/>
|
| 987 |
-
) : (
|
| 988 |
-
<></>
|
| 989 |
-
)}
|
| 990 |
-
</div>
|
| 991 |
-
</Modal>
|
| 992 |
-
<Modal
|
| 993 |
-
onCancel={() => setShowChangePasswordModal(false)}
|
| 994 |
-
visible={showChangePasswordModal}
|
| 995 |
-
size={'small'}
|
| 996 |
-
centered={true}
|
| 997 |
-
onOk={changePassword}
|
| 998 |
-
>
|
| 999 |
-
<div style={{marginTop: 20}}>
|
| 1000 |
-
<Input
|
| 1001 |
-
name='set_new_password'
|
| 1002 |
-
placeholder={t('新密码')}
|
| 1003 |
-
value={inputs.set_new_password}
|
| 1004 |
-
onChange={(value) =>
|
| 1005 |
-
handleInputChange('set_new_password', value)
|
| 1006 |
-
}
|
| 1007 |
-
/>
|
| 1008 |
-
<Input
|
| 1009 |
-
style={{marginTop: 20}}
|
| 1010 |
-
name='set_new_password_confirmation'
|
| 1011 |
-
placeholder={t('确认新密码')}
|
| 1012 |
-
value={inputs.set_new_password_confirmation}
|
| 1013 |
-
onChange={(value) =>
|
| 1014 |
-
handleInputChange('set_new_password_confirmation', value)
|
| 1015 |
-
}
|
| 1016 |
-
/>
|
| 1017 |
-
{turnstileEnabled ? (
|
| 1018 |
-
<Turnstile
|
| 1019 |
-
sitekey={turnstileSiteKey}
|
| 1020 |
-
onVerify={(token) => {
|
| 1021 |
-
setTurnstileToken(token);
|
| 1022 |
-
}}
|
| 1023 |
-
/>
|
| 1024 |
-
) : (
|
| 1025 |
-
<></>
|
| 1026 |
-
)}
|
| 1027 |
-
</div>
|
| 1028 |
-
</Modal>
|
| 1029 |
</div>
|
| 1030 |
-
|
| 1031 |
-
|
| 1032 |
-
|
| 1033 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
};
|
| 1035 |
|
| 1036 |
export default PersonalSetting;
|
|
|
|
| 1 |
+
import React, { useContext, useEffect, useState } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
import {
|
| 4 |
+
API,
|
| 5 |
+
copy,
|
| 6 |
+
isRoot,
|
| 7 |
+
showError,
|
| 8 |
+
showInfo,
|
| 9 |
+
showSuccess
|
| 10 |
} from '../helpers';
|
| 11 |
import Turnstile from 'react-turnstile';
|
| 12 |
+
import { UserContext } from '../context/User';
|
| 13 |
+
import { onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked } from './utils';
|
| 14 |
import {
|
| 15 |
+
Avatar,
|
| 16 |
+
Banner,
|
| 17 |
+
Button,
|
| 18 |
+
Card,
|
| 19 |
+
Descriptions,
|
| 20 |
+
Image,
|
| 21 |
+
Input,
|
| 22 |
+
InputNumber,
|
| 23 |
+
Layout,
|
| 24 |
+
Modal,
|
| 25 |
+
Space,
|
| 26 |
+
Tag,
|
| 27 |
+
Typography,
|
| 28 |
+
Collapsible,
|
| 29 |
+
Select,
|
| 30 |
+
Radio,
|
| 31 |
+
RadioGroup,
|
| 32 |
+
AutoComplete,
|
| 33 |
+
Checkbox,
|
| 34 |
+
Tabs,
|
| 35 |
+
TabPane
|
| 36 |
} from '@douyinfe/semi-ui';
|
| 37 |
import {
|
| 38 |
+
getQuotaPerUnit,
|
| 39 |
+
renderQuota,
|
| 40 |
+
renderQuotaWithPrompt,
|
| 41 |
+
stringToColor
|
| 42 |
} from '../helpers/render';
|
| 43 |
import TelegramLoginButton from 'react-telegram-login';
|
| 44 |
import { useTranslation } from 'react-i18next';
|
| 45 |
|
| 46 |
const PersonalSetting = () => {
|
| 47 |
+
const [userState, userDispatch] = useContext(UserContext);
|
| 48 |
+
let navigate = useNavigate();
|
| 49 |
+
const { t } = useTranslation();
|
| 50 |
|
| 51 |
+
const [inputs, setInputs] = useState({
|
| 52 |
+
wechat_verification_code: '',
|
| 53 |
+
email_verification_code: '',
|
| 54 |
+
email: '',
|
| 55 |
+
self_account_deletion_confirmation: '',
|
| 56 |
+
set_new_password: '',
|
| 57 |
+
set_new_password_confirmation: ''
|
| 58 |
+
});
|
| 59 |
+
const [status, setStatus] = useState({});
|
| 60 |
+
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
|
| 61 |
+
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
| 62 |
+
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
| 63 |
+
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
| 64 |
+
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
| 65 |
+
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
| 66 |
+
const [turnstileToken, setTurnstileToken] = useState('');
|
| 67 |
+
const [loading, setLoading] = useState(false);
|
| 68 |
+
const [disableButton, setDisableButton] = useState(false);
|
| 69 |
+
const [countdown, setCountdown] = useState(30);
|
| 70 |
+
const [affLink, setAffLink] = useState('');
|
| 71 |
+
const [systemToken, setSystemToken] = useState('');
|
| 72 |
+
const [models, setModels] = useState([]);
|
| 73 |
+
const [openTransfer, setOpenTransfer] = useState(false);
|
| 74 |
+
const [transferAmount, setTransferAmount] = useState(0);
|
| 75 |
+
const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
|
| 76 |
+
// Initialize from localStorage if available
|
| 77 |
+
const savedState = localStorage.getItem('modelsExpanded');
|
| 78 |
+
return savedState ? JSON.parse(savedState) : false;
|
| 79 |
+
});
|
| 80 |
+
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
|
| 81 |
+
const [notificationSettings, setNotificationSettings] = useState({
|
| 82 |
+
warningType: 'email',
|
| 83 |
+
warningThreshold: 100000,
|
| 84 |
+
webhookUrl: '',
|
| 85 |
+
webhookSecret: '',
|
| 86 |
+
notificationEmail: '',
|
| 87 |
+
acceptUnsetModelRatioModel: false
|
| 88 |
+
});
|
| 89 |
+
const [showWebhookDocs, setShowWebhookDocs] = useState(false);
|
| 90 |
+
|
| 91 |
+
useEffect(() => {
|
| 92 |
+
let status = localStorage.getItem('status');
|
| 93 |
+
if (status) {
|
| 94 |
+
status = JSON.parse(status);
|
| 95 |
+
setStatus(status);
|
| 96 |
+
if (status.turnstile_check) {
|
| 97 |
+
setTurnstileEnabled(true);
|
| 98 |
+
setTurnstileSiteKey(status.turnstile_site_key);
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
getUserData().then((res) => {
|
| 102 |
+
console.log(userState);
|
| 103 |
});
|
| 104 |
+
loadModels().then();
|
| 105 |
+
getAffLink().then();
|
| 106 |
+
setTransferAmount(getQuotaPerUnit());
|
| 107 |
+
}, []);
|
| 108 |
|
| 109 |
+
useEffect(() => {
|
| 110 |
+
let countdownInterval = null;
|
| 111 |
+
if (disableButton && countdown > 0) {
|
| 112 |
+
countdownInterval = setInterval(() => {
|
| 113 |
+
setCountdown(countdown - 1);
|
| 114 |
+
}, 1000);
|
| 115 |
+
} else if (countdown === 0) {
|
| 116 |
+
setDisableButton(false);
|
| 117 |
+
setCountdown(30);
|
| 118 |
+
}
|
| 119 |
+
return () => clearInterval(countdownInterval); // Clean up on unmount
|
| 120 |
+
}, [disableButton, countdown]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
useEffect(() => {
|
| 123 |
+
if (userState?.user?.setting) {
|
| 124 |
+
const settings = JSON.parse(userState.user.setting);
|
| 125 |
+
setNotificationSettings({
|
| 126 |
+
warningType: settings.notify_type || 'email',
|
| 127 |
+
warningThreshold: settings.quota_warning_threshold || 500000,
|
| 128 |
+
webhookUrl: settings.webhook_url || '',
|
| 129 |
+
webhookSecret: settings.webhook_secret || '',
|
| 130 |
+
notificationEmail: settings.notification_email || '',
|
| 131 |
+
acceptUnsetModelRatioModel: settings.accept_unset_model_ratio_model || false
|
| 132 |
+
});
|
| 133 |
+
}
|
| 134 |
+
}, [userState?.user?.setting]);
|
| 135 |
|
| 136 |
+
// Save models expanded state to localStorage whenever it changes
|
| 137 |
+
useEffect(() => {
|
| 138 |
+
localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
|
| 139 |
+
}, [isModelsExpanded]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
+
const handleInputChange = (name, value) => {
|
| 142 |
+
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
| 143 |
+
};
|
|
|
|
| 144 |
|
| 145 |
+
const generateAccessToken = async () => {
|
| 146 |
+
const res = await API.get('/api/user/token');
|
| 147 |
+
const { success, message, data } = res.data;
|
| 148 |
+
if (success) {
|
| 149 |
+
setSystemToken(data);
|
| 150 |
+
await copy(data);
|
| 151 |
+
showSuccess(t('令牌已重置并已复制到剪贴板'));
|
| 152 |
+
} else {
|
| 153 |
+
showError(message);
|
| 154 |
+
}
|
| 155 |
+
};
|
| 156 |
|
| 157 |
+
const getAffLink = async () => {
|
| 158 |
+
const res = await API.get('/api/user/aff');
|
| 159 |
+
const { success, message, data } = res.data;
|
| 160 |
+
if (success) {
|
| 161 |
+
let link = `${window.location.origin}/register?aff=${data}`;
|
| 162 |
+
setAffLink(link);
|
| 163 |
+
} else {
|
| 164 |
+
showError(message);
|
| 165 |
+
}
|
| 166 |
+
};
|
|
|
|
| 167 |
|
| 168 |
+
const getUserData = async () => {
|
| 169 |
+
let res = await API.get(`/api/user/self`);
|
| 170 |
+
const { success, message, data } = res.data;
|
| 171 |
+
if (success) {
|
| 172 |
+
userDispatch({ type: 'login', payload: data });
|
| 173 |
+
} else {
|
| 174 |
+
showError(message);
|
| 175 |
+
}
|
| 176 |
+
};
|
|
|
|
| 177 |
|
| 178 |
+
const loadModels = async () => {
|
| 179 |
+
let res = await API.get(`/api/user/models`);
|
| 180 |
+
const { success, message, data } = res.data;
|
| 181 |
+
if (success) {
|
| 182 |
+
if (data != null) {
|
| 183 |
+
setModels(data);
|
| 184 |
+
}
|
| 185 |
+
} else {
|
| 186 |
+
showError(message);
|
| 187 |
+
}
|
| 188 |
+
};
|
| 189 |
|
| 190 |
+
const handleAffLinkClick = async (e) => {
|
| 191 |
+
e.target.select();
|
| 192 |
+
await copy(e.target.value);
|
| 193 |
+
showSuccess(t('邀请链接已复制到剪切板'));
|
| 194 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
+
const handleSystemTokenClick = async (e) => {
|
| 197 |
+
e.target.select();
|
| 198 |
+
await copy(e.target.value);
|
| 199 |
+
showSuccess(t('系统令牌已复制到剪切板'));
|
| 200 |
+
};
|
| 201 |
|
| 202 |
+
const deleteAccount = async () => {
|
| 203 |
+
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
| 204 |
+
showError(t('请输入你的账户名以确认删除!'));
|
| 205 |
+
return;
|
| 206 |
+
}
|
| 207 |
|
| 208 |
+
const res = await API.delete('/api/user/self');
|
| 209 |
+
const { success, message } = res.data;
|
|
|
|
|
|
|
|
|
|
| 210 |
|
| 211 |
+
if (success) {
|
| 212 |
+
showSuccess(t('账户已删除!'));
|
| 213 |
+
await API.get('/api/user/logout');
|
| 214 |
+
userDispatch({ type: 'logout' });
|
| 215 |
+
localStorage.removeItem('user');
|
| 216 |
+
navigate('/login');
|
| 217 |
+
} else {
|
| 218 |
+
showError(message);
|
| 219 |
+
}
|
| 220 |
+
};
|
| 221 |
|
| 222 |
+
const bindWeChat = async () => {
|
| 223 |
+
if (inputs.wechat_verification_code === '') return;
|
| 224 |
+
const res = await API.get(
|
| 225 |
+
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
| 226 |
+
);
|
| 227 |
+
const { success, message } = res.data;
|
| 228 |
+
if (success) {
|
| 229 |
+
showSuccess(t('微信账户绑定成功!'));
|
| 230 |
+
setShowWeChatBindModal(false);
|
| 231 |
+
} else {
|
| 232 |
+
showError(message);
|
| 233 |
+
}
|
| 234 |
+
};
|
| 235 |
|
| 236 |
+
const changePassword = async () => {
|
| 237 |
+
if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
|
| 238 |
+
showError(t('两次输入的密码不一致!'));
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
const res = await API.put(`/api/user/self`, {
|
| 242 |
+
password: inputs.set_new_password
|
| 243 |
+
});
|
| 244 |
+
const { success, message } = res.data;
|
| 245 |
+
if (success) {
|
| 246 |
+
showSuccess(t('密码修改成功!'));
|
| 247 |
+
setShowWeChatBindModal(false);
|
| 248 |
+
} else {
|
| 249 |
+
showError(message);
|
| 250 |
+
}
|
| 251 |
+
setShowChangePasswordModal(false);
|
| 252 |
+
};
|
| 253 |
|
| 254 |
+
const transfer = async () => {
|
| 255 |
+
if (transferAmount < getQuotaPerUnit()) {
|
| 256 |
+
showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
|
| 257 |
+
return;
|
| 258 |
+
}
|
| 259 |
+
const res = await API.post(`/api/user/aff_transfer`, {
|
| 260 |
+
quota: transferAmount
|
| 261 |
+
});
|
| 262 |
+
const { success, message } = res.data;
|
| 263 |
+
if (success) {
|
| 264 |
+
showSuccess(message);
|
| 265 |
+
setOpenTransfer(false);
|
| 266 |
+
getUserData().then();
|
| 267 |
+
} else {
|
| 268 |
+
showError(message);
|
| 269 |
+
}
|
| 270 |
+
};
|
| 271 |
|
| 272 |
+
const sendVerificationCode = async () => {
|
| 273 |
+
if (inputs.email === '') {
|
| 274 |
+
showError(t('请输入邮箱!'));
|
| 275 |
+
return;
|
| 276 |
+
}
|
| 277 |
+
setDisableButton(true);
|
| 278 |
+
if (turnstileEnabled && turnstileToken === '') {
|
| 279 |
+
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
| 280 |
+
return;
|
| 281 |
+
}
|
| 282 |
+
setLoading(true);
|
| 283 |
+
const res = await API.get(
|
| 284 |
+
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
| 285 |
+
);
|
| 286 |
+
const { success, message } = res.data;
|
| 287 |
+
if (success) {
|
| 288 |
+
showSuccess(t('验证码发送成功,请检查邮箱!'));
|
| 289 |
+
} else {
|
| 290 |
+
showError(message);
|
| 291 |
+
}
|
| 292 |
+
setLoading(false);
|
| 293 |
+
};
|
| 294 |
|
| 295 |
+
const bindEmail = async () => {
|
| 296 |
+
if (inputs.email_verification_code === '') {
|
| 297 |
+
showError(t('请输入邮箱验证码!'));
|
| 298 |
+
return;
|
| 299 |
+
}
|
| 300 |
+
setLoading(true);
|
| 301 |
+
const res = await API.get(
|
| 302 |
+
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
|
| 303 |
+
);
|
| 304 |
+
const { success, message } = res.data;
|
| 305 |
+
if (success) {
|
| 306 |
+
showSuccess(t('邮箱账户绑定成功!'));
|
| 307 |
+
setShowEmailBindModal(false);
|
| 308 |
+
userState.user.email = inputs.email;
|
| 309 |
+
} else {
|
| 310 |
+
showError(message);
|
| 311 |
+
}
|
| 312 |
+
setLoading(false);
|
| 313 |
+
};
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
+
const getUsername = () => {
|
| 316 |
+
if (userState.user) {
|
| 317 |
+
return userState.user.username;
|
| 318 |
+
} else {
|
| 319 |
+
return 'null';
|
| 320 |
+
}
|
| 321 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
+
const handleCancel = () => {
|
| 324 |
+
setOpenTransfer(false);
|
| 325 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
+
const copyText = async (text) => {
|
| 328 |
+
if (await copy(text)) {
|
| 329 |
+
showSuccess(t('已复制:') + text);
|
| 330 |
+
} else {
|
| 331 |
+
// setSearchKeyword(text);
|
| 332 |
+
Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
|
| 333 |
+
}
|
| 334 |
+
};
|
| 335 |
|
| 336 |
+
const handleNotificationSettingChange = (type, value) => {
|
| 337 |
+
setNotificationSettings(prev => ({
|
| 338 |
+
...prev,
|
| 339 |
+
[type]: value.target ? value.target.value : value // 处理 Radio 事件对象
|
| 340 |
+
}));
|
| 341 |
+
};
|
|
|
|
|
|
|
| 342 |
|
| 343 |
+
const saveNotificationSettings = async () => {
|
| 344 |
+
try {
|
| 345 |
+
const res = await API.put('/api/user/setting', {
|
| 346 |
+
notify_type: notificationSettings.warningType,
|
| 347 |
+
quota_warning_threshold: parseFloat(notificationSettings.warningThreshold),
|
| 348 |
+
webhook_url: notificationSettings.webhookUrl,
|
| 349 |
+
webhook_secret: notificationSettings.webhookSecret,
|
| 350 |
+
notification_email: notificationSettings.notificationEmail,
|
| 351 |
+
accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel
|
| 352 |
+
});
|
| 353 |
|
| 354 |
+
if (res.data.success) {
|
| 355 |
+
showSuccess(t('通知设置已更新'));
|
| 356 |
+
await getUserData();
|
| 357 |
+
} else {
|
| 358 |
+
showError(res.data.message);
|
| 359 |
+
}
|
| 360 |
+
} catch (error) {
|
| 361 |
+
showError(t('更新通知设置失败'));
|
| 362 |
+
}
|
| 363 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
+
return (
|
| 366 |
|
| 367 |
+
<div>
|
| 368 |
+
<Layout>
|
| 369 |
+
<Layout.Content>
|
| 370 |
+
<Modal
|
| 371 |
+
title={t('请输入要划转的数量')}
|
| 372 |
+
visible={openTransfer}
|
| 373 |
+
onOk={transfer}
|
| 374 |
+
onCancel={handleCancel}
|
| 375 |
+
maskClosable={false}
|
| 376 |
+
size={'small'}
|
| 377 |
+
centered={true}
|
| 378 |
+
>
|
| 379 |
+
<div style={{ marginTop: 20 }}>
|
| 380 |
+
<Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
|
| 381 |
+
<Input
|
| 382 |
+
style={{ marginTop: 5 }}
|
| 383 |
+
value={userState?.user?.aff_quota}
|
| 384 |
+
disabled={true}
|
| 385 |
+
></Input>
|
| 386 |
+
</div>
|
| 387 |
+
<div style={{ marginTop: 20 }}>
|
| 388 |
+
<Typography.Text>
|
| 389 |
+
{t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
|
| 390 |
+
</Typography.Text>
|
| 391 |
+
<div>
|
| 392 |
+
<InputNumber
|
| 393 |
+
min={0}
|
| 394 |
+
style={{ marginTop: 5 }}
|
| 395 |
+
value={transferAmount}
|
| 396 |
+
onChange={(value) => setTransferAmount(value)}
|
| 397 |
+
disabled={false}
|
| 398 |
+
></InputNumber>
|
| 399 |
+
</div>
|
| 400 |
+
</div>
|
| 401 |
+
</Modal>
|
| 402 |
+
<div>
|
| 403 |
+
<Card
|
| 404 |
+
title={
|
| 405 |
+
<Card.Meta
|
| 406 |
+
avatar={
|
| 407 |
+
<Avatar
|
| 408 |
+
size="default"
|
| 409 |
+
color={stringToColor(getUsername())}
|
| 410 |
+
style={{ marginRight: 4 }}
|
| 411 |
>
|
| 412 |
+
{typeof getUsername() === 'string' &&
|
| 413 |
+
getUsername().slice(0, 1)}
|
| 414 |
+
</Avatar>
|
| 415 |
+
}
|
| 416 |
+
title={<Typography.Text>{getUsername()}</Typography.Text>}
|
| 417 |
+
description={
|
| 418 |
+
isRoot() ? (
|
| 419 |
+
<Tag color="red">{t('管理员')}</Tag>
|
| 420 |
+
) : (
|
| 421 |
+
<Tag color="blue">{t('普通用户')}</Tag>
|
| 422 |
+
)
|
| 423 |
+
}
|
| 424 |
+
></Card.Meta>
|
| 425 |
+
}
|
| 426 |
+
headerExtraContent={
|
| 427 |
+
<>
|
| 428 |
+
<Space vertical align="start">
|
| 429 |
+
<Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
|
| 430 |
+
<Tag color="blue">{userState?.user?.group}</Tag>
|
| 431 |
+
</Space>
|
| 432 |
+
</>
|
| 433 |
+
}
|
| 434 |
+
footer={
|
| 435 |
+
<>
|
| 436 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
| 437 |
+
<Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
|
| 438 |
+
</div>
|
| 439 |
+
<div style={{ marginTop: 10 }}>
|
| 440 |
+
{models.length <= MODELS_DISPLAY_COUNT ? (
|
| 441 |
+
<Space wrap>
|
| 442 |
+
{models.map((model) => (
|
| 443 |
+
<Tag
|
| 444 |
+
key={model}
|
| 445 |
+
color="cyan"
|
| 446 |
+
onClick={() => {
|
| 447 |
+
copyText(model);
|
| 448 |
+
}}
|
| 449 |
+
>
|
| 450 |
+
{model}
|
| 451 |
+
</Tag>
|
| 452 |
+
))}
|
| 453 |
+
</Space>
|
| 454 |
+
) : (
|
| 455 |
+
<>
|
| 456 |
+
<Collapsible isOpen={isModelsExpanded}>
|
| 457 |
+
<Space wrap>
|
| 458 |
+
{models.map((model) => (
|
| 459 |
+
<Tag
|
| 460 |
+
key={model}
|
| 461 |
+
color="cyan"
|
| 462 |
+
onClick={() => {
|
| 463 |
+
copyText(model);
|
| 464 |
+
}}
|
| 465 |
+
>
|
| 466 |
+
{model}
|
| 467 |
+
</Tag>
|
| 468 |
+
))}
|
| 469 |
+
<Tag
|
| 470 |
+
color="blue"
|
| 471 |
+
type="light"
|
| 472 |
+
style={{ cursor: 'pointer' }}
|
| 473 |
+
onClick={() => setIsModelsExpanded(false)}
|
| 474 |
+
>
|
| 475 |
+
{t('收起')}
|
| 476 |
+
</Tag>
|
| 477 |
+
</Space>
|
| 478 |
+
</Collapsible>
|
| 479 |
+
{!isModelsExpanded && (
|
| 480 |
+
<Space wrap>
|
| 481 |
+
{models.slice(0, MODELS_DISPLAY_COUNT).map((model) => (
|
| 482 |
+
<Tag
|
| 483 |
+
key={model}
|
| 484 |
+
color="cyan"
|
| 485 |
+
onClick={() => {
|
| 486 |
+
copyText(model);
|
| 487 |
+
}}
|
| 488 |
+
>
|
| 489 |
+
{model}
|
| 490 |
+
</Tag>
|
| 491 |
+
))}
|
| 492 |
+
<Tag
|
| 493 |
+
color="blue"
|
| 494 |
+
type="light"
|
| 495 |
+
style={{ cursor: 'pointer' }}
|
| 496 |
+
onClick={() => setIsModelsExpanded(true)}
|
| 497 |
+
>
|
| 498 |
+
{t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')}
|
| 499 |
+
</Tag>
|
| 500 |
+
</Space>
|
| 501 |
+
)}
|
| 502 |
+
</>
|
| 503 |
+
)}
|
| 504 |
+
</div>
|
| 505 |
+
</>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
|
| 507 |
+
}
|
| 508 |
+
>
|
| 509 |
+
<Descriptions row>
|
| 510 |
+
<Descriptions.Item itemKey={t('当前余额')}>
|
| 511 |
+
{renderQuota(userState?.user?.quota)}
|
| 512 |
+
</Descriptions.Item>
|
| 513 |
+
<Descriptions.Item itemKey={t('历史消耗')}>
|
| 514 |
+
{renderQuota(userState?.user?.used_quota)}
|
| 515 |
+
</Descriptions.Item>
|
| 516 |
+
<Descriptions.Item itemKey={t('请求次数')}>
|
| 517 |
+
{userState.user?.request_count}
|
| 518 |
+
</Descriptions.Item>
|
| 519 |
+
</Descriptions>
|
| 520 |
+
</Card>
|
| 521 |
+
<Card
|
| 522 |
+
style={{ marginTop: 10 }}
|
| 523 |
+
footer={
|
| 524 |
+
<div>
|
| 525 |
+
<Typography.Text>{t('邀请链接')}</Typography.Text>
|
| 526 |
+
<Input
|
| 527 |
+
style={{ marginTop: 10 }}
|
| 528 |
+
value={affLink}
|
| 529 |
+
onClick={handleAffLinkClick}
|
| 530 |
+
readOnly
|
| 531 |
+
/>
|
| 532 |
+
</div>
|
| 533 |
+
}
|
| 534 |
+
>
|
| 535 |
+
<Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
|
| 536 |
+
<div style={{ marginTop: 10 }}>
|
| 537 |
+
<Descriptions row>
|
| 538 |
+
<Descriptions.Item itemKey={t('待使用收益')}>
|
| 539 |
+
<span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
|
| 540 |
{renderQuota(userState?.user?.aff_quota)}
|
| 541 |
</span>
|
| 542 |
+
<Button
|
| 543 |
+
type={'secondary'}
|
| 544 |
+
onClick={() => setOpenTransfer(true)}
|
| 545 |
+
size={'small'}
|
| 546 |
+
style={{ marginLeft: 10 }}
|
| 547 |
+
>
|
| 548 |
+
{t('划转')}
|
| 549 |
+
</Button>
|
| 550 |
+
</Descriptions.Item>
|
| 551 |
+
<Descriptions.Item itemKey={t('总收益')}>
|
| 552 |
+
{renderQuota(userState?.user?.aff_history_quota)}
|
| 553 |
+
</Descriptions.Item>
|
| 554 |
+
<Descriptions.Item itemKey={t('邀请人数')}>
|
| 555 |
+
{userState?.user?.aff_count}
|
| 556 |
+
</Descriptions.Item>
|
| 557 |
+
</Descriptions>
|
| 558 |
+
</div>
|
| 559 |
+
</Card>
|
| 560 |
+
<Card style={{ marginTop: 10 }}>
|
| 561 |
+
<Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
|
| 562 |
+
<div style={{ marginTop: 20 }}>
|
| 563 |
+
<Typography.Text strong>{t('邮箱')}</Typography.Text>
|
| 564 |
+
<div
|
| 565 |
+
style={{ display: 'flex', justifyContent: 'space-between' }}
|
| 566 |
+
>
|
| 567 |
+
<div>
|
| 568 |
+
<Input
|
| 569 |
+
value={
|
| 570 |
+
userState.user && userState.user.email !== ''
|
| 571 |
+
? userState.user.email
|
| 572 |
+
: t('未绑定')
|
| 573 |
+
}
|
| 574 |
+
readonly={true}
|
| 575 |
+
></Input>
|
| 576 |
+
</div>
|
| 577 |
+
<div>
|
| 578 |
+
<Button
|
| 579 |
+
onClick={() => {
|
| 580 |
+
setShowEmailBindModal(true);
|
| 581 |
+
}}
|
| 582 |
+
>
|
| 583 |
+
{userState.user && userState.user.email !== ''
|
| 584 |
+
? t('修改绑定')
|
| 585 |
+
: t('绑定邮箱')}
|
| 586 |
+
</Button>
|
| 587 |
+
</div>
|
| 588 |
+
</div>
|
| 589 |
+
</div>
|
| 590 |
+
<div style={{ marginTop: 10 }}>
|
| 591 |
+
<Typography.Text strong>{t('微信')}</Typography.Text>
|
| 592 |
+
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
| 593 |
+
<div>
|
| 594 |
+
<Input
|
| 595 |
+
value={
|
| 596 |
+
userState.user && userState.user.wechat_id !== ''
|
| 597 |
+
? t('已绑定')
|
| 598 |
+
: t('未绑定')
|
| 599 |
+
}
|
| 600 |
+
readonly={true}
|
| 601 |
+
></Input>
|
| 602 |
+
</div>
|
| 603 |
+
<div>
|
| 604 |
+
<Button
|
| 605 |
+
disabled={!status.wechat_login}
|
| 606 |
+
onClick={() => {
|
| 607 |
+
setShowWeChatBindModal(true);
|
| 608 |
+
}}
|
| 609 |
+
>
|
| 610 |
+
{userState.user && userState.user.wechat_id !== ''
|
| 611 |
+
? t('修改绑定')
|
| 612 |
+
: status.wechat_login
|
| 613 |
+
? t('绑定')
|
| 614 |
+
: t('未启用')}
|
| 615 |
+
</Button>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
<div style={{ marginTop: 10 }}>
|
| 620 |
+
<Typography.Text strong>{t('GitHub')}</Typography.Text>
|
| 621 |
+
<div
|
| 622 |
+
style={{ display: 'flex', justifyContent: 'space-between' }}
|
| 623 |
+
>
|
| 624 |
+
<div>
|
| 625 |
+
<Input
|
| 626 |
+
value={
|
| 627 |
+
userState.user && userState.user.github_id !== ''
|
| 628 |
+
? userState.user.github_id
|
| 629 |
+
: t('未绑定')
|
| 630 |
+
}
|
| 631 |
+
readonly={true}
|
| 632 |
+
></Input>
|
| 633 |
+
</div>
|
| 634 |
+
<div>
|
| 635 |
+
<Button
|
| 636 |
+
onClick={() => {
|
| 637 |
+
onGitHubOAuthClicked(status.github_client_id);
|
| 638 |
+
}}
|
| 639 |
+
disabled={
|
| 640 |
+
(userState.user && userState.user.github_id !== '') ||
|
| 641 |
+
!status.github_oauth
|
| 642 |
+
}
|
| 643 |
+
>
|
| 644 |
+
{status.github_oauth ? t('绑定') : t('未启用')}
|
| 645 |
+
</Button>
|
| 646 |
+
</div>
|
| 647 |
+
</div>
|
| 648 |
+
</div>
|
| 649 |
+
<div style={{ marginTop: 10 }}>
|
| 650 |
+
<Typography.Text strong>{t('OIDC')}</Typography.Text>
|
| 651 |
+
<div
|
| 652 |
+
style={{ display: 'flex', justifyContent: 'space-between' }}
|
| 653 |
+
>
|
| 654 |
+
<div>
|
| 655 |
+
<Input
|
| 656 |
+
value={
|
| 657 |
+
userState.user && userState.user.oidc_id !== ''
|
| 658 |
+
? userState.user.oidc_id
|
| 659 |
+
: t('未绑定')
|
| 660 |
+
}
|
| 661 |
+
readonly={true}
|
| 662 |
+
></Input>
|
| 663 |
+
</div>
|
| 664 |
+
<div>
|
| 665 |
+
<Button
|
| 666 |
+
onClick={() => {
|
| 667 |
+
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
|
| 668 |
+
}}
|
| 669 |
+
disabled={
|
| 670 |
+
(userState.user && userState.user.oidc_id !== '') ||
|
| 671 |
+
!status.oidc_enabled
|
| 672 |
+
}
|
| 673 |
+
>
|
| 674 |
+
{status.oidc_enabled ? t('绑定') : t('未启用')}
|
| 675 |
+
</Button>
|
| 676 |
+
</div>
|
| 677 |
+
</div>
|
| 678 |
+
</div>
|
| 679 |
+
<div style={{ marginTop: 10 }}>
|
| 680 |
+
<Typography.Text strong>{t('Telegram')}</Typography.Text>
|
| 681 |
+
<div
|
| 682 |
+
style={{ display: 'flex', justifyContent: 'space-between' }}
|
| 683 |
+
>
|
| 684 |
+
<div>
|
| 685 |
+
<Input
|
| 686 |
+
value={
|
| 687 |
+
userState.user && userState.user.telegram_id !== ''
|
| 688 |
+
? userState.user.telegram_id
|
| 689 |
+
: t('未绑定')
|
| 690 |
+
}
|
| 691 |
+
readonly={true}
|
| 692 |
+
></Input>
|
| 693 |
+
</div>
|
| 694 |
+
<div>
|
| 695 |
+
{status.telegram_oauth ? (
|
| 696 |
+
userState.user.telegram_id !== '' ? (
|
| 697 |
+
<Button disabled={true}>{t('已绑定')}</Button>
|
| 698 |
+
) : (
|
| 699 |
+
<TelegramLoginButton
|
| 700 |
+
dataAuthUrl="/api/oauth/telegram/bind"
|
| 701 |
+
botName={status.telegram_bot_name}
|
| 702 |
+
/>
|
| 703 |
+
)
|
| 704 |
+
) : (
|
| 705 |
+
<Button disabled={true}>{t('未启用')}</Button>
|
| 706 |
+
)}
|
| 707 |
+
</div>
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
<div style={{ marginTop: 10 }}>
|
| 711 |
+
<Typography.Text strong>{t('LinuxDO')}</Typography.Text>
|
| 712 |
+
<div
|
| 713 |
+
style={{ display: 'flex', justifyContent: 'space-between' }}
|
| 714 |
+
>
|
| 715 |
+
<div>
|
| 716 |
+
<Input
|
| 717 |
+
value={
|
| 718 |
+
userState.user && userState.user.linux_do_id !== ''
|
| 719 |
+
? userState.user.linux_do_id
|
| 720 |
+
: t('未绑定')
|
| 721 |
+
}
|
| 722 |
+
readonly={true}
|
| 723 |
+
></Input>
|
| 724 |
+
</div>
|
| 725 |
+
<div>
|
| 726 |
+
<Button
|
| 727 |
+
onClick={() => {
|
| 728 |
+
onLinuxDOOAuthClicked(status.linuxdo_client_id);
|
| 729 |
+
}}
|
| 730 |
+
disabled={
|
| 731 |
+
(userState.user && userState.user.linux_do_id !== '') ||
|
| 732 |
+
!status.linuxdo_oauth
|
| 733 |
+
}
|
| 734 |
+
>
|
| 735 |
+
{status.linuxdo_oauth ? t('绑定') : t('未启用')}
|
| 736 |
+
</Button>
|
| 737 |
+
</div>
|
| 738 |
+
</div>
|
| 739 |
+
</div>
|
| 740 |
+
<div style={{ marginTop: 10 }}>
|
| 741 |
+
<Space>
|
| 742 |
+
<Button onClick={generateAccessToken}>
|
| 743 |
+
{t('生成系统访问令牌')}
|
| 744 |
+
</Button>
|
| 745 |
+
<Button
|
| 746 |
+
onClick={() => {
|
| 747 |
+
setShowChangePasswordModal(true);
|
| 748 |
+
}}
|
| 749 |
+
>
|
| 750 |
+
{t('修改密码')}
|
| 751 |
+
</Button>
|
| 752 |
+
<Button
|
| 753 |
+
type={'danger'}
|
| 754 |
+
onClick={() => {
|
| 755 |
+
setShowAccountDeleteModal(true);
|
| 756 |
+
}}
|
| 757 |
+
>
|
| 758 |
+
{t('删除个人账户')}
|
| 759 |
+
</Button>
|
| 760 |
+
</Space>
|
| 761 |
|
| 762 |
+
{systemToken && (
|
| 763 |
+
<Input
|
| 764 |
+
readOnly
|
| 765 |
+
value={systemToken}
|
| 766 |
+
onClick={handleSystemTokenClick}
|
| 767 |
+
style={{ marginTop: '10px' }}
|
| 768 |
+
/>
|
| 769 |
+
)}
|
| 770 |
+
<Modal
|
| 771 |
+
onCancel={() => setShowWeChatBindModal(false)}
|
| 772 |
+
visible={showWeChatBindModal}
|
| 773 |
+
size={'small'}
|
| 774 |
+
>
|
| 775 |
+
<Image src={status.wechat_qrcode} />
|
| 776 |
+
<div style={{ textAlign: 'center' }}>
|
| 777 |
+
<p>
|
| 778 |
+
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
| 779 |
+
</p>
|
| 780 |
+
</div>
|
| 781 |
+
<Input
|
| 782 |
+
placeholder="验证码"
|
| 783 |
+
name="wechat_verification_code"
|
| 784 |
+
value={inputs.wechat_verification_code}
|
| 785 |
+
onChange={(v) =>
|
| 786 |
+
handleInputChange('wechat_verification_code', v)
|
| 787 |
+
}
|
| 788 |
+
/>
|
| 789 |
+
<Button color="" fluid size="large" onClick={bindWeChat}>
|
| 790 |
+
{t('绑定')}
|
| 791 |
+
</Button>
|
| 792 |
+
</Modal>
|
| 793 |
+
</div>
|
| 794 |
+
</Card>
|
| 795 |
+
<Card style={{ marginTop: 10 }}>
|
| 796 |
+
<Tabs type="line" defaultActiveKey="price">
|
| 797 |
+
<TabPane tab={t('价格设置')} itemKey="price">
|
| 798 |
+
<div style={{ marginTop: 20 }}>
|
| 799 |
+
<Typography.Text strong>{t('接受未设置价格模型')}</Typography.Text>
|
| 800 |
+
<div style={{ marginTop: 10 }}>
|
| 801 |
+
<Checkbox
|
| 802 |
+
checked={notificationSettings.acceptUnsetModelRatioModel}
|
| 803 |
+
onChange={e => handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)}
|
| 804 |
+
>
|
| 805 |
+
{t('接受未设置价格模型')}
|
| 806 |
+
</Checkbox>
|
| 807 |
+
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
|
| 808 |
+
{t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')}
|
| 809 |
+
</Typography.Text>
|
| 810 |
+
</div>
|
| 811 |
+
</div>
|
| 812 |
+
</TabPane>
|
| 813 |
+
<TabPane tab={t('通知设置')} itemKey="notification">
|
| 814 |
+
<div style={{ marginTop: 20 }}>
|
| 815 |
+
<Typography.Text strong>{t('通知方式')}</Typography.Text>
|
| 816 |
+
<div style={{ marginTop: 10 }}>
|
| 817 |
+
<RadioGroup
|
| 818 |
+
value={notificationSettings.warningType}
|
| 819 |
+
onChange={value => handleNotificationSettingChange('warningType', value)}
|
| 820 |
+
>
|
| 821 |
+
<Radio value="email">{t('邮件通知')}</Radio>
|
| 822 |
+
<Radio value="webhook">{t('Webhook通知')}</Radio>
|
| 823 |
+
</RadioGroup>
|
| 824 |
+
</div>
|
| 825 |
+
</div>
|
| 826 |
+
{notificationSettings.warningType === 'webhook' && (
|
| 827 |
+
<>
|
| 828 |
+
<div style={{ marginTop: 20 }}>
|
| 829 |
+
<Typography.Text strong>{t('Webhook地址')}</Typography.Text>
|
| 830 |
+
<div style={{ marginTop: 10 }}>
|
| 831 |
+
<Input
|
| 832 |
+
value={notificationSettings.webhookUrl}
|
| 833 |
+
onChange={val => handleNotificationSettingChange('webhookUrl', val)}
|
| 834 |
+
placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')}
|
| 835 |
+
/>
|
| 836 |
+
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
|
| 837 |
+
{t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')}
|
| 838 |
+
</Typography.Text>
|
| 839 |
+
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
|
| 840 |
+
<div style={{ cursor: 'pointer' }} onClick={() => setShowWebhookDocs(!showWebhookDocs)}>
|
| 841 |
+
{t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'}
|
| 842 |
</div>
|
| 843 |
+
<Collapsible isOpen={showWebhookDocs}>
|
| 844 |
+
<pre style={{
|
| 845 |
+
marginTop: 4,
|
| 846 |
+
background: 'var(--semi-color-fill-0)',
|
| 847 |
+
padding: 8,
|
| 848 |
+
borderRadius: 4
|
| 849 |
+
}}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
{`{
|
| 851 |
"type": "quota_exceed", // 通知类型
|
| 852 |
"title": "标题", // 通知标题
|
|
|
|
| 863 |
"values": ["$0.99"],
|
| 864 |
"timestamp": 1739950503
|
| 865 |
}`}
|
| 866 |
+
</pre>
|
| 867 |
+
</Collapsible>
|
| 868 |
+
</Typography.Text>
|
| 869 |
+
</div>
|
| 870 |
+
</div>
|
| 871 |
+
<div style={{ marginTop: 20 }}>
|
| 872 |
+
<Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
|
| 873 |
+
<div style={{ marginTop: 10 }}>
|
| 874 |
+
<Input
|
| 875 |
+
value={notificationSettings.webhookSecret}
|
| 876 |
+
onChange={val => handleNotificationSettingChange('webhookSecret', val)}
|
| 877 |
+
placeholder={t('请输入密钥')}
|
| 878 |
+
/>
|
| 879 |
+
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
|
| 880 |
+
{t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
|
| 881 |
+
</Typography.Text>
|
| 882 |
+
<Typography.Text type="secondary" style={{ marginTop: 4, display: 'block' }}>
|
| 883 |
+
{t('Authorization: Bearer your-secret-key')}
|
| 884 |
+
</Typography.Text>
|
| 885 |
+
</div>
|
| 886 |
+
</div>
|
| 887 |
+
</>
|
| 888 |
+
)}
|
| 889 |
+
{notificationSettings.warningType === 'email' && (
|
| 890 |
+
<div style={{ marginTop: 20 }}>
|
| 891 |
+
<Typography.Text strong>{t('通知邮箱')}</Typography.Text>
|
| 892 |
+
<div style={{ marginTop: 10 }}>
|
| 893 |
+
<Input
|
| 894 |
+
value={notificationSettings.notificationEmail}
|
| 895 |
+
onChange={val => handleNotificationSettingChange('notificationEmail', val)}
|
| 896 |
+
placeholder={t('留空则使用账号绑定的邮箱')}
|
| 897 |
+
/>
|
| 898 |
+
<Typography.Text type="secondary" style={{ marginTop: 8, display: 'block' }}>
|
| 899 |
+
{t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
|
| 900 |
+
</Typography.Text>
|
| 901 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
</div>
|
| 903 |
+
)}
|
| 904 |
+
<div style={{ marginTop: 20 }}>
|
| 905 |
+
<Typography.Text
|
| 906 |
+
strong>{t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)}</Typography.Text>
|
| 907 |
+
<div style={{ marginTop: 10 }}>
|
| 908 |
+
<AutoComplete
|
| 909 |
+
value={notificationSettings.warningThreshold}
|
| 910 |
+
onChange={val => handleNotificationSettingChange('warningThreshold', val)}
|
| 911 |
+
style={{ width: 200 }}
|
| 912 |
+
placeholder={t('请输入预警额度')}
|
| 913 |
+
data={[
|
| 914 |
+
{ value: 100000, label: '0.2$' },
|
| 915 |
+
{ value: 500000, label: '1$' },
|
| 916 |
+
{ value: 1000000, label: '5$' },
|
| 917 |
+
{ value: 5000000, label: '10$' }
|
| 918 |
+
]}
|
| 919 |
+
/>
|
| 920 |
+
</div>
|
| 921 |
+
<Typography.Text type="secondary" style={{ marginTop: 10, display: 'block' }}>
|
| 922 |
+
{t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')}
|
| 923 |
+
</Typography.Text>
|
| 924 |
+
</div>
|
| 925 |
+
</TabPane>
|
| 926 |
+
</Tabs>
|
| 927 |
+
<div style={{ marginTop: 20 }}>
|
| 928 |
+
<Button type="primary" onClick={saveNotificationSettings}>
|
| 929 |
+
{t('保存设置')}
|
| 930 |
+
</Button>
|
| 931 |
+
</div>
|
| 932 |
+
</Card>
|
| 933 |
+
<Modal
|
| 934 |
+
onCancel={() => setShowEmailBindModal(false)}
|
| 935 |
+
onOk={bindEmail}
|
| 936 |
+
visible={showEmailBindModal}
|
| 937 |
+
size={'small'}
|
| 938 |
+
centered={true}
|
| 939 |
+
maskClosable={false}
|
| 940 |
+
>
|
| 941 |
+
<Typography.Title heading={6}>{t('绑定邮箱地址')}</Typography.Title>
|
| 942 |
+
<div
|
| 943 |
+
style={{
|
| 944 |
+
marginTop: 20,
|
| 945 |
+
display: 'flex',
|
| 946 |
+
justifyContent: 'space-between'
|
| 947 |
+
}}
|
| 948 |
+
>
|
| 949 |
+
<Input
|
| 950 |
+
fluid
|
| 951 |
+
placeholder="输入邮箱地址"
|
| 952 |
+
onChange={(value) => handleInputChange('email', value)}
|
| 953 |
+
name="email"
|
| 954 |
+
type="email"
|
| 955 |
+
/>
|
| 956 |
+
<Button
|
| 957 |
+
onClick={sendVerificationCode}
|
| 958 |
+
disabled={disableButton || loading}
|
| 959 |
+
>
|
| 960 |
+
{disableButton ? `重新发送 (${countdown})` : '获取验证码'}
|
| 961 |
+
</Button>
|
| 962 |
+
</div>
|
| 963 |
+
<div style={{ marginTop: 10 }}>
|
| 964 |
+
<Input
|
| 965 |
+
fluid
|
| 966 |
+
placeholder="验证码"
|
| 967 |
+
name="email_verification_code"
|
| 968 |
+
value={inputs.email_verification_code}
|
| 969 |
+
onChange={(value) =>
|
| 970 |
+
handleInputChange('email_verification_code', value)
|
| 971 |
+
}
|
| 972 |
+
/>
|
| 973 |
+
</div>
|
| 974 |
+
{turnstileEnabled ? (
|
| 975 |
+
<Turnstile
|
| 976 |
+
sitekey={turnstileSiteKey}
|
| 977 |
+
onVerify={(token) => {
|
| 978 |
+
setTurnstileToken(token);
|
| 979 |
+
}}
|
| 980 |
+
/>
|
| 981 |
+
) : (
|
| 982 |
+
<></>
|
| 983 |
+
)}
|
| 984 |
+
</Modal>
|
| 985 |
+
<Modal
|
| 986 |
+
onCancel={() => setShowAccountDeleteModal(false)}
|
| 987 |
+
visible={showAccountDeleteModal}
|
| 988 |
+
size={'small'}
|
| 989 |
+
centered={true}
|
| 990 |
+
onOk={deleteAccount}
|
| 991 |
+
>
|
| 992 |
+
<div style={{ marginTop: 20 }}>
|
| 993 |
+
<Banner
|
| 994 |
+
type="danger"
|
| 995 |
+
description="您正在删除自己的帐户,将清空所有数据且不可恢复"
|
| 996 |
+
closeIcon={null}
|
| 997 |
+
/>
|
| 998 |
+
</div>
|
| 999 |
+
<div style={{ marginTop: 20 }}>
|
| 1000 |
+
<Input
|
| 1001 |
+
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
| 1002 |
+
name="self_account_deletion_confirmation"
|
| 1003 |
+
value={inputs.self_account_deletion_confirmation}
|
| 1004 |
+
onChange={(value) =>
|
| 1005 |
+
handleInputChange(
|
| 1006 |
+
'self_account_deletion_confirmation',
|
| 1007 |
+
value
|
| 1008 |
+
)
|
| 1009 |
+
}
|
| 1010 |
+
/>
|
| 1011 |
+
{turnstileEnabled ? (
|
| 1012 |
+
<Turnstile
|
| 1013 |
+
sitekey={turnstileSiteKey}
|
| 1014 |
+
onVerify={(token) => {
|
| 1015 |
+
setTurnstileToken(token);
|
| 1016 |
+
}}
|
| 1017 |
+
/>
|
| 1018 |
+
) : (
|
| 1019 |
+
<></>
|
| 1020 |
+
)}
|
| 1021 |
+
</div>
|
| 1022 |
+
</Modal>
|
| 1023 |
+
<Modal
|
| 1024 |
+
onCancel={() => setShowChangePasswordModal(false)}
|
| 1025 |
+
visible={showChangePasswordModal}
|
| 1026 |
+
size={'small'}
|
| 1027 |
+
centered={true}
|
| 1028 |
+
onOk={changePassword}
|
| 1029 |
+
>
|
| 1030 |
+
<div style={{ marginTop: 20 }}>
|
| 1031 |
+
<Input
|
| 1032 |
+
name="set_new_password"
|
| 1033 |
+
placeholder={t('新密码')}
|
| 1034 |
+
value={inputs.set_new_password}
|
| 1035 |
+
onChange={(value) =>
|
| 1036 |
+
handleInputChange('set_new_password', value)
|
| 1037 |
+
}
|
| 1038 |
+
/>
|
| 1039 |
+
<Input
|
| 1040 |
+
style={{ marginTop: 20 }}
|
| 1041 |
+
name="set_new_password_confirmation"
|
| 1042 |
+
placeholder={t('确认新密码')}
|
| 1043 |
+
value={inputs.set_new_password_confirmation}
|
| 1044 |
+
onChange={(value) =>
|
| 1045 |
+
handleInputChange('set_new_password_confirmation', value)
|
| 1046 |
+
}
|
| 1047 |
+
/>
|
| 1048 |
+
{turnstileEnabled ? (
|
| 1049 |
+
<Turnstile
|
| 1050 |
+
sitekey={turnstileSiteKey}
|
| 1051 |
+
onVerify={(token) => {
|
| 1052 |
+
setTurnstileToken(token);
|
| 1053 |
+
}}
|
| 1054 |
+
/>
|
| 1055 |
+
) : (
|
| 1056 |
+
<></>
|
| 1057 |
+
)}
|
| 1058 |
+
</div>
|
| 1059 |
+
</Modal>
|
| 1060 |
+
</div>
|
| 1061 |
+
</Layout.Content>
|
| 1062 |
+
</Layout>
|
| 1063 |
+
</div>
|
| 1064 |
+
);
|
| 1065 |
};
|
| 1066 |
|
| 1067 |
export default PersonalSetting;
|
web/src/components/SystemSetting.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
| 1 |
-
import React, { useEffect, useState } from 'react';
|
| 2 |
import {
|
| 3 |
Button,
|
| 4 |
-
Divider,
|
| 5 |
Form,
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
Modal,
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
const SystemSetting = () => {
|
| 16 |
let [inputs, setInputs] = useState({
|
|
@@ -64,144 +71,138 @@ const SystemSetting = () => {
|
|
| 64 |
LinuxDOClientId: '',
|
| 65 |
LinuxDOClientSecret: '',
|
| 66 |
});
|
| 67 |
-
const [originInputs, setOriginInputs] = useState({});
|
| 68 |
-
let [loading, setLoading] = useState(false);
|
| 69 |
-
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
| 70 |
-
const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
|
| 71 |
-
const [showPasswordWarningModal, setShowPasswordWarningModal] =
|
| 72 |
-
useState(false);
|
| 73 |
|
| 74 |
-
const
|
| 75 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
const getOptions = async () => {
|
|
|
|
| 78 |
const res = await API.get('/api/option/');
|
| 79 |
const { success, message, data } = res.data;
|
| 80 |
if (success) {
|
| 81 |
let newInputs = {};
|
| 82 |
data.forEach((item) => {
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
}
|
| 86 |
newInputs[item.key] = item.value;
|
| 87 |
});
|
| 88 |
-
setInputs(
|
| 89 |
-
...newInputs,
|
| 90 |
-
EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
|
| 91 |
-
});
|
| 92 |
setOriginInputs(newInputs);
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}),
|
| 98 |
-
);
|
| 99 |
} else {
|
| 100 |
showError(message);
|
| 101 |
}
|
|
|
|
| 102 |
};
|
| 103 |
|
| 104 |
useEffect(() => {
|
| 105 |
-
getOptions()
|
| 106 |
}, []);
|
| 107 |
-
useEffect(() => {}, [inputs.EmailDomainWhitelist]);
|
| 108 |
|
| 109 |
-
const
|
| 110 |
setLoading(true);
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
const res = await API.put('/api/option/', {
|
| 131 |
-
key,
|
| 132 |
-
value,
|
| 133 |
-
});
|
| 134 |
-
const { success, message } = res.data;
|
| 135 |
-
if (success) {
|
| 136 |
-
if (key === 'EmailDomainWhitelist') {
|
| 137 |
-
value = value.split(',');
|
| 138 |
}
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
}
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
}
|
| 149 |
setLoading(false);
|
| 150 |
};
|
| 151 |
|
| 152 |
-
const
|
| 153 |
-
|
| 154 |
-
// block disabling password login
|
| 155 |
-
setShowPasswordWarningModal(true);
|
| 156 |
-
return;
|
| 157 |
-
}
|
| 158 |
-
if (
|
| 159 |
-
name === 'Notice' ||
|
| 160 |
-
(name.startsWith('SMTP') && name !== 'SMTPSSLEnabled') ||
|
| 161 |
-
name === 'ServerAddress' ||
|
| 162 |
-
name === 'WorkerUrl' ||
|
| 163 |
-
name === 'WorkerValidKey' ||
|
| 164 |
-
name === 'EpayId' ||
|
| 165 |
-
name === 'EpayKey' ||
|
| 166 |
-
name === 'Price' ||
|
| 167 |
-
name === 'PayAddress' ||
|
| 168 |
-
name === 'GitHubClientId' ||
|
| 169 |
-
name === 'GitHubClientSecret' ||
|
| 170 |
-
name === 'oidc.well_known' ||
|
| 171 |
-
name === 'oidc.client_id' ||
|
| 172 |
-
name === 'oidc.client_secret' ||
|
| 173 |
-
name === 'oidc.authorization_endpoint' ||
|
| 174 |
-
name === 'oidc.token_endpoint' ||
|
| 175 |
-
name === 'oidc.user_info_endpoint' ||
|
| 176 |
-
name === 'WeChatServerAddress' ||
|
| 177 |
-
name === 'WeChatServerToken' ||
|
| 178 |
-
name === 'WeChatAccountQRCodeImageURL' ||
|
| 179 |
-
name === 'TurnstileSiteKey' ||
|
| 180 |
-
name === 'TurnstileSecretKey' ||
|
| 181 |
-
name === 'EmailDomainWhitelist' ||
|
| 182 |
-
name === 'TopupGroupRatio' ||
|
| 183 |
-
name === 'TelegramBotToken' ||
|
| 184 |
-
name === 'TelegramBotName' ||
|
| 185 |
-
name === 'LinuxDOClientId' ||
|
| 186 |
-
name === 'LinuxDOClientSecret'
|
| 187 |
-
) {
|
| 188 |
-
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
| 189 |
-
} else {
|
| 190 |
-
await updateOption(name, value);
|
| 191 |
-
}
|
| 192 |
};
|
| 193 |
|
| 194 |
const submitServerAddress = async () => {
|
| 195 |
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
| 196 |
-
await
|
| 197 |
};
|
| 198 |
|
| 199 |
const submitWorker = async () => {
|
| 200 |
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
| 201 |
-
await
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
};
|
| 206 |
|
| 207 |
const submitPayAddress = async () => {
|
|
@@ -214,89 +215,105 @@ const SystemSetting = () => {
|
|
| 214 |
showError('充值分组倍率不是合法的 JSON 字符串');
|
| 215 |
return;
|
| 216 |
}
|
| 217 |
-
await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
|
| 218 |
}
|
| 219 |
-
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
| 221 |
if (inputs.EpayId !== '') {
|
| 222 |
-
|
| 223 |
}
|
| 224 |
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
| 225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
}
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
};
|
| 229 |
|
| 230 |
const submitSMTP = async () => {
|
|
|
|
|
|
|
| 231 |
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
| 232 |
-
|
| 233 |
}
|
| 234 |
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
| 235 |
-
|
| 236 |
}
|
| 237 |
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
-
if (
|
| 241 |
-
|
| 242 |
-
inputs.SMTPPort !== ''
|
| 243 |
-
) {
|
| 244 |
-
await updateOption('SMTPPort', inputs.SMTPPort);
|
| 245 |
}
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
) {
|
| 250 |
-
await updateOption('SMTPToken', inputs.SMTPToken);
|
| 251 |
}
|
| 252 |
};
|
| 253 |
|
| 254 |
const submitEmailDomainWhitelist = async () => {
|
| 255 |
-
if (
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
inputs.EmailDomainWhitelist.join(','),
|
| 263 |
-
);
|
| 264 |
}
|
| 265 |
};
|
| 266 |
|
| 267 |
const submitWeChat = async () => {
|
|
|
|
|
|
|
| 268 |
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
| 269 |
-
|
| 270 |
-
'WeChatServerAddress',
|
| 271 |
-
removeTrailingSlash(inputs.WeChatServerAddress)
|
| 272 |
-
);
|
| 273 |
}
|
| 274 |
-
if (
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
'WeChatAccountQRCodeImageURL',
|
| 280 |
-
inputs.WeChatAccountQRCodeImageURL,
|
| 281 |
-
);
|
| 282 |
}
|
| 283 |
-
if (
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
| 288 |
}
|
| 289 |
};
|
| 290 |
|
| 291 |
const submitGitHubOAuth = async () => {
|
|
|
|
|
|
|
| 292 |
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
| 294 |
}
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
) {
|
| 299 |
-
await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
|
| 300 |
}
|
| 301 |
};
|
| 302 |
|
|
@@ -315,679 +332,599 @@ const SystemSetting = () => {
|
|
| 315 |
} catch (err) {
|
| 316 |
console.error(err);
|
| 317 |
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
|
|
|
|
| 318 |
}
|
| 319 |
}
|
| 320 |
|
|
|
|
|
|
|
| 321 |
if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
|
| 322 |
-
|
| 323 |
}
|
| 324 |
if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
|
| 325 |
-
|
| 326 |
}
|
| 327 |
if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') {
|
| 328 |
-
|
| 329 |
}
|
| 330 |
if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) {
|
| 331 |
-
|
| 332 |
}
|
| 333 |
if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
|
| 334 |
-
|
| 335 |
}
|
| 336 |
if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) {
|
| 337 |
-
|
| 338 |
}
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
const submitTelegramSettings = async () => {
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
};
|
| 346 |
|
| 347 |
const submitTurnstile = async () => {
|
|
|
|
|
|
|
| 348 |
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
| 349 |
-
|
| 350 |
}
|
| 351 |
-
if (
|
| 352 |
-
|
| 353 |
-
inputs.TurnstileSecretKey !== ''
|
| 354 |
-
) {
|
| 355 |
-
await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
|
| 356 |
}
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
const localDomainList = inputs.EmailDomainWhitelist;
|
| 361 |
-
if (
|
| 362 |
-
restrictedDomainInput !== '' &&
|
| 363 |
-
!localDomainList.includes(restrictedDomainInput)
|
| 364 |
-
) {
|
| 365 |
-
setRestrictedDomainInput('');
|
| 366 |
-
setInputs({
|
| 367 |
-
...inputs,
|
| 368 |
-
EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
|
| 369 |
-
});
|
| 370 |
-
setEmailDomainWhitelist([
|
| 371 |
-
...EmailDomainWhitelist,
|
| 372 |
-
{
|
| 373 |
-
key: restrictedDomainInput,
|
| 374 |
-
text: restrictedDomainInput,
|
| 375 |
-
value: restrictedDomainInput,
|
| 376 |
-
},
|
| 377 |
-
]);
|
| 378 |
}
|
| 379 |
};
|
| 380 |
|
| 381 |
const submitLinuxDOOAuth = async () => {
|
|
|
|
|
|
|
| 382 |
if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
|
| 383 |
-
|
| 384 |
}
|
| 385 |
-
if (
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
| 390 |
}
|
| 391 |
};
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
return (
|
| 394 |
-
<
|
| 395 |
-
|
| 396 |
-
<Form
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
</a>
|
| 421 |
-
)
|
| 422 |
-
</Header>
|
| 423 |
-
<Message info>
|
| 424 |
-
注意:代理功能仅对图片请求和 Webhook 请求生效,不会影响其他 API 请求。如需配置 API 请求代理,请参考
|
| 425 |
-
<a
|
| 426 |
-
href='https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md'
|
| 427 |
-
target='_blank'
|
| 428 |
-
rel='noreferrer'
|
| 429 |
-
>
|
| 430 |
-
{' '}API 代理设置文档
|
| 431 |
-
</a>
|
| 432 |
-
。
|
| 433 |
-
</Message>
|
| 434 |
-
<Form.Group widths='equal'>
|
| 435 |
-
<Form.Input
|
| 436 |
-
label='Worker地址,不填写则不启用代理'
|
| 437 |
-
placeholder='例如:https://workername.yourdomain.workers.dev'
|
| 438 |
-
value={inputs.WorkerUrl}
|
| 439 |
-
name='WorkerUrl'
|
| 440 |
-
onChange={handleInputChange}
|
| 441 |
-
/>
|
| 442 |
-
<Form.Input
|
| 443 |
-
label='Worker密钥,根据你部署的 Worker 填写'
|
| 444 |
-
placeholder='例如:your_secret_key'
|
| 445 |
-
value={inputs.WorkerValidKey}
|
| 446 |
-
name='WorkerValidKey'
|
| 447 |
-
onChange={handleInputChange}
|
| 448 |
-
/>
|
| 449 |
-
</Form.Group>
|
| 450 |
-
<Form.Button onClick={submitWorker}>更新Worker设置</Form.Button>
|
| 451 |
-
<Divider />
|
| 452 |
-
<Header as='h3' inverted={isDark}>
|
| 453 |
-
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
|
| 454 |
-
</Header>
|
| 455 |
-
<Form.Group widths='equal'>
|
| 456 |
-
<Form.Input
|
| 457 |
-
label='支付地址,不填写则不启用在线支付'
|
| 458 |
-
placeholder='例如:https://yourdomain.com'
|
| 459 |
-
value={inputs.PayAddress}
|
| 460 |
-
name='PayAddress'
|
| 461 |
-
onChange={handleInputChange}
|
| 462 |
-
/>
|
| 463 |
-
<Form.Input
|
| 464 |
-
label='易支付商户ID'
|
| 465 |
-
placeholder='例如:0001'
|
| 466 |
-
value={inputs.EpayId}
|
| 467 |
-
name='EpayId'
|
| 468 |
-
onChange={handleInputChange}
|
| 469 |
-
/>
|
| 470 |
-
<Form.Input
|
| 471 |
-
label='易支付商户密钥'
|
| 472 |
-
placeholder='敏感信息不会发送到前端显示'
|
| 473 |
-
value={inputs.EpayKey}
|
| 474 |
-
name='EpayKey'
|
| 475 |
-
onChange={handleInputChange}
|
| 476 |
-
/>
|
| 477 |
-
</Form.Group>
|
| 478 |
-
<Form.Group widths='equal'>
|
| 479 |
-
<Form.Input
|
| 480 |
-
label='回调地址,不填写则使用上方服务器地址作为回调地址'
|
| 481 |
-
placeholder='例如:https://yourdomain.com'
|
| 482 |
-
value={inputs.CustomCallbackAddress}
|
| 483 |
-
name='CustomCallbackAddress'
|
| 484 |
-
onChange={handleInputChange}
|
| 485 |
-
/>
|
| 486 |
-
<Form.Input
|
| 487 |
-
label='充值价格(x元/美金)'
|
| 488 |
-
placeholder='例如:7,就是7元/美金'
|
| 489 |
-
value={inputs.Price}
|
| 490 |
-
name='Price'
|
| 491 |
-
min={0}
|
| 492 |
-
onChange={handleInputChange}
|
| 493 |
-
/>
|
| 494 |
-
<Form.Input
|
| 495 |
-
label='最低充值美元数量(以美金为单位,如果使用额度请自行换算!)'
|
| 496 |
-
placeholder='例如:2,就是最低充值2$'
|
| 497 |
-
value={inputs.MinTopUp}
|
| 498 |
-
name='MinTopUp'
|
| 499 |
-
min={1}
|
| 500 |
-
onChange={handleInputChange}
|
| 501 |
-
/>
|
| 502 |
-
</Form.Group>
|
| 503 |
-
<Form.Group widths='equal'>
|
| 504 |
-
<Form.TextArea
|
| 505 |
-
label='充值分组倍率'
|
| 506 |
-
name='TopupGroupRatio'
|
| 507 |
-
onChange={handleInputChange}
|
| 508 |
-
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
| 509 |
-
autoComplete='new-password'
|
| 510 |
-
value={inputs.TopupGroupRatio}
|
| 511 |
-
placeholder='为一个 JSON 文本,键为组名称,值为倍率'
|
| 512 |
-
/>
|
| 513 |
-
</Form.Group>
|
| 514 |
-
<Form.Button onClick={submitPayAddress}>更新支付设置</Form.Button>
|
| 515 |
-
<Divider />
|
| 516 |
-
<Header as='h3' inverted={isDark}>
|
| 517 |
-
配置登录注册
|
| 518 |
-
</Header>
|
| 519 |
-
<Form.Group inline>
|
| 520 |
-
<Form.Checkbox
|
| 521 |
-
checked={inputs.PasswordLoginEnabled === 'true'}
|
| 522 |
-
label='允许通过密码进行登录'
|
| 523 |
-
name='PasswordLoginEnabled'
|
| 524 |
-
onChange={handleInputChange}
|
| 525 |
-
/>
|
| 526 |
-
{showPasswordWarningModal && (
|
| 527 |
-
<Modal
|
| 528 |
-
open={showPasswordWarningModal}
|
| 529 |
-
onClose={() => setShowPasswordWarningModal(false)}
|
| 530 |
-
size={'tiny'}
|
| 531 |
-
style={{ maxWidth: '450px' }}
|
| 532 |
-
>
|
| 533 |
-
<Modal.Header>警告</Modal.Header>
|
| 534 |
-
<Modal.Content>
|
| 535 |
-
<p>
|
| 536 |
-
取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
|
| 537 |
-
</p>
|
| 538 |
-
</Modal.Content>
|
| 539 |
-
<Modal.Actions>
|
| 540 |
-
<Button onClick={() => setShowPasswordWarningModal(false)}>
|
| 541 |
-
取消
|
| 542 |
-
</Button>
|
| 543 |
-
<Button
|
| 544 |
-
color='yellow'
|
| 545 |
-
onClick={async () => {
|
| 546 |
-
setShowPasswordWarningModal(false);
|
| 547 |
-
await updateOption('PasswordLoginEnabled', 'false');
|
| 548 |
-
}}
|
| 549 |
>
|
| 550 |
-
|
| 551 |
-
</
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
<Button
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
submitNewRestrictedDomain();
|
| 656 |
-
}}
|
| 657 |
>
|
| 658 |
-
|
| 659 |
</Button>
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
}
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
>
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
<
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
>
|
| 893 |
-
|
| 894 |
-
</
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
</Header>
|
| 898 |
-
<Message>
|
| 899 |
-
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
| 900 |
-
,Authorization callback URL 填{' '}
|
| 901 |
-
<code>{`${inputs.ServerAddress}/oauth/linuxdo`}</code>
|
| 902 |
-
</Message>
|
| 903 |
-
<Form.Group widths={3}>
|
| 904 |
-
<Form.Input
|
| 905 |
-
label='LinuxDO Client ID'
|
| 906 |
-
name='LinuxDOClientId'
|
| 907 |
-
onChange={handleInputChange}
|
| 908 |
-
autoComplete='new-password'
|
| 909 |
-
value={inputs.LinuxDOClientId}
|
| 910 |
-
placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
|
| 911 |
-
/>
|
| 912 |
-
<Form.Input
|
| 913 |
-
label='LinuxDO Client Secret'
|
| 914 |
-
name='LinuxDOClientSecret'
|
| 915 |
-
onChange={handleInputChange}
|
| 916 |
-
type='password'
|
| 917 |
-
autoComplete='new-password'
|
| 918 |
-
value={inputs.LinuxDOClientSecret}
|
| 919 |
-
placeholder='敏感信息不会发送到前端显示'
|
| 920 |
-
/>
|
| 921 |
-
</Form.Group>
|
| 922 |
-
<Form.Button onClick={submitLinuxDOOAuth}>
|
| 923 |
-
保存 LinuxDO OAuth 设置
|
| 924 |
-
</Form.Button>
|
| 925 |
-
<Divider />
|
| 926 |
-
<Header as='h3' inverted={isDark}>
|
| 927 |
-
配置 OIDC
|
| 928 |
-
<Header.Subheader>
|
| 929 |
-
用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP
|
| 930 |
-
</Header.Subheader>
|
| 931 |
-
</Header>
|
| 932 |
-
<Message>
|
| 933 |
-
主页链接填 <code>{ inputs.ServerAddress }</code>,
|
| 934 |
-
重定向 URL 填 <code>{ `${ inputs.ServerAddress }/oauth/oidc` }</code>
|
| 935 |
-
</Message>
|
| 936 |
-
<Message>
|
| 937 |
-
若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置
|
| 938 |
-
</Message>
|
| 939 |
-
<Form.Group widths={3}>
|
| 940 |
-
<Form.Input
|
| 941 |
-
label='Client ID'
|
| 942 |
-
name='oidc.client_id'
|
| 943 |
-
onChange={handleInputChange}
|
| 944 |
-
value={inputs['oidc.client_id']}
|
| 945 |
-
placeholder='输入 OIDC 的 Client ID'
|
| 946 |
-
/>
|
| 947 |
-
<Form.Input
|
| 948 |
-
label='Client Secret'
|
| 949 |
-
name='oidc.client_secret'
|
| 950 |
-
onChange={handleInputChange}
|
| 951 |
-
type='password'
|
| 952 |
-
value={inputs['oidc.client_secret']}
|
| 953 |
-
placeholder='敏感信息不会发送到前端显示'
|
| 954 |
-
/>
|
| 955 |
-
<Form.Input
|
| 956 |
-
label='Well-Known URL'
|
| 957 |
-
name='oidc.well_known'
|
| 958 |
-
onChange={handleInputChange}
|
| 959 |
-
value={inputs['oidc.well_known']}
|
| 960 |
-
placeholder='请输入 OIDC 的 Well-Known URL'
|
| 961 |
-
/>
|
| 962 |
-
<Form.Input
|
| 963 |
-
label='Authorization Endpoint'
|
| 964 |
-
name='oidc.authorization_endpoint'
|
| 965 |
-
onChange={handleInputChange}
|
| 966 |
-
value={inputs['oidc.authorization_endpoint']}
|
| 967 |
-
placeholder='输入 OIDC 的 Authorization Endpoint'
|
| 968 |
-
/>
|
| 969 |
-
<Form.Input
|
| 970 |
-
label='Token Endpoint'
|
| 971 |
-
name='oidc.token_endpoint'
|
| 972 |
-
onChange={handleInputChange}
|
| 973 |
-
value={inputs['oidc.token_endpoint']}
|
| 974 |
-
placeholder='输入 OIDC 的 Token Endpoint'
|
| 975 |
-
/>
|
| 976 |
-
<Form.Input
|
| 977 |
-
label='Userinfo Endpoint'
|
| 978 |
-
name='oidc.user_info_endpoint'
|
| 979 |
-
onChange={handleInputChange}
|
| 980 |
-
value={inputs['oidc.user_info_endpoint']}
|
| 981 |
-
placeholder='输入 OIDC 的 Userinfo Endpoint'
|
| 982 |
-
/>
|
| 983 |
-
</Form.Group>
|
| 984 |
-
<Form.Button onClick={submitOIDCSettings}>
|
| 985 |
-
保存 OIDC 设置
|
| 986 |
-
</Form.Button>
|
| 987 |
</Form>
|
| 988 |
-
|
| 989 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
);
|
| 991 |
};
|
| 992 |
|
| 993 |
-
export default SystemSetting;
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useRef } from 'react';
|
| 2 |
import {
|
| 3 |
Button,
|
|
|
|
| 4 |
Form,
|
| 5 |
+
Row,
|
| 6 |
+
Col,
|
| 7 |
+
Typography,
|
| 8 |
Modal,
|
| 9 |
+
Banner,
|
| 10 |
+
TagInput,
|
| 11 |
+
Spin,
|
| 12 |
+
} from '@douyinfe/semi-ui';
|
| 13 |
+
const { Text } = Typography;
|
| 14 |
+
import {
|
| 15 |
+
removeTrailingSlash,
|
| 16 |
+
showError,
|
| 17 |
+
showSuccess,
|
| 18 |
+
verifyJSON,
|
| 19 |
+
} from '../helpers/utils';
|
| 20 |
+
import { API } from '../helpers/api';
|
| 21 |
|
| 22 |
const SystemSetting = () => {
|
| 23 |
let [inputs, setInputs] = useState({
|
|
|
|
| 71 |
LinuxDOClientId: '',
|
| 72 |
LinuxDOClientSecret: '',
|
| 73 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
+
const [originInputs, setOriginInputs] = useState({});
|
| 76 |
+
const [loading, setLoading] = useState(false);
|
| 77 |
+
const [isLoaded, setIsLoaded] = useState(false);
|
| 78 |
+
const formApiRef = useRef(null);
|
| 79 |
+
const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
| 80 |
+
const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] = useState(false);
|
| 81 |
+
const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false);
|
| 82 |
|
| 83 |
const getOptions = async () => {
|
| 84 |
+
setLoading(true);
|
| 85 |
const res = await API.get('/api/option/');
|
| 86 |
const { success, message, data } = res.data;
|
| 87 |
if (success) {
|
| 88 |
let newInputs = {};
|
| 89 |
data.forEach((item) => {
|
| 90 |
+
switch (item.key) {
|
| 91 |
+
case 'TopupGroupRatio':
|
| 92 |
+
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
| 93 |
+
break;
|
| 94 |
+
case 'EmailDomainWhitelist':
|
| 95 |
+
setEmailDomainWhitelist(item.value ? item.value.split(',') : []);
|
| 96 |
+
break;
|
| 97 |
+
case 'PasswordLoginEnabled':
|
| 98 |
+
case 'PasswordRegisterEnabled':
|
| 99 |
+
case 'EmailVerificationEnabled':
|
| 100 |
+
case 'GitHubOAuthEnabled':
|
| 101 |
+
case 'WeChatAuthEnabled':
|
| 102 |
+
case 'TelegramOAuthEnabled':
|
| 103 |
+
case 'RegisterEnabled':
|
| 104 |
+
case 'TurnstileCheckEnabled':
|
| 105 |
+
case 'EmailDomainRestrictionEnabled':
|
| 106 |
+
case 'EmailAliasRestrictionEnabled':
|
| 107 |
+
case 'SMTPSSLEnabled':
|
| 108 |
+
case 'LinuxDOOAuthEnabled':
|
| 109 |
+
case 'oidc.enabled':
|
| 110 |
+
item.value = item.value === 'true';
|
| 111 |
+
break;
|
| 112 |
+
case 'Price':
|
| 113 |
+
case 'MinTopUp':
|
| 114 |
+
item.value = parseFloat(item.value);
|
| 115 |
+
break;
|
| 116 |
+
default:
|
| 117 |
+
break;
|
| 118 |
}
|
| 119 |
newInputs[item.key] = item.value;
|
| 120 |
});
|
| 121 |
+
setInputs(newInputs);
|
|
|
|
|
|
|
|
|
|
| 122 |
setOriginInputs(newInputs);
|
| 123 |
+
if (formApiRef.current) {
|
| 124 |
+
formApiRef.current.setValues(newInputs);
|
| 125 |
+
}
|
| 126 |
+
setIsLoaded(true);
|
|
|
|
|
|
|
| 127 |
} else {
|
| 128 |
showError(message);
|
| 129 |
}
|
| 130 |
+
setLoading(false);
|
| 131 |
};
|
| 132 |
|
| 133 |
useEffect(() => {
|
| 134 |
+
getOptions();
|
| 135 |
}, []);
|
|
|
|
| 136 |
|
| 137 |
+
const updateOptions = async (options) => {
|
| 138 |
setLoading(true);
|
| 139 |
+
try {
|
| 140 |
+
// 分离 checkbox 类型的选项和其他选项
|
| 141 |
+
const checkboxOptions = options.filter(opt =>
|
| 142 |
+
opt.key.toLowerCase().endsWith('enabled')
|
| 143 |
+
);
|
| 144 |
+
const otherOptions = options.filter(opt =>
|
| 145 |
+
!opt.key.toLowerCase().endsWith('enabled')
|
| 146 |
+
);
|
| 147 |
+
|
| 148 |
+
// 处理 checkbox 类型的选项
|
| 149 |
+
for (const opt of checkboxOptions) {
|
| 150 |
+
const res = await API.put('/api/option/', {
|
| 151 |
+
key: opt.key,
|
| 152 |
+
value: opt.value.toString()
|
| 153 |
+
});
|
| 154 |
+
if (!res.data.success) {
|
| 155 |
+
showError(res.data.message);
|
| 156 |
+
return;
|
| 157 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
}
|
| 159 |
+
|
| 160 |
+
// 处理其他选项
|
| 161 |
+
if (otherOptions.length > 0) {
|
| 162 |
+
const requestQueue = otherOptions.map(opt =>
|
| 163 |
+
API.put('/api/option/', {
|
| 164 |
+
key: opt.key,
|
| 165 |
+
value: typeof opt.value === 'boolean' ? opt.value.toString() : opt.value
|
| 166 |
+
})
|
| 167 |
+
);
|
| 168 |
+
|
| 169 |
+
const results = await Promise.all(requestQueue);
|
| 170 |
+
|
| 171 |
+
// 检查所有请求是否成功
|
| 172 |
+
const errorResults = results.filter(res => !res.data.success);
|
| 173 |
+
errorResults.forEach(res => {
|
| 174 |
+
showError(res.data.message);
|
| 175 |
+
});
|
| 176 |
}
|
| 177 |
+
|
| 178 |
+
showSuccess('更新成功');
|
| 179 |
+
// 更新本地状态
|
| 180 |
+
const newInputs = { ...inputs };
|
| 181 |
+
options.forEach(opt => {
|
| 182 |
+
newInputs[opt.key] = opt.value;
|
| 183 |
+
});
|
| 184 |
+
setInputs(newInputs);
|
| 185 |
+
} catch (error) {
|
| 186 |
+
showError('更新失败');
|
| 187 |
}
|
| 188 |
setLoading(false);
|
| 189 |
};
|
| 190 |
|
| 191 |
+
const handleFormChange = (values) => {
|
| 192 |
+
setInputs(values);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
};
|
| 194 |
|
| 195 |
const submitServerAddress = async () => {
|
| 196 |
let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
|
| 197 |
+
await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]);
|
| 198 |
};
|
| 199 |
|
| 200 |
const submitWorker = async () => {
|
| 201 |
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
| 202 |
+
await updateOptions([
|
| 203 |
+
{ key: 'WorkerUrl', value: WorkerUrl },
|
| 204 |
+
{ key: 'WorkerValidKey', value: inputs.WorkerValidKey }
|
| 205 |
+
]);
|
| 206 |
};
|
| 207 |
|
| 208 |
const submitPayAddress = async () => {
|
|
|
|
| 215 |
showError('充值分组倍率不是合法的 JSON 字符串');
|
| 216 |
return;
|
| 217 |
}
|
|
|
|
| 218 |
}
|
| 219 |
+
|
| 220 |
+
const options = [
|
| 221 |
+
{ key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }
|
| 222 |
+
];
|
| 223 |
+
|
| 224 |
if (inputs.EpayId !== '') {
|
| 225 |
+
options.push({ key: 'EpayId', value: inputs.EpayId });
|
| 226 |
}
|
| 227 |
if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
|
| 228 |
+
options.push({ key: 'EpayKey', value: inputs.EpayKey });
|
| 229 |
+
}
|
| 230 |
+
if (inputs.Price !== '') {
|
| 231 |
+
options.push({ key: 'Price', value: inputs.Price.toString() });
|
| 232 |
+
}
|
| 233 |
+
if (inputs.MinTopUp !== '') {
|
| 234 |
+
options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() });
|
| 235 |
+
}
|
| 236 |
+
if (inputs.CustomCallbackAddress !== '') {
|
| 237 |
+
options.push({ key: 'CustomCallbackAddress', value: inputs.CustomCallbackAddress });
|
| 238 |
}
|
| 239 |
+
if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) {
|
| 240 |
+
options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio });
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
await updateOptions(options);
|
| 244 |
};
|
| 245 |
|
| 246 |
const submitSMTP = async () => {
|
| 247 |
+
const options = [];
|
| 248 |
+
|
| 249 |
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
| 250 |
+
options.push({ key: 'SMTPServer', value: inputs.SMTPServer });
|
| 251 |
}
|
| 252 |
if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
|
| 253 |
+
options.push({ key: 'SMTPAccount', value: inputs.SMTPAccount });
|
| 254 |
}
|
| 255 |
if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
|
| 256 |
+
options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom });
|
| 257 |
+
}
|
| 258 |
+
if (originInputs['SMTPPort'] !== inputs.SMTPPort && inputs.SMTPPort !== '') {
|
| 259 |
+
options.push({ key: 'SMTPPort', value: inputs.SMTPPort });
|
| 260 |
}
|
| 261 |
+
if (originInputs['SMTPToken'] !== inputs.SMTPToken && inputs.SMTPToken !== '') {
|
| 262 |
+
options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
|
|
|
|
|
|
|
|
|
|
| 263 |
}
|
| 264 |
+
|
| 265 |
+
if (options.length > 0) {
|
| 266 |
+
await updateOptions(options);
|
|
|
|
|
|
|
| 267 |
}
|
| 268 |
};
|
| 269 |
|
| 270 |
const submitEmailDomainWhitelist = async () => {
|
| 271 |
+
if (Array.isArray(emailDomainWhitelist)) {
|
| 272 |
+
await updateOptions([{
|
| 273 |
+
key: 'EmailDomainWhitelist',
|
| 274 |
+
value: emailDomainWhitelist.join(',')
|
| 275 |
+
}]);
|
| 276 |
+
} else {
|
| 277 |
+
showError('邮箱域名白名单格式不正确');
|
|
|
|
|
|
|
| 278 |
}
|
| 279 |
};
|
| 280 |
|
| 281 |
const submitWeChat = async () => {
|
| 282 |
+
const options = [];
|
| 283 |
+
|
| 284 |
if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
|
| 285 |
+
options.push({
|
| 286 |
+
key: 'WeChatServerAddress',
|
| 287 |
+
value: removeTrailingSlash(inputs.WeChatServerAddress)
|
| 288 |
+
});
|
| 289 |
}
|
| 290 |
+
if (originInputs['WeChatAccountQRCodeImageURL'] !== inputs.WeChatAccountQRCodeImageURL) {
|
| 291 |
+
options.push({
|
| 292 |
+
key: 'WeChatAccountQRCodeImageURL',
|
| 293 |
+
value: inputs.WeChatAccountQRCodeImageURL
|
| 294 |
+
});
|
|
|
|
|
|
|
|
|
|
| 295 |
}
|
| 296 |
+
if (originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && inputs.WeChatServerToken !== '') {
|
| 297 |
+
options.push({ key: 'WeChatServerToken', value: inputs.WeChatServerToken });
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
if (options.length > 0) {
|
| 301 |
+
await updateOptions(options);
|
| 302 |
}
|
| 303 |
};
|
| 304 |
|
| 305 |
const submitGitHubOAuth = async () => {
|
| 306 |
+
const options = [];
|
| 307 |
+
|
| 308 |
if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
|
| 309 |
+
options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId });
|
| 310 |
+
}
|
| 311 |
+
if (originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && inputs.GitHubClientSecret !== '') {
|
| 312 |
+
options.push({ key: 'GitHubClientSecret', value: inputs.GitHubClientSecret });
|
| 313 |
}
|
| 314 |
+
|
| 315 |
+
if (options.length > 0) {
|
| 316 |
+
await updateOptions(options);
|
|
|
|
|
|
|
| 317 |
}
|
| 318 |
};
|
| 319 |
|
|
|
|
| 332 |
} catch (err) {
|
| 333 |
console.error(err);
|
| 334 |
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
|
| 335 |
+
return;
|
| 336 |
}
|
| 337 |
}
|
| 338 |
|
| 339 |
+
const options = [];
|
| 340 |
+
|
| 341 |
if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) {
|
| 342 |
+
options.push({ key: 'oidc.well_known', value: inputs['oidc.well_known'] });
|
| 343 |
}
|
| 344 |
if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
|
| 345 |
+
options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] });
|
| 346 |
}
|
| 347 |
if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') {
|
| 348 |
+
options.push({ key: 'oidc.client_secret', value: inputs['oidc.client_secret'] });
|
| 349 |
}
|
| 350 |
if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) {
|
| 351 |
+
options.push({ key: 'oidc.authorization_endpoint', value: inputs['oidc.authorization_endpoint'] });
|
| 352 |
}
|
| 353 |
if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
|
| 354 |
+
options.push({ key: 'oidc.token_endpoint', value: inputs['oidc.token_endpoint'] });
|
| 355 |
}
|
| 356 |
if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) {
|
| 357 |
+
options.push({ key: 'oidc.user_info_endpoint', value: inputs['oidc.user_info_endpoint'] });
|
| 358 |
}
|
| 359 |
+
|
| 360 |
+
if (options.length > 0) {
|
| 361 |
+
await updateOptions(options);
|
| 362 |
+
}
|
| 363 |
+
};
|
| 364 |
|
| 365 |
const submitTelegramSettings = async () => {
|
| 366 |
+
const options = [
|
| 367 |
+
{ key: 'TelegramBotToken', value: inputs.TelegramBotToken },
|
| 368 |
+
{ key: 'TelegramBotName', value: inputs.TelegramBotName }
|
| 369 |
+
];
|
| 370 |
+
await updateOptions(options);
|
| 371 |
};
|
| 372 |
|
| 373 |
const submitTurnstile = async () => {
|
| 374 |
+
const options = [];
|
| 375 |
+
|
| 376 |
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
| 377 |
+
options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey });
|
| 378 |
}
|
| 379 |
+
if (originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && inputs.TurnstileSecretKey !== '') {
|
| 380 |
+
options.push({ key: 'TurnstileSecretKey', value: inputs.TurnstileSecretKey });
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
+
|
| 383 |
+
if (options.length > 0) {
|
| 384 |
+
await updateOptions(options);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
}
|
| 386 |
};
|
| 387 |
|
| 388 |
const submitLinuxDOOAuth = async () => {
|
| 389 |
+
const options = [];
|
| 390 |
+
|
| 391 |
if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) {
|
| 392 |
+
options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId });
|
| 393 |
}
|
| 394 |
+
if (originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret && inputs.LinuxDOClientSecret !== '') {
|
| 395 |
+
options.push({ key: 'LinuxDOClientSecret', value: inputs.LinuxDOClientSecret });
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
if (options.length > 0) {
|
| 399 |
+
await updateOptions(options);
|
| 400 |
}
|
| 401 |
};
|
| 402 |
|
| 403 |
+
const handleCheckboxChange = async (optionKey, event) => {
|
| 404 |
+
const value = event.target.checked;
|
| 405 |
+
|
| 406 |
+
if (optionKey === 'PasswordLoginEnabled' && !value) {
|
| 407 |
+
setShowPasswordLoginConfirmModal(true);
|
| 408 |
+
} else {
|
| 409 |
+
await updateOptions([{ key: optionKey, value }]);
|
| 410 |
+
}
|
| 411 |
+
if (optionKey === 'LinuxDOOAuthEnabled') {
|
| 412 |
+
setLinuxDOOAuthEnabled(value);
|
| 413 |
+
}
|
| 414 |
+
};
|
| 415 |
+
|
| 416 |
+
const handlePasswordLoginConfirm = async () => {
|
| 417 |
+
await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]);
|
| 418 |
+
setShowPasswordLoginConfirmModal(false);
|
| 419 |
+
};
|
| 420 |
+
|
| 421 |
return (
|
| 422 |
+
<div style={{ padding: '20px' }}>
|
| 423 |
+
{isLoaded ? (
|
| 424 |
+
<Form
|
| 425 |
+
initValues={inputs}
|
| 426 |
+
onValueChange={handleFormChange}
|
| 427 |
+
getFormApi={(api) => (formApiRef.current = api)}
|
| 428 |
+
>
|
| 429 |
+
{({ formState, values, formApi }) => (
|
| 430 |
+
<>
|
| 431 |
+
<Form.Section text='通用设置'>
|
| 432 |
+
<Form.Input
|
| 433 |
+
field='ServerAddress'
|
| 434 |
+
label='服务器地址'
|
| 435 |
+
placeholder='例如:https://yourdomain.com'
|
| 436 |
+
style={{ width: '100%' }}
|
| 437 |
+
/>
|
| 438 |
+
<Button onClick={submitServerAddress}>更新服务器地址</Button>
|
| 439 |
+
</Form.Section>
|
| 440 |
+
|
| 441 |
+
<Form.Section text='代理设置'>
|
| 442 |
+
<Text>
|
| 443 |
+
(支持{' '}
|
| 444 |
+
<a
|
| 445 |
+
href='https://github.com/Calcium-Ion/new-api-worker'
|
| 446 |
+
target='_blank'
|
| 447 |
+
rel='noreferrer'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
>
|
| 449 |
+
new-api-worker
|
| 450 |
+
</a>
|
| 451 |
+
)
|
| 452 |
+
</Text>
|
| 453 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 454 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 455 |
+
<Form.Input
|
| 456 |
+
field='WorkerUrl'
|
| 457 |
+
label='Worker地址'
|
| 458 |
+
placeholder='例如:https://workername.yourdomain.workers.dev'
|
| 459 |
+
/>
|
| 460 |
+
</Col>
|
| 461 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 462 |
+
<Form.Input
|
| 463 |
+
field='WorkerValidKey'
|
| 464 |
+
label='Worker密钥'
|
| 465 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 466 |
+
type='password'
|
| 467 |
+
/>
|
| 468 |
+
</Col>
|
| 469 |
+
</Row>
|
| 470 |
+
<Button onClick={submitWorker}>更新Worker设置</Button>
|
| 471 |
+
</Form.Section>
|
| 472 |
+
|
| 473 |
+
<Form.Section text='支付设置'>
|
| 474 |
+
<Text>
|
| 475 |
+
(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
|
| 476 |
+
</Text>
|
| 477 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 478 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 479 |
+
<Form.Input
|
| 480 |
+
field='PayAddress'
|
| 481 |
+
label='支付地址'
|
| 482 |
+
placeholder='例如:https://yourdomain.com'
|
| 483 |
+
/>
|
| 484 |
+
</Col>
|
| 485 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 486 |
+
<Form.Input
|
| 487 |
+
field='EpayId'
|
| 488 |
+
label='易支付商户ID'
|
| 489 |
+
placeholder='例如:0001'
|
| 490 |
+
/>
|
| 491 |
+
</Col>
|
| 492 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 493 |
+
<Form.Input
|
| 494 |
+
field='EpayKey'
|
| 495 |
+
label='易支付商户密钥'
|
| 496 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 497 |
+
type='password'
|
| 498 |
+
/>
|
| 499 |
+
</Col>
|
| 500 |
+
</Row>
|
| 501 |
+
<Row
|
| 502 |
+
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
| 503 |
+
style={{ marginTop: 16 }}
|
| 504 |
+
>
|
| 505 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 506 |
+
<Form.Input
|
| 507 |
+
field='CustomCallbackAddress'
|
| 508 |
+
label='回调地址'
|
| 509 |
+
placeholder='例如:https://yourdomain.com'
|
| 510 |
+
/>
|
| 511 |
+
</Col>
|
| 512 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 513 |
+
<Form.InputNumber
|
| 514 |
+
field='Price'
|
| 515 |
+
precision={2}
|
| 516 |
+
label='充值价格(x元/美金)'
|
| 517 |
+
placeholder='例如:7,就是7元/美金'
|
| 518 |
+
/>
|
| 519 |
+
</Col>
|
| 520 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 521 |
+
<Form.InputNumber
|
| 522 |
+
field='MinTopUp'
|
| 523 |
+
label='最低充值美元数量'
|
| 524 |
+
placeholder='例如:2,就是最低充值2$'
|
| 525 |
+
/>
|
| 526 |
+
</Col>
|
| 527 |
+
</Row>
|
| 528 |
+
<Form.TextArea
|
| 529 |
+
field='TopupGroupRatio'
|
| 530 |
+
label='充值分组倍率'
|
| 531 |
+
placeholder='为一个 JSON 文本,键为组名称,值为倍率'
|
| 532 |
+
autosize
|
| 533 |
+
/>
|
| 534 |
+
<Button onClick={submitPayAddress}>更新支付设置</Button>
|
| 535 |
+
</Form.Section>
|
| 536 |
+
|
| 537 |
+
<Form.Section text='配置登录注册'>
|
| 538 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 539 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 540 |
+
<Form.Checkbox
|
| 541 |
+
field='PasswordLoginEnabled'
|
| 542 |
+
noLabel
|
| 543 |
+
onChange={(e) =>
|
| 544 |
+
handleCheckboxChange('PasswordLoginEnabled', e)
|
| 545 |
+
}
|
| 546 |
+
>
|
| 547 |
+
允许通过密码进行登录
|
| 548 |
+
</Form.Checkbox>
|
| 549 |
+
<Form.Checkbox
|
| 550 |
+
field='PasswordRegisterEnabled'
|
| 551 |
+
noLabel
|
| 552 |
+
onChange={(e) =>
|
| 553 |
+
handleCheckboxChange('PasswordRegisterEnabled', e)
|
| 554 |
+
}
|
| 555 |
+
>
|
| 556 |
+
允许通过密码进行注册
|
| 557 |
+
</Form.Checkbox>
|
| 558 |
+
<Form.Checkbox
|
| 559 |
+
field='EmailVerificationEnabled'
|
| 560 |
+
noLabel
|
| 561 |
+
onChange={(e) =>
|
| 562 |
+
handleCheckboxChange('EmailVerificationEnabled', e)
|
| 563 |
+
}
|
| 564 |
+
>
|
| 565 |
+
通过密码注册时需要进行邮箱验证
|
| 566 |
+
</Form.Checkbox>
|
| 567 |
+
<Form.Checkbox
|
| 568 |
+
field='RegisterEnabled'
|
| 569 |
+
noLabel
|
| 570 |
+
onChange={(e) => handleCheckboxChange('RegisterEnabled', e)}
|
| 571 |
+
>
|
| 572 |
+
允许新用户注册
|
| 573 |
+
</Form.Checkbox>
|
| 574 |
+
<Form.Checkbox
|
| 575 |
+
field='TurnstileCheckEnabled'
|
| 576 |
+
noLabel
|
| 577 |
+
onChange={(e) =>
|
| 578 |
+
handleCheckboxChange('TurnstileCheckEnabled', e)
|
| 579 |
+
}
|
| 580 |
+
>
|
| 581 |
+
启用 Turnstile 用户校验
|
| 582 |
+
</Form.Checkbox>
|
| 583 |
+
</Col>
|
| 584 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 585 |
+
<Form.Checkbox
|
| 586 |
+
field='GitHubOAuthEnabled'
|
| 587 |
+
noLabel
|
| 588 |
+
onChange={(e) =>
|
| 589 |
+
handleCheckboxChange('GitHubOAuthEnabled', e)
|
| 590 |
+
}
|
| 591 |
+
>
|
| 592 |
+
允许通过 GitHub 账户登录 & 注册
|
| 593 |
+
</Form.Checkbox>
|
| 594 |
+
<Form.Checkbox
|
| 595 |
+
field='LinuxDOOAuthEnabled'
|
| 596 |
+
noLabel
|
| 597 |
+
onChange={(e) =>
|
| 598 |
+
handleCheckboxChange('LinuxDOOAuthEnabled', e)
|
| 599 |
+
}
|
| 600 |
+
>
|
| 601 |
+
允许通过 Linux DO 账户登录 & 注册
|
| 602 |
+
</Form.Checkbox>
|
| 603 |
+
<Form.Checkbox
|
| 604 |
+
field='WeChatAuthEnabled'
|
| 605 |
+
noLabel
|
| 606 |
+
onChange={(e) =>
|
| 607 |
+
handleCheckboxChange('WeChatAuthEnabled', e)
|
| 608 |
+
}
|
| 609 |
+
>
|
| 610 |
+
允许通过微信登录 & 注册
|
| 611 |
+
</Form.Checkbox>
|
| 612 |
+
<Form.Checkbox
|
| 613 |
+
field='TelegramOAuthEnabled'
|
| 614 |
+
noLabel
|
| 615 |
+
onChange={(e) =>
|
| 616 |
+
handleCheckboxChange('TelegramOAuthEnabled', e)
|
| 617 |
+
}
|
| 618 |
+
>
|
| 619 |
+
允许通过 Telegram 进行登录
|
| 620 |
+
</Form.Checkbox>
|
| 621 |
+
<Form.Checkbox
|
| 622 |
+
field='oidc.enabled'
|
| 623 |
+
noLabel
|
| 624 |
+
onChange={(e) => handleCheckboxChange('oidc.enabled', e)}
|
| 625 |
+
>
|
| 626 |
+
允许通过 OIDC 进行登录
|
| 627 |
+
</Form.Checkbox>
|
| 628 |
+
</Col>
|
| 629 |
+
</Row>
|
| 630 |
+
</Form.Section>
|
| 631 |
+
|
| 632 |
+
<Form.Section text='配置邮箱域名白名单'>
|
| 633 |
+
<Text>用以防止恶意用户利用临时邮箱批量注册</Text>
|
| 634 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 635 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 636 |
+
<Form.Checkbox
|
| 637 |
+
field='EmailDomainRestrictionEnabled'
|
| 638 |
+
noLabel
|
| 639 |
+
onChange={(e) =>
|
| 640 |
+
handleCheckboxChange('EmailDomainRestrictionEnabled', e)
|
| 641 |
+
}
|
| 642 |
+
>
|
| 643 |
+
启用邮箱域名白名单
|
| 644 |
+
</Form.Checkbox>
|
| 645 |
+
</Col>
|
| 646 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 647 |
+
<Form.Checkbox
|
| 648 |
+
field='EmailAliasRestrictionEnabled'
|
| 649 |
+
noLabel
|
| 650 |
+
onChange={(e) =>
|
| 651 |
+
handleCheckboxChange('EmailAliasRestrictionEnabled', e)
|
| 652 |
+
}
|
| 653 |
+
>
|
| 654 |
+
启用邮箱别名限制
|
| 655 |
+
</Form.Checkbox>
|
| 656 |
+
</Col>
|
| 657 |
+
</Row>
|
| 658 |
+
<TagInput
|
| 659 |
+
value={emailDomainWhitelist}
|
| 660 |
+
onChange={setEmailDomainWhitelist}
|
| 661 |
+
placeholder='输入域名后回车'
|
| 662 |
+
style={{ width: '100%', marginTop: 16 }}
|
| 663 |
+
/>
|
| 664 |
<Button
|
| 665 |
+
onClick={submitEmailDomainWhitelist}
|
| 666 |
+
style={{ marginTop: 10 }}
|
|
|
|
|
|
|
| 667 |
>
|
| 668 |
+
保存邮箱域名白名单设置
|
| 669 |
</Button>
|
| 670 |
+
</Form.Section>
|
| 671 |
+
|
| 672 |
+
<Form.Section text='配置 SMTP'>
|
| 673 |
+
<Text>用以支持系统的邮件发送</Text>
|
| 674 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 675 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 676 |
+
<Form.Input field='SMTPServer' label='SMTP 服务器地址' />
|
| 677 |
+
</Col>
|
| 678 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 679 |
+
<Form.Input field='SMTPPort' label='SMTP 端口' />
|
| 680 |
+
</Col>
|
| 681 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 682 |
+
<Form.Input field='SMTPAccount' label='SMTP 账户' />
|
| 683 |
+
</Col>
|
| 684 |
+
</Row>
|
| 685 |
+
<Row
|
| 686 |
+
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
| 687 |
+
style={{ marginTop: 16 }}
|
| 688 |
+
>
|
| 689 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 690 |
+
<Form.Input field='SMTPFrom' label='SMTP 发送者邮箱' />
|
| 691 |
+
</Col>
|
| 692 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 693 |
+
<Form.Input
|
| 694 |
+
field='SMTPToken'
|
| 695 |
+
label='SMTP 访问凭证'
|
| 696 |
+
type='password'
|
| 697 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 698 |
+
/>
|
| 699 |
+
</Col>
|
| 700 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 701 |
+
<Form.Checkbox
|
| 702 |
+
field='SMTPSSLEnabled'
|
| 703 |
+
noLabel
|
| 704 |
+
onChange={(e) => handleCheckboxChange('SMTPSSLEnabled', e)}
|
| 705 |
+
>
|
| 706 |
+
启用SMTP SSL
|
| 707 |
+
</Form.Checkbox>
|
| 708 |
+
</Col>
|
| 709 |
+
</Row>
|
| 710 |
+
<Button onClick={submitSMTP}>保存 SMTP 设置</Button>
|
| 711 |
+
</Form.Section>
|
| 712 |
+
|
| 713 |
+
<Form.Section text='配置 OIDC'>
|
| 714 |
+
<Text>用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP</Text>
|
| 715 |
+
<Banner
|
| 716 |
+
type='info'
|
| 717 |
+
description={`主页链接填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},重定向 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/oidc`}
|
| 718 |
+
style={{ marginBottom: 20, marginTop: 16 }}
|
| 719 |
+
/>
|
| 720 |
+
<Text>若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置</Text>
|
| 721 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 722 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 723 |
+
<Form.Input
|
| 724 |
+
field='oidc.well_known'
|
| 725 |
+
label='Well-Known URL'
|
| 726 |
+
placeholder='请输入 OIDC 的 Well-Known URL'
|
| 727 |
+
/>
|
| 728 |
+
</Col>
|
| 729 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 730 |
+
<Form.Input
|
| 731 |
+
field='oidc.client_id'
|
| 732 |
+
label='Client ID'
|
| 733 |
+
placeholder='输入 OIDC 的 Client ID'
|
| 734 |
+
/>
|
| 735 |
+
</Col>
|
| 736 |
+
</Row>
|
| 737 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 738 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 739 |
+
<Form.Input
|
| 740 |
+
field='oidc.client_secret'
|
| 741 |
+
label='Client Secret'
|
| 742 |
+
type='password'
|
| 743 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 744 |
+
/>
|
| 745 |
+
</Col>
|
| 746 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 747 |
+
<Form.Input
|
| 748 |
+
field='oidc.authorization_endpoint'
|
| 749 |
+
label='Authorization Endpoint'
|
| 750 |
+
placeholder='输入 OIDC 的 Authorization Endpoint'
|
| 751 |
+
/>
|
| 752 |
+
</Col>
|
| 753 |
+
</Row>
|
| 754 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 755 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 756 |
+
<Form.Input
|
| 757 |
+
field='oidc.token_endpoint'
|
| 758 |
+
label='Token Endpoint'
|
| 759 |
+
placeholder='输入 OIDC 的 Token Endpoint'
|
| 760 |
+
/>
|
| 761 |
+
</Col>
|
| 762 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 763 |
+
<Form.Input
|
| 764 |
+
field='oidc.user_info_endpoint'
|
| 765 |
+
label='User Info Endpoint'
|
| 766 |
+
placeholder='输入 OIDC 的 Userinfo Endpoint'
|
| 767 |
+
/>
|
| 768 |
+
</Col>
|
| 769 |
+
</Row>
|
| 770 |
+
<Button onClick={submitOIDCSettings}>保存 OIDC 设置</Button>
|
| 771 |
+
</Form.Section>
|
| 772 |
+
|
| 773 |
+
<Form.Section text='配置 GitHub OAuth App'>
|
| 774 |
+
<Text>用以支持通过 GitHub 进行登录注册</Text>
|
| 775 |
+
<Banner
|
| 776 |
+
type='info'
|
| 777 |
+
description={`Homepage URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'},Authorization callback URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/github`}
|
| 778 |
+
style={{ marginBottom: 20, marginTop: 16 }}
|
| 779 |
+
/>
|
| 780 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 781 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 782 |
+
<Form.Input field='GitHubClientId' label='GitHub Client ID' />
|
| 783 |
+
</Col>
|
| 784 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 785 |
+
<Form.Input
|
| 786 |
+
field='GitHubClientSecret'
|
| 787 |
+
label='GitHub Client Secret'
|
| 788 |
+
type='password'
|
| 789 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 790 |
+
/>
|
| 791 |
+
</Col>
|
| 792 |
+
</Row>
|
| 793 |
+
<Button onClick={submitGitHubOAuth}>
|
| 794 |
+
保存 GitHub OAuth 设置
|
| 795 |
+
</Button>
|
| 796 |
+
</Form.Section>
|
| 797 |
+
<Form.Section text='配置 Linux DO OAuth'>
|
| 798 |
+
<Text>
|
| 799 |
+
用以支持通过 Linux DO 进行登录注册
|
| 800 |
+
<a
|
| 801 |
+
href='https://connect.linux.do/'
|
| 802 |
+
target='_blank'
|
| 803 |
+
rel='noreferrer'
|
| 804 |
+
style={{ display: 'inline-block', marginLeft: 4, marginRight: 4 }}
|
| 805 |
+
>
|
| 806 |
+
点击此处
|
| 807 |
+
</a>
|
| 808 |
+
管理你的 LinuxDO OAuth App
|
| 809 |
+
</Text>
|
| 810 |
+
<Banner
|
| 811 |
+
type='info'
|
| 812 |
+
description={`回调 URL 填 ${inputs.ServerAddress ? inputs.ServerAddress : '网站地址'}/oauth/linuxdo`}
|
| 813 |
+
style={{ marginBottom: 20, marginTop: 16 }}
|
| 814 |
+
/>
|
| 815 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 816 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 817 |
+
<Form.Input
|
| 818 |
+
field='LinuxDOClientId'
|
| 819 |
+
label='Linux DO Client ID'
|
| 820 |
+
placeholder='输入你注册的 LinuxDO OAuth APP 的 ID'
|
| 821 |
+
/>
|
| 822 |
+
</Col>
|
| 823 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 824 |
+
<Form.Input
|
| 825 |
+
field='LinuxDOClientSecret'
|
| 826 |
+
label='Linux DO Client Secret'
|
| 827 |
+
type='password'
|
| 828 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 829 |
+
/>
|
| 830 |
+
</Col>
|
| 831 |
+
</Row>
|
| 832 |
+
<Button onClick={submitLinuxDOOAuth}>
|
| 833 |
+
保存 Linux DO OAuth 设置
|
| 834 |
+
</Button>
|
| 835 |
+
</Form.Section>
|
| 836 |
+
<Form.Section text='配置 WeChat Server'>
|
| 837 |
+
<Text>用以支持通过微信进行登录注册</Text>
|
| 838 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 839 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 840 |
+
<Form.Input
|
| 841 |
+
field='WeChatServerAddress'
|
| 842 |
+
label='WeChat Server 服务器地址'
|
| 843 |
+
/>
|
| 844 |
+
</Col>
|
| 845 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 846 |
+
<Form.Input
|
| 847 |
+
field='WeChatServerToken'
|
| 848 |
+
label='WeChat Server 访问凭证'
|
| 849 |
+
type='password'
|
| 850 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 851 |
+
/>
|
| 852 |
+
</Col>
|
| 853 |
+
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
|
| 854 |
+
<Form.Input
|
| 855 |
+
field='WeChatAccountQRCodeImageURL'
|
| 856 |
+
label='微信公众号二维码图片链接'
|
| 857 |
+
/>
|
| 858 |
+
</Col>
|
| 859 |
+
</Row>
|
| 860 |
+
<Button onClick={submitWeChat}>保存 WeChat Server 设置</Button>
|
| 861 |
+
</Form.Section>
|
| 862 |
+
<Form.Section text='配置 Telegram 登录'>
|
| 863 |
+
<Text>用以支持通过 Telegram 进行登录注册</Text>
|
| 864 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 865 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 866 |
+
<Form.Input
|
| 867 |
+
field='TelegramBotToken'
|
| 868 |
+
label='Telegram Bot Token'
|
| 869 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 870 |
+
type='password'
|
| 871 |
+
/>
|
| 872 |
+
</Col>
|
| 873 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 874 |
+
<Form.Input
|
| 875 |
+
field='TelegramBotName'
|
| 876 |
+
label='Telegram Bot 名称'
|
| 877 |
+
/>
|
| 878 |
+
</Col>
|
| 879 |
+
</Row>
|
| 880 |
+
<Button onClick={submitTelegramSettings}>
|
| 881 |
+
保存 Telegram 登录设置
|
| 882 |
+
</Button>
|
| 883 |
+
</Form.Section>
|
| 884 |
+
<Form.Section text='配置 Turnstile'>
|
| 885 |
+
<Text>用以支持用户校验</Text>
|
| 886 |
+
<Row gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}>
|
| 887 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 888 |
+
<Form.Input
|
| 889 |
+
field='TurnstileSiteKey'
|
| 890 |
+
label='Turnstile Site Key'
|
| 891 |
+
/>
|
| 892 |
+
</Col>
|
| 893 |
+
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
| 894 |
+
<Form.Input
|
| 895 |
+
field='TurnstileSecretKey'
|
| 896 |
+
label='Turnstile Secret Key'
|
| 897 |
+
type='password'
|
| 898 |
+
placeholder='敏感信息不会发送到前端显示'
|
| 899 |
+
/>
|
| 900 |
+
</Col>
|
| 901 |
+
</Row>
|
| 902 |
+
<Button onClick={submitTurnstile}>保存 Turnstile 设置</Button>
|
| 903 |
+
</Form.Section>
|
| 904 |
+
|
| 905 |
+
<Modal
|
| 906 |
+
title="确认取消密码登录"
|
| 907 |
+
visible={showPasswordLoginConfirmModal}
|
| 908 |
+
onOk={handlePasswordLoginConfirm}
|
| 909 |
+
onCancel={() => {
|
| 910 |
+
setShowPasswordLoginConfirmModal(false);
|
| 911 |
+
formApiRef.current.setValue('PasswordLoginEnabled', true);
|
| 912 |
+
}}
|
| 913 |
+
okText="确认"
|
| 914 |
+
cancelText="取消"
|
| 915 |
>
|
| 916 |
+
<p>您确定要取消密码登录功能吗?这可能会影响用户的登录方式。</p>
|
| 917 |
+
</Modal>
|
| 918 |
+
</>
|
| 919 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 920 |
</Form>
|
| 921 |
+
) : (
|
| 922 |
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
| 923 |
+
<Spin size="large" />
|
| 924 |
+
</div>
|
| 925 |
+
)}
|
| 926 |
+
</div>
|
| 927 |
);
|
| 928 |
};
|
| 929 |
|
| 930 |
+
export default SystemSetting;
|
web/src/context/Style/index.js
CHANGED
|
@@ -60,6 +60,9 @@ export const StyleProvider = ({ children }) => {
|
|
| 60 |
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
| 61 |
dispatch({ type: 'SET_SIDER', payload: false });
|
| 62 |
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
|
|
|
|
|
|
|
|
|
| 63 |
} else {
|
| 64 |
// Only show sidebar on non-mobile devices by default
|
| 65 |
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
|
|
|
|
| 60 |
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
| 61 |
dispatch({ type: 'SET_SIDER', payload: false });
|
| 62 |
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
| 63 |
+
} else if (pathname === '/setup') {
|
| 64 |
+
dispatch({ type: 'SET_SIDER', payload: false });
|
| 65 |
+
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
| 66 |
} else {
|
| 67 |
// Only show sidebar on non-mobile devices by default
|
| 68 |
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
|
web/src/i18n/locales/en.json
CHANGED
|
@@ -1275,6 +1275,7 @@
|
|
| 1275 |
"代理站地址": "Base URL",
|
| 1276 |
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
|
| 1277 |
"渠道额外设置": "Channel extra settings",
|
|
|
|
| 1278 |
"模型请求速率限制": "Model request rate limit",
|
| 1279 |
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
|
| 1280 |
"限制周期": "Limit period",
|
|
|
|
| 1275 |
"代理站地址": "Base URL",
|
| 1276 |
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
|
| 1277 |
"渠道额外设置": "Channel extra settings",
|
| 1278 |
+
"参数覆盖": "Parameters override",
|
| 1279 |
"模型请求速率限制": "Model request rate limit",
|
| 1280 |
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
|
| 1281 |
"限制周期": "Limit period",
|
web/src/pages/Channel/EditChannel.js
CHANGED
|
@@ -983,6 +983,23 @@ const EditChannel = (props) => {
|
|
| 983 |
</Typography.Text>
|
| 984 |
</Space>
|
| 985 |
</>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 986 |
{inputs.type === 1 && (
|
| 987 |
<>
|
| 988 |
<div style={{ marginTop: 10 }}>
|
|
|
|
| 983 |
</Typography.Text>
|
| 984 |
</Space>
|
| 985 |
</>
|
| 986 |
+
<>
|
| 987 |
+
<div style={{ marginTop: 10 }}>
|
| 988 |
+
<Typography.Text strong>
|
| 989 |
+
{t('参数覆盖')}:
|
| 990 |
+
</Typography.Text>
|
| 991 |
+
</div>
|
| 992 |
+
<TextArea
|
| 993 |
+
placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
|
| 994 |
+
name="setting"
|
| 995 |
+
onChange={(value) => {
|
| 996 |
+
handleInputChange('param_override', value);
|
| 997 |
+
}}
|
| 998 |
+
autosize
|
| 999 |
+
value={inputs.param_override}
|
| 1000 |
+
autoComplete="new-password"
|
| 1001 |
+
/>
|
| 1002 |
+
</>
|
| 1003 |
{inputs.type === 1 && (
|
| 1004 |
<>
|
| 1005 |
<div style={{ marginTop: 10 }}>
|
web/src/pages/Home/index.js
CHANGED
|
@@ -66,6 +66,10 @@ const Home = () => {
|
|
| 66 |
};
|
| 67 |
|
| 68 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
displayNotice().then();
|
| 70 |
displayHomePageContent().then();
|
| 71 |
});
|
|
|
|
| 66 |
};
|
| 67 |
|
| 68 |
useEffect(() => {
|
| 69 |
+
if (statusState.status?.setup === false) {
|
| 70 |
+
window.location.href = '/setup';
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
displayNotice().then();
|
| 74 |
displayHomePageContent().then();
|
| 75 |
});
|
web/src/pages/Setting/Operation/SettingsGeneral.js
CHANGED
|
@@ -27,9 +27,10 @@ export default function GeneralSettings(props) {
|
|
| 27 |
const refForm = useRef();
|
| 28 |
const [inputsRow, setInputsRow] = useState(inputs);
|
| 29 |
|
| 30 |
-
function
|
| 31 |
-
|
| 32 |
-
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
function onSubmit() {
|
|
@@ -98,7 +99,7 @@ export default function GeneralSettings(props) {
|
|
| 98 |
label={t('充值链接')}
|
| 99 |
initValue={''}
|
| 100 |
placeholder={t('例如发卡网站的购买链接')}
|
| 101 |
-
onChange={
|
| 102 |
showClear
|
| 103 |
/>
|
| 104 |
</Col>
|
|
@@ -108,7 +109,7 @@ export default function GeneralSettings(props) {
|
|
| 108 |
label={t('文档地址')}
|
| 109 |
initValue={''}
|
| 110 |
placeholder={t('例如 https://docs.newapi.pro')}
|
| 111 |
-
onChange={
|
| 112 |
showClear
|
| 113 |
/>
|
| 114 |
</Col>
|
|
@@ -118,7 +119,7 @@ export default function GeneralSettings(props) {
|
|
| 118 |
label={t('单位美元额度')}
|
| 119 |
initValue={''}
|
| 120 |
placeholder={t('一单位货币能兑换的额度')}
|
| 121 |
-
onChange={
|
| 122 |
showClear
|
| 123 |
onClick={() => setShowQuotaWarning(true)}
|
| 124 |
/>
|
|
@@ -129,7 +130,7 @@ export default function GeneralSettings(props) {
|
|
| 129 |
label={t('失败重试次数')}
|
| 130 |
initValue={''}
|
| 131 |
placeholder={t('失败重试次数')}
|
| 132 |
-
onChange={
|
| 133 |
showClear
|
| 134 |
/>
|
| 135 |
</Col>
|
|
@@ -142,12 +143,7 @@ export default function GeneralSettings(props) {
|
|
| 142 |
size='default'
|
| 143 |
checkedText='|'
|
| 144 |
uncheckedText='〇'
|
| 145 |
-
onChange={(
|
| 146 |
-
setInputs({
|
| 147 |
-
...inputs,
|
| 148 |
-
DisplayInCurrencyEnabled: value,
|
| 149 |
-
});
|
| 150 |
-
}}
|
| 151 |
/>
|
| 152 |
</Col>
|
| 153 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
@@ -157,12 +153,7 @@ export default function GeneralSettings(props) {
|
|
| 157 |
size='default'
|
| 158 |
checkedText='|'
|
| 159 |
uncheckedText='〇'
|
| 160 |
-
onChange={(
|
| 161 |
-
setInputs({
|
| 162 |
-
...inputs,
|
| 163 |
-
DisplayTokenStatEnabled: value,
|
| 164 |
-
})
|
| 165 |
-
}
|
| 166 |
/>
|
| 167 |
</Col>
|
| 168 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
@@ -172,12 +163,7 @@ export default function GeneralSettings(props) {
|
|
| 172 |
size='default'
|
| 173 |
checkedText='|'
|
| 174 |
uncheckedText='〇'
|
| 175 |
-
onChange={(
|
| 176 |
-
setInputs({
|
| 177 |
-
...inputs,
|
| 178 |
-
DefaultCollapseSidebar: value,
|
| 179 |
-
})
|
| 180 |
-
}
|
| 181 |
/>
|
| 182 |
</Col>
|
| 183 |
</Row>
|
|
@@ -189,12 +175,7 @@ export default function GeneralSettings(props) {
|
|
| 189 |
size='default'
|
| 190 |
checkedText='|'
|
| 191 |
uncheckedText='〇'
|
| 192 |
-
onChange={(
|
| 193 |
-
setInputs({
|
| 194 |
-
...inputs,
|
| 195 |
-
DemoSiteEnabled: value
|
| 196 |
-
})
|
| 197 |
-
}
|
| 198 |
/>
|
| 199 |
</Col>
|
| 200 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
@@ -205,12 +186,7 @@ export default function GeneralSettings(props) {
|
|
| 205 |
size='default'
|
| 206 |
checkedText='|'
|
| 207 |
uncheckedText='〇'
|
| 208 |
-
onChange={(
|
| 209 |
-
setInputs({
|
| 210 |
-
...inputs,
|
| 211 |
-
SelfUseModeEnabled: value
|
| 212 |
-
})
|
| 213 |
-
}
|
| 214 |
/>
|
| 215 |
</Col>
|
| 216 |
</Row>
|
|
|
|
| 27 |
const refForm = useRef();
|
| 28 |
const [inputsRow, setInputsRow] = useState(inputs);
|
| 29 |
|
| 30 |
+
function handleFieldChange(fieldName) {
|
| 31 |
+
return (value) => {
|
| 32 |
+
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
|
| 33 |
+
};
|
| 34 |
}
|
| 35 |
|
| 36 |
function onSubmit() {
|
|
|
|
| 99 |
label={t('充值链接')}
|
| 100 |
initValue={''}
|
| 101 |
placeholder={t('例如发卡网站的购买链接')}
|
| 102 |
+
onChange={handleFieldChange('TopUpLink')}
|
| 103 |
showClear
|
| 104 |
/>
|
| 105 |
</Col>
|
|
|
|
| 109 |
label={t('文档地址')}
|
| 110 |
initValue={''}
|
| 111 |
placeholder={t('例如 https://docs.newapi.pro')}
|
| 112 |
+
onChange={handleFieldChange('general_setting.docs_link')}
|
| 113 |
showClear
|
| 114 |
/>
|
| 115 |
</Col>
|
|
|
|
| 119 |
label={t('单位美元额度')}
|
| 120 |
initValue={''}
|
| 121 |
placeholder={t('一单位货币能兑换的额度')}
|
| 122 |
+
onChange={handleFieldChange('QuotaPerUnit')}
|
| 123 |
showClear
|
| 124 |
onClick={() => setShowQuotaWarning(true)}
|
| 125 |
/>
|
|
|
|
| 130 |
label={t('失败重试次数')}
|
| 131 |
initValue={''}
|
| 132 |
placeholder={t('失败重试次数')}
|
| 133 |
+
onChange={handleFieldChange('RetryTimes')}
|
| 134 |
showClear
|
| 135 |
/>
|
| 136 |
</Col>
|
|
|
|
| 143 |
size='default'
|
| 144 |
checkedText='|'
|
| 145 |
uncheckedText='〇'
|
| 146 |
+
onChange={handleFieldChange('DisplayInCurrencyEnabled')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
/>
|
| 148 |
</Col>
|
| 149 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
|
|
| 153 |
size='default'
|
| 154 |
checkedText='|'
|
| 155 |
uncheckedText='〇'
|
| 156 |
+
onChange={handleFieldChange('DisplayTokenStatEnabled')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
/>
|
| 158 |
</Col>
|
| 159 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
|
|
| 163 |
size='default'
|
| 164 |
checkedText='|'
|
| 165 |
uncheckedText='〇'
|
| 166 |
+
onChange={handleFieldChange('DefaultCollapseSidebar')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
/>
|
| 168 |
</Col>
|
| 169 |
</Row>
|
|
|
|
| 175 |
size='default'
|
| 176 |
checkedText='|'
|
| 177 |
uncheckedText='〇'
|
| 178 |
+
onChange={handleFieldChange('DemoSiteEnabled')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
/>
|
| 180 |
</Col>
|
| 181 |
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
|
|
|
| 186 |
size='default'
|
| 187 |
checkedText='|'
|
| 188 |
uncheckedText='〇'
|
| 189 |
+
onChange={handleFieldChange('SelfUseModeEnabled')}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
/>
|
| 191 |
</Col>
|
| 192 |
</Row>
|
web/src/pages/Setup/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useContext, useEffect, useState, useRef } from 'react';
|
| 2 |
+
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
|
| 3 |
+
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
| 4 |
+
import { StatusContext } from '../../context/Status';
|
| 5 |
+
import { marked } from 'marked';
|
| 6 |
+
import { StyleContext } from '../../context/Style/index.js';
|
| 7 |
+
import { useTranslation } from 'react-i18next';
|
| 8 |
+
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
|
| 9 |
+
|
| 10 |
+
const Setup = () => {
|
| 11 |
+
const { t, i18n } = useTranslation();
|
| 12 |
+
const [statusState] = useContext(StatusContext);
|
| 13 |
+
const [styleState, styleDispatch] = useContext(StyleContext);
|
| 14 |
+
const [loading, setLoading] = useState(false);
|
| 15 |
+
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
|
| 16 |
+
const [setupStatus, setSetupStatus] = useState({
|
| 17 |
+
status: false,
|
| 18 |
+
root_init: false,
|
| 19 |
+
database_type: ''
|
| 20 |
+
});
|
| 21 |
+
const { Text, Title } = Typography;
|
| 22 |
+
const formRef = useRef(null);
|
| 23 |
+
|
| 24 |
+
const [formData, setFormData] = useState({
|
| 25 |
+
username: '',
|
| 26 |
+
password: '',
|
| 27 |
+
confirmPassword: '',
|
| 28 |
+
usageMode: 'external'
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
fetchSetupStatus();
|
| 33 |
+
}, []);
|
| 34 |
+
|
| 35 |
+
const fetchSetupStatus = async () => {
|
| 36 |
+
try {
|
| 37 |
+
const res = await API.get('/api/setup');
|
| 38 |
+
const { success, data } = res.data;
|
| 39 |
+
if (success) {
|
| 40 |
+
setSetupStatus(data);
|
| 41 |
+
|
| 42 |
+
// If setup is already completed, redirect to home
|
| 43 |
+
if (data.status) {
|
| 44 |
+
window.location.href = '/';
|
| 45 |
+
}
|
| 46 |
+
} else {
|
| 47 |
+
showError(t('获取初始化状态失败'));
|
| 48 |
+
}
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error('Failed to fetch setup status:', error);
|
| 51 |
+
showError(t('获取初始化状态失败'));
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const handleUsageModeChange = (val) => {
|
| 56 |
+
setFormData({...formData, usageMode: val});
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
const onSubmit = () => {
|
| 60 |
+
if (!formRef.current) {
|
| 61 |
+
console.error("Form reference is null");
|
| 62 |
+
showError(t('表单引用错误,请刷新页面重试'));
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const values = formRef.current.getValues();
|
| 67 |
+
console.log("Form values:", values);
|
| 68 |
+
|
| 69 |
+
// For root_init=false, validate admin username and password
|
| 70 |
+
if (!setupStatus.root_init) {
|
| 71 |
+
if (!values.username || !values.username.trim()) {
|
| 72 |
+
showError(t('请输入管理员用户名'));
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
if (!values.password || values.password.length < 8) {
|
| 77 |
+
showError(t('密码长度至少为8个字符'));
|
| 78 |
+
return;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
if (values.password !== values.confirmPassword) {
|
| 82 |
+
showError(t('两次输入的密码不一致'));
|
| 83 |
+
return;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Prepare submission data
|
| 88 |
+
const formValues = {...values};
|
| 89 |
+
formValues.SelfUseModeEnabled = values.usageMode === 'self';
|
| 90 |
+
formValues.DemoSiteEnabled = values.usageMode === 'demo';
|
| 91 |
+
|
| 92 |
+
// Remove usageMode as it's not needed by the backend
|
| 93 |
+
delete formValues.usageMode;
|
| 94 |
+
|
| 95 |
+
console.log("Submitting data to backend:", formValues);
|
| 96 |
+
setLoading(true);
|
| 97 |
+
|
| 98 |
+
// Submit to backend
|
| 99 |
+
API.post('/api/setup', formValues)
|
| 100 |
+
.then(res => {
|
| 101 |
+
const { success, message } = res.data;
|
| 102 |
+
console.log("API response:", res.data);
|
| 103 |
+
|
| 104 |
+
if (success) {
|
| 105 |
+
showNotice(t('系统初始化成功,正在跳转...'));
|
| 106 |
+
setTimeout(() => {
|
| 107 |
+
window.location.reload();
|
| 108 |
+
}, 1500);
|
| 109 |
+
} else {
|
| 110 |
+
showError(message || t('初始化失败,请重试'));
|
| 111 |
+
}
|
| 112 |
+
})
|
| 113 |
+
.catch(error => {
|
| 114 |
+
console.error('API error:', error);
|
| 115 |
+
showError(t('系统初始化失败,请重试'));
|
| 116 |
+
setLoading(false);
|
| 117 |
+
})
|
| 118 |
+
.finally(() => {
|
| 119 |
+
// setLoading(false);
|
| 120 |
+
});
|
| 121 |
+
};
|
| 122 |
+
|
| 123 |
+
return (
|
| 124 |
+
<>
|
| 125 |
+
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
| 126 |
+
<Card>
|
| 127 |
+
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
|
| 128 |
+
|
| 129 |
+
{setupStatus.database_type === 'sqlite' && (
|
| 130 |
+
<Banner
|
| 131 |
+
type="warning"
|
| 132 |
+
icon={<IconAlertTriangle size="large" />}
|
| 133 |
+
closeIcon={null}
|
| 134 |
+
title={t('数据库警告')}
|
| 135 |
+
description={
|
| 136 |
+
<div>
|
| 137 |
+
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
|
| 138 |
+
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
|
| 139 |
+
</div>
|
| 140 |
+
}
|
| 141 |
+
style={{ marginBottom: '24px' }}
|
| 142 |
+
/>
|
| 143 |
+
)}
|
| 144 |
+
|
| 145 |
+
<Form
|
| 146 |
+
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
|
| 147 |
+
initValues={formData}
|
| 148 |
+
>
|
| 149 |
+
{setupStatus.root_init ? (
|
| 150 |
+
<Banner
|
| 151 |
+
type="info"
|
| 152 |
+
icon={<IconInfoCircle />}
|
| 153 |
+
closeIcon={null}
|
| 154 |
+
description={t('管理员账号已经初始化过,请继续设置系统参数')}
|
| 155 |
+
style={{ marginBottom: '24px' }}
|
| 156 |
+
/>
|
| 157 |
+
) : (
|
| 158 |
+
<Form.Section text={t('管理员账号')}>
|
| 159 |
+
<Form.Input
|
| 160 |
+
field="username"
|
| 161 |
+
label={t('用户名')}
|
| 162 |
+
placeholder={t('请输入管理员用户名')}
|
| 163 |
+
showClear
|
| 164 |
+
onChange={(value) => setFormData({...formData, username: value})}
|
| 165 |
+
/>
|
| 166 |
+
<Form.Input
|
| 167 |
+
field="password"
|
| 168 |
+
label={t('密码')}
|
| 169 |
+
placeholder={t('请输入管理员密码')}
|
| 170 |
+
type="password"
|
| 171 |
+
showClear
|
| 172 |
+
onChange={(value) => setFormData({...formData, password: value})}
|
| 173 |
+
/>
|
| 174 |
+
<Form.Input
|
| 175 |
+
field="confirmPassword"
|
| 176 |
+
label={t('确认密码')}
|
| 177 |
+
placeholder={t('请确认管理员密码')}
|
| 178 |
+
type="password"
|
| 179 |
+
showClear
|
| 180 |
+
onChange={(value) => setFormData({...formData, confirmPassword: value})}
|
| 181 |
+
/>
|
| 182 |
+
</Form.Section>
|
| 183 |
+
)}
|
| 184 |
+
|
| 185 |
+
<Form.Section text={
|
| 186 |
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
| 187 |
+
{t('系统设置')}
|
| 188 |
+
</div>
|
| 189 |
+
}>
|
| 190 |
+
<Form.RadioGroup
|
| 191 |
+
field="usageMode"
|
| 192 |
+
label={
|
| 193 |
+
<div style={{ display: 'flex', alignItems: 'center' }}>
|
| 194 |
+
{t('使用模式')}
|
| 195 |
+
<IconHelpCircle
|
| 196 |
+
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }}
|
| 197 |
+
onClick={(e) => {
|
| 198 |
+
// e.preventDefault();
|
| 199 |
+
// e.stopPropagation();
|
| 200 |
+
setUsageModeInfoVisible(true);
|
| 201 |
+
}}
|
| 202 |
+
/>
|
| 203 |
+
</div>
|
| 204 |
+
}
|
| 205 |
+
extraText={t('可在初始化后修改')}
|
| 206 |
+
initValue="external"
|
| 207 |
+
onChange={handleUsageModeChange}
|
| 208 |
+
>
|
| 209 |
+
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
|
| 210 |
+
<Form.Radio value="self">{t('自用模式')}</Form.Radio>
|
| 211 |
+
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
|
| 212 |
+
</Form.RadioGroup>
|
| 213 |
+
</Form.Section>
|
| 214 |
+
</Form>
|
| 215 |
+
|
| 216 |
+
<div style={{ marginTop: '24px', textAlign: 'right' }}>
|
| 217 |
+
<Button type="primary" onClick={onSubmit} loading={loading}>
|
| 218 |
+
{t('初始化系统')}
|
| 219 |
+
</Button>
|
| 220 |
+
</div>
|
| 221 |
+
</Card>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<Modal
|
| 225 |
+
title={t('使用模式说明')}
|
| 226 |
+
visible={selfUseModeInfoVisible}
|
| 227 |
+
onOk={() => setUsageModeInfoVisible(false)}
|
| 228 |
+
onCancel={() => setUsageModeInfoVisible(false)}
|
| 229 |
+
closeOnEsc={true}
|
| 230 |
+
okText={t('确定')}
|
| 231 |
+
cancelText={null}
|
| 232 |
+
>
|
| 233 |
+
<div style={{ padding: '8px 0' }}>
|
| 234 |
+
<Title heading={6}>{t('对外运营模式')}</Title>
|
| 235 |
+
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
| 236 |
+
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
| 237 |
+
</div>
|
| 238 |
+
<div style={{ padding: '8px 0' }}>
|
| 239 |
+
<Title heading={6}>{t('自用模式')}</Title>
|
| 240 |
+
<p>{t('适用于个人使用的场景。')}</p>
|
| 241 |
+
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
| 242 |
+
</div>
|
| 243 |
+
<div style={{ padding: '8px 0' }}>
|
| 244 |
+
<Title heading={6}>{t('演示站点模式')}</Title>
|
| 245 |
+
<p>{t('适用于展示系统功能的场景。')}</p>
|
| 246 |
+
</div>
|
| 247 |
+
</Modal>
|
| 248 |
+
</>
|
| 249 |
+
);
|
| 250 |
+
};
|
| 251 |
+
|
| 252 |
+
export default Setup;
|