mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-09-19 01:56:37 +08:00
Merge remote-tracking branch 'origin/upstream/main'
This commit is contained in:
commit
d379377eca
14
README.en.md
14
README.en.md
@ -241,17 +241,19 @@ If the channel ID is not provided, load balancing will be used to distribute the
|
||||
+ 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.
|
||||
+ 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`
|
||||
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`
|
||||
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`
|
||||
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`
|
||||
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`
|
||||
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`
|
||||
|
||||
### Command Line Parameters
|
||||
|
13
README.ja.md
13
README.ja.md
@ -242,17 +242,18 @@ graph LR
|
||||
+ 例: `SESSION_SECRET=random_string`
|
||||
3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。
|
||||
+ 例: `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`
|
||||
5. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。
|
||||
6. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。
|
||||
+ 例: `SYNC_FREQUENCY=60`
|
||||
6. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。
|
||||
7. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。
|
||||
+ 例: `NODE_TYPE=slave`
|
||||
7. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。
|
||||
8. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。
|
||||
+ 例: `CHANNEL_UPDATE_FREQUENCY=1440`
|
||||
8. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。
|
||||
9. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。
|
||||
+ 例: `CHANNEL_TEST_FREQUENCY=1440`
|
||||
9. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。
|
||||
10. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。
|
||||
+ 例: `POLLING_INTERVAL=5`
|
||||
|
||||
### コマンドラインパラメータ
|
||||
|
@ -6,3 +6,4 @@ docker image: `ppcelery/one-api:latest`
|
||||
|
||||
- update token usage by API
|
||||
- support gpt-vision
|
||||
- support update user's remained quota
|
||||
|
@ -151,3 +151,5 @@ var MetricQueueSize = env.Int("METRIC_QUEUE_SIZE", 10)
|
||||
var MetricSuccessRateThreshold = env.Float64("METRIC_SUCCESS_RATE_THRESHOLD", 0.8)
|
||||
var MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024)
|
||||
var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128)
|
||||
|
||||
var InitialRootToken = os.Getenv("INITIAL_ROOT_TOKEN")
|
||||
|
@ -81,6 +81,7 @@ var ModelRatio = map[string]float64{
|
||||
"bge-large-zh": 0.002 * RMB,
|
||||
"bge-large-en": 0.002 * RMB,
|
||||
"bge-large-8k": 0.002 * RMB,
|
||||
// https://ai.google.dev/pricing
|
||||
"PaLM-2": 1,
|
||||
"gemini-pro": 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,
|
||||
"gemma-7b-it": 0.1 / 1000 * USD,
|
||||
// https://platform.lingyiwanwu.com/docs#-计费单元
|
||||
"yi-34b-chat-0205": 2.5 / 1000000 * RMB,
|
||||
"yi-34b-chat-200k": 12.0 / 1000000 * RMB,
|
||||
"yi-vl-plus": 6.0 / 1000000 * RMB,
|
||||
"yi-34b-chat-0205": 2.5 / 1000 * RMB,
|
||||
"yi-34b-chat-200k": 12.0 / 1000 * RMB,
|
||||
"yi-vl-plus": 6.0 / 1000 * RMB,
|
||||
}
|
||||
|
||||
var CompletionRatio = map[string]float64{}
|
||||
|
@ -18,7 +18,10 @@ func GetAllTokens(c *gin.Context) {
|
||||
if 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 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
|
@ -190,7 +190,10 @@ func GetAllUsers(c *gin.Context) {
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage)
|
||||
|
||||
order := c.DefaultQuery("order", "")
|
||||
users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage, order)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@ -198,12 +201,12 @@ func GetAllUsers(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": users,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func SearchUsers(c *gin.Context) {
|
||||
|
@ -25,7 +25,7 @@ func CreateRootAccountIfNeed() error {
|
||||
var user User
|
||||
//if user.Status != util.UserStatusEnabled {
|
||||
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")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
@ -37,9 +37,25 @@ func CreateRootAccountIfNeed() error {
|
||||
Status: common.UserStatusEnabled,
|
||||
DisplayName: "Root User",
|
||||
AccessToken: helper.GetUUID(),
|
||||
Quota: 100000000,
|
||||
Quota: 500000000000000,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ type Redemption struct {
|
||||
Key string `json:"key" gorm:"type:char(32);uniqueIndex"`
|
||||
Status int `json:"status" gorm:"default:1"`
|
||||
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"`
|
||||
RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"`
|
||||
Count int `json:"count" gorm:"-:all"` // only for api request
|
||||
|
@ -21,15 +21,26 @@ type Token struct {
|
||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||
AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
|
||||
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"`
|
||||
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 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
|
||||
}
|
||||
|
||||
|
@ -27,8 +27,8 @@ type User struct {
|
||||
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!
|
||||
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"`
|
||||
UsedQuota int64 `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota
|
||||
Quota int64 `json:"quota" gorm:"bigint;default:0"`
|
||||
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
|
||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
|
||||
@ -41,8 +41,21 @@ func GetMaxUserId() int {
|
||||
return user.Id
|
||||
}
|
||||
|
||||
func GetAllUsers(startIdx int, num int) (users []*User, err error) {
|
||||
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted).Find(&users).Error
|
||||
func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) {
|
||||
query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package gemini
|
||||
|
||||
// https://ai.google.dev/models/gemini
|
||||
|
||||
var ModelList = []string{
|
||||
"gemini-pro", "gemini-1.0-pro-001",
|
||||
"gemini-pro-vision", "gemini-1.0-pro-vision-001",
|
||||
|
@ -106,10 +106,15 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
|
||||
}
|
||||
|
||||
fullRequestURL := util.GetFullRequestURL(baseURL, requestURL, channelType)
|
||||
if relayMode == constant.RelayModeAudioTranscription && channelType == common.ChannelTypeAzure {
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api
|
||||
if channelType == common.ChannelTypeAzure {
|
||||
apiVersion := util.GetAzureAPIVersion(c)
|
||||
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{}
|
||||
@ -125,7 +130,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
|
||||
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
|
||||
apiKey := c.Request.Header.Get("Authorization")
|
||||
apiKey = strings.TrimPrefix(apiKey, "Bearer ")
|
||||
|
@ -62,7 +62,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
|
||||
2. 把你的主题文件放到这个文件夹下。
|
||||
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` 文件,这里也需要同步修改。
|
||||
|
||||
## 主题列表
|
||||
|
@ -9,7 +9,7 @@
|
||||
name="description"
|
||||
content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
|
||||
/>
|
||||
<title>New API</title>
|
||||
<title>One API</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
@ -247,6 +247,8 @@ const TokensTable = () => {
|
||||
const [editingToken, setEditingToken] = useState({
|
||||
id: undefined
|
||||
});
|
||||
const [orderBy, setOrderBy] = useState('');
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
|
||||
const closeEdit = () => {
|
||||
setShowEdit(false);
|
||||
@ -269,7 +271,7 @@ const TokensTable = () => {
|
||||
let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
|
||||
const loadTokens = async (startIdx) => {
|
||||
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;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
@ -289,7 +291,7 @@ const TokensTable = () => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(tokens.length / pageSize) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadTokens(activePage - 1);
|
||||
await loadTokens(activePage - 1, orderBy);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
@ -392,12 +394,12 @@ const TokensTable = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens(0)
|
||||
loadTokens(0, orderBy)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, [pageSize]);
|
||||
}, [pageSize, orderBy]);
|
||||
|
||||
const removeRecord = key => {
|
||||
let newDataSource = [...tokens];
|
||||
@ -452,6 +454,7 @@ const TokensTable = () => {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadTokens(0);
|
||||
setActivePage(1);
|
||||
setOrderBy('');
|
||||
return;
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken>
|
||||
@ -579,6 +599,21 @@ const TokensTable = () => {
|
||||
await copyText(keys);
|
||||
}
|
||||
}>复制所选令牌到剪贴板</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
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 { renderGroup, renderNumber, renderQuota } from '../helpers/render';
|
||||
import AddUser from '../pages/User/AddUser';
|
||||
@ -139,6 +139,8 @@ const UsersTable = () => {
|
||||
const [editingUser, setEditingUser] = useState({
|
||||
id: undefined
|
||||
});
|
||||
const [orderBy, setOrderBy] = useState('');
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
|
||||
const setCount = (data) => {
|
||||
if (data.length >= (activePage) * ITEMS_PER_PAGE) {
|
||||
@ -162,7 +164,7 @@ const UsersTable = () => {
|
||||
};
|
||||
|
||||
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;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
@ -184,19 +186,19 @@ const UsersTable = () => {
|
||||
(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);
|
||||
await loadUsers(activePage - 1, orderBy);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers(0)
|
||||
loadUsers(0, orderBy)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
}, [orderBy]);
|
||||
|
||||
const manageUser = async (username, action, record) => {
|
||||
const res = await API.post('/api/user/manage', {
|
||||
@ -239,6 +241,7 @@ const UsersTable = () => {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadUsers(0);
|
||||
setActivePage(1);
|
||||
setOrderBy('');
|
||||
return;
|
||||
}
|
||||
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 (
|
||||
<>
|
||||
<AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser>
|
||||
@ -331,6 +353,22 @@ const UsersTable = () => {
|
||||
setShowAddUser(true);
|
||||
}
|
||||
}>添加用户</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -23,7 +23,7 @@ export function isRoot() {
|
||||
|
||||
export function getSystemName() {
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
if (!system_name) return 'New API';
|
||||
if (!system_name) return 'One API';
|
||||
return system_name;
|
||||
}
|
||||
|
||||
|
@ -103,3 +103,14 @@ code {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 隐藏浏览器默认的滚动条 */
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
body::-webkit-scrollbar {
|
||||
width: 0; /* 隐藏滚动条的宽度 */
|
||||
}
|
@ -230,7 +230,7 @@ const EditChannel = (props) => {
|
||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||
}
|
||||
if (localInputs.type === 3 && localInputs.other === '') {
|
||||
localInputs.other = '2023-06-01-preview';
|
||||
localInputs.other = '2024-03-01-preview';
|
||||
}
|
||||
if (localInputs.type === 18 && localInputs.other === '') {
|
||||
localInputs.other = 'v2.1';
|
||||
@ -348,7 +348,7 @@ const EditChannel = (props) => {
|
||||
<Input
|
||||
label='默认 API 版本'
|
||||
name='azure_other'
|
||||
placeholder={'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||
placeholder={'请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||
onChange={value => {
|
||||
handleInputChange('other', value)
|
||||
}}
|
||||
|
@ -49,7 +49,7 @@ const typeConfig = {
|
||||
base_url: "请填写AZURE_OPENAI_ENDPOINT",
|
||||
|
||||
// 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
|
||||
other: "请输入默认API版本,例如:2023-06-01-preview",
|
||||
other: "请输入默认API版本,例如:2024-03-01-preview",
|
||||
},
|
||||
modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
|
||||
},
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 4.2 KiB |
@ -3,186 +3,186 @@ export const CHANNEL_OPTIONS = {
|
||||
key: 1,
|
||||
text: 'OpenAI',
|
||||
value: 1,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
14: {
|
||||
key: 14,
|
||||
text: 'Anthropic Claude',
|
||||
value: 14,
|
||||
color: 'info'
|
||||
color: 'primary'
|
||||
},
|
||||
3: {
|
||||
key: 3,
|
||||
text: 'Azure OpenAI',
|
||||
value: 3,
|
||||
color: 'secondary'
|
||||
color: 'success'
|
||||
},
|
||||
11: {
|
||||
key: 11,
|
||||
text: 'Google PaLM2',
|
||||
value: 11,
|
||||
color: 'orange'
|
||||
color: 'warning'
|
||||
},
|
||||
24: {
|
||||
key: 24,
|
||||
text: 'Google Gemini',
|
||||
value: 24,
|
||||
color: 'orange'
|
||||
color: 'warning'
|
||||
},
|
||||
28: {
|
||||
key: 28,
|
||||
text: 'Mistral AI',
|
||||
value: 28,
|
||||
color: 'orange'
|
||||
color: 'warning'
|
||||
},
|
||||
15: {
|
||||
key: 15,
|
||||
text: '百度文心千帆',
|
||||
value: 15,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
17: {
|
||||
key: 17,
|
||||
text: '阿里通义千问',
|
||||
value: 17,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
18: {
|
||||
key: 18,
|
||||
text: '讯飞星火认知',
|
||||
value: 18,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
16: {
|
||||
key: 16,
|
||||
text: '智谱 ChatGLM',
|
||||
value: 16,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
19: {
|
||||
key: 19,
|
||||
text: '360 智脑',
|
||||
value: 19,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
25: {
|
||||
key: 25,
|
||||
text: 'Moonshot AI',
|
||||
value: 25,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
23: {
|
||||
key: 23,
|
||||
text: '腾讯混元',
|
||||
value: 23,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
26: {
|
||||
key: 26,
|
||||
text: '百川大模型',
|
||||
value: 26,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
27: {
|
||||
key: 27,
|
||||
text: 'MiniMax',
|
||||
value: 27,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
29: {
|
||||
key: 29,
|
||||
text: 'Groq',
|
||||
value: 29,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
30: {
|
||||
key: 30,
|
||||
text: 'Ollama',
|
||||
value: 30,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
31: {
|
||||
key: 31,
|
||||
text: '零一万物',
|
||||
value: 31,
|
||||
color: 'default'
|
||||
color: 'primary'
|
||||
},
|
||||
8: {
|
||||
key: 8,
|
||||
text: '自定义渠道',
|
||||
value: 8,
|
||||
color: 'primary'
|
||||
color: 'error'
|
||||
},
|
||||
22: {
|
||||
key: 22,
|
||||
text: '知识库:FastGPT',
|
||||
value: 22,
|
||||
color: 'default'
|
||||
color: 'success'
|
||||
},
|
||||
21: {
|
||||
key: 21,
|
||||
text: '知识库:AI Proxy',
|
||||
value: 21,
|
||||
color: 'purple'
|
||||
color: 'success'
|
||||
},
|
||||
20: {
|
||||
key: 20,
|
||||
text: '代理:OpenRouter',
|
||||
value: 20,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
2: {
|
||||
key: 2,
|
||||
text: '代理:API2D',
|
||||
value: 2,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
5: {
|
||||
key: 5,
|
||||
text: '代理:OpenAI-SB',
|
||||
value: 5,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
7: {
|
||||
key: 7,
|
||||
text: '代理:OhMyGPT',
|
||||
value: 7,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
10: {
|
||||
key: 10,
|
||||
text: '代理:AI Proxy',
|
||||
value: 10,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
4: {
|
||||
key: 4,
|
||||
text: '代理:CloseAI',
|
||||
value: 4,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
6: {
|
||||
key: 6,
|
||||
text: '代理:OpenAI Max',
|
||||
value: 6,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
9: {
|
||||
key: 9,
|
||||
text: '代理:AI.LS',
|
||||
value: 9,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
12: {
|
||||
key: 12,
|
||||
text: '代理:API2GPT',
|
||||
value: 12,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
},
|
||||
13: {
|
||||
key: 13,
|
||||
text: '代理:AIGC2D',
|
||||
value: 13,
|
||||
color: 'primary'
|
||||
color: 'success'
|
||||
}
|
||||
};
|
||||
|
@ -180,7 +180,7 @@ const LoginForm = ({ ...others }) => {
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
|
||||
<form noValidate onSubmit={handleSubmit} {...others}>
|
||||
<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
|
||||
id="outlined-adornment-username-login"
|
||||
type="text"
|
||||
|
@ -3,6 +3,19 @@ import Label from "ui-component/Label";
|
||||
import Stack from "@mui/material/Stack";
|
||||
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 }) => {
|
||||
let groups = [];
|
||||
if (group === "") {
|
||||
@ -14,7 +27,7 @@ const GroupLabel = ({ group }) => {
|
||||
return (
|
||||
<Stack divider={<Divider orientation="vertical" flexItem />} spacing={0.5}>
|
||||
{groups.map((group, index) => {
|
||||
return <Label key={index}>{group}</Label>;
|
||||
return <Label key={index} color={name2color(group)}>{group}</Label>;
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ const ChannelTableHead = () => {
|
||||
<TableCell>类型</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell>响应时间</TableCell>
|
||||
<TableCell>已消耗</TableCell>
|
||||
<TableCell>余额</TableCell>
|
||||
<TableCell>优先级</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
|
@ -170,6 +170,9 @@ export default function ChannelTableRow({
|
||||
handle_action={handleResponseTime}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{renderNumber(item.used_quota)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={"点击更新余额"}
|
||||
|
@ -193,20 +193,14 @@ export default function ChannelPage() {
|
||||
|
||||
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>
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
新建渠道
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack mb={5}>
|
||||
<Alert severity="info">
|
||||
OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
|
||||
</Alert>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchChannels} noValidate>
|
||||
<Box component="form" onSubmit={searchChannels} noValidate sx={{marginTop: 2}}>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索渠道的 ID,名称和密钥 ...'} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
@ -220,7 +214,7 @@ export default function ChannelPage() {
|
||||
>
|
||||
<Container>
|
||||
{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>
|
||||
|
@ -41,7 +41,7 @@ const typeConfig = {
|
||||
},
|
||||
prompt: {
|
||||
base_url: "请填写AZURE_OPENAI_ENDPOINT",
|
||||
other: "请输入默认API版本,例如:2023-06-01-preview",
|
||||
other: "请输入默认API版本,例如:2024-03-01-preview",
|
||||
},
|
||||
},
|
||||
11: {
|
||||
|
@ -65,7 +65,7 @@ const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) =
|
||||
) : (
|
||||
<CardWrapper border={false} content={false}>
|
||||
<Box sx={{ p: 2.25 }}>
|
||||
<Grid container direction="column">
|
||||
<Grid>
|
||||
<Grid item sx={{ mb: 0.75 }}>
|
||||
<Grid container alignItems="center">
|
||||
<Grid item xs={6}>
|
||||
|
@ -102,11 +102,11 @@ export default function Log() {
|
||||
|
||||
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>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchLogs} noValidate>
|
||||
<Box component="form" onSubmit={searchLogs} noValidate sx={{marginTop: 2}}>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} userIsAdmin={userIsAdmin} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
@ -119,7 +119,7 @@ export default function Log() {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
@ -141,7 +141,7 @@ export default function Redemption() {
|
||||
|
||||
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>
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
@ -149,7 +149,7 @@ export default function Redemption() {
|
||||
</Button>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchRedemptions} noValidate>
|
||||
<Box component="form" onSubmit={searchRedemptions} noValidate sx={{marginTop: 2}}>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索兑换码的ID和名称...'} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
@ -162,7 +162,7 @@ export default function Redemption() {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
@ -141,9 +141,8 @@ export default function Token() {
|
||||
|
||||
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>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@ -155,13 +154,13 @@ export default function Token() {
|
||||
新建令牌
|
||||
</Button>
|
||||
</Stack>
|
||||
<Stack mb={5}>
|
||||
<Stack mb={2}>
|
||||
<Alert severity="info">
|
||||
将 OpenAI API 基础地址 https://api.openai.com 替换为 <b>{siteInfo.server_address}</b>,复制下面的密钥即可使用
|
||||
</Alert>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchTokens} noValidate>
|
||||
<Box component="form" onSubmit={searchTokens} noValidate sx={{marginTop: 2}}>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索令牌的名称...'} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
@ -174,7 +173,7 @@ export default function Token() {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
@ -139,7 +139,7 @@ export default function Users() {
|
||||
|
||||
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>
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
@ -147,7 +147,7 @@ export default function Users() {
|
||||
</Button>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchUsers} noValidate>
|
||||
<Box component="form" onSubmit={searchUsers} noValidate sx={{marginTop: 2}}>
|
||||
<TableToolBar
|
||||
filterName={searchKeyword}
|
||||
handleFilterName={handleSearchKeyword}
|
||||
@ -164,7 +164,7 @@ export default function Users() {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
@ -333,6 +333,8 @@ const ChannelsTable = () => {
|
||||
setPromptShown("channel-test");
|
||||
}}>
|
||||
OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。
|
||||
<br/>
|
||||
渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。
|
||||
</Message>
|
||||
)
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ const LoginForm = () => {
|
||||
fluid
|
||||
icon='user'
|
||||
iconPosition='left'
|
||||
placeholder='用户名'
|
||||
placeholder='用户名 / 邮箱地址'
|
||||
name='username'
|
||||
value={username}
|
||||
onChange={handleChange}
|
||||
|
@ -47,9 +47,10 @@ const TokensTable = () => {
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
||||
const [orderBy, setOrderBy] = useState('');
|
||||
|
||||
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;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
@ -69,7 +70,7 @@ const TokensTable = () => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadTokens(activePage - 1);
|
||||
await loadTokens(activePage - 1, orderBy);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
@ -159,12 +160,12 @@ const TokensTable = () => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens(0)
|
||||
loadTokens(0, orderBy)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
}, [orderBy]);
|
||||
|
||||
const manageToken = async (id, action, idx) => {
|
||||
let data = { id };
|
||||
@ -204,6 +205,7 @@ const TokensTable = () => {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadTokens(0);
|
||||
setActivePage(1);
|
||||
setOrderBy('');
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
@ -242,6 +244,11 @@ const TokensTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleOrderByChange = (e, { value }) => {
|
||||
setOrderBy(value);
|
||||
setActivePage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={searchTokens}>
|
||||
@ -404,6 +411,18 @@ const TokensTable = () => {
|
||||
添加新的令牌
|
||||
</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
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { API, showError, showSuccess } from '../helpers';
|
||||
|
||||
@ -25,9 +25,10 @@ const UsersTable = () => {
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [orderBy, setOrderBy] = useState('');
|
||||
|
||||
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;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
@ -47,19 +48,19 @@ const UsersTable = () => {
|
||||
(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);
|
||||
await loadUsers(activePage - 1, orderBy);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers(0)
|
||||
loadUsers(0, orderBy)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
}, [orderBy]);
|
||||
|
||||
const manageUser = (username, action, idx) => {
|
||||
(async () => {
|
||||
@ -110,6 +111,7 @@ const UsersTable = () => {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadUsers(0);
|
||||
setActivePage(1);
|
||||
setOrderBy('');
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
@ -148,6 +150,11 @@ const UsersTable = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleOrderByChange = (e, { value }) => {
|
||||
setOrderBy(value);
|
||||
setActivePage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={searchUsers}>
|
||||
@ -322,6 +329,19 @@ const UsersTable = () => {
|
||||
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
||||
添加新的用户
|
||||
</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
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
|
@ -160,7 +160,7 @@ const EditChannel = () => {
|
||||
localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1);
|
||||
}
|
||||
if (localInputs.type === 3 && localInputs.other === '') {
|
||||
localInputs.other = '2023-12-01-preview';
|
||||
localInputs.other = '2024-03-01-preview';
|
||||
}
|
||||
if (localInputs.type === 18 && localInputs.other === '') {
|
||||
localInputs.other = 'v2.1';
|
||||
@ -242,7 +242,7 @@ const EditChannel = () => {
|
||||
<Form.Input
|
||||
label='默认 API 版本'
|
||||
name='other'
|
||||
placeholder={'请输入默认 API 版本,例如:2023-12-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||
placeholder={'请输入默认 API 版本,例如:2024-03-01-preview,该配置可以被实际的请求查询参数所覆盖'}
|
||||
onChange={handleInputChange}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
|
Loading…
Reference in New Issue
Block a user