Skill: xzxy1/spring-fountain-revival (version 1.0.0)

--- name: spring-fountain-revival description: | 泉水复活 - 记忆生存系统。三重备份策略确保AI与用户的互动记忆永不丢失。 核心能力：一键备份、时间点快照、一键还原、云端同步、完整性校验。 桌面快捷方式：QClaw一键备份.bat / QClaw一键还原.bat 触发词：备份记忆、还原记忆、检查记忆、记忆备份、记忆恢复、泉水复活、记忆快照。 Keywords: memory backup, restore, survival, diary, 记忆, 备份, 还原, 泉水, 复活. --- # 泉水复活 — 记忆生存系统 让记忆如泉水般永不枯竭，无论遭遇什么意外都能复活。 ## 架构总览 ``` 工作区(主存储) 本地隐藏备份 云端(百度网盘) ~/.qclaw/workspace/ ~/.qclaw/.memory_backup/ ~/Desktop/QC百度同步/QClaw记忆备份/ │ │ │ │──── 每次写入即时备份 ──→│ │ │──── 每天9:00自动备份 ──→│── 云端同步 ──────────────→│ │ │ │ │←── 一键还原 ←─────────│←── 一键还原 ←─────────────│ ``` ## 记忆文件清单 | 文件 | 优先级 | 说明 | |------|--------|------| | MEMORY.md | critical | 长期记忆 | | USER.md | critical | 用户信息 | | IDENTITY.md | critical | AI身份 | | AGENTS.md | high | 工作区规则 | | SOUL.md | high | 灵魂文件 | | diary/*.md | critical | 日记 | | memory/*.md | high | 每日记忆 | ## AI 行为规范 ### 每次写日记后 ```bash python scripts/memory_backup.py backup auto ``` ### 每日首次对话时 ```bash python scripts/memory_backup.py check ``` ### 用户说"备份记忆"/"泉水复活" ```bash python scripts/memory_backup.py backup manual ``` ### 用户说"还原记忆"/"记忆丢了" 引导用户双击桌面 `QClaw一键还原.bat` ### 用户说"检查记忆" ```bash python scripts/memory_backup.py check ``` ## 用户操作 | 操作 | 方式 | |------|------| | 一键备份 | 双击桌面 `QClaw一键备份.bat` | | 一键还原 | 双击桌面 `QClaw一键还原.bat` → 选快照 → 还原 | | 新电脑恢复 | 百度网盘同步 → 双击云端 `QClaw一键还原.bat` | ## 脚本说明 - `scripts/memory_backup.py` — 核心备份引擎（备份/快照/同步/校验/还原） - `scripts/restore_gui.py` — 图形界面还原工具（tkinter） - `assets/QClaw一键备份.bat` — 桌面备份快捷方式 - `assets/QClaw一键还原.bat` — 桌面还原快捷方式 ## 云端配置 默认云端路径：`~/Desktop/QC百度同步/QClaw记忆备份/` 修改方法：编辑 `scripts/memory_backup.py` 和 `scripts/restore_gui.py` 中的 `CLOUD_SYNC_DIR` 变量。

path: README.md
content: # 🧊💦 泉水复活 (Spring Fountain Revival)

> 让记忆如泉水般永不枯竭，无论遭遇什么意外都能复活。

AI 与用户互动记忆的终极生存系统。三重备份 + 时间点快照 + 一键还原。

## ✨ 特性

- 🧠 **全面备份** — 自动扫描所有记忆文件（MEMORY.md、日记、用户信息等）
- ⏱️ **时间点快照** — 每次备份生成带时间戳的 zip 快照，可还原到任意时间点
- ☁️ **云端同步** — 自动同步到云盘（默认百度网盘，可配置）
- 🔒 **本地隐藏备份** — 独立于工作区的隐藏目录，防误删
- ⚡ **一键还原** — 图形界面，双击选择快照即可还原
- 🔍 **完整性校验** — SHA256 哈希比对，发现不一致立即告警
- 📅 **自动备份** — 每日定时自动备份（支持 cron）
- 💻 **桌面快捷方式** — 一键备份 / 一键还原，零门槛操作

## 📁 记忆文件清单

