Compare commits

...

20 Commits

Author SHA1 Message Date
1808837298@qq.com
82ae6e4e1f chore: change workflow runners to self-hosted for Docker and release jobs 2025-01-05 17:17:57 +08:00
1808837298@qq.com
af0b932535 fix: update iframe styling and permissions in ChatPage component 2025-01-04 22:24:47 +08:00
1808837298@qq.com
f1e3cd6f6d refactor: realtime log render 2025-01-04 17:54:02 +08:00
1808837298@qq.com
f417a109bf refactor: realtime i18n 2025-01-04 17:46:06 +08:00
1808837298@qq.com
99245e4c1f refactor: realtime quota 2025-01-04 15:46:35 +08:00
Calcium-Ion
b5de003ec2 Merge pull request #689 from iszcz/new512 2025-01-03 20:47:56 +08:00
iszcz
8ede1bf121 Update model-ratio.go 2025-01-03 20:42:46 +08:00
1808837298@qq.com
4a0a841e1d feat: support gpt-4o-mini-realtime-preview 2025-01-03 18:51:09 +08:00
1808837298@qq.com
ef4c1a2e48 fix: retry prompt tokens 2025-01-02 16:33:00 +08:00
Calcium-Ion
ba1aad8ac4 Merge pull request #686 from delph1s/main
fix: try to fix pgsql #685
2025-01-02 00:17:02 +08:00
delph1s
42bf95bd54 fix: try to fix pgsql #685 2025-01-02 00:14:16 +08:00
Calcium-Ion
bf9a492f25 Update README.md 2024-12-31 22:19:37 +08:00
Calcium-Ion
16725d1226 Merge pull request #683 from iszcz/new512
Update channel-test.go
2024-12-31 20:22:57 +08:00
CalciumIon
e6ea5e59c0 fix: error page size opts 2024-12-31 15:51:15 +08:00
CalciumIon
4f196a62e1 feat: implement pagination and total count for redemptions API #386
- Updated GetAllRedemptions and SearchRedemptions functions to return total count along with paginated results.
- Modified API endpoints to accept page size as a parameter, enhancing flexibility in data retrieval.
- Adjusted RedemptionsTable component to support pagination and display total count, improving user experience.
- Ensured consistent handling of pagination across related components, including LogsTable and UsersTable.
2024-12-31 15:28:25 +08:00
CalciumIon
014fb7edab feat: enhance user search functionality with pagination support
- Updated SearchUsers function to include pagination parameters (startIdx and num) for improved user search results.
- Modified API response structure to return paginated data, including total user count and current page information.
- Adjusted UsersTable component to handle pagination and search parameters, ensuring a seamless user experience.
- Added internationalization support for new search functionality in the UI.
2024-12-31 15:02:59 +08:00
CalciumIon
be0b2f6a64 feat: enhance user management and pagination features #518
- Updated GetAllUsers function to return total user count along with paginated results, improving data handling in user retrieval.
- Modified GetAllUsers API endpoint to accept page size as a parameter, allowing for dynamic pagination.
- Enhanced UsersTable component to support customizable page sizes and improved pagination logic.
- Added error handling for empty username and password in AddUser component.
- Updated LogsTable component to display pagination information in a user-friendly format.
2024-12-31 14:52:55 +08:00
iszcz
687f07bc10 Update channel-test.go 2024-12-31 12:49:13 +08:00
CalciumIon
a7e5f1e509 fix: try to fix pgsql #682 2024-12-31 02:10:19 +08:00
CalciumIon
87d5e286d5 fix: try to fix pgsql #682 2024-12-31 02:06:30 +08:00
30 changed files with 2899 additions and 3133 deletions

View File

@@ -12,7 +12,7 @@ on:
jobs:
push_to_registries:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest
runs-on: self-hosted
permissions:
packages: write
contents: read

View File

@@ -13,7 +13,7 @@ on:
jobs:
push_to_registries:
name: Push Docker image to multiple registries
runs-on: ubuntu-latest
runs-on: self-hosted
permissions:
packages: write
contents: read

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*'
jobs:
release:
runs-on: ubuntu-latest
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v3

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*'
jobs:
release:
runs-on: macos-latest
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v3

View File

@@ -9,7 +9,7 @@ on:
- '!*-alpha*'
jobs:
release:
runs-on: windows-latest
runs-on: self-hosted
defaults:
run:
shell: bash

View File

