feat(zcode): 3012 判据/文案/诊断模块(照参考逐行移植,含对抗性审计反例)

照参考 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 全绿。
This commit is contained in:
JianFeeeee
2026-10-16 01:38:34 +08:00
parent b871c0c713
commit e6fa2a678f
2 changed files with 689 additions and 8 deletions

View File

@ -52,7 +52,7 @@ plugin.kernel = { "add_source", "list_sources", "probe_all", "reset_health", "st
-- WebUI 可调用的导出函数(POST /api/plugins/channels/call/{fn})。
-- set_endpoints 让运维把渠道端点指向自建代理/镜像;测试用它打假上游。
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe", "__store_dir_probe", "__set_loopback_probe", "reap_loopbacks", "__is_model_gone_probe", "__rank_accounts_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
plugin.exports = { "set_endpoints", "account_add", "account_list", "account_update", "account_delete", "account_test", "account_refresh", "account_reorder", "account_reset", "account_retest", "account_adopt", "model_list", "model_refresh", "model_set_disabled", "model_set_disabled_many", "model_set_all_disabled", "model_clear_dead", "provider_status", "provider_set_enabled", "provider_get_order", "provider_set_order", "credits_claim_all", "credits_status", "credits_balances", "credits_permanent_lock", "backup_export", "backup_import", "backup_status", "auto_checkin", "gateway_get_enabled", "gateway_set_enabled", "usage_token_ledger", "usage_badge", "usage_badge_preference", "onboarding_status", "onboarding_claim", "__trae_derive_probe", "__codearts_sign_probe", "__qoder_machine_probe", "__qoder_refresh_probe", "__codearts_dpop_probe", "__codearts_cred_probe", "__codearts_adapter_sign_probe", "__sync_source_probe", "__trae_solo_probe", "__adapter_install_probe", "__store_dir_probe", "__set_loopback_probe", "reap_loopbacks", "__zcode_diag_probe", "__is_model_gone_probe", "__rank_accounts_probe", "__inject_refresh_platform_probe", "__clear_capabilities_probe", "__qoder_claim_outcome_probe", "__is_free_probe", "__claim_covers_probe", "__schedule_probe", "__parse_capability_probe", "__inject_capability_probe", "__ttl_reset_probe", "__ttl_probe", "__ttl_invalidate_probe", "__serial_probe", "__serial_release_probe", "__set_legacy_cred_probe", "__read_cred_probe", "__clear_all_creds_probe", "credits_tier_probe", "__set_lock_permanent_probe", "__set_expiring_window_probe", "__expiring_window_probe", "__record_dead_probe", "__catalog_gate_probe", "__catalog_gate_fail_probe", "credits_capability", "claims_capable" }
@ -8573,11 +8573,22 @@ function claimZcode(plat, tok, args)
else
local ok, rec = pcall(json.decode, resp.body or "")
if ok and type(rec) == "table" then
-- ⚠️ 业务码可能是**纯数字字符串**("3012"),tonumber 必须能认
local code = tonumber(rec.code) or tonumber(tostring(rec.code or ""))
-- ⚠️ 业务码可能是**纯数字字符串**("3012"):ZD 归一。
local code = (ZD and ZD.parseBusinessCode(rec.code))
or tonumber(rec.code) or tonumber(tostring(rec.code or ""))
local msg = tostring(rec.msg or rec.message or "")
if code == 3012 or msg:find("unusual activity", 1, true) then
failedMsg = "上游风控拦截(3012 unusual activity)—— 需重新获取验证码"
-- ★ 风控判定走**共用模块**(与推理路径同一份源码文本)
-- 见 ZCODE_DIAG_SRC 的头注释:3012 有账号冷却惩罚,
-- "请勿连续重试"必须同时出现在两条路径上。
-- 判定本身:只信业务码(3012 才是风控),无码可读时才看正文语义 ——
-- 裸数字会把 `retry after 3012 ms`(限流)误判成风控,而两者动作相反。
local isRisk = (ZD and ZD.hasUnusualActivity(resp.body or ""))
or (code == 3012) or (msg:find("unusual activity", 1, true) ~= nil)
if isRisk then
failedMsg = (ZD and ZD.formatUnusualActivityMessage({
subject = "领取 " .. tostring(planId or ""),
rawResponse = msg,
})) or "上游风控拦截(3012 unusual activity)—— 需重新获取验证码"
elseif code and code ~= 0 and code ~= 200 then
failedMsg = msg ~= "" and msg or ("业务码 " .. tostring(code))
else
@ -9002,6 +9013,326 @@ end
return adapter
]==]
local ZCODE_DIAG_SRC = [===[
-- ============================================================================
-- ZCode 3012 判据 / 文案 / 诊断(**唯一出处**,两处消费者共用同一份源码)
--
-- 消费者:
-- ① 插件主体(领取路径 claimZcode);
-- ② 自装适配器 channels-zcode(推理路径的错误分类 + 请求体形态诊断)。
-- 两者由 installZcodeAdapter 把本段**前置**到适配器源码里实现共用
-- (照参考的设计:formatZcodeUnusualActivityMessage 被两条路径引用同一份,
-- 因为 3012 不是"重试就能过",而是**有账号冷却惩罚**——"请勿连续重试"
-- 是这段文案里最重要的一句,绝不能只在其中一条路径上有)。
--
-- ## 为什么不能只看裸数字(参考 2026-10-05 对抗性审计的实测结论)
--
-- `too many requests, retry after 3012 ms` → 仅看数字会**误判成风控**
-- `<h1>Error 3012</h1>upstream connect timeout` → 同上
--
-- 前者后果**不是文案问题**:限流是"稍后重试即可"、风控是"**别重试**",
-- 两者用户动作**完全相反**;且领取路径据此**短路**,跳过本可领取的 plan。
--
-- ## 为什么也不能用"独立词 3012"当门禁
--
-- 上游 3012 的 `msg` 原文就是 `request has been blocked due to unusual
-- activity.` —— **不含任何数字**(数字在 `code` 字段里)。以真实文案做样本时,
-- "必须有独立词 3012"这条门禁会**反过来把它漏掉**。
-- ⇒ 判据以**语义短语**为主,数字只作兜底(且必须与 block 字样紧邻)。
-- ============================================================================
local Z = {}
-- isUnusualActivityCode(code):码是否为 3012。只认 number 与**纯数字字符串**。
--
-- ⚠ 必须认字符串形态(真实缺陷 2026-10-05):上游会回
-- {"code":"3012","msg":"..."},而只认 number 的解析会让风控码整条丢失,
-- 用户看到的正是那句裸上游串(漏判的直接原因)。
-- ⚠ 只认**纯数字**(^%-?%d+$),不把任意文本静默变成业务码。
function Z.isUnusualActivityCode(code)
if type(code) == "number" then return code == 3012 end
if type(code) ~= "string" then return false end
return code:match("^%s*3012%s*$") ~= nil
end
-- parseBusinessCode(value):把上游 code 归一成数字;认 number 与纯数字字符串。
function Z.parseBusinessCode(value)
if type(value) == "number" then
if value ~= value or value == math.huge or value == -math.huge then return nil end
return value
end
if type(value) ~= "string" then return nil end
local t = value:match("^%s*(.-)%s*$")
if not t:match("^%-?%d+$") then return nil end
return tonumber(t)
end
-- looksLikeUnusualActivity(body):正文里以**独立词**出现 3012。
--
-- ⚠ 必须是词边界,不能是 body:find("3012", 1, true)(真实缺陷):裸子串会把
-- 其它码里的数字片段一并命中,实测误判形态:
-- {"code":13012} → 误命中
-- plan-3012-trust → 误命中
-- 后果不是"文案不好看":领取路径据此短路,把"plan 名里恰好含 3012"的普通
-- 失败当成风控,**跳过本可领取的 plan**,并把真实业务码(如 1005 额度用尽)
-- 改写成 3012 —— 两者用户动作完全相反(等额度重置 vs 别重试)。
--
-- ⚠ 边界集必须含 **`-` 与 `_`**:初版只排除数字 ⇒ `plan-3012-trust` 仍命中;
-- 第二版补字母 ⇒ 仍命中(连字符不属 [0-9A-Za-z])。故取 [0-9A-Za-z_-]。
-- `.` `,` `}` `:` 空格 行首行尾都**不**被排除,故上游真实形态
-- {"code":3012,...} 与自然语言 `error: 3012 blocked` 照常命中。
function Z.looksLikeUnusualActivity(body)
if type(body) ~= "string" then return false end
-- Lua 无 lookbehind/lookahead ⇒ 手工检查两个前导/后继字符
local from = 1
while true do
local s, e = body:find("3012", from, true)
if not s then return false end
local before = s > 1 and body:sub(s - 1, s - 1) or ""
local after = e < #body and body:sub(e + 1, e + 1) or ""
local function bad(c)
if c == "" then return false end
return c:match("[0-9A-Za-z_%-]") ~= nil
end
if not bad(before) and not bad(after) then return true end
from = s + 1
end
end
-- mentionsUnusualActivity(body):正文是否**表达**了风控(仅凭正文时的唯一入口)。
--
-- ① 主判据:上游**实测原文**的语义短语(覆盖 `request has been blocked due to
-- unusual activity.` 及其变体);
-- ② 兜底:独立词 3012 与 block 字样**紧邻(≤10 字符)**。
--
-- ⚠ 为什么必须"紧邻"而不是"全文共现"(真实缺陷 N2):第一版写成"独立词 3012
-- 且全文含 block",仍会误判限流正文:
-- `rate limited: request blocked, retry after 3012 ms` → 距 14,误判
-- `temporarily blocked; see error 3012` → 距 12,误判
-- 而 `3012 blocked by waf` → 距 1,应命中
-- 窗口 10 是实测收出来的(40 仍放过上面两个反例)。
function Z.mentionsUnusualActivity(body)
if type(body) ~= "string" then return false end
if body:lower():find("unusual activity", 1, true) then return true end
if body:lower():find("blocked due to", 1, true) then return true end
if not Z.looksLikeUnusualActivity(body) then return false end
-- 紧邻检查:3012 前后 10 字符内出现 block
local low = body:lower()
local from = 1
while true do
local s, e = low:find("3012", from, true)
if not s then return false end
local before = s > 1 and low:sub(math.max(1, s - 10), s - 1) or ""
local after = e < #low and low:sub(e + 1, math.min(#low, e + 10)) or ""
if before:find("block", 1, true) or after:find("block", 1, true) then return true end
from = s + 1
end
end
-- readBusinessCodeFromBody(body):从响应正文读业务码(认 number 与数字字符串)。
function Z.readBusinessCodeFromBody(body)
if type(body) ~= "string" then return nil end
local ok, rec = pcall(json.decode, body)
if not ok or type(rec) ~= "table" then return nil end
return Z.parseBusinessCode(rec.code)
end
-- hasUnusualActivity(body):**有优先级**——正文能解析出业务码就只信码;
-- 无码可读才看正文语义。
--
-- ⚠ 优先级是必须的:{"code":3012} 这种正文**不含语义词**,直接走文本判定会
-- 漏判 ⇒ 错误被归为可重试类 ⇒ 有**冷却惩罚**的风控被白重试多次
-- (qoder 那边记过同型缺陷)。
function Z.hasUnusualActivity(body)
local code = Z.readBusinessCodeFromBody(body)
if code ~= nil then return Z.isUnusualActivityCode(code) end
return Z.mentionsUnusualActivity(body)
end
-- formatUnusualActivityMessage(opts):3012 的**共用文案**。
-- opts.subject(定位信息,如 planId)/ opts.diagnostic / opts.rawResponse。
function Z.formatUnusualActivityMessage(opts)
opts = opts or {}
local subject = (type(opts.subject) == "string" and opts.subject ~= "")
and (" · " .. opts.subject) or ""
local diag = (type(opts.diagnostic) == "string" and opts.diagnostic ~= "")
and ("本机诊断:" .. opts.diagnostic .. "\n") or ""
local raw = (type(opts.rawResponse) == "string" and opts.rawResponse ~= "")
and ("原始响应:" .. opts.rawResponse) or ""
return "上游风控拦截(3012 unusual activity)" .. subject .. "。"
.. "⚠ 该错误有账号冷却惩罚(30 分钟,反复触发会升级到 24 小时乃至停用),"
.. "请勿连续重试。\n" .. diag .. raw
end
-- ============================================================================
-- 进程级账号画像(只给诊断用;**绝不写凭据**)
-- ============================================================================
-- 红线(照参考):账号只以**进程内自增序号**出现,不含 accountId/JWT/账号名
-- 的任何字符片段。
local profiles, nextOrdinal = {}, 1
local function normalizeKey(accountId)
if accountId == nil or accountId == "" then return "-" end
return tostring(accountId)
end
-- accountOrdinal(accountId):进程内序号,**首次见到时分配**(之后稳定)。
-- 同一账号跨请求必须是同一个序号,否则"本次是第几个账号"会随请求变化。
function Z.accountOrdinal(accountId)
local k = normalizeKey(accountId)
local p = profiles[k]
if p then return p.ordinal end
p = { ordinal = nextOrdinal, sent = 0, ok = 0, failed = 0 }
nextOrdinal = nextOrdinal + 1
profiles[k] = p
return p.ordinal
end
-- noteRequestSent(accountId, sentAt):记一次"请求已发出"。
-- ⚠ sentAt 必须在**真正发出之前**取,否则"距上一条"会把本次排队时间算进去。
function Z.noteRequestSent(accountId, sentAt)
Z.accountOrdinal(accountId)
local p = profiles[normalizeKey(accountId)]
if not p then return end
p.sent = p.sent + 1
-- ⚠ 旧值**下沉一格**再写新的:诊断要的"距上条"是**上一条**,不是本次。
-- 若拿 lastSentAt 顶替,算出来恒为 0ms,这一项就永远没有信息量
-- (参考做端到端验证时真的打印出过"距上条 0ms")。
p.previousSentAt = p.lastSentAt
p.lastSentAt = sentAt
end
function Z.noteRequestOk(accountId, at)
local p = profiles[normalizeKey(accountId)]
if not p then return end
p.ok = p.ok + 1
p.lastOkAt = at
end
function Z.noteRequestFailed(accountId)
local p = profiles[normalizeKey(accountId)]
if not p then return end
p.failed = p.failed + 1
end
-- formatGap(ms):把毫秒差写成一眼能读的形态。
--
-- ⚠ 各档**逐档单独算**:参考初版把分钟档写成 floor(seconds % 60),于是 90 秒
-- 被渲染成"30分30秒"(分位取的是**秒的余数**)。该错误在真实探针输出里发现,
-- 纯函数单测当时没覆盖 ≥60 秒的区间 —— 故本实现直接按 totalSeconds 拆。
function Z.formatGap(ms)
if ms == nil or ms < 0 then return "—" end
if ms < 1000 then return tostring(math.floor(ms + 0.5)) .. "ms" end
local totalSeconds = math.floor(ms / 1000)
if totalSeconds < 60 then return string.format("%.1fs", ms / 1000) end
local minutes = math.floor(totalSeconds / 60)
if minutes < 60 then
local rest = totalSeconds % 60
if rest == 0 then return tostring(minutes) .. "分" end
return tostring(minutes) .. "分" .. tostring(rest) .. "秒"
end
local hours = math.floor(minutes / 60)
local restMin = minutes % 60
if restMin == 0 then return tostring(hours) .. "小时" end
return tostring(hours) .. "小时" .. tostring(restMin) .. "分"
end
local function formatSinceLastOk(at, now)
if at == nil then return "从未成功过" end
return Z.formatGap(now - at) .. "前"
end
-- describeRequestShape(system, messages):读**真正要发出去的那个对象**的形态。
--
-- ⚠ 刻意**不读**常量(如"官方身份块 2898 字符"):常量只能证明"源码里写了多少
-- 字",证明不了"线上发出去多少字"。两者一旦不一致(例如某条改动把 system
-- 顶掉了),只有读实际值才看得出来 —— 它会**如实显示 0 字符**。
function Z.describeRequestShape(system, messages)
local blocks = type(system) == "table" and system or {}
local function blockChars(b)
if type(b) ~= "table" then return 0 end
return type(b.text) == "string" and #b.text or 0
end
local n = #blocks
local identityChars = 0
local perBlock = {}
for i = 1, math.min(2, n) do
local c = blockChars(blocks[i])
perBlock[#perBlock + 1] = c
identityChars = identityChars + c
end
local first = type(messages) == "table" and messages[1] or nil
local hasDate = false
if type(first) == "table" then
local c = first.content
if type(c) == "table" and type(c[1]) == "table" and type(c[1].text) == "string" then
hasDate = c[1].text:sub(1, 17) == "<system-reminder>"
elseif type(c) == "string" then
hasDate = c:sub(1, 17) == "<system-reminder>"
end
end
return {
systemBlocks = n,
identityChars = identityChars,
identityBlockChars = perBlock,
cliPrefixChars = n >= 1 and blockChars(blocks[1]) or 0,
hasDateBlock = hasDate,
firstRole = (type(first) == "table" and tostring(first.role)) or "",
}
end
-- formatDiagnostic(input):拼一行现场数据(**不含任何凭据片段**)。
function Z.formatDiagnostic(input)
input = input or {}
local p = profiles[normalizeKey(input.accountId)]
local ordinal = (p and p.ordinal) or Z.accountOrdinal(input.accountId)
local gap = (p and p.previousSentAt ~= nil)
and Z.formatGap((input.sentAt or 0) - p.previousSentAt) or "—"
local parts = {
"账号#" .. tostring(ordinal),
"本进程 成功 " .. tostring(p and p.ok or 0) .. "/失败 " .. tostring(p and p.failed or 0),
"最近成功 " .. formatSinceLastOk(p and p.lastOkAt, input.now or 0),
"距上条 " .. gap,
}
local shape = input.shape
if shape then
-- ⚠ 用 ~= nil 而非真值判断:Lua 里表恒为真,真正的分支是"传了/没传"
local per = #(shape.identityBlockChars or {}) > 0
and table.concat(shape.identityBlockChars, "+") or "无"
parts[#parts + 1] = "身份块 " .. tostring(shape.identityChars or 0)
.. " 字符(" .. per .. ")"
parts[#parts + 1] = "日期块 " .. (shape.hasDateBlock and "有" or "无")
end
parts[#parts + 1] = "HTTP " .. tostring(input.status)
return table.concat(parts, " · ")
end
-- resetDiagnostics():清空全部进程级状态(只给判据用)。
function Z.resetDiagnostics()
profiles = {}
nextOrdinal = 1
end
return Z
]===]
-- ★ ZCode 3012 判据/文案/诊断的**唯一出处**(插件主体与 channels-zcode 适配器
-- 共用同一份源码文本)。
--
-- 为什么必须共用:3012 不是"重试就能过",而是**有账号冷却惩罚**(30 分钟起步,
-- 反复触发升级到 24 小时甚至停用)。"请勿连续重试"是这段文案里最重要的一句,
-- 绝不能只在领取路径或只在推理路径上有(参考为此专门把文案抽成单一份)。
--
-- 本段在插件主体里**本地执行**得到 ZD;适配器里同文本前置进它自己的环境。
local ZD = (function()
local env = setmetatable({ json = json }, { __index = _G })
local f = load(ZCODE_DIAG_SRC, "zcode-diag")
if not f then return nil end
local ok, mod = pcall(f)
if not ok or type(mod) ~= "table" then return nil end
return mod
end)()
local ZCODE_ADAPTER_SRC = [===[
-- ============================================================================
-- ZCode 专用适配器(插件自装:channels-zcode)
@ -9291,6 +9622,33 @@ function adapter.transform_request(raw_body)
return json.encode(req)
end
-- ★ transform_error:把 3012 判成**不可重试**并带上现场诊断。
--
-- 为什么不能只依赖通用文案:3012 有账号冷却惩罚(30 分钟起步),
-- 归成"可重试的服务端错误"会让调用方**白重试**、把冷却越推越长。
-- 诊断行(账号序号/成功失败计数/距上条/身份块实测字符数)是排查的全部现场,
-- 且**不含任何凭据片段**(只出现进程内自增序号)。
function adapter.transform_error(status, body)
local text = tostring(body or "")
if Z and Z.hasUnusualActivity(text) then
return Z.formatUnusualActivityMessage({
diagnostic = Z.formatDiagnostic({
accountId = nil,
sentAt = os.time and os.time() * 1000 or 0,
now = os.time and os.time() * 1000 or 0,
status = status,
}),
rawResponse = text:sub(1, 500),
})
end
-- 非风控:交回 anthropic 基座的通用分类
if type(BASE) == "table" and type(BASE.transform_error) == "function" then
local ok, res = pcall(BASE.transform_error, status, body)
if ok and type(res) == "string" then return res end
end
return text
end
-- 暴露给判据:身份块长度与装载状态("装了但身份块为空"这类问题没有它只能靠猜)
adapter.__identity_probe = function()
return {
@ -9473,11 +9831,19 @@ end
-- 幂等(update 失败退化为 install);失败只记日志,不阻断插件加载。
local function installZcodeAdapter()
if not __llmsproxy_adapters then return false, "无适配器桥(旧内核?)" end
local f, lerr = load(ZCODE_ADAPTER_SRC)
-- ★ 把 3012 判据/文案/诊断模块**前置**到适配器源码里。
-- 这样适配器与插件主体用的是**同一份源码文本**(ZCODE_DIAG_SRC)——
-- 否则"请勿连续重试"这句话会只在一条路径上有(参考为此专门抽成单一份)。
-- ⚠ 不能直接拼接:ZCODE_DIAG_SRC 末尾是 `return Z`,直接拼会让那个 return
-- 结束**整个 chunk** ⇒ 后面的适配器代码永不执行("加载成功但适配器不存在",
-- 且装载会静默成功)。故把它包进一个函数,让 return 只结束该函数。
local full = "local Z = (function()\n" .. ZCODE_DIAG_SRC ..
"\nend)()\n" .. ZCODE_ADAPTER_SRC
local f, lerr = load(full)
if not f then return false, "源码语法错误: " .. tostring(lerr) end
local _, uerr = __llmsproxy_adapters.update("channels-zcode", ZCODE_ADAPTER_SRC)
local _, uerr = __llmsproxy_adapters.update("channels-zcode", full)
if uerr then
local _, ierr = __llmsproxy_adapters.install("channels-zcode", ZCODE_ADAPTER_SRC)
local _, ierr = __llmsproxy_adapters.install("channels-zcode", full)
if ierr then return false, ierr end
end
return true, nil
@ -10375,6 +10741,36 @@ function plugin.claims_capable(args)
} }
end
-- plugin.__zcode_diag_probe:只给测试/诊断用 —— 暴露共用的 3012 判据模块。
--
-- 为什么需要它:该模块被**两条路径共用**(领取路径在插件主体、推理路径在
-- channels-zcode 适配器)。判据必须能直接打**那一份**实现,否则会退化成
-- “各自测各自的副本”,而共用一旦漂移就再也测不出来。
function plugin.__zcode_diag_probe(args)
args = args or {}
local fn = tostring(args.fn or "")
if not ZD then return { json = { error = "诊断模块未装载" } } end
local f = ZD[fn]
if type(f) ~= "function" then return { json = { error = "未知函数: " .. fn } } end
if fn == "formatGap" then return { json = { result = f(tonumber(args.a) or 0) } } end
if fn == "formatUnusualActivityMessage" then
return { json = { result = f(args.opts or {}) } }
end
if fn == "formatDiagnostic" then return { json = { result = f(args.opts or {}) } } end
if fn == "describeRequestShape" then
return { json = { result = f(args.system, args.messages) } }
end
if fn == "accountOrdinal" then return { json = { result = f(args.a) } } end
if fn == "noteRequestSent" then f(args.a, tonumber(args.a2) or 0) return { json = { ok = true } } end
if fn == "noteRequestOk" then f(args.a, tonumber(args.a2) or 0) return { json = { ok = true } } end
if fn == "noteRequestFailed" then f(args.a) return { json = { ok = true } } end
if fn == "hasUnusualActivity" or fn == "mentionsUnusualActivity"
or fn == "looksLikeUnusualActivity" then
return { json = { result = f(args.a) } }
end
return { json = { error = "探针未暴露该函数: " .. fn } }
end
-- plugin.__adapter_install_probe:只给测试/诊断用 —— 报告本插件自装适配器的
-- 装载结果(含语法错误详情)。"装了但没生效"这类问题没有它只能靠猜。
function plugin.__adapter_install_probe()