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:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
package tools
import "strings"
// editMatch 是 old_string 在文件内容里的一个字节区间 [start, end)。
type editMatch struct {
start int
end int
}
// findEditMatches 按三级降级查找 old_string 的出现位置:
//
// 1. 精确匹配:字节完全一致,命中后不再降级;
// 2. 字形归一:把弯引号、破折号、不间断空格等"看起来一样"的字符归一后匹配
// (模型从网页/文档复制的文本经常带这些字符,精确匹配会失败);
// 3. 逐行降级:忽略每行行尾空白后再匹配,用来兜住"多了几个空格/少了几个空格"。
//
// 返回的区间始终指向**原文**,调用方据此替换,不会把归一化后的字符写回文件。
func findEditMatches(content, old string) []editMatch {
if old == "" {
return nil
}
if matches := findExactMatches(content, old); len(matches) > 0 {
return matches
}
if matches := findNormalizedMatches(content, old); len(matches) > 0 {
return matches
}
return findLineMatches(content, old)
}
// findExactMatches 返回不重叠的精确出现位置(已是字节区间)。
func findExactMatches(content, old string) []editMatch {
var out []editMatch
offset := 0
for {
i := strings.Index(content[offset:], old)
if i < 0 {
return out
}
start := offset + i
out = append(out, editMatch{start: start, end: start + len(old)})
offset = start + len(old)
}
}
// findNormalizedMatches 在字形归一后的 rune 序列上匹配,再把 rune 下标映射回
// 原始字节区间。归一化是逐 rune 的一对一替换,所以下标可以直接对应。
func findNormalizedMatches(content, old string) []editMatch {
contentRunes := []rune(content)
oldRunes := []rune(old)
if len(oldRunes) == 0 || len(oldRunes) > len(contentRunes) {
return nil
}
prefix := runeByteOffsets(contentRunes)
var out []editMatch
for i := 0; i+len(oldRunes) <= len(contentRunes); i++ {
match := true
for j := range oldRunes {
if normalizeEditRune(contentRunes[i+j]) != normalizeEditRune(oldRunes[j]) {
match = false
break
}
}
if !match {
continue
}
out = append(out, editMatch{start: prefix[i], end: prefix[i+len(oldRunes)]})
i += len(oldRunes) - 1
}
return out
}
// findLineMatches 逐行比较(忽略行尾空白与字形差异),返回整行窗口的字节区间。
// old 带结尾换行时连同最后一行的换行一起替换;不带时保留原文换行。
func findLineMatches(content, old string) []editMatch {
trailingNewline := strings.HasSuffix(old, "\n")
expected := strings.Split(old, "\n")
if trailingNewline {
expected = expected[:len(expected)-1]
}
if len(expected) == 0 {
return nil
}
lines := splitLines(content)
var out []editMatch
for i := 0; i+len(expected) <= len(lines); i++ {
matched := true
for j := range expected {
if normalizeEditLine(lines[i+j].text) != normalizeEditLine(expected[j]) {
matched = false
break
}
}
if !matched {
continue
}
last := lines[i+len(expected)-1]
end := last.end
if !trailingNewline {
end = last.contentEnd
}
if len(out) > 0 && out[len(out)-1].end > lines[i].start {
continue // 与上一个命中重叠:同一个位置只算一次
}
out = append(out, editMatch{start: lines[i].start, end: end})
i += len(expected) - 1
}
return out
}
type lineSpan struct {
start int
end int // 含换行
contentEnd int // 不含 \r\n / \n
text string
}
// splitLines 切出每一行及其字节区间。空内容返回空切片;末尾换行不产生空行。
func splitLines(s string) []lineSpan {
var out []lineSpan
for start := 0; start < len(s); {
i := strings.IndexByte(s[start:], '\n')
if i < 0 {
contentEnd := len(s)
if contentEnd > start && s[contentEnd-1] == '\r' {
contentEnd--
}
out = append(out, lineSpan{start: start, end: len(s), contentEnd: contentEnd, text: s[start:contentEnd]})
break
}
end := start + i + 1
contentEnd := end - 1
if contentEnd > start && s[contentEnd-1] == '\r' {
contentEnd--
}
out = append(out, lineSpan{start: start, end: end, contentEnd: contentEnd, text: s[start:contentEnd]})
start = end
}
return out
}
// runeByteOffsets 返回每个 rune 起始处的字节偏移,外加末尾哨兵。
func runeByteOffsets(runes []rune) []int {
out := make([]int, len(runes)+1)
offset := 0
for i, r := range runes {
out[i] = offset
offset += len(string(r))
}
out[len(runes)] = offset
return out
}
// normalizeEditRune 把视觉上等价、字节上不同的字符归一。
func normalizeEditRune(r rune) rune {
switch r {
case '‘', '’', '‚', '‛':
return '\''
case '“', '”', '„', '‟':
return '"'
case '‐', '‑', '‒', '–', '—', '―', '−', '-':
return '-'
case '\u00A0', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008',
'\u2009', '\u200A', '\u202F', '\u205F', '\u3000':
return ' '
}
return r
}
func normalizeEditLine(line string) string {
line = strings.TrimRight(line, " \t\r")
return string([]rune(normalizeEditTextRunes(line)))
}
func normalizeEditTextRunes(s string) []rune {
out := []rune(s)
for i := range out {
out[i] = normalizeEditRune(out[i])
}
return out
}
// applyEditMatches 从后往前替换,前面的偏移因此不受影响。
// 替换文本沿用命中区域的换行风格:命中区是 CRLF 而 new 用 LF 时,自动转成 CRLF,
// 避免一次编辑把整个文件的行尾改掉。
func applyEditMatches(content string, matches []editMatch, newText string) string {
updated := content
for i := len(matches) - 1; i >= 0; i-- {
m := matches[i]
region := content[m.start:m.end]
replacement := newText
if strings.Contains(region, "\r\n") && !strings.Contains(replacement, "\r\n") {
replacement = strings.ReplaceAll(replacement, "\n", "\r\n")
}
updated = updated[:m.start] + replacement + updated[m.end:]
}
return updated
}