Compare commits

...

27 Commits

Author SHA1 Message Date
Calcium-Ion 3e55cbcda2 Merge pull request #925 from Calcium-Ion/setup
 feat: Implement system setup functionality
2025-04-03 19:01:45 +08:00
CaIon 07d31760da feat: Refine personal mode description in setup page for clarity 2025-04-03 19:01:16 +08:00
CaIon f33ebc8e2c feat: Implement system setup functionality 2025-04-03 18:57:15 +08:00
CaIon 1de0d9123b Merge remote-tracking branch 'origin/main' 2025-04-03 17:33:03 +08:00
CaIon f0925dc105 feat: Enhance user settings and notification options 2025-04-03 17:32:48 +08:00
Calcium-Ion d9223147f2 Merge pull request #909 from jasinliu/feature/fix-dify-thinking
feat: fix dify thinking
2025-04-03 16:23:12 +08:00
Calcium-Ion f502e49c96 Merge pull request #893 from wizcas/replace-linux-do-icon
替换登录界面的 Linux.do OAuth 图标
2025-03-31 22:38:41 +08:00
Calcium-Ion 28fffb30a4 Merge pull request #895 from Feiyuyu0503/main
docs: fix a typo
2025-03-31 22:38:25 +08:00
Calcium-Ion fbc33e9a18 Merge pull request #912 from OrdinarySF/main
fix: fixed bug where target.id was null when clicking 'x' icon
2025-03-31 22:38:08 +08:00
Calcium-Ion fd31803f9b Merge pull request #914 from JoeyLearnsToCode/main
feat: Add Parameters Override
2025-03-31 22:37:26 +08:00
Calcium-Ion 9f34ea059c Merge pull request #916 from xifan2333/fix/systemSettingsUI
 feat: Update option handling in SystemSetting
