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。
118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
// 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
|
|
}
|