Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,5 @@ docs
.gocache
/web/node_modules
/web/default/node_modules
/web/default/dist
/web/classic/node_modules
/web/classic/dist
!THIRD-PARTY-LICENSES.md
22 changes: 0 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,23 +1,3 @@
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder

WORKDIR /build
COPY web/default/package.json .
COPY web/default/bun.lock .
RUN bun install
COPY ./web/default .
COPY ./VERSION .
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build

FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-classic

WORKDIR /build
COPY web/classic/package.json .
COPY web/classic/bun.lock .
RUN bun install
COPY ./web/classic .
COPY ./VERSION .
RUN VITE_REACT_APP_VERSION=$(cat VERSION) bun run build

FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
ENV GO111MODULE=on CGO_ENABLED=0

Expand All @@ -32,8 +12,6 @@ ADD go.mod go.sum ./
RUN go mod download

COPY . .
COPY --from=builder /build/dist ./web/default/dist
COPY --from=builder-classic /build/dist ./web/classic/dist
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api

FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
Expand Down
2 changes: 2 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ var QuotaForInvitee = 0
var ChannelDisableThreshold = 5.0
var AutomaticDisableChannelEnabled = false
var AutomaticEnableChannelEnabled = false
var AutomaticDeleteChannelEnabled = false
var QuotaRemindThreshold = 1000
var PreConsumedQuota = 500

Expand All @@ -172,6 +173,7 @@ var RelayTimeout int // unit is second

var RelayMaxIdleConns int
var RelayMaxIdleConnsPerHost int
var RelayResponseHeaderTimeout int

var GeminiSafetySetting string

Expand Down
1 change: 1 addition & 0 deletions common/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ func InitEnv() {
RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0)
RelayMaxIdleConns = GetEnvOrDefault("RELAY_MAX_IDLE_CONNS", 500)
RelayMaxIdleConnsPerHost = GetEnvOrDefault("RELAY_MAX_IDLE_CONNS_PER_HOST", 100)
RelayResponseHeaderTimeout = GetEnvOrDefault("RELAY_RESPONSE_HEADER_TIMEOUT", 30)

// Initialize string variables with GetEnvOrDefaultString
GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
Expand Down
10 changes: 10 additions & 0 deletions controller/channel-test.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,16 @@ func testAllChannels(notify bool) error {
shouldBanChannel = service.ShouldDisableChannel(result.newAPIError)
}

// check auto-delete first (takes priority over disable)
if newAPIError != nil && service.ShouldDeleteChannel(result.newAPIError) {
if isChannelEnabled && channel.GetAutoBan() {
processChannelError(result.context, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError)
}
if shouldBanChannel {
shouldBanChannel = false
}
}

