基于 backend-code-audit 审计报告,修复剩余 P0/P1/P2 共 34 项问题: P0 生产 Bug: - 修复 time.Since(time.Now()) 计时逻辑错误 (P0-03) - generateRandomID 改用 crypto/rand 替代固定索引 (P0-04) - IncrementQuotaUsed 重写为 Ent 原子操作消除 TOCTOU 竞态 (P0-05) 安全加固: - gateway/openai handler 错误响应替换为泛化消息,防止内部信息泄露 (P1-14) - usage_log_repo dateFormat 参数改用白名单映射,防止 SQL 注入 (P1-16) - 默认配置安全加固:sslmode=prefer、response_headers=true、mode=release (P1-18/19, P2-15) 性能优化: - gateway handler 循环内 defer 替换为显式 releaseWait 闭包 (P1-02) - group_repo/promo_code_repo Count 前 Clone 查询避免状态污染 (P1-03) - usage_log_repo 四个查询添加 LIMIT 10000 防止 OOM (P1-07) - GetBatchUsageStats 添加时间范围参数,默认最近 30 天 (P1-10) - ip.go CIDR 预编译为包级变量 (P1-11) - BatchUpdateCredentials 重构为先验证后更新 (P1-13) 缓存一致性: - billing_cache 添加 jitteredTTL 防止缓存雪崩 (P2-10) - DeductUserBalance/UpdateSubscriptionUsage 错误传播修复 (P2-12) - UserService.UpdateBalance 成功后异步失效 billingCache (P2-13) 代码质量: - search 截断改为按 rune 处理,支持多字节字符 (P2-01) - TLS Handshake 改为 HandshakeContext 支持 context 取消 (P2-07) - CORS 预检添加 Access-Control-Max-Age: 86400 (P2-16) 测试覆盖: - 新增 user_service_test.go(UpdateBalance 缓存失效 6 个用例) - 新增 batch_update_credentials_test.go(fail-fast + 类型验证 7 个用例) - 新增 response_transformer_test.go、ip_test.go、usage_log_repo_unit_test.go、search_truncate_test.go - 集成测试:IncrementQuotaUsed 并发测试、billing_cache 错误传播测试 - config_test.go 补充 server.mode/sslmode 默认值断言 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
358 lines
9.1 KiB
Go
358 lines
9.1 KiB
Go
package antigravity
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
)
|
||
|
||
// TransformGeminiToClaude 将 Gemini 响应转换为 Claude 格式(非流式)
|
||
func TransformGeminiToClaude(geminiResp []byte, originalModel string) ([]byte, *ClaudeUsage, error) {
|
||
// 解包 v1internal 响应
|
||
var v1Resp V1InternalResponse
|
||
if err := json.Unmarshal(geminiResp, &v1Resp); err != nil {
|
||
// 尝试直接解析为 GeminiResponse
|
||
var directResp GeminiResponse
|
||
if err2 := json.Unmarshal(geminiResp, &directResp); err2 != nil {
|
||
return nil, nil, fmt.Errorf("parse gemini response: %w", err)
|
||
}
|
||
v1Resp.Response = directResp
|
||
v1Resp.ResponseID = directResp.ResponseID
|
||
v1Resp.ModelVersion = directResp.ModelVersion
|
||
} else if len(v1Resp.Response.Candidates) == 0 {
|
||
// 第一次解析成功但 candidates 为空,说明是直接的 GeminiResponse 格式
|
||
var directResp GeminiResponse
|
||
if err2 := json.Unmarshal(geminiResp, &directResp); err2 != nil {
|
||
return nil, nil, fmt.Errorf("parse gemini response as direct: %w", err2)
|
||
}
|
||
v1Resp.Response = directResp
|
||
v1Resp.ResponseID = directResp.ResponseID
|
||
v1Resp.ModelVersion = directResp.ModelVersion
|
||
}
|
||
|
||
// 使用处理器转换
|
||
processor := NewNonStreamingProcessor()
|
||
claudeResp := processor.Process(&v1Resp.Response, v1Resp.ResponseID, originalModel)
|
||
|
||
// 序列化
|
||
respBytes, err := json.Marshal(claudeResp)
|
||
if err != nil {
|
||
return nil, nil, fmt.Errorf("marshal claude response: %w", err)
|
||
}
|
||
|
||
return respBytes, &claudeResp.Usage, nil
|
||
}
|
||
|
||
// NonStreamingProcessor 非流式响应处理器
|
||
type NonStreamingProcessor struct {
|
||
contentBlocks []ClaudeContentItem
|
||
textBuilder string
|
||
thinkingBuilder string
|
||
thinkingSignature string
|
||
trailingSignature string
|
||
hasToolCall bool
|
||
}
|
||
|
||
// NewNonStreamingProcessor 创建非流式响应处理器
|
||
func NewNonStreamingProcessor() *NonStreamingProcessor {
|
||
return &NonStreamingProcessor{
|
||
contentBlocks: make([]ClaudeContentItem, 0),
|
||
}
|
||
}
|
||
|
||
// Process 处理 Gemini 响应
|
||
func (p *NonStreamingProcessor) Process(geminiResp *GeminiResponse, responseID, originalModel string) *ClaudeResponse {
|
||
// 获取 parts
|
||
var parts []GeminiPart
|
||
if len(geminiResp.Candidates) > 0 && geminiResp.Candidates[0].Content != nil {
|
||
parts = geminiResp.Candidates[0].Content.Parts
|
||
}
|
||
|
||
// 处理所有 parts
|
||
for _, part := range parts {
|
||
p.processPart(&part)
|
||
}
|
||
|
||
if len(geminiResp.Candidates) > 0 {
|
||
if grounding := geminiResp.Candidates[0].GroundingMetadata; grounding != nil {
|
||
p.processGrounding(grounding)
|
||
}
|
||
}
|
||
|
||
// 刷新剩余内容
|
||
p.flushThinking()
|
||
p.flushText()
|
||
|
||
// 处理 trailingSignature
|
||
if p.trailingSignature != "" {
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: p.trailingSignature,
|
||
})
|
||
}
|
||
|
||
// 构建响应
|
||
return p.buildResponse(geminiResp, responseID, originalModel)
|
||
}
|
||
|
||
// processPart 处理单个 part
|
||
func (p *NonStreamingProcessor) processPart(part *GeminiPart) {
|
||
signature := part.ThoughtSignature
|
||
|
||
// 1. FunctionCall 处理
|
||
if part.FunctionCall != nil {
|
||
p.flushThinking()
|
||
p.flushText()
|
||
|
||
// 处理 trailingSignature
|
||
if p.trailingSignature != "" {
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: p.trailingSignature,
|
||
})
|
||
p.trailingSignature = ""
|
||
}
|
||
|
||
p.hasToolCall = true
|
||
|
||
// 生成 tool_use id
|
||
toolID := part.FunctionCall.ID
|
||
if toolID == "" {
|
||
toolID = fmt.Sprintf("%s-%s", part.FunctionCall.Name, generateRandomID())
|
||
}
|
||
|
||
item := ClaudeContentItem{
|
||
Type: "tool_use",
|
||
ID: toolID,
|
||
Name: part.FunctionCall.Name,
|
||
Input: part.FunctionCall.Args,
|
||
}
|
||
|
||
if signature != "" {
|
||
item.Signature = signature
|
||
}
|
||
|
||
p.contentBlocks = append(p.contentBlocks, item)
|
||
return
|
||
}
|
||
|
||
// 2. Text 处理
|
||
if part.Text != "" || part.Thought {
|
||
if part.Thought {
|
||
// Thinking part
|
||
p.flushText()
|
||
|
||
// 处理 trailingSignature
|
||
if p.trailingSignature != "" {
|
||
p.flushThinking()
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: p.trailingSignature,
|
||
})
|
||
p.trailingSignature = ""
|
||
}
|
||
|
||
p.thinkingBuilder += part.Text
|
||
if signature != "" {
|
||
p.thinkingSignature = signature
|
||
}
|
||
} else {
|
||
// 普通 Text
|
||
if part.Text == "" {
|
||
// 空 text 带签名 - 暂存
|
||
if signature != "" {
|
||
p.trailingSignature = signature
|
||
}
|
||
return
|
||
}
|
||
|
||
p.flushThinking()
|
||
|
||
// 处理之前的 trailingSignature
|
||
if p.trailingSignature != "" {
|
||
p.flushText()
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: p.trailingSignature,
|
||
})
|
||
p.trailingSignature = ""
|
||
}
|
||
|
||
// 非空 text 带签名 - 特殊处理:先输出 text,再输出空 thinking 块
|
||
if signature != "" {
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "text",
|
||
Text: part.Text,
|
||
})
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: signature,
|
||
})
|
||
} else {
|
||
// 普通 text (无签名) - 累积到 builder
|
||
p.textBuilder += part.Text
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. InlineData (Image) 处理
|
||
if part.InlineData != nil && part.InlineData.Data != "" {
|
||
p.flushThinking()
|
||
markdownImg := fmt.Sprintf("",
|
||
part.InlineData.MimeType, part.InlineData.Data)
|
||
p.textBuilder += markdownImg
|
||
p.flushText()
|
||
}
|
||
}
|
||
|
||
func (p *NonStreamingProcessor) processGrounding(grounding *GeminiGroundingMetadata) {
|
||
groundingText := buildGroundingText(grounding)
|
||
if groundingText == "" {
|
||
return
|
||
}
|
||
|
||
p.flushThinking()
|
||
p.flushText()
|
||
p.textBuilder += groundingText
|
||
p.flushText()
|
||
}
|
||
|
||
// flushText 刷新 text builder
|
||
func (p *NonStreamingProcessor) flushText() {
|
||
if p.textBuilder == "" {
|
||
return
|
||
}
|
||
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "text",
|
||
Text: p.textBuilder,
|
||
})
|
||
p.textBuilder = ""
|
||
}
|
||
|
||
// flushThinking 刷新 thinking builder
|
||
func (p *NonStreamingProcessor) flushThinking() {
|
||
if p.thinkingBuilder == "" && p.thinkingSignature == "" {
|
||
return
|
||
}
|
||
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: p.thinkingBuilder,
|
||
Signature: p.thinkingSignature,
|
||
})
|
||
p.thinkingBuilder = ""
|
||
p.thinkingSignature = ""
|
||
}
|
||
|
||
// buildResponse 构建最终响应
|
||
func (p *NonStreamingProcessor) buildResponse(geminiResp *GeminiResponse, responseID, originalModel string) *ClaudeResponse {
|
||
var finishReason string
|
||
if len(geminiResp.Candidates) > 0 {
|
||
finishReason = geminiResp.Candidates[0].FinishReason
|
||
if finishReason == "MALFORMED_FUNCTION_CALL" {
|
||
log.Printf("[Antigravity] MALFORMED_FUNCTION_CALL detected in response for model %s", originalModel)
|
||
if geminiResp.Candidates[0].Content != nil {
|
||
if b, err := json.Marshal(geminiResp.Candidates[0].Content); err == nil {
|
||
log.Printf("[Antigravity] Malformed content: %s", string(b))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
stopReason := "end_turn"
|
||
if p.hasToolCall {
|
||
stopReason = "tool_use"
|
||
} else if finishReason == "MAX_TOKENS" {
|
||
stopReason = "max_tokens"
|
||
}
|
||
|
||
// 注意:Gemini 的 promptTokenCount 包含 cachedContentTokenCount,
|
||
// 但 Claude 的 input_tokens 不包含 cache_read_input_tokens,需要减去
|
||
usage := ClaudeUsage{}
|
||
if geminiResp.UsageMetadata != nil {
|
||
cached := geminiResp.UsageMetadata.CachedContentTokenCount
|
||
usage.InputTokens = geminiResp.UsageMetadata.PromptTokenCount - cached
|
||
usage.OutputTokens = geminiResp.UsageMetadata.CandidatesTokenCount
|
||
usage.CacheReadInputTokens = cached
|
||
}
|
||
|
||
// 生成响应 ID
|
||
respID := responseID
|
||
if respID == "" {
|
||
respID = geminiResp.ResponseID
|
||
}
|
||
if respID == "" {
|
||
respID = "msg_" + generateRandomID()
|
||
}
|
||
|
||
return &ClaudeResponse{
|
||
ID: respID,
|
||
Type: "message",
|
||
Role: "assistant",
|
||
Model: originalModel,
|
||
Content: p.contentBlocks,
|
||
StopReason: stopReason,
|
||
Usage: usage,
|
||
}
|
||
}
|
||
|
||
func buildGroundingText(grounding *GeminiGroundingMetadata) string {
|
||
if grounding == nil {
|
||
return ""
|
||
}
|
||
|
||
var builder strings.Builder
|
||
|
||
if len(grounding.WebSearchQueries) > 0 {
|
||
_, _ = builder.WriteString("\n\n---\nWeb search queries: ")
|
||
_, _ = builder.WriteString(strings.Join(grounding.WebSearchQueries, ", "))
|
||
}
|
||
|
||
if len(grounding.GroundingChunks) > 0 {
|
||
var links []string
|
||
for i, chunk := range grounding.GroundingChunks {
|
||
if chunk.Web == nil {
|
||
continue
|
||
}
|
||
title := strings.TrimSpace(chunk.Web.Title)
|
||
if title == "" {
|
||
title = "Source"
|
||
}
|
||
uri := strings.TrimSpace(chunk.Web.URI)
|
||
if uri == "" {
|
||
uri = "#"
|
||
}
|
||
links = append(links, fmt.Sprintf("[%d] [%s](%s)", i+1, title, uri))
|
||
}
|
||
|
||
if len(links) > 0 {
|
||
_, _ = builder.WriteString("\n\nSources:\n")
|
||
_, _ = builder.WriteString(strings.Join(links, "\n"))
|
||
}
|
||
}
|
||
|
||
return builder.String()
|
||
}
|
||
|
||
// generateRandomID 生成密码学安全的随机 ID
|
||
func generateRandomID() string {
|
||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
result := make([]byte, 12)
|
||
randBytes := make([]byte, 12)
|
||
if _, err := rand.Read(randBytes); err != nil {
|
||
panic("crypto/rand unavailable: " + err.Error())
|
||
}
|
||
for i, b := range randBytes {
|
||
result[i] = chars[int(b)%len(chars)]
|
||
}
|
||
return string(result)
|
||
}
|