Merge remote-tracking branch 'origin/upstream/main'

This commit is contained in:
Laisky.Cai 2024-03-18 01:31:21 +00:00
commit d379377eca
40 changed files with 340 additions and 145 deletions

View File

@ -241,17 +241,19 @@ If the channel ID is not provided, load balancing will be used to distribute the
+ Example: `SESSION_SECRET=random_string` + Example: `SESSION_SECRET=random_string`
3. `SQL_DSN`: When set, the specified database will be used instead of SQLite. Please use MySQL version 8.0. 3. `SQL_DSN`: When set, the specified database will be used instead of SQLite. Please use MySQL version 8.0.
+ Example: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` + Example: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
4. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address. 4. `LOG_SQL_DSN`: When set, a separate database will be used for the `logs` table; please use MySQL or PostgreSQL.
+ Example: `LOG_SQL_DSN=root:123456@tcp(localhost:3306)/oneapi-logs`
5. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address.
+ Example: `FRONTEND_BASE_URL=https://openai.justsong.cn` + Example: `FRONTEND_BASE_URL=https://openai.justsong.cn`
5. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen. 6. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen.
+ Example: `SYNC_FREQUENCY=60` + Example: `SYNC_FREQUENCY=60`
6. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`. 7. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`.
+ Example: `NODE_TYPE=slave` + Example: `NODE_TYPE=slave`
7. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen. 8. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen.
+ Example: `CHANNEL_UPDATE_FREQUENCY=1440` + Example: `CHANNEL_UPDATE_FREQUENCY=1440`
8. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen. 9. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen.
+ Example: `CHANNEL_TEST_FREQUENCY=1440` + Example: `CHANNEL_TEST_FREQUENCY=1440`
9. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval. 10. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval.
+ Example: `POLLING_INTERVAL=5` + Example: `POLLING_INTERVAL=5`
### Command Line Parameters ### Command Line Parameters

View File

@ -242,17 +242,18 @@ graph LR
+ 例: `SESSION_SECRET=random_string` + 例: `SESSION_SECRET=random_string`
3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。 3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。
+ 例: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` + 例: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi`
4. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。 4. `LOG_SQL_DSN`: を設定すると、`logs`テーブルには独立したデータベースが使用されます。MySQLまたはPostgreSQLを使用してください。
5. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。
+ 例: `FRONTEND_BASE_URL=https://openai.justsong.cn` + 例: `FRONTEND_BASE_URL=https://openai.justsong.cn`
5. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。 6. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。
+ 例: `SYNC_FREQUENCY=60` + 例: `SYNC_FREQUENCY=60`
6. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master``slave` である。設定されていない場合、デフォルトは `master` 7. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master``slave` である。設定されていない場合、デフォルトは `master`
+ 例: `NODE_TYPE=slave` + 例: `NODE_TYPE=slave`
7. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。 8. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。
+ 例: `CHANNEL_UPDATE_FREQUENCY=1440` + 例: `CHANNEL_UPDATE_FREQUENCY=1440`
8. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。 9. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。
+ 例: `CHANNEL_TEST_FREQUENCY=1440` + 例: `CHANNEL_TEST_FREQUENCY=1440`
9. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。 10. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。
+ 例: `POLLING_INTERVAL=5` + 例: `POLLING_INTERVAL=5`
### コマンドラインパラメータ ### コマンドラインパラメータ

View File

@ -6,3 +6,4 @@ docker image: `ppcelery/one-api:latest`
- update token usage by API - update token usage by API
- support gpt-vision - support gpt-vision
- support update user's remained quota

View File

@ -151,3 +151,5 @@ var MetricQueueSize = env.Int("METRIC_QUEUE_SIZE", 10)
var MetricSuccessRateThreshold = env.Float64("METRIC_SUCCESS_RATE_THRESHOLD", 0.8) var MetricSuccessRateThreshold = env.Float64("METRIC_SUCCESS_RATE_THRESHOLD", 0.8)
var MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024) var MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024)
var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128) var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128)
var InitialRootToken = os.Getenv("INITIAL_ROOT_TOKEN")

View File

@ -73,14 +73,15 @@ var ModelRatio = map[string]float64{
"claude-3-sonnet-20240229": 3.0 / 1000 * USD, "claude-3-sonnet-20240229": 3.0 / 1000 * USD,
"claude-3-opus-20240229": 15.0 / 1000 * USD, "claude-3-opus-20240229": 15.0 / 1000 * USD,
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/hlrk4akp7 // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/hlrk4akp7
"ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens
"ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens
"ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens
"ERNIE-Bot-8k": 0.024 * RMB, "ERNIE-Bot-8k": 0.024 * RMB,
"Embedding-V1": 0.1429, // ¥0.002 / 1k tokens "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens
"bge-large-zh": 0.002 * RMB, "bge-large-zh": 0.002 * RMB,
"bge-large-en": 0.002 * RMB, "bge-large-en": 0.002 * RMB,
"bge-large-8k": 0.002 * RMB, "bge-large-8k": 0.002 * RMB,
// https://ai.google.dev/pricing
"PaLM-2": 1, "PaLM-2": 1,
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
"gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
@ -134,9 +135,9 @@ var ModelRatio = map[string]float64{
"mixtral-8x7b-32768": 0.27 / 1000 * USD, "mixtral-8x7b-32768": 0.27 / 1000 * USD,
"gemma-7b-it": 0.1 / 1000 * USD, "gemma-7b-it": 0.1 / 1000 * USD,
// https://platform.lingyiwanwu.com/docs#-计费单元 // https://platform.lingyiwanwu.com/docs#-计费单元
"yi-34b-chat-0205": 2.5 / 1000000 * RMB, "yi-34b-chat-0205": 2.5 / 1000 * RMB,
"yi-34b-chat-200k": 12.0 / 1000000 * RMB, "yi-34b-chat-200k": 12.0 / 1000 * RMB,
"yi-vl-plus": 6.0 / 1000000 * RMB, "yi-vl-plus": 6.0 / 1000 * RMB,
} }
var CompletionRatio = map[string]float64{} var CompletionRatio = map[string]float64{}

