diff --git a/README.en.md b/README.en.md index eec0047b..bce47353 100644 --- a/README.en.md +++ b/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 diff --git a/README.ja.md b/README.ja.md index e9149d71..c15915ec 100644 --- a/README.ja.md +++ b/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` ### コマンドラインパラメータ diff --git a/README.md b/README.md index 6bbc0aa6..71414a78 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,4 @@ docker image: `ppcelery/one-api:latest` - update token usage by API - support gpt-vision +- support update user's remained quota diff --git a/common/config/config.go b/common/config/config.go index 6c4f42cb..66cfee06 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -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") diff --git a/common/model-ratio.go b/common/model-ratio.go index a4dbffbf..b418aa7d 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -73,14 +73,15 @@ var ModelRatio = map[string]float64{ "claude-3-sonnet-20240229": 3.0 / 1000 * USD, "claude-3-opus-20240229": 15.0 / 1000 * USD, // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/hlrk4akp7 - "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens - "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens - "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens - "ERNIE-Bot-8k": 0.024 * RMB, - "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens - "bge-large-zh": 0.002 * RMB, - "bge-large-en": 0.002 * RMB, - "bge-large-8k": 0.002 * RMB, + "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens + "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens + "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens + "ERNIE-Bot-8k": 0.024 * RMB, + "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens + "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{} diff --git a/controller/token.go b/controller/token.go index a94617e1..ff1333ee 100644 --- a/controller/token.go +++ b/controller/token.go @@ -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, diff --git a/controller/user.go b/controller/user.go index e6aec36a..691378ec 100644 --- a/controller/user.go +++ b/controller/user.go @@ -186,24 +186,27 @@ func Register(c *gin.Context) { } func GetAllUsers(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": users, - }) - return + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + + 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, + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": users, + }) } func SearchUsers(c *gin.Context) { diff --git a/model/main.go b/model/main.go index 0ef26c8d..4bbfde27 100644 --- a/model/main.go +++ b/model/main.go @@ -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 } diff --git a/model/redemption.go b/model/redemption.go index 47c75d68..c3ed2576 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -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 diff --git a/model/token.go b/model/token.go index fda4a563..c5491d06 100644 --- a/model/token.go +++ b/model/token.go @@ -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 } diff --git a/model/user.go b/model/user.go index 26279c5f..e1244e3c 100644 --- a/model/user.go +++ b/model/user.go @@ -27,9 +27,9 @@ 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 - RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number + 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"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` @@ -41,9 +41,22 @@ 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 - return users, err +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 } func SearchUsers(keyword string) (users []*User, err error) { diff --git a/relay/channel/gemini/constants.go b/relay/channel/gemini/constants.go index 4e7c57f9..e8d3a155 100644 --- a/relay/channel/gemini/constants.go +++ b/relay/channel/gemini/constants.go @@ -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", diff --git a/relay/controller/audio.go b/relay/controller/audio.go index 96f40e7f..2e97d62b 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -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) - 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{} @@ -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 ") diff --git a/relay/controller/image.go b/relay/controller/image.go index f5e4e74f..d88dc271 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -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) } diff --git a/web/README.md b/web/README.md index 59d91424..29f4713e 100644 --- a/web/README.md +++ b/web/README.md @@ -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` 文件,这里也需要同步修改。 ## 主题列表 diff --git a/web/air/public/index.html b/web/air/public/index.html index e9697b92..36365c7e 100644 --- a/web/air/public/index.html +++ b/web/air/public/index.html @@ -9,7 +9,7 @@ name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> - New API + One API diff --git a/web/air/src/components/TokensTable.js b/web/air/src/components/TokensTable.js index 9c4deb6e..c106b388 100644 --- a/web/air/src/components/TokensTable.js +++ b/web/air/src/components/TokensTable.js @@ -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 ( <> @@ -579,6 +599,21 @@ const TokensTable = () => { await copyText(keys); } }>复制所选令牌到剪贴板 + setDropdownVisible(visible)} + render={ + + handleOrderByChange('', { value: '' })}>默认排序 + handleOrderByChange('', { value: 'remain_quota' })}>按剩余额度排序 + handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序 + + } + > + + ); }; diff --git a/web/air/src/components/UsersTable.js b/web/air/src/components/UsersTable.js index f3de46d6..4fc16ba5 100644 --- a/web/air/src/components/UsersTable.js +++ b/web/air/src/components/UsersTable.js @@ -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 ( <> @@ -331,6 +353,22 @@ const UsersTable = () => { setShowAddUser(true); } }>添加用户 + setDropdownVisible(visible)} + render={ + + handleOrderByChange('', { value: '' })}>默认排序 + handleOrderByChange('', { value: 'quota' })}>按剩余额度排序 + handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序 + handleOrderByChange('', { value: 'request_count' })}>按请求次数排序 + + } + > + + ); }; diff --git a/web/air/src/helpers/utils.js b/web/air/src/helpers/utils.js index 3e1fdb20..580c77ce 100644 --- a/web/air/src/helpers/utils.js +++ b/web/air/src/helpers/utils.js @@ -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; } diff --git a/web/air/src/index.css b/web/air/src/index.css index 8e53624e..271f14e2 100644 --- a/web/air/src/index.css +++ b/web/air/src/index.css @@ -103,3 +103,14 @@ code { display: none !important; } } + + +/* 隐藏浏览器默认的滚动条 */ +body { + overflow: hidden; +} + +/* 自定义滚动条样式 */ +body::-webkit-scrollbar { + width: 0; /* 隐藏滚动条的宽度 */ +} \ No newline at end of file diff --git a/web/air/src/pages/Channel/EditChannel.js b/web/air/src/pages/Channel/EditChannel.js index 2b84011b..efb2cee8 100644 --- a/web/air/src/pages/Channel/EditChannel.js +++ b/web/air/src/pages/Channel/EditChannel.js @@ -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) => { { handleInputChange('other', value) }} diff --git a/web/berry/README.md b/web/berry/README.md index 170feedc..84b2bc2c 100644 --- a/web/berry/README.md +++ b/web/berry/README.md @@ -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 }, diff --git a/web/berry/public/favicon.ico b/web/berry/public/favicon.ico index fbcfb14a..c2c8de0c 100644 Binary files a/web/berry/public/favicon.ico and b/web/berry/public/favicon.ico differ diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index 2c506881..ec049f7d 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -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' } }; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index 70aa2230..9420b098 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -180,7 +180,7 @@ const LoginForm = ({ ...others }) => { {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
- 用户名 + 用户名 / 邮箱 { let groups = []; if (group === "") { @@ -14,7 +27,7 @@ const GroupLabel = ({ group }) => { return ( } spacing={0.5}> {groups.map((group, index) => { - return ; + return ; })} ); diff --git a/web/berry/src/views/Channel/component/TableHead.js b/web/berry/src/views/Channel/component/TableHead.js index 736dd8aa..8c47e440 100644 --- a/web/berry/src/views/Channel/component/TableHead.js +++ b/web/berry/src/views/Channel/component/TableHead.js @@ -10,6 +10,7 @@ const ChannelTableHead = () => { 类型 状态 响应时间 + 已消耗 余额 优先级 操作 diff --git a/web/berry/src/views/Channel/component/TableRow.js b/web/berry/src/views/Channel/component/TableRow.js index baca42cd..f7acb92e 100644 --- a/web/berry/src/views/Channel/component/TableRow.js +++ b/web/berry/src/views/Channel/component/TableRow.js @@ -170,6 +170,9 @@ export default function ChannelTableRow({ handle_action={handleResponseTime} /> + + {renderNumber(item.used_quota)} + - + 渠道 - - - - OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 - - - + {matchUpMd ? ( - + diff --git a/web/berry/src/views/Channel/type/Config.js b/web/berry/src/views/Channel/type/Config.js index 8dfe77a4..7e42ca8d 100644 --- a/web/berry/src/views/Channel/type/Config.js +++ b/web/berry/src/views/Channel/type/Config.js @@ -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: { diff --git a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js index 53cd46b0..9daa9519 100644 --- a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js +++ b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js @@ -65,7 +65,7 @@ const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) = ) : ( - + diff --git a/web/berry/src/views/Log/index.js b/web/berry/src/views/Log/index.js index da24b4fd..f8cef0e8 100644 --- a/web/berry/src/views/Log/index.js +++ b/web/berry/src/views/Log/index.js @@ -102,11 +102,11 @@ export default function Log() { return ( <> - + 日志 - + - + diff --git a/web/berry/src/views/Redemption/index.js b/web/berry/src/views/Redemption/index.js index fc2d02f1..f617faaf 100644 --- a/web/berry/src/views/Redemption/index.js +++ b/web/berry/src/views/Redemption/index.js @@ -141,7 +141,7 @@ export default function Redemption() { return ( <> - + 兑换 - + - + diff --git a/web/berry/src/views/Token/index.js b/web/berry/src/views/Token/index.js index 97ece35f..b3315eb9 100644 --- a/web/berry/src/views/Token/index.js +++ b/web/berry/src/views/Token/index.js @@ -141,9 +141,8 @@ export default function Token() { return ( <> - + 令牌 - diff --git a/web/berry/src/views/User/index.js b/web/berry/src/views/User/index.js index 463f525a..e53e5bbb 100644 --- a/web/berry/src/views/User/index.js +++ b/web/berry/src/views/User/index.js @@ -139,7 +139,7 @@ export default function Users() { return ( <> - + 用户 - + - + diff --git a/web/default/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js index 5f837d03..5fd6129a 100644 --- a/web/default/src/components/ChannelsTable.js +++ b/web/default/src/components/ChannelsTable.js @@ -333,6 +333,8 @@ const ChannelsTable = () => { setPromptShown("channel-test"); }}> OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 +
+ 渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。 ) } diff --git a/web/default/src/components/LoginForm.js b/web/default/src/components/LoginForm.js index a3913220..b48f64c4 100644 --- a/web/default/src/components/LoginForm.js +++ b/web/default/src/components/LoginForm.js @@ -94,7 +94,7 @@ const LoginForm = () => { fluid icon='user' iconPosition='left' - placeholder='用户名' + placeholder='用户名 / 邮箱地址' name='username' value={username} onChange={handleChange} diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 5170e765..471b302d 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -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 ( <> @@ -404,6 +411,18 @@ const TokensTable = () => { 添加新的令牌 + { 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 ( <> @@ -322,6 +329,19 @@ const UsersTable = () => { + { 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 = () => {