Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
4df71262
Commit
4df71262
authored
Jan 04, 2026
by
shaw
Browse files
Merge branch 'slovx2/main'
parents
a5275595
63453fbf
Changes
7
Hide whitespace changes
Inline
Side-by-side
.github/workflows/release.yml
View file @
4df71262
...
...
@@ -4,6 +4,22 @@ 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
# 环境变量:合并 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
...
...
@@ -19,7 +35,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"
...
...
@@ -66,6 +87,7 @@ jobs:
uses
:
actions/checkout@v4
with
:
fetch-depth
:
0
ref
:
${{ github.event.inputs.tag || github.ref }}
-
name
:
Download VERSION artifact
uses
:
actions/download-artifact@v4
...
...
@@ -93,7 +115,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 }}
...
...
@@ -113,7 +138,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(跳过第一行标题)
...
...
@@ -137,18 +166,21 @@ jobs:
uses
:
goreleaser/goreleaser-action@v6
with
:
version
:
'
~>
v2'
args
:
release --clean --skip=validate
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 }}
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.SIMPLE_RELEASE != 'true' && env.DOCKERHUB_USERNAME != '' }}
uses
:
peter-evans/dockerhub-description@v4
env
:
DOCKERHUB_USERNAME
:
${{ secrets.DOCKERHUB_USERNAME }}
with
:
username
:
${{ secrets.DOCKERHUB_USERNAME }}
password
:
${{ secrets.DOCKERHUB_TOKEN }}
...
...
@@ -158,9 +190,11 @@ jobs:
# Send Telegram notification
-
name
:
Send Telegram Notification
if
:
${{ env.SIMPLE_RELEASE != 'true' }}
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
:
|
# 检查必要的环境变量
...
...
@@ -169,10 +203,13 @@ 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 }}"
DOCKER_IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/sub2api"
GHCR_IMAGE="ghcr.io/${REPO,,}" # ${,,} converts to lowercase
# 获取 tag message 内容
...
...
@@ -194,14 +231,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//./_}"
...
...
.goreleaser.simple.yaml
0 → 100644
View file @
4df71262
# 简化版 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 }})
.goreleaser.yaml
View file @
4df71262
...
...
@@ -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 }}
```
...
...
backend/internal/handler/gateway_handler.go
View file @
4df71262
...
...
@@ -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
}
...
...
backend/internal/handler/openai_gateway_handler.go
View file @
4df71262
...
...
@@ -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
}
...
...
backend/internal/service/antigravity_gateway_service.go
View file @
4df71262
...
...
@@ -20,11 +20,27 @@ import (
const
(
antigravityStickySessionTTL
=
time
.
Hour
antigravityMaxRetries
=
5
antigravityMaxRetries
=
3
antigravityRetryBaseDelay
=
1
*
time
.
Second
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
,
...
...
@@ -46,10 +62,11 @@ var antigravityPrefixMapping = []struct {
target
string
}{
// 长前缀优先
{
"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
{
"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-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"
},
...
...
@@ -310,6 +327,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 +383,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 +396,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 +420,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
}
...
...
@@ -419,6 +439,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
...
...
@@ -443,6 +464,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 +541,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 +554,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 +582,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
}
...
...
@@ -580,6 +604,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
...
...
@@ -628,7 +653,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 +664,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 +789,7 @@ func (s *AntigravityGatewayService) writeClaudeError(c *gin.Context, status int,
func
(
s
*
AntigravityGatewayService
)
writeMappedClaudeError
(
c
*
gin
.
Context
,
upstreamStatus
int
,
body
[]
byte
)
error
{
// 记录上游错误详情便于调试
log
.
Printf
(
"
A
ntigravity upstream
error
%d:
%s"
,
upstreamStatus
,
string
(
body
))
log
.
Printf
(
"
[a
ntigravity
-Forward]
upstream
_
error
status=%d body=
%s"
,
upstreamStatus
,
string
(
body
))
var
statusCode
int
var
errType
,
errMsg
string
...
...
@@ -832,7 +863,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"
)
}
...
...
backend/internal/service/token_refresh_service.go
View file @
4df71262
...
...
@@ -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
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment