mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-18 03:23:42 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b421478c1 | ||
|
|
569751f5db | ||
|
|
608c945ae6 | ||
|
|
05beade3ad | ||
|
|
81167c4322 | ||
|
|
6fa1837c24 | ||
|
|
ba0c5bb4d9 | ||
|
|
5482fff62d | ||
|
|
1cde58c089 | ||
|
|
a0c4e9f10e | ||
|
|
d6d91e4340 | ||
|
|
f19b11be8c | ||
|
|
1bfc46aa70 | ||
|
|
5c8f8b4901 | ||
|
|
affe5111cc | ||
|
|
0004c1022d | ||
|
|
4274b925da | ||
|
|
166770eebf | ||
|
|
6d0479632a | ||
|
|
364d4f96c7 | ||
|
|
bce8a3e2be | ||
|
|
4920922929 | ||
|
|
70bdb8ca90 | ||
|
|
3dc5d4d95a | ||
|
|
ccaf64bbfc | ||
|
|
ac3e27859c | ||
|
|
e8188902c2 | ||
|
|
1ee8edcfd4 | ||
|
|
a3921ea54d | ||
|
|
705171e495 | ||
|
|
b3f46223a8 | ||
|
|
5b2377eea9 | ||
|
|
c59a33e8e9 | ||
|
|
e485bc7613 | ||
|
|
9bcd24fc2c | ||
|
|
cbdce181af | ||
|
|
800f494698 | ||
|
|
7601610767 | ||
|
|
3a8be505c1 | ||
|
|
51f7ad5de2 | ||
|
|
f73a180fc3 | ||
|
|
e8db0a2c72 |
@@ -1,9 +1,10 @@
|
||||
name: Publish Docker image (amd64, English)
|
||||
name: Publish Docker image (amd64)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '!*-alpha*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
name:
|
||||
@@ -24,21 +25,26 @@ jobs:
|
||||
run: |
|
||||
git describe --tags > VERSION
|
||||
|
||||
- name: Translate
|
||||
run: |
|
||||
python ./i18n/translate.py --repository_path . --json_file_path ./i18n/en.json
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
justsong/one-api-en
|
||||
calciumion/new-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v3
|
||||
2
.github/workflows/docker-image-arm64.yml
vendored
2
.github/workflows/docker-image-arm64.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
justsong/one-api
|
||||
calciumion/new-api
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
|
||||
@@ -21,6 +21,7 @@ var Footer = ""
|
||||
var Logo = ""
|
||||
var TopUpLink = ""
|
||||
var ChatLink = ""
|
||||
var ChatLink2 = ""
|
||||
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
||||
var DisplayInCurrencyEnabled = true
|
||||
var DisplayTokenStatEnabled = true
|
||||
@@ -86,6 +87,7 @@ var QuotaForInviter = 0
|
||||
var QuotaForInvitee = 0
|
||||
var ChannelDisableThreshold = 5.0
|
||||
var AutomaticDisableChannelEnabled = false
|
||||
var AutomaticEnableChannelEnabled = false
|
||||
var QuotaRemindThreshold = 1000
|
||||
var PreConsumedQuota = 500
|
||||
|
||||
|
||||
@@ -68,17 +68,29 @@ func DecodeUrlImageData(imageUrl string) (image.Config, string, error) {
|
||||
SysLog(fmt.Sprintf("fail to get image from url: %s", err.Error()))
|
||||
return image.Config{}, "", err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// 限制读取的字节数,防止下载整个图片
|
||||
limitReader := io.LimitReader(response.Body, 1024*20)
|
||||
//data, err := io.ReadAll(limitReader)
|
||||
//if err != nil {
|
||||
// log.Fatal(err)
|
||||
//}
|
||||
//log.Printf("%x", data)
|
||||
config, format, err := getImageConfig(limitReader)
|
||||
response.Body.Close()
|
||||
return config, format, err
|
||||
var readData []byte
|
||||
for _, limit := range []int64{1024 * 8, 1024 * 24, 1024 * 64} {
|
||||
SysLog(fmt.Sprintf("try to decode image config with limit: %d", limit))
|
||||
|
||||
// 从response.Body读取更多的数据直到达到当前的限制
|
||||
additionalData := make([]byte, limit-int64(len(readData)))
|
||||
n, _ := io.ReadFull(response.Body, additionalData)
|
||||
readData = append(readData, additionalData[:n]...)
|
||||
|
||||
// 使用io.MultiReader组合已经读取的数据和response.Body
|
||||
limitReader := io.MultiReader(bytes.NewReader(readData), response.Body)
|
||||
|
||||
var config image.Config
|
||||
var format string
|
||||
config, format, err = getImageConfig(limitReader)
|
||||
if err == nil {
|
||||
return config, format, nil
|
||||
}
|
||||
}
|
||||
|
||||
return image.Config{}, "", err // 返回最后一个错误
|
||||
}
|
||||
|
||||
func getImageConfig(reader io.Reader) (image.Config, string, error) {
|
||||
|
||||
@@ -23,6 +23,8 @@ var ModelRatio = map[string]float64{
|
||||
"gpt-4-32k-0314": 30,
|
||||
"gpt-4-32k-0613": 30,
|
||||
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-3.5-turbo": 0.75, // $0.0015 / 1K tokens
|
||||
@@ -32,8 +34,9 @@ var ModelRatio = map[string]float64{
|
||||
"gpt-3.5-turbo-16k-0613": 1.5,
|
||||
"gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
|
||||
"gpt-3.5-turbo-1106": 0.5, // $0.001 / 1K tokens
|
||||
"babbage-002": 0.2, // $0.0004 / 1K tokens
|
||||
"davinci-002": 1, // $0.002 / 1K tokens
|
||||
"gpt-3.5-turbo-0125": 0.25,
|
||||
"babbage-002": 0.2, // $0.0004 / 1K tokens
|
||||
"davinci-002": 1, // $0.002 / 1K tokens
|
||||
"text-ada-001": 0.2,
|
||||
"text-babbage-001": 0.25,
|
||||
"text-curie-001": 1,
|
||||
@@ -50,6 +53,8 @@ var ModelRatio = map[string]float64{
|
||||
"curie": 10,
|
||||
"babbage": 10,
|
||||
"ada": 10,
|
||||
"text-embedding-3-small": 0.01,
|
||||
"text-embedding-3-large": 0.065,
|
||||
"text-embedding-ada-002": 0.05,
|
||||
"text-search-ada-doc-001": 10,
|
||||
"text-moderation-stable": 0.1,
|
||||
@@ -103,13 +108,15 @@ func UpdateModelPriceByJSONString(jsonStr string) error {
|
||||
return json.Unmarshal([]byte(jsonStr), &ModelPrice)
|
||||
}
|
||||
|
||||
func GetModelPrice(name string) float64 {
|
||||
func GetModelPrice(name string, printErr bool) float64 {
|
||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
||||
name = "gpt-4-gizmo-*"
|
||||
}
|
||||
price, ok := ModelPrice[name]
|
||||
if !ok {
|
||||
//SysError("model price not found: " + name)
|
||||
if printErr {
|
||||
SysError("model price not found: " + name)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return price
|
||||
@@ -142,6 +149,9 @@ func GetModelRatio(name string) float64 {
|
||||
|
||||
func GetCompletionRatio(name string) float64 {
|
||||
if strings.HasPrefix(name, "gpt-3.5") {
|
||||
if strings.HasSuffix(name, "0125") {
|
||||
return 3
|
||||
}
|
||||
if strings.HasSuffix(name, "1106") {
|
||||
return 2
|
||||
}
|
||||
|
||||
@@ -57,12 +57,51 @@ func RedisGet(key string) (string, error) {
|
||||
return RDB.Get(ctx, key).Result()
|
||||
}
|
||||
|
||||
func RedisExpire(key string, expiration time.Duration) error {
|
||||
ctx := context.Background()
|
||||
return RDB.Expire(ctx, key, expiration).Err()
|
||||
}
|
||||
|
||||
func RedisGetEx(key string, expiration time.Duration) (string, error) {
|
||||
ctx := context.Background()
|
||||
return RDB.GetSet(ctx, key, expiration).Result()
|
||||
}
|
||||
|
||||
func RedisDel(key string) error {
|
||||
ctx := context.Background()
|
||||
return RDB.Del(ctx, key).Err()
|
||||
}
|
||||
|
||||
func RedisDecrease(key string, value int64) error {
|
||||
ctx := context.Background()
|
||||
return RDB.DecrBy(ctx, key, value).Err()
|
||||
|
||||
// 检查键的剩余生存时间
|
||||
ttlCmd := RDB.TTL(context.Background(), key)
|
||||
ttl, err := ttlCmd.Result()
|
||||
if err != nil {
|
||||
// 失败则尝试直接减少
|
||||
return RDB.DecrBy(context.Background(), key, value).Err()
|
||||
}
|
||||
|
||||
// 如果剩余生存时间大于0,则进行减少操作
|
||||
if ttl > 0 {
|
||||
ctx := context.Background()
|
||||
// 开始一个Redis事务
|
||||
txn := RDB.TxPipeline()
|
||||
|
||||
// 减少余额
|
||||
decrCmd := txn.DecrBy(ctx, key, value)
|
||||
if err := decrCmd.Err(); err != nil {
|
||||
return err // 如果减少失败,则直接返回错误
|
||||
}
|
||||
|
||||
// 重新设置过期时间,使用原来的过期时间
|
||||
txn.Expire(ctx, key, ttl)
|
||||
|
||||
// 执行事务
|
||||
_, err = txn.Exec(ctx)
|
||||
return err
|
||||
} else {
|
||||
_ = RedisDel(key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func testChannel(channel *model.Channel, request ChatRequest) (err error, openaiErr *OpenAIError) {
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s", channel.Id, request.Model))
|
||||
switch channel.Type {
|
||||
case common.ChannelTypePaLM:
|
||||
fallthrough
|
||||
@@ -34,23 +35,27 @@ func testChannel(channel *model.Channel, request ChatRequest) (err error, openai
|
||||
case common.ChannelTypeXunfei:
|
||||
return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil
|
||||
case common.ChannelTypeAzure:
|
||||
request.Model = "gpt-35-turbo"
|
||||
if request.Model == "" {
|
||||
request.Model = "gpt-35-turbo"
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = errors.New("请确保已在 Azure 上创建了 gpt-35-turbo 模型,并且 apiVersion 已正确填写!")
|
||||
}
|
||||
}()
|
||||
default:
|
||||
request.Model = "gpt-3.5-turbo"
|
||||
}
|
||||
requestURL := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.Type == common.ChannelTypeAzure {
|
||||
requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.GetBaseURL(), request.Model)
|
||||
} else {
|
||||
if channel.GetBaseURL() != "" {
|
||||
requestURL = channel.GetBaseURL()
|
||||
if request.Model == "" {
|
||||
request.Model = "gpt-3.5-turbo"
|
||||
}
|
||||
requestURL += "/v1/chat/completions"
|
||||
}
|
||||
baseUrl := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() != "" {
|
||||
baseUrl = channel.GetBaseURL()
|
||||
}
|
||||
requestURL := getFullRequestURL(baseUrl, "/v1/chat/completions", channel.Type)
|
||||
|
||||
if channel.Type == common.ChannelTypeAzure {
|
||||
requestURL = getFullRequestURL(channel.GetBaseURL(), fmt.Sprintf("/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", request.Model), channel.Type)
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(request)
|
||||
@@ -78,6 +83,9 @@ func testChannel(channel *model.Channel, request ChatRequest) (err error, openai
|
||||
return err, nil
|
||||
}
|
||||
if response.Usage.CompletionTokens == 0 {
|
||||
if response.Error.Message == "" {
|
||||
response.Error.Message = "补全 tokens 非预期返回 0"
|
||||
}
|
||||
return errors.New(fmt.Sprintf("type %s, code %v, message %s", response.Error.Type, response.Error.Code, response.Error.Message)), &response.Error
|
||||
}
|
||||
return nil, nil
|
||||
@@ -106,6 +114,7 @@ func TestChannel(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
testModel := c.Query("model")
|
||||
channel, err := model.GetChannelById(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -115,6 +124,9 @@ func TestChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
testRequest := buildTestRequest()
|
||||
if testModel != "" {
|
||||
testRequest.Model = testModel
|
||||
}
|
||||
tik := time.Now()
|
||||
err, _ = testChannel(channel, *testRequest)
|
||||
tok := time.Now()
|
||||
@@ -142,12 +154,23 @@ var testAllChannelsRunning bool = false
|
||||
|
||||
// disable & notify
|
||||
func disableChannel(channelId int, channelName string, reason string) {
|
||||
if common.RootUserEmail == "" {
|
||||
common.RootUserEmail = model.GetRootUserEmail()
|
||||
}
|
||||
model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
|
||||
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
|
||||
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
|
||||
notifyRootUser(subject, content)
|
||||
}
|
||||
|
||||
func enableChannel(channelId int, channelName string) {
|
||||
model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled)
|
||||
subject := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
|
||||
content := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId)
|
||||
notifyRootUser(subject, content)
|
||||
}
|
||||
|
||||
func notifyRootUser(subject string, content string) {
|
||||
if common.RootUserEmail == "" {
|
||||
common.RootUserEmail = model.GetRootUserEmail()
|
||||
}
|
||||
err := common.SendEmail(subject, common.RootUserEmail, content)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("failed to send email: %s", err.Error()))
|
||||
@@ -176,9 +199,7 @@ func testAllChannels(notify bool) error {
|
||||
}
|
||||
go func() {
|
||||
for _, channel := range channels {
|
||||
if channel.Status != common.ChannelStatusEnabled {
|
||||
continue
|
||||
}
|
||||
isChannelEnabled := channel.Status == common.ChannelStatusEnabled
|
||||
tik := time.Now()
|
||||
err, openaiErr := testChannel(channel, *testRequest)
|
||||
tok := time.Now()
|
||||
@@ -197,9 +218,12 @@ func testAllChannels(notify bool) error {
|
||||
if channel.AutoBan != nil && *channel.AutoBan == 0 {
|
||||
ban = false
|
||||
}
|
||||
if shouldDisableChannel(openaiErr, -1) && ban {
|
||||
if isChannelEnabled && shouldDisableChannel(openaiErr, -1) && ban {
|
||||
disableChannel(channel.Id, channel.Name, err.Error())
|
||||
}
|
||||
if !isChannelEnabled && shouldEnableChannel(err, openaiErr) {
|
||||
enableChannel(channel.Id, channel.Name)
|
||||
}
|
||||
channel.UpdateResponseTime(milliseconds)
|
||||
time.Sleep(common.RequestInterval)
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@ func GetStatus(c *gin.Context) {
|
||||
"turnstile_site_key": common.TurnstileSiteKey,
|
||||
"top_up_link": common.TopUpLink,
|
||||
"chat_link": common.ChatLink,
|
||||
"chat_link2": common.ChatLink2,
|
||||
"quota_per_unit": common.QuotaPerUnit,
|
||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||
"enable_batch_update": common.BatchUpdateEnabled,
|
||||
"enable_drawing": common.DrawingEnabled,
|
||||
"enable_data_export": common.DataExportEnabled,
|
||||
"data_export_default_time": common.DataExportDefaultTime,
|
||||
"enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "",
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
@@ -180,6 +180,15 @@ func init() {
|
||||
Root: "gpt-3.5-turbo-1106",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "gpt-3.5-turbo-0125",
|
||||
Object: "model",
|
||||
Created: 1706232090,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "gpt-3.5-turbo-0125",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "gpt-3.5-turbo-instruct",
|
||||
Object: "model",
|
||||
@@ -252,6 +261,24 @@ func init() {
|
||||
Root: "gpt-4-1106-preview",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "gpt-4-0125-preview",
|
||||
Object: "model",
|
||||
Created: 1699593571,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "gpt-4-0125-preview",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "gpt-4-turbo-preview",
|
||||
Object: "model",
|
||||
Created: 1699593571,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "gpt-4-turbo-preview",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "gpt-4-vision-preview",
|
||||
Object: "model",
|
||||
@@ -270,6 +297,24 @@ func init() {
|
||||
Root: "gpt-4-1106-vision-preview",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-embedding-3-small",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-embedding-ada-002",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-embedding-3-large",
|
||||
Object: "model",
|
||||
Created: 1677649963,
|
||||
OwnedBy: "openai",
|
||||
Permission: permission,
|
||||
Root: "text-embedding-ada-002",
|
||||
Parent: nil,
|
||||
},
|
||||
{
|
||||
Id: "text-embedding-ada-002",
|
||||
Object: "model",
|
||||
|
||||
@@ -88,17 +88,17 @@ func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest {
|
||||
message := request.Messages[i]
|
||||
if message.Role == "system" {
|
||||
messages = append(messages, AliMessage{
|
||||
User: string(message.Content),
|
||||
User: message.StringContent(),
|
||||
Bot: "Okay",
|
||||
})
|
||||
continue
|
||||
} else {
|
||||
if i == len(request.Messages)-1 {
|
||||
prompt = string(message.Content)
|
||||
prompt = message.StringContent()
|
||||
break
|
||||
}
|
||||
messages = append(messages, AliMessage{
|
||||
User: string(message.Content),
|
||||
User: message.StringContent(),
|
||||
Bot: string(request.Messages[i+1].Content),
|
||||
})
|
||||
i++
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var availableVoices = []string{
|
||||
@@ -24,12 +25,12 @@ var availableVoices = []string{
|
||||
}
|
||||
|
||||
func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
|
||||
tokenId := c.GetInt("token_id")
|
||||
channelType := c.GetInt("channel")
|
||||
channelId := c.GetInt("channel_id")
|
||||
userId := c.GetInt("id")
|
||||
group := c.GetString("group")
|
||||
startTime := time.Now()
|
||||
|
||||
var audioRequest AudioRequest
|
||||
if !strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") {
|
||||
@@ -154,6 +155,7 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
|
||||
defer func(ctx context.Context) {
|
||||
go func() {
|
||||
useTimeSeconds := time.Now().Unix() - startTime.Unix()
|
||||
quota := 0
|
||||
var promptTokens = 0
|
||||
if strings.HasPrefix(audioRequest.Model, "tts-1") {
|
||||
@@ -178,7 +180,7 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
if quota != 0 {
|
||||
tokenName := c.GetString("token_name")
|
||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId, userQuota)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, 0, audioRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
channelId := c.GetInt("channel_id")
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
|
||||
@@ -89,7 +89,7 @@ func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
||||
if message.Role == "system" {
|
||||
messages = append(messages, BaiduMessage{
|
||||
Role: "user",
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
messages = append(messages, BaiduMessage{
|
||||
Role: "assistant",
|
||||
@@ -98,7 +98,7 @@ func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest {
|
||||
} else {
|
||||
messages = append(messages, BaiduMessage{
|
||||
Role: message.Role,
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func requestOpenAI2Gemini(textRequest GeneralOpenAIRequest) *GeminiChatRequest {
|
||||
Role: message.Role,
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: string(message.Content),
|
||||
Text: message.StringContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
@@ -21,6 +22,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
userId := c.GetInt("id")
|
||||
consumeQuota := c.GetBool("consume_quota")
|
||||
group := c.GetString("group")
|
||||
startTime := time.Now()
|
||||
|
||||
var imageRequest ImageRequest
|
||||
if consumeQuota {
|
||||
@@ -169,6 +171,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
|
||||
var textResponse ImageResponse
|
||||
defer func(ctx context.Context) {
|
||||
useTimeSeconds := time.Now().Unix() - startTime.Unix()
|
||||
if consumeQuota {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
@@ -184,7 +187,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
if quota != 0 {
|
||||
tokenName := c.GetString("token_name")
|
||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageRequest.Model, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), false)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
channelId := c.GetInt("channel_id")
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
|
||||
@@ -392,7 +392,7 @@ func relayMidjourneySubmit(c *gin.Context, relayMode int) *MidjourneyResponse {
|
||||
requestBody = c.Request.Body
|
||||
}
|
||||
mjAction := "mj_" + strings.ToLower(midjRequest.Action)
|
||||
modelPrice := common.GetModelPrice(mjAction)
|
||||
modelPrice := common.GetModelPrice(mjAction, true)
|
||||
// 如果没有配置价格,则使用默认价格
|
||||
if modelPrice == -1 {
|
||||
defaultPrice, ok := DefaultModelPrice[mjAction]
|
||||
@@ -478,7 +478,7 @@ func relayMidjourneySubmit(c *gin.Context, relayMode int) *MidjourneyResponse {
|
||||
if quota != 0 {
|
||||
tokenName := c.GetString("token_name")
|
||||
logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, midjRequest.Action)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageModel, tokenName, quota, logContent, tokenId, userQuota)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, 0, 0, imageModel, tokenName, quota, logContent, tokenId, userQuota, 0, false)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
channelId := c.GetInt("channel_id")
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
|
||||
@@ -59,7 +59,7 @@ func requestOpenAI2PaLM(textRequest GeneralOpenAIRequest) *PaLMChatRequest {
|
||||
}
|
||||
for _, message := range textRequest.Messages {
|
||||
palmMessage := PaLMChatMessage{
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
}
|
||||
if message.Role == "user" {
|
||||
palmMessage.Author = "0"
|
||||
|
||||
@@ -84,7 +84,7 @@ func requestOpenAI2Tencent(request GeneralOpenAIRequest) *TencentChatRequest {
|
||||
if message.Role == "system" {
|
||||
messages = append(messages, TencentMessage{
|
||||
Role: "user",
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
messages = append(messages, TencentMessage{
|
||||
Role: "assistant",
|
||||
@@ -93,7 +93,7 @@ func requestOpenAI2Tencent(request GeneralOpenAIRequest) *TencentChatRequest {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, TencentMessage{
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
Role: message.Role,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -149,7 +149,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
model_ = strings.TrimSuffix(model_, "-0301")
|
||||
model_ = strings.TrimSuffix(model_, "-0314")
|
||||
model_ = strings.TrimSuffix(model_, "-0613")
|
||||
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task)
|
||||
requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task)
|
||||
fullRequestURL = getFullRequestURL(baseURL, requestURL, channelType)
|
||||
}
|
||||
case APITypeClaude:
|
||||
fullRequestURL = "https://api.anthropic.com/v1/complete"
|
||||
@@ -232,7 +233,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
case RelayModeModerations:
|
||||
promptTokens = countTokenInput(textRequest.Input, textRequest.Model)
|
||||
}
|
||||
modelPrice := common.GetModelPrice(textRequest.Model)
|
||||
modelPrice := common.GetModelPrice(textRequest.Model, false)
|
||||
groupRatio := common.GetGroupRatio(group)
|
||||
|
||||
var preConsumedQuota int
|
||||
@@ -474,6 +475,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
defer func(ctx context.Context) {
|
||||
// c.Writer.Flush()
|
||||
go func() {
|
||||
useTimeSeconds := time.Now().Unix() - startTime.Unix()
|
||||
promptTokens = textResponse.Usage.PromptTokens
|
||||
completionTokens = textResponse.Usage.CompletionTokens
|
||||
|
||||
@@ -489,38 +491,41 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
|
||||
quota = int(modelPrice * common.QuotaPerUnit * groupRatio)
|
||||
}
|
||||
totalTokens := promptTokens + completionTokens
|
||||
var logContent string
|
||||
if modelPrice == -1 {
|
||||
logContent = fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||
} else {
|
||||
logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f", modelPrice, groupRatio)
|
||||
}
|
||||
|
||||
// record all the consume log even if quota is 0
|
||||
if totalTokens == 0 {
|
||||
// in this case, must be some error happened
|
||||
// we cannot just return, because we may have to return the pre-consumed quota
|
||||
quota = 0
|
||||
}
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
err = model.CacheUpdateUserQuota(userId)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error update user quota cache: "+err.Error())
|
||||
logContent += fmt.Sprintf("(有疑问请联系管理员)")
|
||||
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, tokenId %d, model %s, pre-consumed quota %d", userId, channelId, tokenId, textRequest.Model, preConsumedQuota))
|
||||
} else {
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
err = model.CacheUpdateUserQuota(userId)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error update user quota cache: "+err.Error())
|
||||
}
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
}
|
||||
|
||||
// record all the consume log even if quota is 0
|
||||
useTimeSeconds := time.Now().Unix() - startTime.Unix()
|
||||
var logContent string
|
||||
if modelPrice == -1 {
|
||||
logContent = fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,用时 %d秒", modelRatio, groupRatio, useTimeSeconds)
|
||||
} else {
|
||||
logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f,用时 %d秒", modelPrice, groupRatio, useTimeSeconds)
|
||||
}
|
||||
logModel := textRequest.Model
|
||||
if strings.HasPrefix(logModel, "gpt-4-gizmo") {
|
||||
logModel = "gpt-4-gizmo-*"
|
||||
logContent += fmt.Sprintf(",模型 %s", textRequest.Model)
|
||||
}
|
||||
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, completionTokens, logModel, tokenName, quota, logContent, tokenId, userQuota, int(useTimeSeconds), isStream)
|
||||
|
||||
model.RecordConsumeLog(ctx, userId, channelId, promptTokens, completionTokens, logModel, tokenName, quota, logContent, tokenId, userQuota)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, quota)
|
||||
model.UpdateChannelUsedQuota(channelId, quota)
|
||||
//if quota != 0 {
|
||||
//
|
||||
//}
|
||||
|
||||
@@ -145,47 +145,48 @@ func countTokenMessages(messages []Message, model string) (int, error) {
|
||||
for _, message := range messages {
|
||||
tokenNum += tokensPerMessage
|
||||
tokenNum += getTokenNum(tokenEncoder, message.Role)
|
||||
var arrayContent []MediaMessage
|
||||
if err := json.Unmarshal(message.Content, &arrayContent); err != nil {
|
||||
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(message.Content, &stringContent); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
tokenNum += getTokenNum(tokenEncoder, stringContent)
|
||||
if message.Name != nil {
|
||||
tokenNum += tokensPerName
|
||||
tokenNum += getTokenNum(tokenEncoder, *message.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, m := range arrayContent {
|
||||
if m.Type == "image_url" {
|
||||
var imageTokenNum int
|
||||
if str, ok := m.ImageUrl.(string); ok {
|
||||
imageTokenNum, err = getImageToken(&MessageImageUrl{Url: str, Detail: "auto"})
|
||||
} else {
|
||||
imageUrlMap := m.ImageUrl.(map[string]interface{})
|
||||
detail, ok := imageUrlMap["detail"]
|
||||
if ok {
|
||||
imageUrlMap["detail"] = detail.(string)
|
||||
} else {
|
||||
imageUrlMap["detail"] = "auto"
|
||||
}
|
||||
imageUrl := MessageImageUrl{
|
||||
Url: imageUrlMap["url"].(string),
|
||||
Detail: imageUrlMap["detail"].(string),
|
||||
}
|
||||
imageTokenNum, err = getImageToken(&imageUrl)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tokenNum += imageTokenNum
|
||||
log.Printf("image token num: %d", imageTokenNum)
|
||||
if len(message.Content) > 0 {
|
||||
var arrayContent []MediaMessage
|
||||
if err := json.Unmarshal(message.Content, &arrayContent); err != nil {
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(message.Content, &stringContent); err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
tokenNum += getTokenNum(tokenEncoder, m.Text)
|
||||
tokenNum += getTokenNum(tokenEncoder, stringContent)
|
||||
if message.Name != nil {
|
||||
tokenNum += tokensPerName
|
||||
tokenNum += getTokenNum(tokenEncoder, *message.Name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, m := range arrayContent {
|
||||
if m.Type == "image_url" {
|
||||
var imageTokenNum int
|
||||
if str, ok := m.ImageUrl.(string); ok {
|
||||
imageTokenNum, err = getImageToken(&MessageImageUrl{Url: str, Detail: "auto"})
|
||||
} else {
|
||||
imageUrlMap := m.ImageUrl.(map[string]interface{})
|
||||
detail, ok := imageUrlMap["detail"]
|
||||
if ok {
|
||||
imageUrlMap["detail"] = detail.(string)
|
||||
} else {
|
||||
imageUrlMap["detail"] = "auto"
|
||||
}
|
||||
imageUrl := MessageImageUrl{
|
||||
Url: imageUrlMap["url"].(string),
|
||||
Detail: imageUrlMap["detail"].(string),
|
||||
}
|
||||
imageTokenNum, err = getImageToken(&imageUrl)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
tokenNum += imageTokenNum
|
||||
log.Printf("image token num: %d", imageTokenNum)
|
||||
} else {
|
||||
tokenNum += getTokenNum(tokenEncoder, m.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +258,19 @@ func shouldDisableChannel(err *OpenAIError, statusCode int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldEnableChannel(err error, openAIErr *OpenAIError) bool {
|
||||
if !common.AutomaticEnableChannelEnabled {
|
||||
return false
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if openAIErr != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setEventStreamHeaders(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
@@ -294,9 +308,13 @@ func relayErrorHandler(resp *http.Response) (openAIErrorWithStatusCode *OpenAIEr
|
||||
|
||||
func getFullRequestURL(baseURL string, requestURL string, channelType int) string {
|
||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||
if channelType == common.ChannelTypeOpenAI {
|
||||
if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") {
|
||||
|
||||
if strings.HasPrefix(baseURL, "https://gateway.ai.cloudflare.com") {
|
||||
switch channelType {
|
||||
case common.ChannelTypeOpenAI:
|
||||
fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/v1"))
|
||||
case common.ChannelTypeAzure:
|
||||
fullRequestURL = fmt.Sprintf("%s%s", baseURL, strings.TrimPrefix(requestURL, "/openai/deployments"))
|
||||
}
|
||||
}
|
||||
return fullRequestURL
|
||||
|
||||
@@ -81,7 +81,7 @@ func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, doma
|
||||
if message.Role == "system" {
|
||||
messages = append(messages, XunfeiMessage{
|
||||
Role: "user",
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
messages = append(messages, XunfeiMessage{
|
||||
Role: "assistant",
|
||||
@@ -90,7 +90,7 @@ func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, doma
|
||||
} else {
|
||||
messages = append(messages, XunfeiMessage{
|
||||
Role: message.Role,
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
||||
if message.Role == "system" {
|
||||
messages = append(messages, ZhipuMessage{
|
||||
Role: "system",
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
messages = append(messages, ZhipuMessage{
|
||||
Role: "user",
|
||||
@@ -123,7 +123,7 @@ func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest {
|
||||
} else {
|
||||
messages = append(messages, ZhipuMessage{
|
||||
Role: message.Role,
|
||||
Content: string(message.Content),
|
||||
Content: message.StringContent(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Role string `json:"role"`
|
||||
Content json.RawMessage `json:"content"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
ToolCalls any `json:"tool_calls,omitempty"`
|
||||
ToolCallId string `json:"tool_call_id,omitempty"`
|
||||
}
|
||||
|
||||
type MediaMessage struct {
|
||||
@@ -34,6 +36,14 @@ const (
|
||||
ContentTypeImageURL = "image_url"
|
||||
)
|
||||
|
||||
func (m Message) StringContent() string {
|
||||
var stringContent string
|
||||
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
|
||||
return stringContent
|
||||
}
|
||||
return string(m.Content)
|
||||
}
|
||||
|
||||
func (m Message) ParseContent() []MediaMessage {
|
||||
var contentList []MediaMessage
|
||||
var stringContent string
|
||||
@@ -130,6 +140,8 @@ type GeneralOpenAIRequest struct {
|
||||
Tools any `json:"tools,omitempty"`
|
||||
ToolChoice any `json:"tool_choice,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
LogProbs bool `json:"logprobs,omitempty"`
|
||||
TopLogProbs int `json:"top_logprobs,omitempty"`
|
||||
}
|
||||
|
||||
func (r GeneralOpenAIRequest) ParseInput() []string {
|
||||
@@ -397,12 +409,13 @@ func RelayMidjourney(c *gin.Context) {
|
||||
if err.Code == 30 {
|
||||
err.Result = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。"
|
||||
}
|
||||
c.JSON(400, gin.H{
|
||||
"error": err.Description + " " + err.Result,
|
||||
c.JSON(429, gin.H{
|
||||
"error": fmt.Sprintf("%s %s", err.Description, err.Result),
|
||||
"type": "upstream_error",
|
||||
})
|
||||
}
|
||||
channelId := c.GetInt("channel_id")
|
||||
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Result))
|
||||
common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, fmt.Sprintf("%s %s", err.Description, err.Result)))
|
||||
//if shouldDisableChannel(&err.OpenAIError) {
|
||||
// channelId := c.GetInt("channel_id")
|
||||
// channelName := c.GetString("channel_name")
|
||||
|
||||
3
main.go
3
main.go
@@ -63,6 +63,9 @@ func main() {
|
||||
common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
|
||||
model.InitChannelCache()
|
||||
}
|
||||
if common.RedisEnabled {
|
||||
go model.SyncTokenCache(common.SyncFrequency)
|
||||
}
|
||||
if common.MemoryCacheEnabled {
|
||||
go model.SyncOptions(common.SyncFrequency)
|
||||
go model.SyncChannelCache(common.SyncFrequency)
|
||||
|
||||
@@ -18,8 +18,6 @@ type ModelRequest struct {
|
||||
func Distribute() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
||||
c.Set("group", userGroup)
|
||||
var channel *model.Channel
|
||||
channelId, ok := c.Get("channelId")
|
||||
if ok {
|
||||
@@ -50,7 +48,7 @@ func Distribute() func(c *gin.Context) {
|
||||
err = common.UnmarshalBodyReusable(c, &modelRequest)
|
||||
}
|
||||
if err != nil {
|
||||
abortWithMessage(c, http.StatusBadRequest, "无效的请求: "+err.Error())
|
||||
abortWithMessage(c, http.StatusBadRequest, "无效的请求, "+err.Error())
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") {
|
||||
@@ -98,6 +96,10 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
||||
c.Set("group", userGroup)
|
||||
|
||||
channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model)
|
||||
if err != nil {
|
||||
message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model)
|
||||
|
||||
@@ -20,34 +20,88 @@ var (
|
||||
UserId2StatusCacheSeconds = common.SyncFrequency
|
||||
)
|
||||
|
||||
func CacheGetTokenByKey(key string) (*Token, error) {
|
||||
keyCol := "`key`"
|
||||
if common.UsingPostgreSQL {
|
||||
keyCol = `"key"`
|
||||
}
|
||||
var token Token
|
||||
// 仅用于定时同步缓存
|
||||
var token2UserId = make(map[string]int)
|
||||
var token2UserIdLock sync.RWMutex
|
||||
|
||||
func cacheSetToken(token *Token) error {
|
||||
if !common.RedisEnabled {
|
||||
err := DB.Where(keyCol+" = ?", key).First(&token).Error
|
||||
return &token, err
|
||||
return token.SelectUpdate()
|
||||
}
|
||||
jsonBytes, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = common.RedisSet(fmt.Sprintf("token:%s", token.Key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("failed to set token %s to redis: %s", token.Key, err.Error()))
|
||||
return err
|
||||
}
|
||||
token2UserIdLock.Lock()
|
||||
defer token2UserIdLock.Unlock()
|
||||
token2UserId[token.Key] = token.UserId
|
||||
return nil
|
||||
}
|
||||
|
||||
// CacheGetTokenByKey 从缓存中获取 token 并续期时间,如果缓存中不存在,则从数据库中获取
|
||||
func CacheGetTokenByKey(key string) (*Token, error) {
|
||||
if !common.RedisEnabled {
|
||||
return GetTokenByKey(key)
|
||||
}
|
||||
var token *Token
|
||||
tokenObjectString, err := common.RedisGet(fmt.Sprintf("token:%s", key))
|
||||
if err != nil {
|
||||
err := DB.Where(keyCol+" = ?", key).First(&token).Error
|
||||
// 如果缓存中不存在,则从数据库中获取
|
||||
token, err = GetTokenByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jsonBytes, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = common.RedisSet(fmt.Sprintf("token:%s", key), string(jsonBytes), time.Duration(TokenCacheSeconds)*time.Second)
|
||||
if err != nil {
|
||||
common.SysError("Redis set token error: " + err.Error())
|
||||
}
|
||||
return &token, nil
|
||||
err = cacheSetToken(token)
|
||||
return token, nil
|
||||
}
|
||||
// 如果缓存中存在,则续期时间
|
||||
err = common.RedisExpire(fmt.Sprintf("token:%s", key), time.Duration(TokenCacheSeconds)*time.Second)
|
||||
err = json.Unmarshal([]byte(tokenObjectString), &token)
|
||||
return &token, err
|
||||
return token, err
|
||||
}
|
||||
|
||||
func SyncTokenCache(frequency int) {
|
||||
for {
|
||||
time.Sleep(time.Duration(frequency) * time.Second)
|
||||
common.SysLog("syncing tokens from database")
|
||||
token2UserIdLock.Lock()
|
||||
// 从token2UserId中获取所有的key
|
||||
var copyToken2UserId = make(map[string]int)
|
||||
for s, i := range token2UserId {
|
||||
copyToken2UserId[s] = i
|
||||
}
|
||||
token2UserId = make(map[string]int)
|
||||
token2UserIdLock.Unlock()
|
||||
|
||||
for key := range copyToken2UserId {
|
||||
token, err := GetTokenByKey(key)
|
||||
if err != nil {
|
||||
// 如果数据库中不存在,则删除缓存
|
||||
common.SysError(fmt.Sprintf("failed to get token %s from database: %s", key, err.Error()))
|
||||
//delete redis
|
||||
err := common.RedisDel(fmt.Sprintf("token:%s", key))
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("failed to delete token %s from redis: %s", key, err.Error()))
|
||||
}
|
||||
} else {
|
||||
// 如果数据库中存在,先检查redis
|
||||
_, err := common.RedisGet(fmt.Sprintf("token:%s", key))
|
||||
if err != nil {
|
||||
// 如果redis中不存在,则跳过
|
||||
continue
|
||||
}
|
||||
err = cacheSetToken(token)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("failed to update token %s to redis: %s", key, err.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CacheGetUserGroup(id int) (group string, err error) {
|
||||
|
||||
@@ -20,6 +20,8 @@ type Log struct {
|
||||
Quota int `json:"quota" gorm:"default:0"`
|
||||
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
|
||||
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
|
||||
UseTime int `json:"use_time" gorm:"default:0"`
|
||||
IsStream bool `json:"is_stream" gorm:"default:false"`
|
||||
ChannelId int `json:"channel" gorm:"index"`
|
||||
TokenId int `json:"token_id" gorm:"default:0;index"`
|
||||
}
|
||||
@@ -55,7 +57,7 @@ func RecordLog(userId int, logType int, content string) {
|
||||
}
|
||||
}
|
||||
|
||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int) {
|
||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int, isStream bool) {
|
||||
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, 用户调用前余额=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, userQuota, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
|
||||
if !common.LogConsumeEnabled {
|
||||
return
|
||||
@@ -74,6 +76,8 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
|
||||
Quota: quota,
|
||||
ChannelId: channelId,
|
||||
TokenId: tokenId,
|
||||
UseTime: useTimeSeconds,
|
||||
IsStream: isStream,
|
||||
}
|
||||
err := DB.Create(log).Error
|
||||
if err != nil {
|
||||
|
||||
@@ -34,6 +34,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
||||
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
||||
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
|
||||
common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
|
||||
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
|
||||
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
|
||||
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
|
||||
@@ -76,6 +77,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
common.OptionMap["ChatLink2"] = common.ChatLink2
|
||||
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
|
||||
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
|
||||
common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval)
|
||||
@@ -157,6 +159,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.EmailDomainRestrictionEnabled = boolValue
|
||||
case "AutomaticDisableChannelEnabled":
|
||||
common.AutomaticDisableChannelEnabled = boolValue
|
||||
case "AutomaticEnableChannelEnabled":
|
||||
common.AutomaticEnableChannelEnabled = boolValue
|
||||
case "LogConsumeEnabled":
|
||||
common.LogConsumeEnabled = boolValue
|
||||
case "DisplayInCurrencyEnabled":
|
||||
@@ -241,6 +245,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.TopUpLink = value
|
||||
case "ChatLink":
|
||||
common.ChatLink = value
|
||||
case "ChatLink2":
|
||||
common.ChatLink2 = value
|
||||
case "ChannelDisableThreshold":
|
||||
common.ChannelDisableThreshold, _ = strconv.ParseFloat(value, 64)
|
||||
case "QuotaPerUnit":
|
||||
|
||||
@@ -47,7 +47,9 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
token, err = CacheGetTokenByKey(key)
|
||||
if err == nil {
|
||||
if token.Status == common.TokenStatusExhausted {
|
||||
return nil, errors.New("该令牌额度已用尽 token.Status == common.TokenStatusExhausted " + key)
|
||||
keyPrefix := key[:3]
|
||||
keySuffix := key[len(key)-3:]
|
||||
return nil, errors.New("该令牌额度已用尽 TokenStatusExhausted[sk-" + keyPrefix + "***" + keySuffix + "]")
|
||||
} else if token.Status == common.TokenStatusExpired {
|
||||
return nil, errors.New("该令牌已过期")
|
||||
}
|
||||
@@ -73,7 +75,9 @@ func ValidateUserToken(key string) (token *Token, err error) {
|
||||
common.SysError("failed to update token status" + err.Error())
|
||||
}
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("%s 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", token.Key, token.RemainQuota))
|
||||
keyPrefix := key[:3]
|
||||
keySuffix := key[len(key)-3:]
|
||||
return nil, errors.New(fmt.Sprintf("[sk-%s***%s] 该令牌额度已用尽 !token.UnlimitedQuota && token.RemainQuota = %d", keyPrefix, keySuffix, token.RemainQuota))
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
@@ -100,6 +104,16 @@ func GetTokenById(id int) (*Token, error) {
|
||||
return &token, err
|
||||
}
|
||||
|
||||
func GetTokenByKey(key string) (*Token, error) {
|
||||
keyCol := "`key`"
|
||||
if common.UsingPostgreSQL {
|
||||
keyCol = `"key"`
|
||||
}
|
||||
var token Token
|
||||
err := DB.Where(keyCol+" = ?", key).First(&token).Error
|
||||
return &token, err
|
||||
}
|
||||
|
||||
func (token *Token) Insert() error {
|
||||
var err error
|
||||
err = DB.Create(token).Error
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-ui": "^2.46.1",
|
||||
"@visactor/vchart": "~1.7.2",
|
||||
"@visactor/react-vchart": "~1.7.2",
|
||||
"@visactor/vchart-semi-theme": "~1.7.2",
|
||||
"@douyinfe/semi-icons": "^2.46.1",
|
||||
"@visactor/vchart": "~1.8.8",
|
||||
"@visactor/react-vchart": "~1.8.8",
|
||||
"@visactor/vchart-semi-theme": "~1.8.8",
|
||||
"axios": "^0.27.2",
|
||||
"history": "^5.3.0",
|
||||
"marked": "^4.1.1",
|
||||
|
||||
@@ -57,6 +57,11 @@ function App() {
|
||||
} else {
|
||||
localStorage.removeItem('chat_link');
|
||||
}
|
||||
if (data.chat_link2) {
|
||||
localStorage.setItem('chat_link2', data.chat_link2);
|
||||
} else {
|
||||
localStorage.removeItem('chat_link2');
|
||||
}
|
||||
// if (
|
||||
// data.version !== process.env.REACT_APP_VERSION &&
|
||||
// data.version !== 'v0.0.0' &&
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {useEffect, useState} from 'react';
|
||||
import {
|
||||
API,
|
||||
isMobile,
|
||||
setPromptShown,
|
||||
shouldShowPrompt,
|
||||
showError,
|
||||
showInfo,
|
||||
@@ -24,9 +23,10 @@ import {
|
||||
Space,
|
||||
Tooltip,
|
||||
Switch,
|
||||
Typography, InputNumber
|
||||
Typography, InputNumber, Dropdown, SplitButtonGroup
|
||||
} from "@douyinfe/semi-ui";
|
||||
import EditChannel from "../pages/Channel/EditChannel";
|
||||
import {IconTreeTriangleDown} from "@douyinfe/semi-icons";
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
@@ -196,7 +196,14 @@ const ChannelsTable = () => {
|
||||
dataIndex: 'operate',
|
||||
render: (text, record, index) => (
|
||||
<div>
|
||||
<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>
|
||||
<SplitButtonGroup style={{marginRight: 1}} aria-label="测试操作项目组">
|
||||
<Button theme="light" onClick={()=>{testChannel(record, '')}}>测试</Button>
|
||||
<Dropdown trigger="click" position="bottomRight" menu={record.test_models}
|
||||
>
|
||||
<Button style={ { padding: '8px 4px'}} type="primary" icon={<IconTreeTriangleDown />}></Button>
|
||||
</Dropdown>
|
||||
</SplitButtonGroup>
|
||||
{/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
|
||||
<Popconfirm
|
||||
title="确定是否要删除此渠道?"
|
||||
content="此修改将不可逆"
|
||||
@@ -278,6 +285,17 @@ const ChannelsTable = () => {
|
||||
const setChannelFormat = (channels) => {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
channels[i].key = '' + channels[i].id;
|
||||
let test_models = []
|
||||
channels[i].models.split(',').forEach((item, index) => {
|
||||
test_models.push({
|
||||
node: 'item',
|
||||
name: item,
|
||||
onClick: () => {
|
||||
testChannel(channels[i], item)
|
||||
}
|
||||
})
|
||||
})
|
||||
channels[i].test_models = test_models
|
||||
}
|
||||
// data.key = '' + data.id
|
||||
setChannels(channels);
|
||||
@@ -441,14 +459,12 @@ const ChannelsTable = () => {
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const testChannel = async (record) => {
|
||||
const res = await API.get(`/api/channel/test/${record.id}/`);
|
||||
const testChannel = async (record, model) => {
|
||||
const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
|
||||
const {success, message, time} = res.data;
|
||||
if (success) {
|
||||
let newChannels = [...channels];
|
||||
record.response_time = time * 1000;
|
||||
record.test_time = Date.now() / 1000;
|
||||
setChannels(newChannels);
|
||||
showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -459,7 +475,7 @@ const ChannelsTable = () => {
|
||||
const res = await API.get(`/api/channel/test`);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showInfo('已成功开始测试所有已启用通道,请刷新页面查看结果。');
|
||||
showInfo('已成功开始测试所有通道,请刷新页面查看结果。');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -683,7 +699,7 @@ const ChannelsTable = () => {
|
||||
onConfirm={testAllChannels}
|
||||
position={isMobile()?'top':'top'}
|
||||
>
|
||||
<Button theme='light' type='warning' style={{marginRight: 8}}>测试所有已启用通道</Button>
|
||||
<Button theme='light' type='warning' style={{marginRight: 8}}>测试所有通道</Button>
|
||||
</Popconfirm>
|
||||
<Popconfirm
|
||||
title="确定?"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { Container, Segment } from 'semantic-ui-react';
|
||||
import { getFooterHTML, getSystemName } from '../helpers';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, {useContext, useEffect, useRef, useState} from 'react';
|
||||
import {Link, useNavigate} from 'react-router-dom';
|
||||
import {UserContext} from '../context/User';
|
||||
|
||||
import {Button, Container, Icon, Menu, Segment} from 'semantic-ui-react';
|
||||
import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
@@ -44,7 +43,8 @@ const HeaderBar = () => {
|
||||
const logo = getLogo();
|
||||
var themeMode = localStorage.getItem('theme-mode');
|
||||
const currentDate = new Date();
|
||||
const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1;
|
||||
// enable fireworks on new year(1.1 and 2.9-2.24)
|
||||
const isNewYear = (currentDate.getMonth() === 0 && currentDate.getDate() === 1) || (currentDate.getMonth() === 1 && currentDate.getDate() >= 9 && currentDate.getDate() <= 24);
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react';
|
||||
import {Label} from 'semantic-ui-react';
|
||||
import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers';
|
||||
|
||||
import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal, Spin} from '@douyinfe/semi-ui';
|
||||
import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal, Spin, Space} from '@douyinfe/semi-ui';
|
||||
import {ITEMS_PER_PAGE} from '../constants';
|
||||
import {renderNumber, renderQuota, stringToColor} from '../helpers/render';
|
||||
import {
|
||||
@@ -18,11 +18,9 @@ import {
|
||||
IconHome,
|
||||
IconMore
|
||||
} from '@douyinfe/semi-icons';
|
||||
import Paragraph from "@douyinfe/semi-ui/lib/es/typography/paragraph";
|
||||
|
||||
const {Sider, Content, Header} = Layout;
|
||||
const {Column} = Table;
|
||||
|
||||
|
||||
const {Header} = Layout;
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
<>
|
||||
@@ -56,6 +54,25 @@ function renderType(type) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderIsStream(bool) {
|
||||
if (bool) {
|
||||
return <Tag color='blue' size='large'>流</Tag>;
|
||||
} else {
|
||||
return <Tag color='purple' size='large'>非流</Tag>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderUseTime(type) {
|
||||
const time = parseInt(type);
|
||||
if (time < 101) {
|
||||
return <Tag color='green' size='large'> {time} s </Tag>;
|
||||
} else if (time < 300) {
|
||||
return <Tag color='orange' size='large'> {time} s </Tag>;
|
||||
} else {
|
||||
return <Tag color='red' size='large'> {time} s </Tag>;
|
||||
}
|
||||
}
|
||||
|
||||
const LogsTable = () => {
|
||||
const columns = [
|
||||
{
|
||||
@@ -142,6 +159,20 @@ const LogsTable = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '用时',
|
||||
dataIndex: 'use_time',
|
||||
render: (text, record, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Space>
|
||||
{renderUseTime(text)}
|
||||
{renderIsStream(record.is_stream)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '提示',
|
||||
dataIndex: 'prompt_tokens',
|
||||
@@ -189,6 +220,11 @@ const LogsTable = () => {
|
||||
{
|
||||
title: '详情',
|
||||
dataIndex: 'content',
|
||||
render: (text, record, index) => {
|
||||
return <Paragraph ellipsis={{ rows: 2, showTooltip: { type: 'popover', opts: { style: { width: 240 } } } }} style={{ maxWidth: 240}}>
|
||||
{text}
|
||||
</Paragraph>
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Label} from 'semantic-ui-react';
|
||||
import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers';
|
||||
|
||||
import {
|
||||
|
||||
@@ -15,8 +15,10 @@ const OperationSetting = () => {
|
||||
GroupRatio: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
ChatLink2: '', // 添加的新状态变量
|
||||
QuotaPerUnit: 0,
|
||||
AutomaticDisableChannelEnabled: '',
|
||||
AutomaticEnableChannelEnabled: '',
|
||||
ChannelDisableThreshold: 0,
|
||||
LogConsumeEnabled: '',
|
||||
DisplayInCurrencyEnabled: '',
|
||||
@@ -141,6 +143,9 @@ const OperationSetting = () => {
|
||||
if (originInputs['ChatLink'] !== inputs.ChatLink) {
|
||||
await updateOption('ChatLink', inputs.ChatLink);
|
||||
}
|
||||
if (originInputs['ChatLink2'] !== inputs.ChatLink2) {
|
||||
await updateOption('ChatLink2', inputs.ChatLink2);
|
||||
}
|
||||
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
||||
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
||||
}
|
||||
@@ -179,7 +184,7 @@ const OperationSetting = () => {
|
||||
placeholder='例如发卡网站的购买链接'
|
||||
/>
|
||||
<Form.Input
|
||||
label='聊天页面链接'
|
||||
label='默认聊天页面链接'
|
||||
name='ChatLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
@@ -187,6 +192,15 @@ const OperationSetting = () => {
|
||||
type='link'
|
||||
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||
/>
|
||||
<Form.Input
|
||||
label='聊天页面2链接'
|
||||
name='ChatLink2'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChatLink2}
|
||||
type='link'
|
||||
placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
|
||||
/>
|
||||
<Form.Input
|
||||
label='单位美元额度'
|
||||
name='QuotaPerUnit'
|
||||
@@ -319,6 +333,12 @@ const OperationSetting = () => {
|
||||
name='AutomaticDisableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticEnableChannelEnabled === 'true'}
|
||||
label='成功时自动启用通道'
|
||||
name='AutomaticEnableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
|
||||
@@ -397,7 +397,9 @@ const PersonalSetting = () => {
|
||||
></Input>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={()=>{setShowEmailBindModal(true)}} disabled={userState.user && userState.user.email !== ''}>绑定邮箱</Button>
|
||||
<Button onClick={()=>{setShowEmailBindModal(true)}}>{
|
||||
userState.user && userState.user.email !== ''?'修改绑定':'绑定邮箱'
|
||||
}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,7 +83,7 @@ const SiderBar = () => {
|
||||
icon: <IconHistogram/>
|
||||
},
|
||||
{
|
||||
text: '数据看版',
|
||||
text: '数据看板',
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers';
|
||||
|
||||
import {ITEMS_PER_PAGE} from '../constants';
|
||||
@@ -21,17 +20,14 @@ import {
|
||||
IconTreeTriangleDown,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditToken from "../pages/Token/EditToken";
|
||||
|
||||
const {Column} = Table;
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{key: 'next', text: 'ChatGPT Next Web', value: 'next'},
|
||||
{key: 'ama', text: 'AMA 问天', value: 'ama'},
|
||||
{key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama'},
|
||||
{key: 'opencat', text: 'OpenCat', value: 'opencat'},
|
||||
];
|
||||
|
||||
const OPEN_LINK_OPTIONS = [
|
||||
{key: 'ama', text: 'AMA 问天', value: 'ama'},
|
||||
{key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama'},
|
||||
{key: 'opencat', text: 'OpenCat', value: 'opencat'},
|
||||
];
|
||||
|
||||
@@ -67,6 +63,7 @@ const TokensTable = () => {
|
||||
const link_menu = [
|
||||
{node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next')}},
|
||||
{node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama'},
|
||||
{node: 'item', key: 'next-mj', name: 'ChatGPT Web & Midjourney', value: 'next-mj', onClick: () => {onOpenLink('next-mj')}},
|
||||
{node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat'},
|
||||
];
|
||||
|
||||
@@ -154,7 +151,8 @@ const TokensTable = () => {
|
||||
<Button theme="light" style={{ color: 'rgba(var(--semi-teal-7), 1)' }} onClick={()=>{onOpenLink('next', record.key)}}>聊天</Button>
|
||||
<Dropdown trigger="click" position="bottomRight" menu={
|
||||
[
|
||||
{node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next', record.key)}},
|
||||
{node: 'item', key: 'next', disabled: !localStorage.getItem('chat_link'), name: 'ChatGPT Next Web', onClick: () => {onOpenLink('next', record.key)}},
|
||||
{node: 'item', key: 'next-mj', disabled: !localStorage.getItem('chat_link2'), name: 'ChatGPT Web & Midjourney', onClick: () => {onOpenLink('next-mj', record.key)}},
|
||||
{node: 'item', key: 'ama', name: 'AMA 问天(BotGrem)', onClick: () => {onOpenLink('ama', record.key)}},
|
||||
{node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {onOpenLink('opencat', record.key)}},
|
||||
]
|
||||
@@ -289,6 +287,7 @@ const TokensTable = () => {
|
||||
}
|
||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||
const nextLink = localStorage.getItem('chat_link');
|
||||
const mjLink = localStorage.getItem('chat_link2');
|
||||
let nextUrl;
|
||||
|
||||
if (nextLink) {
|
||||
@@ -300,7 +299,7 @@ const TokensTable = () => {
|
||||
let url;
|
||||
switch (type) {
|
||||
case 'ama':
|
||||
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||
url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
break;
|
||||
case 'opencat':
|
||||
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||
@@ -340,25 +339,28 @@ const TokensTable = () => {
|
||||
}
|
||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||
const chatLink = localStorage.getItem('chat_link');
|
||||
const mjLink = localStorage.getItem('chat_link2');
|
||||
let defaultUrl;
|
||||
|
||||
if (chatLink) {
|
||||
defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
} else {
|
||||
showError('管理员未设置聊天链接')
|
||||
return
|
||||
}
|
||||
let url;
|
||||
switch (type) {
|
||||
case 'ama':
|
||||
url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`;
|
||||
break;
|
||||
|
||||
case 'opencat':
|
||||
url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
|
||||
break;
|
||||
|
||||
case 'next-mj':
|
||||
url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
|
||||
break;
|
||||
default:
|
||||
if (!chatLink) {
|
||||
showError('管理员未设置聊天链接')
|
||||
return;
|
||||
}
|
||||
url = defaultUrl;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
import VChart from "@visactor/vchart";
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import {BrowserRouter} from 'react-router-dom';
|
||||
|
||||
@@ -213,6 +213,7 @@ const EditChannel = (props) => {
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
localInputs.auto_ban = autoBan ? 1 : 0;
|
||||
localInputs.models = localInputs.models.join(',');
|
||||
localInputs.group = localInputs.groups.join(',');
|
||||
if (isEdit) {
|
||||
@@ -528,7 +529,6 @@ const EditChannel = (props) => {
|
||||
onChange={
|
||||
() => {
|
||||
setAutoBan(!autoBan);
|
||||
|
||||
}
|
||||
}
|
||||
// onChange={handleInputChange}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import ChannelsTable from '../../components/ChannelsTable';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
import RedemptionsTable from "../../components/RedemptionsTable";
|
||||
|
||||
const File = () => (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {Button, Col, Form, Layout, Row, Spin} from "@douyinfe/semi-ui";
|
||||
import VChart from '@visactor/vchart';
|
||||
import {useEffectOnce} from "usehooks-ts";
|
||||
import {API, isAdmin, showError, timestamp2string, timestamp2string1} from "../../helpers";
|
||||
import {
|
||||
getQuotaWithUnit, modelColorMap,
|
||||
@@ -91,9 +90,16 @@ const Detail = (props) => {
|
||||
// sort by value
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
// add $
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
sum += parseFloat(array[i].value);
|
||||
array[i].value = renderQuotaNumberWithDigit(parseFloat(array[i].value), 4);
|
||||
}
|
||||
// add to first
|
||||
array.unshift({
|
||||
key: '总计',
|
||||
value: renderQuotaNumberWithDigit(sum, 4)
|
||||
});
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Header, Segment } from 'semantic-ui-react';
|
||||
import LogsTable from '../../components/LogsTable';
|
||||
|
||||
const Token = () => (
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Segment, Header } from 'semantic-ui-react';
|
||||
import RedemptionsTable from '../../components/RedemptionsTable';
|
||||
import TokensTable from "../../components/TokensTable";
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
|
||||
const Redemption = () => (
|
||||
|
||||
@@ -8,8 +8,10 @@ const TopUp = () => {
|
||||
const [redemptionCode, setRedemptionCode] = useState('');
|
||||
const [topUpCode, setTopUpCode] = useState('');
|
||||
const [topUpCount, setTopUpCount] = useState(10);
|
||||
const [minTopupCount, setMinTopUpCount] = useState(1);
|
||||
const [amount, setAmount] = useState(0.0);
|
||||
const [topUpLink, setTopUpLink] = useState('');
|
||||
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false);
|
||||
const [userQuota, setUserQuota] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -52,6 +54,10 @@ const TopUp = () => {
|
||||
};
|
||||
|
||||
const preTopUp = async (payment) => {
|
||||
if (!enableOnlineTopUp) {
|
||||
showError('管理员未开启在线充值!');
|
||||
return;
|
||||
}
|
||||
if (amount === 0) {
|
||||
await getAmount();
|
||||
}
|
||||
@@ -126,6 +132,9 @@ const TopUp = () => {
|
||||
if (status.top_up_link) {
|
||||
setTopUpLink(status.top_up_link);
|
||||
}
|
||||
if (status.enable_online_topup) {
|
||||
setEnableOnlineTopUp(status.enable_online_topup);
|
||||
}
|
||||
}
|
||||
getUserQuota().then();
|
||||
}, []);
|
||||
@@ -227,13 +236,23 @@ const TopUp = () => {
|
||||
</Divider>
|
||||
<Form>
|
||||
<Form.Input
|
||||
disabled={!enableOnlineTopUp}
|
||||
field={'redemptionCount'}
|
||||
label={'实付金额:' + renderAmount()}
|
||||
placeholder='充值数量'
|
||||
name='redemptionCount'
|
||||
type={'number'}
|
||||
value={topUpCount}
|
||||
suffix={'$'}
|
||||
min={1}
|
||||
max={100000}
|
||||
onChange={async (value) => {
|
||||
if (value < 1) {
|
||||
value = 1;
|
||||
}
|
||||
if (value > 100000) {
|
||||
value = 100000;
|
||||
}
|
||||
setTopUpCount(value);
|
||||
await getAmount(value);
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { Segment, Header } from 'semantic-ui-react';
|
||||
import UsersTable from '../../components/UsersTable';
|
||||
import {Layout} from "@douyinfe/semi-ui";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user