From f08f9111e540e436bbd639967577b0c7ae16c9e7 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 12 Jun 2026 00:27:10 +0800 Subject: [PATCH] =?UTF-8?q?refactor(platform-seam):=20gatewayplatform=20?= =?UTF-8?q?=E6=8E=A5=E7=BC=9D=E2=80=94=E2=80=94Provider/Registry=20+=20?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=20Forward=20=E5=88=86=E5=8F=91=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 经双视角评审裁决的收窄版 Phase-3 接缝(零行为变更): - internal/gatewayplatform:Provider{Platform,Forward} 两方法 v1 接口 + ForwardRequest{Parsed,Body,IsStickySession,SessionGroupID,SessionKey} + 构造期注册 Registry(重复 panic,运行期并发只读) - 3 个单语句直返 adapter(anthropic/antigravity/gemini,错误零包裹—— BetaBlockedError/PromptTooLong 的 errors.As 链由 T4 特征化守护) - Messages 端点两处平台分发改 registry 查找;Type!=APIKey 条件保留调用点 - OpenAI(独立 handler)、v1beta 端点、内核 internal/plugin 零触碰 门禁:T1-T5 特征化全绿、安全网 45 包、bench allocs 全部持平(266/329/93/134)。 --- backend/cmd/server/wire_gen.go | 3 +- backend/internal/gatewayplatform/adapters.go | 91 ++++++++++++++ backend/internal/gatewayplatform/provider.go | 52 ++++++++ backend/internal/gatewayplatform/registry.go | 38 ++++++ .../internal/gatewayplatform/registry_test.go | 112 ++++++++++++++++++ backend/internal/handler/gateway_handler.go | 33 ++++-- backend/internal/handler/gateway_platform.go | 20 ++++ ...platform_dispatch_characterization_test.go | 1 + .../scheduling_invariants_failover_test.go | 7 +- backend/internal/handler/wire.go | 1 + .../TASK-002_gatewayplatform_seam.md | 24 ++++ 11 files changed, 369 insertions(+), 13 deletions(-) create mode 100644 backend/internal/gatewayplatform/adapters.go create mode 100644 backend/internal/gatewayplatform/provider.go create mode 100644 backend/internal/gatewayplatform/registry.go create mode 100644 backend/internal/gatewayplatform/registry_test.go create mode 100644 backend/internal/handler/gateway_platform.go create mode 100644 issues/plugin-refactor/phase-3_platform/TASK-002_gatewayplatform_seam.md diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 3eedd8559f..8135b47f75 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -251,11 +251,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { runtime := plugin.ProvideModuleRuntime(host, pluginConfig) moduleHandler := admin.NewModuleHandler(runtime) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, contentModerationHandler, paymentHandler, affiliateHandler, complianceHandler, moduleHandler) + gatewayplatformRegistry := handler.ProvideGatewayPlatformRegistry(gatewayService, antigravityGatewayService) usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig) chain := handler.ProvideGatewayHookChain(contentModerationService) userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient) userMessageQueueService := service.ProvideUserMessageQueueService(userMsgQueueCache, rpmCache, configConfig) - gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, chain, userMessageQueueService, configConfig, settingService) + gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, gatewayplatformRegistry, userService, concurrencyService, billingCacheService, usageService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, chain, userMessageQueueService, configConfig, settingService) openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, apiKeyService, usageRecordWorkerPool, errorPassthroughService, contentModerationService, chain, configConfig) handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo, notificationEmailService) totpHandler := handler.NewTotpHandler(totpService) diff --git a/backend/internal/gatewayplatform/adapters.go b/backend/internal/gatewayplatform/adapters.go new file mode 100644 index 0000000000..a93800777c --- /dev/null +++ b/backend/internal/gatewayplatform/adapters.go @@ -0,0 +1,91 @@ +package gatewayplatform + +// 本文件的三个 adapter 仅包装现有 service 的 Forward 入口(实现零移动)。 +// +// 错误透传铁律(SEAM-DESIGN 裁决 / T4 特征化锁定):底层返回的 error 一律 +// 原样 return,禁止任何包裹——handler 对 *service.BetaBlockedError / +// *service.PromptTooLongError / *service.UpstreamFailoverError 的 errors.As +// 断言链依赖错误原值。 + +import ( + "context" + + "github.com/Wei-Shaw/sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// geminiForwardAction 是 /v1/messages gemini 分支传给 ForwardGemini 的 action +// 常量(SEAM-DESIGN 裁决:硬编码 "generateContent" + 测试锁定;该参数仅决定 +// 客户端响应形态合法性,上游恒为 streamGenerateContent)。 +const geminiForwardAction = "generateContent" + +// AnthropicProvider 包装 GatewayService.Forward(Claude 协议主路径; +// :794 条件不命中侧,含 antigravity 平台的 APIKey 账号)。 +type AnthropicProvider struct { + gateway *service.GatewayService +} + +// NewAnthropicProvider 构造 anthropic 平台 Provider。 +func NewAnthropicProvider(gateway *service.GatewayService) *AnthropicProvider { + return &AnthropicProvider{gateway: gateway} +} + +// Platform 返回 service.PlatformAnthropic。 +func (p *AnthropicProvider) Platform() string { return service.PlatformAnthropic } + +// Forward 转发到 GatewayService.Forward;error 原样透传。 +func (p *AnthropicProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) { + return p.gateway.Forward(ctx, c, account, req.Parsed) +} + +// AntigravityProvider 包装 AntigravityGatewayService.Forward(Claude 协议 → +// antigravity 上游;:794 条件命中侧:Platform == Antigravity && Type != APIKey)。 +type AntigravityProvider struct { + antigravity *service.AntigravityGatewayService +} + +// NewAntigravityProvider 构造 antigravity 平台 Provider。 +func NewAntigravityProvider(antigravity *service.AntigravityGatewayService) *AntigravityProvider { + return &AntigravityProvider{antigravity: antigravity} +} + +// Platform 返回 service.PlatformAntigravity。 +func (p *AntigravityProvider) Platform() string { return service.PlatformAntigravity } + +// Forward 转发到 AntigravityGatewayService.Forward;error 原样透传。 +func (p *AntigravityProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) { + return p.antigravity.Forward(ctx, c, account, req.Body, req.IsStickySession) +} + +// GeminiProvider 包装 AntigravityGatewayService.ForwardGemini(gemini 平台 +// 分组下的 antigravity 账号,:444 命中侧;gemini 平台的非 antigravity 账号 +// 走 GeminiMessagesCompatService,映射逻辑保留在调用点)。 +type GeminiProvider struct { + antigravity *service.AntigravityGatewayService +} + +// NewGeminiProvider 构造 gemini 平台 Provider。 +func NewGeminiProvider(antigravity *service.AntigravityGatewayService) *GeminiProvider { + return &GeminiProvider{antigravity: antigravity} +} + +// Platform 返回 service.PlatformGemini。 +func (p *GeminiProvider) Platform() string { return service.PlatformGemini } + +// Forward 转发到 AntigravityGatewayService.ForwardGemini;error 原样透传。 +// model/stream 取自 req.Parsed(与原调用点 reqModel/reqStream 同源), +// 粘性会话维度经 WithForwardGeminiSession 透传。 +func (p *GeminiProvider) Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) { + return p.antigravity.ForwardGemini( + ctx, + c, + account, + req.Parsed.Model, + geminiForwardAction, + req.Parsed.Stream, + req.Body, + req.IsStickySession, + service.WithForwardGeminiSession(req.SessionGroupID, req.SessionKey), + ) +} diff --git a/backend/internal/gatewayplatform/provider.go b/backend/internal/gatewayplatform/provider.go new file mode 100644 index 0000000000..9ac10ecfe4 --- /dev/null +++ b/backend/internal/gatewayplatform/provider.go @@ -0,0 +1,52 @@ +// Package gatewayplatform 定义 /v1/messages 平台 Forward 分发的 Provider 接缝 +// (Phase-3 SEAM-DESIGN v2 裁决记录)。 +// +// 设计要点: +// - Provider v1 接口仅 Platform/Forward 两方法(DefaultModels / FallbackModel / +// InvalidateToken 等经评审裁决不进接口); +// - adapter 实现零移动:仅包装现有 service 的 Forward 入口;底层 error 一律 +// 原样透传(handler 对 BetaBlockedError / PromptTooLongError 等的 +// errors.As 断言链依赖错误原值,禁止任何包裹); +// - Registry 构造期注册、运行期并发只读;平台 → Provider 的映射逻辑 +// (如 :794 的 Type != APIKey 条件)保留在调用点,registry 只做查找; +// - OpenAI 走独立 OpenAIGatewayHandler 与独立计费管线,不进本注册表; +// 禁止统一跨平台返回类型(OpenAIForwardResult 载荷计费字段会丢失)。 +package gatewayplatform + +import ( + "context" + + "github.com/Wei-Shaw/sub2api/internal/service" + + "github.com/gin-gonic/gin" +) + +// ForwardRequest 是平台 Forward 的统一入参面。字段集为两处分发点现有入参的 +// 并集 {Parsed, Body, IsStickySession, SessionGroupID, SessionKey},不为未接入 +// 平台虚构维度;各调用点保留自己的入参表达式,按各 Provider 的消费面填充。 +type ForwardRequest struct { + // Parsed 为已解析的网关请求(anthropic / gemini Provider 消费; + // gemini Provider 读取 Parsed.Model 与 Parsed.Stream)。 + Parsed *service.ParsedRequest + // Body 为转发请求体原文(antigravity / gemini Provider 消费)。 + Body []byte + // IsStickySession 表示请求已命中粘性会话绑定(antigravity / gemini + // Provider 消费)。 + IsStickySession bool + // SessionGroupID 与 SessionKey 为粘性会话维度(仅 gemini Provider 消费, + // 经 WithForwardGeminiSession 透传到模型限流切换时的粘性绑定清除)。 + SessionGroupID int64 + SessionKey string +} + +// Provider 是单一平台的 Forward 入口。 +// +// 契约: +// - Platform 返回 service.Platform* 现有常量,作为 Registry 的查找键; +// - Forward 将请求转发到对应平台 service,error 必须原样透传(禁止包裹)。 +type Provider interface { + // Platform 返回该 Provider 服务的平台标识(service.Platform* 常量)。 + Platform() string + // Forward 执行平台转发。 + Forward(ctx context.Context, c *gin.Context, account *service.Account, req *ForwardRequest) (*service.ForwardResult, error) +} diff --git a/backend/internal/gatewayplatform/registry.go b/backend/internal/gatewayplatform/registry.go new file mode 100644 index 0000000000..05aad336f1 --- /dev/null +++ b/backend/internal/gatewayplatform/registry.go @@ -0,0 +1,38 @@ +package gatewayplatform + +import "fmt" + +// Registry 是 platform → Provider 的只读查找表:构造期一次性注册, +// 构造完成后并发只读(无锁)。 +type Registry struct { + providers map[string]Provider +} + +// NewRegistry 构造注册表。providers 中出现 nil Provider、空 Platform() 或 +// 重复平台时 panic——装配错误必须在启动期(Wire 装配)暴露,而非请求期。 +func NewRegistry(providers ...Provider) *Registry { + m := make(map[string]Provider, len(providers)) + for _, p := range providers { + if p == nil { + panic("gatewayplatform: nil provider") + } + platform := p.Platform() + if platform == "" { + panic("gatewayplatform: provider with empty platform") + } + if _, dup := m[platform]; dup { + panic(fmt.Sprintf("gatewayplatform: duplicate provider for platform %q", platform)) + } + m[platform] = p + } + return &Registry{providers: m} +} + +// Get 返回平台对应的 Provider;未注册(或 nil Registry)返回 nil。 +// 调用点须保证所查平台已在装配期注册(Wire 注册全集,miss 属编程错误)。 +func (r *Registry) Get(platform string) Provider { + if r == nil { + return nil + } + return r.providers[platform] +} diff --git a/backend/internal/gatewayplatform/registry_test.go b/backend/internal/gatewayplatform/registry_test.go new file mode 100644 index 0000000000..34606e95d8 --- /dev/null +++ b/backend/internal/gatewayplatform/registry_test.go @@ -0,0 +1,112 @@ +package gatewayplatform + +import ( + "context" + "sync" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/service" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +// stubProvider 是注册表测试用的最小 Provider 实现。 +type stubProvider struct { + platform string +} + +func (s *stubProvider) Platform() string { return s.platform } + +func (s *stubProvider) Forward(_ context.Context, _ *gin.Context, _ *service.Account, _ *ForwardRequest) (*service.ForwardResult, error) { + return nil, nil +} + +func TestRegistry_GetReturnsRegisteredProvider(t *testing.T) { + anthropic := &stubProvider{platform: service.PlatformAnthropic} + antigravity := &stubProvider{platform: service.PlatformAntigravity} + gemini := &stubProvider{platform: service.PlatformGemini} + + registry := NewRegistry(anthropic, antigravity, gemini) + + require.Same(t, Provider(anthropic), registry.Get(service.PlatformAnthropic)) + require.Same(t, Provider(antigravity), registry.Get(service.PlatformAntigravity)) + require.Same(t, Provider(gemini), registry.Get(service.PlatformGemini)) +} + +func TestRegistry_GetUnregisteredPlatformReturnsNil(t *testing.T) { + registry := NewRegistry(&stubProvider{platform: service.PlatformAnthropic}) + + require.Nil(t, registry.Get(service.PlatformOpenAI), "未注册平台必须返回 nil") + require.Nil(t, registry.Get(""), "空平台必须返回 nil") + + var nilRegistry *Registry + require.Nil(t, nilRegistry.Get(service.PlatformAnthropic), "nil Registry 必须返回 nil 而非 panic") +} + +func TestNewRegistry_DuplicatePlatformPanics(t *testing.T) { + require.PanicsWithValue(t, + `gatewayplatform: duplicate provider for platform "anthropic"`, + func() { + NewRegistry( + &stubProvider{platform: service.PlatformAnthropic}, + &stubProvider{platform: service.PlatformAnthropic}, + ) + }) +} + +func TestNewRegistry_NilProviderPanics(t *testing.T) { + require.PanicsWithValue(t, "gatewayplatform: nil provider", func() { + NewRegistry(&stubProvider{platform: service.PlatformAnthropic}, nil) + }) +} + +func TestNewRegistry_EmptyPlatformPanics(t *testing.T) { + require.PanicsWithValue(t, "gatewayplatform: provider with empty platform", func() { + NewRegistry(&stubProvider{platform: ""}) + }) +} + +// TestRegistry_ConcurrentGet 验证构造完成后的注册表可被并发只读访问 +// (配合 -race 检测数据竞争)。 +func TestRegistry_ConcurrentGet(t *testing.T) { + registry := NewRegistry( + &stubProvider{platform: service.PlatformAnthropic}, + &stubProvider{platform: service.PlatformAntigravity}, + &stubProvider{platform: service.PlatformGemini}, + ) + + platforms := []string{ + service.PlatformAnthropic, + service.PlatformAntigravity, + service.PlatformGemini, + service.PlatformOpenAI, // miss 路径同样并发安全 + } + + var wg sync.WaitGroup + for i := 0; i < 32; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 200; j++ { + platform := platforms[(i+j)%len(platforms)] + got := registry.Get(platform) + if platform == service.PlatformOpenAI { + require.Nil(t, got) + } else { + require.NotNil(t, got) + require.Equal(t, platform, got.Platform()) + } + } + }(i) + } + wg.Wait() +} + +// TestAdapters_PlatformIdentity 锁定三个 adapter 的注册键 = service.Platform* 常量 +// (Registry 查找键与 :444/:794 调用点的查找表达式一致)。 +func TestAdapters_PlatformIdentity(t *testing.T) { + require.Equal(t, service.PlatformAnthropic, NewAnthropicProvider(nil).Platform()) + require.Equal(t, service.PlatformAntigravity, NewAntigravityProvider(nil).Platform()) + require.Equal(t, service.PlatformGemini, NewGeminiProvider(nil).Platform()) +} diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index f24e20e88c..559478ac2c 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -16,6 +16,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/domain" "github.com/Wei-Shaw/sub2api/internal/gatewayhook" + "github.com/Wei-Shaw/sub2api/internal/gatewayplatform" "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity" "github.com/Wei-Shaw/sub2api/internal/pkg/claude" "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" @@ -42,6 +43,7 @@ type GatewayHandler struct { gatewayService *service.GatewayService geminiCompatService *service.GeminiMessagesCompatService antigravityGatewayService *service.AntigravityGatewayService + platformRegistry *gatewayplatform.Registry userService *service.UserService billingCacheService *service.BillingCacheService usageService *service.UsageService @@ -62,6 +64,7 @@ func NewGatewayHandler( gatewayService *service.GatewayService, geminiCompatService *service.GeminiMessagesCompatService, antigravityGatewayService *service.AntigravityGatewayService, + platformRegistry *gatewayplatform.Registry, userService *service.UserService, concurrencyService *service.ConcurrencyService, billingCacheService *service.BillingCacheService, @@ -97,6 +100,7 @@ func NewGatewayHandler( gatewayService: gatewayService, geminiCompatService: geminiCompatService, antigravityGatewayService: antigravityGatewayService, + platformRegistry: platformRegistry, userService: userService, billingCacheService: billingCacheService, usageService: usageService, @@ -442,16 +446,20 @@ func (h *GatewayHandler) Messages(c *gin.Context) { // 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover writerSizeBeforeForward := c.Writer.Size() if account.Platform == service.PlatformAntigravity { - result, err = h.antigravityGatewayService.ForwardGemini( + // gemini provider → AntigravityGatewayService.ForwardGemini: + // model/stream 经 Parsed 透传(与原 reqModel/reqStream 同源), + // action 由 adapter 硬编码 "generateContent"。 + result, err = h.platformRegistry.Get(service.PlatformGemini).Forward( requestCtx, c, account, - reqModel, - "generateContent", - reqStream, - body, - hasBoundSession, - service.WithForwardGeminiSession(derefGroupID(apiKey.GroupID), sessionKey), + &gatewayplatform.ForwardRequest{ + Parsed: parsedReq, + Body: body, + IsStickySession: hasBoundSession, + SessionGroupID: derefGroupID(apiKey.GroupID), + SessionKey: sessionKey, + }, ) } else { result, err = h.geminiCompatService.Forward(requestCtx, c, account, body) @@ -791,10 +799,17 @@ func (h *GatewayHandler) Messages(c *gin.Context) { } // 记录 Forward 前已写入字节数,Forward 后若增加则说明 SSE 内容已发,禁止 failover writerSizeBeforeForward := c.Writer.Size() + // 平台 → Provider 的映射逻辑保留在调用点:antigravity 平台的 + // APIKey 账号走 anthropic provider(Claude 协议直连上游)。 + forwardReq := &gatewayplatform.ForwardRequest{ + Parsed: attemptParsedReq, + Body: attemptBody, + IsStickySession: hasBoundSession, + } if account.Platform == service.PlatformAntigravity && account.Type != service.AccountTypeAPIKey { - result, err = h.antigravityGatewayService.Forward(requestCtx, c, account, attemptBody, hasBoundSession) + result, err = h.platformRegistry.Get(service.PlatformAntigravity).Forward(requestCtx, c, account, forwardReq) } else { - result, err = h.gatewayService.Forward(requestCtx, c, account, attemptParsedReq) + result, err = h.platformRegistry.Get(service.PlatformAnthropic).Forward(requestCtx, c, account, forwardReq) } // 兜底释放串行锁(正常情况已通过回调提前释放) diff --git a/backend/internal/handler/gateway_platform.go b/backend/internal/handler/gateway_platform.go new file mode 100644 index 0000000000..64e9cf8f20 --- /dev/null +++ b/backend/internal/handler/gateway_platform.go @@ -0,0 +1,20 @@ +package handler + +import ( + "github.com/Wei-Shaw/sub2api/internal/gatewayplatform" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +// ProvideGatewayPlatformRegistry 装配 /v1/messages 平台 Forward 分发注册表 +// (Phase-3 SEAM-DESIGN 裁决:anthropic / antigravity / gemini 三 adapter; +// OpenAI 走独立 OpenAIGatewayHandler 与独立计费管线,不进本注册表)。 +func ProvideGatewayPlatformRegistry( + gatewayService *service.GatewayService, + antigravityGatewayService *service.AntigravityGatewayService, +) *gatewayplatform.Registry { + return gatewayplatform.NewRegistry( + gatewayplatform.NewAnthropicProvider(gatewayService), + gatewayplatform.NewAntigravityProvider(antigravityGatewayService), + gatewayplatform.NewGeminiProvider(antigravityGatewayService), + ) +} diff --git a/backend/internal/handler/gateway_platform_dispatch_characterization_test.go b/backend/internal/handler/gateway_platform_dispatch_characterization_test.go index 2b664b8fe5..e61a11108b 100644 --- a/backend/internal/handler/gateway_platform_dispatch_characterization_test.go +++ b/backend/internal/handler/gateway_platform_dispatch_characterization_test.go @@ -282,6 +282,7 @@ func p3CharNewHandler(t *testing.T, group *service.Group, accounts []*service.Ac gwSvc, nil, // geminiCompatService(:444 非 antigravity 分支误入会 panic → 用例显式失败) agSvc, + ProvideGatewayPlatformRegistry(gwSvc, agSvc), // 与生产 Wire 装配同构的平台分发注册表 nil, // userService concurrencySvc, billingCacheSvc, diff --git a/backend/internal/handler/scheduling_invariants_failover_test.go b/backend/internal/handler/scheduling_invariants_failover_test.go index dc7fdee16e..a1ef93b02d 100644 --- a/backend/internal/handler/scheduling_invariants_failover_test.go +++ b/backend/internal/handler/scheduling_invariants_failover_test.go @@ -346,6 +346,7 @@ func schedInvNewHandler(t *testing.T, group *service.Group, accounts []*service. gwSvc, nil, // geminiCompatService nil, // antigravityGatewayService + ProvideGatewayPlatformRegistry(gwSvc, nil), // anthropic 账号经 anthropic provider → gwSvc.Forward nil, // userService concurrencySvc, billingCacheSvc, @@ -406,7 +407,7 @@ func schedInvMessagesBody() []byte { func TestSchedulingInvariant_FailoverSwitchLimit_DefaultValues(t *testing.T) { t.Run("anthropic与gemini默认上限", func(t *testing.T) { - h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) require.Equal(t, 10, h.maxAccountSwitches, "anthropic 默认换号上限必须为 10") require.Equal(t, 3, h.maxAccountSwitchesGemini, "gemini 默认换号上限必须为 3") }) @@ -420,7 +421,7 @@ func TestSchedulingInvariant_FailoverSwitchLimit_DefaultValues(t *testing.T) { cfg := &config.Config{} cfg.Gateway.MaxAccountSwitches = 5 cfg.Gateway.MaxAccountSwitchesGemini = 2 - h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, cfg, nil) + h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, cfg, nil) require.Equal(t, 5, h.maxAccountSwitches) require.Equal(t, 2, h.maxAccountSwitchesGemini) }) @@ -546,7 +547,7 @@ func TestSchedulingInvariant_FailoverSameAccountRetry_FullChain(t *testing.T) { // (gemini 平台 Forward 的服务层内部 500 重试带秒级退避,完整 e2e 不可在 // 单测时间预算内执行,故此处固化 handler 循环契约层语义。) func TestSchedulingInvariant_FailoverGemini_SwitchLimitLoopContract(t *testing.T) { - h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) + h := NewGatewayHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) require.Equal(t, 3, h.maxAccountSwitchesGemini) mock := &mockTempUnscheduler{} diff --git a/backend/internal/handler/wire.go b/backend/internal/handler/wire.go index 97ed8db2c1..c6d8c1df4b 100644 --- a/backend/internal/handler/wire.go +++ b/backend/internal/handler/wire.go @@ -150,6 +150,7 @@ var ProviderSet = wire.NewSet( NewAnnouncementHandler, NewChannelMonitorUserHandler, ProvideGatewayHookChain, + ProvideGatewayPlatformRegistry, NewGatewayHandler, NewOpenAIGatewayHandler, NewTotpHandler, diff --git a/issues/plugin-refactor/phase-3_platform/TASK-002_gatewayplatform_seam.md b/issues/plugin-refactor/phase-3_platform/TASK-002_gatewayplatform_seam.md new file mode 100644 index 0000000000..5684f3029c --- /dev/null +++ b/issues/plugin-refactor/phase-3_platform/TASK-002_gatewayplatform_seam.md @@ -0,0 +1,24 @@ +# 完成报告: [TASK-002] gatewayplatform 接缝(Provider/Registry/3 adapter + 分发替换) + +- **完成状态**: Success +- **关联设计**: [SEAM-DESIGN.md v2 裁决记录](../../../.claude/plugin-refactor/phases/phase-3_platform/SEAM-DESIGN.md) +- **完成日期**: 2026-06-12 + +## 1. 任务完成简报 + +`internal/gatewayplatform` 落地:`Provider{Platform; Forward}`(v1 仅两方法)+ `ForwardRequest{Parsed, Body, IsStickySession, SessionGroupID, SessionKey}` + 构造期注册 Registry(重复/nil panic,运行期并发只读无锁)+ 3 个单语句直返 adapter(anthropic/antigravity/gemini,action="generateContent" 常量)。`:444`/`:794` 两处分发改 registry 查找,**`Type != APIKey` 条件保留在调用点**。OpenAI、v1beta 端点的 ForwardGemini、内核全部零触碰。 + +## 2. 等价性证据(要点) + +- :444 逐字段对照:reqModel/reqStream 与 Parsed.Model/Stream 同源且无 mutation(gemini 分支不经 :794 的克隆循环);session 两字段经 ForwardRequest 重组同名 option——T2/T3 断言; +- :794 双分支共享 ForwardRequest(attemptBody=attemptParsedReq.Body.Bytes() 关系不变);session 维度留零值(现状不传、adapter 不消费——不虚构维度); +- gemini 平台非 antigravity 账号的 else 分支(geminiCompatService.Forward)保持原状(不在裁决 3 adapter 内); +- 错误透传:adapter 单语句 `return p.svc.Forward(...)`,T4 两用例(BetaBlockedError/PromptTooLong 的 errors.As 链)经新路径全绿。 + +## 3. 文件变更 + +新增 gatewayplatform 包 4 文件 + handler/gateway_platform.go(Wire provider);修改 gateway_handler.go(字段+两处替换)、handler/wire.go、wire_gen.go(wire@v0.7.0 生成);两个测试文件仅夹具加参(断言零改动)。 + +## 4. 验证(主控复跑确认) + +T1-T4+action 契约全绿、gatewayplatform 包 -race 通过、`make test-invariants` 45 包、全量 unit 零失败、build/vet/gofmt 干净、bench compare exit 0(allocs 全部持平:266/329/93/134)。