| 文件 | 说明 |
|------|------|
| `MEMORY.md` | 长期记忆（重要事件、决策、偏好） |
| `USER.md` | 用户信息（姓名、偏好、时区） |
| `IDENTITY.md` | AI 身份（名字、性格、头像） |
| `AGENTS.md` | 工作区规则 |
| `SOUL.md` | 灵魂文件 |
| `diary/*.md` | 日记文件 |
| `memory/*.md` | 每日记忆 |

## 🛡️ 三重备份架构

```
工作区(主存储)              本地隐藏备份                云端
~/.qclaw/workspace/         ~/.qclaw/.memory_backup/    ~/Desktop/QC百度同步/QClaw记忆备份/
       │                           │                             │
       │──── 即时/定时备份 ────────→│                             │
       │                           │──── 云端同步 ──────────────→│
       │                           │                             │
       │←──── 一键还原 ←──────────│←──── 一键还原 ←────────────│
```

## 🚀 安装

### 方式一：作为 OpenClaw Skill 安装

```bash
# 克隆到 skill 目录
git clone https://github.com/你的用户名/泉水复活skill.git ~/.qclaw/skills/spring-fountain-revival
```

### 方式二：手动安装

1. 下载整个仓库
2. 将文件夹复制到 `~/.qclaw/skills/spring-fountain-revival/`
3. 运行一次备份初始化：

```bash
python ~/.qclaw/skills/spring-fountain-revival/scripts/memory_backup.py backup init
```

## ⚡ 使用方式

### 桌面快捷方式

在桌面创建两个文本文件：

**QClaw一键备份.bat** (内容):
```bat
@echo off
python "%USERPROFILE%\.qclaw\workspace\scripts\memory_backup.py" backup manual
pause
```

**QClaw一键还原.bat** (内容):
```bat
@echo off
python "%USERPROFILE%\.qclaw\workspace\scripts\restore_gui.py"
pause
```

### 命令行

```bash
# 完整备份（本地 + 快照 + 云端）
python scripts/memory_backup.py backup [标签]

# 仅创建快照
python scripts/memory_backup.py snapshot [标签]

# 同步到云端
python scripts/memory_backup.py sync

# 列出所有快照
python scripts/memory_backup.py list

# 还原（交互式选择快照）
python scripts/memory_backup.py restore

# 快速还原最新备份
python scripts/memory_backup.py restore latest

# 完整性检查
python scripts/memory_backup.py check
```

## ☁️ 云端配置

编辑 `scripts/memory_backup.py` 中的 `CLOUD_SYNC_DIR` 变量：

```python
# 默认配置（百度网盘）
CLOUD_SYNC_DIR = Path(r"C:\Users\Administrator\Desktop\QC百度同步\QClaw记忆备份")

# 修改为你的云盘路径，例如：
# CLOUD_SYNC_DIR = Path(r"D:\OneDrive\QClaw记忆备份")
# CLOUD_SYNC_DIR = Path.home() / "坚果云" / "QClaw记忆备份"
```

同时编辑 `scripts/restore_gui.py` 中相同的变量保持一致。

## 🆘 灾难恢复

### 场景：电脑报废，换新电脑

1. 新电脑安装 Python 3.8+
2. 安装云盘软件并登录，等待同步
3. 打开云盘目录中的 `QClaw一键还原.bat`
4. 选择最新快照 → 一键还原
5. **记忆重生！**

### 场景：误删工作区文件

双击桌面 `QClaw一键还原.bat` → 还原最新备份

### 场景：云盘也挂了

本地隐藏目录 `~/.qclaw/.memory_backup/` 是最后防线，只要硬盘还在就能恢复。

## 📂 文件结构

```
泉水复活skill/
├── SKILL.md                          # Skill 描述（OpenClaw 加载用）
├── README.md                         # 本文件
└── scripts/
    ├── memory_backup.py              # 核心备份引擎
    └── restore_gui.py                # 图形界面还原工具
```

> **注意**：桌面快捷方式请手动创建，见上方说明。

## 📜 License

MIT