View File

@ -18,7 +18,10 @@ func GetAllTokens(c *gin.Context) {
if p < 0 { if p < 0 {
p = 0 p = 0
} }
tokens, err := model.GetAllUserTokens(userId, p*config.ItemsPerPage, config.ItemsPerPage)
order := c.Query("order")
tokens, err := model.GetAllUserTokens(userId, p*config.ItemsPerPage, config.ItemsPerPage, order)
if err != nil { if err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,

View File

@ -186,24 +186,27 @@ func Register(c *gin.Context) {
} }
func GetAllUsers(c *gin.Context) { func GetAllUsers(c *gin.Context) {
p, _ := strconv.Atoi(c.Query("p")) p, _ := strconv.Atoi(c.Query("p"))
if p < 0 { if p < 0 {
p = 0 p = 0
} }
users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage)
if err != nil { order := c.DefaultQuery("order", "")
c.JSON(http.StatusOK, gin.H{ users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage, order)
"success": false,
"message": err.Error(), if err != nil {
}) c.JSON(http.StatusOK, gin.H{
return "success": false,
} "message": err.Error(),
c.JSON(http.StatusOK, gin.H{ })
"success": true, return
"message": "", }
"data": users,
}) c.JSON(http.StatusOK, gin.H{
return "success": true,
"message": "",
"data": users,
})
} }
func SearchUsers(c *gin.Context) { func SearchUsers(c *gin.Context) {

View File

@ -25,7 +25,7 @@ func CreateRootAccountIfNeed() error {
var user User var user User
//if user.Status != util.UserStatusEnabled { //if user.Status != util.UserStatusEnabled {
if err := DB.First(&user).Error; err != nil { if err := DB.First(&user).Error; err != nil {
logger.SysLog("no user exists, create a root user for you: username is root, password is 123456") logger.SysLog("no user exists, creating a root user for you: username is root, password is 123456")
hashedPassword, err := common.Password2Hash("123456") hashedPassword, err := common.Password2Hash("123456")
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
@ -37,9 +37,25 @@ func CreateRootAccountIfNeed() error {
Status: common.UserStatusEnabled, Status: common.UserStatusEnabled,
DisplayName: "Root User", DisplayName: "Root User",
AccessToken: helper.GetUUID(), AccessToken: helper.GetUUID(),
Quota: 100000000, Quota: 500000000000000,
} }
DB.Create(&rootUser) DB.Create(&rootUser)
if config.InitialRootToken != "" {
logger.SysLog("creating initial root token as requested")
token := Token{
Id: 1,
UserId: rootUser.Id,
Key: config.InitialRootToken,
Status: common.TokenStatusEnabled,
Name: "Initial Root Token",
CreatedTime: helper.GetTimestamp(),
AccessedTime: helper.GetTimestamp(),
ExpiredTime: -1,
RemainQuota: 500000000000000,
UnlimitedQuota: true,
}
DB.Create(&token)
}
} }
return nil return nil
} }

View File

@ -14,7 +14,7 @@ type Redemption struct {
Key string `json:"key" gorm:"type:char(32);uniqueIndex"` Key string `json:"key" gorm:"type:char(32);uniqueIndex"`
Status int `json:"status" gorm:"default:1"` Status int `json:"status" gorm:"default:1"`
Name string `json:"name" gorm:"index"` Name string `json:"name" gorm:"index"`
Quota int64 `json:"quota" gorm:"default:100"` Quota int64 `json:"quota" gorm:"bigint;default:100"`
CreatedTime int64 `json:"created_time" gorm:"bigint"` CreatedTime int64 `json:"created_time" gorm:"bigint"`
RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"` RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"`
Count int `json:"count" gorm:"-:all"` // only for api request Count int `json:"count" gorm:"-:all"` // only for api request

View File

@ -21,15 +21,26 @@ type Token struct {
CreatedTime int64 `json:"created_time" gorm:"bigint"` CreatedTime int64 `json:"created_time" gorm:"bigint"`
AccessedTime int64 `json:"accessed_time" gorm:"bigint"` AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired
RemainQuota int64 `json:"remain_quota" gorm:"default:0"` RemainQuota int64 `json:"remain_quota" gorm:"bigint;default:0"`
UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"`
UsedQuota int64 `json:"used_quota" gorm:"default:0"` // used quota UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota
} }
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token, error) {
var tokens []*Token var tokens []*Token
var err error var err error
err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&tokens).Error query := DB.Where("user_id = ?", userId)
switch order {
case "remain_quota":
query = query.Order("unlimited_quota desc, remain_quota desc")
case "used_quota":
query = query.Order("used_quota desc")
default:
query = query.Order("id desc")
}
err = query.Limit(num).Offset(startIdx).Find(&tokens).Error
return tokens, err return tokens, err
} }

