diff --git a/common/config/config.go b/common/config/config.go index 11da0b96..43f56862 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -35,6 +35,7 @@ var PasswordLoginEnabled = true var PasswordRegisterEnabled = true var EmailVerificationEnabled = false var GitHubOAuthEnabled = false +var OidcEnabled = false var WeChatAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true @@ -70,6 +71,13 @@ var GitHubClientSecret = "" var LarkClientId = "" var LarkClientSecret = "" +var OidcClientId = "" +var OidcClientSecret = "" +var OidcWellKnown = "" +var OidcAuthorizationEndpoint = "" +var OidcTokenEndpoint = "" +var OidcUserinfoEndpoint = "" + var WeChatServerAddress = "" var WeChatServerToken = "" var WeChatAccountQRCodeImageURL = "" diff --git a/common/gin.go b/common/gin.go index 549d3279..815b4ee5 100644 --- a/common/gin.go +++ b/common/gin.go @@ -31,15 +31,15 @@ func UnmarshalBodyReusable(c *gin.Context, v any) error { contentType := c.Request.Header.Get("Content-Type") if strings.HasPrefix(contentType, "application/json") { err = json.Unmarshal(requestBody, &v) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) } else { - // skip for now - // TODO: someday non json request have variant model, we will need to implementation this + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + err = c.ShouldBind(&v) } if err != nil { return err } // Reset request body - c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) return nil } diff --git a/controller/auth/oidc.go b/controller/auth/oidc.go new file mode 100644 index 00000000..7b4ad4b9 --- /dev/null +++ b/controller/auth/oidc.go @@ -0,0 +1,225 @@ +package auth + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/controller" + "github.com/songquanpeng/one-api/model" + "net/http" + "strconv" + "time" +) + +type OidcResponse struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +type OidcUser struct { + OpenID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Picture string `json:"picture"` +} + +func getOidcUserInfoByCode(code string) (*OidcUser, error) { + if code == "" { + return nil, errors.New("无效的参数") + } + values := map[string]string{ + "client_id": config.OidcClientId, + "client_secret": config.OidcClientSecret, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": fmt.Sprintf("%s/oauth/oidc", config.ServerAddress), + } + jsonData, err := json.Marshal(values) + if err != nil { + return nil, err + } + req, err := http.NewRequest("POST", config.OidcTokenEndpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + client := http.Client{ + Timeout: 5 * time.Second, + } + res, err := client.Do(req) + if err != nil { + logger.SysLog(err.Error()) + return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!") + } + defer res.Body.Close() + var oidcResponse OidcResponse + err = json.NewDecoder(res.Body).Decode(&oidcResponse) + if err != nil { + return nil, err + } + req, err = http.NewRequest("GET", config.OidcUserinfoEndpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+oidcResponse.AccessToken) + res2, err := client.Do(req) + if err != nil { + logger.SysLog(err.Error()) + return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!") + } + var oidcUser OidcUser + err = json.NewDecoder(res2.Body).Decode(&oidcUser) + if err != nil { + return nil, err + } + return &oidcUser, nil +} + +func OidcAuth(c *gin.Context) { + session := sessions.Default(c) + state := c.Query("state") + if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "state is empty or not same", + }) + return + } + username := session.Get("username") + if username != nil { + OidcBind(c) + return + } + if !config.OidcEnabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员未开启通过 OIDC 登录以及注册", + }) + return + } + code := c.Query("code") + oidcUser, err := getOidcUserInfoByCode(code) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user := model.User{ + OidcId: oidcUser.OpenID, + } + if model.IsOidcIdAlreadyTaken(user.OidcId) { + err := user.FillUserByOidcId() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { + if config.RegisterEnabled { + user.Email = oidcUser.Email + if oidcUser.PreferredUsername != "" { + user.Username = oidcUser.PreferredUsername + } else { + user.Username = "oidc_" + strconv.Itoa(model.GetMaxUserId()+1) + } + if oidcUser.Name != "" { + user.DisplayName = oidcUser.Name + } else { + user.DisplayName = "OIDC User" + } + err := user.Insert(0) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员关闭了新用户注册", + }) + return + } + } + + if user.Status != model.UserStatusEnabled { + c.JSON(http.StatusOK, gin.H{ + "message": "用户已被封禁", + "success": false, + }) + return + } + controller.SetupLogin(&user, c) +} + +func OidcBind(c *gin.Context) { + if !config.OidcEnabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员未开启通过 OIDC 登录以及注册", + }) + return + } + code := c.Query("code") + oidcUser, err := getOidcUserInfoByCode(code) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user := model.User{ + OidcId: oidcUser.OpenID, + } + if model.IsOidcIdAlreadyTaken(user.OidcId) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "该 OIDC 账户已被绑定", + }) + return + } + session := sessions.Default(c) + id := session.Get("id") + // id := c.GetInt("id") // critical bug! + user.Id = id.(int) + err = user.FillUserById() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user.OidcId = oidcUser.OpenID + err = user.Update(false) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "bind", + }) + return +} diff --git a/controller/billing.go b/controller/billing.go index 0d03e4c1..e837157f 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -17,9 +17,11 @@ func GetSubscription(c *gin.Context) { if config.DisplayTokenStatEnabled { tokenId := c.GetInt(ctxkey.TokenId) token, err = model.GetTokenById(tokenId) - expiredTime = token.ExpiredTime - remainQuota = token.RemainQuota - usedQuota = token.UsedQuota + if err == nil { + expiredTime = token.ExpiredTime + remainQuota = token.RemainQuota + usedQuota = token.UsedQuota + } } else { userId := c.GetInt(ctxkey.Id) remainQuota, err = model.GetUserQuota(userId) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 53592744..a6ffaafe 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -81,6 +81,26 @@ type APGC2DGPTUsageResponse struct { TotalUsed float64 `json:"total_used"` } +type SiliconFlowUsageResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Status bool `json:"status"` + Data struct { + ID string `json:"id"` + Name string `json:"name"` + Image string `json:"image"` + Email string `json:"email"` + IsAdmin bool `json:"isAdmin"` + Balance string `json:"balance"` + Status string `json:"status"` + Introduction string `json:"introduction"` + Role string `json:"role"` + ChargeBalance string `json:"chargeBalance"` + TotalBalance string `json:"totalBalance"` + Category string `json:"category"` + } `json:"data"` +} + // GetAuthHeader get auth header func GetAuthHeader(token string) http.Header { h := http.Header{} @@ -203,6 +223,28 @@ func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) { return response.TotalAvailable, nil } +func updateChannelSiliconFlowBalance(channel *model.Channel) (float64, error) { + url := "https://api.siliconflow.cn/v1/user/info" + body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) + if err != nil { + return 0, err + } + response := SiliconFlowUsageResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return 0, err + } + if response.Code != 20000 { + return 0, fmt.Errorf("code: %d, message: %s", response.Code, response.Message) + } + balance, err := strconv.ParseFloat(response.Data.Balance, 64) + if err != nil { + return 0, err + } + channel.UpdateBalance(balance) + return balance, nil +} + func updateChannelBalance(channel *model.Channel) (float64, error) { baseURL := channeltype.ChannelBaseURLs[channel.Type] if channel.GetBaseURL() == "" { @@ -227,6 +269,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { return updateChannelAPI2GPTBalance(channel) case channeltype.AIGC2D: return updateChannelAIGC2DBalance(channel) + case channeltype.SiliconFlow: + return updateChannelSiliconFlowBalance(channel) default: return 0, errors.New("尚未实现") } diff --git a/controller/misc.go b/controller/misc.go index 2928b8fb..ae900870 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -18,24 +18,30 @@ func GetStatus(c *gin.Context) { "success": true, "message": "", "data": gin.H{ - "version": common.Version, - "start_time": common.StartTime, - "email_verification": config.EmailVerificationEnabled, - "github_oauth": config.GitHubOAuthEnabled, - "github_client_id": config.GitHubClientId, - "lark_client_id": config.LarkClientId, - "system_name": config.SystemName, - "logo": config.Logo, - "footer_html": config.Footer, - "wechat_qrcode": config.WeChatAccountQRCodeImageURL, - "wechat_login": config.WeChatAuthEnabled, - "server_address": config.ServerAddress, - "turnstile_check": config.TurnstileCheckEnabled, - "turnstile_site_key": config.TurnstileSiteKey, - "top_up_link": config.TopUpLink, - "chat_link": config.ChatLink, - "quota_per_unit": config.QuotaPerUnit, - "display_in_currency": config.DisplayInCurrencyEnabled, + "version": common.Version, + "start_time": common.StartTime, + "email_verification": config.EmailVerificationEnabled, + "github_oauth": config.GitHubOAuthEnabled, + "github_client_id": config.GitHubClientId, + "lark_client_id": config.LarkClientId, + "system_name": config.SystemName, + "logo": config.Logo, + "footer_html": config.Footer, + "wechat_qrcode": config.WeChatAccountQRCodeImageURL, + "wechat_login": config.WeChatAuthEnabled, + "server_address": config.ServerAddress, + "turnstile_check": config.TurnstileCheckEnabled, + "turnstile_site_key": config.TurnstileSiteKey, + "top_up_link": config.TopUpLink, + "chat_link": config.ChatLink, + "quota_per_unit": config.QuotaPerUnit, + "display_in_currency": config.DisplayInCurrencyEnabled, + "oidc": config.OidcEnabled, + "oidc_client_id": config.OidcClientId, + "oidc_well_known": config.OidcWellKnown, + "oidc_authorization_endpoint": config.OidcAuthorizationEndpoint, + "oidc_token_endpoint": config.OidcTokenEndpoint, + "oidc_userinfo_endpoint": config.OidcUserinfoEndpoint, }, }) return diff --git a/middleware/distributor.go b/middleware/distributor.go index 0c4b04c3..e2f75110 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -12,7 +12,7 @@ import ( ) type ModelRequest struct { - Model string `json:"model"` + Model string `json:"model" form:"model"` } func Distribute() func(c *gin.Context) { diff --git a/model/log.go b/model/log.go index 6fba776a..58fdd513 100644 --- a/model/log.go +++ b/model/log.go @@ -3,6 +3,7 @@ package model import ( "context" "fmt" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" @@ -152,7 +153,11 @@ func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) { } func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (quota int64) { - tx := LOG_DB.Table("logs").Select("ifnull(sum(quota),0)") + ifnull := "ifnull" + if common.UsingPostgreSQL { + ifnull = "COALESCE" + } + tx := LOG_DB.Table("logs").Select(fmt.Sprintf("%s(sum(quota),0)", ifnull)) if username != "" { tx = tx.Where("username = ?", username) } @@ -176,7 +181,11 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa } func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) { - tx := LOG_DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)") + ifnull := "ifnull" + if common.UsingPostgreSQL { + ifnull = "COALESCE" + } + tx := LOG_DB.Table("logs").Select(fmt.Sprintf("%s(sum(prompt_tokens),0) + %s(sum(completion_tokens),0)", ifnull, ifnull)) if username != "" { tx = tx.Where("username = ?", username) } diff --git a/model/option.go b/model/option.go index bed8d4c3..8fd30aee 100644 --- a/model/option.go +++ b/model/option.go @@ -28,6 +28,7 @@ func InitOptionMap() { config.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(config.PasswordRegisterEnabled) config.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(config.EmailVerificationEnabled) config.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(config.GitHubOAuthEnabled) + config.OptionMap["OidcEnabled"] = strconv.FormatBool(config.OidcEnabled) config.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(config.WeChatAuthEnabled) config.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(config.TurnstileCheckEnabled) config.OptionMap["RegisterEnabled"] = strconv.FormatBool(config.RegisterEnabled) @@ -130,6 +131,8 @@ func updateOptionMap(key string, value string) (err error) { config.EmailVerificationEnabled = boolValue case "GitHubOAuthEnabled": config.GitHubOAuthEnabled = boolValue + case "OidcEnabled": + config.OidcEnabled = boolValue case "WeChatAuthEnabled": config.WeChatAuthEnabled = boolValue case "TurnstileCheckEnabled": @@ -176,6 +179,18 @@ func updateOptionMap(key string, value string) (err error) { config.LarkClientId = value case "LarkClientSecret": config.LarkClientSecret = value + case "OidcClientId": + config.OidcClientId = value + case "OidcClientSecret": + config.OidcClientSecret = value + case "OidcWellKnown": + config.OidcWellKnown = value + case "OidcAuthorizationEndpoint": + config.OidcAuthorizationEndpoint = value + case "OidcTokenEndpoint": + config.OidcTokenEndpoint = value + case "OidcUserinfoEndpoint": + config.OidcUserinfoEndpoint = value case "Footer": config.Footer = value case "SystemName": diff --git a/model/token.go b/model/token.go index 96e6b491..91e72a82 100644 --- a/model/token.go +++ b/model/token.go @@ -30,7 +30,7 @@ type Token struct { RemainQuota int64 `json:"remain_quota" gorm:"bigint;default:0"` UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota - Models *string `json:"models" gorm:"default:''"` // allowed models + Models *string `json:"models" gorm:"type:text"` // allowed models Subnet *string `json:"subnet" gorm:"default:''"` // allowed subnet } @@ -121,30 +121,40 @@ func GetTokenById(id int) (*Token, error) { return &token, err } -func (token *Token) Insert() error { +func (t *Token) Insert() error { var err error - err = DB.Create(token).Error + err = DB.Create(t).Error return err } // Update Make sure your token's fields is completed, because this will update non-zero values -func (token *Token) Update() error { +func (t *Token) Update() error { var err error - err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models", "subnet").Updates(token).Error + err = DB.Model(t).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models", "subnet").Updates(t).Error return err } -func (token *Token) SelectUpdate() error { +func (t *Token) SelectUpdate() error { // This can update zero values - return DB.Model(token).Select("accessed_time", "status").Updates(token).Error + return DB.Model(t).Select("accessed_time", "status").Updates(t).Error } -func (token *Token) Delete() error { +func (t *Token) Delete() error { var err error - err = DB.Delete(token).Error + err = DB.Delete(t).Error return err } +func (t *Token) GetModels() string { + if t == nil { + return "" + } + if t.Models == nil { + return "" + } + return *t.Models +} + func DeleteTokenById(id int, userId int) (err error) { // Why we need userId here? In case user want to delete other's token. if id == 0 || userId == 0 { @@ -254,14 +264,14 @@ func PreConsumeTokenQuota(tokenId int, quota int64) (err error) { func PostConsumeTokenQuota(tokenId int, quota int64) (err error) { token, err := GetTokenById(tokenId) + if err != nil { + return err + } if quota > 0 { err = DecreaseUserQuota(token.UserId, quota) } else { err = IncreaseUserQuota(token.UserId, -quota) } - if err != nil { - return err - } if !token.UnlimitedQuota { if quota > 0 { err = DecreaseTokenQuota(tokenId, quota) diff --git a/model/user.go b/model/user.go index 924d72f9..a964a0d7 100644 --- a/model/user.go +++ b/model/user.go @@ -39,6 +39,7 @@ type User struct { GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` LarkId string `json:"lark_id" gorm:"column:lark_id;index"` + OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"` VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management Quota int64 `json:"quota" gorm:"bigint;default:0"` @@ -245,6 +246,14 @@ func (user *User) FillUserByLarkId() error { return nil } +func (user *User) FillUserByOidcId() error { + if user.OidcId == "" { + return errors.New("oidc id 为空!") + } + DB.Where(User{OidcId: user.OidcId}).First(user) + return nil +} + func (user *User) FillUserByWeChatId() error { if user.WeChatId == "" { return errors.New("WeChat id 为空!") @@ -277,6 +286,10 @@ func IsLarkIdAlreadyTaken(githubId string) bool { return DB.Where("lark_id = ?", githubId).Find(&User{}).RowsAffected == 1 } +func IsOidcIdAlreadyTaken(oidcId string) bool { + return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1 +} + func IsUsernameAlreadyTaken(username string) bool { return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 } diff --git a/monitor/manage.go b/monitor/manage.go index 946e78af..44c13612 100644 --- a/monitor/manage.go +++ b/monitor/manage.go @@ -1,10 +1,11 @@ package monitor import ( - "github.com/songquanpeng/one-api/common/config" - "github.com/songquanpeng/one-api/relay/model" "net/http" "strings" + + "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/relay/model" ) func ShouldDisableChannel(err *model.Error, statusCode int) bool { @@ -18,31 +19,23 @@ func ShouldDisableChannel(err *model.Error, statusCode int) bool { return true } switch err.Type { - case "insufficient_quota": - return true - // https://docs.anthropic.com/claude/reference/errors - case "authentication_error": - return true - case "permission_error": - return true - case "forbidden": + case "insufficient_quota", "authentication_error", "permission_error", "forbidden": return true } if err.Code == "invalid_api_key" || err.Code == "account_deactivated" { return true } - if strings.HasPrefix(err.Message, "Your credit balance is too low") { // anthropic - return true - } else if strings.HasPrefix(err.Message, "This organization has been disabled.") { - return true - } - //if strings.Contains(err.Message, "quota") { - // return true - //} - if strings.Contains(err.Message, "credit") { - return true - } - if strings.Contains(err.Message, "balance") { + + lowerMessage := strings.ToLower(err.Message) + if strings.Contains(lowerMessage, "your access was terminated") || + strings.Contains(lowerMessage, "violation of our policies") || + strings.Contains(lowerMessage, "your credit balance is too low") || + strings.Contains(lowerMessage, "organization has been disabled") || + strings.Contains(lowerMessage, "credit") || + strings.Contains(lowerMessage, "balance") || + strings.Contains(lowerMessage, "permission denied") || + strings.Contains(lowerMessage, "organization has been restricted") || // groq + strings.Contains(lowerMessage, "已欠费") { return true } return false diff --git a/relay/adaptor/ali/main.go b/relay/adaptor/ali/main.go index f9039dbe..ec5848ce 100644 --- a/relay/adaptor/ali/main.go +++ b/relay/adaptor/ali/main.go @@ -3,6 +3,7 @@ package ali import ( "bufio" "encoding/json" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/render" "io" "net/http" @@ -59,7 +60,7 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest { return &EmbeddingRequest{ - Model: "text-embedding-v1", + Model: request.Model, Input: struct { Texts []string `json:"texts"` }{ @@ -102,8 +103,9 @@ func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStat StatusCode: resp.StatusCode, }, nil } - + requestModel := c.GetString(ctxkey.RequestModel) fullTextResponse := embeddingResponseAli2OpenAI(&aliResponse) + fullTextResponse.Model = requestModel jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/relay/adaptor/openai/constants.go b/relay/adaptor/openai/constants.go index 156a50e7..aacdba1a 100644 --- a/relay/adaptor/openai/constants.go +++ b/relay/adaptor/openai/constants.go @@ -8,6 +8,8 @@ var ModelList = []string{ "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", "gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4o", "gpt-4o-2024-05-13", + "gpt-4o-2024-08-06", + "chatgpt-4o-latest", "gpt-4o-mini", "gpt-4o-mini-2024-07-18", "gpt-4-vision-preview", "text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large", diff --git a/relay/adaptor/openai/main.go b/relay/adaptor/openai/main.go index 9ee547b3..97080738 100644 --- a/relay/adaptor/openai/main.go +++ b/relay/adaptor/openai/main.go @@ -55,8 +55,8 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E render.StringData(c, data) // if error happened, pass the data to client continue // just ignore the error } - if len(streamResponse.Choices) == 0 { - // but for empty choice, we should not pass it to client, this is for azure + if len(streamResponse.Choices) == 0 && streamResponse.Usage == nil { + // but for empty choice and no usage, we should not pass it to client, this is for azure continue // just ignore empty choice } render.StringData(c, data) diff --git a/relay/adaptor/stepfun/constants.go b/relay/adaptor/stepfun/constants.go index a82e562b..6a2346ca 100644 --- a/relay/adaptor/stepfun/constants.go +++ b/relay/adaptor/stepfun/constants.go @@ -1,7 +1,13 @@ package stepfun var ModelList = []string{ + "step-1-8k", "step-1-32k", + "step-1-128k", + "step-1-256k", + "step-1-flash", + "step-2-16k", + "step-1v-8k", "step-1v-32k", - "step-1-200k", + "step-1x-medium", } diff --git a/relay/adaptor/tencent/constants.go b/relay/adaptor/tencent/constants.go index be415a94..e8631e5f 100644 --- a/relay/adaptor/tencent/constants.go +++ b/relay/adaptor/tencent/constants.go @@ -5,4 +5,5 @@ var ModelList = []string{ "hunyuan-standard", "hunyuan-standard-256K", "hunyuan-pro", + "hunyuan-vision", } diff --git a/relay/adaptor/xunfei/constants.go b/relay/adaptor/xunfei/constants.go index 12a56210..c2992c98 100644 --- a/relay/adaptor/xunfei/constants.go +++ b/relay/adaptor/xunfei/constants.go @@ -5,6 +5,7 @@ var ModelList = []string{ "SparkDesk-v1.1", "SparkDesk-v2.1", "SparkDesk-v3.1", + "SparkDesk-v3.1-128K", "SparkDesk-v3.5", "SparkDesk-v4.0", } diff --git a/relay/adaptor/xunfei/main.go b/relay/adaptor/xunfei/main.go index ef6120e5..99c4d1f4 100644 --- a/relay/adaptor/xunfei/main.go +++ b/relay/adaptor/xunfei/main.go @@ -272,9 +272,9 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl, } func parseAPIVersionByModelName(modelName string) string { - parts := strings.Split(modelName, "-") - if len(parts) == 2 { - return parts[1] + index := strings.IndexAny(modelName, "-") + if index != -1 { + return modelName[index+1:] } return "" } @@ -288,6 +288,8 @@ func apiVersion2domain(apiVersion string) string { return "generalv2" case "v3.1": return "generalv3" + case "v3.1-128K": + return "pro-128k" case "v3.5": return "generalv3.5" case "v4.0": @@ -297,7 +299,14 @@ func apiVersion2domain(apiVersion string) string { } func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) { + var authUrl string domain := apiVersion2domain(apiVersion) - authUrl := buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret) + switch apiVersion { + case "v3.1-128K": + authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/pro-128k", apiVersion), apiKey, apiSecret) + break + default: + authUrl = buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret) + } return domain, authUrl } diff --git a/relay/billing/ratio/image.go b/relay/billing/ratio/image.go index ced0c667..c8c42a15 100644 --- a/relay/billing/ratio/image.go +++ b/relay/billing/ratio/image.go @@ -30,6 +30,14 @@ var ImageSizeRatios = map[string]map[string]float64{ "720x1280": 1, "1280x720": 1, }, + "step-1x-medium": { + "256x256": 1, + "512x512": 1, + "768x768": 1, + "1024x1024": 1, + "1280x800": 1, + "800x1280": 1, + }, } var ImageGenerationAmounts = map[string][2]int{ @@ -39,6 +47,7 @@ var ImageGenerationAmounts = map[string][2]int{ "ali-stable-diffusion-v1.5": {1, 4}, // Ali "wanx-v1": {1, 4}, // Ali "cogview-3": {1, 1}, + "step-1x-medium": {1, 1}, } var ImagePromptLengthLimitations = map[string]int{ @@ -48,6 +57,7 @@ var ImagePromptLengthLimitations = map[string]int{ "ali-stable-diffusion-v1.5": 4000, "wanx-v1": 4000, "cogview-3": 833, + "step-1x-medium": 4000, } var ImageOriginModelName = map[string]string{ diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index 7bc6cd54..cf526875 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -34,7 +34,9 @@ var ModelRatio = map[string]float64{ "gpt-4-turbo": 5, // $0.01 / 1K tokens "gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens "gpt-4o": 2.5, // $0.005 / 1K tokens + "chatgpt-4o-latest": 2.5, // $0.005 / 1K tokens "gpt-4o-2024-05-13": 2.5, // $0.005 / 1K tokens + "gpt-4o-2024-08-06": 1.25, // $0.0025 / 1K tokens "gpt-4o-mini": 0.075, // $0.00015 / 1K tokens "gpt-4o-mini-2024-07-18": 0.075, // $0.00015 / 1K tokens "gpt-4-vision-preview": 5, // $0.01 / 1K tokens @@ -126,6 +128,7 @@ var ModelRatio = map[string]float64{ "SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens "SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens "SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens + "SparkDesk-v3.1-128K": 1.2858, // ¥0.018 / 1k tokens "SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens "SparkDesk-v4.0": 1.2858, // ¥0.018 / 1k tokens "360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens @@ -171,10 +174,15 @@ var ModelRatio = map[string]float64{ "yi-34b-chat-0205": 2.5 / 1000 * RMB, "yi-34b-chat-200k": 12.0 / 1000 * RMB, "yi-vl-plus": 6.0 / 1000 * RMB, - // stepfun todo - "step-1v-32k": 0.024 * RMB, - "step-1-32k": 0.024 * RMB, - "step-1-200k": 0.15 * RMB, + // https://platform.stepfun.com/docs/pricing/details + "step-1-8k": 0.005 / 1000 * RMB, + "step-1-32k": 0.015 / 1000 * RMB, + "step-1-128k": 0.040 / 1000 * RMB, + "step-1-256k": 0.095 / 1000 * RMB, + "step-1-flash": 0.001 / 1000 * RMB, + "step-2-16k": 0.038 / 1000 * RMB, + "step-1v-8k": 0.005 / 1000 * RMB, + "step-1v-32k": 0.015 / 1000 * RMB, // aws llama3 https://aws.amazon.com/cn/bedrock/pricing/ "llama3-8b-8192(33)": 0.0003 / 0.002, // $0.0003 / 1K tokens "llama3-70b-8192(33)": 0.00265 / 0.002, // $0.00265 / 1K tokens @@ -200,8 +208,10 @@ var CompletionRatio = map[string]float64{ "llama3-70b-8192(33)": 0.0035 / 0.00265, } -var DefaultModelRatio map[string]float64 -var DefaultCompletionRatio map[string]float64 +var ( + DefaultModelRatio map[string]float64 + DefaultCompletionRatio map[string]float64 +) func init() { DefaultModelRatio = make(map[string]float64) @@ -313,7 +323,7 @@ func GetCompletionRatio(name string, channelType int) float64 { return 4.0 / 3.0 } if strings.HasPrefix(name, "gpt-4") { - if strings.HasPrefix(name, "gpt-4o-mini") { + if strings.HasPrefix(name, "gpt-4o-mini") || name == "gpt-4o-2024-08-06" { return 4 } if strings.HasPrefix(name, "gpt-4-turbo") || @@ -323,6 +333,9 @@ func GetCompletionRatio(name string, channelType int) float64 { } return 2 } + if name == "chatgpt-4o-latest" { + return 3 + } if strings.HasPrefix(name, "claude-3") { return 5 } diff --git a/relay/model/general.go b/relay/model/general.go index c34c1c2d..aacc8467 100644 --- a/relay/model/general.go +++ b/relay/model/general.go @@ -1,7 +1,15 @@ package model type ResponseFormat struct { - Type string `json:"type,omitempty"` + Type string `json:"type,omitempty"` + JsonSchema *JSONSchema `json:"json_schema,omitempty"` +} + +type JSONSchema struct { + Description string `json:"description,omitempty"` + Name string `json:"name"` + Schema map[string]interface{} `json:"schema,omitempty"` + Strict *bool `json:"strict,omitempty"` } type GeneralOpenAIRequest struct { diff --git a/router/api.go b/router/api.go index d2ada4eb..6d00c6ea 100644 --- a/router/api.go +++ b/router/api.go @@ -23,6 +23,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), auth.GitHubOAuth) + apiRouter.GET("/oauth/oidc", middleware.CriticalRateLimit(), auth.OidcAuth) apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth) apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth) diff --git a/web/air/src/components/TokensTable.js b/web/air/src/components/TokensTable.js index 0853ddfb..c87657dc 100644 --- a/web/air/src/components/TokensTable.js +++ b/web/air/src/components/TokensTable.js @@ -11,12 +11,14 @@ import EditToken from '../pages/Token/EditToken'; const COPY_OPTIONS = [ { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' } + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + { key: 'lobechat', text: 'LobeChat', value: 'lobechat' }, ]; const OPEN_LINK_OPTIONS = [ { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, - { key: 'opencat', text: 'OpenCat', value: 'opencat' } + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + { key: 'lobechat', text: 'LobeChat', value: 'lobechat' } ]; function renderTimestamp(timestamp) { @@ -60,7 +62,12 @@ const TokensTable = () => { onOpenLink('next-mj'); } }, - { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' } + { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' }, + { + node: 'item', key: 'lobechat', name: 'LobeChat', onClick: () => { + onOpenLink('lobechat'); + } + } ]; const columns = [ @@ -177,6 +184,11 @@ const TokensTable = () => { node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => { onOpenLink('opencat', record.key); } + }, + { + node: 'item', key: 'lobechat', name: 'LobeChat', onClick: () => { + onOpenLink('lobechat'); + } } ] } @@ -382,6 +394,9 @@ const TokensTable = () => { case 'next-mj': url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; break; + case 'lobechat': + url = chatLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}"/v1"}}}`; + break; default: if (!chatLink) { showError('管理员未设置聊天链接'); diff --git a/web/air/src/pages/Channel/EditChannel.js b/web/air/src/pages/Channel/EditChannel.js index 73fd2da2..b50a6e77 100644 --- a/web/air/src/pages/Channel/EditChannel.js +++ b/web/air/src/pages/Channel/EditChannel.js @@ -78,7 +78,7 @@ const EditChannel = (props) => { localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; break; case 18: - localModels = ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.5', 'SparkDesk-v4.0']; + localModels = ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.1-128K', 'SparkDesk-v3.5', 'SparkDesk-v4.0']; break; case 19: localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1']; diff --git a/web/berry/src/assets/images/icons/lark.svg b/web/berry/src/assets/images/icons/lark.svg index 239e1bef..79688e2a 100644 --- a/web/berry/src/assets/images/icons/lark.svg +++ b/web/berry/src/assets/images/icons/lark.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + diff --git a/web/berry/src/assets/images/icons/oidc.svg b/web/berry/src/assets/images/icons/oidc.svg new file mode 100644 index 00000000..96e01f81 --- /dev/null +++ b/web/berry/src/assets/images/icons/oidc.svg @@ -0,0 +1,7 @@ + + + + diff --git a/web/berry/src/config.js b/web/berry/src/config.js index eeeda99a..8c1faf9b 100644 --- a/web/berry/src/config.js +++ b/web/berry/src/config.js @@ -22,7 +22,12 @@ const config = { turnstile_site_key: '', version: '', wechat_login: false, - wechat_qrcode: '' + wechat_qrcode: '', + oidc: false, + oidc_client_id: '', + oidc_authorization_endpoint: '', + oidc_token_endpoint: '', + oidc_userinfo_endpoint: '', } }; diff --git a/web/berry/src/hooks/useLogin.js b/web/berry/src/hooks/useLogin.js index 39d8b407..6d89727d 100644 --- a/web/berry/src/hooks/useLogin.js +++ b/web/berry/src/hooks/useLogin.js @@ -70,6 +70,28 @@ const useLogin = () => { } }; + const oidcLogin = async (code, state) => { + try { + const res = await API.get(`/api/oauth/oidc?code=${code}&state=${state}`); + const { success, message, data } = res.data; + if (success) { + if (message === 'bind') { + showSuccess('绑定成功!'); + navigate('/panel'); + } else { + dispatch({ type: LOGIN, payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + showSuccess('登录成功!'); + navigate('/panel'); + } + } + return { success, message }; + } catch (err) { + // 请求失败,设置错误信息 + return { success: false, message: '' }; + } + } + const wechatLogin = async (code) => { try { const res = await API.get(`/api/oauth/wechat?code=${code}`); @@ -94,7 +116,7 @@ const useLogin = () => { navigate('/'); }; - return { login, logout, githubLogin, wechatLogin, larkLogin }; + return { login, logout, githubLogin, wechatLogin, larkLogin,oidcLogin }; }; export default useLogin; diff --git a/web/berry/src/routes/OtherRoutes.js b/web/berry/src/routes/OtherRoutes.js index 58c0b660..a4bdb5d3 100644 --- a/web/berry/src/routes/OtherRoutes.js +++ b/web/berry/src/routes/OtherRoutes.js @@ -9,6 +9,7 @@ const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')) const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register'))); const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth'))); const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth'))); +const OidcOAuth = Loadable(lazy(() => import('views/Authentication/Auth/OidcOAuth'))); const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword'))); const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword'))); const Home = Loadable(lazy(() => import('views/Home'))); @@ -53,6 +54,10 @@ const OtherRoutes = { path: '/oauth/lark', element: }, + { + path: 'oauth/oidc', + element: + }, { path: '/404', element: diff --git a/web/berry/src/utils/common.js b/web/berry/src/utils/common.js index d74d032e..f9c2896c 100644 --- a/web/berry/src/utils/common.js +++ b/web/berry/src/utils/common.js @@ -98,6 +98,21 @@ export async function onLarkOAuthClicked(lark_client_id) { window.open(`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`); } +export async function onOidcClicked(auth_url, client_id, openInNewTab = false) { + const state = await getOAuthState(); + if (!state) return; + const redirect_uri = `${window.location.origin}/oauth/oidc`; + const response_type = "code"; + const scope = "openid profile email"; + const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`; + if (openInNewTab) { + window.open(url); + } else + { + window.location.href = url; + } +} + export function isAdmin() { let user = localStorage.getItem('user'); if (!user) return false; diff --git a/web/berry/src/views/Authentication/Auth/OidcOAuth.js b/web/berry/src/views/Authentication/Auth/OidcOAuth.js new file mode 100644 index 00000000..55d9372d --- /dev/null +++ b/web/berry/src/views/Authentication/Auth/OidcOAuth.js @@ -0,0 +1,94 @@ +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { showError } from 'utils/common'; +import useLogin from 'hooks/useLogin'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material'; + +// project imports +import AuthWrapper from '../AuthWrapper'; +import AuthCardWrapper from '../AuthCardWrapper'; +import Logo from 'ui-component/Logo'; + +// assets + +// ================================|| AUTH3 - LOGIN ||================================ // + +const OidcOAuth = () => { + const theme = useTheme(); + const matchDownSM = useMediaQuery(theme.breakpoints.down('md')); + + const [searchParams] = useSearchParams(); + const [prompt, setPrompt] = useState('处理中...'); + const { oidcLogin } = useLogin(); + + let navigate = useNavigate(); + + const sendCode = async (code, state, count) => { + const { success, message } = await oidcLogin(code, state); + if (!success) { + if (message) { + showError(message); + } + if (count === 0) { + setPrompt(`操作失败,重定向至登录界面中...`); + await new Promise((resolve) => setTimeout(resolve, 2000)); + navigate('/login'); + return; + } + count++; + setPrompt(`出现错误,第 ${count} 次重试中...`); + await new Promise((resolve) => setTimeout(resolve, 2000)); + await sendCode(code, state, count); + } + }; + + useEffect(() => { + let code = searchParams.get('code'); + let state = searchParams.get('state'); + sendCode(code, state, 0).then(); + }, []); + + return ( + + + + + + + + + + + + + + + + + + OIDC 登录 + + + + + + + + + {prompt} + + + + + + + + + + ); +}; + +export default OidcOAuth; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index bc7a35c0..7efd0362 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -36,7 +36,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; import Github from 'assets/images/icons/github.svg'; import Wechat from 'assets/images/icons/wechat.svg'; import Lark from 'assets/images/icons/lark.svg'; -import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common'; +import OIDC from 'assets/images/icons/oidc.svg'; +import { onGitHubOAuthClicked, onLarkOAuthClicked, onOidcClicked } from 'utils/common'; // ============================|| FIREBASE - LOGIN ||============================ // @@ -50,7 +51,7 @@ const LoginForm = ({ ...others }) => { // const [checked, setChecked] = useState(true); let tripartiteLogin = false; - if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) { + if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id || siteInfo.oidc) { tripartiteLogin = true; } @@ -145,6 +146,29 @@ const LoginForm = ({ ...others }) => { )} + {siteInfo.oidc && ( + + + + + + )} ¥{balance.toFixed(2)}; case 13: // AIGC2D return {renderNumber(balance)}; + case 44: // SiliconFlow + return ¥{balance.toFixed(2)}; default: return 不支持; } diff --git a/web/berry/src/views/Channel/type/Config.js b/web/berry/src/views/Channel/type/Config.js index 73b2dfe8..4a8fc27a 100644 --- a/web/berry/src/views/Channel/type/Config.js +++ b/web/berry/src/views/Channel/type/Config.js @@ -91,7 +91,7 @@ const typeConfig = { other: '版本号' }, input: { - models: ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.5', 'SparkDesk-v4.0'] + models: ['SparkDesk', 'SparkDesk-v1.1', 'SparkDesk-v2.1', 'SparkDesk-v3.1', 'SparkDesk-v3.1-128K', 'SparkDesk-v3.5', 'SparkDesk-v4.0'] }, prompt: { key: '按照如下格式输入:APPID|APISecret|APIKey', diff --git a/web/berry/src/views/Profile/index.js b/web/berry/src/views/Profile/index.js index 4705d8af..b8fa0e7f 100644 --- a/web/berry/src/views/Profile/index.js +++ b/web/berry/src/views/Profile/index.js @@ -20,7 +20,7 @@ import SubCard from 'ui-component/cards/SubCard'; import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react'; import Label from 'ui-component/Label'; import { API } from 'utils/api'; -import { showError, showSuccess } from 'utils/common'; +import { onOidcClicked, showError, showSuccess } from 'utils/common'; import { onGitHubOAuthClicked, onLarkOAuthClicked, copy } from 'utils/common'; import * as Yup from 'yup'; import WechatModal from 'views/Authentication/AuthForms/WechatModal'; @@ -28,6 +28,7 @@ import { useSelector } from 'react-redux'; import EmailModal from './component/EmailModal'; import Turnstile from 'react-turnstile'; import { ReactComponent as Lark } from 'assets/images/icons/lark.svg'; +import { ReactComponent as OIDC } from 'assets/images/icons/oidc.svg'; const validationSchema = Yup.object().shape({ username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'), @@ -123,6 +124,15 @@ export default function Profile() { loadUser().then(); }, [status]); + function getOidcId(){ + if (!inputs.oidc_id) return ''; + let oidc_id = inputs.oidc_id; + if (inputs.oidc_id.length > 8) { + oidc_id = inputs.oidc_id.slice(0, 6) + '...' + inputs.oidc_id.slice(-6); + } + return oidc_id; + } + return ( <> @@ -141,6 +151,9 @@ export default function Profile() { + @@ -216,6 +229,13 @@ export default function Profile() { )} + {status.oidc && !inputs.oidc_id && ( + + + + )} + + + + ¥{balance.toFixed(2)}; case 13: // AIGC2D return {renderNumber(balance)}; + case 44: // SiliconFlow + return ¥{balance.toFixed(2)}; default: return 不支持; } diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 40890f5d..946c52cc 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -10,12 +10,14 @@ const COPY_OPTIONS = [ { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, { key: 'ama', text: 'BotGem', value: 'ama' }, { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + { key: 'lobechat', text: 'LobeChat', value: 'lobechat' }, ]; const OPEN_LINK_OPTIONS = [ { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, { key: 'ama', text: 'BotGem', value: 'ama' }, { key: 'opencat', text: 'OpenCat', value: 'opencat' }, + { key: 'lobechat', text: 'LobeChat', value: 'lobechat' }, ]; function renderTimestamp(timestamp) { @@ -114,6 +116,9 @@ const TokensTable = () => { case 'next': url = nextUrl; break; + case 'lobechat': + url = nextLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}"/v1"}}}`; + break; default: url = `sk-${key}`; } @@ -153,7 +158,11 @@ const TokensTable = () => { case 'opencat': url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; break; - + + case 'lobechat': + url = chatLink + `/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${serverAddress}"/v1"}}}`; + break; + default: url = defaultUrl; }