feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
+2
-2
@@ -20,7 +20,7 @@ import (
|
||||
// GetAudioDuration 使用纯 Go 库获取音频文件的时长(秒)。
|
||||
// 它不再依赖外部的 ffmpeg 或 ffprobe 程序。
|
||||
func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duration float64, err error) {
|
||||
SysLog(fmt.Sprintf("GetAudioDuration: ext=%s", ext))
|
||||
SysLog(LogText("GetAudioDuration: ext=%s", ext))
|
||||
ext = strings.ToLower(ext)
|
||||
// 根据文件扩展名选择解析器
|
||||
switch ext {
|
||||
@@ -46,7 +46,7 @@ func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duratio
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported audio format: %s", ext)
|
||||
}
|
||||
SysLog(fmt.Sprintf("GetAudioDuration: duration=%f", duration))
|
||||
SysLog(LogText("GetAudioDuration: duration=%f", duration))
|
||||
return duration, err
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ func CreateBodyStorage(data []byte) (BodyStorage, error) {
|
||||
storage, err := newDiskStorage(data, GetDiskCachePath())
|
||||
if err != nil {
|
||||
// 如果磁盘存储失败,回退到内存存储
|
||||
SysError(fmt.Sprintf("failed to create disk storage, falling back to memory: %v", err))
|
||||
SysError(LogText("failed to create disk storage, falling back to memory: %v", err))
|
||||
return newMemoryStorage(data), nil
|
||||
}
|
||||
return storage, nil
|
||||
|
||||
@@ -35,6 +35,11 @@ var DefaultCollapseSidebar = false // default value of collapse sidebar
|
||||
var SessionSecret = uuid.New().String()
|
||||
var CryptoSecret = uuid.New().String()
|
||||
var SessionCookieSecure = false
|
||||
|
||||
// DefaultLanguage is the DEFAULT_LANGUAGE environment variable: the language of
|
||||
// server log lines and of text for a reader who states no language: en, zh-CN
|
||||
// or zh-TW. English when empty.
|
||||
var DefaultLanguage string
|
||||
var SessionCookieTrustedURLs []string
|
||||
|
||||
const (
|
||||
@@ -128,7 +133,11 @@ var QuotaForInvitee = 0
|
||||
var ChannelDisableThreshold = 5.0
|
||||
var AutomaticDisableChannelEnabled = false
|
||||
var AutomaticEnableChannelEnabled = false
|
||||
var QuotaRemindEnabled = true
|
||||
var QuotaRemindThreshold = 1000
|
||||
|
||||
// PreConsumedQuota is retained for old option clients; token reservations now
|
||||
// use quota_setting.pre_consume_multiplier and the estimated input cost.
|
||||
var PreConsumedQuota = 500
|
||||
|
||||
var RetryTimes = 0
|
||||
@@ -215,6 +224,10 @@ var (
|
||||
GlobalWebRateLimitNum int
|
||||
GlobalWebRateLimitDuration int64
|
||||
|
||||
GlobalStaticRateLimitEnable bool
|
||||
GlobalStaticRateLimitNum int
|
||||
GlobalStaticRateLimitDuration int64
|
||||
|
||||
CriticalRateLimitEnable bool
|
||||
CriticalRateLimitNum = 20
|
||||
CriticalRateLimitDuration int64 = 20 * 60
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 容器自身资源采样。
|
||||
//
|
||||
// 历史实现直接读取宿主机口径(/proc/stat 与 /proc/meminfo),把同节点其它
|
||||
// 工作负载的占用也算成本进程过载,导致正常的 relay 流量被 503 熔断。这里
|
||||
// 改为优先读取当前容器的 cgroup 配额与用量,只有在拿不到 cgroup 数据时才由
|
||||
// 调用方回退到宿主机口径。
|
||||
|
||||
const (
|
||||
defaultCgroupRoot = "/sys/fs/cgroup"
|
||||
// cgroup v1 用接近 int64 上限的值表示"未限制"。
|
||||
unlimitedMemoryBytes = uint64(1) << 60
|
||||
)
|
||||
|
||||
// containerUsage 记录上一次采样点,用来计算区间内的 CPU 占用。
|
||||
type containerUsage struct {
|
||||
cgroupRoot string
|
||||
// schedulableCPUs 是本进程可调度的逻辑 CPU 数(Linux 上含 cpuset/affinity 限制)。
|
||||
schedulableCPUs float64
|
||||
cpuLastUsec uint64
|
||||
cpuLastSampled time.Time
|
||||
}
|
||||
|
||||
func newContainerUsage() *containerUsage {
|
||||
// runtime.NumCPU 在 Linux 上于进程启动时按 sched_getaffinity 计数,不受 GOMAXPROCS 影响。
|
||||
return &containerUsage{cgroupRoot: defaultCgroupRoot, schedulableCPUs: float64(runtime.NumCPU())}
|
||||
}
|
||||
|
||||
// cpuSaturation 返回相对容器可用 CPU 的使用率(百分比)。
|
||||
// 可用 CPU 取 cgroup 配额与可调度 CPU 的较小者:配额大于 cpuset 或节点核数时,
|
||||
// 按配额计算会让容器打满也无法触发保护。
|
||||
// ok=false 表示当前环境没有可用的 cgroup CPU 数据,调用方应回退到宿主机口径。
|
||||
func (u *containerUsage) cpuSaturation(now time.Time) (float64, bool) {
|
||||
limitCores, ok := u.readCgroupCPUQuotaCores()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if u.schedulableCPUs > 0 {
|
||||
limitCores = min(limitCores, u.schedulableCPUs)
|
||||
}
|
||||
usageUsec, ok := u.readCgroupCPUUsageUsec()
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if u.cpuLastSampled.IsZero() || usageUsec < u.cpuLastUsec || !now.After(u.cpuLastSampled) {
|
||||
u.cpuLastUsec = usageUsec
|
||||
u.cpuLastSampled = now
|
||||
return 0, false
|
||||
}
|
||||
elapsed := now.Sub(u.cpuLastSampled)
|
||||
if elapsed < time.Second {
|
||||
return 0, false
|
||||
}
|
||||
deltaUsec := usageUsec - u.cpuLastUsec
|
||||
u.cpuLastUsec = usageUsec
|
||||
u.cpuLastSampled = now
|
||||
|
||||
usedCores := float64(deltaUsec) / 1e6 / elapsed.Seconds()
|
||||
return usedCores / limitCores * 100, true
|
||||
}
|
||||
|
||||
// memoryUsage 返回容器内存工作集相对上限的使用率(百分比)。
|
||||
// 工作集 = 用量 - 非活跃文件缓存,与 docker stats、kubelet 一致:用量包含可回收的
|
||||
// 页缓存,写日志或文件后会长期贴近上限,不代表内存压力。
|
||||
// ok=false 时调用方回退宿主机口径(同样不含缓存):内存未限制、文件不可读、
|
||||
// 或 memory.stat 缺少非活跃缓存字段。宁可回退旧口径,也不把含缓存的用量当作压力。
|
||||
func (u *containerUsage) memoryUsage() (float64, bool) {
|
||||
usagePath := filepath.Join(u.cgroupRoot, "memory.current")
|
||||
limitPath := filepath.Join(u.cgroupRoot, "memory.max")
|
||||
statPath := filepath.Join(u.cgroupRoot, "memory.stat")
|
||||
inactiveFileKey := "inactive_file"
|
||||
if _, err := os.Stat(usagePath); err != nil {
|
||||
v1Dir := filepath.Join(u.cgroupRoot, "memory")
|
||||
usagePath = filepath.Join(v1Dir, "memory.usage_in_bytes")
|
||||
limitPath = filepath.Join(v1Dir, "memory.limit_in_bytes")
|
||||
statPath = filepath.Join(v1Dir, "memory.stat")
|
||||
inactiveFileKey = "total_inactive_file"
|
||||
}
|
||||
|
||||
// cgroup v2 未限制时 memory.max 为 "max",解析失败即视为不可用。
|
||||
limit, ok := readCgroupUint(limitPath)
|
||||
if !ok || limit == 0 || limit >= unlimitedMemoryBytes {
|
||||
return 0, false
|
||||
}
|
||||
usage, ok := readCgroupUint(usagePath)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
inactiveFile, ok := readCgroupStatValue(statPath, inactiveFileKey)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
// 用量与 memory.stat 不是同一时刻的快照,缓存可能短暂大于用量,按 0 处理防止下溢。
|
||||
workingSet := usage - min(inactiveFile, usage)
|
||||
return float64(workingSet) / float64(limit) * 100, true
|
||||
}
|
||||
|
||||
// readCgroupCPUQuotaCores 返回 cgroup CPU 配额(核)。ok=false 表示未限制或不可读。
|
||||
func (u *containerUsage) readCgroupCPUQuotaCores() (float64, bool) {
|
||||
if raw, err := os.ReadFile(filepath.Join(u.cgroupRoot, "cpu.max")); err == nil {
|
||||
quotaText, periodText, found := strings.Cut(strings.TrimSpace(string(raw)), " ")
|
||||
if !found || quotaText == "max" {
|
||||
return 0, false
|
||||
}
|
||||
quota, err := strconv.ParseUint(quotaText, 10, 64)
|
||||
if err != nil || quota == 0 {
|
||||
return 0, false
|
||||
}
|
||||
period, err := strconv.ParseUint(strings.TrimSpace(periodText), 10, 64)
|
||||
if err != nil || period == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return float64(quota) / float64(period), true
|
||||
}
|
||||
|
||||
// cgroup v1 未限制时 cfs_quota_us 为 -1,解析失败即视为不可用。
|
||||
v1Dir := filepath.Join(u.cgroupRoot, "cpu")
|
||||
quota, ok := readCgroupUint(filepath.Join(v1Dir, "cpu.cfs_quota_us"))
|
||||
if !ok || quota == 0 {
|
||||
return 0, false
|
||||
}
|
||||
period, ok := readCgroupUint(filepath.Join(v1Dir, "cpu.cfs_period_us"))
|
||||
if !ok || period == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return float64(quota) / float64(period), true
|
||||
}
|
||||
|
||||
func (u *containerUsage) readCgroupCPUUsageUsec() (uint64, bool) {
|
||||
statPath := filepath.Join(u.cgroupRoot, "cpu.stat")
|
||||
if _, err := os.Stat(statPath); err == nil {
|
||||
return readCgroupStatValue(statPath, "usage_usec")
|
||||
}
|
||||
// cgroup v1 的 cpuacct.usage 单位是纳秒。
|
||||
nanos, ok := readCgroupUint(filepath.Join(u.cgroupRoot, "cpuacct", "cpuacct.usage"))
|
||||
return nanos / 1000, ok
|
||||
}
|
||||
|
||||
// readCgroupUint 读取只含一个无符号整数的 cgroup 文件。
|
||||
func readCgroupUint(path string) (uint64, bool) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
value, err := strconv.ParseUint(strings.TrimSpace(string(raw)), 10, 64)
|
||||
return value, err == nil
|
||||
}
|
||||
|
||||
// readCgroupStatValue 从 "key value" 逐行格式的 cgroup 统计文件(cpu.stat、memory.stat)中读取一项。
|
||||
func readCgroupStatValue(path string, key string) (uint64, bool) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
for line := range strings.SplitSeq(string(raw), "\n") {
|
||||
name, valueText, found := strings.Cut(line, " ")
|
||||
if !found || name != key {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.ParseUint(strings.TrimSpace(valueText), 10, 64)
|
||||
return value, err == nil
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
+2
-2
@@ -84,7 +84,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
return err2
|
||||
}
|
||||
if SMTPServer == "" && SMTPAccount == "" {
|
||||
return fmt.Errorf("SMTP 服务器未配置")
|
||||
return NewMessage("SMTP server is not configured")
|
||||
}
|
||||
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
||||
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
@@ -130,7 +130,7 @@ func SendEmail(subject string, receiver string, content string) error {
|
||||
}
|
||||
err = client.Quit()
|
||||
if err != nil {
|
||||
SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err))
|
||||
SysError(LogText("failed to send email to %s: %v", receiver, err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -21,6 +21,8 @@ var defaultEndpointInfoMap = map[constant.EndpointType]EndpointInfo{
|
||||
constant.EndpointTypeOpenAIResponse: {Path: "/v1/responses", Method: "POST"},
|
||||
constant.EndpointTypeOpenAIResponseCompact: {Path: "/v1/responses/compact", Method: "POST"},
|
||||
constant.EndpointTypeOpenAIAlphaSearch: {Path: "/v1/alpha/search", Method: "POST"},
|
||||
constant.EndpointTypeDeepSeekToolsSearch: {Path: "/v1/tools/search", Method: "POST"},
|
||||
constant.EndpointTypeDeepSeekToolsFetch: {Path: "/v1/tools/fetch", Method: "POST"},
|
||||
constant.EndpointTypeAnthropic: {Path: "/v1/messages", Method: "POST"},
|
||||
constant.EndpointTypeGemini: {Path: "/v1beta/models/{model}:generateContent", Method: "POST"},
|
||||
constant.EndpointTypeJinaRerank: {Path: "/v1/rerank", Method: "POST"},
|
||||
|
||||
@@ -4,6 +4,13 @@ import "github.com/QuantumNous/new-api/constant"
|
||||
|
||||
// GetEndpointTypesByChannelType 获取渠道最优先端点类型(所有的渠道都支持 OpenAI 端点)
|
||||
func GetEndpointTypesByChannelType(channelType int, modelName string) []constant.EndpointType {
|
||||
// The DeepSeek Search and Fetch routing names serve only their endpoint.
|
||||
switch modelName {
|
||||
case constant.DeepSeekSearchModel:
|
||||
return []constant.EndpointType{constant.EndpointTypeDeepSeekToolsSearch}
|
||||
case constant.DeepSeekFetchModel:
|
||||
return []constant.EndpointType{constant.EndpointTypeDeepSeekToolsFetch}
|
||||
}
|
||||
var endpointTypes []constant.EndpointType
|
||||
switch channelType {
|
||||
case constant.ChannelTypeJina:
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
@@ -12,7 +11,7 @@ func GetEnvOrDefault(env string, defaultValue int) int {
|
||||
}
|
||||
num, err := strconv.Atoi(os.Getenv(env))
|
||||
if err != nil {
|
||||
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
||||
SysError(LogText("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
||||
return defaultValue
|
||||
}
|
||||
return num
|
||||
@@ -31,7 +30,7 @@ func GetEnvOrDefaultBool(env string, defaultValue bool) bool {
|
||||
}
|
||||
b, err := strconv.ParseBool(os.Getenv(env))
|
||||
if err != nil {
|
||||
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %t", env, err.Error(), defaultValue))
|
||||
SysError(LogText("failed to parse %s: %s, using default value: %t", env, err.Error(), defaultValue))
|
||||
return defaultValue
|
||||
}
|
||||
return b
|
||||
|
||||
+105
-3
@@ -4,21 +4,26 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/relaykit/dto"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/net/http/httpproxy"
|
||||
)
|
||||
|
||||
const KeyRequestBody = "key_request_body"
|
||||
const KeyBodyStorage = "key_body_storage"
|
||||
const keyBodyReadError = "key_body_read_error"
|
||||
|
||||
var ErrRequestBodyTooLarge = errors.New("request body too large")
|
||||
|
||||
@@ -57,6 +62,12 @@ func GetRequestBody(c *gin.Context) (io.Seeker, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// A failed read has consumed and closed the body. Later readers get the
|
||||
// same error instead of "http: invalid Read on closed Body".
|
||||
if readErr, failed := c.Get(keyBodyReadError); failed {
|
||||
return nil, readErr.(error)
|
||||
}
|
||||
|
||||
maxMB := constant.MaxRequestBodyMB
|
||||
if maxMB <= 0 {
|
||||
maxMB = 128 // 默认 128MB
|
||||
@@ -71,8 +82,9 @@ func GetRequestBody(c *gin.Context) (io.Seeker, error) {
|
||||
|
||||
if err != nil {
|
||||
if IsRequestBodyTooLargeError(err) {
|
||||
return nil, errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
|
||||
err = errors.Wrap(ErrRequestBodyTooLarge, fmt.Sprintf("request body exceeds %d MB", maxMB))
|
||||
}
|
||||
c.Set(keyBodyReadError, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -166,6 +178,62 @@ func GetContextKeyString(c *gin.Context, key constant.ContextKey) string {
|
||||
return c.GetString(string(key))
|
||||
}
|
||||
|
||||
// presetUpstreamHosts are the hosts, with any port, of the default base URLs
|
||||
// new-api presets for channel types. They are public endpoints, so errors keep
|
||||
// them readable. The port keeps a configured localhost upstream apart from the
|
||||
// Ollama preset.
|
||||
var presetUpstreamHosts = sync.OnceValue(func() map[string]struct{} {
|
||||
hosts := make(map[string]struct{}, len(constant.ChannelBaseURLs))
|
||||
for _, baseURL := range constant.ChannelBaseURLs {
|
||||
if parsed, err := url.Parse(baseURL); err == nil && parsed.Host != "" {
|
||||
hosts[strings.ToLower(parsed.Host)] = struct{}{}
|
||||
}
|
||||
}
|
||||
return hosts
|
||||
})
|
||||
|
||||
// isPresetUpstreamHost reports whether address points at one of the preset
|
||||
// default base URL hosts.
|
||||
func isPresetUpstreamHost(address string) bool {
|
||||
rawURL := strings.TrimSpace(address)
|
||||
if !strings.Contains(rawURL, "://") {
|
||||
rawURL = "//" + rawURL
|
||||
}
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, preset := presetUpstreamHosts()[strings.ToLower(parsed.Host)]
|
||||
return preset
|
||||
}
|
||||
|
||||
// UpstreamAddresses returns the addresses masked in errors shown to users: the
|
||||
// upstream the admin configured for the selected channel and the proxies its
|
||||
// requests may go through. Preset default base URLs are public and stay
|
||||
// readable. The origin actually requested only counts for a configured base
|
||||
// URL or an Advanced Custom channel, whose routes may target absolute URLs;
|
||||
// other adaptors build official endpoints themselves.
|
||||
func UpstreamAddresses(c *gin.Context) []string {
|
||||
// A channel without its own proxy dials through the environment proxy.
|
||||
environmentProxy := httpproxy.FromEnvironment()
|
||||
candidates := []string{environmentProxy.HTTPProxy, environmentProxy.HTTPSProxy}
|
||||
if setting, ok := GetContextKeyType[dto.ChannelSettings](c, constant.ContextKeyChannelSetting); ok {
|
||||
candidates = append(candidates, setting.Proxy)
|
||||
}
|
||||
baseURL := strings.TrimSpace(GetContextKeyString(c, constant.ContextKeyChannelBaseUrl))
|
||||
candidates = append(candidates, baseURL)
|
||||
if GetContextKeyInt(c, constant.ContextKeyChannelType) == constant.ChannelTypeAdvancedCustom || (baseURL != "" && !isPresetUpstreamHost(baseURL)) {
|
||||
candidates = append(candidates, GetContextKeyString(c, constant.ContextKeyUpstreamRequestOrigin))
|
||||
}
|
||||
addresses := make([]string, 0, len(candidates))
|
||||
for _, address := range candidates {
|
||||
if strings.TrimSpace(address) != "" && !isPresetUpstreamHost(address) {
|
||||
addresses = append(addresses, address)
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
func GetContextKeyInt(c *gin.Context, key constant.ContextKey) int {
|
||||
return c.GetInt(string(key))
|
||||
}
|
||||
@@ -196,11 +264,45 @@ func GetContextKeyType[T any](c *gin.Context, key constant.ContextKey) (T, bool)
|
||||
return t, false
|
||||
}
|
||||
|
||||
// ApiError responds with err's text. When err is or wraps a *Message, the
|
||||
// response also carries message_key and message_params for the web console.
|
||||
func ApiError(c *gin.Context, err error) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
ApiErrorStatus(c, http.StatusOK, err)
|
||||
}
|
||||
|
||||
// ApiErrorStatus is ApiError with another HTTP status. fields adds response
|
||||
// fields such as a stable error code or data.
|
||||
func ApiErrorStatus(c *gin.Context, status int, err error, fields ...gin.H) {
|
||||
body := gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
}
|
||||
var message *Message
|
||||
if errors.As(err, &message) {
|
||||
maps.Copy(body, message.Fields())
|
||||
}
|
||||
for _, extra := range fields {
|
||||
maps.Copy(body, extra)
|
||||
}
|
||||
c.JSON(status, body)
|
||||
}
|
||||
|
||||
// ApiErrorT responds with a web console message; key is the English source
|
||||
// text, which the web console translates with params.
|
||||
func ApiErrorT(c *gin.Context, key string, params ...map[string]any) {
|
||||
ApiError(c, NewMessage(key, params...))
|
||||
}
|
||||
|
||||
// ApiSuccessT is the success counterpart of ApiErrorT.
|
||||
func ApiSuccessT(c *gin.Context, key string, data any, params ...map[string]any) {
|
||||
message := NewMessage(key, params...)
|
||||
body := gin.H{
|
||||
"success": true,
|
||||
"message": message.Error(),
|
||||
"data": data,
|
||||
}
|
||||
maps.Copy(body, message.Fields())
|
||||
c.JSON(http.StatusOK, body)
|
||||
}
|
||||
|
||||
func ApiErrorMsg(c *gin.Context, msg string) {
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
@@ -16,7 +15,7 @@ func init() {
|
||||
if stopChan, ok := ctx.Value("stop_chan").(chan bool); ok {
|
||||
SafeSendBool(stopChan, true)
|
||||
}
|
||||
SysError(fmt.Sprintf("panic in gopool.RelayPool: %v", i))
|
||||
SysError(LogText("panic in gopool.RelayPool: %v", i))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+13
-7
@@ -47,12 +47,14 @@ func InitEnv() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
DefaultLanguage = os.Getenv("DEFAULT_LANGUAGE")
|
||||
SetLogLanguage(DefaultLanguage)
|
||||
|
||||
if os.Getenv("SESSION_SECRET") != "" {
|
||||
ss := os.Getenv("SESSION_SECRET")
|
||||
if ss == "random_string" {
|
||||
log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
|
||||
log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
|
||||
log.Fatal("Please set SESSION_SECRET to a random string.")
|
||||
log.Println(LogText("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string."))
|
||||
log.Fatal(LogText("Please set SESSION_SECRET to a random string."))
|
||||
} else {
|
||||
SessionSecret = ss
|
||||
}
|
||||
@@ -95,7 +97,7 @@ func InitEnv() {
|
||||
if tr.TLSClientConfig != nil {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
} else {
|
||||
tr.TLSClientConfig = InsecureTLSConfig
|
||||
tr.TLSClientConfig = InsecureTLSConfig.Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +130,10 @@ func InitEnv() {
|
||||
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 120)
|
||||
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
||||
|
||||
GlobalStaticRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_STATIC_RATE_LIMIT_ENABLE", false)
|
||||
GlobalStaticRateLimitNum = GetEnvOrDefault("GLOBAL_STATIC_RATE_LIMIT", 1000)
|
||||
GlobalStaticRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_STATIC_RATE_LIMIT_DURATION", 180))
|
||||
|
||||
CriticalRateLimitEnable = GetEnvOrDefaultBool("CRITICAL_RATE_LIMIT_ENABLE", true)
|
||||
CriticalRateLimitNum = GetEnvOrDefault("CRITICAL_RATE_LIMIT", 20)
|
||||
CriticalRateLimitDuration = int64(GetEnvOrDefault("CRITICAL_RATE_LIMIT_DURATION", 20*60))
|
||||
@@ -147,7 +153,7 @@ func initUserSessionSettings() {
|
||||
|
||||
const secondsPerDay = 24 * 60 * 60
|
||||
if int64(UserSessionRevokedRetentionDays) > math.MaxInt64/secondsPerDay {
|
||||
SysError(fmt.Sprintf(
|
||||
SysError(LogText(
|
||||
"USER_SESSION_REVOKED_RETENTION_DAYS is too large, using default value: %d",
|
||||
DefaultUserSessionRevokedRetentionDays,
|
||||
))
|
||||
@@ -157,7 +163,7 @@ func initUserSessionSettings() {
|
||||
if UserSessionIssuanceWindowSeconds > retentionSeconds {
|
||||
configuredWindow := UserSessionIssuanceWindowSeconds
|
||||
UserSessionIssuanceWindowSeconds = retentionSeconds
|
||||
SysError(fmt.Sprintf(
|
||||
SysError(LogText(
|
||||
"USER_SESSION_ISSUANCE_WINDOW_SECONDS exceeds revoked retention; configured_window_seconds=%d revoked_retention_seconds=%d effective_window_seconds=%d",
|
||||
configuredWindow,
|
||||
retentionSeconds,
|
||||
@@ -169,7 +175,7 @@ func initUserSessionSettings() {
|
||||
func positiveUserSessionEnv(name string, fallback int) int {
|
||||
value := GetEnvOrDefault(name, fallback)
|
||||
if value <= 0 {
|
||||
SysError(fmt.Sprintf("%s must be positive, using default value: %d", name, fallback))
|
||||
SysError(LogText("%s must be positive, using default value: %d", name, fallback))
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
|
||||
@@ -28,7 +28,7 @@ func New(ctx context.Context, r *redis.Client) *RedisLimiter {
|
||||
// 预加载脚本
|
||||
limitSHA, err := r.ScriptLoad(ctx, rateLimitScript).Result()
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("Failed to load rate limit script: %v", err))
|
||||
common.SysLog(common.LogText("Failed to load rate limit script: %v", err))
|
||||
}
|
||||
instance = &RedisLimiter{
|
||||
client: r,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Server log lines are English. logTextZhCN holds the Chinese text of the
|
||||
// lines that have one, keyed by their English format.
|
||||
//
|
||||
//go:embed log_text.zh-CN.json
|
||||
var logTextZhCN []byte
|
||||
|
||||
// logTexts is the translation table DEFAULT_LANGUAGE selected; nil prints English.
|
||||
var logTexts map[string]string
|
||||
|
||||
// SetLogLanguage selects the language of server log lines. A language that
|
||||
// starts with "zh" prints the Chinese text; anything else prints English.
|
||||
func SetLogLanguage(language string) {
|
||||
logTexts = nil
|
||||
if !strings.HasPrefix(strings.ToLower(strings.TrimSpace(language)), "zh") {
|
||||
return
|
||||
}
|
||||
_ = Unmarshal(logTextZhCN, &logTexts)
|
||||
}
|
||||
|
||||
// LogText formats a server log line in the language of DEFAULT_LANGUAGE. format is
|
||||
// the English text and the key of its translation in log_text.zh-CN.json.
|
||||
func LogText(format string, args ...any) string {
|
||||
if translated, ok := logTexts[format]; ok {
|
||||
return fmt.Sprintf(translated, args...)
|
||||
}
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"%d quota points": "%d 点额度",
|
||||
"%s after refresh: %s": "%s(刷新凭证后重试): %s",
|
||||
"%s failed: %s": "%s 构建失败,%s",
|
||||
"%s must be positive, using default value: %d": "%s 必须大于 0,已使用默认值: %d",
|
||||
"%s must be positive; using default": "%s 必须大于 0,已使用默认值",
|
||||
"%s%.6f quota": "%s%.6f 额度",
|
||||
"--form 'file=@\"%s\"' (size: %d bytes, content-type: %s)": "--form 'file=@\"%s\"' (大小: %d 字节, content-type: %s)",
|
||||
"Add abilities for channel %d failed: %s": "修复渠道一致性: 为渠道 %d 重建 abilities 记录失败: %s",
|
||||
"Auto selected group: %s": "Auto 分组选定: %s",
|
||||
"Auto selecting group: %s, priorityRetry: %d": "Auto 分组尝试: %s, priorityRetry: %d",
|
||||
"Batch task %s already transitioned by another process, skip billing": "批量轮询: 任务 %s 的状态已由其他进程更新,跳过计费",
|
||||
"Batch task %s reached terminal status without data; preserving existing task data": "批量轮询: 任务 %s 已结束,但上游没有返回数据,保留已有的任务数据",
|
||||
"Batch task response ignored: unknown task_id=%s": "批量轮询: 上游返回了未知的 task_id=%s,已忽略",
|
||||
"CacheUpdateChannel after: id=%d, name=%s, status=%d, polling_index=%d": "CacheUpdateChannel 更新后: id=%d, name=%s, status=%d, polling_index=%d",
|
||||
"CacheUpdateChannel before: id=%d, name=%s, status=%d, polling_index=%d": "CacheUpdateChannel 更新前: id=%d, name=%s, status=%d, polling_index=%d",
|
||||
"Channel #%d failed to update video async tasks: %s": "渠道 #%d 更新视频异步任务失败: %s",
|
||||
"Channel #%d pending video tasks: %d": "渠道 #%d 有 %d 个视频任务未完成",
|
||||
"CheckUserExistOrDeleted error: %v": "CheckUserExistOrDeleted 出错: %v",
|
||||
"Client IP %s passed the token IP restrictions check": "客户端 IP %s 在令牌的 IP 白名单内",
|
||||
"Creem API response received trade_no=%s status_code=%d body=%q": "Creem API 响应已收到 trade_no=%s status_code=%d body=%q",
|
||||
"Creem callback customer email is empty trade_no=%s creem_order_id=%s": "Creem 回调客户邮箱为空 trade_no=%s creem_order_id=%s",
|
||||
"Creem callback customer name is empty trade_no=%s creem_order_id=%s": "Creem 回调客户姓名为空 trade_no=%s creem_order_id=%s",
|
||||
"Creem failed to create payment link user_id=%d trade_no=%s product_id=%s error=%q": "Creem 创建支付链接失败 user_id=%d trade_no=%s product_id=%s error=%q",
|
||||
"Creem failed to create subscription payment link trade_no=%s product_id=%s error=%q": "Creem 订阅支付链接创建失败 trade_no=%s product_id=%s error=%q",
|
||||
"Creem failed to create top-up order user_id=%d trade_no=%s product_id=%s error=%q": "Creem 创建充值订单失败 user_id=%d trade_no=%s product_id=%s error=%q",
|
||||
"Creem failed to parse product config user_id=%d error=%q": "Creem 产品配置解析失败 user_id=%d error=%q",
|
||||
"Creem failed to read payment request error=%q": "Creem 支付请求读取失败 error=%q",
|
||||
"Creem failed to read subscription payment request error=%q": "Creem 订阅支付请求读取失败 error=%q",
|
||||
"Creem order not paid, ignored request_id=%s order_id=%s order_status=%s": "Creem 订单状态未支付,忽略处理 request_id=%s order_id=%s order_status=%s",
|
||||
"Creem order type not supported, ignored request_id=%s creem_order_id=%s order_type=%s": "Creem 暂不支持该订单类型,忽略处理 request_id=%s creem_order_id=%s order_type=%s",
|
||||
"Creem payment completed callback trade_no=%s creem_order_id=%s amount_paid=%d currency=%s product_name=%q customer_email=%q customer_name=%q": "Creem 支付完成回调 trade_no=%s creem_order_id=%s amount_paid=%d currency=%s product_name=%q customer_email=%q customer_name=%q",
|
||||
"Creem payment link created trade_no=%s response_id=%s checkout_url=%q": "Creem 支付链接创建成功 trade_no=%s response_id=%s checkout_url=%q",
|
||||
"Creem payment request received user_id=%d body=%q": "Creem 支付请求已收到 user_id=%d body=%q",
|
||||
"Creem payment request sent api_url=%s product_id=%s email=%q trade_no=%s": "Creem 支付请求已发送 api_url=%s product_id=%s email=%q trade_no=%s",
|
||||
"Creem subscription order processed trade_no=%s creem_order_id=%s": "Creem 订阅订单处理成功 trade_no=%s creem_order_id=%s",
|
||||
"Creem subscription order processing failed trade_no=%s creem_order_id=%s error=%q": "Creem 订阅订单处理失败 trade_no=%s creem_order_id=%s error=%q",
|
||||
"Creem top-up order created user_id=%d trade_no=%s product_id=%s product_name=%q quota=%d money=%.2f": "Creem 充值订单创建成功 user_id=%d trade_no=%s product_id=%s product_name=%q quota=%d money=%.2f",
|
||||
"Creem top-up order not found trade_no=%s creem_order_id=%s": "Creem 充值订单不存在 trade_no=%s creem_order_id=%s",
|
||||
"Creem top-up order not pending, ignored trade_no=%s status=%s creem_order_id=%s": "Creem 充值订单状态非 pending,忽略处理 trade_no=%s status=%s creem_order_id=%s",
|
||||
"Creem top-up processing failed trade_no=%s creem_order_id=%s client_ip=%s error=%q": "Creem 充值处理失败 trade_no=%s creem_order_id=%s client_ip=%s error=%q",
|
||||
"Creem top-up succeeded trade_no=%s creem_order_id=%s quota=%d money=%.2f client_ip=%s": "Creem 充值成功 trade_no=%s creem_order_id=%s quota=%d money=%.2f client_ip=%s",
|
||||
"Creem using test environment api_url=%s": "Creem 使用测试环境 api_url=%s",
|
||||
"Creem webhook failed to parse path=%q client_ip=%s error=%q body=%q": "Creem webhook 解析失败 path=%q client_ip=%s error=%q body=%q",
|
||||
"Creem webhook failed to read request body path=%q client_ip=%s error=%q": "Creem webhook 读取请求体失败 path=%q client_ip=%s error=%q",
|
||||
"Creem webhook ignored event event_type=%s event_id=%s": "Creem webhook 忽略事件 event_type=%s event_id=%s",
|
||||
"Creem webhook missing request_id event_id=%s order_id=%s": "Creem webhook 缺少 request_id event_id=%s order_id=%s",
|
||||
"Creem webhook missing signature path=%q client_ip=%s body=%q": "Creem webhook 缺少签名 path=%q client_ip=%s body=%q",
|
||||
"Creem webhook parsed event_type=%s event_id=%s request_id=%s order_id=%s order_status=%s": "Creem webhook 解析成功 event_type=%s event_id=%s request_id=%s order_id=%s order_status=%s",
|
||||
"Creem webhook received request path=%q client_ip=%s signature=%q body=%q": "Creem webhook 收到请求 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Creem webhook rejected reason=webhook_disabled path=%q client_ip=%s": "Creem webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s",
|
||||
"Creem webhook secret is not configured test_mode=%t signature=%q body=%q": "Creem webhook secret 未配置 test_mode=%t signature=%q body=%q",
|
||||
"Creem webhook signature check skipped reason=test_mode signature=%q body=%q": "Creem webhook 验签已跳过 reason=test_mode signature=%q body=%q",
|
||||
"Creem webhook signature verification failed path=%q client_ip=%s signature=%q body=%q": "Creem webhook 验签失败 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Creem webhook signature verified path=%q client_ip=%s": "Creem webhook 验签成功 path=%q client_ip=%s",
|
||||
"Current group %s retries exhausted (priorityRetry=%d >= RetryTimes=%d), preparing switch to next group for next retry": "当前分组 %s 重试次数已用完 (priorityRetry=%d >= RetryTimes=%d),下次重试将切换到下一个分组",
|
||||
"Database pinged successfully": "数据库连接正常",
|
||||
"Delete abilities failed: %s": "修复渠道一致性: 删除 abilities 记录失败: %s",
|
||||
"Do gemini request failed: %s": "请求 Gemini 失败: %s",
|
||||
"Epay callback order not found trade_no=%s callback_type=%s client_ip=%s verify_info=%q": "易支付 回调订单不存在 trade_no=%s callback_type=%s client_ip=%s verify_info=%q",
|
||||
"Epay client is not initialized path=%q client_ip=%s": "易支付 client 未初始化 path=%q client_ip=%s",
|
||||
"Epay duplicate callback ignored trade_no=%s callback_type=%s client_ip=%s": "易支付 重复回调幂等忽略 trade_no=%s callback_type=%s client_ip=%s",
|
||||
"Epay failed to create top-up order user_id=%d trade_no=%s payment_method=%s amount=%d error=%q": "易支付 创建充值订单失败 user_id=%d trade_no=%s payment_method=%s amount=%d error=%q",
|
||||
"Epay failed to start payment user_id=%d trade_no=%s payment_method=%s amount=%d error=%q": "易支付 拉起支付失败 user_id=%d trade_no=%s payment_method=%s amount=%d error=%q",
|
||||
"Epay order payment gateway mismatch trade_no=%s callback_type=%s client_ip=%s": "易支付 订单支付网关不匹配 trade_no=%s callback_type=%s client_ip=%s",
|
||||
"Epay order status invalid trade_no=%s callback_type=%s client_ip=%s": "易支付 订单状态非法 trade_no=%s callback_type=%s client_ip=%s",
|
||||
"Epay top-up order created user_id=%d trade_no=%s payment_method=%s amount=%d money=%.2f uri=%q params=%q": "易支付 充值订单创建成功 user_id=%d trade_no=%s payment_method=%s amount=%d money=%.2f uri=%q params=%q",
|
||||
"Epay top-up processing failed trade_no=%s client_ip=%s error=%q": "易支付 充值处理失败 trade_no=%s client_ip=%s error=%q",
|
||||
"Epay top-up succeeded trade_no=%s callback_type=%s client_ip=%s": "易支付 充值成功 trade_no=%s callback_type=%s client_ip=%s",
|
||||
"Epay top-up succeeded trade_no=%s user_id=%d quota_to_add=%d money=%.2f": "易支付充值成功 trade_no=%s user_id=%d quota_to_add=%d money=%.2f",
|
||||
"Epay webhook failed to parse POST form path=%q client_ip=%s error=%q": "易支付 webhook POST 表单解析失败 path=%q client_ip=%s error=%q",
|
||||
"Epay webhook failed to write response path=%q client_ip=%s error=%q": "易支付 webhook 响应写入失败 path=%q client_ip=%s error=%q",
|
||||
"Epay webhook failed to write response trade_no=%s client_ip=%s error=%q": "易支付 webhook 响应写入失败 trade_no=%s client_ip=%s error=%q",
|
||||
"Epay webhook ignored event trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q": "易支付 webhook 忽略事件 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q",
|
||||
"Epay webhook params are empty path=%q client_ip=%s": "易支付 webhook 参数为空 path=%q client_ip=%s",
|
||||
"Epay webhook received request path=%q client_ip=%s method=%s params=%q": "易支付 webhook 收到请求 path=%q client_ip=%s method=%s params=%q",
|
||||
"Epay webhook rejected reason=webhook_disabled path=%q client_ip=%s": "易支付 webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s",
|
||||
"Epay webhook signature verification failed path=%q client_ip=%s verify_error=%q": "易支付 webhook 验签失败 path=%q client_ip=%s verify_error=%q",
|
||||
"Epay webhook signature verification failed path=%q client_ip=%s verify_status=false": "易支付 webhook 验签失败 path=%q client_ip=%s verify_status=false",
|
||||
"Epay webhook signature verified trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q": "易支付 webhook 验签成功 trade_no=%s callback_type=%s trade_status=%s client_ip=%s verify_info=%q",
|
||||
"Error getting sql.DB from GORM: %v": "从 GORM 取得 sql.DB 失败: %v",
|
||||
"Error pinging DB: %v": "数据库 ping 失败: %v",
|
||||
"Error: Failed to decode base64 string": "base64 字符串解码失败",
|
||||
"FRONTEND_BASE_URL is ignored on master node": "当前是主节点,FRONTEND_BASE_URL 设置已忽略",
|
||||
"Failed to get binding count for provider %s: %s": "查询 OAuth 提供商 %s 的绑定用户数失败: %s",
|
||||
"Failed to load custom OAuth providers: %s": "加载自定义 OAuth 提供商失败: %s",
|
||||
"Failed to load rate limit script: %v": "加载限流脚本失败: %v",
|
||||
"Failed to project plugin video for task %s": "读取任务 %s 的制品列表失败",
|
||||
"Failed to query task %s: %s": "查询任务 %s 失败: %s",
|
||||
"Failed to resolve plugin video content for task %s: %v": "任务 %s: 生成视频内容的下载请求失败: %v",
|
||||
"Failed to stream image: %v": "向客户端转发图片失败: %v",
|
||||
"Failed to stream task media: %v": "向客户端转发任务媒体文件失败: %v",
|
||||
"Failed to update task %s: %s": "更新任务 %s 失败: %s",
|
||||
"Failed to update video task %s: %s": "更新视频任务 %s 失败: %s",
|
||||
"Failed to write to disk cache, falling back to memory: %v": "写入磁盘缓存失败,改用内存缓存: %v",
|
||||
"File cached to disk: %s, size: %d bytes": "文件已缓存到磁盘: %s,大小: %d 字节",
|
||||
"Fix null mj_id task error: %v": "修复 mj_id 为空的任务出错: %v",
|
||||
"Fix null mj_id task success: %v": "修复 mj_id 为空的任务成功: %v",
|
||||
"Fix null task_id task error: %v": "修复 task_id 为空的任务出错: %v",
|
||||
"Fix null task_id task success: %v": "修复 task_id 为空的任务成功: %v",
|
||||
"Gemini embedding request body: %s": "Gemini 嵌入请求体: %s",
|
||||
"Gemini native embedding response body: %s": "Gemini 原生格式嵌入响应体: %s",
|
||||
"Gemini native response body: %s": "Gemini 原生格式响应体: %s",
|
||||
"Gemini request body: %s": "Gemini 请求体: %s",
|
||||
"Gemini response body: %s": "Gemini 响应体: %s",
|
||||
"Gemini responses response body: %s": "Gemini Responses 响应体: %s",
|
||||
"Gemini stream ended unexpectedly: %s": "Gemini 流式响应异常中断: %s",
|
||||
"Get Mjp Task parse body error2: %v, body: %s": "解析 Midjourney 任务查询响应失败: %v,响应体: %s",
|
||||
"Get Mjp Task parse body error: %v": "读取 Midjourney 任务查询响应失败: %v",
|
||||
"Get Suno Task parse body error: %v": "读取 Suno 任务查询响应失败: %v",
|
||||
"Get Task Do req error: %v": "发送任务查询请求失败: %v",
|
||||
"Get Task error: %v": "创建任务查询请求失败: %v",
|
||||
"Get Task marshal body error: %v": "序列化任务查询请求体失败: %v",
|
||||
"Get Task status code: %d": "任务查询返回状态码 %d",
|
||||
"GetAllEnableAbilityWithChannels error: %v": "GetAllEnableAbilityWithChannels 出错: %v",
|
||||
"GetPasskeyByCredentialID: database error for credential ID: %v": "GetPasskeyByCredentialID: 按凭证 ID 查询数据库出错: %v",
|
||||
"GetPasskeyByCredentialID: empty credential ID": "GetPasskeyByCredentialID: 凭证 ID 为空",
|
||||
"GetPasskeyByCredentialID: passkey not found for credential ID length %d": "GetPasskeyByCredentialID: 未找到凭证 ID 对应的通行密钥,凭证 ID 长度 %d",
|
||||
"GetPasskeyByUserID: database error for user %d: %v": "GetPasskeyByUserID: 查询用户 %d 时数据库出错: %v",
|
||||
"GetPasskeyByUserID: empty user ID": "GetPasskeyByUserID: 用户 ID 为空",
|
||||
"GetPreferredModelOwnerChannelTypes error: %v": "GetPreferredModelOwnerChannelTypes 出错: %v",
|
||||
"InitChannelCache failed: %s": "InitChannelCache 失败: %s",
|
||||
"InitChannelCache panic: %v": "InitChannelCache 发生 panic: %v",
|
||||
"InitChannelCache panic: %v, retrying once": "InitChannelCache 发生 panic: %v,重试一次",
|
||||
"LoadFileSource starting for: %s": "LoadFileSource 开始加载: %s",
|
||||
"Loaded %d custom OAuth providers": "已加载 %d 个自定义 OAuth 提供商",
|
||||
"Loaded custom OAuth provider: %s (%s)": "已加载自定义 OAuth 提供商: %s (%s)",
|
||||
"Local:": "本机地址:",
|
||||
"Login database error for user %s: %v": "用户 %s 登录时数据库出错: %v",
|
||||
"Midjourney action uses origin channel: id=%s, base_url=%s": "Midjourney 后续操作使用原任务所在的渠道: id=%s, base_url=%s",
|
||||
"Midjourney refund succeeded but clearing the quota failed, task %s: %s": "Midjourney 退款成功但清除 quota 失败 task %s: %s",
|
||||
"Midjourney task response ignored: unknown mj_id=%s": "上游返回了未知的 Midjourney 任务 mj_id=%s,已忽略",
|
||||
"Network:": "网络地址:",
|
||||
"New API %s started": "New API %s 已启动",
|
||||
"No .env file found, using default environment variables. If needed, please create a .env file and set the relevant variables.": "未找到 .env 文件,直接使用进程的环境变量。需要时可以新建 .env 文件,把变量写在里面。",
|
||||
"No available channel in group %s for model %s at priorityRetry %d, trying next group": "分组 %s 没有模型 %s 的可用渠道 (priorityRetry=%d),尝试下一个分组",
|
||||
"No update needed for task %s": "任务 %s 没有变化,跳过更新",
|
||||
"OpenRouter parse failed from %s: %s": "解析 %s 返回的 OpenRouter 数据失败: %s",
|
||||
"PasskeyLogin: userHandle parse error for credential, length: %d": "PasskeyLogin: 凭证的 userHandle 无法解析为用户 ID,长度: %d",
|
||||
"Please set SESSION_SECRET to a random string.": "请将 SESSION_SECRET 设置为随机字符串。",
|
||||
"REDIS_CONN_STRING not set, Redis is not enabled": "未设置 REDIS_CONN_STRING,Redis 未启用",
|
||||
"Redis connected to %s": "Redis 已连接到 %s",
|
||||
"Redis database: %d": "Redis 数据库: %d",
|
||||
"Redis is enabled": "Redis 已启用",
|
||||
"Redis ping test failed: %s": "Redis 连接测试失败: %s",
|
||||
"RemoveDisabledFields Marshal error :%s": "RemoveDisabledFields 序列化失败 :%s",
|
||||
"RemoveDisabledFields Unmarshal error :%s": "RemoveDisabledFields 反序列化失败 :%s",
|
||||
"RemoveGeminiDisabledFields Marshal error: %s": "RemoveGeminiDisabledFields 序列化失败: %s",
|
||||
"RemoveGeminiDisabledFields Unmarshal error: %s": "RemoveGeminiDisabledFields 反序列化失败: %s",
|
||||
"Rerank request body: %s": "重排序请求体: %s",
|
||||
"SKIP_64BIT_QUOTA_SCHEMA_CHECK=true; skipping user quota schema check": "已设置 SKIP_64BIT_QUOTA_SCHEMA_CHECK=true,跳过用户额度字段的类型检查",
|
||||
"SQL_DSN not set, using SQLite as database": "未设置 SQL_DSN,数据库使用 SQLite",
|
||||
"SSE ping data sent": "SSE ping 数据已发送",
|
||||
"SSE ping error, stopping goroutine: %s": "SSE ping 出错,停止 goroutine: %s",
|
||||
"SSE ping error: %s": "SSE ping 出错: %s",
|
||||
"SSE ping goroutine panic recovered: %v": "SSE ping goroutine 发生 panic,已恢复: %v",
|
||||
"SSE ping goroutine started": "SSE ping goroutine 已启动",
|
||||
"SSE ping goroutine stopped": "SSE ping goroutine 已停止",
|
||||
"SSE ping goroutine stopped by defer": "SSE ping goroutine 已由 defer 停止",
|
||||
"SSE ping goroutine timeout, stopping": "SSE ping goroutine 超时,正在停止",
|
||||
"SSE ping ticker stopped": "SSE ping ticker 已停止",
|
||||
"SYNC_FREQUENCY not set, use default value 60": "未设置 SYNC_FREQUENCY,使用默认值 60",
|
||||
"SetPayloadHash body: %s": "SetPayloadHash 请求体: %s",
|
||||
"Stripe Checkout payment not completed, waiting for async result trade_no=%s payment_status=%s client_ip=%s": "Stripe Checkout 支付未完成,等待异步结果 trade_no=%s payment_status=%s client_ip=%s",
|
||||
"Stripe async payment failed but local order not found trade_no=%s client_ip=%s": "Stripe 异步支付失败但本地订单不存在 trade_no=%s client_ip=%s",
|
||||
"Stripe async payment failed but order not pending, ignored trade_no=%s status=%s client_ip=%s": "Stripe 异步支付失败但订单状态非 pending,忽略处理 trade_no=%s status=%s client_ip=%s",
|
||||
"Stripe async payment failed but order payment gateway mismatch trade_no=%s payment_provider=%s client_ip=%s": "Stripe 异步支付失败但订单支付网关不匹配 trade_no=%s payment_provider=%s client_ip=%s",
|
||||
"Stripe async payment failed event missing order number client_ip=%s": "Stripe 异步支付失败事件缺少订单号 client_ip=%s",
|
||||
"Stripe async payment failed trade_no=%s client_ip=%s": "Stripe 异步支付失败 trade_no=%s client_ip=%s",
|
||||
"Stripe async payment succeeded trade_no=%s client_ip=%s": "Stripe 异步支付成功 trade_no=%s client_ip=%s",
|
||||
"Stripe checkout.completed has unexpected status, ignored trade_no=%s status=%s client_ip=%s": "Stripe checkout.completed 状态异常,忽略处理 trade_no=%s status=%s client_ip=%s",
|
||||
"Stripe checkout.expired has unexpected status, ignored trade_no=%s status=%s": "Stripe checkout.expired 状态异常,忽略处理 trade_no=%s status=%s",
|
||||
"Stripe checkout.expired missing order number": "Stripe checkout.expired 缺少订单号",
|
||||
"Stripe failed to create Checkout Session user_id=%d trade_no=%s amount=%d error=%q": "Stripe 创建 Checkout Session 失败 user_id=%d trade_no=%s amount=%d error=%q",
|
||||
"Stripe failed to create subscription payment link trade_no=%s plan_id=%d error=%q": "Stripe 订阅支付链接创建失败 trade_no=%s plan_id=%d error=%q",
|
||||
"Stripe failed to create top-up order user_id=%d trade_no=%s amount=%d error=%q": "Stripe 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q",
|
||||
"Stripe failed to mark top-up order as failed trade_no=%s client_ip=%s error=%q": "Stripe 标记充值订单失败状态失败 trade_no=%s client_ip=%s error=%q",
|
||||
"Stripe order completion missing order number client_ip=%s": "Stripe 完成订单时缺少订单号 client_ip=%s",
|
||||
"Stripe subscription order expired trade_no=%s": "Stripe 订阅订单已过期 trade_no=%s",
|
||||
"Stripe subscription order expiry processing failed trade_no=%s error=%q": "Stripe 订阅订单过期处理失败 trade_no=%s error=%q",
|
||||
"Stripe subscription order processed trade_no=%s event_type=%s client_ip=%s": "Stripe 订阅订单处理成功 trade_no=%s event_type=%s client_ip=%s",
|
||||
"Stripe subscription order processing failed trade_no=%s event_type=%s client_ip=%s error=%q": "Stripe 订阅订单处理失败 trade_no=%s event_type=%s client_ip=%s error=%q",
|
||||
"Stripe top-up order created user_id=%d trade_no=%s amount=%d money=%.2f": "Stripe 充值订单创建成功 user_id=%d trade_no=%s amount=%d money=%.2f",
|
||||
"Stripe top-up order expired trade_no=%s": "Stripe 充值订单已过期 trade_no=%s",
|
||||
"Stripe top-up order expiry processing failed trade_no=%s error=%q": "Stripe 充值订单过期处理失败 trade_no=%s error=%q",
|
||||
"Stripe top-up order marked as failed trade_no=%s client_ip=%s": "Stripe 充值订单已标记为失败 trade_no=%s client_ip=%s",
|
||||
"Stripe top-up order not found, cannot mark as expired trade_no=%s": "Stripe 充值订单不存在,无法标记过期 trade_no=%s",
|
||||
"Stripe top-up processing failed trade_no=%s event_type=%s client_ip=%s error=%q": "Stripe 充值处理失败 trade_no=%s event_type=%s client_ip=%s error=%q",
|
||||
"Stripe top-up succeeded trade_no=%s amount_total=%.2f currency=%s event_type=%s client_ip=%s": "Stripe 充值成功 trade_no=%s amount_total=%.2f currency=%s event_type=%s client_ip=%s",
|
||||
"Stripe webhook failed to read request body path=%q client_ip=%s error=%q": "Stripe webhook 读取请求体失败 path=%q client_ip=%s error=%q",
|
||||
"Stripe webhook ignored event event_type=%s client_ip=%s": "Stripe webhook 忽略事件 event_type=%s client_ip=%s",
|
||||
"Stripe webhook received request path=%q client_ip=%s signature=%q body=%q": "Stripe webhook 收到请求 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Stripe webhook rejected reason=webhook_disabled path=%q client_ip=%s": "Stripe webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s",
|
||||
"Stripe webhook signature verification failed path=%q client_ip=%s error=%q": "Stripe webhook 验签失败 path=%q client_ip=%s error=%q",
|
||||
"Stripe webhook signature verified event_type=%s client_ip=%s path=%q": "Stripe webhook 验签成功 event_type=%s client_ip=%s path=%q",
|
||||
"Successfully migrated %s.%s to decimal(10,6)": "字段 %s.%s 的类型已改为 decimal(10,6)",
|
||||
"Successfully migrated %s.%s to text": "字段 %s.%s 的类型已改为 text",
|
||||
"Task %s CAS lost or no-op update, skip billing": "任务 %s 的状态已由其他进程更新或没有变化,跳过计费",
|
||||
"Task %s failed": "任务 %s 失败",
|
||||
"Task %s failed: %s": "任务 %s 失败: %s",
|
||||
"Task %s not found in taskM": "任务 %s 不在本次轮询的任务列表中",
|
||||
"Token has IP restrictions, checking client IP %s": "令牌设置了 IP 白名单,检查客户端 IP %s",
|
||||
"TokenAuth GetUserCache error for user %d: %v": "TokenAuth GetUserCache 获取用户 %d 出错: %v",
|
||||
"TokenAuth ValidateUserToken database error: %s": "TokenAuth ValidateUserToken 数据库出错: %s",
|
||||
"TokenAuthReadOnly GetTokenByKey database error: %s": "TokenAuthReadOnly GetTokenByKey 数据库出错: %s",
|
||||
"TokenAuthReadOnly GetUserCache error for user %d: %v": "TokenAuthReadOnly GetUserCache 获取用户 %d 出错: %v",
|
||||
"Truncate abilities failed: %s": "修复渠道一致性: 清空 abilities 表失败: %s",
|
||||
"Trying to read %d bytes to determine file type": "读取前 %d 字节来判断文件类型",
|
||||
"USER_SESSION_ISSUANCE_WINDOW_SECONDS exceeds revoked retention; configured_window_seconds=%d revoked_retention_seconds=%d effective_window_seconds=%d": "USER_SESSION_ISSUANCE_WINDOW_SECONDS 大于已吊销会话的保留时长,已按保留时长生效: configured_window_seconds=%d revoked_retention_seconds=%d effective_window_seconds=%d",
|
||||
"USER_SESSION_REVOKED_RETENTION_DAYS is too large, using default value: %d": "USER_SESSION_REVOKED_RETENTION_DAYS 的值过大,已使用默认值: %d",
|
||||
"UpdateBatchTasks fail: %s": "UpdateBatchTasks 失败: %s",
|
||||
"UpdateMidjourneyTask error: %v": "UpdateMidjourneyTask 出错: %v",
|
||||
"UpdateMidjourneyTask task error: %s": "UpdateMidjourneyTask 更新任务出错: %s",
|
||||
"UpdateSunoTask error: %v": "UpdateSunoTask 出错: %v",
|
||||
"UpdateSunoTask task error: %s": "UpdateSunoTask 更新任务出错: %s",
|
||||
"UpdateVideoTask error: %v": "UpdateVideoTask 出错: %v",
|
||||
"UpdateVideoTasks fail: %s": "UpdateVideoTasks 失败: %s",
|
||||
"UpdateWithStatus failed for task %s: %s": "更新任务 %s 的状态失败 (UpdateWithStatus): %s",
|
||||
"UpsertPasskeyCredential: failed to create credential for user %d: %v": "UpsertPasskeyCredential: 为用户 %d 创建凭证失败: %v",
|
||||
"UpsertPasskeyCredential: failed to delete existing credential for user %d: %v": "UpsertPasskeyCredential: 删除用户 %d 的现有凭证失败: %v",
|
||||
"ValidateUserToken: failed to get token: %s": "ValidateUserToken: 获取令牌失败: %s",
|
||||
"WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.": "警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。",
|
||||
"WARNING: TRUSTED_PROXIES is unset or blank; trusting loopback, RFC 1918, and IPv6 ULA proxy addresses for compatibility. Set TRUSTED_PROXIES=none to trust no proxies, or configure explicit proxy IPs/CIDRs to replace these defaults.": "警告:TRUSTED_PROXIES 未设置或为空。为兼容已有部署,来自回环地址、RFC 1918 内网地址和 IPv6 ULA 地址的代理会被信任。设置 TRUSTED_PROXIES=none 表示任何代理都不信任,也可以填写代理的 IP 或 CIDR 来替换这组默认值。",
|
||||
"Waffo Pancake failed to create checkout session user_id=%d trade_no=%s error=%q": "Waffo Pancake 创建结账会话失败 user_id=%d trade_no=%s error=%q",
|
||||
"Waffo Pancake failed to create plan product store_id=%q name=%q amount=%q error=%q": "Waffo Pancake 创建套餐产品失败 store_id=%q name=%q amount=%q error=%q",
|
||||
"Waffo Pancake failed to create store and product orphan_store=%t store_id=%q error=%q": "Waffo Pancake 创建店铺与产品失败 orphan_store=%t store_id=%q error=%q",
|
||||
"Waffo Pancake failed to create subscription checkout session user_id=%d plan_id=%d trade_no=%s error=%q": "Waffo Pancake 订阅结账会话创建失败 user_id=%d plan_id=%d trade_no=%s error=%q",
|
||||
"Waffo Pancake failed to create subscription order user_id=%d plan_id=%d trade_no=%s error=%q": "Waffo Pancake 订阅订单创建失败 user_id=%d plan_id=%d trade_no=%s error=%q",
|
||||
"Waffo Pancake failed to create top-up order user_id=%d trade_no=%s amount=%d error=%q": "Waffo Pancake 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q",
|
||||
"Waffo Pancake failed to fetch store and product catalog error=%q": "Waffo Pancake 拉取店铺与产品目录失败 error=%q",
|
||||
"Waffo Pancake failed to fetch subscription product list store_id=%q error=%q": "Waffo Pancake 拉取订阅产品列表失败 store_id=%q error=%q",
|
||||
"Waffo Pancake failed to save config store_id=%q product_id=%q error=%q": "Waffo Pancake 保存配置失败 store_id=%q product_id=%q error=%q",
|
||||
"Waffo Pancake subscription completed trade_no=%s event_id=%s order_id=%s client_ip=%s": "Waffo Pancake 订阅完成 trade_no=%s event_id=%s order_id=%s client_ip=%s",
|
||||
"Waffo Pancake subscription completion failed trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q": "Waffo Pancake 订阅完成失败 trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q",
|
||||
"Waffo Pancake subscription order created user_id=%d plan_id=%d trade_no=%s session_id=%s money=%.2f": "Waffo Pancake 订阅订单创建成功 user_id=%d plan_id=%d trade_no=%s session_id=%s money=%.2f",
|
||||
"Waffo Pancake top-up order created user_id=%d trade_no=%s session_id=%s amount=%d money=%.2f": "Waffo Pancake 充值订单创建成功 user_id=%d trade_no=%s session_id=%s amount=%d money=%.2f",
|
||||
"Waffo Pancake top-up processing failed trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q": "Waffo Pancake 充值处理失败 trade_no=%s event_id=%s order_id=%s client_ip=%s error=%q",
|
||||
"Waffo Pancake top-up succeeded trade_no=%s event_id=%s order_id=%s client_ip=%s": "Waffo Pancake 充值成功 trade_no=%s event_id=%s order_id=%s client_ip=%s",
|
||||
"Waffo Pancake webhook env mismatch expected=%q actual_mode=%q event_id=%s order_id=%s client_ip=%s": "Waffo Pancake webhook 环境不匹配 expected=%q actual_mode=%q event_id=%s order_id=%s client_ip=%s",
|
||||
"Waffo Pancake webhook failed to read request body path=%q client_ip=%s error=%q": "Waffo Pancake webhook 读取请求体失败 path=%q client_ip=%s error=%q",
|
||||
"Waffo Pancake webhook failed to resolve order event_id=%s order_id=%s buyer_identity=%q client_ip=%s error=%q": "Waffo Pancake webhook 订单解析失败 event_id=%s order_id=%s buyer_identity=%q client_ip=%s error=%q",
|
||||
"Waffo Pancake webhook failed to resolve subscription order event_id=%s order_id=%s buyer_identity=%q client_ip=%s error=%q": "Waffo Pancake webhook 订阅订单解析失败 event_id=%s order_id=%s buyer_identity=%q client_ip=%s error=%q",
|
||||
"Waffo Pancake webhook invalid env path segment env=%q path=%q client_ip=%s": "Waffo Pancake webhook 路径环境段无效 env=%q path=%q client_ip=%s",
|
||||
"Waffo Pancake webhook received request path=%q client_ip=%s signature=%q body=%q": "Waffo Pancake webhook 收到请求 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Waffo Pancake webhook rejected reason=webhook_disabled path=%q client_ip=%s": "Waffo Pancake webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s",
|
||||
"Waffo Pancake webhook signature verification failed path=%q client_ip=%s signature=%q body=%q error=%q": "Waffo Pancake webhook 验签失败 path=%q client_ip=%s signature=%q body=%q error=%q",
|
||||
"Waffo Pancake webhook signature verified event_type=%s event_id=%s order_id=%s client_ip=%s": "Waffo Pancake webhook 验签成功 event_type=%s event_id=%s order_id=%s client_ip=%s",
|
||||
"Waffo SDK initialization failed user_id=%d trade_no=%s error=%q": "Waffo SDK 初始化失败 user_id=%d trade_no=%s error=%q",
|
||||
"Waffo failed to create order user_id=%d trade_no=%s error=%q": "Waffo 创建订单失败 user_id=%d trade_no=%s error=%q",
|
||||
"Waffo failed to create top-up order user_id=%d trade_no=%s amount=%d error=%q": "Waffo 创建充值订单失败 user_id=%d trade_no=%s amount=%d error=%q",
|
||||
"Waffo failed to mark order as failed trade_no=%s error=%q": "Waffo 标记失败订单状态失败 trade_no=%s error=%q",
|
||||
"Waffo failed to parse payment callback payload event_type=%s client_ip=%s error=%q body=%q": "Waffo 支付回调载荷解析失败 event_type=%s client_ip=%s error=%q body=%q",
|
||||
"Waffo invalid payment method index user_id=%d pay_method_index=%d method_count=%d": "Waffo 支付方式索引无效 user_id=%d pay_method_index=%d method_count=%d",
|
||||
"Waffo invalid payment method user_id=%d pay_method_type=%s pay_method_name=%q": "Waffo 支付方式无效 user_id=%d pay_method_type=%s pay_method_name=%q",
|
||||
"Waffo order creation rejected user_id=%d trade_no=%s code=%s message=%q response=%q": "Waffo 创建订单业务失败 user_id=%d trade_no=%s code=%s message=%q response=%q",
|
||||
"Waffo order not successful, top-up skipped trade_no=%s order_status=%s client_ip=%s": "Waffo 订单状态非成功,忽略充值 trade_no=%s order_status=%s client_ip=%s",
|
||||
"Waffo top-up order created user_id=%d trade_no=%s amount=%d money=%.2f pay_method_type=%s pay_method_name=%q": "Waffo 充值订单创建成功 user_id=%d trade_no=%s amount=%d money=%.2f pay_method_type=%s pay_method_name=%q",
|
||||
"Waffo top-up processing failed trade_no=%s client_ip=%s error=%q": "Waffo 充值处理失败 trade_no=%s client_ip=%s error=%q",
|
||||
"Waffo top-up succeeded trade_no=%s client_ip=%s": "Waffo 充值成功 trade_no=%s client_ip=%s",
|
||||
"Waffo webhook SDK initialization failed path=%q client_ip=%s error=%q": "Waffo webhook SDK 初始化失败 path=%q client_ip=%s error=%q",
|
||||
"Waffo webhook failed to parse path=%q client_ip=%s error=%q body=%q": "Waffo webhook 解析失败 path=%q client_ip=%s error=%q body=%q",
|
||||
"Waffo webhook failed to read request body path=%q client_ip=%s error=%q": "Waffo webhook 读取请求体失败 path=%q client_ip=%s error=%q",
|
||||
"Waffo webhook ignored event event_type=%s client_ip=%s": "Waffo webhook 忽略事件 event_type=%s client_ip=%s",
|
||||
"Waffo webhook received request path=%q client_ip=%s signature=%q body=%q": "Waffo webhook 收到请求 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Waffo webhook rejected reason=webhook_disabled path=%q client_ip=%s": "Waffo webhook 被拒绝 reason=webhook_disabled path=%q client_ip=%s",
|
||||
"Waffo webhook signature verification failed path=%q client_ip=%s signature=%q body=%q": "Waffo webhook 验签失败 path=%q client_ip=%s signature=%q body=%q",
|
||||
"Waffo webhook verified and parsed event_type=%s merchant_order_id=%s order_status=%s client_ip=%s": "Waffo webhook 验签并解析成功 event_type=%s merchant_order_id=%s order_status=%s client_ip=%s",
|
||||
"Warning: Refresh cookie is not secure and refresh/logout Origin validation is disabled. Please set SESSION_COOKIE_SECURE=true in production.": "警告:SESSION_COOKIE_SECURE 未开启。登录 Cookie 可经 HTTP 明文传输,续期登录和退出登录接口接受任意来源(Origin)的请求。生产环境请设置 SESSION_COOKIE_SECURE=true。",
|
||||
"Warning: failed to migrate %s.%s to decimal: %v": "警告:把字段 %s.%s 的类型改为 decimal 失败: %v",
|
||||
"Warning: failed to query metadata for %s.%s: %v": "警告:查询字段 %s.%s 的类型信息失败: %v",
|
||||
"[OAuth-Discord] ExchangeToken decode error: %s": "[OAuth-Discord] ExchangeToken 解析响应失败: %s",
|
||||
"[OAuth-Discord] ExchangeToken error: %s": "[OAuth-Discord] ExchangeToken 请求失败: %s",
|
||||
"[OAuth-Discord] ExchangeToken failed: empty access token": "[OAuth-Discord] ExchangeToken 失败: 响应里没有 access token",
|
||||
"[OAuth-Discord] ExchangeToken response status: %d": "[OAuth-Discord] ExchangeToken 响应状态码: %d",
|
||||
"[OAuth-Discord] ExchangeToken success: scope=%s": "[OAuth-Discord] ExchangeToken 成功: scope=%s",
|
||||
"[OAuth-Discord] GetUserInfo decode error: %s": "[OAuth-Discord] GetUserInfo 解析响应失败: %s",
|
||||
"[OAuth-Discord] GetUserInfo error: %s": "[OAuth-Discord] GetUserInfo 请求失败: %s",
|
||||
"[OAuth-Discord] GetUserInfo failed: empty user fields": "[OAuth-Discord] GetUserInfo 失败: 返回的用户信息为空",
|
||||
"[OAuth-Discord] GetUserInfo failed: status=%d": "[OAuth-Discord] GetUserInfo 失败: status=%d",
|
||||
"[OAuth-Discord] GetUserInfo response status: %d": "[OAuth-Discord] GetUserInfo 响应状态码: %d",
|
||||
"[OAuth-Discord] GetUserInfo success: uid=%s, username=%s, name=%s": "[OAuth-Discord] GetUserInfo 成功: uid=%s, username=%s, name=%s",
|
||||
"[OAuth-Discord] GetUserInfo: fetching user info": "[OAuth-Discord] GetUserInfo: 开始获取用户信息",
|
||||
"[OAuth-Generic-%s] ExchangeToken OAuth error: %s - %s": "[OAuth-Generic-%s] ExchangeToken 提供商返回错误: %s - %s",
|
||||
"[OAuth-Generic-%s] ExchangeToken error: %s": "[OAuth-Generic-%s] ExchangeToken 请求失败: %s",
|
||||
"[OAuth-Generic-%s] ExchangeToken failed: empty access token": "[OAuth-Generic-%s] ExchangeToken 失败: 响应里没有 access token",
|
||||
"[OAuth-Generic-%s] ExchangeToken parse error: %s": "[OAuth-Generic-%s] ExchangeToken 解析响应失败: %s",
|
||||
"[OAuth-Generic-%s] ExchangeToken read body error: %s": "[OAuth-Generic-%s] ExchangeToken 读取响应体失败: %s",
|
||||
"[OAuth-Generic-%s] ExchangeToken response status: %d": "[OAuth-Generic-%s] ExchangeToken 响应状态码: %d",
|
||||
"[OAuth-Generic-%s] ExchangeToken success: scope=%s": "[OAuth-Generic-%s] ExchangeToken 成功: scope=%s",
|
||||
"[OAuth-Generic-%s] GetUserInfo error: %s": "[OAuth-Generic-%s] GetUserInfo 请求失败: %s",
|
||||
"[OAuth-Generic-%s] GetUserInfo failed: empty user ID (field: %s)": "[OAuth-Generic-%s] GetUserInfo 失败: 用户 ID 为空 (字段: %s)",
|
||||
"[OAuth-Generic-%s] GetUserInfo failed: status=%d": "[OAuth-Generic-%s] GetUserInfo 失败: status=%d",
|
||||
"[OAuth-Generic-%s] GetUserInfo read body error: %s": "[OAuth-Generic-%s] GetUserInfo 读取响应体失败: %s",
|
||||
"[OAuth-Generic-%s] GetUserInfo response status: %d": "[OAuth-Generic-%s] GetUserInfo 响应状态码: %d",
|
||||
"[OAuth-Generic-%s] GetUserInfo success: id=%s, username=%s, name=%s, email=%s": "[OAuth-Generic-%s] GetUserInfo 成功: id=%s, username=%s, name=%s, email=%s",
|
||||
"[OAuth-Generic-%s] GetUserInfo: fetching user info from %s": "[OAuth-Generic-%s] GetUserInfo: 开始从 %s 获取用户信息",
|
||||
"[OAuth-Generic-%s] access denied by policy: field=%s op=%s expected=%v current=%v": "[OAuth-Generic-%s] 不满足访问策略,已拒绝登录: field=%s op=%s expected=%v current=%v",
|
||||
"[OAuth-Generic-%s] invalid access policy: %s": "[OAuth-Generic-%s] 访问策略配置无效: %s",
|
||||
"[OAuth-GitHub] ExchangeToken decode error: %s": "[OAuth-GitHub] ExchangeToken 解析响应失败: %s",
|
||||
"[OAuth-GitHub] ExchangeToken error: %s": "[OAuth-GitHub] ExchangeToken 请求失败: %s",
|
||||
"[OAuth-GitHub] ExchangeToken failed: empty access token": "[OAuth-GitHub] ExchangeToken 失败: 响应里没有 access token",
|
||||
"[OAuth-GitHub] ExchangeToken response status: %d": "[OAuth-GitHub] ExchangeToken 响应状态码: %d",
|
||||
"[OAuth-GitHub] ExchangeToken success: scope=%s": "[OAuth-GitHub] ExchangeToken 成功: scope=%s",
|
||||
"[OAuth-GitHub] GetUserInfo decode error: %s": "[OAuth-GitHub] GetUserInfo 解析响应失败: %s",
|
||||
"[OAuth-GitHub] GetUserInfo error: %s": "[OAuth-GitHub] GetUserInfo 请求失败: %s",
|
||||
"[OAuth-GitHub] GetUserInfo failed: empty id or login field": "[OAuth-GitHub] GetUserInfo 失败: id 或 login 字段为空",
|
||||
"[OAuth-GitHub] GetUserInfo failed: status=%d, body=%s": "[OAuth-GitHub] GetUserInfo 失败: status=%d, body=%s",
|
||||
"[OAuth-GitHub] GetUserInfo response status: %d": "[OAuth-GitHub] GetUserInfo 响应状态码: %d",
|
||||
"[OAuth-GitHub] GetUserInfo success: id=%d, login=%s, name=%s, email=%s": "[OAuth-GitHub] GetUserInfo 成功: id=%d, login=%s, name=%s, email=%s",
|
||||
"[OAuth-GitHub] GetUserInfo: fetching user info": "[OAuth-GitHub] GetUserInfo: 开始获取用户信息",
|
||||
"[OAuth-GitHub] GetVerifiedEmails decode error: %s": "[OAuth-GitHub] GetVerifiedEmails 解析响应失败: %s",
|
||||
"[OAuth-GitHub] GetVerifiedEmails error: %s": "[OAuth-GitHub] GetVerifiedEmails 请求失败: %s",
|
||||
"[OAuth-GitHub] GetVerifiedEmails failed: status=%d": "[OAuth-GitHub] GetVerifiedEmails 失败: status=%d",
|
||||
"[OAuth-GitHub] GetVerifiedEmails success: verified=%d": "[OAuth-GitHub] GetVerifiedEmails 成功: verified=%d",
|
||||
"[OAuth-LinuxDO] ExchangeToken decode error: %s": "[OAuth-LinuxDO] ExchangeToken 解析响应失败: %s",
|
||||
"[OAuth-LinuxDO] ExchangeToken error: %s": "[OAuth-LinuxDO] ExchangeToken 请求失败: %s",
|
||||
"[OAuth-LinuxDO] ExchangeToken failed: %s": "[OAuth-LinuxDO] ExchangeToken 失败: %s",
|
||||
"[OAuth-LinuxDO] ExchangeToken response status: %d": "[OAuth-LinuxDO] ExchangeToken 响应状态码: %d",
|
||||
"[OAuth-LinuxDO] ExchangeToken success": "[OAuth-LinuxDO] ExchangeToken 成功",
|
||||
"[OAuth-LinuxDO] GetUserInfo decode error: %s": "[OAuth-LinuxDO] GetUserInfo 解析响应失败: %s",
|
||||
"[OAuth-LinuxDO] GetUserInfo error: %s": "[OAuth-LinuxDO] GetUserInfo 请求失败: %s",
|
||||
"[OAuth-LinuxDO] GetUserInfo failed: invalid user id": "[OAuth-LinuxDO] GetUserInfo 失败: 用户 id 无效",
|
||||
"[OAuth-LinuxDO] GetUserInfo response status: %d": "[OAuth-LinuxDO] GetUserInfo 响应状态码: %d",
|
||||
"[OAuth-LinuxDO] GetUserInfo success: id=%d, username=%s": "[OAuth-LinuxDO] GetUserInfo 成功: id=%d, username=%s",
|
||||
"[OAuth-LinuxDO] GetUserInfo: trust level too low (required=%d, current=%d)": "[OAuth-LinuxDO] GetUserInfo: 信任级别不足 (required=%d, current=%d)",
|
||||
"[OAuth-OIDC] ExchangeToken decode error: %s": "[OAuth-OIDC] ExchangeToken 解析响应失败: %s",
|
||||
"[OAuth-OIDC] ExchangeToken error: %s": "[OAuth-OIDC] ExchangeToken 请求失败: %s",
|
||||
"[OAuth-OIDC] ExchangeToken failed: empty access token": "[OAuth-OIDC] ExchangeToken 失败: 响应里没有 access token",
|
||||
"[OAuth-OIDC] ExchangeToken response status: %d": "[OAuth-OIDC] ExchangeToken 响应状态码: %d",
|
||||
"[OAuth-OIDC] ExchangeToken success: scope=%s": "[OAuth-OIDC] ExchangeToken 成功: scope=%s",
|
||||
"[OAuth-OIDC] GetUserInfo decode error: %s": "[OAuth-OIDC] GetUserInfo 解析响应失败: %s",
|
||||
"[OAuth-OIDC] GetUserInfo error: %s": "[OAuth-OIDC] GetUserInfo 请求失败: %s",
|
||||
"[OAuth-OIDC] GetUserInfo failed: empty fields (sub=%s, email=%s)": "[OAuth-OIDC] GetUserInfo 失败: 字段为空 (sub=%s, email=%s)",
|
||||
"[OAuth-OIDC] GetUserInfo failed: status=%d": "[OAuth-OIDC] GetUserInfo 失败: status=%d",
|
||||
"[OAuth-OIDC] GetUserInfo response status: %d": "[OAuth-OIDC] GetUserInfo 响应状态码: %d",
|
||||
"[OAuth-OIDC] GetUserInfo success: sub=%s, username=%s, name=%s, email=%s": "[OAuth-OIDC] GetUserInfo 成功: sub=%s, username=%s, name=%s, email=%s",
|
||||
"[OAuth] Failed to load verified emails for user %d: %s": "[OAuth] 加载用户 %d 的已验证邮箱失败: %s",
|
||||
"access token last-use update failed (token_id=%d): %v": "更新访问令牌最后使用时间失败 (token_id=%d): %v",
|
||||
"access token route is not declared: %s": "这条路由没有声明访问令牌的访问规则,已拒绝: %s",
|
||||
"admin reset subscription plan %d quota: reset_count=%d user_count=%d advance_reset_time=%t": "管理员重置了订阅套餐 %d 的额度: reset_count=%d user_count=%d advance_reset_time=%t",
|
||||
"admin subscription creation": "管理员创建订阅",
|
||||
"admin subscription deletion": "管理员删除订阅",
|
||||
"admin subscription update": "管理员修改订阅",
|
||||
"api_version not found, using default: %s": "未找到 api_version,使用默认值: %s",
|
||||
"audit actor role unavailable (request_id=%s, actor_role=%d)": "无法识别审计日志的操作者角色 (request_id=%s, actor_role=%d)",
|
||||
"audit log write failed (request_id=%s): %v": "审计日志写入失败 (request_id=%s): %v",
|
||||
"audit log write failed (request_id=%s): log database unavailable": "审计日志写入失败 (request_id=%s): 日志数据库不可用",
|
||||
"auth session internal error (%s %s): %v": "登录会话接口内部错误 (%s %s): %v",
|
||||
"authorization policy %d has an unsupported legacy scope; loaded as deny; review the stored policy before granting access": "授权策略 %d 带有已不支持的旧范围设置,已按拒绝(deny)加载。放行前请先检查这条已保存的策略。",
|
||||
"bad response status code %d with empty error message, body: %s": "上游返回状态码 %d,错误信息为空,响应体: %s",
|
||||
"bad response status code %d, body: %s": "上游返回异常状态码 %d,响应体: %s",
|
||||
"batch update enabled with interval %ss": "已启用批量更新,每 %s 秒执行一次",
|
||||
"batch update finished": "批量更新完成",
|
||||
"batch update overflow: type=%d id=%d old=%d value=%d": "批量更新的累加值溢出: type=%d id=%d old=%d value=%d",
|
||||
"batch update started": "批量更新开始",
|
||||
"billable tool count ignored: tool=%s count=%d": "工具计费次数无效,已忽略: tool=%s count=%d",
|
||||
"build initial plugin router: %s": "初始化插件路由失败: %s",
|
||||
"build request failed: %s": "创建请求失败: %s",
|
||||
"build task plugin native submit view failed: %s": "任务插件提交响应: 构建任务数据失败: %s",
|
||||
"build task protocol relay info failed: %s": "任务协议: 构建请求上下文失败: %s",
|
||||
"build task protocol renderer context failed": "任务协议: 构建渲染钩子的上下文失败",
|
||||
"build task protocol view failed: %s": "任务协议: 构建任务数据失败: %s",
|
||||
"channel #%d failed to update async tasks: %s": "渠道 #%d 更新异步任务失败: %s",
|
||||
"channel #%d has %d unfinished tasks": "渠道 #%d 未完成的任务有: %d",
|
||||
"channel %d polling index: %d": "渠道 %d 的多密钥轮询位置: %d",
|
||||
"channel %q (#%d) failed, disabling it, reason: %s": "通道「%s」(#%d)发生错误,准备禁用,原因:%s",
|
||||
"channel %q (#%d) has automatic disabling turned off, skipping": "通道「%s」(#%d)未启用自动禁用功能,跳过禁用操作",
|
||||
"channel affinity cache delete current failed: err=%v": "渠道亲和性缓存: 删除当前请求的记录失败: err=%v",
|
||||
"channel affinity cache delete many failed: err=%v": "渠道亲和性缓存: 批量删除失败: err=%v",
|
||||
"channel affinity cache get failed: key=%s, err=%v": "渠道亲和性缓存: 读取失败: key=%s, err=%v",
|
||||
"channel affinity cache list keys failed: err=%v": "渠道亲和性缓存: 列出缓存键失败: err=%v",
|
||||
"channel affinity cache set failed: key=%s, err=%v": "渠道亲和性缓存: 写入失败: key=%s, err=%v",
|
||||
"channel error (channel #%d, status code: %d): %s": "渠道请求出错 (渠道 #%d,状态码 %d): %s",
|
||||
"channel pin overridden: winning_source=%s winning_channel_id=%d overridden_source=%s overridden_channel_id=%d": "请求被多处指定了渠道,已按优先级采用其中一个: winning_source=%s winning_channel_id=%d overridden_source=%s overridden_channel_id=%d",
|
||||
"channel test bad response: channel_id=%d name=%s type=%d model=%s endpoint_type=%s status=%d err=%v": "渠道测试响应异常: channel_id=%d name=%s type=%d model=%s endpoint_type=%s status=%d err=%v",
|
||||
"channels synced from database": "已从数据库同步渠道",
|
||||
"channels update done": "所有渠道的余额更新完成",
|
||||
"charging the difference after pre-consume: %s (actual %s, pre-consumed %s)": "预扣费后补扣费:%s(实际消耗:%s,预扣费:%s)",
|
||||
"claude response usage is not complete, maybe upstream error": "Claude 响应里的用量信息不完整,可能是上游出错",
|
||||
"closed %d active websocket connection(s), channels=%v, kinds=%v, reason=%s": "已关闭 %d 个使用中的 WebSocket 连接: channels=%v, kinds=%v, reason=%s",
|
||||
"codex credential auto-refresh task started: tick=%s threshold=%s": "Codex 凭证自动刷新任务已启动: tick=%s threshold=%s",
|
||||
"codex credential auto-refresh: InitChannelCache panic: %v": "Codex 凭证自动刷新: InitChannelCache 发生 panic: %v",
|
||||
"codex credential auto-refresh: channel_id=%d name=%s refresh failed: %v": "Codex 凭证自动刷新: channel_id=%d name=%s 刷新失败: %v",
|
||||
"codex credential auto-refresh: channel_id=%d name=%s refreshed, expires_at=%s": "Codex 凭证自动刷新: channel_id=%d name=%s 已刷新,expires_at=%s",
|
||||
"codex credential auto-refresh: query channels failed: %v": "Codex 凭证自动刷新: 查询渠道失败: %v",
|
||||
"codex credential auto-refresh: scanned=%d refreshed=%d": "Codex 凭证自动刷新: scanned=%d refreshed=%d",
|
||||
"compile task plugin %s@%s: %v": "编译任务插件 %s@%s 失败: %v",
|
||||
"configure plugin router trusted proxies: %s": "为插件路由设置可信代理失败: %s",
|
||||
"conversion diagnostic: code=%q severity=%q from=%q to=%q path=%q message=%q": "格式转换诊断: code=%q severity=%q from=%q to=%q path=%q message=%q",
|
||||
"conversion diagnostics truncated after %d distinct entries": "格式转换诊断已达 %d 条上限,后续不再记录",
|
||||
"converted embedding request body: %s": "转换后的嵌入请求体: %s",
|
||||
"cpu usage too high": "CPU 使用率过高",
|
||||
"creem topup": "Creem 充值",
|
||||
"creem topup failed: %s": "Creem 充值失败: %s",
|
||||
"dashboard authentication error: %s": "控制台登录校验出错: %s",
|
||||
"data handler goroutine panic: %v": "数据处理 goroutine 发生 panic: %v",
|
||||
"database migration started": "开始数据库迁移",
|
||||
"do request failed: %s": "请求上游失败: %s",
|
||||
"downloading file from worker: %s, reason: %s": "通过 Worker 下载文件: %s,原因: %s",
|
||||
"downloading from origin: %s, reason: %s": "直接从源地址下载: %s,原因: %s",
|
||||
"encode task plugin native submit view failed: %s": "任务插件提交响应: 序列化任务数据失败: %s",
|
||||
"encode task protocol view failed: %s": "任务协议: 序列化任务数据失败: %s",
|
||||
"epay topup": "易支付充值",
|
||||
"epay topup failed: %s": "易支付充值失败: %s",
|
||||
"error adjusting token quota after funding settled (userId=%d, tokenId=%d, delta=%d): %s": "资金来源已结算,调整令牌额度时出错 (userId=%d, tokenId=%d, delta=%d): %s",
|
||||
"error closing websocket connection: %s": "关闭 WebSocket 连接出错: %s",
|
||||
"error consuming Midjourney quota: %s": "Midjourney 扣费出错: %s",
|
||||
"error converting Claude stream response: %s": "转换 Claude 流式响应出错: %s",
|
||||
"error converting final Gemini stream response: %s": "转换 Gemini 流式响应的最后一段出错: %s",
|
||||
"error creating Gemini stream state: %s": "初始化 Gemini 流式转换状态出错: %s",
|
||||
"error finalizing Gemini stream response: %s": "生成 Gemini 流式响应的结束段出错: %s",
|
||||
"error handling last response: %s, lastStreamData: [%s]": "处理最后一段流数据出错: %s, lastStreamData: [%s]",
|
||||
"error handling stream format: %s": "转换并发送流数据出错: %s",
|
||||
"error marshalling chats: %s": "序列化聊天配置出错: %s",
|
||||
"error marshalling model ratio: %s": "序列化模型倍率出错: %s",
|
||||
"error marshalling stream response: %s": "序列化流式响应出错: %s",
|
||||
"error marshalling topup group ratio: %s": "序列化充值分组倍率出错: %s",
|
||||
"error marshalling user groups: %s": "序列化用户分组出错: %s",
|
||||
"error processing stream token data: %s": "统计流数据的 token 出错: %s",
|
||||
"error reading stream response: %s": "读取流式响应出错: %s",
|
||||
"error reading stream: %s": "读取流式响应出错: %s",
|
||||
"error refunding billing source: %s": "退还资金来源出错: %s",
|
||||
"error refunding subscription extra reserved quota: %s": "退还订阅额外预扣的额度出错: %s",
|
||||
"error refunding token quota: %s": "退还令牌额度出错: %s",
|
||||
"error rolling back subscription funding reserve: %s": "回滚订阅预扣额度出错: %s",
|
||||
"error rolling back token quota (userId=%d, tokenId=%d, amount=%d, fundingErr=%s): %s": "回滚令牌额度出错 (userId=%d, tokenId=%d, amount=%d, fundingErr=%s): %s",
|
||||
"error rolling back wallet funding reserve: %s": "回滚钱包预扣额度出错: %s",
|
||||
"error sending final Gemini stream response: %s": "发送 Gemini 流式响应的最后一段出错: %s",
|
||||
"error sending finalized Gemini stream response: %s": "发送 Gemini 流式响应的结束段出错: %s",
|
||||
"error sending stream response: %s": "发送流式响应出错: %s",
|
||||
"error settling Midjourney quota: %s": "Midjourney 结算出错: %s",
|
||||
"error settling billing: %s": "计费结算出错: %s",
|
||||
"error unmarshalling stream response: %s": "解析流式响应出错: %s",
|
||||
"error: %s": "错误: %s",
|
||||
"error_rendering_final_usage_response: %s": "发送用量数据出错: %s",
|
||||
"error_rendering_stream_response: %s": "发送流式响应出错: %s",
|
||||
"error_scanning_stream_response: %s": "读取流式响应出错: %s",
|
||||
"error_unmarshalling_stream_response: %s": "解析流式响应出错: %s",
|
||||
"expected Claude stream responses, got %T": "这里需要 Claude 流式响应,实际类型是 %T",
|
||||
"fail to decode image config(gif, jpg, png): %s": "解析图片头信息失败 (gif, jpg, png): %s",
|
||||
"fail to decode image config(webp): %s": "解析图片头信息失败 (webp): %s",
|
||||
"fail to get file type from url: %s, error: %s": "获取文件类型失败,URL: %s,错误: %s",
|
||||
"fail to get image from url: %s": "下载图片失败: %s",
|
||||
"failed to add user field: %s": "写入表单字段 user 失败: %s",
|
||||
"failed to adjust funding for the difference, task %s: %s": "差额结算资金调整失败 task %s: %s",
|
||||
"failed to adjust token quota (delta=%d, task=%s): %s": "调整令牌额度失败 (delta=%d, task=%s): %s",
|
||||
"failed to batch update token quota: %s": "批量更新令牌额度失败: %s",
|
||||
"failed to batch update user quota, used quota and request count: %s": "批量更新用户额度、已用额度和请求次数失败: %s",
|
||||
"failed to bind upstream request: %s": "解析上游倍率同步的请求参数失败: %s",
|
||||
"failed to cache refresh-reuse session revoke: %s": "刷新令牌被重复使用,会话已吊销,写入缓存失败: %s",
|
||||
"failed to charge violation fee: %s": "违规扣费失败: %s",
|
||||
"failed to check channel status before closing active websockets: channel_id=%d, error=%v": "关闭 WebSocket 连接前查询渠道状态失败: channel_id=%d, error=%v",
|
||||
"failed to check notification limit: %s": "检查通知频率限制失败: %s",
|
||||
"failed to check the successful request limit: %s": "检查成功请求数限制失败: %s",
|
||||
"failed to check the total request limit: %s": "检查总请求数限制失败: %s",
|
||||
"failed to cleanup expired perf metrics: %s": "清理过期性能指标失败: %s",
|
||||
"failed to clone channel: %s": "复制渠道失败: %s",
|
||||
"failed to close database: %s": "关闭数据库失败: %s",
|
||||
"failed to close response body: %s": "关闭响应体失败: %s",
|
||||
"failed to compensate reserved token quota: result=%d error=%v": "回补缓存中已预扣的令牌额度失败: result=%d error=%v",
|
||||
"failed to compensate reserved user quota: result=%d error=%v": "回补缓存中已预扣的用户额度失败: result=%d error=%v",
|
||||
"failed to configure trusted proxies: %s": "设置可信代理失败: %s",
|
||||
"failed to copy file content: %s": "复制文件内容失败: %s",
|
||||
"failed to copy response body: %s": "复制响应体失败: %s",
|
||||
"failed to count backup codes: %s": "获取备用码数量失败: %s",
|
||||
"failed to count channel types: %s": "统计渠道类型失败: %s",
|
||||
"failed to count channels: %s": "统计渠道数量失败: %s",
|
||||
"failed to count hourly user session issuance: %s": "统计近一小时新建的登录会话数失败: %s",
|
||||
"failed to count search tokens: %s": "统计令牌搜索结果数量失败: %s",
|
||||
"failed to count search topups: %s": "统计充值记录搜索结果数量失败: %s",
|
||||
"failed to count tags: %s": "统计标签数量失败: %s",
|
||||
"failed to count user logs: %s": "统计用户日志数量失败: %s",
|
||||
"failed to count user tokens: %s": "统计用户令牌数量失败: %s",
|
||||
"failed to create disk storage, falling back to memory: %v": "请求体写入磁盘失败,改为保存在内存: %v",
|
||||
"failed to create form file: %s": "创建表单文件失败: %s",
|
||||
"failed to create request: %s": "创建请求失败: %s",
|
||||
"failed to create setup record: %s": "创建系统初始化记录失败: %s",
|
||||
"failed to create temp file: %s": "创建临时文件失败: %s",
|
||||
"failed to create the pprof directory: %s": "创建pprof文件夹失败 %s",
|
||||
"failed to create the pprof file: %s": "创建pprof文件失败 %s",
|
||||
"failed to decode base64: %s": "base64 解码失败: %s",
|
||||
"failed to decode response: %s": "解析响应失败: %s",
|
||||
"failed to decrease token quota: %s": "扣减令牌额度失败: %s",
|
||||
"failed to decrease user quota cache after subscription balance purchase: %s": "余额购买订阅后,扣减用户额度缓存失败: %s",
|
||||
"failed to decrease user quota: %s": "扣减用户额度失败: %s",
|
||||
"failed to delete expired access tokens: %s": "清理过期的访问令牌失败: %s",
|
||||
"failed to delete expired authentication flows: %s": "清理过期的登录验证流程失败: %s",
|
||||
"failed to delete expired user sessions: %s": "清理过期的登录会话失败: %s",
|
||||
"failed to delete old revoked user sessions: %s": "清理超过保留期的已吊销会话失败: %s",
|
||||
"failed to download file from %s, status code: %d": "从 %s 下载文件失败,状态码: %d",
|
||||
"failed to encode log admin info: %s": "序列化日志的 admin_info 失败: %s",
|
||||
"failed to fetch codex reset credits": "获取 Codex 重置次数失败",
|
||||
"failed to fetch codex usage": "获取 Codex 用量失败",
|
||||
"failed to finalize bulk user session revoke tombstone: %s": "批量吊销会话后,写入吊销缓存失败: %s",
|
||||
"failed to finalize refresh-authenticated session revoke tombstone: %s": "按刷新令牌吊销会话后,写入吊销缓存失败: %s",
|
||||
"failed to finalize user session revoke tombstone: %s": "吊销会话后,写入吊销缓存失败: %s",
|
||||
"failed to flush perf metric bucket model=%s group=%s bucket=%d: %s": "保存性能指标失败 model=%s group=%s bucket=%d: %s",
|
||||
"failed to generate backup codes: %s": "生成备用码失败: %s",
|
||||
"failed to generate the default sidebar config: %s": "生成默认边栏配置失败: %s",
|
||||
"failed to generate token key: %s": "生成令牌密钥失败: %s",
|
||||
"failed to get audio duration: %v": "获取音频时长失败: %v",
|
||||
"failed to get channel by id: %s": "按 ID 查询渠道失败: %s",
|
||||
"failed to get channels by tag: %s": "按标签查询渠道失败: %s",
|
||||
"failed to get channels: %s": "查询渠道失败: %s",
|
||||
"failed to get paginated tags: %s": "分页查询标签失败: %s",
|
||||
"failed to get token by key: %s": "按密钥查询令牌失败: %s",
|
||||
"failed to get token key (tokenId=%d, task=%s): %s": "获取令牌 key 失败 (tokenId=%d, task=%s): %s",
|
||||
"failed to get user quota: %s": "查询用户额度失败: %s",
|
||||
"failed to increase token quota: %s": "增加令牌额度失败: %s",
|
||||
"failed to increase user quota: %s": "增加用户额度失败: %s",
|
||||
"failed to init token cache: %s": "初始化令牌缓存失败: %s",
|
||||
"failed to initialize authorization: %s": "初始化权限控制失败: %s",
|
||||
"failed to initialize database: %s": "初始化数据库失败: %s",
|
||||
"failed to initialize i18n: %s": "初始化 i18n 失败: %s",
|
||||
"failed to initialize password encryption: %s": "初始化密码加密失败: %s",
|
||||
"failed to initialize resources: %s": "初始化资源失败: %s",
|
||||
"failed to insert redemption: %s": "保存兑换码失败: %s",
|
||||
"failed to invalidate token cache after hard deleting user %d: %v": "永久删除用户 %d 后清除令牌缓存失败: %v",
|
||||
"failed to invalidate token cache before batch delete: %s": "批量删除令牌前清除缓存失败: %s",
|
||||
"failed to invalidate token cache before delete: %s": "删除令牌前清除缓存失败: %s",
|
||||
"failed to invalidate token cache before status update: %s": "更新令牌状态前清除缓存失败: %s",
|
||||
"failed to invalidate token cache before update: %s": "更新令牌前清除缓存失败: %s",
|
||||
"failed to invalidate tokens cache for user %d: %s": "清除用户 %d 的令牌缓存失败: %s",
|
||||
"failed to invalidate user cache after hard deleting user %d: %v": "永久删除用户 %d 后清除用户缓存失败: %v",
|
||||
"failed to load custom OAuth providers: %s": "加载自定义 OAuth 提供商失败: %s",
|
||||
"failed to load tool prices, using hardcoded fallbacks: %s": "加载工具价格失败,将使用硬编码兜底: %s",
|
||||
"failed to marshal gemini response: %s": "序列化 Gemini 响应失败: %s",
|
||||
"failed to marshal log other: %s": "序列化日志 other 字段失败: %s",
|
||||
"failed to marshal other info: channel_id=%d, tag=%s, name=%s, error=%v": "序列化渠道的 other_info 失败: channel_id=%d, tag=%s, name=%s, error=%v",
|
||||
"failed to marshal setting: %s": "序列化用户设置失败: %s",
|
||||
"failed to marshal setting: channel_id=%d, error=%v": "序列化渠道设置失败: channel_id=%d, error=%v",
|
||||
"failed to migrate options primary key: %s": "为 options 表补主键失败: %s",
|
||||
"failed to migrate retired frontend options: %s": "迁移旧前端的配置项失败: %s",
|
||||
"failed to open log file": "打开日志文件失败",
|
||||
"failed to parse %s: %s, using default value: %d": "环境变量 %s 解析失败: %s,已使用默认值: %d",
|
||||
"failed to parse %s: %s, using default value: %t": "环境变量 %s 解析失败: %s,已使用默认值: %t",
|
||||
"failed to parse CHANNEL_UPDATE_FREQUENCY: %s": "解析 CHANNEL_UPDATE_FREQUENCY 失败: %s",
|
||||
"failed to parse Redis connection string: %s": "解析 Redis 连接字符串失败: %s",
|
||||
"failed to parse auto groups for token %d: %v": "解析令牌 %d 的 Auto 分组失败: %v",
|
||||
"failed to parse oauth key: %s": "解析渠道的 OAuth 凭证失败: %s",
|
||||
"failed to populate newly created user session cache: %s": "新建登录会话后写入缓存失败: %s",
|
||||
"failed to publish auth tombstone after hard deleting user %d: %v": "永久删除用户 %d 后发布登录失效通知失败: %v",
|
||||
"failed to publish websocket close event: %v": "向其他节点广播 WebSocket 关闭事件失败: %v",
|
||||
"failed to query administrators to notify: %s": "查询需要通知的管理员失败: %s",
|
||||
"failed to query channels: %s": "查询渠道失败: %s",
|
||||
"failed to query log stat: %s": "查询日志统计失败: %s",
|
||||
"failed to query rpm/tpm stat: %s": "查询 rpm/tpm 统计失败: %s",
|
||||
"failed to read TTS response body: %v": "读取 TTS 响应体失败: %v",
|
||||
"failed to record log: %s": "记录日志失败: %s",
|
||||
"failed to record task billing log: %s": "记录任务计费日志失败: %s",
|
||||
"failed to record topup log: %s": "记录充值日志失败: %s",
|
||||
"failed to redeem key %s for user %d: %s": "用户 %[2]d 使用兑换码 %[1]s 兑换失败: %[3]s",
|
||||
"failed to refresh codex channel credential: %s": "刷新 Codex 渠道凭证失败: %s",
|
||||
"failed to refresh user group cache after %s for user %d: %v": "刷新用户 %[2]d 的分组缓存失败(操作: %[1]s): %[3]v",
|
||||
"failed to refund Midjourney token quota, task %s: %s": "退还 Midjourney 令牌额度失败 task %s: %s",
|
||||
"failed to refund Midjourney user quota, task %s: %s": "退还 Midjourney 用户额度失败 task %s: %s",
|
||||
"failed to refund the funding source, task %s: %s": "退还资金来源失败 task %s: %s",
|
||||
"failed to reload authz policy: %s": "重新加载授权策略失败: %s",
|
||||
"failed to reset codex usage": "重置 Codex 用量失败",
|
||||
"failed to save backup codes: %s": "保存备用码失败: %s",
|
||||
"failed to search tokens: %s": "搜索令牌失败: %s",
|
||||
"failed to search topups: %s": "搜索充值记录失败: %s",
|
||||
"failed to search user logs: %s": "搜索用户日志失败: %s",
|
||||
"failed to send email to %s: %v": "向 %s 发送邮件失败: %v",
|
||||
"failed to send notice %s to user %d: %s": "向用户 %[2]d 发送通知 %[1]s 失败: %[3]s",
|
||||
"failed to send password reset email to %s: %s": "向 %s 发送密码重置邮件失败: %s",
|
||||
"failed to send quota notify to user %d: %s": "向用户 %d 发送额度通知失败: %s",
|
||||
"failed to send request: %s": "发送请求失败: %s",
|
||||
"failed to send subscription quota notify to user %d: %s": "向用户 %d 发送订阅额度通知失败: %s",
|
||||
"failed to serialize VideoUrls: %v": "序列化 VideoUrls 失败: %v",
|
||||
"failed to start HTTP server: %s": "HTTP 服务启动失败: %s",
|
||||
"failed to start pprof: %s": "启动pprof失败 %s",
|
||||
"failed to sync %s credit to user quota cache: %s": "把到账额度同步到用户额度缓存失败(操作: %s): %s",
|
||||
"failed to sync manual quota adjustment for user %d: %s": "把用户 %d 的手动调额同步到缓存失败: %s",
|
||||
"failed to synchronously populate user cache: %s": "同步写入用户缓存失败: %s",
|
||||
"failed to synchronously populate user session cache: %s": "同步写入用户会话缓存失败: %s",
|
||||
"failed to unmarshal Claude stream event: %s": "解析 Claude 流事件失败: %s",
|
||||
"failed to unmarshal buffered responses stream event: %s": "解析已缓冲的 Responses 流事件失败: %s",
|
||||
"failed to unmarshal chat stream response: %s": "解析 Chat 流式响应失败: %s",
|
||||
"failed to unmarshal header override: channel_id=%d, error=%v": "解析渠道的请求头覆盖失败: channel_id=%d, error=%v",
|
||||
"failed to unmarshal other info: channel_id=%d, tag=%s, name=%s, error=%v": "解析渠道的 other_info 失败: channel_id=%d, tag=%s, name=%s, error=%v",
|
||||
"failed to unmarshal param override: channel_id=%d, error=%v": "解析渠道的参数覆盖失败: channel_id=%d, error=%v",
|
||||
"failed to unmarshal responses stream event: %s": "解析 Responses 流事件失败: %s",
|
||||
"failed to unmarshal setting: %s": "解析用户设置失败: %s",
|
||||
"failed to unmarshal setting: channel_id=%d, error=%v": "解析渠道设置失败: channel_id=%d, error=%v",
|
||||
"failed to unmarshal stream response: %s": "解析流式响应失败: %s",
|
||||
"failed to unmarshal user: %s": "解析 user 失败: %s",
|
||||
"failed to unmarshal websocket close event: %v": "解析 WebSocket 关闭事件失败: %v",
|
||||
"failed to update abilities: channel_id=%d, tag=%s, error=%v": "更新渠道的 abilities 记录失败: channel_id=%d, tag=%s, error=%v",
|
||||
"failed to update ability status: channel_id=%d, error=%v": "更新渠道的 abilities 状态失败: channel_id=%d, error=%v",
|
||||
"failed to update balance: channel_id=%d, error=%v": "更新余额失败: channel_id=%d, error=%v",
|
||||
"failed to update channel status: channel_id=%d, status=%d, error=%v": "更新渠道状态失败: channel_id=%d, status=%d, error=%v",
|
||||
"failed to update channel used quota: channel_id=%d, delta_quota=%d, error=%v": "更新渠道已用额度失败: channel_id=%d, delta_quota=%d, error=%v",
|
||||
"failed to update config %s: %s": "更新配置 %s 失败: %s",
|
||||
"failed to update multi-key status: channel_id=%d, using key not found": "更新多密钥状态失败: channel_id=%d,密钥列表里找不到本次使用的密钥",
|
||||
"failed to update option map: %s": "加载配置项失败: %s",
|
||||
"failed to update response time: channel_id=%d, error=%v": "更新响应时间失败: channel_id=%d, error=%v",
|
||||
"failed to update rotated user session cache: %s": "续期登录后更新会话缓存失败: %s",
|
||||
"failed to update token status%s": "更新令牌状态失败: %s",
|
||||
"failed to update user group cache: %s": "更新用户分组缓存失败: %s",
|
||||
"failed to update user last_login_at: %s": "更新用户最后登录时间失败: %s",
|
||||
"failed to update user name cache: %s": "更新用户名缓存失败: %s",
|
||||
"failed to update user setting cache: %s": "更新用户设置缓存失败: %s",
|
||||
"failed to update user used quota and request count: %s": "更新用户已用额度和请求次数失败: %s",
|
||||
"failed to update user used quota: %s": "更新用户已用额度失败: %s",
|
||||
"failed to validate cloned channel: %s": "复制出的渠道未通过校验: %s",
|
||||
"failed to write TTS response: %v": "写入 TTS 响应失败: %v",
|
||||
"failed to write back the settled task quota, task %s: %s": "差额结算回写 quota 失败 task %s: %s",
|
||||
"failed to write stream data: %s": "写入流数据失败: %s",
|
||||
"failed to write to temp file: %s": "写入临时文件失败: %s",
|
||||
"final group ratio: %f": "最终分组倍率: %f",
|
||||
"final group: %s": "最终使用的分组: %s",
|
||||
"getAndValidateTextRequest failed: %s": "getAndValidateTextRequest 失败: %s",
|
||||
"get_channel_null: %s": "查询渠道失败: %s",
|
||||
"group ratio not found: %s": "未找到分组倍率: %s",
|
||||
"hourly user session issuance exceeded alert threshold: count=%d threshold=%d window_seconds=%d": "近一小时新建的登录会话数超过告警阈值: count=%d threshold=%d window_seconds=%d",
|
||||
"http error on %s: %s": "请求 %s 出错: %s",
|
||||
"http transport negotiated: host=%s protocol=%s shards=%d policy=%s negotiated=%s": "HTTP 连接协商结果: host=%s protocol=%s shards=%d policy=%s negotiated=%s",
|
||||
"http transport select: host=%s protocol=%s shards=%d policy=%s": "HTTP 连接选择: host=%s protocol=%s shards=%d policy=%s",
|
||||
"http transport: host=%s protocol=%s shard=%d/%d policy=%s negotiated=%s": "HTTP 连接: host=%s protocol=%s shard=%d/%d policy=%s negotiated=%s",
|
||||
"i18n initialized with languages: %s": "i18n 已初始化,语言: %s",
|
||||
"image request body: %s": "图片请求体: %s",
|
||||
"image token input: format=%s, width=%d, height=%d": "计算图片 token: format=%s, width=%d, height=%d",
|
||||
"increaseQuotaData error: %s": "increaseQuotaData 出错: %s",
|
||||
"initialize legacy access token deadline: %s": "设置旧访问令牌的停用时间失败: %s",
|
||||
"initialized the sidebar config for new user %s (role: %d)": "为新用户 %s (角色: %d) 初始化边栏配置",
|
||||
"insert task error: %s": "保存任务失败: %s",
|
||||
"invalid SQL_SLOW_THRESHOLD_MS %d (allowed 0-%d, 0 disables slow query log), using default %d": "SQL_SLOW_THRESHOLD_MS=%d 超出范围(可填 0 到 %d,0 表示关闭慢查询日志),已使用默认值 %d",
|
||||
"invalid access token scopes: %s": "访问令牌的作用域无效: %s",
|
||||
"invalid channel http transport setting clamped: %s=%q": "渠道的 HTTP 连接设置超出范围,已调整为允许的值: %s=%q",
|
||||
"invalid image cache token breakdown; using aggregate cache billing": "图片缓存 token 的分项数据无效,按缓存 token 总量计费",
|
||||
"invalid image stream response": "图片流式响应无效",
|
||||
"invalid request policy: %s": "请求策略无效: %s",
|
||||
"invalid response or response body": "上游响应为空或没有响应体",
|
||||
"invalid task artifact store configuration: %s; using upstream mode": "任务制品存储配置无效: %s,改用 upstream 模式",
|
||||
"invalid thinking_model_blacklist regex %q: %v": "thinking_model_blacklist 正则 %q 无效: %v",
|
||||
"invalid thinking_model_blacklist regex %q: pattern is empty": "thinking_model_blacklist 正则 %q 无效: 表达式为空",
|
||||
"invalid zhipu key: %s": "智谱密钥格式无效: %s",
|
||||
"json decode failed from %s: %s": "解析 %s 返回的 JSON 失败: %s",
|
||||
"json marshal failed: %s": "JSON 序列化失败: %s",
|
||||
"legacy Uptime Kuma options were not migrated: %v": "旧的 Uptime Kuma 配置迁移失败: %v",
|
||||
"legacy Uptime Kuma options were not migrated: both URL and slug are required": "旧的 Uptime Kuma 配置没有迁移: URL 和 slug 都需要填写",
|
||||
"legacy option %s was not migrated: %v": "旧配置项 %s 迁移失败: %v",
|
||||
"legacy proxy URL suffix ignored at runtime: scheme=%s host=%s; update the channel proxy setting": "渠道代理地址带有旧写法的后缀,运行时已忽略: scheme=%s host=%s。请修改渠道的代理设置",
|
||||
"load advanced custom channel settings error: channel_id=%d, error=%v": "加载高级自定义渠道设置出错: channel_id=%d, error=%v",
|
||||
"loadFromURL: decoding image config": "loadFromURL: 解析图片头信息",
|
||||
"loadFromURL: initiating download": "loadFromURL: 开始下载",
|
||||
"loadFromURL: reading response body": "loadFromURL: 读取响应体",
|
||||
"manual topup": "管理员补单",
|
||||
"memory cache enabled": "已启用内存缓存",
|
||||
"midjourney response body: %s": "Midjourney 响应体: %s",
|
||||
"model %s is free, skipping pre-consume": "模型 %s 免费,跳过预扣费",
|
||||
"model price not found: %s": "未找到模型价格: %s",
|
||||
"model_price_helper result: %s": "model_price_helper 结果: %s",
|
||||
"model_price_helper_tiered result: model=%s preConsume=%d quotaBeforeGroup=%.2f groupRatio=%.2f tier=%s": "model_price_helper_tiered 结果: model=%s preConsume=%d quotaBeforeGroup=%.2f groupRatio=%.2f tier=%s",
|
||||
"models.dev parse failed from %s: %s": "解析 %s 返回的 models.dev 数据失败: %s",
|
||||
"no such user %s": "用户 %s 不存在",
|
||||
"no user exists, create a root user for you: username is root, password is 123456": "系统里还没有用户,已自动创建 root 账号:用户名 root,密码 123456",
|
||||
"non-200 from %s: %s": "%s 返回了非 200 状态: %s",
|
||||
"ollama stream json decode error: %s line=%s": "Ollama 流式响应 JSON 解析失败: %s line=%s",
|
||||
"ollama stream scan error: %s": "Ollama 流式响应读取出错: %s",
|
||||
"openrouter enterprise response success=false, data: %s": "OpenRouter Enterprise 响应 success=false, data: %s",
|
||||
"options key %q had conflicting values; keeping the last row": "options 表的键 %q 有多行且值不同,保留最后一行的值",
|
||||
"options table has duplicate pricing keys [%s]; the table is missing a primary key": "options 表缺少主键,定价配置出现重复的键 [%s]",
|
||||
"panic detected: %v": "发生 panic: %v",
|
||||
"panic in gopool.RelayPool: %v": "gopool.RelayPool 中发生 panic: %v",
|
||||
"panic recovered in plugin route": "插件路由发生 panic,已恢复",
|
||||
"passkey verification rejected: code=%s reason=%s rp_id=%q": "通行密钥验证未通过: code=%s reason=%s rp_id=%q",
|
||||
"payment compliance confirmed user_id=%d ip=%s terms_version=%s confirmed_at=%d": "用户已确认支付合规条款 user_id=%d ip=%s terms_version=%s confirmed_at=%d",
|
||||
"persist task background flag failed: %s": "保存任务的后台运行标记失败: %s",
|
||||
"ping data error: %s": "发送 ping 数据出错: %s",
|
||||
"ping data sent": "ping 数据已发送",
|
||||
"ping goroutine exited": "ping goroutine 已退出",
|
||||
"ping goroutine max duration reached": "ping goroutine 运行达到最长时限,已停止",
|
||||
"ping goroutine panic: %v": "ping goroutine 发生 panic: %v",
|
||||
"ping interval seconds: %d": "ping 间隔: %d 秒",
|
||||
"pinned task plugin does not support the requested protocol form": "选定的任务插件不支持本次请求的调用方式(流式、后台或同步)",
|
||||
"pprof enabled": "已启用 pprof",
|
||||
"pre-consumed quota matches the actual quota, no adjustment: %s (per-request billing)": "预扣费与实际消耗一致,无需调整:%s(按次计费)",
|
||||
"quota saturation on consume log: op=%s kind=%s original=%g clamped=%d user=%d model=%s": "消费日志: 额度超出上限,已按上限记录: op=%s kind=%s original=%g clamped=%d user=%d model=%s",
|
||||
"rate limit check failed (key=%s): %v": "限流检查失败 (key=%s): %v",
|
||||
"rate limit check failed (mark=%s): %v": "限流检查失败 (mark=%s): %v",
|
||||
"read response failed from %s: %s": "读取 %s 的响应失败: %s",
|
||||
"ready in %d ms": "启动完成,耗时 %d ms",
|
||||
"realtime error: %s": "Realtime 出错: %s",
|
||||
"realtime streaming consume quota success, quota: %d": "Realtime 扣费成功,额度: %d",
|
||||
"realtime streaming localUsage: %v": "Realtime 流式 localUsage: %v",
|
||||
"realtime streaming sumUsage: %v": "Realtime 流式 sumUsage: %v",
|
||||
"rebuild task alias view: %s": "重建任务模型别名表失败: %s",
|
||||
"rebuilt options table with primary key from %d rows into %d keys; previous rows kept in %s": "已重建 options 表并加上主键,%d 行合并成 %d 个键,重建前的数据保存在 %s",
|
||||
"received [DONE], stopping scanner": "收到 [DONE],停止读取流",
|
||||
"received signal: %v, shutting down...": "收到信号 %v,正在关闭服务...",
|
||||
"record consume log: userId=%d, params=%s": "记录消费日志: userId=%d, params=%s",
|
||||
"record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s": "记录错误日志: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s",
|
||||
"redemption": "兑换码兑换",
|
||||
"redemption failed: %s": "兑换失败: %s",
|
||||
"refund succeeded but clearing the task quota failed, task %s: %s": "退款成功但清除 task quota 失败 task %s: %s",
|
||||
"refunding the difference after pre-consume: %s (actual %s, pre-consumed %s)": "预扣费后返还扣费:%s(实际消耗:%s,预扣费:%s)",
|
||||
"relay error (channel #%d, status code %d): %s": "转发请求出错 (渠道 #%d,状态码 %d): %s",
|
||||
"relay error: %s": "转发请求出错: %s",
|
||||
"relay max idle conns per host: %d": "转发连接池每个主机的最大空闲连接数: %d",
|
||||
"relay max idle conns: %d": "转发连接池的最大空闲连接数: %d",
|
||||
"relay timeout seconds: %d": "转发超时: %d 秒",
|
||||
"reranker response body: %s": "重排序响应体: %s",
|
||||
"reserve adjusted task billing error: %s": "按调整后的任务费用补扣预扣费失败: %s",
|
||||
"responses stream failed after output was delivered, billing the delivered output: %s": "Responses 流中途失败,已输出的内容照常计费: %s",
|
||||
"responses websocket relay error: %s": "Responses WebSocket 转发出错: %s",
|
||||
"retry: %s": "重试:%s",
|
||||
"running in debug mode": "调试模式已开启",
|
||||
"saved dashboard data, %d records": "保存数据看板数据成功,共保存%d条数据",
|
||||
"scanner error: %s": "读取流出错: %s",
|
||||
"scanner goroutine exited": "scanner goroutine 已退出",
|
||||
"scanner goroutine panic: %v": "scanner goroutine 发生 panic: %v",
|
||||
"send final Gemini stream response failed: %s": "发送 Gemini 流式响应的最后一段失败: %s",
|
||||
"send final response failed: %s": "发送最后一段响应失败: %s",
|
||||
"send_stream_response_failed: %s": "发送流式响应失败: %s",
|
||||
"server exited": "服务已退出",
|
||||
"server forced to shutdown: %v": "服务未能正常关闭,已强制退出: %v",
|
||||
"settle task billing error: %s": "任务计费结算出错: %s",
|
||||
"setup log is already working": "日志正在初始化,跳过本次调用",
|
||||
"skip password reset email for %s: %s": "未向 %s 发送密码重置邮件: %s",
|
||||
"skipped %d options rows with empty keys while rebuilding the primary key": "重建主键时跳过了 %d 行键为空的 options 记录",
|
||||
"stacktrace from panic: %s": "panic 堆栈: %s",
|
||||
"start pyroscope error : %v": "启动 pyroscope 出错: %v",
|
||||
"stream ended: %s": "流式响应结束: %s",
|
||||
"stream ended: %s, received=%d": "流式响应结束: %s, received=%d",
|
||||
"stream event error: %v %v": "流事件报错: %v %v",
|
||||
"stream scanner data: %s": "收到流数据: %s",
|
||||
"streaming timeout seconds: %d": "流式超时: %d 秒",
|
||||
"stripe topup": "Stripe 充值",
|
||||
"subscription balance purchase": "余额购买订阅",
|
||||
"subscription expiration": "订阅到期",
|
||||
"subscription expire task failed: %v": "处理过期订阅失败: %v",
|
||||
"subscription maintenance: reset_count=%d, expired_count=%d": "订阅维护: reset_count=%d, expired_count=%d",
|
||||
"subscription payment completion": "订阅支付完成",
|
||||
"subscription quota reset task failed: %v": "重置订阅额度失败: %v",
|
||||
"subscription quota reset task started: tick=%s": "订阅额度重置任务已启动: tick=%s",
|
||||
"sweepTimedOutTasks CAS update error for task %s: %v": "sweepTimedOutTasks: 更新任务 %s 的状态出错: %v",
|
||||
"sweepTimedOutTasks: task %s already transitioned, skip": "sweepTimedOutTasks: 任务 %s 的状态已由其他进程更新,跳过",
|
||||
"sweepTimedOutTasks: timed out %d tasks": "sweepTimedOutTasks: 已把 %d 个超时任务标记为失败",
|
||||
"sync frequency: %d seconds": "同步间隔: %d 秒",
|
||||
"syncing channels from database": "正在从数据库同步渠道",
|
||||
"syncing options from database": "正在从数据库同步配置项",
|
||||
"system instance report failed: %v": "上报本实例信息失败: %v",
|
||||
"system is already initialized at: %s": "系统已于 %s 完成初始化",
|
||||
"system is not initialized and no root user exists": "系统尚未初始化,还没有 root 用户",
|
||||
"system is not initialized, but root user exists": "系统尚未初始化,但已有 root 用户",
|
||||
"system task %s failed to persist result: %v": "系统任务 %s 保存结果失败: %v",
|
||||
"system task %s failed to save failure state: %v": "系统任务 %s 保存失败状态时出错: %v",
|
||||
"system task %s failed: %v": "系统任务 %s 失败: %v",
|
||||
"system task %s lock lost": "系统任务 %s 的执行锁已失效",
|
||||
"system task %s update failed: %v": "系统任务 %s 更新失败: %v",
|
||||
"system task claim failed: %v": "领取系统任务失败: %v",
|
||||
"system task runner query failed: %v": "查询待执行的系统任务失败: %v",
|
||||
"system task runner started: runner=%s idle_interval=%s": "系统任务执行器已启动: runner=%s idle_interval=%s",
|
||||
"system task scheduler active lookup failed: type=%s err=%v": "系统任务调度: 查询进行中的任务失败: type=%s err=%v",
|
||||
"system task scheduler create failed: type=%s err=%v": "系统任务调度: 创建任务失败: type=%s err=%v",
|
||||
"system task scheduler query failed: %v": "系统任务调度: 查询失败: %v",
|
||||
"system task stale lock cleanup failed: %v": "清理系统任务的过期执行锁失败: %v",
|
||||
"task %s expression settlement failed, keeping the pre-consumed quota: %v": "任务 %s 表达式结算失败,保留预扣额度: %v",
|
||||
"task %s expression settlement quota was clamped: %+v": "任务 %s 表达式结算额度发生饱和: %+v",
|
||||
"task %s is billed per request, skipping difference settlement": "任务 %s 按次计费,跳过差额结算",
|
||||
"task %s poll %s (failures=%d, http=%d): %s": "任务 %s 轮询结果异常: %s (failures=%d, http=%d): %s",
|
||||
"task %s pre-consumed quota matches the actual quota (%s, %s)": "任务 %s 预扣费准确(%s,%s)",
|
||||
"task %s settles the difference: delta=%s (actual %s, pre-consumed %s, %s)": "任务 %s 差额结算:delta=%s(实际:%s,预扣:%s,%s)",
|
||||
"task alias mapping cycle dropped: channel=%d key=%q": "渠道的模型映射成环,这个任务模型别名已忽略: channel=%d key=%q",
|
||||
"task alias view: channel %d model_mapping: %s": "解析渠道 %d 的 model_mapping 失败,任务模型别名未包含该渠道: %s",
|
||||
"task artifact S3 storage is not implemented; using upstream mode": "任务制品 S3 存储尚未实现,改用 upstream 模式",
|
||||
"task billing other ratio key rejected: %s": "任务计费: 倍率键 %s 不能写入日志,已跳过",
|
||||
"task immediate usage settlement failed; retaining reserved quota: %v": "任务提交即完成,按用量结算失败,保留预扣额度: %v",
|
||||
"task model alias %q dropped: maps to multiple plugins %v": "任务模型别名 %q 对应多个插件 %v,已忽略",
|
||||
"task plugin %s completion context failed; retaining reserved quota: %v": "任务插件 %s: 构建完成结算的上下文失败,保留预扣额度: %v",
|
||||
"task plugin %s completion usage failed; retaining reserved quota: %v": "任务插件 %s: 提取完成用量失败,保留预扣额度: %v",
|
||||
"task plugin %s completion usage rejected; retaining reserved quota: %v": "任务插件 %s: 返回的完成用量无效,保留预扣额度: %v",
|
||||
"task plugin %s rejected invalid %s billing facts": "任务插件 %s 的 %s 钩子返回了无效的计费数据,已忽略",
|
||||
"task plugin %s rejected invalid or oversized poll state": "任务插件 %s 返回的轮询状态无效或过大,未保存",
|
||||
"task plugin %s rejected oversized poll state (%d bytes)": "任务插件 %s 返回的轮询状态过大(%d 字节),未保存",
|
||||
"task plugin %s rejected oversized submit state (%d bytes)": "任务插件 %s 返回的提交状态过大(%d 字节),未保存",
|
||||
"task plugin native submit presenter failed: %s": "任务插件提交响应: 渲染钩子执行失败: %s",
|
||||
"task plugin route %s %s declares retainResult: false but returned an asynchronous result; retaining task %s": "任务插件路由 %s %s 声明了 retainResult: false,但返回的是异步结果,任务 %s 仍会保存",
|
||||
"task poll auth failure channel_id=%d http=%d": "任务轮询被上游拒绝(鉴权失败) channel_id=%d http=%d",
|
||||
"task poll auth failure channel_id=%d task=%s http=%d": "任务轮询被上游拒绝(鉴权失败) channel_id=%d task=%s http=%d",
|
||||
"task progress polling finished": "任务进度轮询完成",
|
||||
"task progress polling started": "任务进度轮询开始",
|
||||
"task protocol database observation failed": "任务协议: 读取任务状态失败",
|
||||
"task protocol database observation overloaded; plugin=%s task=%s": "任务协议: 读取任务状态超时,数据库繁忙 plugin=%s task=%s",
|
||||
"task protocol failure event failed: %s": "任务协议: 生成失败事件出错: %s",
|
||||
"task protocol failure response failed: %s": "任务协议: 生成失败响应出错: %s",
|
||||
"task protocol final hook failed": "任务协议: 插件的最终响应钩子执行失败",
|
||||
"task protocol final hook overloaded; plugin=%s task=%s": "任务协议: 插件运行时繁忙,最终响应钩子等待超时 plugin=%s task=%s",
|
||||
"task protocol image download failed; returning the upstream URL instead of b64_json: %s": "任务协议: 图片下载失败,b64_json 改用上游 URL 返回: %s",
|
||||
"task protocol image poll failed for task %s: %s": "任务协议: 轮询图片任务 %s 失败: %s",
|
||||
"task protocol image render hook failed: %s": "任务协议: 插件的图片渲染钩子执行失败: %s",
|
||||
"task protocol image render hook must return an object with a data array": "任务协议: 插件的图片渲染钩子需要返回带 data 数组的对象",
|
||||
"task protocol render hook failed": "任务协议: 插件的渲染钩子执行失败",
|
||||
"task protocol render hook overloaded; plugin=%s task=%s": "任务协议: 插件运行时繁忙,渲染钩子等待超时 plugin=%s task=%s",
|
||||
"task protocol render hook slow; plugin=%s task=%s elapsed_ms=%d": "任务协议: 插件的渲染钩子执行过慢 plugin=%s task=%s elapsed_ms=%d",
|
||||
"task protocol render result invalid: %s": "任务协议: 插件渲染钩子返回的结果无效: %s",
|
||||
"task protocol retrieve lookup failed": "任务协议: 查询任务失败",
|
||||
"task protocol retrieve render failed": "任务协议: 生成查询响应失败",
|
||||
"task protocol state transition failed: %s": "任务协议: 状态切换失败: %s",
|
||||
"task protocol submission returned an invalid durable outcome": "任务协议: 提交后没有得到有效的任务记录",
|
||||
"task protocol terminal failure response failed: %s": "任务协议: 生成任务失败的最终响应出错: %s",
|
||||
"task protocol timeout event failed: %s": "任务协议: 生成超时事件出错: %s",
|
||||
"task protocol timeout response failed: %s": "任务协议: 生成超时响应出错: %s",
|
||||
"testing channel #%d, response: \n%s": "测试渠道 #%d,响应: \n%s",
|
||||
"testing channel %d with model %s , info %+v ": "测试渠道 %d,模型 %s,信息 %+v ",
|
||||
"text request body: %s": "文本请求体: %s",
|
||||
"token quota cache reserve unavailable, falling back to database: %s": "无法在缓存中预扣令牌额度,改为直接扣数据库: %s",
|
||||
"tool surcharge call count clamped: tool=%s count=%d": "工具调用次数超出计费上限,按上限计费: tool=%s count=%d",
|
||||
"tool surcharge call count overflow for %s": "工具 %s 的计费调用次数溢出",
|
||||
"topup failed: %s": "充值失败: %s",
|
||||
"topup group ratio not found: %s": "未找到充值分组倍率: %s",
|
||||
"total tokens is 0, cannot consume quota, userId %d, channelId %d, tokenId %d, model %s, pre-consumed quota %d": "token 总数为 0,无法扣费,userId %d, channelId %d, tokenId %d, model %s,预扣额度 %d",
|
||||
"try to decode image config with limit: %d": "读取前 %d 字节来解析图片头信息",
|
||||
"unexpected content-type from %s: %s": "%s 返回的 content-type 不符合预期: %s",
|
||||
"unfinished tasks found: %v": "检测到未完成的任务数有: %v",
|
||||
"union is nil or unknown type": "union 为 nil 或类型未知",
|
||||
"unknown tag: %v": "未知 tag: %v",
|
||||
"unrecognized data format from %s: %s": "无法识别 %s 返回的数据格式: %s",
|
||||
"updateVideoSingleTask parsed as new api response format: %+v": "updateVideoSingleTask 按 New API 响应格式解析: %+v",
|
||||
"updateVideoSingleTask response: %s": "updateVideoSingleTask 响应: %s",
|
||||
"updating all channels": "开始更新所有渠道的余额",
|
||||
"updating dashboard data...": "正在更新数据看板数据...",
|
||||
"upstream model update check failed: channel_id=%d channel_name=%s err=%v": "检查渠道的上游模型变化失败: channel_id=%d channel_name=%s err=%v",
|
||||
"upstream model update notification skipped in 24h window: changed_channels=%d failed_channels=%d": "24 小时内已发过相同内容的上游模型更新通知,本次不发送: changed_channels=%d failed_channels=%d",
|
||||
"upstream model update notifications sent: %d": "已发送 %d 条上游模型更新通知",
|
||||
"upstream model update task done: checked_channels=%d changed_channels=%d detected_add_models=%d detected_remove_models=%d failed_channels=%d auto_added_models=%d": "上游模型更新检查完成: checked_channels=%d changed_channels=%d detected_add_models=%d detected_remove_models=%d failed_channels=%d auto_added_models=%d",
|
||||
"upstream model update task query failed: %v": "上游模型更新检查: 查询渠道失败: %v",
|
||||
"upstream response body: %s": "上游响应体: %s",
|
||||
"usage extracted from second last SSE: PromptTokens=%d, CompletionTokens=%d, TotalTokens=%d, InputTokens=%d, OutputTokens=%d": "从倒数第二条 SSE 数据中取得用量: PromptTokens=%d, CompletionTokens=%d, TotalTokens=%d, InputTokens=%d, OutputTokens=%d",
|
||||
"user %d has enough quota, trusted without pre-consume (funding=%s)": "用户 %d 额度充足, 信任且不需要预扣费 (funding=%s)",
|
||||
"user %d has no bark url, skip sending bark": "用户 %d 没有填写 Bark 推送地址,未发送 Bark 通知",
|
||||
"user %d has no email, skip sending email": "用户 %d 没有绑定邮箱,未发送邮件通知",
|
||||
"user %d has no gotify url or token, skip sending gotify": "用户 %d 没有填写 Gotify 地址或令牌,未发送 Gotify 通知",
|
||||
"user %d has no webhook url, skip sending webhook": "用户 %d 没有填写 Webhook 地址,未发送 Webhook 通知",
|
||||
"user %d needs pre-consume %s (funding=%s)": "用户 %d 需要预扣费 %s (funding=%s)",
|
||||
"user %d request failed, refunding the pre-consumed quota (token_quota=%s, funding=%s)": "用户 %d 请求失败, 返还预扣费(token_quota=%s, funding=%s)",
|
||||
"user %d | %s": "用户 %d | %s",
|
||||
"user quota cache reserve unavailable, falling back to database: %s": "无法在缓存中预扣用户额度,改为直接扣数据库: %s",
|
||||
"using ClickHouse as log database": "日志数据库使用 ClickHouse",
|
||||
"using MySQL as database": "数据库使用 MySQL",
|
||||
"using PostgreSQL as database": "数据库使用 PostgreSQL",
|
||||
"waffo pancake topup": "Waffo Pancake 充值",
|
||||
"waffo pancake topup failed: %s": "Waffo Pancake 充值失败: %s",
|
||||
"waffo topup": "Waffo 充值",
|
||||
"waffo topup failed: %s": "Waffo 充值失败: %s",
|
||||
"websocket connection is nil": "WebSocket 连接为空",
|
||||
"zhipu_image_empty_b64": "智谱图片接口返回的 b64 为空",
|
||||
"zhipu_image_get_b64_failed: %s": "智谱图片转 b64 失败: %s",
|
||||
"zhipu_image_missing_url": "智谱图片接口没有返回 URL",
|
||||
"¥%.6f quota": "¥%.6f 额度",
|
||||
"$%.6f quota": "$%.6f 额度"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Message is text shown in the web console. Key is the English source text and
|
||||
// also its key in web/src/i18n/locales; Params fill its {{name}} placeholders.
|
||||
// The web console translates Key with Params, so the backend only renders the
|
||||
// English text, for API callers and log fallbacks. Params carry display-ready
|
||||
// values: format quota with logger.FormatQuota before passing it.
|
||||
type Message struct {
|
||||
Key string `json:"key"`
|
||||
Params map[string]any `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func NewMessage(key string, params ...map[string]any) *Message {
|
||||
message := &Message{Key: key}
|
||||
if len(params) > 0 {
|
||||
message.Params = params[0]
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
// Error renders the English text, so a Message can be returned as an error and
|
||||
// reach the web console through ApiError.
|
||||
func (m *Message) Error() string {
|
||||
if len(m.Params) == 0 {
|
||||
return m.Key
|
||||
}
|
||||
// One pass over the key, so a value that contains "{{name}}" stays as is.
|
||||
pairs := make([]string, 0, len(m.Params)*2)
|
||||
for name, value := range m.Params {
|
||||
pairs = append(pairs, "{{"+name+"}}", fmt.Sprint(value))
|
||||
}
|
||||
return strings.NewReplacer(pairs...).Replace(m.Key)
|
||||
}
|
||||
|
||||
// Fields returns the response fields the web console translates. Callers put
|
||||
// the English text in the field their response shape already uses.
|
||||
func (m *Message) Fields() gin.H {
|
||||
fields := gin.H{"message_key": m.Key}
|
||||
if len(m.Params) > 0 {
|
||||
fields["message_params"] = m.Params
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -2,9 +2,6 @@ package common
|
||||
|
||||
import "strings"
|
||||
|
||||
// ZImagePromptExtendMultiplier is the legacy Ali image request surcharge.
|
||||
const ZImagePromptExtendMultiplier = 2
|
||||
|
||||
var (
|
||||
// OpenAIResponseOnlyModels is a list of models that are only available for OpenAI responses.
|
||||
OpenAIResponseOnlyModels = []string{
|
||||
@@ -33,13 +30,6 @@ var (
|
||||
"flux-",
|
||||
"flux.1-",
|
||||
}
|
||||
OpenAITextModels = []string{
|
||||
"gpt-",
|
||||
"o1",
|
||||
"o3",
|
||||
"o4",
|
||||
"chatgpt",
|
||||
}
|
||||
)
|
||||
|
||||
func IsOpenAIResponseOnlyModel(modelName string) bool {
|
||||
@@ -66,13 +56,3 @@ func IsImageGenerationModel(modelName string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsOpenAITextModel(modelName string) bool {
|
||||
modelName = strings.ToLower(modelName)
|
||||
for _, m := range OpenAITextModels {
|
||||
if strings.Contains(modelName, m) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -26,3 +26,13 @@ func TestWanEndpointsDistinguishImagesFromVideos(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// /v1/models reports the DeepSeek Search and Fetch routing names with their
|
||||
// own endpoint on the channels that relay them; chat models keep their list.
|
||||
func TestDeepSeekToolRoutingNamesReportTheirEndpoint(t *testing.T) {
|
||||
for _, channelType := range []int{constant.ChannelTypeDeepSeek, constant.ChannelTypeNewAPI} {
|
||||
assert.Equal(t, []constant.EndpointType{constant.EndpointTypeDeepSeekToolsSearch}, common.GetEndpointTypesByChannelType(channelType, constant.DeepSeekSearchModel))
|
||||
assert.Equal(t, []constant.EndpointType{constant.EndpointTypeDeepSeekToolsFetch}, common.GetEndpointTypesByChannelType(channelType, constant.DeepSeekFetchModel))
|
||||
}
|
||||
assert.Equal(t, []constant.EndpointType{constant.EndpointTypeOpenAI}, common.GetEndpointTypesByChannelType(constant.ChannelTypeDeepSeek, "deepseek-flash"))
|
||||
}
|
||||
+4
-4
@@ -17,23 +17,23 @@ func Monitor() {
|
||||
panic(err)
|
||||
}
|
||||
if percent[0] > 80 {
|
||||
fmt.Println("cpu usage too high")
|
||||
fmt.Println(LogText("cpu usage too high"))
|
||||
// write pprof file
|
||||
if _, err := os.Stat("./pprof"); os.IsNotExist(err) {
|
||||
err := os.Mkdir("./pprof", os.ModePerm)
|
||||
if err != nil {
|
||||
SysLog("创建pprof文件夹失败 " + err.Error())
|
||||
SysLog(LogText("failed to create the pprof directory: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := os.Create("./pprof/" + fmt.Sprintf("cpu-%s.pprof", time.Now().Format("20060102150405")))
|
||||
if err != nil {
|
||||
SysLog("创建pprof文件失败 " + err.Error())
|
||||
SysLog(LogText("failed to create the pprof file: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
SysLog("启动pprof失败 " + err.Error())
|
||||
SysLog(LogText("failed to start pprof: %s", err.Error()))
|
||||
continue
|
||||
}
|
||||
time.Sleep(10 * time.Second) // profile for 30 seconds
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package common
|
||||
|
||||
func GetTrustQuota() int {
|
||||
return int(10 * QuotaPerUnit)
|
||||
}
|
||||
+17
-17
@@ -24,17 +24,17 @@ func RedisKeyCacheSeconds() int {
|
||||
func InitRedisClient() (err error) {
|
||||
if os.Getenv("REDIS_CONN_STRING") == "" {
|
||||
RedisEnabled = false
|
||||
SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
|
||||
SysLog(LogText("REDIS_CONN_STRING not set, Redis is not enabled"))
|
||||
return nil
|
||||
}
|
||||
if os.Getenv("SYNC_FREQUENCY") == "" {
|
||||
SysLog("SYNC_FREQUENCY not set, use default value 60")
|
||||
SysLog(LogText("SYNC_FREQUENCY not set, use default value 60"))
|
||||
SyncFrequency = 60
|
||||
}
|
||||
SysLog("Redis is enabled")
|
||||
SysLog(LogText("Redis is enabled"))
|
||||
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||
if err != nil {
|
||||
FatalLog("failed to parse Redis connection string: " + err.Error())
|
||||
FatalLog(LogText("failed to parse Redis connection string: %s", err.Error()))
|
||||
}
|
||||
opt.PoolSize = GetEnvOrDefault("REDIS_POOL_SIZE", 10)
|
||||
RDB = redis.NewClient(opt)
|
||||
@@ -44,11 +44,11 @@ func InitRedisClient() (err error) {
|
||||
|
||||
_, err = RDB.Ping(ctx).Result()
|
||||
if err != nil {
|
||||
FatalLog("Redis ping test failed: " + err.Error())
|
||||
FatalLog(LogText("Redis ping test failed: %s", err.Error()))
|
||||
}
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis connected to %s", opt.Addr))
|
||||
SysLog(fmt.Sprintf("Redis database: %d", opt.DB))
|
||||
SysLog(LogText("Redis connected to %s", opt.Addr))
|
||||
SysLog(LogText("Redis database: %d", opt.DB))
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -56,14 +56,14 @@ func InitRedisClient() (err error) {
|
||||
func ParseRedisOption() *redis.Options {
|
||||
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
||||
if err != nil {
|
||||
FatalLog("failed to parse Redis connection string: " + err.Error())
|
||||
FatalLog(LogText("failed to parse Redis connection string: %s", err.Error()))
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
func RedisSet(key string, value string, expiration time.Duration) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration))
|
||||
SysLog(LogText("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration))
|
||||
}
|
||||
ctx := context.Background()
|
||||
return RDB.Set(ctx, key, value, expiration).Err()
|
||||
@@ -71,7 +71,7 @@ func RedisSet(key string, value string, expiration time.Duration) error {
|
||||
|
||||
func RedisGet(key string) (string, error) {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis GET: key=%s", key))
|
||||
SysLog(LogText("Redis GET: key=%s", key))
|
||||
}
|
||||
ctx := context.Background()
|
||||
val, err := RDB.Get(ctx, key).Result()
|
||||
@@ -90,7 +90,7 @@ func RedisGet(key string) (string, error) {
|
||||
|
||||
func RedisDel(key string) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis DEL: key=%s", key))
|
||||
SysLog(LogText("Redis DEL: key=%s", key))
|
||||
}
|
||||
ctx := context.Background()
|
||||
return RDB.Del(ctx, key).Err()
|
||||
@@ -98,7 +98,7 @@ func RedisDel(key string) error {
|
||||
|
||||
func RedisDelKey(key string) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis DEL Key: key=%s", key))
|
||||
SysLog(LogText("Redis DEL Key: key=%s", key))
|
||||
}
|
||||
ctx := context.Background()
|
||||
return RDB.Del(ctx, key).Err()
|
||||
@@ -106,7 +106,7 @@ func RedisDelKey(key string) error {
|
||||
|
||||
func RedisHSetObj(key string, obj any, expiration time.Duration) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration))
|
||||
SysLog(LogText("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration))
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -160,7 +160,7 @@ func RedisHSetObj(key string, obj any, expiration time.Duration) error {
|
||||
|
||||
func RedisHGetObj(key string, obj any) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis HGETALL: key=%s", key))
|
||||
SysLog(LogText("Redis HGETALL: key=%s", key))
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -241,7 +241,7 @@ func RedisHGetObj(key string, obj any) error {
|
||||
// RedisIncr Add this function to handle atomic increments
|
||||
func RedisIncr(key string, delta int64) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis INCR: key=%s, delta=%d", key, delta))
|
||||
SysLog(LogText("Redis INCR: key=%s, delta=%d", key, delta))
|
||||
}
|
||||
// 检查键的剩余生存时间
|
||||
ttlCmd := RDB.TTL(context.Background(), key)
|
||||
@@ -274,7 +274,7 @@ func RedisIncr(key string, delta int64) error {
|
||||
|
||||
func RedisHIncrBy(key, field string, delta int64) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta))
|
||||
SysLog(LogText("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta))
|
||||
}
|
||||
ttlCmd := RDB.TTL(context.Background(), key)
|
||||
ttl, err := ttlCmd.Result()
|
||||
@@ -301,7 +301,7 @@ func RedisHIncrBy(key, field string, delta int64) error {
|
||||
|
||||
func RedisHSetField(key, field string, value any) error {
|
||||
if DebugEnabled {
|
||||
SysLog(fmt.Sprintf("Redis HSET field: key=%s, field=%s, value=%v", key, field, value))
|
||||
SysLog(LogText("Redis HSET field: key=%s, field=%s, value=%v", key, field, value))
|
||||
}
|
||||
ttlCmd := RDB.TTL(context.Background(), key)
|
||||
ttl, err := ttlCmd.Result()
|
||||
|
||||
+2
-2
@@ -135,6 +135,6 @@ func MaskEmail(email string) string {
|
||||
|
||||
// MaskSensitiveInfo moved to the conversion kit (kitutil) because the types
|
||||
// package error formatting depends on it; host callers keep this name.
|
||||
func MaskSensitiveInfo(str string) string {
|
||||
return kitutil.MaskSensitiveInfo(str)
|
||||
func MaskSensitiveInfo(str string, upstreamURLs ...string) string {
|
||||
return kitutil.MaskSensitiveInfo(str, upstreamURLs...)
|
||||
}
|
||||
+4
-4
@@ -50,20 +50,20 @@ func LogStartupSuccess(startTime time.Time, port string) {
|
||||
// Warn when the local HTTP compatibility mode disables cookie transport
|
||||
// security and refresh/logout Origin validation.
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
fmt.Fprintf(gin.DefaultWriter, " \033[33mWarning: Refresh cookie is not secure and refresh/logout Origin validation is disabled. Please set SESSION_COOKIE_SECURE=true in production.\033[0m\n")
|
||||
fmt.Fprintf(gin.DefaultWriter, " \033[33m%s\033[0m\n", LogText("Warning: Refresh cookie is not secure and refresh/logout Origin validation is disabled. Please set SESSION_COOKIE_SECURE=true in production."))
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m ready in %d ms\n", SystemName, Version, durationMs)
|
||||
fmt.Fprintf(gin.DefaultWriter, " \033[32m%s %s\033[0m %s\n", SystemName, Version, LogText("ready in %d ms", durationMs))
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
|
||||
if !IsRunningInContainer() {
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mLocal:\033[0m http://localhost:%s/\n", port)
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1m%-8s\033[0m http://localhost:%s/\n", LogText("Local:"), port)
|
||||
}
|
||||
|
||||
for _, ip := range networkIps {
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1mNetwork:\033[0m http://%s:%s/\n", ip, port)
|
||||
fmt.Fprintf(gin.DefaultWriter, " ➜ \033[1m%-8s\033[0m http://%s:%s/\n", LogText("Network:"), ip, port)
|
||||
}
|
||||
|
||||
fmt.Fprintf(gin.DefaultWriter, "\n")
|
||||
|
||||
+96
-17
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -25,10 +26,36 @@ type SystemStatus struct {
|
||||
CPUUsage float64
|
||||
MemoryUsage float64
|
||||
DiskUsage float64
|
||||
SampledAt time.Time
|
||||
// 过载判定带连续性要求:单一采样尖峰不再直接熔断,需连续多次超阈值。
|
||||
CPUOverloaded bool
|
||||
MemoryOverloaded bool
|
||||
DiskOverloaded bool
|
||||
}
|
||||
|
||||
// SystemOverload 是哨兵据以拒绝请求的过载项,Error 文本带当前值与生效阈值。
|
||||
type SystemOverload struct {
|
||||
Code string
|
||||
Resource string
|
||||
Usage float64
|
||||
Threshold int
|
||||
}
|
||||
|
||||
func (o *SystemOverload) Error() string {
|
||||
return fmt.Sprintf("system %s overloaded (current: %.1f%%, threshold: %d%%)", o.Resource, o.Usage, o.Threshold)
|
||||
}
|
||||
|
||||
var latestSystemStatus atomic.Value
|
||||
|
||||
const (
|
||||
systemMonitorInterval = 5 * time.Second
|
||||
// 连续 3 次采样(约 15 秒)仍高于阈值才判定过载,
|
||||
// 任一采样回落到阈值以下即恢复,避免瞬时尖峰打满重试。
|
||||
overloadConsecutiveSamples = 3
|
||||
// 采样超过该时长未刷新(监控未运行或被阻塞)时,哨兵不据此熔断。
|
||||
systemStatusMaxAge = 30 * time.Second
|
||||
)
|
||||
|
||||
func init() {
|
||||
latestSystemStatus.Store(SystemStatus{})
|
||||
}
|
||||
@@ -36,46 +63,98 @@ func init() {
|
||||
// StartSystemMonitor 启动系统监控
|
||||
func StartSystemMonitor() {
|
||||
go func() {
|
||||
usage := newContainerUsage()
|
||||
var detector overloadDetector
|
||||
for {
|
||||
config := GetPerformanceMonitorConfig()
|
||||
if !config.Enabled {
|
||||
time.Sleep(30 * time.Second)
|
||||
detector = overloadDetector{}
|
||||
latestSystemStatus.Store(SystemStatus{SampledAt: time.Now()})
|
||||
time.Sleep(systemMonitorInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
updateSystemStatus()
|
||||
time.Sleep(5 * time.Second)
|
||||
status := collectSystemStatus(usage)
|
||||
detector.mark(&status, config)
|
||||
latestSystemStatus.Store(status)
|
||||
|
||||
time.Sleep(systemMonitorInterval)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func updateSystemStatus() {
|
||||
var status SystemStatus
|
||||
// overloadDetector 为每项资源累计连续超阈值的采样次数:连续 overloadConsecutiveSamples 次
|
||||
// 才标记过载,任一次回落立即清零并重新累计。
|
||||
type overloadDetector struct {
|
||||
cpuSamples int
|
||||
memorySamples int
|
||||
diskSamples int
|
||||
}
|
||||
|
||||
// CPU
|
||||
// 注意:cpu.Percent(0, false) 返回自上次调用以来的 CPU 使用率
|
||||
// 如果是第一次调用,可能会返回错误或不准确的值,但在循环中会逐渐正常
|
||||
percents, err := cpu.Percent(0, false)
|
||||
if err == nil && len(percents) > 0 {
|
||||
func (d *overloadDetector) mark(status *SystemStatus, config PerformanceMonitorConfig) {
|
||||
status.CPUOverloaded = countOverloadSample(&d.cpuSamples, exceedsThreshold(status.CPUUsage, config.CPUThreshold))
|
||||
status.MemoryOverloaded = countOverloadSample(&d.memorySamples, exceedsThreshold(status.MemoryUsage, config.MemoryThreshold))
|
||||
status.DiskOverloaded = countOverloadSample(&d.diskSamples, exceedsThreshold(status.DiskUsage, config.DiskThreshold))
|
||||
}
|
||||
|
||||
func countOverloadSample(samples *int, over bool) bool {
|
||||
if !over {
|
||||
*samples = 0
|
||||
return false
|
||||
}
|
||||
*samples = min(*samples+1, overloadConsecutiveSamples)
|
||||
return *samples == overloadConsecutiveSamples
|
||||
}
|
||||
|
||||
// exceedsThreshold 判断采样值是否超过阈值;阈值 <= 0 表示不检查该项。
|
||||
func exceedsThreshold(usage float64, threshold int) bool {
|
||||
return threshold > 0 && int(usage) > threshold
|
||||
}
|
||||
|
||||
func collectSystemStatus(usage *containerUsage) SystemStatus {
|
||||
now := time.Now()
|
||||
status := SystemStatus{SampledAt: now}
|
||||
|
||||
// CPU / 内存优先按容器 cgroup 配额计算;容器口径不可用时回退宿主机口径,
|
||||
// 保持对未限制资源或被挂载为宿主 cgroup 的环境的兼容。
|
||||
if saturation, ok := usage.cpuSaturation(now); ok {
|
||||
status.CPUUsage = saturation
|
||||
} else if percents, err := cpu.Percent(0, false); err == nil && len(percents) > 0 {
|
||||
status.CPUUsage = percents[0]
|
||||
}
|
||||
|
||||
// Memory
|
||||
memInfo, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
if containerMemory, ok := usage.memoryUsage(); ok {
|
||||
status.MemoryUsage = containerMemory
|
||||
} else if memInfo, err := mem.VirtualMemory(); err == nil {
|
||||
status.MemoryUsage = memInfo.UsedPercent
|
||||
}
|
||||
|
||||
// Disk
|
||||
diskInfo := GetDiskSpaceInfo()
|
||||
if diskInfo.Total > 0 {
|
||||
status.DiskUsage = diskInfo.UsedPercent
|
||||
}
|
||||
|
||||
latestSystemStatus.Store(status)
|
||||
return status
|
||||
}
|
||||
|
||||
// GetSystemStatus 获取当前系统状态
|
||||
func GetSystemStatus() SystemStatus {
|
||||
return latestSystemStatus.Load().(SystemStatus)
|
||||
}
|
||||
|
||||
// CheckSystemOverload 返回哨兵应据以熔断的过载项。监控关闭、采样缺失或已超过
|
||||
// systemStatusMaxAge 时返回 nil。过载标记来自连续采样,阈值按当前配置复核,
|
||||
// 运行时关闭或调高阈值后立即放行,不必等待下一次采样。
|
||||
func CheckSystemOverload(status SystemStatus, config PerformanceMonitorConfig, now time.Time) *SystemOverload {
|
||||
if !config.Enabled || status.SampledAt.IsZero() || now.Sub(status.SampledAt) > systemStatusMaxAge {
|
||||
return nil
|
||||
}
|
||||
if status.CPUOverloaded && exceedsThreshold(status.CPUUsage, config.CPUThreshold) {
|
||||
return &SystemOverload{Code: "system_cpu_overloaded", Resource: "cpu", Usage: status.CPUUsage, Threshold: config.CPUThreshold}
|
||||
}
|
||||
if status.MemoryOverloaded && exceedsThreshold(status.MemoryUsage, config.MemoryThreshold) {
|
||||
return &SystemOverload{Code: "system_memory_overloaded", Resource: "memory", Usage: status.MemoryUsage, Threshold: config.MemoryThreshold}
|
||||
}
|
||||
if status.DiskOverloaded && exceedsThreshold(status.DiskUsage, config.DiskThreshold) {
|
||||
return &SystemOverload{Code: "system_disk_overloaded", Resource: "disk", Usage: status.DiskUsage, Threshold: config.DiskThreshold}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestContainerUsage 在临时目录中构造 cgroup 文件,避免依赖运行环境的真实 cgroup。
|
||||
func newTestContainerUsage(t *testing.T, schedulableCPUs float64, files map[string]string) *containerUsage {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
for name, content := range files {
|
||||
writeTestCgroupFile(t, filepath.Join(root, name), content)
|
||||
}
|
||||
return &containerUsage{cgroupRoot: root, schedulableCPUs: schedulableCPUs}
|
||||
}
|
||||
|
||||
func writeTestCgroupFile(t *testing.T, path string, content string) {
|
||||
t.Helper()
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
|
||||
}
|
||||
|
||||
func TestContainerCPUSaturation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schedulableCPUs float64
|
||||
files map[string]string
|
||||
usageFile string
|
||||
usageAfter string
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "cgroup v2 quota",
|
||||
schedulableCPUs: 4,
|
||||
files: map[string]string{"cpu.max": "200000 100000\n", "cpu.stat": "usage_usec 1000000\nuser_usec 900000\n"},
|
||||
usageFile: "cpu.stat",
|
||||
usageAfter: "usage_usec 6000000\n", // 5 秒用 1 核,配额 2 核
|
||||
want: 50,
|
||||
},
|
||||
{
|
||||
name: "fractional quota",
|
||||
schedulableCPUs: 4,
|
||||
files: map[string]string{"cpu.max": "50000 100000\n", "cpu.stat": "usage_usec 1000000\n"},
|
||||
usageFile: "cpu.stat",
|
||||
usageAfter: "usage_usec 3500000\n", // 5 秒用 0.5 核,配额 0.5 核
|
||||
want: 100,
|
||||
},
|
||||
{
|
||||
name: "quota above schedulable CPUs",
|
||||
schedulableCPUs: 1,
|
||||
files: map[string]string{"cpu.max": "200000 100000\n", "cpu.stat": "usage_usec 1000000\n"},
|
||||
usageFile: "cpu.stat",
|
||||
usageAfter: "usage_usec 6000000\n", // cpuset 只有 1 核,已打满
|
||||
want: 100,
|
||||
},
|
||||
{
|
||||
name: "cgroup v1 quota",
|
||||
schedulableCPUs: 4,
|
||||
files: map[string]string{
|
||||
"cpu/cpu.cfs_quota_us": "200000\n",
|
||||
"cpu/cpu.cfs_period_us": "100000\n",
|
||||
"cpuacct/cpuacct.usage": "1000000000\n",
|
||||
},
|
||||
usageFile: "cpuacct/cpuacct.usage",
|
||||
usageAfter: "11000000000\n", // 纳秒:5 秒用 2 核
|
||||
want: 100,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
usage := newTestContainerUsage(t, tt.schedulableCPUs, tt.files)
|
||||
base := time.Date(2026, 9, 11, 3, 0, 0, 0, time.UTC)
|
||||
_, ok := usage.cpuSaturation(base)
|
||||
assert.False(t, ok, "首次采样只建立基线")
|
||||
|
||||
writeTestCgroupFile(t, filepath.Join(usage.cgroupRoot, tt.usageFile), tt.usageAfter)
|
||||
got, ok := usage.cpuSaturation(base.Add(5 * time.Second))
|
||||
require.True(t, ok)
|
||||
assert.InDelta(t, tt.want, got, 0.01)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerCPUSaturationFallsBackWithoutQuota(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
}{
|
||||
{name: "cgroup v2 unlimited", files: map[string]string{"cpu.max": "max 100000\n", "cpu.stat": "usage_usec 1000000\n"}},
|
||||
{name: "cgroup v1 unlimited", files: map[string]string{"cpu/cpu.cfs_quota_us": "-1\n", "cpu/cpu.cfs_period_us": "100000\n", "cpuacct/cpuacct.usage": "1000\n"}},
|
||||
{name: "no cgroup files"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
usage := newTestContainerUsage(t, 4, tt.files)
|
||||
base := time.Date(2026, 9, 11, 3, 0, 0, 0, time.UTC)
|
||||
usage.cpuSaturation(base)
|
||||
_, ok := usage.cpuSaturation(base.Add(5 * time.Second))
|
||||
assert.False(t, ok, "没有配额时交由调用方回退宿主机口径")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerMemoryUsageUsesWorkingSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
name: "cgroup v2 excludes inactive file cache",
|
||||
files: map[string]string{
|
||||
"memory.current": "266338304\n",
|
||||
"memory.max": "268435456\n",
|
||||
"memory.stat": "anon 4435968\nfile 253362176\ninactive_file 251662336\n",
|
||||
},
|
||||
want: 5.4672,
|
||||
},
|
||||
{
|
||||
name: "cgroup v2 high working set keeps protection",
|
||||
files: map[string]string{
|
||||
"memory.current": "255852544\n",
|
||||
"memory.max": "268435456\n",
|
||||
"memory.stat": "anon 254803968\ninactive_file 1048576\n",
|
||||
},
|
||||
want: 94.9219,
|
||||
},
|
||||
{
|
||||
name: "cgroup v1 uses hierarchical inactive file cache",
|
||||
files: map[string]string{
|
||||
"memory/memory.usage_in_bytes": "805306368\n",
|
||||
"memory/memory.limit_in_bytes": "1610612736\n",
|
||||
"memory/memory.stat": "inactive_file 999\ntotal_inactive_file 268435456\n",
|
||||
},
|
||||
want: 33.3333,
|
||||
},
|
||||
{
|
||||
name: "cache larger than usage does not underflow",
|
||||
files: map[string]string{
|
||||
"memory.current": "1000\n",
|
||||
"memory.max": "268435456\n",
|
||||
"memory.stat": "inactive_file 2000\n",
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := newTestContainerUsage(t, 4, tt.files).memoryUsage()
|
||||
require.True(t, ok)
|
||||
assert.InDelta(t, tt.want, got, 0.001)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerMemoryUsageFallsBackWhenUnavailable(t *testing.T) {
|
||||
limited := map[string]string{"memory.current": "266338304\n", "memory.max": "268435456\n"}
|
||||
tests := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
stat string
|
||||
}{
|
||||
{name: "cgroup v2 unlimited", files: map[string]string{"memory.current": "1000\n", "memory.max": "max\n"}, stat: "inactive_file 0\n"},
|
||||
{name: "cgroup v1 unlimited", files: map[string]string{
|
||||
"memory/memory.usage_in_bytes": "1000\n",
|
||||
"memory/memory.limit_in_bytes": "9223372036854771712\n",
|
||||
"memory/memory.stat": "total_inactive_file 0\n",
|
||||
}},
|
||||
{name: "memory.stat missing", files: limited},
|
||||
{name: "inactive file field missing", files: limited, stat: "anon 4435968\n"},
|
||||
{name: "inactive file field corrupt", files: limited, stat: "inactive_file abc\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
usage := newTestContainerUsage(t, 4, tt.files)
|
||||
if tt.stat != "" {
|
||||
writeTestCgroupFile(t, filepath.Join(usage.cgroupRoot, "memory.stat"), tt.stat)
|
||||
}
|
||||
_, ok := usage.memoryUsage()
|
||||
assert.False(t, ok, "拿不到可信工作集时交由调用方回退宿主机口径")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemOverloadRequiresConsecutiveSamples(t *testing.T) {
|
||||
config := PerformanceMonitorConfig{Enabled: true, CPUThreshold: 90, MemoryThreshold: 90, DiskThreshold: 95}
|
||||
now := time.Date(2026, 9, 11, 3, 0, 0, 0, time.UTC)
|
||||
cpuSamples := []struct {
|
||||
usage float64
|
||||
wantOverload bool
|
||||
}{
|
||||
{usage: 95, wantOverload: false},
|
||||
{usage: 95, wantOverload: false},
|
||||
{usage: 95, wantOverload: true},
|
||||
{usage: 95, wantOverload: true},
|
||||
{usage: 50, wantOverload: false}, // 回落即恢复
|
||||
{usage: 95, wantOverload: false}, // 重新累计
|
||||
{usage: 95, wantOverload: false},
|
||||
{usage: 95, wantOverload: true},
|
||||
}
|
||||
|
||||
var detector overloadDetector
|
||||
for i, sample := range cpuSamples {
|
||||
status := SystemStatus{CPUUsage: sample.usage, SampledAt: now}
|
||||
detector.mark(&status, config)
|
||||
overload := CheckSystemOverload(status, config, now)
|
||||
if !sample.wantOverload {
|
||||
assert.Nil(t, overload, "sample %d", i+1)
|
||||
continue
|
||||
}
|
||||
require.NotNil(t, overload, "sample %d", i+1)
|
||||
assert.Equal(t, "system_cpu_overloaded", overload.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSystemOverload(t *testing.T) {
|
||||
now := time.Date(2026, 9, 11, 3, 0, 0, 0, time.UTC)
|
||||
enabled := PerformanceMonitorConfig{Enabled: true, CPUThreshold: 90, MemoryThreshold: 90, DiskThreshold: 95}
|
||||
cpuOverloaded := SystemStatus{CPUUsage: 99.5, CPUOverloaded: true, SampledAt: now}
|
||||
tests := []struct {
|
||||
name string
|
||||
status SystemStatus
|
||||
config PerformanceMonitorConfig
|
||||
wantCode string
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
name: "fresh cpu overload",
|
||||
status: cpuOverloaded,
|
||||
config: enabled,
|
||||
wantCode: "system_cpu_overloaded",
|
||||
wantMessage: "system cpu overloaded (current: 99.5%, threshold: 90%)",
|
||||
},
|
||||
{
|
||||
name: "fresh memory overload",
|
||||
status: SystemStatus{MemoryUsage: 96.2, MemoryOverloaded: true, SampledAt: now},
|
||||
config: enabled,
|
||||
wantCode: "system_memory_overloaded",
|
||||
wantMessage: "system memory overloaded (current: 96.2%, threshold: 90%)",
|
||||
},
|
||||
{
|
||||
name: "fresh disk overload",
|
||||
status: SystemStatus{DiskUsage: 97, DiskOverloaded: true, SampledAt: now},
|
||||
config: enabled,
|
||||
wantCode: "system_disk_overloaded",
|
||||
wantMessage: "system disk overloaded (current: 97.0%, threshold: 95%)",
|
||||
},
|
||||
{
|
||||
name: "lowered threshold reports the active threshold",
|
||||
status: cpuOverloaded,
|
||||
config: PerformanceMonitorConfig{Enabled: true, CPUThreshold: 80},
|
||||
wantCode: "system_cpu_overloaded",
|
||||
wantMessage: "system cpu overloaded (current: 99.5%, threshold: 80%)",
|
||||
},
|
||||
{name: "monitor disabled", status: cpuOverloaded, config: PerformanceMonitorConfig{Enabled: false, CPUThreshold: 90}},
|
||||
{name: "cpu threshold disabled at runtime", status: cpuOverloaded, config: PerformanceMonitorConfig{Enabled: true, CPUThreshold: 0}},
|
||||
{name: "cpu threshold raised above usage", status: cpuOverloaded, config: PerformanceMonitorConfig{Enabled: true, CPUThreshold: 100}},
|
||||
{name: "stale sample", status: SystemStatus{CPUUsage: 99.5, CPUOverloaded: true, SampledAt: now.Add(-31 * time.Second)}, config: enabled},
|
||||
{name: "missing sample", status: SystemStatus{CPUUsage: 99.5, CPUOverloaded: true}, config: enabled},
|
||||
{name: "usage above threshold without consecutive samples", status: SystemStatus{CPUUsage: 99.5, SampledAt: now}, config: enabled},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
overload := CheckSystemOverload(tt.status, tt.config, now)
|
||||
if tt.wantCode == "" {
|
||||
assert.Nil(t, overload)
|
||||
return
|
||||
}
|
||||
require.NotNil(t, overload)
|
||||
assert.Equal(t, tt.wantCode, overload.Code)
|
||||
assert.Equal(t, tt.wantMessage, overload.Error())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ func TopupGroupRatio2JSONString() string {
|
||||
defer topupGroupRatioMutex.RUnlock()
|
||||
jsonBytes, err := json.Marshal(topupGroupRatio)
|
||||
if err != nil {
|
||||
SysError("error marshalling topup group ratio: " + err.Error())
|
||||
SysError(LogText("error marshalling topup group ratio: %s", err.Error()))
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func GetTopupGroupRatio(name string) float64 {
|
||||
defer topupGroupRatioMutex.RUnlock()
|
||||
ratio, ok := topupGroupRatio[name]
|
||||
if !ok {
|
||||
SysError("topup group ratio not found: " + name)
|
||||
SysError(LogText("topup group ratio not found: %s", name))
|
||||
return 1
|
||||
}
|
||||
return ratio
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package common
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -130,12 +131,12 @@ func ValidateNumericCode(code string) (string, error) {
|
||||
code = strings.ReplaceAll(code, " ", "")
|
||||
|
||||
if len(code) != 6 {
|
||||
return "", fmt.Errorf("验证码必须是6位数字")
|
||||
return "", errors.New("the verification code must be 6 digits")
|
||||
}
|
||||
|
||||
// 检查是否为纯数字
|
||||
if _, err := strconv.Atoi(code); err != nil {
|
||||
return "", fmt.Errorf("验证码只能包含数字")
|
||||
return "", errors.New("the verification code must contain only digits")
|
||||
}
|
||||
|
||||
return code, nil
|
||||
|
||||
@@ -163,31 +163,6 @@ func Bytes2Size(num int64) string {
|
||||
return numStr + " " + unit
|
||||
}
|
||||
|
||||
func Seconds2Time(num int) (time string) {
|
||||
if num/31104000 > 0 {
|
||||
time += strconv.Itoa(num/31104000) + " 年 "
|
||||
num %= 31104000
|
||||
}
|
||||
if num/2592000 > 0 {
|
||||
time += strconv.Itoa(num/2592000) + " 个月 "
|
||||
num %= 2592000
|
||||
}
|
||||
if num/86400 > 0 {
|
||||
time += strconv.Itoa(num/86400) + " 天 "
|
||||
num %= 86400
|
||||
}
|
||||
if num/3600 > 0 {
|
||||
time += strconv.Itoa(num/3600) + " 小时 "
|
||||
num %= 3600
|
||||
}
|
||||
if num/60 > 0 {
|
||||
time += strconv.Itoa(num/60) + " 分钟 "
|
||||
num %= 60
|
||||
}
|
||||
time += strconv.Itoa(num) + " 秒"
|
||||
return
|
||||
}
|
||||
|
||||
func Interface2String(inter any) string {
|
||||
switch inter.(type) {
|
||||
case string:
|
||||
|
||||
Reference in new issue
Block a user