Files
HomeAgent/internal/snapshot/manager.go
root bc26850b50 feat: complete HomeAgent architecture v2
- IO abstraction layer with OutputChannel routing and capability validation
- Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning
- OneBot V11 QQ protocol plugin with Reverse WebSocket client
- Plugin system with hot-reload (SKILL.md + native factories)
- Knowledge system with TF-IDF vector indexing
- Personality system (personal.md)
- Text memory (JSONL with rotation)
- Change tracker (overlayfs) with rollback
- Lua adapter VM
- Design document (DESIGN.md)

Module: gitcode.com/JianFeeeee/HomeAgent
2026-07-02 12:04:36 +08:00

197 lines
4.7 KiB
Go

package snapshot
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/container"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
)
type Manager struct {
mu sync.RWMutex
dataDir string
container *container.Manager
snapshots map[types.AgentID][]types.Snapshot
}
func NewManager(dataDir string, cm *container.Manager) *Manager {
return &Manager{
dataDir: filepath.Join(dataDir, "snapshots"),
container: cm,
snapshots: make(map[types.AgentID][]types.Snapshot),
}
}
func (m *Manager) Create(ctx context.Context, agentID types.AgentID, containerID string, reason string) (*types.Snapshot, error) {
snapDir := filepath.Join(m.dataDir, string(agentID))
if err := os.MkdirAll(snapDir, 0755); err != nil {
return nil, fmt.Errorf("create snapshot dir: %w", err)
}
snapID := types.SnapshotID(fmt.Sprintf("snap_%s_%d", agentID, time.Now().UnixNano()))
imageTag := fmt.Sprintf("homeagent/snap-%s:%s", agentID, snapID)
imagePath := filepath.Join(snapDir, string(snapID)+".tar")
if err := m.container.Commit(ctx, containerID, imageTag); err != nil {
return nil, fmt.Errorf("commit container: %w", err)
}
if err := m.container.SaveImage(ctx, imageTag, imagePath); err != nil {
return nil, fmt.Errorf("save image: %w", err)
}
info, err := os.Stat(imagePath)
var size int64
if err == nil {
size = info.Size()
}
snap := types.Snapshot{
ID: snapID,
AgentID: agentID,
CreatedAt: time.Now(),
Reason: reason,
Size: size,
DockerImage: imageTag,
Valid: true,
}
m.mu.Lock()
m.snapshots[agentID] = append(m.snapshots[agentID], snap)
m.mu.Unlock()
log.Printf("[snapshot] created %s for agent %s (reason: %s, size: %d bytes)", snapID, agentID, reason, size)
m.enforceLimit(agentID)
return &snap, nil
}
func (m *Manager) Restore(ctx context.Context, agentID types.AgentID, containerID string, snapID types.SnapshotID) error {
m.mu.RLock()
snapshots := m.snapshots[agentID]
var target *types.Snapshot
for _, s := range snapshots {
if s.ID == snapID && s.Valid {
target = &s
break
}
}
m.mu.RUnlock()
if target == nil {
return fmt.Errorf("snapshot %s not found or invalid", snapID)
}
snapDir := filepath.Join(m.dataDir, string(agentID))
imagePath := filepath.Join(snapDir, string(snapID)+".tar")
if _, err := os.Stat(imagePath); os.IsNotExist(err) {
return fmt.Errorf("snapshot file %s not found", imagePath)
}
if err := m.container.Stop(ctx, containerID); err != nil {
log.Printf("[snapshot] warning: stop container during restore: %v", err)
}
if err := m.container.Remove(ctx, containerID); err != nil {
return fmt.Errorf("remove container for restore: %w", err)
}
if err := m.container.LoadImage(ctx, imagePath); err != nil {
return fmt.Errorf("load snapshot image: %w", err)
}
log.Printf("[snapshot] restored agent %s to snapshot %s", agentID, snapID)
return nil
}
func (m *Manager) List(agentID types.AgentID) []types.Snapshot {
m.mu.RLock()
defer m.mu.RUnlock()
snapshots := m.snapshots[agentID]
result := make([]types.Snapshot, len(snapshots))
copy(result, snapshots)
sort.Slice(result, func(i, j int) bool {
return result[i].CreatedAt.After(result[j].CreatedAt)
})
return result
}
func (m *Manager) Latest(agentID types.AgentID) *types.Snapshot {
snapshots := m.List(agentID)
if len(snapshots) == 0 {
return nil
}
return &snapshots[0]
}
func (m *Manager) MarkInvalid(agentID types.AgentID, snapID types.SnapshotID) {
m.mu.Lock()
defer m.mu.Unlock()
for i, s := range m.snapshots[agentID] {
if s.ID == snapID {
m.snapshots[agentID][i].Valid = false
return
}
}
}
func (m *Manager) enforceLimit(agentID types.AgentID) {
m.mu.Lock()
defer m.mu.Unlock()
snapshots := m.snapshots[agentID]
if len(snapshots) <= 20 {
return
}
sort.Slice(snapshots, func(i, j int) bool {
return snapshots[i].CreatedAt.Before(snapshots[j].CreatedAt)
})
toRemove := len(snapshots) - 20
for i := 0; i < toRemove; i++ {
s := snapshots[i]
snapDir := filepath.Join(m.dataDir, string(agentID))
imagePath := filepath.Join(snapDir, string(s.ID)+".tar")
os.Remove(imagePath)
}
m.snapshots[agentID] = snapshots[toRemove:]
}
func (m *Manager) Cleanup(agentID types.AgentID, keep int) {
m.mu.Lock()
defer m.mu.Unlock()
snapshots := m.snapshots[agentID]
if len(snapshots) <= keep {
return
}
sort.Slice(snapshots, func(i, j int) bool {
return snapshots[i].CreatedAt.Before(snapshots[j].CreatedAt)
})
toRemove := len(snapshots) - keep
for i := 0; i < toRemove; i++ {
s := snapshots[i]
snapDir := filepath.Join(m.dataDir, string(agentID))
imagePath := filepath.Join(snapDir, string(s.ID)+".tar")
os.Remove(imagePath)
}
m.snapshots[agentID] = snapshots[toRemove:]
}