mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-05 10:23:15 +00:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a8216a43 | ||
|
|
5d1087a6a9 | ||
|
|
cf8b30edfa | ||
|
|
56ccb30a94 | ||
|
|
2c79811cb1 | ||
|
|
1e1a22e7b3 | ||
|
|
70b5a7fd88 | ||
|
|
dd293f80ae | ||
|
|
904a1858e4 | ||
|
|
568d4e3f71 | ||
|
|
3eca58093f | ||
|
|
aa82adc5a9 | ||
|
|
195ab1fdd5 | ||
|
|
b9007ced90 | ||
|
|
8c42ea19b9 | ||
|
|
9ce8940d11 | ||
|
|
3aa591785d | ||
|
|
a7b5d684cc | ||
|
|
be556a23cc | ||
|
|
98373f486e | ||
|
|
45bf496183 | ||
|
|
c645bf7eb0 | ||
|
|
b5d273b680 | ||
|
|
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 | ||
|
|
6e6e390f6f | ||
|
|
807385d3d1 | ||
|
|
0ce600ed49 | ||
|
|
e0f19e5ed7 | ||
|
|
3d33079de0 | ||
|
|
1d064a2e88 | ||
|
|
4eae3b2177 |
3
BT.md
Normal file
3
BT.md
Normal file
@@ -0,0 +1,3 @@
|
||||
密钥为环境变量SESSION_SECRET
|
||||
|
||||

