- Admit 不再同步调用审查模型:触发时只把审查**派发**到后台 goroutine,准入只读 当前已有结论。三重上界:同一适配器至多一次在飞(reviewInFlight)、同一会话每窗口 至多一次(lastReview)、整个适配器每窗口至多 defaultReviewPerWindow=6 次(reviewHits, 写成可测断言)。 - st.lastReview 改为在**派发时**置位 ⇒ 成功与失败两条路径同样被节流。修复前它只在 err == nil 分支置位,而实测 68% 的审查因对端回带 BOM 解析失败 ⇒ 缓冲满后每条入站帧 都付一次 ~6s 的模型调用(qq-ws/kook-ws/weixin-poll 三路单读 goroutine 全被钉住)。 - 新增 adapter.ParseReviewIndices:容忍 UTF-8 BOM、首尾空白/换行、Markdown 代码围栏、 数组外解说文字;越界/重复编号照旧丢弃。channelbridge.parseKeepIndices 改为委派它 (仅函数体 + 注释,未动限流/丢弃/审查触发语义)。 - 语义差异(已写进 flowgate_review.go 文件头):rate_limited 的门口丢与 FIFO 上限 一字不改;审查修剪降级为后台积压去噪,只修剪它当时看到的那批历史条目 —— review_pruned 不再从门口丢消息(实测 41–74 条/日),完整性↑、噪声↑,是否开口仍由 engage_judge/冷避/owner 闸判(三处判据未动)。 - cmd/cyrene:逐账号启动日志如实报出生效的入口流控(塔裁决 A:零行为改动、不翻默认), 让「没配流控键的账号闸门仍开着」一眼可见。 - 测试:4s 慢审查替身证明入站帧不被阻塞(变异回同步 ⇒ 红)、节流在失败路径生效 (摘掉派发时置位 ⇒ 红)、每适配器窗口预算上界(摘掉预算 ⇒ 红)、同适配器单飞 (摘掉 in-flight ⇒ 红)、在飞期间到达的消息不被连带修剪、带 BOM 字节的夹具 (夹具去掉 BOM ⇒ 红)。闸门用例 -race 通过。
95 lines
3.3 KiB
Go
95 lines
3.3 KiB
Go
package adapter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// utf8BOM 是 UTF-8 的字节序标记。对端(审查模型网关)会把它原样回带,而
|
|
// encoding/json 见到它报 `invalid character 'ï' looking for beginning of value` ——
|
|
// 实测 9 天 1,909 次审查里 1,288 次(67%)因此解析失败,每一次都白烧一次模型调用
|
|
// 并把「每窗口一次」的节流打穿(见 DevDocs/audit/群聊排队与回旧话-2026-10-24.md)。
|
|
const utf8BOM = "\ufeff"
|
|
|
|
// reviewExcerptLen 是解析失败时附带在错误里的模型输出长度(字节)。它只含模型
|
|
// 返回的编号数组(数字/方括号/逗号/围栏),足以定位「又换了什么壳」,不至于把
|
|
// 消息内容带进日志。
|
|
const reviewExcerptLen = 40
|
|
|
|
// ParseReviewIndices decodes the keep-index array a review model returned, rising
|
|
// above the shapes real gateways produce instead of requiring a bare JSON array:
|
|
//
|
|
// - a UTF-8 BOM prefix (`\ufeff`) — the actual cause of the 67% failure rate;
|
|
// - leading/trailing whitespace and newlines;
|
|
// - a Markdown code fence (` ```json … ``` ` or ` ``` … ``` `);
|
|
// - surrounding prose ("好的,保留以下编号:[0, 2]") — the outermost `[ … ]`
|
|
// span is used.
|
|
//
|
|
// The decoded indices are validated the same way the previous strict parser did:
|
|
// out-of-range and duplicate values are dropped and the rest sorted ascending, so a
|
|
// hallucinated number can never evict a real message. An empty or absent array is
|
|
// NOT an error here — the caller decides; a missing/non-array answer is.
|
|
func ParseReviewIndices(s string, bound int) ([]int, error) {
|
|
body, ok := reviewArraySpan(s)
|
|
if !ok {
|
|
return nil, fmt.Errorf("no JSON array in reviewer output %q", reviewExcerpt(s))
|
|
}
|
|
var idx []int
|
|
if err := json.Unmarshal([]byte(body), &idx); err != nil {
|
|
return nil, fmt.Errorf("decode reviewer indices %q: %w", reviewExcerpt(body), err)
|
|
}
|
|
seen := make(map[int]bool, len(idx))
|
|
out := make([]int, 0, len(idx))
|
|
for _, i := range idx {
|
|
if i >= 0 && i < bound && !seen[i] {
|
|
seen[i] = true
|
|
out = append(out, i)
|
|
}
|
|
}
|
|
sort.Ints(out)
|
|
return out, nil
|
|
}
|
|
|
|
// reviewArraySpan strips the decorations a model puts around its answer (BOM,
|
|
// whitespace, code fence, prose) and returns the outermost JSON array span.
|
|
func reviewArraySpan(s string) (string, bool) {
|
|
s = strings.TrimSpace(strings.TrimPrefix(s, utf8BOM))
|
|
s = strings.TrimSpace(unwrapCodeFence(s))
|
|
start := strings.IndexByte(s, '[')
|
|
end := strings.LastIndexByte(s, ']')
|
|
if start < 0 || end < start {
|
|
return "", false
|
|
}
|
|
return s[start : end+1], true
|
|
}
|
|
|
|
// unwrapCodeFence drops a leading ``` fence line and the closing ``` so the fenced
|
|
// body is left for the array scan. Anything without a leading fence is returned
|
|
// unchanged; a fence with no body is returned as-is and fails the array scan.
|
|
func unwrapCodeFence(s string) string {
|
|
if !strings.HasPrefix(s, "```") {
|
|
return s
|
|
}
|
|
nl := strings.IndexByte(s, '\n')
|
|
if nl < 0 {
|
|
return s
|
|
}
|
|
s = s[nl+1:]
|
|
if close := strings.LastIndex(s, "```"); close >= 0 {
|
|
s = s[:close]
|
|
}
|
|
return s
|
|
}
|
|
|
|
// reviewExcerpt truncates text for an error message so the next parse failure is
|
|
// diagnosable without logging the whole completion.
|
|
func reviewExcerpt(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if len(s) <= reviewExcerptLen {
|
|
return s
|
|
}
|
|
return s[:reviewExcerptLen] + "…"
|
|
}
|