mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-11-04 15:53:42 +08:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v0.5.6-alp
			...
			v0.5.7-alp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b4b4acc288 | ||
| 
						 | 
					d663de3e3a | ||
| 
						 | 
					a85ecace2e | ||
| 
						 | 
					fbdea91ea1 | ||
| 
						 | 
					8d34b7a77e | ||
| 
						 | 
					cbd62011b8 | ||
| 
						 | 
					4701897e2e | ||
| 
						 | 
					0f6c132a80 | ||
| 
						 | 
					3cac45dc85 | 
							
								
								
									
										35
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								README.md
									
									
									
									
									
								
							@@ -72,6 +72,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
 | 
			
		||||
   + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html)
 | 
			
		||||
   + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
 | 
			
		||||
   + [x] [360 智脑](https://ai.360.cn)
 | 
			
		||||
   + [x] [腾讯混元大模型](https://cloud.tencent.com/document/product/1729)
 | 
			
		||||
2. 支持配置镜像以及众多第三方代理服务:
 | 
			
		||||
   + [x] [OpenAI-SB](https://openai-sb.com)
 | 
			
		||||
   + [x] [CloseAI](https://console.closeai-asia.com/r/2412)
 | 
			
		||||
@@ -106,11 +107,17 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 
 | 
			
		||||
 | 
			
		||||
## 部署
 | 
			
		||||
### 基于 Docker 进行部署
 | 
			
		||||
部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
 | 
			
		||||
```shell
 | 
			
		||||
# 使用 SQLite 的部署命令:
 | 
			
		||||
docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
 | 
			
		||||
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数,不清楚如何修改请参见下面环境变量一节。
 | 
			
		||||
# 例如:
 | 
			
		||||
docker run --name one-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
 | 
			
		||||
 | 
			
		||||
数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
 | 
			
		||||
数据和日志将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
 | 
			
		||||
 | 
			
		||||
如果启动失败,请添加 `--privileged=true`,具体参考 https://github.com/songquanpeng/one-api/issues/482 。
 | 
			
		||||
 | 
			
		||||
@@ -239,7 +246,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 | 
			
		||||
<summary><strong>部署到 Zeabur</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
 | 
			
		||||
> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用
 | 
			
		||||
 | 
			
		||||
1. 首先 fork 一份代码。
 | 
			
		||||
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
 | 
			
		||||
@@ -254,6 +261,17 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
<details>
 | 
			
		||||
<summary><strong>部署到 Render</strong></summary>
 | 
			
		||||
<div>
 | 
			
		||||
 | 
			
		||||
> Render 提供免费额度,绑卡后可以进一步提升额度
 | 
			
		||||
 | 
			
		||||
Render 可以直接部署 docker 镜像,不需要 fork 仓库:https://dashboard.render.com
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
## 配置
 | 
			
		||||
系统本身开箱即用。
 | 
			
		||||
 | 
			
		||||
@@ -281,10 +299,11 @@ OPENAI_API_BASE="https://<HOST>:<PORT>/v1"
 | 
			
		||||
```mermaid
 | 
			
		||||
graph LR
 | 
			
		||||
    A(用户)
 | 
			
		||||
    A --->|请求| B(One API)
 | 
			
		||||
    A --->|使用 One API 分发的 key 进行请求| B(One API)
 | 
			
		||||
    B -->|中继请求| C(OpenAI)
 | 
			
		||||
    B -->|中继请求| D(Azure)
 | 
			
		||||
    B -->|中继请求| E(其他下游渠道)
 | 
			
		||||
    B -->|中继请求| E(其他 OpenAI API 格式下游渠道)
 | 
			
		||||
    B -->|中继并修改请求体和返回体| F(非 OpenAI API 格式下游渠道)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
可以通过在令牌后面添加渠道 ID 的方式指定使用哪一个渠道处理本次请求,例如:`Authorization: Bearer ONE_API_KEY-CHANNEL_ID`。
 | 
			
		||||
@@ -371,6 +390,12 @@ https://openai.justsong.cn
 | 
			
		||||
   + 检查是否启用了 HTTPS,浏览器会拦截 HTTPS 域名下的 HTTP 请求。
 | 
			
		||||
6. 报错:`当前分组负载已饱和,请稍后再试`
 | 
			
		||||
   + 上游通道 429 了。
 | 
			
		||||
7. 升级之后我的数据会丢失吗?
 | 
			
		||||
   + 如果使用 MySQL,不会。
 | 
			
		||||
   + 如果使用 SQLite,需要按照我所给的部署命令挂载 volume 持久化 one-api.db 数据库文件,否则容器重启后数据会丢失。
 | 
			
		||||
8. 升级之前数据库需要做变更吗?
 | 
			
		||||
   + 一般情况下不需要,系统将在初始化的时候自动调整。
 | 
			
		||||
   + 如果需要的话,我会在更新日志中说明,并给出脚本。
 | 
			
		||||
 | 
			
		||||
## 相关项目
 | 
			
		||||
* [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统
 | 
			
		||||
 
 | 
			
		||||
@@ -156,9 +156,10 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ChannelStatusUnknown  = 0
 | 
			
		||||
	ChannelStatusEnabled  = 1 // don't use 0, 0 is the default value!
 | 
			
		||||
	ChannelStatusDisabled = 2 // also don't use 0
 | 
			
		||||
	ChannelStatusUnknown          = 0
 | 
			
		||||
	ChannelStatusEnabled          = 1 // don't use 0, 0 is the default value!
 | 
			
		||||
	ChannelStatusManuallyDisabled = 2 // also don't use 0
 | 
			
		||||
	ChannelStatusAutoDisabled     = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -185,30 +186,32 @@ const (
 | 
			
		||||
	ChannelTypeOpenRouter     = 20
 | 
			
		||||
	ChannelTypeAIProxyLibrary = 21
 | 
			
		||||
	ChannelTypeFastGPT        = 22
 | 
			
		||||
	ChannelTypeTencent        = 23
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChannelBaseURLs = []string{
 | 
			
		||||
	"",                                // 0
 | 
			
		||||
	"https://api.openai.com",          // 1
 | 
			
		||||
	"https://oa.api2d.net",            // 2
 | 
			
		||||
	"",                                // 3
 | 
			
		||||
	"https://api.closeai-proxy.xyz",   // 4
 | 
			
		||||
	"https://api.openai-sb.com",       // 5
 | 
			
		||||
	"https://api.openaimax.com",       // 6
 | 
			
		||||
	"https://api.ohmygpt.com",         // 7
 | 
			
		||||
	"",                                // 8
 | 
			
		||||
	"https://api.caipacity.com",       // 9
 | 
			
		||||
	"https://api.aiproxy.io",          // 10
 | 
			
		||||
	"",                                // 11
 | 
			
		||||
	"https://api.api2gpt.com",         // 12
 | 
			
		||||
	"https://api.aigc2d.com",          // 13
 | 
			
		||||
	"https://api.anthropic.com",       // 14
 | 
			
		||||
	"https://aip.baidubce.com",        // 15
 | 
			
		||||
	"https://open.bigmodel.cn",        // 16
 | 
			
		||||
	"https://dashscope.aliyuncs.com",  // 17
 | 
			
		||||
	"",                                // 18
 | 
			
		||||
	"https://ai.360.cn",               // 19
 | 
			
		||||
	"https://openrouter.ai/api",       // 20
 | 
			
		||||
	"https://api.aiproxy.io",          // 21
 | 
			
		||||
	"https://fastgpt.run/api/openapi", // 22
 | 
			
		||||
	"",                                  // 0
 | 
			
		||||
	"https://api.openai.com",            // 1
 | 
			
		||||
	"https://oa.api2d.net",              // 2
 | 
			
		||||
	"",                                  // 3
 | 
			
		||||
	"https://api.closeai-proxy.xyz",     // 4
 | 
			
		||||
	"https://api.openai-sb.com",         // 5
 | 
			
		||||
	"https://api.openaimax.com",         // 6
 | 
			
		||||
	"https://api.ohmygpt.com",           // 7
 | 
			
		||||
	"",                                  // 8
 | 
			
		||||
	"https://api.caipacity.com",         // 9
 | 
			
		||||
	"https://api.aiproxy.io",            // 10
 | 
			
		||||
	"",                                  // 11
 | 
			
		||||
	"https://api.api2gpt.com",           // 12
 | 
			
		||||
	"https://api.aigc2d.com",            // 13
 | 
			
		||||
	"https://api.anthropic.com",         // 14
 | 
			
		||||
	"https://aip.baidubce.com",          // 15
 | 
			
		||||
	"https://open.bigmodel.cn",          // 16
 | 
			
		||||
	"https://dashscope.aliyuncs.com",    // 17
 | 
			
		||||
	"",                                  // 18
 | 
			
		||||
	"https://ai.360.cn",                 // 19
 | 
			
		||||
	"https://openrouter.ai/api",         // 20
 | 
			
		||||
	"https://api.aiproxy.io",            // 21
 | 
			
		||||
	"https://fastgpt.run/api/openapi",   // 22
 | 
			
		||||
	"https://hunyuan.cloud.tencent.com", //23
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ var ModelRatio = map[string]float64{
 | 
			
		||||
	"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
 | 
			
		||||
	"360GPT_S2_V9.4":            0.8572, // ¥0.012 / 1k tokens
 | 
			
		||||
	"hunyuan":                   7.143,  // ¥0.1 / 1k tokens  // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ModelRatio2JSONString() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@ func disableChannel(channelId int, channelName string, reason string) {
 | 
			
		||||
	if common.RootUserEmail == "" {
 | 
			
		||||
		common.RootUserEmail = model.GetRootUserEmail()
 | 
			
		||||
	}
 | 
			
		||||
	model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled)
 | 
			
		||||
	model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
 | 
			
		||||
	subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
 | 
			
		||||
	content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
 | 
			
		||||
	err := common.SendEmail(subject, common.RootUserEmail, content)
 | 
			
		||||
 
 | 
			
		||||
@@ -127,6 +127,23 @@ func DeleteChannel(c *gin.Context) {
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DeleteManuallyDisabledChannel(c *gin.Context) {
 | 
			
		||||
	rows, err := model.DeleteChannelByStatus(common.ChannelStatusManuallyDisabled)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
			"success": false,
 | 
			
		||||
			"message": err.Error(),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
		"success": true,
 | 
			
		||||
		"message": "",
 | 
			
		||||
		"data":    rows,
 | 
			
		||||
	})
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateChannel(c *gin.Context) {
 | 
			
		||||
	channel := model.Channel{}
 | 
			
		||||
	err := c.ShouldBindJSON(&channel)
 | 
			
		||||
 
 | 
			
		||||
@@ -424,12 +424,12 @@ func init() {
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			Id:         "360GPT_S2_V9.4",
 | 
			
		||||
			Id:         "hunyuan",
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1677649963,
 | 
			
		||||
			OwnedBy:    "360",
 | 
			
		||||
			OwnedBy:    "tencent",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       "360GPT_S2_V9.4",
 | 
			
		||||
			Root:       "hunyuan",
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,7 @@ func UpdateOption(c *gin.Context) {
 | 
			
		||||
		if option.Value == "true" && common.GitHubClientId == "" {
 | 
			
		||||
			c.JSON(http.StatusOK, gin.H{
 | 
			
		||||
				"success": false,
 | 
			
		||||
				"message": "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!",
 | 
			
		||||
				"message": "无法启用 GitHub OAuth,请先填入 GitHub Client Id 以及 GitHub Client Secret!",
 | 
			
		||||
			})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										287
									
								
								controller/relay-tencent.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								controller/relay-tencent.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,287 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"crypto/hmac"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"one-api/common"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// https://cloud.tencent.com/document/product/1729/97732
 | 
			
		||||
 | 
			
		||||
type TencentMessage struct {
 | 
			
		||||
	Role    string `json:"role"`
 | 
			
		||||
	Content string `json:"content"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TencentChatRequest struct {
 | 
			
		||||
	AppId    int64  `json:"app_id"`    // 腾讯云账号的 APPID
 | 
			
		||||
	SecretId string `json:"secret_id"` // 官网 SecretId
 | 
			
		||||
	// Timestamp当前 UNIX 时间戳,单位为秒,可记录发起 API 请求的时间。
 | 
			
		||||
	// 例如1529223702,如果与当前时间相差过大,会引起签名过期错误
 | 
			
		||||
	Timestamp int64 `json:"timestamp"`
 | 
			
		||||
	// Expired 签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值,
 | 
			
		||||
	// 单位为秒;Expired 必须大于 Timestamp 且 Expired-Timestamp 小于90天
 | 
			
		||||
	Expired int64  `json:"expired"`
 | 
			
		||||
	QueryID string `json:"query_id"` //请求 Id,用于问题排查
 | 
			
		||||
	// Temperature 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定
 | 
			
		||||
	// 默认 1.0,取值区间为[0.0,2.0],非必要不建议使用,不合理的取值会影响效果
 | 
			
		||||
	// 建议该参数和 top_p 只设置1个,不要同时更改 top_p
 | 
			
		||||
	Temperature float64 `json:"temperature"`
 | 
			
		||||
	// TopP 影响输出文本的多样性,取值越大,生成文本的多样性越强
 | 
			
		||||
	// 默认1.0,取值区间为[0.0, 1.0],非必要不建议使用, 不合理的取值会影响效果
 | 
			
		||||
	// 建议该参数和 temperature 只设置1个,不要同时更改
 | 
			
		||||
	TopP float64 `json:"top_p"`
 | 
			
		||||
	// Stream 0:同步,1:流式 (默认,协议:SSE)
 | 
			
		||||
	// 同步请求超时:60s,如果内容较长建议使用流式
 | 
			
		||||
	Stream int `json:"stream"`
 | 
			
		||||
	// Messages 会话内容, 长度最多为40, 按对话时间从旧到新在数组中排列
 | 
			
		||||
	// 输入 content 总数最大支持 3000 token。
 | 
			
		||||
	Messages []TencentMessage `json:"messages"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TencentError struct {
 | 
			
		||||
	Code    int    `json:"code"`
 | 
			
		||||
	Message string `json:"message"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TencentUsage struct {
 | 
			
		||||
	InputTokens  int `json:"input_tokens"`
 | 
			
		||||
	OutputTokens int `json:"output_tokens"`
 | 
			
		||||
	TotalTokens  int `json:"total_tokens"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TencentResponseChoices struct {
 | 
			
		||||
	FinishReason string         `json:"finish_reason,omitempty"` // 流式结束标志位,为 stop 则表示尾包
 | 
			
		||||
	Messages     TencentMessage `json:"messages,omitempty"`      // 内容,同步模式返回内容,流模式为 null 输出 content 内容总数最多支持 1024token。
 | 
			
		||||
	Delta        TencentMessage `json:"delta,omitempty"`         // 内容,流模式返回内容,同步模式为 null 输出 content 内容总数最多支持 1024token。
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type TencentChatResponse struct {
 | 
			
		||||
	Choices []TencentResponseChoices `json:"choices,omitempty"` // 结果
 | 
			
		||||
	Created string                   `json:"created,omitempty"` // unix 时间戳的字符串
 | 
			
		||||
	Id      string                   `json:"id,omitempty"`      // 会话 id
 | 
			
		||||
	Usage   Usage                    `json:"usage,omitempty"`   // token 数量
 | 
			
		||||
	Error   TencentError             `json:"error,omitempty"`   // 错误信息 注意:此字段可能返回 null,表示取不到有效值
 | 
			
		||||
	Note    string                   `json:"note,omitempty"`    // 注释
 | 
			
		||||
	ReqID   string                   `json:"req_id,omitempty"`  // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func requestOpenAI2Tencent(request GeneralOpenAIRequest) *TencentChatRequest {
 | 
			
		||||
	messages := make([]TencentMessage, 0, len(request.Messages))
 | 
			
		||||
	for i := 0; i < len(request.Messages); i++ {
 | 
			
		||||
		message := request.Messages[i]
 | 
			
		||||
		if message.Role == "system" {
 | 
			
		||||
			messages = append(messages, TencentMessage{
 | 
			
		||||
				Role:    "user",
 | 
			
		||||
				Content: message.Content,
 | 
			
		||||
			})
 | 
			
		||||
			messages = append(messages, TencentMessage{
 | 
			
		||||
				Role:    "assistant",
 | 
			
		||||
				Content: "Okay",
 | 
			
		||||
			})
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		messages = append(messages, TencentMessage{
 | 
			
		||||
			Content: message.Content,
 | 
			
		||||
			Role:    message.Role,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	stream := 0
 | 
			
		||||
	if request.Stream {
 | 
			
		||||
		stream = 1
 | 
			
		||||
	}
 | 
			
		||||
	return &TencentChatRequest{
 | 
			
		||||
		Timestamp:   common.GetTimestamp(),
 | 
			
		||||
		Expired:     common.GetTimestamp() + 24*60*60,
 | 
			
		||||
		QueryID:     common.GetUUID(),
 | 
			
		||||
		Temperature: request.Temperature,
 | 
			
		||||
		TopP:        request.TopP,
 | 
			
		||||
		Stream:      stream,
 | 
			
		||||
		Messages:    messages,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func responseTencent2OpenAI(response *TencentChatResponse) *OpenAITextResponse {
 | 
			
		||||
	fullTextResponse := OpenAITextResponse{
 | 
			
		||||
		Object:  "chat.completion",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Usage:   response.Usage,
 | 
			
		||||
	}
 | 
			
		||||
	if len(response.Choices) > 0 {
 | 
			
		||||
		choice := OpenAITextResponseChoice{
 | 
			
		||||
			Index: 0,
 | 
			
		||||
			Message: Message{
 | 
			
		||||
				Role:    "assistant",
 | 
			
		||||
				Content: response.Choices[0].Messages.Content,
 | 
			
		||||
			},
 | 
			
		||||
			FinishReason: response.Choices[0].FinishReason,
 | 
			
		||||
		}
 | 
			
		||||
		fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
 | 
			
		||||
	}
 | 
			
		||||
	return &fullTextResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func streamResponseTencent2OpenAI(TencentResponse *TencentChatResponse) *ChatCompletionsStreamResponse {
 | 
			
		||||
	response := ChatCompletionsStreamResponse{
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: common.GetTimestamp(),
 | 
			
		||||
		Model:   "tencent-hunyuan",
 | 
			
		||||
	}
 | 
			
		||||
	if len(TencentResponse.Choices) > 0 {
 | 
			
		||||
		var choice ChatCompletionsStreamResponseChoice
 | 
			
		||||
		choice.Delta.Content = TencentResponse.Choices[0].Delta.Content
 | 
			
		||||
		if TencentResponse.Choices[0].FinishReason == "stop" {
 | 
			
		||||
			choice.FinishReason = &stopFinishReason
 | 
			
		||||
		}
 | 
			
		||||
		response.Choices = append(response.Choices, choice)
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func tencentStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
 | 
			
		||||
	var responseText string
 | 
			
		||||
	scanner := bufio.NewScanner(resp.Body)
 | 
			
		||||
	scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
 | 
			
		||||
		if atEOF && len(data) == 0 {
 | 
			
		||||
			return 0, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if i := strings.Index(string(data), "\n"); i >= 0 {
 | 
			
		||||
			return i + 1, data[0:i], nil
 | 
			
		||||
		}
 | 
			
		||||
		if atEOF {
 | 
			
		||||
			return len(data), data, nil
 | 
			
		||||
		}
 | 
			
		||||
		return 0, nil, nil
 | 
			
		||||
	})
 | 
			
		||||
	dataChan := make(chan string)
 | 
			
		||||
	stopChan := make(chan bool)
 | 
			
		||||
	go func() {
 | 
			
		||||
		for scanner.Scan() {
 | 
			
		||||
			data := scanner.Text()
 | 
			
		||||
			if len(data) < 5 { // ignore blank line or wrong format
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			if data[:5] != "data:" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			data = data[5:]
 | 
			
		||||
			dataChan <- data
 | 
			
		||||
		}
 | 
			
		||||
		stopChan <- true
 | 
			
		||||
	}()
 | 
			
		||||
	setEventStreamHeaders(c)
 | 
			
		||||
	c.Stream(func(w io.Writer) bool {
 | 
			
		||||
		select {
 | 
			
		||||
		case data := <-dataChan:
 | 
			
		||||
			var TencentResponse TencentChatResponse
 | 
			
		||||
			err := json.Unmarshal([]byte(data), &TencentResponse)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error unmarshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			response := streamResponseTencent2OpenAI(&TencentResponse)
 | 
			
		||||
			if len(response.Choices) != 0 {
 | 
			
		||||
				responseText += response.Choices[0].Delta.Content
 | 
			
		||||
			}
 | 
			
		||||
			jsonResponse, err := json.Marshal(response)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				common.SysError("error marshalling stream response: " + err.Error())
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
 | 
			
		||||
			return true
 | 
			
		||||
		case <-stopChan:
 | 
			
		||||
			c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
	err := resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
 | 
			
		||||
	}
 | 
			
		||||
	return nil, responseText
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func tencentHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
 | 
			
		||||
	var TencentResponse TencentChatResponse
 | 
			
		||||
	responseBody, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = resp.Body.Close()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	err = json.Unmarshal(responseBody, &TencentResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	if TencentResponse.Error.Code != 0 {
 | 
			
		||||
		return &OpenAIErrorWithStatusCode{
 | 
			
		||||
			OpenAIError: OpenAIError{
 | 
			
		||||
				Message: TencentResponse.Error.Message,
 | 
			
		||||
				Code:    TencentResponse.Error.Code,
 | 
			
		||||
			},
 | 
			
		||||
			StatusCode: resp.StatusCode,
 | 
			
		||||
		}, nil
 | 
			
		||||
	}
 | 
			
		||||
	fullTextResponse := responseTencent2OpenAI(&TencentResponse)
 | 
			
		||||
	jsonResponse, err := json.Marshal(fullTextResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
 | 
			
		||||
	}
 | 
			
		||||
	c.Writer.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	c.Writer.WriteHeader(resp.StatusCode)
 | 
			
		||||
	_, err = c.Writer.Write(jsonResponse)
 | 
			
		||||
	return nil, &fullTextResponse.Usage
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseTencentConfig(config string) (appId int64, secretId string, secretKey string, err error) {
 | 
			
		||||
	parts := strings.Split(config, "|")
 | 
			
		||||
	if len(parts) != 3 {
 | 
			
		||||
		err = errors.New("invalid tencent config")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	appId, err = strconv.ParseInt(parts[0], 10, 64)
 | 
			
		||||
	secretId = parts[1]
 | 
			
		||||
	secretKey = parts[2]
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getTencentSign(req TencentChatRequest, secretKey string) string {
 | 
			
		||||
	params := make([]string, 0)
 | 
			
		||||
	params = append(params, "app_id="+strconv.FormatInt(req.AppId, 10))
 | 
			
		||||
	params = append(params, "secret_id="+req.SecretId)
 | 
			
		||||
	params = append(params, "timestamp="+strconv.FormatInt(req.Timestamp, 10))
 | 
			
		||||
	params = append(params, "query_id="+req.QueryID)
 | 
			
		||||
	params = append(params, "temperature="+strconv.FormatFloat(req.Temperature, 'f', -1, 64))
 | 
			
		||||
	params = append(params, "top_p="+strconv.FormatFloat(req.TopP, 'f', -1, 64))
 | 
			
		||||
	params = append(params, "stream="+strconv.Itoa(req.Stream))
 | 
			
		||||
	params = append(params, "expired="+strconv.FormatInt(req.Expired, 10))
 | 
			
		||||
 | 
			
		||||
	var messageStr string
 | 
			
		||||
	for _, msg := range req.Messages {
 | 
			
		||||
		messageStr += fmt.Sprintf(`{"role":"%s","content":"%s"},`, msg.Role, msg.Content)
 | 
			
		||||
	}
 | 
			
		||||
	messageStr = strings.TrimSuffix(messageStr, ",")
 | 
			
		||||
	params = append(params, "messages=["+messageStr+"]")
 | 
			
		||||
 | 
			
		||||
	sort.Sort(sort.StringSlice(params))
 | 
			
		||||
	url := "hunyuan.cloud.tencent.com/hyllm/v1/chat/completions?" + strings.Join(params, "&")
 | 
			
		||||
	mac := hmac.New(sha1.New, []byte(secretKey))
 | 
			
		||||
	signURL := url
 | 
			
		||||
	mac.Write([]byte(signURL))
 | 
			
		||||
	sign := mac.Sum([]byte(nil))
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(sign)
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +24,7 @@ const (
 | 
			
		||||
	APITypeAli
 | 
			
		||||
	APITypeXunfei
 | 
			
		||||
	APITypeAIProxyLibrary
 | 
			
		||||
	APITypeTencent
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var httpClient *http.Client
 | 
			
		||||
@@ -109,6 +110,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		apiType = APITypeXunfei
 | 
			
		||||
	case common.ChannelTypeAIProxyLibrary:
 | 
			
		||||
		apiType = APITypeAIProxyLibrary
 | 
			
		||||
	case common.ChannelTypeTencent:
 | 
			
		||||
		apiType = APITypeTencent
 | 
			
		||||
	}
 | 
			
		||||
	baseURL := common.ChannelBaseURLs[channelType]
 | 
			
		||||
	requestURL := c.Request.URL.String()
 | 
			
		||||
@@ -179,6 +182,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
		if relayMode == RelayModeEmbeddings {
 | 
			
		||||
			fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeTencent:
 | 
			
		||||
		fullRequestURL = "https://hunyuan.cloud.tencent.com/hyllm/v1/chat/completions"
 | 
			
		||||
	case APITypeAIProxyLibrary:
 | 
			
		||||
		fullRequestURL = fmt.Sprintf("%s/api/library/ask", baseURL)
 | 
			
		||||
	}
 | 
			
		||||
@@ -285,6 +290,23 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	case APITypeTencent:
 | 
			
		||||
		apiKey := c.Request.Header.Get("Authorization")
 | 
			
		||||
		apiKey = strings.TrimPrefix(apiKey, "Bearer ")
 | 
			
		||||
		appId, secretId, secretKey, err := parseTencentConfig(apiKey)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "invalid_tencent_config", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		tencentRequest := requestOpenAI2Tencent(textRequest)
 | 
			
		||||
		tencentRequest.AppId = appId
 | 
			
		||||
		tencentRequest.SecretId = secretId
 | 
			
		||||
		jsonStr, err := json.Marshal(tencentRequest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
 | 
			
		||||
		}
 | 
			
		||||
		sign := getTencentSign(*tencentRequest, secretKey)
 | 
			
		||||
		c.Request.Header.Set("Authorization", sign)
 | 
			
		||||
		requestBody = bytes.NewBuffer(jsonStr)
 | 
			
		||||
	case APITypeAIProxyLibrary:
 | 
			
		||||
		aiProxyLibraryRequest := requestOpenAI2AIProxyLibrary(textRequest)
 | 
			
		||||
		aiProxyLibraryRequest.LibraryId = c.GetString("library_id")
 | 
			
		||||
@@ -332,6 +354,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			if textRequest.Stream {
 | 
			
		||||
				req.Header.Set("X-DashScope-SSE", "enable")
 | 
			
		||||
			}
 | 
			
		||||
		case APITypeTencent:
 | 
			
		||||
			req.Header.Set("Authorization", apiKey)
 | 
			
		||||
		default:
 | 
			
		||||
			req.Header.Set("Authorization", "Bearer "+apiKey)
 | 
			
		||||
		}
 | 
			
		||||
@@ -584,6 +608,25 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	case APITypeTencent:
 | 
			
		||||
		if isStream {
 | 
			
		||||
			err, responseText := tencentStreamHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			textResponse.Usage.PromptTokens = promptTokens
 | 
			
		||||
			textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
 | 
			
		||||
			return nil
 | 
			
		||||
		} else {
 | 
			
		||||
			err, usage := tencentHandler(c, resp)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			if usage != nil {
 | 
			
		||||
				textResponse.Usage = *usage
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,12 +25,12 @@ func Distribute() func(c *gin.Context) {
 | 
			
		||||
		if ok {
 | 
			
		||||
			id, err := strconv.Atoi(channelId.(string))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				abortWithMessage(c, http.StatusBadRequest, "无效的渠道 ID")
 | 
			
		||||
				abortWithMessage(c, http.StatusBadRequest, "无效的渠道 Id")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			channel, err = model.GetChannelById(id, true)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				abortWithMessage(c, http.StatusBadRequest, "无效的渠道 ID")
 | 
			
		||||
				abortWithMessage(c, http.StatusBadRequest, "无效的渠道 Id")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if channel.Status != common.ChannelStatusEnabled {
 | 
			
		||||
 
 | 
			
		||||
@@ -176,3 +176,8 @@ func updateChannelUsedQuota(id int, quota int) {
 | 
			
		||||
		common.SysError("failed to update channel used quota: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DeleteChannelByStatus(status int64) (int64, error) {
 | 
			
		||||
	result := DB.Where("status = ?", status).Delete(&Channel{})
 | 
			
		||||
	return result.RowsAffected, result.Error
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								model/log.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								model/log.go
									
									
									
									
									
								
							@@ -8,14 +8,14 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Log struct {
 | 
			
		||||
	Id               int    `json:"id"`
 | 
			
		||||
	Id               int    `json:"id;index:idx_created_at_id,priority:1"`
 | 
			
		||||
	UserId           int    `json:"user_id" gorm:"index"`
 | 
			
		||||
	CreatedAt        int64  `json:"created_at" gorm:"bigint;index"`
 | 
			
		||||
	Type             int    `json:"type" gorm:"index"`
 | 
			
		||||
	CreatedAt        int64  `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
 | 
			
		||||
	Type             int    `json:"type" gorm:"index:idx_created_at_type"`
 | 
			
		||||
	Content          string `json:"content"`
 | 
			
		||||
	Username         string `json:"username" gorm:"index;default:''"`
 | 
			
		||||
	Username         string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"`
 | 
			
		||||
	TokenName        string `json:"token_name" gorm:"index;default:''"`
 | 
			
		||||
	ModelName        string `json:"model_name" gorm:"index;default:''"`
 | 
			
		||||
	ModelName        string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
 | 
			
		||||
	Quota            int    `json:"quota" gorm:"default:0"`
 | 
			
		||||
	PromptTokens     int    `json:"prompt_tokens" gorm:"default:0"`
 | 
			
		||||
	CompletionTokens int    `json:"completion_tokens" gorm:"default:0"`
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,7 @@ func InitDB() (err error) {
 | 
			
		||||
		if !common.IsMasterNode {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		common.SysLog("database migration started")
 | 
			
		||||
		err = db.AutoMigrate(&Channel{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,7 @@ func SetApiRouter(router *gin.Engine) {
 | 
			
		||||
			channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
 | 
			
		||||
			channelRoute.POST("/", controller.AddChannel)
 | 
			
		||||
			channelRoute.PUT("/", controller.UpdateChannel)
 | 
			
		||||
			channelRoute.DELETE("/manually_disabled", controller.DeleteManuallyDisabledChannel)
 | 
			
		||||
			channelRoute.DELETE("/:id", controller.DeleteChannel)
 | 
			
		||||
		}
 | 
			
		||||
		tokenRoute := apiRouter.Group("/token")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import {Button, Form, Input, Label, Pagination, Popup, Table} from 'semantic-ui-react';
 | 
			
		||||
import { Button, Form, Input, Label, Pagination, Popup, Table } from 'semantic-ui-react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { API, showError, showInfo, showNotice, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
 | 
			
		||||
@@ -96,7 +96,7 @@ const ChannelsTable = () => {
 | 
			
		||||
      });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const manageChannel = async (id, action, idx, priority) => {
 | 
			
		||||
  const manageChannel = async (id, action, idx, value) => {
 | 
			
		||||
    let data = { id };
 | 
			
		||||
    let res;
 | 
			
		||||
    switch (action) {
 | 
			
		||||
@@ -112,10 +112,20 @@ const ChannelsTable = () => {
 | 
			
		||||
        res = await API.put('/api/channel/', data);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'priority':
 | 
			
		||||
        if (priority === '') {
 | 
			
		||||
        if (value === '') {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        data.priority = parseInt(priority);
 | 
			
		||||
        data.priority = parseInt(value);
 | 
			
		||||
        res = await API.put('/api/channel/', data);
 | 
			
		||||
        break;
 | 
			
		||||
      case 'weight':
 | 
			
		||||
        if (value === '') {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        data.weight = parseInt(value);
 | 
			
		||||
        if (data.weight < 0) {
 | 
			
		||||
          data.weight = 0;
 | 
			
		||||
        }
 | 
			
		||||
        res = await API.put('/api/channel/', data);
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -142,9 +152,23 @@ const ChannelsTable = () => {
 | 
			
		||||
        return <Label basic color='green'>已启用</Label>;
 | 
			
		||||
      case 2:
 | 
			
		||||
        return (
 | 
			
		||||
          <Label basic color='red'>
 | 
			
		||||
            已禁用
 | 
			
		||||
          </Label>
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={<Label basic color='red'>
 | 
			
		||||
              已禁用
 | 
			
		||||
            </Label>}
 | 
			
		||||
            content='本渠道被手动禁用'
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      case 3:
 | 
			
		||||
        return (
 | 
			
		||||
          <Popup
 | 
			
		||||
            trigger={<Label basic color='yellow'>
 | 
			
		||||
              已禁用
 | 
			
		||||
            </Label>}
 | 
			
		||||
            content='本渠道被程序自动禁用'
 | 
			
		||||
            basic
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return (
 | 
			
		||||
@@ -202,7 +226,7 @@ const ChannelsTable = () => {
 | 
			
		||||
      showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
      showNotice("当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。")
 | 
			
		||||
      showNotice('当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -216,6 +240,17 @@ const ChannelsTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteAllManuallyDisabledChannels = async () => {
 | 
			
		||||
    const res = await API.delete(`/api/channel/manually_disabled`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess(`已删除所有手动禁用渠道,共计 ${data} 个`);
 | 
			
		||||
      await refresh();
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const updateChannelBalance = async (id, name, idx) => {
 | 
			
		||||
    const res = await API.get(`/api/channel/update_balance/${id}/`);
 | 
			
		||||
    const { success, message, balance } = res.data;
 | 
			
		||||
@@ -343,10 +378,10 @@ const ChannelsTable = () => {
 | 
			
		||||
              余额
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
            <Table.HeaderCell
 | 
			
		||||
                style={{ cursor: 'pointer' }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  sortChannel('priority');
 | 
			
		||||
                }}
 | 
			
		||||
              style={{ cursor: 'pointer' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                sortChannel('priority');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              优先级
 | 
			
		||||
            </Table.HeaderCell>
 | 
			
		||||
@@ -390,18 +425,18 @@ const ChannelsTable = () => {
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
                    <Popup
 | 
			
		||||
                        trigger={<Input type="number"  defaultValue={channel.priority} onBlur={(event) => {
 | 
			
		||||
                          manageChannel(
 | 
			
		||||
                              channel.id,
 | 
			
		||||
                              'priority',
 | 
			
		||||
                              idx,
 | 
			
		||||
                              event.target.value,
 | 
			
		||||
                          );
 | 
			
		||||
                        }}>
 | 
			
		||||
                          <input style={{maxWidth:'60px'}} />
 | 
			
		||||
                        </Input>}
 | 
			
		||||
                        content='渠道选择优先级,越高越优先'
 | 
			
		||||
                        basic
 | 
			
		||||
                      trigger={<Input type='number' defaultValue={channel.priority} onBlur={(event) => {
 | 
			
		||||
                        manageChannel(
 | 
			
		||||
                          channel.id,
 | 
			
		||||
                          'priority',
 | 
			
		||||
                          idx,
 | 
			
		||||
                          event.target.value
 | 
			
		||||
                        );
 | 
			
		||||
                      }}>
 | 
			
		||||
                        <input style={{ maxWidth: '60px' }} />
 | 
			
		||||
                      </Input>}
 | 
			
		||||
                      content='渠道选择优先级,越高越优先'
 | 
			
		||||
                      basic
 | 
			
		||||
                    />
 | 
			
		||||
                  </Table.Cell>
 | 
			
		||||
                  <Table.Cell>
 | 
			
		||||
@@ -481,6 +516,20 @@ const ChannelsTable = () => {
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button size='small' onClick={updateAllChannelsBalance}
 | 
			
		||||
                      loading={loading || updatingBalance}>更新所有已启用通道余额</Button>
 | 
			
		||||
              <Popup
 | 
			
		||||
                trigger={
 | 
			
		||||
                  <Button size='small' loading={loading}>
 | 
			
		||||
                    删除所有手动禁用渠道
 | 
			
		||||
                  </Button>
 | 
			
		||||
                }
 | 
			
		||||
                on='click'
 | 
			
		||||
                flowing
 | 
			
		||||
                hoverable
 | 
			
		||||
              >
 | 
			
		||||
                <Button size='small' loading={loading} negative onClick={deleteAllManuallyDisabledChannels}>
 | 
			
		||||
                  确认删除
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popup>
 | 
			
		||||
              <Pagination
 | 
			
		||||
                floated='right'
 | 
			
		||||
                activePage={activePage}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export const CHANNEL_OPTIONS = [
 | 
			
		||||
  { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
 | 
			
		||||
  { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
 | 
			
		||||
  { key: 19, text: '360 智脑', value: 19, color: 'blue' },
 | 
			
		||||
  { key: 23, text: '腾讯混元', value: 23, color: 'teal' },
 | 
			
		||||
  { key: 8, text: '自定义渠道', value: 8, color: 'pink' },
 | 
			
		||||
  { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
 | 
			
		||||
  { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,8 @@ function type2secretPrompt(type) {
 | 
			
		||||
      return '按照如下格式输入:APPID|APISecret|APIKey';
 | 
			
		||||
    case 22:
 | 
			
		||||
      return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
 | 
			
		||||
    case 23:
 | 
			
		||||
      return '按照如下格式输入:AppId|SecretId|SecretKey';
 | 
			
		||||
    default:
 | 
			
		||||
      return '请输入渠道对应的鉴权密钥';
 | 
			
		||||
  }
 | 
			
		||||
@@ -76,7 +78,10 @@ const EditChannel = () => {
 | 
			
		||||
          localModels = ['SparkDesk'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 19:
 | 
			
		||||
          localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1', '360GPT_S2_V9.4'];
 | 
			
		||||
          localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
 | 
			
		||||
          break;
 | 
			
		||||
        case 23:
 | 
			
		||||
          localModels = ['hunyuan'];
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      setInputs((inputs) => ({ ...inputs, models: localModels }));
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user