mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
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
This commit is contained in:
196
internal/snapshot/manager.go
Normal file
196
internal/snapshot/manager.go
Normal file
@ -0,0 +1,196 @@
|
||||
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:]
|
||||
}
|
||||
Reference in New Issue
Block a user