// Package service provides business logic and domain services for the application. package service import ( "encoding/json" "sort" "strconv" "strings" "time" "github.com/Wei-Shaw/sub2api/internal/domain" ) type Account struct { ID int64 Name string Notes *string Platform string Type string Credentials map[string]any Extra map[string]any ProxyID *int64 Concurrency int Priority int // RateMultiplier 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。 // 使用指针用于兼容旧版本调度缓存(Redis)中缺字段的情况:nil 表示按 1.0 处理。 RateMultiplier *float64 Status string ErrorMessage string LastUsedAt *time.Time ExpiresAt *time.Time AutoPauseOnExpired bool CreatedAt time.Time UpdatedAt time.Time Schedulable bool RateLimitedAt *time.Time RateLimitResetAt *time.Time OverloadUntil *time.Time TempUnschedulableUntil *time.Time TempUnschedulableReason string SessionWindowStart *time.Time SessionWindowEnd *time.Time SessionWindowStatus string Proxy *Proxy AccountGroups []AccountGroup GroupIDs []int64 Groups []*Group } type TempUnschedulableRule struct { ErrorCode int `json:"error_code"` Keywords []string `json:"keywords"` DurationMinutes int `json:"duration_minutes"` Description string `json:"description"` } func (a *Account) IsActive() bool { return a.Status == StatusActive } // BillingRateMultiplier 返回账号计费倍率。 // - nil 表示未配置/旧缓存缺字段,按 1.0 处理 // - 允许 0,表示该账号计费为 0 // - 负数属于非法数据,出于安全考虑按 1.0 处理 func (a *Account) BillingRateMultiplier() float64 { if a == nil || a.RateMultiplier == nil { return 1.0 } if *a.RateMultiplier < 0 { return 1.0 } return *a.RateMultiplier } func (a *Account) IsSchedulable() bool { if !a.IsActive() || !a.Schedulable { return false } now := time.Now() if a.AutoPauseOnExpired && a.ExpiresAt != nil && !now.Before(*a.ExpiresAt) { return false } if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) { return false } if a.RateLimitResetAt != nil && now.Before(*a.RateLimitResetAt) { return false } if a.TempUnschedulableUntil != nil && now.Before(*a.TempUnschedulableUntil) { return false } return true } func (a *Account) IsRateLimited() bool { if a.RateLimitResetAt == nil { return false } return time.Now().Before(*a.RateLimitResetAt) } func (a *Account) IsOverloaded() bool { if a.OverloadUntil == nil { return false } return time.Now().Before(*a.OverloadUntil) } func (a *Account) IsOAuth() bool { return a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken } func (a *Account) IsGemini() bool { return a.Platform == PlatformGemini } func (a *Account) GeminiOAuthType() string { if a.Platform != PlatformGemini || a.Type != AccountTypeOAuth { return "" } oauthType := strings.TrimSpace(a.GetCredential("oauth_type")) if oauthType == "" && strings.TrimSpace(a.GetCredential("project_id")) != "" { return "code_assist" } return oauthType } func (a *Account) GeminiTierID() string { tierID := strings.TrimSpace(a.GetCredential("tier_id")) return tierID } func (a *Account) IsGeminiCodeAssist() bool { if a.Platform != PlatformGemini || a.Type != AccountTypeOAuth { return false } oauthType := a.GeminiOAuthType() if oauthType == "" { return strings.TrimSpace(a.GetCredential("project_id")) != "" } return oauthType == "code_assist" } func (a *Account) CanGetUsage() bool { return a.Type == AccountTypeOAuth } func (a *Account) GetCredential(key string) string { if a.Credentials == nil { return "" } v, ok := a.Credentials[key] if !ok || v == nil { return "" } // 支持多种类型(兼容历史数据中 expires_at 等字段可能是数字或字符串) switch val := v.(type) { case string: return val case json.Number: // GORM datatypes.JSONMap 使用 UseNumber() 解析,数字类型为 json.Number return val.String() case float64: // JSON 解析后数字默认为 float64 return strconv.FormatInt(int64(val), 10) case int64: return strconv.FormatInt(val, 10) case int: return strconv.Itoa(val) default: return "" } } // GetCredentialAsTime 解析凭证中的时间戳字段,支持多种格式 // 兼容以下格式: // - RFC3339 字符串: "2025-01-01T00:00:00Z" // - Unix 时间戳字符串: "1735689600" // - Unix 时间戳数字: 1735689600 (float64/int64/json.Number) func (a *Account) GetCredentialAsTime(key string) *time.Time { s := a.GetCredential(key) if s == "" { return nil } // 尝试 RFC3339 格式 if t, err := time.Parse(time.RFC3339, s); err == nil { return &t } // 尝试 Unix 时间戳(纯数字字符串) if ts, err := strconv.ParseInt(s, 10, 64); err == nil { t := time.Unix(ts, 0) return &t } return nil } // GetCredentialAsInt64 解析凭证中的 int64 字段 // 用于读取 _token_version 等内部字段 func (a *Account) GetCredentialAsInt64(key string) int64 { if a == nil || a.Credentials == nil { return 0 } val, ok := a.Credentials[key] if !ok || val == nil { return 0 } switch v := val.(type) { case int64: return v case float64: return int64(v) case int: return int64(v) case json.Number: if i, err := v.Int64(); err == nil { return i } case string: if i, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64); err == nil { return i } } return 0 } func (a *Account) IsTempUnschedulableEnabled() bool { if a.Credentials == nil { return false } raw, ok := a.Credentials["temp_unschedulable_enabled"] if !ok || raw == nil { return false } enabled, ok := raw.(bool) return ok && enabled } func (a *Account) GetTempUnschedulableRules() []TempUnschedulableRule { if a.Credentials == nil { return nil } raw, ok := a.Credentials["temp_unschedulable_rules"] if !ok || raw == nil { return nil } arr, ok := raw.([]any) if !ok { return nil } rules := make([]TempUnschedulableRule, 0, len(arr)) for _, item := range arr { entry, ok := item.(map[string]any) if !ok || entry == nil { continue } rule := TempUnschedulableRule{ ErrorCode: parseTempUnschedInt(entry["error_code"]), Keywords: parseTempUnschedStrings(entry["keywords"]), DurationMinutes: parseTempUnschedInt(entry["duration_minutes"]), Description: parseTempUnschedString(entry["description"]), } if rule.ErrorCode <= 0 || rule.DurationMinutes <= 0 || len(rule.Keywords) == 0 { continue } rules = append(rules, rule) } return rules } func parseTempUnschedString(value any) string { s, ok := value.(string) if !ok { return "" } return strings.TrimSpace(s) } func parseTempUnschedStrings(value any) []string { if value == nil { return nil } var raw []string switch v := value.(type) { case []string: raw = v case []any: raw = make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok { raw = append(raw, s) } } default: return nil } out := make([]string, 0, len(raw)) for _, item := range raw { s := strings.TrimSpace(item) if s != "" { out = append(out, s) } } return out } func normalizeAccountNotes(value *string) *string { if value == nil { return nil } trimmed := strings.TrimSpace(*value) if trimmed == "" { return nil } return &trimmed } func parseTempUnschedInt(value any) int { switch v := value.(type) { case int: return v case int64: return int(v) case float64: return int(v) case json.Number: if i, err := v.Int64(); err == nil { return int(i) } case string: if i, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { return i } } return 0 } func (a *Account) GetModelMapping() map[string]string { if a.Credentials == nil { // Antigravity 平台使用默认映射 if a.Platform == domain.PlatformAntigravity { return domain.DefaultAntigravityModelMapping } return nil } raw, ok := a.Credentials["model_mapping"] if !ok || raw == nil { // Antigravity 平台使用默认映射 if a.Platform == domain.PlatformAntigravity { return domain.DefaultAntigravityModelMapping } return nil } if m, ok := raw.(map[string]any); ok { result := make(map[string]string) for k, v := range m { if s, ok := v.(string); ok { result[k] = s } } if len(result) > 0 { return result } } // Antigravity 平台使用默认映射 if a.Platform == domain.PlatformAntigravity { return domain.DefaultAntigravityModelMapping } return nil } // IsModelSupported 检查模型是否在 model_mapping 中(支持通配符) // 如果未配置 mapping,返回 true(允许所有模型) func (a *Account) IsModelSupported(requestedModel string) bool { mapping := a.GetModelMapping() if len(mapping) == 0 { return true // 无映射 = 允许所有 } // 精确匹配 if _, exists := mapping[requestedModel]; exists { return true } // 通配符匹配 for pattern := range mapping { if matchWildcard(pattern, requestedModel) { return true } } return false } // GetMappedModel 获取映射后的模型名(支持通配符,最长优先匹配) // 如果未配置 mapping,返回原始模型名 func (a *Account) GetMappedModel(requestedModel string) string { mapping := a.GetModelMapping() if len(mapping) == 0 { return requestedModel } // 精确匹配优先 if mappedModel, exists := mapping[requestedModel]; exists { return mappedModel } // 通配符匹配(最长优先) return matchWildcardMapping(mapping, requestedModel) } func (a *Account) GetBaseURL() string { if a.Type != AccountTypeAPIKey { return "" } baseURL := a.GetCredential("base_url") if baseURL == "" { return "https://api.anthropic.com" } if a.Platform == PlatformAntigravity { return strings.TrimRight(baseURL, "/") + "/antigravity" } return baseURL } // GetGeminiBaseURL 返回 Gemini 兼容端点的 base URL。 // Antigravity 平台的 APIKey 账号自动拼接 /antigravity。 func (a *Account) GetGeminiBaseURL(defaultBaseURL string) string { baseURL := strings.TrimSpace(a.GetCredential("base_url")) if baseURL == "" { return defaultBaseURL } if a.Platform == PlatformAntigravity && a.Type == AccountTypeAPIKey { return strings.TrimRight(baseURL, "/") + "/antigravity" } return baseURL } func (a *Account) GetExtraString(key string) string { if a.Extra == nil { return "" } if v, ok := a.Extra[key]; ok { if s, ok := v.(string); ok { return s } } return "" } func (a *Account) GetClaudeUserID() string { if v := strings.TrimSpace(a.GetExtraString("claude_user_id")); v != "" { return v } if v := strings.TrimSpace(a.GetExtraString("anthropic_user_id")); v != "" { return v } if v := strings.TrimSpace(a.GetCredential("claude_user_id")); v != "" { return v } if v := strings.TrimSpace(a.GetCredential("anthropic_user_id")); v != "" { return v } return "" } // matchAntigravityWildcard 通配符匹配(仅支持末尾 *) // 用于 model_mapping 的通配符匹配 func matchAntigravityWildcard(pattern, str string) bool { if strings.HasSuffix(pattern, "*") { prefix := pattern[:len(pattern)-1] return strings.HasPrefix(str, prefix) } return pattern == str } // matchWildcard 通用通配符匹配(仅支持末尾 *) // 复用 Antigravity 的通配符逻辑,供其他平台使用 func matchWildcard(pattern, str string) bool { return matchAntigravityWildcard(pattern, str) } // matchWildcardMapping 通配符映射匹配(最长优先) // 如果没有匹配,返回原始字符串 func matchWildcardMapping(mapping map[string]string, requestedModel string) string { // 收集所有匹配的 pattern,按长度降序排序(最长优先) type patternMatch struct { pattern string target string } var matches []patternMatch for pattern, target := range mapping { if matchWildcard(pattern, requestedModel) { matches = append(matches, patternMatch{pattern, target}) } } if len(matches) == 0 { return requestedModel // 无匹配,返回原始模型名 } // 按 pattern 长度降序排序 sort.Slice(matches, func(i, j int) bool { if len(matches[i].pattern) != len(matches[j].pattern) { return len(matches[i].pattern) > len(matches[j].pattern) } return matches[i].pattern < matches[j].pattern }) return matches[0].target } func (a *Account) IsCustomErrorCodesEnabled() bool { if a.Type != AccountTypeAPIKey || a.Credentials == nil { return false } if v, ok := a.Credentials["custom_error_codes_enabled"]; ok { if enabled, ok := v.(bool); ok { return enabled } } return false } func (a *Account) GetCustomErrorCodes() []int { if a.Credentials == nil { return nil } raw, ok := a.Credentials["custom_error_codes"] if !ok || raw == nil { return nil } if arr, ok := raw.([]any); ok { result := make([]int, 0, len(arr)) for _, v := range arr { if f, ok := v.(float64); ok { result = append(result, int(f)) } } return result } return nil } func (a *Account) ShouldHandleErrorCode(statusCode int) bool { if !a.IsCustomErrorCodesEnabled() { return true } codes := a.GetCustomErrorCodes() if len(codes) == 0 { return true } for _, code := range codes { if code == statusCode { return true } } return false } func (a *Account) IsInterceptWarmupEnabled() bool { if a.Credentials == nil { return false } if v, ok := a.Credentials["intercept_warmup_requests"]; ok { if enabled, ok := v.(bool); ok { return enabled } } return false } func (a *Account) IsOpenAI() bool { return a.Platform == PlatformOpenAI } func (a *Account) IsAnthropic() bool { return a.Platform == PlatformAnthropic } func (a *Account) IsOpenAIOAuth() bool { return a.IsOpenAI() && a.Type == AccountTypeOAuth } func (a *Account) IsOpenAIApiKey() bool { return a.IsOpenAI() && a.Type == AccountTypeAPIKey } func (a *Account) GetOpenAIBaseURL() string { if !a.IsOpenAI() { return "" } if a.Type == AccountTypeAPIKey { baseURL := a.GetCredential("base_url") if baseURL != "" { return baseURL } } return "https://api.openai.com" } func (a *Account) GetOpenAIAccessToken() string { if !a.IsOpenAI() { return "" } return a.GetCredential("access_token") } func (a *Account) GetOpenAIRefreshToken() string { if !a.IsOpenAIOAuth() { return "" } return a.GetCredential("refresh_token") } func (a *Account) GetOpenAIIDToken() string { if !a.IsOpenAIOAuth() { return "" } return a.GetCredential("id_token") } func (a *Account) GetOpenAIApiKey() string { if !a.IsOpenAIApiKey() { return "" } return a.GetCredential("api_key") } func (a *Account) GetOpenAIUserAgent() string { if !a.IsOpenAI() { return "" } return a.GetCredential("user_agent") } func (a *Account) GetChatGPTAccountID() string { if !a.IsOpenAIOAuth() { return "" } return a.GetCredential("chatgpt_account_id") } func (a *Account) GetChatGPTUserID() string { if !a.IsOpenAIOAuth() { return "" } return a.GetCredential("chatgpt_user_id") } func (a *Account) GetOpenAIOrganizationID() string { if !a.IsOpenAIOAuth() { return "" } return a.GetCredential("organization_id") } func (a *Account) GetOpenAITokenExpiresAt() *time.Time { if !a.IsOpenAIOAuth() { return nil } return a.GetCredentialAsTime("expires_at") } func (a *Account) IsOpenAITokenExpired() bool { expiresAt := a.GetOpenAITokenExpiresAt() if expiresAt == nil { return false } return time.Now().Add(60 * time.Second).After(*expiresAt) } // IsMixedSchedulingEnabled 检查 antigravity 账户是否启用混合调度 // 启用后可参与 anthropic/gemini 分组的账户调度 func (a *Account) IsMixedSchedulingEnabled() bool { if a.Platform != PlatformAntigravity { return false } if a.Extra == nil { return false } if v, ok := a.Extra["mixed_scheduling"]; ok { if enabled, ok := v.(bool); ok { return enabled } } return false } // WindowCostSchedulability 窗口费用调度状态 type WindowCostSchedulability int const ( // WindowCostSchedulable 可正常调度 WindowCostSchedulable WindowCostSchedulability = iota // WindowCostStickyOnly 仅允许粘性会话 WindowCostStickyOnly // WindowCostNotSchedulable 完全不可调度 WindowCostNotSchedulable ) // IsAnthropicOAuthOrSetupToken 判断是否为 Anthropic OAuth 或 SetupToken 类型账号 // 仅这两类账号支持 5h 窗口额度控制和会话数量控制 func (a *Account) IsAnthropicOAuthOrSetupToken() bool { return a.Platform == PlatformAnthropic && (a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken) } // IsTLSFingerprintEnabled 检查是否启用 TLS 指纹伪装 // 仅适用于 Anthropic OAuth/SetupToken 类型账号 // 启用后将模拟 Claude Code (Node.js) 客户端的 TLS 握手特征 func (a *Account) IsTLSFingerprintEnabled() bool { // 仅支持 Anthropic OAuth/SetupToken 账号 if !a.IsAnthropicOAuthOrSetupToken() { return false } if a.Extra == nil { return false } if v, ok := a.Extra["enable_tls_fingerprint"]; ok { if enabled, ok := v.(bool); ok { return enabled } } return false } // IsSessionIDMaskingEnabled 检查是否启用会话ID伪装 // 仅适用于 Anthropic OAuth/SetupToken 类型账号 // 启用后将在一段时间内(15分钟)固定 metadata.user_id 中的 session ID, // 使上游认为请求来自同一个会话 func (a *Account) IsSessionIDMaskingEnabled() bool { if !a.IsAnthropicOAuthOrSetupToken() { return false } if a.Extra == nil { return false } if v, ok := a.Extra["session_id_masking_enabled"]; ok { if enabled, ok := v.(bool); ok { return enabled } } return false } // GetWindowCostLimit 获取 5h 窗口费用阈值(美元) // 返回 0 表示未启用 func (a *Account) GetWindowCostLimit() float64 { if a.Extra == nil { return 0 } if v, ok := a.Extra["window_cost_limit"]; ok { return parseExtraFloat64(v) } return 0 } // GetWindowCostStickyReserve 获取粘性会话预留额度(美元) // 默认值为 10 func (a *Account) GetWindowCostStickyReserve() float64 { if a.Extra == nil { return 10.0 } if v, ok := a.Extra["window_cost_sticky_reserve"]; ok { val := parseExtraFloat64(v) if val > 0 { return val } } return 10.0 } // GetMaxSessions 获取最大并发会话数 // 返回 0 表示未启用 func (a *Account) GetMaxSessions() int { if a.Extra == nil { return 0 } if v, ok := a.Extra["max_sessions"]; ok { return parseExtraInt(v) } return 0 } // GetSessionIdleTimeoutMinutes 获取会话空闲超时分钟数 // 默认值为 5 分钟 func (a *Account) GetSessionIdleTimeoutMinutes() int { if a.Extra == nil { return 5 } if v, ok := a.Extra["session_idle_timeout_minutes"]; ok { val := parseExtraInt(v) if val > 0 { return val } } return 5 } // CheckWindowCostSchedulability 根据当前窗口费用检查调度状态 // - 费用 < 阈值: WindowCostSchedulable(可正常调度) // - 费用 >= 阈值 且 < 阈值+预留: WindowCostStickyOnly(仅粘性会话) // - 费用 >= 阈值+预留: WindowCostNotSchedulable(不可调度) func (a *Account) CheckWindowCostSchedulability(currentWindowCost float64) WindowCostSchedulability { limit := a.GetWindowCostLimit() if limit <= 0 { return WindowCostSchedulable } if currentWindowCost < limit { return WindowCostSchedulable } stickyReserve := a.GetWindowCostStickyReserve() if currentWindowCost < limit+stickyReserve { return WindowCostStickyOnly } return WindowCostNotSchedulable } // GetCurrentWindowStartTime 获取当前有效的窗口开始时间 // 逻辑: // 1. 如果窗口未过期(SessionWindowEnd 存在且在当前时间之后),使用记录的 SessionWindowStart // 2. 否则(窗口过期或未设置),使用新的预测窗口开始时间(从当前整点开始) func (a *Account) GetCurrentWindowStartTime() time.Time { now := time.Now() // 窗口未过期,使用记录的窗口开始时间 if a.SessionWindowStart != nil && a.SessionWindowEnd != nil && now.Before(*a.SessionWindowEnd) { return *a.SessionWindowStart } // 窗口已过期或未设置,预测新的窗口开始时间(从当前整点开始) // 与 ratelimit_service.go 中 UpdateSessionWindow 的预测逻辑保持一致 return time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location()) } // parseExtraFloat64 从 extra 字段解析 float64 值 func parseExtraFloat64(value any) float64 { switch v := value.(type) { case float64: return v case float32: return float64(v) case int: return float64(v) case int64: return float64(v) case json.Number: if f, err := v.Float64(); err == nil { return f } case string: if f, err := strconv.ParseFloat(strings.TrimSpace(v), 64); err == nil { return f } } return 0 } // parseExtraInt 从 extra 字段解析 int 值 func parseExtraInt(value any) int { switch v := value.(type) { case int: return v case int64: return int(v) case float64: return int(v) case json.Number: if i, err := v.Int64(); err == nil { return int(i) } case string: if i, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { return i } } return 0 }