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
+117
@@ -0,0 +1,117 @@
|
||||
// Package spill 把超长的工具结果落盘,并返回一个模型可用 read 工具取回的路径。
|
||||
// 动机:视图层为省上下文会截断超长 tool result,模型因此拿不回原文;落盘后
|
||||
// 截断提示里带上路径,模型需要时能读回全文。
|
||||
package spill
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// maxAge 是落盘文件的保留时长,超过即在 Prune 时删除(防无限增长)。
|
||||
const maxAge = 7 * 24 * time.Hour
|
||||
|
||||
// Dir 返回落盘目录(~/.autogo/results)。与 sessions/images/logs 同惯例;
|
||||
// 取不到 HOME 时退回相对目录。
|
||||
func Dir() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return filepath.Join(".autogo", "results")
|
||||
}
|
||||
return filepath.Join(home, ".autogo", "results")
|
||||
}
|
||||
|
||||
// Save 把 text 写入 dir 并返回文件路径。
|
||||
// 文件名优先用消毒后的 callID(同一工具调用幂等、可读),为空时退回内容哈希。
|
||||
// 已存在同路径文件则直接复用,不重复写。
|
||||
func Save(dir, callID, text string) (string, error) {
|
||||
if dir == "" {
|
||||
return "", fmt.Errorf("spill: empty dir")
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := sanitize(callID)
|
||||
if name == "" {
|
||||
sum := sha256.Sum256([]byte(text))
|
||||
name = "sha-" + hex.EncodeToString(sum[:])[:16]
|
||||
}
|
||||
path := filepath.Join(dir, name+".txt")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, nil // 幂等:内容已落盘
|
||||
}
|
||||
// tmp + rename 原子写,避免读到半截文件。
|
||||
tmp, err := os.CreateTemp(dir, name+".*.tmp")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
if _, err := tmp.WriteString(text); err != nil {
|
||||
tmp.Close()
|
||||
os.Remove(tmpName)
|
||||
return "", err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// Prune 删除 dir 下超过 maxAge 的文件;目录不存在时静默返回。
|
||||
func Prune(dir string) {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cutoff := time.Now().Add(-maxAge)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil || info.ModTime().After(cutoff) {
|
||||
continue
|
||||
}
|
||||
os.Remove(filepath.Join(dir, e.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
// sanitize 把 callID 收敛为安全的文件名片段:只留字母数字与 . _ -,
|
||||
// 其余替换为 _,并丢弃 . / .. 等特殊值(防路径穿越)。
|
||||
func sanitize(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9',
|
||||
r == '_', r == '-':
|
||||
b.WriteRune(r)
|
||||
case r == '.':
|
||||
b.WriteRune('_') // 不留 ".",杜绝 ".." 之类
|
||||
default:
|
||||
b.WriteRune('_')
|
||||
}
|
||||
}
|
||||
out := b.String()
|
||||
if out == "" {
|
||||
return ""
|
||||
}
|
||||
// 限长,避免超长 callID 造成文件名过长。
|
||||
if len(out) > 64 {
|
||||
out = out[:64]
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user