View File

@ -27,9 +27,9 @@ type User struct {
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
Quota int64 `json:"quota" gorm:"type:int;default:0"` Quota int64 `json:"quota" gorm:"bigint;default:0"`
UsedQuota int64 `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0;column:used_quota"` // used quota
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
Group string `json:"group" gorm:"type:varchar(32);default:'default'"` Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"` AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
@ -41,9 +41,22 @@ func GetMaxUserId() int {
return user.Id return user.Id
} }
func GetAllUsers(startIdx int, num int) (users []*User, err error) { func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) {
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted).Find(&users).Error query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted)
return users, err
switch order {
case "quota":
query = query.Order("quota desc")
case "used_quota":
query = query.Order("used_quota desc")
case "request_count":
query = query.Order("request_count desc")
default:
query = query.Order("id desc")
}
err = query.Find(&users).Error
return users, err
} }
func SearchUsers(keyword string) (users []*User, err error) { func SearchUsers(keyword string) (users []*User, err error) {

View File

@ -1,5 +1,7 @@
package gemini package gemini
// https://ai.google.dev/models/gemini
var ModelList = []string{ var ModelList = []string{
"gemini-pro", "gemini-1.0-pro-001", "gemini-pro", "gemini-1.0-pro-001",
"gemini-pro-vision", "gemini-1.0-pro-vision-001", "gemini-pro-vision", "gemini-1.0-pro-vision-001",

View File

@ -106,10 +106,15 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
} }
fullRequestURL := util.GetFullRequestURL(baseURL, requestURL, channelType) fullRequestURL := util.GetFullRequestURL(baseURL, requestURL, channelType)
if relayMode == constant.RelayModeAudioTranscription && channelType == common.ChannelTypeAzure { if channelType == common.ChannelTypeAzure {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
apiVersion := util.GetAzureAPIVersion(c) apiVersion := util.GetAzureAPIVersion(c)
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, audioModel, apiVersion) if relayMode == constant.RelayModeAudioTranscription {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, audioModel, apiVersion)
} else if relayMode == constant.RelayModeAudioSpeech {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/text-to-speech-quickstart?tabs=command-line#rest-api
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/speech?api-version=%s", baseURL, audioModel, apiVersion)
}
} }
requestBody := &bytes.Buffer{} requestBody := &bytes.Buffer{}
@ -125,7 +130,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
return openai.ErrorWrapper(err, "new_request_failed", http.StatusInternalServerError) return openai.ErrorWrapper(err, "new_request_failed", http.StatusInternalServerError)
} }
if relayMode == constant.RelayModeAudioTranscription && channelType == common.ChannelTypeAzure { if (relayMode == constant.RelayModeAudioTranscription || relayMode == constant.RelayModeAudioSpeech) && channelType == common.ChannelTypeAzure {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
apiKey := c.Request.Header.Get("Authorization") apiKey := c.Request.Header.Get("Authorization")
apiKey = strings.TrimPrefix(apiKey, "Bearer ") apiKey = strings.TrimPrefix(apiKey, "Bearer ")

View File

@ -62,7 +62,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
if meta.ChannelType == common.ChannelTypeAzure { if meta.ChannelType == common.ChannelTypeAzure {
// https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api // https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api
apiVersion := util.GetAzureAPIVersion(c) apiVersion := util.GetAzureAPIVersion(c)
// https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2023-06-01-preview // https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2024-03-01-preview
fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.BaseURL, imageRequest.Model, apiVersion) fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.BaseURL, imageRequest.Model, apiVersion)
} }

View File

@ -9,7 +9,7 @@
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。 1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
2. 把你的主题文件放到这个文件夹下。 2. 把你的主题文件放到这个文件夹下。
3. 修改你的 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。 3. 修改你的 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
4. 修改 `common/constants.go` 中的 `ValidThemes`,把你的主题名称注册进去。 4. 修改 `common/config/config.go` 中的 `ValidThemes`,把你的主题名称注册进去。
5. 修改 `web/THEMES` 文件,这里也需要同步修改。 5. 修改 `web/THEMES` 文件,这里也需要同步修改。
## 主题列表 ## 主题列表

View File

