package common import ( "context" "net/http" "github.com/qianfree/team-api/relay/constant" ) // DebugSession 渠道调试日志会话:挂载于 RelayContext.Debug,整请求存活。 // 重试循环中首个「调试开关开启」的渠道尝试触发创建:捕获段1(客户端请求)并包装段4 // 的客户端响应 writer(保持到请求结束,中间失败尝试不向客户端写字节,累积字节即最终响应)。 // 中间失败尝试经 DebugAttempt.Submit 立即落库(is_final=false);最终尝试经 MarkFinal // 挂起,由外层 handler 在响应写完后调用 FinalizeAndSubmit 补段4并统一提交(幂等)。 type DebugSession struct { requestID string tenantID int64 userID int64 apiKeyID int64 path string clientReqHeaders map[string]string // 段1(脱敏后) clientReqBody []byte // 段1 完整请求体 clientWriter *DebugClientWriter // 段4 捕获 writer pending *DebugAttempt // MarkFinal 挂起的最终尝试 finalized bool } // NewDebugSession 创建调试会话 func NewDebugSession(requestID string, tenantID, userID, apiKeyID int64, path string) *DebugSession { return &DebugSession{ requestID: requestID, tenantID: tenantID, userID: userID, apiKeyID: apiKeyID, path: path, } } // CaptureClientRequest 捕获段1:客户端请求头(脱敏)+ 完整请求体 func (s *DebugSession) CaptureClientRequest(headers http.Header, body []byte) { s.clientReqHeaders = MaskHeaders(headers) s.clientReqBody = body } // SetClientWriter 回填段4捕获 writer(由 RelayHandler 包装 rc.Writer 后调用) func (s *DebugSession) SetClientWriter(w *DebugClientWriter) { s.clientWriter = w } // BeginAttempt 为当前渠道尝试创建捕获上下文(每轮重试一次) func (s *DebugSession) BeginAttempt(selection *ChannelSelection, modelName, relayModeStr string, isStream bool, retryIndex int) *DebugAttempt { return &DebugAttempt{ session: s, Capture: &DebugAttemptCapture{}, channelID: selection.ChannelID, channelName: selection.ChannelName, channelType: selection.ChannelType, modelName: modelName, upstreamModel: selection.UpstreamModelName, relayMode: relayModeStr, isStream: isStream, retryIndex: retryIndex, } } // BeginTaskAttempt 为异步任务提交创建捕获上下文。任务提交为单次尝试(无重试循环), // isStream 恒 false,渠道元信息取 ChannelMeta(任务链路经 selectTaskChannel 物化, // 无 ChannelSelection)。上游请求经适配器 DoRequest(ctx) 携带的捕获器镜像段2/3。 func (s *DebugSession) BeginTaskAttempt(meta *ChannelMeta, modelName, relayModeStr string, retryIndex int) *DebugAttempt { return &DebugAttempt{ session: s, Capture: &DebugAttemptCapture{}, channelID: meta.ChannelID, channelName: meta.ChannelName, channelType: meta.ChannelType, modelName: modelName, upstreamModel: meta.UpstreamModelName, relayMode: relayModeStr, isStream: false, retryIndex: retryIndex, } } // DebugAttempt 单次尝试的调试上下文:会话引用 + 渠道元数据 + 传输层捕获器。 // Capture 经 WithDebugAttempt 挂入 ctx,由 DebugRoundTripper 填充段2/段3。 type DebugAttempt struct { session *DebugSession Capture *DebugAttemptCapture channelID int64 channelName string channelType int modelName string upstreamModel string relayMode string isStream bool retryIndex int errText string conversion *DebugConversion // 协议转换信息(CaptureProtocol 快照) } // CaptureProtocol 快照协议转换信息(RelayHandler 在 buildRelayInfo 之后调用一次)。 // 上游协议按桥接标志与渠道类型推导:直传 > Responses 桥接 > Responses 直连 > 渠道原生协议。 func (a *DebugAttempt) CaptureProtocol(info *RelayInfo) { if a == nil || info == nil { return } conv := &DebugConversion{ ClientFormat: string(info.GetOriginalClientFormat()), } switch { case info.ChannelMeta != nil && info.ChannelMeta.Settings.PassThroughBodyEnabled: conv.UpstreamFormat = conv.ClientFormat conv.Bridge = "pass_through" case info.UseResponsesAPI: conv.UpstreamFormat = string(constant.RelayFormatResponses) conv.Bridge = "responses_api" case info.ChannelMeta != nil && info.ChannelMeta.UpstreamSpeaksResponses(): conv.UpstreamFormat = string(constant.RelayFormatResponses) conv.Bridge = "responses_direct" default: conv.UpstreamFormat = nativeFormatOfProvider(a.channelType) } for _, f := range info.ConversionChain() { conv.Chain = append(conv.Chain, string(f)) } // relaykit 链未覆盖(直传/同构/adaptor 手写转换)且两端协议不同时,兜底记录两端 if len(conv.Chain) == 0 && conv.UpstreamFormat != conv.ClientFormat { conv.Chain = []string{conv.ClientFormat, conv.UpstreamFormat} } a.conversion = conv } // nativeFormatOfProvider 渠道类型 → 上游原生协议(展示用途)。 // openai 兼容系渠道占绝大多数,默认 openai;anthropic / gemini 系例外。 func nativeFormatOfProvider(providerType int) string { switch constant.ProviderType(providerType) { case constant.ProviderClaude: return string(constant.RelayFormatClaude) case constant.ProviderGemini, constant.ProviderVertex: return string(constant.RelayFormatGemini) default: return string(constant.RelayFormatOpenAI) } } // Submit 中间失败尝试:立即经钩子异步落库(is_final=false,段4为空——该尝试未向客户端写出字节) func (a *DebugAttempt) Submit(err error) { if a == nil { return } a.errText = debugErrText(err) if hook := SubmitDebugLog; hook != nil { hook(context.Background(), a.buildRecord(false, 0, 0)) } } // MarkFinal 标记为最终尝试(成功/终止/流中断):挂起等待外层 FinalizeAndSubmit 补段4后提交 func (a *DebugAttempt) MarkFinal(err error) { if a == nil { return } a.errText = debugErrText(err) a.session.pending = a } // FinalizeAndSubmit 补段4并提交最终尝试记录。由外层 handler 在客户端响应写完后调用 // (recordAudit 收尾处),幂等:重复调用或无挂起尝试时为 no-op。 func (s *DebugSession) FinalizeAndSubmit(totalLatencyMs, firstTokenMs int64) { if s == nil || s.finalized { return } s.finalized = true a := s.pending if a == nil { return } if hook := SubmitDebugLog; hook != nil { hook(context.Background(), a.buildRecord(true, totalLatencyMs, firstTokenMs)) } } // buildRecord 汇总四段与元数据构建落库记录。isFinal=true 时从 clientWriter 补段4。 func (a *DebugAttempt) buildRecord(isFinal bool, totalLatencyMs, firstTokenMs int64) *DebugLogRecord { s := a.session snap := a.Capture.snapshot() rec := &DebugLogRecord{ ChannelID: a.channelID, ChannelName: a.channelName, ChannelType: a.channelType, RequestID: s.requestID, TenantID: s.tenantID, UserID: s.userID, ApiKeyID: s.apiKeyID, ModelName: a.modelName, UpstreamModel: a.upstreamModel, RelayMode: a.relayMode, InboundPath: s.path, UpstreamURL: snap.URL, IsStream: a.isStream, RetryIndex: a.retryIndex, IsFinal: isFinal, UpstreamStatusCode: snap.Status, Error: a.errText, // 段1(会话级,全部尝试共享) ClientReqHeaders: s.clientReqHeaders, ClientReqBody: s.clientReqBody, // 段2/段3(传输层捕获) UpstreamReqHeaders: snap.ReqHeaders, UpstreamReqBody: snap.ReqBody, UpstreamRespHeaders: snap.RespHeaders, UpstreamRespBody: snap.RespBody, UpstreamLatencyMs: snap.LatencyMs, Conversion: a.conversion, } if snap.Err != "" && rec.Error == "" { // 传输层错误(连接失败等)未被上层 err 覆盖时兜底保留 rec.Error = snap.Err } if isFinal && s.clientWriter != nil { // 段4:客户端响应 rec.ClientRespHeaders = s.clientWriter.HeaderSnapshot() rec.ClientRespBody = s.clientWriter.Bytes() rec.ClientStatusCode = s.clientWriter.StatusCode() rec.TotalLatencyMs = totalLatencyMs rec.FirstTokenMs = firstTokenMs } return rec } func debugErrText(err error) string { if err == nil { return "" } return err.Error() }