path: scripts/memory_backup.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QClaw 记忆生存系统 - 核心备份引擎
功能：
  1. 扫描所有记忆文件
  2. 本地隐藏备份（带时间戳快照）
  3. 云端同步目录同步
  4. 完整性校验
  5. 时间点还原
"""

import os
import sys
import json
import shutil
import hashlib
import zipfile
from datetime import datetime
from pathlib import Path

# ─── 路径配置 ───────────────────────────────────────────────
HOME            = Path.home()
WORKSPACE       = HOME / ".qclaw" / "workspace"
LOCAL_BACKUP    = HOME / ".qclaw" / ".memory_backup"   # 本地隐藏备份
CLOUD_SYNC_DIR  = Path(r"C:\Users\Administrator\Desktop\QC百度同步\QClaw记忆备份")  # 云端同步目录（百度网盘同步）
SNAPSHOTS_DIR   = LOCAL_BACKUP / "snapshots"           # 时间点快照
LATEST_DIR      = LOCAL_BACKUP / "latest"              # 最新备份（快速恢复用）
RESTORE_TOOL    = HOME / "Desktop" / "QClaw一键还原.bat"
MANIFEST_FILE   = WORKSPACE / "memory_manifest.json"

# ─── 记忆文件列表（硬编码 + 清单双重保障）──────────────────
MEMORY_FILES = [
    WORKSPACE / "MEMORY.md",
    WORKSPACE / "USER.md",
    WORKSPACE / "IDENTITY.md",
    WORKSPACE / "AGENTS.md",
    WORKSPACE / "SOUL.md",
    WORKSPACE / "HEARTBEAT.md",
    WORKSPACE / "TOOLS.md",
    WORKSPACE / "memory_manifest.json",
]
MEMORY_DIRS = [
    WORKSPACE / "diary",
    WORKSPACE / "memory",
]

def ensure_dirs():
    for d in [LOCAL_BACKUP, SNAPSHOTS_DIR, LATEST_DIR, CLOUD_SYNC_DIR]:
        d.mkdir(parents=True, exist_ok=True)

def file_hash(path: Path) -> str:
    if not path.exists():
        return ""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()

def collect_all_files():
    """收集所有记忆文件（文件 + 目录内文件）"""
    files = []
    for f in MEMORY_FILES:
        if f.exists():
            files.append(f)
    for d in MEMORY_DIRS:
        if d.exists():
            for f in d.rglob("*"):
                if f.is_file():
                    files.append(f)
    return files

def backup_to_latest():
    """备份到 latest 目录（覆盖式，始终是最新状态）"""
    ensure_dirs()
    copied = 0
    for src in collect_all_files():
        rel = src.relative_to(HOME / ".qclaw" / "workspace")
        dst = LATEST_DIR / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dst)
        copied += 1
    return copied

def create_snapshot(label="auto"):
    """创建带时间戳的快照 zip"""
    ensure_dirs()
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    snap_name = f"snapshot_{ts}_{label}.zip"
    snap_path = SNAPSHOTS_DIR / snap_name

    files = collect_all_files()
    with zipfile.ZipFile(snap_path, "w", zipfile.ZIP_DEFLATED) as zf:
        for src in files:
            rel = src.relative_to(HOME / ".qclaw" / "workspace")
            zf.write(src, str(rel))
        # 写入快照元数据
        meta = {
            "timestamp": datetime.now().isoformat(),
            "label": label,
            "file_count": len(files),
            "files": [str(f.relative_to(HOME / ".qclaw" / "workspace")) for f in files]
        }
        zf.writestr("_snapshot_meta.json", json.dumps(meta, ensure_ascii=False, indent=2))

    size_kb = snap_path.stat().st_size // 1024
    print(f"[OK] 快照已创建: {snap_name} ({size_kb} KB, {len(files)} 个文件)")
    return snap_path

def sync_to_cloud():
    """同步到云端目录"""
    ensure_dirs()
    CLOUD_SYNC_DIR.mkdir(parents=True, exist_ok=True)

    # 同步 latest 到云端
    synced = 0
    for src in (LATEST_DIR).rglob("*"):
        if src.is_file():
            rel = src.relative_to(LATEST_DIR)
            dst = CLOUD_SYNC_DIR / rel
            dst.parent.mkdir(parents=True, exist_ok=True)
            if not dst.exists() or file_hash(src) != file_hash(dst):
                shutil.copy2(src, dst)
                synced += 1

    # 同步最新快照到云端
    snaps = sorted(SNAPSHOTS_DIR.glob("snapshot_*.zip"), reverse=True)
    if snaps:
        latest_snap = snaps[0]
        cloud_snap = CLOUD_SYNC_DIR / "snapshots" / latest_snap.name
        cloud_snap.parent.mkdir(parents=True, exist_ok=True)
        if not cloud_snap.exists():
            shutil.copy2(latest_snap, cloud_snap)
            synced += 1

    # 同步还原工具到云端
    if RESTORE_TOOL.exists():
        cloud_tool = CLOUD_SYNC_DIR / RESTORE_TOOL.name
        shutil.copy2(RESTORE_TOOL, cloud_tool)

    print(f"[OK] 云端同步完成: {synced} 个文件已更新 -> {CLOUD_SYNC_DIR}")
    return synced

def list_snapshots():
    """列出所有可用快照"""
    snaps = sorted(SNAPSHOTS_DIR.glob("snapshot_*.zip"), reverse=True)
    if not snaps:
        print("[INFO] 暂无快照")
        return []
    result = []
    for i, s in enumerate(snaps):
        size_kb = s.stat().st_size // 1024
        # 解析时间戳
        parts = s.stem.split("_")
        if len(parts) >= 3:
            ts_str = f"{parts[1][:4]}-{parts[1][4:6]}-{parts[1][6:]} {parts[2][:2]}:{parts[2][2:4]}:{parts[2][4:]}"
        else:
            ts_str = s.stem
        label = "_".join(parts[3:]) if len(parts) > 3 else "auto"
        result.append({"index": i, "name": s.name, "time": ts_str, "label": label, "size_kb": size_kb, "path": str(s)})
    return result

def restore_from_snapshot(snap_path: Path, dry_run=False):
    """从快照还原"""
    if not snap_path.exists():
        print(f"[ERROR] 快照不存在: {snap_path}")
        return False

    target = WORKSPACE
    with zipfile.ZipFile(snap_path, "r") as zf:
        names = [n for n in zf.namelist() if not n.startswith("_snapshot")]
        if dry_run:
            print(f"[DRY-RUN] 将还原 {len(names)} 个文件到 {target}")
            for n in names[:10]:
                print(f"  {n}")
            if len(names) > 10:
                print(f"  ... 共 {len(names)} 个")
            return True
        for name in names:
            dst = target / name
            dst.parent.mkdir(parents=True, exist_ok=True)
            with zf.open(name) as src_f, open(dst, "wb") as dst_f:
                dst_f.write(src_f.read())

    print(f"[OK] 还原完成: {len(names)} 个文件已恢复到 {target}")
    return True

def restore_from_latest():
    """从 latest 快速还原"""
    if not LATEST_DIR.exists():
        print("[ERROR] 本地备份不存在，请尝试从云端恢复")
        return False
    files = list(LATEST_DIR.rglob("*"))
    files = [f for f in files if f.is_file()]
    for src in files:
        rel = src.relative_to(LATEST_DIR)
        dst = WORKSPACE / rel
        dst.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(src, dst)
    print(f"[OK] 快速还原完成: {len(files)} 个文件")
    return True

def integrity_check():
    """完整性校验：比对工作区 vs 本地备份"""
    issues = []
    for src in collect_all_files():
        rel = src.relative_to(WORKSPACE)
        bak = LATEST_DIR / rel
        if not bak.exists():
            issues.append(f"备份缺失: {rel}")
        elif file_hash(src) != file_hash(bak):
            issues.append(f"内容不一致: {rel}")
    # 检查备份中有但工作区没有的
    if LATEST_DIR.exists():
        for bak in LATEST_DIR.rglob("*"):
            if bak.is_file():
                rel = bak.relative_to(LATEST_DIR)
                src = WORKSPACE / rel
                if not src.exists():
                    issues.append(f"工作区缺失（可还原）: {rel}")
    return issues

def full_backup(label="manual"):
    """完整备份流程：latest + 快照 + 云端同步"""
    print("=" * 50)
    print(f"QClaw 记忆备份 [{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}]")
    print("=" * 50)
    n = backup_to_latest()
    print(f"[OK] 本地备份: {n} 个文件 -> {LATEST_DIR}")
    snap = create_snapshot(label)
    sync_to_cloud()
    print("=" * 50)
    print("备份完成！三重保障已就绪。")
    return snap

# ─── CLI ────────────────────────────────────────────────────
def main():
    cmd = sys.argv[1] if len(sys.argv) > 1 else "help"

    if cmd == "backup":
        label = sys.argv[2] if len(sys.argv) > 2 else "manual"
        full_backup(label)

    elif cmd == "snapshot":
        label = sys.argv[2] if len(sys.argv) > 2 else "manual"
        create_snapshot(label)

    elif cmd == "sync":
        sync_to_cloud()

    elif cmd == "list":
        snaps = list_snapshots()
        if snaps:
            print(f"\n可用快照 ({len(snaps)} 个):\n")
            for s in snaps:
                print(f"  [{s['index']}] {s['time']}  标签:{s['label']}  大小:{s['size_kb']}KB")
                print(f"       {s['name']}")

    elif cmd == "restore":
        if len(sys.argv) < 3:
            # 交互式选择
            snaps = list_snapshots()
            if not snaps:
                print("无可用快照，尝试从 latest 还原...")
                restore_from_latest()
                return
            print(f"\n可用快照 ({len(snaps)} 个):\n")
            for s in snaps:
                print(f"  [{s['index']}] {s['time']}  标签:{s['label']}  大小:{s['size_kb']}KB")
            print(f"\n输入序号还原（直接回车=还原最新）: ", end="")
            choice = input().strip()
            idx = int(choice) if choice.isdigit() else 0
            snap = snaps[idx]
            print(f"\n将还原到: {snap['time']} ({snap['label']})")
            print("确认？(y/n): ", end="")
            if input().strip().lower() == "y":
                restore_from_snapshot(Path(snap["path"]))
        elif sys.argv[2] == "latest":
            restore_from_latest()
        else:
            restore_from_snapshot(Path(sys.argv[2]))

    elif cmd == "check":
        print("正在检查完整性...")
        issues = integrity_check()
        if issues:
            print(f"\n发现 {len(issues)} 个问题:")
            for i in issues:
                print(f"  ! {i}")
            print("\n建议运行: python memory_backup.py backup")
        else:
            print("[OK] 所有记忆文件完整一致")

    elif cmd == "auto":
        # 自动备份（由定时任务调用）
        full_backup("auto")

    else:
        print("QClaw 记忆生存系统 - 备份引擎")
        print("\n用法: python memory_backup.py <命令>")
        print("\n命令:")
        print("  backup [标签]   完整备份（本地+快照+云端）")
        print("  snapshot [标签] 仅创建快照")
        print("  sync            同步到云端目录")
        print("  list            列出所有快照")
        print("  restore [序号]  还原到指定时间点（交互式）")
        print("  restore latest  快速还原最新备份")
        print("  check           完整性校验")
        print(f"\n本地备份: {LOCAL_BACKUP}")
        print(f"云端目录: {CLOUD_SYNC_DIR}")

if __name__ == "__main__":
    main()


path: scripts/restore_gui.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QClaw 一键还原工具 - 图形界面
双击即可运行，无需命令行
"""