@ -9,7 +9,7 @@
name="description" name="description"
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure可用于二次分发管理 key仅单可执行文件已打包好 Docker 镜像,一键部署,开箱即用" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure可用于二次分发管理 key仅单可执行文件已打包好 Docker 镜像,一键部署,开箱即用"
/> />
<title>New API</title> <title>One API</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -247,6 +247,8 @@ const TokensTable = () => {
const [editingToken, setEditingToken] = useState({ const [editingToken, setEditingToken] = useState({
id: undefined id: undefined
}); });
const [orderBy, setOrderBy] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
const closeEdit = () => { const closeEdit = () => {
setShowEdit(false); setShowEdit(false);
@ -269,7 +271,7 @@ const TokensTable = () => {
let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize); let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
const loadTokens = async (startIdx) => { const loadTokens = async (startIdx) => {
setLoading(true); setLoading(true);
const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`); const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}&order=${orderBy}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (startIdx === 0) { if (startIdx === 0) {
@ -289,7 +291,7 @@ const TokensTable = () => {
(async () => { (async () => {
if (activePage === Math.ceil(tokens.length / pageSize) + 1) { if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
// In this case we have to load more data and then append them. // In this case we have to load more data and then append them.
await loadTokens(activePage - 1); await loadTokens(activePage - 1, orderBy);
} }
setActivePage(activePage); setActivePage(activePage);
})(); })();
@ -392,12 +394,12 @@ const TokensTable = () => {
}; };
useEffect(() => { useEffect(() => {
loadTokens(0) loadTokens(0, orderBy)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}, [pageSize]); }, [pageSize, orderBy]);
const removeRecord = key => { const removeRecord = key => {
let newDataSource = [...tokens]; let newDataSource = [...tokens];
@ -452,6 +454,7 @@ const TokensTable = () => {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadTokens(0); await loadTokens(0);
setActivePage(1); setActivePage(1);
setOrderBy('');
return; return;
} }
setSearching(true); setSearching(true);
@ -520,6 +523,23 @@ const TokensTable = () => {
} }
}; };
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
setDropdownVisible(false);
};
const renderSelectedOption = (orderBy) => {
switch (orderBy) {
case 'remain_quota':
return '按剩余额度排序';
case 'used_quota':
return '按已用额度排序';
default:
return '默认排序';
}
};
return ( return (
<> <>
<EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken> <EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken>
@ -579,6 +599,21 @@ const TokensTable = () => {
await copyText(keys); await copyText(keys);
} }
}>复制所选令牌到剪贴板</Button> }>复制所选令牌到剪贴板</Button>
<Dropdown
trigger="click"
position="bottomLeft"
visible={dropdownVisible}
onVisibleChange={(visible) => setDropdownVisible(visible)}
render={
<Dropdown.Menu>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: '' })}>默认排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'remain_quota' })}>按剩余额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button style={{ marginLeft: '10px' }}>{renderSelectedOption(orderBy)}</Button>
</Dropdown>
</> </>
); );
}; };

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui'; import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip, Dropdown } from '@douyinfe/semi-ui';
import { ITEMS_PER_PAGE } from '../constants'; import { ITEMS_PER_PAGE } from '../constants';
import { renderGroup, renderNumber, renderQuota } from '../helpers/render'; import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
import AddUser from '../pages/User/AddUser'; import AddUser from '../pages/User/AddUser';
@ -139,6 +139,8 @@ const UsersTable = () => {
const [editingUser, setEditingUser] = useState({ const [editingUser, setEditingUser] = useState({
id: undefined id: undefined
}); });
const [orderBy, setOrderBy] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
const setCount = (data) => { const setCount = (data) => {
if (data.length >= (activePage) * ITEMS_PER_PAGE) { if (data.length >= (activePage) * ITEMS_PER_PAGE) {
@ -162,7 +164,7 @@ const UsersTable = () => {
}; };
const loadUsers = async (startIdx) => { const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`); const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (startIdx === 0) { if (startIdx === 0) {
@ -184,19 +186,19 @@ const UsersTable = () => {
(async () => { (async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them. // In this case we have to load more data and then append them.
await loadUsers(activePage - 1); await loadUsers(activePage - 1, orderBy);
} }
setActivePage(activePage); setActivePage(activePage);
})(); })();
}; };
useEffect(() => { useEffect(() => {
loadUsers(0) loadUsers(0, orderBy)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}, []); }, [orderBy]);
const manageUser = async (username, action, record) => { const manageUser = async (username, action, record) => {
const res = await API.post('/api/user/manage', { const res = await API.post('/api/user/manage', {
@ -239,6 +241,7 @@ const UsersTable = () => {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadUsers(0); await loadUsers(0);
setActivePage(1); setActivePage(1);
setOrderBy('');
return; return;
} }
setSearching(true); setSearching(true);
@ -301,6 +304,25 @@ const UsersTable = () => {
} }
}; };
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
setDropdownVisible(false);
};
const renderSelectedOption = (orderBy) => {
switch (orderBy) {
case 'quota':
return '按剩余额度排序';
case 'used_quota':
return '按已用额度排序';
case 'request_count':
return '按请求次数排序';
default:
return '默认排序';
}
};
return ( return (
<> <>
<AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser> <AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser>
@ -331,6 +353,22 @@ const UsersTable = () => {
setShowAddUser(true); setShowAddUser(true);
} }
}>添加用户</Button> }>添加用户</Button>
<Dropdown
trigger="click"
position="bottomLeft"
visible={dropdownVisible}
onVisibleChange={(visible) => setDropdownVisible(visible)}
render={
<Dropdown.Menu>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: '' })}>默认排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'quota' })}>按剩余额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序</Dropdown.Item>
<Dropdown.Item onClick={() => handleOrderByChange('', { value: 'request_count' })}>按请求次数排序</Dropdown.Item>
</Dropdown.Menu>
}
>
<Button style={{ marginLeft: '10px' }}>{renderSelectedOption(orderBy)}</Button>
</Dropdown>
</> </>
); );
}; };

View File

@ -23,7 +23,7 @@ export function isRoot() {
export function getSystemName() { export function getSystemName() {
let system_name = localStorage.getItem('system_name'); let system_name = localStorage.getItem('system_name');
if (!system_name) return 'New API'; if (!system_name) return 'One API';
return system_name; return system_name;
} }

View File

@ -103,3 +103,14 @@ code {
display: none !important; display: none !important;
} }
} }
/* 隐藏浏览器默认的滚动条 */
body {
overflow: hidden;
}
/* 自定义滚动条样式 */
body::-webkit-scrollbar {
width: 0; /* 隐藏滚动条的宽度 */
}

