mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-18 18:57:27 +00:00
Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b2f675308 | ||
|
|
4c809277aa | ||
|
|
c75bc956b3 | ||
|
|
3089af6b08 | ||
|
|
d9b622c8ed | ||
|
|
4937a6d1ed | ||
|
|
de9a0d65ae | ||
|
|
ee99041910 | ||
|
|
c8a29251ac | ||
|
|
07b1c9a4db | ||
|
|
5d8de46e4c | ||
|
|
28885feea2 | ||
|
|
f693c13ce6 | ||
|
|
89cd0db28c | ||
|
|
ae57dd7b8b | ||
|
|
87d763e641 | ||
|
|
08f3562e53 | ||
|
|
88b0e6a768 | ||
|
|
a9f739a7e2 | ||
|
|
6d4edc1f5b | ||
|
|
2d1b2676f7 | ||
|
|
1035a8e0df | ||
|
|
ea433b2ed6 | ||
|
|
bb0c504709 | ||
|
|
48abfd055c | ||
|
|
6693072c49 | ||
|
|
3053d94170 | ||
|
|
1774be8536 | ||
|
|
821f3a7522 | ||
|
|
9c4d30602c | ||
|
|
7b3394d863 | ||
|
|
999ba11363 | ||
|
|
7ebc1cfb60 | ||
|
|
b71e33b095 | ||
|
|
15842163be | ||
|
|
e57788375e | ||
|
|
78cac7085c | ||
|
|
76f7474640 | ||
|
|
0dd1953cd6 | ||
|
|
019361a762 | ||
|
|
9b9d73e725 | ||
|
|
9e08709756 | ||
|
|
05b5d6f255 | ||
|
|
79b6c0a73e | ||
|
|
462c2cc1a1 | ||
|
|
e8286e479b | ||
|
|
ed2ec69545 | ||
|
|
a167dd9a23 | ||
|
|
6e6e390f6f | ||
|
|
807385d3d1 | ||
|
|
0ce600ed49 | ||
|
|
334a2424e9 | ||
|
|
7db703374c | ||
|
|
6a42ccf00e | ||
|
|
7aa7114bb9 | ||
|
|
c3e6b2408e | ||
|
|
4601932902 | ||
|
|
5d96f7b2cc | ||
|
|
8eb32e9b3f | ||
|
|
320e6ec5a4 | ||
|
|
e0f19e5ed7 | ||
|
|
3d33079de0 | ||
|
|
1d064a2e88 | ||
|
|
4eae3b2177 |
@@ -53,7 +53,7 @@
|
|||||||
# Gemini 安全设置
|
# Gemini 安全设置
|
||||||
# GEMINI_SAFETY_SETTING=BLOCK_NONE
|
# GEMINI_SAFETY_SETTING=BLOCK_NONE
|
||||||
# Gemini版本设置
|
# Gemini版本设置
|
||||||
# GEMINI_MODEL_MAP=gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta
|
# GEMINI_MODEL_MAP=gemini-1.0-pro:v1
|
||||||
# Cohere 安全设置
|
# Cohere 安全设置
|
||||||
# COHERE_SAFETY_SETTING=NONE
|
# COHERE_SAFETY_SETTING=NONE
|
||||||
# 是否统计图片token
|
# 是否统计图片token
|
||||||
|
|||||||
3
BT.md
Normal file
3
BT.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
密钥为环境变量SESSION_SECRET
|
||||||
|
|
||||||
|

