antigravity_gateway_gemini.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "strings"
  11. "time"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  14. "github.com/gin-gonic/gin"
  15. )
  16. // ForwardGemini 转发 Gemini 协议请求
  17. //
  18. // 限流处理流程:
  19. //
  20. // 请求 → antigravityRetryLoop → 预检查(remaining>0? → 切换账号) → 发送上游
  21. // ├─ 成功 → 正常返回
  22. // └─ 429/503 → handleSmartRetry
  23. // ├─ retryDelay >= 7s → 设置模型限流 + 清除粘性绑定 → 切换账号
  24. // └─ retryDelay < 7s → 等待后重试 1 次
  25. // ├─ 成功 → 正常返回
  26. // └─ 失败 → 设置模型限流 + 清除粘性绑定 → 切换账号
  27. type ForwardGeminiOption func(*forwardGeminiOptions)
  28. type forwardGeminiOptions struct {
  29. groupID int64
  30. sessionHash string
  31. }
  32. func WithForwardGeminiSession(groupID int64, sessionHash string) ForwardGeminiOption {
  33. return func(opts *forwardGeminiOptions) {
  34. opts.groupID = groupID
  35. opts.sessionHash = sessionHash
  36. }
  37. }
  38. func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Context, account *Account, originalModel string, action string, stream bool, body []byte, isStickySession bool, options ...ForwardGeminiOption) (*ForwardResult, error) {
  39. beginUpstreamResponseModelObservation(c)
  40. startTime := time.Now()
  41. forwardOpts := forwardGeminiOptions{}
  42. for _, apply := range options {
  43. if apply != nil {
  44. apply(&forwardOpts)
  45. }
  46. }
  47. sessionID := getSessionID(c)
  48. prefix := logPrefix(sessionID, account.Name)
  49. if strings.TrimSpace(originalModel) == "" {
  50. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing model in URL")
  51. }
  52. if strings.TrimSpace(action) == "" {
  53. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing action in URL")
  54. }
  55. if len(body) == 0 {
  56. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Request body is empty")
  57. }
  58. // 解析请求以获取 image_size(用于图片计费)
  59. imageInputSize := s.extractImageInputSize(body)
  60. imageSize := normalizeOpenAIImageSizeTier(imageInputSize)
  61. switch action {
  62. case "generateContent", "streamGenerateContent":
  63. // ok
  64. case "countTokens":
  65. // 直接返回空值,不透传上游
  66. c.JSON(http.StatusOK, map[string]any{"totalTokens": 0})
  67. return &ForwardResult{
  68. RequestID: "",
  69. Usage: ClaudeUsage{},
  70. Model: originalModel,
  71. Stream: false,
  72. Duration: time.Since(startTime),
  73. FirstTokenMs: nil,
  74. }, nil
  75. default:
  76. return nil, s.writeGoogleError(c, http.StatusNotFound, "Unsupported action: "+action)
  77. }
  78. mappedModel := s.getMappedModel(account, originalModel)
  79. if mappedModel == "" {
  80. MarkOpsClientBusinessLimited(c, OpsClientBusinessLimitedReasonLocalFeatureGate)
  81. return nil, s.writeGoogleError(c, http.StatusForbidden, fmt.Sprintf("model %s not in whitelist", originalModel))
  82. }
  83. forwardedModel := mappedModel
  84. // 获取 access_token
  85. if s.tokenProvider == nil {
  86. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Antigravity token provider not configured")
  87. }
  88. accessToken, err := s.tokenProvider.GetAccessToken(ctx, account)
  89. if err != nil {
  90. return nil, &UpstreamFailoverError{
  91. StatusCode: http.StatusBadGateway,
  92. ResponseBody: []byte(`{"error":{"message":"Failed to get upstream access token","status":"UNAVAILABLE"}}`),
  93. }
  94. }
  95. projectID, err := resolveAntigravityProjectID(account)
  96. if err != nil {
  97. _ = s.writeGoogleError(c, http.StatusBadRequest, err.Error())
  98. return nil, err
  99. }
  100. // 代理 URL
  101. proxyURL := ""
  102. if account.ProxyID != nil && account.Proxy != nil {
  103. proxyURL = account.Proxy.URL()
  104. }
  105. // Antigravity 上游要求必须包含身份提示词,注入到请求中
  106. injectedBody, err := injectIdentityPatchToGeminiRequest(body)
  107. if err != nil {
  108. return nil, s.writeGoogleError(c, http.StatusBadRequest, "Invalid request body")
  109. }
  110. // 清理 Schema
  111. if cleanedBody, err := cleanGeminiRequest(injectedBody); err == nil {
  112. injectedBody = cleanedBody
  113. logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Cleaned request schema in forwarded request for account %s", account.Name)
  114. } else {
  115. logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Failed to clean schema: %v", err)
  116. }
  117. // 包装请求
  118. wrappedBody, err := s.wrapV1InternalRequest(projectID, mappedModel, injectedBody)
  119. if err != nil {
  120. return nil, s.writeGoogleError(c, http.StatusInternalServerError, "Failed to build upstream request")
  121. }
  122. // Antigravity 上游只支持流式请求,统一使用 streamGenerateContent
  123. // 如果客户端请求非流式,在响应处理阶段会收集完整流式响应后返回
  124. upstreamAction := "streamGenerateContent"
  125. // 执行带重试的请求
  126. result, err := s.antigravityRetryLoop(antigravityRetryLoopParams{
  127. ctx: ctx,
  128. prefix: prefix,
  129. account: account,
  130. proxyURL: proxyURL,
  131. accessToken: accessToken,
  132. action: upstreamAction,
  133. body: wrappedBody,
  134. c: c,
  135. httpUpstream: s.httpUpstream,
  136. settingService: s.settingService,
  137. accountRepo: s.accountRepo,
  138. handleError: s.handleUpstreamError,
  139. requestedModel: originalModel,
  140. isStickySession: isStickySession, // ForwardGemini 由上层判断粘性会话
  141. groupID: forwardOpts.groupID,
  142. sessionHash: forwardOpts.sessionHash,
  143. })
  144. if err != nil {
  145. // 检查是否是账号切换信号,转换为 UpstreamFailoverError 让 Handler 切换账号
  146. if switchErr, ok := IsAntigravityAccountSwitchError(err); ok {
  147. return nil, &UpstreamFailoverError{
  148. StatusCode: http.StatusServiceUnavailable,
  149. ForceCacheBilling: switchErr.IsStickySession,
  150. }
  151. }
  152. // 区分客户端取消和真正的上游失败,返回更准确的错误消息
  153. if c.Request.Context().Err() != nil {
  154. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Client disconnected before upstream response")
  155. }
  156. return nil, s.writeGoogleError(c, http.StatusBadGateway, "Upstream request failed after retries")
  157. }
  158. resp := result.resp
  159. defer func() {
  160. if resp != nil && resp.Body != nil {
  161. _ = resp.Body.Close()
  162. }
  163. }()
  164. // 处理错误响应
  165. if resp.StatusCode >= 400 {
  166. respBody := s.readUpstreamErrorBody(resp)
  167. contentType := resp.Header.Get("Content-Type")
  168. // 尽早关闭原始响应体,释放连接;后续逻辑仍可能需要读取 body,因此用内存副本重新包装。
  169. _ = resp.Body.Close()
  170. resp.Body = io.NopCloser(bytes.NewReader(respBody))
  171. // 模型兜底:模型不存在且开启 fallback 时,自动用 fallback 模型重试一次
  172. if s.settingService != nil && s.settingService.IsModelFallbackEnabled(ctx) &&
  173. isModelNotFoundError(resp.StatusCode, respBody) {
  174. fallbackModel := s.settingService.GetFallbackModel(ctx, PlatformAntigravity)
  175. if fallbackModel != "" && fallbackModel != mappedModel {
  176. logger.LegacyPrintf("service.antigravity_gateway", "[Antigravity] Model not found (%s), retrying with fallback model %s (account: %s)", mappedModel, fallbackModel, account.Name)
  177. fallbackWrapped, err := s.wrapV1InternalRequest(projectID, fallbackModel, injectedBody)
  178. if err == nil {
  179. fallbackReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, fallbackWrapped)
  180. if err == nil {
  181. fallbackResp, err := s.httpUpstream.Do(WithAccountTrafficRequest(fallbackReq, account), proxyURL, account.ID, account.Concurrency)
  182. if err == nil && fallbackResp.StatusCode < 400 {
  183. _ = resp.Body.Close()
  184. resp = fallbackResp
  185. forwardedModel = fallbackModel
  186. } else if fallbackResp != nil {
  187. _ = fallbackResp.Body.Close()
  188. }
  189. }
  190. }
  191. }
  192. }
  193. // Gemini 原生请求中的 thoughtSignature 可能来自旧上下文/旧账号,触发上游严格校验后返回
  194. // "Corrupted thought signature."。检测到此类 400 时,将 thoughtSignature 清理为 dummy 值后重试一次。
  195. signatureCheckBody := respBody
  196. if unwrapped, unwrapErr := s.unwrapV1InternalResponse(respBody); unwrapErr == nil && len(unwrapped) > 0 {
  197. signatureCheckBody = unwrapped
  198. }
  199. if resp.StatusCode == http.StatusBadRequest &&
  200. s.settingService != nil &&
  201. s.settingService.IsSignatureRectifierEnabled(ctx) &&
  202. isSignatureRelatedError(signatureCheckBody) &&
  203. bytes.Contains(injectedBody, []byte(`"thoughtSignature"`)) {
  204. upstreamMsg := sanitizeUpstreamErrorMessage(strings.TrimSpace(extractAntigravityErrorMessage(signatureCheckBody)))
  205. upstreamDetail := s.getUpstreamErrorDetail(signatureCheckBody)
  206. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  207. ProxyID: opsUpstreamProxyID(account),
  208. ProxyName: opsUpstreamProxyName(account),
  209. Platform: account.Platform,
  210. AccountID: account.ID,
  211. AccountName: account.Name,
  212. UpstreamStatusCode: resp.StatusCode,
  213. UpstreamRequestID: resp.Header.Get("x-request-id"),
  214. Kind: "signature_error",
  215. Message: upstreamMsg,
  216. Detail: upstreamDetail,
  217. })
  218. logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: detected signature-related 400, retrying with cleaned thought signatures", account.ID)
  219. cleanedInjectedBody := CleanGeminiNativeThoughtSignatures(injectedBody)
  220. retryWrappedBody, wrapErr := s.wrapV1InternalRequest(projectID, mappedModel, cleanedInjectedBody)
  221. if wrapErr == nil {
  222. retryResult, retryErr := s.antigravityRetryLoop(antigravityRetryLoopParams{
  223. ctx: ctx,
  224. prefix: prefix,
  225. account: account,
  226. proxyURL: proxyURL,
  227. accessToken: accessToken,
  228. action: upstreamAction,
  229. body: retryWrappedBody,
  230. c: c,
  231. httpUpstream: s.httpUpstream,
  232. settingService: s.settingService,
  233. accountRepo: s.accountRepo,
  234. handleError: s.handleUpstreamError,
  235. requestedModel: originalModel,
  236. isStickySession: isStickySession,
  237. groupID: forwardOpts.groupID,
  238. sessionHash: forwardOpts.sessionHash,
  239. })
  240. if retryErr == nil {
  241. retryResp := retryResult.resp
  242. if retryResp.StatusCode < 400 {
  243. resp = retryResp
  244. } else {
  245. retryRespBody := s.readUpstreamErrorBody(retryResp)
  246. _ = retryResp.Body.Close()
  247. retryOpsBody := retryRespBody
  248. if retryUnwrapped, unwrapErr := s.unwrapV1InternalResponse(retryRespBody); unwrapErr == nil && len(retryUnwrapped) > 0 {
  249. retryOpsBody = retryUnwrapped
  250. }
  251. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  252. ProxyID: opsUpstreamProxyID(account),
  253. ProxyName: opsUpstreamProxyName(account),
  254. Platform: account.Platform,
  255. AccountID: account.ID,
  256. AccountName: account.Name,
  257. UpstreamStatusCode: retryResp.StatusCode,
  258. UpstreamRequestID: retryResp.Header.Get("x-request-id"),
  259. Kind: "signature_retry",
  260. Message: sanitizeUpstreamErrorMessage(strings.TrimSpace(extractAntigravityErrorMessage(retryOpsBody))),
  261. Detail: s.getUpstreamErrorDetail(retryOpsBody),
  262. })
  263. respBody = retryRespBody
  264. resp = &http.Response{
  265. StatusCode: retryResp.StatusCode,
  266. Header: retryResp.Header.Clone(),
  267. Body: io.NopCloser(bytes.NewReader(retryRespBody)),
  268. }
  269. contentType = resp.Header.Get("Content-Type")
  270. }
  271. } else {
  272. if switchErr, ok := IsAntigravityAccountSwitchError(retryErr); ok {
  273. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  274. ProxyID: opsUpstreamProxyID(account),
  275. ProxyName: opsUpstreamProxyName(account),
  276. Platform: account.Platform,
  277. AccountID: account.ID,
  278. AccountName: account.Name,
  279. UpstreamStatusCode: http.StatusServiceUnavailable,
  280. Kind: "failover",
  281. Message: sanitizeUpstreamErrorMessage(retryErr.Error()),
  282. })
  283. return nil, &UpstreamFailoverError{
  284. StatusCode: http.StatusServiceUnavailable,
  285. ForceCacheBilling: switchErr.IsStickySession,
  286. }
  287. }
  288. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  289. ProxyID: opsUpstreamProxyID(account),
  290. ProxyName: opsUpstreamProxyName(account),
  291. Platform: account.Platform,
  292. AccountID: account.ID,
  293. AccountName: account.Name,
  294. UpstreamStatusCode: 0,
  295. Kind: "signature_retry_request_error",
  296. Message: sanitizeUpstreamErrorMessage(retryErr.Error()),
  297. })
  298. logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: signature retry request failed: %v", account.ID, retryErr)
  299. }
  300. } else {
  301. logger.LegacyPrintf("service.antigravity_gateway", "Antigravity Gemini account %d: signature retry wrap failed: %v", account.ID, wrapErr)
  302. }
  303. }
  304. // fallback 成功:继续按正常响应处理
  305. if resp.StatusCode < 400 {
  306. goto handleSuccess
  307. }
  308. requestID := resp.Header.Get("x-request-id")
  309. if requestID != "" {
  310. c.Header("x-request-id", requestID)
  311. }
  312. unwrapped, unwrapErr := s.unwrapV1InternalResponse(respBody)
  313. unwrappedForOps := unwrapped
  314. if unwrapErr != nil || len(unwrappedForOps) == 0 {
  315. unwrappedForOps = respBody
  316. }
  317. s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody, originalModel, forwardOpts.groupID, forwardOpts.sessionHash, isStickySession)
  318. upstreamMsg := strings.TrimSpace(extractAntigravityErrorMessage(unwrappedForOps))
  319. upstreamMsg = sanitizeUpstreamErrorMessage(upstreamMsg)
  320. upstreamDetail := s.getUpstreamErrorDetail(unwrappedForOps)
  321. // Always record upstream context for Ops error logs, even when we will failover.
  322. setOpsUpstreamError(c, resp.StatusCode, upstreamMsg, upstreamDetail)
  323. // 精确匹配服务端配置类 400 错误,触发同账号重试 + failover
  324. if resp.StatusCode == http.StatusBadRequest && isGoogleProjectConfigError(strings.ToLower(upstreamMsg)) {
  325. log.Printf("%s status=400 google_config_error failover=true upstream_message=%q account=%d", prefix, upstreamMsg, account.ID)
  326. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  327. ProxyID: opsUpstreamProxyID(account),
  328. ProxyName: opsUpstreamProxyName(account),
  329. Platform: account.Platform,
  330. AccountID: account.ID,
  331. AccountName: account.Name,
  332. UpstreamStatusCode: resp.StatusCode,
  333. UpstreamRequestID: requestID,
  334. Kind: "failover",
  335. Message: upstreamMsg,
  336. Detail: upstreamDetail,
  337. })
  338. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: unwrappedForOps, RetryableOnSameAccount: true}
  339. }
  340. if s.shouldFailoverUpstreamError(resp.StatusCode) {
  341. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  342. ProxyID: opsUpstreamProxyID(account),
  343. ProxyName: opsUpstreamProxyName(account),
  344. Platform: account.Platform,
  345. AccountID: account.ID,
  346. AccountName: account.Name,
  347. UpstreamStatusCode: resp.StatusCode,
  348. UpstreamRequestID: requestID,
  349. Kind: "failover",
  350. Message: upstreamMsg,
  351. Detail: upstreamDetail,
  352. })
  353. return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode, ResponseBody: unwrappedForOps}
  354. }
  355. if contentType == "" {
  356. contentType = "application/json"
  357. }
  358. appendOpsUpstreamError(c, OpsUpstreamErrorEvent{
  359. ProxyID: opsUpstreamProxyID(account),
  360. ProxyName: opsUpstreamProxyName(account),
  361. Platform: account.Platform,
  362. AccountID: account.ID,
  363. AccountName: account.Name,
  364. UpstreamStatusCode: resp.StatusCode,
  365. UpstreamRequestID: requestID,
  366. Kind: "http_error",
  367. Message: upstreamMsg,
  368. Detail: upstreamDetail,
  369. })
  370. logger.LegacyPrintf("service.antigravity_gateway", "[antigravity-Forward] upstream error status=%d body=%s", resp.StatusCode, truncateForLog(unwrappedForOps, 500))
  371. MarkResponseCommitted(c)
  372. c.Data(resp.StatusCode, contentType, unwrappedForOps)
  373. return nil, fmt.Errorf("antigravity upstream error: %d", resp.StatusCode)
  374. }
  375. handleSuccess:
  376. requestID := resp.Header.Get("x-request-id")
  377. if requestID != "" {
  378. c.Header("x-request-id", requestID)
  379. }
  380. var usage *ClaudeUsage
  381. var firstTokenMs *int
  382. var clientDisconnect bool
  383. if stream {
  384. // 客户端要求流式,直接透传
  385. streamRes, err := s.handleGeminiStreamingResponse(c, resp, startTime)
  386. if err != nil {
  387. logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_error error=%v", prefix, err)
  388. return nil, err
  389. }
  390. usage = streamRes.usage
  391. firstTokenMs = streamRes.firstTokenMs
  392. clientDisconnect = streamRes.clientDisconnect
  393. } else {
  394. // 客户端要求非流式,收集流式响应后返回
  395. streamRes, err := s.handleGeminiStreamToNonStreaming(c, resp, startTime)
  396. if err != nil {
  397. logger.LegacyPrintf("service.antigravity_gateway", "%s status=stream_collect_error error=%v", prefix, err)
  398. return nil, err
  399. }
  400. usage = streamRes.usage
  401. firstTokenMs = streamRes.firstTokenMs
  402. }
  403. if usage == nil {
  404. usage = &ClaudeUsage{}
  405. }
  406. // 判断是否为图片生成模型
  407. imageCount := 0
  408. if isImageGenerationModel(mappedModel) {
  409. // Gemini 图片生成 API 每次请求只生成一张图片(API 限制)
  410. imageCount = 1
  411. }
  412. return &ForwardResult{
  413. RequestID: requestID,
  414. UpstreamHeaders: resp.Header,
  415. Usage: *usage,
  416. Model: originalModel,
  417. UpstreamModel: forwardedModel,
  418. UpstreamResponseModel: observedUpstreamResponseModel(c),
  419. UpstreamResponseModelConflict: observedUpstreamResponseModelConflict(c),
  420. Stream: stream,
  421. Duration: time.Since(startTime),
  422. FirstTokenMs: firstTokenMs,
  423. ClientDisconnect: clientDisconnect,
  424. ImageCount: imageCount,
  425. ImageSize: imageSize,
  426. ImageInputSize: imageInputSize,
  427. }, nil
  428. }
  429. // cleanGeminiRequest 清理 Gemini 请求体中的 Schema
  430. func cleanGeminiRequest(body []byte) ([]byte, error) {
  431. var payload map[string]any
  432. if err := json.Unmarshal(body, &payload); err != nil {
  433. return nil, err
  434. }
  435. modified := false
  436. // 1. 清理 Tools
  437. if tools, ok := payload["tools"].([]any); ok && len(tools) > 0 {
  438. for _, t := range tools {
  439. toolMap, ok := t.(map[string]any)
  440. if !ok {
  441. continue
  442. }
  443. // function_declarations (snake_case) or functionDeclarations (camelCase)
  444. var funcs []any
  445. if f, ok := toolMap["functionDeclarations"].([]any); ok {
  446. funcs = f
  447. } else if f, ok := toolMap["function_declarations"].([]any); ok {
  448. funcs = f
  449. }
  450. if len(funcs) == 0 {
  451. continue
  452. }
  453. for _, f := range funcs {
  454. funcMap, ok := f.(map[string]any)
  455. if !ok {
  456. continue
  457. }
  458. if params, ok := funcMap["parameters"].(map[string]any); ok {
  459. antigravity.DeepCleanUndefined(params)
  460. cleaned := antigravity.CleanJSONSchema(params)
  461. funcMap["parameters"] = cleaned
  462. modified = true
  463. }
  464. }
  465. }
  466. }
  467. if !modified {
  468. return body, nil
  469. }
  470. return json.Marshal(payload)
  471. }
  472. // filterEmptyPartsFromGeminiRequest 过滤掉 parts 为空的消息
  473. // Gemini API 不接受空 parts,需要在请求前过滤
  474. func filterEmptyPartsFromGeminiRequest(body []byte) ([]byte, error) {
  475. var payload map[string]any
  476. if err := json.Unmarshal(body, &payload); err != nil {
  477. return nil, err
  478. }
  479. contents, ok := payload["contents"].([]any)
  480. if !ok || len(contents) == 0 {
  481. return body, nil
  482. }
  483. filtered := make([]any, 0, len(contents))
  484. modified := false
  485. for _, c := range contents {
  486. contentMap, ok := c.(map[string]any)
  487. if !ok {
  488. filtered = append(filtered, c)
  489. continue
  490. }
  491. parts, hasParts := contentMap["parts"]
  492. if !hasParts {
  493. filtered = append(filtered, c)
  494. continue
  495. }
  496. partsSlice, ok := parts.([]any)
  497. if !ok {
  498. filtered = append(filtered, c)
  499. continue
  500. }
  501. // 跳过 parts 为空数组的消息
  502. if len(partsSlice) == 0 {
  503. modified = true
  504. continue
  505. }
  506. filtered = append(filtered, c)
  507. }
  508. if !modified {
  509. return body, nil
  510. }
  511. payload["contents"] = filtered
  512. return json.Marshal(payload)
  513. }