luowuyin commited on
Commit
23048ad
·
1 Parent(s): df5a20a

25:04:03 19:20:32 v0.6.0.6

Browse files
VERSION CHANGED
@@ -1 +1 @@
1
- v0.6.0.3
 
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" // 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
  )
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
- priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
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().Enabled {
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 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
  }
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
- - 用于标识是否将思考内容`reasoning_conetnt`转换为`<think>`标签拼接到内容中返回
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), &paramOverride)
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
- err = createRootAccountIfNeed()
 
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
- if info.UserId == 1 {
44
- return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
45
- } else {
46
- return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
 
 
 
 
 
 
 
 
 
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
- if strings.HasPrefix(name, "gpt-4.5") {
393
- return 2
 
394
  }
395
- if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") {
396
- return 3
397
  }
398
- return 2
 
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 24 24'
10
  version='1.1'
11
  xmlns='http://www.w3.org/2000/svg'
12
  width='1em'
13
  height='1em'
14
  {...props}
15
  >
16
- <path
17
- d='M19.7,17.6c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.4-0.2-0.7-0.5-1c-0.1-0.1-0.3-0.2-0.4-0.2c0.6-1.8-0.3-3.6-1.3-4.9c0,0,0,0,0,0c-0.8-1.2-2-2.1-1.9-3.7c0-1.9,0.2-5.4-3.3-5.1C8.5,2.3,9.5,6,9.4,7.3c0,1.1-0.5,2.2-1.3,3.1c-0.2,0.2-0.4,0.5-0.5,0.7c-1,1.2-1.5,2.8-1.5,4.3c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.3,0.2-0.5,0.3c-0.4,0.1-0.7,0.3-0.9,0.7c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,0.2,0.1,0.4,0,0.7c-0.2,0.4-0.2,0.9,0,1.4c0.3,0.4,0.8,0.5,1.5,0.6c0.5,0,1.1,0.2,1.6,0.4l0,0c0.5,0.3,1.1,0.5,1.7,0.5c0.3,0,0.7-0.1,1-0.2c0.3-0.2,0.5-0.4,0.6-0.7c0.4,0,1-0.2,1.7-0.2c0.6,0,1.2,0.2,2,0.1c0,0.1,0,0.2,0.1,0.3c0.2,0.5,0.7,0.9,1.3,1c0.1,0,0.1,0,0.2,0c0.8-0.1,1.6-0.5,2.1-1.1l0,0c0.4-0.4,0.9-0.7,1.4-0.9c0.6-0.3,1-0.5,1.1-1C20.3,18.6,20.1,18.2,19.7,17.6z M12.8,4.8c0.6,0.1,1.1,0.6,1,1.2c0,0.3-0.1,0.6-0.3,0.9c0,0,0,0-0.1,0c-0.2-0.1-0.3-0.1-0.4-0.2c0.1-0.1,0.1-0.3,0.2-0.5c0-0.4-0.2-0.7-0.4-0.7c-0.3,0-0.5,0.3-0.5,0.7c0,0,0,0.1,0,0.1c-0.1-0.1-0.3-0.1-0.4-0.2c0,0,0-0.1,0-0.1C11.8,5.5,12.2,4.9,12.8,4.8z M12.5,6.8c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0.2c0.2,0.1,0.4,0.2,0.4,0.5c0,0.3-0.3,0.6-0.9,0.8c-0.2,0.1-0.3,0.1-0.4,0.2c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.2-0.8-0.4c-0.1-0.1-0.2-0.2-0.4-0.3C10.1,8.2,9.9,8,9.8,7.7c0-0.1,0.1-0.2,0.2-0.3c0.3-0.2,0.4-0.3,0.5-0.4l0.1-0.1c0.2-0.3,0.6-0.5,1-0.5C11.9,6.5,12.2,6.6,12.5,6.8z M10.4,5c0.4,0,0.7,0.4,0.8,1.1c0,0.1,0,0.1,0,0.2c-0.1,0-0.3,0.1-0.4,0.2c0,0,0-0.1,0-0.2c0-0.3-0.2-0.6-0.4-0.5c-0.2,0-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.4l0,0c0,0-0.1,0.1-0.2,0.1C9.9,6.7,9.7,6.4,9.7,6.1C9.7,5.5,10,5,10.4,5z M9.4,21.1c-0.7,0.3-1.6,0.2-2.2-0.2c-0.6-0.3-1.1-0.4-1.8-0.4c-0.5-0.1-1-0.1-1.1-0.3c-0.1-0.2-0.1-0.5,0.1-1c0.1-0.3,0.1-0.6,0-0.9c-0.1-0.3-0.1-0.5,0-0.8C4.5,17.2,4.7,17.1,5,17c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.4c0.3-0.4,0.5-0.6,0.8-0.6c0.6,0.1,1.1,1,1.5,1.9c0.2,0.3,0.4,0.7,0.7,1c0.4,0.5,0.9,1.2,0.9,1.6C9.9,20.6,9.7,20.9,9.4,21.1z M14.3,18.9c0,0.1,0,0.1-0.1,0.2c-1.2,0.9-2.8,1-4.1,0.3c-0.2-0.3-0.4-0.6-0.6-0.9c0.9-0.1,0.7-1.3-1.2-2.5c-2-1.3-0.6-3.7,0.1-4.8c0.1-0.1,0.1,0-0.3,0.8c-0.3,0.6-0.9,2.1-0.1,3.2c0-0.8,0.2-1.6,0.5-2.4c0.7-1.3,1.2-2.8,1.5-4.3c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0.1,0.2,0.2,0.3,0.2c0.2,0.3,0.6,0.4,0.9,0.4c0,0,0.1,0,0.1,0c0.4,0,0.8-0.1,1.1-0.4c0.1-0.1,0.2-0.2,0.4-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.4,1.3,0.8,2.5,1.4,3.6c0.4,0.8,0.7,1.6,0.9,2.5c0.3,0,0.7,0.1,1,0.3c0.8,0.4,1.1,0.7,1,1.2c-0.1,0-0.1,0-0.2,0c0-0.3-0.2-0.6-0.9-0.9c-0.7-0.3-1.3-0.3-1.5,0.4c-0.1,0-0.2,0.1-0.3,0.1c-0.8,0.4-0.8,1.5-0.9,2.6C14.5,18.2,14.4,18.5,14.3,18.9z M18.9,19.5c-0.6,0.2-1.1,0.6-1.5,1.1c-0.4,0.6-1.1,1-1.9,0.9c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.6-0.1-1.2,0.2-1.8c0.1-0.4,0.2-0.7,0.3-1.1c0.1-1.2,0.1-1.9,0.6-2.2h0c0,0.5,0.3,0.8,0.7,1c0.5,0,1-0.1,1.4-0.5c0.1,0,0.1,0,0.2,0c0.3,0,0.5,0,0.7,0.2c0.2,0.2,0.3,0.5,0.3,0.7c0,0.3,0.2,0.6,0.3,0.9c0.5,0.5,0.5,0.8,0.5,0.9C19.7,19.1,19.3,19.3,18.9,19.5z M9.9,7.5c-0.1,0-0.1,0-0.1,0.1c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0.1,0.1,0.1c0.3,0.4,0.8,0.6,1.4,0.7c0.5-0.1,1-0.2,1.5-0.6c0.2-0.1,0.4-0.2,0.6-0.3c0.1,0,0.1-0.1,0.1-0.1c0-0.1,0-0.1-0.1-0.1l0,0c-0.2,0.1-0.5,0.2-0.7,0.3c-0.4,0.3-0.9,0.5-1.4,0.5c-0.5,0-0.9-0.3-1.2-0.6C10.1,7.6,10,7.5,9.9,7.5z'
18
- fill='currentColor'
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
- 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
  } from '@douyinfe/semi-ui';
