diff --git a/controller/playground.go b/controller/playground.go index 501c4e156573..b3f54600547b 100644 --- a/controller/playground.go +++ b/controller/playground.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" relaycommon "github.com/QuantumNous/new-api/relay/common" @@ -25,7 +26,7 @@ func Playground(c *gin.Context) { useAccessToken := c.GetBool("use_access_token") if useAccessToken { - newAPIError = types.NewError(errors.New("暂不支持使用 access token"), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry()) + newAPIError = types.NewError(errors.New(i18n.T(c, i18n.MsgPlaygroundAccessTokenUnsupported)), types.ErrorCodeAccessDenied, types.ErrOptionWithSkipRetry()) return } diff --git a/controller/relay.go b/controller/relay.go index 5e2db44c25a4..fc4311dde8ba 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -12,6 +12,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/constant" "github.com/QuantumNous/new-api/dto" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/middleware" "github.com/QuantumNous/new-api/model" @@ -308,10 +309,10 @@ func getChannel(c *gin.Context, info *relaycommon.RelayInfo, retryParam *service info.PriceData.GroupRatioInfo = helper.HandleGroupRatio(c, info) if err != nil { - return nil, types.NewError(fmt.Errorf("获取分组 %s 下模型 %s 的可用渠道失败(retry): %s", selectGroup, info.OriginModelName, err.Error()), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) + return nil, types.NewError(errors.New(i18n.T(c, i18n.MsgChannelGetAvailableRetryFailed, map[string]any{"Group": selectGroup, "Model": info.OriginModelName, "Error": err.Error()})), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) } if channel == nil { - return nil, types.NewError(fmt.Errorf("分组 %s 下模型 %s 的可用渠道不存在(retry)", selectGroup, info.OriginModelName), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) + return nil, types.NewError(errors.New(i18n.T(c, i18n.MsgChannelNoAvailableRetry, map[string]any{"Group": selectGroup, "Model": info.OriginModelName})), types.ErrorCodeGetChannelFailed, types.ErrOptionWithSkipRetry()) } newAPIError := middleware.SetupContextForSelectedChannel(c, channel, info.OriginModelName) @@ -430,7 +431,7 @@ func RelayMidjourney(c *gin.Context) { if mjErr != nil { statusCode := http.StatusBadRequest if mjErr.Code == 30 { - mjErr.Result = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" + mjErr.Result = i18n.T(c, i18n.MsgChannelUpstreamSaturatedUpgrade) statusCode = http.StatusTooManyRequests } c.JSON(statusCode, gin.H{ @@ -605,7 +606,7 @@ func RelayTask(c *gin.Context) { // respondTaskError 统一输出 Task 错误响应(含 429 限流提示改写) func respondTaskError(c *gin.Context, taskErr *dto.TaskError) { if taskErr.StatusCode == http.StatusTooManyRequests { - taskErr.Message = "当前分组上游负载已饱和,请稍后再试" + taskErr.Message = i18n.T(c, i18n.MsgChannelUpstreamSaturated) } c.JSON(taskErr.StatusCode, taskErr) } diff --git a/i18n/keys.go b/i18n/keys.go index 6cf5c1bdc333..63c09db67713 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -38,6 +38,11 @@ const ( MsgAuthUserIdMismatch = "auth.user_id_mismatch" MsgAuthUserBanned = "auth.user_banned" MsgAuthInsufficientPrivilege = "auth.insufficient_privilege" + MsgAuthClientIpParseFailed = "auth.client_ip_parse_failed" + MsgAuthIpNotAllowed = "auth.ip_not_allowed" + MsgAuthGroupAccessDenied = "auth.group_access_denied" + MsgAuthGroupDeprecated = "auth.group_deprecated" + MsgAuthSpecificChannelDenied = "auth.specific_channel_denied" ) // Token related messages @@ -166,17 +171,30 @@ const ( // Channel related messages const ( - MsgChannelNotExists = "channel.not_exists" - MsgChannelIdFormatError = "channel.id_format_error" - MsgChannelNoAvailableKey = "channel.no_available_key" - MsgChannelGetListFailed = "channel.get_list_failed" - MsgChannelGetTagsFailed = "channel.get_tags_failed" - MsgChannelGetKeyFailed = "channel.get_key_failed" - MsgChannelGetOllamaFailed = "channel.get_ollama_failed" - MsgChannelQueryFailed = "channel.query_failed" - MsgChannelNoValidUpstream = "channel.no_valid_upstream" - MsgChannelUpstreamSaturated = "channel.upstream_saturated" - MsgChannelGetAvailableFailed = "channel.get_available_failed" + MsgChannelNotExists = "channel.not_exists" + MsgChannelIdFormatError = "channel.id_format_error" + MsgChannelNoAvailableKey = "channel.no_available_key" + MsgChannelGetListFailed = "channel.get_list_failed" + MsgChannelGetTagsFailed = "channel.get_tags_failed" + MsgChannelGetKeyFailed = "channel.get_key_failed" + MsgChannelGetOllamaFailed = "channel.get_ollama_failed" + MsgChannelQueryFailed = "channel.query_failed" + MsgChannelNoValidUpstream = "channel.no_valid_upstream" + MsgChannelUpstreamSaturated = "channel.upstream_saturated" + MsgChannelUpstreamSaturatedUpgrade = "channel.upstream_saturated_upgrade" + MsgChannelGetAvailableFailed = "channel.get_available_failed" + MsgChannelGetAvailableRetryFailed = "channel.get_available_retry_failed" + MsgChannelNoAvailableRetry = "channel.no_available_retry" +) + +// Playground related messages +const ( + MsgPlaygroundAccessTokenUnsupported = "playground.access_token_unsupported" +) + +// Task related messages +const ( + MsgTaskFileTooLarge = "task.file_too_large" ) // Model related messages diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 198aa27412b8..c01454f6fbe0 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -32,6 +32,11 @@ auth.user_id_format_error: "Unauthorized, New-Api-User header format error" auth.user_id_mismatch: "Unauthorized, New-Api-User does not match logged in user" auth.user_banned: "User has been banned" auth.insufficient_privilege: "Unauthorized, insufficient privileges" +auth.client_ip_parse_failed: "Failed to parse client IP address" +auth.ip_not_allowed: "Your IP is not in this token's allowed IP list" +auth.group_access_denied: "You do not have permission to access group {{.Group}}" +auth.group_deprecated: "Group {{.Group}} has been deprecated" +auth.specific_channel_denied: "Regular users cannot specify a channel" # Token messages token.name_too_long: "Token name is too long" @@ -154,7 +159,16 @@ channel.get_ollama_failed: "Failed to get Ollama models" channel.query_failed: "Failed to query channel" channel.no_valid_upstream: "No valid upstream channel" channel.upstream_saturated: "Current group upstream load is saturated, please try again later" +channel.upstream_saturated_upgrade: "Current group upstream load is saturated, please try again later or upgrade your account for better service quality" channel.get_available_failed: "Failed to get available channels for model {{.Model}} under group {{.Group}}" +channel.get_available_retry_failed: "Failed to get available channels for model {{.Model}} under group {{.Group}} (retry): {{.Error}}" +channel.no_available_retry: "No available channel for model {{.Model}} under group {{.Group}} (retry)" + +# Playground messages +playground.access_token_unsupported: "Access tokens are not supported here" + +# Task messages +task.file_too_large: "File {{.File}} exceeds the size limit. Maximum allowed is {{.MaxMB}} MB" # Model messages model.name_empty: "Model name cannot be empty" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 45a10a584e53..a23fc19d4524 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -33,6 +33,11 @@ auth.user_id_format_error: "无权进行此操作,New-Api-User 格式错误" auth.user_id_mismatch: "无权进行此操作,New-Api-User 与登录用户不匹配" auth.user_banned: "用户已被封禁" auth.insufficient_privilege: "无权进行此操作,权限不足" +auth.client_ip_parse_failed: "无法解析客户端 IP 地址" +auth.ip_not_allowed: "您的 IP 不在令牌允许访问的列表中" +auth.group_access_denied: "无权访问 {{.Group}} 分组" +auth.group_deprecated: "分组 {{.Group}} 已被弃用" +auth.specific_channel_denied: "普通用户不支持指定渠道" # Token messages token.name_too_long: "令牌名称过长" @@ -155,7 +160,16 @@ channel.get_ollama_failed: "获取Ollama模型失败" channel.query_failed: "查询渠道失败" channel.no_valid_upstream: "无有效上游渠道" channel.upstream_saturated: "当前分组上游负载已饱和,请稍后再试" +channel.upstream_saturated_upgrade: "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量" channel.get_available_failed: "获取分组 {{.Group}} 下模型 {{.Model}} 的可用渠道失败" +channel.get_available_retry_failed: "获取分组 {{.Group}} 下模型 {{.Model}} 的可用渠道失败(retry):{{.Error}}" +channel.no_available_retry: "分组 {{.Group}} 下模型 {{.Model}} 的可用渠道不存在(retry)" + +# Playground messages +playground.access_token_unsupported: "暂不支持使用 access token" + +# Task messages +task.file_too_large: "文件 {{.File}} 大小超过限制,最大允许 {{.MaxMB}} MB" # Model messages model.name_empty: "模型名称不能为空" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index ad5a6eae3220..68e387b34014 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -33,6 +33,11 @@ auth.user_id_format_error: "無權進行此操作,New-Api-User 格式錯誤" auth.user_id_mismatch: "無權進行此操作,New-Api-User 與登入使用者不匹配" auth.user_banned: "使用者已被封禁" auth.insufficient_privilege: "無權進行此操作,權限不足" +auth.client_ip_parse_failed: "無法解析客戶端 IP 位址" +auth.ip_not_allowed: "您的 IP 不在令牌允許存取的清單中" +auth.group_access_denied: "無權存取 {{.Group}} 分組" +auth.group_deprecated: "分組 {{.Group}} 已被棄用" +auth.specific_channel_denied: "普通使用者不支援指定管道" # Token messages token.name_too_long: "令牌名稱過長" @@ -155,7 +160,16 @@ channel.get_ollama_failed: "獲取Ollama模型失敗" channel.query_failed: "查詢管道失敗" channel.no_valid_upstream: "無有效上游管道" channel.upstream_saturated: "當前分組上游負載已飽和,請稍後再試" +channel.upstream_saturated_upgrade: "當前分組負載已飽和,請稍後再試,或升級帳戶以提升服務品質" channel.get_available_failed: "獲取分組 {{.Group}} 下模型 {{.Model}} 的可用管道失敗" +channel.get_available_retry_failed: "獲取分組 {{.Group}} 下模型 {{.Model}} 的可用管道失敗(retry):{{.Error}}" +channel.no_available_retry: "分組 {{.Group}} 下模型 {{.Model}} 的可用管道不存在(retry)" + +# Playground messages +playground.access_token_unsupported: "暫不支援使用 access token" + +# Task messages +task.file_too_large: "檔案 {{.File}} 大小超過限制,最大允許 {{.MaxMB}} MB" # Model messages model.name_empty: "模型名稱不能為空" diff --git a/middleware/auth.go b/middleware/auth.go index 23d933fbe0c1..c4683b578b19 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -354,11 +354,11 @@ func TokenAuth() func(c *gin.Context) { logger.LogDebug(c, "Token has IP restrictions, checking client IP %s", clientIp) ip := net.ParseIP(clientIp) if ip == nil { - abortWithOpenAiMessage(c, http.StatusForbidden, "无法解析客户端 IP 地址") + abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthClientIpParseFailed)) return } if common.IsIpInCIDRList(ip, allowIps) == false { - abortWithOpenAiMessage(c, http.StatusForbidden, "您的 IP 不在令牌允许访问的列表中", types.ErrorCodeAccessDenied) + abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthIpNotAllowed), types.ErrorCodeAccessDenied) return } logger.LogDebug(c, "Client IP %s passed the token IP restrictions check", clientIp) @@ -384,13 +384,13 @@ func TokenAuth() func(c *gin.Context) { if tokenGroup != "" { // check common.UserUsableGroups[userGroup] if _, ok := service.GetUserUsableGroups(userGroup)[tokenGroup]; !ok { - abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("无权访问 %s 分组", tokenGroup)) + abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthGroupAccessDenied, map[string]any{"Group": tokenGroup})) return } // check group in common.GroupRatio if !ratio_setting.ContainsGroupRatio(tokenGroup) { if tokenGroup != "auto" { - abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup)) + abortWithOpenAiMessage(c, http.StatusForbidden, common.TranslateMessage(c, i18n.MsgAuthGroupDeprecated, map[string]any{"Group": tokenGroup})) return } } @@ -431,8 +431,9 @@ func SetupContextForToken(c *gin.Context, token *model.Token, parts ...string) e c.Set("specific_channel_id", parts[1]) } else { c.Header("specific_channel_version", "701e3ae1dc3f7975556d354e0675168d004891c8") - abortWithOpenAiMessage(c, http.StatusForbidden, "普通用户不支持指定渠道") - return fmt.Errorf("普通用户不支持指定渠道") + msg := common.TranslateMessage(c, i18n.MsgAuthSpecificChannelDenied) + abortWithOpenAiMessage(c, http.StatusForbidden, msg) + return fmt.Errorf("%s", msg) } } return nil diff --git a/middleware/model-rate-limit.go b/middleware/model-rate-limit.go index 80a3995df097..b3c91fdb7bd5 100644 --- a/middleware/model-rate-limit.go +++ b/middleware/model-rate-limit.go @@ -10,6 +10,7 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/common/limiter" "github.com/QuantumNous/new-api/constant" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/setting" "github.com/gin-gonic/gin" @@ -90,7 +91,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g return } if !allowed { - abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到请求数限制:%d分钟内最多请求%d次", setting.ModelRequestRateLimitDurationMinutes, successMaxCount)) + abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": successMaxCount})) return } @@ -114,7 +115,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g } if !allowed { - abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount)) + abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitTotalReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": totalMaxCount})) } } diff --git a/relay/channel/task/jimeng/adaptor.go b/relay/channel/task/jimeng/adaptor.go index e6211b1e4f47..73f68b07c635 100644 --- a/relay/channel/task/jimeng/adaptor.go +++ b/relay/channel/task/jimeng/adaptor.go @@ -15,6 +15,7 @@ import ( "time" "github.com/QuantumNous/new-api/common" + "github.com/QuantumNous/new-api/i18n" "github.com/QuantumNous/new-api/model" "github.com/samber/lo" @@ -145,7 +146,7 @@ func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayIn for _, fileHeader := range files { // 检查文件大小 if fileHeader.Size > MaxFileSize { - return nil, fmt.Errorf("文件 %s 大小超过限制,最大允许 %d MB", fileHeader.Filename, MaxFileSize/(1024*1024)) + return nil, fmt.Errorf("%s", i18n.T(c, i18n.MsgTaskFileTooLarge, map[string]any{"File": fileHeader.Filename, "MaxMB": MaxFileSize / (1024 * 1024)})) } file, err := fileHeader.Open()