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,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
|
||||
}
|
||||
Reference in new issue
Block a user