View File

@ -230,7 +230,7 @@ const EditChannel = (props) => {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
} }
if (localInputs.type === 3 && localInputs.other === '') { if (localInputs.type === 3 && localInputs.other === '') {
localInputs.other = '2023-06-01-preview'; localInputs.other = '2024-03-01-preview';
} }
if (localInputs.type === 18 && localInputs.other === '') { if (localInputs.type === 18 && localInputs.other === '') {
localInputs.other = 'v2.1'; localInputs.other = 'v2.1';
@ -348,7 +348,7 @@ const EditChannel = (props) => {
<Input <Input
label='默认 API 版本' label='默认 API 版本'
name='azure_other' name='azure_other'
placeholder={'请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖'} placeholder={'请输入默认 API 版本例如2024-03-01-preview该配置可以被实际的请求查询参数所覆盖'}
onChange={value => { onChange={value => {
handleInputChange('other', value) handleInputChange('other', value)
}} }}

View File

@ -49,7 +49,7 @@ const typeConfig = {
base_url: "请填写AZURE_OPENAI_ENDPOINT", base_url: "请填写AZURE_OPENAI_ENDPOINT",
// 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的 // 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
other: "请输入默认API版本例如2023-06-01-preview", other: "请输入默认API版本例如2024-03-01-preview",
}, },
modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
}, },

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -3,186 +3,186 @@ export const CHANNEL_OPTIONS = {
key: 1, key: 1,
text: 'OpenAI', text: 'OpenAI',
value: 1, value: 1,
color: 'primary' color: 'success'
}, },
14: { 14: {
key: 14, key: 14,
text: 'Anthropic Claude', text: 'Anthropic Claude',
value: 14, value: 14,
color: 'info' color: 'primary'
}, },
3: { 3: {
key: 3, key: 3,
text: 'Azure OpenAI', text: 'Azure OpenAI',
value: 3, value: 3,
color: 'secondary' color: 'success'
}, },
11: { 11: {
key: 11, key: 11,
text: 'Google PaLM2', text: 'Google PaLM2',
value: 11, value: 11,
color: 'orange' color: 'warning'
}, },
24: { 24: {
key: 24, key: 24,
text: 'Google Gemini', text: 'Google Gemini',
value: 24, value: 24,
color: 'orange' color: 'warning'
}, },
28: { 28: {
key: 28, key: 28,
text: 'Mistral AI', text: 'Mistral AI',
value: 28, value: 28,
color: 'orange' color: 'warning'
}, },
15: { 15: {
key: 15, key: 15,
text: '百度文心千帆', text: '百度文心千帆',
value: 15, value: 15,
color: 'default' color: 'primary'
}, },
17: { 17: {
key: 17, key: 17,
text: '阿里通义千问', text: '阿里通义千问',
value: 17, value: 17,
color: 'default' color: 'primary'
}, },
18: { 18: {
key: 18, key: 18,
text: '讯飞星火认知', text: '讯飞星火认知',
value: 18, value: 18,
color: 'default' color: 'primary'
}, },
16: { 16: {
key: 16, key: 16,
text: '智谱 ChatGLM', text: '智谱 ChatGLM',
value: 16, value: 16,
color: 'default' color: 'primary'
}, },
19: { 19: {
key: 19, key: 19,
text: '360 智脑', text: '360 智脑',
value: 19, value: 19,
color: 'default' color: 'primary'
}, },
25: { 25: {
key: 25, key: 25,
text: 'Moonshot AI', text: 'Moonshot AI',
value: 25, value: 25,
color: 'default' color: 'primary'
}, },
23: { 23: {
key: 23, key: 23,
text: '腾讯混元', text: '腾讯混元',
value: 23, value: 23,
color: 'default' color: 'primary'
}, },
26: { 26: {
key: 26, key: 26,
text: '百川大模型', text: '百川大模型',
value: 26, value: 26,
color: 'default' color: 'primary'
}, },
27: { 27: {
key: 27, key: 27,
text: 'MiniMax', text: 'MiniMax',
value: 27, value: 27,
color: 'default' color: 'primary'
}, },
29: { 29: {
key: 29, key: 29,
text: 'Groq', text: 'Groq',
value: 29, value: 29,
color: 'default' color: 'primary'
}, },
30: { 30: {
key: 30, key: 30,
text: 'Ollama', text: 'Ollama',
value: 30, value: 30,
color: 'default' color: 'primary'
}, },
31: { 31: {
key: 31, key: 31,
text: '零一万物', text: '零一万物',
value: 31, value: 31,
color: 'default' color: 'primary'
}, },
8: { 8: {
key: 8, key: 8,
text: '自定义渠道', text: '自定义渠道',
value: 8, value: 8,
color: 'primary' color: 'error'
}, },
22: { 22: {
key: 22, key: 22,
text: '知识库FastGPT', text: '知识库FastGPT',
value: 22, value: 22,
color: 'default' color: 'success'
}, },
21: { 21: {
key: 21, key: 21,
text: '知识库AI Proxy', text: '知识库AI Proxy',
value: 21, value: 21,
color: 'purple' color: 'success'
}, },
20: { 20: {
key: 20, key: 20,
text: '代理OpenRouter', text: '代理OpenRouter',
value: 20, value: 20,
color: 'primary' color: 'success'
}, },
2: { 2: {
key: 2, key: 2,
text: '代理API2D', text: '代理API2D',
value: 2, value: 2,
color: 'primary' color: 'success'
}, },
5: { 5: {
key: 5, key: 5,
text: '代理OpenAI-SB', text: '代理OpenAI-SB',
value: 5, value: 5,
color: 'primary' color: 'success'
}, },
7: { 7: {
key: 7, key: 7,
text: '代理OhMyGPT', text: '代理OhMyGPT',
value: 7, value: 7,
color: 'primary' color: 'success'
}, },
10: { 10: {
key: 10, key: 10,
text: '代理AI Proxy', text: '代理AI Proxy',
value: 10, value: 10,
color: 'primary' color: 'success'
}, },
4: { 4: {
key: 4, key: 4,
text: '代理CloseAI', text: '代理CloseAI',
value: 4, value: 4,
color: 'primary' color: 'success'
}, },
6: { 6: {
key: 6, key: 6,
text: '代理OpenAI Max', text: '代理OpenAI Max',
value: 6, value: 6,
color: 'primary' color: 'success'
}, },
9: { 9: {
key: 9, key: 9,
text: '代理AI.LS', text: '代理AI.LS',
value: 9, value: 9,
color: 'primary' color: 'success'
}, },
12: { 12: {
key: 12, key: 12,
text: '代理API2GPT', text: '代理API2GPT',
value: 12, value: 12,
color: 'primary' color: 'success'
}, },
13: { 13: {
key: 13, key: 13,
text: '代理AIGC2D', text: '代理AIGC2D',
value: 13, value: 13,
color: 'primary' color: 'success'
} }
}; };