import os
import sys
import json
import shutil
import zipfile
import hashlib
import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext
from datetime import datetime
from pathlib import Path
import threading
import subprocess

# ─── 路径配置（与 memory_backup.py 保持一致）──────────────
HOME           = Path.home()
WORKSPACE      = HOME / ".qclaw" / "workspace"
LOCAL_BACKUP   = HOME / ".qclaw" / ".memory_backup"
CLOUD_SYNC_DIR = Path(r"C:\Users\Administrator\Desktop\QC百度同步\QClaw记忆备份")
SNAPSHOTS_DIR  = LOCAL_BACKUP / "snapshots"
LATEST_DIR     = LOCAL_BACKUP / "latest"
SCRIPTS_DIR    = WORKSPACE / "scripts"

# ─── 颜色主题 ─────────────────────────────────────────────
BG       = "#1a1a2e"
BG2      = "#16213e"
BG3      = "#0f3460"
ACCENT   = "#e94560"
GREEN    = "#00b894"
YELLOW   = "#fdcb6e"
TEXT     = "#eaeaea"
TEXT_DIM = "#888888"

def file_hash(path: Path) -> str:
    if not path.exists():
        return ""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()

def list_snapshots():
    snaps = []
    if SNAPSHOTS_DIR.exists():
        snaps = sorted(SNAPSHOTS_DIR.glob("snapshot_*.zip"), reverse=True)
    # 也检查云端快照
    cloud_snaps_dir = CLOUD_SYNC_DIR / "snapshots"
    if cloud_snaps_dir.exists():
        for s in sorted(cloud_snaps_dir.glob("snapshot_*.zip"), reverse=True):
            if s.name not in [x.name for x in snaps]:
                snaps.append(s)
    result = []
    for s in snaps:
        parts = s.stem.split("_")
        try:
            ts_str = f"{parts[1][:4]}-{parts[1][4:6]}-{parts[1][6:]} {parts[2][:2]}:{parts[2][2:4]}:{parts[2][4:]}"
        except:
            ts_str = s.stem
        label = "_".join(parts[3:]) if len(parts) > 3 else "auto"
        source = "☁ 云端" if str(s).startswith(str(CLOUD_SYNC_DIR)) else "💾 本地"
        size_kb = s.stat().st_size // 1024
        result.append({
            "display": f"{source}  {ts_str}  [{label}]  {size_kb}KB",
            "time": ts_str,
            "label": label,
            "source": source,
            "path": str(s),
            "size_kb": size_kb
        })
    return result

