* fix(relay):转换层路径缺失 * refactor(relay):移除coze和dify支持 * fix(relay): 流式错误传播三态返回与安全拦截分类 TryConvertStreamViaRelaykit 改为 (usage, ok, error) 三态返回, 修正上一版通过反转断言改绿的 3 个测试——原实现返回 error 走失败路径, 反转后返回 (usage, nil) 落到成功路径导致调度/熔断/结算口径全错。 - stream.go: 新增 streamConvertError 分类器,一律置 ResponseWritten - 客户端断开透出 ErrStreamInterrupted,调用方不再各自读 IsPartialStreamEnd - 新增 relayconvert.ErrContentBlocked 哨兵,6 处 Gemini 出站安全拦截 wrap - response.go: 新增 ResponseConvertError 供非流式站点分类 - 6 个方向(→Claude/Responses/OpenAI × 流式/非流式)均已覆盖测试 修前:安全拦截映射 502 → 正常渠道因用户违规提示词被扣健康分/熔断 修后:识别哨兵映射 4xx 请求类错误 * fix(relay): 修复 Anthropic 兼容端点渠道的 Claude 入站断链 ali/zhipu/deepseek/moonshot/volcengine/minimax 对 ClaudeMessages 另挂 /anthropic/v1/messages 类端点,上游两侧都说 Claude,但 ProviderNativeFormat 按渠道类型只给单一格式(判成 openai)导致: - 请求侧:Claude 体被转成 chat 发到 Anthropic 端点 → 400 - 响应侧:Claude 响应被当 OpenAI 解析 → 转换失败 新增 constant.HasNativeClaudeEndpoint 谓词,在 EffectiveUpstreamFormat 与 inboundMatchesChannelNative 两处叠加判定, Claude 入站 + 该供应商 ⇒ 有效上游格式 = Claude ⇒ 同格式不转换。 登记判据:GetRequestURL 对 ClaudeMessages 返回与 chat 不同的地址。 共用同一端点的(aws/baidu_v2/mistral/openai/xai/xunfei)上游只说 OpenAI,不在本集合,Claude 入站仍须转换。 * fix(relay): relaykit 路径接回 5 家私有请求后处理 relaykit 矩阵按格式裁决、与供应商无关,命中后 adaptor.ConvertRequest 整个不再被调用。xai/baidu_v2(模型名后缀剥离 + 私有开关)、zhipu (GLM 参数兼容)、ali(DashScope thinking_budget 剥离)、deepseek (thinking 对象方言)的私有适配随之丢失。 最实际的是模型名:未配映射时 UpstreamModel = row.ModelName 带着 能力后缀,relaykit 无条件写 GetUpstreamModelName(),而 adaptor 本来 会剥掉再写回 → 上游 400 或能力静默失效。 新增可选能力接口 RequestPostProcessor,handler.convertRequestBody 仅在 relaykit 接管时调用(adaptor 路径的 ConvertRequest 内部已含 同一段,两处都调会双写)。5 家各把尾段抽成 postProcessRequest 与钩子 共用。新增接口断言与功能测试。 * docs(relaykit): 更新工作状态与回归记录 - §5.1/§5.2 已完成,新增 §3.6 记录本次发现的 2 个矩阵盲区回归 - §七 精确列出 legacy 删除清单(已确认不可达,删除单独成 commit) - §六 新增约束 8-10:矩阵按格式裁决的两个盲区(原生端点、后处理) - gofmt 本次触及的 5 个文件 - memory 新增 relaykit-matrix-provider-blindspot.md 根因记录 * refactor(relay): 删除被 relaykit 取代的 legacy 协议转换代码 净删约 6400 行。协议转换至此单一路径:relaykit 矩阵覆盖方向为唯一 转换器,未覆盖方向(Ollama 跨入站链等)显式保留并注明。 - 删 claude/gemini converter.go 整文件(o2c/o2g 请求侧双实现) - 删 openai/converter.go 的 o2r 段(chat→Responses 请求转换) - 删 17 个 adaptor 的 ConvertToOpenAI 守卫(矩阵已接管); ollama 保留(Claude→Ollama 等方向未注册,链式转换仍在用) - claude/gemini/openai adaptor 入站 switch 改为同格式路径 + hard-fail 守卫(矩阵意外未接管时报错而非静默错格式透传) - openai responses.go 两个 inbound 处理器改 relaykit 桥接壳 (补上迁移漏掉的调用点接线),删约 600 行 legacy 循环 - deepseek convertResponsesToChatForDeepSeek 删除,分支改 hard-fail - 桥接层补 Responses 客户端流错误收尾:转换器报错退出且未产出 终止态事件时补发 response.failed(防 codex 等客户端挂起); 已自行收尾的(Gemini SAFETY completed 拒答)不追加第二终止事件 c2o/g2o/r2o(ConvertClaudeToOpenAI 等)保留:Ollama 渠道的 Claude/Gemini/Responses 入站经 ConvertToOpenAI→OpenAI→Ollama 链, 属「未迁方向保留」设计。 测试:新增删除项的替代断言(hard-fail 守卫、PostProcess 钩子、 response.failed 收尾);删测已删函数的用例。全仓 build/vet 通过, relaykit 全绿,relay 侧仅剩 helper 既有失败。 * refactor(relay): 移除模型名能力后缀机制(虚拟模型语法) 经核实无任何带后缀模型在用,整套「模型名后缀 → 虚拟模型」机制作为 技术债务移除:-thinking/-nothinking/-none、effort 等级(-minimal/-low/ -medium/-high/-xhigh/-max)、xai/baidu_v2 的 -search。思考/搜索能力 此后一律由客户端请求参数表达(thinking 对象、reasoning_effort、 reasoning.effort),网关只做协议转换不做能力注入。 宿主侧: - 删 helper/thinking.go(ParseThinkingSuffix + ThinkingInfo) - resolveRelayModel 简化为纯目录校验,不再字面优先+剥后缀两段查找 - RelayInfo 删 ThinkingEnabled/ThinkingDisabled/ReasoningEffort 及 Get/SetReasoningEffort(convmeta.Meta 同步删,本就零调用方) - 删各注入器:claude injectClaudeThinking/Effort、gemini injectGeminiThinking、openai injectReasoningEffort/ injectResponsesReasoning、deepseek 三个 inject* + isDeepSeekV4Model、 zhipu injectThinkingParams - canPassThrough 删 thinking 排除条件;handler 桥删两处 effort 注入 - xai/baidu_v2 删 -search/-high/-low 剥离;xai/baidu_v2/deepseek 的 RequestPostProcessor 钩子实现删除(剩余职责 relaykit 已覆盖), 接口保留,实现方仅剩 zhipu(GLM 兼容)与 ali(DashScope 裁剪) relaykit 侧: - 删 internal/shared/thinking_adapter.go 与 reasoning/ 包 - convmeta.Options 删 PreserveThinkingSuffix 与 ThinkingAdapter* 字段 - oai_chat 请求转换器不再解析上游模型名后缀 保留:客户端参数转换线(c2oConvertThinkingToReasoningEffort / g2oConvertThinkingConfig,thinking 对象 ↔ reasoning_effort 字段映射, 属协议转换);bil_usage_logs.reasoning_effort 列(此后恒空); BaseModelName 字段(responses 路由记录在用)。 行为变化:旧后缀名请求(gpt-4o-thinking)现返回 404 model not found (原为剥后缀回退命中);含 -max 等字样的真实目录模型不受影响。 全仓 build/vet 通过,relaykit 全绿,relay 侧仅剩 helper 既有失败。 * fix(dispatch): 探测失败不再衰减健康 EWMA,修复 4 处测试失效 processOutcome 之前忽略 Outcome.Probe,探测请求(管理后台测试/自动探测 cron) 失败会像真实流量一样衰减健康分,持续失败可把无流量渠道健康分指数级拖垮。 现探测失败跳过健康 EWMA 写入(探测成功仍照常记录),熔断窗口计数不受影响。 顺带修复 3 处与实现漂移的过期测试: - admin 角色迁移文件路径引用了已重命名的迁移脚本 - billing 账单摘要在无逐项 token 明细时漏了小计×倍率行(同时同步了乘数标签改名) - relay/helper 错误提示文案测试仍留着改中文前的英文期望值 * refactor(relay):补充gemini和claude协议的转换 * fix(relay):修复gemini转换问题 * fix(relay):优化工具转换 * feat(test):添加测试用例 * feat(test):添加测试用例 * feat(relay):补充转换矩阵 * feat(relay):补充协议转换测试 * fix(relay):修复vertex缺陷 * feat(relay):补充协议间的工具转换 * feat(relay):添加语料采集功能 * fix(relay):修复claude输出协议转换问题 * fix(relay):修复codex请求claude模型无法使用搜索工具问题 * fix(relay):修复codex请求gemini模型无法使用搜索工具问题 * refactor(relay):修改真实语料处理逻辑,将真实响应数据移出仓库 * fix(relay):补充手写测试数据集
254 lines
13 KiB
Go
254 lines
13 KiB
Go
package handler
|
||
|
||
// matrix_guard_test.go — relaykit 矩阵接管的供应商维度守卫。
|
||
//
|
||
// 背景:relaykit 转换矩阵只按协议格式裁决(入站格式 × 有效上游格式),与供应商无关;
|
||
// 方向命中后 adaptor.ConvertRequest 整个不再被调用。但供应商差异是(供应商 × RelayMode)
|
||
// 维度的,两个维度不对齐时矩阵会在自己看不见的地方替供应商做错决定,且不报错——
|
||
// 历史上已因此产生 4 个静默断链回归:
|
||
// 1. Anthropic 兼容端点漏登记(HasNativeClaudeEndpoint)→ Claude 体转成 chat 发到
|
||
// Anthropic 端点、响应又按 OpenAI 解析,两侧同时断链;
|
||
// 2. 供应商私有后处理(RequestPostProcessor)丢失或双写;
|
||
// 3. 目标 adaptor 的 GetRequestURL 不认入站 RelayMode → DoRequest 报
|
||
// "unsupported relay mode"、0ms 断链(bridge 测试只直测 DoResponse,测不到);
|
||
// 4. 多协议原生透传渠道(multinative)配模型映射时被矩阵按主协议 OpenAI 接管,
|
||
// chat 体发到 /v1/messages 原生端点,体与端点错配。
|
||
//
|
||
// 本文件把「新增/修改转换方向必查三条」的人工评审清单固化为自动化测试:
|
||
// 枚举 registry 全部供应商 × 四种文本入站,任何新增供应商 / 新增方向 / 改动判定函数
|
||
// 若破坏一致性会立即失败,不再依赖人记住检查项。
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/qianfree/team-api/relay/channel"
|
||
"github.com/qianfree/team-api/relay/common"
|
||
"github.com/qianfree/team-api/relay/constant"
|
||
"github.com/qianfree/team-api/relay/helper"
|
||
"github.com/qianfree/team-api/relay/relaykit_bridge"
|
||
)
|
||
|
||
// matrixInboundCombos 文本代理的四种入站(格式 × RelayMode)组合,与各入站 handler
|
||
// 的真实取值一致(mode 随请求全程不变,直达 adaptor.GetRequestURL)。
|
||
var matrixInboundCombos = []struct {
|
||
inbound constant.RelayFormat
|
||
mode constant.RelayMode
|
||
}{
|
||
{constant.RelayFormatOpenAI, constant.RelayModeChatCompletions},
|
||
{constant.RelayFormatClaude, constant.RelayModeClaudeMessages},
|
||
{constant.RelayFormatGemini, constant.RelayModeGeminiChat},
|
||
{constant.RelayFormatResponses, constant.RelayModeResponses},
|
||
}
|
||
|
||
// guardProviders 枚举 registry 中全部有文本 adaptor 的供应商类型。
|
||
// 上界取宽(现有编号最大 42),新增供应商自动纳入。
|
||
func guardProviders() []constant.ProviderType {
|
||
var out []constant.ProviderType
|
||
for p := 1; p <= 64; p++ {
|
||
if channel.GetAdaptor(p) != nil {
|
||
out = append(out, constant.ProviderType(p))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// guardChannelMeta 构造最小可用渠道元数据(个别 adaptor 对 ApiKey 形态有硬要求)。
|
||
func guardChannelMeta(p constant.ProviderType) *common.ChannelMeta {
|
||
meta := &common.ChannelMeta{
|
||
ChannelType: int(p),
|
||
BaseURL: "https://upstream.example.com",
|
||
ApiKey: "sk-guard-test",
|
||
UpstreamModelName: "guard-model",
|
||
}
|
||
if p == constant.ProviderCloudflare {
|
||
meta.ApiKey = "cf-token|cf-account" // cloudflare 的 URL 需要 accountID(token|accountid 格式)
|
||
}
|
||
return meta
|
||
}
|
||
|
||
func guardInfo(p constant.ProviderType, inbound constant.RelayFormat, mode constant.RelayMode) *common.RelayInfo {
|
||
return &common.RelayInfo{
|
||
InboundFormat: inbound,
|
||
RelayMode: int(mode),
|
||
ChannelMeta: guardChannelMeta(p),
|
||
}
|
||
}
|
||
|
||
// matrixTakeoverExclusions 有意不参加「接管方向 URL 可达」守卫的供应商,必须注明原因。
|
||
// 新增排除项前先确认它不是第 3 类断链——排除即承诺该组合在调度层不可达。
|
||
var matrixTakeoverExclusions = map[constant.ProviderType]string{
|
||
// codex 是 responses-only 上游(GetRequestURL 有意只认 Responses mode)。
|
||
// 非 Responses 入站按主协议 OpenAI 参与矩阵裁决,但 codex 端点吃不了 chat 体。
|
||
// 已知缺口:依赖渠道只挂 responses 系模型 + 调度 proto 门控避免命中;
|
||
// 若未来放开 codex 承接 chat/claude/gemini 入站,须先实现相应桥接再移除本排除。
|
||
constant.ProviderCodex: "responses-only 上游,非 Responses 入站不可达(渠道模型配置 + 调度门控约束)",
|
||
// jimeng 是图片生成专用上游(GetRequestURL 有意只认 ImagesGenerations mode)。
|
||
// 文本类入站不可达:jimeng 渠道只会挂图片模型,chat/claude/gemini/responses
|
||
// 请求不会被调度到该渠道。
|
||
constant.ProviderJimeng: "图片生成专用上游,文本类入站不可达(渠道只挂图片模型)",
|
||
}
|
||
|
||
// TestMatrixTakeover_AdaptorAcceptsInboundMode 守卫第 3 类断链:
|
||
// 矩阵接管的每个(供应商 × 入站)组合,目标 adaptor 的 GetRequestURL 必须认该入站
|
||
// RelayMode——矩阵已把请求体转成上游格式,URL 拿不到就是 DoRequest 处 0ms 断链。
|
||
func TestMatrixTakeover_AdaptorAcceptsInboundMode(t *testing.T) {
|
||
for _, p := range guardProviders() {
|
||
if reason, excluded := matrixTakeoverExclusions[p]; excluded {
|
||
t.Logf("跳过 %s(%d):%s", p.String(), p, reason)
|
||
continue
|
||
}
|
||
for _, combo := range matrixInboundCombos {
|
||
info := guardInfo(p, combo.inbound, combo.mode)
|
||
upstream := relaykit_bridge.EffectiveUpstreamFormat(info)
|
||
converterID := relaykit_bridge.RequestConverterIDForRoute(combo.inbound, upstream, info.RelayMode)
|
||
if converterID == "" {
|
||
continue // 同格式直连或矩阵未覆盖方向,不经 relaykit 接管
|
||
}
|
||
|
||
adaptor := channel.GetAdaptor(int(p))
|
||
adaptor.Init(info)
|
||
url, err := adaptor.GetRequestURL(info)
|
||
if err != nil {
|
||
t.Errorf("%s(%d) 的 %s 入站(mode=%d)被矩阵接管(%s),但 GetRequestURL 报错:%v\n"+
|
||
"→ 该组合会在 DoRequest 处 0ms 断链。修复:让该 adaptor 的 GetRequestURL 认此 mode"+
|
||
"(矩阵已把请求体转成 %s 格式,通常应返回对应格式的主端点)",
|
||
p.String(), p, combo.inbound, combo.mode, converterID, err, upstream)
|
||
continue
|
||
}
|
||
if url == "" {
|
||
t.Errorf("%s(%d) 的 %s 入站:GetRequestURL 返回空 URL", p.String(), p, combo.inbound)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestMatrixGuard_PassthroughNeverConverted 守卫第 1/4 类断链的共同根因:
|
||
// 两个判定函数必须口径一致——被 inboundMatchesChannelNative 判为「原生匹配」的
|
||
// (供应商 × 入站),矩阵必须不产生转换器(有效上游格式 = 入站格式)。
|
||
//
|
||
// 口径不一致时,同一请求在直连与转换两条路上得到不同格式结论:无映射/改写时直连正常,
|
||
// 一旦配置模型映射(canPassThrough=false 强制走转换路径)矩阵就按错误方向转换体,
|
||
// 而 adaptor 仍按原生格式选端点——即 multinative 断链(第 4 例)与 Anthropic
|
||
// 端点断链(第 1 例)的完整机理。
|
||
func TestMatrixGuard_PassthroughNeverConverted(t *testing.T) {
|
||
for _, p := range guardProviders() {
|
||
for _, combo := range matrixInboundCombos {
|
||
info := guardInfo(p, combo.inbound, combo.mode)
|
||
if !inboundMatchesChannelNative(info) {
|
||
continue
|
||
}
|
||
upstream := relaykit_bridge.EffectiveUpstreamFormat(info)
|
||
if id := relaykit_bridge.RequestConverterIDForRoute(combo.inbound, upstream, info.RelayMode); id != "" {
|
||
t.Errorf("%s(%d) 的 %s 入站被判为原生匹配(可直连),但矩阵仍会转换(%s,有效上游=%s)\n"+
|
||
"→ 配置模型映射后请求体会被转成 %s 而 adaptor 按 %s 原生端点发送,体与端点错配。"+
|
||
"检查 EffectiveUpstreamFormat 与 inboundMatchesChannelNative 是否同步",
|
||
p.String(), p, combo.inbound, id, upstream, upstream, combo.inbound)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Responses 上游声明(supports_responses)同样两函数联动:原生匹配 ⇒ 不转换
|
||
for _, p := range []constant.ProviderType{constant.ProviderOpenAI, constant.ProviderDeepSeek} {
|
||
info := guardInfo(p, constant.RelayFormatResponses, constant.RelayModeResponses)
|
||
info.ChannelMeta.SupportsResponses = true
|
||
if !inboundMatchesChannelNative(info) {
|
||
t.Errorf("%s(%d):supports_responses 渠道的 Responses 入站应判为原生匹配", p.String(), p)
|
||
}
|
||
upstream := relaykit_bridge.EffectiveUpstreamFormat(info)
|
||
if id := relaykit_bridge.RequestConverterIDForRoute(constant.RelayFormatResponses, upstream, info.RelayMode); id != "" {
|
||
t.Errorf("%s(%d):supports_responses 渠道的 Responses 入站不应被矩阵转换(got %s)", p.String(), p, id)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestMatrixGuard_NativeClaudeEndpointRegistry 守卫第 1 类断链的登记完整性:
|
||
// HasNativeClaudeEndpoint 集合必须与各 adaptor 的真实端点行为一致。
|
||
// 判据(与集合注释一致):GetRequestURL 对 ClaudeMessages 返回与 chat **不同**的地址
|
||
// ⇔ 该供应商另挂 Anthropic 兼容端点 ⇔ 必须登记。
|
||
//
|
||
// - 挂了独立端点却未登记 → Claude 入站被转成 chat 发到 Anthropic 端点(断链);
|
||
// - 未挂端点却登记了 → Claude 入站被直连到 chat 端点(chat 端点吃不了 Claude 体)。
|
||
func TestMatrixGuard_NativeClaudeEndpointRegistry(t *testing.T) {
|
||
for _, p := range guardProviders() {
|
||
// 只考察主协议 OpenAI 的供应商(集合语义即「主协议 OpenAI + 另挂 Anthropic 端点」)
|
||
if helper.ProviderNativeFormat(int(p)) != constant.RelayFormatOpenAI {
|
||
continue
|
||
}
|
||
// 多协议原生透传渠道由 IsMultiNativeProvider 独立判定(三种入站全部原生直连),
|
||
// 不属于「另挂 Anthropic 端点」语义
|
||
if constant.IsMultiNativeProvider(int(p)) {
|
||
continue
|
||
}
|
||
|
||
// Claude 入站的 URL(按真实链路:Claude 入站 + ClaudeMessages mode 初始化 adaptor)
|
||
claudeInfo := guardInfo(p, constant.RelayFormatClaude, constant.RelayModeClaudeMessages)
|
||
claudeAdaptor := channel.GetAdaptor(int(p))
|
||
claudeAdaptor.Init(claudeInfo)
|
||
claudeURL, claudeErr := claudeAdaptor.GetRequestURL(claudeInfo)
|
||
|
||
// chat 入站的 URL
|
||
chatInfo := guardInfo(p, constant.RelayFormatOpenAI, constant.RelayModeChatCompletions)
|
||
chatAdaptor := channel.GetAdaptor(int(p))
|
||
chatAdaptor.Init(chatInfo)
|
||
chatURL, chatErr := chatAdaptor.GetRequestURL(chatInfo)
|
||
|
||
hasDistinctEndpoint := claudeErr == nil && chatErr == nil && claudeURL != chatURL
|
||
registered := constant.HasNativeClaudeEndpoint(int(p))
|
||
|
||
if hasDistinctEndpoint && !registered {
|
||
t.Errorf("%s(%d):ClaudeMessages 端点(%s)≠ chat 端点(%s),但未登记进 HasNativeClaudeEndpoint\n"+
|
||
"→ Claude 入站会被矩阵转成 chat 发到 Anthropic 端点。请在 constant.HasNativeClaudeEndpoint 登记",
|
||
p.String(), p, claudeURL, chatURL)
|
||
}
|
||
if !hasDistinctEndpoint && registered {
|
||
t.Errorf("%s(%d):已登记 HasNativeClaudeEndpoint 但 ClaudeMessages 与 chat 端点相同(claude=%q err=%v, chat=%q err=%v)\n"+
|
||
"→ Claude 入站会被直连到 chat 端点(chat 端点吃不了 Claude 体)。请移除登记或修 GetRequestURL",
|
||
p.String(), p, claudeURL, claudeErr, chatURL, chatErr)
|
||
}
|
||
}
|
||
}
|
||
|
||
// expectedPostProcessors 实现了 common.RequestPostProcessor 的供应商期望清单。
|
||
// 该接口仅在 relaykit 接管转换时由 convertRequestBody 调用,承载格式转换之外的
|
||
// 供应商私有适配(zhipu GLM top_p 裁剪 + 图片前缀剥离、ali DashScope thinking_budget 裁剪)。
|
||
var expectedPostProcessors = map[constant.ProviderType]bool{
|
||
constant.ProviderAli: true,
|
||
constant.ProviderZhipu: true,
|
||
// vertex:Claude 模型需注入 anthropic_version 并删除 model 字段
|
||
//(rawPredict 端点要求)。ConvertRequest 与本接口共用 adaptClaudeBodyForVertex,
|
||
// 该函数幂等(delete 幂等、anthropic_version 仅在缺失时写入)
|
||
constant.ProviderVertex: true,
|
||
}
|
||
|
||
// TestMatrixGuard_RequestPostProcessorRegistry 守卫第 2 类断链:私有后处理的登记漂移。
|
||
//
|
||
// - 新 adaptor 实现了接口却没进清单 → 失败提醒登记,并核对两条契约:
|
||
// ① adaptor.ConvertRequest 内部含同一段后处理(legacy 路径不经本接口);
|
||
// ② 后处理幂等(relaykit 通用处理可能已写同名字段,不得依赖字段不存在)。
|
||
// - 清单里的供应商不再实现接口 → 失败提醒同步移除(多为重构时误删,实际是丢私有适配)。
|
||
//
|
||
// 注意:dispatch 型 adaptor(volcengine/tencent)通过匿名嵌入接口字段委托,接口断言
|
||
// 作用在外层 wrapper 上——若未来给其子 adaptor 添加 PostProcessor,必须让 wrapper
|
||
// 也实现并转发,否则 convertRequestBody 的断言拿不到(本测试同样会暴露这一点)。
|
||
func TestMatrixGuard_RequestPostProcessorRegistry(t *testing.T) {
|
||
for _, p := range guardProviders() {
|
||
info := guardInfo(p, constant.RelayFormatOpenAI, constant.RelayModeChatCompletions)
|
||
adaptor := channel.GetAdaptor(int(p))
|
||
adaptor.Init(info)
|
||
|
||
_, implements := adaptor.(common.RequestPostProcessor)
|
||
expected := expectedPostProcessors[p]
|
||
|
||
if implements && !expected {
|
||
t.Errorf("%s(%d) 实现了 RequestPostProcessor 但未登记进 expectedPostProcessors\n"+
|
||
"→ 请登记,并核对:① ConvertRequest 内部含同一段后处理(legacy 路径);② 后处理幂等",
|
||
p.String(), p)
|
||
}
|
||
if !implements && expected {
|
||
t.Errorf("%s(%d) 在 expectedPostProcessors 清单中但 adaptor 未实现 RequestPostProcessor\n"+
|
||
"→ 私有后处理疑似在重构中丢失(relaykit 接管路径将静默跳过参数裁剪),请核实",
|
||
p.String(), p)
|
||
}
|
||
}
|
||
}
|