34
  import {
35
- getQuotaPerUnit,
36
- renderQuota,
37
- renderQuotaWithPrompt,
38
- stringToColor,
39
  } from '../helpers/render';
40
  import TelegramLoginButton from 'react-telegram-login';
41
  import { useTranslation } from 'react-i18next';
42
 
43
  const PersonalSetting = () => {
44
- const [userState, userDispatch] = useContext(UserContext);
45
- let navigate = useNavigate();
46
- const { t } = useTranslation();
47
 
48
- const [inputs, setInputs] = useState({
49
- wechat_verification_code: '',
50
- email_verification_code: '',
51
- email: '',
52
- self_account_deletion_confirmation: '',
53
- set_new_password: '',
54
- set_new_password_confirmation: '',
55
- });
56
- const [status, setStatus] = useState({});
57
- const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
58
- const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
59
- const [showEmailBindModal, setShowEmailBindModal] = useState(false);
60
- const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
61
- const [turnstileEnabled, setTurnstileEnabled] = useState(false);
62
- const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
63
- const [turnstileToken, setTurnstileToken] = useState('');
64
- const [loading, setLoading] = useState(false);
65
- const [disableButton, setDisableButton] = useState(false);
66
- const [countdown, setCountdown] = useState(30);
67
- const [affLink, setAffLink] = useState('');
68
- const [systemToken, setSystemToken] = useState('');
69
- const [models, setModels] = useState([]);
70
- const [openTransfer, setOpenTransfer] = useState(false);
71
- const [transferAmount, setTransferAmount] = useState(0);
72
- const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
73
- // Initialize from localStorage if available
74
- const savedState = localStorage.getItem('modelsExpanded');
75
- return savedState ? JSON.parse(savedState) : false;
76
- });
77
- const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
78
- const [notificationSettings, setNotificationSettings] = useState({
79
- warningType: 'email',
80
- warningThreshold: 100000,
81
- webhookUrl: '',
82
- webhookSecret: '',
83
- notificationEmail: ''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  });
85
- const [showWebhookDocs, setShowWebhookDocs] = useState(false);
 
 
 
86
 
87
- useEffect(() => {
88
- let status = localStorage.getItem('status');
89
- if (status) {
90
- status = JSON.parse(status);
91
- setStatus(status);
92
- if (status.turnstile_check) {
93
- setTurnstileEnabled(true);
94
- setTurnstileSiteKey(status.turnstile_site_key);
95
- }
96
- }
97
- getUserData().then((res) => {
98
- console.log(userState);
99
- });
100
- loadModels().then();
101
- getAffLink().then();
102
- setTransferAmount(getQuotaPerUnit());
103
- }, []);
104
 
105
- useEffect(() => {
106
- let countdownInterval = null;
107
- if (disableButton && countdown > 0) {
108
- countdownInterval = setInterval(() => {
109
- setCountdown(countdown - 1);
110
- }, 1000);
111
- } else if (countdown === 0) {
112
- setDisableButton(false);
113
- setCountdown(30);
114
- }
115
- return () => clearInterval(countdownInterval); // Clean up on unmount
116
- }, [disableButton, countdown]);
 
117
 
