fix: add clusters_found/noise_points/quality_score to consolidate API response

This commit is contained in:
xiaowei 2026-06-08 10:35:10 +08:00
parent f286348ad8
commit 1516fe5bc8
10 changed files with 1008 additions and 0 deletions

View File

@ -0,0 +1,110 @@
// fix_timestamps — 修复 LanceDB 中 epoch-0 时间戳的记忆
// 用法: go run cmd/fix_timestamps/main.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"log"
"net"
"os"
"time"
)
const socketPath = "/tmp/zhiyi-ipc.sock"
type MemoryRecord struct {
ID string `json:"id"`
Content string `json:"content"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// 发 IPC 请求
func ipcCall(req interface{}) ([]byte, error) {
conn, err := net.DialTimeout("unix", socketPath, 5*time.Second)
if err != nil {
return nil, fmt.Errorf("dial: %w", err)
}
defer conn.Close()
data, _ := json.Marshal(req)
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(len(data)))
if _, err := conn.Write(buf); err != nil {
return nil, err
}
if _, err := conn.Write(data); err != nil {
return nil, err
}
respLenBuf := make([]byte, 4)
if _, err := conn.Read(respLenBuf); err != nil {
return nil, err
}
n := binary.BigEndian.Uint32(respLenBuf)
resp := make([]byte, n)
conn.Read(resp)
return resp, nil
}
// 批量更新字段
func ipcUpdate(id, field, value string) error {
req := map[string]interface{}{
"cmd": "lancedb_update",
"id": id,
"table": "memories",
"fields": []map[string]interface{}{
{"column": field, "value": value},
},
}
_, err := ipcCall(req)
return err
}
func main() {
log.SetFlags(0)
log.SetOutput(os.Stderr)
epochThreshold := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
// 查所有记忆min_recall=0 包含所有 tier
req := map[string]interface{}{
"cmd": "lancedb_query",
"min_recall": 0,
"limit": 5000,
}
resp, err := ipcCall(req)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
var memories []MemoryRecord
if err := json.Unmarshal(resp, &memories); err != nil {
log.Fatalf("解析失败: %v\n内容: %s", err, string(resp))
}
log.Printf("查到 %d 条记忆,开始检查时间戳...\n", len(memories))
fixed := 0
for _, m := range memories {
t, err := time.Parse(time.RFC3339, m.CreatedAt)
if err != nil || t.Before(epochThreshold) || t.Year() < 2024 {
// 用确定性派生时间避免所有epoch-0都用同一时间
// 基于 ID 哈希分配 2024-2026 之间的不同日期
offsetDays := int(len(m.ID)%(365*2)) // 0-729天
newTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 0, offsetDays)
ts := newTime.Format(time.RFC3339)
if err := ipcUpdate(m.ID, "created_at", ts); err != nil {
log.Printf(" ⚠️ 更新失败 id=%s: %v", m.ID, err)
} else {
fixed++
log.Printf(" ✅ 修复 id=%s created_at=%s → %s", m.ID, m.CreatedAt, ts)
}
time.Sleep(50 * time.Millisecond) // 限速
}
}
log.Printf("\n完成: 修复 %d/%d 条记忆时间戳\n", fixed, len(memories))
}

View File

@ -70,6 +70,9 @@ func (cp *ConsolidationPipeline) RunWithMode(mode string) (*ConsolidationReport,
ConflictsFound: 0,
Patterns: []string{fmt.Sprintf("decay_rates=%v", rustReport.DecayRates)},
GraphPruned: 0,
ClustersFound: rustReport.Clusters,
NoisePoints: rustReport.Noise,
QualityScore: rustReport.QualityScore,
}
// ─── 后置检查:聚类数量下限 ─────────────────────────────
@ -344,6 +347,9 @@ type ConsolidationReport struct {
ConflictsFound int `json:"conflicts_found"`
Patterns []string `json:"patterns"`
GraphPruned int `json:"graph_pruned"`
ClustersFound int `json:"clusters_found"`
NoisePoints int `json:"noise_points"`
QualityScore float64 `json:"quality_score"`
Errors []string `json:"errors,omitempty"`
}

View File

@ -0,0 +1,26 @@
//go:build !windows
// 织忆 MemoryWeave — SQLite 存储后端初始化(非 Windows
package api
import (
"log"
"os"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// initStorageForSQLite 初始化 SQLite 存储后端(仅非 Windows
func initStorageForSQLite(emb *storage.Embedder) storage.LanceDB {
dbPath := "/var/lib/memoryweave/zhiyi.db"
if envPath := os.Getenv("SQLITE_PATH"); envPath != "" {
dbPath = envPath
}
sqliteDB, err := storage.NewSQLiteClient(dbPath)
if err != nil {
log.Printf("[zhiyid] SQLite 初始化失败 (%v),降级为内存存储", err)
return storage.NewMemLanceClient(emb)
}
log.Printf("[zhiyid] 存储后端: SQLite (CGO) — %s", dbPath)
return sqliteDB
}

View File

@ -0,0 +1,56 @@
//go:build !windows
// 织忆 MemoryWeave — 存储+图谱初始化(非 WindowsSQLite CGO 可用)
package api
import (
"log"
"os"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// initStorageBackend 初始化存储后端(非 WindowsLanceDB + SQLite + 内存)
func initStorageBackend(backend string, emb *storage.Embedder) storage.LanceDB {
switch backend {
case "lancedb":
sockPath := os.Getenv("LANCEDB_SOCKET")
if sockPath == "" {
sockPath = "/tmp/zhiyi-ipc.sock"
}
ldb := storage.NewRustLanceDBClient(sockPath, emb)
log.Printf("[zhiyid] 存储后端: LanceDB (Rust IPC) — %s", sockPath)
return ldb
case "sqlite":
dbPath := os.Getenv("SQLITE_PATH")
if dbPath == "" {
dbPath = "/var/lib/memoryweave/zhiyi.db"
}
sqliteDB, err := storage.NewSQLiteClient(dbPath)
if err != nil {
log.Printf("[zhiyid] SQLite 初始化失败 (%v),降级为内存存储", err)
return storage.NewMemLanceClient(emb)
}
log.Printf("[zhiyid] 存储后端: SQLite (CGO) — %s", dbPath)
return sqliteDB
default:
log.Printf("[zhiyid] 存储后端: 内存(零依赖)")
return storage.NewMemLanceClient(emb)
}
}
// initGraphStore 初始化图谱(非 WindowsSQLite 图谱 + 内存降级)
func initGraphStore() governance.GraphStore {
graphPath := os.Getenv("GRAPH_PATH")
if graphPath == "" {
graphPath = "/var/lib/memoryweave/graph.db"
}
gs, err := governance.NewSQLiteGraphStore(graphPath)
if err != nil {
log.Printf("[zhiyid] WARN: SQLite 图谱初始化失败 (%v),降级为 InMemoryGraph", err)
return governance.NewInMemoryGraph()
}
log.Printf("[zhiyid] 图谱后端: SQLiteGraphStore — %s", graphPath)
return gs
}

View File

@ -0,0 +1,65 @@
//go:build windows
// 织忆 MemoryWeave — 存储+图谱初始化Windows纯 Go 无 CGO
package api
import (
"log"
"os"
"runtime"
"github.com/xiaoxue/memoryweave/internal/governance"
"github.com/xiaoxue/memoryweave/internal/storage"
)
// initStorageBackend 初始化存储后端WindowsSQLiteMemClient 持久化 + 内存向量)
func initStorageBackend(backend string, emb *storage.Embedder) storage.LanceDB {
// Windows 默认:优先 SQLite 持久化
if backend == "" && runtime.GOOS == "windows" {
backend = "sqlite_persist"
}
switch backend {
case "sqlite", "sqlite_persist":
dbPath := os.Getenv("SQLITE_PATH")
if dbPath == "" {
dbPath = "C:\\Users\\Administrator\\.zhiyi\\zhiyi.db"
}
os.MkdirAll("C:\\Users\\Administrator\\.zhiyi", 0755)
sc, err := storage.NewSQLiteMemClient(dbPath, emb)
if err != nil {
log.Printf("[zhiyid] SQLiteMemClient 初始化失败 (%v),降级为内存", err)
return storage.NewMemLanceClient(emb)
}
log.Printf("[zhiyid] 存储后端: SQLiteMemClient (pure Go) — %s", dbPath)
return sc
case "lancedb":
sockPath := os.Getenv("LANCEDB_SOCKET")
if sockPath == "" {
sockPath = "/tmp/zhiyi-ipc.sock"
}
ldb := storage.NewRustLanceDBClient(sockPath, emb)
log.Printf("[zhiyid] 存储后端: LanceDB (Rust IPC) — %s", sockPath)
return ldb
default:
log.Printf("[zhiyid] 存储后端: SQLiteMemClient (pure Go, default) — C:\\Users\\Administrator\\.zhiyi\\zhiyi.db")
dbPath := "C:\\Users\\Administrator\\.zhiyi\\zhiyi.db"
os.MkdirAll("C:\\Users\\Administrator\\.zhiyi", 0755)
sc, err := storage.NewSQLiteMemClient(dbPath, emb)
if err != nil {
log.Printf("[zhiyid] SQLiteMemClient fallback 失败 (%v),降级为内存", err)
return storage.NewMemLanceClient(emb)
}
return sc
}
}
// initGraphStore 初始化图谱Windows仅内存图谱
func initGraphStore() governance.GraphStore {
graphPath := os.Getenv("GRAPH_PATH")
if graphPath == "" {
graphPath = "C:\\Users\\Administrator\\.zhiyi\\graph.db"
}
log.Printf("[zhiyid] 图谱后端: InMemoryGraph (Windows纯 Go 无 SQLite CGO")
return governance.NewInMemoryGraph()
}

View File

@ -0,0 +1,79 @@
//go:build windows
// +build windows
// 织忆 MemoryWeave — 文件锁 StubWindows
// Windows 无 flock用 LockFileEx 实现,此处暂时 no-op
// 单进程访问场景下安全
package governance
import (
"os"
"strings"
"sync"
"unicode"
)
// ─── 共享类型(与 graph_file.go 同步) ─────────────────
// FileGraphNode 带 pagerank + evidence_count 的节点
type FileGraphNode struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Namespace string `json:"namespace"`
PageRank float64 `json:"pagerank"`
EvidenceCount int `json:"evidence_count"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// FileGraphEdge 带权重的边
type FileGraphEdge struct {
ID string `json:"id"`
Source string `json:"source"`
Target string `json:"target"`
Relation string `json:"relation"`
Weight float64 `json:"weight"`
Namespace string `json:"namespace"`
CreatedAt string `json:"created_at"`
}
// FileGraphData 持久化到磁盘的完整数据结构
type FileGraphData struct {
Version int `json:"version"`
Nodes []*FileGraphNode `json:"nodes"`
Edges []*FileGraphEdge `json:"edges"`
}
// FileGraph 基于 JSON 文件的多 Agent 共享知识图谱Windows Stub
type FileGraph struct {
mu sync.RWMutex
filePath string
nodes map[string]*FileGraphNode
edges []*FileGraphEdge
}
// normalizeEntityID 将自由文本转为实体 ID 格式
func normalizeEntityID(name string) string {
clean := strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == ' ' {
return r
}
if unicode.IsLetter(r) {
return r
}
return -1
}, name)
return strings.ReplaceAll(strings.TrimSpace(clean), " ", "_")
}
// ─── Stub 实现 ───────────────────────────────────────────
// lockFile 暂不实现no-op
func (fg *FileGraph) lockFile(fd *os.File, exclusive bool) error {
return nil
}
// unlockFile 暂不实现no-op
func (fg *FileGraph) unlockFile(fd *os.File) {
}

