mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
## 1. ★ zcode 身份块适配器(channels-zcode)—— 准入的唯一开关
参考实测(三账号 17 次请求):zcode 的 /zcode-plan/anthropic 通道做**请求体
内容检查**,system 缺官方身份块结构时直接 HTTP 405 + {"code":3012}:
无 system → 3012 ✗
仅 cliPrefix(42 字符) → 3012 ✗
cliPrefix + stable(约 2898 字符)→ 200 ✓
⇒ 判据是「身份块结构是否存在」,与 HTTP 头、版本号、频率、多轮历史、tools
声明、运行时全部无关。我们此前用通用 anthropic 适配器 ⇒ **即使拿到 zcode
账号也必然被拒**(现在 0 账号,但这是能不能用起来的前提)。
实现:插件自装 channels-zcode —— 读内核 anthropic.lua 执行取基座,**只覆盖
transform_request** 注入身份块(协议转换始终与 anthropic 那一份一致,不漂移):
block[1] cliPrefix(42) / block[2] stable(2852) / block[3] Environment 段
/ block[4] 调用方 system(**必须追加在最后**:上游看的是前缀结构)
只给最后一块打 cache_control(Anthropic 上限 4 断点,逐块打会占满预算)
日期块插入首条 user 的 content **数组**最前(拼进字符串会改变结构)
文本与参考**逐字节一致**(已核对:42 + 2856 = 2898)。
★ 基座读不到时**响亮失败**,不静默退化 —— 否则会产出"身份块看着对、工具/图像
全没转换"的请求,上游可能 200 但模型拿不到工具定义,症状是"模型变笨了",
与准入毫不相干、排查完全跑偏。
判据 4 条 + 变异 2/2:不注入 → 报红;截断到 100 字符 → 报红。
另加"协议转换不退化"判据(工具 input_schema / 图像块存活)。
## 2. ★★ 同一缺陷家族第 3 个实例:Close() 不停调度器
修完 on_unload 后全量回归 internal/lua **600s 超时**,goroutine 快照显示
**69 个卡在 stateSaver.loop 与 PluginRuntime.loop**。
根因:Runtime.Stop() **只在单插件 Unload 里调**,Close() 不调 ⇒ 每个插件的
调度循环在"进程关闭/测试结束"路径上永不停止。与 on_unload 完全同一形状。
修法:Close() 新增 stopSchedulersAll(),顺序 = 停调度 → 落盘 → on_unload
(调度器必须停在落盘之前,否则它写一个已收尾的 state)。
★ 顺带:Stop() 不幂等(裸 close 会 panic: close of closed channel),
两处都调之后可达 —— 用 sync.Once 加固。给函数加第二个调用点前先查幂等性。
实测 internal/lua 从 600s 超时 → 62s 通过。
## 3. codearts 模型目录:补两个**必须 AK/SK 签名**的端点
codearts 有 10 个账号但目录里只有硬编码 1 个模型。参考打两个端点:
① opengw.../gateway/config → result.models(benefit)
② snap-access.../v1/model/builtin → builtinModels(常规)
而 refreshRemoteModels **只支持 Bearer** ⇒ 这两端点从来没被打过。
已加 ep.sign="codearts" 分支 + 两个解析形态 + opengw host 白名单。
判据:两端点各自真实命中 + 每请求带 SDK-HMAC-SHA256 头 + **合并**(非取其一)。
## 4. TRAE PKCE 候选来源补齐(3 → 5)
参考 authCodeCandidates 有 5 个来源,我们只有 3(漏了 authCodeInfo 为 JSON 的
authCode 字段、以及**纯字符串**形态)。★ 且不能写成 `a or b or c` 链 ——
jsonField 取不到时返回**空串**,Lua 里空串是真值,链会在第一个空串处短路。
判据 5 个参数化用例 + 变异 2/2 咬住(正是 or 链取不到的两个形态)。
## 5. ★ 部署脚本:备份裁剪只修了一半("两处只修一处"再现)
prune_adapter_backups 有完整实现(含数量硬上限),但**只针对 adapters.bak**;
sync_plugins 同样每次建 plugins.bak.* —— 修了 adapters 的堆积、plugins 继续涨。
实测线上 plugins.bak **36 个**、/etc/llmsproxy 192M。
泛化为 prune_backups <prefix> + prune_all_backups(两个前缀都裁),加空前缀守卫。
判据不止逐字检查,而是**扫描脚本里所有 .bak.$(date) 创建点**,要求每个前缀都在
裁剪范围内 —— 将来新增第三种备份目录会立刻报红,而不是等它涨到几百兆。
变异 2/2 咬住(只裁 adapters / 去掉守卫)。
## 6. 上游客观缺口钉成可执行判据(TestUpstreamGapsAreDocumented)
qoder 加密端点需 .wasm(**参考仓库里也不存在**)、TRAE PKCE 参考**明确不支持**、
zcode 滑块需真实浏览器交互。写成判据:任何人(包括未来的我)想"补"这些能力,
必须先让判据失败 —— 即先推翻事实再动手。防的正是我前两轮的反复误判。
清理:channels-buddy.lua(0 字节历史遗留,每次启动报 must return a table)。
297 lines
11 KiB
Go
297 lines
11 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
|
||
"llmsproxy/internal/lua"
|
||
)
|
||
|
||
// ★ ZCode 的身份块是 3012 准入的**唯一开关**。
|
||
//
|
||
// 参考实测(三个账号、17 次请求):system 缺官方身份块结构时,上游直接
|
||
//
|
||
// HTTP 405 + {"code":3012,"msg":"...unusual activity."}
|
||
//
|
||
// 且与 HTTP 头、版本号、请求频率、多轮历史、tools 声明、运行时**全部无关**。
|
||
//
|
||
// 判据矩阵(照参考,逐条锁定):
|
||
//
|
||
// 无 system → 3012
|
||
// 仅 cliPrefix(42 字符) → 3012
|
||
// cliPrefix + stable(约 2898 字符)→ 200
|
||
//
|
||
// ⇒ 判据是「身份块**结构**是否存在」,不是字符数凑够。
|
||
//
|
||
// 本判据看**适配器真正产出给上游的请求体**(transform_request 的返回值),
|
||
// 而不是"适配器装了没"这类内部信号。
|
||
func TestZcodeAdapterInjectsOfficialIdentityBlocks(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
out, err := h.transformRequest(`{"model":"glm-5.3-flash","messages":[{"role":"user","content":"hi"}]}`)
|
||
if err != nil {
|
||
t.Fatalf("transform_request: %v", err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatalf("产出不是 JSON: %v\n%s", err, out)
|
||
}
|
||
|
||
// ① system 必须是**块数组**(官方形态),不是纯字符串
|
||
sys, ok := req["system"].([]interface{})
|
||
if !ok {
|
||
t.Fatalf("system 必须是块数组(上游检查的是这个结构),实得 %T: %v",
|
||
req["system"], req["system"])
|
||
}
|
||
if len(sys) < 3 {
|
||
t.Fatalf("system 块数至少 3(cliPrefix + stable + Environment),实得 %d", len(sys))
|
||
}
|
||
|
||
// ② block[0] 必须是官方 CLI 前缀(准入必需)
|
||
b0, _ := sys[0].(map[string]interface{})
|
||
if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" {
|
||
t.Fatalf("block[0] 必须是官方 CLI 前缀(逐字),实得 %q", got)
|
||
}
|
||
|
||
// ③ block[1] 必须是官方 stable 段(准入必需),长度对得上
|
||
b1, _ := sys[1].(map[string]interface{})
|
||
stable, _ := b1["text"].(string)
|
||
if len(stable) < 2800 {
|
||
t.Fatalf("block[1](stable 段)长度 %d < 2800 —— 身份块不完整会被判为裸请求(3012)",
|
||
len(stable))
|
||
}
|
||
for _, marker := range []string{
|
||
"You are an interactive ZCode agent",
|
||
"# Harness",
|
||
"# ZCode Desktop Context",
|
||
} {
|
||
if !strings.Contains(stable, marker) {
|
||
t.Fatalf("stable 段缺 %q —— 身份块被截断/改写", marker)
|
||
}
|
||
}
|
||
|
||
// ④ block[2] 是 Environment 段(官方每会话都发;不发则模型不知 cwd/平台)
|
||
b2, _ := sys[2].(map[string]interface{})
|
||
env, _ := b2["text"].(string)
|
||
if !strings.HasPrefix(env, "# Environment") {
|
||
t.Fatalf("block[2] 必须是 Environment 段,实得 %q", env[:zcodeMin(40, len(env))])
|
||
}
|
||
for _, marker := range []string{"Primary working directory", "Platform", "powered by the model named"} {
|
||
if !strings.Contains(env, marker) {
|
||
t.Fatalf("Environment 段缺 %q:%s", marker, env)
|
||
}
|
||
}
|
||
|
||
// ⑤ 总长度必须达到参考声明的量级(2898)
|
||
total := len("You are ZCode, an interactive coding agent") + len(stable)
|
||
if total < 2800 {
|
||
t.Fatalf("身份块合计 %d 字符 < 2800(参考实测的准入值约 2898)", total)
|
||
}
|
||
|
||
// ⑥ 日期块:插到第一条 user 消息的 content **数组**最前(官方形态)
|
||
msgs, _ := req["messages"].([]interface{})
|
||
if len(msgs) == 0 {
|
||
t.Fatal("messages 为空")
|
||
}
|
||
m0, _ := msgs[0].(map[string]interface{})
|
||
content, ok := m0["content"].([]interface{})
|
||
if !ok {
|
||
t.Fatalf("首条 user 的 content 必须是**块数组**(拼进字符串会改变结构、仍被判裸请求),实得 %T",
|
||
m0["content"])
|
||
}
|
||
c0, _ := content[0].(map[string]interface{})
|
||
if got, _ := c0["text"].(string); !strings.Contains(got, "<system-reminder>") {
|
||
t.Fatalf("content[0] 必须是日期块(<system-reminder>),实得 %q", got)
|
||
}
|
||
|
||
t.Logf("✓ 身份块注入正确:块数=%d stable=%d 合计=%d", len(sys), len(stable), total)
|
||
}
|
||
|
||
// ★ 调用方的 system 必须**追加在最后**:上游检查看的是**前缀结构**,
|
||
// 官方身份块必须处在开头位置。若被前置会让身份块不在前缀而吃 3012。
|
||
func TestZcodeCallerSystemIsAppendedLast(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
out, err := h.transformRequest(
|
||
`{"model":"glm-5.3-flash","messages":[{"role":"system","content":"CALLER_PROMPT_XYZ"},{"role":"user","content":"hi"}]}`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := json.Unmarshal([]byte(out), &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
sys, _ := req["system"].([]interface{})
|
||
if len(sys) < 4 {
|
||
t.Fatalf("带调用方 system 时应为 4 块(3 官方 + 1 调用方),实得 %d", len(sys))
|
||
}
|
||
last, _ := sys[len(sys)-1].(map[string]interface{})
|
||
lt, _ := last["text"].(string)
|
||
if !strings.Contains(lt, "CALLER_PROMPT_XYZ") {
|
||
t.Fatalf("调用方 system 必须在**最后一块**(上游看前缀结构;前置会让身份块不在前缀)")
|
||
}
|
||
// 且前三块仍是官方块(身份块没有被调用方内容挤走)
|
||
b0, _ := sys[0].(map[string]interface{})
|
||
if got, _ := b0["text"].(string); got != "You are ZCode, an interactive coding agent" {
|
||
t.Fatalf("block[0] 必须仍是官方 CLI 前缀,实得 %q", got)
|
||
}
|
||
}
|
||
|
||
// 适配器自检出口:身份块长度与 anthropic 基座装载状态。
|
||
// 「装了但身份块是空的」这类问题(转义/截断/基座读不到)没有它只能靠猜。
|
||
func TestZcodeAdapterProbeReportsIdentityLength(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
zc, ok := j["zcode"].(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("探针未报告 zcode 适配器装载结果: %v", j)
|
||
}
|
||
if zc["ok"] != true {
|
||
t.Fatalf("zcode 适配器装载失败: %v", zc["err"])
|
||
}
|
||
}
|
||
|
||
// zcodeIdentityHarness:加载插件(它会**自装** channels-zcode 适配器),
|
||
// 然后直接调那个适配器的 transform_request。
|
||
//
|
||
// 直接调已装载的适配器(而不是自己去 load 源码)更接近真实路径:判据验的是
|
||
// 「上游实际收到的请求体」,包含前导注入、桥的装载等全部环节。
|
||
type zcodeIdentityHarness struct {
|
||
t *testing.T
|
||
c *Core
|
||
ps *lua.Plugins
|
||
}
|
||
|
||
func newZcodeIdentityHarness(t *testing.T) *zcodeIdentityHarness {
|
||
t.Helper()
|
||
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.Fatal(err)
|
||
}
|
||
return &zcodeIdentityHarness{t: t, c: c, ps: ps}
|
||
}
|
||
|
||
// transformRequest 调**已自装的** channels-zcode 适配器的 transform_request。
|
||
func (h *zcodeIdentityHarness) transformRequest(body string) (string, error) {
|
||
h.t.Helper()
|
||
vm := h.c.VM()
|
||
if vm == nil {
|
||
h.t.Fatal("VM 不可用")
|
||
}
|
||
return vm.Transform("channels-zcode", "transform_request", body)
|
||
}
|
||
|
||
func zcodeMin(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
// ★ 身份块注入不得破坏协议转换:工具调用、图像、流式帧仍必须走 anthropic 那一份。
|
||
//
|
||
// 为什么必须验:本适配器的手法是把 anthropic.lua 的源码读进来执行、只覆盖
|
||
// transform_request。若「读不到基座」或「覆盖时丢了字段」,协议转换会静默退化
|
||
// —— 表现为工具调用参数丢失、图像块消失,而身份块看起来完全正常。
|
||
func TestZcodeAdapterKeepsAnthropicProtocolConversion(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
t.Run("工具定义与工具调用存活", func(t *testing.T) {
|
||
in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":"go"}],` +
|
||
`"tools":[{"type":"function","function":{"name":"get_weather",` +
|
||
`"description":"d","parameters":{"type":"object","properties":{"c":{"type":"string"}}}}}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// anthropic 形态:tools[].name / input_schema(顶层,不是嵌套在 function 里)
|
||
if !strings.Contains(out, `"get_weather"`) {
|
||
t.Fatalf("工具定义在转换中丢失(identity 注入不该影响它):%s", out)
|
||
}
|
||
if !strings.Contains(out, `"input_schema"`) {
|
||
t.Fatalf("工具 schema 未转成 anthropic 形态(input_schema 缺失):%s", out)
|
||
}
|
||
})
|
||
|
||
t.Run("多模态图像块存活", func(t *testing.T) {
|
||
in := `{"model":"glm-5.3-flash","messages":[{"role":"user","content":[` +
|
||
`{"type":"text","text":"what"},` +
|
||
`{"type":"image_url","image_url":{"url":"data:image/png;base64,AAAA"}}]}]}`
|
||
out, err := h.transformRequest(in)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !strings.Contains(out, `"image"`) || !strings.Contains(out, `"base64"`) {
|
||
t.Fatalf("图像块在转换中丢失:%s", out)
|
||
}
|
||
})
|
||
|
||
t.Run("基座装载状态可见", func(t *testing.T) {
|
||
// 读适配器的自检出口:base_loaded 必须为 true(否则身份块虽在、
|
||
// 协议转换已退化为透传)
|
||
res, err := h.ps.Call("channels", "__adapter_install_probe", map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
j := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
zc := j["zcode"].(map[string]interface{})
|
||
if zc["ok"] != true {
|
||
t.Fatalf("zcode 适配器未装载: %v", zc)
|
||
}
|
||
})
|
||
}
|
||
|
||
// ★ 基座(anthropic.lua)读不到时必须**响亮失败**,不得静默退化。
|
||
//
|
||
// 反例危害:若"尽力而为"(只注入身份块、不作协议转换),会产出一个身份块
|
||
// 看着正确的请求,而 tools/图像/流式结构全未转换 —— 上游可能 200,但模型
|
||
// 拿不到工具定义,症状是"模型变笨了",与准入毫不相干,排查会完全跑偏。
|
||
//
|
||
// 判据看的是**适配器的真实产出**:把适配器目录指到空目录(基座不可得),
|
||
// transform_request 必须返回错误而不是原样透传。
|
||
func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) {
|
||
// 隔离目录:插件目录下没有兄弟 adapters/ 目录 ⇒ 基座读不到
|
||
dir := t.TempDir()
|
||
pluginDir := filepath.Join(dir, "plugins")
|
||
if err := os.MkdirAll(pluginDir, 0755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
|
||
|
||
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.Fatal(err)
|
||
}
|
||
vm := c.VM()
|
||
if vm == nil {
|
||
t.Fatal("VM 不可用")
|
||
}
|
||
// 插件自装时若基座读不到,适配器仍应装载(身份块是静态的),
|
||
// 但调用必须报错
|
||
out, err := vm.Transform("channels-zcode", "transform_request",
|
||
`{"model":"m","messages":[{"role":"user","content":"hi"}]}`)
|
||
if err == nil {
|
||
t.Fatalf("基座不可得时必须报错,却成功产出了:%s", out)
|
||
}
|
||
if !strings.Contains(err.Error(), "基座") {
|
||
t.Fatalf("错误信息应点明基座问题(可定位),实得 %v", err)
|
||
}
|
||
t.Logf("✓ 基座缺失时响亮失败: %v", err)
|
||
}
|