View File

@ -180,7 +180,7 @@ const LoginForm = ({ ...others }) => {
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit} {...others}> <form noValidate onSubmit={handleSubmit} {...others}>
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.customInput }}> <FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-username-login">用户名</InputLabel> <InputLabel htmlFor="outlined-adornment-username-login">用户名 / 邮箱</InputLabel>
<OutlinedInput <OutlinedInput
id="outlined-adornment-username-login" id="outlined-adornment-username-login"
type="text" type="text"

View File

@ -3,6 +3,19 @@ import Label from "ui-component/Label";
import Stack from "@mui/material/Stack"; import Stack from "@mui/material/Stack";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
function name2color(name) {
switch (name) {
case "default":
return "info";
case "vip":
return "warning"
case "svip":
return "error"
default:
return "info"
}
}
const GroupLabel = ({ group }) => { const GroupLabel = ({ group }) => {
let groups = []; let groups = [];
if (group === "") { if (group === "") {
@ -14,7 +27,7 @@ const GroupLabel = ({ group }) => {
return ( return (
<Stack divider={<Divider orientation="vertical" flexItem />} spacing={0.5}> <Stack divider={<Divider orientation="vertical" flexItem />} spacing={0.5}>
{groups.map((group, index) => { {groups.map((group, index) => {
return <Label key={index}>{group}</Label>; return <Label key={index} color={name2color(group)}>{group}</Label>;
})} })}
</Stack> </Stack>
); );

View File

@ -10,6 +10,7 @@ const ChannelTableHead = () => {
<TableCell>类型</TableCell> <TableCell>类型</TableCell>
<TableCell>状态</TableCell> <TableCell>状态</TableCell>
<TableCell>响应时间</TableCell> <TableCell>响应时间</TableCell>
<TableCell>已消耗</TableCell>
<TableCell>余额</TableCell> <TableCell>余额</TableCell>
<TableCell>优先级</TableCell> <TableCell>优先级</TableCell>
<TableCell>操作</TableCell> <TableCell>操作</TableCell>

View File

@ -170,6 +170,9 @@ export default function ChannelTableRow({
handle_action={handleResponseTime} handle_action={handleResponseTime}
/> />
</TableCell> </TableCell>
<TableCell>
{renderNumber(item.used_quota)}
</TableCell>
<TableCell> <TableCell>
<Tooltip <Tooltip
title={"点击更新余额"} title={"点击更新余额"}

View File

@ -193,20 +193,14 @@ export default function ChannelPage() {
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}> <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">渠道</Typography> <Typography variant="h4">渠道</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}> <Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
新建渠道 新建渠道
</Button> </Button>
</Stack> </Stack>
<Stack mb={5}>
<Alert severity="info">
OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新
</Alert>
</Stack>
<Card> <Card>
<Box component="form" onSubmit={searchChannels} noValidate> <Box component="form" onSubmit={searchChannels} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索渠道的 ID名称和密钥 ...'} /> <TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索渠道的 ID名称和密钥 ...'} />
</Box> </Box>
<Toolbar <Toolbar
@ -220,7 +214,7 @@ export default function ChannelPage() {
> >
<Container> <Container>
{matchUpMd ? ( {matchUpMd ? (
<ButtonGroup variant="outlined" aria-label="outlined small primary button group"> <ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}> <Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新 刷新
</Button> </Button>

View File

@ -41,7 +41,7 @@ const typeConfig = {
}, },
prompt: { prompt: {
base_url: "请填写AZURE_OPENAI_ENDPOINT", base_url: "请填写AZURE_OPENAI_ENDPOINT",
other: "请输入默认API版本例如2023-06-01-preview", other: "请输入默认API版本例如2024-03-01-preview",
}, },
}, },
11: { 11: {

View File

@ -65,7 +65,7 @@ const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) =
) : ( ) : (
<CardWrapper border={false} content={false}> <CardWrapper border={false} content={false}>
<Box sx={{ p: 2.25 }}> <Box sx={{ p: 2.25 }}>
<Grid container direction="column"> <Grid>
<Grid item sx={{ mb: 0.75 }}> <Grid item sx={{ mb: 0.75 }}>
<Grid container alignItems="center"> <Grid container alignItems="center">
<Grid item xs={6}> <Grid item xs={6}>

View File

@ -102,11 +102,11 @@ export default function Log() {
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}> <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">日志</Typography> <Typography variant="h4">日志</Typography>
</Stack> </Stack>
<Card> <Card>
<Box component="form" onSubmit={searchLogs} noValidate> <Box component="form" onSubmit={searchLogs} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} userIsAdmin={userIsAdmin} /> <TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} userIsAdmin={userIsAdmin} />
</Box> </Box>
<Toolbar <Toolbar
@ -119,7 +119,7 @@ export default function Log() {
}} }}
> >
<Container> <Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group"> <ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}> <Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新/清除搜索条件 刷新/清除搜索条件
</Button> </Button>

View File

@ -141,7 +141,7 @@ export default function Redemption() {
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}> <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">兑换</Typography> <Typography variant="h4">兑换</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}> <Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
@ -149,7 +149,7 @@ export default function Redemption() {
</Button> </Button>
</Stack> </Stack>
<Card> <Card>
<Box component="form" onSubmit={searchRedemptions} noValidate> <Box component="form" onSubmit={searchRedemptions} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索兑换码的ID和名称...'} /> <TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索兑换码的ID和名称...'} />
</Box> </Box>
<Toolbar <Toolbar
@ -162,7 +162,7 @@ export default function Redemption() {
}} }}
> >
<Container> <Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group"> <ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}> <Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新 刷新
</Button> </Button>

View File

@ -141,9 +141,8 @@ export default function Token() {
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}> <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">令牌</Typography> <Typography variant="h4">令牌</Typography>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
@ -155,13 +154,13 @@ export default function Token() {
新建令牌 新建令牌
</Button> </Button>
</Stack> </Stack>
<Stack mb={5}> <Stack mb={2}>
<Alert severity="info"> <Alert severity="info">
OpenAI API 基础地址 https://api.openai.com 替换为 <b>{siteInfo.server_address}</b>,复制下面的密钥即可使用 OpenAI API 基础地址 https://api.openai.com 替换为 <b>{siteInfo.server_address}</b>,复制下面的密钥即可使用
</Alert> </Alert>
</Stack> </Stack>
<Card> <Card>
<Box component="form" onSubmit={searchTokens} noValidate> <Box component="form" onSubmit={searchTokens} noValidate sx={{marginTop: 2}}>
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索令牌的名称...'} /> <TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索令牌的名称...'} />
</Box> </Box>
<Toolbar <Toolbar
@ -174,7 +173,7 @@ export default function Token() {
}} }}
> >
<Container> <Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group"> <ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}> <Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新 刷新
</Button> </Button>