|
||||||
37
README.md
37
README.md
@@ -68,19 +68,37 @@
|
|||||||
|
|
||||||
## 比原版One API多出的配置
|
## 比原版One API多出的配置
|
||||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
|
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
|
||||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
|
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。
|
||||||
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
||||||
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。
|
- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。
|
||||||
- `GET_MEDIA_TOKEN`:是统计图片token,默认为 `true`,关闭后将不再在本地计算图片token,可能会导致和上游计费不同,此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
|
- `GET_MEDIA_TOKEN`:是否统计图片token,默认为 `true`,关闭后将不再在本地计算图片token,可能会导致和上游计费不同,此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
|
||||||
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`)情况下统计图片token,默认为 `true`。
|
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`)情况下统计图片token,默认为 `true`。
|
||||||
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。
|
- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。
|
||||||
- `GEMINI_MODEL_MAP`:Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置
|
- `GEMINI_MODEL_MAP`:Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta)
|
||||||
- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`,`STRICT`,默认为 `NONE`。
|
- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`,`STRICT`,默认为 `NONE`。
|
||||||
## 部署
|
## 部署
|
||||||
### 部署要求
|
### 部署要求
|
||||||
- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机)
|
- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机)
|
||||||
- 远程数据库:MySQL 版本 >= 5.7.8,PgSQL 版本 >= 9.6
|
- 远程数据库:MySQL 版本 >= 5.7.8,PgSQL 版本 >= 9.6
|
||||||
|
|
||||||
|
### 使用宝塔面板Docker功能部署
|
||||||
|
安装宝塔面板 (**9.2.0版本**及以上),前往 [宝塔面板](https://www.bt.cn/new/download.html) 官网,选择正式版的脚本下载安装
|
||||||
|
安装后登录宝塔面板,在菜单栏中点击 Docker ,首次进入会提示安装 Docker 服务,点击立即安装,按提示完成安装
|
||||||
|
安装完成后在应用商店中找到 **New-API** ,点击安装,配置基本选项 即可完成安装
|
||||||
|
[图文教程](BT.md)
|
||||||
|
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
|
### 使用 Docker Compose 部署(推荐)
|
||||||
|
```shell
|
||||||
|
# 下载项目
|
||||||
|
git clone https://github.com/Calcium-Ion/new-api.git
|
||||||
|
cd new-api
|
||||||
|
# 按需编辑 docker-compose.yml
|
||||||
|
# 启动
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 直接使用 Docker 镜像
|
||||||
```shell
|
```shell
|
||||||
# 使用 SQLite 的部署命令:
|
# 使用 SQLite 的部署命令:
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
@@ -88,16 +106,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -
|
|||||||
# 例如:
|
# 例如:
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
|
||||||
```
|
```
|
||||||
### 使用宝塔面板Docker功能部署
|
|
||||||
```shell
|
|
||||||
# 使用 SQLite 的部署命令:
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /www/wwwroot/new-api:/data calciumion/new-api:latest
|
|
||||||
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。
|
|
||||||
# 例如:
|
|
||||||
# 注意:数据库要开启远程访问,并且只允许服务器IP访问
|
|
||||||
docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(宝塔的服务器地址:宝塔数据库端口)/宝塔数据库名称" -e TZ=Asia/Shanghai -v /www/wwwroot/new-api:/data calciumion/new-api:latest
|
|
||||||
# 注意:数据库要开启远程访问,并且只允许服务器IP访问
|
|
||||||
```
|
|
||||||
|
|
||||||
## 渠道重试
|
## 渠道重试
|
||||||
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
||||||
@@ -128,6 +136,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
|||||||

|

|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
夜间模式
|
夜间模式
|
||||||
@@ -135,7 +144,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
|||||||

|

|
||||||
|
|
||||||
## 交流群
|
## 交流群
|
||||||
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="200">
|
<img src="https://github.com/user-attachments/assets/9ca0bc82-e057-4230-a28d-9f198fa022e3" width="200">
|
||||||
|
|
||||||
## 相关项目
|
## 相关项目
|
||||||
- [One API](https://github.com/songquanpeng/one-api):原版项目
|
- [One API](https://github.com/songquanpeng/one-api):原版项目
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ var ChannelBaseURLs = []string{
|
|||||||
"https://open.bigmodel.cn", // 16
|
"https://open.bigmodel.cn", // 16
|
||||||
"https://dashscope.aliyuncs.com", // 17
|
"https://dashscope.aliyuncs.com", // 17
|
||||||
"", // 18
|
"", // 18
|
||||||
"https://ai.360.cn", // 19
|
"https://api.360.cn", // 19
|
||||||
"https://openrouter.ai/api", // 20
|
"https://openrouter.ai/api", // 20
|
||||||
"https://api.aiproxy.io", // 21
|
"https://api.aiproxy.io", // 21
|
||||||
"https://fastgpt.run/api/openapi", // 22
|
"https://fastgpt.run/api/openapi", // 22
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func generateMessageID() string {
|
func generateMessageID() (string, error) {
|
||||||
|
split := strings.Split(SMTPAccount, "@")
|
||||||
|
if len(split) < 2 {
|
||||||
|
return "", fmt.Errorf("invalid SMTP account")
|
||||||
|
}
|
||||||
domain := strings.Split(SMTPAccount, "@")[1]
|
domain := strings.Split(SMTPAccount, "@")[1]
|
||||||
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain)
|
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendEmail(subject string, receiver string, content string) error {
|
func SendEmail(subject string, receiver string, content string) error {
|
||||||
|
id, err2 := generateMessageID()
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
if SMTPFrom == "" { // for compatibility
|
if SMTPFrom == "" { // for compatibility
|
||||||
SMTPFrom = SMTPAccount
|
SMTPFrom = SMTPAccount
|
||||||
}
|
}
|
||||||
@@ -28,7 +36,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
|||||||
"Date: %s\r\n"+
|
"Date: %s\r\n"+
|
||||||
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
||||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||||
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), generateMessageID(), content))
|
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
|
||||||
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
||||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||||
to := strings.Split(receiver, ";")
|
to := strings.Split(receiver, ";")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package common
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GroupRatio = map[string]float64{
|
var GroupRatio = map[string]float64{
|
||||||
@@ -31,3 +32,17 @@ func GetGroupRatio(name string) float64 {
|
|||||||
}
|
}
|
||||||
return ratio
|
return ratio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CheckGroupRatio(jsonStr string) error {
|
||||||
|
checkGroupRatio := make(map[string]float64)
|
||||||
|
err := json.Unmarshal([]byte(jsonStr), &checkGroupRatio)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for name, ratio := range checkGroupRatio {
|
||||||
|
if ratio < 0 {
|
||||||
|
return errors.New("group ratio must be not less than 0: " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,17 +33,18 @@ var defaultModelRatio = map[string]float64{
|
|||||||
"gpt-4-32k": 30,
|
"gpt-4-32k": 30,
|
||||||
//"gpt-4-32k-0314": 30, //deprecated
|
//"gpt-4-32k-0314": 30, //deprecated
|
||||||
"gpt-4-32k-0613": 30,
|
"gpt-4-32k-0613": 30,
|
||||||
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-1106-preview": 5, // $10 / 1M tokens
|
||||||
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-0125-preview": 5, // $10 / 1M tokens
|
||||||
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
|
||||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-vision-preview": 5, // $10 / 1M tokens
|
||||||
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
|
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
|
||||||
"chatgpt-4o-latest": 2.5, // $0.01 / 1K tokens
|
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
|
||||||
"gpt-4o": 1.25, // $0.01 / 1K tokens
|
"gpt-4o": 1.25, // $2.5 / 1M tokens
|
||||||
"gpt-4o-audio-preview": 1.25, // $0.0015 / 1K tokens
|
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
|
||||||
"gpt-4o-audio-preview-2024-10-01": 1.25, // $0.0015 / 1K tokens
|
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
|
||||||
"gpt-4o-2024-08-06": 1.25, // $0.01 / 1K tokens
|
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
|
||||||
"gpt-4o-2024-05-13": 2.5,
|
"gpt-4o-2024-08-06": 1.25, // $2.5 / 1M tokens
|
||||||
|
"gpt-4o-2024-11-20": 1.25, // $2.5 / 1M tokens
|
||||||
"gpt-4o-realtime-preview": 2.5,
|
"gpt-4o-realtime-preview": 2.5,
|
||||||
"o1-preview": 7.5,
|
"o1-preview": 7.5,
|
||||||
"o1-preview-2024-09-12": 7.5,
|
"o1-preview-2024-09-12": 7.5,
|
||||||
@@ -149,6 +150,7 @@ var defaultModelRatio = map[string]float64{
|
|||||||
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
|
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
|
||||||
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
|
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
|
||||||
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
|
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
|
||||||
|
"360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens
|
||||||
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 30)
|
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
|
||||||
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
||||||
|
|
||||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||||
@@ -20,16 +20,7 @@ var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STR
|
|||||||
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
||||||
|
|
||||||
var GeminiModelMap = map[string]string{
|
var GeminiModelMap = map[string]string{
|
||||||
"gemini-1.5-pro-latest": "v1beta",
|
"gemini-1.0-pro": "v1",
|
||||||
"gemini-1.5-pro-001": "v1beta",
|
|
||||||
"gemini-1.5-pro": "v1beta",
|
|
||||||
"gemini-1.5-pro-exp-0801": "v1beta",
|
|
||||||
"gemini-1.5-pro-exp-0827": "v1beta",
|
|
||||||
"gemini-1.5-flash-latest": "v1beta",
|
|
||||||
"gemini-1.5-flash-exp-0827": "v1beta",
|
|
||||||
"gemini-1.5-flash-001": "v1beta",
|
|
||||||
"gemini-1.5-flash": "v1beta",
|
|
||||||
"gemini-ultra": "v1beta",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitEnv() {
|
func InitEnv() {
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OpenAIModel struct {
|
type OpenAIModel struct {
|
||||||
@@ -48,19 +49,41 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
if pageSize < 0 {
|
if pageSize < 0 {
|
||||||
pageSize = common.ItemsPerPage
|
pageSize = common.ItemsPerPage
|
||||||
}
|
}
|
||||||
|
channelData := make([]*model.Channel, 0)
|
||||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||||
if err != nil {
|
if enableTagMode {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
|
||||||
"success": false,
|
if err != nil {
|
||||||
"message": err.Error(),
|
c.JSON(http.StatusOK, gin.H{
|
||||||
})
|
"success": false,
|
||||||
return
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if tag != nil && *tag != "" {
|
||||||
|
tagChannel, err := model.GetChannelsByTag(*tag)
|
||||||
|
if err == nil {
|
||||||
|
channelData = append(channelData, tagChannel...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channelData = channels
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": channels,
|
"data": channelData,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -144,8 +167,8 @@ func SearchChannels(c *gin.Context) {
|
|||||||
keyword := c.Query("keyword")
|
keyword := c.Query("keyword")
|
||||||
group := c.Query("group")
|
group := c.Query("group")
|
||||||
modelKeyword := c.Query("model")
|
modelKeyword := c.Query("model")
|
||||||
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||||
channels, err := model.SearchChannels(keyword, group, modelKeyword)
|
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -279,6 +302,98 @@ func DeleteDisabledChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelTag struct {
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
NewTag *string `json:"new_tag"`
|
||||||
|
Priority *int64 `json:"priority"`
|
||||||
|
Weight *uint `json:"weight"`
|
||||||
|
ModelMapping *string `json:"model_mapping"`
|
||||||
|
Models *string `json:"models"`
|
||||||
|
Groups *string `json:"groups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableTagChannels(c *gin.Context) {
|
||||||
|
channelTag := ChannelTag{}
|
||||||
|
err := c.ShouldBindJSON(&channelTag)
|
||||||
|
if err != nil || channelTag.Tag == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = model.DisableChannelByTag(channelTag.Tag)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnableTagChannels(c *gin.Context) {
|
||||||
|
channelTag := ChannelTag{}
|
||||||
|
err := c.ShouldBindJSON(&channelTag)
|
||||||
|
if err != nil || channelTag.Tag == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = model.EnableChannelByTag(channelTag.Tag)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditTagChannels(c *gin.Context) {
|
||||||
|
channelTag := ChannelTag{}
|
||||||
|
err := c.ShouldBindJSON(&channelTag)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if channelTag.Tag == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "tag不能为空",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type ChannelBatch struct {
|
type ChannelBatch struct {
|
||||||
Ids []int `json:"ids"`
|
Ids []int `json:"ids"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -142,8 +142,13 @@ func GitHubOAuth(c *gin.Context) {
|
|||||||
user.Email = githubUser.Email
|
user.Email = githubUser.Email
|
||||||
user.Role = common.RoleCommonUser
|
user.Role = common.RoleCommonUser
|
||||||
user.Status = common.UserStatusEnabled
|
user.Status = common.UserStatusEnabled
|
||||||
|
affCode := session.Get("aff")
|
||||||
|
inviterId := 0
|
||||||
|
if affCode != nil {
|
||||||
|
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
|
||||||
|
}
|
||||||
|
|
||||||
if err := user.Insert(0); err != nil {
|
if err := user.Insert(inviterId); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": err.Error(),
|
"message": err.Error(),
|
||||||
@@ -227,6 +232,10 @@ func GitHubBind(c *gin.Context) {
|
|||||||
func GenerateOAuthCode(c *gin.Context) {
|
func GenerateOAuthCode(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
state := common.GetRandomString(12)
|
state := common.GetRandomString(12)
|
||||||
|
affCode := c.Query("aff")
|
||||||
|
if affCode != "" {
|
||||||
|
session.Set("aff", affCode)
|
||||||
|
}
|
||||||
session.Set("oauth_state", state)
|
session.Set("oauth_state", state)
|
||||||
err := session.Save()
|
err := session.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -237,7 +237,13 @@ func LinuxdoOAuth(c *gin.Context) {
|
|||||||
user.Role = common.RoleCommonUser
|
user.Role = common.RoleCommonUser
|
||||||
user.Status = common.UserStatusEnabled
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
if err := user.Insert(0); err != nil {
|
affCode := session.Get("aff")
|
||||||
|
inviterId := 0
|
||||||
|
if affCode != nil {
|
||||||
|
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Insert(inviterId); err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": err.Error(),
|
"message": err.Error(),
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "GroupRatio":
|
||||||
|
err = common.CheckGroupRatio(option.Value)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = model.UpdateOption(option.Key, option.Value)
|
err = model.UpdateOption(option.Key, option.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ version: '3.4'
|
|||||||
services:
|
services:
|
||||||
new-api:
|
new-api:
|
||||||
image: calciumion/new-api:latest
|
image: calciumion/new-api:latest
|
||||||
# build: .
|
|
||||||
container_name: new-api
|
container_name: new-api
|
||||||
restart: always
|
restart: always
|
||||||
command: --log-dir /app/logs
|
command: --log-dir /app/logs
|
||||||
@@ -13,16 +12,17 @@ services:
|
|||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- SQL_DSN=root:123456@tcp(host.docker.internal:3306)/new-api # 修改此行,或注释掉以使用 SQLite 作为数据库
|
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
|
||||||
- REDIS_CONN_STRING=redis://redis
|
- REDIS_CONN_STRING=redis://redis
|
||||||
- SESSION_SECRET=random_string # 修改为随机字符串
|
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
# - NODE_TYPE=slave # 多机部署时从节点取消注释该行
|
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
||||||
# - SYNC_FREQUENCY=60 # 需要定期从数据库加载数据时取消注释该行
|
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
||||||
# - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行
|
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
|
||||||
|
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
|
- mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ]
|
test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -33,3 +33,18 @@ services:
|
|||||||
image: redis:latest
|
image: redis:latest
|
||||||
container_name: redis
|
container_name: redis
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.2
|
||||||
|
container_name: mysql
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: 123456 # Ensure this matches the password in SQL_DSN
|
||||||
|
MYSQL_DATABASE: new-api
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
# ports:
|
||||||
|
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
|||||||
15
go.mod
15
go.mod
@@ -7,11 +7,13 @@ toolchain go1.22.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Calcium-Ion/go-epay v0.0.2
|
github.com/Calcium-Ion/go-epay v0.0.2
|
||||||
|
github.com/andybalholm/brotli v1.1.1
|
||||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
|
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.26.1
|
github.com/aws/aws-sdk-go-v2 v1.26.1
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.11
|
||||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
||||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
|
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b
|
||||||
|
github.com/bytedance/sonic v1.12.4
|
||||||
github.com/gin-contrib/cors v1.4.0
|
github.com/gin-contrib/cors v1.4.0
|
||||||
github.com/gin-contrib/gzip v0.0.6
|
github.com/gin-contrib/gzip v0.0.6
|
||||||
github.com/gin-contrib/sessions v0.0.5
|
github.com/gin-contrib/sessions v0.0.5
|
||||||
@@ -23,6 +25,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pkoukk/tiktoken-go v0.1.7
|
github.com/pkoukk/tiktoken-go v0.1.7
|
||||||
github.com/samber/lo v1.39.0
|
github.com/samber/lo v1.39.0
|
||||||
@@ -41,9 +44,10 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
||||||
github.com/aws/smithy-go v1.20.2 // indirect
|
github.com/aws/smithy-go v1.20.2 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
@@ -63,9 +67,8 @@ require (
|
|||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
@@ -79,11 +82,11 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.12.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||||
golang.org/x/net v0.28.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.24.0 // indirect
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
golang.org/x/text v0.17.0 // indirect
|
golang.org/x/text v0.17.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
36
go.sum
36
go.sum
@@ -1,5 +1,7 @@
|
|||||||
github.com/Calcium-Ion/go-epay v0.0.2 h1:3knFBuaBFpHzsGeGQU/QxUqZSHh5s0+jGo0P62pJzWc=
|
github.com/Calcium-Ion/go-epay v0.0.2 h1:3knFBuaBFpHzsGeGQU/QxUqZSHh5s0+jGo0P62pJzWc=
|
||||||
github.com/Calcium-Ion/go-epay v0.0.2/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
github.com/Calcium-Ion/go-epay v0.0.2/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0 h1:onfun1RA+KcxaMk1lfrRnwCd1UUuOjJM/lri5eM1qMs=
|
||||||
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
|
github.com/anknown/ahocorasick v0.0.0-20190904063843-d75dbd5169c0/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
|
||||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
|
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
|
||||||
@@ -20,14 +22,17 @@ github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
|||||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
|
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b h1:LTGVFpNmNHhj0vhOlfgWueFJ32eK9blaIlHR2ciXOT0=
|
||||||
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
|
github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b/go.mod h1:2ZlV9BaUH4+NXIBF0aMdKKAnHTzqH+iMU4KUjAbL23Q=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -117,8 +122,9 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
|||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
@@ -194,11 +200,12 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
|||||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
@@ -219,12 +226,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -262,4 +268,4 @@ gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
|||||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||||
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
|
||||||
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
|
c.Set("token_key", token.Key)
|
||||||
c.Set("token_name", token.Name)
|
c.Set("token_name", token.Name)
|
||||||
c.Set("token_unlimited_quota", token.UnlimitedQuota)
|
c.Set("token_unlimited_quota", token.UnlimitedQuota)
|
||||||
if !token.UnlimitedQuota {
|
if !token.UnlimitedQuota {
|
||||||
|
|||||||
38
middleware/gzip.go
Normal file
38
middleware/gzip.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"github.com/andybalholm/brotli"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DecompressRequestMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.Body == nil || c.Request.Method == http.MethodGet {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch c.GetHeader("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
gzipReader, err := gzip.NewReader(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
|
||||||
|
// Replace the request body with the decompressed data
|
||||||
|
c.Request.Body = io.NopCloser(gzipReader)
|
||||||
|
c.Request.Header.Del("Content-Encoding")
|
||||||
|
case "br":
|
||||||
|
reader := brotli.NewReader(c.Request.Body)
|
||||||
|
c.Request.Body = io.NopCloser(reader)
|
||||||
|
c.Request.Header.Del("Content-Encoding")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue processing the request
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,12 +10,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Ability struct {
|
type Ability struct {
|
||||||
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
|
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
|
||||||
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
|
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
|
||||||
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
|
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
|
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
|
||||||
Weight uint `json:"weight" gorm:"default:0;index"`
|
Weight uint `json:"weight" gorm:"default:0;index"`
|
||||||
|
Tag *string `json:"tag" gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetGroupModels(group string) []string {
|
func GetGroupModels(group string) []string {
|
||||||
@@ -149,6 +150,7 @@ func (channel *Channel) AddAbilities() error {
|
|||||||
Enabled: channel.Status == common.ChannelStatusEnabled,
|
Enabled: channel.Status == common.ChannelStatusEnabled,
|
||||||
Priority: channel.Priority,
|
Priority: channel.Priority,
|
||||||
Weight: uint(channel.GetWeight()),
|
Weight: uint(channel.GetWeight()),
|
||||||
|
Tag: channel.Tag,
|
||||||
}
|
}
|
||||||
abilities = append(abilities, ability)
|
abilities = append(abilities, ability)
|
||||||
}
|
}
|
||||||
@@ -190,6 +192,24 @@ func UpdateAbilityStatus(channelId int, status bool) error {
|
|||||||
return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error
|
return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateAbilityStatusByTag(tag string, status bool) error {
|
||||||
|
return DB.Model(&Ability{}).Where("tag = ?", tag).Select("enabled").Update("enabled", status).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAbilityByTag(tag string, newTag *string, priority *int64, weight *uint) error {
|
||||||
|
ability := Ability{}
|
||||||
|
if newTag != nil {
|
||||||
|
ability.Tag = newTag
|
||||||
|
}
|
||||||
|
if priority != nil {
|
||||||
|
ability.Priority = priority
|
||||||
|
}
|
||||||
|
if weight != nil {
|
||||||
|
ability.Weight = *weight
|
||||||
|
}
|
||||||
|
return DB.Model(&Ability{}).Where("tag = ?", tag).Updates(ability).Error
|
||||||
|
}
|
||||||
|
|
||||||
func FixAbility() (int, error) {
|
func FixAbility() (int, error) {
|
||||||
var channelIds []int
|
var channelIds []int
|
||||||
count := 0
|
count := 0
|
||||||
|
|||||||
104
model/channel.go
104
model/channel.go
@@ -2,9 +2,10 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -32,6 +33,7 @@ type Channel struct {
|
|||||||
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
||||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||||
OtherInfo string `json:"other_info"`
|
OtherInfo string `json:"other_info"`
|
||||||
|
Tag *string `json:"tag" gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) GetModels() []string {
|
func (channel *Channel) GetModels() []string {
|
||||||
@@ -61,6 +63,17 @@ func (channel *Channel) SetOtherInfo(otherInfo map[string]interface{}) {
|
|||||||
channel.OtherInfo = string(otherInfoBytes)
|
channel.OtherInfo = string(otherInfoBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) GetTag() string {
|
||||||
|
if channel.Tag == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *channel.Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) SetTag(tag string) {
|
||||||
|
channel.Tag = &tag
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) GetAutoBan() bool {
|
func (channel *Channel) GetAutoBan() bool {
|
||||||
if channel.AutoBan == nil {
|
if channel.AutoBan == nil {
|
||||||
return false
|
return false
|
||||||
@@ -87,7 +100,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
|
|||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
|
func GetChannelsByTag(tag string) ([]*Channel, error) {
|
||||||
|
var channels []*Channel
|
||||||
|
err := DB.Where("tag = ?", tag).Find(&channels).Error
|
||||||
|
return channels, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
keyCol := "`key`"
|
keyCol := "`key`"
|
||||||
groupCol := "`group`"
|
groupCol := "`group`"
|
||||||
@@ -100,6 +119,11 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
|
|||||||
modelsCol = `"models"`
|
modelsCol = `"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order := "priority desc"
|
||||||
|
if idSort {
|
||||||
|
order = "id desc"
|
||||||
|
}
|
||||||
|
|
||||||
// 构造基础查询
|
// 构造基础查询
|
||||||
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
|
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
|
||||||
|
|
||||||
@@ -122,7 +146,7 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 执行查询
|
// 执行查询
|
||||||
err := baseQuery.Where(whereClause, args...).Order("priority desc").Find(&channels).Error
|
err := baseQuery.Where(whereClause, args...).Order(order).Find(&channels).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -288,6 +312,74 @@ func UpdateChannelStatusById(id int, status int, reason string) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EnableChannelByTag(tag string) error {
|
||||||
|
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusEnabled).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = UpdateAbilityStatusByTag(tag, true)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DisableChannelByTag(tag string) error {
|
||||||
|
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusManuallyDisabled).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = UpdateAbilityStatusByTag(tag, false)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *string, group *string, priority *int64, weight *uint) error {
|
||||||
|
updateData := Channel{}
|
||||||
|
shouldReCreateAbilities := false
|
||||||
|
updatedTag := tag
|
||||||
|
// 如果 newTag 不为空且不等于 tag,则更新 tag
|
||||||
|
if newTag != nil && *newTag != tag {
|
||||||
|
updateData.Tag = newTag
|
||||||
|
updatedTag = *newTag
|
||||||
|
}
|
||||||
|
if modelMapping != nil && *modelMapping != "" {
|
||||||
|
updateData.ModelMapping = modelMapping
|
||||||
|
}
|
||||||
|
if models != nil && *models != "" {
|
||||||
|
shouldReCreateAbilities = true
|
||||||
|
updateData.Models = *models
|
||||||
|
}
|
||||||
|
if group != nil && *group != "" {
|
||||||
|
shouldReCreateAbilities = true
|
||||||
|
updateData.Group = *group
|
||||||
|
}
|
||||||
|
if priority != nil {
|
||||||
|
updateData.Priority = priority
|
||||||
|
}
|
||||||
|
if weight != nil {
|
||||||
|
updateData.Weight = weight
|
||||||
|
}
|
||||||
|
|
||||||
|
err := DB.Model(&Channel{}).Where("tag = ?", tag).Updates(updateData).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if shouldReCreateAbilities {
|
||||||
|
channels, err := GetChannelsByTag(updatedTag)
|
||||||
|
if err == nil {
|
||||||
|
for _, channel := range channels {
|
||||||
|
err = channel.UpdateAbilities()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update abilities: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := UpdateAbilityByTag(tag, newTag, priority, weight)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateChannelUsedQuota(id int, quota int) {
|
func UpdateChannelUsedQuota(id int, quota int) {
|
||||||
if common.BatchUpdateEnabled {
|
if common.BatchUpdateEnabled {
|
||||||
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
|
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
|
||||||
@@ -312,3 +404,9 @@ func DeleteDisabledChannel() (int64, error) {
|
|||||||
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
||||||
return result.RowsAffected, result.Error
|
return result.RowsAffected, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPaginatedTags(offset int, limit int) ([]*string, error) {
|
||||||
|
var tags []*string
|
||||||
|
err := DB.Model(&Channel{}).Select("DISTINCT tag").Where("tag != ''").Offset(offset).Limit(limit).Find(&tags).Error
|
||||||
|
return tags, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ var ModelList = []string{
|
|||||||
"360gpt-turbo",
|
"360gpt-turbo",
|
||||||
"360gpt-turbo-responsibility-8k",
|
"360gpt-turbo-responsibility-8k",
|
||||||
"360gpt-pro",
|
"360gpt-pro",
|
||||||
|
"360gpt2-pro",
|
||||||
"360GPT_S2_V9",
|
"360GPT_S2_V9",
|
||||||
"embedding-bert-512-v1",
|
"embedding-bert-512-v1",
|
||||||
"embedding_s1_v1",
|
"embedding_s1_v1",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ var awsModelIDMap = map[string]string{
|
|||||||
"claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0",
|
"claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0",
|
||||||
"claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
"claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||||
"claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
"claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||||
|
"claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var ChannelName = "aws"
|
var ChannelName = "aws"
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
type AwsClaudeRequest struct {
|
type AwsClaudeRequest struct {
|
||||||
// AnthropicVersion should be "bedrock-2023-05-31"
|
// AnthropicVersion should be "bedrock-2023-05-31"
|
||||||
AnthropicVersion string `json:"anthropic_version"`
|
AnthropicVersion string `json:"anthropic_version"`
|
||||||
System string `json:"system"`
|
System string `json:"system,omitempty"`
|
||||||
Messages []claude.ClaudeMessage `json:"messages"`
|
Messages []claude.ClaudeMessage `json:"messages"`
|
||||||
MaxTokens int `json:"max_tokens,omitempty"`
|
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||||
Temperature float64 `json:"temperature,omitempty"`
|
Temperature float64 `json:"temperature,omitempty"`
|
||||||
TopP float64 `json:"top_p,omitempty"`
|
TopP float64 `json:"top_p,omitempty"`
|
||||||
TopK int `json:"top_k,omitempty"`
|
TopK int `json:"top_k,omitempty"`
|
||||||
@@ -17,3 +17,18 @@ type AwsClaudeRequest struct {
|
|||||||
Tools []claude.Tool `json:"tools,omitempty"`
|
Tools []claude.Tool `json:"tools,omitempty"`
|
||||||
ToolChoice any `json:"tool_choice,omitempty"`
|
ToolChoice any `json:"tool_choice,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyRequest(req *claude.ClaudeRequest) *AwsClaudeRequest {
|
||||||
|
return &AwsClaudeRequest{
|
||||||
|
AnthropicVersion: "bedrock-2023-05-31",
|
||||||
|
System: req.System,
|
||||||
|
Messages: req.Messages,
|
||||||
|
MaxTokens: req.MaxTokens,
|
||||||
|
Temperature: req.Temperature,
|
||||||
|
TopP: req.TopP,
|
||||||
|
TopK: req.TopK,
|
||||||
|
StopSequences: req.StopSequences,
|
||||||
|
Tools: req.Tools,
|
||||||
|
ToolChoice: req.ToolChoice,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/copier"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -78,13 +77,7 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
|
|||||||
return wrapErr(errors.New("request not found")), nil
|
return wrapErr(errors.New("request not found")), nil
|
||||||
}
|
}
|
||||||
claudeReq := claudeReq_.(*claude.ClaudeRequest)
|
claudeReq := claudeReq_.(*claude.ClaudeRequest)
|
||||||
awsClaudeReq := &AwsClaudeRequest{
|
awsClaudeReq := copyRequest(claudeReq)
|
||||||
AnthropicVersion: "bedrock-2023-05-31",
|
|
||||||
}
|
|
||||||
if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
|
|
||||||
return wrapErr(errors.Wrap(err, "copy request")), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapErr(errors.Wrap(err, "marshal request")), nil
|
return wrapErr(errors.Wrap(err, "marshal request")), nil
|
||||||
@@ -136,12 +129,7 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
|||||||
}
|
}
|
||||||
claudeReq := claudeReq_.(*claude.ClaudeRequest)
|
claudeReq := claudeReq_.(*claude.ClaudeRequest)
|
||||||
|
|
||||||
awsClaudeReq := &AwsClaudeRequest{
|
awsClaudeReq := copyRequest(claudeReq)
|
||||||
AnthropicVersion: "bedrock-2023-05-31",
|
|
||||||
}
|
|
||||||
if err = copier.Copy(awsClaudeReq, claudeReq); err != nil {
|
|
||||||
return wrapErr(errors.Wrap(err, "copy request")), nil
|
|
||||||
}
|
|
||||||
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
awsReq.Body, err = json.Marshal(awsClaudeReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapErr(errors.Wrap(err, "marshal request")), nil
|
return wrapErr(errors.Wrap(err, "marshal request")), nil
|
||||||
|
|||||||
@@ -30,13 +30,13 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||||
// 从映射中获取模型名称对应的版本,如果找不到就使用 info.ApiVersion 或默认的版本 "v1"
|
// 从映射中获取模型名称对应的版本,如果找不到就使用 info.ApiVersion 或默认的版本 "v1beta"
|
||||||
version, beta := constant.GeminiModelMap[info.UpstreamModelName]
|
version, beta := constant.GeminiModelMap[info.UpstreamModelName]
|
||||||
if !beta {
|
if !beta {
|
||||||
if info.ApiVersion != "" {
|
if info.ApiVersion != "" {
|
||||||
version = info.ApiVersion
|
version = info.ApiVersion
|
||||||
} else {
|
} else {
|
||||||
version = "v1"
|
version = "v1beta"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const (
|
|||||||
var ModelList = []string{
|
var ModelList = []string{
|
||||||
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
|
"gemini-1.0-pro-latest", "gemini-1.0-pro-001", "gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-ultra",
|
||||||
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
|
"gemini-1.0-pro-vision-latest", "gemini-1.0-pro-vision-001", "gemini-1.5-pro-exp-0827", "gemini-1.5-flash-exp-0827",
|
||||||
|
"gemini-exp-1114",
|
||||||
}
|
}
|
||||||
|
|
||||||
var ChannelName = "google gemini"
|
var ChannelName = "google gemini"
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
|||||||
|
|
||||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||||
if info.RelayMode == constant.RelayModeRealtime {
|
if info.RelayMode == constant.RelayModeRealtime {
|
||||||
// trim https
|
if strings.HasPrefix(info.BaseUrl, "https://") {
|
||||||
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
||||||
baseUrl = strings.TrimPrefix(baseUrl, "http://")
|
baseUrl = "wss://" + baseUrl
|
||||||
baseUrl = "wss://" + baseUrl
|
info.BaseUrl = baseUrl
|
||||||
info.BaseUrl = baseUrl
|
} else if strings.HasPrefix(info.BaseUrl, "http://") {
|
||||||
|
baseUrl := strings.TrimPrefix(info.BaseUrl, "http://")
|
||||||
|
baseUrl = "ws://" + baseUrl
|
||||||
|
info.BaseUrl = baseUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch info.ChannelType {
|
switch info.ChannelType {
|
||||||
case common.ChannelTypeAzure:
|
case common.ChannelTypeAzure:
|
||||||
@@ -132,6 +136,19 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
|||||||
|
|
||||||
writer.WriteField("model", request.Model)
|
writer.WriteField("model", request.Model)
|
||||||
|
|
||||||
|
// 获取所有表单字段
|
||||||
|
formData := c.Request.PostForm
|
||||||
|
|
||||||
|
// 遍历表单字段并打印输出
|
||||||
|
for key, values := range formData {
|
||||||
|
if key == "model" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, value := range values {
|
||||||
|
writer.WriteField(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加文件字段
|
// 添加文件字段
|
||||||
file, header, err := c.Request.FormFile("file")
|
file, header, err := c.Request.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ var ModelList = []string{
|
|||||||
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
|
||||||
"gpt-4-vision-preview",
|
"gpt-4-vision-preview",
|
||||||
"chatgpt-4o-latest",
|
"chatgpt-4o-latest",
|
||||||
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06",
|
"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06", "gpt-4o-2024-11-20",
|
||||||
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
|
"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
|
||||||
"o1-preview", "o1-preview-2024-09-12",
|
"o1-preview", "o1-preview-2024-09-12",
|
||||||
"o1-mini", "o1-mini-2024-09-12",
|
"o1-mini", "o1-mini-2024-09-12",
|
||||||
|
|||||||
@@ -98,6 +98,11 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
|||||||
shouldSendLastResp = false
|
shouldSendLastResp = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, choice := range lastStreamResponse.Choices {
|
||||||
|
if choice.FinishReason != nil {
|
||||||
|
shouldSendLastResp = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if shouldSendLastResp {
|
if shouldSendLastResp {
|
||||||
service.StringData(c, lastStreamData)
|
service.StringData(c, lastStreamData)
|
||||||
@@ -279,7 +284,6 @@ func OpenaiTTSHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
|||||||
}
|
}
|
||||||
|
|
||||||
func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, responseFormat string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||||
var audioResp dto.AudioResponse
|
|
||||||
responseBody, err := io.ReadAll(resp.Body)
|
responseBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||||
@@ -288,11 +292,6 @@ func OpenaiSTTHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(responseBody, &audioResp)
|
|
||||||
if err != nil {
|
|
||||||
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset response body
|
// Reset response body
|
||||||
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
resp.Body = io.NopCloser(bytes.NewBuffer(responseBody))
|
||||||
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
// We shouldn't set the header before we parse the response body, because the parse part may fail.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type RelayInfo struct {
|
|||||||
ChannelType int
|
ChannelType int
|
||||||
ChannelId int
|
ChannelId int
|
||||||
TokenId int
|
TokenId int
|
||||||
|
TokenKey string
|
||||||
UserId int
|
UserId int
|
||||||
Group string
|
Group string
|
||||||
TokenUnlimited bool
|
TokenUnlimited bool
|
||||||
@@ -58,6 +59,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|||||||
channelId := c.GetInt("channel_id")
|
channelId := c.GetInt("channel_id")
|
||||||
|
|
||||||
tokenId := c.GetInt("token_id")
|
tokenId := c.GetInt("token_id")
|
||||||
|
tokenKey := c.GetString("token_key")
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
group := c.GetString("group")
|
group := c.GetString("group")
|
||||||
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
||||||
@@ -73,6 +75,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
|||||||
ChannelType: channelType,
|
ChannelType: channelType,
|
||||||
ChannelId: channelId,
|
ChannelId: channelId,
|
||||||
TokenId: tokenId,
|
TokenId: tokenId,
|
||||||
|
TokenKey: tokenKey,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
Group: group,
|
Group: group,
|
||||||
TokenUnlimited: tokenUnlimited,
|
TokenUnlimited: tokenUnlimited,
|
||||||
|
|||||||
@@ -33,12 +33,19 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if audioRequest.Model == "" {
|
err = c.Request.ParseForm()
|
||||||
audioRequest.Model = c.PostForm("model")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
formData := c.Request.PostForm
|
||||||
|
if audioRequest.Model == "" {
|
||||||
|
audioRequest.Model = formData.Get("model")
|
||||||
|
}
|
||||||
|
|
||||||
if audioRequest.Model == "" {
|
if audioRequest.Model == "" {
|
||||||
return nil, errors.New("model is required")
|
return nil, errors.New("model is required")
|
||||||
}
|
}
|
||||||
|
audioRequest.ResponseFormat = formData.Get("response_format")
|
||||||
if audioRequest.ResponseFormat == "" {
|
if audioRequest.ResponseFormat == "" {
|
||||||
audioRequest.ResponseFormat = "json"
|
audioRequest.ResponseFormat = "json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package relay
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -19,6 +18,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
|
|||||||
textRequest.Model = c.Param("model")
|
textRequest.Model = c.Param("model")
|
||||||
}
|
}
|
||||||
|
|
||||||
if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 {
|
if textRequest.MaxTokens > math.MaxInt32/2 {
|
||||||
return nil, errors.New("max_tokens is invalid")
|
return nil, errors.New("max_tokens is invalid")
|
||||||
}
|
}
|
||||||
if textRequest.Model == "" {
|
if textRequest.Model == "" {
|
||||||
@@ -47,12 +48,12 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
|
|||||||
return nil, errors.New("field prompt is required")
|
return nil, errors.New("field prompt is required")
|
||||||
}
|
}
|
||||||
case relayconstant.RelayModeChatCompletions:
|
case relayconstant.RelayModeChatCompletions:
|
||||||
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
|
if len(textRequest.Messages) == 0 {
|
||||||
return nil, errors.New("field messages is required")
|
return nil, errors.New("field messages is required")
|
||||||
}
|
}
|
||||||
case relayconstant.RelayModeEmbeddings:
|
case relayconstant.RelayModeEmbeddings:
|
||||||
case relayconstant.RelayModeModerations:
|
case relayconstant.RelayModeModerations:
|
||||||
if textRequest.Input == "" || textRequest.Input == nil {
|
if textRequest.Input == nil || textRequest.Input == "" {
|
||||||
return nil, errors.New("field input is required")
|
return nil, errors.New("field input is required")
|
||||||
}
|
}
|
||||||
case relayconstant.RelayModeEdits:
|
case relayconstant.RelayModeEdits:
|
||||||
@@ -76,7 +77,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map model name
|
// map model name
|
||||||
isModelMapped := false
|
//isModelMapped := false
|
||||||
modelMapping := c.GetString("model_mapping")
|
modelMapping := c.GetString("model_mapping")
|
||||||
//isModelMapped := false
|
//isModelMapped := false
|
||||||
if modelMapping != "" && modelMapping != "{}" {
|
if modelMapping != "" && modelMapping != "{}" {
|
||||||
@@ -86,7 +87,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
|||||||
return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
if modelMap[textRequest.Model] != "" {
|
if modelMap[textRequest.Model] != "" {
|
||||||
isModelMapped = true
|
//isModelMapped = true
|
||||||
textRequest.Model = modelMap[textRequest.Model]
|
textRequest.Model = modelMap[textRequest.Model]
|
||||||
// set upstream model name
|
// set upstream model name
|
||||||
//isModelMapped = true
|
//isModelMapped = true
|
||||||
@@ -165,23 +166,25 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
|||||||
adaptor.Init(relayInfo)
|
adaptor.Init(relayInfo)
|
||||||
var requestBody io.Reader
|
var requestBody io.Reader
|
||||||
|
|
||||||
if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
|
//if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped {
|
||||||
body, err := common.GetRequestBody(c)
|
// body, err := common.GetRequestBody(c)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
|
// return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError)
|
||||||
}
|
// }
|
||||||
requestBody = bytes.NewBuffer(body)
|
// requestBody = bytes.NewBuffer(body)
|
||||||
} else {
|
//} else {
|
||||||
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
|
//
|
||||||
if err != nil {
|
//}
|
||||||
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
|
|
||||||
}
|
convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest)
|
||||||
jsonData, err := json.Marshal(convertedRequest)
|
if err != nil {
|
||||||
if err != nil {
|
return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError)
|
||||||
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
requestBody = bytes.NewBuffer(jsonData)
|
|
||||||
}
|
}
|
||||||
|
jsonData, err := sonic.Marshal(convertedRequest)
|
||||||
|
if err != nil {
|
||||||
|
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
requestBody = bytes.NewBuffer(jsonData)
|
||||||
|
|
||||||
statusCodeMappingStr := c.GetString("status_code_mapping")
|
statusCodeMappingStr := c.GetString("status_code_mapping")
|
||||||
var httpResp *http.Response
|
var httpResp *http.Response
|
||||||
@@ -261,7 +264,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
|
|||||||
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
|
||||||
}
|
}
|
||||||
if userQuota-preConsumedQuota < 0 {
|
if userQuota-preConsumedQuota < 0 {
|
||||||
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota)), "insufficient_user_quota", http.StatusBadRequest)
|
return 0, 0, service.OpenAIErrorWrapperLocal(fmt.Errorf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota), "insufficient_user_quota", http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
|
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -295,13 +298,14 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
|
|||||||
|
|
||||||
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
|
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
|
||||||
if preConsumedQuota != 0 {
|
if preConsumedQuota != 0 {
|
||||||
go func(ctx context.Context) {
|
go func() {
|
||||||
// return pre-consumed quota
|
relayInfoCopy := *relayInfo
|
||||||
err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
|
|
||||||
|
err := model.PostConsumeTokenQuota(&relayInfoCopy, userQuota, -preConsumedQuota, 0, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("error return pre-consumed quota: " + err.Error())
|
common.SysError("error return pre-consumed quota: " + err.Error())
|
||||||
}
|
}
|
||||||
}(c)
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.POST("/", controller.AddChannel)
|
channelRoute.POST("/", controller.AddChannel)
|
||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
channelRoute.PUT("/", controller.UpdateChannel)
|
||||||
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
|
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
|
||||||
|
channelRoute.POST("/tag/disabled", controller.DisableTagChannels)
|
||||||
|
channelRoute.POST("/tag/enabled", controller.EnableTagChannels)
|
||||||
|
channelRoute.PUT("/tag", controller.EditTagChannels)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||||
channelRoute.POST("/batch", controller.DeleteChannelBatch)
|
channelRoute.POST("/batch", controller.DeleteChannelBatch)
|
||||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
func SetRelayRouter(router *gin.Engine) {
|
func SetRelayRouter(router *gin.Engine) {
|
||||||
router.Use(middleware.CORS())
|
router.Use(middleware.CORS())
|
||||||
|
router.Use(middleware.DecompressRequestMiddleware())
|
||||||
// https://platform.openai.com/docs/api-reference/introduction
|
// https://platform.openai.com/docs/api-reference/introduction
|
||||||
modelsRouter := router.Group("/v1/models")
|
modelsRouter := router.Group("/v1/models")
|
||||||
modelsRouter.Use(middleware.TokenAuth())
|
modelsRouter.Use(middleware.TokenAuth())
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.ApiKey, "sk-"))
|
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.TokenKey, "sk-"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
|||||||
return errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
|
return errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
|
||||||
}
|
}
|
||||||
|
|
||||||
if token.RemainQuota < quota {
|
if !token.UnlimitedQuota && token.RemainQuota < quota {
|
||||||
return errors.New(fmt.Sprintf("令牌额度不足,剩余额度为 %d", token.RemainQuota))
|
return errors.New(fmt.Sprintf("令牌额度不足,剩余额度为 %d", token.RemainQuota))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -44,8 +44,15 @@ const LoginForm = () => {
|
|||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
|
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
||||||
|
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
|
|
||||||
|
let affCode = new URLSearchParams(window.location.search).get('aff');
|
||||||
|
if (affCode) {
|
||||||
|
localStorage.setItem('aff', affCode);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get('expired')) {
|
if (searchParams.get('expired')) {
|
||||||
showError('未登录或登录已过期,请重新登录!');
|
showError('未登录或登录已过期,请重新登录!');
|
||||||
@@ -61,7 +68,6 @@ const LoginForm = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
|
||||||
|
|
||||||
const onWeChatLoginClicked = () => {
|
const onWeChatLoginClicked = () => {
|
||||||
setShowWeChatLoginModal(true);
|
setShowWeChatLoginModal(true);
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { Button, Card, Form, Layout } from '@douyinfe/semi-ui';
|
import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui';
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||||
|
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||||
|
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils.js';
|
||||||
|
import LinuxDoIcon from './LinuxDoIcon.js';
|
||||||
|
import WeChatIcon from './WeChatIcon.js';
|
||||||
|
import TelegramLoginButton from 'react-telegram-login/src';
|
||||||
|
import { setUserData } from '../helpers/data.js';
|
||||||
|
|
||||||
const RegisterForm = () => {
|
const RegisterForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@@ -20,7 +26,11 @@ const RegisterForm = () => {
|
|||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
|
||||||
|
const [status, setStatus] = useState({});
|
||||||
|
let navigate = useNavigate();
|
||||||
const logo = getLogo();
|
const logo = getLogo();
|
||||||
|
|
||||||
let affCode = new URLSearchParams(window.location.search).get('aff');
|
let affCode = new URLSearchParams(window.location.search).get('aff');
|
||||||
if (affCode) {
|
if (affCode) {
|
||||||
localStorage.setItem('aff', affCode);
|
localStorage.setItem('aff', affCode);
|
||||||
@@ -30,6 +40,7 @@ const RegisterForm = () => {
|
|||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
if (status) {
|
if (status) {
|
||||||
status = JSON.parse(status);
|
status = JSON.parse(status);
|
||||||
|
setStatus(status);
|
||||||
setShowEmailVerification(status.email_verification);
|
setShowEmailVerification(status.email_verification);
|
||||||
if (status.turnstile_check) {
|
if (status.turnstile_check) {
|
||||||
setTurnstileEnabled(true);
|
setTurnstileEnabled(true);
|
||||||
@@ -38,7 +49,32 @@ const RegisterForm = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let navigate = useNavigate();
|
|
||||||
|
const onWeChatLoginClicked = () => {
|
||||||
|
setShowWeChatLoginModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmitWeChatVerificationCode = async () => {
|
||||||
|
if (turnstileEnabled && turnstileToken === '') {
|
||||||
|
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await API.get(
|
||||||
|
`/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
|
||||||
|
);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
setUserData(data);
|
||||||
|
updateAPI();
|
||||||
|
navigate('/');
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
setShowWeChatLoginModal(false);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function handleChange(name, value) {
|
function handleChange(name, value) {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
@@ -189,14 +225,127 @@ const RegisterForm = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
{status.github_oauth ||
|
||||||
|
status.wechat_login ||
|
||||||
|
status.telegram_oauth ||
|
||||||
|
status.linuxdo_oauth ? (
|
||||||
|
<>
|
||||||
|
<Divider margin='12px' align='center'>
|
||||||
|
第三方登录
|
||||||
|
</Divider>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status.github_oauth ? (
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<IconGithubLogo />}
|
||||||
|
onClick={() =>
|
||||||
|
onGitHubOAuthClicked(status.github_client_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{status.linuxdo_oauth ? (
|
||||||
|
<Button
|
||||||
|
icon={<LinuxDoIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
onLinuxDOOAuthClicked(status.linuxdo_client_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{status.wechat_login ? (
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
style={{ color: 'rgba(var(--semi-green-5), 1)' }}
|
||||||
|
icon={<Icon svg={<WeChatIcon />} />}
|
||||||
|
onClick={onWeChatLoginClicked}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{status.telegram_oauth ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TelegramLoginButton
|
||||||
|
dataOnauth={onTelegramLoginClicked}
|
||||||
|
botName={status.telegram_bot_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
{turnstileEnabled ? (
|
<Modal
|
||||||
<Turnstile
|
title='微信扫码登录'
|
||||||
sitekey={turnstileSiteKey}
|
visible={showWeChatLoginModal}
|
||||||
onVerify={(token) => {
|
maskClosable={true}
|
||||||
setTurnstileToken(token);
|
onOk={onSubmitWeChatVerificationCode}
|
||||||
|
onCancel={() => setShowWeChatLoginModal(false)}
|
||||||
|
okText={'登录'}
|
||||||
|
size={'small'}
|
||||||
|
centered={true}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItem: 'center',
|
||||||
|
flexDirection: 'column',
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<img src={status.wechat_qrcode} />
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<p>
|
||||||
|
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Form size='large'>
|
||||||
|
<Form.Input
|
||||||
|
field={'wechat_verification_code'}
|
||||||
|
placeholder='验证码'
|
||||||
|
label={'验证码'}
|
||||||
|
value={inputs.wechat_verification_code}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleChange('wechat_verification_code', value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
{turnstileEnabled ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Turnstile
|
||||||
|
sitekey={turnstileSiteKey}
|
||||||
|
onVerify={(token) => {
|
||||||
|
setTurnstileToken(token);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|||||||
21
web/src/components/custom/TextInput.js
Normal file
21
web/src/components/custom/TextInput.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Input, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>{label}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
name={name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
value={value}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextInput;
|
||||||
21
web/src/components/custom/TextNumberInput.js
Normal file
21
web/src/components/custom/TextNumberInput.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Input, InputNumber, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>{label}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<InputNumber
|
||||||
|
name={name}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onChange={(value) => onChange(value)}
|
||||||
|
value={value}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TextNumberInput;
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import { API, showError } from '../helpers';
|
import { API, showError } from '../helpers';
|
||||||
|
|
||||||
export async function getOAuthState() {
|
export async function getOAuthState() {
|
||||||
const res = await API.get('/api/oauth/state');
|
let path = '/api/oauth/state';
|
||||||
|
let affCode = localStorage.getItem('aff');
|
||||||
|
if (affCode && affCode.length > 0) {
|
||||||
|
path += `?aff=${affCode}`;
|
||||||
|
}
|
||||||
|
const res = await API.get(path);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ export let API = axios.create({
|
|||||||
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||||
: '',
|
: '',
|
||||||
headers: {
|
headers: {
|
||||||
'New-API-User': getUserIdFromLocalStorage()
|
'New-API-User': getUserIdFromLocalStorage(),
|
||||||
|
'Cache-Control': 'no-store'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ export function updateAPI() {
|
|||||||
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||||
: '',
|
: '',
|
||||||
headers: {
|
headers: {
|
||||||
'New-API-User': getUserIdFromLocalStorage()
|
'New-API-User': getUserIdFromLocalStorage(),
|
||||||
|
'Cache-Control': 'no-store'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function renderNumberWithPoint(num) {
|
export function renderNumberWithPoint(num) {
|
||||||
|
if (num === undefined)
|
||||||
|
return '';
|
||||||
num = num.toFixed(2);
|
num = num.toFixed(2);
|
||||||
if (num >= 100000) {
|
if (num >= 100000) {
|
||||||
// Convert number to string to manipulate it
|
// Convert number to string to manipulate it
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
showError,
|
showError,
|
||||||
showInfo,
|
showInfo,
|
||||||
showSuccess,
|
showSuccess,
|
||||||
verifyJSON,
|
verifyJSON
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import { CHANNEL_OPTIONS } from '../../constants';
|
import { CHANNEL_OPTIONS } from '../../constants';
|
||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
@@ -21,28 +21,26 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
TextArea,
|
TextArea,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Banner,
|
Banner
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { Divider } from 'semantic-ui-react';
|
import { Divider } from 'semantic-ui-react';
|
||||||
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const MODEL_MAPPING_EXAMPLE = {
|
const MODEL_MAPPING_EXAMPLE = {
|
||||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||||
'gpt-4-0314': 'gpt-4',
|
|
||||||
'gpt-4-32k-0314': 'gpt-4-32k',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_CODE_MAPPING_EXAMPLE = {
|
const STATUS_CODE_MAPPING_EXAMPLE = {
|
||||||
400: '500',
|
400: '500'
|
||||||
};
|
};
|
||||||
|
|
||||||
const REGION_EXAMPLE = {
|
const REGION_EXAMPLE = {
|
||||||
"default": "us-central1",
|
'default': 'us-central1',
|
||||||
"claude-3-5-sonnet-20240620": "europe-west1"
|
'claude-3-5-sonnet-20240620': 'europe-west1'
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchButtonTips = "1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出"
|
const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出';
|
||||||
|
|
||||||
function type2secretPrompt(type) {
|
function type2secretPrompt(type) {
|
||||||
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
||||||
@@ -84,6 +82,9 @@ const EditChannel = (props) => {
|
|||||||
auto_ban: 1,
|
auto_ban: 1,
|
||||||
test_model: '',
|
test_model: '',
|
||||||
groups: ['default'],
|
groups: ['default'],
|
||||||
|
priority: 0,
|
||||||
|
weight: 0,
|
||||||
|
tag: ''
|
||||||
};
|
};
|
||||||
const [batch, setBatch] = useState(false);
|
const [batch, setBatch] = useState(false);
|
||||||
const [autoBan, setAutoBan] = useState(true);
|
const [autoBan, setAutoBan] = useState(true);
|
||||||
@@ -108,7 +109,7 @@ const EditChannel = (props) => {
|
|||||||
'mj_blend',
|
'mj_blend',
|
||||||
'mj_upscale',
|
'mj_upscale',
|
||||||
'mj_describe',
|
'mj_describe',
|
||||||
'mj_uploads',
|
'mj_uploads'
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
@@ -128,13 +129,13 @@ const EditChannel = (props) => {
|
|||||||
'mj_high_variation',
|
'mj_high_variation',
|
||||||
'mj_low_variation',
|
'mj_low_variation',
|
||||||
'mj_pan',
|
'mj_pan',
|
||||||
'mj_uploads',
|
'mj_uploads'
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case 36:
|
case 36:
|
||||||
localModels = [
|
localModels = [
|
||||||
'suno_music',
|
'suno_music',
|
||||||
'suno_lyrics',
|
'suno_lyrics'
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -171,7 +172,7 @@ const EditChannel = (props) => {
|
|||||||
data.model_mapping = JSON.stringify(
|
data.model_mapping = JSON.stringify(
|
||||||
JSON.parse(data.model_mapping),
|
JSON.parse(data.model_mapping),
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setInputs(data);
|
setInputs(data);
|
||||||
@@ -190,70 +191,69 @@ const EditChannel = (props) => {
|
|||||||
|
|
||||||
|
|
||||||
const fetchUpstreamModelList = async (name) => {
|
const fetchUpstreamModelList = async (name) => {
|
||||||
if (inputs["type"] !== 1) {
|
if (inputs['type'] !== 1) {
|
||||||
showError("仅支持 OpenAI 接口格式")
|
showError('仅支持 OpenAI 接口格式');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
const models = inputs["models"] || []
|
const models = inputs['models'] || [];
|
||||||
let err = false;
|
let err = false;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
const res = await API.get("/api/channel/fetch_models/" + channelId)
|
const res = await API.get('/api/channel/fetch_models/' + channelId);
|
||||||
if (res.data && res.data?.success) {
|
if (res.data && res.data?.success) {
|
||||||
models.push(...res.data.data)
|
models.push(...res.data.data);
|
||||||
} else {
|
} else {
|
||||||
err = true
|
err = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!inputs?.["key"]) {
|
if (!inputs?.['key']) {
|
||||||
showError("请填写密钥")
|
showError('请填写密钥');
|
||||||
err = true
|
err = true;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const host = new URL((inputs["base_url"] || "https://api.openai.com"))
|
const host = new URL((inputs['base_url'] || 'https://api.openai.com'));
|
||||||
|
|
||||||
const url = `https://${host.hostname}/v1/models`;
|
const url = `https://${host.hostname}/v1/models`;
|
||||||
const key = inputs["key"];
|
const key = inputs['key'];
|
||||||
const res = await axios.get(url, {
|
const res = await axios.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${key}`
|
'Authorization': `Bearer ${key}`
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
if (res.data && res.data?.success) {
|
if (res.data && res.data?.success) {
|
||||||
models.push(...res.data.data.map((model) => model.id))
|
models.push(...res.data.data.map((model) => model.id));
|
||||||
} else {
|
} else {
|
||||||
err = true
|
err = true;
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
catch (error) {
|
err = true;
|
||||||
err = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!err) {
|
if (!err) {
|
||||||
handleInputChange(name, Array.from(new Set(models)));
|
handleInputChange(name, Array.from(new Set(models)));
|
||||||
showSuccess("获取模型列表成功");
|
showSuccess('获取模型列表成功');
|
||||||
} else {
|
} else {
|
||||||
showError('获取模型列表失败');
|
showError('获取模型列表失败');
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchModels = async () => {
|
const fetchModels = async () => {
|
||||||
try {
|
try {
|
||||||
let res = await API.get(`/api/channel/models`);
|
let res = await API.get(`/api/channel/models`);
|
||||||
let localModelOptions = res.data.data.map((model) => ({
|
let localModelOptions = res.data.data.map((model) => ({
|
||||||
label: model.id,
|
label: model.id,
|
||||||
value: model.id,
|
value: model.id
|
||||||
}));
|
}));
|
||||||
setOriginModelOptions(localModelOptions);
|
setOriginModelOptions(localModelOptions);
|
||||||
setFullModels(res.data.data.map((model) => model.id));
|
setFullModels(res.data.data.map((model) => model.id));
|
||||||
setBasicModels(
|
setBasicModels(
|
||||||
res.data.data
|
res.data.data
|
||||||
.filter((model) => {
|
.filter((model) => {
|
||||||
return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
|
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||||
})
|
})
|
||||||
.map((model) => model.id),
|
.map((model) => model.id)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
@@ -269,8 +269,8 @@ const EditChannel = (props) => {
|
|||||||
setGroupOptions(
|
setGroupOptions(
|
||||||
res.data.data.map((group) => ({
|
res.data.data.map((group) => ({
|
||||||
label: group,
|
label: group,
|
||||||
value: group,
|
value: group
|
||||||
})),
|
}))
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error.message);
|
showError(error.message);
|
||||||
@@ -280,10 +280,10 @@ const EditChannel = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let localModelOptions = [...originModelOptions];
|
let localModelOptions = [...originModelOptions];
|
||||||
inputs.models.forEach((model) => {
|
inputs.models.forEach((model) => {
|
||||||
if (!localModelOptions.find((option) => option.key === model)) {
|
if (!localModelOptions.find((option) => option.label === model)) {
|
||||||
localModelOptions.push({
|
localModelOptions.push({
|
||||||
label: model,
|
label: model,
|
||||||
value: model,
|
value: model
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -320,7 +320,7 @@ const EditChannel = (props) => {
|
|||||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(
|
localInputs.base_url = localInputs.base_url.slice(
|
||||||
0,
|
0,
|
||||||
localInputs.base_url.length - 1,
|
localInputs.base_url.length - 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (localInputs.type === 3 && localInputs.other === '') {
|
if (localInputs.type === 3 && localInputs.other === '') {
|
||||||
@@ -341,7 +341,7 @@ const EditChannel = (props) => {
|
|||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
res = await API.put(`/api/channel/`, {
|
res = await API.put(`/api/channel/`, {
|
||||||
...localInputs,
|
...localInputs,
|
||||||
id: parseInt(channelId),
|
id: parseInt(channelId)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res = await API.post(`/api/channel/`, localInputs);
|
res = await API.post(`/api/channel/`, localInputs);
|
||||||
@@ -378,7 +378,7 @@ const EditChannel = (props) => {
|
|||||||
// 添加到下拉选项
|
// 添加到下拉选项
|
||||||
key: model,
|
key: model,
|
||||||
text: model,
|
text: model,
|
||||||
value: model,
|
value: model
|
||||||
});
|
});
|
||||||
} else if (model) {
|
} else if (model) {
|
||||||
showError('某些模型已存在!');
|
showError('某些模型已存在!');
|
||||||
@@ -409,11 +409,11 @@ const EditChannel = (props) => {
|
|||||||
footer={
|
footer={
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button theme='solid' size={'large'} onClick={submit}>
|
<Button theme="solid" size={'large'} onClick={submit}>
|
||||||
提交
|
提交
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme='solid'
|
theme="solid"
|
||||||
size={'large'}
|
size={'large'}
|
||||||
type={'tertiary'}
|
type={'tertiary'}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
@@ -432,7 +432,7 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>类型:</Typography.Text>
|
<Typography.Text strong>类型:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
name='type'
|
name="type"
|
||||||
required
|
required
|
||||||
optionList={CHANNEL_OPTIONS}
|
optionList={CHANNEL_OPTIONS}
|
||||||
value={inputs.type}
|
value={inputs.type}
|
||||||
@@ -450,8 +450,8 @@ const EditChannel = (props) => {
|
|||||||
,因为 One API 会把请求体中的 model
|
,因为 One API 会把请求体中的 model
|
||||||
参数替换为你的部署名称(模型名称中的点会被剔除),
|
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||||
<a
|
<a
|
||||||
target='_blank'
|
target="_blank"
|
||||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
|
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
|
||||||
>
|
>
|
||||||
图片演示
|
图片演示
|
||||||
</a>
|
</a>
|
||||||
@@ -466,8 +466,8 @@ const EditChannel = (props) => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label='AZURE_OPENAI_ENDPOINT'
|
label="AZURE_OPENAI_ENDPOINT"
|
||||||
name='azure_base_url'
|
name="azure_base_url"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
||||||
}
|
}
|
||||||
@@ -475,14 +475,14 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('base_url', value);
|
handleInputChange('base_url', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label='默认 API 版本'
|
label="默认 API 版本"
|
||||||
name='azure_other'
|
name="azure_other"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
|
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||||
}
|
}
|
||||||
@@ -490,7 +490,7 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('other', value);
|
handleInputChange('other', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -512,7 +512,7 @@ const EditChannel = (props) => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name='base_url'
|
name="base_url"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
|
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
|
||||||
}
|
}
|
||||||
@@ -520,49 +520,84 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('base_url', value);
|
handleInputChange('base_url', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>代理:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="代理"
|
||||||
|
name="base_url"
|
||||||
|
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('base_url', value);
|
||||||
|
}}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{inputs.type === 22 && (
|
||||||
|
<>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>私有部署地址:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
name="base_url"
|
||||||
|
placeholder={
|
||||||
|
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
|
||||||
|
}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('base_url', value);
|
||||||
|
}}
|
||||||
|
value={inputs.base_url}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{inputs.type === 36 && (
|
{inputs.type === 36 && (
|
||||||
<>
|
<>
|
||||||
<div style={{marginTop: 10}}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Typography.Text strong>
|
<Typography.Text strong>
|
||||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name='base_url'
|
name="base_url"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||||
}
|
}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('base_url', value);
|
handleInputChange('base_url', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div style={{marginTop: 10}}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Typography.Text strong>名称:</Typography.Text>
|
<Typography.Text strong>名称:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
required
|
required
|
||||||
name='name'
|
name="name"
|
||||||
placeholder={'请为渠道命名'}
|
placeholder={'请为渠道命名'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('name', value);
|
handleInputChange('name', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.name}
|
value={inputs.name}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Typography.Text strong>分组:</Typography.Text>
|
<Typography.Text strong>分组:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
placeholder={'请选择可以使用该渠道的分组'}
|
placeholder={'请选择可以使用该渠道的分组'}
|
||||||
name='groups'
|
name="groups"
|
||||||
required
|
required
|
||||||
multiple
|
multiple
|
||||||
selection
|
selection
|
||||||
@@ -572,7 +607,7 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('groups', value);
|
handleInputChange('groups', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.groups}
|
value={inputs.groups}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
optionList={groupOptions}
|
optionList={groupOptions}
|
||||||
/>
|
/>
|
||||||
{inputs.type === 18 && (
|
{inputs.type === 18 && (
|
||||||
@@ -581,7 +616,7 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>模型版本:</Typography.Text>
|
<Typography.Text strong>模型版本:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name='other'
|
name="other"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
|
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
|
||||||
}
|
}
|
||||||
@@ -589,7 +624,7 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('other', value);
|
handleInputChange('other', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -599,7 +634,7 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>部署地区:</Typography.Text>
|
<Typography.Text strong>部署地区:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
name='other'
|
name="other"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||||
'{\n' +
|
'{\n' +
|
||||||
@@ -612,18 +647,18 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('other', value);
|
handleInputChange('other', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
color: 'rgba(var(--semi-blue-5), 1)',
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
'other',
|
'other',
|
||||||
JSON.stringify(REGION_EXAMPLE, null, 2),
|
JSON.stringify(REGION_EXAMPLE, null, 2)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -637,14 +672,14 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>知识库 ID:</Typography.Text>
|
<Typography.Text strong>知识库 ID:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label='知识库 ID'
|
label="知识库 ID"
|
||||||
name='other'
|
name="other"
|
||||||
placeholder={'请输入知识库 ID,例如:123456'}
|
placeholder={'请输入知识库 ID,例如:123456'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('other', value);
|
handleInputChange('other', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -654,7 +689,7 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>Account ID:</Typography.Text>
|
<Typography.Text strong>Account ID:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name='other'
|
name="other"
|
||||||
placeholder={
|
placeholder={
|
||||||
'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
|
'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
|
||||||
}
|
}
|
||||||
@@ -662,7 +697,7 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('other', value);
|
handleInputChange('other', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.other}
|
value={inputs.other}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -671,7 +706,7 @@ const EditChannel = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
placeholder={'请选择该渠道所支持的模型'}
|
placeholder={'请选择该渠道所支持的模型'}
|
||||||
name='models'
|
name="models"
|
||||||
required
|
required
|
||||||
multiple
|
multiple
|
||||||
selection
|
selection
|
||||||
@@ -679,13 +714,13 @@ const EditChannel = (props) => {
|
|||||||
handleInputChange('models', value);
|
handleInputChange('models', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.models}
|
value={inputs.models}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
optionList={modelOptions}
|
optionList={modelOptions}
|
||||||
/>
|
/>
|
||||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
type='primary'
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange('models', basicModels);
|
handleInputChange('models', basicModels);
|
||||||
}}
|
}}
|
||||||
@@ -693,7 +728,7 @@ const EditChannel = (props) => {
|
|||||||
填入相关模型
|
填入相关模型
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type='secondary'
|
type="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange('models', fullModels);
|
handleInputChange('models', fullModels);
|
||||||
}}
|
}}
|
||||||
@@ -702,7 +737,7 @@ const EditChannel = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Tooltip content={fetchButtonTips}>
|
<Tooltip content={fetchButtonTips}>
|
||||||
<Button
|
<Button
|
||||||
type='tertiary'
|
type="tertiary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
fetchUpstreamModelList('models');
|
fetchUpstreamModelList('models');
|
||||||
}}
|
}}
|
||||||
@@ -711,7 +746,7 @@ const EditChannel = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
type='warning'
|
type="warning"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange('models', []);
|
handleInputChange('models', []);
|
||||||
}}
|
}}
|
||||||
@@ -721,11 +756,11 @@ const EditChannel = (props) => {
|
|||||||
</Space>
|
</Space>
|
||||||
<Input
|
<Input
|
||||||
addonAfter={
|
addonAfter={
|
||||||
<Button type='primary' onClick={addCustomModels}>
|
<Button type="primary" onClick={addCustomModels}>
|
||||||
填入
|
填入
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
placeholder='输入自定义模型名称'
|
placeholder="输入自定义模型名称"
|
||||||
value={customModel}
|
value={customModel}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setCustomModel(value.trim());
|
setCustomModel(value.trim());
|
||||||
@@ -737,24 +772,24 @@ const EditChannel = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
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={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('model_mapping', value);
|
handleInputChange('model_mapping', value);
|
||||||
}}
|
}}
|
||||||
autosize
|
autosize
|
||||||
value={inputs.model_mapping}
|
value={inputs.model_mapping}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
color: 'rgba(var(--semi-blue-5), 1)',
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
'model_mapping',
|
'model_mapping',
|
||||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
|
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -765,8 +800,8 @@ const EditChannel = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
{batch ? (
|
{batch ? (
|
||||||
<TextArea
|
<TextArea
|
||||||
label='密钥'
|
label="密钥"
|
||||||
name='key'
|
name="key"
|
||||||
required
|
required
|
||||||
placeholder={'请输入密钥,一行一个'}
|
placeholder={'请输入密钥,一行一个'}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -774,14 +809,14 @@ const EditChannel = (props) => {
|
|||||||
}}
|
}}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{inputs.type === 41 ? (
|
{inputs.type === 41 ? (
|
||||||
<TextArea
|
<TextArea
|
||||||
label='鉴权json'
|
label="鉴权json"
|
||||||
name='key'
|
name="key"
|
||||||
required
|
required
|
||||||
placeholder={'{\n' +
|
placeholder={'{\n' +
|
||||||
' "type": "service_account",\n' +
|
' "type": "service_account",\n' +
|
||||||
@@ -801,23 +836,36 @@ const EditChannel = (props) => {
|
|||||||
}}
|
}}
|
||||||
autosize={{ minRows: 10 }}
|
autosize={{ minRows: 10 }}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
label='密钥'
|
label="密钥"
|
||||||
name='key'
|
name="key"
|
||||||
required
|
required
|
||||||
placeholder={type2secretPrompt(inputs.type)}
|
placeholder={type2secretPrompt(inputs.type)}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('key', value);
|
handleInputChange('key', value);
|
||||||
}}
|
}}
|
||||||
value={inputs.key}
|
value={inputs.key}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
{!isEdit && (
|
||||||
|
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||||
|
<Space>
|
||||||
|
<Checkbox
|
||||||
|
checked={batch}
|
||||||
|
label="批量创建"
|
||||||
|
name="batch"
|
||||||
|
onChange={() => setBatch(!batch)}
|
||||||
|
/>
|
||||||
|
<Typography.Text strong>批量创建</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{inputs.type === 1 && (
|
{inputs.type === 1 && (
|
||||||
<>
|
<>
|
||||||
@@ -825,9 +873,9 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>组织:</Typography.Text>
|
<Typography.Text strong>组织:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
label='组织,可选,不填则为默认组织'
|
label="组织,可选,不填则为默认组织"
|
||||||
name='openai_organization'
|
name="openai_organization"
|
||||||
placeholder='请输入组织org-xxx'
|
placeholder="请输入组织org-xxx"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('openai_organization', value);
|
handleInputChange('openai_organization', value);
|
||||||
}}
|
}}
|
||||||
@@ -839,8 +887,8 @@ const EditChannel = (props) => {
|
|||||||
<Typography.Text strong>默认测试模型:</Typography.Text>
|
<Typography.Text strong>默认测试模型:</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
name='test_model'
|
name="test_model"
|
||||||
placeholder='不填则为模型列表第一个'
|
placeholder="不填则为模型列表第一个"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('test_model', value);
|
handleInputChange('test_model', value);
|
||||||
}}
|
}}
|
||||||
@@ -849,7 +897,7 @@ const EditChannel = (props) => {
|
|||||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
name='auto_ban'
|
name="auto_ban"
|
||||||
checked={autoBan}
|
checked={autoBan}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setAutoBan(!autoBan);
|
setAutoBan(!autoBan);
|
||||||
@@ -861,55 +909,6 @@ const EditChannel = (props) => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEdit && (
|
|
||||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
|
||||||
<Space>
|
|
||||||
<Checkbox
|
|
||||||
checked={batch}
|
|
||||||
label='批量创建'
|
|
||||||
name='batch'
|
|
||||||
onChange={() => setBatch(!batch)}
|
|
||||||
/>
|
|
||||||
<Typography.Text strong>批量创建</Typography.Text>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
|
||||||
<>
|
|
||||||
<div style={{ marginTop: 10 }}>
|
|
||||||
<Typography.Text strong>代理:</Typography.Text>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
label='代理'
|
|
||||||
name='base_url'
|
|
||||||
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleInputChange('base_url', value);
|
|
||||||
}}
|
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{inputs.type === 22 && (
|
|
||||||
<>
|
|
||||||
<div style={{ marginTop: 10 }}>
|
|
||||||
<Typography.Text strong>私有部署地址:</Typography.Text>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
name='base_url'
|
|
||||||
placeholder={
|
|
||||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
|
|
||||||
}
|
|
||||||
onChange={(value) => {
|
|
||||||
handleInputChange('base_url', value);
|
|
||||||
}}
|
|
||||||
value={inputs.base_url}
|
|
||||||
autoComplete='new-password'
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div style={{ marginTop: 10 }}>
|
<div style={{ marginTop: 10 }}>
|
||||||
<Typography.Text strong>
|
<Typography.Text strong>
|
||||||
状态码复写(仅影响本地判断,不修改返回到上游的状态码):
|
状态码复写(仅影响本地判断,不修改返回到上游的状态码):
|
||||||
@@ -917,43 +916,74 @@ const EditChannel = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
|
placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
|
||||||
name='status_code_mapping'
|
name="status_code_mapping"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
handleInputChange('status_code_mapping', value);
|
handleInputChange('status_code_mapping', value);
|
||||||
}}
|
}}
|
||||||
autosize
|
autosize
|
||||||
value={inputs.status_code_mapping}
|
value={inputs.status_code_mapping}
|
||||||
autoComplete='new-password'
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
color: 'rgba(var(--semi-blue-5), 1)',
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer'
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleInputChange(
|
handleInputChange(
|
||||||
'status_code_mapping',
|
'status_code_mapping',
|
||||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
|
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
填入模板
|
填入模板
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{/*<div style={{ marginTop: 10 }}>*/}
|
<div style={{ marginTop: 10 }}>
|
||||||
{/* <Typography.Text strong>*/}
|
<Typography.Text strong>
|
||||||
{/* 最大请求token(0表示不限制):*/}
|
渠道标签
|
||||||
{/* </Typography.Text>*/}
|
</Typography.Text>
|
||||||
{/*</div>*/}
|
</div>
|
||||||
{/*<Input*/}
|
<Input
|
||||||
{/* label='最大请求token'*/}
|
label="渠道标签"
|
||||||
{/* name='max_input_tokens'*/}
|
name="tag"
|
||||||
{/* placeholder='默认为0,表示不限制'*/}
|
placeholder={'渠道标签'}
|
||||||
{/* onChange={(value) => {*/}
|
onChange={(value) => {
|
||||||
{/* handleInputChange('max_input_tokens', value);*/}
|
handleInputChange('tag', value);
|
||||||
{/* }}*/}
|
}}
|
||||||
{/* value={inputs.max_input_tokens}*/}
|
value={inputs.tag}
|
||||||
{/*/>*/}
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>
|
||||||
|
渠道优先级
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="渠道优先级"
|
||||||
|
name="priority"
|
||||||
|
placeholder={'渠道优先级'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('priority', parseInt(value));
|
||||||
|
}}
|
||||||
|
value={inputs.priority}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>
|
||||||
|
渠道权重
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="渠道权重"
|
||||||
|
name="weight"
|
||||||
|
placeholder={'渠道权重'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('weight', parseInt(value));
|
||||||
|
}}
|
||||||
|
value={inputs.weight}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
</>
|
</>
|
||||||
|
|||||||
317
web/src/pages/Channel/EditTagModal.js
Normal file
317
web/src/pages/Channel/EditTagModal.js
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
|
||||||
|
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
|
||||||
|
import TextInput from '../../components/custom/TextInput.js';
|
||||||
|
import { getChannelModels } from '../../components/utils.js';
|
||||||
|
|
||||||
|
const MODEL_MAPPING_EXAMPLE = {
|
||||||
|
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditTagModal = (props) => {
|
||||||
|
const { visible, tag, handleClose, refresh } = props;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [originModelOptions, setOriginModelOptions] = useState([]);
|
||||||
|
const [modelOptions, setModelOptions] = useState([]);
|
||||||
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
|
const [basicModels, setBasicModels] = useState([]);
|
||||||
|
const [fullModels, setFullModels] = useState([]);
|
||||||
|
const originInputs = {
|
||||||
|
tag: '',
|
||||||
|
new_tag: null,
|
||||||
|
model_mapping: null,
|
||||||
|
groups: [],
|
||||||
|
models: [],
|
||||||
|
}
|
||||||
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
|
|
||||||
|
const handleInputChange = (name, value) => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
|
if (name === 'type') {
|
||||||
|
let localModels = [];
|
||||||
|
switch (value) {
|
||||||
|
case 2:
|
||||||
|
localModels = [
|
||||||
|
'mj_imagine',
|
||||||
|
'mj_variation',
|
||||||
|
'mj_reroll',
|
||||||
|
'mj_blend',
|
||||||
|
'mj_upscale',
|
||||||
|
'mj_describe',
|
||||||
|
'mj_uploads'
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
localModels = [
|
||||||
|
'swap_face',
|
||||||
|
'mj_imagine',
|
||||||
|
'mj_variation',
|
||||||
|
'mj_reroll',
|
||||||
|
'mj_blend',
|
||||||
|
'mj_upscale',
|
||||||
|
'mj_describe',
|
||||||
|
'mj_zoom',
|
||||||
|
'mj_shorten',
|
||||||
|
'mj_modal',
|
||||||
|
'mj_inpaint',
|
||||||
|
'mj_custom_zoom',
|
||||||
|
'mj_high_variation',
|
||||||
|
'mj_low_variation',
|
||||||
|
'mj_pan',
|
||||||
|
'mj_uploads'
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 36:
|
||||||
|
localModels = [
|
||||||
|
'suno_music',
|
||||||
|
'suno_lyrics'
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
localModels = getChannelModels(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (inputs.models.length === 0) {
|
||||||
|
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
||||||
|
}
|
||||||
|
setBasicModels(localModels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchModels = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/channel/models`);
|
||||||
|
let localModelOptions = res.data.data.map((model) => ({
|
||||||
|
label: model.id,
|
||||||
|
value: model.id
|
||||||
|
}));
|
||||||
|
setOriginModelOptions(localModelOptions);
|
||||||
|
setFullModels(res.data.data.map((model) => model.id));
|
||||||
|
setBasicModels(
|
||||||
|
res.data.data
|
||||||
|
.filter((model) => {
|
||||||
|
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||||
|
})
|
||||||
|
.map((model) => model.id)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGroups = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/group/`);
|
||||||
|
if (res === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGroupOptions(
|
||||||
|
res.data.data.map((group) => ({
|
||||||
|
label: group,
|
||||||
|
value: group
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
let data = {
|
||||||
|
tag: tag,
|
||||||
|
}
|
||||||
|
if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
|
||||||
|
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
|
||||||
|
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data.model_mapping = inputs.model_mapping
|
||||||
|
}
|
||||||
|
if (inputs.groups.length > 0) {
|
||||||
|
data.groups = inputs.groups.join(',');
|
||||||
|
}
|
||||||
|
if (inputs.models.length > 0) {
|
||||||
|
data.models = inputs.models.join(',');
|
||||||
|
}
|
||||||
|
data.new_tag = inputs.new_tag;
|
||||||
|
// check have any change
|
||||||
|
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
|
||||||
|
showWarning('没有任何修改!');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await submit(data);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (data) => {
|
||||||
|
try {
|
||||||
|
const res = await API.put('/api/channel/tag', data);
|
||||||
|
if (res?.data?.success) {
|
||||||
|
showSuccess('标签更新成功!');
|
||||||
|
refresh();
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let localModelOptions = [...originModelOptions];
|
||||||
|
inputs.models.forEach((model) => {
|
||||||
|
if (!localModelOptions.find((option) => option.label === model)) {
|
||||||
|
localModelOptions.push({
|
||||||
|
label: model,
|
||||||
|
value: model
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setModelOptions(localModelOptions);
|
||||||
|
}, [originModelOptions, inputs.models]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setInputs({
|
||||||
|
...originInputs,
|
||||||
|
tag: tag,
|
||||||
|
new_tag: tag,
|
||||||
|
})
|
||||||
|
fetchModels().then();
|
||||||
|
fetchGroups().then();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SideSheet
|
||||||
|
title="编辑标签"
|
||||||
|
visible={visible}
|
||||||
|
onCancel={handleClose}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={handleClose}>取消</Button>
|
||||||
|
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Banner
|
||||||
|
type={'warning'}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
所有编辑均为覆盖操作,留空则不更改
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
></Banner>
|
||||||
|
</div>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<TextInput
|
||||||
|
label="新标签,留空则不更改"
|
||||||
|
name="newTag"
|
||||||
|
value={inputs.new_tag}
|
||||||
|
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
|
||||||
|
placeholder="请输入新标签"
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>模型,留空则不更改:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
placeholder={'请选择该渠道所支持的模型,留空则不更改'}
|
||||||
|
name="models"
|
||||||
|
required
|
||||||
|
multiple
|
||||||
|
selection
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('models', value);
|
||||||
|
}}
|
||||||
|
value={inputs.models}
|
||||||
|
autoComplete="new-password"
|
||||||
|
optionList={modelOptions}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>分组,留空则不更改:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
|
||||||
|
name="groups"
|
||||||
|
required
|
||||||
|
multiple
|
||||||
|
selection
|
||||||
|
allowAdditions
|
||||||
|
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('groups', value);
|
||||||
|
}}
|
||||||
|
value={inputs.groups}
|
||||||
|
autoComplete="new-password"
|
||||||
|
optionList={groupOptions}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: 10 }}>
|
||||||
|
<Typography.Text strong>模型重定向:</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<TextArea
|
||||||
|
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
|
||||||
|
name="model_mapping"
|
||||||
|
onChange={(value) => {
|
||||||
|
handleInputChange('model_mapping', value);
|
||||||
|
}}
|
||||||
|
autosize
|
||||||
|
value={inputs.model_mapping}
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
|
<Space>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(
|
||||||
|
'model_mapping',
|
||||||
|
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
填入模板
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(
|
||||||
|
'model_mapping',
|
||||||
|
JSON.stringify({}, null, 2)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
清空重定向
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
color: 'rgba(var(--semi-blue-5), 1)',
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
handleInputChange(
|
||||||
|
'model_mapping',
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
不更改
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</Spin>
|
||||||
|
</SideSheet>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditTagModal;
|
||||||
@@ -50,10 +50,17 @@ export default function SettingsMagnification(props) {
|
|||||||
if (res.includes(undefined))
|
if (res.includes(undefined))
|
||||||
return showError('部分保存失败,请重试');
|
return showError('部分保存失败,请重试');
|
||||||
}
|
}
|
||||||
|
for (let i = 0; i < res.length; i++) {
|
||||||
|
if (!res[i].data.success) {
|
||||||
|
return showError(res[i].data.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
showSuccess('保存成功');
|
showSuccess('保存成功');
|
||||||
props.refresh();
|
props.refresh();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(error => {
|
||||||
|
console.error('Unexpected error in Promise.all:', error);
|
||||||
|
|
||||||
showError('保存失败,请重试');
|
showError('保存失败,请重试');
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user