// 当错误检查通过,才检查响应时间
if common.AutomaticDisableChannelEnabled && !shouldBanChannel {
if milliseconds > disableThreshold {
Expand Down
6 changes: 5 additions & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,11 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, common.LocalLogPreview(err.Error())))
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
// do not use context to get channel info, there may be inconsistent channel info when processing asynchronously
if service.ShouldDisableChannel(err) && channelError.AutoBan {
if service.ShouldDeleteChannel(err) && channelError.AutoBan {
gopool.Go(func() {
service.DeleteChannel(channelError, err.ErrorWithStatusCode())
})
} else if service.ShouldDisableChannel(err) && channelError.AutoBan {
gopool.Go(func() {
service.DisableChannel(channelError, err.ErrorWithStatusCode())
})
Expand Down
8 changes: 8 additions & 0 deletions model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,14 @@ func DeleteChannelByStatus(status int64) (int64, error) {
return result.RowsAffected, result.Error
}

func DeleteChannelByStatusAndId(channelId int) error {
var channel Channel
if err := DB.First(&channel, channelId).Error; err != nil {
return err
}
return channel.Delete()
}
Comment on lines +875 to +881

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Misleading function name: no status parameter despite "ByStatusAndId" naming.

The function is named DeleteChannelByStatusAndId but only accepts channelId as a parameter—there is no status parameter or status-based filtering. This is inconsistent with the existing DeleteChannelByStatus(status int64) on line 870, which does filter by status. The current implementation simply loads a channel by ID and deletes it.

Consider renaming to DeleteChannelById to accurately reflect its behavior and avoid confusion.

Proposed rename
-func DeleteChannelByStatusAndId(channelId int) error {
+func DeleteChannelById(channelId int) error {
 	var channel Channel
 	if err := DB.First(&channel, channelId).Error; err != nil {
 		return err
 	}
 	return channel.Delete()
 }

Also update the call site in service/channel.go line 104:

-	err := model.DeleteChannelByStatusAndId(channelError.ChannelId)
+	err := model.DeleteChannelById(channelError.ChannelId)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func DeleteChannelByStatusAndId(channelId int) error {
var channel Channel
if err := DB.First(&channel, channelId).Error; err != nil {
return err
}
return channel.Delete()
}
func DeleteChannelById(channelId int) error {
var channel Channel
if err := DB.First(&channel, channelId).Error; err != nil {
return err
}
return channel.Delete()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/channel.go` around lines 875 - 881, Rename the misleading function
DeleteChannelByStatusAndId to DeleteChannelById and update its signature/usage
accordingly: keep the implementation that loads a Channel by ID and calls
channel.Delete(), and remove any expectation of a status parameter (the
status-based logic remains in DeleteChannelByStatus); then find and update all
call sites that reference DeleteChannelByStatusAndId (e.g., in the channel
service layer) to call DeleteChannelById instead so names accurately reflect
behavior.


func DeleteDisabledChannel() (int64, error) {
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
return result.RowsAffected, result.Error
Expand Down
6 changes: 6 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func InitOptionMap() {
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
common.OptionMap["AutomaticDisableChannelEnabled"] = strconv.FormatBool(common.AutomaticDisableChannelEnabled)
common.OptionMap["AutomaticEnableChannelEnabled"] = strconv.FormatBool(common.AutomaticEnableChannelEnabled)
common.OptionMap["AutomaticDeleteChannelEnabled"] = strconv.FormatBool(common.AutomaticDeleteChannelEnabled)
common.OptionMap["LogConsumeEnabled"] = strconv.FormatBool(common.LogConsumeEnabled)
common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled)
common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled)
Expand Down Expand Up @@ -170,6 +171,7 @@ func InitOptionMap() {
common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
common.OptionMap["AutomaticDeleteKeywords"] = operation_setting.AutomaticDeleteKeywordsToString()
common.OptionMap["AutomaticDisableStatusCodes"] = operation_setting.AutomaticDisableStatusCodesToString()
common.OptionMap["AutomaticRetryStatusCodes"] = operation_setting.AutomaticRetryStatusCodesToString()
common.OptionMap["ExposeRatioEnabled"] = strconv.FormatBool(ratio_setting.IsExposeRatioEnabled())
Expand Down Expand Up @@ -304,6 +306,8 @@ func updateOptionMap(key string, value string) (err error) {
common.AutomaticDisableChannelEnabled = boolValue
case "AutomaticEnableChannelEnabled":
common.AutomaticEnableChannelEnabled = boolValue
case "AutomaticDeleteChannelEnabled":
common.AutomaticDeleteChannelEnabled = boolValue
case "LogConsumeEnabled":
common.LogConsumeEnabled = boolValue
case "DisplayInCurrencyEnabled":
Expand Down Expand Up @@ -554,6 +558,8 @@ func updateOptionMap(key string, value string) (err error) {
setting.SensitiveWordsFromString(value)
case "AutomaticDisableKeywords":
operation_setting.AutomaticDisableKeywordsFromString(value)
case "AutomaticDeleteKeywords":
operation_setting.AutomaticDeleteKeywordsFromString(value)
case "AutomaticDisableStatusCodes":
err = operation_setting.AutomaticDisableStatusCodesFromString(value)
case "AutomaticRetryStatusCodes":
Expand Down
Binary file added new-api.tar
Binary file not shown.
35 changes: 35 additions & 0 deletions service/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,38 @@ func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool {
}
return true
}

func ShouldDeleteChannel(err *types.NewAPIError) bool {
if !common.AutomaticDeleteChannelEnabled {
return false
}
if err == nil {
return false
}
if types.IsSkipRetryError(err) {
return false
}

lowerMessage := strings.ToLower(err.Error())
search, _ := AcSearch(lowerMessage, operation_setting.AutomaticDeleteKeywords, true)
return search
}

func DeleteChannel(channelError types.ChannelError, reason string) {
common.SysLog(fmt.Sprintf("通道「%s」(#%d)发生错误,准备删除,原因:%s", channelError.ChannelName, channelError.ChannelId, reason))

if !channelError.AutoBan {
common.SysLog(fmt.Sprintf("通道「%s」(#%d)未启用自动禁用功能,跳过删除操作", channelError.ChannelName, channelError.ChannelId))
return
}

err := model.DeleteChannelByStatusAndId(channelError.ChannelId)
if err != nil {
common.SysLog(fmt.Sprintf("删除渠道失败(#%d): %v", channelError.ChannelId, err))
return
}
model.InitChannelCache()
subject := fmt.Sprintf("通道「%s」(#%d)已被自动删除", channelError.ChannelName, channelError.ChannelId)
content := fmt.Sprintf("通道「%s」(#%d)已被自动删除,原因:%s", channelError.ChannelName, channelError.ChannelId, reason)
NotifyRootUser(formatNotifyType(channelError.ChannelId, common.ChannelStatusUnknown), subject, content)
}
42 changes: 31 additions & 11 deletions service/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,18 @@ func checkRedirect(req *http.Request, via []*http.Request) error {

func InitHttpClient() {
transport := &http.Transport{
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
Proxy: http.ProxyFromEnvironment, // Support HTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: time.Duration(common.RelayResponseHeaderTimeout) * time.Second,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
Proxy: http.ProxyFromEnvironment, // Support HTTP_PROXY, HTTPS_PROXY, NO_PROXY env vars
}
if common.TLSInsecureSkipVerify {
transport.TLSClientConfig = common.InsecureTLSConfig
Expand Down Expand Up @@ -106,10 +114,18 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
switch parsedURL.Scheme {
case "http", "https":
transport := &http.Transport{
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
Proxy: http.ProxyURL(parsedURL),
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: time.Duration(common.RelayResponseHeaderTimeout) * time.Second,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
Proxy: http.ProxyURL(parsedURL),
}
if common.TLSInsecureSkipVerify {
transport.TLSClientConfig = common.InsecureTLSConfig
Expand Down Expand Up @@ -145,12 +161,16 @@ func NewProxyHttpClient(proxyURL string) (*http.Client, error) {
}

transport := &http.Transport{
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: time.Duration(common.RelayResponseHeaderTimeout) * time.Second,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: common.RelayMaxIdleConns,
MaxIdleConnsPerHost: common.RelayMaxIdleConnsPerHost,
ForceAttemptHTTP2: true,
}
if common.TLSInsecureSkipVerify {
transport.TLSClientConfig = common.InsecureTLSConfig
Expand Down
18 changes: 18 additions & 0 deletions setting/operation_setting/operation_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ func AutomaticDisableKeywordsFromString(s string) {
}
}
}

var AutomaticDeleteKeywords = []string{}

func AutomaticDeleteKeywordsToString() string {
return strings.Join(AutomaticDeleteKeywords, "\n")
}

func AutomaticDeleteKeywordsFromString(s string) {
AutomaticDeleteKeywords = []string{}
ak := strings.Split(s, "\n")
for _, k := range ak {
k = strings.TrimSpace(k)
k = strings.ToLower(k)
if k != "" {
AutomaticDeleteKeywords = append(AutomaticDeleteKeywords, k)
}
}
}
2 changes: 2 additions & 0 deletions web/classic/src/components/settings/OperationSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ const OperationSetting = () => {
QuotaRemindThreshold: 0,
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDeleteChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDeleteKeywords: '',
AutomaticDisableStatusCodes: '401',
AutomaticRetryStatusCodes:
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
Expand Down
Loading
Loading