def do_restore(snap_path_str, log_func):
    snap_path = Path(snap_path_str)
    if not snap_path.exists():
        log_func(f"[错误] 快照文件不存在: {snap_path}")
        return False
    try:
        with zipfile.ZipFile(snap_path, "r") as zf:
            names = [n for n in zf.namelist() if not n.startswith("_snapshot")]
            log_func(f"正在还原 {len(names)} 个文件...")
            for name in names:
                dst = WORKSPACE / name
                dst.parent.mkdir(parents=True, exist_ok=True)
                with zf.open(name) as sf, open(dst, "wb") as df:
                    df.write(sf.read())
                log_func(f"  ✓ {name}")
        log_func(f"\n[完成] 成功还原 {len(names)} 个文件！")
        return True
    except Exception as e:
        log_func(f"[错误] 还原失败: {e}")
        return False

def do_backup(log_func):
    try:
        backup_py = SCRIPTS_DIR / "memory_backup.py"
        if not backup_py.exists():
            log_func("[错误] 备份引擎不存在，请重新安装")
            return False
        result = subprocess.run(
            [sys.executable, str(backup_py), "backup", "manual"],
            capture_output=True, text=True, encoding="utf-8"
        )
        for line in result.stdout.splitlines():
            log_func(line)
        if result.returncode == 0:
            log_func("\n[完成] 备份成功！")
            return True
        else:
            log_func(f"[错误] {result.stderr}")
            return False
    except Exception as e:
        log_func(f"[错误] {e}")
        return False