118
- useEffect(() => {
119
- if (userState?.user?.setting) {
120
- const settings = JSON.parse(userState.user.setting);
121
- setNotificationSettings({
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
- // Save models expanded state to localStorage whenever it changes
132
- useEffect(() => {
133
- localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
134
- }, [isModelsExpanded]);
135
 
136
- const handleInputChange = (name, value) => {
137
- setInputs((inputs) => ({...inputs, [name]: value}));
138
- };
 
 
 
 
 
 
 
 
139
 
140
- const generateAccessToken = async () => {
141
- const res = await API.get('/api/user/token');
142
- const {success, message, data} = res.data;
143
- if (success) {
144
- setSystemToken(data);
145
- await copy(data);
146
- showSuccess(t('令牌已重置并已复制到剪贴板'));
147
- } else {
148
- showError(message);
149
- }
150
- };
151
 
152
- const getAffLink = async () => {
153
- const res = await API.get('/api/user/aff');
154
- const {success, message, data} = res.data;
155
- if (success) {
156
- let link = `${window.location.origin}/register?aff=${data}`;
157
- setAffLink(link);
158
- } else {
159
- showError(message);
160
- }
161
- };
162
 
163
- const getUserData = async () => {
164
- let res = await API.get(`/api/user/self`);
165
- const {success, message, data} = res.data;
166
- if (success) {
167
- userDispatch({type: 'login', payload: data});
168
- } else {
169
- showError(message);
170
- }
171
- };
 
 
172
 
173
- const loadModels = async () => {
174
- let res = await API.get(`/api/user/models`);
175
- const {success, message, data} = res.data;
176
- if (success) {
177
- if (data != null) {
178
- setModels(data);
179
- }
180
- } else {
181
- showError(message);
182
- }
183
- };
184
 
185
- const handleAffLinkClick = async (e) => {
186
- e.target.select();
187
- await copy(e.target.value);
188
- showSuccess(t('邀请链接已复制到剪切板'));
189
- };
190
 
191
- const handleSystemTokenClick = async (e) => {
192
- e.target.select();
193
- await copy(e.target.value);
194
- showSuccess(t('系统令牌已复制到剪切板'));
195
- };
196
 
197
- const deleteAccount = async () => {
198
- if (inputs.self_account_deletion_confirmation !== userState.user.username) {
199
- showError(t('请输入你的账户名以确认删除!'));
200
- return;
201
- }
202
 
203
- const res = await API.delete('/api/user/self');
204
- const {success, message} = res.data;
 
 
 
 
 
 
 
 
205
 
206
- if (success) {
207
- showSuccess(t('账户已删除!'));
208
- await API.get('/api/user/logout');
209
- userDispatch({type: 'logout'});
210
- localStorage.removeItem('user');
211
- navigate('/login');
212
- } else {
213
- showError(message);
214
- }
215
- };
 
 
 
216
 
217
- const bindWeChat = async () => {
218
- if (inputs.wechat_verification_code === '') return;
219
- const res = await API.get(
220
- `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
221
- );
222
- const {success, message} = res.data;
223
- if (success) {
224
- showSuccess(t('微信账户绑定成功!'));
225
- setShowWeChatBindModal(false);
226
- } else {
227
- showError(message);
228
- }
229
- };
 
 
 
 
230
 
231
- const changePassword = async () => {
232
- if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
233
- showError(t('两次输入的密码不一致!'));
234
- return;
235
- }
236
- const res = await API.put(`/api/user/self`, {
237
- password: inputs.set_new_password,
238
- });
239
- const {success, message} = res.data;
240
- if (success) {
241
- showSuccess(t('密码修改成功!'));
242
- setShowWeChatBindModal(false);
243
- } else {
244
- showError(message);
245
- }
246
- setShowChangePasswordModal(false);
247
- };
248
 
249
- const transfer = async () => {
250
- if (transferAmount < getQuotaPerUnit()) {
251
- showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit()));
252
- return;
253
- }
254
- const res = await API.post(`/api/user/aff_transfer`, {
255
- quota: transferAmount,
256
- });
257
- const {success, message} = res.data;
258
- if (success) {
259
- showSuccess(message);
260
- setOpenTransfer(false);
261
- getUserData().then();
262
- } else {
263
- showError(message);
264
- }
265
- };
 
 
 
 
 
266
 
267
- const sendVerificationCode = async () => {
268
- if (inputs.email === '') {
269
- showError(t('请输入邮箱!'));
270
- return;
271
- }
272
- setDisableButton(true);
273
- if (turnstileEnabled && turnstileToken === '') {
274
- showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
275
- return;
276
- }
277
- setLoading(true);
278
- const res = await API.get(
279
- `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
280
- );
281
- const {success, message} = res.data;
282
- if (success) {
283
- showSuccess(t('验证码发送成功,请检查邮箱!'));
284
- } else {
285
- showError(message);
286
- }
287
- setLoading(false);
288
- };
289
 
290
- const bindEmail = async () => {
291
- if (inputs.email_verification_code === '') {
292
- showError(t('请输入邮箱验证码!'));
293
- return;
294
- }
295
- setLoading(true);
296
- const res = await API.get(
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
- const getUsername = () => {
311
- if (userState.user) {
312
- return userState.user.username;
313
- } else {
314
- return 'null';
315
- }
316
- };
317
 
318
- const handleCancel = () => {
319
- setOpenTransfer(false);
320
- };
 
 
 
 
 
321
 
322
- const copyText = async (text) => {
323
- if (await copy(text)) {
324
- showSuccess(t('已复制:') + text);
325
- } else {
326
- // setSearchKeyword(text);
327
- Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text});
328
- }
329
- };
330
 
331
- const handleNotificationSettingChange = (type, value) => {
332
- setNotificationSettings(prev => ({
333
- ...prev,
334
- [type]: value.target ? value.target.value : value // 处理 Radio 事件对象
335
- }));
336
- };
 
 
 
 
337
 
338
- const saveNotificationSettings = async () => {
339
- try {
340
- const res = await API.put('/api/user/setting', {
341
- notify_type: notificationSettings.warningType,
342
- quota_warning_threshold: parseFloat(notificationSettings.warningThreshold),
343
- webhook_url: notificationSettings.webhookUrl,
344
- webhook_secret: notificationSettings.webhookSecret,
345
- notification_email: notificationSettings.notificationEmail
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
- return (
360
 
361
- <div>
362
- <Layout>
363
- <Layout.Content>
364
- <Modal
365
- title={t('请输入要划转的数量')}
366
- visible={openTransfer}
367
- onOk={transfer}
368
- onCancel={handleCancel}
369
- maskClosable={false}
370
- size={'small'}
371
- centered={true}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  >
373
- <div style={{marginTop: 20}}>
374
- <Typography.Text>{t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)}</Typography.Text>
375
- <Input
376
- style={{marginTop: 5}}
377
- value={userState?.user?.aff_quota}
378
- disabled={true}
379
- ></Input>
380
- </div>
381
- <div style={{marginTop: 20}}>
382
- <Typography.Text>
383
- {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())}
384
- </Typography.Text>
385
- <div>
386
- <InputNumber
387
- min={0}
388
- style={{marginTop: 5}}
389
- value={transferAmount}
390
- onChange={(value) => setTransferAmount(value)}
391
- disabled={false}
392
- ></InputNumber>
393
- </div>
394
- </div>
395
- </Modal>
396
- <div>
397
- <Card
398
- title={
399
- <Card.Meta
400
- avatar={
401
- <Avatar
402
- size='default'
403
- color={stringToColor(getUsername())}
404
- style={{marginRight: 4}}
405
- >
406
- {typeof getUsername() === 'string' &&
407
- getUsername().slice(0, 1)}
408
- </Avatar>
409
- }
410
- title={<Typography.Text>{getUsername()}</Typography.Text>}
411
- description={
412
- isRoot() ? (
413
- <Tag color='red'>{t('管理员')}</Tag>
414
- ) : (
415
- <Tag color='blue'>{t('普通用户')}</Tag>
416
- )
417
- }
418
- ></Card.Meta>
419
- }
420
- headerExtraContent={
421
- <>
422
- <Space vertical align='start'>
423
- <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag>
424
- <Tag color='blue'>{userState?.user?.group}</Tag>
425
- </Space>
426
- </>
427
- }
428
- footer={
429
- <>
430
- <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
431
- <Typography.Title heading={6}>{t('可用模型')}</Typography.Title>
432
- </div>
433
- <div style={{marginTop: 10}}>
434
- {models.length <= MODELS_DISPLAY_COUNT ? (
435
- <Space wrap>
436
- {models.map((model) => (
437
- <Tag
438
- key={model}
439
- color='cyan'
440
- onClick={() => {
441
- copyText(model);
442
- }}
443
- >
444
- {model}
445
- </Tag>
446
- ))}
447
- </Space>
448
- ) : (
449
- <>
450
- <Collapsible isOpen={isModelsExpanded}>
451
- <Space wrap>
452
- {models.map((model) => (
453
- <Tag
454
- key={model}
455
- color='cyan'
456
- onClick={() => {
457
- copyText(model);
458
- }}
459
- >
460
- {model}
461
- </Tag>
462
- ))}
463
- <Tag
464
- color='blue'
465
- type="light"
466
- style={{ cursor: 'pointer' }}
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
- <Descriptions row>
504
- <Descriptions.Item itemKey={t('当前余额')}>
505
- {renderQuota(userState?.user?.quota)}
506
- </Descriptions.Item>
507
- <Descriptions.Item itemKey={t('历史消耗')}>
508
- {renderQuota(userState?.user?.used_quota)}
509
- </Descriptions.Item>
510
- <Descriptions.Item itemKey={t('请求次数')}>
511
- {userState.user?.request_count}
512
- </Descriptions.Item>
513
- </Descriptions>
514
- </Card>
515
- <Card
516
- style={{marginTop: 10}}
517
- footer={
518
- <div>
519
- <Typography.Text>{t('邀请链接')}</Typography.Text>
520
- <Input
521
- style={{marginTop: 10}}
522
- value={affLink}
523
- onClick={handleAffLinkClick}
524
- readOnly
525
- />
526
- </div>
527
- }
528
- >
529
- <Typography.Title heading={6}>{t('邀请信息')}</Typography.Title>
530
- <div style={{marginTop: 10}}>
531
- <Descriptions row>
532
- <Descriptions.Item itemKey={t('待使用收益')}>
533
- <span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
534
  {renderQuota(userState?.user?.aff_quota)}
535
  </span>
536
- <Button
537
- type={'secondary'}
538
- onClick={() => setOpenTransfer(true)}
539
- size={'small'}
540
- style={{marginLeft: 10}}
541
- >
542
- {t('划转')}
543
- </Button>
544
- </Descriptions.Item>
545
- <Descriptions.Item itemKey={t('总收益')}>
546
- {renderQuota(userState?.user?.aff_history_quota)}
547
- </Descriptions.Item>
548
- <Descriptions.Item itemKey={t('邀请人数')}>
549
- {userState?.user?.aff_count}
550
- </Descriptions.Item>
551
- </Descriptions>
552
- </div>
553
- </Card>
554
- <Card style={{marginTop: 10}}>
555
- <Typography.Title heading={6}>{t('个人信息')}</Typography.Title>
556
- <div style={{marginTop: 20}}>
557
- <Typography.Text strong>{t('邮箱')}</Typography.Text>
558
- <div
559
- style={{display: 'flex', justifyContent: 'space-between'}}
560
- >
561
- <div>
562
- <Input
563
- value={
564
- userState.user && userState.user.email !== ''
565
- ? userState.user.email
566
- : t('未绑定')
567
- }
568
- readonly={true}
569
- ></Input>
570
- </div>
571
- <div>
572
- <Button
573
- onClick={() => {
574
- setShowEmailBindModal(true);
575
- }}
576
- >
577
- {userState.user && userState.user.email !== ''
578
- ? t('修改绑定')
579
- : t('绑定邮箱')}
580
- </Button>
581
- </div>
582
- </div>
583
- </div>
584
- <div style={{marginTop: 10}}>
585
- <Typography.Text strong>{t('微信')}</Typography.Text>
586
- <div style={{display: 'flex', justifyContent: 'space-between'}}>
587
- <div>
588
- <Input
589
- value={
590
- userState.user && userState.user.wechat_id !== ''
591
- ? t('已绑定')
592
- : t('未绑定')
593
- }
594
- readonly={true}
595
- ></Input>
596
- </div>
597
- <div>
598
- <Button
599
- disabled={!status.wechat_login}
600
- onClick={() => {
601
- setShowWeChatBindModal(true);
602
- }}
603
- >
604
- {userState.user && userState.user.wechat_id !== ''
605
- ? t('修改绑定')
606
- : status.wechat_login
607
- ? t('绑定')
608
- : t('未启用')}
609
- </Button>
610
- </div>
611
- </div>
612
- </div>
613
- <div style={{marginTop: 10}}>
614
- <Typography.Text strong>{t('GitHub')}</Typography.Text>
615
- <div
616
- style={{display: 'flex', justifyContent: 'space-between'}}
617
- >
618
- <div>
619
- <Input
620
- value={
621
- userState.user && userState.user.github_id !== ''
622
- ? userState.user.github_id
623
- : t('未绑定')
624
- }
625
- readonly={true}
626
- ></Input>
627
- </div>
628
- <div>
629
- <Button
630
- onClick={() => {
631
- onGitHubOAuthClicked(status.github_client_id);
632
- }}
633
- disabled={
634
- (userState.user && userState.user.github_id !== '') ||
635
- !status.github_oauth
636
- }
637
- >
638
- {status.github_oauth ? t('绑定') : t('未启用')}
639
- </Button>
640
- </div>
641
- </div>
642
- </div>
643
- <div style={{marginTop: 10}}>
644
- <Typography.Text strong>{t('OIDC')}</Typography.Text>
645
- <div
646
- style={{display: 'flex', justifyContent: 'space-between'}}
647
- >
648
- <div>
649
- <Input
650
- value={
651
- userState.user && userState.user.oidc_id !== ''
652
- ? userState.user.oidc_id
653
- : t('未绑定')
654
- }
655
- readonly={true}
656
- ></Input>
657
- </div>
658
- <div>
659
- <Button
660
- onClick={() => {
661
- onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
662
- }}
663
- disabled={
664
- (userState.user && userState.user.oidc_id !== '') ||
665
- !status.oidc_enabled
666
- }
667
- >
668
- {status.oidc_enabled ? t('绑定') : t('未启用')}
669
- </Button>
670
- </div>
671
- </div>
672
- </div>
673
- <div style={{marginTop: 10}}>
674
- <Typography.Text strong>{t('Telegram')}</Typography.Text>
675
- <div
676
- style={{display: 'flex', justifyContent: 'space-between'}}
677
- >
678
- <div>
679
- <Input
680
- value={
681
- userState.user && userState.user.telegram_id !== ''
682
- ? userState.user.telegram_id
683
- : t('未绑定')
684
- }
685
- readonly={true}
686
- ></Input>
687
- </div>
688
- <div>
689
- {status.telegram_oauth ? (
690
- userState.user.telegram_id !== '' ? (
691
- <Button disabled={true}>{t('已绑定')}</Button>
692
- ) : (
693
- <TelegramLoginButton
694
- dataAuthUrl='/api/oauth/telegram/bind'
695
- botName={status.telegram_bot_name}
696
- />
697
- )
698
- ) : (
699
- <Button disabled={true}>{t('未启用')}</Button>
700
- )}
701
- </div>
702
- </div>
703
- </div>
704
- <div style={{marginTop: 10}}>
705
- <Typography.Text strong>{t('LinuxDO')}</Typography.Text>
706
- <div
707
- style={{display: 'flex', justifyContent: 'space-between'}}
708
- >
709
- <div>
710
- <Input
711
- value={
712
- userState.user && userState.user.linux_do_id !== ''
713
- ? userState.user.linux_do_id
714
- : t('未绑定')
715
- }
716
- readonly={true}
717
- ></Input>
718
- </div>
719
- <div>
720
- <Button
721
- onClick={() => {
722
- onLinuxDOOAuthClicked(status.linuxdo_client_id);
723
- }}
724
- disabled={
725
- (userState.user && userState.user.linux_do_id !== '') ||
726
- !status.linuxdo_oauth
727
- }
728
- >
729
- {status.linuxdo_oauth ? t('绑定') : t('未启用')}
730
- </Button>
731
- </div>
732
- </div>
733
- </div>
734
- <div style={{marginTop: 10}}>
735
- <Space>
736
- <Button onClick={generateAccessToken}>
737
- {t('生成系统访问令牌')}
738
- </Button>
739
- <Button
740
- onClick={() => {
741
- setShowChangePasswordModal(true);
742
- }}
743
- >
744
- {t('修改密码')}
745
- </Button>
746
- <Button
747
- type={'danger'}
748
- onClick={() => {
749
- setShowAccountDeleteModal(true);
750
- }}
751
- >
752
- {t('删除个人账户')}
753
- </Button>
754
- </Space>
755
 
756
- {systemToken && (
757
- <Input
758
- readOnly
759
- value={systemToken}
760
- onClick={handleSystemTokenClick}
761
- style={{marginTop: '10px'}}
762
- />
763
- )}
764
- <Modal
765
- onCancel={() => setShowWeChatBindModal(false)}
766
- visible={showWeChatBindModal}
767
- size={'small'}
768
- >
769
- <Image src={status.wechat_qrcode}/>
770
- <div style={{textAlign: 'center'}}>
771
- <p>
772
- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
773
- </p>
774
- </div>
775
- <Input
776
- placeholder='验证码'
777
- name='wechat_verification_code'
778
- value={inputs.wechat_verification_code}
779
- onChange={(v) =>
780
- handleInputChange('wechat_verification_code', v)
781
- }
782
- />
783
- <Button color='' fluid size='large' onClick={bindWeChat}>
784
- {t('绑定')}
785
- </Button>
786
- </Modal>
787
- </div>
788
- </Card>
789
- <Card style={{marginTop: 10}}>
790
- <Typography.Title heading={6}>{t('通知设置')}</Typography.Title>
791
- <div style={{marginTop: 20}}>
792
- <Typography.Text strong>{t('通知方式')}</Typography.Text>
793
- <div style={{marginTop: 10}}>
794
- <RadioGroup
795
- value={notificationSettings.warningType}
796
- onChange={value => handleNotificationSettingChange('warningType', value)}
797
- >
798
- <Radio value="email">{t('邮件通知')}</Radio>
799
- <Radio value="webhook">{t('Webhook通知')}</Radio>
800
- </RadioGroup>
801
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  </div>
803
- {notificationSettings.warningType === 'webhook' && (
804
- <>
805
- <div style={{marginTop: 20}}>
806
- <Typography.Text strong>{t('Webhook地址')}</Typography.Text>
807
- <div style={{marginTop: 10}}>
808
- <Input
809
- value={notificationSettings.webhookUrl}
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
- </pre>
839
- </Collapsible>
840
- </Typography.Text>
841
- </div>
842
- </div>
843
- <div style={{marginTop: 20}}>
844
- <Typography.Text strong>{t('接口凭证(可选)')}</Typography.Text>
845
- <div style={{marginTop: 10}}>
846
- <Input
847
- value={notificationSettings.webhookSecret}
848
- onChange={val => handleNotificationSettingChange('webhookSecret', val)}
849
- placeholder={t('请输入密钥')}
850
- />
851
- <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
852
- {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')}
853
- </Typography.Text>
854
- <Typography.Text type="secondary" style={{marginTop: 4, display: 'block'}}>
855
- {t('Authorization: Bearer your-secret-key')}
856
- </Typography.Text>
857
- </div>
858
- </div>
859
- </>
860
- )}
861
- {notificationSettings.warningType === 'email' && (
862
- <div style={{marginTop: 20}}>
863
- <Typography.Text strong>{t('通知邮箱')}</Typography.Text>
864
- <div style={{marginTop: 10}}>
865
- <Input
866
- value={notificationSettings.notificationEmail}
867
- onChange={val => handleNotificationSettingChange('notificationEmail', val)}
868
- placeholder={t('留空则使用账号绑定的邮箱')}
869
- />
870
- <Typography.Text type="secondary" style={{marginTop: 8, display: 'block'}}>
871
- {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')}
872
- </Typography.Text>
873
- </div>
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
- </Layout.Content>
1031
- </Layout>
1032
- </div>
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
- Grid,
7
- Header,
8
- Message,
9
  Modal,
10
- } from 'semantic-ui-react';
11
- import { API, removeTrailingSlash, showError, showSuccess, verifyJSON } from '../helpers';
12
-
13
- import { useTheme } from '../context/Theme';
 
 
 
 
 
 
 
 
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 theme = useTheme();
75
- const isDark = theme === 'dark';
 
 
 
 
 
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
- if (item.key === 'TopupGroupRatio') {
84
- item.value = JSON.stringify(JSON.parse(item.value), null, 2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  }
86
  newInputs[item.key] = item.value;
87
  });
88
- setInputs({
89
- ...newInputs,
90
- EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
91
- });
92
  setOriginInputs(newInputs);
93
-
94
- setEmailDomainWhitelist(
95
- newInputs.EmailDomainWhitelist.split(',').map((item) => {
96
- return { key: item, text: item, value: item };
97
- }),
98
- );
99
  } else {
100
  showError(message);
101
  }
 
102
  };
103
 
104
  useEffect(() => {
105
- getOptions().then();
106
  }, []);
107
- useEffect(() => {}, [inputs.EmailDomainWhitelist]);
108
 
109
- const updateOption = async (key, value) => {
110
  setLoading(true);
111
- switch (key) {
112
- case 'PasswordLoginEnabled':
113
- case 'PasswordRegisterEnabled':
114
- case 'EmailVerificationEnabled':
115
- case 'GitHubOAuthEnabled':
116
- case 'oidc.enabled':
117
- case 'LinuxDOOAuthEnabled':
118
- case 'WeChatAuthEnabled':
119
- case 'TelegramOAuthEnabled':
120
- case 'TurnstileCheckEnabled':
121
- case 'EmailDomainRestrictionEnabled':
122
- case 'EmailAliasRestrictionEnabled':
123
- case 'SMTPSSLEnabled':
124
- case 'RegisterEnabled':
125
- value = inputs[key] === 'true' ? 'false' : 'true';
126
- break;
127
- default:
128
- break;
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
- if (key === 'Price') {
140
- value = parseFloat(value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
- setInputs((inputs) => ({
143
- ...inputs,
144
- [key]: value,
145
- }));
146
- } else {
147
- showError(message);
 
 
 
 
148
  }
149
  setLoading(false);
150
  };
151
 
152
- const handleInputChange = async (e, { name, value }) => {
153
- if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') {
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 updateOption('ServerAddress', ServerAddress);
197
  };
198
 
199
  const submitWorker = async () => {
200
  let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
201
- await updateOption('WorkerUrl', WorkerUrl);
202
- if (inputs.WorkerValidKey !== '') {
203
- await updateOption('WorkerValidKey', inputs.WorkerValidKey);
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
- let PayAddress = removeTrailingSlash(inputs.PayAddress);
220
- await updateOption('PayAddress', PayAddress);
 
 
 
221
  if (inputs.EpayId !== '') {
222
- await updateOption('EpayId', inputs.EpayId);
223
  }
224
  if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') {
225
- await updateOption('EpayKey', inputs.EpayKey);
 
 
 
 
 
 
 
 
 
226
  }
227
- await updateOption('Price', '' + inputs.Price);
 
 
 
 
228
  };
229
 
230
  const submitSMTP = async () => {
 
 
231
  if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
232
- await updateOption('SMTPServer', inputs.SMTPServer);
233
  }
234
  if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
235
- await updateOption('SMTPAccount', inputs.SMTPAccount);
236
  }
237
  if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) {
238
- await updateOption('SMTPFrom', inputs.SMTPFrom);
 
 
 
239
  }
240
- if (
241
- originInputs['SMTPPort'] !== inputs.SMTPPort &&
242
- inputs.SMTPPort !== ''
243
- ) {
244
- await updateOption('SMTPPort', inputs.SMTPPort);
245
  }
246
- if (
247
- originInputs['SMTPToken'] !== inputs.SMTPToken &&
248
- inputs.SMTPToken !== ''
249
- ) {
250
- await updateOption('SMTPToken', inputs.SMTPToken);
251
  }
252
  };
253
 
254
  const submitEmailDomainWhitelist = async () => {
255
- if (
256
- originInputs['EmailDomainWhitelist'] !==
257
- inputs.EmailDomainWhitelist.join(',') &&
258
- inputs.SMTPToken !== ''
259
- ) {
260
- await updateOption(
261
- 'EmailDomainWhitelist',
262
- inputs.EmailDomainWhitelist.join(','),
263
- );
264
  }
265
  };
266
 
267
  const submitWeChat = async () => {
 
 
268
  if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
269
- await updateOption(
270
- 'WeChatServerAddress',
271
- removeTrailingSlash(inputs.WeChatServerAddress),
272
- );
273
  }
274
- if (
275
- originInputs['WeChatAccountQRCodeImageURL'] !==
276
- inputs.WeChatAccountQRCodeImageURL
277
- ) {
278
- await updateOption(
279
- 'WeChatAccountQRCodeImageURL',
280
- inputs.WeChatAccountQRCodeImageURL,
281
- );
282
  }
283
- if (
284
- originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
285
- inputs.WeChatServerToken !== ''
286
- ) {
287
- await updateOption('WeChatServerToken', inputs.WeChatServerToken);
 
288
  }
289
  };
290
 
291
  const submitGitHubOAuth = async () => {
 
 
292
  if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
293
- await updateOption('GitHubClientId', inputs.GitHubClientId);
 
 
 
294
  }
295
- if (
296
- originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
297
- inputs.GitHubClientSecret !== ''
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
- await updateOption('oidc.well_known', inputs['oidc.well_known']);
323
  }
324
  if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) {
325
- await updateOption('oidc.client_id', inputs['oidc.client_id']);
326
  }
327
  if (originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && inputs['oidc.client_secret'] !== '') {
328
- await updateOption('oidc.client_secret', inputs['oidc.client_secret']);
329
  }
330
  if (originInputs['oidc.authorization_endpoint'] !== inputs['oidc.authorization_endpoint']) {
331
- await updateOption('oidc.authorization_endpoint', inputs['oidc.authorization_endpoint']);
332
  }
333
  if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) {
334
- await updateOption('oidc.token_endpoint', inputs['oidc.token_endpoint']);
335
  }
336
  if (originInputs['oidc.user_info_endpoint'] !== inputs['oidc.user_info_endpoint']) {
337
- await updateOption('oidc.user_info_endpoint', inputs['oidc.user_info_endpoint']);
338
  }
339
- }
 
 
 
 
340
 
341
  const submitTelegramSettings = async () => {
342
- // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
343
- await updateOption('TelegramBotToken', inputs.TelegramBotToken);
344
- await updateOption('TelegramBotName', inputs.TelegramBotName);
 
 
345
  };
346
 
347
  const submitTurnstile = async () => {
 
 
348
  if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
349
- await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
350
  }
351
- if (
352
- originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
353
- inputs.TurnstileSecretKey !== ''
354
- ) {
355
- await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
356
  }
357
- };
358
-
359
- const submitNewRestrictedDomain = () => {
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
- await updateOption('LinuxDOClientId', inputs.LinuxDOClientId);
384
  }
385
- if (
386
- originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret &&
387
- inputs.LinuxDOClientSecret !== ''
388
- ) {
389
- await updateOption('LinuxDOClientSecret', inputs.LinuxDOClientSecret);
 
390
  }
391
  };
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  return (
394
- <Grid columns={1}>
395
- <Grid.Column>
396
- <Form loading={loading} inverted={isDark}>
397
- <Header as='h3' inverted={isDark}>
398
- 通用设置
399
- </Header>
400
- <Form.Group widths='equal'>
401
- <Form.Input
402
- label='服务器地址'
403
- placeholder='例如:https://yourdomain.com'
404
- value={inputs.ServerAddress}
405
- name='ServerAddress'
406
- onChange={handleInputChange}
407
- />
408
- </Form.Group>
409
- <Form.Button onClick={submitServerAddress}>
410
- 更新服务器地址
411
- </Form.Button>
412
- <Header as='h3' inverted={isDark}>
413
- 代理设置(支持{' '}
414
- <a
415
- href='https://github.com/Calcium-Ion/new-api-worker'
416
- target='_blank'
417
- rel='noreferrer'
418
- >
419
- new-api-worker
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
- </Button>
552
- </Modal.Actions>
553
- </Modal>
554
- )}
555
- <Form.Checkbox
556
- checked={inputs.PasswordRegisterEnabled === 'true'}
557
- label='允许通过密码进行注册'
558
- name='PasswordRegisterEnabled'
559
- onChange={handleInputChange}
560
- />
561
- <Form.Checkbox
562
- checked={inputs.EmailVerificationEnabled === 'true'}
563
- label='通过密码注册时需要进行邮箱验证'
564
- name='EmailVerificationEnabled'
565
- onChange={handleInputChange}
566
- />
567
- <Form.Checkbox
568
- checked={inputs.GitHubOAuthEnabled === 'true'}
569
- label='允许通过 GitHub 账户登录 & 注册'
570
- name='GitHubOAuthEnabled'
571
- onChange={handleInputChange}
572
- />
573
- <Form.Checkbox
574
- checked={inputs['oidc.enabled'] === 'true'}
575
- label='允许通过 OIDC 登录 & 注册'
576
- name='oidc.enabled'
577
- onChange={handleInputChange}
578
- />
579
- <Form.Checkbox
580
- checked={inputs.LinuxDOOAuthEnabled === 'true'}
581
- label='允许通过 LinuxDO 账户登录 & 注册'
582
- name='LinuxDOOAuthEnabled'
583
- onChange={handleInputChange}
584
- />
585
- <Form.Checkbox
586
- checked={inputs.WeChatAuthEnabled === 'true'}
587
- label='允许通过微信登录 & 注册'
588
- name='WeChatAuthEnabled'
589
- onChange={handleInputChange}
590
- />
591
- <Form.Checkbox
592
- checked={inputs.TelegramOAuthEnabled === 'true'}
593
- label='允许通过 Telegram 进行登录'
594
- name='TelegramOAuthEnabled'
595
- onChange={handleInputChange}
596
- />
597
- </Form.Group>
598
- <Form.Group inline>
599
- <Form.Checkbox
600
- checked={inputs.RegisterEnabled === 'true'}
601
- label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
602
- name='RegisterEnabled'
603
- onChange={handleInputChange}
604
- />
605
- <Form.Checkbox
606
- checked={inputs.TurnstileCheckEnabled === 'true'}
607
- label='启用 Turnstile 用户校验'
608
- name='TurnstileCheckEnabled'
609
- onChange={handleInputChange}
610
- />
611
- </Form.Group>
612
- <Divider />
613
- <Header as='h3' inverted={isDark}>
614
- 配置邮箱域名白名单
615
- <Header.Subheader>
616
- 用以防止恶意用户利用临时邮箱批量注册
617
- </Header.Subheader>
618
- </Header>
619
- <Form.Group widths={3}>
620
- <Form.Checkbox
621
- label='启用邮箱域名白名单'
622
- name='EmailDomainRestrictionEnabled'
623
- onChange={handleInputChange}
624
- checked={inputs.EmailDomainRestrictionEnabled === 'true'}
625
- />
626
- </Form.Group>
627
- <Form.Group widths={3}>
628
- <Form.Checkbox
629
- label='启用邮箱别名限制(例如:ab.cd@gmail.com)'
630
- name='EmailAliasRestrictionEnabled'
631
- onChange={handleInputChange}
632
- checked={inputs.EmailAliasRestrictionEnabled === 'true'}
633
- />
634
- </Form.Group>
635
- <Form.Group widths={2}>
636
- <Form.Dropdown
637
- label='允许的邮箱域名'
638
- placeholder='允许的邮箱域名'
639
- name='EmailDomainWhitelist'
640
- required
641
- fluid
642
- multiple
643
- selection
644
- onChange={handleInputChange}
645
- value={inputs.EmailDomainWhitelist}
646
- autoComplete='new-password'
647
- options={EmailDomainWhitelist}
648
- />
649
- <Form.Input
650
- label='添加新的允许的邮箱域名'
651
- action={
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  <Button
653
- type='button'
654
- onClick={() => {
655
- submitNewRestrictedDomain();
656
- }}
657
  >
658
- 填入
659
  </Button>
660
- }
661
- onKeyDown={(e) => {
662
- if (e.key === 'Enter') {
663
- submitNewRestrictedDomain();
664
- }
665
- }}
666
- autoComplete='new-password'
667
- placeholder='输入新的允许的邮箱域名'
668
- value={restrictedDomainInput}
669
- onChange={(e, { value }) => {
670
- setRestrictedDomainInput(value);
671
- }}
672
- />
673
- </Form.Group>
674
- <Form.Button onClick={submitEmailDomainWhitelist}>
675
- 保存邮箱域名白名单设置
676
- </Form.Button>
677
- <Divider />
678
- <Header as='h3' inverted={isDark}>
679
- 配置 SMTP
680
- <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
681
- </Header>
682
- <Form.Group widths={3}>
683
- <Form.Input
684
- label='SMTP 服务器地址'
685
- name='SMTPServer'
686
- onChange={handleInputChange}
687
- autoComplete='new-password'
688
- value={inputs.SMTPServer}
689
- placeholder='例如:smtp.qq.com'
690
- />
691
- <Form.Input
692
- label='SMTP 端口'
693
- name='SMTPPort'
694
- onChange={handleInputChange}
695
- autoComplete='new-password'
696
- value={inputs.SMTPPort}
697
- placeholder='默认: 587'
698
- />
699
- <Form.Input
700
- label='SMTP 账户'
701
- name='SMTPAccount'
702
- onChange={handleInputChange}
703
- autoComplete='new-password'
704
- value={inputs.SMTPAccount}
705
- placeholder='通常是邮箱地址'
706
- />
707
- </Form.Group>
708
- <Form.Group widths={3}>
709
- <Form.Input
710
- label='SMTP 发送者邮箱'
711
- name='SMTPFrom'
712
- onChange={handleInputChange}
713
- autoComplete='new-password'
714
- value={inputs.SMTPFrom}
715
- placeholder='通常和邮箱地址保持一致'
716
- />
717
- <Form.Input
718
- label='SMTP 访问凭证'
719
- name='SMTPToken'
720
- onChange={handleInputChange}
721
- type='password'
722
- autoComplete='new-password'
723
- checked={inputs.RegisterEnabled === 'true'}
724
- placeholder='敏感信息不会发送到前端显示'
725
- />
726
- </Form.Group>
727
- <Form.Group widths={3}>
728
- <Form.Checkbox
729
- label='启用SMTP SSL(465端口强制开启)'
730
- name='SMTPSSLEnabled'
731
- onChange={handleInputChange}
732
- checked={inputs.SMTPSSLEnabled === 'true'}
733
- />
734
- </Form.Group>
735
- <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
736
- <Divider />
737
- <Header as='h3' inverted={isDark}>
738
- 配置 GitHub OAuth App
739
- <Header.Subheader>
740
- 用以支持通过 GitHub 进行登录注册,
741
- <a
742
- href='https://github.com/settings/developers'
743
- target='_blank'
744
- rel='noreferrer'
745
- >
746
- 点击此处
747
- </a>
748
- 管理你的 GitHub OAuth App
749
- </Header.Subheader>
750
- </Header>
751
- <Message>
752
- Homepage URL <code>{inputs.ServerAddress}</code>
753
- ,Authorization callback URL 填{' '}
754
- <code>{`${inputs.ServerAddress}/oauth/github`}</code>
755
- </Message>
756
- <Form.Group widths={3}>
757
- <Form.Input
758
- label='GitHub Client ID'
759
- name='GitHubClientId'
760
- onChange={handleInputChange}
761
- autoComplete='new-password'
762
- value={inputs.GitHubClientId}
763
- placeholder='输入你注册的 GitHub OAuth APP 的 ID'
764
- />
765
- <Form.Input
766
- label='GitHub Client Secret'
767
- name='GitHubClientSecret'
768
- onChange={handleInputChange}
769
- type='password'
770
- autoComplete='new-password'
771
- value={inputs.GitHubClientSecret}
772
- placeholder='敏感信息不会发送到前端显示'
773
- />
774
- </Form.Group>
775
- <Form.Button onClick={submitGitHubOAuth}>
776
- 保存 GitHub OAuth 设置
777
- </Form.Button>
778
- <Divider />
779
- <Header as='h3' inverted={isDark}>
780
- 配置 WeChat Server
781
- <Header.Subheader>
782
- 用以支持通过微信进行登录注册,
783
- <a
784
- href='https://github.com/songquanpeng/wechat-server'
785
- target='_blank'
786
- rel='noreferrer'
787
- >
788
- 点击此处
789
- </a>
790
- 了解 WeChat Server
791
- </Header.Subheader>
792
- </Header>
793
- <Form.Group widths={3}>
794
- <Form.Input
795
- label='WeChat Server 服务器地址'
796
- name='WeChatServerAddress'
797
- placeholder='例如:https://yourdomain.com'
798
- onChange={handleInputChange}
799
- autoComplete='new-password'
800
- value={inputs.WeChatServerAddress}
801
- />
802
- <Form.Input
803
- label='WeChat Server 访问凭证'
804
- name='WeChatServerToken'
805
- type='password'
806
- onChange={handleInputChange}
807
- autoComplete='new-password'
808
- value={inputs.WeChatServerToken}
809
- placeholder='敏感信息不会发送到前端显示'
810
- />
811
- <Form.Input
812
- label='微信公众号二维码图片链接'
813
- name='WeChatAccountQRCodeImageURL'
814
- onChange={handleInputChange}
815
- autoComplete='new-password'
816
- value={inputs.WeChatAccountQRCodeImageURL}
817
- placeholder='输入一个图片链接'
818
- />
819
- </Form.Group>
820
- <Form.Button onClick={submitWeChat}>
821
- 保存 WeChat Server 设置
822
- </Form.Button>
823
- <Divider />
824
- <Header as='h3' inverted={isDark}>
825
- 配置 Telegram 登录
826
- </Header>
827
- <Form.Group inline>
828
- <Form.Input
829
- label='Telegram Bot Token'
830
- name='TelegramBotToken'
831
- onChange={handleInputChange}
832
- value={inputs.TelegramBotToken}
833
- placeholder='输入你的 Telegram Bot Token'
834
- />
835
- <Form.Input
836
- label='Telegram Bot 名称'
837
- name='TelegramBotName'
838
- onChange={handleInputChange}
839
- value={inputs.TelegramBotName}
840
- placeholder='输入你的 Telegram Bot 名称'
841
- />
842
- </Form.Group>
843
- <Form.Button onClick={submitTelegramSettings}>
844
- 保存 Telegram 登录设置
845
- </Form.Button>
846
- <Divider />
847
- <Header as='h3' inverted={isDark}>
848
- 配置 Turnstile
849
- <Header.Subheader>
850
- 用以支持用户校验,
851
- <a
852
- href='https://dash.cloudflare.com/'
853
- target='_blank'
854
- rel='noreferrer'
855
- >
856
- 点击此处
857
- </a>
858
- 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
859
- </Header.Subheader>
860
- </Header>
861
- <Form.Group widths={3}>
862
- <Form.Input
863
- label='Turnstile Site Key'
864
- name='TurnstileSiteKey'
865
- onChange={handleInputChange}
866
- autoComplete='new-password'
867
- value={inputs.TurnstileSiteKey}
868
- placeholder='输入你注册的 Turnstile Site Key'
869
- />
870
- <Form.Input
871
- label='Turnstile Secret Key'
872
- name='TurnstileSecretKey'
873
- onChange={handleInputChange}
874
- type='password'
875
- autoComplete='new-password'
876
- value={inputs.TurnstileSecretKey}
877
- placeholder='敏感信息不会发送到前端显示'
878
- />
879
- </Form.Group>
880
- <Form.Button onClick={submitTurnstile}>
881
- 保存 Turnstile 设置
882
- </Form.Button>
883
- <Divider />
884
- <Header as='h3' inverted={isDark}>
885
- 配置 LinuxDO OAuth App
886
- <Header.Subheader>
887
- 用以支持通过 LinuxDO 进行登录注册,
888
- <a
889
- href='https://connect.linux.do/'
890
- target='_blank'
891
- rel='noreferrer'
 
 
 
 
 
 
 
 
 
 
 
 
 
892
  >
893
- 点击此处
894
- </a>
895
- 管理你的 LinuxDO OAuth App
896
- </Header.Subheader>
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
- </Grid.Column>
989
- </Grid>
 
 
 
 
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 onChange(value, e) {
31
- const name = e.target.id;
32
- setInputs((inputs) => ({ ...inputs, [name]: value }));
 
33
  }
34
 
35
  function onSubmit() {
@@ -98,7 +99,7 @@ export default function GeneralSettings(props) {
98
  label={t('充值链接')}
99
  initValue={''}
100
  placeholder={t('例如发卡网站的购买链接')}
101
- onChange={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={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={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={onChange}
133
  showClear
134
  />
135
  </Col>
@@ -142,12 +143,7 @@ export default function GeneralSettings(props) {
142
  size='default'
143
  checkedText='|'
144
  uncheckedText='〇'
145
- onChange={(value) => {
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={(value) =>
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={(value) =>
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={(value) =>
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={(value) =>
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;