- 前端: 所有界面显示、i18n 文本、组件中的品牌名称 - 后端: 服务层、设置默认值、邮件模板、安装向导 - 数据库: 迁移脚本注释 - 保持功能完全一致,仅更改品牌名称 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
274 lines
7.0 KiB
Go
274 lines
7.0 KiB
Go
package antigravity
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
)
|
||
|
||
// 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
|
||
}
|
||
|
||
// 使用处理器转换
|
||
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)
|
||
}
|
||
|
||
// 刷新剩余内容
|
||
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 = ""
|
||
}
|
||
|
||
p.textBuilder += part.Text
|
||
|
||
// 非空 text 带签名 - 立即刷新并输出空 thinking 块
|
||
if signature != "" {
|
||
p.flushText()
|
||
p.contentBlocks = append(p.contentBlocks, ClaudeContentItem{
|
||
Type: "thinking",
|
||
Thinking: "",
|
||
Signature: signature,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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()
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
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,
|
||
}
|
||
}
|
||
|
||
// generateRandomID 生成随机 ID
|
||
func generateRandomID() string {
|
||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
result := make([]byte, 12)
|
||
for i := range result {
|
||
result[i] = chars[i%len(chars)]
|
||
}
|
||
return string(result)
|
||
}
|