| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "strings"
- "sync"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
- "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
- "github.com/google/uuid"
- "github.com/tidwall/gjson"
- )
- const (
- antigravityStickySessionTTL = time.Hour
- antigravityMaxRetries = 3
- antigravityRetryBaseDelay = 1 * time.Second
- antigravityRetryMaxDelay = 16 * time.Second
- // 限流相关常量
- // antigravityRateLimitThreshold 限流等待/切换阈值
- // - 智能重试:retryDelay < 此阈值时等待后重试,>= 此阈值时直接限流模型
- // - 预检查:剩余限流时间 < 此阈值时等待,>= 此阈值时切换账号
- antigravityRateLimitThreshold = 7 * time.Second
- antigravitySmartRetryMinWait = 1 * time.Second // 智能重试最小等待时间
- antigravitySmartRetryMaxAttempts = 1 // 智能重试最大次数(仅重试 1 次,防止重复限流/长期等待)
- antigravityDefaultRateLimitDuration = 30 * time.Second // 默认限流时间(无 retryDelay 时使用)
- // MODEL_CAPACITY_EXHAUSTED 专用重试参数
- // 模型容量不足时,所有账号共享同一容量池,切换账号无意义
- // 使用固定 1s 间隔重试,最多重试 60 次
- antigravityModelCapacityRetryMaxAttempts = 60
- antigravityModelCapacityRetryWait = 1 * time.Second
- // Google RPC 状态和类型常量
- googleRPCStatusResourceExhausted = "RESOURCE_EXHAUSTED"
- googleRPCStatusUnavailable = "UNAVAILABLE"
- googleRPCTypeRetryInfo = "type.googleapis.com/google.rpc.RetryInfo"
- googleRPCTypeErrorInfo = "type.googleapis.com/google.rpc.ErrorInfo"
- googleRPCReasonModelCapacityExhausted = "MODEL_CAPACITY_EXHAUSTED"
- googleRPCReasonRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
- // 单账号 503 退避重试:Service 层原地重试的最大次数
- // 在 handleSmartRetry 中,对于 shouldRateLimitModel(长延迟 ≥ 7s)的情况,
- // 多账号模式下会设限流+切换账号;但单账号模式下改为原地等待+重试。
- antigravitySingleAccountSmartRetryMaxAttempts = 3
- // 单账号 503 退避重试:原地重试时单次最大等待时间
- // 防止上游返回过长的 retryDelay 导致请求卡住太久
- antigravitySingleAccountSmartRetryMaxWait = 15 * time.Second
- // 单账号 503 退避重试:原地重试的总累计等待时间上限
- // 超过此上限将不再重试,直接返回 503
- antigravitySingleAccountSmartRetryTotalMaxWait = 30 * time.Second
- // MODEL_CAPACITY_EXHAUSTED 全局去重:重试全部失败后的 cooldown 时间
- antigravityModelCapacityCooldown = 10 * time.Second
- )
- // antigravityPassthroughErrorMessages 透传给客户端的错误消息白名单(小写)
- // 匹配时使用 strings.Contains,无需完全匹配
- var antigravityPassthroughErrorMessages = []string{
- "prompt is too long",
- }
- // MODEL_CAPACITY_EXHAUSTED 全局去重:避免多个并发请求同时对同一模型进行容量耗尽重试
- var (
- modelCapacityExhaustedMu sync.RWMutex
- modelCapacityExhaustedUntil = make(map[string]time.Time) // modelName -> cooldown until
- )
- const (
- antigravityForwardBaseURLEnv = "GATEWAY_ANTIGRAVITY_FORWARD_BASE_URL"
- antigravityFallbackSecondsEnv = "GATEWAY_ANTIGRAVITY_FALLBACK_COOLDOWN_SECONDS"
- )
- const antigravityProjectIDFallbackCredentialKey = "antigravity_project_id"
- var errAntigravityProjectIDRequired = errors.New("该 standard-tier Antigravity 账号需配置 project_id")
- // AntigravityAccountSwitchError 账号切换信号
- // 当账号限流时间超过阈值时,通知上层切换账号
- type AntigravityAccountSwitchError struct {
- OriginalAccountID int64
- RateLimitedModel string
- IsStickySession bool // 是否为粘性会话切换(决定是否缓存计费)
- }
- func (e *AntigravityAccountSwitchError) Error() string {
- return fmt.Sprintf("account %d model %s rate limited, need switch",
- e.OriginalAccountID, e.RateLimitedModel)
- }
- // IsAntigravityAccountSwitchError 检查错误是否为账号切换信号
- func IsAntigravityAccountSwitchError(err error) (*AntigravityAccountSwitchError, bool) {
- var switchErr *AntigravityAccountSwitchError
- if errors.As(err, &switchErr) {
- return switchErr, true
- }
- return nil, false
- }
- // PromptTooLongError 表示上游明确返回 prompt too long
- type PromptTooLongError struct {
- StatusCode int
- RequestID string
- Body []byte
- }
- func (e *PromptTooLongError) Error() string {
- return fmt.Sprintf("prompt too long: status=%d", e.StatusCode)
- }
- // AntigravityGatewayService 处理 Antigravity 平台的 API 转发
- type AntigravityGatewayService struct {
- accountRepo AccountRepository
- tokenProvider *AntigravityTokenProvider
- rateLimitService *RateLimitService
- httpUpstream HTTPUpstream
- settingService *SettingService
- cache GatewayCache // 用于模型级限流时清除粘性会话绑定
- schedulerSnapshot *SchedulerSnapshotService
- internal500Cache Internal500CounterCache // INTERNAL 500 渐进惩罚计数器
- }
- func (s *AntigravityGatewayService) upstreamErrorBodyReadLimit() int64 {
- limit := gatewayUpstreamErrorBodyReadLimit
- if s != nil && s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.LogUpstreamErrorBody && s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes > int(limit) {
- limit = int64(s.settingService.cfg.Gateway.LogUpstreamErrorBodyMaxBytes)
- }
- return limit
- }
- func (s *AntigravityGatewayService) readUpstreamErrorBody(resp *http.Response) []byte {
- if resp == nil || resp.Body == nil {
- return nil
- }
- body, _ := io.ReadAll(io.LimitReader(resp.Body, s.upstreamErrorBodyReadLimit()))
- return body
- }
- func NewAntigravityGatewayService(
- accountRepo AccountRepository,
- cache GatewayCache,
- schedulerSnapshot *SchedulerSnapshotService,
- tokenProvider *AntigravityTokenProvider,
- rateLimitService *RateLimitService,
- httpUpstream HTTPUpstream,
- settingService *SettingService,
- internal500Cache Internal500CounterCache,
- ) *AntigravityGatewayService {
- return &AntigravityGatewayService{
- accountRepo: accountRepo,
- tokenProvider: tokenProvider,
- rateLimitService: rateLimitService,
- httpUpstream: httpUpstream,
- settingService: settingService,
- cache: cache,
- schedulerSnapshot: schedulerSnapshot,
- internal500Cache: internal500Cache,
- }
- }
- // GetTokenProvider 返回 token provider
- func (s *AntigravityGatewayService) GetTokenProvider() *AntigravityTokenProvider {
- return s.tokenProvider
- }
- // getLogConfig 获取上游错误日志配置
- // 返回是否记录日志体和最大字节数
- func (s *AntigravityGatewayService) getLogConfig() (logBody bool, maxBytes int) {
- maxBytes = 2048 // 默认值
- if s.settingService == nil || s.settingService.cfg == nil {
- return false, maxBytes
- }
- cfg := s.settingService.cfg.Gateway
- if cfg.LogUpstreamErrorBodyMaxBytes > 0 {
- maxBytes = cfg.LogUpstreamErrorBodyMaxBytes
- }
- return cfg.LogUpstreamErrorBody, maxBytes
- }
- // getUpstreamErrorDetail 获取上游错误详情(用于日志记录)
- func (s *AntigravityGatewayService) getUpstreamErrorDetail(body []byte) string {
- logBody, maxBytes := s.getLogConfig()
- if !logBody {
- return ""
- }
- return truncateString(string(body), maxBytes)
- }
- // checkErrorPolicy nil 安全的包装
- func (s *AntigravityGatewayService) checkErrorPolicy(ctx context.Context, account *Account, statusCode int, body []byte, requestedModel ...string) ErrorPolicyResult {
- if s.rateLimitService == nil {
- return ErrorPolicyNone
- }
- return s.rateLimitService.CheckErrorPolicy(ctx, account, statusCode, body, firstRequestedModel(requestedModel))
- }
- // applyErrorPolicy 应用错误策略结果,返回是否应终止当前循环及应返回的状态码。
- // ErrorPolicySkipped 时 outStatus 为 500(前端约定:未命中的错误返回 500)。
- func (s *AntigravityGatewayService) applyErrorPolicy(p antigravityRetryLoopParams, statusCode int, headers http.Header, respBody []byte) (handled bool, outStatus int, retErr error) {
- modelKey := resolveFinalAntigravityModelKey(p.ctx, p.account, p.requestedModel)
- switch s.checkErrorPolicy(p.ctx, p.account, statusCode, respBody, modelKey) {
- case ErrorPolicySkipped:
- if s.handleAntigravityModelRateLimitBeforePolicy(p, statusCode, headers, respBody) {
- return true, statusCode, nil
- }
- return true, http.StatusInternalServerError, nil
- case ErrorPolicyMatched:
- if s.handleAntigravityModelRateLimitBeforePolicy(p, statusCode, headers, respBody) {
- return true, statusCode, nil
- }
- _ = p.handleError(p.ctx, p.prefix, p.account, statusCode, headers, respBody,
- p.requestedModel, p.groupID, p.sessionHash, p.isStickySession)
- return true, statusCode, nil
- case ErrorPolicyTempUnscheduled:
- slog.Info("temp_unschedulable_matched",
- "prefix", p.prefix, "status_code", statusCode, "account_id", p.account.ID)
- return true, statusCode, &AntigravityAccountSwitchError{OriginalAccountID: p.account.ID, RateLimitedModel: p.requestedModel, IsStickySession: p.isStickySession}
- }
- return false, statusCode, nil
- }
- func (s *AntigravityGatewayService) handleAntigravityModelRateLimitBeforePolicy(p antigravityRetryLoopParams, statusCode int, headers http.Header, respBody []byte) bool {
- if statusCode != http.StatusTooManyRequests && statusCode != http.StatusServiceUnavailable {
- return false
- }
- if p.account == nil || p.account.Platform != PlatformAntigravity {
- return false
- }
- _, shouldRateLimitModel, waitDuration, modelName, isModelCapacityExhausted := shouldTriggerAntigravitySmartRetry(p.account, respBody)
- if isModelCapacityExhausted || !shouldRateLimitModel || strings.TrimSpace(modelName) == "" {
- return false
- }
- rateLimitDuration := waitDuration
- if rateLimitDuration <= 0 {
- rateLimitDuration = antigravityDefaultRateLimitDuration
- }
- resetAt := time.Now().Add(rateLimitDuration)
- if !s.setAntigravityModelRateLimits(p.ctx, p.accountRepo, p.account, modelName, p.prefix, statusCode, resetAt, false) {
- return false
- }
- s.clearStickySession(p.ctx, p.groupID, p.sessionHash)
- logger.LegacyPrintf("service.antigravity_gateway", "%s status=%d model_rate_limited_before_error_policy model=%s account=%d reset_in=%v",
- p.prefix, statusCode, modelName, p.account.ID, rateLimitDuration)
- return true
- }
- // mapAntigravityModel 获取映射后的模型名
- // 完全依赖映射配置:账户映射(通配符)→ 默认映射兜底(DefaultAntigravityModelMapping)
- // 注意:返回空字符串表示模型不被支持,调度时会过滤掉该账号
- func mapAntigravityModel(account *Account, requestedModel string) string {
- if account == nil {
- return ""
- }
- requestedModel = strings.TrimPrefix(requestedModel, "models/")
- // 获取映射表(未配置时自动使用 DefaultAntigravityModelMapping)
- mapping := account.GetModelMapping()
- if len(mapping) == 0 {
- return "" // 无映射配置(非 Antigravity 平台)
- }
- // 通过映射表查询(支持精确匹配 + 通配符)
- mapped := account.GetMappedModel(requestedModel)
- // 判断是否映射成功(mapped != requestedModel 说明找到了映射规则)
- if mapped != requestedModel {
- return mapped
- }
- // 如果 mapped == requestedModel,检查是否在映射表中配置(精确或通配符)
- // 这区分两种情况:
- // 1. 映射表中有 "model-a": "model-a"(显式透传)→ 返回 model-a
- // 2. 通配符匹配 "claude-*": "claude-sonnet-4-5" 恰好目标等于请求名 → 返回 model-a
- // 3. 映射表中没有 model-a 的配置 → 返回空(不支持)
- if account.IsModelSupported(requestedModel) {
- return requestedModel
- }
- // 未在映射表中配置的模型,返回空字符串(不支持)
- return ""
- }
- // getMappedModel 获取映射后的模型名
- // 完全依赖映射配置:账户映射(通配符)→ 默认映射兜底
- func (s *AntigravityGatewayService) getMappedModel(account *Account, requestedModel string) string {
- return mapAntigravityModel(account, requestedModel)
- }
- func resolveAntigravityProjectID(account *Account) (string, error) {
- if account == nil {
- return "", errAntigravityProjectIDRequired
- }
- if projectID := strings.TrimSpace(account.GetCredential("project_id")); projectID != "" {
- return projectID, nil
- }
- if projectID := strings.TrimSpace(account.GetCredential(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
- return projectID, nil
- }
- if projectID := strings.TrimSpace(account.GetExtraString(antigravityProjectIDFallbackCredentialKey)); projectID != "" {
- return projectID, nil
- }
- return "", errAntigravityProjectIDRequired
- }
- // applyThinkingModelSuffix 根据 thinking 配置调整模型名
- // 当映射结果是 claude-sonnet-4-5 且请求开启了 thinking 时,改为 claude-sonnet-4-5-thinking
- func applyThinkingModelSuffix(mappedModel string, thinkingEnabled bool) string {
- if !thinkingEnabled {
- return mappedModel
- }
- if mappedModel == "claude-sonnet-4-5" {
- return "claude-sonnet-4-5-thinking"
- }
- return mappedModel
- }
- // IsModelSupported 检查模型是否被支持
- // 所有 claude- 和 gemini- 前缀的模型都能通过映射或透传支持
- func (s *AntigravityGatewayService) IsModelSupported(requestedModel string) bool {
- return strings.HasPrefix(requestedModel, "claude-") ||
- strings.HasPrefix(requestedModel, "gemini-")
- }
- // TestConnectionResult 测试连接结果
- type TestConnectionResult struct {
- Text string // 响应文本
- MappedModel string // 实际使用的模型
- }
- // TestConnection 测试 Antigravity 账号连接。
- // 复用 antigravityRetryLoop 的完整重试 / credits overages / 智能重试逻辑,
- // 与真实调度行为一致。差异:不做账号切换(测试指定账号)、不记录 ops 错误。
- func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account *Account, modelID string) (*TestConnectionResult, error) {
- // 获取 token
- if s.tokenProvider == nil {
- return nil, errors.New("antigravity token provider not configured")
- }
- accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
- if err != nil {
- return nil, fmt.Errorf("获取 access_token 失败: %w", err)
- }
- projectID, err := resolveAntigravityProjectID(account)
- if err != nil {
- return nil, err
- }
- // 模型映射
- mappedModel := s.getMappedModel(account, modelID)
- if mappedModel == "" {
- return nil, fmt.Errorf("model %s not in whitelist", modelID)
- }
- // 构建请求体
- var requestBody []byte
- if strings.HasPrefix(modelID, "gemini-") {
- requestBody, err = s.buildGeminiTestRequest(projectID, mappedModel)
- } else {
- requestBody, err = s.buildClaudeTestRequest(projectID, mappedModel)
- }
- if err != nil {
- return nil, fmt.Errorf("构建请求失败: %w", err)
- }
- if intelligentPrompt(ctx) != "" {
- requestBody, err = intelligentAntigravityBody(ctx, requestBody)
- if err != nil {
- return nil, err
- }
- }
- // 代理 URL
- proxyURL := ""
- if account.ProxyID != nil && account.Proxy != nil {
- proxyURL = account.Proxy.URL()
- }
- // 复用 antigravityRetryLoop:完整的重试 / credits overages / 智能重试
- prefix := fmt.Sprintf("[antigravity-Test] account=%d(%s)", account.ID, account.Name)
- p := antigravityRetryLoopParams{
- ctx: ctx,
- prefix: prefix,
- account: account,
- proxyURL: proxyURL,
- accessToken: accessToken,
- action: "streamGenerateContent",
- body: requestBody,
- c: nil, // 无 gin.Context → 跳过 ops 追踪
- httpUpstream: s.httpUpstream,
- settingService: s.settingService,
- accountRepo: s.accountRepo,
- requestedModel: modelID,
- handleError: testConnectionHandleError,
- }
- var result *antigravityRetryLoopResult
- if run := intelligentContext(ctx); run != nil {
- // Capability observations make one request. The production retry loop
- // may apply account error policies, change cache scheduling and enable
- // credit fallback; those side effects do not belong to a test result.
- var resp *http.Response
- resp, err = s.runIntelligentAntigravity(ctx, account, accessToken, requestBody)
- result = &antigravityRetryLoopResult{resp: resp}
- } else {
- result, err = s.antigravityRetryLoop(p)
- }
- if err != nil {
- // AccountSwitchError → 测试时不切换账号,返回友好提示
- var switchErr *AntigravityAccountSwitchError
- if errors.As(err, &switchErr) {
- return nil, fmt.Errorf("该账号模型 %s 当前限流中,请稍后重试", switchErr.RateLimitedModel)
- }
- return nil, err
- }
- if result == nil || result.resp == nil {
- return nil, errors.New("upstream returned empty response")
- }
- defer func() { _ = result.resp.Body.Close() }()
- respBody, err := io.ReadAll(io.LimitReader(result.resp.Body, s.upstreamErrorBodyReadLimit()))
- if err != nil {
- return nil, fmt.Errorf("读取响应失败: %w", err)
- }
- if result.resp.StatusCode >= 400 {
- return nil, fmt.Errorf("API 返回 %d: %s", result.resp.StatusCode, string(respBody))
- }
- text := extractTextFromSSEResponse(respBody)
- return &TestConnectionResult{Text: text, MappedModel: mappedModel}, nil
- }
- // testConnectionHandleError 是 TestConnection 使用的轻量 handleError 回调。
- // 仅记录日志,不做 ops 错误追踪或粘性会话清除。
- func testConnectionHandleError(
- _ context.Context, prefix string, account *Account,
- statusCode int, _ http.Header, body []byte,
- requestedModel string, _ int64, _ string, _ bool,
- ) *handleModelRateLimitResult {
- logger.LegacyPrintf("service.antigravity_gateway",
- "%s test_handle_error status=%d model=%s account=%d body=%s",
- prefix, statusCode, requestedModel, account.ID, truncateForLog(body, 200))
- return nil
- }
- // buildGeminiTestRequest 构建 Gemini 格式测试请求
- // 使用最小 token 消耗:输入 "." + maxOutputTokens: 1
- func (s *AntigravityGatewayService) buildGeminiTestRequest(projectID, model string) ([]byte, error) {
- payload := map[string]any{
- "contents": []map[string]any{
- {
- "role": "user",
- "parts": []map[string]any{
- {"text": "."},
- },
- },
- },
- // Antigravity 上游要求必须包含身份提示词
- "systemInstruction": map[string]any{
- "parts": []map[string]any{
- {"text": antigravity.GetDefaultIdentityPatch()},
- },
- },
- "generationConfig": map[string]any{
- "maxOutputTokens": 1,
- },
- }
- payloadBytes, _ := json.Marshal(payload)
- return s.wrapV1InternalRequest(projectID, model, payloadBytes)
- }
- // buildClaudeTestRequest 构建 Claude 格式测试请求并转换为 Gemini 格式
- // 使用最小 token 消耗:输入 "." + MaxTokens: 1
- func (s *AntigravityGatewayService) buildClaudeTestRequest(projectID, mappedModel string) ([]byte, error) {
- claudeReq := &antigravity.ClaudeRequest{
- Model: mappedModel,
- Messages: []antigravity.ClaudeMessage{
- {
- Role: "user",
- Content: json.RawMessage(`"."`),
- },
- },
- MaxTokens: 1,
- Stream: false,
- }
- return antigravity.TransformClaudeToGemini(claudeReq, projectID, mappedModel)
- }
- func (s *AntigravityGatewayService) getClaudeTransformOptions(ctx context.Context) antigravity.TransformOptions {
- opts := antigravity.DefaultTransformOptions()
- if s.settingService == nil {
- return opts
- }
- opts.EnableIdentityPatch = s.settingService.IsIdentityPatchEnabled(ctx)
- opts.IdentityPatch = s.settingService.GetIdentityPatchPrompt(ctx)
- return opts
- }
- // extractTextFromSSEResponse 从 SSE 流式响应中提取文本
- func extractTextFromSSEResponse(respBody []byte) string {
- var texts []string
- lines := bytes.Split(respBody, []byte("\n"))
- for _, line := range lines {
- line = bytes.TrimSpace(line)
- if len(line) == 0 {
- continue
- }
- // 跳过 SSE 前缀
- if bytes.HasPrefix(line, []byte("data:")) {
- line = bytes.TrimPrefix(line, []byte("data:"))
- line = bytes.TrimSpace(line)
- }
- // 跳过非 JSON 行
- if len(line) == 0 || line[0] != '{' {
- continue
- }
- // 解析 JSON
- var data map[string]any
- if err := json.Unmarshal(line, &data); err != nil {
- continue
- }
- // 尝试从 response.candidates[0].content.parts[].text 提取
- response, ok := data["response"].(map[string]any)
- if !ok {
- // 尝试直接从 candidates 提取(某些响应格式)
- response = data
- }
- candidates, ok := response["candidates"].([]any)
- if !ok || len(candidates) == 0 {
- continue
- }
- candidate, ok := candidates[0].(map[string]any)
- if !ok {
- continue
- }
- content, ok := candidate["content"].(map[string]any)
- if !ok {
- continue
- }
- parts, ok := content["parts"].([]any)
- if !ok {
- continue
- }
- for _, part := range parts {
- if partMap, ok := part.(map[string]any); ok {
- if text, ok := partMap["text"].(string); ok && text != "" {
- texts = append(texts, text)
- }
- }
- }
- }
- return strings.Join(texts, "")
- }
- // injectIdentityPatchToGeminiRequest 为 Gemini 格式请求注入身份提示词
- // 如果请求中已包含 "You are Antigravity" 则不重复注入
- func injectIdentityPatchToGeminiRequest(body []byte) ([]byte, error) {
- var request map[string]any
- if err := json.Unmarshal(body, &request); err != nil {
- return nil, fmt.Errorf("解析 Gemini 请求失败: %w", err)
- }
- // 检查现有 systemInstruction 是否已包含身份提示词
- if sysInst, ok := request["systemInstruction"].(map[string]any); ok {
- if parts, ok := sysInst["parts"].([]any); ok {
- for _, part := range parts {
- if partMap, ok := part.(map[string]any); ok {
- if text, ok := partMap["text"].(string); ok {
- if strings.Contains(text, "You are Antigravity") {
- // 已包含身份提示词,直接返回原始请求
- return body, nil
- }
- }
- }
- }
- }
- }
- // 获取默认身份提示词
- identityPatch := antigravity.GetDefaultIdentityPatch()
- // 构建新的 systemInstruction
- newPart := map[string]any{"text": identityPatch}
- if existing, ok := request["systemInstruction"].(map[string]any); ok {
- // 已有 systemInstruction,在开头插入身份提示词
- if parts, ok := existing["parts"].([]any); ok {
- existing["parts"] = append([]any{newPart}, parts...)
- } else {
- existing["parts"] = []any{newPart}
- }
- } else {
- // 没有 systemInstruction,创建新的
- request["systemInstruction"] = map[string]any{
- "parts": []any{newPart},
- }
- }
- return json.Marshal(request)
- }
- // wrapV1InternalRequest 包装请求为 v1internal 格式
- func (s *AntigravityGatewayService) wrapV1InternalRequest(projectID, model string, originalBody []byte) ([]byte, error) {
- var request any
- if err := json.Unmarshal(originalBody, &request); err != nil {
- return nil, fmt.Errorf("解析请求体失败: %w", err)
- }
- projectID = strings.TrimSpace(projectID)
- if projectID == "" {
- return nil, errAntigravityProjectIDRequired
- }
- wrapped := map[string]any{
- "project": projectID,
- "requestId": "agent-" + uuid.New().String(),
- "userAgent": "antigravity", // 固定值,与官方客户端一致
- "requestType": "agent",
- "model": model,
- "request": request,
- }
- return json.Marshal(wrapped)
- }
- // unwrapV1InternalResponse 解包 v1internal 响应
- // 使用 gjson 零拷贝提取 response 字段,避免 Unmarshal+Marshal 双重开销
- func (s *AntigravityGatewayService) unwrapV1InternalResponse(body []byte) ([]byte, error) {
- result := gjson.GetBytes(body, "response")
- if result.Exists() {
- return []byte(result.Raw), nil
- }
- return body, nil
- }
|