feat: CRS 同步增强 - 自动刷新 OAuth token 和修复测试配置 (#27)
* fix(service): 修复 OpenAI Responses API 测试负载配置 - 所有账号类型统一添加 instructions 字段(不再仅限 OAuth) - Responses API 要求所有请求必须包含 instructions 参数 * feat(crs-sync): CRS 同步时自动刷新 OAuth token 并保留完整 extra 字段 **核心功能**: - CRSSyncService 注入 OAuth 服务依赖(Anthropic + OpenAI) - 账号创建/更新后自动刷新 OAuth token,确保可用性 - 完整保留 CRS extra 字段,避免数据丢失 **Extra 字段增强**: - 保留 CRS 所有原始 extra 字段 - 新增同步元数据: crs_account_id, crs_kind, crs_synced_at - Claude 账号: 从 credentials 提取 org_uuid/account_uuid 到 extra - OpenAI 账号: 映射 crs_email -> email **Token 刷新逻辑**: - 新增 refreshOAuthToken() 方法处理 Anthropic/OpenAI 平台 - 保留原有 credentials 字段,仅更新 token 相关字段 - 刷新失败静默处理,不中断同步流程 **依赖注入**: - wire_gen.go: CRSSyncService 新增 oAuthService/openaiOAuthService * style(crs-sync): 使用 switch 替代 if-else 修复 golangci-lint 警告 - 将 refreshOAuthToken 中的 if-else 改为 switch 语句 - 符合 staticcheck 规范 - 添加 default 分支处理未知平台
This commit is contained in:
@@ -86,7 +86,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
accountTestService := service.NewAccountTestService(accountRepository, oAuthService, openAIOAuthService, httpUpstream)
|
||||
concurrencyCache := repository.NewConcurrencyCache(client)
|
||||
concurrencyService := service.NewConcurrencyService(concurrencyCache)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository)
|
||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService)
|
||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||
openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
|
||||
|
||||
@@ -388,12 +388,14 @@ func createOpenAITestPayload(modelID string, isOAuth bool) map[string]any {
|
||||
"stream": true,
|
||||
}
|
||||
|
||||
// OAuth accounts using ChatGPT internal API require store: false and instructions
|
||||
// OAuth accounts using ChatGPT internal API require store: false
|
||||
if isOAuth {
|
||||
payload["store"] = false
|
||||
payload["instructions"] = openai.DefaultInstructions
|
||||
}
|
||||
|
||||
// All accounts require instructions for Responses API
|
||||
payload["instructions"] = openai.DefaultInstructions
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,23 @@ import (
|
||||
)
|
||||
|
||||
type CRSSyncService struct {
|
||||
accountRepo ports.AccountRepository
|
||||
proxyRepo ports.ProxyRepository
|
||||
accountRepo ports.AccountRepository
|
||||
proxyRepo ports.ProxyRepository
|
||||
oauthService *OAuthService
|
||||
openaiOAuthService *OpenAIOAuthService
|
||||
}
|
||||
|
||||
func NewCRSSyncService(accountRepo ports.AccountRepository, proxyRepo ports.ProxyRepository) *CRSSyncService {
|
||||
func NewCRSSyncService(
|
||||
accountRepo ports.AccountRepository,
|
||||
proxyRepo ports.ProxyRepository,
|
||||
oauthService *OAuthService,
|
||||
openaiOAuthService *OpenAIOAuthService,
|
||||
) *CRSSyncService {
|
||||
return &CRSSyncService{
|
||||
accountRepo: accountRepo,
|
||||
proxyRepo: proxyRepo,
|
||||
accountRepo: accountRepo,
|
||||
proxyRepo: proxyRepo,
|
||||
oauthService: oauthService,
|
||||
openaiOAuthService: openaiOAuthService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,12 +241,23 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
concurrency := 3
|
||||
status := mapCRSStatus(src.IsActive, src.Status)
|
||||
|
||||
// 🔧 Use CRS extra data directly, add sync metadata
|
||||
extra := src.Extra
|
||||
if extra == nil {
|
||||
extra = make(map[string]any)
|
||||
// 🔧 Preserve all CRS extra fields and add sync metadata
|
||||
extra := make(map[string]any)
|
||||
if src.Extra != nil {
|
||||
for k, v := range src.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
extra["crs_account_id"] = src.ID
|
||||
extra["crs_kind"] = src.Kind
|
||||
extra["crs_synced_at"] = now
|
||||
// Extract org_uuid and account_uuid from CRS credentials to extra
|
||||
if orgUUID, ok := src.Credentials["org_uuid"]; ok {
|
||||
extra["org_uuid"] = orgUUID
|
||||
}
|
||||
if accountUUID, ok := src.Credentials["account_uuid"]; ok {
|
||||
extra["account_uuid"] = accountUUID
|
||||
}
|
||||
|
||||
existing, err := s.accountRepo.GetByCRSAccountID(ctx, src.ID)
|
||||
if err != nil {
|
||||
@@ -268,6 +288,13 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
result.Items = append(result.Items, item)
|
||||
continue
|
||||
}
|
||||
// 🔄 Refresh OAuth token after creation
|
||||
if targetType == model.AccountTypeOAuth {
|
||||
if refreshedCreds := s.refreshOAuthToken(ctx, account); refreshedCreds != nil {
|
||||
account.Credentials = refreshedCreds
|
||||
_ = s.accountRepo.Update(ctx, account)
|
||||
}
|
||||
}
|
||||
item.Action = "created"
|
||||
result.Created++
|
||||
result.Items = append(result.Items, item)
|
||||
@@ -296,6 +323,14 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
continue
|
||||
}
|
||||
|
||||
// 🔄 Refresh OAuth token after update
|
||||
if targetType == model.AccountTypeOAuth {
|
||||
if refreshedCreds := s.refreshOAuthToken(ctx, existing); refreshedCreds != nil {
|
||||
existing.Credentials = refreshedCreds
|
||||
_ = s.accountRepo.Update(ctx, existing)
|
||||
}
|
||||
}
|
||||
|
||||
item.Action = "updated"
|
||||
result.Updated++
|
||||
result.Items = append(result.Items, item)
|
||||
@@ -449,12 +484,20 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
concurrency := 3
|
||||
status := mapCRSStatus(src.IsActive, src.Status)
|
||||
|
||||
// 🔧 Use CRS extra data directly, add sync metadata
|
||||
extra := src.Extra
|
||||
if extra == nil {
|
||||
extra = make(map[string]any)
|
||||
// 🔧 Preserve all CRS extra fields and add sync metadata
|
||||
extra := make(map[string]any)
|
||||
if src.Extra != nil {
|
||||
for k, v := range src.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
}
|
||||
extra["crs_account_id"] = src.ID
|
||||
extra["crs_kind"] = src.Kind
|
||||
extra["crs_synced_at"] = now
|
||||
// Extract email from CRS extra (crs_email -> email)
|
||||
if crsEmail, ok := src.Extra["crs_email"]; ok {
|
||||
extra["email"] = crsEmail
|
||||
}
|
||||
|
||||
existing, err := s.accountRepo.GetByCRSAccountID(ctx, src.ID)
|
||||
if err != nil {
|
||||
@@ -485,6 +528,11 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
result.Items = append(result.Items, item)
|
||||
continue
|
||||
}
|
||||
// 🔄 Refresh OAuth token after creation
|
||||
if refreshedCreds := s.refreshOAuthToken(ctx, account); refreshedCreds != nil {
|
||||
account.Credentials = refreshedCreds
|
||||
_ = s.accountRepo.Update(ctx, account)
|
||||
}
|
||||
item.Action = "created"
|
||||
result.Created++
|
||||
result.Items = append(result.Items, item)
|
||||
@@ -512,6 +560,12 @@ func (s *CRSSyncService) SyncFromCRS(ctx context.Context, input SyncFromCRSInput
|
||||
continue
|
||||
}
|
||||
|
||||
// 🔄 Refresh OAuth token after update
|
||||
if refreshedCreds := s.refreshOAuthToken(ctx, existing); refreshedCreds != nil {
|
||||
existing.Credentials = refreshedCreds
|
||||
_ = s.accountRepo.Update(ctx, existing)
|
||||
}
|
||||
|
||||
item.Action = "updated"
|
||||
result.Updated++
|
||||
result.Items = append(result.Items, item)
|
||||
@@ -841,3 +895,67 @@ func crsExportAccounts(ctx context.Context, client *http.Client, baseURL, adminT
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
// refreshOAuthToken attempts to refresh OAuth token for a synced account
|
||||
// Returns updated credentials or nil if refresh failed/not applicable
|
||||
func (s *CRSSyncService) refreshOAuthToken(ctx context.Context, account *model.Account) model.JSONB {
|
||||
if account.Type != model.AccountTypeOAuth {
|
||||
return nil
|
||||
}
|
||||
|
||||
var newCredentials map[string]any
|
||||
var err error
|
||||
|
||||
switch account.Platform {
|
||||
case model.PlatformAnthropic:
|
||||
if s.oauthService == nil {
|
||||
return nil
|
||||
}
|
||||
tokenInfo, refreshErr := s.oauthService.RefreshAccountToken(ctx, account)
|
||||
if refreshErr != nil {
|
||||
err = refreshErr
|
||||
} else {
|
||||
// Preserve existing credentials
|
||||
newCredentials = make(map[string]any)
|
||||
for k, v := range account.Credentials {
|
||||
newCredentials[k] = v
|
||||
}
|
||||
// Update token fields
|
||||
newCredentials["access_token"] = tokenInfo.AccessToken
|
||||
newCredentials["token_type"] = tokenInfo.TokenType
|
||||
newCredentials["expires_in"] = tokenInfo.ExpiresIn
|
||||
newCredentials["expires_at"] = tokenInfo.ExpiresAt
|
||||
if tokenInfo.RefreshToken != "" {
|
||||
newCredentials["refresh_token"] = tokenInfo.RefreshToken
|
||||
}
|
||||
if tokenInfo.Scope != "" {
|
||||
newCredentials["scope"] = tokenInfo.Scope
|
||||
}
|
||||
}
|
||||
case model.PlatformOpenAI:
|
||||
if s.openaiOAuthService == nil {
|
||||
return nil
|
||||
}
|
||||
tokenInfo, refreshErr := s.openaiOAuthService.RefreshAccountToken(ctx, account)
|
||||
if refreshErr != nil {
|
||||
err = refreshErr
|
||||
} else {
|
||||
newCredentials = s.openaiOAuthService.BuildAccountCredentials(tokenInfo)
|
||||
// Preserve non-token settings from existing credentials
|
||||
for k, v := range account.Credentials {
|
||||
if _, exists := newCredentials[k]; !exists {
|
||||
newCredentials[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Log but don't fail the sync - token might still be valid or refreshable later
|
||||
return nil
|
||||
}
|
||||
|
||||
return model.JSONB(newCredentials)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user