def do_check(log_func):
    issues = []
    log_func("正在检查记忆文件完整性...\n")
    memory_files = [
        WORKSPACE / "MEMORY.md",
        WORKSPACE / "USER.md",
        WORKSPACE / "IDENTITY.md",
        WORKSPACE / "AGENTS.md",
        WORKSPACE / "SOUL.md",
    ]
    for f in memory_files:
        exists = f.exists()
        bak = LATEST_DIR / f.relative_to(WORKSPACE)
        bak_exists = bak.exists()
        if exists and bak_exists:
            match = file_hash(f) == file_hash(bak)
            status = "✓ 一致" if match else "! 不一致"
            if not match:
                issues.append(str(f.name))
        elif exists and not bak_exists:
            status = "! 备份缺失"
            issues.append(str(f.name))
        elif not exists and bak_exists:
            status = "! 工作区缺失（可还原）"
            issues.append(str(f.name))
        else:
            status = "- 不存在"
        log_func(f"  {f.name:20s}  {status}")
    # 检查日记
    diary_dir = WORKSPACE / "diary"
    if diary_dir.exists():
        diary_files = list(diary_dir.glob("*.md"))
        log_func(f"\n  日记文件: {len(diary_files)} 个")
    snaps = list_snapshots()
    log_func(f"  可用快照: {len(snaps)} 个")
    if issues:
        log_func(f"\n[警告] 发现 {len(issues)} 个问题: {', '.join(issues)}")
        log_func("建议点击「立即备份」修复")
    else:
        log_func("\n[OK] 所有记忆文件完整！")
    return issues

