mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-10-30 05:13:42 +08:00
Compare commits
18 Commits
v0.4.9-alp
...
v0.4.9-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bab5b48bf | ||
|
|
f3bccee3b5 | ||
|
|
d84b0b0f5d | ||
|
|
d383302e8a | ||
|
|
04f40def2f | ||
|
|
c48b7bc0f5 | ||
|
|
b09daf5ec1 | ||
|
|
c90c0ecef4 | ||
|
|
1ab5fb7d2d | ||
|
|
f769711c19 | ||
|
|
edc5156693 | ||
|
|
9ec6506c32 | ||
|
|
f387cc5ead | ||
|
|
569b68c43b | ||
|
|
f0c40a6cd0 | ||
|
|
0cea9e6a6f | ||
|
|
b1b3651e84 | ||
|
|
8f6bd51f58 |
@@ -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.
|
||||||
|
|||||||
25
README.md
25
README.md
@@ -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 协议,使用者需自行承担使用本项目的风险与责任,本开源项目开发者与此无关。
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ const (
|
|||||||
ChannelTypeAIProxy = 10
|
ChannelTypeAIProxy = 10
|
||||||
ChannelTypePaLM = 11
|
ChannelTypePaLM = 11
|
||||||
ChannelTypeAPI2GPT = 12
|
ChannelTypeAPI2GPT = 12
|
||||||
|
ChannelTypeAIGC2D = 13
|
||||||
)
|
)
|
||||||
|
|
||||||
var ChannelBaseURLs = []string{
|
var ChannelBaseURLs = []string{
|
||||||
@@ -155,7 +156,7 @@ var ChannelBaseURLs = []string{
|
|||||||
"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
|
||||||
@@ -164,4 +165,5 @@ var ChannelBaseURLs = []string{
|
|||||||
"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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("尚未实现")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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' }
|
||||||
];
|
];
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user