ソースを参照

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週間前
コミット
73d10b5799
6個のファイルの変更69行の追加18行の削除
  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 ファイルの表示

@@ -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)


+ 9
- 1
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)
}
}



+ 18
- 0
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())
}


+ 10
- 8
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.


+ 7
- 5
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],


+ 19
- 4
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) {


読み込み中…
キャンセル
保存