From 60373a373d41b174d30b7df53fa1662f894aa665 Mon Sep 17 00:00:00 2001 From: qian Date: Mon, 3 Aug 2026 00:00:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(relay):=20=E4=BF=AE=E5=A4=8D=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E6=96=AD=E5=BC=80=E6=97=B6=E8=AF=AF=E4=BC=A4?= =?UTF-8?q?=E6=B8=A0=E9=81=93=E5=81=A5=E5=BA=B7=E8=AF=84=E5=88=86=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 客户端断开连接(context.Canceled / DeadlineExceeded)时,原来的行为: 1. MaterializeSelection 调用 getChannelKey(ctx, ...) 因 ctx 已取消而失败 2. 失败被包装为 ErrChannelUnavailable(掩盖真实原因) 3. reportMaterializeFailure 对渠道记了一次健康分惩罚 实际上渠道本身完全正常,是客户端主动断开导致的。 修复: - MaterializeSelection:key 查询前后各加一次 ctx.Err() 检查, 有 ctx 错误时直接返回 ctx.Err() 而非 ErrChannelUnavailable - relay_handler.go 调度循环:MaterializeSelection 返回 context 错误时 静默退出(不记 channelErrors、不调 reportMaterializeFailure), 用 context.WithoutCancel 退还预扣款项后直接返回,渠道健康分不受影响 --- internal/logic/relay/provider.go | 10 ++++++++++ relay/handler/relay_handler.go | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/logic/relay/provider.go b/internal/logic/relay/provider.go index 1d09a25..2d5c428 100644 --- a/internal/logic/relay/provider.go +++ b/internal/logic/relay/provider.go @@ -866,6 +866,12 @@ func InitHealthScore(ctx context.Context, channelID int64) error { // MaterializeSelection 由新调度引擎的决策构造 ChannelSelection(阶段 3): // 转发元数据来自目录内存快照(零 DB),渠道 Key 按 keyID 解密(keyID=0 回退取首个 active Key)。 func (p *DataProviderImpl) MaterializeSelection(ctx context.Context, channelID, keyID int64, modelName string) (*common.ChannelSelection, error) { + // 优先检查上下文:客户端已断开时直接返回 ctx.Err(),而非 ErrChannelUnavailable, + // 让上层能区分"客户端断开"与"真实渠道无可用 Key",避免误伤渠道健康评分 + if err := ctx.Err(); err != nil { + return nil, err + } + catalog := dispatchadapter.CatalogInstance() if catalog == nil { return nil, common.ErrChannelUnavailable @@ -883,6 +889,10 @@ func (p *DataProviderImpl) MaterializeSelection(ctx context.Context, channelID, apiKey, err = getChannelKey(ctx, channelID) } if err != nil || apiKey == "" { + // Key 查询失败:再次检查 ctx,区分"客户端断开导致查询中断"与"真实无可用 Key" + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } return nil, common.ErrChannelUnavailable } diff --git a/relay/handler/relay_handler.go b/relay/handler/relay_handler.go index 46fe81a..5aace7c 100644 --- a/relay/handler/relay_handler.go +++ b/relay/handler/relay_handler.go @@ -480,6 +480,14 @@ func RelayHandler(ctx context.Context, body []byte, path string, headers http.He selection, mErr := provider.MaterializeSelection(ctx, d.Channel.ID, d.KeyID, v.lookupModel) if mErr != nil { + // 客户端已断开(context.Canceled / DeadlineExceeded):Key 查询被中断不代表渠道有问题, + // 静默退出,不记入 channelErrors,不触发渠道健康惩罚,退还预扣款项后返回 + if errors.Is(mErr, context.Canceled) || errors.Is(mErr, context.DeadlineExceeded) { + if billing != nil && preDeductAmount > 0 { + _ = billing.SettleFailed(context.WithoutCancel(ctx), rc.TenantID, rc.RequestID, preDeductAmount) + } + return nil, v.billingResult, mErr + } // Key 解密失败 / 目录元数据缺失:按渠道级致命上报换渠道,不发起上游请求 channelErrors = append(channelErrors, fmt.Sprintf("attempt=%d channel=%d materialize_error=[%v]", attempt, d.Channel.ID, mErr)) if decision := reportMaterializeFailure(ctx, sess, mErr); decision == dispatch.DecisionAbort {