根据 Gemini CLI 代码审查建议: ## 修改内容 - 将 Gemini OAuth 同步中的 `mergeJSONB` 调用替换为 `mergeMap` - 删除不再使用的 `mergeJSONB` 函数定义 ## 原因 - 其他平台(OpenAI、Anthropic)的账户同步都使用 `mergeMap` - `mergeJSONB` 是为旧的 `model.JSONB` 类型设计,与重构后的架构不一致 - 统一函数命名提高代码可读性和可维护性 ## 影响范围 - backend/internal/service/crs_sync_service.go (4处替换) - backend/internal/service/account.go (删除 mergeJSONB 函数) ## 验证 ✓ 编译通过 ✓ 功能逻辑无变化(mergeMap 和 mergeJSONB 实现相同)
329 lines
6.3 KiB
Go
329 lines
6.3 KiB
Go
package service
|
|
|
|
import "time"
|
|
|
|
type Account struct {
|
|
ID int64
|
|
Name string
|
|
Platform string
|
|
Type string
|
|
Credentials map[string]any
|
|
Extra map[string]any
|
|
ProxyID *int64
|
|
Concurrency int
|
|
Priority int
|
|
Status string
|
|
ErrorMessage string
|
|
LastUsedAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
|
|
Schedulable bool
|
|
|
|
RateLimitedAt *time.Time
|
|
RateLimitResetAt *time.Time
|
|
OverloadUntil *time.Time
|
|
|
|
SessionWindowStart *time.Time
|
|
SessionWindowEnd *time.Time
|
|
SessionWindowStatus string
|
|
|
|
Proxy *Proxy
|
|
AccountGroups []AccountGroup
|
|
GroupIDs []int64
|
|
Groups []*Group
|
|
}
|
|
|
|
func (a *Account) IsActive() bool {
|
|
return a.Status == StatusActive
|
|
}
|
|
|
|
func (a *Account) IsSchedulable() bool {
|
|
if !a.IsActive() || !a.Schedulable {
|
|
return false
|
|
}
|
|
now := time.Now()
|
|
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
|
return false
|
|
}
|
|
if a.RateLimitResetAt != nil && now.Before(*a.RateLimitResetAt) {
|
|
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) CanGetUsage() bool {
|
|
return a.Type == AccountTypeOAuth
|
|
}
|
|
|
|
func (a *Account) GetCredential(key string) string {
|
|
if a.Credentials == nil {
|
|
return ""
|
|
}
|
|
if v, ok := a.Credentials[key]; ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (a *Account) GetModelMapping() map[string]string {
|
|
if a.Credentials == nil {
|
|
return nil
|
|
}
|
|
raw, ok := a.Credentials["model_mapping"]
|
|
if !ok || raw == nil {
|
|
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
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *Account) IsModelSupported(requestedModel string) bool {
|
|
mapping := a.GetModelMapping()
|
|
if len(mapping) == 0 {
|
|
return true
|
|
}
|
|
_, exists := mapping[requestedModel]
|
|
return exists
|
|
}
|
|
|
|
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 requestedModel
|
|
}
|
|
|
|
func (a *Account) GetBaseURL() string {
|
|
if a.Type != AccountTypeApiKey {
|
|
return ""
|
|
}
|
|
baseURL := a.GetCredential("base_url")
|
|
if baseURL == "" {
|
|
return "https://api.anthropic.com"
|
|
}
|
|
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) 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
|
|
}
|
|
expiresAtStr := a.GetCredential("expires_at")
|
|
if expiresAtStr == "" {
|
|
return nil
|
|
}
|
|
t, err := time.Parse(time.RFC3339, expiresAtStr)
|
|
if err != nil {
|
|
if v, ok := a.Credentials["expires_at"].(float64); ok {
|
|
tt := time.Unix(int64(v), 0)
|
|
return &tt
|
|
}
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
func (a *Account) IsOpenAITokenExpired() bool {
|
|
expiresAt := a.GetOpenAITokenExpiresAt()
|
|
if expiresAt == nil {
|
|
return false
|
|
}
|
|
return time.Now().Add(60 * time.Second).After(*expiresAt)
|
|
}
|