Files
Sakurasan d5dac5a6fc 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。
2026-10-26 00:54:46 +08:00

292 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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"
}
}