fix(channels): 渠道鉴权头三修 + 源名契约 + 夹具形状判据

P1-1 anthropic 基座补 build_headers(x-api-key + anthropic-version)
  根因:基座只有静态 anthropic-version、没有 build_headers ⇒ 内核兜底发
  Authorization: Bearer,而 Anthropic 协议要 x-api-key。
  ★ 证据冲突(计划 P1-1 原文一半有误):zcode 的
  /zcode-plan/anthropic/v1/messages 虽然**说 Anthropic 协议**,鉴权却是
  Bearer <zcode_jwt>(参考 zcode-transport.ts:121-125;全仓 grep x-api-key
  = 0 次命中)。故 zcode 必须**显式覆盖** build_headers 回到 Bearer,
  否则基座的正向修复会把它从"对"改成"错"。
  内核改动:internal/provider/provider.go —— 兜底 Bearer 让位给 x-api-key,
  避免同一请求带两套鉴权(部分 Anthropic 兼容上游对歧义鉴权直接 401)。

P1-2 channels-cline 的 build_headers 补 workos: 前缀
  根因:参考 cline.ts 发 `Bearer workos:<jwt>`,而池里存的是第二步 register
  换回的裸 token ⇒ 之前无 build_headers,内核兜底发裸 Bearer。
  幂等:已带前缀(含大小写变体)不重复拼。

P1-3 channels-gemini 只确认不改
  参考 gemini.ts:353-366 同样是 OAuth Bearer + 身份五头、刻意不发
  x-goog-api-key ⇒ 我们现状本来就对。加判据把这个结论钉住,防下轮照
  P1-1 的模子把它改坏。

P2 源名契约:仅 [A-Za-z0-9_-],禁 : 与 /
  根因:: 与 / 是限定名分隔符,出现在源名里会让 `源名:模型名` 有两种都
  自圆其说的切法,同一请求串在不同部署下可能路由到不同源,且**不报错**。
  校验加在写入口 core.normalizeSource(硬拒,API 400);启动时只告警,
  避免存量带 : 的源名让升级 brick 掉整个网关。
  解析入口 splitPinned 不做校验(解析不该拒绝已有数据)。

P3 夹具形状判据(本轮最重要的一条)
  根因:trae 的 Result 信封缺陷在扁平夹具下所有判据全绿,真实登录却报
  「缺少 Token」—— 夹具形状错 ⇒ 判据必然虚假通过。
  通用形状层:shapeFamilies 登记表,每族给 real/naive/fact/eval/assert,
  断言 fact(real) 且 !fact(naive) 且 assert(eval(real))。
  刻意**不比较 eval 输出形状**:trae 余额实现合法保留顶层兼容路径,
  拿输出比对会把合法兼容判成错误;能区分真假的是 fact(额度在哪一层)。
  另有仓库扫描:对特征明确的退化形状静态反查,需退化夹具时写 SHAPE-OK 豁免。

判据:11 个文件 / 30 用例。全部变异已验证会报红(9 处)。
This commit is contained in:
JianFeeeee
2026-10-18 00:38:49 +08:00
parent fc5326402f
commit 4174310421
18 changed files with 2194 additions and 36 deletions

View File

@ -0,0 +1,68 @@
package core
import (
"testing"
)
// ★ 适配器**真实装载**必须可观测:`__adapter_install_probe` 要报告全部 6 个
// 自装适配器,且每个都是 ok。
//
// 为什么这条判据值得存在(plan §3 第 1 条):
//
// `luac -p` 对 Lua 长字符串里的适配器代码**无效**(它只看外层插件脚本),
// 会给出假绿。真实故障态是「插件加载成功、适配器却没装进去」—— 此时每次
// 调用内核才响亮报 `missing transform_response`,而原因指向内核不像配置。
//
// 另一层:cline / qoder 此前**不在探针输出里**,于是「装了但没生效」在这两个
// 适配器上完全不可观测 —— 本轮修 cline 的 `workos:` 前缀时正是因此先卡了一下。
// 装上就该看得见。
func TestAdapterInstallProbeReportsAllSelfInstalled(t *testing.T) {
c := kernelTestCore(t)
ps := c.Plugins()
src, err := readBundledChannels(t)
if err != nil {
t.Fatal(err)
}
if err := ps.LoadSource("channels", src); err != nil {
t.Fatalf("加载插件: %v", err)
}
res, err := ps.Call("channels", "__adapter_install_probe", map[string]interface{}{})
if err != nil {
t.Fatalf("__adapter_install_probe: %v", err)
}
// 探针的诊断价值就在于它能在 CI 里跑通(zcode 的基座在 core 测试台里
// 是可得 —— pluginDir 与 adaptersDir 同一套配置)。
top, ok := res.(map[string]interface{})
if !ok {
t.Fatalf("探针返回值不是对象: %#v", res)
}
j, ok := top["json"].(map[string]interface{})
if !ok {
t.Fatalf("探针返回值缺 json 层: %#v", top)
}
// 6 个自装适配器:缺任何一个都是"插件装了、适配器没装"的故障态。
want := []string{"codearts", "trae", "gemini", "zcode", "cline", "qoder"}
for _, name := range want {
entry, ok := j[name].(map[string]interface{})
if !ok {
t.Fatalf("★ 探针未报告 %s 适配器的装载结果(装上就该看得见;"+
"漏报等于问题不可观测): %v", name, j)
}
if entry["ok"] != true {
t.Fatalf("★ %s 适配器装载失败: %v\n"+
" luac -p 对长字符串里的适配器代码**无效**(plan §3 第 1 条),"+
"必须以本探针为准", name, entry["err"])
}
}
t.Logf("✓ 6/6 自装适配器全部装载成功(probe: %v)", keysOfProbe(j))
}
func keysOfProbe(m map[string]interface{}) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}

