Compare commits

...

18 Commits

Author SHA1 Message Date
mrhaoji
3bab5b48bf chore: remove email in UsersTable to make room for other fields (#246)
* chore: remove email in UsersTable to make room for others

* fix: fix not working properly

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-07-04 18:57:44 +08:00
JustSong
f3bccee3b5 chore: update channel add & edit page 2023-07-04 18:40:36 +08:00
mrhaoji
d84b0b0f5d chore: add model parameter to the time_test script (#245)
default value is gpt-3.5-turbo
2023-07-04 18:13:59 +08:00
Cross Fire
d383302e8a feat: support balance query for CloseAI (#240)
* 增加CloseAI余额查询功能

* 去除debug代码

* fix: bug fix

* docs: update README

---------

Co-authored-by: sudongyang <sudongyang@xiaomi.com>
Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-07-03 20:43:42 +08:00
JustSong
04f40def2f docs: update README 2023-07-03 19:51:12 +08:00
JustSong
c48b7bc0f5 docs: update README 2023-07-03 19:50:17 +08:00
quzard
b09daf5ec1 chore: update countTokenMessages (#238) 2023-07-03 09:42:34 +08:00
JustSong
c90c0ecef4 chore: set the default token quota to 1$ 2023-07-02 16:45:42 +08:00
JustSong
1ab5fb7d2d docs: update README 2023-07-02 16:35:40 +08:00
JustSong
f769711c19 docs: update README 2023-07-02 16:25:14 +08:00
JustSong
edc5156693 chore: ignore FRONTEND_BASE_URL on master node 2023-07-02 16:07:12 +08:00
JustSong
9ec6506c32 fix: fix refresh not working properly (close #229) 2023-07-02 15:55:49 +08:00
JustSong
f387cc5ead docs: update README 2023-07-02 14:57:10 +08:00
JustSong
569b68c43b docs: update README 2023-07-02 14:53:50 +08:00
JustSong
f0c40a6cd0 fix: fix cannot enable token if set to unlimited time (close #230) 2023-07-02 14:47:06 +08:00
JustSong
0cea9e6a6f fix: fix channel search is not working with MySQL 2023-06-29 21:46:13 +08:00
Alone88
b1b3651e84 feat: support channel type AIGC2D (#220)
* feat: add AIGC2D Channel

* chore: remove console logging & update balance rendering

---------

Co-authored-by: Alone88 <im@aloen88.cn>
Co-authored-by: JustSong <songquanpeng@foxmail.com>
2023-06-29 21:34:17 +08:00
JustSong
8f6bd51f58 docs: update README 2023-06-29 21:22:23 +08:00
17 changed files with 140 additions and 60 deletions

View File

@@ -285,6 +285,10 @@ If the channel ID is not provided, load balancing will be used to distribute the
## Note ## Note
This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes. This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes.
This project is open-sourced under the MIT license. One must somehow retain the copyright information of One API. This project is released under the MIT license. Based on this, attribution and a link to this project must be included at the bottom of the page.
The same applies to derivative projects based on this project.
If you do not wish to include attribution, prior authorization must be obtained.
According to the MIT license, users should bear the risk and responsibility of using this project, and the developer of this open-source project is not responsible for this. According to the MIT license, users should bear the risk and responsibility of using this project, and the developer of this open-source project is not responsible for this.

View File

@@ -51,6 +51,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
<a href="https://iamazing.cn/page/reward">赞赏支持</a> <a href="https://iamazing.cn/page/reward">赞赏支持</a>
</p> </p>
> **Note**:本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 > **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 > **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。
@@ -64,6 +66,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] [API2D](https://api2d.com/r/197971) + [x] [API2D](https://api2d.com/r/197971)
+ [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf)
+ [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI` + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
+ [x] 自定义渠道:例如各种未收录的第三方代理服务 + [x] 自定义渠道:例如各种未收录的第三方代理服务
2. 支持通过**负载均衡**的方式访问多个渠道。 2. 支持通过**负载均衡**的方式访问多个渠道。
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
@@ -93,6 +96,10 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
### 基于 Docker 进行部署 ### 基于 Docker 进行部署
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api` 部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。
如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。
更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR`
`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 `-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
@@ -158,8 +165,8 @@ sudo service nginx restart
### 多机部署 ### 多机部署
1. 所有服务器 `SESSION_SECRET` 设置一样的值。 1. 所有服务器 `SESSION_SECRET` 设置一样的值。
2. 必须设置 `SQL_DSN`,使用 MySQL 数据库而非 SQLite所有服务器连接同一个数据库。 2. 必须设置 `SQL_DSN`,使用 MySQL 数据库而非 SQLite所有服务器连接同一个数据库。
3. 所有从服务器必须设置 `NODE_TYPE` 为 `slave`。 3. 所有从服务器必须设置 `NODE_TYPE` 为 `slave`,不设置则默认为主服务器
4. 设置 `SYNC_FREQUENCY` 后服务器将定期从数据库同步配置。 4. 设置 `SYNC_FREQUENCY` 后服务器将定期从数据库同步配置,在使用远程数据库的情况下,推荐设置该项并启用 Redis无论主从
5. 从服务器可以选择设置 `FRONTEND_BASE_URL`,以重定向页面请求到主服务器。 5. 从服务器可以选择设置 `FRONTEND_BASE_URL`,以重定向页面请求到主服务器。
6. 从服务器上**分别**装好 Redis设置好 `REDIS_CONN_STRING`,这样可以做到在缓存未过期的情况下数据库零访问,可以减少延迟。 6. 从服务器上**分别**装好 Redis设置好 `REDIS_CONN_STRING`,这样可以做到在缓存未过期的情况下数据库零访问,可以减少延迟。
7. 如果主服务器访问数据库延迟也比较高,则也需要启用 Redis并设置 `SYNC_FREQUENCY`,以定期从数据库同步配置。 7. 如果主服务器访问数据库延迟也比较高,则也需要启用 Redis并设置 `SYNC_FREQUENCY`,以定期从数据库同步配置。
@@ -231,6 +238,8 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
等到系统启动后,使用 `root` 用户登录系统并做进一步的配置。 等到系统启动后,使用 `root` 用户登录系统并做进一步的配置。
**Note**:如果你不知道某个配置项的含义,可以临时删掉值以看到进一步的提示文字。
## 使用方法 ## 使用方法
在`渠道`页面中添加你的 API Key之后在`令牌`页面中新增访问令牌。 在`渠道`页面中添加你的 API Key之后在`令牌`页面中新增访问令牌。
@@ -261,7 +270,10 @@ 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`:设置之后将使用指定的前端地址,而非后端地址,仅限从服务器设置 + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表
+ 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。
+ 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn` + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。 5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
+ 例子:`SYNC_FREQUENCY=60` + 例子:`SYNC_FREQUENCY=60`
@@ -308,13 +320,16 @@ https://openai.justsong.cn
5. ChatGPT Next Web 报错:`Failed to fetch` 5. ChatGPT Next Web 报错:`Failed to fetch`
+ 部署的时候不要设置 `BASE_URL`。 + 部署的时候不要设置 `BASE_URL`。
+ 检查你的接口地址和 API Key 有没有填对。 + 检查你的接口地址和 API Key 有没有填对。
6. 报错:`当前分组负载已饱和,请稍后再试`
+ 上游通道 429 了。
## 相关项目 ## 相关项目
[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库 [FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库
## 注意 ## 注意
本项目为开源项目,请在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
本项目使用 MIT 协议进行开源,请以某种方式保留 One API 的版权信息 本项目使用 MIT 协议进行开源,**在此基础上**,必须在页面底部保留署名以及指向本项目的链接。如果不想保留署名,必须首先获得授权
同样适用于基于本项目的二开项目。
依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。 依据 MIT 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。

View File

@@ -1,13 +1,15 @@
#!/bin/bash #!/bin/bash
if [ $# -ne 3 ]; then if [ $# -lt 3 ]; then
echo "Usage: time_test.sh <domain> <key> <count>" echo "Usage: time_test.sh <domain> <key> <count> [<model>]"
exit 1 exit 1
fi fi
domain=$1 domain=$1
key=$2 key=$2
count=$3 count=$3
model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo
total_time=0 total_time=0
times=() times=()
@@ -16,7 +18,7 @@ for ((i=1; i<=count; i++)); do
https://"$domain"/v1/chat/completions \ https://"$domain"/v1/chat/completions \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer $key" \ -H "Authorization: Bearer $key" \
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "gpt-3.5-turbo", "stream": false, "max_tokens": 1}') -d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
http_code=$(echo "$result" | awk '{print $1}') http_code=$(echo "$result" | awk '{print $1}')
time=$(echo "$result" | awk '{print $2}') time=$(echo "$result" | awk '{print $2}')
echo "HTTP status code: $http_code, Time taken: $time" echo "HTTP status code: $http_code, Time taken: $time"

View File

@@ -148,20 +148,22 @@ const (
ChannelTypeAIProxy = 10 ChannelTypeAIProxy = 10
ChannelTypePaLM = 11 ChannelTypePaLM = 11
ChannelTypeAPI2GPT = 12 ChannelTypeAPI2GPT = 12
ChannelTypeAIGC2D = 13
) )
var ChannelBaseURLs = []string{ var ChannelBaseURLs = []string{
"", // 0 "", // 0
"https://api.openai.com", // 1 "https://api.openai.com", // 1
"https://oa.api2d.net", // 2 "https://oa.api2d.net", // 2
"", // 3 "", // 3
"https://api.openai-proxy.org", // 4 "https://api.closeai-proxy.xyz", // 4
"https://api.openai-sb.com", // 5 "https://api.openai-sb.com", // 5
"https://api.openaimax.com", // 6 "https://api.openaimax.com", // 6
"https://api.ohmygpt.com", // 7 "https://api.ohmygpt.com", // 7
"", // 8 "", // 8
"https://api.caipacity.com", // 9 "https://api.caipacity.com", // 9
"https://api.aiproxy.io", // 10 "https://api.aiproxy.io", // 10
"", // 11 "", // 11
"https://api.api2gpt.com", // 12 "https://api.api2gpt.com", // 12
"https://api.aigc2d.com", // 13
} }

View File

@@ -32,6 +32,13 @@ type OpenAIUsageDailyCost struct {
} }
} }
type OpenAICreditGrants struct {
Object string `json:"object"`
TotalGranted float64 `json:"total_granted"`
TotalUsed float64 `json:"total_used"`
TotalAvailable float64 `json:"total_available"`
}
type OpenAIUsageResponse struct { type OpenAIUsageResponse struct {
Object string `json:"object"` Object string `json:"object"`
//DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"` //DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"`
@@ -61,6 +68,14 @@ type API2GPTUsageResponse struct {
TotalRemaining float64 `json:"total_remaining"` TotalRemaining float64 `json:"total_remaining"`
} }
type APGC2DGPTUsageResponse struct {
//Grants interface{} `json:"grants"`
Object string `json:"object"`
TotalAvailable float64 `json:"total_available"`
TotalGranted float64 `json:"total_granted"`
TotalUsed float64 `json:"total_used"`
}
// GetAuthHeader get auth header // GetAuthHeader get auth header
func GetAuthHeader(token string) http.Header { func GetAuthHeader(token string) http.Header {
h := http.Header{} h := http.Header{}
@@ -92,6 +107,22 @@ func GetResponseBody(method, url string, channel *model.Channel, headers http.He
return body, nil return body, nil
} }
func updateChannelCloseAIBalance(channel *model.Channel) (float64, error) {
url := fmt.Sprintf("%s/dashboard/billing/credit_grants", channel.BaseURL)
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {
return 0, err
}
response := OpenAICreditGrants{}
err = json.Unmarshal(body, &response)
if err != nil {
return 0, err
}
channel.UpdateBalance(response.TotalAvailable)
return response.TotalAvailable, nil
}
func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) {
url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key) url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key)
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
@@ -150,8 +181,26 @@ func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) {
return response.TotalRemaining, nil return response.TotalRemaining, nil
} }
func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
url := "https://api.aigc2d.com/dashboard/billing/credit_grants"
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
if err != nil {
return 0, err
}
response := APGC2DGPTUsageResponse{}
err = json.Unmarshal(body, &response)
if err != nil {
return 0, err
}
channel.UpdateBalance(response.TotalAvailable)
return response.TotalAvailable, nil
}
func updateChannelBalance(channel *model.Channel) (float64, error) { func updateChannelBalance(channel *model.Channel) (float64, error) {
baseURL := common.ChannelBaseURLs[channel.Type] baseURL := common.ChannelBaseURLs[channel.Type]
if channel.BaseURL == "" {
channel.BaseURL = baseURL
}
switch channel.Type { switch channel.Type {
case common.ChannelTypeOpenAI: case common.ChannelTypeOpenAI:
if channel.BaseURL != "" { if channel.BaseURL != "" {
@@ -161,12 +210,16 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
return 0, errors.New("尚未实现") return 0, errors.New("尚未实现")
case common.ChannelTypeCustom: case common.ChannelTypeCustom:
baseURL = channel.BaseURL baseURL = channel.BaseURL
case common.ChannelTypeCloseAI:
return updateChannelCloseAIBalance(channel)
case common.ChannelTypeOpenAISB: case common.ChannelTypeOpenAISB:
return updateChannelOpenAISBBalance(channel) return updateChannelOpenAISBBalance(channel)
case common.ChannelTypeAIProxy: case common.ChannelTypeAIProxy:
return updateChannelAIProxyBalance(channel) return updateChannelAIProxyBalance(channel)
case common.ChannelTypeAPI2GPT: case common.ChannelTypeAPI2GPT:
return updateChannelAPI2GPTBalance(channel) return updateChannelAPI2GPTBalance(channel)
case common.ChannelTypeAIGC2D:
return updateChannelAIGC2DBalance(channel)
default: default:
return 0, errors.New("尚未实现") return 0, errors.New("尚未实现")
} }

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"github.com/pkoukk/tiktoken-go" "github.com/pkoukk/tiktoken-go"
"one-api/common" "one-api/common"
"strings"
) )
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{} var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
@@ -34,12 +33,9 @@ func countTokenMessages(messages []Message, model string) int {
// Every message follows <|start|>{role/name}\n{content}<|end|>\n // Every message follows <|start|>{role/name}\n{content}<|end|>\n
var tokensPerMessage int var tokensPerMessage int
var tokensPerName int var tokensPerName int
if strings.HasPrefix(model, "gpt-3.5") { if model == "gpt-3.5-turbo-0301" {
tokensPerMessage = 4 tokensPerMessage = 4
tokensPerName = -1 // If there's a name, the role is omitted tokensPerName = -1 // If there's a name, the role is omitted
} else if strings.HasPrefix(model, "gpt-4") {
tokensPerMessage = 3
tokensPerName = 1
} else { } else {
tokensPerMessage = 3 tokensPerMessage = 3
tokensPerName = 1 tokensPerName = 1

View File

@@ -180,10 +180,10 @@ func UpdateToken(c *gin.Context) {
return return
} }
if token.Status == common.TokenStatusEnabled { if token.Status == common.TokenStatusEnabled {
if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() { if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() && cleanToken.ExpiredTime != -1 {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": false, "success": false,
"message": "令牌已过期,无法启用,请先修改令牌过期时间", "message": "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期",
}) })
return return
} }

View File

@@ -36,7 +36,7 @@
"通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)", "通过令牌「%s」使用模型 %s 消耗 %s模型倍率 %.2f,分组倍率 %.2f": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)",
"当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.", "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.",
"令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20", "令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20",
"令牌已过期,无法启用,请先修改令牌过期时间": "The token has expired and cannot be enabled. Please modify the token expiration time first", "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.",
"令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota", "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota",
"管理员关闭了密码登录": "The administrator has turned off password login", "管理员关闭了密码登录": "The administrator has turned off password login",
"无法保存会话信息,请重试": "Unable to save session information, please try again", "无法保存会话信息,请重试": "Unable to save session information, please try again",

View File

@@ -37,7 +37,7 @@ func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
} }
func SearchChannels(keyword string) (channels []*Channel, err error) { func SearchChannels(keyword string) (channels []*Channel, err error) {
err = DB.Omit("key").Where("id = ? or name LIKE ? or key = ?", keyword, keyword+"%", keyword).Find(&channels).Error err = DB.Omit("key").Where("id = ? or name LIKE ? or `key` = ?", keyword, keyword+"%", keyword).Find(&channels).Error
return channels, err return channels, err
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
"one-api/common"
"os" "os"
"strings" "strings"
) )
@@ -14,6 +15,10 @@ func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
SetDashboardRouter(router) SetDashboardRouter(router)
SetRelayRouter(router) SetRelayRouter(router)
frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL") frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
if common.IsMasterNode && frontendBaseUrl != "" {
frontendBaseUrl = ""
common.SysLog("FRONTEND_BASE_URL is ignored on master node")
}
if frontendBaseUrl == "" { if frontendBaseUrl == "" {
SetWebRouter(router, buildFS, indexPage) SetWebRouter(router, buildFS, indexPage)
} else { } else {

View File

@@ -30,6 +30,9 @@ function renderType(type) {
function renderBalance(type, balance) { function renderBalance(type, balance) {
switch (type) { switch (type) {
case 1: // OpenAI case 1: // OpenAI
return <span>${balance.toFixed(2)}</span>;
case 4: // CloseAI
return <span>¥{balance.toFixed(2)}</span>;
case 8: // 自定义 case 8: // 自定义
return <span>${balance.toFixed(2)}</span>; return <span>${balance.toFixed(2)}</span>;
case 5: // OpenAI-SB case 5: // OpenAI-SB
@@ -38,6 +41,8 @@ function renderBalance(type, balance) {
return <span>{renderNumber(balance)}</span>; return <span>{renderNumber(balance)}</span>;
case 12: // API2GPT case 12: // API2GPT
return <span>¥{balance.toFixed(2)}</span>; return <span>¥{balance.toFixed(2)}</span>;
case 13: // AIGC2D
return <span>{renderNumber(balance)}</span>;
default: default:
return <span>不支持</span>; return <span>不支持</span>;
} }
@@ -58,8 +63,8 @@ const ChannelsTable = () => {
if (startIdx === 0) { if (startIdx === 0) {
setChannels(data); setChannels(data);
} else { } else {
let newChannels = channels; let newChannels = [...channels];
newChannels.push(...data); newChannels.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
setChannels(newChannels); setChannels(newChannels);
} }
} else { } else {
@@ -80,7 +85,7 @@ const ChannelsTable = () => {
const refresh = async () => { const refresh = async () => {
setLoading(true); setLoading(true);
await loadChannels(0); await loadChannels(activePage - 1);
}; };
useEffect(() => { useEffect(() => {
@@ -238,7 +243,7 @@ const ChannelsTable = () => {
if (channels.length === 0) return; if (channels.length === 0) return;
setLoading(true); setLoading(true);
let sortedChannels = [...channels]; let sortedChannels = [...channels];
if (typeof sortedChannels[0][key] === 'string'){ if (typeof sortedChannels[0][key] === 'string') {
sortedChannels.sort((a, b) => { sortedChannels.sort((a, b) => {
return ('' + a[key]).localeCompare(b[key]); return ('' + a[key]).localeCompare(b[key]);
}); });

View File

@@ -108,7 +108,7 @@ const LogsTable = () => {
setLogs(data); setLogs(data);
} else { } else {
let newLogs = [...logs]; let newLogs = [...logs];
newLogs.push(...data); newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
setLogs(newLogs); setLogs(newLogs);
} }
} else { } else {

View File

@@ -45,8 +45,8 @@ const TokensTable = () => {
if (startIdx === 0) { if (startIdx === 0) {
setTokens(data); setTokens(data);
} else { } else {
let newTokens = tokens; let newTokens = [...tokens];
newTokens.push(...data); newTokens.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
setTokens(newTokens); setTokens(newTokens);
} }
} else { } else {
@@ -67,7 +67,7 @@ const TokensTable = () => {
const refresh = async () => { const refresh = async () => {
setLoading(true); setLoading(true);
await loadTokens(0); await loadTokens(activePage - 1);
} }
useEffect(() => { useEffect(() => {

View File

@@ -183,14 +183,6 @@ const UsersTable = () => {
> >
分组 分组
</Table.HeaderCell> </Table.HeaderCell>
<Table.HeaderCell
style={{ cursor: 'pointer' }}
onClick={() => {
sortUser('email');
}}
>
邮箱地址
</Table.HeaderCell>
<Table.HeaderCell <Table.HeaderCell
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
onClick={() => { onClick={() => {
@@ -233,20 +225,19 @@ const UsersTable = () => {
<Table.Cell> <Table.Cell>
<Popup <Popup
content={user.email ? user.email : '未绑定邮箱地址'} content={user.email ? user.email : '未绑定邮箱地址'}
key={user.display_name} key={user.username}
header={user.display_name ? user.display_name : user.username}
trigger={<span>{renderText(user.username, 10)}</span>} trigger={<span>{renderText(user.username, 10)}</span>}
hoverable hoverable
/> />
</Table.Cell> </Table.Cell>
<Table.Cell>{renderGroup(user.group)}</Table.Cell> <Table.Cell>{renderGroup(user.group)}</Table.Cell>
{/*<Table.Cell>*/}
{/* {user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'}*/}
{/*</Table.Cell>*/}
<Table.Cell> <Table.Cell>
{user.email ? <Popup hoverable content={user.email} trigger={<span>{renderText(user.email, 24)}</span>} /> : '无'} <Popup content='剩余额度' trigger={<Label basic>{renderQuota(user.quota)}</Label>} />
</Table.Cell> <Popup content='已用额度' trigger={<Label basic>{renderQuota(user.used_quota)}</Label>} />
<Table.Cell> <Popup content='请求次数' trigger={<Label basic>{renderNumber(user.request_count)}</Label>} />
<Popup content='剩余额度' trigger={<Label>{renderQuota(user.quota)}</Label>} />
<Popup content='已用额度' trigger={<Label>{renderQuota(user.used_quota)}</Label>} />
<Popup content='请求次数' trigger={<Label>{renderNumber(user.request_count)}</Label>} />
</Table.Cell> </Table.Cell>
<Table.Cell>{renderRole(user.role)}</Table.Cell> <Table.Cell>{renderRole(user.role)}</Table.Cell>
<Table.Cell>{renderStatus(user.status)}</Table.Cell> <Table.Cell>{renderStatus(user.status)}</Table.Cell>
@@ -320,7 +311,7 @@ const UsersTable = () => {
<Table.Footer> <Table.Footer>
<Table.Row> <Table.Row>
<Table.HeaderCell colSpan='8'> <Table.HeaderCell colSpan='7'>
<Button size='small' as={Link} to='/user/add' loading={loading}> <Button size='small' as={Link} to='/user/add' loading={loading}>
添加新的用户 添加新的用户
</Button> </Button>

View File

@@ -9,5 +9,6 @@ export const CHANNEL_OPTIONS = [
{ key: 7, text: 'OhMyGPT', value: 7, color: 'purple' }, { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' },
{ key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, { key: 9, text: 'AI.LS', value: 9, color: 'yellow' },
{ key: 10, text: 'AI Proxy', value: 10, color: 'purple' }, { key: 10, text: 'AI Proxy', value: 10, color: 'purple' },
{ key: 12, text: 'API2GPT', value: 12, color: 'blue' } { key: 12, text: 'API2GPT', value: 12, color: 'blue' },
{ key: 13, text: 'AIGC2D', value: 13, color: 'purple' }
]; ];

View File

@@ -145,6 +145,7 @@ const EditChannel = () => {
<Form.Select <Form.Select
label='类型' label='类型'
name='type' name='type'
required
options={CHANNEL_OPTIONS} options={CHANNEL_OPTIONS}
value={inputs.type} value={inputs.type}
onChange={handleInputChange} onChange={handleInputChange}
@@ -201,7 +202,7 @@ const EditChannel = () => {
<Form.Input <Form.Input
label='镜像' label='镜像'
name='base_url' name='base_url'
placeholder={'输入镜像站地址格式为https://domain.com,可不填,不填则使用渠道默认值'} placeholder={'此项可选,输入镜像站地址格式为https://domain.com'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.base_url} value={inputs.base_url}
autoComplete='new-password' autoComplete='new-password'
@@ -212,6 +213,7 @@ const EditChannel = () => {
<Form.Field> <Form.Field>
<Form.Input <Form.Input
label='名称' label='名称'
required
name='name' name='name'
placeholder={'请输入名称'} placeholder={'请输入名称'}
onChange={handleInputChange} onChange={handleInputChange}
@@ -224,6 +226,7 @@ const EditChannel = () => {
label='分组' label='分组'
placeholder={'请选择分组'} placeholder={'请选择分组'}
name='groups' name='groups'
required
fluid fluid
multiple multiple
selection selection
@@ -240,6 +243,7 @@ const EditChannel = () => {
label='模型' label='模型'
placeholder={'请选择该通道所支持的模型'} placeholder={'请选择该通道所支持的模型'}
name='models' name='models'
required
fluid fluid
multiple multiple
selection selection
@@ -263,7 +267,7 @@ const EditChannel = () => {
<Form.Field> <Form.Field>
<Form.TextArea <Form.TextArea
label='模型映射' label='模型映射'
placeholder={`为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`} placeholder={`此项可选,为一个 JSON 文本,键为用户请求的模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
name='model_mapping' name='model_mapping'
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.model_mapping} value={inputs.model_mapping}
@@ -276,6 +280,7 @@ const EditChannel = () => {
<Form.TextArea <Form.TextArea
label='密钥' label='密钥'
name='key' name='key'
required
placeholder={'请输入密钥,一行一个'} placeholder={'请输入密钥,一行一个'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.key} value={inputs.key}
@@ -286,6 +291,7 @@ const EditChannel = () => {
<Form.Input <Form.Input
label='密钥' label='密钥'
name='key' name='key'
required
placeholder={'请输入密钥'} placeholder={'请输入密钥'}
onChange={handleInputChange} onChange={handleInputChange}
value={inputs.key} value={inputs.key}

View File

@@ -11,7 +11,7 @@ const EditToken = () => {
const [loading, setLoading] = useState(isEdit); const [loading, setLoading] = useState(isEdit);
const originInputs = { const originInputs = {
name: '', name: '',
remain_quota: 0, remain_quota: isEdit ? 0 : 500000,
expired_time: -1, expired_time: -1,
unlimited_quota: false unlimited_quota: false
}; };