diff --git a/server/model/app/response/ai_proxy.go b/server/model/app/response/ai_proxy.go index d4f5938..22047a8 100644 --- a/server/model/app/response/ai_proxy.go +++ b/server/model/app/response/ai_proxy.go @@ -7,7 +7,7 @@ type ChatCompletionResponse struct { Created int64 `json:"created"` Model string `json:"model"` Choices []ChatCompletionChoice `json:"choices"` - Usage ChatCompletionUsage `json:"usage"` + Usage *ChatCompletionUsage `json:"usage,omitempty"` // 统一格式的用量统计(用于各种上游的标准化) StandardUsage *ChatCompletionUsage `json:"standard_usage,omitempty"` // 扩展字段:正则脚本执行日志 diff --git a/server/service/app/ai_claude.go b/server/service/app/ai_claude.go index 0d3e395..4a2d796 100644 --- a/server/service/app/ai_claude.go +++ b/server/service/app/ai_claude.go @@ -21,6 +21,13 @@ import ( // ProcessClaudeMessage 处理 Claude 消息请求 func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request.ClaudeMessageRequest) (*response.ClaudeMessageResponse, error) { + // 记录请求参数 + global.GVA_LOG.Info("收到 Claude Messages 请求", + zap.String("model", req.Model), + zap.Any("messages", req.Messages), + zap.Any("full_request", req), + ) + // 1. 根据模型获取配置 if req.Model == "" { return nil, fmt.Errorf("model 参数不能为空") @@ -32,8 +39,9 @@ func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request. } // 2. 注入预设 + var injector *PresetInjector if preset != nil { - injector := NewPresetInjector(preset) + injector = NewPresetInjector(preset) req.Messages = s.convertClaudeMessages(injector.InjectMessages(s.convertToOpenAIMessages(req.Messages))) } @@ -43,24 +51,74 @@ func (s *AiProxyService) ProcessClaudeMessage(ctx context.Context, req *request. return nil, err } - // 4. 处理响应 - if preset != nil && len(resp.Content) > 0 { - injector := NewPresetInjector(preset) + // 获取 AI 输出内容 + aiOutput := "" + if len(resp.Content) > 0 { + aiOutput = resp.Content[0].Text + } + + // 4. 处理响应(使用同一个 injector 实例) + if injector != nil && len(resp.Content) > 0 { resp.Content[0].Text = injector.ProcessResponse(resp.Content[0].Text) + aiOutput = resp.Content[0].Text } // 5. 统一填充 standard_usage,转换为 OpenAI 风格的用量统计 - resp.StandardUsage = &response.ChatCompletionUsage{ - PromptTokens: resp.Usage.InputTokens, - CompletionTokens: resp.Usage.OutputTokens, - TotalTokens: resp.Usage.InputTokens + resp.Usage.OutputTokens, + if resp.Usage.InputTokens > 0 || resp.Usage.OutputTokens > 0 { + resp.StandardUsage = &response.ChatCompletionUsage{ + PromptTokens: resp.Usage.InputTokens, + CompletionTokens: resp.Usage.OutputTokens, + TotalTokens: resp.Usage.InputTokens + resp.Usage.OutputTokens, + } } + // 记录响应内容 + logFields := []zap.Field{ + zap.String("ai_output", aiOutput), + zap.Any("usage", resp.Usage), + } + + // 添加正则脚本执行日志(使用同一个 injector 实例) + if injector != nil { + regexLogs := injector.GetRegexLogs() + if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) { + // 收集触发的脚本名称 + triggeredScripts := make([]string, 0) + for _, scriptLog := range regexLogs.InputScripts { + if scriptLog.MatchCount > 0 { + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) + } + } + for _, scriptLog := range regexLogs.OutputScripts { + if scriptLog.MatchCount > 0 { + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) + } + } + + if len(triggeredScripts) > 0 { + logFields = append(logFields, + zap.Strings("triggered_regex_scripts", triggeredScripts), + zap.Int("total_matches", regexLogs.TotalMatches), + ) + } + } + } + + logFields = append(logFields, zap.Any("full_response", resp)) + global.GVA_LOG.Info("Claude Messages 响应", logFields...) + return resp, nil } // ProcessClaudeMessageStream 处理 Claude 流式消息请求 func (s *AiProxyService) ProcessClaudeMessageStream(c *gin.Context, req *request.ClaudeMessageRequest) { + // 记录请求参数 + global.GVA_LOG.Info("收到 Claude Messages 流式请求", + zap.String("model", req.Model), + zap.Any("messages", req.Messages), + zap.Any("full_request", req), + ) + // 1. 根据模型获取配置 if req.Model == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"}) @@ -142,9 +200,22 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap } defer httpResp.Body.Close() + if httpResp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(httpResp.Body) + global.GVA_LOG.Error("Claude 流式请求上游返回错误", + zap.Int("status_code", httpResp.StatusCode), + zap.String("response_body", string(body)), + ) + return fmt.Errorf("上游返回错误: %d - %s", httpResp.StatusCode, string(body)) + } + reader := bufio.NewReader(httpResp.Body) flusher, _ := c.Writer.(http.Flusher) + // 聚合完整输出用于日志 + var fullContent bytes.Buffer + var totalInputTokens, totalOutputTokens int + for { line, err := reader.ReadBytes('\n') if err == io.EOF { @@ -157,10 +228,21 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap if bytes.HasPrefix(line, []byte("data: ")) { data := bytes.TrimPrefix(line, []byte("data: ")) var chunk response.ClaudeStreamResponse - if json.Unmarshal(data, &chunk) == nil && chunk.Delta != nil { - if injector != nil { - chunk.Delta.Text = injector.ProcessResponse(chunk.Delta.Text) + if json.Unmarshal(data, &chunk) == nil { + // 收集 usage 信息 + if chunk.Usage != nil { + totalInputTokens = chunk.Usage.InputTokens + totalOutputTokens = chunk.Usage.OutputTokens } + + // 处理文本内容 + if chunk.Delta != nil && chunk.Delta.Text != "" { + fullContent.WriteString(chunk.Delta.Text) + if injector != nil { + chunk.Delta.Text = injector.ProcessResponse(chunk.Delta.Text) + } + } + processedData, _ := json.Marshal(chunk) c.Writer.Write([]byte("data: ")) c.Writer.Write(processedData) @@ -169,6 +251,43 @@ func (s *AiProxyService) forwardClaudeStreamRequest(c *gin.Context, provider *ap } } } + + // 记录完整的流式响应日志 + logFields := []zap.Field{ + zap.String("ai_output", fullContent.String()), + zap.Int("input_tokens", totalInputTokens), + zap.Int("output_tokens", totalOutputTokens), + zap.Int("total_tokens", totalInputTokens+totalOutputTokens), + } + + // 添加正则脚本执行日志 + if injector != nil { + regexLogs := injector.GetRegexLogs() + if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) { + // 收集触发的脚本名称 + triggeredScripts := make([]string, 0) + for _, scriptLog := range regexLogs.InputScripts { + if scriptLog.MatchCount > 0 { + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) + } + } + for _, scriptLog := range regexLogs.OutputScripts { + if scriptLog.MatchCount > 0 { + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) + } + } + + if len(triggeredScripts) > 0 { + logFields = append(logFields, + zap.Strings("triggered_regex_scripts", triggeredScripts), + zap.Int("total_matches", regexLogs.TotalMatches), + ) + } + } + } + + global.GVA_LOG.Info("Claude Messages 流式响应完成", logFields...) + return nil } diff --git a/server/service/app/ai_proxy.go b/server/service/app/ai_proxy.go index 5bf120c..521f18a 100644 --- a/server/service/app/ai_proxy.go +++ b/server/service/app/ai_proxy.go @@ -23,6 +23,13 @@ type AiProxyService struct{} // ProcessChatCompletion 处理聊天补全请求 func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request.ChatCompletionRequest) (*response.ChatCompletionResponse, error) { + // 记录请求参数 + global.GVA_LOG.Info("收到 ChatCompletion 请求", + zap.String("model", req.Model), + zap.Any("messages", req.Messages), + zap.Any("full_request", req), + ) + // 1. 根据模型获取配置 if req.Model == "" { return nil, fmt.Errorf("model 参数不能为空") @@ -48,73 +55,87 @@ func (s *AiProxyService) ProcessChatCompletion(ctx context.Context, req *request } // 4. 处理响应并收集正则日志 - if resp != nil { + if resp != nil && resp.Usage != nil { // 统一填充 standard_usage,方便上游使用统一格式解析 - resp.StandardUsage = &response.ChatCompletionUsage{ - PromptTokens: resp.Usage.PromptTokens, - CompletionTokens: resp.Usage.CompletionTokens, - TotalTokens: resp.Usage.TotalTokens, + if resp.Usage.PromptTokens > 0 || resp.Usage.CompletionTokens > 0 || resp.Usage.TotalTokens > 0 { + resp.StandardUsage = &response.ChatCompletionUsage{ + PromptTokens: resp.Usage.PromptTokens, + CompletionTokens: resp.Usage.CompletionTokens, + TotalTokens: resp.Usage.TotalTokens, + } } } + // 获取 AI 输出内容 + aiOutput := "" + if len(resp.Choices) > 0 { + aiOutput = resp.Choices[0].Message.Content + } + + // 应用预设处理 if preset != nil && injector != nil && len(resp.Choices) > 0 { resp.Choices[0].Message.Content = injector.ProcessResponse(resp.Choices[0].Message.Content) + aiOutput = resp.Choices[0].Message.Content // 添加正则执行日志到响应 regexLogs := injector.GetRegexLogs() if regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0 { resp.RegexLogs = regexLogs } + } - // 记录匹配到正则时的脚本名称 - if regexLogs.TotalMatches > 0 { - matchedScripts := make([]string, 0) + // 记录响应内容(统一日志输出) + logFields := []zap.Field{ + zap.String("ai_output", aiOutput), + } + if resp.Usage != nil { + logFields = append(logFields, zap.Any("usage", resp.Usage)) + } + if resp.StandardUsage != nil { + logFields = append(logFields, zap.Any("standard_usage", resp.StandardUsage)) + } + + // 添加正则脚本执行日志 + if injector != nil { + regexLogs := injector.GetRegexLogs() + if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) { + // 收集触发的脚本名称 + triggeredScripts := make([]string, 0) for _, scriptLog := range regexLogs.InputScripts { if scriptLog.MatchCount > 0 { - matchedScripts = append(matchedScripts, scriptLog.ScriptName) + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) } } for _, scriptLog := range regexLogs.OutputScripts { if scriptLog.MatchCount > 0 { - matchedScripts = append(matchedScripts, scriptLog.ScriptName) + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) } } - // 记录接口调用参数和 AI 输出内容 - aiOutput := resp.Choices[0].Message.Content - global.GVA_LOG.Info("AI 请求完成", - zap.Any("request", req), - zap.String("ai_output", aiOutput), - zap.Strings("matched_regex_scripts", matchedScripts), - ) - } else { - // 未匹配到正则时仅记录请求和输出 - aiOutput := resp.Choices[0].Message.Content - global.GVA_LOG.Info("AI 请求完成(无正则匹配)", - zap.Any("request", req), - zap.String("ai_output", aiOutput), - ) - } - } else { - // 无预设或无注入器时也记录基础请求与输出 - if len(resp.Choices) > 0 { - aiOutput := resp.Choices[0].Message.Content - global.GVA_LOG.Info("AI 请求完成(无预设/注入器)", - zap.Any("request", req), - zap.String("ai_output", aiOutput), - ) - } else { - global.GVA_LOG.Info("AI 请求完成(无输出)", - zap.Any("request", req), - ) + if len(triggeredScripts) > 0 { + logFields = append(logFields, + zap.Strings("triggered_regex_scripts", triggeredScripts), + zap.Int("total_matches", regexLogs.TotalMatches), + ) + } } } + logFields = append(logFields, zap.Any("full_response", resp)) + global.GVA_LOG.Info("ChatCompletion 响应", logFields...) + return resp, nil } // ProcessChatCompletionStream 处理流式聊天补全请求 func (s *AiProxyService) ProcessChatCompletionStream(c *gin.Context, req *request.ChatCompletionRequest) { + // 记录请求参数 + global.GVA_LOG.Info("收到 ChatCompletion 流式请求", + zap.String("model", req.Model), + zap.Any("messages", req.Messages), + zap.Any("full_request", req), + ) + // 1. 根据模型获取配置 if req.Model == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "model 参数不能为空"}) @@ -304,39 +325,38 @@ func (s *AiProxyService) forwardStreamRequest(c *gin.Context, provider *app.AiPr } // 流式请求结束后记录日志 + logFields := []zap.Field{ + zap.String("ai_output", fullContent.String()), + } + + // 添加正则脚本执行日志 if injector != nil { regexLogs := injector.GetRegexLogs() - if regexLogs != nil && (regexLogs.TotalMatches > 0 || len(regexLogs.InputScripts) > 0 || len(regexLogs.OutputScripts) > 0) { - matchedScripts := make([]string, 0) + // 收集触发的脚本名称 + triggeredScripts := make([]string, 0) for _, scriptLog := range regexLogs.InputScripts { if scriptLog.MatchCount > 0 { - matchedScripts = append(matchedScripts, scriptLog.ScriptName) + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输入:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) } } for _, scriptLog := range regexLogs.OutputScripts { if scriptLog.MatchCount > 0 { - matchedScripts = append(matchedScripts, scriptLog.ScriptName) + triggeredScripts = append(triggeredScripts, fmt.Sprintf("%s(输出:%d次)", scriptLog.ScriptName, scriptLog.MatchCount)) } } - global.GVA_LOG.Info("AI 流式请求完成", - zap.Any("request", req), - zap.String("ai_output", fullContent.String()), - zap.Strings("matched_regex_scripts", matchedScripts), - ) - } else { - global.GVA_LOG.Info("AI 流式请求完成(无正则匹配)", - zap.Any("request", req), - zap.String("ai_output", fullContent.String()), - ) + if len(triggeredScripts) > 0 { + logFields = append(logFields, + zap.Strings("triggered_regex_scripts", triggeredScripts), + zap.Int("total_matches", regexLogs.TotalMatches), + ) + } } - } else { - global.GVA_LOG.Info("AI 流式请求完成(无预设/注入器)", - zap.Any("request", req), - ) } + global.GVA_LOG.Info("ChatCompletion 流式响应完成", logFields...) + return nil }