From fb9930004c14ddbe2804a3cccabac29769f97b88 Mon Sep 17 00:00:00 2001 From: song Date: Fri, 2 Jan 2026 18:04:11 +0800 Subject: [PATCH 01/14] =?UTF-8?q?ci:=20DockerHub=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=AF=E9=80=89=EF=BC=8C=E6=9C=AA=E9=85=8D=E7=BD=AE=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B7=B3=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 24 ++++++++++++++++++------ .goreleaser.yaml | 10 ++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d20ed0c8..bc0959d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,10 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub + if: ${{ env.DOCKERHUB_USERNAME != '' }} uses: docker/login-action@v3 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -144,11 +147,14 @@ jobs: GITHUB_REPO_OWNER: ${{ github.repository_owner }} GITHUB_REPO_OWNER_LOWER: ${{ steps.lowercase.outputs.owner }} GITHUB_REPO_NAME: ${{ github.event.repository.name }} - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'skip' }} # Update DockerHub description - name: Update DockerHub description + if: ${{ env.DOCKERHUB_USERNAME != '' }} uses: peter-evans/dockerhub-description@v4 + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -161,6 +167,7 @@ jobs: env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} continue-on-error: true run: | # 检查必要的环境变量 @@ -172,7 +179,6 @@ jobs: TAG_NAME=${GITHUB_REF#refs/tags/} VERSION=${TAG_NAME#v} REPO="${{ github.repository }}" - DOCKER_IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/sub2api" GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase # 获取 tag message 内容 @@ -194,14 +200,20 @@ jobs: MESSAGE+="🐳 *Docker 部署:*"$'\n' MESSAGE+="\`\`\`bash"$'\n' - MESSAGE+="# Docker Hub"$'\n' - MESSAGE+="docker pull ${DOCKER_IMAGE}:${TAG_NAME}"$'\n' - MESSAGE+="# GitHub Container Registry"$'\n' + # 根据是否配置 DockerHub 动态生成 + if [ -n "$DOCKERHUB_USERNAME" ]; then + DOCKER_IMAGE="${DOCKERHUB_USERNAME}/sub2api" + MESSAGE+="# Docker Hub"$'\n' + MESSAGE+="docker pull ${DOCKER_IMAGE}:${TAG_NAME}"$'\n' + MESSAGE+="# GitHub Container Registry"$'\n' + fi MESSAGE+="docker pull ${GHCR_IMAGE}:${TAG_NAME}"$'\n' MESSAGE+="\`\`\`"$'\n'$'\n' MESSAGE+="🔗 *相关链接:*"$'\n' MESSAGE+="• [GitHub Release](https://github.com/${REPO}/releases/tag/${TAG_NAME})"$'\n' - MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n' + if [ -n "$DOCKERHUB_USERNAME" ]; then + MESSAGE+="• [Docker Hub](https://hub.docker.com/r/${DOCKER_IMAGE})"$'\n' + fi MESSAGE+="• [GitHub Packages](https://github.com/${REPO}/pkgs/container/sub2api)"$'\n'$'\n' MESSAGE+="#Sub2API #Release #${TAG_NAME//./_}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml index c72f7422..da2f9aa5 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -54,9 +54,11 @@ changelog: # Docker images dockers: + # DockerHub images (skipped if DOCKERHUB_USERNAME is 'skip') - id: amd64 goos: linux goarch: amd64 + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" dockerfile: Dockerfile.goreleaser @@ -69,6 +71,7 @@ dockers: - id: arm64 goos: linux goarch: arm64 + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" dockerfile: Dockerfile.goreleaser @@ -107,22 +110,27 @@ dockers: # Docker manifests for multi-arch support docker_manifests: + # DockerHub manifests (skipped if DOCKERHUB_USERNAME is 'skip') - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}" + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:latest" + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Major }}.{{ .Minor }}" + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" - name_template: "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Major }}" + skip_push: '{{ if eq .Env.DOCKERHUB_USERNAME "skip" }}true{{ else }}false{{ end }}' image_templates: - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-amd64" - "{{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }}-arm64" @@ -169,9 +177,11 @@ release: **Docker:** ```bash + {{ if ne .Env.DOCKERHUB_USERNAME "skip" -}} # Docker Hub docker pull {{ .Env.DOCKERHUB_USERNAME }}/sub2api:{{ .Version }} + {{ end -}} # GitHub Container Registry docker pull ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }} ``` From c8e55ab2ac7ce51409d09071156fd153d526c81d Mon Sep 17 00:00:00 2001 From: song Date: Fri, 2 Jan 2026 11:44:32 +0800 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=20antigravity?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E4=B8=AD=E7=9A=84=20[Debug]=20=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这些调试日志不应在生产环境中输出。 --- .../pkg/antigravity/request_transformer.go | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index d662be0e..fbaedd3d 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -172,6 +172,31 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT // 参考: https://ai.google.dev/gemini-api/docs/thought-signatures const dummyThoughtSignature = "skip_thought_signature_validator" +// isValidThoughtSignature 验证 thought signature 是否有效 +// Claude API 要求 signature 必须是 base64 编码的字符串,长度至少 32 字节 +func isValidThoughtSignature(signature string) bool { + // 空字符串无效 + if signature == "" { + return false + } + + // signature 应该是 base64 编码,长度至少 40 个字符(约 30 字节) + // 参考 Claude API 文档和实际观察到的有效 signature + if len(signature) < 40 { + return false + } + + // 检查是否是有效的 base64 字符 + // base64 字符集: A-Z, a-z, 0-9, +, /, = + for _, c := range signature { + if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && + (c < '0' || c > '9') && c != '+' && c != '/' && c != '=' { + return false + } + } + + return true +} // buildParts 构建消息的 parts // allowDummyThought: 只有 Gemini 模型支持 dummy thought signature func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDummyThought bool) ([]GeminiPart, error) { @@ -200,22 +225,17 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDu } case "thinking": - part := GeminiPart{ - Text: block.Thinking, - Thought: true, - } - // 保留原有 signature(Claude 模型需要有效的 signature) - if block.Signature != "" { - part.ThoughtSignature = block.Signature - } else if !allowDummyThought { - // Claude 模型需要有效 signature,跳过无 signature 的 thinking block - log.Printf("Warning: skipping thinking block without signature for Claude model") + // Claude 模型:仅在提供有效 signature 时保留 thinking block;否则跳过以避免上游校验失败。 + signature := strings.TrimSpace(block.Signature) + if signature == "" || signature == dummyThoughtSignature { + log.Printf("[Warning] Skipping thinking block for Claude model (missing or dummy signature)") continue - } else { - // Gemini 模型使用 dummy signature - part.ThoughtSignature = dummyThoughtSignature } - parts = append(parts, part) + parts = append(parts, GeminiPart{ + Text: block.Thinking, + Thought: true, + ThoughtSignature: signature, + }) case "image": if block.Source != nil && block.Source.Type == "base64" { From 0dc4b113d87cba6cf4cf7a3500174e9c0dda4fc0 Mon Sep 17 00:00:00 2001 From: song Date: Sat, 3 Jan 2026 19:27:46 +0800 Subject: [PATCH 03/14] =?UTF-8?q?fix(antigravity):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=BD=AC=E5=8F=91=E6=97=A5=E5=BF=97=E6=A0=BC=E5=BC=8F=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20session=5Fid=20=E8=BF=BD=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/antigravity_gateway_service.go | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index e4843f1b..05ac96d3 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -25,6 +25,22 @@ const ( antigravityRetryMaxDelay = 16 * time.Second ) +// getSessionID 从 gin.Context 获取 session_id(用于日志追踪) +func getSessionID(c *gin.Context) string { + if c == nil { + return "" + } + return c.GetHeader("session_id") +} + +// logPrefix 生成统一的日志前缀 +func logPrefix(sessionID, accountName string) string { + if sessionID != "" { + return fmt.Sprintf("[antigravity-Forward] session=%s account=%s", sessionID, accountName) + } + return fmt.Sprintf("[antigravity-Forward] account=%s", accountName) +} + // Antigravity 直接支持的模型(精确匹配透传) var antigravitySupportedModels = map[string]bool{ "claude-opus-4-5-thinking": true, @@ -310,6 +326,8 @@ func (s *AntigravityGatewayService) unwrapV1InternalResponse(body []byte) ([]byt // Forward 转发 Claude 协议请求(Claude → Gemini 转换) func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) { startTime := time.Now() + sessionID := getSessionID(c) + prefix := logPrefix(sessionID, account.Name) // 解析 Claude 请求 var claudeReq antigravity.ClaudeRequest @@ -364,10 +382,11 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, resp, err = s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency) if err != nil { if attempt < antigravityMaxRetries { - log.Printf("Antigravity account %d: upstream request failed, retry %d/%d: %v", account.ID, attempt, antigravityMaxRetries, err) + log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err) sleepAntigravityBackoff(attempt) continue } + log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err) return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Upstream request failed after retries") } @@ -376,13 +395,13 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, _ = resp.Body.Close() if attempt < antigravityMaxRetries { - log.Printf("Antigravity account %d: upstream status %d, retry %d/%d", account.ID, resp.StatusCode, attempt, antigravityMaxRetries) + log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries) sleepAntigravityBackoff(attempt) continue } // 所有重试都失败,标记限流状态 if resp.StatusCode == 429 { - s.handleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody) + s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody) } // 最后一次尝试也失败 resp = &http.Response{ @@ -400,7 +419,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, // 处理错误响应 if resp.StatusCode >= 400 { respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) - s.handleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody) + s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody) if s.shouldFailoverUpstreamError(resp.StatusCode) { return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode} @@ -443,6 +462,8 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, // ForwardGemini 转发 Gemini 协议请求 func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Context, account *Account, originalModel string, action string, stream bool, body []byte) (*ForwardResult, error) { startTime := time.Now() + sessionID := getSessionID(c) + prefix := logPrefix(sessionID, account.Name) if strings.TrimSpace(originalModel) == "" { return nil, s.writeGoogleError(c, http.StatusBadRequest, "Missing model in URL") @@ -518,10 +539,11 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co resp, err = s.httpUpstream.Do(upstreamReq, proxyURL, account.ID, account.Concurrency) if err != nil { if attempt < antigravityMaxRetries { - log.Printf("Antigravity account %d: upstream request failed, retry %d/%d: %v", account.ID, attempt, antigravityMaxRetries, err) + log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err) sleepAntigravityBackoff(attempt) continue } + log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err) return nil, s.writeGoogleError(c, http.StatusBadGateway, "Upstream request failed after retries") } @@ -530,13 +552,13 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co _ = resp.Body.Close() if attempt < antigravityMaxRetries { - log.Printf("Antigravity account %d: upstream status %d, retry %d/%d", account.ID, resp.StatusCode, attempt, antigravityMaxRetries) + log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries) sleepAntigravityBackoff(attempt) continue } // 所有重试都失败,标记限流状态 if resp.StatusCode == 429 { - s.handleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody) + s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody) } resp = &http.Response{ StatusCode: resp.StatusCode, @@ -558,7 +580,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co // 处理错误响应 if resp.StatusCode >= 400 { respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) - s.handleUpstreamError(ctx, account, resp.StatusCode, resp.Header, respBody) + s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody) if s.shouldFailoverUpstreamError(resp.StatusCode) { return nil, &UpstreamFailoverError{StatusCode: resp.StatusCode} @@ -628,7 +650,7 @@ func sleepAntigravityBackoff(attempt int) { sleepGeminiBackoff(attempt) // 复用 Gemini 的退避逻辑 } -func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, account *Account, statusCode int, headers http.Header, body []byte) { +func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte) { // 429 使用 Gemini 格式解析(从 body 解析重置时间) if statusCode == 429 { resetAt := ParseGeminiRateLimitResetTime(body) @@ -639,17 +661,23 @@ func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, acc defaultDur = 5 * time.Minute } ra := time.Now().Add(defaultDur) + log.Printf("%s status=429 rate_limited reset_in=%v (fallback)", prefix, defaultDur) _ = s.accountRepo.SetRateLimited(ctx, account.ID, ra) return } - _ = s.accountRepo.SetRateLimited(ctx, account.ID, time.Unix(*resetAt, 0)) + resetTime := time.Unix(*resetAt, 0) + log.Printf("%s status=429 rate_limited reset_at=%v reset_in=%v", prefix, resetTime.Format("15:04:05"), time.Until(resetTime).Truncate(time.Second)) + _ = s.accountRepo.SetRateLimited(ctx, account.ID, resetTime) return } // 其他错误码继续使用 rateLimitService if s.rateLimitService == nil { return } - s.rateLimitService.HandleUpstreamError(ctx, account, statusCode, headers, body) + shouldDisable := s.rateLimitService.HandleUpstreamError(ctx, account, statusCode, headers, body) + if shouldDisable { + log.Printf("%s status=%d marked_error", prefix, statusCode) + } } type antigravityStreamResult struct { @@ -758,7 +786,7 @@ func (s *AntigravityGatewayService) writeClaudeError(c *gin.Context, status int, func (s *AntigravityGatewayService) writeMappedClaudeError(c *gin.Context, upstreamStatus int, body []byte) error { // 记录上游错误详情便于调试 - log.Printf("Antigravity upstream error %d: %s", upstreamStatus, string(body)) + log.Printf("[antigravity-Forward] upstream_error status=%d body=%s", upstreamStatus, string(body)) var statusCode int var errType, errMsg string @@ -832,7 +860,7 @@ func (s *AntigravityGatewayService) handleClaudeNonStreamingResponse(c *gin.Cont // 转换 Gemini 响应为 Claude 格式 claudeResp, agUsage, err := antigravity.TransformGeminiToClaude(body, originalModel) if err != nil { - log.Printf("Transform Gemini to Claude failed: %v, body: %s", err, string(body)) + log.Printf("[antigravity-Forward] transform_error error=%v body=%s", err, string(body)) return nil, s.writeClaudeError(c, http.StatusBadGateway, "upstream_error", "Failed to parse upstream response") } From c0e296f4a90ba72b586a8d4ed4d05de318400d9f Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 09:36:21 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat(ci):=20=E5=A2=9E=E5=8A=A0=20SIMPLE?= =?UTF-8?q?=5FRELEASE=20=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 40 ++++++++++++++-- .goreleaser.simple.yaml | 86 +++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 5 deletions(-) create mode 100644 .goreleaser.simple.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc0959d5..cdb5812c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,17 @@ on: push: tags: - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., v1.0.0)' + required: true + type: string + simple_release: + description: 'Simple release: only x86_64 GHCR image, skip other artifacts' + required: false + type: boolean + default: false permissions: contents: write @@ -19,7 +30,12 @@ jobs: - name: Update VERSION file run: | - VERSION=${GITHUB_REF#refs/tags/v} + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION=${{ github.event.inputs.tag }} + VERSION=${VERSION#v} + else + VERSION=${GITHUB_REF#refs/tags/v} + fi echo "$VERSION" > backend/cmd/server/VERSION echo "Updated VERSION file to: $VERSION" @@ -32,6 +48,7 @@ jobs: build-frontend: runs-on: ubuntu-latest + if: ${{ github.event.inputs.simple_release != 'true' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -60,12 +77,15 @@ jobs: release: needs: [update-version, build-frontend] + # 等待 build-frontend 完成(除非是 simple_release 则跳过检查) + if: ${{ always() && needs.update-version.result == 'success' && (github.event.inputs.simple_release == 'true' || needs.build-frontend.result == 'success') }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 + ref: ${{ github.event.inputs.tag || github.ref }} - name: Download VERSION artifact uses: actions/download-artifact@v4 @@ -74,6 +94,7 @@ jobs: path: backend/cmd/server/ - name: Download frontend artifact + if: ${{ github.event.inputs.simple_release != 'true' }} uses: actions/download-artifact@v4 with: name: frontend-dist @@ -116,7 +137,11 @@ jobs: - name: Get tag message id: tag_message run: | - TAG_NAME=${GITHUB_REF#refs/tags/} + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_NAME=${{ github.event.inputs.tag }} + else + TAG_NAME=${GITHUB_REF#refs/tags/} + fi echo "Processing tag: $TAG_NAME" # 获取完整的 tag message(跳过第一行标题) @@ -140,7 +165,7 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: version: '~> v2' - args: release --clean --skip=validate + args: release --clean --skip=validate ${{ github.event.inputs.simple_release == 'true' && '--config=.goreleaser.simple.yaml' || '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_MESSAGE: ${{ steps.tag_message.outputs.message }} @@ -151,7 +176,7 @@ jobs: # Update DockerHub description - name: Update DockerHub description - if: ${{ env.DOCKERHUB_USERNAME != '' }} + if: ${{ github.event.inputs.simple_release != 'true' && env.DOCKERHUB_USERNAME != '' }} uses: peter-evans/dockerhub-description@v4 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} @@ -164,6 +189,7 @@ jobs: # Send Telegram notification - name: Send Telegram Notification + if: ${{ github.event.inputs.simple_release != 'true' }} env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} @@ -176,7 +202,11 @@ jobs: exit 0 fi - TAG_NAME=${GITHUB_REF#refs/tags/} + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG_NAME=${{ github.event.inputs.tag }} + else + TAG_NAME=${GITHUB_REF#refs/tags/} + fi VERSION=${TAG_NAME#v} REPO="${{ github.repository }}" GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase diff --git a/.goreleaser.simple.yaml b/.goreleaser.simple.yaml new file mode 100644 index 00000000..2155ed9d --- /dev/null +++ b/.goreleaser.simple.yaml @@ -0,0 +1,86 @@ +# 简化版 GoReleaser 配置 - 仅发布 x86_64 GHCR 镜像 +version: 2 + +project_name: sub2api + +before: + hooks: + - go mod tidy -C backend + +builds: + - id: sub2api + dir: backend + main: ./cmd/server + binary: sub2api + flags: + - -tags=embed + env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + ldflags: + - -s -w + - -X main.Commit={{.Commit}} + - -X main.Date={{.Date}} + - -X main.BuildType=release + +# 跳过 archives +archives: [] + +# 跳过 checksum +checksum: + disable: true + +changelog: + disable: true + +# 仅 GHCR x86_64 镜像 +dockers: + - id: ghcr-amd64 + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}-amd64" + - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }}" + - "ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:latest" + dockerfile: Dockerfile.goreleaser + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.revision={{ .Commit }}" + - "--label=org.opencontainers.image.source=https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}" + +# 跳过 manifests(单架构不需要) +docker_manifests: [] + +release: + github: + owner: "{{ .Env.GITHUB_REPO_OWNER }}" + name: "{{ .Env.GITHUB_REPO_NAME }}" + draft: false + prerelease: auto + name_template: "Sub2API {{.Version}} (Simple)" + # 跳过上传二进制包 + skip_upload: true + header: | + > AI API Gateway Platform - 将 AI 订阅配额分发和管理 + > ⚡ Simple Release: 仅包含 x86_64 GHCR 镜像 + + {{ .Env.TAG_MESSAGE }} + + footer: | + --- + + ## 📥 Installation + + **Docker (x86_64 only):** + ```bash + docker pull ghcr.io/{{ .Env.GITHUB_REPO_OWNER_LOWER }}/sub2api:{{ .Version }} + ``` + + ## 📚 Documentation + + - [GitHub Repository](https://github.com/{{ .Env.GITHUB_REPO_OWNER }}/{{ .Env.GITHUB_REPO_NAME }}) From 84d6480b4e994162a6ce7caebd03c8dd516bda36 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 09:46:01 +0800 Subject: [PATCH 05/14] =?UTF-8?q?fix(ci):=20simple=20release=20=E4=B8=8D?= =?UTF-8?q?=E5=B5=8C=E5=85=A5=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .goreleaser.simple.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.goreleaser.simple.yaml b/.goreleaser.simple.yaml index 2155ed9d..1846d386 100644 --- a/.goreleaser.simple.yaml +++ b/.goreleaser.simple.yaml @@ -12,8 +12,9 @@ builds: dir: backend main: ./cmd/server binary: sub2api - flags: - - -tags=embed + # 不嵌入前端,使用独立部署模式 + # flags: + # - -tags=embed env: - CGO_ENABLED=0 goos: From e91fba82a8fa4073e09c8ec587504ad0ba0be8e2 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 09:47:22 +0800 Subject: [PATCH 06/14] =?UTF-8?q?fix(ci):=20simple=20release=20=E4=B9=9F?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 4 ---- .goreleaser.simple.yaml | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdb5812c..696adbf0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,6 @@ jobs: build-frontend: runs-on: ubuntu-latest - if: ${{ github.event.inputs.simple_release != 'true' }} steps: - name: Checkout uses: actions/checkout@v4 @@ -77,8 +76,6 @@ jobs: release: needs: [update-version, build-frontend] - # 等待 build-frontend 完成(除非是 simple_release 则跳过检查) - if: ${{ always() && needs.update-version.result == 'success' && (github.event.inputs.simple_release == 'true' || needs.build-frontend.result == 'success') }} runs-on: ubuntu-latest steps: - name: Checkout @@ -94,7 +91,6 @@ jobs: path: backend/cmd/server/ - name: Download frontend artifact - if: ${{ github.event.inputs.simple_release != 'true' }} uses: actions/download-artifact@v4 with: name: frontend-dist diff --git a/.goreleaser.simple.yaml b/.goreleaser.simple.yaml index 1846d386..2155ed9d 100644 --- a/.goreleaser.simple.yaml +++ b/.goreleaser.simple.yaml @@ -12,9 +12,8 @@ builds: dir: backend main: ./cmd/server binary: sub2api - # 不嵌入前端,使用独立部署模式 - # flags: - # - -tags=embed + flags: + - -tags=embed env: - CGO_ENABLED=0 goos: From 44785a9a8c8e966b32dc3e1ca1ce7d72318f2a5b Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 09:53:46 +0800 Subject: [PATCH 07/14] =?UTF-8?q?feat(ci):=20=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E8=BF=87=20repository=20variable=20=E6=8E=A7=E5=88=B6=20SIMPLE?= =?UTF-8?q?=5FRELEASE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 696adbf0..1dc2278e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,11 @@ on: type: boolean default: false +# 环境变量:合并 workflow_dispatch 输入和 repository variable +# tag push 触发时读取 vars.SIMPLE_RELEASE,workflow_dispatch 时使用输入参数 +env: + SIMPLE_RELEASE: ${{ github.event.inputs.simple_release == 'true' || vars.SIMPLE_RELEASE == 'true' }} + permissions: contents: write packages: write @@ -161,7 +166,7 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: version: '~> v2' - args: release --clean --skip=validate ${{ github.event.inputs.simple_release == 'true' && '--config=.goreleaser.simple.yaml' || '' }} + args: release --clean --skip=validate ${{ env.SIMPLE_RELEASE == 'true' && '--config=.goreleaser.simple.yaml' || '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_MESSAGE: ${{ steps.tag_message.outputs.message }} @@ -172,7 +177,7 @@ jobs: # Update DockerHub description - name: Update DockerHub description - if: ${{ github.event.inputs.simple_release != 'true' && env.DOCKERHUB_USERNAME != '' }} + if: ${{ env.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }} uses: peter-evans/dockerhub-description@v4 env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} @@ -185,7 +190,7 @@ jobs: # Send Telegram notification - name: Send Telegram Notification - if: ${{ github.event.inputs.simple_release != 'true' }} + if: ${{ env.SIMPLE_RELEASE != 'true' }} env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} From 1dd3521190117a8b8240597e8425d8c3ae6f5c59 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 11:49:34 +0800 Subject: [PATCH 08/14] =?UTF-8?q?fix(antigravity):=20=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?token=20=E5=88=B7=E6=96=B0=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 不可重试错误(invalid_grant等)直接标记 error,不重试 - 其他错误仅记录日志,不标记 error(可能是临时网络问题) - 仅影响 Antigravity 账户,其他平台保持原有逻辑 --- .../internal/service/token_refresh_service.go | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index 76ca61fd..3ed35f04 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "sync" "time" @@ -171,6 +172,15 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc return nil } + // Antigravity 账户:不可重试错误直接标记 error 状态并返回 + if account.Platform == PlatformAntigravity && isNonRetryableRefreshError(err) { + errorMsg := fmt.Sprintf("Token refresh failed (non-retryable): %v", err) + if setErr := s.accountRepo.SetError(ctx, account.ID, errorMsg); setErr != nil { + log.Printf("[TokenRefresh] Failed to set error status for account %d: %v", account.ID, setErr) + } + return err + } + lastErr = err log.Printf("[TokenRefresh] Account %d attempt %d/%d failed: %v", account.ID, attempt, s.cfg.MaxRetries, err) @@ -183,11 +193,37 @@ func (s *TokenRefreshService) refreshWithRetry(ctx context.Context, account *Acc } } - // 所有重试都失败,标记账号为error状态 - errorMsg := fmt.Sprintf("Token refresh failed after %d retries: %v", s.cfg.MaxRetries, lastErr) - if err := s.accountRepo.SetError(ctx, account.ID, errorMsg); err != nil { - log.Printf("[TokenRefresh] Failed to set error status for account %d: %v", account.ID, err) + // Antigravity 账户:其他错误仅记录日志,不标记 error(可能是临时网络问题) + // 其他平台账户:重试失败后标记 error + if account.Platform == PlatformAntigravity { + log.Printf("[TokenRefresh] Account %d: refresh failed after %d retries: %v", account.ID, s.cfg.MaxRetries, lastErr) + } else { + errorMsg := fmt.Sprintf("Token refresh failed after %d retries: %v", s.cfg.MaxRetries, lastErr) + if err := s.accountRepo.SetError(ctx, account.ID, errorMsg); err != nil { + log.Printf("[TokenRefresh] Failed to set error status for account %d: %v", account.ID, err) + } } return lastErr } + +// isNonRetryableRefreshError 判断是否为不可重试的刷新错误 +// 这些错误通常表示凭证已失效,需要用户重新授权 +func isNonRetryableRefreshError(err error) bool { + if err == nil { + return false + } + msg := strings.ToLower(err.Error()) + nonRetryable := []string{ + "invalid_grant", // refresh_token 已失效 + "invalid_client", // 客户端配置错误 + "unauthorized_client", // 客户端未授权 + "access_denied", // 访问被拒绝 + } + for _, needle := range nonRetryable { + if strings.Contains(msg, needle) { + return true + } + } + return false +} From 60afc7f3ed2b43b48d9b2c3470f5f1ed75c56b53 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 11:54:54 +0800 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=20thinking=20bl?= =?UTF-8?q?ock=20=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复合并冲突导致的逻辑错误 - Gemini 模型使用 dummy signature - Claude 模型跳过无 signature 的 thinking block - 删除未使用的 isValidThoughtSignature 函数 --- .../pkg/antigravity/request_transformer.go | 50 ++++++------------- 1 file changed, 15 insertions(+), 35 deletions(-) diff --git a/backend/internal/pkg/antigravity/request_transformer.go b/backend/internal/pkg/antigravity/request_transformer.go index fbaedd3d..d662be0e 100644 --- a/backend/internal/pkg/antigravity/request_transformer.go +++ b/backend/internal/pkg/antigravity/request_transformer.go @@ -172,31 +172,6 @@ func buildContents(messages []ClaudeMessage, toolIDToName map[string]string, isT // 参考: https://ai.google.dev/gemini-api/docs/thought-signatures const dummyThoughtSignature = "skip_thought_signature_validator" -// isValidThoughtSignature 验证 thought signature 是否有效 -// Claude API 要求 signature 必须是 base64 编码的字符串,长度至少 32 字节 -func isValidThoughtSignature(signature string) bool { - // 空字符串无效 - if signature == "" { - return false - } - - // signature 应该是 base64 编码,长度至少 40 个字符(约 30 字节) - // 参考 Claude API 文档和实际观察到的有效 signature - if len(signature) < 40 { - return false - } - - // 检查是否是有效的 base64 字符 - // base64 字符集: A-Z, a-z, 0-9, +, /, = - for _, c := range signature { - if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && - (c < '0' || c > '9') && c != '+' && c != '/' && c != '=' { - return false - } - } - - return true -} // buildParts 构建消息的 parts // allowDummyThought: 只有 Gemini 模型支持 dummy thought signature func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDummyThought bool) ([]GeminiPart, error) { @@ -225,17 +200,22 @@ func buildParts(content json.RawMessage, toolIDToName map[string]string, allowDu } case "thinking": - // Claude 模型:仅在提供有效 signature 时保留 thinking block;否则跳过以避免上游校验失败。 - signature := strings.TrimSpace(block.Signature) - if signature == "" || signature == dummyThoughtSignature { - log.Printf("[Warning] Skipping thinking block for Claude model (missing or dummy signature)") - continue + part := GeminiPart{ + Text: block.Thinking, + Thought: true, } - parts = append(parts, GeminiPart{ - Text: block.Thinking, - Thought: true, - ThoughtSignature: signature, - }) + // 保留原有 signature(Claude 模型需要有效的 signature) + if block.Signature != "" { + part.ThoughtSignature = block.Signature + } else if !allowDummyThought { + // Claude 模型需要有效 signature,跳过无 signature 的 thinking block + log.Printf("Warning: skipping thinking block without signature for Claude model") + continue + } else { + // Gemini 模型使用 dummy signature + part.ThoughtSignature = dummyThoughtSignature + } + parts = append(parts, part) case "image": if block.Source != nil && block.Source.Type == "base64" { From 0aa216915b5bd1e6cfccc7689e02e9fd9c1bd39d Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 13:31:23 +0800 Subject: [PATCH 10/14] =?UTF-8?q?fix(antigravity):=20=E5=87=8F=E5=B0=91=20?= =?UTF-8?q?API=20=E8=BD=AC=E5=8F=91=E6=9C=80=E5=A4=A7=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E6=AC=A1=E6=95=B0=E8=87=B3=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/antigravity_gateway_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 05ac96d3..573c1f8b 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -20,7 +20,7 @@ import ( const ( antigravityStickySessionTTL = time.Hour - antigravityMaxRetries = 5 + antigravityMaxRetries = 3 antigravityRetryBaseDelay = 1 * time.Second antigravityRetryMaxDelay = 16 * time.Second ) From ce2422324cebd26a2405ae3fcade4d82ed5faaf7 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 14:38:55 +0800 Subject: [PATCH 11/14] =?UTF-8?q?fix(antigravity):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E8=AF=BB=E5=8F=96=E9=94=99=E8=AF=AF=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9A=84=E8=B4=A6=E6=88=B7=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/antigravity_gateway_service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 573c1f8b..d28e2fc8 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -438,6 +438,7 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, if claudeReq.Stream { streamRes, err := s.handleClaudeStreamingResponse(c, resp, startTime, originalModel) if err != nil { + log.Printf("%s status=stream_error error=%v", prefix, err) return nil, err } usage = streamRes.usage @@ -602,6 +603,7 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co if stream || upstreamAction == "streamGenerateContent" { streamRes, err := s.handleGeminiStreamingResponse(c, resp, startTime) if err != nil { + log.Printf("%s status=stream_error error=%v", prefix, err) return nil, err } usage = streamRes.usage From 3932bf03539370ae032202f582f2a5ca37b51f87 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 16:45:11 +0800 Subject: [PATCH 12/14] =?UTF-8?q?fix:=20=E8=BD=AC=E5=8F=91=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=97=A5=E5=BF=97=E6=B7=BB=E5=8A=A0=E8=B4=A6=E6=88=B7?= =?UTF-8?q?ID=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/gateway_handler.go | 2 +- backend/internal/handler/openai_gateway_handler.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/internal/handler/gateway_handler.go b/backend/internal/handler/gateway_handler.go index bbc9c181..c3a1af21 100644 --- a/backend/internal/handler/gateway_handler.go +++ b/backend/internal/handler/gateway_handler.go @@ -373,7 +373,7 @@ func (h *GatewayHandler) Messages(c *gin.Context) { continue } // 错误响应已在Forward中处理,这里只记录日志 - log.Printf("Forward request failed: %v", err) + log.Printf("Account %d: Forward request failed: %v", account.ID, err) return } diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index 9931052d..5d4e5334 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -225,7 +225,7 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) { continue } // Error response already handled in Forward, just log - log.Printf("Forward request failed: %v", err) + log.Printf("Account %d: Forward request failed: %v", account.ID, err) return } From 50f92728509ca269d52b4ce84d88ba7ea1a51ee3 Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 16:53:35 +0800 Subject: [PATCH 13/14] =?UTF-8?q?feat(antigravity):=20gemini-2.5-flash-ima?= =?UTF-8?q?ge=20=E8=BD=AC=E5=8F=91=E5=88=B0=20gemini-3-pro-image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/antigravity_gateway_service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index d28e2fc8..5429af17 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -62,7 +62,8 @@ var antigravityPrefixMapping = []struct { target string }{ // 长前缀优先 - {"gemini-3-pro-image", "gemini-3-pro-image"}, // gemini-3-pro-image-preview 等 + {"gemini-2.5-flash-image", "gemini-3-pro-image"}, // gemini-2.5-flash-image → 3-pro-image + {"gemini-3-pro-image", "gemini-3-pro-image"}, // gemini-3-pro-image-preview 等 {"claude-3-5-sonnet", "claude-sonnet-4-5"}, // 旧版 claude-3-5-sonnet-xxx {"claude-sonnet-4-5", "claude-sonnet-4-5"}, // claude-sonnet-4-5-xxx {"claude-haiku-4-5", "claude-sonnet-4-5"}, // claude-haiku-4-5-xxx → sonnet From 63453fbfa0a422e243390066629ae11134a77e0d Mon Sep 17 00:00:00 2001 From: song Date: Sun, 4 Jan 2026 16:58:51 +0800 Subject: [PATCH 14/14] style: gofmt --- backend/internal/service/antigravity_gateway_service.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index 5429af17..21c07ec3 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -64,9 +64,9 @@ var antigravityPrefixMapping = []struct { // 长前缀优先 {"gemini-2.5-flash-image", "gemini-3-pro-image"}, // gemini-2.5-flash-image → 3-pro-image {"gemini-3-pro-image", "gemini-3-pro-image"}, // gemini-3-pro-image-preview 等 - {"claude-3-5-sonnet", "claude-sonnet-4-5"}, // 旧版 claude-3-5-sonnet-xxx - {"claude-sonnet-4-5", "claude-sonnet-4-5"}, // claude-sonnet-4-5-xxx - {"claude-haiku-4-5", "claude-sonnet-4-5"}, // claude-haiku-4-5-xxx → sonnet + {"claude-3-5-sonnet", "claude-sonnet-4-5"}, // 旧版 claude-3-5-sonnet-xxx + {"claude-sonnet-4-5", "claude-sonnet-4-5"}, // claude-sonnet-4-5-xxx + {"claude-haiku-4-5", "claude-sonnet-4-5"}, // claude-haiku-4-5-xxx → sonnet {"claude-opus-4-5", "claude-opus-4-5-thinking"}, {"claude-3-haiku", "claude-sonnet-4-5"}, // 旧版 claude-3-haiku-xxx → sonnet {"claude-sonnet-4", "claude-sonnet-4-5"},