2025-03-31 22:36:14 +08:00
xifan f5b4316213 feat: Update option handling in SystemSetting
-  Add backend validation for OIDC & Telegram OAuth config
- ♻️ Refactor frontend option updates with batch processing
2025-03-31 00:46:13 +08:00
JoeyLearnsToCode f00ac5f27b feat: Add Parameters Override 2025-03-29 14:39:39 +08:00
Ordinary 9c68911a68 refactor: use handleFieldChange function on change event 2025-03-28 12:44:40 +00:00
Ordinary f8efe264f7 fix: fixed bug where target.id was null when clicking 'x' icon 2025-03-28 12:43:26 +00:00
jasinliu 111cedf795 fix dify thinking 2025-03-28 00:21:27 +08:00
1808837298@qq.com 638950c230 feat: Add new cache ratios for o3-mini and gpt-4.5-preview models 2025-03-27 18:47:50 +08:00
1808837298@qq.com 3b047b18fd update model ratio 2025-03-27 17:02:09 +08:00
1808837298@qq.com a696cf5832 feat: Enhance GetCompletionRatio function 2025-03-27 16:38:29 +08:00
1808837298@qq.com 29ef130794 update model ratio 2025-03-27 16:24:30 +08:00
feiyuyu 93087f3e9e docs: fix a typo 2025-03-22 21:28:25 +08:00
Wizcas Chen 5809b174bd replace the linuxdo icon in the login form 2025-03-22 17:16:07 +08:00
Calcium-Ion cd2c8fd5ab Merge pull request #886 from seefs001/main
fix: claude function calling type
2025-03-20 23:22:20 +08:00
Seefs f501a3e92e fix: claude function calling type 2025-03-19 22:48:49 +08:00
1808837298@qq.com 18b5122a9b fix: Adjust MaxTokens logic for non-Claude models in test request 2025-03-17 23:44:32 +08:00
1808837298@qq.com 6e17d31e92 feat: Add support for cross-region AWS model handling in awsStreamHandler 2025-03-17 23:41:00 +08:00
1808837298@qq.com 5e06085744 refactor: Improve token quota consumption logic 2025-03-17 17:52:54 +08:00
34 changed files with 5409 additions and 4308 deletions
+3
View File
@@ -0,0 +1,3 @@
package constant
var Setup = false
+6 -5
View File
@@ -1,11 +1,12 @@
package constant
var (
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
)
var (
+9 -5
View File
@@ -105,6 +105,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
request := buildTestRequest(testModel)
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
if err != nil {
return err, nil
}
adaptor.Init(info)
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
@@ -143,10 +148,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return err, nil
}
info.PromptTokens = usage.PromptTokens
priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
if err != nil {
return err, nil
}
quota := 0
if !priceData.UsePrice {
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
@@ -187,7 +189,9 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
if strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") {
testRequest.MaxCompletionTokens = 10
} else if strings.Contains(model, "thinking") {
testRequest.MaxTokens = 50
if !strings.Contains(model, "claude") {
testRequest.MaxTokens = 50
}
} else {
testRequest.MaxTokens = 10
}
+2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting"
"one-api/setting/operation_setting"
@@ -72,6 +73,7 @@ func GetStatus(c *gin.Context) {
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
"setup": constant.Setup,
},
})
return
+12 -1
View File
@@ -53,11 +53,12 @@ func UpdateOption(c *gin.Context) {
return
}
case "oidc.enabled":
if option.Value == "true" && system_setting.GetOIDCSettings().Enabled {
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret",
})
return
}
case "LinuxDOOAuthEnabled":
if option.Value == "true" && common.LinuxDOClientId == "" {
@@ -89,6 +90,15 @@ func UpdateOption(c *gin.Context) {
"success": false,
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
})
return
}
case "TelegramOAuthEnabled":
if option.Value == "true" && common.TelegramBotToken == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法启用 Telegram OAuth,请先填入 Telegram Bot Token",
})
return
}
case "GroupRatio":
@@ -100,6 +110,7 @@ func UpdateOption(c *gin.Context) {
})
return
}
}
err = model.UpdateOption(option.Key, option.Value)
if err != nil {
+167
View File
@@ -0,0 +1,167 @@
package controller
import (
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/constant"
"one-api/model"
"one-api/setting/operation_setting"
)
type Setup struct {
Status bool `json:"status"`
RootInit bool `json:"root_init"`
DatabaseType string `json:"database_type"`
}
type SetupRequest struct {
Username string `json:"username"`
Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"`
SelfUseModeEnabled bool `json:"SelfUseModeEnabled"`
DemoSiteEnabled bool `json:"DemoSiteEnabled"`
}
func GetSetup(c *gin.Context) {
setup := Setup{
Status: constant.Setup,
}
if constant.Setup {
c.JSON(200, gin.H{
"success": true,
"data": setup,
})
return
}
setup.RootInit = model.RootUserExists()
if common.UsingMySQL {
setup.DatabaseType = "mysql"
}
if common.UsingPostgreSQL {
setup.DatabaseType = "postgres"
}
if common.UsingSQLite {
setup.DatabaseType = "sqlite"
}
c.JSON(200, gin.H{
"success": true,
"data": setup,
})
}
func PostSetup(c *gin.Context) {
// Check if setup is already completed
if constant.Setup {
c.JSON(400, gin.H{
"success": false,
"message": "系统已经初始化完成",
})
return
}
// Check if root user already exists
rootExists := model.RootUserExists()
var req SetupRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(400, gin.H{
"success": false,
"message": "请求参数有误",
})
return
}
// If root doesn't exist, validate and create admin account
if !rootExists {
// Validate password
if req.Password != req.ConfirmPassword {
c.JSON(400, gin.H{
"success": false,
"message": "两次输入的密码不一致",
})
return
}
if len(req.Password) < 8 {
c.JSON(400, gin.H{
"success": false,
"message": "密码长度至少为8个字符",
})
return
}
// Create root user
hashedPassword, err := common.Password2Hash(req.Password)
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "系统错误: " + err.Error(),
})
return
}
rootUser := model.User{
Username: req.Username,
Password: hashedPassword,
Role: common.RoleRootUser,
Status: common.UserStatusEnabled,
DisplayName: "Root User",
AccessToken: nil,
Quota: 100000000,
}
err = model.DB.Create(&rootUser).Error
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "创建管理员账号失败: " + err.Error(),
})
return
}
}
// Set operation modes
operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled
operation_setting.DemoSiteEnabled = req.DemoSiteEnabled
// Save operation modes to database for persistence
err = model.UpdateOption("self_use_mode", boolToString(req.SelfUseModeEnabled))
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "保存自用模式设置失败: " + err.Error(),
})
return
}
err = model.UpdateOption("demo_site_mode", boolToString(req.DemoSiteEnabled))
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "保存演示站点模式设置失败: " + err.Error(),
})
return
}
// Update setup status
constant.Setup = true
err = model.UpdateOption("setup", "true")
if err != nil {
c.JSON(500, gin.H{
"success": false,
"message": "设置初始化状态失败: " + err.Error(),
})
return
}
c.JSON(200, gin.H{
"success": true,
"message": "系统初始化成功",
})
}
func boolToString(b bool) string {
if b {
return "true"
}
return "false"
}
+7 -5
View File
@@ -913,11 +913,12 @@ func TopUp(c *gin.Context) {
}
type UpdateUserSettingRequest struct {
QuotaWarningType string `json:"notify_type"`
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
WebhookUrl string `json:"webhook_url,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"`
QuotaWarningType string `json:"notify_type"`
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
WebhookUrl string `json:"webhook_url,omitempty"`
WebhookSecret string `json:"webhook_secret,omitempty"`
NotificationEmail string `json:"notification_email,omitempty"`
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
}
func UpdateUserSetting(c *gin.Context) {
@@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) {
settings := map[string]interface{}{
constant.UserSettingNotifyType: req.QuotaWarningType,
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
"accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel,
}
// 如果是webhook类型,添加webhook相关设置
+2 -2
View File
@@ -11,7 +11,7 @@
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
3. thinking_to_content
- 用于标识是否将思考内容`reasoning_conetnt`转换为`<think>`标签拼接到内容中返回
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回
- 类型为布尔值,设置为 true 时启用思考内容转换
--------------------------------------------------------------
@@ -30,4 +30,4 @@
--------------------------------------------------------------
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
+1
View File
@@ -212,6 +212,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
c.Set("channel_name", channel.Name)
c.Set("channel_type", channel.Type)
c.Set("channel_setting", channel.GetSetting())
c.Set("param_override", channel.GetParamOverride())
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
c.Set("channel_organization", *channel.OpenAIOrganization)
}
+12
View File
@@ -36,6 +36,7 @@ type Channel struct {
OtherInfo string `json:"other_info"`
Tag *string `json:"tag" gorm:"index"`
Setting *string `json:"setting" gorm:"type:text"`
ParamOverride *string `json:"param_override" gorm:"type:text"`
}
func (channel *Channel) GetModels() []string {
@@ -511,6 +512,17 @@ func (channel *Channel) SetSetting(setting map[string]interface{}) {
channel.Setting = common.GetPointer[string](string(settingBytes))
}
func (channel *Channel) GetParamOverride() map[string]interface{} {
paramOverride := make(map[string]interface{})
if channel.ParamOverride != nil && *channel.ParamOverride != "" {
err := json.Unmarshal([]byte(*channel.ParamOverride), &paramOverride)
if err != nil {
common.SysError("failed to unmarshal param override: " + err.Error())
}
}
return paramOverride
}
func GetChannelsByIds(ids []int) ([]*Channel, error) {
var channels []*Channel
err := DB.Where("id in (?)", ids).Find(&channels).Error
+24 -1
View File
@@ -3,6 +3,7 @@ package model
import (
"log"
"one-api/common"
"one-api/constant"
"os"
"strings"
"sync"
@@ -55,6 +56,26 @@ func createRootAccountIfNeed() error {
return nil
}
func checkSetup() {
if GetSetup() == nil {
if RootUserExists() {
common.SysLog("system is not initialized, but root user exists")
// Create setup record
setup := Setup{
Version: common.Version,
InitializedAt: time.Now().Unix(),
}
err := DB.Create(&setup).Error
if err != nil {
common.SysLog("failed to create setup record: " + err.Error())
}
constant.Setup = true
} else {
constant.Setup = false
}
}
}
func chooseDB(envName string) (*gorm.DB, error) {
defer func() {
initCol()
@@ -214,8 +235,10 @@ func migrateDB() error {
if err != nil {
return err
}
err = DB.AutoMigrate(&Setup{})
common.SysLog("database migrated")
err = createRootAccountIfNeed()
checkSetup()
//err = createRootAccountIfNeed()
return err
}
+16
View File
@@ -0,0 +1,16 @@
package model
type Setup struct {
ID uint `json:"id" gorm:"primaryKey"`
Version string `json:"version" gorm:"type:varchar(50);not null"`
InitializedAt int64 `json:"initialized_at" gorm:"type:bigint;not null"`
}
func GetSetup() *Setup {
var setup Setup
err := DB.First(&setup).Error
if err != nil {
return nil
}
return &setup
}
+9
View File
@@ -808,3 +808,12 @@ func (user *User) FillUserByLinuxDOId() error {
err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error
return err
}
func RootUserExists() bool {
var user User
err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error
if err != nil {
return false
}
return true
}
+6
View File
@@ -135,6 +135,12 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
return wrapErr(errors.Wrap(err, "awsModelID")), nil
}
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
if canCrossRegion {
awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix)
}
awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{
ModelId: aws.String(awsModelId),
Accept: aws.String("application/json"),
+3 -1
View File
@@ -70,7 +70,9 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
Description: tool.Function.Description,
}
claudeTool.InputSchema = make(map[string]interface{})
claudeTool.InputSchema["type"] = params["type"].(string)
if params["type"] != nil {
claudeTool.InputSchema["type"] = params["type"].(string)
}
claudeTool.InputSchema["properties"] = params["properties"]
claudeTool.InputSchema["required"] = params["required"]
for s, a := range params {
+6
View File
@@ -198,6 +198,12 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt
choice.Delta.SetReasoningContent(text + "\n")
}
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
if difyResponse.Answer == "<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>\n" {
difyResponse.Answer = "<think>"
} else if difyResponse.Answer == "</details>" {
difyResponse.Answer = "</think>"
}
choice.Delta.SetContentString(difyResponse.Answer)
}
response.Choices = append(response.Choices, choice)
+3
View File
@@ -76,6 +76,7 @@ type RelayInfo struct {
AudioUsage bool
ReasoningEffort string
ChannelSetting map[string]interface{}
ParamOverride map[string]interface{}
UserSetting map[string]interface{}
UserEmail string
UserQuota int
@@ -131,6 +132,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
channelType := c.GetInt("channel_type")
channelId := c.GetInt("channel_id")
channelSetting := c.GetStringMap("channel_setting")
paramOverride := c.GetStringMap("param_override")
tokenId := c.GetInt("token_id")
tokenKey := c.GetString("token_key")
@@ -168,6 +170,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
Organization: c.GetString("channel_organization"),
ChannelSetting: channelSetting,
ParamOverride: paramOverride,
RelayFormat: RelayFormatOpenAI,
ThinkingContentInfo: ThinkingContentInfo{
IsFirstThinkingContent: true,
+27 -6
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/gin-gonic/gin"
"one-api/common"
constant2 "one-api/constant"
relaycommon "one-api/relay/common"
"one-api/setting"
"one-api/setting/operation_setting"
@@ -20,6 +21,10 @@ type PriceData struct {
ShouldPreConsumedQuota int
}
func (p PriceData) ToSetting() string {
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota)
}
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false)
groupRatio := setting.GetGroupRatio(info.Group)
@@ -36,10 +41,19 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var success bool
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
if !success {
if info.UserId == 1 {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
} else {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
acceptUnsetRatio := false
if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok {
b, ok := accept.(bool)
if ok {
acceptUnsetRatio = b
}
}
if !acceptUnsetRatio {
if info.UserId == 1 {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
} else {
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
}
}
}
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
@@ -50,7 +64,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
} else {
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio)
}
return PriceData{
priceData := PriceData{
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
@@ -59,5 +74,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
CacheRatio: cacheRatio,
CacheCreationRatio: cacheCreationRatio,
ShouldPreConsumedQuota: preConsumedQuota,
}, nil
}
if common.DebugEnabled {
println(fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting()))
}
return priceData, nil
}
+26 -8
View File
@@ -109,7 +109,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
c.Set("prompt_tokens", promptTokens)
}
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens))
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(math.Max(float64(textRequest.MaxTokens), float64(textRequest.MaxCompletionTokens))))
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
}
@@ -168,6 +168,23 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
}
// apply param override
if len(relayInfo.ParamOverride) > 0 {
reqMap := make(map[string]interface{})
err = json.Unmarshal(jsonData, &reqMap)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError)
}
for key, value := range relayInfo.ParamOverride {
reqMap[key] = value
}
jsonData, err = json.Marshal(reqMap)
if err != nil {
return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError)
}
}
if common.DebugEnabled {
println("requestBody: ", string(jsonData))
}
@@ -372,17 +389,18 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
"tokenId %d, model %s pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota))
} else {
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
}
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
logModel := modelName
if strings.HasPrefix(logModel, "gpt-4-gizmo") {
logModel = "gpt-4-gizmo-*"
+2
View File
@@ -13,6 +13,8 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
apiRouter.Use(middleware.GlobalAPIRateLimit())
{
apiRouter.GET("/setup", controller.GetSetup)
apiRouter.POST("/setup", controller.PostSetup)
apiRouter.GET("/status", controller.GetStatus)
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
+16 -17
View File
@@ -243,20 +243,18 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
"tokenId %d, model %s pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota))
} else {
//if sensitiveResp != nil {
// logContent += fmt.Sprintf(",敏感词:%s", strings.Join(sensitiveResp.SensitiveWords, ", "))
//}
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
}
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio,
cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice)
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, modelName,
@@ -318,17 +316,18 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
"tokenId %d, model %s pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, relayInfo.OriginModelName, preConsumedQuota))
} else {
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
}
quotaDelta := quota - preConsumedQuota
if quotaDelta != 0 {
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
if err != nil {
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
}
}
logModel := relayInfo.OriginModelName
if extraContent != "" {
logContent += ", " + extraContent
+4
View File
@@ -14,6 +14,8 @@ var defaultCacheRatio = map[string]float64{
"o1-preview": 0.5,
"o1-mini-2024-09-12": 0.5,
"o1-mini": 0.5,
"o3-mini": 0.5,
"o3-mini-2025-01-31": 0.5,
"gpt-4o-2024-11-20": 0.5,
"gpt-4o-2024-08-06": 0.5,
"gpt-4o": 0.5,
@@ -21,6 +23,8 @@ var defaultCacheRatio = map[string]float64{
"gpt-4o-mini": 0.5,
"gpt-4o-realtime-preview": 0.5,
"gpt-4o-mini-realtime-preview": 0.5,
"gpt-4.5-preview": 0.5,
"gpt-4.5-preview-2025-02-27": 0.5,
"deepseek-chat": 0.25,
"deepseek-reasoner": 0.25,
"deepseek-coder": 0.25,
+44 -34
View File
@@ -375,6 +375,17 @@ func GetCompletionRatio(name string) float64 {
return ratio
}
}
hardCodedRatio, contain := getHardcodedCompletionModelRatio(name)
if contain {
return hardCodedRatio
}
if ratio, ok := CompletionRatio[name]; ok {
return ratio
}
return hardCodedRatio
}
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
lowercaseName := strings.ToLower(name)
if strings.HasPrefix(name, "gpt-4-gizmo") {
name = "gpt-4-gizmo-*"
@@ -385,87 +396,86 @@ func GetCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
if strings.HasPrefix(name, "gpt-4o") {
if name == "gpt-4o-2024-05-13" {
return 3
return 3, true
}
return 4
return 4, true
}
if strings.HasPrefix(name, "gpt-4.5") {
return 2
// gpt-4.5-preview匹配
if strings.HasPrefix(name, "gpt-4.5-preview") {
return 2, true
}
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") {
return 3
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "gpt-4-1106") || strings.HasSuffix(name, "gpt-4-1105") {
return 3, true
}
return 2
// 没有特殊标记的 gpt-4 模型默认倍率为 2
return 2, false
}
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
return 4
return 4, true
}
if name == "chatgpt-4o-latest" {
return 3
return 3, true
}
if strings.Contains(name, "claude-instant-1") {
return 3
return 3, true
} else if strings.Contains(name, "claude-2") {
return 3
return 3, true
} else if strings.Contains(name, "claude-3") {
return 5
return 5, true
}
if strings.HasPrefix(name, "gpt-3.5") {
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
// https://openai.com/blog/new-embedding-models-and-api-updates
// Updated GPT-3.5 Turbo model and lower pricing
return 3
return 3, true
}
if strings.HasSuffix(name, "1106") {
return 2
return 2, true
}
return 4.0 / 3.0
return 4.0 / 3.0, true
}
if strings.HasPrefix(name, "mistral-") {
return 3
return 3, true
}
if strings.HasPrefix(name, "gemini-") {
return 4
return 4, true
}
if strings.HasPrefix(name, "command") {
switch name {
case "command-r":
return 3
return 3, true
case "command-r-plus":
return 5
return 5, true
case "command-r-08-2024":
return 4
return 4, true
case "command-r-plus-08-2024":
return 4
return 4, true
default:
return 4
return 4, true
}
}
// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
return 4
return 4, true
}
if strings.HasPrefix(name, "ERNIE-Speed-") {
return 2
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
return 2
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Character") {
return 2
return 2, true
} else if strings.HasPrefix(name, "ERNIE-Functions") {
return 2
return 2, true
}
switch name {
case "llama2-70b-4096":
return 0.8 / 0.64
return 0.8 / 0.64, true
case "llama3-8b-8192":
return 2
return 2, true
case "llama3-70b-8192":
return 0.79 / 0.59
return 0.79 / 0.59, true
}
if ratio, ok := CompletionRatio[name]; ok {
return ratio
}
return 1
return 1, false
}
func GetAudioRatio(name string) float64 {
+2929 -2390
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -25,6 +25,7 @@ import Task from "./pages/Task/index.js";
import Playground from './pages/Playground/Playground.js';
import OAuth2Callback from "./components/OAuth2Callback.js";
import PersonalSetting from './components/PersonalSetting.js';
import Setup from './pages/Setup/index.js';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
@@ -44,6 +45,14 @@ function App() {
</Suspense>
}
/>
<Route
path='/setup'
element={
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
<Setup />
</Suspense>
}
/>
<Route
path='/channel'
element={
+16 -6
View File
@@ -6,17 +6,27 @@ const LinuxDoIcon = (props) => {
return (
<svg
className='icon'
viewBox='0 0 24 24'
viewBox='0 0 16 16'
version='1.1'
xmlns='http://www.w3.org/2000/svg'
width='1em'
height='1em'
{...props}
>
<path
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'
fill='currentColor'
/>
<g id='linuxdo_icon' data-name='linuxdo_icon'>
<path
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'
fill='#EFEFEF'
/>
<path
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'
fill='#FEB005'
/>
<path
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'
fill='#1D1D1F'
/>
</g>
</svg>
);
}
@@ -24,4 +34,4 @@ const LinuxDoIcon = (props) => {
return <Icon svg={<CustomIcon />} />;
};
export default LinuxDoIcon;
export default LinuxDoIcon;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -60,6 +60,9 @@ export const StyleProvider = ({ children }) => {
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else if (pathname === '/setup') {
dispatch({ type: 'SET_SIDER', payload: false });
dispatch({ type: 'SET_INNER_PADDING', payload: false });
} else {
// Only show sidebar on non-mobile devices by default
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
+1
View File
@@ -1275,6 +1275,7 @@
"代理站地址": "Base URL",
"对于官方渠道,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",
"渠道额外设置": "Channel extra settings",
"参数覆盖": "Parameters override",
"模型请求速率限制": "Model request rate limit",
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
"限制周期": "Limit period",
+17
View File
@@ -983,6 +983,23 @@ const EditChannel = (props) => {
</Typography.Text>
</Space>
</>
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
{t('参数覆盖')}
</Typography.Text>
</div>
<TextArea
placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
name="setting"
onChange={(value) => {
handleInputChange('param_override', value);
}}
autosize
value={inputs.param_override}
autoComplete="new-password"
/>
</>
{inputs.type === 1 && (
<>
<div style={{ marginTop: 10 }}>
+4
View File
@@ -66,6 +66,10 @@ const Home = () => {
};
useEffect(() => {
if (statusState.status?.setup === false) {
window.location.href = '/setup';
return;
}
displayNotice().then();
displayHomePageContent().then();
});
@@ -27,9 +27,10 @@ export default function GeneralSettings(props) {
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
function onChange(value, e) {
const name = e.target.id;
setInputs((inputs) => ({ ...inputs, [name]: value }));
function handleFieldChange(fieldName) {
return (value) => {
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
};
}
function onSubmit() {
@@ -98,7 +99,7 @@ export default function GeneralSettings(props) {
label={t('充值链接')}
initValue={''}
placeholder={t('例如发卡网站的购买链接')}
onChange={onChange}
onChange={handleFieldChange('TopUpLink')}
showClear
/>
</Col>
@@ -108,7 +109,7 @@ export default function GeneralSettings(props) {
label={t('文档地址')}
initValue={''}
placeholder={t('例如 https://docs.newapi.pro')}
onChange={onChange}
onChange={handleFieldChange('general_setting.docs_link')}
showClear
/>
</Col>
@@ -118,7 +119,7 @@ export default function GeneralSettings(props) {
label={t('单位美元额度')}
initValue={''}
placeholder={t('一单位货币能兑换的额度')}
onChange={onChange}
onChange={handleFieldChange('QuotaPerUnit')}
showClear
onClick={() => setShowQuotaWarning(true)}
/>
@@ -129,7 +130,7 @@ export default function GeneralSettings(props) {
label={t('失败重试次数')}
initValue={''}
placeholder={t('失败重试次数')}
onChange={onChange}
onChange={handleFieldChange('RetryTimes')}
showClear
/>
</Col>
@@ -142,12 +143,7 @@ export default function GeneralSettings(props) {
size='default'
checkedText=''
uncheckedText=''
onChange={(value) => {
setInputs({
...inputs,
DisplayInCurrencyEnabled: value,
});
}}
onChange={handleFieldChange('DisplayInCurrencyEnabled')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
@@ -157,12 +153,7 @@ export default function GeneralSettings(props) {
size='default'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
DisplayTokenStatEnabled: value,
})
}
onChange={handleFieldChange('DisplayTokenStatEnabled')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
@@ -172,12 +163,7 @@ export default function GeneralSettings(props) {
size='default'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
DefaultCollapseSidebar: value,
})
}
onChange={handleFieldChange('DefaultCollapseSidebar')}
/>
</Col>
</Row>
@@ -189,12 +175,7 @@ export default function GeneralSettings(props) {
size='default'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
DemoSiteEnabled: value
})
}
onChange={handleFieldChange('DemoSiteEnabled')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
@@ -205,12 +186,7 @@ export default function GeneralSettings(props) {
size='default'
checkedText=''
uncheckedText=''
onChange={(value) =>
setInputs({
...inputs,
SelfUseModeEnabled: value
})
}
onChange={handleFieldChange('SelfUseModeEnabled')}
/>
</Col>
</Row>
+252
View File
@@ -0,0 +1,252 @@
import React, { useContext, useEffect, useState, useRef } from 'react';
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
import { API, showError, showNotice, timestamp2string } from '../../helpers';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
import { StyleContext } from '../../context/Style/index.js';
import { useTranslation } from 'react-i18next';
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
const Setup = () => {
const { t, i18n } = useTranslation();
const [statusState] = useContext(StatusContext);
const [styleState, styleDispatch] = useContext(StyleContext);
const [loading, setLoading] = useState(false);
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
const [setupStatus, setSetupStatus] = useState({
status: false,
root_init: false,
database_type: ''
});
const { Text, Title } = Typography;
const formRef = useRef(null);
const [formData, setFormData] = useState({
username: '',
password: '',
confirmPassword: '',
usageMode: 'external'
});
useEffect(() => {
fetchSetupStatus();
}, []);
const fetchSetupStatus = async () => {
try {
const res = await API.get('/api/setup');
const { success, data } = res.data;
if (success) {
setSetupStatus(data);
// If setup is already completed, redirect to home
if (data.status) {
window.location.href = '/';
}
} else {
showError(t('获取初始化状态失败'));
}
} catch (error) {
console.error('Failed to fetch setup status:', error);
showError(t('获取初始化状态失败'));
}
};
const handleUsageModeChange = (val) => {
setFormData({...formData, usageMode: val});
};
const onSubmit = () => {
if (!formRef.current) {
console.error("Form reference is null");
showError(t('表单引用错误,请刷新页面重试'));
return;
}
const values = formRef.current.getValues();
console.log("Form values:", values);
// For root_init=false, validate admin username and password
if (!setupStatus.root_init) {
if (!values.username || !values.username.trim()) {
showError(t('请输入管理员用户名'));
return;
}
if (!values.password || values.password.length < 8) {
showError(t('密码长度至少为8个字符'));
return;
}
if (values.password !== values.confirmPassword) {
showError(t('两次输入的密码不一致'));
return;
}
}
// Prepare submission data
const formValues = {...values};
formValues.SelfUseModeEnabled = values.usageMode === 'self';
formValues.DemoSiteEnabled = values.usageMode === 'demo';
// Remove usageMode as it's not needed by the backend
delete formValues.usageMode;
console.log("Submitting data to backend:", formValues);
setLoading(true);
// Submit to backend
API.post('/api/setup', formValues)
.then(res => {
const { success, message } = res.data;
console.log("API response:", res.data);
if (success) {
showNotice(t('系统初始化成功,正在跳转...'));
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showError(message || t('初始化失败,请重试'));
}
})
.catch(error => {
console.error('API error:', error);
showError(t('系统初始化失败,请重试'));
setLoading(false);
})
.finally(() => {
// setLoading(false);
});
};
return (
<>
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
<Card>
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
{setupStatus.database_type === 'sqlite' && (
<Banner
type="warning"
icon={<IconAlertTriangle size="large" />}
closeIcon={null}
title={t('数据库警告')}
description={
<div>
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
</div>
}
style={{ marginBottom: '24px' }}
/>
)}
<Form
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
initValues={formData}
>
{setupStatus.root_init ? (
<Banner
type="info"
icon={<IconInfoCircle />}
closeIcon={null}
description={t('管理员账号已经初始化过,请继续设置系统参数')}
style={{ marginBottom: '24px' }}
/>
) : (
<Form.Section text={t('管理员账号')}>
<Form.Input
field="username"
label={t('用户名')}
placeholder={t('请输入管理员用户名')}
showClear
onChange={(value) => setFormData({...formData, username: value})}
/>
<Form.Input
field="password"
label={t('密码')}
placeholder={t('请输入管理员密码')}
type="password"
showClear
onChange={(value) => setFormData({...formData, password: value})}
/>
<Form.Input
field="confirmPassword"
label={t('确认密码')}
placeholder={t('请确认管理员密码')}
type="password"
showClear
onChange={(value) => setFormData({...formData, confirmPassword: value})}
/>
</Form.Section>
)}
<Form.Section text={
<div style={{ display: 'flex', alignItems: 'center' }}>
{t('系统设置')}
</div>
}>
<Form.RadioGroup
field="usageMode"
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
{t('使用模式')}
<IconHelpCircle
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }}
onClick={(e) => {
// e.preventDefault();
// e.stopPropagation();
setUsageModeInfoVisible(true);
}}
/>
</div>
}
extraText={t('可在初始化后修改')}
initValue="external"
onChange={handleUsageModeChange}
>
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
<Form.Radio value="self">{t('自用模式')}</Form.Radio>
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
</Form.RadioGroup>
</Form.Section>
</Form>
<div style={{ marginTop: '24px', textAlign: 'right' }}>
<Button type="primary" onClick={onSubmit} loading={loading}>
{t('初始化系统')}
</Button>
</div>
</Card>
</div>
<Modal
title={t('使用模式说明')}
visible={selfUseModeInfoVisible}
onOk={() => setUsageModeInfoVisible(false)}
onCancel={() => setUsageModeInfoVisible(false)}
closeOnEsc={true}
okText={t('确定')}
cancelText={null}
>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('对外运营模式')}</Title>
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
</div>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('自用模式')}</Title>
<p>{t('适用于个人使用的场景。')}</p>
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
</div>
<div style={{ padding: '8px 0' }}>
<Title heading={6}>{t('演示站点模式')}</Title>
<p>{t('适用于展示系统功能的场景。')}</p>
</div>
</Modal>
</>
);
};
export default Setup;