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:
root
2026-07-02 12:04:36 +08:00
parent 1a7a846d58
commit bc26850b50
43 changed files with 10296 additions and 0 deletions

View File

@ -0,0 +1,130 @@
package tracker
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"time"
)
type ChangeType string
const (
ChangeFileCreated ChangeType = "created"
ChangeFileModified ChangeType = "modified"
ChangeFileDeleted ChangeType = "deleted"
)
type FileChange struct {
Path string `json:"path"`
Type ChangeType `json:"type"`
SizeBefore int64 `json:"size_before,omitempty"`
SizeAfter int64 `json:"size_after,omitempty"`
HashBefore string `json:"hash_before,omitempty"`
HashAfter string `json:"hash_after,omitempty"`
Content []byte `json:"-"` // stored separately, not in JSON
}
type ChangeSet struct {
ID string `json:"id"`
Action string `json:"action"`
Timestamp time.Time `json:"timestamp"`
Files []FileChange `json:"files"`
}
func NewChangeSet(action string) *ChangeSet {
return &ChangeSet{
ID: fmt.Sprintf("cs_%d", time.Now().UnixNano()),
Action: action,
Timestamp: time.Now(),
}
}
func fileHash(path string) (string, int64, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", 0, err
}
h := sha256.Sum256(data)
return hex.EncodeToString(h[:]), int64(len(data)), nil
}
func fileInfo(path string) (size int64, modTime time.Time, err error) {
info, err := os.Stat(path)
if err != nil {
return 0, time.Time{}, err
}
return info.Size(), info.ModTime(), nil
}
type FSState struct {
Files map[string]FileChange `json:"files"`
Root string `json:"root"`
}
func captureFSState(root string) (*FSState, error) {
state := &FSState{
Files: make(map[string]FileChange),
Root: root,
}
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
rel, _ := filepath.Rel(root, path)
hash, size, err := fileHash(path)
if err != nil {
return nil
}
state.Files[rel] = FileChange{
Path: rel,
HashAfter: hash,
SizeAfter: size,
}
return nil
})
return state, err
}
func diffStates(before, after *FSState) []FileChange {
var changes []FileChange
seen := make(map[string]bool)
for path, afterFile := range after.Files {
seen[path] = true
if beforeFile, ok := before.Files[path]; ok {
if beforeFile.HashAfter != afterFile.HashAfter {
changes = append(changes, FileChange{
Path: path,
Type: ChangeFileModified,
HashBefore: beforeFile.HashAfter,
HashAfter: afterFile.HashAfter,
SizeBefore: beforeFile.SizeAfter,
SizeAfter: afterFile.SizeAfter,
})
}
} else {
changes = append(changes, FileChange{
Path: path,
Type: ChangeFileCreated,
HashAfter: afterFile.HashAfter,
SizeAfter: afterFile.SizeAfter,
})
}
}
for path := range before.Files {
if !seen[path] {
changes = append(changes, FileChange{
Path: path,
Type: ChangeFileDeleted,
HashBefore: before.Files[path].HashAfter,
SizeBefore: before.Files[path].SizeAfter,
})
}
}
return changes
}

223
internal/tracker/tracker.go Normal file
View File

