Compare commits
18 Commits
v0.4.7.0
...
v0.4.7.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| e661578515 | |||
| 65faa158b6 | |||
| d43c5d6003 | |||
| 27a1e49366 | |||
| 3688d8f968 | |||
| 2869dbf60a | |||
| 9e2433e2a6 | |||
| 025b6d1226 | |||
| 5665b1a58b | |||
| ed888fb717 | |||
| e84d602e17 | |||
| cc5e683784 | |||
| 6fe7b15cd0 | |||
| 2c255b6598 | |||
| bec1b752d6 | |||
| ca10ec1a0c | |||
| cbca378e61 | |||
| 02bb1c6580 |
+4
-2
@@ -7,18 +7,20 @@ COPY ./web .
|
||||
COPY ./VERSION .
|
||||
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
||||
|
||||
FROM golang AS builder2
|
||||
FROM golang:alpine AS builder2
|
||||
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0 \
|
||||
GOOS=linux
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ADD go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
COPY --from=builder /build/dist ./web/dist
|
||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
|
||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)'" -o one-api
|
||||
|
||||
FROM alpine
|
||||
|
||||
|
||||
+34
-5
@@ -91,13 +91,10 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model
|
||||
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, if not specified in channel settings, use this version, default `2024-12-01-preview`
|
||||
|
||||
## Deployment
|
||||
|
||||
> [!TIP]
|
||||
> Latest Docker image: `calciumion/new-api:latest`
|
||||
> Default account: root, password: 123456
|
||||
> Update command:
|
||||
> ```
|
||||
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
> ```
|
||||
> Default account: root, password: 123456
|
||||
|
||||
### Multi-Server Deployment
|
||||
- Must set `SESSION_SECRET` environment variable, otherwise login state will not be consistent across multiple servers.
|
||||
@@ -107,26 +104,58 @@ You can add custom models gpt-4-gizmo-* in channels. These are third-party model
|
||||
- Local database (default): SQLite (Docker deployment must mount `/data` directory)
|
||||
- Remote database: MySQL >= 5.7.8, PgSQL >= 9.6
|
||||
|
||||
### Deployment with BT Panel
|
||||
Install BT Panel (**version 9.2.0** or above) from [BT Panel Official Website](https://www.bt.cn/new/download.html), choose the stable version script to download and install.
|
||||
After installation, log in to BT Panel and click Docker in the menu bar. First-time access will prompt to install Docker service. Click Install Now and follow the prompts to complete installation.
|
||||
After installation, find **New-API** in the app store, click install, configure basic options to complete installation.
|
||||
[Pictorial Guide](BT.md)
|
||||
|
||||
### Docker Deployment
|
||||
|
||||
### Using Docker Compose (Recommended)
|
||||
```shell
|
||||
# Clone project
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# Edit docker-compose.yml as needed
|
||||
# nano docker-compose.yml
|
||||
# vim docker-compose.yml
|
||||
# Start
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Update Version
|
||||
```shell
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Direct Docker Image Usage
|
||||
```shell
|
||||
# SQLite deployment:
|
||||
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
|
||||
|
||||
# MySQL deployment (add -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"), modify database connection parameters as needed
|
||||
# Example:
|
||||
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
|
||||
```
|
||||
|
||||
#### Update Version
|
||||
```shell
|
||||
# Pull the latest image
|
||||
docker pull calciumion/new-api:latest
|
||||
# Stop and remove the old container
|
||||
docker stop new-api
|
||||
docker rm new-api
|
||||
# Run the new container with the same parameters as before
|
||||
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
|
||||
```
|
||||
|
||||
Alternatively, you can use Watchtower for automatic updates (not recommended, may cause database incompatibility):
|
||||
```shell
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
```
|
||||
|
||||
## Channel Retry
|
||||
Channel retry is implemented, configurable in `Settings->Operation Settings->General Settings`. **Cache recommended**.
|
||||
First retry uses same priority, second retry uses next priority, and so on.
|
||||
|
||||
@@ -96,13 +96,10 @@
|
||||
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容。
|
||||
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,如果渠道设置中未指定API版本,则使用此版本,默认为 `2024-12-01-preview`
|
||||
## 部署
|
||||
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
> 默认账号root 密码123456
|
||||
> 更新指令:
|
||||
> ```
|
||||
> docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
> ```
|
||||
> 默认账号root 密码123456
|
||||
|
||||
### 多机部署
|
||||
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致。
|
||||
@@ -119,25 +116,54 @@
|
||||
[图文教程](BT.md)
|
||||
|
||||
### 基于 Docker 进行部署
|
||||
|
||||
> [!TIP]
|
||||
> 默认管理员账号root 密码123456
|
||||
|
||||
### 使用 Docker Compose 部署(推荐)
|
||||
```shell
|
||||
# 下载项目
|
||||
git clone https://github.com/Calcium-Ion/new-api.git
|
||||
cd new-api
|
||||
# 按需编辑 docker-compose.yml
|
||||
# nano docker-compose.yml
|
||||
# vim docker-compose.yml
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### 更新版本
|
||||
```shell
|
||||
docker-compose pull
|
||||
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
|
||||
|
||||
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。
|
||||
# 例如:
|
||||
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
|
||||
```
|
||||
|
||||
#### 更新版本
|
||||
```shell
|
||||
# 拉取最新镜像
|
||||
docker pull calciumion/new-api:latest
|
||||
# 停止并删除旧容器
|
||||
docker stop new-api
|
||||
docker rm new-api
|
||||
# 使用相同参数运行新容器
|
||||
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
|
||||
```
|
||||
|
||||
或者使用 Watchtower 自动更新(不推荐,可能会导致数据库不兼容):
|
||||
```shell
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR
|
||||
```
|
||||
|
||||
## 渠道重试
|
||||
渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。
|
||||
如果开启了重试功能,第一次重试使用同优先级,第二次重试使用下一个优先级,以此类推。
|
||||
|
||||
@@ -3,5 +3,6 @@ package common
|
||||
var UsingSQLite = false
|
||||
var UsingPostgreSQL = false
|
||||
var UsingMySQL = false
|
||||
var UsingClickHouse = false
|
||||
|
||||
var SQLitePath = "one-api.db?_busy_timeout=5000"
|
||||
|
||||
+13
-15
@@ -41,36 +41,34 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
|
||||
|
||||
requestPath := "/v1/chat/completions"
|
||||
|
||||
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(testModel), "embedding") ||
|
||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||
strings.Contains(testModel, "bge-") || // bge 系列模型
|
||||
strings.HasPrefix(testModel, "m3e") || // m3e 系列模型
|
||||
strings.Contains(testModel, "bge-") || // bge 系列模型
|
||||
testModel == "text-embedding-v1" ||
|
||||
channel.Type == common.ChannelTypeMokaAI{ // 其他 embedding 模型
|
||||
requestPath = "/v1/embeddings" // 修改请求路径
|
||||
channel.Type == common.ChannelTypeMokaAI { // 其他 embedding 模型
|
||||
requestPath = "/v1/embeddings" // 修改请求路径
|
||||
}
|
||||
|
||||
|
||||
c.Request = &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{Path: requestPath}, // 使用动态路径
|
||||
URL: &url.URL{Path: requestPath}, // 使用动态路径
|
||||
Body: nil,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
|
||||
if testModel == "" {
|
||||
common.SysLog(fmt.Sprintf("testModel 为空, channel 的 TestModel 是 %s", string(*channel.TestModel)))
|
||||
if channel.TestModel != nil && *channel.TestModel != "" {
|
||||
testModel = *channel.TestModel
|
||||
} else {
|
||||
if len(channel.GetModels()) > 0 {
|
||||
testModel = channel.GetModels()[0]
|
||||
} else {
|
||||
testModel = "gpt-3.5-turbo"
|
||||
testModel = "gpt-4o-mini"
|
||||
}
|
||||
common.SysLog(fmt.Sprintf("testModel 为空, channel 的 TestModel 为空:", string(testModel)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +100,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
|
||||
request := buildTestRequest(testModel)
|
||||
meta.UpstreamModelName = testModel
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , meta %s ", channel.Id, testModel, meta))
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , meta %v ", channel.Id, testModel, meta))
|
||||
|
||||
adaptor.Init(meta)
|
||||
|
||||
@@ -173,9 +171,9 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
|
||||
// 先判断是否为 Embedding 模型
|
||||
if strings.Contains(strings.ToLower(model), "embedding") ||
|
||||
strings.HasPrefix(model, "m3e") || // m3e 系列模型
|
||||
strings.Contains(model, "bge-") || // bge 系列模型
|
||||
model == "text-embedding-v1" { // 其他 embedding 模型
|
||||
strings.HasPrefix(model, "m3e") || // m3e 系列模型
|
||||
strings.Contains(model, "bge-") || // bge 系列模型
|
||||
model == "text-embedding-v1" { // 其他 embedding 模型
|
||||
// Embedding 请求
|
||||
testRequest.Input = []string{"hello world"}
|
||||
return testRequest
|
||||
|
||||
@@ -18,6 +18,7 @@ type GeneralOpenAIRequest struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
Messages []Message `json:"messages,omitempty"`
|
||||
Prompt any `json:"prompt,omitempty"`
|
||||
Suffix any `json:"suffix,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||||
MaxTokens uint `json:"max_tokens,omitempty"`
|
||||
|
||||
+28
-6
@@ -133,9 +133,6 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
|
||||
tx = LOG_DB.Where("logs.type = ?", logType)
|
||||
}
|
||||
|
||||
tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id")
|
||||
tx = tx.Select("logs.*, channels.name as channel_name")
|
||||
|
||||
if modelName != "" {
|
||||
tx = tx.Where("logs.model_name like ?", modelName)
|
||||
}
|
||||
@@ -165,6 +162,30 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
channelIds := make([]int, 0)
|
||||
channelMap := make(map[int]string)
|
||||
for _, log := range logs {
|
||||
if log.ChannelId != 0 {
|
||||
channelIds = append(channelIds, log.ChannelId)
|
||||
}
|
||||
}
|
||||
if len(channelIds) > 0 {
|
||||
var channels []struct {
|
||||
Id int `gorm:"column:id"`
|
||||
Name string `gorm:"column:name"`
|
||||
}
|
||||
if err = DB.Table("channels").Select("id, name").Where("id IN ?", channelIds).Find(&channels).Error; err != nil {
|
||||
return logs, total, err
|
||||
}
|
||||
for _, channel := range channels {
|
||||
channelMap[channel.Id] = channel.Name
|
||||
}
|
||||
for i := range logs {
|
||||
logs[i].ChannelName = channelMap[logs[i].ChannelId]
|
||||
}
|
||||
}
|
||||
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
@@ -176,9 +197,6 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
|
||||
tx = LOG_DB.Where("logs.user_id = ? and logs.type = ?", userId, logType)
|
||||
}
|
||||
|
||||
tx = tx.Joins("LEFT JOIN channels ON logs.channel_id = channels.id")
|
||||
tx = tx.Select("logs.*, channels.name as channel_name")
|
||||
|
||||
if modelName != "" {
|
||||
tx = tx.Where("logs.model_name like ?", modelName)
|
||||
}
|
||||
@@ -199,6 +217,10 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
|
||||
return nil, 0, err
|
||||
}
|
||||
err = tx.Order("logs.id desc").Limit(num).Offset(startIdx).Find(&logs).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
formatUserLogs(logs)
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
@@ -110,6 +110,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
|
||||
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
|
||||
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
|
||||
common.OptionMap["AutomaticDisableKeywords"] = setting.AutomaticDisableKeywordsToString()
|
||||
|
||||
common.OptionMapRWMutex.Unlock()
|
||||
loadOptionsFromDatabase()
|
||||
@@ -335,6 +336,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
common.QuotaPerUnit, _ = strconv.ParseFloat(value, 64)
|
||||
case "SensitiveWords":
|
||||
setting.SensitiveWordsFromString(value)
|
||||
case "AutomaticDisableKeywords":
|
||||
setting.AutomaticDisableKeywordsFromString(value)
|
||||
case "StreamCacheQueueLength":
|
||||
setting.StreamCacheQueueLength, _ = strconv.Atoi(value)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"math"
|
||||
@@ -20,10 +23,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func sendStreamData(c *gin.Context, data string, forceFormat bool) error {
|
||||
@@ -91,11 +90,12 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
if len(data) < 6 { // ignore blank line or wrong format
|
||||
continue
|
||||
}
|
||||
if data[:6] != "data: " && data[:6] != "[DONE]" {
|
||||
if data[:5] != "data:" && data[:6] != "[DONE]" {
|
||||
continue
|
||||
}
|
||||
mu.Lock()
|
||||
data = data[6:]
|
||||
data = data[5:]
|
||||
data = strings.TrimSpace(data)
|
||||
if !strings.HasPrefix(data, "[DONE]") {
|
||||
if lastStreamData != "" {
|
||||
err := sendStreamData(c, lastStreamData, forceFormat)
|
||||
|
||||
@@ -36,6 +36,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
return fmt.Sprintf("%s/v1/embeddings", info.BaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeChatCompletions {
|
||||
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeCompletions {
|
||||
return fmt.Sprintf("%s/v1/completions", info.BaseUrl), nil
|
||||
}
|
||||
return "", errors.New("invalid relay mode")
|
||||
}
|
||||
@@ -72,6 +74,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
} else {
|
||||
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
|
||||
}
|
||||
case constant.RelayModeCompletions:
|
||||
if info.IsStream {
|
||||
err, usage = openai.OaiStreamHandler(c, resp, info)
|
||||
} else {
|
||||
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
|
||||
}
|
||||
case constant.RelayModeEmbeddings:
|
||||
err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"one-api/relay/channel/openai"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/relay/constant"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Adaptor struct {
|
||||
@@ -32,6 +33,9 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeChatCompletions:
|
||||
if strings.HasPrefix(info.UpstreamModelName, "bot") {
|
||||
return fmt.Sprintf("%s/api/v3/bots/chat/completions", info.BaseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/api/v3/chat/completions", info.BaseUrl), nil
|
||||
case constant.RelayModeEmbeddings:
|
||||
return fmt.Sprintf("%s/api/v3/embeddings", info.BaseUrl), nil
|
||||
|
||||
@@ -112,7 +112,8 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
}
|
||||
if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic ||
|
||||
info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini ||
|
||||
info.ChannelType == common.ChannelCloudflare || info.ChannelType == common.ChannelTypeAzure {
|
||||
info.ChannelType == common.ChannelCloudflare || info.ChannelType == common.ChannelTypeAzure ||
|
||||
info.ChannelType == common.ChannelTypeVolcEngine {
|
||||
info.SupportStreamOptions = true
|
||||
}
|
||||
return info
|
||||
|
||||
+4
-14
@@ -6,6 +6,7 @@ import (
|
||||
"one-api/common"
|
||||
relaymodel "one-api/dto"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -64,21 +65,10 @@ func ShouldDisableChannel(channelType int, err *relaymodel.OpenAIErrorWithStatus
|
||||
case "forbidden":
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(err.Error.Message, "Your credit balance is too low") { // anthropic
|
||||
return true
|
||||
} else if strings.HasPrefix(err.Error.Message, "This organization has been disabled.") {
|
||||
return true
|
||||
} else if strings.HasPrefix(err.Error.Message, "You exceeded your current quota") {
|
||||
return true
|
||||
} else if strings.HasPrefix(err.Error.Message, "Permission denied") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error.Message, "The security token included in the request is invalid") { // anthropic
|
||||
return true
|
||||
} else if strings.Contains(err.Error.Message, "Operation not allowed") {
|
||||
return true
|
||||
} else if strings.Contains(err.Error.Message, "Your account is not authorized") {
|
||||
lowerMessage := strings.ToLower(err.Error.Message)
|
||||
search, _ := AcSearch(lowerMessage, setting.AutomaticDisableKeywords, true)
|
||||
if search {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
+2
-12
@@ -60,17 +60,7 @@ func SensitiveWordContains(text string) (bool, []string) {
|
||||
return false, nil
|
||||
}
|
||||
checkText := strings.ToLower(text)
|
||||
// 构建一个AC自动机
|
||||
m := InitAc()
|
||||
hits := m.MultiPatternSearch([]rune(checkText), false)
|
||||
if len(hits) > 0 {
|
||||
words := make([]string, 0)
|
||||
for _, hit := range hits {
|
||||
words = append(words, string(hit.Word))
|
||||
}
|
||||
return true, words
|
||||
}
|
||||
return false, nil
|
||||
return AcSearch(checkText, setting.SensitiveWords, false)
|
||||
}
|
||||
|
||||
// SensitiveWordReplace 敏感词替换,返回是否包含敏感词和替换后的文本
|
||||
@@ -79,7 +69,7 @@ func SensitiveWordReplace(text string, returnImmediately bool) (bool, []string,
|
||||
return false, nil, text
|
||||
}
|
||||
checkText := strings.ToLower(text)
|
||||
m := InitAc()
|
||||
m := InitAc(setting.SensitiveWords)
|
||||
hits := m.MultiPatternSearch([]rune(checkText), returnImmediately)
|
||||
if len(hits) > 0 {
|
||||
words := make([]string, 0)
|
||||
|
||||
+26
-5
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
goahocorasick "github.com/anknown/ahocorasick"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -57,9 +56,9 @@ func RemoveDuplicate(s []string) []string {
|
||||
return result
|
||||
}
|
||||
|
||||
func InitAc() *goahocorasick.Machine {
|
||||
func InitAc(words []string) *goahocorasick.Machine {
|
||||
m := new(goahocorasick.Machine)
|
||||
dict := readRunes()
|
||||
dict := readRunes(words)
|
||||
if err := m.Build(dict); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
@@ -67,10 +66,10 @@ func InitAc() *goahocorasick.Machine {
|
||||
return m
|
||||
}
|
||||
|
||||
func readRunes() [][]rune {
|
||||
func readRunes(words []string) [][]rune {
|
||||
var dict [][]rune
|
||||
|
||||
for _, word := range setting.SensitiveWords {
|
||||
for _, word := range words {
|
||||
word = strings.ToLower(word)
|
||||
l := bytes.TrimSpace([]byte(word))
|
||||
dict = append(dict, bytes.Runes(l))
|
||||
@@ -78,3 +77,25 @@ func readRunes() [][]rune {
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
func AcSearch(findText string, dict []string, stopImmediately bool) (bool, []string) {
|
||||
if len(dict) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if len(findText) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
m := InitAc(dict)
|
||||
if m == nil {
|
||||
return false, nil
|
||||
}
|
||||
hits := m.MultiPatternSearch([]rune(findText), stopImmediately)
|
||||
if len(hits) > 0 {
|
||||
words := make([]string, 0)
|
||||
for _, hit := range hits {
|
||||
words = append(words, string(hit.Word))
|
||||
}
|
||||
return true, words
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
package setting
|
||||
|
||||
import "strings"
|
||||
|
||||
var DemoSiteEnabled = false
|
||||
|
||||
var AutomaticDisableKeywords = []string{
|
||||
"Your credit balance is too low",
|
||||
"This organization has been disabled.",
|
||||
"You exceeded your current quota",
|
||||
"Permission denied",
|
||||
"The security token included in the request is invalid",
|
||||
"Operation not allowed",
|
||||
"Your account is not authorized",
|
||||
}
|
||||
|
||||
func AutomaticDisableKeywordsToString() string {
|
||||
return strings.Join(AutomaticDisableKeywords, "\n")
|
||||
}
|
||||
|
||||
func AutomaticDisableKeywordsFromString(s string) {
|
||||
AutomaticDisableKeywords = []string{}
|
||||
ak := strings.Split(s, "\n")
|
||||
for _, k := range ak {
|
||||
k = strings.TrimSpace(k)
|
||||
if k != "" {
|
||||
AutomaticDisableKeywords = append(AutomaticDisableKeywords, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +357,13 @@ const ChannelsTable = () => {
|
||||
dataIndex: 'operate',
|
||||
render: (text, record, index) => {
|
||||
if (record.children === undefined) {
|
||||
// 构建模型测试菜单
|
||||
const modelMenuItems = record.models.split(',').map(model => ({
|
||||
node: 'item',
|
||||
name: model,
|
||||
onClick: () => testChannel(record, model)
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SplitButtonGroup
|
||||
@@ -374,7 +381,7 @@ const ChannelsTable = () => {
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
menu={record.test_models}
|
||||
menu={modelMenuItems} // 使用即时生成的菜单项
|
||||
>
|
||||
<Button
|
||||
style={{ padding: '8px 4px' }}
|
||||
@@ -545,17 +552,6 @@ const ChannelsTable = () => {
|
||||
let channelTags = {};
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
channels[i].key = '' + channels[i].id;
|
||||
let test_models = [];
|
||||
channels[i].models.split(',').forEach((item, index) => {
|
||||
test_models.push({
|
||||
node: 'item',
|
||||
name: item,
|
||||
onClick: () => {
|
||||
testChannel(channels[i], item);
|
||||
}
|
||||
});
|
||||
});
|
||||
channels[i].test_models = test_models;
|
||||
if (!enableTagMode) {
|
||||
channelDates.push(channels[i]);
|
||||
} else {
|
||||
@@ -798,16 +794,59 @@ const ChannelsTable = () => {
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const updateChannelProperty = (channelId, updateFn) => {
|
||||
// Create a new copy of channels array
|
||||
const newChannels = [...channels];
|
||||
let updated = false;
|
||||
|
||||
// Find and update the correct channel
|
||||
newChannels.forEach(channel => {
|
||||
if (channel.children !== undefined) {
|
||||
// If this is a tag group, search in its children
|
||||
channel.children.forEach(child => {
|
||||
if (child.id === channelId) {
|
||||
updateFn(child);
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
} else if (channel.id === channelId) {
|
||||
// Direct channel match
|
||||
updateFn(channel);
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Only update state if we actually modified a channel
|
||||
if (updated) {
|
||||
setChannels(newChannels);
|
||||
}
|
||||
};
|
||||
|
||||
const testChannel = async (record, model) => {
|
||||
const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
|
||||
const { success, message, time } = res.data;
|
||||
if (success) {
|
||||
record.response_time = time * 1000;
|
||||
record.test_time = Date.now() / 1000;
|
||||
// Also update the channels state to persist the change
|
||||
updateChannelProperty(record.id, (channel) => {
|
||||
channel.response_time = time * 1000;
|
||||
channel.test_time = Date.now() / 1000;
|
||||
});
|
||||
|
||||
showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2)));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新列表
|
||||
await refresh();
|
||||
const updateChannelBalance = async (record) => {
|
||||
const res = await API.get(`/api/channel/update_balance/${record.id}/`);
|
||||
const { success, message, balance } = res.data;
|
||||
if (success) {
|
||||
updateChannelProperty(record.id, (channel) => {
|
||||
channel.balance = balance;
|
||||
channel.balance_updated_time = Date.now() / 1000;
|
||||
});
|
||||
showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@@ -834,20 +873,6 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const updateChannelBalance = async (record) => {
|
||||
const res = await API.get(`/api/channel/update_balance/${record.id}/`);
|
||||
const { success, message, balance } = res.data;
|
||||
if (success) {
|
||||
record.balance = balance;
|
||||
record.balance_updated_time = Date.now() / 1000;
|
||||
showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name));
|
||||
// 刷新列表
|
||||
await refresh();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAllChannelsBalance = async () => {
|
||||
setUpdatingBalance(true);
|
||||
const res = await API.get(`/api/channel/update_balance`);
|
||||
|
||||
@@ -59,6 +59,7 @@ const OperationSetting = () => {
|
||||
RetryTimes: 0,
|
||||
Chats: "[]",
|
||||
DemoSiteEnabled: false,
|
||||
AutomaticDisableKeywords: '',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -201,7 +201,7 @@
|
||||
"相关 API 显示令牌额度而非用户额度": "Related APIs show token quota instead of user quota",
|
||||
"保存通用设置": "Save General Settings",
|
||||
"监控设置": "Monitoring Settings",
|
||||
"最长响应时间": "Maximum Response Time",
|
||||
"测试所有渠道的最长响应时间": "Maximum response time for testing all channels",
|
||||
"单位秒": "Unit: seconds",
|
||||
"当运行通道全部测试时": "When running all channel tests",
|
||||
"超过此时间将自动禁用通道": "Channels exceeding this time will be automatically disabled",
|
||||
@@ -1108,7 +1108,7 @@
|
||||
"如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.",
|
||||
"完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}",
|
||||
"请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions",
|
||||
"此项可选,用于通过代理站来进行 API 调用": "Optional, used for API calls through proxy sites",
|
||||
"此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through proxy sites, do not end with /v1 and /",
|
||||
"私有部署地址": "Private Deployment Address",
|
||||
"请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi",
|
||||
"注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work",
|
||||
@@ -1246,5 +1246,8 @@
|
||||
"请输入要设置的标签名称": "Please enter the tag name to be set",
|
||||
"请输入标签名称": "Please enter the tag name",
|
||||
"支持搜索用户的 ID、用户名、显示名称和邮箱地址": "Support searching for user ID, username, display name, and email address",
|
||||
"已注销": "Logged out"
|
||||
"已注销": "Logged out",
|
||||
"自动禁用关键词": "Automatic disable keywords",
|
||||
"一行一个,不区分大小写": "One line per keyword, not case-sensitive",
|
||||
"当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道": "When the upstream channel returns an error containing these keywords (not case-sensitive), automatically disable the channel"
|
||||
}
|
||||
@@ -429,6 +429,7 @@ const EditChannel = (props) => {
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
|
||||
<Typography.Text strong>{t('类型')}:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
@@ -442,6 +443,28 @@ const EditChannel = (props) => {
|
||||
searchPosition='dropdown'
|
||||
placeholder={t('请选择渠道类型')}
|
||||
/>
|
||||
{inputs.type === 40 && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type="info"
|
||||
description={
|
||||
<div>
|
||||
<Typography.Text strong>
|
||||
{t('邀请链接')}:
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
link
|
||||
underline
|
||||
style={{marginLeft: 8}}
|
||||
onClick={() => window.open('https://cloud.siliconflow.cn/i/hij0YNTZ')}
|
||||
>
|
||||
https://cloud.siliconflow.cn/i/hij0YNTZ
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{inputs.type === 3 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
@@ -520,12 +543,12 @@ const EditChannel = (props) => {
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('代理')}:</Typography.Text>
|
||||
<Typography.Text strong>{t('BaseURL')}:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label={t('代理')}
|
||||
label={t('BaseURL')}
|
||||
name="base_url"
|
||||
placeholder={t('此项可选,用于通过代理站来进行 API 调用')}
|
||||
placeholder={t('此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
showWarning, verifyJSON
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function SettingsMonitoring(props) {
|
||||
QuotaRemindThreshold: '',
|
||||
AutomaticDisableChannelEnabled: false,
|
||||
AutomaticEnableChannelEnabled: false,
|
||||
AutomaticDisableKeywords: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -79,7 +80,7 @@ export default function SettingsMonitoring(props) {
|
||||
<Row gutter={16}>
|
||||
<Col span={8}>
|
||||
<Form.InputNumber
|
||||
label={t('最长响应时间')}
|
||||
label={t('测试所有渠道的最长响应时间')}
|
||||
step={1}
|
||||
min={0}
|
||||
suffix={t('秒')}
|
||||
@@ -144,6 +145,18 @@ export default function SettingsMonitoring(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.TextArea
|
||||
label={t('自动禁用关键词')}
|
||||
placeholder={t('一行一个,不区分大小写')}
|
||||
extraText={t('当上游通道返回错误中包含这些关键词时(不区分大小写),自动禁用通道')}
|
||||
field={'AutomaticDisableKeywords'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
onChange={(value) => setInputs({ ...inputs, AutomaticDisableKeywords: value })}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Button size='default' onClick={onSubmit}>
|
||||
{t('保存监控设置')}
|
||||
|
||||
Reference in New Issue
Block a user