mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
照参考 zcode-diagnostics.ts 移植,作为**两处消费者共用的一份源码文本**:
① 插件主体(领取路径 claimZcode);② channels-zcode 适配器(推理路径)。
共用是必须的:3012 有**账号冷却惩罚**(30 分钟起步,反复触发升级到 24 小时乃至
停用),"请勿连续重试"这句话绝不能只在一条路径上(参考为此专门抽成单一份)。
## 为什么这次移植的不是"锦上添花"
我们原来的领取路径是 `code == 3012 or msg:find("unusual activity")`,而参考用
**对抗性审计**打出了两个方向的真实缺陷(2026-10-05,实测记录在源码注释里):
**误判方向**(把限流当风控):两者用户动作**完全相反**(稍后重试 vs 别重试),
且领取路径据此**短路**、跳过本可领取的 plan。实测反例:
`too many requests, retry after 3012 ms` ← 毫秒数被当风控
`<h1>Error 3012</h1>upstream connect timeout` ← HTML 里的数字
`rate limited: request blocked, retry after 3012 ms` ← block 距 14 字符
`temporarily blocked; see error 3012` ← block 距 12 字符
`plan-3012-trust` / `trust_3012` / `x3012y` / `13012` ← 标识符形态
**漏判方向**(真风控被放过):上游 3012 的 msg 原文
`request has been blocked due to unusual activity.` **不含任何数字**(数字在 code
字段里)⇒ "必须有独立词 3012"这条门禁会反过来把它漏掉。
⇒ 判据以**语义短语**为主、数字只作兜底且必须与 block **紧邻(≤10 字符)**;
词边界集必须含 `-`/`_`(参考两版才收全:先只排数字 ⇒ plan-3012 仍命中;
再补字母 ⇒ 仍命中,因连字符不属 [0-9A-Za-z])。
另:业务码认**纯数字字符串**(上游回 {"code":"3012"} 时,只认 number 会让风控码
整条丢失 ⇒ 用户看到那句裸上游串)。
## 一并移植的其它实测教训
- `formatGap`:分钟档必须**逐档单独算**。参考初版写 `floor(seconds % 60)`,
90 秒被渲染成「30分30秒」(分位取的是**秒的余数**)——该错误是在真实探针
输出里发现的,纯函数单测当时没覆盖 ≥60 秒区间。
- 诊断的「距上条」必须用**单独存的 previousSentAt**,不能拿 lastSentAt 顶替
(调用顺序是"先记本次、再诊断",顶替会让它恒为 0ms、永远没有信息量)。
- 形态诊断必须读**真正要发出去的那个对象**,不读常量:常量只能证明"源码里写了
多少字",证明不了"线上发出去多少字";空 system 时必须**如实显示 0**。
- 诊断行**绝不写凭据**:账号只以进程内自增序号出现。
## ★ 接线上的一个真实 bug(我自己引入并自查出来)
`installZcodeAdapter` 最初把 DIAG 源码直接 `.. ZCODE_ADAPTER_SRC` 拼接 ——
而 DIAG 末尾是 `return Z`,直接拼会让那个 return 结束**整个 chunk** ⇒ 适配器
代码永不执行,且**装载会静默成功**("加载成功但适配器不存在")。
修法:包进 `(function() ... end)()` 让 return 只结束该函数。
## 判据
`internal/core/channels_zcode_3012_test.go`(走共用探针打**那一份**实现):
- 19 条分类矩阵(含上列全部审计反例)
- formatGap 逐档(含 90000ms 必为「1分30秒」)
- 诊断行凭据泄漏守卫(账号 id 的每个 ≥4 字符子串都不得出现)
- 文案共用 + 必含「请勿连续重试」+ 空参不留空壳
- 形态诊断读实际对象(空 system 必须显示 0)
★ 变异 2/2 咬住。其中分类矩阵**第一版没咬住** —— 因为"紧邻检查"独立挡住了
那些用例,从未**隔离**词边界规则;补 5 个"block 紧邻但 3012 在标识符里"的
用例后才有效。教训:**必须有一组用例只被被测规则区分**,否则别的守卫会把它
的失效掩盖掉(本轮第 4 次"判据太弱")。
验证:internal/core 351s 全绿。
286 lines
11 KiB
Go
286 lines
11 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ ZCode 3012 的判据必须是**多形态**的,且误判不是"文案不好看"——
|
||
// 领取路径会据此**短路**(跳过本可领取的 plan),而限流与风控的用户动作
|
||
// **完全相反**(稍后重试 vs 别重试)。
|
||
//
|
||
// 本判据逐行搬自参考 zcode-diagnostics.ts 的对抗性审计实测矩阵
|
||
// (2026-10-05 自审 + 审计打出的真实缺陷)。
|
||
//
|
||
// 为什么用**共用模块**验:这段逻辑被领取路径(插件主体)与推理路径
|
||
// (channels-zcode 适配器)共用,任一侧漂移都会让"请勿连续重试"只出现在
|
||
// 一条路径上。判据直接打共用模块的导出,两边用的是同一份源码文本。
|
||
func TestZcode3012ClassificationMatrix(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
cases := []struct {
|
||
name string
|
||
body string
|
||
want bool
|
||
}{
|
||
// ── 真风控(必须命中)──
|
||
{"上游真实文案(带码)", `{"code":3012,"msg":"request has been blocked due to unusual activity."}`, true},
|
||
{"码是纯数字字符串", `{"code":"3012","msg":"x"}`, true},
|
||
{"仅正文语义(无数字)", `request has been blocked due to unusual activity.`, true},
|
||
{"3012 与 block 紧邻", `3012 blocked by waf`, true},
|
||
|
||
// ── 必须**不**命中(误判会让用户做错事)──
|
||
{"13012 不是 3012", `{"code":13012}`, false},
|
||
{"标识符 plan-3012-trust", `plan-3012-trust`, false},
|
||
{"标识符 trust_3012", `trust_3012`, false},
|
||
{"字母包裹 x3012y", `x3012y`, false},
|
||
{"限流的毫秒数", `too many requests, retry after 3012 ms`, false},
|
||
{"HTML 里的 3012", `<h1>Error 3012</h1>upstream connect timeout`, false},
|
||
{"限流+block 距 14", `rate limited: request blocked, retry after 3012 ms`, false},
|
||
{"block 距 12", `temporarily blocked; see error 3012`, false},
|
||
{"额度码 1005", `{"code":1005,"msg":"quota"}`, false},
|
||
|
||
// ── 隔离「词边界」规则:这些用例同时满足"block 紧邻",
|
||
// 只有词边界能区分 ⇒ 裸子串实现会把前三个判成风控。
|
||
// (最初的矩阵没覆盖这一组,变异测试因此没咬住 —— 补上后才有效。)
|
||
{"标识符+block 紧邻", `plan-3012 blocked`, false},
|
||
{"字母包裹+block 紧邻", `x3012 blocked`, false},
|
||
{"下划线+block 紧邻", `trust_3012 blocked`, false},
|
||
{"独立词+block 紧邻", `3012 blocked`, true},
|
||
{"13012 里的片段+block 紧邻", `13012 blocked`, false},
|
||
}
|
||
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
got := h.callZD("hasUnusualActivity", tc.body)
|
||
if got != tc.want {
|
||
t.Errorf("hasUnusualActivity(%q) = %v, want %v\n"+
|
||
"(误判的后果不是文案问题:领取路径会据此短路,且限流='稍后重试'、\n"+
|
||
" 风控='别重试',两者用户动作完全相反)", tc.body, got, tc.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// ★ 时间差格式化必须逐档单独算:参考初版把分钟档写成 `floor(seconds % 60)`,
|
||
// 于是 90 秒被渲染成「30分30秒」(分位取的是**秒的余数**)。
|
||
// 该错误是在**真实探针输出**里发现的,纯函数单测当时没覆盖 ≥60 秒的区间。
|
||
func TestZcodeFormatGapDoesNotMisrenderMinutes(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
for _, tc := range []struct {
|
||
ms int
|
||
want string
|
||
}{
|
||
{90000, "1分30秒"}, // 参考踩过的坑:错法会得到 "30分30秒"
|
||
{59000, "59.0s"}, // 不足一分钟
|
||
{60000, "1分"}, // 整分钟
|
||
{3600000, "1小时"}, // 整小时
|
||
{3900000, "1小时5分"}, // 小时+分
|
||
{500, "500ms"},
|
||
} {
|
||
got := h.callZDString("formatGap", tc.ms)
|
||
if got != tc.want {
|
||
t.Errorf("formatGap(%d) = %q, want %q", tc.ms, got, tc.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★ 诊断行**绝不能含凭据片段**(参考的设计红线之一)。
|
||
//
|
||
// 账号只以"进程内自增序号"出现。判据用「传入的账号 id 的每个子串都不得出现在
|
||
// 文案里」来钉死这条 —— 不是"看起来没有"。
|
||
func TestZcodeDiagnosticLeaksNoCredentials(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
const secretID = "zcode-abcdef1234567890-SECRET"
|
||
h.callZDAction("accountOrdinal", secretID)
|
||
h.callZDAction2("noteRequestSent", secretID, 1000)
|
||
h.callZDAction2("noteRequestOk", secretID, 1100)
|
||
|
||
diag := h.callZDString("formatDiagnostic", map[string]interface{}{
|
||
"accountId": secretID,
|
||
"sentAt": 1200,
|
||
"now": 1300,
|
||
"status": 405,
|
||
})
|
||
if diag == "" {
|
||
t.Fatal("formatDiagnostic 返回空")
|
||
}
|
||
// 账号 id 的每个 >=4 字符子串都不得出现
|
||
for i := 0; i+4 <= len(secretID); i++ {
|
||
if strings.Contains(diag, secretID[i:i+4]) {
|
||
t.Fatalf("诊断行泄漏账号 id 片段 %q:%s", secretID[i:i+4], diag)
|
||
}
|
||
}
|
||
if !strings.Contains(diag, "账号#") {
|
||
t.Fatalf("诊断行应含「账号#序号」形态:%s", diag)
|
||
}
|
||
t.Logf("✓ 诊断行无凭据片段: %s", diag)
|
||
}
|
||
|
||
// ★ 3012 的文案必须**共用**(推理 + 领取两条路径),且必须含"请勿连续重试"。
|
||
//
|
||
// 3012 不是"重试就能过"(那是 3007),而是**有账号冷却惩罚**:
|
||
// 30 分钟起步,反复触发升级到 24 小时乃至停用。
|
||
func TestZcode3012MessageIsSharedAndWarnsAboutRetries(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
msg := h.callZDString("formatUnusualActivityMessage", map[string]interface{}{
|
||
"subject": "领取 plan-x",
|
||
"rawResponse": "request has been blocked",
|
||
})
|
||
for _, want := range []string{"3012", "请勿连续重试", "冷却惩罚", "领取 plan-x"} {
|
||
if !strings.Contains(msg, want) {
|
||
t.Fatalf("风控文案缺 %q:%s", want, msg)
|
||
}
|
||
}
|
||
|
||
// 空 subject/diagnostic/rawResponse 时不得留下空壳("本机诊断:"后面啥都没有)
|
||
bare := h.callZDString("formatUnusualActivityMessage", map[string]interface{}{})
|
||
if strings.Contains(bare, "本机诊断:\n") || strings.Contains(bare, "原始响应:\n") {
|
||
t.Fatalf("空参数不应留下空壳段:%q", bare)
|
||
}
|
||
t.Logf("✓ 文案: %s", strings.SplitN(msg, "\n", 2)[0])
|
||
}
|
||
|
||
// ★ 请求体形态诊断必须读**真正要发出去的对象**,而不是常量。
|
||
//
|
||
// 常量只能证明"源码里写了多少字",证明不了"线上发出去多少字"。
|
||
// 两者一旦不一致(例如某条改动把 system 顶掉了),只有读实际值才看得出来
|
||
// —— 它会**如实显示 0 字符**,而不是拿常量里的 2898 把问题盖住。
|
||
func TestZcodeRequestShapeReadsActualObject(t *testing.T) {
|
||
h := newZcodeIdentityHarness(t)
|
||
|
||
// 用真实的适配器产出 → 再诊断它(而不是手搓一个假 system)
|
||
out, err := h.transformRequest(`{"model":"m","messages":[{"role":"user","content":"hi"}]}`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var req map[string]interface{}
|
||
if err := jsonUnmarshal(out, &req); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
shape := h.callZDMap("describeRequestShape", req["system"], req["messages"])
|
||
if shape == nil {
|
||
t.Fatal("describeRequestShape 返回空")
|
||
}
|
||
chars, _ := shape["identityChars"].(float64)
|
||
if chars < 2800 {
|
||
t.Fatalf("身份块实测字符数 %v < 2800(诊断必须读实际值)", chars)
|
||
}
|
||
if shape["hasDateBlock"] != true {
|
||
t.Fatalf("应检测到日期块,实得 %v", shape["hasDateBlock"])
|
||
}
|
||
if shape["firstRole"] != "user" {
|
||
t.Fatalf("firstRole 应为 user,实得 %v", shape["firstRole"])
|
||
}
|
||
t.Logf("✓ 形态诊断: 块数=%v 身份块=%v 日期块=%v role=%v",
|
||
shape["systemBlocks"], chars, shape["hasDateBlock"], shape["firstRole"])
|
||
|
||
// 反例:空 system ⇒ 必须**如实显示 0**(不能被常量盖住)
|
||
empty := h.callZDMap("describeRequestShape", []interface{}{}, req["messages"])
|
||
if c, _ := empty["identityChars"].(float64); c != 0 {
|
||
t.Fatalf("空 system 时 identityChars 必须为 0(如实反映实际值),实得 %v", c)
|
||
}
|
||
}
|
||
|
||
// ── harness 助手:经共用的探针打**那一份** ZD 实现 ──
|
||
//
|
||
// 走探针而不是让测试自己 load 一份副本:判据必须打真实被两条路径共用的实现,
|
||
// 自己 load 副本会退化成"各自测各自的",共用一旦漂移就测不出来。
|
||
|
||
func (h *zcodeIdentityHarness) callZD(fn string, a interface{}) bool {
|
||
h.t.Helper()
|
||
res, err := h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": a})
|
||
if err != nil {
|
||
h.t.Fatalf("__zcode_diag_probe(%s): %v", fn, err)
|
||
}
|
||
m := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
if e, ok := m["error"]; ok {
|
||
h.t.Fatalf("探针错误(%s): %v", fn, e)
|
||
}
|
||
v, ok := m["result"].(bool)
|
||
if !ok {
|
||
h.t.Fatalf("%s 返回非布尔: %#v", fn, m["result"])
|
||
}
|
||
return v
|
||
}
|
||
|
||
func (h *zcodeIdentityHarness) callZDString(fn string, a interface{}) string {
|
||
h.t.Helper()
|
||
var res interface{}
|
||
var err error
|
||
if opts, ok := a.(map[string]interface{}); ok {
|
||
res, err = h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "opts": opts})
|
||
} else if fn == "formatGap" {
|
||
res, err = h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": a})
|
||
} else {
|
||
res, err = h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": a})
|
||
}
|
||
if err != nil {
|
||
h.t.Fatalf("__zcode_diag_probe(%s): %v", fn, err)
|
||
}
|
||
m := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
if e, ok := m["error"]; ok {
|
||
h.t.Fatalf("探针错误(%s): %v", fn, e)
|
||
}
|
||
s, _ := m["result"].(string)
|
||
return s
|
||
}
|
||
|
||
// callZDAction / callZDAction2:调"动作型"探针(不返回结果,只看没报错)。
|
||
func (h *zcodeIdentityHarness) callZDAction(fn string, a interface{}) {
|
||
h.t.Helper()
|
||
res, err := h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": a})
|
||
if err != nil {
|
||
h.t.Fatalf("__zcode_diag_probe(%s): %v", fn, err)
|
||
}
|
||
if m := res.(map[string]interface{})["json"].(map[string]interface{}); m["error"] != nil {
|
||
h.t.Fatalf("探针错误(%s): %v", fn, m["error"])
|
||
}
|
||
}
|
||
|
||
func (h *zcodeIdentityHarness) callZDAction2(fn string, a interface{}, a2 int) {
|
||
h.t.Helper()
|
||
res, err := h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": a, "a2": a2})
|
||
if err != nil {
|
||
h.t.Fatalf("__zcode_diag_probe(%s): %v", fn, err)
|
||
}
|
||
if m := res.(map[string]interface{})["json"].(map[string]interface{}); m["error"] != nil {
|
||
h.t.Fatalf("探针错误(%s): %v", fn, m["error"])
|
||
}
|
||
}
|
||
|
||
func (h *zcodeIdentityHarness) callZDMap(fn string, args ...interface{}) map[string]interface{} {
|
||
h.t.Helper()
|
||
var res interface{}
|
||
var err error
|
||
if len(args) == 2 {
|
||
res, err = h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "system": args[0], "messages": args[1]})
|
||
} else {
|
||
res, err = h.ps.Call("channels", "__zcode_diag_probe",
|
||
map[string]interface{}{"fn": fn, "a": args[0]})
|
||
}
|
||
if err != nil {
|
||
h.t.Fatalf("__zcode_diag_probe(%s): %v", fn, err)
|
||
}
|
||
m := res.(map[string]interface{})["json"].(map[string]interface{})
|
||
if e, ok := m["error"]; ok {
|
||
h.t.Fatalf("探针错误(%s): %v", fn, e)
|
||
}
|
||
out, _ := m["result"].(map[string]interface{})
|
||
return out
|
||
}
|
||
|
||
// jsonUnmarshal 是本文件用的小助手(避免与其它测试文件的导入冲突)。
|
||
func jsonUnmarshal(s string, v interface{}) error {
|
||
return json.Unmarshal([]byte(s), v)
|
||
}
|