feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
@@ -0,0 +1,54 @@
|
||||
// Package textx 截字符串而不劈开一个字符。
|
||||
//
|
||||
// 这个包里只有一个问题:`s[:n]` 里的 n 是**字节**下标,而本仓库的文本大量是中文与
|
||||
// emoji——按字节切正好落在半个字上时,尾部会留下一个非法字节序列。它不会被拒绝,
|
||||
// 只会被渲染成乱码(终端上的 )、发给模型后变成一条它读不懂的提示,而 JSON 里
|
||||
// 带非法 UTF-8 时某些网关直接 400。写这类代码时"截个短"的直觉与字节下标一致,
|
||||
// 所以错误很容易长出来:这一层就是给调用方一个不用想的答案。
|
||||
package textx
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// ClipBytes 取不超过 n 字节的**前缀**,退到一个字符边界上。
|
||||
// 用于"预算是字节"的场景:发给模型的提示词段、工具输出上限——这些数字本来就是
|
||||
// 按字节定的(token 粗估与体积限制都看字节),只是不能切坏字符。
|
||||
func ClipBytes(s string, n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
// 从 n 往回退到下一个字符的起始字节(RuneStart 看的是 UTF-8 首字节的高位模式)。
|
||||
for n > 0 && !utf8.RuneStart(s[n]) {
|
||||
n--
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
// ClipRunes 取前 n 个**字符**。
|
||||
// 用于"个数是给人看的"场景:状态标签、列表行、预览——那里 40 就该是 40 个字,
|
||||
// 而不是"40 个字节里能塞多少个中文"。
|
||||
//
|
||||
// 注意它按字符数而不是终端显示宽度算:中文在这里算 1 个。要在终端里对齐列宽,
|
||||
// 用带宽度的截断(TUI 侧是 cutDisplay),别用这一版。
|
||||
func ClipRunes(s string, n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
// 先用便宜的字节判断过掉绝大多数短文本。
|
||||
if utf8.RuneCountInString(s) <= n {
|
||||
return s
|
||||
}
|
||||
// DecodeRuneInString 按下标逐步走,不需要先 make([]rune) 复制整串。
|
||||
i, seen := 0, 0
|
||||
for i < len(s) {
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
i += size
|
||||
seen++
|
||||
if seen == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return s[:i]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package textx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// 中文每字 3 字节、emoji 4 字节:按字节下标切下去,正好切开的位置都要覆盖到。
|
||||
func TestClipBytesNeverSplitsACharacter(t *testing.T) {
|
||||
s := "删除全部缓存文件🗑️继续" // 混合:中文 3 字节、带变体选择符的 emoji、再中文
|
||||
cases := []struct {
|
||||
name string
|
||||
s string
|
||||
n int
|
||||
}{
|
||||
{"切在中文中间", s, 4},
|
||||
{"切在中文中间2", s, 5},
|
||||
{"切在 emoji 中间", s, 8},
|
||||
{"预算为 0", s, 0},
|
||||
{"预算为负", s, -3},
|
||||
{"正好边界", "你好", 3},
|
||||
{"一个半字", "你好", 4},
|
||||
{"短于预算", "hi", 10},
|
||||
{"4 字节字符", "😀😀", 5},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := ClipBytes(c.s, c.n)
|
||||
if !utf8.ValidString(got) {
|
||||
t.Errorf("%s:结果非法 UTF-8(%q ← %q 取 %d 字节)", c.name, got, c.s, c.n)
|
||||
}
|
||||
if len(got) > c.n && c.n > 0 {
|
||||
t.Errorf("%s:超出预算 %d 字节,得到 %d", c.name, c.n, len(got))
|
||||
}
|
||||
if c.n >= len(c.s) && got != c.s {
|
||||
t.Errorf("%s:没超预算却改动了原文 %q", c.name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClipRunesCountsCharacters(t *testing.T) {
|
||||
if got := ClipRunes("删除全部缓存", 3); got != "删除全" {
|
||||
t.Errorf("按字截断 = %q,want 删除全", got)
|
||||
}
|
||||
// 按码点算:😀 是 1 个,而 🗑️ 是「U+1F5D1 + 变体选择符 U+FE0F」两个码点。
|
||||
// 这不是切坏字符(每个码点都完整),只是"字数"的口径本来就按码点——
|
||||
// 要按终端显示宽度算,用带宽度版本的截断(TUI 侧是 cutDisplay)。
|
||||
if got := ClipRunes("😀🗑️x", 2); got != "😀🗑" {
|
||||
t.Errorf("按码点计数不对:%q", got)
|
||||
}
|
||||
if got := ClipRunes("😀x", 1); got != "😀" {
|
||||
t.Errorf("4 字节字符应整体保留:%q", got)
|
||||
}
|
||||
if got := ClipRunes("abc", 0); got != "" {
|
||||
t.Errorf("0 应得空串,得到 %q", got)
|
||||
}
|
||||
if got := ClipRunes("abc", 9); got != "abc" {
|
||||
t.Errorf("未超字数却改动了原文:%q", got)
|
||||
}
|
||||
// 长串必须仍然是合法 UTF-8(这是这类 bug 的表现形式)。
|
||||
long := strings.Repeat("中文", 500)
|
||||
if !utf8.ValidString(ClipRunes(long, 3)) {
|
||||
t.Error("按字截断产生了非法字符")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClipBothValidOnEveryOffset 是回归护栏:对同一串中文,任意预算下两种截断
|
||||
// 都必须落在字符边界上。原来的 `s[:n]` 写法在这里会成片失败。
|
||||
func TestClipBothValidOnEveryOffset(t *testing.T) {
|
||||
s := "改一下 a.txt:把中文标题与 emoji 😀 都留下"
|
||||
for n := 0; n <= len(s)+2; n++ {
|
||||
if b := ClipBytes(s, n); !utf8.ValidString(b) {
|
||||
t.Fatalf("ClipBytes(%d) 非法:%q", n, b)
|
||||
}
|
||||
}
|
||||
for n := 0; n <= utf8.RuneCountInString(s)+2; n++ {
|
||||
r := ClipRunes(s, n)
|
||||
if !utf8.ValidString(r) {
|
||||
t.Fatalf("ClipRunes(%d) 非法:%q", n, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user