fix(zcode): 日期块对齐参考的两条规则(幂等 + 只看第一条),修 off-by-one

通读参考 zcode-identity.ts 的 withContextPrefix 后发现我的实现漏了两条官方行为,
且其中一条藏着真实 bug:

## ① ★ 幂等判断 off-by-one(真 bug)
`"<system-reminder>"` 是 **17** 个字符,而我写的是 `c:sub(1, 16) == SR`
—— 判断**永远为假** ⇒ 幂等完全失效:同一请求重试/多次 transform 时日期块
会叠加成多层,改变 content 的前缀结构(上游检查的正是这个结构)。
修法:用 `#SR` 算长度,不写死数字。

## ② 只看**第一条**消息,且它必须 role == "user"
我原来实现成「往后扫第一个 user 消息」。官方行为是:只处理 `messages[0]`,
且若它不是 user 就**完全不动** —— 不是去找后面那条 user。已对齐。

## ③ 其它类型兜底
参考对非字符串/非数组的 content 也会置为 `[prefix]`,照做。

判据 2 条(新增):
- TestZcodeContextPrefixIsIdempotent     已含日期块时不重复插
- TestZcodeContextPrefixOnlyTouchesFirstMessage  首条非 user 时不动、首条 user 时插入
变异 2/2 咬住:把 #SR 改回写死的 16 → 报红;去掉幂等判断 → 报红。

★ 我自己在写这条判据时又犯了一个错并当场自查出来:断言「文本 content 应保持
字符串」,而 anthropic 基座把文本转成块数组是**正常**的协议转换行为 —— 差点把
正确的实现判成错。判据要验的是「**日期块**有没有被插进来」这个事实,不是
content 的容器类型。

验证:internal/core 335s + internal/gateway 207s 全绿。
This commit is contained in:
JianFeeeee
2026-10-16 01:13:11 +08:00
parent 8b318a8507
commit b871c0c713
2 changed files with 137 additions and 7 deletions

View File

@ -9250,18 +9250,40 @@ function adapter.transform_request(raw_body)
req.system = buildSystemBlocks(caller, opts)
-- 日期块:插到**第一条** user 消息的 content 数组最前(官方形态)。
-- ★ 日期块:只处理**第一条**消息,且它必须是 role == "user"(官方行为)。
--
-- 两条容易漏掉的规则(照参考 withContextPrefix,「优化」它们会偏离官方):
-- ① **幂等**:已以 <system-reminder> 开头则不重复插。不判的话,同一请求
-- 在重试/多次 transform 时会把日期块叠成好几层,改变前缀结构;
-- ② **只看第一条**:若首条不是 user(例如 assistant 开场),**不动** ——
-- 不是"往后找一个 user 消息"。我第一版就是往后找,与官方行为不符。
local msgs = req.messages
if type(msgs) == "table" then
for _, m in ipairs(msgs) do
if type(m) == "table" and m.role == "user" then
local c = m.content
local first = msgs[1]
if type(first) == "table" and first.role == "user" then
local c = first.content
-- 已插过(字符串或块数组两种形态都要判)→ 不重复插。
-- ⚠ 长度必须用 #SR 算,不能写死 —— 我第一版写 `sub(1, 16)` 而
-- "<system-reminder>" 是 **17** 字符,判断**永远为假** ⇒ 幂等失效、
-- 重试时日期块叠加(判据当场抓到)。
local SR = "<system-reminder>"
local already = false
if type(c) == "string" then
already = c:sub(1, #SR) == SR
elseif type(c) == "table" and type(c[1]) == "table" then
local t = c[1].text
already = type(t) == "string" and t:sub(1, #SR) == SR
end
if not already then
local blk = contextPrefixBlock()
if type(c) == "table" then
table.insert(c, 1, contextPrefixBlock())
table.insert(c, 1, blk)
elseif type(c) == "string" then
m.content = { contextPrefixBlock(), { type = "text", text = c } }
-- 纯文本 content 转成块数组(官方就是数组形态)
first.content = { blk, { type = "text", text = c } }
else
first.content = { blk }
end
break
end
end
end