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。
55 lines
2.0 KiB
Go
55 lines
2.0 KiB
Go
// 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]
|
|
}
|