mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
- 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
197 lines
4.7 KiB
Go
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:]
|
|
}
|