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 处)。
5.6 KiB
源名与限定名契约
本文只说一件事:源名允许哪些字符,以及为什么。 判据:
internal/config/source_name_contract_test.go、internal/core/source_name_contract_test.go。
1. 契约
| 项 | 值 |
|---|---|
| 允许字符集 | 仅 [A-Za-z0-9_-] |
| 禁止 | : 与 /(限定名分隔符,见下) |
| 其它禁止 | 空白、换行、.、+、@、非 ASCII 等一切不在允许集内的字符 |
| 长度 | 非空(上限未强制,但超长源名无实际用途) |
合法的真实形态(本仓渠道源):
channels-trae-b8dd6b3df30e
channels-buddy-buddy-main
channels-zcode-<accountId>
注意 - 是允许的:渠道源名天生含 -,而 - 作分隔符时是「源名之后」
才出现,不产生歧义。
2. 为什么禁 : 与 /
2.1 背景:限定名是有意设计,不是兼容尾巴
网关支持 源名:模型名 这种限定名来指定「从哪个源调用」:
channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro
这不是历史包袱 —— 生产实例里 44 个模型被多个源同时提供
(glm-5.2 由 6 个源提供),WebUI 的模型下拉框用 源名:模型名 作
option value。没有限定名,用户就无法指定源。
: / / / - 三种分隔符都支持(历史写了三种,都保留)。
2.2 根因:源名含分隔符 ⇒ 限定名有两种都自圆其说的切法
剥离源名的那一步是 internal/provider/splitPinned:先按已知源名做最长
前缀匹配,命中后要求源名之后紧跟一个分隔符。
若源名自己含 : 或 /,那个字符同时是「源名的一部分」和「分隔符」:
| 源名 | 模型 | 限定名 | 可能的切法 |
|---|---|---|---|
a:b |
m |
a:b:m |
("a", "b:m") 或 ("a:b", "m") |
a/b |
m |
a/b/m |
("a", "b/m") 或 ("a/b", "m") |
两种切法都能自圆其说,结果取决于当前恰好存在哪些源名 —— 也就是 同一个客户端请求串,在不同部署、不同启动顺序下可能落到不同的源。
更糟的是它不会报错:splitPinned 会命中某一个分支,然后路由到别处。
历史上这一族缺陷就非常难查:
IndexAny(model, "-:/")按第一个分隔符切,把channels-trae-b8dd6b3df30e:Doubao-Seed-2.1-Pro切成src="channels"、model="trae-b8dd6b3df30e:..."⇒ 源名永不匹配 ⇒Resolve()返回 nil ⇒ 客户端看到model "channels-trae-xxx:..." is not configured。 用户只看到「模型不能用」,完全看不出是字符串切分错了。
所以约束必须放在写入口,而不是每次解析时去猜。
3. 校验加在哪(三个位置,职责不同)
| 位置 | 函数 | 行为 | 为什么在这儿 |
|---|---|---|---|
| 写入口(硬拒) | core.normalizeSource(internal/core/core.go) |
AddSource 返回 error(API 400) |
WebUI 新增/修改源、插件 add_source 都走这里。新数据必须从源头就是合法的 |
| 启动(只告警) | config.ApplyDefaults(internal/config/config.go) |
log.Printf 一行,不返回 error |
存量配置可能已有带 : 的源名。升级瞬间让网关起不来比「限定名歧义」更坏 —— 整个服务不可用 |
| 校验器本体 | config.ValidateSourceName / config.SourceNameWarning |
纯函数 | 单一定义,上面两处都调它,不会漂移 |
解析入口(不在上面三处,但需要知道)
:internal/provider/registry.go 的 splitPinned 是限定名的解析入口,
Resolve / ResolvePinned / EffectiveModel 三个都用它。
它不做校验(解析不该拒绝已有数据),只做最长前缀匹配 + 分隔符判定。
校验的责任全在写入口 —— 这样解析路径保持纯函数、无副作用。
4. 存量数据怎么办
启动时告警、不阻断:
[config] source name "legacy:v1" contains ":", which is a reserved pinning
separator; "legacy:v1" is ambiguous (source names allow only [A-Za-z0-9_-])
— `source:model` pinning may resolve to the wrong source
运维看到告警后可以:
- 改名(推荐,WebUI 里编辑即可 —— 改名后
channels-*源会在下次账号同步时重建) - 或者继续用,但不要对这种源使用
源名:模型名限定名(用「模型 + 源」的 显式槽位,即 AUTO 链里的(model, source)对,那条路径不经过splitPinned)
5. 判据覆盖
| 判据 | 文件 | 钉住什么 |
|---|---|---|
TestValidateSourceNameAllowsContractCharset |
internal/config/source_name_contract_test.go |
合法集(含真实渠道源形态、含 -)不被误拒 |
TestValidateSourceNameRejectsPinningSeparators |
同上 | : / / 被点名拒绝(错误文案含 pinning separator + 回显源名) |
TestValidateSourceNameRejectsOtherIllegalChars |
同上 | 空白/换行/./+/@/非 ASCII/空串 |
TestSourceNameWarningIsSoftForLegacyConfig |
同上 | 存量带 : 能启动(不 brick),且有告警文案 |
TestAddSourceRejectsPinnedSeparatorInName |
internal/core/source_name_contract_test.go |
真实写入入口 Core.AddSource 硬拒,且拒绝发生在写盘之前 |
TestAddSourceAcceptsChannelStyleNames |
同上 | 真实渠道源名照常接受(防校验器写太严把渠道全废掉) |
写入口那条特意在
internal/core而不是internal/config:校验器写对了但 没接到入口上,问题依旧存在 —— 两条判据分别咬「规则对不对」和「规则有没有被调用」。