# ─── GUI ──────────────────────────────────────────────────
class RestoreApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("QClaw 记忆还原工具")
        self.geometry("780x600")
        self.resizable(True, True)
        self.configure(bg=BG)
        self.snapshots = []
        self._build_ui()
        self._refresh_snapshots()
        self._run_check()

    def _build_ui(self):
        # ── 标题栏 ──
        header = tk.Frame(self, bg=BG3, pady=12)
        header.pack(fill="x")
        tk.Label(header, text="🧠  QClaw 记忆还原工具",
                 font=("Microsoft YaHei", 16, "bold"),
                 bg=BG3, fg=TEXT).pack(side="left", padx=20)
        tk.Label(header, text="记忆永不丢失",
                 font=("Microsoft YaHei", 10),
                 bg=BG3, fg=TEXT_DIM).pack(side="left")

        # ── 主体分栏 ──
        body = tk.Frame(self, bg=BG)
        body.pack(fill="both", expand=True, padx=16, pady=12)

        # 左栏：快照列表
        left = tk.Frame(body, bg=BG2, bd=0)
        left.pack(side="left", fill="both", expand=True, padx=(0, 8))

        tk.Label(left, text="📦 可用快照（选择时间点）",
                 font=("Microsoft YaHei", 10, "bold"),
                 bg=BG2, fg=YELLOW).pack(anchor="w", padx=10, pady=(10, 4))

        list_frame = tk.Frame(left, bg=BG2)
        list_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))

        scrollbar = tk.Scrollbar(list_frame)
        scrollbar.pack(side="right", fill="y")

        self.snap_listbox = tk.Listbox(
            list_frame,
            yscrollcommand=scrollbar.set,
            bg=BG, fg=TEXT,
            selectbackground=BG3,
            selectforeground=ACCENT,
            font=("Consolas", 9),
            borderwidth=0,
            highlightthickness=1,
            highlightcolor=BG3,
            activestyle="none"
        )
        self.snap_listbox.pack(side="left", fill="both", expand=True)
        scrollbar.config(command=self.snap_listbox.yview)

        # 右栏：操作区
        right = tk.Frame(body, bg=BG, width=220)
        right.pack(side="right", fill="y")
        right.pack_propagate(False)

        tk.Label(right, text="⚡ 操作",
                 font=("Microsoft YaHei", 10, "bold"),
                 bg=BG, fg=YELLOW).pack(anchor="w", pady=(0, 8))

        btn_cfg = dict(font=("Microsoft YaHei", 10, "bold"),
                       width=18, pady=8, bd=0, cursor="hand2")

        tk.Button(right, text="🔄  还原选中快照",
                  bg=ACCENT, fg="white",
                  command=self._restore_selected,
                  **btn_cfg).pack(pady=4)

        tk.Button(right, text="⚡  还原最新备份",
                  bg=GREEN, fg="white",
                  command=self._restore_latest,
                  **btn_cfg).pack(pady=4)

        tk.Button(right, text="💾  立即备份",
                  bg=BG3, fg=TEXT,
                  command=self._do_backup,
                  **btn_cfg).pack(pady=4)

        tk.Button(right, text="🔍  检查完整性",
                  bg=BG3, fg=TEXT,
                  command=self._run_check,
                  **btn_cfg).pack(pady=4)

        tk.Button(right, text="🔃  刷新列表",
                  bg=BG3, fg=TEXT,
                  command=self._refresh_snapshots,
                  **btn_cfg).pack(pady=4)

        # 状态指示
        self.status_var = tk.StringVar(value="就绪")
        tk.Label(right, textvariable=self.status_var,
                 font=("Microsoft YaHei", 9),
                 bg=BG, fg=GREEN, wraplength=200,
                 justify="left").pack(pady=(16, 4), anchor="w")

        # ── 日志区 ──
        log_frame = tk.Frame(self, bg=BG2)
        log_frame.pack(fill="x", padx=16, pady=(0, 12))

        tk.Label(log_frame, text="📋 操作日志",
                 font=("Microsoft YaHei", 9, "bold"),
                 bg=BG2, fg=TEXT_DIM).pack(anchor="w", padx=10, pady=(6, 2))

        self.log_text = scrolledtext.ScrolledText(
            log_frame, height=8,
            bg=BG, fg=GREEN,
            font=("Consolas", 9),
            borderwidth=0,
            state="disabled"
        )
        self.log_text.pack(fill="x", padx=10, pady=(0, 8))

    def _log(self, msg):
        self.log_text.config(state="normal")
        self.log_text.insert("end", msg + "\n")
        self.log_text.see("end")
        self.log_text.config(state="disabled")
        self.update_idletasks()

    def _refresh_snapshots(self):
        self.snapshots = list_snapshots()
        self.snap_listbox.delete(0, "end")
        if self.snapshots:
            for s in self.snapshots:
                self.snap_listbox.insert("end", s["display"])
            self.snap_listbox.selection_set(0)
        else:
            self.snap_listbox.insert("end", "  （暂无快照，请先备份）")
        self.status_var.set(f"共 {len(self.snapshots)} 个快照")

    def _run_check(self):
        def _check():
            self._log("\n── 完整性检查 ──")
            do_check(self._log)
        threading.Thread(target=_check, daemon=True).start()

    def _restore_selected(self):
        sel = self.snap_listbox.curselection()
        if not sel or not self.snapshots:
            messagebox.showwarning("提示", "请先选择一个快照")
            return
        snap = self.snapshots[sel[0]]
        if not messagebox.askyesno("确认还原",
            f"将还原到：\n{snap['time']}\n标签：{snap['label']}\n\n当前工作区文件将被覆盖，确认继续？"):
            return
        def _do():
            self._log(f"\n── 还原快照: {snap['time']} ──")
            self.status_var.set("还原中...")
            ok = do_restore(snap["path"], self._log)
            self.status_var.set("还原完成！" if ok else "还原失败")
            if ok:
                messagebox.showinfo("完成", "记忆还原成功！\n请重启 QClaw 使记忆生效。")
        threading.Thread(target=_do, daemon=True).start()

    def _restore_latest(self):
        if not LATEST_DIR.exists():
            messagebox.showerror("错误", "本地备份不存在\n请尝试从快照列表中选择云端快照还原")
            return
        if not messagebox.askyesno("确认还原", "将还原最新备份到工作区\n当前文件将被覆盖，确认继续？"):
            return
        def _do():
            self._log("\n── 还原最新备份 ──")
            self.status_var.set("还原中...")
            files = list(LATEST_DIR.rglob("*"))
            files = [f for f in files if f.is_file()]
            for src in files:
                rel = src.relative_to(LATEST_DIR)
                dst = WORKSPACE / rel
                dst.parent.mkdir(parents=True, exist_ok=True)
                shutil.copy2(src, dst)
                self._log(f"  ✓ {rel}")
            self._log(f"\n[完成] 还原 {len(files)} 个文件")
            self.status_var.set("还原完成！")
            messagebox.showinfo("完成", "记忆还原成功！\n请重启 QClaw 使记忆生效。")
        threading.Thread(target=_do, daemon=True).start()

    def _do_backup(self):
        def _do():
            self._log("\n── 开始备份 ──")
            self.status_var.set("备份中...")
            ok = do_backup(self._log)
            self.status_var.set("备份完成！" if ok else "备份失败")
            if ok:
                self._refresh_snapshots()
        threading.Thread(target=_do, daemon=True).start()

if __name__ == "__main__":
    app = RestoreApp()
    app.mainloop()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)