Merge branch 'feat/m51-lastexits-close-remaining-upstream-f' into dev
# Conflicts: # internal/runtime/trace/errortoken.go
This commit is contained in:
commit
656c35f1e8
20 files changed
+1252
-58
No files matched your search
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
|
||||
)
|
||||
|
||||
// utf8BOM 是 UTF-8 的字节序标记。对端(审查模型网关)会把它原样回带,而
|
||||
@@ -13,9 +15,13 @@ import (
|
||||
// 并把「每窗口一次」的节流打穿(见 DevDocs/audit/群聊排队与回旧话-2026-10-24.md)。
|
||||
const utf8BOM = "\ufeff"
|
||||
|
||||
// reviewExcerptLen 是解析失败时附带在错误里的模型输出长度(字节)。它只含模型
|
||||
// reviewExcerptLen 是解析失败时附带在错误里的模型输出长度(**rune**)。它只含模型
|
||||
// 返回的编号数组(数字/方括号/逗号/围栏),足以定位「又换了什么壳」,不至于把
|
||||
// 消息内容带进日志。
|
||||
//
|
||||
// 🔴 单位是 rune 不是字节:审查模型回的中文/全角标点每个字 3 字节,按 40 **字节**
|
||||
// 截会把其中一个字切成半个 —— 错误串变成非法 UTF-8,终端显示 U+FFFD、JSON 客户端
|
||||
// 各自乱码。带上限出镜是刻意的(失败原因对排障有用),但上限必须以字符计。
|
||||
const reviewExcerptLen = 40
|
||||
|
||||
// ParseReviewIndices decodes the keep-index array a review model returned, rising
|
||||
@@ -83,12 +89,12 @@ func unwrapCodeFence(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// reviewExcerpt truncates text for an error message so the next parse failure is
|
||||
// diagnosable without logging the whole completion.
|
||||
// reviewExcerpt 把模型输出裁成一段可以进错误串的摘要。两步都不是可选的:
|
||||
//
|
||||
// - trace.RedactTextLimited 先清洗凭据形状(审查网关偶有把 Authorization 回显进
|
||||
// 输出的),再按 **rune** 截到 reviewExcerptLen —— 按字节截会把多字节字符切成
|
||||
// 半个,错误串因此变成非法 UTF-8;
|
||||
// - 压平空白:一条诊断只占一行,也挡掉换行注入。
|
||||
func reviewExcerpt(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) <= reviewExcerptLen {
|
||||
return s
|
||||
}
|
||||
return s[:reviewExcerptLen] + "…"
|
||||
return trace.RedactTextLimited(strings.Join(strings.Fields(s), " "), reviewExcerptLen)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// bomReviewFixture 是审查模型网关的实际回包形状:UTF-8 BOM(EF BB BF)紧接一个
|
||||
@@ -126,3 +127,45 @@ func TestParseReviewIndices_RejectsGarbage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReviewExcerpt_TruncatesByRune 钉住「上限按字符计,不按字节计」:审查模型回的中文
|
||||
// 每个字 3 字节,按 40 **字节**截会把其中一个字切成半个,错误串因此成为非法 UTF-8
|
||||
// (终端显示 U+FFFD、JSON 客户端各自乱码)。
|
||||
//
|
||||
// 变异判据:把 reviewExcerpt 换回 `s[:reviewExcerptLen] + "…"` 的字节截断 ⇒ 本用例红
|
||||
// (utf8.ValidString 为 false)。
|
||||
func TestReviewExcerpt_TruncatesByRune(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := reviewExcerpt(strings.Repeat("中", reviewExcerptLen*2))
|
||||
if !utf8.ValidString(got) {
|
||||
t.Fatalf("截断后不是合法 UTF-8(按字节截断了半个汉字):%q", got)
|
||||
}
|
||||
body := strings.TrimSuffix(got, "…")
|
||||
if n := utf8.RuneCountInString(body); n != reviewExcerptLen {
|
||||
t.Fatalf("保留 %d 个字符,期望 %d:%q", n, reviewExcerptLen, got)
|
||||
}
|
||||
if !strings.HasSuffix(got, "…") {
|
||||
t.Errorf("超限时应当带省略号:%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReviewExcerpt_RedactsCredentialShapes 钉住「带上限出镜 ≠ 原样出镜」:摘要仍要
|
||||
// 过 trace.RedactText,网关回显的凭据形状(sk-… / token=… 等)必须被打码。
|
||||
//
|
||||
// 变异判据:让 reviewExcerpt 只做截断、不过 trace.RedactTextLimited ⇒ 本用例红。
|
||||
func TestReviewExcerpt_RedactsCredentialShapes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const secret = "sk-abcdefghijklmnop"
|
||||
got := reviewExcerpt("[0, 1] Authorization: Bearer " + secret)
|
||||
if strings.Contains(got, secret) {
|
||||
t.Fatalf("凭据形状没被打码:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "[redacted]") {
|
||||
t.Errorf("打码标记缺失:%q", got)
|
||||
}
|
||||
if !strings.Contains(got, "[0, 1]") {
|
||||
t.Errorf("摘要丢了诊断价值(编号数组):%q", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user