View File

@ -139,7 +139,7 @@ export default function Users() {
return ( return (
<> <>
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}> <Stack direction="row" alignItems="center" justifyContent="space-between" mb={2.5}>
<Typography variant="h4">用户</Typography> <Typography variant="h4">用户</Typography>
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}> <Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
@ -147,7 +147,7 @@ export default function Users() {
</Button> </Button>
</Stack> </Stack>
<Card> <Card>
<Box component="form" onSubmit={searchUsers} noValidate> <Box component="form" onSubmit={searchUsers} noValidate sx={{marginTop: 2}}>
<TableToolBar <TableToolBar
filterName={searchKeyword} filterName={searchKeyword}
handleFilterName={handleSearchKeyword} handleFilterName={handleSearchKeyword}
@ -164,7 +164,7 @@ export default function Users() {
}} }}
> >
<Container> <Container>
<ButtonGroup variant="outlined" aria-label="outlined small primary button group"> <ButtonGroup variant="outlined" aria-label="outlined small primary button group" sx={{marginBottom: 2}}>
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}> <Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
刷新 刷新
</Button> </Button>

View File

@ -333,6 +333,8 @@ const ChannelsTable = () => {
setPromptShown("channel-test"); setPromptShown("channel-test");
}}> }}>
OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新 OpenAI 渠道已经不再支持通过 key 获取余额因此余额显示为 0对于支持的渠道类型请点击余额进行刷新
<br/>
渠道测试仅支持 chat 模型优先使用 gpt-3.5-turbo如果该模型不可用则使用你所配置的模型列表中的第一个模型
</Message> </Message>
) )
} }

View File

