From 73d10b57991931a0345fad986b1bea163ad6f92b Mon Sep 17 00:00:00 2001 From: fengsilin Date: Wed, 29 Apr 2026 09:40:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E4=B8=8A=E6=B8=B8=20request-id=20=E5=92=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E4=BD=93=EF=BC=8CPlayground=20=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E8=B7=AF=E7=94=B1=E6=94=B9=E4=B8=BA=20header=20?= =?UTF-8?q?=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 错误日志新增 upstream_request_id(从 Anthropic/OpenAI 响应 header 提取)和 upstream_body(截断 2KB) - 修复 RelayErrorHandler 内部 WithOpenAIError/NewOpenAIError 分支丢失上游字段的 bug - Playground 渠道和分组改为通过 X-Channel-Id/X-Group header 传递,而非 body 字段 - Distributor 中间件支持从 header 回退读取 channel_id 和 group Co-Authored-By: Claude --- controller/relay.go | 6 ++++++ middleware/distributor.go | 10 +++++++++- service/error.go | 18 +++++++++++++++++ types/error.go | 18 +++++++++-------- web/src/hooks/playground/useApiRequest.jsx | 12 ++++++----- web/src/pages/Playground/index.jsx | 23 ++++++++++++++++++---- 6 files changed, 69 insertions(+), 18 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index 713f270..687ddeb 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -369,6 +369,12 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t other["channel_id"] = channelId other["channel_name"] = c.GetString("channel_name") other["channel_type"] = c.GetInt("channel_type") + if err.UpstreamRequestId != "" { + other["upstream_request_id"] = err.UpstreamRequestId + } + if err.UpstreamBody != "" { + other["upstream_body"] = err.UpstreamBody + } adminInfo := make(map[string]interface{}) adminInfo["use_channel"] = c.GetStringSlice("use_channel") isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey) diff --git a/middleware/distributor.go b/middleware/distributor.go index 053ab21..4a94aca 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -367,10 +367,18 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { return nil, false, err } modelRequest.Model = req.Model - modelRequest.Group = req.Group + // group: body 优先,fallback 到 header + if req.Group != "" { + modelRequest.Group = req.Group + } else if g := c.GetHeader("X-Group"); g != "" { + modelRequest.Group = g + } common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group) + // channel_id: body 优先,fallback 到 header if req.ChannelId > 0 { common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId)) + } else if ch := c.GetHeader("X-Channel-Id"); ch != "" { + common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, ch) } } diff --git a/service/error.go b/service/error.go index a2ff0aa..bd242e8 100644 --- a/service/error.go +++ b/service/error.go @@ -86,11 +86,25 @@ func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.Claude func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) { newApiErr = types.InitOpenAIError(types.ErrorCodeBadResponseStatusCode, resp.StatusCode) + // Capture upstream request-id from response headers + upstreamReqId := resp.Header.Get("request-id") + if upstreamReqId == "" { + upstreamReqId = resp.Header.Get("x-request-id") + } + newApiErr.UpstreamRequestId = upstreamReqId + responseBody, err := io.ReadAll(resp.Body) if err != nil { return } CloseResponseBodyGracefully(resp) + // Capture upstream response body (truncate to 2KB to avoid oversized logs) + const maxBodyLen = 2048 + bodyStr := string(responseBody) + if len(bodyStr) > maxBodyLen { + bodyStr = bodyStr[:maxBodyLen] + "...(truncated)" + } + newApiErr.UpstreamBody = bodyStr var errResponse dto.GeneralErrorResponse buildErrWithBody := func(message string) error { if message == "" { @@ -115,6 +129,8 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai oaiError := errResponse.TryToOpenAIError() if oaiError != nil { newApiErr = types.WithOpenAIError(*oaiError, resp.StatusCode) + newApiErr.UpstreamRequestId = upstreamReqId + newApiErr.UpstreamBody = bodyStr if showBodyWhenFail { newApiErr.Err = buildErrWithBody(newApiErr.Error()) } @@ -122,6 +138,8 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai } } newApiErr = types.NewOpenAIError(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode) + newApiErr.UpstreamRequestId = upstreamReqId + newApiErr.UpstreamBody = bodyStr if showBodyWhenFail { newApiErr.Err = buildErrWithBody(newApiErr.Error()) } diff --git a/types/error.go b/types/error.go index 6af39f7..4e58c2f 100644 --- a/types/error.go +++ b/types/error.go @@ -88,14 +88,16 @@ const ( ) type NewAPIError struct { - Err error - RelayError any - skipRetry bool - recordErrorLog *bool - errorType ErrorType - errorCode ErrorCode - StatusCode int - Metadata json.RawMessage + Err error + RelayError any + skipRetry bool + recordErrorLog *bool + errorType ErrorType + errorCode ErrorCode + StatusCode int + Metadata json.RawMessage + UpstreamRequestId string + UpstreamBody string } // Unwrap enables errors.Is / errors.As to work with NewAPIError by exposing the underlying error. diff --git a/web/src/hooks/playground/useApiRequest.jsx b/web/src/hooks/playground/useApiRequest.jsx index 8ec50cf..853a8d2 100644 --- a/web/src/hooks/playground/useApiRequest.jsx +++ b/web/src/hooks/playground/useApiRequest.jsx @@ -173,7 +173,7 @@ export const useApiRequest = ( // 非流式请求 const handleNonStreamRequest = useCallback( - async (payload) => { + async (payload, extraHeaders = {}) => { setDebugData((prev) => ({ ...prev, request: payload, @@ -190,6 +190,7 @@ export const useApiRequest = ( headers: { 'Content-Type': 'application/json', 'New-Api-User': getUserIdFromLocalStorage(), + ...extraHeaders, }, body: JSON.stringify(payload), }); @@ -290,7 +291,7 @@ export const useApiRequest = ( // SSE请求 const handleSSE = useCallback( - (payload) => { + (payload, extraHeaders = {}) => { setDebugData((prev) => ({ ...prev, request: payload, @@ -305,6 +306,7 @@ export const useApiRequest = ( headers: { 'Content-Type': 'application/json', 'New-Api-User': getUserIdFromLocalStorage(), + ...extraHeaders, }, method: 'POST', payload: JSON.stringify(payload), @@ -499,11 +501,11 @@ export const useApiRequest = ( // 发送请求 const sendRequest = useCallback( - (payload, isStream) => { + (payload, isStream, extraHeaders = {}) => { if (isStream) { - handleSSE(payload); + handleSSE(payload, extraHeaders); } else { - handleNonStreamRequest(payload); + handleNonStreamRequest(payload, extraHeaders); } }, [handleSSE, handleNonStreamRequest], diff --git a/web/src/pages/Playground/index.jsx b/web/src/pages/Playground/index.jsx index 8505bd6..75f90ef 100644 --- a/web/src/pages/Playground/index.jsx +++ b/web/src/pages/Playground/index.jsx @@ -286,8 +286,17 @@ const Playground = () => { setMessage((prevMessage) => { const newMessages = [...prevMessage, userMessage, loadingMessage]; - // 发送自定义请求体 - sendRequest(customPayload, customPayload.stream !== false); + // 自定义请求体也通过 header 传递网关字段 + const customHeaders = {}; + if (inputs.channelId && inputs.channelId > 0) { + customHeaders['X-Channel-Id'] = String(inputs.channelId); + } + if (customPayload.group) { + customHeaders['X-Group'] = customPayload.group; + delete customPayload.group; + } + delete customPayload.channel_id; + sendRequest(customPayload, customPayload.stream !== false, customHeaders); // 发送消息后保存,传入新消息列表 setTimeout(() => saveMessagesImmediately(newMessages), 0); @@ -323,10 +332,16 @@ const Playground = () => { inputs, parameterEnabled, ); + const extraHeaders = {}; if (inputs.channelId && inputs.channelId > 0) { - payload.channel_id = inputs.channelId; + extraHeaders['X-Channel-Id'] = String(inputs.channelId); + } + if (payload.group) { + extraHeaders['X-Group'] = payload.group; + delete payload.group; } - sendRequest(payload, inputs.stream); + delete payload.channel_id; + sendRequest(payload, inputs.stream, extraHeaders); // 禁用图片模式 if (inputs.imageEnabled) {