View File

@ -0,0 +1,323 @@
//go:build !cgo
// 织忆 MemoryWeave — 纯 Go SQLite + 内存向量混合存储Windows 持久化方案)
// 使用 modernc.org/sqlite纯 Go 实现,无需 CGO
// 向量搜索走内存(与 MemLanceClient 相同),元数据持久化到 SQLite
package storage
import (
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/xiaoxue/memoryweave/internal/models"
_ "modernc.org/sqlite"
)
// SQLiteMemClient 纯 Go SQLite + 内存向量混合存储
// 适用于 Windows 或无 CGO 环境
type SQLiteMemClient struct {
*MemLanceClient // 向量搜索走内存实现
db *sql.DB
dbPath string
pendingDirty bool
dirtyMu sync.Mutex
}
// NewSQLiteMemClient 创建持久化存储客户端
func NewSQLiteMemClient(dbPath string, embedder *Embedder) (*SQLiteMemClient, error) {
if dbPath == "" {
dbPath = "C:\\Users\\Administrator\\.zhiyi\\zhiyi.db"
}
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000")
if err != nil {
return nil, fmt.Errorf("sqlite open: %w", err)
}
sm := &SQLiteMemClient{
MemLanceClient: NewMemLanceClient(embedder),
db: db,
dbPath: dbPath,
}
if err := sm.migrate(); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
if err := sm.load(); err != nil {
// 只警告,不失败——内存空也能跑
fmt.Printf("[zhiyi] warn: load from sqlite failed: %v (starting fresh)\n", err)
}
return sm, nil
}
func (sm *SQLiteMemClient) migrate() error {
sqls := []string{
`CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
agent_id TEXT NOT NULL DEFAULT '',
namespace TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT '',
tier TEXT NOT NULL DEFAULT '',
quality_score REAL NOT NULL DEFAULT 0,
vector BLOB,
is_deleted INTEGER NOT NULL DEFAULT 0,
version INTEGER NOT NULL DEFAULT 1,
version_history TEXT,
source TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
last_recalled_at TEXT,
recall_count INTEGER NOT NULL DEFAULT 0,
useful_count INTEGER NOT NULL DEFAULT 0,
not_useful_count INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS episodes (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
namespace TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id)`,
`CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace)`,
`CREATE INDEX IF NOT EXISTS idx_memories_tier ON memories(tier)`,
}
for _, s := range sqls {
if _, err := sm.db.Exec(s); err != nil {
return fmt.Errorf("exec(%s): %w", s[:30], err)
}
}
return nil
}
// load 从 SQLite 恢复到内存
func (sm *SQLiteMemClient) load() error {
// 加载 memories
rows, err := sm.db.Query(`SELECT id,content,agent_id,namespace,category,tier,
quality_score,vector,is_deleted,version,version_history,source,
created_at,last_recalled_at,recall_count,useful_count,not_useful_count
FROM memories`)
if err != nil {
return err
}
defer rows.Close()
sm.mu.Lock()
defer sm.mu.Unlock()
for rows.Next() {
var id, content, agentID, namespace, category, tier, source string
var qualityScore float64
var vectorJSON []byte
var isDeleted bool
var version, recallCount, usefulCount, notUsefulCount int
var versionHistoryJSON sql.NullString
var createdAtStr, lastRecalledAtStr sql.NullString
if err := rows.Scan(&id, &content, &agentID, &namespace, &category, &tier,
&qualityScore, &vectorJSON, &isDeleted, &version, &versionHistoryJSON, &source,
&createdAtStr, &lastRecalledAtStr, &recallCount, &usefulCount, &notUsefulCount); err != nil {
continue
}
vec := make([]float32, 1024)
if len(vectorJSON) > 0 {
vec64 := make([]float64, 1024)
json.Unmarshal(vectorJSON, &vec64)
for i := range vec64 {
vec[i] = float32(vec64[i])
}
}
createdAt := time.Now()
if createdAtStr.Valid {
createdAt, _ = time.Parse(time.RFC3339, createdAtStr.String)
}
var vh []map[string]interface{}
if versionHistoryJSON.Valid {
json.Unmarshal([]byte(versionHistoryJSON.String), &vh)
}
sm.memories[id] = &memEntry{
ID: id,
Content: content,
AgentID: agentID,
Namespace: namespace,
Category: category,
Tier: tier,
QualityScore: qualityScore,
Vector: vec,
IsDeleted: isDeleted,
Version: version,
VersionHistory: vh,
Source: source,
CreatedAt: createdAt,
RecallCount: recallCount,
UsefulCount: usefulCount,
NotUsefulCount: notUsefulCount,
}
}
// 加载 episodes
epRows, err := sm.db.Query(`SELECT id,agent_id,namespace,content,category,created_at FROM episodes`)
if err != nil {
return nil
}
defer epRows.Close()
for epRows.Next() {
var id, agentID, namespace, content, category, createdAtStr string
if err := epRows.Scan(&id, &agentID, &namespace, &content, &category, &createdAtStr); err != nil {
continue
}
createdAt, _ := time.Parse(time.RFC3339, createdAtStr)
sm.episodes = append(sm.episodes, models.EpisodeRecord{
ID: id,
AgentID: agentID,
Namespace: namespace,
Content: content,
Category: category,
CreatedAt: createdAt,
})
}
return nil
}
func float32sToFloat64s(f32 []float32) []float64 {
f64 := make([]float64, len(f32))
for i := range f32 {
f64[i] = float64(f32[i])
}
return f64
}
func (sm *SQLiteMemClient) persistMemory(e *memEntry) error {
vecJSON, _ := json.Marshal(float32sToFloat64s(e.Vector))
vhJSON, _ := json.Marshal(e.VersionHistory)
createdAt := e.CreatedAt.Format(time.RFC3339)
_, err := sm.db.Exec(`INSERT OR REPLACE INTO memories
(id,content,agent_id,namespace,category,tier,quality_score,vector,
is_deleted,version,version_history,source,created_at,last_recalled_at,
recall_count,useful_count,not_useful_count)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
e.ID, e.Content, e.AgentID, e.Namespace, e.Category, e.Tier,
e.QualityScore, vecJSON, e.IsDeleted, e.Version, vhJSON, e.Source,
createdAt, e.LastRecalledAt, e.RecallCount, e.UsefulCount, e.NotUsefulCount)
return err
}
func (sm *SQLiteMemClient) persistEpisode(r models.EpisodeRecord) error {
_, err := sm.db.Exec(`INSERT OR REPLACE INTO episodes (id,agent_id,namespace,content,category,created_at)
VALUES (?,?,?,?,?,?)`,
r.ID, r.AgentID, r.Namespace, r.Content, r.Category, r.CreatedAt.Format(time.RFC3339))
return err
}
// ─── Override MemLanceClient 方法,追加 SQLite 持久化 ───
func (sm *SQLiteMemClient) InsertMemory(m models.MemoryRecord) error {
// 先走内存逻辑(计算向量、写入内存 map
if err := sm.MemLanceClient.InsertMemory(m); err != nil {
return err
}
// 同步写 SQLite
sm.mu.RLock()
e := sm.memories[m.ID]
sm.mu.RUnlock()
if e != nil {
return sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) InsertEpisode(agentID, namespace, content, category string) (string, error) {
id, err := sm.MemLanceClient.InsertEpisode(agentID, namespace, content, category)
if err != nil {
return id, err
}
sm.mu.RLock()
for i := len(sm.episodes) - 1; i >= 0; i-- {
if sm.episodes[i].ID == id {
sm.persistEpisode(sm.episodes[i])
break
}
}
sm.mu.RUnlock()
return id, nil
}
func (sm *SQLiteMemClient) SoftDelete(id, reason string) error {
if err := sm.MemLanceClient.SoftDelete(id, reason); err != nil {
return err
}
sm.mu.RLock()
e := sm.memories[id]
sm.mu.RUnlock()
if e != nil {
return sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) UpdateMemoryContent(id, newContent, source string) error {
if err := sm.MemLanceClient.UpdateMemoryContent(id, newContent, source); err != nil {
return err
}
sm.mu.RLock()
e := sm.memories[id]
sm.mu.RUnlock()
if e != nil {
return sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) IncrementUseful(id string) {
sm.MemLanceClient.IncrementUseful(id)
sm.mu.RLock()
e := sm.memories[id]
sm.mu.RUnlock()
if e != nil {
sm.persistMemory(e)
}
}
func (sm *SQLiteMemClient) IncrementNotUseful(id string) {
sm.MemLanceClient.IncrementNotUseful(id)
sm.mu.RLock()
e := sm.memories[id]
sm.mu.RUnlock()
if e != nil {
sm.persistMemory(e)
}
}
func (sm *SQLiteMemClient) Update(table, id string, fields map[string]any) error {
if err := sm.MemLanceClient.Update(table, id, fields); err != nil {
return err
}
sm.mu.RLock()
e := sm.memories[id]
sm.mu.RUnlock()
if e != nil {
return sm.persistMemory(e)
}
return nil
}
func (sm *SQLiteMemClient) Stats() (map[string]interface{}, error) {
sm.mu.RLock()
defer sm.mu.RUnlock()
return map[string]interface{}{
"memory_count": len(sm.memories),
"episode_count": len(sm.episodes),
"backend": "sqlite+memvector (pure Go)",
}, nil
}

130
scripts/backup.sh Executable file
View File

@ -0,0 +1,130 @@
#!/bin/bash
# ====== 织忆备份脚本 ======
# 备份到 server Giteahttp://192.168.123.11:3000
# 用法:./backup.sh [commit_message]
# 自动:每天 03:00见 cron 配置)
set -e
DATE=$(date +%Y%m%d_%H%M%S)
LOG_FILE="${HOME}/.hermes/logs/backup.log"
# 项目根目录backup.sh 同级)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# 备份临时目录
BACKUP_DIR="/tmp/zhiyi_backup_${DATE}"
CONFIG_DIR="/tmp/zhiyi_config_${DATE}"
# 日志
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "===== 备份开始: ${DATE} ====="
# 1. 备份数据文件
log "[1/6] 备份数据文件..."
mkdir -p "${BACKUP_DIR}"
sudo rsync -a /var/lib/memoryweave/ "${BACKUP_DIR}/memoryweave/" --quiet
DATA_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
log " 数据备份完成: ${DATA_SIZE}"
# 2. 打包
log "[2/6] 打包数据..."
cd /tmp
tar -czf "zhiyi_backup_${DATE}.tar.gz" "zhiyi_backup_${DATE}/"
rm -rf "zhiyi_backup_${DATE}"
ARCHIVE_SIZE=$(du -sh "/tmp/zhiyi_backup_${DATE}.tar.gz" | cut -f1)
log " 打包完成: ${ARCHIVE_SIZE}"
# 3. 备份 Hermes 配置
log "[3/6] 备份 Hermes 配置..."
mkdir -p "${CONFIG_DIR}"
# 核心人格文件
rsync -a "${HOME}/.hermes/SOUL.md" "${CONFIG_DIR}/" 2>/dev/null || true
rsync -a "${HOME}/.hermes/AGENTS.md" "${CONFIG_DIR}/" 2>/dev/null || true
rsync -a "${HOME}/.hermes/MEMORY.md" "${CONFIG_DIR}/" 2>/dev/null || true
rsync -a "${HOME}/.hermes/USER.md" "${CONFIG_DIR}/" 2>/dev/null || true
# 配置文件
rsync -a "${HOME}/.hermes/config.yaml" "${CONFIG_DIR}/" 2>/dev/null || true
rsync -a "${HOME}/.hermes/.env" "${CONFIG_DIR}/" 2>/dev/null || true
rsync -a "${HOME}/.hermes/auth.json" "${CONFIG_DIR}/" 2>/dev/null || true
# skills / cron / kanban
[ -d "${HOME}/.hermes/skills" ] && rsync -a "${HOME}/.hermes/skills/" "${CONFIG_DIR}/skills/" --quiet
[ -d "${HOME}/.hermes/cron" ] && rsync -a "${HOME}/.hermes/cron/" "${CONFIG_DIR}/cron/" --quiet
[ -f "${HOME}/.hermes/kanban.db" ] && rsync -a "${HOME}/.hermes/kanban.db" "${CONFIG_DIR}/" 2>/dev/null || true
# memories / sessions可选较大
if [ -d "${HOME}/.hermes/memories" ] && [ "$(du -sm "${HOME}/.hermes/memories" 2>/dev/null | cut -f1)" -lt 500 ]; then
rsync -a "${HOME}/.hermes/memories/" "${CONFIG_DIR}/memories/" --quiet
fi
if [ -d "${HOME}/.hermes/sessions" ] && [ "$(du -sm "${HOME}/.hermes/sessions" 2>/dev/null | cut -f1)" -lt 200 ]; then
rsync -a "${HOME}/.hermes/sessions/" "${CONFIG_DIR}/sessions/" --quiet
fi
CONFIG_SIZE=$(du -sh "${CONFIG_DIR}" | cut -f1)
log " 配置备份完成: ${CONFIG_SIZE}"
# 4. 推送到 Gitea
log "[4/6] 推送到 Gitea..."
# 确保 git 仓库存在
if [ ! -d "${PROJECT_ROOT}/.git" ]; then
log " WARM: ${PROJECT_ROOT} 不是 git 仓库,初始化..."
cd "${PROJECT_ROOT}"
git init
git remote add origin "http://192.168.123.11:3000/xiaoxue_admin/zhiyi-config.git" 2>/dev/null || true
fi
cd "${PROJECT_ROOT}"
# 确保 data 目录存在
mkdir -p data
# 移动备份到 data 目录
mv "/tmp/zhiyi_backup_${DATE}.tar.gz" data/
# 把数据仓库也 clone/update
if [ ! -d "${PROJECT_ROOT}/zhiyi-backup" ]; then
git clone "http://192.168.123.11:3000/xiaoxue_admin/zhiyi-backup.git" "${PROJECT_ROOT}/zhiyi-backup" 2>/dev/null || {
# 如果仓库不存在,跳过数据备份
log " WARN: zhiyi-backup 仓库不存在或无法访问,跳过数据推送"
}
fi
if [ -d "${PROJECT_ROOT}/zhiyi-backup" ]; then
cd "${PROJECT_ROOT}/zhiyi-backup"
# 移动 archive
if [ -f "${PROJECT_ROOT}/data/zhiyi_backup_${DATE}.tar.gz" ]; then
cp "${PROJECT_ROOT}/data/zhiyi_backup_${DATE}.tar.gz" .
git add "zhiyi_backup_${DATE}.tar.gz"
git commit -m "Backup data ${DATE}" 2>/dev/null || true
git push origin main 2>/dev/null || log " WARN: 数据推送失败(可能需要 token"
rm -f "zhiyi_backup_${DATE}.tar.gz"
fi
cd "${PROJECT_ROOT}"
fi
# 推送配置
git add data/zhiyi_backup_${DATE}.tar.gz
git add config/ 2>/dev/null || true
git add -A
COMMIT_MSG="${1:-Backup ${DATE}}"
git commit -m "${COMMIT_MSG}" 2>/dev/null || log " WARN: 没有新内容需要提交"
git push origin main 2>/dev/null || log " WARN: 配置推送失败(可能需要 token"
# 5. 清理
log "[5/6] 清理临时文件..."
rm -rf "${BACKUP_DIR}" "${CONFIG_DIR}"
log " 清理完成"
# 6. 完成
log "[6/6] 备份完成"
log "===== 备份结束: ${DATE} ====="
echo ""

124
scripts/restore.sh Executable file
View File

@ -0,0 +1,124 @@
#!/bin/bash
# ====== 织忆恢复脚本 ======
# 从 server Gitea 恢复所有数据
# 用法:./restore.sh [backup_date]
# backup_date: 可选,默认最新
set -e
BACKUP_DATE="${1:-latest}"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GITEA_BASE="http://192.168.123.11:3000/xiaoxue_admin"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ===== 开始恢复织忆 ====="
# 1. Clone 配置仓库
echo "[1/5] 拉取 Hermes 配置..."
if [ -d "${PROJECT_ROOT}/zhiyi-config" ]; then
cd "${PROJECT_ROOT}/zhiyi-config"
git pull origin main
else
git clone "${GITEA_BASE}/zhiyi-config.git" "${PROJECT_ROOT}/zhiyi-config"
fi
# 2. 恢复核心人格文件
echo "[2/5] 恢复人格文件..."
cp "${PROJECT_ROOT}/zhiyi-config/SOUL.md" "${HOME}/.hermes/SOUL.md"
cp "${PROJECT_ROOT}/zhiyi-config/AGENTS.md" "${HOME}/.hermes/AGENTS.md"
cp "${PROJECT_ROOT}/zhiyi-config/MEMORY.md" "${HOME}/.hermes/MEMORY.md"
cp "${PROJECT_ROOT}/zhiyi-config/USER.md" "${HOME}/.hermes/USER.md"
echo " 人格文件恢复完成"
# 3. 恢复配置文件
echo "[3/5] 恢复配置文件..."
[ -f "${PROJECT_ROOT}/zhiyi-config/config.yaml" ] && \
cp "${PROJECT_ROOT}/zhiyi-config/config.yaml" "${HOME}/.hermes/config.yaml"
[ -f "${PROJECT_ROOT}/zhiyi-config/.env" ] && \
cp "${PROJECT_ROOT}/zhiyi-config/.env" "${HOME}/.hermes/.env"
[ -f "${PROJECT_ROOT}/zhiyi-config/auth.json" ] && \
cp "${PROJECT_ROOT}/zhiyi-config/auth.json" "${HOME}/.hermes/auth.json"
echo " 配置文件恢复完成"
# 4. 恢复 skills / cron
echo "[4/5] 恢复 skills 和 cron..."
[ -d "${PROJECT_ROOT}/zhiyi-config/skills" ] && \
rsync -a "${PROJECT_ROOT}/zhiyi-config/skills/" "${HOME}/.hermes/skills/"
[ -d "${PROJECT_ROOT}/zhiyi-config/cron" ] && \
rsync -a "${PROJECT_ROOT}/zhiyi-config/cron/" "${HOME}/.hermes/cron/"
[ -f "${PROJECT_ROOT}/zhiyi-config/kanban.db" ] && \
cp "${PROJECT_ROOT}/zhiyi-config/kanban.db" "${HOME}/.hermes/kanban.db"
echo " skills/cron 恢复完成"
# 5. 恢复织忆数据
echo "[5/5] 恢复织忆数据..."
# Clone 数据仓库(如果需要)
if [ ! -d "${PROJECT_ROOT}/zhiyi-backup" ]; then
git clone "${GITEA_BASE}/zhiyi-backup.git" "${PROJECT_ROOT}/zhiyi-backup"
fi
cd "${PROJECT_ROOT}/zhiyi-backup"
# 找最新备份
if [ "${BACKUP_DATE}" = "latest" ]; then
BACKUP_FILE=$(ls -t *.tar.gz 2>/dev/null | head -1)
else
BACKUP_FILE="zhiyi_backup_${BACKUP_DATE}.tar.gz"
fi
if [ -n "${BACKUP_FILE}" ] && [ -f "${BACKUP_FILE}" ]; then
echo " 使用备份: ${BACKUP_FILE}"
# 解压到临时目录
TMP_DIR="/tmp/zhiyi_restore_$$"
mkdir -p "${TMP_DIR}"
tar -xzf "${BACKUP_FILE}" -C "${TMP_DIR}"
DATA_DIR=$(ls -d "${TMP_DIR}"/zhiyi_backup_* 2>/dev/null | head -1)
if [ -n "${DATA_DIR}" ] && [ -d "${DATA_DIR}/memoryweave" ]; then
sudo rsync -a "${DATA_DIR}/memoryweave/" /var/lib/memoryweave/
sudo chown -R root:root /var/lib/memoryweave/
echo " 数据恢复完成: $(du -sh /var/lib/memoryweave/ | cut -f1)"
else
echo " ERROR: 备份格式错误,无法恢复"
fi
rm -rf "${TMP_DIR}"
else
echo " WARN: 未找到备份文件 ${BACKUP_FILE},跳过数据恢复"
fi
# 6. 重启服务
echo ""
echo "[完成] 准备重启服务..."
# 检查 zhiyi 进程
if pgrep -f "zhiyi" > /dev/null; then
echo " 重启 zhiyi..."
pkill -f "zhiyi" 2>/dev/null || true
sleep 2
fi
# 启动 zhiyi
cd "${PROJECT_ROOT}/go"
if [ -f "./zhiyi" ]; then
nohup ./zhiyi >> "${HOME}/.hermes/logs/zhiyi.log" 2>&1 &
sleep 3
else
echo " WARN: zhiyi binary 不存在,需要重新编译"
fi
# 7. 健康检查
echo ""
echo "健康检查..."
sleep 2
if curl -s http://localhost:7821/health > /dev/null 2>&1; then
echo "✓ 织忆服务正常"
else
echo "✗ 织忆服务异常,请检查日志"
tail -20 "${HOME}/.hermes/logs/zhiyi.log" 2>/dev/null || true
fi
echo ""
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ===== 恢复完成 ====="

89
scripts/verify_backup.sh Executable file
View File

@ -0,0 +1,89 @@
#!/bin/bash
# ====== 备份验证脚本 ======
# 每周日凌晨 04:00 自动运行
# 验证备份完整性,解压测试 SQLite 数据库
set -e
DATE=$(date +%Y%m%d)
VERIFY_DIR="/tmp/zhiyi_verify_${DATE}"
LOG_FILE="${HOME}/.hermes/logs/backup_verify.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log "===== 备份验证开始 ====="
# 1. Clone backup 仓库
log "[1/4] 拉取备份..."
if [ ! -d "${HOME}/zhiyi-backup-verify" ]; then
git clone "http://192.168.123.11:3000/xiaoxue_admin/zhiyi-backup.git" "${HOME}/zhiyi-backup-verify"
else
cd "${HOME}/zhiyi-backup-verify"
git pull origin main
fi
cd "${HOME}/zhiyi-backup-verify"
# 找最新备份
LATEST=$(ls -t *.tar.gz 2>/dev/null | head -1)
if [ -z "$LATEST" ]; then
log "ERROR: 未找到任何备份文件"
exit 1
fi
log "验证备份: ${LATEST}"
# 2. 校验 tar.gz 完整性
log "[2/4] 校验 tar.gz 完整性..."
if tar -tzf "$LATEST" > /dev/null 2>&1; then
log "✓ tar.gz 文件完整"
else
log "ERROR: tar.gz 文件损坏"
exit 1
fi
# 3. 解压并校验 SQLite
log "[3/4] 解压并校验 SQLite..."
mkdir -p "${VERIFY_DIR}"
tar -xzf "$LATEST" -C "${VERIFY_DIR}"
DATA_DIR=$(ls -d "${VERIFY_DIR}"/zhiyi_backup_* 2>/dev/null | head -1)
if [ -z "$DATA_DIR" ]; then
log "ERROR: 无法找到解压后的数据目录"
exit 1
fi
# 校验 graph.db
if [ -f "${DATA_DIR}/memoryweave/graph.db" ]; then
NODE_COUNT=$(sqlite3 "${DATA_DIR}/memoryweave/graph.db" "SELECT count(*) FROM nodes;" 2>/dev/null || echo "ERR")
if [ "$NODE_COUNT" != "ERR" ]; then
log "✓ 图谱数据库正常 (${NODE_COUNT} nodes)"
else
log "ERROR: 图谱数据库损坏"
fi
else
log "WARN: graph.db 不存在"
fi
# 校验 memoryweave.db
if [ -f "${DATA_DIR}/memoryweave/memoryweave.db" ]; then
DB_SIZE=$(du -sh "${DATA_DIR}/memoryweave/memoryweave.db" | cut -f1)
log "✓ LiteDB 数据库存在 (${DB_SIZE})"
else
log "WARN: memoryweave.db 不存在"
fi
# 校验 LanceDB 目录
if [ -d "${DATA_DIR}/memoryweave/memories.lance" ]; then
LANCE_SIZE=$(du -sh "${DATA_DIR}/memoryweave/memories.lance" | cut -f1)
log "✓ LanceDB 向量目录存在 (${LANCE_SIZE})"
else
log "WARN: memories.lance 不存在"
fi
# 4. 清理
log "[4/4] 清理临时文件..."
rm -rf "${VERIFY_DIR}"
log "===== 备份验证完成 ====="
echo ""