@ -94,7 +94,7 @@ const LoginForm = () => {
fluid fluid
icon='user' icon='user'
iconPosition='left' iconPosition='left'
placeholder='用户名' placeholder='用户名 / 邮箱地址'
name='username' name='username'
value={username} value={username}
onChange={handleChange} onChange={handleChange}

View File

@ -47,9 +47,10 @@ const TokensTable = () => {
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [showTopUpModal, setShowTopUpModal] = useState(false); const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0); const [targetTokenIdx, setTargetTokenIdx] = useState(0);
const [orderBy, setOrderBy] = useState('');
const loadTokens = async (startIdx) => { const loadTokens = async (startIdx) => {
const res = await API.get(`/api/token/?p=${startIdx}`); const res = await API.get(`/api/token/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (startIdx === 0) { if (startIdx === 0) {
@ -69,7 +70,7 @@ const TokensTable = () => {
(async () => { (async () => {
if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) { if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them. // In this case we have to load more data and then append them.
await loadTokens(activePage - 1); await loadTokens(activePage - 1, orderBy);
} }
setActivePage(activePage); setActivePage(activePage);
})(); })();
@ -159,12 +160,12 @@ const TokensTable = () => {
} }
useEffect(() => { useEffect(() => {
loadTokens(0) loadTokens(0, orderBy)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}, []); }, [orderBy]);
const manageToken = async (id, action, idx) => { const manageToken = async (id, action, idx) => {
let data = { id }; let data = { id };
@ -204,6 +205,7 @@ const TokensTable = () => {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadTokens(0); await loadTokens(0);
setActivePage(1); setActivePage(1);
setOrderBy('');
return; return;
} }
setSearching(true); setSearching(true);
@ -242,6 +244,11 @@ const TokensTable = () => {
setLoading(false); setLoading(false);
}; };
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
};
return ( return (
<> <>
<Form onSubmit={searchTokens}> <Form onSubmit={searchTokens}>
@ -404,6 +411,18 @@ const TokensTable = () => {
添加新的令牌 添加新的令牌
</Button> </Button>
<Button size='small' onClick={refresh} loading={loading}>刷新</Button> <Button size='small' onClick={refresh} loading={loading}>刷新</Button>
<Dropdown
placeholder='排序方式'
selection
options={[
{ key: '', text: '默认排序', value: '' },
{ key: 'remain_quota', text: '按剩余额度排序', value: 'remain_quota' },
{ key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
]}
value={orderBy}
onChange={handleOrderByChange}
style={{ marginLeft: '10px' }}
/>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import { Button, Form, Label, Pagination, Popup, Table, Dropdown } from 'semantic-ui-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
@ -25,9 +25,10 @@ const UsersTable = () => {
const [activePage, setActivePage] = useState(1); const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState(''); const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [orderBy, setOrderBy] = useState('');
const loadUsers = async (startIdx) => { const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`); const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
if (startIdx === 0) { if (startIdx === 0) {
@ -47,19 +48,19 @@ const UsersTable = () => {
(async () => { (async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them. // In this case we have to load more data and then append them.
await loadUsers(activePage - 1); await loadUsers(activePage - 1, orderBy);
} }
setActivePage(activePage); setActivePage(activePage);
})(); })();
}; };
useEffect(() => { useEffect(() => {
loadUsers(0) loadUsers(0, orderBy)
.then() .then()
.catch((reason) => { .catch((reason) => {
showError(reason); showError(reason);
}); });
}, []); }, [orderBy]);
const manageUser = (username, action, idx) => { const manageUser = (username, action, idx) => {
(async () => { (async () => {
@ -110,6 +111,7 @@ const UsersTable = () => {
// if keyword is blank, load files instead. // if keyword is blank, load files instead.
await loadUsers(0); await loadUsers(0);
setActivePage(1); setActivePage(1);
setOrderBy('');
return; return;
} }
setSearching(true); setSearching(true);
@ -148,6 +150,11 @@ const UsersTable = () => {
setLoading(false); setLoading(false);
}; };
const handleOrderByChange = (e, { value }) => {
setOrderBy(value);
setActivePage(1);
};
return ( return (
<> <>
<Form onSubmit={searchUsers}> <Form onSubmit={searchUsers}>
@ -322,6 +329,19 @@ const UsersTable = () => {
<Button size='small' as={Link} to='/user/add' loading={loading}> <Button size='small' as={Link} to='/user/add' loading={loading}>
添加新的用户 添加新的用户
</Button> </Button>
<Dropdown
placeholder='排序方式'
selection
options={[
{ key: '', text: '默认排序', value: '' },
{ key: 'quota', text: '按剩余额度排序', value: 'quota' },
{ key: 'used_quota', text: '按已用额度排序', value: 'used_quota' },
{ key: 'request_count', text: '按请求次数排序', value: 'request_count' },
]}
value={orderBy}
onChange={handleOrderByChange}
style={{ marginLeft: '10px' }}
/>
<Pagination <Pagination
floated='right' floated='right'
activePage={activePage} activePage={activePage}

View File

@ -160,7 +160,7 @@ const EditChannel = () => {
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
} }
if (localInputs.type === 3 && localInputs.other === '') { if (localInputs.type === 3 && localInputs.other === '') {
localInputs.other = '2023-12-01-preview'; localInputs.other = '2024-03-01-preview';
} }
if (localInputs.type === 18 && localInputs.other === '') { if (localInputs.type === 18 && localInputs.other === '') {
localInputs.other = 'v2.1'; localInputs.other = 'v2.1';
@ -242,7 +242,7 @@ const EditChannel = () => {
<Form.Input <Form.Input
label='默认 API 版本' label='默认 API 版本'
name='other' name='other'
placeholder={'请输入默认 API 版本例如2023-12-01-preview该配置可以被实际的请求查询参数所覆盖'} placeholder={'请输入默认 API 版本例如2024-03-01-preview该配置可以被实际的请求查询参数所覆盖'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.other} value={inputs.other}
autoComplete='new-password' autoComplete='new-password'