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,291 @@
|
||||
// Package imagecache 实现磁盘图片缓存:历史消息中的图片只保留文件名引用,
|
||||
// 最新消息的图片才加载 base64 交给大模型处理。
|
||||
package imagecache
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cache 是一个磁盘图片缓存。
|
||||
type Cache struct {
|
||||
dir string
|
||||
maxAge time.Duration
|
||||
maxSize int64
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// New 创建图片缓存目录并返回 Cache 实例。
|
||||
// dir: 缓存目录(如 ~/.autogo/images/)
|
||||
// maxAge: 图片最大存活时间(0 = 永不过期)
|
||||
// maxSize: 缓存目录最大字节数(0 = 不限制)
|
||||
func New(dir string, maxAge time.Duration, maxSize int64) (*Cache, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("imagecache: create dir: %w", err)
|
||||
}
|
||||
c := &Cache{dir: dir, maxAge: maxAge, maxSize: maxSize}
|
||||
// 启动时清理过期图片
|
||||
go c.cleanup()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DefaultCache 创建默认缓存(~/.autogo/images/,30天过期,1GB限制)。
|
||||
func DefaultCache() (*Cache, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
home = "."
|
||||
}
|
||||
dir := filepath.Join(home, ".autogo", "images")
|
||||
return New(dir, 30*24*time.Hour, 1<<30)
|
||||
}
|
||||
|
||||
// Store 存储图片数据到缓存,返回文件名(不含路径)。
|
||||
// ext: 文件扩展名(如 ".jpg", ".png")
|
||||
func (c *Cache) Store(data []byte, ext string) (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if ext == "" {
|
||||
ext = ".jpg"
|
||||
}
|
||||
if !strings.HasPrefix(ext, ".") {
|
||||
ext = "." + ext
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
filename, err := c.generateFilename(ext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path := filepath.Join(c.dir, filename)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return "", fmt.Errorf("imagecache: write file: %w", err)
|
||||
}
|
||||
|
||||
return filename, nil
|
||||
}
|
||||
|
||||
// StoreReader 从 reader 存储图片,返回文件名。
|
||||
func (c *Cache) StoreReader(r io.Reader, ext string) (string, error) {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.Store(data, ext)
|
||||
}
|
||||
|
||||
// Load 从缓存加载图片数据。
|
||||
func (c *Cache) Load(filename string) ([]byte, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
path := c.safePath(filename)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("imagecache: file not found: %s", filename)
|
||||
}
|
||||
|
||||
return os.ReadFile(path)
|
||||
}
|
||||
|
||||
// LoadBase64 从缓存加载图片并返回 base64 编码字符串。
|
||||
func (c *Cache) LoadBase64(filename string) (string, error) {
|
||||
data, err := c.Load(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encodeBase64(data), nil
|
||||
}
|
||||
|
||||
// LoadWithMIME 从缓存加载图片,返回 base64 和 MIME 类型。
|
||||
func (c *Cache) LoadWithMIME(filename string) (string, string, error) {
|
||||
data, err := c.Load(filename)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
mime := detectMIME(filename)
|
||||
return encodeBase64(data), mime, nil
|
||||
}
|
||||
|
||||
// Exists 检查文件是否存在。
|
||||
func (c *Cache) Exists(filename string) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
path := c.safePath(filename)
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Delete 删除缓存中的图片。
|
||||
func (c *Cache) Delete(filename string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
path := c.safePath(filename)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// List 列出所有缓存的图片文件名。
|
||||
func (c *Cache) List() ([]string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var files []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
files = append(files, e.Name())
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// Stats 返回缓存统计信息。
|
||||
func (c *Cache) Stats() (count int, totalSize int64, err error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
totalSize += info.Size()
|
||||
}
|
||||
return count, totalSize, nil
|
||||
}
|
||||
|
||||
// Cleanup 手动触发清理。
|
||||
func (c *Cache) Cleanup() error {
|
||||
return c.cleanup()
|
||||
}
|
||||
|
||||
// generateFilename 生成唯一文件名。
|
||||
func (c *Cache) generateFilename(ext string) (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
timestamp := time.Now().Format("20060102-150405")
|
||||
return fmt.Sprintf("%s-%s%s", timestamp, hex.EncodeToString(b), ext), nil
|
||||
}
|
||||
|
||||
// safePath 安全地拼接路径,防止路径遍历攻击。
|
||||
func (c *Cache) safePath(filename string) string {
|
||||
// 清理文件名,移除路径分隔符
|
||||
filename = filepath.Base(filename)
|
||||
return filepath.Join(c.dir, filename)
|
||||
}
|
||||
|
||||
// cleanup 清理过期和超限的图片。
|
||||
func (c *Cache) cleanup() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
entries, err := os.ReadDir(c.dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var totalSize int64
|
||||
type fileEntry struct {
|
||||
name string
|
||||
modTime time.Time
|
||||
size int64
|
||||
}
|
||||
var files []fileEntry
|
||||
|
||||
// 收集文件信息
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
totalSize += info.Size()
|
||||
files = append(files, fileEntry{
|
||||
name: e.Name(),
|
||||
modTime: info.ModTime(),
|
||||
size: info.Size(),
|
||||
})
|
||||
}
|
||||
|
||||
// 删除过期文件
|
||||
if c.maxAge > 0 {
|
||||
for _, f := range files {
|
||||
if now.Sub(f.modTime) > c.maxAge {
|
||||
os.Remove(filepath.Join(c.dir, f.name))
|
||||
totalSize -= f.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果超过大小限制,删除最旧的文件
|
||||
if c.maxSize > 0 && totalSize > c.maxSize {
|
||||
// 按修改时间排序(最旧在前)
|
||||
for i := 0; i < len(files)-1; i++ {
|
||||
for j := i + 1; j < len(files); j++ {
|
||||
if files[i].modTime.After(files[j].modTime) {
|
||||
files[i], files[j] = files[j], files[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if totalSize <= c.maxSize {
|
||||
break
|
||||
}
|
||||
os.Remove(filepath.Join(c.dir, f.name))
|
||||
totalSize -= f.size
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeBase64 编码 base64。
|
||||
func encodeBase64(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
// detectMIME 根据扩展名检测 MIME 类型。
|
||||
func detectMIME(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
default:
|
||||
return "image/jpeg"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user