@ -0,0 +1,223 @@
package tracker
import (
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
)
type Tracker struct {
mu sync.Mutex
dataDir string
workDir string
lowerDir string
upperDir string
mergeDir string
mounted bool
active bool
before *FSState
changeSets []*ChangeSet
}
func NewTracker(dataDir, workDir string) *Tracker {
return &Tracker{
dataDir: dataDir,
workDir: workDir,
lowerDir: filepath.Join(workDir, "lower"),
upperDir: filepath.Join(workDir, "upper"),
mergeDir: filepath.Join(workDir, "merged"),
changeSets: make([]*ChangeSet, 0),
}
}
func (t *Tracker) Init() error {
for _, d := range []string{t.lowerDir, t.upperDir, t.mergeDir} {
if err := os.MkdirAll(d, 0755); err != nil {
return fmt.Errorf("create overlay dir %s: %w", d, err)
}
}
log.Printf("[tracker] initialized (work=%s)", t.workDir)
return nil
}
func (t *Tracker) Start() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.mounted {
return nil
}
if err := t.mountOverlay(); err != nil {
return fmt.Errorf("mount overlay: %w", err)
}
t.mounted = true
t.active = true
t.before = t.capture()
log.Printf("[tracker] overlay mounted at %s", t.mergeDir)
return nil
}
func (t *Tracker) Stop() error {
t.mu.Lock()
defer t.mu.Unlock()
if !t.mounted {
return nil
}
if err := t.umountOverlay(); err != nil {
return fmt.Errorf("umount overlay: %w", err)
}
t.mounted = false
t.active = false
return nil
}
func (t *Tracker) PreAction(action string) *ChangeSet {
t.mu.Lock()
defer t.mu.Unlock()
cs := NewChangeSet(action)
t.before = t.capture()
return cs
}
func (t *Tracker) PostAction(action string) *ChangeSet {
t.mu.Lock()
defer t.mu.Unlock()
after := t.capture()
changes := diffStates(t.before, after)
cs := NewChangeSet(action)
cs.Files = changes
if len(changes) > 0 {
t.changeSets = append(t.changeSets, cs)
t.saveChangeSet(cs)
log.Printf("[tracker] action=%s changed=%d files", action, len(changes))
for _, f := range changes {
log.Printf(" %s: %s", f.Type, f.Path)
}
}
t.before = t.capture()
return cs
}
func (t *Tracker) HasChanges() bool {
return len(t.changeSets) > 0
}
func (t *Tracker) ChangeSets() []*ChangeSet {
t.mu.Lock()
defer t.mu.Unlock()
result := make([]*ChangeSet, len(t.changeSets))
copy(result, t.changeSets)
return result
}
func (t *Tracker) capture() *FSState {
state, err := captureFSState(t.upperDir)
if err != nil {
return &FSState{Files: make(map[string]FileChange), Root: t.upperDir}
}
return state
}
func (t *Tracker) mountOverlay() error {
workDir := filepath.Join(t.workDir, "work")
os.MkdirAll(workDir, 0755)
args := []string{
"-t", "overlay",
"overlay",
"-o", fmt.Sprintf("lowerdir=%s,upperdir=%s,workdir=%s", t.lowerDir, t.upperDir, workDir),
t.mergeDir,
}
cmd := exec.Command("mount", args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("mount overlayfs failed: %s: %w", string(output), err)
}
return nil
}
func (t *Tracker) umountOverlay() error {
cmd := exec.Command("umount", t.mergeDir)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("umount overlayfs failed: %s: %w", string(output), err)
}
return nil
}
func (t *Tracker) saveChangeSet(cs *ChangeSet) {
dir := filepath.Join(t.dataDir, "changesets")
os.MkdirAll(dir, 0755)
path := filepath.Join(dir, cs.ID+".json")
data, err := json.MarshalIndent(cs, "", " ")
if err != nil {
log.Printf("[tracker] save changeset %s: %v", cs.ID, err)
return
}
if err := os.WriteFile(path, data, 0644); err != nil {
log.Printf("[tracker] write changeset %s: %v", cs.ID, err)
}
}
func (t *Tracker) Rollback() error {
t.mu.Lock()
defer t.mu.Unlock()
if t.mounted {
if err := t.umountOverlay(); err != nil {
return fmt.Errorf("umount for rollback: %w", err)
}
}
if err := os.RemoveAll(t.upperDir); err != nil {
return fmt.Errorf("remove upper: %w", err)
}
if err := os.RemoveAll(filepath.Join(t.workDir, "work")); err != nil {
return fmt.Errorf("remove work: %w", err)
}
if err := os.MkdirAll(t.upperDir, 0755); err != nil {
return fmt.Errorf("recreate upper: %w", err)
}
t.changeSets = nil
t.before = nil
t.mounted = false
log.Printf("[tracker] rollback complete")
return nil
}
func (t *Tracker) MergeDir() string {
return t.mergeDir
}
func (t *Tracker) Stats() map[string]interface{} {
t.mu.Lock()
defer t.mu.Unlock()
totalChanges := 0
for _, cs := range t.changeSets {
totalChanges += len(cs.Files)
}
return map[string]interface{}{
"mounted": t.mounted,
"active": t.active,
"change_sets": len(t.changeSets),
"total_changes": totalChanges,
"merge_dir": t.mergeDir,
"upper_dir": t.upperDir,
}
}