diff --git a/README.md b/README.md index f852c0e..33fd7aa 100644 --- a/README.md +++ b/README.md @@ -14,30 +14,37 @@ > 最新版Docker镜像 calciumion/new-api:latest > 更新指令 docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR -## 此分叉版本的主要变更 +## 主要变更 +此分叉版本的主要变更如下: + 1. 全新的UI界面(部分界面还待更新) -2. 添加[Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy)接口的支持: - + [x] /mj/submit/imagine - + [x] /mj/submit/change - + [x] /mj/submit/blend - + [x] /mj/submit/describe - + [x] /mj/image/{id} (通过此接口获取图片,**请必须在系统设置中填写服务器地址!!**) - + [x] /mj/task/{id}/fetch (此接口返回的图片地址为经过One API转发的地址) - + [x] /task/list-by-condition +2. 添加[Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy)接口的支持 + + [x] /mj/submit/imagine + + [x] /mj/submit/change + + [x] /mj/submit/blend + + [x] /mj/submit/describe + + [x] /mj/image/{id} (通过此接口获取图片,**请必须在系统设置中填写服务器地址!!**) + + [x] /mj/task/{id}/fetch (此接口返回的图片地址为经过One API转发的地址) + + [x] /task/list-by-condition 3. 支持在线充值功能,可在系统设置中设置,当前支持的支付接口: - + [x] 易支付 + + [x] 易支付 4. 支持用key查询使用额度: - + 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用情况,方便二次分销 + + 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用 5. 渠道显示已使用额度,支持指定组织访问 6. 分页支持选择每页显示数量 -7. 支持 gpt-4-1106-vision-preview,dall-e-3,tts-1 -8. 支持第三方模型 **gps** (gpt-4-gizmo-*),在渠道中添加自定义模型gpt-4-gizmo-*即可 -9. 兼容原版One API的数据库,可直接使用原版数据库(one-api.db) -10. 支持模型按次数收费,可在 系统设置-运营设置 中设置 -11. 支持gemini-pro,gemini-pro-vision模型 -12. 支持渠道**加权随机** -13. 数据看板 -14. 可设置令牌能调用的模型 +7. 兼容原版One API的数据库,可直接使用原版数据库(one-api.db) +8. 支持模型按次数收费,可在 系统设置-运营设置 中设置 +9. 支持渠道**加权随机** +10. 数据看板 +11. 可设置令牌能调用的模型 +12. 支持Telegram授权登录 + +## 模型支持 +此版本额外支持以下模型: +1. 第三方模型 **gps** (gpt-4-gizmo-*) +2. 智谱glm-4v,glm-4v识图 + +您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。 ## 部署 ### 基于 Docker 进行部署 diff --git a/common/constants.go b/common/constants.go index e11d7fe..c89280d 100644 --- a/common/constants.go +++ b/common/constants.go @@ -9,14 +9,19 @@ import ( "github.com/google/uuid" ) +// Pay Settings + +var PayAddress = "" +var CustomCallbackAddress = "" +var EpayId = "" +var EpayKey = "" +var Price = 7.3 +var MinTopUp = 1 + var StartTime = time.Now().Unix() // unit: second var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change var SystemName = "New API" var ServerAddress = "http://localhost:3000" -var PayAddress = "" -var EpayId = "" -var EpayKey = "" -var Price = 7.3 var Footer = "" var Logo = "" var TopUpLink = "" @@ -46,6 +51,7 @@ var PasswordRegisterEnabled = true var EmailVerificationEnabled = false var GitHubOAuthEnabled = false var WeChatAuthEnabled = false +var TelegramOAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true @@ -83,6 +89,9 @@ var WeChatAccountQRCodeImageURL = "" var TurnstileSiteKey = "" var TurnstileSecretKey = "" +var TelegramBotToken = "" +var TelegramBotName = "" + var QuotaForNewUser = 0 var QuotaForInviter = 0 var QuotaForInvitee = 0 diff --git a/common/model-ratio.go b/common/model-ratio.go index 30b87ee..648f9fc 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -80,7 +80,10 @@ var ModelRatio = map[string]float64{ "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens "qwen-plus": 10, // ¥0.14 / 1k tokens "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens - "SparkDesk": 1.2858, // ¥0.018 / 1k tokens + "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.5": 1.2858, // ¥0.018 / 1k tokens "360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens "embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens "embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens diff --git a/controller/misc.go b/controller/misc.go index 3ed3c5e..0259426 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -11,6 +11,22 @@ import ( "github.com/gin-gonic/gin" ) +func TestStatus(c *gin.Context) { + err := model.PingDB() + if err != nil { + c.JSON(http.StatusServiceUnavailable, gin.H{ + "success": false, + "message": "数据库连接失败", + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Server is running", + }) + return +} + func GetStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, @@ -20,6 +36,8 @@ func GetStatus(c *gin.Context) { "email_verification": common.EmailVerificationEnabled, "github_oauth": common.GitHubOAuthEnabled, "github_client_id": common.GitHubClientId, + "telegram_oauth": common.TelegramOAuthEnabled, + "telegram_bot_name": common.TelegramBotName, "system_name": common.SystemName, "logo": common.Logo, "footer_html": common.Footer, @@ -27,6 +45,7 @@ func GetStatus(c *gin.Context) { "wechat_login": common.WeChatAuthEnabled, "server_address": common.ServerAddress, "price": common.Price, + "min_topup": common.MinTopUp, "turnstile_check": common.TurnstileCheckEnabled, "turnstile_site_key": common.TurnstileSiteKey, "top_up_link": common.TopUpLink, diff --git a/controller/model.go b/controller/model.go index 8909de4..38c6c46 100644 --- a/controller/model.go +++ b/controller/model.go @@ -129,6 +129,13 @@ func ListModels(c *gin.Context) { }) } +func ChannelListModels(c *gin.Context) { + c.JSON(200, gin.H{ + "object": "list", + "data": openAIModels, + }) +} + func RetrieveModel(c *gin.Context) { modelId := c.Param("model") if model, ok := openAIModelsMap[modelId]; ok { diff --git a/controller/telegram.go b/controller/telegram.go new file mode 100644 index 0000000..b5bc0c0 --- /dev/null +++ b/controller/telegram.go @@ -0,0 +1,116 @@ +package controller + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "io" + "one-api/common" + "one-api/model" + "sort" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +func TelegramBind(c *gin.Context) { + if !common.TelegramOAuthEnabled { + c.JSON(200, gin.H{ + "message": "管理员未开启通过 Telegram 登录以及注册", + "success": false, + }) + return + } + params := c.Request.URL.Query() + if !checkTelegramAuthorization(params, common.TelegramBotToken) { + c.JSON(200, gin.H{ + "message": "无效的请求", + "success": false, + }) + return + } + telegramId := params["id"][0] + if model.IsTelegramIdAlreadyTaken(telegramId) { + c.JSON(200, gin.H{ + "message": "该 Telegram 账户已被绑定", + "success": false, + }) + return + } + + session := sessions.Default(c) + id := session.Get("id") + user := model.User{Id: id.(int)} + if err := user.FillUserById(); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + user.TelegramId = telegramId + if err := user.Update(false); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + + c.Redirect(302, "/setting") +} + +func TelegramLogin(c *gin.Context) { + if !common.TelegramOAuthEnabled { + c.JSON(200, gin.H{ + "message": "管理员未开启通过 Telegram 登录以及注册", + "success": false, + }) + return + } + params := c.Request.URL.Query() + if !checkTelegramAuthorization(params, common.TelegramBotToken) { + c.JSON(200, gin.H{ + "message": "无效的请求", + "success": false, + }) + return + } + + telegramId := params["id"][0] + user := model.User{TelegramId: telegramId} + if err := user.FillUserByTelegramId(); err != nil { + c.JSON(200, gin.H{ + "message": err.Error(), + "success": false, + }) + return + } + setupLogin(&user, c) +} + +func checkTelegramAuthorization(params map[string][]string, token string) bool { + strs := []string{} + var hash = "" + for k, v := range params { + if k == "hash" { + hash = v[0] + continue + } + strs = append(strs, k+"="+v[0]) + } + sort.Strings(strs) + var imploded = "" + for _, s := range strs { + if imploded != "" { + imploded += "\n" + } + imploded += s + } + sha256hash := sha256.New() + io.WriteString(sha256hash, token) + hmachash := hmac.New(sha256.New, sha256hash.Sum(nil)) + io.WriteString(hmachash, imploded) + ss := hex.EncodeToString(hmachash.Sum(nil)) + return hash == ss +} diff --git a/controller/topup.go b/controller/topup.go index 961ffa2..3203eb8 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -9,6 +9,7 @@ import ( "net/url" "one-api/common" "one-api/model" + "one-api/service" "strconv" "time" ) @@ -55,14 +56,14 @@ func RequestEpay(c *gin.Context) { c.JSON(200, gin.H{"message": err.Error(), "data": 10}) return } - if req.Amount < 1 { - c.JSON(200, gin.H{"message": "充值金额不能小于1", "data": 10}) + if req.Amount < common.MinTopUp { + c.JSON(200, gin.H{"message": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp), "data": 10}) return } id := c.GetInt("id") user, _ := model.GetUserById(id, false) - amount := GetAmount(float64(req.Amount), *user) + payMoney := GetAmount(float64(req.Amount), *user) var payType epay.PurchaseType if req.PaymentMethod == "zfb" { @@ -72,11 +73,10 @@ func RequestEpay(c *gin.Context) { req.PaymentMethod = "wxpay" payType = epay.WechatPay } - + callBackAddress := service.GetCallbackAddress() returnUrl, _ := url.Parse(common.ServerAddress + "/log") - notifyUrl, _ := url.Parse(common.ServerAddress + "/api/user/epay/notify") + notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify") tradeNo := strconv.FormatInt(time.Now().Unix(), 10) - payMoney := amount client := GetEpayClient() if client == nil { c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"}) @@ -169,8 +169,8 @@ func RequestAmount(c *gin.Context) { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } - if req.Amount < 1 { - c.JSON(200, gin.H{"message": "error", "data": "充值金额不能小于1"}) + if req.Amount < common.MinTopUp { + c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", common.MinTopUp)}) return } id := c.GetInt("id") diff --git a/docker-compose.yml b/docker-compose.yml index 40da248..fff2716 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,7 @@ version: '3.4' services: new-api: image: calciumion/new-api:latest + # build: . container_name: new-api restart: always command: --log-dir /app/logs diff --git a/makefile b/makefile new file mode 100644 index 0000000..f846d30 --- /dev/null +++ b/makefile @@ -0,0 +1,14 @@ +FRONTEND_DIR = ./web +BACKEND_DIR = . + +.PHONY: all build-frontend start-backend + +all: build-frontend start-backend + +build-frontend: + @echo "Building frontend..." + @cd $(FRONTEND_DIR) && npm install && DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build npm run build + +start-backend: + @echo "Starting backend dev server..." + @cd $(BACKEND_DIR) && go run main.go & diff --git a/model/cache.go b/model/cache.go index b1199e2..8294e73 100644 --- a/model/cache.go +++ b/model/cache.go @@ -291,24 +291,27 @@ func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error } } } + + // 平滑系数 + smoothingFactor := 10 // Calculate the total weight of all channels up to endIdx totalWeight := 0 for _, channel := range channels[:endIdx] { - totalWeight += channel.GetWeight() + totalWeight += channel.GetWeight() + smoothingFactor } - if totalWeight == 0 { - // If all weights are 0, select a channel randomly - return channels[rand.Intn(endIdx)], nil - } + //if totalWeight == 0 { + // // If all weights are 0, select a channel randomly + // return channels[rand.Intn(endIdx)], nil + //} // Generate a random value in the range [0, totalWeight) randomWeight := rand.Intn(totalWeight) // Find a channel based on its weight for _, channel := range channels[:endIdx] { - randomWeight -= channel.GetWeight() - if randomWeight <= 0 { + randomWeight -= channel.GetWeight() + smoothingFactor + if randomWeight < 0 { return channel, nil } } diff --git a/model/channel.go b/model/channel.go index 7c7b0d9..b06f578 100644 --- a/model/channel.go +++ b/model/channel.go @@ -8,7 +8,7 @@ import ( type Channel struct { Id int `json:"id"` Type int `json:"type" gorm:"default:0"` - Key string `json:"key" gorm:"not null;index"` + Key string `json:"key" gorm:"not null"` OpenAIOrganization *string `json:"openai_organization"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index"` diff --git a/model/log.go b/model/log.go index 44bc5b7..2740c5a 100644 --- a/model/log.go +++ b/model/log.go @@ -85,7 +85,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke } if common.DataExportEnabled { common.SafeGoroutine(func() { - LogQuotaData(userId, username, modelName, quota, common.GetTimestamp()) + LogQuotaData(userId, username, modelName, quota, common.GetTimestamp(), promptTokens+completionTokens) }) } } diff --git a/model/main.go b/model/main.go index ade3cc1..bb91255 100644 --- a/model/main.go +++ b/model/main.go @@ -5,9 +5,11 @@ import ( "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" + "log" "one-api/common" "os" "strings" + "sync" "time" ) @@ -148,3 +150,33 @@ func CloseDB() error { err = sqlDB.Close() return err } + +var ( + lastPingTime time.Time + pingMutex sync.Mutex +) + +func PingDB() error { + pingMutex.Lock() + defer pingMutex.Unlock() + + if time.Since(lastPingTime) < time.Second*10 { + return nil + } + + sqlDB, err := DB.DB() + if err != nil { + log.Printf("Error getting sql.DB from GORM: %v", err) + return err + } + + err = sqlDB.Ping() + if err != nil { + log.Printf("Error pinging DB: %v", err) + return err + } + + lastPingTime = time.Now() + common.SysLog("Database pinged successfully") + return nil +} diff --git a/model/option.go b/model/option.go index 03c0230..9a7ad60 100644 --- a/model/option.go +++ b/model/option.go @@ -30,6 +30,7 @@ func InitOptionMap() { common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled) common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled) common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled) + common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled) common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled) common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled) common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled) @@ -56,12 +57,16 @@ func InitOptionMap() { common.OptionMap["Logo"] = common.Logo common.OptionMap["ServerAddress"] = "" common.OptionMap["PayAddress"] = "" + common.OptionMap["CustomCallbackAddress"] = "" common.OptionMap["EpayId"] = "" common.OptionMap["EpayKey"] = "" common.OptionMap["Price"] = strconv.FormatFloat(common.Price, 'f', -1, 64) + common.OptionMap["MinTopUp"] = strconv.Itoa(common.MinTopUp) common.OptionMap["TopupGroupRatio"] = common.TopupGroupRatio2JSONString() common.OptionMap["GitHubClientId"] = "" common.OptionMap["GitHubClientSecret"] = "" + common.OptionMap["TelegramBotToken"] = "" + common.OptionMap["TelegramBotName"] = "" common.OptionMap["WeChatServerAddress"] = "" common.OptionMap["WeChatServerToken"] = "" common.OptionMap["WeChatAccountQRCodeImageURL"] = "" @@ -152,6 +157,8 @@ func updateOptionMap(key string, value string) (err error) { common.GitHubOAuthEnabled = boolValue case "WeChatAuthEnabled": common.WeChatAuthEnabled = boolValue + case "TelegramOAuthEnabled": + common.TelegramOAuthEnabled = boolValue case "TurnstileCheckEnabled": common.TurnstileCheckEnabled = boolValue case "RegisterEnabled": @@ -194,12 +201,16 @@ func updateOptionMap(key string, value string) (err error) { common.ServerAddress = value case "PayAddress": common.PayAddress = value + case "CustomCallbackAddress": + common.CustomCallbackAddress = value case "EpayId": common.EpayId = value case "EpayKey": common.EpayKey = value case "Price": common.Price, _ = strconv.ParseFloat(value, 64) + case "MinTopUp": + common.MinTopUp, _ = strconv.Atoi(value) case "TopupGroupRatio": err = common.UpdateTopupGroupRatioByJSONString(value) case "GitHubClientId": @@ -218,6 +229,10 @@ func updateOptionMap(key string, value string) (err error) { common.WeChatServerToken = value case "WeChatAccountQRCodeImageURL": common.WeChatAccountQRCodeImageURL = value + case "TelegramBotToken": + common.TelegramBotToken = value + case "TelegramBotName": + common.TelegramBotName = value case "TurnstileSiteKey": common.TurnstileSiteKey = value case "TurnstileSecretKey": diff --git a/model/usedata.go b/model/usedata.go index 9ca623d..b2f3025 100644 --- a/model/usedata.go +++ b/model/usedata.go @@ -15,6 +15,7 @@ type QuotaData struct { Username string `json:"username" gorm:"index:idx_qdt_model_user_name,priority:2;size:64;default:''"` ModelName string `json:"model_name" gorm:"index:idx_qdt_model_user_name,priority:1;size:64;default:''"` CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_qdt_created_at,priority:2"` + TokenUsed int `json:"token_used" gorm:"default:0"` Count int `json:"count" gorm:"default:0"` Quota int `json:"quota" gorm:"default:0"` } @@ -38,7 +39,7 @@ func UpdateQuotaData() { var CacheQuotaData = make(map[string]*QuotaData) var CacheQuotaDataLock = sync.Mutex{} -func logQuotaDataCache(userId int, username string, modelName string, quota int, createdAt int64) { +func logQuotaDataCache(userId int, username string, modelName string, quota int, createdAt int64, tokenUsed int) { key := fmt.Sprintf("%d-%s-%s-%d", userId, username, modelName, createdAt) quotaData, ok := CacheQuotaData[key] if ok { @@ -52,18 +53,19 @@ func logQuotaDataCache(userId int, username string, modelName string, quota int, CreatedAt: createdAt, Count: 1, Quota: quota, + TokenUsed: tokenUsed, } } CacheQuotaData[key] = quotaData } -func LogQuotaData(userId int, username string, modelName string, quota int, createdAt int64) { +func LogQuotaData(userId int, username string, modelName string, quota int, createdAt int64, tokenUsed int) { // 只精确到小时 createdAt = createdAt - (createdAt % 3600) CacheQuotaDataLock.Lock() defer CacheQuotaDataLock.Unlock() - logQuotaDataCache(userId, username, modelName, quota, createdAt) + logQuotaDataCache(userId, username, modelName, quota, createdAt, tokenUsed) } func SaveQuotaDataCache() { diff --git a/model/user.go b/model/user.go index 3e727a0..00294b2 100644 --- a/model/user.go +++ b/model/user.go @@ -3,10 +3,11 @@ package model import ( "errors" "fmt" - "gorm.io/gorm" "one-api/common" "strings" "time" + + "gorm.io/gorm" ) // User if you add sensitive fields, don't forget to clean them in setupLogin function. @@ -21,6 +22,7 @@ type User struct { Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` + TelegramId string `json:"telegram_id" gorm:"column:telegram_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 int `json:"quota" gorm:"type:int;default:0"` @@ -286,6 +288,17 @@ func (user *User) FillUserByUsername() error { return nil } +func (user *User) FillUserByTelegramId() error { + if user.TelegramId == "" { + return errors.New("Telegram id 为空!") + } + err := DB.Where(User{TelegramId: user.TelegramId}).First(user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("该 Telegram 账户未绑定") + } + return nil +} + func IsEmailAlreadyTaken(email string) bool { return DB.Where("email = ?", email).Find(&User{}).RowsAffected == 1 } @@ -302,6 +315,10 @@ func IsUsernameAlreadyTaken(username string) bool { return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1 } +func IsTelegramIdAlreadyTaken(telegramId string) bool { + return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1 +} + func ResetUserPasswordByEmail(email string, password string) error { if email == "" || password == "" { return errors.New("邮箱地址或密码为空!") diff --git a/relay/relay-text.go b/relay/relay-text.go index 63b1ff6..d38afaa 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -59,6 +59,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo) } } relayInfo.IsStream = textRequest.Stream + relayInfo.UpstreamModelName = textRequest.Model return textRequest, nil } @@ -114,7 +115,7 @@ func TextHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode { // pre-consume quota 预消耗配额 preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, preConsumedQuota, relayInfo) - if err != nil { + if openaiErr != nil { return openaiErr } @@ -168,6 +169,8 @@ func getPromptTokens(textRequest *dto.GeneralOpenAIRequest, info *relaycommon.Re promptTokens, err = service.CountTokenInput(textRequest.Prompt, textRequest.Model), nil case relayconstant.RelayModeModerations: promptTokens, err = service.CountTokenInput(textRequest.Input, textRequest.Model), nil + case relayconstant.RelayModeEmbeddings: + promptTokens, err = service.CountTokenInput(textRequest.Input, textRequest.Model), nil default: err = errors.New("unknown relay mode") promptTokens = 0 @@ -182,7 +185,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo if err != nil { return 0, 0, service.OpenAIErrorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError) } - if userQuota < 0 || userQuota-preConsumedQuota < 0 { + if userQuota <= 0 || userQuota-preConsumedQuota < 0 { return 0, 0, service.OpenAIErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden) } err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota) diff --git a/router/api-router.go b/router/api-router.go index 04d3490..592e8ed 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -14,6 +14,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.Use(middleware.GlobalAPIRateLimit()) { apiRouter.GET("/status", controller.GetStatus) + apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) apiRouter.GET("/notice", controller.GetNotice) apiRouter.GET("/about", controller.GetAbout) apiRouter.GET("/midjourney", controller.GetMidjourney) @@ -26,6 +27,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) + apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin) + apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind) userRoute := apiRouter.Group("/user") { @@ -73,7 +76,7 @@ func SetApiRouter(router *gin.Engine) { { channelRoute.GET("/", controller.GetAllChannels) channelRoute.GET("/search", controller.SearchChannels) - channelRoute.GET("/models", controller.ListModels) + channelRoute.GET("/models", controller.ChannelListModels) channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test/:id", controller.TestChannel) diff --git a/service/epay.go b/service/epay.go new file mode 100644 index 0000000..7ce4aad --- /dev/null +++ b/service/epay.go @@ -0,0 +1,10 @@ +package service + +import "one-api/common" + +func GetCallbackAddress() string { + if common.CustomCallbackAddress == "" { + return common.ServerAddress + } + return common.CustomCallbackAddress +} diff --git a/web/package.json b/web/package.json index 009092a..d6d7ad5 100644 --- a/web/package.json +++ b/web/package.json @@ -3,10 +3,10 @@ "version": "0.1.0", "private": true, "dependencies": { - "@douyinfe/semi-ui": "^2.46.1", "@douyinfe/semi-icons": "^2.46.1", - "@visactor/vchart": "~1.8.8", + "@douyinfe/semi-ui": "^2.46.1", "@visactor/react-vchart": "~1.8.8", + "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", "axios": "^0.27.2", "history": "^5.3.0", @@ -17,6 +17,7 @@ "react-fireworks": "^1.0.4", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-telegram-login": "^1.1.2", "react-toastify": "^9.0.8", "react-turnstile": "^1.0.5", "semantic-ui-css": "^2.5.0", diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index 03aec65..eb94784 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -1,14 +1,15 @@ -import React, {useContext, useEffect, useState} from 'react'; -import {Link, useNavigate, useSearchParams} from 'react-router-dom'; -import {UserContext} from '../context/User'; -import {API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning} from '../helpers'; -import {onGitHubOAuthClicked} from './utils'; +import React, { useContext, useEffect, useState } from 'react'; +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import { UserContext } from '../context/User'; +import { API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning } from '../helpers'; +import { onGitHubOAuthClicked } from './utils'; import Turnstile from "react-turnstile"; -import {Layout, Card, Image, Form, Button, Divider, Modal} from "@douyinfe/semi-ui"; +import { Layout, Card, Image, Form, Button, Divider, Modal } from "@douyinfe/semi-ui"; import Title from "@douyinfe/semi-ui/lib/es/typography/title"; import Text from "@douyinfe/semi-ui/lib/es/typography/text"; +import TelegramLoginButton from 'react-telegram-login'; -import {IconGithubLogo} from '@douyinfe/semi-icons'; +import { IconGithubLogo } from '@douyinfe/semi-icons'; const LoginForm = () => { const [inputs, setInputs] = useState({ @@ -18,7 +19,7 @@ const LoginForm = () => { }); const [searchParams, setSearchParams] = useSearchParams(); const [submitted, setSubmitted] = useState(false); - const {username, password} = inputs; + const { username, password } = inputs; const [userState, userDispatch] = useContext(UserContext); const [turnstileEnabled, setTurnstileEnabled] = useState(false); const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); @@ -56,9 +57,9 @@ const LoginForm = () => { const res = await API.get( `/api/oauth/wechat?code=${inputs.wechat_verification_code}` ); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { - userDispatch({type: 'login', payload: data}); + userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); navigate('/'); showSuccess('登录成功!'); @@ -69,7 +70,7 @@ const LoginForm = () => { }; function handleChange(name, value) { - setInputs((inputs) => ({...inputs, [name]: value})); + setInputs((inputs) => ({ ...inputs, [name]: value })); } async function handleSubmit(e) { @@ -83,13 +84,13 @@ const LoginForm = () => { username, password }); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { - userDispatch({type: 'login', payload: data}); + userDispatch({ type: 'login', payload: data }); localStorage.setItem('user', JSON.stringify(data)); showSuccess('登录成功!'); if (username === 'root' && password === '123456') { - Modal.error({title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true}); + Modal.error({ title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true }); } navigate('/token'); } else { @@ -100,16 +101,37 @@ const LoginForm = () => { } } + // 添加Telegram登录处理函数 + const onTelegramLoginClicked = async (response) => { + const fields = ["id", "first_name", "last_name", "username", "photo_url", "auth_date", "hash", "lang"]; + const params = {}; + fields.forEach((field) => { + if (response[field]) { + params[field] = response[field]; + } + }); + const res = await API.get(`/api/oauth/telegram/login`, { params }); + const { success, message, data } = res.data; + if (success) { + userDispatch({ type: 'login', payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + showSuccess('登录成功!'); + navigate('/'); + } else { + showError(message); + } + }; + return (
-
-
+
+
- + <Title heading={2} style={{ textAlign: 'center' }}> 用户登录
@@ -129,12 +151,12 @@ const LoginForm = () => { onChange={(value) => handleChange('password', value)} /> -
-
+
没有账号请先 注册账号 @@ -142,16 +164,16 @@ const LoginForm = () => { 忘记密码 点击重置
- {status.github_oauth || status.wechat_login ? ( + {status.github_oauth || status.wechat_login || status.telegram_oauth ? ( <> 第三方登录 -
+
{status.github_oauth ? (
) : ( @@ -208,7 +236,7 @@ const LoginForm = () => { {/**/} {turnstileEnabled ? ( -
+
{ diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 78d3240..1216b99 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -21,6 +21,7 @@ import {getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor} from import EditToken from "../pages/Token/EditToken"; import EditUser from "../pages/User/EditUser"; import passwordResetConfirm from "./PasswordResetConfirm"; +import TelegramLoginButton from 'react-telegram-login'; const PersonalSetting = () => { const [userState, userDispatch] = useContext(UserContext); @@ -443,6 +444,25 @@ const PersonalSetting = () => {
+
+ Telegram +
+
+ +
+
+ {status.telegram_oauth ? + userState.user.telegram_id !== '' ? + : + : + } +
+
+
+
diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 197050e..e955afe 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,6 +1,6 @@ -import React, {useEffect, useState} from 'react'; -import {Button, Divider, Form, Grid, Header, Modal, Message} from 'semantic-ui-react'; -import {API, removeTrailingSlash, showError, verifyJSON} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { Button, Divider, Form, Grid, Header, Modal, Message } from 'semantic-ui-react'; +import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; const SystemSetting = () => { let [inputs, setInputs] = useState({ @@ -20,8 +20,10 @@ const SystemSetting = () => { EpayId: '', EpayKey: '', Price: 7.3, + MinTopUp: 1, TopupGroupRatio: '', PayAddress: '', + CustomCallbackAddress: '', Footer: '', WeChatAuthEnabled: '', WeChatServerAddress: '', @@ -32,7 +34,11 @@ const SystemSetting = () => { TurnstileSecretKey: '', RegisterEnabled: '', EmailDomainRestrictionEnabled: '', - EmailDomainWhitelist: '' + EmailDomainWhitelist: '', + // telegram login + TelegramOAuthEnabled: '', + TelegramBotToken: '', + TelegramBotName: '', }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); @@ -42,7 +48,7 @@ const SystemSetting = () => { const getOptions = async () => { const res = await API.get('/api/option/'); - const {success, message, data} = res.data; + const { success, message, data } = res.data; if (success) { let newInputs = {}; data.forEach((item) => { @@ -58,7 +64,7 @@ const SystemSetting = () => { setOriginInputs(newInputs); setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => { - return {key: item, text: item, value: item}; + return { key: item, text: item, value: item }; })); } else { showError(message); @@ -77,6 +83,7 @@ const SystemSetting = () => { case 'EmailVerificationEnabled': case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': + case 'TelegramOAuthEnabled': case 'TurnstileCheckEnabled': case 'EmailDomainRestrictionEnabled': case 'RegisterEnabled': @@ -89,7 +96,7 @@ const SystemSetting = () => { key, value }); - const {success, message} = res.data; + const { success, message } = res.data; if (success) { if (key === 'EmailDomainWhitelist') { value = value.split(','); @@ -106,7 +113,7 @@ const SystemSetting = () => { setLoading(false); }; - const handleInputChange = async (e, {name, value}) => { + const handleInputChange = async (e, { name, value }) => { if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') { // block disabling password login setShowPasswordWarningModal(true); @@ -128,9 +135,11 @@ const SystemSetting = () => { name === 'TurnstileSiteKey' || name === 'TurnstileSecretKey' || name === 'EmailDomainWhitelist' || - name === 'TopupGroupRatio' + name === 'TopupGroupRatio' || + name === 'TelegramBotToken' || + name === 'TelegramBotName' ) { - setInputs((inputs) => ({...inputs, [name]: value})); + setInputs((inputs) => ({ ...inputs, [name]: value })); } else { await updateOption(name, value); } @@ -234,6 +243,12 @@ const SystemSetting = () => { } }; + const submitTelegramSettings = async () => { + // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); + await updateOption('TelegramBotToken', inputs.TelegramBotToken); + await updateOption('TelegramBotName', inputs.TelegramBotName); + }; + const submitTurnstile = async () => { if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); @@ -280,7 +295,7 @@ const SystemSetting = () => { 更新服务器地址 -
支付设置(当前仅支持易支付接口,使用上方服务器地址作为回调地址!)
+
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
{ name='EpayKey' onChange={handleInputChange} /> - + + + + @@ -318,7 +350,7 @@ const SystemSetting = () => { label='充值分组倍率' name='TopupGroupRatio' onChange={handleInputChange} - style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}} + style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} autoComplete='new-password' value={inputs.TopupGroupRatio} placeholder='为一个 JSON 文本,键为组名称,值为倍率' @@ -327,7 +359,7 @@ const SystemSetting = () => { 更新支付设置 - +
配置登录注册
{ open={showPasswordWarningModal} onClose={() => setShowPasswordWarningModal(false)} size={'tiny'} - style={{maxWidth: '450px'}} + style={{ maxWidth: '450px' }} > 警告 @@ -386,6 +418,12 @@ const SystemSetting = () => { name='WeChatAuthEnabled' onChange={handleInputChange} /> + { onChange={handleInputChange} /> - +
配置邮箱域名白名单 用以防止恶意用户利用临时邮箱批量注册 @@ -443,13 +481,13 @@ const SystemSetting = () => { autoComplete='new-password' placeholder='输入新的允许的邮箱域名' value={restrictedDomainInput} - onChange={(e, {value}) => { + onChange={(e, { value }) => { setRestrictedDomainInput(value); }} /> 保存邮箱域名白名单设置 - +
配置 SMTP 用以支持系统的邮件发送 @@ -500,7 +538,7 @@ const SystemSetting = () => { /> 保存 SMTP 设置 - +
配置 GitHub OAuth App @@ -538,7 +576,7 @@ const SystemSetting = () => { 保存 GitHub OAuth 设置 - +
配置 WeChat Server @@ -582,7 +620,28 @@ const SystemSetting = () => { 保存 WeChat Server 设置 - + +
配置 Telegram 登录
+ + + + + + 保存 Telegram 登录设置 + +
配置 Turnstile diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 9802db5..62fb0dc 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -132,6 +132,8 @@ export const modelColorMap = { 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝 + 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝 + 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色 diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index f61c38e..876015d 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -241,7 +241,7 @@ const EditChannel = (props) => { const addCustomModel = () => { if (customModel.trim() === '') return; - if (inputs.models.includes(customModel)) return; + if (inputs.models.includes(customModel)) return showError("该模型已存在!"); let localModels = [...inputs.models]; localModels.push(customModel); let localModelOptions = []; @@ -454,7 +454,7 @@ const EditChannel = (props) => { placeholder='输入自定义模型名称' value={customModel} onChange={(value) => { - setCustomModel(value); + setCustomModel(value.trim()); }} />
diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index 4dd3f37..f3e2980 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -5,117 +5,126 @@ import { StatusContext } from '../../context/Status'; import { marked } from 'marked'; const Home = () => { - const [statusState] = useContext(StatusContext); - const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); - const [homePageContent, setHomePageContent] = useState(''); + const [statusState] = useContext(StatusContext); + const [homePageContentLoaded, setHomePageContentLoaded] = useState(false); + const [homePageContent, setHomePageContent] = useState(''); - const displayNotice = async () => { - const res = await API.get('/api/notice'); - const { success, message, data } = res.data; - if (success) { - let oldNotice = localStorage.getItem('notice'); - if (data !== oldNotice && data !== '') { - const htmlNotice = marked(data); - showNotice(htmlNotice, true); - localStorage.setItem('notice', data); - } - } else { - showError(message); - } - }; + const displayNotice = async () => { + const res = await API.get('/api/notice'); + const { success, message, data } = res.data; + if (success) { + let oldNotice = localStorage.getItem('notice'); + if (data !== oldNotice && data !== '') { + const htmlNotice = marked(data); + showNotice(htmlNotice, true); + localStorage.setItem('notice', data); + } + } else { + showError(message); + } + }; - const displayHomePageContent = async () => { - setHomePageContent(localStorage.getItem('home_page_content') || ''); - const res = await API.get('/api/home_page_content'); - const { success, message, data } = res.data; - if (success) { - let content = data; - if (!data.startsWith('https://')) { - content = marked.parse(data); - } - setHomePageContent(content); - localStorage.setItem('home_page_content', content); - } else { - showError(message); - setHomePageContent('加载首页内容失败...'); - } - setHomePageContentLoaded(true); - }; + const displayHomePageContent = async () => { + setHomePageContent(localStorage.getItem('home_page_content') || ''); + const res = await API.get('/api/home_page_content'); + const { success, message, data } = res.data; + if (success) { + let content = data; + if (!data.startsWith('https://')) { + content = marked.parse(data); + } + setHomePageContent(content); + localStorage.setItem('home_page_content', content); + } else { + showError(message); + setHomePageContent('加载首页内容失败...'); + } + setHomePageContentLoaded(true); + }; - const getStartTimeString = () => { - const timestamp = statusState?.status?.start_time; - return statusState.status ? timestamp2string(timestamp) : ''; - }; + const getStartTimeString = () => { + const timestamp = statusState?.status?.start_time; + return statusState.status ? timestamp2string(timestamp) : ''; + }; - useEffect(() => { - displayNotice().then(); - displayHomePageContent().then(); - }, []); - return ( - <> + useEffect(() => { + displayNotice().then(); + displayHomePageContent().then(); + }, []); + return ( + <> + { + homePageContentLoaded && homePageContent === '' ? + <> + + + + 系统信息总览}> +

名称:{statusState?.status?.system_name}

+

版本:{statusState?.status?.version ? statusState?.status?.version : 'unknown'}

+

+ 源码: + + https://github.com/songquanpeng/one-api + +

+

启动时间:{getStartTimeString()}

+
+ + + 系统配置总览}> +

+ 邮箱验证: + {statusState?.status?.email_verification === true ? '已启用' : '未启用'} +

+

+ GitHub 身份验证: + {statusState?.status?.github_oauth === true ? '已启用' : '未启用'} +

+

+ 微信身份验证: + {statusState?.status?.wechat_login === true ? '已启用' : '未启用'} +

+

+ Turnstile 用户校验: + {statusState?.status?.turnstile_check === true ? '已启用' : '未启用'} +

+

+ Telegram 身份验证: + {statusState?.status?.telegram_oauth === true + ? '已启用' : '未启用'} +

+
+ +
+
+ + + : <> { - homePageContentLoaded && homePageContent === '' ? <> - - - - 系统信息总览}> -

名称:{statusState?.status?.system_name}

-

版本:{statusState?.status?.version ? statusState?.status?.version : "unknown"}

-

- 源码: - - https://github.com/songquanpeng/one-api - -

-

启动时间:{getStartTimeString()}

-
- - - 系统配置总览}> -

- 邮箱验证: - {statusState?.status?.email_verification === true ? '已启用': '未启用'} -

-

- GitHub 身份验证: - {statusState?.status?.github_oauth === true ? '已启用' : '未启用'} -

-

- 微信身份验证: - {statusState?.status?.wechat_login === true ? '已启用' : '未启用'} -

-

- Turnstile 用户校验: - {statusState?.status?.turnstile_check === true ? '已启用' : '未启用'} -

-
- -
-
- : <> - { - homePageContent.startsWith('https://') ?