View File

@ -1007,6 +1007,13 @@ func normalizeSource(s *config.Source) error {
if s.Name == "" || s.BaseURL == "" {
return fmt.Errorf("source requires name and base_url")
}
// ★ 契约:源名只允许 [A-Za-z0-9_-](见 config.ValidateSourceName)。
// 这里对**新增/修改的源硬拒**(API 400),而不是像启动时那样只告警 ——
// 带 `:` 或 `/` 的源名会让 `源名:模型名` 这个限定名产生两种都自圆其说的
// 切法,同一个请求串在不同部署下可能路由到不同源,且**不会报错**。
if err := config.ValidateSourceName(s.Name); err != nil {
return err
}
if len(s.Models) == 0 {
return fmt.Errorf("source requires at least one model")
}

View File

@ -0,0 +1,72 @@
package core
import (
"strings"
"testing"
"llmsproxy/internal/config"
)
// ★ P2(内核改动):新增/修改源时,源名含 `:` 或 `/` 必须**硬拒**(API 400)。
//
// 这里验的是**真实写入入口** Core.AddSource(WebUI / 插件的 add_source 都走它),
// 不是 config 层那个校验器 —— 校验器写对了但没接到入口上,问题依旧存在。
//
// 根因见 internal/config/config.go 的 ValidateSourceName 注释:`:` 与 `/` 是
// `源名:模型名` 的**分隔符**,出现在源名里会让限定名产生两种都自圆其说的切法,
// 同一请求串在不同部署下可能路由到不同源,且**不报错**。
func TestAddSourceRejectsPinnedSeparatorInName(t *testing.T) {
for _, name := range []string{
"a:b",
"channels-trae-abc:glm-5.2", // 把"限定名"当源名写进来
"a/b",
"openai/gpt",
} {
t.Run(name, func(t *testing.T) {
c := kernelTestCore(t)
err := c.AddSource(config.Source{
Name: name,
BaseURL: "http://127.0.0.1:1/v1",
Adapter: "openai",
Models: []config.Model{{ID: "m1", Kind: "chat"}},
})
if err == nil {
t.Fatalf("★ 源名 %q 含限定名分隔符,AddSource 必须拒绝\n"+
" 否则 `%s:m1` 既可能解析成源 %q,也可能解析成别的源 —— 且不报错",
name, name, name)
}
msg := err.Error()
if !strings.Contains(msg, "pinning separator") {
t.Fatalf("错误信息应点名「pinning separator」(运维才知道是限定名冲突),实得: %v", err)
}
// 源不得被写进配置(拒绝必须是**彻底的**,不能先写后报错)
for _, s := range c.Sources() {
if s.Name == name {
t.Fatalf("★ 被拒绝的源 %q 竟然写进了配置 —— 拒绝必须发生在写盘之前", name)
}
}
})
}
}
// 配对判据:合法的渠道源名(含 `-`)必须照常接受。
// 这条防"校验器写太严把真实渠道源全拒了"——那会让整个渠道功能失效。
func TestAddSourceAcceptsChannelStyleNames(t *testing.T) {
for _, name := range []string{
"channels-trae-b8dd6b3df30e",
"channels-buddy-buddy-main",
"my_source_1",
} {
t.Run(name, func(t *testing.T) {
c := kernelTestCore(t)
if err := c.AddSource(config.Source{
Name: name,
BaseURL: "http://127.0.0.1:1/v1",
Adapter: "openai",
Models: []config.Model{{ID: "m1", Kind: "chat"}},
}); err != nil {
t.Fatalf("★ 合法渠道源名 %q 被拒(会把真实渠道全废掉): %v", name, err)
}
})
}
}