|
||||
52
README.md
52
README.md
@@ -16,15 +16,6 @@
|
||||
> 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
||||
> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
> 默认账号root 密码123456
|
||||
> 更新指令:
|
||||
> ```
|
||||
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
> ```
|
||||
|
||||
|
||||
## 主要变更
|
||||
此分叉版本的主要变更如下:
|
||||
|
||||
@@ -68,19 +59,45 @@
|
||||
|
||||
## 比原版One API多出的配置
|
||||
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。
|
||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
|
||||
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。
|
||||
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。
|
||||
- `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`。
|
||||
- `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",为空则使用默认配置(v1beta)
|
||||
- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`,`STRICT`,默认为 `NONE`。
|
||||
## 部署
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
> 默认账号root 密码123456
|
||||
> 更新指令:
|
||||
> ```
|
||||
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
> ```
|
||||
|
||||
### 部署要求
|
||||
- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机)
|
||||
- 远程数据库: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 Compose 部署(推荐)
|
||||
```shell
|
||||
# 下载项目
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# 按需编辑 docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 直接使用 Docker 镜像
|
||||
```shell
|
||||
# 使用 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
|
||||
@@ -88,16 +105,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功能部署
|
||||
```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 +135,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
夜间模式
|
||||
@@ -135,7 +143,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):原版项目
|
||||
|
||||
@@ -229,6 +229,7 @@ const (
|
||||
ChannelTypeSiliconFlow = 40
|
||||
ChannelTypeVertexAi = 41
|
||||
ChannelTypeMistral = 42
|
||||
ChannelTypeDeepSeek = 43
|
||||
|
||||
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
||||
|
||||
@@ -254,7 +255,7 @@ var ChannelBaseURLs = []string{
|
||||
"https://open.bigmodel.cn", // 16
|
||||
"https://dashscope.aliyuncs.com", // 17
|
||||
"", // 18
|
||||
"https://ai.360.cn", // 19
|
||||
"https://api.360.cn", // 19
|
||||
"https://openrouter.ai/api", // 20
|
||||
"https://api.aiproxy.io", // 21
|
||||
"https://fastgpt.run/api/openapi", // 22
|
||||
@@ -278,4 +279,5 @@ var ChannelBaseURLs = []string{
|
||||
"https://api.siliconflow.cn", //40
|
||||
"", //41
|
||||
"https://api.mistral.ai", //42
|
||||
"https://api.deepseek.com", //43
|
||||
}
|
||||
|
||||
@@ -9,15 +9,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func generateMessageID() string {
|
||||
domain := strings.Split(SMTPAccount, "@")[1]
|
||||
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain)
|
||||
func generateMessageID() (string, error) {
|
||||
split := strings.Split(SMTPFrom, "@")
|
||||
if len(split) < 2 {
|
||||
return "", fmt.Errorf("invalid SMTP account")
|
||||
}
|
||||
domain := strings.Split(SMTPFrom, "@")[1]
|
||||
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
|
||||
}
|
||||
|
||||
func SendEmail(subject string, receiver string, content string) error {
|
||||
if SMTPFrom == "" { // for compatibility
|
||||
SMTPFrom = SMTPAccount
|
||||
}
|
||||
id, err2 := generateMessageID()
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
if SMTPServer == "" && SMTPAccount == "" {
|
||||
return fmt.Errorf("SMTP 服务器未配置")
|
||||
}
|
||||
@@ -28,7 +36,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
"Date: %s\r\n"+
|
||||
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
||||
"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)
|
||||
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
||||
to := strings.Split(receiver, ";")
|
||||
@@ -71,11 +79,11 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if isOutlookServer(SMTPAccount) {
|
||||
} else if isOutlookServer(SMTPAccount) || SMTPServer == "smtp.azurecomm.net" {
|
||||
auth = LoginAuth(SMTPAccount, SMTPToken)
|
||||
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
|
||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
|
||||
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var GroupRatio = map[string]float64{
|
||||
@@ -31,3 +32,17 @@ func GetGroupRatio(name string) float64 {
|
||||
}
|
||||
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-0314": 30, //deprecated
|
||||
"gpt-4-32k-0613": 30,
|
||||
"gpt-4-1106-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-0125-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-turbo-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"gpt-4-1106-vision-preview": 5, // $0.01 / 1K tokens
|
||||
"chatgpt-4o-latest": 2.5, // $0.01 / 1K tokens
|
||||
"gpt-4o": 1.25, // $0.01 / 1K tokens
|
||||
"gpt-4o-audio-preview": 1.25, // $0.0015 / 1K tokens
|
||||
"gpt-4o-audio-preview-2024-10-01": 1.25, // $0.0015 / 1K tokens
|
||||
"gpt-4o-2024-08-06": 1.25, // $0.01 / 1K tokens
|
||||
"gpt-4o-2024-05-13": 2.5,
|
||||
"gpt-4-1106-preview": 5, // $10 / 1M tokens
|
||||
"gpt-4-0125-preview": 5, // $10 / 1M tokens
|
||||
"gpt-4-turbo-preview": 5, // $10 / 1M tokens
|
||||
"gpt-4-vision-preview": 5, // $10 / 1M tokens
|
||||
"gpt-4-1106-vision-preview": 5, // $10 / 1M tokens
|
||||
"chatgpt-4o-latest": 2.5, // $5 / 1M tokens
|
||||
"gpt-4o": 1.25, // $2.5 / 1M tokens
|
||||
"gpt-4o-audio-preview": 1.25, // $2.5 / 1M tokens
|
||||
"gpt-4o-audio-preview-2024-10-01": 1.25, // $2.5 / 1M tokens
|
||||
"gpt-4o-2024-05-13": 2.5, // $5 / 1M tokens
|
||||
"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,
|
||||
"o1-preview": 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-responsibility-8k": 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_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 30)
|
||||
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
|
||||
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
||||
|
||||
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
||||
@@ -20,7 +20,7 @@ var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STR
|
||||
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
||||
|
||||
var GeminiModelMap = map[string]string{
|
||||
"gemini-1.0-pro": "v1",
|
||||
"gemini-1.0-pro": "v1",
|
||||
}
|
||||
|
||||
func InitEnv() {
|
||||
|
||||
6
constant/finish_reason.go
Normal file
6
constant/finish_reason.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
FinishReasonStop = "stop"
|
||||
FinishReasonToolCalls = "tool_calls"
|
||||
)
|
||||
@@ -3,12 +3,13 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type OpenAIModel struct {
|
||||
@@ -48,19 +49,41 @@ func GetAllChannels(c *gin.Context) {
|
||||
if pageSize < 0 {
|
||||
pageSize = common.ItemsPerPage
|
||||
}
|
||||
channelData := make([]*model.Channel, 0)
|
||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||
if enableTagMode {
|
||||
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if tag != nil && *tag != "" {
|
||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
|
||||
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{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": channels,
|
||||
"data": channelData,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -144,19 +167,41 @@ func SearchChannels(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
group := c.Query("group")
|
||||
modelKeyword := c.Query("model")
|
||||
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
channels, err := model.SearchChannels(keyword, group, modelKeyword)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
|
||||
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
|
||||
channelData := make([]*model.Channel, 0)
|
||||
if enableTagMode {
|
||||
tags, err := model.SearchTags(keyword, group, modelKeyword, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if tag != nil && *tag != "" {
|
||||
tagChannel, err := model.GetChannelsByTag(*tag, idSort)
|
||||
if err == nil {
|
||||
channelData = append(channelData, tagChannel...)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
channelData = channels
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": channels,
|
||||
"data": channelData,
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -279,6 +324,98 @@ func DeleteDisabledChannel(c *gin.Context) {
|
||||
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 {
|
||||
Ids []int `json:"ids"`
|
||||
}
|
||||
|
||||
@@ -82,6 +82,15 @@ func UpdateOption(c *gin.Context) {
|
||||
})
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,14 +85,13 @@ func RequestEpay(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"})
|
||||
return
|
||||
}
|
||||
|
||||
var payType epay.PurchaseType
|
||||
payType := "wxpay"
|
||||
if req.PaymentMethod == "zfb" {
|
||||
payType = epay.Alipay
|
||||
payType = "alipay"
|
||||
}
|
||||
if req.PaymentMethod == "wx" {
|
||||
req.PaymentMethod = "wxpay"
|
||||
payType = epay.WechatPay
|
||||
payType = "wxpay"
|
||||
}
|
||||
callBackAddress := service.GetCallbackAddress()
|
||||
returnUrl, _ := url.Parse(constant.ServerAddress + "/log")
|
||||
|
||||
@@ -3,7 +3,6 @@ version: '3.4'
|
||||
services:
|
||||
new-api:
|
||||
image: calciumion/new-api:latest
|
||||
# build: .
|
||||
container_name: new-api
|
||||
restart: always
|
||||
command: --log-dir /app/logs
|
||||
@@ -13,16 +12,17 @@ services:
|
||||
- ./data:/data
|
||||
- ./logs:/app/logs
|
||||
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
|
||||
- SESSION_SECRET=random_string # 修改为随机字符串
|
||||
- TZ=Asia/Shanghai
|
||||
# - NODE_TYPE=slave # 多机部署时从节点取消注释该行
|
||||
# - SYNC_FREQUENCY=60 # 需要定期从数据库加载数据时取消注释该行
|
||||
# - FRONTEND_BASE_URL=https://openai.justsong.cn # 多机部署时从节点取消注释该行
|
||||
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
|
||||
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
|
||||
# - 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:
|
||||
- redis
|
||||
- mysql
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "wget -q -O - http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk -F: '{print $2}'" ]
|
||||
interval: 30s
|
||||
@@ -33,3 +33,18 @@ services:
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
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:
|
||||
|
||||
30
go.mod
30
go.mod
@@ -1,24 +1,23 @@
|
||||
module one-api
|
||||
|
||||
// +heroku goVersion go1.18
|
||||
go 1.21
|
||||
|
||||
toolchain go1.22.4
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/Calcium-Ion/go-epay v0.0.2
|
||||
github.com/Calcium-Ion/go-epay v0.0.4
|
||||
github.com/andybalholm/brotli v1.1.1
|
||||
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/credentials v1.17.11
|
||||
github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4
|
||||
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.7.2
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-contrib/static v0.0.1
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-playground/validator/v10 v10.19.0
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -29,8 +28,8 @@ require (
|
||||
github.com/pkoukk/tiktoken-go v0.1.7
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
golang.org/x/crypto v0.26.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/image v0.23.0
|
||||
gorm.io/driver/mysql v1.4.3
|
||||
gorm.io/driver/postgres v1.5.2
|
||||
gorm.io/driver/sqlite v1.4.3
|
||||
@@ -61,9 +60,9 @@ require (
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -74,19 +73,18 @@ require (
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.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.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
55
go.sum
55
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/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A=
|
||||
github.com/Calcium-Ion/go-epay v0.0.4/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/go.mod h1:4yg+jNTYlDEzBjhGS96v+zjyA3lfXlFd5CiTLIkPBLI=
|
||||
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 h1:HblK3eJHq54yET63qPCTJnks3loDse5xRmmqHgHzwoI=
|
||||
@@ -43,8 +45,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
@@ -72,8 +74,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||
@@ -101,12 +103,12 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
|
||||
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -157,8 +159,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -176,6 +178,7 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -183,7 +186,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
@@ -196,25 +199,27 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
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/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/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/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
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/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -231,8 +236,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
|
||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
|
||||
2
main.go
2
main.go
@@ -33,7 +33,7 @@ var indexPage []byte
|
||||
func main() {
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
common.SysLog("Can't load .env file")
|
||||
common.SysError("failed to load .env file: " + err.Error())
|
||||
}
|
||||
|
||||
common.SetupLogger()
|
||||
|
||||
@@ -212,6 +212,7 @@ func TokenAuth() func(c *gin.Context) {
|
||||
}
|
||||
c.Set("id", token.UserId)
|
||||
c.Set("token_id", token.Id)
|
||||
c.Set("token_key", token.Key)
|
||||
c.Set("token_name", token.Name)
|
||||
c.Set("token_unlimited_quota", 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 {
|
||||
Group string `json:"group" 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"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
|
||||
Weight uint `json:"weight" gorm:"default:0;index"`
|
||||
Group string `json:"group" 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"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
|
||||
Weight uint `json:"weight" gorm:"default:0;index"`
|
||||
Tag *string `json:"tag" gorm:"index"`
|
||||
}
|
||||
|
||||
func GetGroupModels(group string) []string {
|
||||
@@ -149,6 +150,7 @@ func (channel *Channel) AddAbilities() error {
|
||||
Enabled: channel.Status == common.ChannelStatusEnabled,
|
||||
Priority: channel.Priority,
|
||||
Weight: uint(channel.GetWeight()),
|
||||
Tag: channel.Tag,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
var channelIds []int
|
||||
count := 0
|
||||
|
||||
163
model/channel.go
163
model/channel.go
@@ -2,9 +2,10 @@ package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"one-api/common"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
@@ -32,6 +33,7 @@ type Channel struct {
|
||||
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
|
||||
AutoBan *int `json:"auto_ban" gorm:"default:1"`
|
||||
OtherInfo string `json:"other_info"`
|
||||
Tag *string `json:"tag" gorm:"index"`
|
||||
}
|
||||
|
||||
func (channel *Channel) GetModels() []string {
|
||||
@@ -61,6 +63,17 @@ func (channel *Channel) SetOtherInfo(otherInfo map[string]interface{}) {
|
||||
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 {
|
||||
if channel.AutoBan == nil {
|
||||
return false
|
||||
@@ -87,7 +100,17 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
|
||||
func GetChannelsByTag(tag string, idSort bool) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
order := "priority desc"
|
||||
if idSort {
|
||||
order = "id desc"
|
||||
}
|
||||
err := DB.Where("tag = ?", tag).Order(order).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
keyCol := "`key`"
|
||||
groupCol := "`group`"
|
||||
@@ -100,6 +123,11 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
|
||||
modelsCol = `"models"`
|
||||
}
|
||||
|
||||
order := "priority desc"
|
||||
if idSort {
|
||||
order = "id desc"
|
||||
}
|
||||
|
||||
// 构造基础查询
|
||||
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
|
||||
|
||||
@@ -122,7 +150,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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -288,6 +316,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, false)
|
||||
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) {
|
||||
if common.BatchUpdateEnabled {
|
||||
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
|
||||
@@ -312,3 +408,64 @@ func DeleteDisabledChannel() (int64, error) {
|
||||
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
|
||||
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
|
||||
}
|
||||
|
||||
func SearchTags(keyword string, group string, model string, idSort bool) ([]*string, error) {
|
||||
var tags []*string
|
||||
keyCol := "`key`"
|
||||
groupCol := "`group`"
|
||||
modelsCol := "`models`"
|
||||
|
||||
// 如果是 PostgreSQL,使用双引号
|
||||
if common.UsingPostgreSQL {
|
||||
keyCol = `"key"`
|
||||
groupCol = `"group"`
|
||||
modelsCol = `"models"`
|
||||
}
|
||||
|
||||
order := "priority desc"
|
||||
if idSort {
|
||||
order = "id desc"
|
||||
}
|
||||
|
||||
// 构造基础查询
|
||||
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
|
||||
|
||||
// 构造WHERE子句
|
||||
var whereClause string
|
||||
var args []interface{}
|
||||
if group != "" && group != "null" {
|
||||
var groupCondition string
|
||||
if common.UsingMySQL {
|
||||
groupCondition = `CONCAT(',', ` + groupCol + `, ',') LIKE ?`
|
||||
} else {
|
||||
// sqlite, PostgreSQL
|
||||
groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
|
||||
}
|
||||
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
|
||||
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%", "%,"+group+",%")
|
||||
} else {
|
||||
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
|
||||
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
|
||||
}
|
||||
|
||||
subQuery := baseQuery.Where(whereClause, args...).
|
||||
Select("tag").
|
||||
Where("tag != ''").
|
||||
Order(order)
|
||||
|
||||
err := DB.Table("(?) as sub", subQuery).
|
||||
Select("DISTINCT tag").
|
||||
Find(&tags).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ var ModelList = []string{
|
||||
"360gpt-turbo",
|
||||
"360gpt-turbo-responsibility-8k",
|
||||
"360gpt-pro",
|
||||
"360gpt2-pro",
|
||||
"360GPT_S2_V9",
|
||||
"embedding-bert-512-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-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-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
}
|
||||
|
||||
var ChannelName = "aws"
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -75,7 +75,7 @@ func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *dto.Cha
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
choice.Delta.SetContentString(baiduResponse.Result)
|
||||
if baiduResponse.IsEnd {
|
||||
choice.FinishReason = &relaycommon.StopFinishReason
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
}
|
||||
response := dto.ChatCompletionsStreamResponse{
|
||||
Id: baiduResponse.Id,
|
||||
|
||||
71
relay/channel/deepseek/adaptor.go
Normal file
71
relay/channel/deepseek/adaptor.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package deepseek
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel"
|
||||
"one-api/relay/channel/openai"
|
||||
relaycommon "one-api/relay/common"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return fmt.Sprintf("%s/chat/completions", info.BaseUrl), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
channel.SetupApiRequestHeader(info, c, req)
|
||||
req.Set("Authorization", "Bearer "+info.ApiKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
|
||||
if info.IsStream {
|
||||
err, usage = openai.OaiStreamHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetModelList() []string {
|
||||
return ModelList
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetChannelName() string {
|
||||
return ChannelName
|
||||
}
|
||||
7
relay/channel/deepseek/constants.go
Normal file
7
relay/channel/deepseek/constants.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package deepseek
|
||||
|
||||
var ModelList = []string{
|
||||
"deepseek-chat", "deepseek-coder",
|
||||
}
|
||||
|
||||
var ChannelName = "deepseek"
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
@@ -186,10 +187,11 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
},
|
||||
FinishReason: relaycommon.StopFinishReason,
|
||||
FinishReason: constant.FinishReasonStop,
|
||||
}
|
||||
if len(candidate.Content.Parts) > 0 {
|
||||
if candidate.Content.Parts[0].FunctionCall != nil {
|
||||
choice.FinishReason = constant.FinishReasonToolCalls
|
||||
choice.Message.ToolCalls = getToolCalls(&candidate)
|
||||
} else {
|
||||
choice.Message.SetStringContent(candidate.Content.Parts[0].Text)
|
||||
@@ -262,7 +264,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
}
|
||||
}
|
||||
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, relaycommon.StopFinishReason)
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
|
||||
service.ObjectData(c, response)
|
||||
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
|
||||
@@ -32,11 +32,15 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
if info.RelayMode == constant.RelayModeRealtime {
|
||||
// trim https
|
||||
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
||||
baseUrl = strings.TrimPrefix(baseUrl, "http://")
|
||||
baseUrl = "wss://" + baseUrl
|
||||
info.BaseUrl = baseUrl
|
||||
if strings.HasPrefix(info.BaseUrl, "https://") {
|
||||
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
||||
baseUrl = "wss://" + 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 {
|
||||
case common.ChannelTypeAzure:
|
||||
@@ -132,6 +136,19 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
|
||||
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")
|
||||
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-vision-preview",
|
||||
"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",
|
||||
"o1-preview", "o1-preview-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
|
||||
}
|
||||
}
|
||||
for _, choice := range lastStreamResponse.Choices {
|
||||
if choice.FinishReason != nil {
|
||||
shouldSendLastResp = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if shouldSendLastResp {
|
||||
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) {
|
||||
var audioResp dto.AudioResponse
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != 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 {
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *dto.ChatCompleti
|
||||
if len(palmResponse.Candidates) > 0 {
|
||||
choice.Delta.SetContentString(palmResponse.Candidates[0].Content)
|
||||
}
|
||||
choice.FinishReason = &relaycommon.StopFinishReason
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
var response dto.ChatCompletionsStreamResponse
|
||||
response.Object = "chat.completion.chunk"
|
||||
response.Model = "palm2"
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -81,7 +81,7 @@ func streamResponseTencent2OpenAI(TencentResponse *TencentChatResponse) *dto.Cha
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
choice.Delta.SetContentString(TencentResponse.Choices[0].Delta.Content)
|
||||
if TencentResponse.Choices[0].FinishReason == "stop" {
|
||||
choice.FinishReason = &relaycommon.StopFinishReason
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
}
|
||||
response.Choices = append(response.Choices, choice)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -67,7 +67,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *dto.OpenAITextResponse
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
},
|
||||
FinishReason: relaycommon.StopFinishReason,
|
||||
FinishReason: constant.FinishReasonStop,
|
||||
}
|
||||
fullTextResponse := dto.OpenAITextResponse{
|
||||
Object: "chat.completion",
|
||||
@@ -89,7 +89,7 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *dto.ChatCo
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
choice.Delta.SetContentString(xunfeiResponse.Payload.Choices.Text[0].Content)
|
||||
if xunfeiResponse.Payload.Choices.Status == 2 {
|
||||
choice.FinishReason = &relaycommon.StopFinishReason
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
}
|
||||
response := dto.ChatCompletionsStreamResponse{
|
||||
Object: "chat.completion.chunk",
|
||||
@@ -245,7 +245,7 @@ func xunfeiMakeRequest(textRequest dto.GeneralOpenAIRequest, domain, authUrl, ap
|
||||
func apiVersion2domain(apiVersion string) string {
|
||||
switch apiVersion {
|
||||
case "v1.1":
|
||||
return "general"
|
||||
return "lite"
|
||||
case "v2.1":
|
||||
return "generalv2"
|
||||
case "v3.1":
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -139,7 +139,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *dto.ChatCompletionsStream
|
||||
func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*dto.ChatCompletionsStreamResponse, *dto.Usage) {
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
choice.Delta.SetContentString("")
|
||||
choice.FinishReason = &relaycommon.StopFinishReason
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
response := dto.ChatCompletionsStreamResponse{
|
||||
Id: zhipuResponse.RequestId,
|
||||
Object: "chat.completion.chunk",
|
||||
|
||||
@@ -14,6 +14,7 @@ type RelayInfo struct {
|
||||
ChannelType int
|
||||
ChannelId int
|
||||
TokenId int
|
||||
TokenKey string
|
||||
UserId int
|
||||
Group string
|
||||
TokenUnlimited bool
|
||||
@@ -58,6 +59,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
channelId := c.GetInt("channel_id")
|
||||
|
||||
tokenId := c.GetInt("token_id")
|
||||
tokenKey := c.GetString("token_key")
|
||||
userId := c.GetInt("id")
|
||||
group := c.GetString("group")
|
||||
tokenUnlimited := c.GetBool("token_unlimited_quota")
|
||||
@@ -73,6 +75,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
ChannelType: channelType,
|
||||
ChannelId: channelId,
|
||||
TokenId: tokenId,
|
||||
TokenKey: tokenKey,
|
||||
UserId: userId,
|
||||
Group: group,
|
||||
TokenUnlimited: tokenUnlimited,
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var StopFinishReason = "stop"
|
||||
|
||||
func GetFullRequestURL(baseURL string, requestURL string, channelType int) string {
|
||||
fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
APITypeSiliconFlow
|
||||
APITypeVertexAi
|
||||
APITypeMistral
|
||||
APITypeDeepSeek
|
||||
|
||||
APITypeDummy // this one is only for count, do not add any channel after this
|
||||
)
|
||||
@@ -75,6 +76,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
|
||||
apiType = APITypeVertexAi
|
||||
case common.ChannelTypeMistral:
|
||||
apiType = APITypeMistral
|
||||
case common.ChannelTypeDeepSeek:
|
||||
apiType = APITypeDeepSeek
|
||||
}
|
||||
if apiType == -1 {
|
||||
return APITypeOpenAI, false
|
||||
|
||||
@@ -33,12 +33,19 @@ func getAndValidAudioRequest(c *gin.Context, info *relaycommon.RelayInfo) (*dto.
|
||||
}
|
||||
}
|
||||
default:
|
||||
if audioRequest.Model == "" {
|
||||
audioRequest.Model = c.PostForm("model")
|
||||
err = c.Request.ParseForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
formData := c.Request.PostForm
|
||||
if audioRequest.Model == "" {
|
||||
audioRequest.Model = formData.Get("model")
|
||||
}
|
||||
|
||||
if audioRequest.Model == "" {
|
||||
return nil, errors.New("model is required")
|
||||
}
|
||||
audioRequest.ResponseFormat = formData.Get("response_format")
|
||||
if audioRequest.ResponseFormat == "" {
|
||||
audioRequest.ResponseFormat = "json"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ package relay
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bytedance/sonic"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
@@ -20,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
|
||||
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")
|
||||
}
|
||||
if textRequest.Model == "" {
|
||||
@@ -48,12 +48,12 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
|
||||
return nil, errors.New("field prompt is required")
|
||||
}
|
||||
case relayconstant.RelayModeChatCompletions:
|
||||
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
|
||||
if len(textRequest.Messages) == 0 {
|
||||
return nil, errors.New("field messages is required")
|
||||
}
|
||||
case relayconstant.RelayModeEmbeddings:
|
||||
case relayconstant.RelayModeModerations:
|
||||
if textRequest.Input == "" || textRequest.Input == nil {
|
||||
if textRequest.Input == nil || textRequest.Input == "" {
|
||||
return nil, errors.New("field input is required")
|
||||
}
|
||||
case relayconstant.RelayModeEdits:
|
||||
@@ -264,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)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -298,13 +298,14 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
|
||||
|
||||
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
|
||||
if preConsumedQuota != 0 {
|
||||
go func(ctx context.Context) {
|
||||
// return pre-consumed quota
|
||||
err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
|
||||
go func() {
|
||||
relayInfoCopy := *relayInfo
|
||||
|
||||
err := model.PostConsumeTokenQuota(&relayInfoCopy, userQuota, -preConsumedQuota, 0, false)
|
||||
if err != nil {
|
||||
common.SysError("error return pre-consumed quota: " + err.Error())
|
||||
}
|
||||
}(c)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"one-api/relay/channel/claude"
|
||||
"one-api/relay/channel/cloudflare"
|
||||
"one-api/relay/channel/cohere"
|
||||
"one-api/relay/channel/deepseek"
|
||||
"one-api/relay/channel/dify"
|
||||
"one-api/relay/channel/gemini"
|
||||
"one-api/relay/channel/jina"
|
||||
@@ -71,6 +72,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
|
||||
return &vertex.Adaptor{}
|
||||
case constant.APITypeMistral:
|
||||
return &mistral.Adaptor{}
|
||||
case constant.APITypeDeepSeek:
|
||||
return &deepseek.Adaptor{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ func SetApiRouter(router *gin.Engine) {
|
||||
channelRoute.POST("/", controller.AddChannel)
|
||||
channelRoute.PUT("/", controller.UpdateChannel)
|
||||
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.POST("/batch", controller.DeleteChannelBatch)
|
||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
func SetRelayRouter(router *gin.Engine) {
|
||||
router.Use(middleware.CORS())
|
||||
router.Use(middleware.DecompressRequestMiddleware())
|
||||
// https://platform.openai.com/docs/api-reference/introduction
|
||||
modelsRouter := router.Group("/v1/models")
|
||||
modelsRouter.Use(middleware.TokenAuth())
|
||||
|
||||
@@ -22,7 +22,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
return err
|
||||
}
|
||||
|
||||
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.ApiKey, "sk-"))
|
||||
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.TokenKey, "sk-"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
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))
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,21 @@ const defaultMessage = [
|
||||
}
|
||||
];
|
||||
|
||||
const roleInfo = {
|
||||
user: {
|
||||
name: 'User',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
|
||||
},
|
||||
assistant: {
|
||||
name: 'Assistant',
|
||||
avatar: 'logo.png'
|
||||
},
|
||||
system: {
|
||||
name: 'System',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
|
||||
}
|
||||
}
|
||||
|
||||
let id = 4;
|
||||
function getId() {
|
||||
return `${id++}`
|
||||
@@ -269,6 +284,7 @@ const Playground = () => {
|
||||
name='model'
|
||||
required
|
||||
selection
|
||||
searchPosition='dropdown'
|
||||
filter
|
||||
onChange={(value) => {
|
||||
handleInputChange('model', value);
|
||||
@@ -330,6 +346,7 @@ const Playground = () => {
|
||||
return <div></div>
|
||||
}
|
||||
}}
|
||||
roleConfig={roleInfo}
|
||||
style={commonOuterStyle}
|
||||
chats={message}
|
||||
onMessageSend={onMessageSend}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
@@ -11,6 +11,7 @@ import LinuxDoIcon from './LinuxDoIcon.js';
|
||||
import WeChatIcon from './WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -22,6 +23,7 @@ const RegisterForm = () => {
|
||||
});
|
||||
const { username, password, password2 } = inputs;
|
||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
@@ -133,6 +135,38 @@ const RegisterForm = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onTelegramLoginClicked = async (response) => {
|
||||
const fields = [
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'username',
|
||||
'photo_url',
|
||||
'auth_date',
|
||||
'hash',
|
||||
'lang',
|
||||
];
|
||||
const params = {};
|
||||
fields.forEach((field) => {
|
||||
if (response[field]) {
|
||||
params[field] = response[field];
|
||||
}
|
||||
});
|
||||
const res = await API.get(`/api/oauth/telegram/login`, { params });
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
showSuccess('登录成功!');
|
||||
setUserData(data);
|
||||
updateAPI();
|
||||
navigate('/');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Layout>
|
||||
|
||||
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;
|
||||
@@ -44,13 +44,6 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'teal',
|
||||
label: 'Azure OpenAI'
|
||||
},
|
||||
{
|
||||
key: 24,
|
||||
text: 'Google Gemini',
|
||||
value: 24,
|
||||
color: 'orange',
|
||||
label: 'Google Gemini'
|
||||
},
|
||||
{
|
||||
key: 34,
|
||||
text: 'Cohere',
|
||||
@@ -58,6 +51,8 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: 'Cohere'
|
||||
},
|
||||
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
|
||||
{ key: 43, text: 'DeepSeek', value: 43, color: 'blue', label: 'DeepSeek' },
|
||||
{
|
||||
key: 15,
|
||||
text: '百度文心千帆',
|
||||
@@ -93,6 +88,13 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: '智谱 GLM-4V'
|
||||
},
|
||||
{
|
||||
key: 24,
|
||||
text: 'Google Gemini',
|
||||
value: 24,
|
||||
color: 'orange',
|
||||
label: 'Google Gemini'
|
||||
},
|
||||
{
|
||||
key: 11,
|
||||
text: 'Google PaLM2',
|
||||
@@ -100,7 +102,6 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'orange',
|
||||
label: 'Google PaLM2'
|
||||
},
|
||||
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
|
||||
{ key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
|
||||
|
||||
@@ -6,7 +6,8 @@ export let API = axios.create({
|
||||
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||
: '',
|
||||
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
|
||||
: '',
|
||||
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) {
|
||||
if (num === undefined)
|
||||
return '';
|
||||
num = num.toFixed(2);
|
||||
if (num >= 100000) {
|
||||
// Convert number to string to manipulate it
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
verifyJSON
|
||||
} from '../../helpers';
|
||||
import { CHANNEL_OPTIONS } from '../../constants';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
@@ -21,28 +21,26 @@ import {
|
||||
Select,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Banner
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Divider } from 'semantic-ui-react';
|
||||
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
||||
'gpt-4-0314': 'gpt-4',
|
||||
'gpt-4-32k-0314': 'gpt-4-32k',
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||
};
|
||||
|
||||
const STATUS_CODE_MAPPING_EXAMPLE = {
|
||||
400: '500',
|
||||
400: '500'
|
||||
};
|
||||
|
||||
const REGION_EXAMPLE = {
|
||||
"default": "us-central1",
|
||||
"claude-3-5-sonnet-20240620": "europe-west1"
|
||||
}
|
||||
'default': 'us-central1',
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1'
|
||||
};
|
||||
|
||||
const fetchButtonTips = "1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出"
|
||||
const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出';
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
||||
@@ -84,6 +82,9 @@ const EditChannel = (props) => {
|
||||
auto_ban: 1,
|
||||
test_model: '',
|
||||
groups: ['default'],
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
tag: ''
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
const [autoBan, setAutoBan] = useState(true);
|
||||
@@ -108,7 +109,7 @@ const EditChannel = (props) => {
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_uploads',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 5:
|
||||
@@ -128,13 +129,13 @@ const EditChannel = (props) => {
|
||||
'mj_high_variation',
|
||||
'mj_low_variation',
|
||||
'mj_pan',
|
||||
'mj_uploads',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 36:
|
||||
localModels = [
|
||||
'suno_music',
|
||||
'suno_lyrics',
|
||||
'suno_lyrics'
|
||||
];
|
||||
break;
|
||||
default:
|
||||
@@ -171,7 +172,7 @@ const EditChannel = (props) => {
|
||||
data.model_mapping = JSON.stringify(
|
||||
JSON.parse(data.model_mapping),
|
||||
null,
|
||||
2,
|
||||
2
|
||||
);
|
||||
}
|
||||
setInputs(data);
|
||||
@@ -190,70 +191,69 @@ const EditChannel = (props) => {
|
||||
|
||||
|
||||
const fetchUpstreamModelList = async (name) => {
|
||||
if (inputs["type"] !== 1) {
|
||||
showError("仅支持 OpenAI 接口格式")
|
||||
if (inputs['type'] !== 1) {
|
||||
showError('仅支持 OpenAI 接口格式');
|
||||
return;
|
||||
}
|
||||
setLoading(true)
|
||||
const models = inputs["models"] || []
|
||||
setLoading(true);
|
||||
const models = inputs['models'] || [];
|
||||
let err = false;
|
||||
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) {
|
||||
models.push(...res.data.data)
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
err = true
|
||||
err = true;
|
||||
}
|
||||
} else {
|
||||
if (!inputs?.["key"]) {
|
||||
showError("请填写密钥")
|
||||
err = true
|
||||
if (!inputs?.['key']) {
|
||||
showError('请填写密钥');
|
||||
err = true;
|
||||
} else {
|
||||
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 key = inputs["key"];
|
||||
const key = inputs['key'];
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`
|
||||
}
|
||||
})
|
||||
});
|
||||
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 {
|
||||
err = true
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
err = true
|
||||
} catch (error) {
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!err) {
|
||||
handleInputChange(name, Array.from(new Set(models)));
|
||||
showSuccess("获取模型列表成功");
|
||||
showSuccess('获取模型列表成功');
|
||||
} else {
|
||||
showError('获取模型列表失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
value: model.id
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
setFullModels(res.data.data.map((model) => model.id));
|
||||
setBasicModels(
|
||||
res.data.data
|
||||
.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) {
|
||||
showError(error.message);
|
||||
@@ -269,8 +269,8 @@ const EditChannel = (props) => {
|
||||
setGroupOptions(
|
||||
res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group,
|
||||
})),
|
||||
value: group
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -280,10 +280,10 @@ const EditChannel = (props) => {
|
||||
useEffect(() => {
|
||||
let localModelOptions = [...originModelOptions];
|
||||
inputs.models.forEach((model) => {
|
||||
if (!localModelOptions.find((option) => option.key === model)) {
|
||||
if (!localModelOptions.find((option) => option.label === model)) {
|
||||
localModelOptions.push({
|
||||
label: model,
|
||||
value: model,
|
||||
value: model
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -320,7 +320,7 @@ const EditChannel = (props) => {
|
||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||
localInputs.base_url = localInputs.base_url.slice(
|
||||
0,
|
||||
localInputs.base_url.length - 1,
|
||||
localInputs.base_url.length - 1
|
||||
);
|
||||
}
|
||||
if (localInputs.type === 3 && localInputs.other === '') {
|
||||
@@ -341,7 +341,7 @@ const EditChannel = (props) => {
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...localInputs,
|
||||
id: parseInt(channelId),
|
||||
id: parseInt(channelId)
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, localInputs);
|
||||
@@ -378,7 +378,7 @@ const EditChannel = (props) => {
|
||||
// 添加到下拉选项
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
value: model
|
||||
});
|
||||
} else if (model) {
|
||||
showError('某些模型已存在!');
|
||||
@@ -409,11 +409,11 @@ const EditChannel = (props) => {
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>
|
||||
<Button theme="solid" size={'large'} onClick={submit}>
|
||||
提交
|
||||
</Button>
|
||||
<Button
|
||||
theme='solid'
|
||||
theme="solid"
|
||||
size={'large'}
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
@@ -432,7 +432,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>类型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
name='type'
|
||||
name="type"
|
||||
required
|
||||
optionList={CHANNEL_OPTIONS}
|
||||
value={inputs.type}
|
||||
@@ -450,8 +450,8 @@ const EditChannel = (props) => {
|
||||
,因为 One API 会把请求体中的 model
|
||||
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||
<a
|
||||
target='_blank'
|
||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
|
||||
target="_blank"
|
||||
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
|
||||
>
|
||||
图片演示
|
||||
</a>
|
||||
@@ -466,8 +466,8 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='AZURE_OPENAI_ENDPOINT'
|
||||
name='azure_base_url'
|
||||
label="AZURE_OPENAI_ENDPOINT"
|
||||
name="azure_base_url"
|
||||
placeholder={
|
||||
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
||||
}
|
||||
@@ -475,14 +475,14 @@ const EditChannel = (props) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='默认 API 版本'
|
||||
name='azure_other'
|
||||
label="默认 API 版本"
|
||||
name="azure_other"
|
||||
placeholder={
|
||||
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||
}
|
||||
@@ -490,7 +490,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -512,7 +512,7 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
|
||||
}
|
||||
@@ -520,49 +520,84 @@ const EditChannel = (props) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
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 && (
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>
|
||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
placeholder={
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
<Input
|
||||
required
|
||||
name='name'
|
||||
required
|
||||
name="name"
|
||||
placeholder={'请为渠道命名'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('name', value);
|
||||
}}
|
||||
value={inputs.name}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组'}
|
||||
name='groups'
|
||||
name="groups"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -572,7 +607,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
{inputs.type === 18 && (
|
||||
@@ -581,7 +616,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>模型版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
|
||||
}
|
||||
@@ -589,7 +624,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -599,7 +634,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>部署地区:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
'{\n' +
|
||||
@@ -612,18 +647,18 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'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>
|
||||
</div>
|
||||
<Input
|
||||
label='知识库 ID'
|
||||
name='other'
|
||||
label="知识库 ID"
|
||||
name="other"
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -654,7 +689,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>Account ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
|
||||
}
|
||||
@@ -662,7 +697,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -671,7 +706,7 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
name="models"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -679,13 +714,13 @@ const EditChannel = (props) => {
|
||||
handleInputChange('models', value);
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||
<Space>
|
||||
<Button
|
||||
type='primary'
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleInputChange('models', basicModels);
|
||||
}}
|
||||
@@ -693,7 +728,7 @@ const EditChannel = (props) => {
|
||||
填入相关模型
|
||||
</Button>
|
||||
<Button
|
||||
type='secondary'
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
handleInputChange('models', fullModels);
|
||||
}}
|
||||
@@ -702,7 +737,7 @@ const EditChannel = (props) => {
|
||||
</Button>
|
||||
<Tooltip content={fetchButtonTips}>
|
||||
<Button
|
||||
type='tertiary'
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
fetchUpstreamModelList('models');
|
||||
}}
|
||||
@@ -711,7 +746,7 @@ const EditChannel = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type='warning'
|
||||
type="warning"
|
||||
onClick={() => {
|
||||
handleInputChange('models', []);
|
||||
}}
|
||||
@@ -721,11 +756,11 @@ const EditChannel = (props) => {
|
||||
</Space>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type='primary' onClick={addCustomModels}>
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
填入
|
||||
</Button>
|
||||
}
|
||||
placeholder='输入自定义模型名称'
|
||||
placeholder="输入自定义模型名称"
|
||||
value={customModel}
|
||||
onChange={(value) => {
|
||||
setCustomModel(value.trim());
|
||||
@@ -737,24 +772,24 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name='model_mapping'
|
||||
name="model_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.model_mapping}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -765,8 +800,8 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
{batch ? (
|
||||
<TextArea
|
||||
label='密钥'
|
||||
name='key'
|
||||
label="密钥"
|
||||
name="key"
|
||||
required
|
||||
placeholder={'请输入密钥,一行一个'}
|
||||
onChange={(value) => {
|
||||
@@ -774,14 +809,14 @@ const EditChannel = (props) => {
|
||||
}}
|
||||
value={inputs.key}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<TextArea
|
||||
label='鉴权json'
|
||||
name='key'
|
||||
label="鉴权json"
|
||||
name="key"
|
||||
required
|
||||
placeholder={'{\n' +
|
||||
' "type": "service_account",\n' +
|
||||
@@ -801,23 +836,36 @@ const EditChannel = (props) => {
|
||||
}}
|
||||
autosize={{ minRows: 10 }}
|
||||
value={inputs.key}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label='密钥'
|
||||
name='key'
|
||||
label="密钥"
|
||||
name="key"
|
||||
required
|
||||
placeholder={type2secretPrompt(inputs.type)}
|
||||
onChange={(value) => {
|
||||
handleInputChange('key', value);
|
||||
}}
|
||||
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 && (
|
||||
<>
|
||||
@@ -825,9 +873,9 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>组织:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='组织,可选,不填则为默认组织'
|
||||
name='openai_organization'
|
||||
placeholder='请输入组织org-xxx'
|
||||
label="组织,可选,不填则为默认组织"
|
||||
name="openai_organization"
|
||||
placeholder="请输入组织org-xxx"
|
||||
onChange={(value) => {
|
||||
handleInputChange('openai_organization', value);
|
||||
}}
|
||||
@@ -839,8 +887,8 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>默认测试模型:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='test_model'
|
||||
placeholder='不填则为模型列表第一个'
|
||||
name="test_model"
|
||||
placeholder="不填则为模型列表第一个"
|
||||
onChange={(value) => {
|
||||
handleInputChange('test_model', value);
|
||||
}}
|
||||
@@ -849,7 +897,7 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
name='auto_ban'
|
||||
name="auto_ban"
|
||||
checked={autoBan}
|
||||
onChange={() => {
|
||||
setAutoBan(!autoBan);
|
||||
@@ -861,55 +909,6 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</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 }}>
|
||||
<Typography.Text strong>
|
||||
状态码复写(仅影响本地判断,不修改返回到上游的状态码):
|
||||
@@ -917,43 +916,84 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name='status_code_mapping'
|
||||
name="status_code_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('status_code_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.status_code_mapping}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'status_code_mapping',
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
</Typography.Text>
|
||||
{/*<div style={{ marginTop: 10 }}>*/}
|
||||
{/* <Typography.Text strong>*/}
|
||||
{/* 最大请求token(0表示不限制):*/}
|
||||
{/* </Typography.Text>*/}
|
||||
{/*</div>*/}
|
||||
{/*<Input*/}
|
||||
{/* label='最大请求token'*/}
|
||||
{/* name='max_input_tokens'*/}
|
||||
{/* placeholder='默认为0,表示不限制'*/}
|
||||
{/* onChange={(value) => {*/}
|
||||
{/* handleInputChange('max_input_tokens', value);*/}
|
||||
{/* }}*/}
|
||||
{/* value={inputs.max_input_tokens}*/}
|
||||
{/*/>*/}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道标签
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道标签"
|
||||
name="tag"
|
||||
placeholder={'渠道标签'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('tag', value);
|
||||
}}
|
||||
value={inputs.tag}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道优先级
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道优先级"
|
||||
name="priority"
|
||||
placeholder={'渠道优先级'}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
handleInputChange('priority', value);
|
||||
} else {
|
||||
handleInputChange('priority', number);
|
||||
}
|
||||
}}
|
||||
value={inputs.priority}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道权重
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道权重"
|
||||
name="weight"
|
||||
placeholder={'渠道权重'}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
handleInputChange('weight', value);
|
||||
} else {
|
||||
handleInputChange('weight', number);
|
||||
}
|
||||
}}
|
||||
value={inputs.weight}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Spin>
|
||||
</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))
|
||||
return showError('部分保存失败,请重试');
|
||||
}
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message)
|
||||
}
|
||||
}
|
||||
showSuccess('保存成功');
|
||||
props.refresh();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch(error => {
|
||||
console.error('Unexpected error in Promise.all:', error);
|
||||
|
||||
showError('保存失败,请重试');
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
Reference in New Issue
Block a user