@@ -159,15 +159,14 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
[对接文档](Suno.md)
## 界面截图
![796df8d287b7b7bd7853b2497e7df511](https://github.com/user-attachments/assets/255b5e97-2d3a-4434-b4fa-e922ad88ff5a)
![image](https://github.com/user-attachments/assets/a0dcd349-5df8-4dc8-9acf-ca272b239919)
![image](https://github.com/user-attachments/assets/c7d0f7e1-729c-43e2-ac7c-2cb73b0afc8e)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4)
![image](https://github.com/user-attachments/assets/29f81de5-33fc-4fc5-a5ff-f9b54b653c7c)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)
夜间模式
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/1c66b593-bb9e-4757-9720-ff2759539242)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e)
![image](https://github.com/user-attachments/assets/4fa53e18-d2c5-477a-9b26-b86e44c71e35)
## 交流群
<img src="https://github.com/user-attachments/assets/9ca0bc82-e057-4230-a28d-9f198fa022e3" width="200">

View File

@@ -32,30 +32,34 @@ var defaultModelRatio = map[string]float64{
"gpt-4-0613": 15,
"gpt-4-32k": 30,
//"gpt-4-32k-0314": 30, //deprecated
"gpt-4-32k-0613": 30,
"gpt-4-1106-preview": 5, // $10 / 1M tokens
"gpt-4-0125-preview": 5, // $10 / 1M tokens
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
"gpt-4-vision-preview": 5, // $10 / 1M tokens
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
"gpt-4o": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
"gpt-4o-realtime-preview": 2.5,
"o1": 7.5,
"o1-2024-12-17": 7.5,
"o1-preview": 7.5,
"o1-preview-2024-09-12": 7.5,
"o1-mini": 1.5,
"o1-mini-2024-09-12": 1.5,
"gpt-4o-mini": 0.075,
"gpt-4o-mini-2024-07-18": 0.075,
"gpt-4-turbo": 5, // $0.01 / 1K tokens
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
"gpt-4-32k-0613": 30,
"gpt-4-1106-preview": 5, // $10 / 1M tokens
"gpt-4-0125-preview": 5, // $10 / 1M tokens
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
"gpt-4-vision-preview": 5, // $10 / 1M tokens
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
"gpt-4o": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
"gpt-4o-realtime-preview": 2.5,
"gpt-4o-realtime-preview-2024-10-01": 2.5,
"gpt-4o-realtime-preview-2024-12-17": 2.5,
"gpt-4o-mini-realtime-preview": 0.3,
"gpt-4o-mini-realtime-preview-2024-12-17": 0.3,
"o1": 7.5,
"o1-2024-12-17": 7.5,
"o1-preview": 7.5,
"o1-preview-2024-09-12": 7.5,
"o1-mini": 1.5,
"o1-mini-2024-09-12": 1.5,
"gpt-4o-mini": 0.075,
"gpt-4o-mini-2024-07-18": 0.075,
"gpt-4-turbo": 5, // $0.01 / 1K tokens
"gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens
//"gpt-3.5-turbo-0301": 0.75, //deprecated
"gpt-3.5-turbo": 0.25,
"gpt-3.5-turbo-0613": 0.75,
@@ -427,10 +431,23 @@ func GetCompletionRatio(name string) float64 {
}
func GetAudioRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4o-realtime") {
return 20
} else if strings.HasPrefix(name, "gpt-4o-audio") {
return 40
if strings.Contains(name, "-realtime") {
if strings.HasSuffix(name, "gpt-4o-realtime-preview-2024-12-17") {
return 8
} else if strings.Contains(name, "mini") {
return 10 / 0.6
} else {
return 20
}
}
if strings.Contains(name, "-audio") {
if strings.HasSuffix(name, "gpt-4o-audio-preview-2024-12-17") {
return 16
} else if strings.Contains(name, "mini") {
return 10 / 0.15
} else {
return 40
}
}
return 20
}
@@ -438,6 +455,8 @@ func GetAudioRatio(name string) float64 {
func GetAudioCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4o-realtime") {
return 2
} else if strings.HasPrefix(name, "gpt-4o-mini-realtime") {
return 2
}
return 2
}

View File

@@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/bytedance/gopkg/util/gopool"
"io"
"math"
"net/http"
@@ -24,6 +23,8 @@ import (
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/gin-gonic/gin"
)
@@ -32,6 +33,9 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
if channel.Type == common.ChannelTypeMidjourney {
return errors.New("midjourney channel test is not supported"), nil
}
if channel.Type == common.ChannelTypeMidjourneyPlus {
return errors.New("midjourney plus channel test is not supported!!!"), nil
}
if channel.Type == common.ChannelTypeSunoAPI {
return errors.New("suno channel test is not supported"), nil
}

View File

@@ -1,19 +1,24 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"github.com/gin-gonic/gin"
)
func GetAllRedemptions(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 {
p = 0
}
redemptions, err := model.GetAllRedemptions(p*common.ItemsPerPage, common.ItemsPerPage)
if pageSize < 1 {
pageSize = common.ItemsPerPage
}
redemptions, total, err := model.GetAllRedemptions((p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -24,14 +29,27 @@ func GetAllRedemptions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": redemptions,
"data": gin.H{
"items": redemptions,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}
func SearchRedemptions(c *gin.Context) {
keyword := c.Query("keyword")
redemptions, err := model.SearchRedemptions(keyword)
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 0 {
p = 0
}
if pageSize < 1 {
pageSize = common.ItemsPerPage
}
redemptions, total, err := model.SearchRedemptions(keyword, (p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -42,7 +60,12 @@ func SearchRedemptions(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": redemptions,
"data": gin.H{
"items": redemptions,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}

View File

@@ -11,9 +11,10 @@ import (
"strings"
"sync"
"one-api/constant"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"one-api/constant"
)
type LoginRequest struct {
@@ -242,10 +243,14 @@ func Register(c *gin.Context) {
func GetAllUsers(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p"))
if p < 0 {
p = 0
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 1 {
p = 1
}
users, err := model.GetAllUsers(p*common.ItemsPerPage, common.ItemsPerPage)
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
users, total, err := model.GetAllUsers((p-1)*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -256,7 +261,12 @@ func GetAllUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": users,
"data": gin.H{
"items": users,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}
@@ -264,7 +274,16 @@ func GetAllUsers(c *gin.Context) {
func SearchUsers(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
users, err := model.SearchUsers(keyword, group)
p, _ := strconv.Atoi(c.Query("p"))
pageSize, _ := strconv.Atoi(c.Query("page_size"))
if p < 1 {
p = 1
}
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
startIdx := (p - 1) * pageSize
users, total, err := model.SearchUsers(keyword, group, startIdx, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -275,7 +294,12 @@ func SearchUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": users,
"data": gin.H{
"items": users,
"total": total,
"page": p,
"page_size": pageSize,
},
})
return
}

View File

@@ -23,10 +23,6 @@ type Ability struct {
func GetGroupModels(group string) []string {
var models []string
// Find distinct models
groupCol := "`group`"
if common.UsingPostgreSQL {
groupCol = `"group"`
}
DB.Table("abilities").Where(groupCol+" = ? and enabled = ?", group, true).Distinct("model").Pluck("model", &models)
return models
}
@@ -45,10 +41,8 @@ func GetAllEnableAbilities() []Ability {
}
func getPriority(group string, model string, retry int) (int, error) {
groupCol := "`group`"
trueVal := "1"
if common.UsingPostgreSQL {
groupCol = `"group"`
trueVal = "true"
}
@@ -81,10 +75,8 @@ func getPriority(group string, model string, retry int) (int, error) {
}
func getChannelQuery(group string, model string, retry int) *gorm.DB {
groupCol := "`group`"
trueVal := "1"
if common.UsingPostgreSQL {
groupCol = `"group"`
trueVal = "true"
}
maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model)

View File

@@ -114,14 +114,11 @@ func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
}
@@ -437,14 +434,10 @@ func GetPaginatedTags(offset int, limit int) ([]*string, error) {
func SearchTags(keyword string, group string, model string, idSort bool) ([]*string, error) {
var tags []*string
keyCol := "`key`"
groupCol := "`group`"
modelsCol := "`models`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
groupCol = `"group"`
modelsCol = `"models"`
}

View File

@@ -56,7 +56,7 @@ func formatUserLogs(logs []*Log) {
func GetLogByKey(key string) (logs []*Log, err error) {
if os.Getenv("LOG_SQL_DSN") != "" {
var tk Token
if err = DB.Model(&Token{}).Where("`key`=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
if err = DB.Model(&Token{}).Where(keyCol+"=?", strings.TrimPrefix(key, "sk-")).First(&tk).Error; err != nil {
return nil, err
}
err = LOG_DB.Model(&Log{}).Where("token_id=?", tk.Id).Find(&logs).Error

View File

@@ -16,7 +16,7 @@ import (
var groupCol string
var keyCol string
func init() {
func initCol() {
if common.UsingPostgreSQL {
groupCol = `"group"`
keyCol = `"key"`
@@ -55,6 +55,9 @@ func createRootAccountIfNeed() error {
}
func chooseDB(envName string) (*gorm.DB, error) {
defer func() {
initCol()
}()
dsn := os.Getenv(envName)
if dsn != "" {
if strings.HasPrefix(dsn, "postgres://") {

View File

@@ -3,8 +3,10 @@ package model
import (
"errors"
"fmt"
"gorm.io/gorm"
"one-api/common"
"strconv"
"gorm.io/gorm"
)
type Redemption struct {
@@ -21,16 +23,80 @@ type Redemption struct {
DeletedAt gorm.DeletedAt `gorm:"index"`
}
func GetAllRedemptions(startIdx int, num int) ([]*Redemption, error) {
var redemptions []*Redemption
var err error
err = DB.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
return redemptions, err
func GetAllRedemptions(startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
// 开始事务
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 获取总数
err = tx.Model(&Redemption{}).Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 获取分页数据
err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return redemptions, total, nil
}
func SearchRedemptions(keyword string) (redemptions []*Redemption, err error) {
err = DB.Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&redemptions).Error
return redemptions, err
func SearchRedemptions(keyword string, startIdx int, num int) (redemptions []*Redemption, total int64, err error) {
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Build query based on keyword type
query := tx.Model(&Redemption{})
// Only try to convert to ID if the string represents a valid integer
if id, err := strconv.Atoi(keyword); err == nil {
query = query.Where("id = ? OR name LIKE ?", id, keyword+"%")
} else {
query = query.Where("name LIKE ?", keyword+"%")
}
// Get total count
err = query.Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Get paginated data
err = query.Order("id desc").Limit(num).Offset(startIdx).Find(&redemptions).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return redemptions, total, nil
}
func GetRedemptionById(id int) (*Redemption, error) {

View File

@@ -68,7 +68,7 @@ func SearchUserTokens(userId int, keyword string, token string) (tokens []*Token
if token != "" {
token = strings.Trim(token, "sk-")
}
err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where("`key` LIKE ?", "%"+token+"%").Find(&tokens).Error
err = DB.Where("user_id = ?", userId).Where("name LIKE ?", "%"+keyword+"%").Where(keyCol+" LIKE ?", "%"+token+"%").Find(&tokens).Error
return tokens, err
}

View File

@@ -81,41 +81,100 @@ func GetMaxUserId() int {
return user.Id
}
func GetAllUsers(startIdx int, num int) (users []*User, err error) {
err = DB.Unscoped().Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error
return users, err
func GetAllUsers(startIdx int, num int) (users []*User, total int64, err error) {
// Start transaction
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// Get total count within transaction
err = tx.Unscoped().Model(&User{}).Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Get paginated users within same transaction
err = tx.Unscoped().Order("id desc").Limit(num).Offset(startIdx).Omit("password").Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// Commit transaction
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func SearchUsers(keyword string, group string) ([]*User, error) {
func SearchUsers(keyword string, group string, startIdx int, num int) ([]*User, int64, error) {
var users []*User
var total int64
var err error
// 开始事务
tx := DB.Begin()
if tx.Error != nil {
return nil, 0, tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 构建基础查询
query := tx.Unscoped().Model(&User{})
// 尝试将关键字转换为整数ID
keywordInt, err := strconv.Atoi(keyword)
if err == nil {
// 如果转换成功按照ID和可选的组别搜索用户
query := DB.Unscoped().Omit("password").Where("id = ?", keywordInt)
if group != "" {
query = query.Where(groupCol+" = ?", group) // 使用反引号包围group
query = query.Where("id = ? AND "+groupCol+" = ?", keywordInt, group)
} else {
query = query.Where("id = ?", keywordInt)
}
err = query.Find(&users).Error
if err != nil || len(users) > 0 {
return users, err
}
}
err = nil
query := DB.Unscoped().Omit("password")
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
if group != "" {
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
} else {
query = query.Where(likeCondition, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
// 如果不是ID搜索则使用模糊匹配
likeCondition := "username LIKE ? OR email LIKE ? OR display_name LIKE ?"
if group != "" {
query = query.Where("("+likeCondition+") AND "+groupCol+" = ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", group)
} else {
query = query.Where(likeCondition,
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
}
err = query.Find(&users).Error
return users, err
// 获取总数
err = query.Count(&total).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 获取分页数据
err = query.Omit("password").Order("id desc").Limit(num).Offset(startIdx).Find(&users).Error
if err != nil {
tx.Rollback()
return nil, 0, err
}
// 提交事务
if err = tx.Commit().Error; err != nil {
return nil, 0, err
}
return users, total, nil
}
func GetUserById(id int, selectAll bool) (*User, error) {

View File

@@ -15,7 +15,8 @@ var ModelList = []string{
"o1-mini", "o1-mini-2024-09-12",
"o1", "o1-2024-12-17",
"gpt-4o-audio-preview", "gpt-4o-audio-preview-2024-10-01",
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01",
"gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01", "gpt-4o-realtime-preview-2024-12-17",
"gpt-4o-mini-realtime-preview", "gpt-4o-mini-realtime-preview-2024-12-17",
"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",
"text-curie-001", "text-babbage-001", "text-ada-001",
"text-moderation-latest", "text-moderation-stable",

View File

@@ -112,6 +112,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
var promptTokens int
if value, exists := c.Get("prompt_tokens"); exists {
promptTokens = value.(int)
relayInfo.PromptTokens = promptTokens
} else {
promptTokens, err = getPromptTokens(textRequest, relayInfo)
// count messages token error 计算promptTokens错误
@@ -218,7 +219,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
}
if strings.HasPrefix(relayInfo.UpstreamModelName, "gpt-4o-audio") {
service.PostAudioConsumeQuota(c, relayInfo, usage.(*dto.Usage), ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
service.PostAudioConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
} else {
postConsumeQuota(c, relayInfo, textRequest.Model, usage.(*dto.Usage), ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
}

View File

@@ -13,24 +13,6 @@ import (
"one-api/setting"
)
//func getAndValidateWssRequest(c *gin.Context, ws *websocket.Conn) (*dto.RealtimeEvent, error) {
// _, p, err := ws.ReadMessage()
// if err != nil {
// return nil, err
// }
// realtimeEvent := &dto.RealtimeEvent{}
// err = json.Unmarshal(p, realtimeEvent)
// if err != nil {
// return nil, err
// }
// // save the original request
// if realtimeEvent.Session == nil {
// return nil, errors.New("session object is nil")
// }
// c.Set("first_wss_request", p)
// return realtimeEvent, nil
//}
func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWithStatusCode) {
relayInfo := relaycommon.GenRelayInfoWs(c, ws)
@@ -129,32 +111,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
service.ResetStatusCode(openaiErr, statusCodeMappingStr)
return openaiErr
}
service.PostWssConsumeQuota(c, relayInfo, relayInfo.UpstreamModelName, usage.(*dto.RealtimeUsage), ratio, preConsumedQuota, userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
service.PostWssConsumeQuota(c, relayInfo, relayInfo.UpstreamModelName, usage.(*dto.RealtimeUsage), preConsumedQuota,
userQuota, modelRatio, groupRatio, modelPrice, getModelPriceSuccess, "")
return nil
}
//func getWssPromptTokens(textRequest *dto.RealtimeEvent, info *relaycommon.RelayInfo) (int, error) {
// var promptTokens int
// var err error
// switch info.RelayMode {
// default:
// promptTokens, err = service.CountTokenRealtime(*textRequest, info.UpstreamModelName)
// }
// info.PromptTokens = promptTokens
// return promptTokens, err
//}
//func checkWssRequestSensitive(textRequest *dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) error {
// var err error
// switch info.RelayMode {
// case relayconstant.RelayModeChatCompletions:
// err = service.CheckSensitiveMessages(textRequest.Messages)
// case relayconstant.RelayModeCompletions:
// err = service.CheckSensitiveInput(textRequest.Prompt)
// case relayconstant.RelayModeModerations:
// err = service.CheckSensitiveInput(textRequest.Input)
// case relayconstant.RelayModeEmbeddings:
// err = service.CheckSensitiveInput(textRequest.Input)
// }
// return err
//}

View File

@@ -3,7 +3,6 @@ package service
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"math"
"one-api/common"
"one-api/dto"
@@ -12,8 +11,47 @@ import (
"one-api/setting"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type TokenDetails struct {
TextTokens int
AudioTokens int
}
type QuotaInfo struct {
InputDetails TokenDetails
OutputDetails TokenDetails
ModelName string
UsePrice bool
ModelPrice float64
ModelRatio float64
GroupRatio float64
}
func calculateAudioQuota(info QuotaInfo) int {
if info.UsePrice {
return int(info.ModelPrice * common.QuotaPerUnit * info.GroupRatio)
}
completionRatio := common.GetCompletionRatio(info.ModelName)
audioRatio := common.GetAudioRatio(info.ModelName)
audioCompletionRatio := common.GetAudioCompletionRatio(info.ModelName)
ratio := info.GroupRatio * info.ModelRatio
quota := info.InputDetails.TextTokens + int(math.Round(float64(info.OutputDetails.TextTokens)*completionRatio))
quota += int(math.Round(float64(info.InputDetails.AudioTokens)*audioRatio)) +
int(math.Round(float64(info.OutputDetails.AudioTokens)*audioRatio*audioCompletionRatio))
quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 {
quota = 1
}
return quota
}
func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.RealtimeUsage) error {
if relayInfo.UsePrice {
return nil
@@ -33,23 +71,26 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
textOutTokens := usage.OutputTokenDetails.TextTokens
audioInputTokens := usage.InputTokenDetails.AudioTokens
audioOutTokens := usage.OutputTokenDetails.AudioTokens
completionRatio := common.GetCompletionRatio(modelName)
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
audioCompletionRatio := common.GetAudioCompletionRatio(modelName)
groupRatio := setting.GetGroupRatio(relayInfo.Group)
modelRatio := common.GetModelRatio(modelName)
ratio := groupRatio * modelRatio
quota := textInputTokens + int(math.Round(float64(textOutTokens)*completionRatio))
quota += int(math.Round(float64(audioInputTokens)*audioRatio)) + int(math.Round(float64(audioOutTokens)*audioRatio*audioCompletionRatio))
quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 {
quota = 1
quotaInfo := QuotaInfo{
InputDetails: TokenDetails{
TextTokens: textInputTokens,
AudioTokens: audioInputTokens,
},
OutputDetails: TokenDetails{
TextTokens: textOutTokens,
AudioTokens: audioOutTokens,
},
ModelName: modelName,
UsePrice: relayInfo.UsePrice,
ModelRatio: modelRatio,
GroupRatio: groupRatio,
}
quota := calculateAudioQuota(quotaInfo)
if userQuota < quota {
return errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
}
@@ -67,8 +108,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
}
func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelName string,
usage *dto.RealtimeUsage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64,
groupRatio float64,
usage *dto.RealtimeUsage, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
modelPrice float64, usePrice bool, extraContent string) {
useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
@@ -83,17 +123,23 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
audioCompletionRatio := common.GetAudioCompletionRatio(modelName)
quota := 0
if !usePrice {
quota = int(math.Round(float64(textInputTokens) + float64(textOutTokens)*completionRatio))
quota += int(math.Round(float64(audioInputTokens)*audioRatio + float64(audioOutTokens)*audioRatio*audioCompletionRatio))
quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 {
quota = 1
}
} else {
quota = int(modelPrice * common.QuotaPerUnit * groupRatio)
quotaInfo := QuotaInfo{
InputDetails: TokenDetails{
TextTokens: textInputTokens,
AudioTokens: audioInputTokens,
},
OutputDetails: TokenDetails{
TextTokens: textOutTokens,
AudioTokens: audioOutTokens,
},
ModelName: modelName,
UsePrice: usePrice,
ModelRatio: modelRatio,
GroupRatio: groupRatio,
}
quota := calculateAudioQuota(quotaInfo)
totalTokens := usage.TotalTokens
var logContent string
if !usePrice {
@@ -111,21 +157,6 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
"tokenId %d, model %s pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota))
} else {
//if sensitiveResp != nil {
// logContent += fmt.Sprintf(",敏感词:%s", strings.Join(sensitiveResp.SensitiveWords, ", "))
//}
//quotaDelta := quota - preConsumedQuota
//if quotaDelta != 0 {
// err := model.PostConsumeQuota(relayInfo, userQuota, quotaDelta, preConsumedQuota, true)
// if err != nil {
// common.LogError(ctx, "error consuming token remain quota: "+err.Error())
// }
//}
//err := model.CacheUpdateUserQuota(relayInfo.UserId)
//if err != nil {
// common.LogError(ctx, "error update user quota cache: "+err.Error())
//}
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
}
@@ -140,8 +171,7 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
}
func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
usage *dto.Usage, ratio float64, preConsumedQuota int, userQuota int, modelRatio float64,
groupRatio float64,
usage *dto.Usage, preConsumedQuota int, userQuota int, modelRatio float64, groupRatio float64,
modelPrice float64, usePrice bool, extraContent string) {
useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix()
@@ -156,17 +186,23 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
audioCompletionRatio := common.GetAudioCompletionRatio(relayInfo.UpstreamModelName)
quota := 0
if !usePrice {
quota = int(math.Round(float64(textInputTokens) + float64(textOutTokens)*completionRatio))
quota += int(math.Round(float64(audioInputTokens)*audioRatio + float64(audioOutTokens)*audioRatio*audioCompletionRatio))
quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 {
quota = 1
}
} else {
quota = int(modelPrice * common.QuotaPerUnit * groupRatio)
quotaInfo := QuotaInfo{
InputDetails: TokenDetails{
TextTokens: textInputTokens,
AudioTokens: audioInputTokens,
},
OutputDetails: TokenDetails{
TextTokens: textOutTokens,
AudioTokens: audioOutTokens,
},
ModelName: relayInfo.UpstreamModelName,
UsePrice: usePrice,
ModelRatio: modelRatio,
GroupRatio: groupRatio,
}
quota := calculateAudioQuota(quotaInfo)
totalTokens := usage.TotalTokens
var logContent string
if !usePrice {

View File

@@ -92,13 +92,6 @@ func getImageToken(info *relaycommon.RelayInfo, imageUrl *dto.MessageImageUrl, m
if !constant.GetMediaTokenNotStream && !stream {
return 256, nil
}
// 是否统计图片token
if !constant.GetMediaToken {
return 256, nil
}
if info.ChannelType == common.ChannelTypeGemini || info.ChannelType == common.ChannelTypeVertexAi || info.ChannelType == common.ChannelTypeAnthropic {
return 256, nil
}
// 同步One API的图片计费逻辑
if imageUrl.Detail == "auto" || imageUrl.Detail == "" {
imageUrl.Detail = "high"
@@ -109,6 +102,13 @@ func getImageToken(info *relaycommon.RelayInfo, imageUrl *dto.MessageImageUrl, m
tileTokens = 5667
baseTokens = 2833
}
// 是否统计图片token
if !constant.GetMediaToken {
return 3 * baseTokens, nil
}
if info.ChannelType == common.ChannelTypeGemini || info.ChannelType == common.ChannelTypeVertexAi || info.ChannelType == common.ChannelTypeAnthropic {
return 3 * baseTokens, nil
}
var config image.Config
var err error
var format string

5127
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -825,6 +825,12 @@ const LogsTable = () => {
dataSource={logs}
rowKey="key"
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: logs.length
}),
currentPage: activePage,
pageSize: pageSize,
total: logCount,

View File

@@ -178,6 +178,7 @@ const RedemptionsTable = () => {
const [searching, setSearching] = useState(false);
const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
const [selectedKeys, setSelectedKeys] = useState([]);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [editingRedemption, setEditingRedemption] = useState({
id: undefined,
});
@@ -187,40 +188,20 @@ const RedemptionsTable = () => {
setShowEdit(false);
};
// const setCount = (data) => {
// if (data.length >= (activePage) * ITEMS_PER_PAGE) {
// setTokenCount(data.length + 1);
// } else {
// setTokenCount(data.length);
// }
// }
const setRedemptionFormat = (redeptions) => {
// for (let i = 0; i < redeptions.length; i++) {
// redeptions[i].key = '' + redeptions[i].id;
// }
// data.key = '' + data.id
setRedemptions(redeptions);
if (redeptions.length >= activePage * ITEMS_PER_PAGE) {
setTokenCount(redeptions.length + 1);
} else {
setTokenCount(redeptions.length);
}
};
const loadRedemptions = async (startIdx) => {
const res = await API.get(`/api/redemption/?p=${startIdx}`);
const loadRedemptions = async (startIdx, pageSize) => {
const res = await API.get(`/api/redemption/?p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setRedemptionFormat(data);
} else {
let newRedemptions = redemptions;
newRedemptions.push(...data);
setRedemptionFormat(newRedemptions);
}
const newPageData = data.items;
setActivePage(data.page);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
showError(message);
}
setLoading(false);
};
@@ -248,16 +229,15 @@ const RedemptionsTable = () => {
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadRedemptions(activePage - 1);
if (activePage === Math.ceil(redemptions.length / pageSize) + 1) {
await loadRedemptions(activePage - 1, pageSize);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadRedemptions(0)
loadRedemptions(0, pageSize)
.then()
.catch((reason) => {
showError(reason);
@@ -265,7 +245,7 @@ const RedemptionsTable = () => {
}, []);
const refresh = async () => {
await loadRedemptions(activePage - 1);
await loadRedemptions(activePage - 1, pageSize);
};
const manageRedemption = async (id, action, record) => {
@@ -300,23 +280,21 @@ const RedemptionsTable = () => {
}
};
const searchRedemptions = async () => {
const searchRedemptions = async (keyword, page, pageSize) => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadRedemptions(0);
setActivePage(1);
return;
await loadRedemptions(page, pageSize);
return;
}
setSearching(true);
const res = await API.get(
`/api/redemption/search?keyword=${searchKeyword}`,
);
const res = await API.get(`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
setRedemptions(data);
setActivePage(1);
const newPageData = data.items;
setActivePage(data.page);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
showError(message);
}
setSearching(false);
};
@@ -341,16 +319,14 @@ const RedemptionsTable = () => {
const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadRedemptions(page - 1).then((r) => {});
if (searchKeyword === '') {
loadRedemptions(page, pageSize).then();
} else {
searchRedemptions(searchKeyword, page, pageSize).then();
}
};
let pageData = redemptions.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE,
);
let pageData = redemptions;
const rowSelection = {
onSelect: (record, selected) => {},
onSelectAll: (selected, selectedRows) => {},
@@ -379,7 +355,9 @@ const RedemptionsTable = () => {
visiable={showEdit}
handleClose={closeEdit}
></EditRedemption>
<Form onSubmit={searchRedemptions}>
<Form onSubmit={()=> {
searchRedemptions(searchKeyword, activePage, pageSize).then();
}}>
<Form.Input
label={t('搜索关键字')}
field='keyword'
@@ -431,20 +409,25 @@ const RedemptionsTable = () => {
dataSource={pageData}
pagination={{
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
pageSize: pageSize,
total: tokenCount,
// showSizeChanger: true,
// pageSizeOptions: [10, 20, 50, 100],
showSizeChanger: true,
pageSizeOpts: [10, 20, 50, 100],
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: redemptions.length
total: tokenCount
}),
// onPageSizeChange: (size) => {
// setPageSize(size);
// setActivePage(1);
// },
onPageSizeChange: (size) => {
setPageSize(size);
setActivePage(1);
if (searchKeyword === '') {
loadRedemptions(1, size).then();
} else {
searchRedemptions(searchKeyword, 1, size).then();
}
},
onPageChange: handlePageChange,
}}
loading={loading}

View File

@@ -231,6 +231,7 @@ const UsersTable = () => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [searchGroup, setSearchGroup] = useState('');
@@ -242,14 +243,6 @@ const UsersTable = () => {
id: undefined,
});
const setCount = (data) => {
if (data.length >= activePage * ITEMS_PER_PAGE) {
setUserCount(data.length + 1);
} else {
setUserCount(data.length);
}
};
const removeRecord = (key) => {
let newDataSource = [...users];
if (key != null) {
@@ -263,37 +256,30 @@ const UsersTable = () => {
}
};
const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`);
const setUserFormat = (users) => {
for (let i = 0; i < users.length; i++) {
users[i].key = users[i].id;
}
setUsers(users);
}
const loadUsers = async (startIdx, pageSize) => {
const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
if (startIdx === 0) {
setUsers(data);
setCount(data);
} else {
let newUsers = users;
newUsers.push(...data);
setUsers(newUsers);
setCount(newUsers);
}
const newPageData = data.items;
setActivePage(data.page);
setUserCount(data.total);
setUserFormat(newPageData);
} else {
showError(message);
}
setLoading(false);
};
const onPaginationChange = (e, { activePage }) => {
(async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadUsers(activePage - 1);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers(0)
loadUsers(0, pageSize)
.then()
.catch((reason) => {
showError(reason);
@@ -341,21 +327,22 @@ const UsersTable = () => {
}
};
const searchUsers = async (searchKeyword, searchGroup) => {
const searchUsers = async (startIdx, pageSize, searchKeyword, searchGroup) => {
if (searchKeyword === '' && searchGroup === '') {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
return;
// if keyword is blank, load files instead.
await loadUsers(startIdx, pageSize);
return;
}
setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}`);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
setActivePage(1);
const newPageData = data.items;
setActivePage(data.page);
setUserCount(data.total);
setUserFormat(newPageData);
} else {
showError(message);
showError(message);
}
setSearching(false);
};
@@ -364,33 +351,15 @@ const UsersTable = () => {
setSearchKeyword(value.trim());
};
const sortUser = (key) => {
if (users.length === 0) return;
setLoading(true);
let sortedUsers = [...users];
sortedUsers.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]);
});
if (sortedUsers[0].id === users[0].id) {
sortedUsers.reverse();
}
setUsers(sortedUsers);
setLoading(false);
};
const handlePageChange = (page) => {
setActivePage(page);
if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
loadUsers(page - 1).then((r) => {});
if (searchKeyword === '' && searchGroup === '') {
loadUsers(page, pageSize).then();
} else {
searchUsers(page, pageSize, searchKeyword, searchGroup).then();
}
};
const pageData = users.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE,
);
const closeAddUser = () => {
setShowAddUser(false);
};
@@ -403,8 +372,9 @@ const UsersTable = () => {
};
const refresh = async () => {
setActivePage(1)
if (searchKeyword === '') {
await loadUsers(activePage - 1);
await loadUsers(activePage, pageSize);
} else {
await searchUsers(searchKeyword, searchGroup);
}
@@ -429,6 +399,17 @@ const UsersTable = () => {
}
};
const handlePageSizeChange = async (size) => {
localStorage.setItem('page-size', size + '');
setPageSize(size);
setActivePage(1);
loadUsers(activePage, size)
.then()
.catch((reason) => {
showError(reason);
});
};
return (
<>
<AddUser
@@ -444,29 +425,32 @@ const UsersTable = () => {
></EditUser>
<Form
onSubmit={() => {
searchUsers(searchKeyword, searchGroup);
searchUsers(activePage, pageSize, searchKeyword, searchGroup);
}}
labelPosition='left'
>
<div style={{ display: 'flex' }}>
<Space>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索用户的 ID用户名显示名称以及邮箱地址 ...')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
<Tooltip content={t('支持搜索用户的 ID、用户名、显示名称和邮箱地址')}>
<Form.Input
label={t('搜索关键字')}
icon='search'
field='keyword'
iconPosition='left'
placeholder={t('搜索关键字')}
value={searchKeyword}
loading={searching}
onChange={(value) => handleKeywordChange(value)}
/>
</Tooltip>
<Form.Select
field='group'
label={t('分组')}
optionList={groupOptions}
onChange={(value) => {
setSearchGroup(value);
searchUsers(searchKeyword, value);
searchUsers(activePage, pageSize, searchKeyword, value);
}}
/>
<Button
@@ -492,7 +476,7 @@ const UsersTable = () => {
<Table
columns={columns}
dataSource={pageData}
dataSource={users}
pagination={{
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
@@ -501,9 +485,13 @@ const UsersTable = () => {
total: users.length
}),
currentPage: activePage,
pageSize: ITEMS_PER_PAGE,
pageSize: pageSize,
total: userCount,
pageSizeOpts: [10, 20, 50, 100],
showSizeChanger: true,
onPageSizeChange: (size) => {
handlePageSizeChange(size);
},
onPageChange: handlePageChange,
}}
loading={loading}

View File

@@ -315,6 +315,9 @@ export function renderAudioModelPrice(
if (completionRatio === undefined) {
completionRatio = 0;
}
// try toFixed audioRatio
audioRatio = parseFloat(audioRatio).toFixed(6);
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = modelRatio * 2.0;
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
@@ -326,13 +329,31 @@ export function renderAudioModelPrice(
return (
<>
<article>
<p>提示${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p>
<p>补全${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p>
<p>音频提示${inputRatioPrice} * {groupRatio} * {audioRatio} = ${inputRatioPrice * audioRatio * groupRatio} / 1M tokens</p>
<p>音频补全${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
<p></p>
<p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: inputRatioPrice,
ratio: groupRatio,
total: inputRatioPrice * groupRatio
})}</p>
<p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
price: completionRatioPrice,
ratio: groupRatio,
total: completionRatioPrice * groupRatio
})}</p>
<p>{i18next.t('音频提示:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens', {
price: inputRatioPrice,
ratio: groupRatio,
audioRatio,
total: inputRatioPrice * audioRatio * groupRatio
})}</p>
<p>{i18next.t('音频补全:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens', {
price: inputRatioPrice,
ratio: groupRatio,
audioRatio,
audioCompRatio: audioCompletionRatio,
total: inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio
})}</p>
<p>
{i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
{i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
input: inputTokens,
price: inputRatioPrice,
completion: completionTokens,
@@ -340,13 +361,21 @@ export function renderAudioModelPrice(
})}
</p>
<p>
音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
{i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}', {
input: audioInputTokens,
completion: audioCompletionTokens,
price: inputRatioPrice,
audioRatio,
audioCompRatio: audioCompletionRatio
})}
</p>
<p>
文字 + 音频 * 分组 {groupRatio} =
${price.toFixed(6)}
{i18next.t('(文字 + 音频)* 分组倍率 {{ratio}} = ${{total}}', {
ratio: groupRatio,
total: price.toFixed(6)
})}
</p>
<p>仅供参考以实际扣费为准</p>
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
</article>
</>
);
@@ -377,13 +406,13 @@ const colors = [
'red',
'teal',
'violet',
'yellow',
'yellow'
];
// 基础10色色板 (N ≤ 10)
const baseColors = [
'#1664FF', // 主色
'#1AC6FF',
'#1AC6FF',
'#FF8A00',
'#3CC780',
'#7442D4',

View File

@@ -1053,6 +1053,11 @@
"模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}": "Model price: ${{price}} * Group ratio: {{ratio}} = ${{total}}",
"提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Prompt: ${{price}} * {{ratio}} = ${{total}} / 1M tokens",
"补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens": "Completion: ${{price}} * {{ratio}} = ${{total}} / 1M tokens",
"音频提示:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens": "Audio prompt: ${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens",
"音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}": "Audio prompt {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + Audio completion {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}",
"音频补全:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens": "Audio completion: ${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens",
"(文字 + 音频)* 分组倍率 {{ratio}} = ${{total}}": "(Text + Audio) * Group ratio {{ratio}} = ${{total}}",
"文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +": "Text prompt {{input}} tokens / 1M tokens * ${{price}} + Text completion {{completion}} tokens / 1M tokens * ${{compPrice}} +",
"提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{input}} tokens / 1M tokens * ${{price}} + Completion {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}",
"价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}",
"模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}",
@@ -1240,5 +1245,7 @@
"时间范围": "Time range",
"批量设置标签": "Batch set tag",
"请输入要设置的标签名称": "Please enter the tag name to be set",
"请输入标签名称": "Please enter the tag name"
"请输入标签名称": "Please enter the tag name",
"支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address",
"已注销": "Logged out"
}

View File

@@ -34,8 +34,9 @@ const ChatPage = () => {
return !isLoading && iframeSrc ? (
<iframe
src={iframeSrc}
style={{ width: '100%', height: '85vh', border: 'none' }}
style={{ width: '100%', height: '100%', border: 'none' }}
title="Token Frame"
allow="camera;microphone"
/>
) : (
<div>

View File

@@ -19,7 +19,11 @@ const AddUser = (props) => {
const submit = async () => {
setLoading(true);
if (inputs.username === '' || inputs.password === '') return;
if (inputs.username === '' || inputs.password === '') {
setLoading(false);
showError('用户名和密码不能为空!');
return;
}
const res = await API.post(`/api/user/`, inputs);
const { success, message } = res.data;
if (success) {