Selaa lähdekoodia

feat: 错误日志记录上游 request-id 和响应体,Playground 渠道路由改为 header 传递

- 错误日志新增 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 <noreply@anthropic.com>
master
fengsilin 1 viikko sitten
vanhempi
commit
73d10b5799
6 muutettua tiedostoa jossa 69 lisäystä ja 18 poistoa
  1. +6
    -0
      controller/relay.go
  2. +9
    -1
      middleware/distributor.go
  3. +18
    -0
      service/error.go
  4. +10
    -8
      types/error.go
  5. +7
    -5
      web/src/hooks/playground/useApiRequest.jsx
  6. +19
    -4
      web/src/pages/Playground/index.jsx

+ 6
- 0
controller/relay.go Näytä tiedosto

@@ -369,6 +369,12 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t
other["channel_id"] = channelId other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name") other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type") 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 := make(map[string]interface{})
adminInfo["use_channel"] = c.GetStringSlice("use_channel") adminInfo["use_channel"] = c.GetStringSlice("use_channel")
isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey) isMultiKey := common.GetContextKeyBool(c, constant.ContextKeyChannelIsMultiKey)


+ 9
- 1
middleware/distributor.go Näytä tiedosto

@@ -367,10 +367,18 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) {
return nil, false, err return nil, false, err
} }
modelRequest.Model = req.Model 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) common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group)
// channel_id: body 优先,fallback 到 header
if req.ChannelId > 0 { if req.ChannelId > 0 {
common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId)) common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId))
} else if ch := c.GetHeader("X-Channel-Id"); ch != "" {
common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, ch)
} }
} }




+ 18
- 0
service/error.go Näytä tiedosto

@@ -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) { func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFail bool) (newApiErr *types.NewAPIError) {
newApiErr = types.InitOpenAIError(types.ErrorCodeBadResponseStatusCode, resp.StatusCode) 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) responseBody, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return return
} }
CloseResponseBodyGracefully(resp) 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 var errResponse dto.GeneralErrorResponse
buildErrWithBody := func(message string) error { buildErrWithBody := func(message string) error {
if message == "" { if message == "" {
@@ -115,6 +129,8 @@ func RelayErrorHandler(ctx context.Context, resp *http.Response, showBodyWhenFai
oaiError := errResponse.TryToOpenAIError() oaiError := errResponse.TryToOpenAIError()
if oaiError != nil { if oaiError != nil {
newApiErr = types.WithOpenAIError(*oaiError, resp.StatusCode) newApiErr = types.WithOpenAIError(*oaiError, resp.StatusCode)
newApiErr.UpstreamRequestId = upstreamReqId
newApiErr.UpstreamBody = bodyStr
if showBodyWhenFail { if showBodyWhenFail {
newApiErr.Err = buildErrWithBody(newApiErr.Error()) 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 = types.NewOpenAIError(errors.New(errResponse.ToMessage()), types.ErrorCodeBadResponseStatusCode, resp.StatusCode)
newApiErr.UpstreamRequestId = upstreamReqId
newApiErr.UpstreamBody = bodyStr
if showBodyWhenFail { if showBodyWhenFail {
newApiErr.Err = buildErrWithBody(newApiErr.Error()) newApiErr.Err = buildErrWithBody(newApiErr.Error())
} }


+ 10
- 8
types/error.go Näytä tiedosto

@@ -88,14 +88,16 @@ const (
) )


type NewAPIError struct { 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. // Unwrap enables errors.Is / errors.As to work with NewAPIError by exposing the underlying error.


+ 7
- 5
web/src/hooks/playground/useApiRequest.jsx Näytä tiedosto

@@ -173,7 +173,7 @@ export const useApiRequest = (


// 非流式请求 // 非流式请求
const handleNonStreamRequest = useCallback( const handleNonStreamRequest = useCallback(
async (payload) => {
async (payload, extraHeaders = {}) => {
setDebugData((prev) => ({ setDebugData((prev) => ({
...prev, ...prev,
request: payload, request: payload,
@@ -190,6 +190,7 @@ export const useApiRequest = (
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(), 'New-Api-User': getUserIdFromLocalStorage(),
...extraHeaders,
}, },
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
@@ -290,7 +291,7 @@ export const useApiRequest = (


// SSE请求 // SSE请求
const handleSSE = useCallback( const handleSSE = useCallback(
(payload) => {
(payload, extraHeaders = {}) => {
setDebugData((prev) => ({ setDebugData((prev) => ({
...prev, ...prev,
request: payload, request: payload,
@@ -305,6 +306,7 @@ export const useApiRequest = (
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'New-Api-User': getUserIdFromLocalStorage(), 'New-Api-User': getUserIdFromLocalStorage(),
...extraHeaders,
}, },
method: 'POST', method: 'POST',
payload: JSON.stringify(payload), payload: JSON.stringify(payload),
@@ -499,11 +501,11 @@ export const useApiRequest = (


// 发送请求 // 发送请求
const sendRequest = useCallback( const sendRequest = useCallback(
(payload, isStream) => {
(payload, isStream, extraHeaders = {}) => {
if (isStream) { if (isStream) {
handleSSE(payload);
handleSSE(payload, extraHeaders);
} else { } else {
handleNonStreamRequest(payload);
handleNonStreamRequest(payload, extraHeaders);
} }
}, },
[handleSSE, handleNonStreamRequest], [handleSSE, handleNonStreamRequest],


+ 19
- 4
web/src/pages/Playground/index.jsx Näytä tiedosto

@@ -286,8 +286,17 @@ const Playground = () => {
setMessage((prevMessage) => { setMessage((prevMessage) => {
const newMessages = [...prevMessage, userMessage, loadingMessage]; 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); setTimeout(() => saveMessagesImmediately(newMessages), 0);
@@ -323,10 +332,16 @@ const Playground = () => {
inputs, inputs,
parameterEnabled, parameterEnabled,
); );
const extraHeaders = {};
if (inputs.channelId && inputs.channelId > 0) { 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) { if (inputs.imageEnabled) {


Ladataan…
Peruuta
Tallenna