Skill: king6381/mjolnir-shadow (version 2.0.0)

--- name: mjolnir-shadow description: "Mjolnir Shadow (雷神之影) — Automated rotating backup system for OpenClaw workspaces. Creates GPG-encrypted, rotating backups of workspace files, configs, and data to WebDAV storage. Requires bash, curl, Python 3, gpg (gnupg). Optional env var MJOLNIR_SHADOW_PASS for non-interactive GPG decryption. Triggers on backup, data safety, disaster recovery, workspace protection, system backup, 备份, 数据安全, 灾备, 雷神之影." metadata: {"clawdbot":{"homepage":"https://github.com/king6381/mjolnir-shadow","requires":{"binaries":["bash","curl","python3","gpg"],"env_optional":["MJOLNIR_SHADOW_PASS"]}}} --- # Mjolnir Shadow (雷神之影) 🌑 Automated rotating backup for OpenClaw workspaces with GPG-encrypted credential storage. 自动轮转备份系统，支持 GPG 加密凭证。 ## Requirements / 依赖 - **bash** + **curl** — most Linux/macOS systems - **Python 3.6+** — for setup wizard - **GPG (gnupg)** — for credential encryption (recommended) - **WebDAV server** — Nextcloud, ownCloud, Synology, etc. - **Env var** (optional): `MJOLNIR_SHADOW_PASS` — GPG passphrase for non-interactive cron runs ## Quick Start / 快速开始 ```bash # 1. Interactive setup (creates encrypted config) # 1. 交互式配置（创建加密配置） python3 scripts/setup_backup.py # 2. Run backup # 2. 运行备份 bash scripts/backup.sh # 3. Check status # 3. 检查状态 cat memory/backup-state.json ``` ## How It Works / 工作原理 1. **Decrypt** config via GPG (or read plaintext fallback) / GPG 解密配置 2. **Collect** workspace, configs, strategies, skills into archives / 打包文件 3. **Upload** to WebDAV via `--netrc-file` (credentials never in process listings) / 上传到 WebDAV 4. **Rotate** — keep N newest, delete oldest (FIFO) / 轮转：保留最新 N 个 5. **Clean up** — all temp files removed on exit / 退出时清理临时文件 ## Security / 安全 - **GPG AES-256** encrypted credential storage / 凭证加密存储 - **--netrc-file** for all curl calls (backup + restore) — no `-u user:pass` / 所有 curl 调用使用 netrc - **HTTPS warning** if WebDAV URL is not HTTPS / 非 HTTPS 警告 - **Channel auth excluded** by default (configurable) / 默认排除通道认证 token - **Temp cleanup** via `trap EXIT` — netrc + archives removed even on failure / 异常退出也清理 - **Secret exclusion** — `.gpg`, `.key`, `.secret`, `.env` always excluded / 始终排除密钥文件 ## What Gets Backed Up / 备份内容 | Component / 组件 | Includes / 包含 | Excludes / 排除 | |-----------|----------|----------| | **Workspace** | Memory, docs, scripts | .git, node_modules, venv, .env, secrets/*.gpg | | **Config** | openclaw.json, cron/ | Channel auth (default), *.gpg, *.secret | | **Strategies** | Strategy files, monitors | venv, __pycache__ | | **Skills** | Installed skill packages | node_modules, venv, *.gpg | ## Cron / 定时备份 Setup wizard prints the cron command (does not auto-create). Add manually: 配置向导打印 cron 命令（不会自动创建）。手动添加： ```bash # Every 3 days at 03:00 / 每 3 天凌晨 3 点 openclaw cron add --name "🌑 Mjolnir Shadow" \ --schedule "0 3 */3 * *" --tz "Asia/Shanghai" \ --isolated --timeout 300 \ --message "bash /path/to/scripts/backup.sh" ``` ### Non-interactive GPG decryption / 非交互式 GPG 解密 For unattended cron runs, use `gpg-agent` (recommended) or set `MJOLNIR_SHADOW_PASS` via a **secure secret store** — do NOT put plaintext passphrases in cron commands or world-readable files. 非交互运行时，推荐使用 `gpg-agent`，或通过 **安全的密钥存储** 设置 `MJOLNIR_SHADOW_PASS` — **不要** 在 cron 命令或明文文件中写入密码。 ```bash # Recommended: use gpg-agent (pre-cache passphrase) # 推荐：使用 gpg-agent（预缓存密码） gpg-connect-agent /bye # ensure agent is running # Alternative: read from encrypted credential store # 备选：从加密凭证存储读取 # export MJOLNIR_SHADOW_PASS="$(your-secret-manager get mjolnir-shadow-pass)" ``` ## Restore / 恢复 ### 🚀 一键恢复（推荐新手） ```bash # 方式1: 全自动模式 — 带进度条，一键搞定 bash scripts/restore.sh --auto # 方式2: 独立恢复引导 — 不依赖 OpenClaw，全新系统也能用 bash scripts/restore-kit.sh ``` **`--auto` 模式**会自动完成：解密配置 → 查找最新备份 → 下载 → 解压 → 恢复全部组件 → 重启 OpenClaw。全程进度条显示，小白友好。 **`restore-kit.sh`** 是完全独立的脚本，不依赖任何已安装技能。适用于： - 全新系统重装后恢复 - OpenClaw 还没装的时候 - 从另一台机器 SCP 过来直接运行 ```bash # 从另一台机器拷贝过来运行 scp kaixin:/path/to/restore-kit.sh . && bash restore-kit.sh ``` ### 手动模式（高级用户） ```bash bash scripts/restore.sh --list # List backups / 列出备份 bash scripts/restore.sh --latest # Restore latest (manual) / 恢复最新（手动确认） bash scripts/restore.sh --file NAME # Restore specific / 恢复指定 ``` 手动模式先解压到临时目录，你可以检查后再决定覆盖哪些组件。 ## Configuration Reference / 配置参考 See `references/config-reference.md` for all options. / 详见配置参考文档。

path: README.md
content: # 🌑 Mjolnir Shadow (雷神之影)

> *Like a shadow — silent, faithful, always there.*
> *如影随形，默默守护。*

**OpenClaw 智能体的自动备份与一键恢复系统。**

让你的 AI 智能体永远不会失忆——哪怕系统崩了、电脑换了，一条命令就能满血复活。

---

## 🤔 这是什么？为什么需要它？

你的 OpenClaw 智能体会积累记忆、学会你的习惯、记住你的项目进度。但如果系统崩了、换了电脑、或者不小心删了文件——**这些记忆就全没了**。

**雷神之影**就是你的保险：
- ⏰ **自动备份** — 定时把记忆、配置、技能打包上传到你的私有云
- 🔐 **安全加密** — 凭证用 GPG 加密，不怕泄露
- 🔄 **智能轮转** — 自动保留最近 N 个备份，不占满存储
- 🚀 **一键恢复** — 系统挂了？一条命令，5 分钟满血复活

---

## 📦 雷神四件套

雷神之影是「**雷神系列**」开源工具集的一员：

| 工具 | 用途 | 状态 |
|------|------|------|
| 🧠 [雷神之脑](https://github.com/king6381/mjolnir-brain) | AI Agent 自进化记忆系统 | ✅ 已发布 |
| 🌑 **雷神之影** (本项目) | 自动备份与一键恢复 | ✅ 已发布 |
| 🛡️ 雷神之盾 | GPG 加密凭证管理 | 🔧 内部使用 |
| ⚡ 雷神之锤 | A 股智能量化分析 | 🔒 私有 |

装了这四件套，你的 OpenClaw 智能体就有了：**长期记忆 + 数据安全 + 密码管理 + 量化分析**。

---

## 🚀 5 分钟上手指南

> **适合人群**：从没用过 Linux 命令行也没关系，跟着步骤走就行。

### 前提条件

你需要：
- 一台电脑（**Windows 11**、**macOS**、**Ubuntu** 都行）
- 一个 WebDAV 存储（Nextcloud、群晖 NAS、ownCloud 都行）
- OpenClaw 会在恢复过程中自动安装，不用提前装

| 你的系统 | 用哪个脚本 | 怎么运行 |
|---------|-----------|---------|
| 🪟 **Windows 11** | `restore-kit.ps1` | PowerShell 管理员运行（自动装 WSL2 + Ubuntu） |
| 🍎 **macOS** | `restore-kit.sh` | 终端运行 `bash restore-kit.sh` |
| 🐧 **Ubuntu** | `restore-kit.sh` | 终端运行 `bash restore-kit.sh` |

> 💡 **Windows 用户不用怕 Linux！** PowerShell 脚本会自动帮你装好 WSL2 和 Ubuntu，全程中文提示，点点鼠标就行。

### 第 1 步：安装雷神之影

```bash
# 方式 A：如果你有 OpenClaw（推荐）
# 直接复制到技能目录
cp -r mjolnir-shadow ~/.openclaw/workspace/skills/

# 方式 B：从 GitHub 克隆
git clone https://github.com/king6381/mjolnir-shadow.git
cd mjolnir-shadow
```

### 第 2 步：配置你的存储

运行配置向导，跟着提示一步步填：

```bash
python3 scripts/setup_backup.py
```

向导会问你：
1. **WebDAV 地址** — 你的 Nextcloud/NAS 的 WebDAV 地址
2. **用户名和密码** — 登录凭证（会用 GPG 加密保存）
3. **备份频率** — 几天备份一次（默认 3 天）
4. **保留数量** — 保留几个备份（默认 3 个）

> 💡 **小贴士**：如果你用群晖 NAS，WebDAV 地址通常是 `http://你的NAS_IP:5005`
> 如果你用 Nextcloud，地址通常是 `http://你的IP/remote.php/webdav`

### 第 3 步：首次备份

```bash
bash scripts/backup.sh
```

你会看到：
```
🌑 Mjolnir Shadow Backup - 2026-03-23_2200
==========================================
📦 Packing workspace...
📦 Packing config...
📦 Packing skills...
📤 Uploading...
✅ Upload successful! HTTP 201
==========================================
🌑 Mjolnir Shadow - SUCCESS
📦 Size: 2.1M
🔄 Rotation: 1/3 slots used
==========================================
```

**恭喜！你的智能体已经有了第一份保险。** 🎉

### 第 4 步：设置自动备份（可选但强烈推荐）

让它每 3 天自动跑一次，你再也不用操心：

```bash
# 在 OpenClaw 里添加定时任务
openclaw cron add --name "🌑 雷神之影" \
  --schedule "0 3 */3 * *" --tz "Asia/Shanghai" \
  --isolated --timeout 300 \
  --message "bash ~/path/to/scripts/backup.sh"
```

> 💡 这表示每 3 天凌晨 3 点自动备份。你睡觉的时候，雷神之影替你守护数据。

---

## 🆘 灾难恢复 — 系统崩了怎么办？

> **这是最重要的部分。** 收藏这一节，关键时刻救命。

### 场景 1：OpenClaw 还在，只是数据丢了

一条命令搞定：

```bash
bash scripts/restore.sh --auto
```

你会看到进度条一步步走完：

```
🌑 ═══════════════════════════════════════════
   雷神之影 — 一键全自动恢复
🌑 ═══════════════════════════════════════════

  [███░░░░░░░░░░░░░░░░░░░░░░░░░░░]  16%  🔐 解密配置文件...
       ✓ 配置加载成功

  [██████░░░░░░░░░░░░░░░░░░░░░░░░]  33%  🔍 查找最新备份...
       ✓ 找到: backup_2026-03-23_0300.tar.gz (2026-03-23)

  [█████████░░░░░░░░░░░░░░░░░░░░░]  50%  📥 下载备份包...
       ✓ 下载完成 (2.1M)

  [████████████░░░░░░░░░░░░░░░░░░]  66%  📦 解压备份包...
       ✓ 解压完成: workspace.tar.gz, config.tar.gz, skills.tar.gz

  [███████████████░░░░░░░░░░░░░░░]  83%  🔄 恢复文件到系统...
       ✓ workspace → ~/.openclaw/workspace
       ✓ config → ~/.openclaw
       ✓ skills → ~/.openclaw/workspace/skills/
       ✓ 共恢复 3 个组件

  [██████████████████████████████] 100%  🚀 重启 OpenClaw...
       ✓ OpenClaw 已重启

🌑 ═══════════════════════════════════════════
   ✅ 恢复完成！
🌑 ═══════════════════════════════════════════

  📦 备份文件: backup_2026-03-23_0300.tar.gz
  📅 备份日期: 2026-03-23
  🔄 恢复组件: 3 个

  智能体下次醒来就有完整记忆了 🧠
```

### 场景 2：全新电脑，什么都没装

**这才是真正的考验。** 你的电脑炸了，换了一台全新的，什么都没有。

别慌。拿出你提前备份的恢复脚本（U 盘、邮箱、网盘都行），然后根据你的系统：

#### 🪟 Windows 11

```powershell
# 右键 PowerShell → "以管理员身份运行"，然后：
Set-ExecutionPolicy Bypass -Scope Process -Force
.\restore-kit.ps1
```

脚本会自动完成：
1. ✅ 检测 Windows 版本
2. ✅ 安装 WSL2（可能需要重启一次）
3. ✅ 安装 Ubuntu
4. ✅ 在 Ubuntu 里装 Node.js + OpenClaw
5. ✅ 连接你的 WebDAV 备份存储
6. ✅ 下载并恢复所有数据
7. ✅ 启动智能体

> 💡 如果提示需要重启，重启后再运行一次就行，脚本会自动从断点继续。

#### 🍎 macOS / 🐧 Ubuntu

```bash
bash restore-kit.sh
```

脚本会自动完成：
1. ✅ 检测系统 + 安装缺失依赖
2. ✅ 安装 Node.js v22
3. ✅ 安装 OpenClaw
4. ✅ 连接 WebDAV + 下载备份
5. ✅ 恢复数据 + 启动智能体

效果预览：

```
🌑 ════════════════════════════════════════════════════════
   雷神之影 — 一键恢复引导 (Restore Kit v2.0)
   从全新电脑到满血复活，一条命令搞定
🌑 ════════════════════════════════════════════════════════

  [██████████████████████████████] 100%

  ✅ 完美！从裸机到满血，一步到位！

  📦 备份来源:  backup_2026-03-23_0300.tar.gz
  📅 备份日期:  2026-03-23
  🔄 恢复组件:  4 个

  🧠 智能体下次醒来就有完整记忆了！
```

> **重点**：恢复脚本是**完全独立**的——不需要提前装任何东西。
> Windows 只要有 PowerShell，macOS/Ubuntu 只要有 bash。
> 这就是为什么你应该**把它单独备份一份**（U 盘、邮箱、打印出来贴墙上都行 😄）。

### 场景 3：想看看有哪些备份

```bash
bash scripts/restore.sh --list
```

```
🌑 可用备份列表

  1. 📦 backup_2026-03-20_0300.tar.gz  (2026-03-20)
  2. 📦 backup_2026-03-23_0300.tar.gz  (2026-03-23)
```

### 场景 4：想恢复特定版本（不是最新的）

```bash
bash scripts/restore.sh --file backup_2026-03-20_0300.tar.gz
```

---

## 📋 命令速查表

| 我想... | 命令 |
|---------|------|
| 首次配置 | `python3 scripts/setup_backup.py` |
| 手动备份一次 | `bash scripts/backup.sh` |
| 查看备份列表 | `bash scripts/restore.sh --list` |
| **一键恢复（推荐）** | `bash scripts/restore.sh --auto` |
| 🐧🍎 全新系统恢复 | `bash scripts/restore-kit.sh` |
| 🪟 Win11 全新恢复 | `.\restore-kit.ps1`（管理员 PowerShell） |
| 恢复最新（手动模式） | `bash scripts/restore.sh --latest` |
| 恢复指定版本 | `bash scripts/restore.sh --file <名字>` |

---

## 🔒 安全设计

**你的数据安全是第一优先级。**

| 安全措施 | 说明 |
|---------|------|
| 🔐 **GPG 加密** | 你的 WebDAV 密码用 AES-256 加密存储，不是明文 |
| 🔒 **进程隐藏** | 所有 curl 请求用 `--netrc-file`，`ps` 命令看不到密码 |
| ⚠️ **HTTPS 提醒** | 如果你的地址不是 HTTPS，脚本会警告你 |
| 🚫 **敏感排除** | `.gpg`、`.key`、`.secret`、`.env` 文件永远不会被备份 |
| 🧹 **自动清理** | 临时文件（包括 netrc）在脚本结束时自动删除，崩溃也不留 |
| 🔑 **通道保护** | 微信/QQ 等登录 token 默认不备份（防止 token 泄露） |

---

## 📦 备份了什么？没备份什么？

### ✅ 会备份的

| 类型 | 包含内容 |
|------|---------|
| **记忆文件** | MEMORY.md、memory/*.md（你的智能体的全部记忆） |
| **人格文件** | SOUL.md、IDENTITY.md、USER.md（智能体的性格和对你的了解） |
| **配置文件** | openclaw.json、定时任务（cron） |
| **工作文档** | AGENTS.md、TOOLS.md 及 workspace 下所有文件 |
| **技能包** | 已安装的所有技能（包括雷神之影自己） |
| **交易策略** | 策略文件和监控脚本（如果有） |

### ❌ 不会备份的（安全考虑）

| 类型 | 原因 |
|------|------|
| `.git/` 目录 | 太大，可以重新 clone |
| `node_modules/` | 太大，可以重新 npm install |
| `venv/` 虚拟环境 | 太大，可以重新创建 |
| `.env` 环境变量 | 可能包含密钥 |
| `*.gpg` 加密文件 | 已经是加密的凭证 |
| 微信/QQ 登录 token | 安全原因，需要重新扫码 |

---

## 🔄 轮转机制

雷神之影不会无限占用存储空间：

```
第 1 次备份:  [A]
第 2 次备份:  [A] [B]
第 3 次备份:  [A] [B] [C]          ← 存满了（默认保留 3 个）
第 4 次备份:  [B] [C] [D]          ← A 自动删除
第 5 次备份:  [C] [D] [E]          ← B 自动删除
```

始终保留最近 N 个备份（默认 3 个），旧的自动清理。

---

## 🛠️ 高级配置

### 修改备份参数

配置文件在 `config/backup-config.json`（GPG 加密）。重新运行向导即可修改：

```bash
python3 scripts/setup_backup.py
```

可配置项：

| 参数 | 默认值 | 说明 |
|------|--------|------|
| `webdav_url` | — | WebDAV 地址（必填） |
| `webdav_user` | — | 用户名（必填） |
| `webdav_pass` | — | 密码（必填，加密存储） |
| `remote_dir` | `openclaw-backups` | 远程备份目录名 |
| `max_backups` | `3` | 最多保留几个备份 |
| `interval_days` | `3` | 自动备份间隔（天） |
| `backup_workspace` | `true` | 是否备份工作空间 |
| `backup_config` | `true` | 是否备份配置 |
| `backup_strategies` | `true` | 是否备份交易策略 |
| `backup_skills` | `true` | 是否备份技能包 |
| `exclude_channel_auth` | `true` | 是否排除通道认证 token |

### 定时备份（Cron）

```bash
# 每 3 天凌晨 3 点自动备份
openclaw cron add --name "🌑 雷神之影" \
  --schedule "0 3 */3 * *" --tz "Asia/Shanghai" \
  --isolated --timeout 300 \
  --message "bash /path/to/scripts/backup.sh"
```

### 非交互式运行（服务器/Cron 场景）

定时任务运行时没人输密码，需要提前设置：

```bash
# 推荐：用 gpg-agent 缓存密码
gpg-connect-agent /bye

# 或者：设置环境变量（通过安全的密钥管理器）
export MJOLNIR_SHADOW_PASS="$(your-secret-manager get mjolnir-shadow-pass)"
```

> ⚠️ **绝对不要**在 cron 命令或明文文件里写密码。

---

## ❓ 常见问题 (FAQ)

### Q: 我的 NAS 重启后备份会失败吗？
**A:** 会。NAS 重启后 WebDAV 服务可能没有自动启动，检查一下 NAS 的 WebDAV 设置。

### Q: 备份文件有多大？
**A:** 取决于你的 workspace 大小。一般 1-5MB，非常小。

### Q: 可以备份到多个地方吗？
**A:** 目前支持一个 WebDAV 目标。如果需要多份备份，可以在 NAS 端设置同步。

### Q: restore-kit.sh 需要 GPG 吗？
**A:** 如果你之前的配置是 GPG 加密的，恢复时需要 GPG + 密码。如果是明文配置，不需要。但在 restore-kit.sh 的交互模式下（手动输入驿站信息），完全不需要 GPG。

### Q: 支持哪些操作系统？
**A:** 三大平台全覆盖：
- 🪟 **Windows 11** — PowerShell 脚本自动装 WSL2（需要 Build 19041+）
- 🍎 **macOS** — 原生 bash 支持（需要 Homebrew）
- 🐧 **Ubuntu/Debian** — 原生支持

### Q: 支持哪些 WebDAV 服务？
**A:** 已测试：✅ Nextcloud、✅ ownCloud、✅ 群晖 NAS、✅ Apache mod_dav

### Q: 换了电脑，恢复脚本怎么拿到？
**A:** 提前备一份！建议：
- 📧 发一份到自己邮箱（Win11 存 `.ps1`，Mac/Ubuntu 存 `.sh`，或者两个都存）
- 💾 U 盘存一份
- ☁️ 网盘存一份
- 🖨️ 打印出来（没开玩笑，关键时刻纸比电子靠谱）

### Q: Windows 用户需要学 Linux 吗？
**A:** 完全不需要！PowerShell 脚本会自动帮你装好 WSL2 和 Ubuntu，全程中文提��。装完后日常使用也只要在开始菜单搜 "Ubuntu" 打开就行。

---

## 📁 项目结构

```
mjolnir-shadow/
├── README.md                   # 你正在看的这个文件
├── SKILL.md                    # OpenClaw 技能清单
├── LICENSE                     # MIT 开源许可证
├── scripts/
│   ├── backup.sh               # 核心备份脚本（带轮转）
│   ├── restore.sh              # 恢复工具（支持 --auto 一键模式）
│   ├── restore-kit.sh          # 🐧🍎 独立恢复引导（Ubuntu/macOS，零依赖）
│   ├── restore-kit.ps1         # 🪟 Windows 11 恢复引导（自动装 WSL2）
│   └── setup_backup.py         # 交互式配置向导
├── references/
│   └── config-reference.md     # 配置详细文档
└── config/                     # 你的配置（git-ignored，不会上传）
    └── backup-config.json.gpg  # GPG 加密的配置文件
```

---

## 🙏 致谢

- [OpenClaw](https://github.com/openclaw/openclaw) — 让 AI 智能体真正有用的开源平台
- 所有给 Star 的朋友们 ⭐

---

## 📜 法律与知识产权

### 版权

Copyright © 2026 King Lei (雷哥). All rights reserved.

**雷神系列** 品牌名称，包括但不限于 **雷神之脑**、**雷神之影**、**雷神之盾**、**雷神之锤** 及所有相关标识，均为 King Lei 的商标。

### 许可证

本项目采用 **MIT 许可证** — 详见 [LICENSE](LICENSE)。

你可以自由使用、修改和分发本软件。但 **雷神系列** 品牌名称未经书面许可不得用于衍生作品。

### 联系方式

- 📧 **Email**: [REDACTED_SECRET]
- 🐙 **GitHub**: [@king6381](https://github.com/king6381)
- 📱 **微信公众号**: 雷哥玩AI

商务合作、品牌授权请联系。

---

*Built with ❤️ by a 45-year-old who learned AI and decided to protect everyone's data.*

*一个 45 岁学 AI 的人，决定守护所有人的数据安全。*

**⭐ 觉得有用？给个 Star，让更多人看到！**


path: references/config-reference.md
content: # Configuration Reference

## backup-config.json

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `webdav_url` | string | — | WebDAV base URL (required) |
| `webdav_user` | string | — | WebDAV username (required) |
| `webdav_pass` | string | — | WebDAV password (required) |
| `remote_dir` | string | `openclaw-backups` | Remote folder name |
| `max_backups` | int | `3` | Maximum backups to keep |
| `interval_days` | int | `3` | Days between auto backups |
| `workspace_path` | string | `~/.openclaw/workspace` | Local workspace path |
| `openclaw_dir` | string | `~/.openclaw` | OpenClaw config directory |
| `backup_workspace` | bool | `true` | Include workspace files |
| `backup_config` | bool | `true` | Include OpenClaw config |
| `backup_strategies` | bool | `true` | Include trading strategies |
| `backup_skills` | bool | `true` | Include installed skills |

## Excluded Files (always)

- `.git/` — version control history
- `node_modules/` — npm dependencies (reinstallable)
- `venv/`, `.venv/` — Python virtual environments
- `__pycache__/`, `*.pyc` — Python cache
- `secrets/*.gpg` — encrypted credentials (security)

## WebDAV Compatibility

Tested with:
- ✅ Nextcloud
- ✅ ownCloud
- ✅ Synology WebDAV
- ✅ Apache mod_dav

## Rotation Strategy

The rotation uses a simple FIFO (First In, First Out) approach:

1. List all `backup_*.tar.gz` files in remote directory
2. Sort alphabetically (timestamp in name ensures chronological order)
3. If count >= max_backups, delete oldest (count - max_backups + 1) files
4. Upload new backup

This ensures exactly `max_backups` copies exist after each run.

## Cron Examples

```bash
# Every 3 days at 03:00
0 3 */3 * * Asia/Shanghai

# Daily at 02:00
0 2 * * * Asia/Shanghai

# Weekly on Sunday at 04:00
0 4 * * 0 Asia/Shanghai
```


path: scripts/backup.sh
content: #!/bin/bash
# ⚡🌑 Mjolnir Shadow (雷神之影) - Automated Rotating Backup
# Backs up OpenClaw workspace to WebDAV with automatic rotation
# Security: credentials encrypted via GPG, never exposed in process listings

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_FILE="${SKILL_DIR}/config/backup-config.json"
ENCRYPTED_CONFIG="${SKILL_DIR}/config/backup-config.json.gpg"
NETRC_FILE="/tmp/.mjolnir_shadow_netrc_$$"

cleanup() {
  rm -f "$NETRC_FILE"
  rm -rf "$BACKUP_DIR" "$FINAL_FILE" 2>/dev/null
}
trap cleanup EXIT

# ── Load Config ──────────────────────────────────────────────
decrypt_config() {
  if [ -f "$ENCRYPTED_CONFIG" ]; then
    if [ -n "$MJOLNIR_SHADOW_PASS" ]; then
      gpg --quiet --batch --yes --passphrase "$MJOLNIR_SHADOW_PASS" \
        --decrypt "$ENCRYPTED_CONFIG" 2>/dev/null
    else
      gpg --quiet --batch --yes --decrypt "$ENCRYPTED_CONFIG" 2>/dev/null
    fi
  elif [ -f "$CONFIG_FILE" ]; then
    echo "⚠️  Warning: Using unencrypted config. Run setup to encrypt." >&2
    cat "$CONFIG_FILE"
  else
    echo "❌ No config found. Run: python3 ${SCRIPT_DIR}/setup_backup.py" >&2
    exit 1
  fi
}

CONFIG_JSON=$(decrypt_config)
if [ -z "$CONFIG_JSON" ]; then
  echo "❌ Failed to load config. Check GPG passphrase or run setup."
  exit 1
fi

WEBDAV_URL=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_url'])")
WEBDAV_USER=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_user'])")
WEBDAV_PASS=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_pass'])")
REMOTE_DIR=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['remote_dir'])")
MAX_BACKUPS=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('max_backups', 3))")
BACKUP_WORKSPACE=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('backup_workspace', True))")
BACKUP_CONFIG=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('backup_config', True))")
BACKUP_STRATEGIES=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('backup_strategies', True))")
BACKUP_SKILLS=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('backup_skills', True))")
WORKSPACE_PATH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('workspace_path', '$HOME/.openclaw/workspace'))")
OPENCLAW_DIR=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('openclaw_dir', '$HOME/.openclaw'))")
EXCLUDE_AUTH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin).get('exclude_channel_auth', True))")

# ── Security: Use netrc file instead of -u flag ─────────────
# This prevents credentials from appearing in process listings
WEBDAV_HOST=$(echo "$WEBDAV_URL" | grep -oP '://\K[^/]+')
cat > "$NETRC_FILE" << EOF
machine ${WEBDAV_HOST}
login ${WEBDAV_USER}
password ${WEBDAV_PASS}
EOF
chmod 600 "$NETRC_FILE"
CURL_AUTH="--netrc-file ${NETRC_FILE}"

# ── HTTPS Check ──────────────────────────────────────────────
if [[ "$WEBDAV_URL" != https://* ]]; then
  echo "⚠️  WARNING: WebDAV URL is not HTTPS! Credentials will be sent in plaintext."
  echo "   Strongly recommend using HTTPS for production backups."
  echo "   Current URL: ${WEBDAV_URL}"
  echo ""
fi

FULL_URL="${WEBDAV_URL}/${REMOTE_DIR}"
BACKUP_DIR="/tmp/mjolnir_shadow_backup_$$"
TIMESTAMP=$(date +"%Y-%m-%d_%H%M")
BACKUP_NAME="backup_${TIMESTAMP}"
STATE_FILE="${WORKSPACE_PATH}/memory/backup-state.json"

echo "🌑 Mjolnir Shadow Backup - ${TIMESTAMP}"
echo "=========================================="

# ── Clean up ─────────────────────────────────────────────────
rm -rf "$BACKUP_DIR"
mkdir -p "$BACKUP_DIR"

# ── Pack components ──────────────────────────────────────────
if [ "$BACKUP_WORKSPACE" = "True" ]; then
  echo "📦 Packing workspace..."
  tar czf "$BACKUP_DIR/workspace.tar.gz" \
    -C "$WORKSPACE_PATH" \
    --exclude='.git' \
    --exclude='node_modules' \
    --exclude='venv' \
    --exclude='.venv' \
    --exclude='__pycache__' \
    --exclude='*.pyc' \
    --exclude='secrets/*.gpg' \
    --exclude='secrets/*.key' \
    --exclude='.env' \
    --exclude='*.secret' \
    . 2>/dev/null || echo "⚠️ Workspace pack had warnings (non-fatal)"
fi

if [ "$BACKUP_CONFIG" = "True" ]; then
  echo "📦 Packing config..."
  EXCLUDE_ARGS=""
  if [ "$EXCLUDE_AUTH" = "True" ]; then
    EXCLUDE_ARGS="--exclude=openclaw-weixin --exclude=qqbot --exclude=*/accounts --exclude=*/auth"
  fi
  tar czf "$BACKUP_DIR/config.tar.gz" \
    -C "$OPENCLAW_DIR" \
    $EXCLUDE_ARGS \
    --exclude='*.gpg' \
    --exclude='*.secret' \
    --exclude='.env' \
    openclaw.json \
    cron/ \
    2>/dev/null || echo "⚠️ Config pack had warnings (non-fatal)"
fi

if [ "$BACKUP_STRATEGIES" = "True" ]; then
  STRAT_DIR="${OPENCLAW_DIR}/workspace/projects/auto_trading"
  if [ -d "$STRAT_DIR/strategies" ]; then
    echo "📦 Packing strategies..."
    tar czf "$BACKUP_DIR/strategies.tar.gz" \
      -C "$STRAT_DIR" \
      --exclude='venv' --exclude='__pycache__' --exclude='*.pyc' \
      strategies/ monitor/ 2>/dev/null || echo "⚠️ Strategies pack had warnings"
  fi
fi

if [ "$BACKUP_SKILLS" = "True" ]; then
  SKILLS_DIR="${OPENCLAW_DIR}/workspace/skills"
  if [ -d "$SKILLS_DIR" ]; then
    echo "📦 Packing skills..."
    tar czf "$BACKUP_DIR/skills.tar.gz" \
      -C "$OPENCLAW_DIR/workspace" \
      --exclude='node_modules' --exclude='venv' --exclude='.venv' \
      --exclude='__pycache__' --exclude='*.pyc' \
      --exclude='*.gpg' --exclude='*.secret' --exclude='.env' \
      skills/ 2>/dev/null || echo "⚠️ Skills pack had warnings"
  fi
fi

# ── Create final archive ─────────────────────────────────────
echo "📦 Creating final archive..."
FINAL_FILE="/tmp/${BACKUP_NAME}_$$.tar.gz"
tar czf "$FINAL_FILE" -C "$BACKUP_DIR" . 2>/dev/null
FILESIZE=$(du -h "$FINAL_FILE" | cut -f1)
FILESIZE_BYTES=$(stat -c%s "$FINAL_FILE" 2>/dev/null || stat -f%z "$FINAL_FILE" 2>/dev/null)
echo "📦 Size: ${FILESIZE}"

# ── Ensure remote directory ──────────────────────────────────
curl -s $CURL_AUTH -X MKCOL "${FULL_URL}/" 2>/dev/null || true

# ── Check existing backups ───────────────────────────────────
echo "🔄 Checking existing backups..."
EXISTING=$(curl -s $CURL_AUTH \
  -X PROPFIND "${FULL_URL}/" -H "Depth: 1" 2>/dev/null \
  | grep -oP 'backup_[^<]+\.tar\.gz' | sort -u || true)
BACKUP_COUNT=$(echo "$EXISTING" | grep -c "backup_" 2>/dev/null || true)
BACKUP_COUNT=$(echo "$BACKUP_COUNT" | tr -d '[:space:]')
BACKUP_COUNT=${BACKUP_COUNT:-0}
echo "📊 Existing: ${BACKUP_COUNT} / ${MAX_BACKUPS}"

# ── Rotate ───────────────────────────────────────────────────
if [ "$BACKUP_COUNT" -ge "$MAX_BACKUPS" ]; then
  DELETE_COUNT=$((BACKUP_COUNT - MAX_BACKUPS + 1))
  echo "🗑️ Rotating: deleting ${DELETE_COUNT} oldest backup(s)..."
  echo "$EXISTING" | head -n "$DELETE_COUNT" | while read -r OLD_BACKUP; do
    if [ -n "$OLD_BACKUP" ]; then
      echo "   Deleting: ${OLD_BACKUP}"
      curl -s $CURL_AUTH -X DELETE "${FULL_URL}/${OLD_BACKUP}" 2>/dev/null
    fi
  done
fi

# ── Upload ───────────────────────────────────────────────────
echo "📤 Uploading..."
HTTP_CODE=$(curl -s $CURL_AUTH \
  -T "$FINAL_FILE" \
  "${FULL_URL}/${BACKUP_NAME}.tar.gz" \
  -w "%{http_code}" -o /dev/null 2>&1)

if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then
  echo "✅ Upload successful! HTTP ${HTTP_CODE}"
  STATUS="success"
else
  echo "❌ Upload failed! HTTP ${HTTP_CODE}"
  STATUS="failed"
fi

# ── Update state ─────────────────────────────────────────────
mkdir -p "$(dirname "$STATE_FILE")"
NEW_COUNT=$((BACKUP_COUNT >= MAX_BACKUPS ? MAX_BACKUPS : BACKUP_COUNT + 1))
cat > "$STATE_FILE" << EOF
{
  "lastBackup": "${TIMESTAMP}",
  "backupName": "${BACKUP_NAME}.tar.gz",
  "fileSize": "${FILESIZE}",
  "fileSizeBytes": ${FILESIZE_BYTES},
  "httpStatus": ${HTTP_CODE},
  "status": "${STATUS}",
  "backupCount": ${NEW_COUNT},
  "maxBackups": ${MAX_BACKUPS},
  "remoteDir": "${REMOTE_DIR}",
  "credentialSecurity": "gpg-encrypted"
}
EOF

echo ""
echo "=========================================="
echo "🌑 Mjolnir Shadow - ${STATUS^^}"
echo "📅 Time: ${TIMESTAMP}"
echo "📦 Size: ${FILESIZE}"
echo "📍 Remote: ${REMOTE_DIR}/${BACKUP_NAME}.tar.gz"
echo "🔄 Rotation: ${NEW_COUNT}/${MAX_BACKUPS} slots used"
echo "=========================================="

exit $([ "$STATUS" = "success" ] && echo 0 || echo 1)


path: scripts/restore-kit.sh
content: #!/bin/bash
# 🌑 Mjolnir Shadow (雷神之影) - Standalone Restore Kit / 独立恢复引导脚本
# 
# ╔══════════════════════════════════════════════════════════════╗
# ║  从裸机到满血，一条命令搞定一切。                              ║
# ║  检查系统 → 装依赖 → 装 Node.js → 装 OpenClaw →              ║
# ║  恢复数据 → 启动服务 → 智能体满血复活！                       ║
# ║                                                              ║
# ║  From bare metal to fully restored — one command.            ║
# ╚══════════════════════════════════════════════════════════════╝
#
# 用法 / Usage:
#   bash restore-kit.sh              # 交互式引导
#   bash restore-kit.sh --skip-env   # 跳过环境安装（已有 Node/OpenClaw）
#
# 支持系统: Ubuntu / macOS / Windows (WSL2, 用 restore-kit.ps1)
# 需要: bash, curl (其他全自动安装)

set -e

# ── Colors & UI ──────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

STEP_CURRENT=0
STEP_TOTAL=10
SKIP_ENV=false
ERRORS=()

NETRC_FILE="/tmp/.mjolnir_restore_kit_netrc_$$"
RESTORE_DIR="/tmp/mjolnir_restore_kit_$$"
DL_FILE="/tmp/mjolnir_restore_kit_dl_$$.tar.gz"

cleanup() {
  rm -f "$NETRC_FILE" "$DL_FILE" 2>/dev/null
}
trap cleanup EXIT

# Parse args
for arg in "$@"; do
  case "$arg" in
    --skip-env) SKIP_ENV=true ;;
  esac
done

# ── Progress Bar ─────────────────────────────────────────────
progress_bar() {
  local percent=$1
  local width=30
  local filled=$((percent * width / 100))
  local empty=$((width - filled))
  local bar=""
  for ((i=0; i<filled; i++)); do bar+="█"; done
  for ((i=0; i<empty; i++)); do bar+="░"; done
  printf "\r  ${CYAN}[${bar}]${NC} ${BOLD}%3d%%${NC}" "$percent"
}

step() {
  STEP_CURRENT=$((STEP_CURRENT + 1))
  local percent=$((STEP_CURRENT * 100 / STEP_TOTAL))
  local icon="$1"
  local msg="$2"
  echo ""
  progress_bar "$percent"
  echo "  ${icon} ${msg}"
}

ok() { echo -e "       ${GREEN}✓ $1${NC}"; }
fail() { echo -e "       ${RED}✗ $1${NC}"; ERRORS+=("$1"); }
warn() { echo -e "       ${YELLOW}⚠ $1${NC}"; }
info() { echo -e "       ${DIM}$1${NC}"; }

# ── Detect OS ────────────────────────────────────────────────
detect_os() {
  if [ -f /etc/os-release ]; then
    . /etc/os-release
    OS_ID="$ID"
    OS_NAME="$PRETTY_NAME"
  elif [ "$(uname)" = "Darwin" ]; then
    OS_ID="macos"
    OS_NAME="macOS $(sw_vers -productVersion 2>/dev/null || echo '')"
  else
    OS_ID="unknown"
    OS_NAME="Unknown OS"
  fi
}

# ── Package installer (Ubuntu/macOS only) ────────────────────
install_pkg() {
  local pkg="$1"
  case "$OS_ID" in
    ubuntu|debian)
      sudo apt-get install -y -qq "$pkg" 2>/dev/null
      ;;
    macos)
      if command -v brew &>/dev/null; then
        brew install "$pkg" 2>/dev/null
      else
        warn "请先安装 Homebrew: https://brew.sh"
        return 1
      fi
      ;;
    *)
      warn "不支持的系统: $OS_NAME"
      warn "本脚本支持: Ubuntu / macOS / Windows (用 restore-kit.ps1)"
      return 1
      ;;
  esac
}

# ── Banner ───────────────────────────────────────────────────
clear 2>/dev/null || true
echo ""
echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}   雷神之影 — 一键恢复引导 (Restore Kit v2.0)${NC}"
echo -e "${BOLD}   从全新电脑到满血复活，一条命令搞定${NC}"
echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
echo ""

# ══════════════════════════════════════════════════════════════
# Step 1: Detect system
# ══════════════════════════════════════════════════════════════
step "🖥️" "检测系统环境..."

detect_os
ok "操作系统: ${OS_NAME}"

ARCH=$(uname -m)
case "$ARCH" in
  x86_64)  ARCH_LABEL="x64"; NODE_ARCH="x64" ;;
  aarch64|arm64) ARCH_LABEL="ARM64"; NODE_ARCH="arm64" ;;
  *) ARCH_LABEL="$ARCH"; NODE_ARCH="$ARCH" ;;
esac
ok "架构: ${ARCH_LABEL}"

# Check if running as root
if [ "$(id -u)" = "0" ]; then
  warn "正在以 root 运行，建议使用普通用户"
  SUDO=""
else
  SUDO="sudo"
fi

# ══════════════════════════════════════════════════════════════
# Step 2: Check & install basic dependencies
# ══════════════════════════════════════════════════════════════
step "📦" "检查基础依赖..."

NEED_INSTALL=()

# curl (required)
if command -v curl &>/dev/null; then
  ok "curl $(curl --version 2>/dev/null | head -1 | awk '{print $2}')"
else
  NEED_INSTALL+=("curl")
  warn "curl 未安装，将自动安装"
fi

# python3
if command -v python3 &>/dev/null; then
  ok "python3 $(python3 --version 2>&1 | awk '{print $2}')"
else
  NEED_INSTALL+=("python3")
  warn "python3 未安装，将自动安装"
fi

# gpg
if command -v gpg &>/dev/null; then
  ok "gpg $(gpg --version 2>/dev/null | head -1 | awk '{print $3}')"
else
  NEED_INSTALL+=("gnupg")
  warn "gpg 未安装，将自动安装"
fi

# git (useful but not critical)
if command -v git &>/dev/null; then
  ok "git $(git --version 2>/dev/null | awk '{print $3}')"
else
  NEED_INSTALL+=("git")
  warn "git 未安装，将自动安装"
fi

# Install missing packages
if [ ${#NEED_INSTALL[@]} -gt 0 ]; then
  echo ""
  info "正在安装: ${NEED_INSTALL[*]}"

  # Update package list first (Ubuntu/Debian)
  case "$OS_ID" in
    ubuntu|debian)
      $SUDO apt-get update -qq 2>/dev/null
      ;;
  esac

  for pkg in "${NEED_INSTALL[@]}"; do
    if install_pkg "$pkg"; then
      ok "已安装: $pkg"
    else
      fail "安装失败: $pkg — 请手动安装后重试"
    fi
  done
fi

# Verify critical tools
for cmd in curl python3; do
  if ! command -v "$cmd" &>/dev/null; then
    fail "$cmd 仍未安装，无法继续"
    echo -e "\n  ${RED}❌ 缺少必要工具，请手动安装后重新运行本脚本。${NC}"
    exit 1
  fi
done

# ══════════════════════════════════════════════════════════════
# Step 3: Check & install Node.js
# ══════════════════════════════════════════════════════════════
step "💚" "检查 Node.js..."

NODE_OK=false
if command -v node &>/dev/null; then
  NODE_VER=$(node --version 2>/dev/null)
  NODE_MAJOR=$(echo "$NODE_VER" | sed 's/v//' | cut -d. -f1)
  if [ "$NODE_MAJOR" -ge 18 ] 2>/dev/null; then
    ok "Node.js ${NODE_VER} (满足要求 ≥ v18)"
    NODE_OK=true
  else
    warn "Node.js ${NODE_VER} 版本太低，需要 ≥ v18"
  fi
fi

if [ "$NODE_OK" = false ] && [ "$SKIP_ENV" = false ]; then
  info "正在安装 Node.js v22 (LTS)..."
  
  # Try nvm first (recommended, no sudo needed)
  if [ -s "$HOME/.nvm/nvm.sh" ]; then
    . "$HOME/.nvm/nvm.sh"
    nvm install 22 2>/dev/null && nvm use 22 2>/dev/null
  else
    # Install via NodeSource
    info "使用 NodeSource 安装..."
    if curl -fsSL https://deb.nodesource.com/setup_22.x 2>/dev/null | $SUDO bash - 2>/dev/null; then
      $SUDO apt-get install -y -qq nodejs 2>/dev/null
    else
      # Fallback: direct binary
      info "NodeSource 失败，尝试直接下载..."
      NODE_URL="https://nodejs.org/dist/v22.12.0/node-v22.12.0-linux-${NODE_ARCH}.tar.xz"
      if curl -fsSL "$NODE_URL" -o /tmp/node.tar.xz 2>/dev/null; then
        $SUDO tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 2>/dev/null
        rm -f /tmp/node.tar.xz
      fi
    fi
  fi

  # Verify
  if command -v node &>/dev/null; then
    ok "Node.js $(node --version) 安装成功"
    NODE_OK=true
  else
    fail "Node.js 安装失败"
    echo -e "\n       ${YELLOW}请手动安装:${NC}"
    echo -e "       ${DIM}curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -${NC}"
    echo -e "       ${DIM}sudo apt install -y nodejs${NC}"
    echo -e "       ${DIM}然后重新运行: bash restore-kit.sh --skip-env${NC}"
  fi
elif [ "$SKIP_ENV" = true ] && [ "$NODE_OK" = false ]; then
  warn "跳过 Node.js 安装 (--skip-env)，但 Node.js 未找到"
fi

# ══════════════════════════════════════════════════════════════
# Step 4: Check & install OpenClaw
# ══════════════════════════════════════════════════════════════
step "🤖" "检查 OpenClaw..."

OPENCLAW_OK=false
if command -v openclaw &>/dev/null; then
  OC_VER=$(openclaw --version 2>/dev/null || echo "已安装")
  ok "OpenClaw ${OC_VER}"
  OPENCLAW_OK=true
elif [ "$NODE_OK" = true ] && [ "$SKIP_ENV" = false ]; then
  info "正在安装 OpenClaw..."

  # Try global install
  if npm install -g openclaw 2>/dev/null; then
    ok "OpenClaw 安装成功"
    OPENCLAW_OK=true
  elif npm install -g openclaw-cn 2>/dev/null; then
    ok "OpenClaw (CN) 安装成功"
    OPENCLAW_OK=true
  else
    # Try with user prefix
    info "全局安装失败，尝试用户目录..."
    mkdir -p "$HOME/.npm-global"
    npm config set prefix "$HOME/.npm-global" 2>/dev/null
    export PATH="$HOME/.npm-global/bin:$PATH"
    
    if npm install -g openclaw 2>/dev/null; then
      ok "OpenClaw 安装成功 (用户目录)"
      OPENCLAW_OK=true
    else
      fail "OpenClaw 安装失败"
      echo -e "       ${DIM}稍后手动安装: npm install -g openclaw${NC}"
    fi
  fi
else
  warn "跳过 OpenClaw 安装（Node.js 不可用）"
fi

# ══════════════════════════════════════════════════════════════
# Step 5: Collect WebDAV connection info
# ══════════════════════════════════════════════════════════════
step "📝" "连接你的备份存储..."

# Check for existing config
EXISTING_CONFIG=""
for candidate in \
  "$HOME/.openclaw/workspace/skills/mjolnir-shadow/config/backup-config.json.gpg" \
  "$HOME/.openclaw/workspace/skills/mjolnir-shadow/config/backup-config.json" \
  "$(cd "$(dirname "$0")" 2>/dev/null && pwd)/config/backup-config.json.gpg" \
  "$(cd "$(dirname "$0")" 2>/dev/null && pwd)/config/backup-config.json" \
  ; do
  if [ -f "$candidate" ] 2>/dev/null; then
    EXISTING_CONFIG="$candidate"
    break
  fi
done

CONFIG_LOADED=false

if [ -n "$EXISTING_CONFIG" ]; then
  info "发现已有配置: $(basename "$EXISTING_CONFIG")"
  echo -n "       使用已有配置？[Y/n] "
  read -r USE_EXISTING
  USE_EXISTING="${USE_EXISTING:-Y}"
  
  if [[ "$USE_EXISTING" =~ ^[Yy] ]]; then
    if [[ "$EXISTING_CONFIG" == *.gpg ]]; then
      if [ -n "$MJOLNIR_SHADOW_PASS" ]; then
        CONFIG_JSON=$(gpg --quiet --batch --yes --passphrase "$MJOLNIR_SHADOW_PASS" \
          --decrypt "$EXISTING_CONFIG" 2>/dev/null)
      else
        CONFIG_JSON=$(gpg --quiet --batch --yes --decrypt "$EXISTING_CONFIG" 2>/dev/null)
      fi
    else
      CONFIG_JSON=$(cat "$EXISTING_CONFIG")
    fi
    
    if [ -n "$CONFIG_JSON" ]; then
      WEBDAV_URL=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_url'])")
      WEBDAV_USER=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_user'])")
      WEBDAV_PASS=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_pass'])")
      REMOTE_DIR=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['remote_dir'])")
      ok "配置加载成功"
      CONFIG_LOADED=true
    else
      warn "配置解密失败，将手动输入"
    fi
  fi
fi

if [ "$CONFIG_LOADED" = false ]; then
  echo ""
  echo -e "       ${BOLD}请输入你的备份存储信息:${NC}"
  echo ""
  echo -e "       ${DIM}常见 WebDAV 地址:${NC}"
  echo -e "       ${DIM}  Nextcloud:  http://IP地址/remote.php/webdav${NC}"
  echo -e "       ${DIM}  群晖 NAS:   http://IP地址:5005${NC}"
  echo -e "       ${DIM}  ownCloud:   http://IP地址/remote.php/webdav${NC}"
  echo ""

  echo -n "       WebDAV 地址: "
  read -r WEBDAV_URL
  WEBDAV_URL="${WEBDAV_URL%/}"

  echo -n "       用户名: "
  read -r WEBDAV_USER

  echo -n "       密码: "
  read -rs WEBDAV_PASS
  echo ""

  echo -n "       备份目录名 [openclaw-backups]: "
  read -r REMOTE_DIR
  REMOTE_DIR="${REMOTE_DIR:-openclaw-backups}"

  ok "信息已收集"
fi

FULL_URL="${WEBDAV_URL}/${REMOTE_DIR}"

# ══════════════════════════════════════════════════════════════
# Step 6: Test connection
# ══════════════════════════════════════════════════════════════
step "🌐" "测试连接..."

WEBDAV_HOST=$(echo "$WEBDAV_URL" | grep -oP '://\K[^/]+' || echo "$WEBDAV_URL")
cat > "$NETRC_FILE" << EOF
machine ${WEBDAV_HOST}
login ${WEBDAV_USER}
password ${WEBDAV_PASS}
EOF
chmod 600 "$NETRC_FILE"
CURL_AUTH="--netrc-file ${NETRC_FILE}"

HTTP_CODE=$(curl -s --netrc-file "$NETRC_FILE" -o /dev/null -w "%{http_code}" \
  -X PROPFIND "${FULL_URL}/" -H "Depth: 0" --max-time 15 2>&1 || echo "000")

if [ "$HTTP_CODE" = "207" ] || [ "$HTTP_CODE" = "200" ]; then
  ok "连接成功 (HTTP ${HTTP_CODE})"
elif [ "$HTTP_CODE" = "401" ]; then
  fail "认证失败！用户名或密码错误"
  echo -e "       ${DIM}请检查后重新运行本脚本${NC}"
  exit 1
elif [ "$HTTP_CODE" = "000" ]; then
  fail "无法连接！请检查地址和网络"
  echo -e "       ${DIM}地址: ${WEBDAV_URL}${NC}"
  echo -e "       ${DIM}确认目标机器已开机且网络可达${NC}"
  exit 1
else
  warn "收到 HTTP ${HTTP_CODE}，尝试继续..."
fi

# ══════════════════════════════════════════════════════════════
# Step 7: Find latest backup
# ══════════════════════════════════════════════════════════════
step "🔍" "查找备份..."

BACKUP_LIST=$(curl -s $CURL_AUTH \
  -X PROPFIND "${FULL_URL}/" -H "Depth: 1" --max-time 15 2>/dev/null \
  | grep -oP 'backup_[^<]+\.tar\.gz' | sort -u || true)

BACKUP_COUNT=$(echo "$BACKUP_LIST" | grep -c "backup_" 2>/dev/null || echo "0")

if [ "$BACKUP_COUNT" -eq 0 ] || [ -z "$BACKUP_LIST" ]; then
  fail "未找到任何备份！"
  echo -e "       ${DIM}目录 ${REMOTE_DIR} 中没有备份文件${NC}"
  echo -e "       ${DIM}请确认备份目录名称正确${NC}"
  exit 1
fi

LATEST=$(echo "$BACKUP_LIST" | tail -1)
BACKUP_DATE=$(echo "$LATEST" | grep -oP '\d{4}-\d{2}-\d{2}' | head -1)

info "找到 ${BACKUP_COUNT} 个备份:"
echo "$BACKUP_LIST" | while read -r name; do
  if [ "$name" = "$LATEST" ]; then
    echo -e "       ${GREEN}→ ${name} (最新，将恢复这个)${NC}"
  else
    echo -e "       ${DIM}  ${name}${NC}"
  fi
done

# ══════════════════════════════════════════════════════════════
# Step 8: Download
# ══════════════════════════════════════════════════════════════
step "📥" "下载备份包..."

SIZE_BYTES=$(curl -sI $CURL_AUTH "${FULL_URL}/${LATEST}" 2>/dev/null \
  | grep -i content-length | tail -1 | tr -dc '0-9')

if [ -n "$SIZE_BYTES" ] && [ "$SIZE_BYTES" -gt 0 ] 2>/dev/null; then
  SIZE_MB=$(python3 -c "print(f'{${SIZE_BYTES}/1048576:.1f}')" 2>/dev/null || echo "?")
  info "文件大小: ${SIZE_MB} MB"
fi

curl --progress-bar $CURL_AUTH \
  -o "$DL_FILE" \
  "${FULL_URL}/${LATEST}" 2>&1

ACTUAL_SIZE=$(du -h "$DL_FILE" 2>/dev/null | cut -f1)
ok "下载完成 (${ACTUAL_SIZE})"

# ══════════════════════════════════════════════════════════════
# Step 9: Extract & restore
# ══════════════════════════════════════════════════════════════
step "🔄" "恢复数据..."

WORKSPACE_PATH="$HOME/.openclaw/workspace"
OPENCLAW_DIR="$HOME/.openclaw"
RESTORED=0

mkdir -p "$RESTORE_DIR"
tar xzf "$DL_FILE" -C "$RESTORE_DIR"
rm -f "$DL_FILE"

# Workspace
if [ -f "$RESTORE_DIR/workspace.tar.gz" ]; then
  mkdir -p "$WORKSPACE_PATH"
  tar xzf "$RESTORE_DIR/workspace.tar.gz" -C "$WORKSPACE_PATH" 2>/dev/null
  ok "记忆 & 文档 → ${WORKSPACE_PATH}"
  RESTORED=$((RESTORED + 1))
fi

# Config
if [ -f "$RESTORE_DIR/config.tar.gz" ]; then
  mkdir -p "$OPENCLAW_DIR"
  tar xzf "$RESTORE_DIR/config.tar.gz" -C "$OPENCLAW_DIR" 2>/dev/null
  ok "配置 & 定时任务 → ${OPENCLAW_DIR}"
  RESTORED=$((RESTORED + 1))
fi

# Strategies
if [ -f "$RESTORE_DIR/strategies.tar.gz" ]; then
  local_strat="${OPENCLAW_DIR}/workspace/projects/auto_trading"
  mkdir -p "$local_strat"
  tar xzf "$RESTORE_DIR/strategies.tar.gz" -C "$local_strat" 2>/dev/null
  ok "交易策略 → ${local_strat}"
  RESTORED=$((RESTORED + 1))
fi

# Skills
if [ -f "$RESTORE_DIR/skills.tar.gz" ]; then
  mkdir -p "${OPENCLAW_DIR}/workspace"
  tar xzf "$RESTORE_DIR/skills.tar.gz" -C "${OPENCLAW_DIR}/workspace" 2>/dev/null
  ok "技能包 → ${OPENCLAW_DIR}/workspace/skills/"
  RESTORED=$((RESTORED + 1))
fi

ok "共恢复 ${RESTORED} 个组件"

# Cleanup temp
rm -rf "$RESTORE_DIR"

# ══════════════════════════════════════════════════════════════
# Step 10: Start OpenClaw
# ══════════════════════════════════════════════════════════════
step "🚀" "启动智能体..."

if [ "$OPENCLAW_OK" = true ] || command -v openclaw &>/dev/null; then
  if openclaw gateway start 2>/dev/null; then
    ok "OpenClaw 已启动！"
  elif openclaw gateway restart 2>/dev/null; then
    ok "OpenClaw 已重启！"
  else
    warn "自动启动失败，请手动运行: openclaw gateway start"
  fi
else
  warn "OpenClaw 未安装，跳过启动"
fi

# ══════════════════════════════════════════════════════════════
# Final Summary
# ══════════════════════════════════════════════════════════════
echo ""
progress_bar 100
echo ""
echo ""

# Check for errors
if [ ${#ERRORS[@]} -gt 0 ]; then
  echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
  echo -e "${BOLD}   ⚠️  恢复完成（有部分警告）${NC}"
  echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
  echo ""
  echo -e "  ${YELLOW}需要注意:${NC}"
  for err in "${ERRORS[@]}"; do
    echo -e "  ${YELLOW}  • ${err}${NC}"
  done
  echo ""
else
  echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
  echo -e "${BOLD}   ✅ 完美！从裸机到满血，一步到位！${NC}"
  echo -e "${BOLD}🌑 ════════════════════════════════════════════════════════${NC}"
  echo ""
fi

echo -e "  📦 备份来源:  ${CYAN}${LATEST}${NC}"
echo -e "  📅 备份日期:  ${BOLD}${BACKUP_DATE}${NC}"
echo -e "  🔄 恢复组件:  ${GREEN}${RESTORED} 个${NC}"
echo -e "  📁 工作空间:  ${WORKSPACE_PATH}"
echo ""

# Environment summary
echo -e "  ${BOLD}环境状态:${NC}"
if command -v node &>/dev/null; then
  echo -e "  ${GREEN}  ✓ Node.js $(node --version)${NC}"
else
  echo -e "  ${RED}  ✗ Node.js 未安装${NC}"
fi
if command -v openclaw &>/dev/null; then
  echo -e "  ${GREEN}  ✓ OpenClaw 已安装${NC}"
else
  echo -e "  ${RED}  ✗ OpenClaw 未安装${NC}"
fi
echo -e "  ${GREEN}  ✓ 记忆数据 已恢复${NC}"
echo -e "  ${GREEN}  ✓ 技能包   已恢复${NC}"
echo ""

# Next steps if anything is missing
if ! command -v openclaw &>/dev/null; then
  echo -e "  ${YELLOW}📌 还需要:${NC}"
  if ! command -v node &>/dev/null; then
    echo -e "     1. 安装 Node.js:  ${CYAN}curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash - && sudo apt install -y nodejs${NC}"
    echo -e "     2. 安装 OpenClaw: ${CYAN}npm install -g openclaw${NC}"
    echo -e "     3. 启动服务:     ${CYAN}openclaw gateway start${NC}"
  else
    echo -e "     1. 安装 OpenClaw: ${CYAN}npm install -g openclaw${NC}"
    echo -e "     2. 启动服务:     ${CYAN}openclaw gateway start${NC}"
  fi
  echo ""
fi

echo -e "  ${GREEN}🧠 智能体下次醒来就有完整记忆了！${NC}"
echo ""
echo -e "  ${DIM}临时文件已清理，凭证未留在磁盘上。${NC}"
echo -e "  ${DIM}有问题？GitHub: https://github.com/king6381/mjolnir-shadow${NC}"
echo ""


path: scripts/restore.sh
content: #!/bin/bash
# 🌑 Mjolnir Shadow (雷神之影) - Restore Tool / 恢复工具
# Security: uses --netrc-file, never exposes credentials in process listings
# v2.0 — Added --auto mode with progress bar for one-click restore

set -e

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_FILE="${SKILL_DIR}/config/backup-config.json"
ENCRYPTED_CONFIG="${SKILL_DIR}/config/backup-config.json.gpg"
NETRC_FILE="/tmp/.mjolnir_shadow_netrc_$$"

# ── Colors & Symbols ─────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

STEP_CURRENT=0
STEP_TOTAL=6

cleanup() {
  rm -f "$NETRC_FILE" 2>/dev/null
}
trap cleanup EXIT

# ── Progress Bar ─────────────────────────────────────────────
progress_bar() {
  local percent=$1
  local width=30
  local filled=$((percent * width / 100))
  local empty=$((width - filled))
  local bar=""
  for ((i=0; i<filled; i++)); do bar+="█"; done
  for ((i=0; i<empty; i++)); do bar+="░"; done
  printf "\r  ${CYAN}[${bar}]${NC} ${BOLD}%3d%%${NC}" "$percent"
}

step() {
  STEP_CURRENT=$((STEP_CURRENT + 1))
  local percent=$((STEP_CURRENT * 100 / STEP_TOTAL))
  local icon="$1"
  local msg="$2"
  echo ""
  progress_bar "$percent"
  echo "  ${icon} ${msg}"
}

step_done() {
  echo "       ${GREEN}✓ $1${NC}"
}

step_fail() {
  echo "       ${RED}✗ $1${NC}"
}

banner() {
  echo ""
  echo -e "${BOLD}🌑 ═══════════════════════════════════════════${NC}"
  echo -e "${BOLD}   雷神之影 — $1${NC}"
  echo -e "${BOLD}🌑 ═══════════════════════════════════════════${NC}"
}

# ── Load Config ──────────────────────────────────────────────
decrypt_config() {
  if [ -f "$ENCRYPTED_CONFIG" ]; then
    if [ -n "$MJOLNIR_SHADOW_PASS" ]; then
      gpg --quiet --batch --yes --passphrase "$MJOLNIR_SHADOW_PASS" \
        --decrypt "$ENCRYPTED_CONFIG" 2>/dev/null
    else
      gpg --quiet --batch --yes --decrypt "$ENCRYPTED_CONFIG" 2>/dev/null
    fi
  elif [ -f "$CONFIG_FILE" ]; then
    echo "⚠️  Warning: Using unencrypted config. / 警告：使用未加密配置。" >&2
    cat "$CONFIG_FILE"
  else
    echo "❌ No config found. Run: python3 ${SCRIPT_DIR}/setup_backup.py" >&2
    exit 1
  fi
}

load_config() {
  CONFIG_JSON=$(decrypt_config)
  if [ -z "$CONFIG_JSON" ]; then
    echo -e "${RED}❌ Failed to load config. Check GPG passphrase.${NC}"
    echo -e "${RED}   配置加载失败，检查密码。${NC}"
    exit 1
  fi

  WEBDAV_URL=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_url'])")
  WEBDAV_USER=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_user'])")
  WEBDAV_PASS=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['webdav_pass'])")
  REMOTE_DIR=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; print(json.load(sys.stdin)['remote_dir'])")
  WORKSPACE_PATH=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; c=json.load(sys.stdin); print(c.get('workspace_path','$HOME/.openclaw/workspace'))")
  OPENCLAW_DIR=$(echo "$CONFIG_JSON" | python3 -c "import json,sys; c=json.load(sys.stdin); print(c.get('openclaw_dir','$HOME/.openclaw'))")
  FULL_URL="${WEBDAV_URL}/${REMOTE_DIR}"

  # Security: netrc file
  WEBDAV_HOST=$(echo "$WEBDAV_URL" | grep -oP '://\K[^/]+')
  cat > "$NETRC_FILE" << EOF
machine ${WEBDAV_HOST}
login ${WEBDAV_USER}
password ${WEBDAV_PASS}
EOF
  chmod 600 "$NETRC_FILE"
  CURL_AUTH="--netrc-file ${NETRC_FILE}"
}

# ── Download with progress ───────────────────────────────────
download_with_progress() {
  local url="$1"
  local output="$2"
  local tmpfile="${output}.tmp"

  # Get file size first
  local size_bytes
  size_bytes=$(curl -sI $CURL_AUTH "$url" 2>/dev/null | grep -i content-length | tail -1 | tr -dc '0-9')

  if [ -n "$size_bytes" ] && [ "$size_bytes" -gt 0 ] 2>/dev/null; then
    local size_mb=$(echo "scale=1; $size_bytes / 1048576" | bc 2>/dev/null || echo "?")
    echo -e "       ${DIM}文件大小: ${size_mb}MB${NC}"
    # Download with curl progress bar
    curl -# $CURL_AUTH -o "$tmpfile" "$url" 2>&1 | while IFS= read -r -d $'\r' line; do
      # curl -# outputs progress like "###   5.0%"
      local pct
      pct=$(echo "$line" | grep -oP '[\d.]+%' | head -1 | cut -d. -f1)
      if [ -n "$pct" ]; then
        progress_bar "$pct"
        printf "  📥 下载中..."
      fi
    done
    echo ""
  else
    # Fallback: no size info, just download
    curl -s $CURL_AUTH -o "$tmpfile" "$url"
  fi

  mv "$tmpfile" "$output"
}

# ── Find latest backup ───────────────────────────────────────
find_latest_backup() {
  curl -s $CURL_AUTH \
    -X PROPFIND "${FULL_URL}/" -H "Depth: 1" 2>/dev/null \
    | grep -oP 'backup_[^<]+\.tar\.gz' | sort -u | tail -1
}

# ── List backups ─────────────────────────────────────────────
list_backups() {
  banner "可用备份列表"
  echo ""
  local count=0
  curl -s $CURL_AUTH \
    -X PROPFIND "${FULL_URL}/" -H "Depth: 1" 2>/dev/null \
    | grep -oP 'backup_[^<]+\.tar\.gz' | sort -u | while read -r name; do
      count=$((count + 1))
      # Extract date from filename
      local date_part
      date_part=$(echo "$name" | grep -oP '\d{4}-\d{2}-\d{2}_\d{4}' | head -1)
      echo -e "  ${CYAN}${count}.${NC} 📦 ${name}  ${DIM}(${date_part})${NC}"
    done
  echo ""
}

# ── Manual restore (original behavior) ───────────────────────
restore_manual() {
  local BACKUP_FILE="$1"
  local RESTORE_DIR="/tmp/mjolnir_shadow_restore_$$"

  echo "🌑 Restoring / 正在恢复: ${BACKUP_FILE}"
  echo "=========================================="

  mkdir -p "$RESTORE_DIR"

  echo "📥 Downloading... / 下载中..."
  curl -s $CURL_AUTH \
    -o "/tmp/${BACKUP_FILE}" \
    "${FULL_URL}/${BACKUP_FILE}"

  echo "📦 Extracting... / 解压中..."
  tar xzf "/tmp/${BACKUP_FILE}" -C "$RESTORE_DIR"

  echo ""
  echo "📋 Contents / 内容:"
  ls -lh "$RESTORE_DIR/"
  echo ""
  echo "⚠️  Files extracted to / 文件已解压到: ${RESTORE_DIR}"
  echo "    Review before overwriting! / 覆盖前请检查！"
  echo ""
  echo "    Restore workspace / 恢复工作空间:"
  echo "      tar xzf ${RESTORE_DIR}/workspace.tar.gz -C ~/.openclaw/workspace/"
  echo ""
  echo "    Restore config / 恢复配置:"
  echo "      tar xzf ${RESTORE_DIR}/config.tar.gz -C ~/.openclaw/"
  echo ""

  rm -f "/tmp/${BACKUP_FILE}"
  echo "✅ Ready at / 就绪: ${RESTORE_DIR}"
}

# ══════════════════════════════════════════════════════════════
# ── AUTO MODE — One-click full restore with progress bar ─────
# ══════════════════════════════════════════════════════════════
restore_auto() {
  banner "一键全自动恢复"
  echo ""
  echo -e "  ${DIM}所有步骤将自动完成，请稍候...${NC}"

  local RESTORE_DIR="/tmp/mjolnir_shadow_restore_$$"
  local DL_FILE="/tmp/mjolnir_shadow_dl_$$.tar.gz"
  mkdir -p "$RESTORE_DIR"

  # ── Step 1: Load config ──
  step "🔐" "解密配置文件..."
  load_config
  step_done "配置加载成功"

  # ── Step 2: Find latest backup ──
  step "🔍" "查找最新备份..."
  local LATEST
  LATEST=$(find_latest_backup)
  if [ -z "$LATEST" ]; then
    step_fail "未找到任何备份！"
    echo -e "\n  ${RED}❌ 驿站上没有备份文件，请先运行 backup.sh 创建备份。${NC}"
    exit 1
  fi
  local backup_date
  backup_date=$(echo "$LATEST" | grep -oP '\d{4}-\d{2}-\d{2}' | head -1)
  step_done "找到: ${LATEST} (${backup_date})"

  # ── Step 3: Download ──
  step "📥" "下载备份包..."
  download_with_progress "${FULL_URL}/${LATEST}" "$DL_FILE"
  local filesize
  filesize=$(du -h "$DL_FILE" 2>/dev/null | cut -f1)
  step_done "下载完成 (${filesize})"

  # ── Step 4: Extract ──
  step "📦" "解压备份包..."
  tar xzf "$DL_FILE" -C "$RESTORE_DIR"
  rm -f "$DL_FILE"
  local contents
  contents=$(ls "$RESTORE_DIR/" | tr '\n' ', ' | sed 's/,$//')
  step_done "解压完成: ${contents}"

  # ── Step 5: Restore all components ──
  step "🔄" "恢复文件到系统..."

  local restored=0

  # Workspace
  if [ -f "$RESTORE_DIR/workspace.tar.gz" ]; then
    mkdir -p "$WORKSPACE_PATH"
    tar xzf "$RESTORE_DIR/workspace.tar.gz" -C "$WORKSPACE_PATH" 2>/dev/null
    echo -e "       ${GREEN}✓${NC} workspace → ${WORKSPACE_PATH}"
    restored=$((restored + 1))
  fi

  # Config
  if [ -f "$RESTORE_DIR/config.tar.gz" ]; then
    mkdir -p "$OPENCLAW_DIR"
    tar xzf "$RESTORE_DIR/config.tar.gz" -C "$OPENCLAW_DIR" 2>/dev/null
    echo -e "       ${GREEN}✓${NC} config → ${OPENCLAW_DIR}"
    restored=$((restored + 1))
  fi

  # Strategies
  if [ -f "$RESTORE_DIR/strategies.tar.gz" ]; then
    local strat_dir="${OPENCLAW_DIR}/workspace/projects/auto_trading"
    mkdir -p "$strat_dir"
    tar xzf "$RESTORE_DIR/strategies.tar.gz" -C "$strat_dir" 2>/dev/null
    echo -e "       ${GREEN}✓${NC} strategies → ${strat_dir}"
    restored=$((restored + 1))
  fi

  # Skills
  if [ -f "$RESTORE_DIR/skills.tar.gz" ]; then
    local skills_dir="${OPENCLAW_DIR}/workspace/skills"
    mkdir -p "$skills_dir"
    tar xzf "$RESTORE_DIR/skills.tar.gz" -C "${OPENCLAW_DIR}/workspace" 2>/dev/null
    echo -e "       ${GREEN}✓${NC} skills → ${skills_dir}"
    restored=$((restored + 1))
  fi

  step_done "共恢复 ${restored} 个组件"

  # ── Step 6: Restart OpenClaw ──
  step "🚀" "重启 OpenClaw..."
  if command -v openclaw &>/dev/null; then
    openclaw gateway restart 2>/dev/null && \
      step_done "OpenClaw 已重启" || \
      step_done "OpenClaw 重启需手动执行"
  else
    echo -e "       ${YELLOW}⚠ OpenClaw 未安装，跳过重启${NC}"
    echo -e "       ${DIM}  安装: npm install -g openclaw${NC}"
  fi

  # ── Cleanup ──
  rm -rf "$RESTORE_DIR"

  # ── Summary ──
  echo ""
  progress_bar 100
  echo ""
  echo ""
  echo -e "${BOLD}🌑 ═══════════════════════════════════════════${NC}"
  echo -e "${BOLD}   ✅ 恢复完成！${NC}"
  echo -e "${BOLD}🌑 ═══════════════════════════════════════════${NC}"
  echo ""
  echo -e "  📦 备份文件: ${CYAN}${LATEST}${NC}"
  echo -e "  📅 备份日期: ${backup_date}"
  echo -e "  🔄 恢复组件: ${restored} 个"
  echo -e "  📁 工作空间: ${WORKSPACE_PATH}"
  echo ""
  echo -e "  ${DIM}智能体下次醒来就有完整记忆了 🧠${NC}"
  echo ""
}

# ══════════════════════════════════════════════════════════════
# ── Main ─────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════
ACTION="${1:---help}"

case "$ACTION" in
  --auto|-a)
    restore_auto
    ;;
  --list|-l)
    load_config
    list_backups
    ;;
  --latest)
    load_config
    LATEST=$(find_latest_backup)
    if [ -z "$LATEST" ]; then
      echo "❌ No backups found / 未找到备份"
      exit 1
    fi
    restore_manual "$LATEST"
    ;;
  --file|-f)
    load_config
    if [ -z "$2" ]; then
      echo "Usage: restore.sh --file <name>"
      exit 1
    fi
    restore_manual "$2"
    ;;
  *)
    echo ""
    echo -e "${BOLD}🌑 雷神之影 — 恢复工具${NC}"
    echo ""
    echo "用法 / Usage:"
    echo ""
    echo -e "  ${CYAN}--auto, -a${NC}     一键全自动恢复（推荐！带进度条）"
    echo -e "  ${CYAN}--list, -l${NC}     列出所有可用备份"
    echo -e "  ${CYAN}--latest${NC}       恢复最新备份（手动模式）"
    echo -e "  ${CYAN}--file, -f${NC}     恢复指定备份文件"
    echo ""
    echo "示例 / Examples:"
    echo ""
    echo "  bash restore.sh --auto              # 一键恢复，小白推荐"
    echo "  bash restore.sh --list              # 看看有哪些备份"
    echo "  bash restore.sh --file backup_xxx   # 恢复指定版本"
    echo ""
    exit 0
    ;;
esac


path: scripts/setup_backup.py
content: #!/usr/bin/env python3
"""Mjolnir Shadow (雷神之影) - Interactive Setup Wizard / 交互式配置向导"""

import json
import os
import subprocess
import sys
from pathlib import Path

def ask(prompt, default=None):
    suffix = f" [{default}]" if default else ""
    val = input(f"{prompt}{suffix}: ").strip()
    return val if val else default

def encrypt_config(config_file: Path, encrypted_file: Path):
    """Encrypt config with GPG / 使用 GPG 加密配置"""
    passphrase = ask("Set encryption passphrase for config / 设置配置加密密码")
    if not passphrase:
        print("⚠️  No passphrase set. Config stored unencrypted. / 未设密码，配置未加密。")
        return False
    
    result = subprocess.run(
        ["gpg", "--quiet", "--batch", "--yes", "--symmetric",
         "--cipher-algo", "AES256",
         "--passphrase", passphrase,
         "--output", str(encrypted_file),
         str(config_file)],
        capture_output=True, text=True
    )
    
    if result.returncode == 0:
        # Remove plaintext config
        config_file.unlink()
        print(f"🔒 Config encrypted: {encrypted_file}")
        print(f"   For non-interactive use, set MJOLNIR_SHADOW_PASS via gpg-agent or a secure secret store.")
        print(f"   ⚠️  Do NOT put the passphrase in cron commands or world-readable files.")
        print(f"   非交互运行时，通过 gpg-agent 或安全密钥存储设置 MJOLNIR_SHADOW_PASS。")
        print(f"   ⚠️  不要在 cron 命令或明文文件中写入密码。")
        return True
    else:
        print(f"⚠️  GPG encryption failed: {result.stderr}")
        return False

def main():
    print("🌑 Mjolnir Shadow (雷神之影) - Setup Wizard / 配置向导")
    print("=" * 50)
    print()

    skill_dir = Path(__file__).parent.parent
    config_dir = skill_dir / "config"
    config_dir.mkdir(exist_ok=True)
    config_file = config_dir / "backup-config.json"
    encrypted_file = config_dir / "backup-config.json.gpg"

    # Load existing config if present / 加载已有配置
    existing = {}
    if encrypted_file.exists():
        print("🔒 Encrypted config found. Decrypting... / 发现加密配置，正在解密...")
        passphrase = ask("Enter passphrase / 输入密码")
        result = subprocess.run(
            ["gpg", "--quiet", "--batch", "--yes",
             "--passphrase", passphrase,
             "--decrypt", str(encrypted_file)],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            existing = json.loads(result.stdout)
            print("✅ Decrypted. Press Enter to keep current values. / 解密成功，回车保留当前值。\n")
        else:
            print("⚠️  Decryption failed. Starting fresh. / 解密失败，重新配置。\n")
    elif config_file.exists():
        existing = json.loads(config_file.read_text())
        print(f"📋 Existing config found. Press Enter to keep current values. / 发现已有配置。\n")

    # WebDAV Connection / WebDAV 连接
    print("── WebDAV Connection / WebDAV 连接 ──")
    webdav_url = ask("WebDAV URL (e.g. https://your-server/remote.php/webdav)",
                     existing.get("webdav_url", ""))
    
    # HTTPS warning / HTTPS 安全警告
    if webdav_url and not webdav_url.startswith("https://"):
        print("⚠️  WARNING: URL is not HTTPS! Credentials will be sent in plaintext.")
        print("   警告：URL 不是 HTTPS！凭证将以明文传输。")
        confirm = ask("Continue anyway? / 仍然继续？ (y/n)", "n")
        if confirm.lower() != "y":
            print("Aborted. Use HTTPS URL. / 已中止，请使用 HTTPS 地址。")
            sys.exit(1)
    
    webdav_user = ask("Username / 用户名", existing.get("webdav_user", ""))
    webdav_pass = ask("Password / 密码", existing.get("webdav_pass", ""))
    remote_dir = ask("Remote folder name / 远程文件夹名", existing.get("remote_dir", "openclaw-backups"))

    # Backup Settings / 备份设置
    print("\n── Backup Settings / 备份设置 ──")
    max_backups = int(ask("Max backups to keep / 最多保留几个备份", str(existing.get("max_backups", 3))))
    interval_days = int(ask("Backup interval (days) / 备份间隔（天）", str(existing.get("interval_days", 3))))

    # Paths / 路径
    print("\n── Paths / 路径 ──")
    workspace_path = ask("Workspace path / 工作空间路径",
                         existing.get("workspace_path", os.path.expanduser("~/.openclaw/workspace")))
    openclaw_dir = ask("OpenClaw dir / OpenClaw 目录",
                       existing.get("openclaw_dir", os.path.expanduser("~/.openclaw")))

    # What to backup / 备份内容
    print("\n── What to Back Up / 备份内容 ──")
    backup_workspace = ask("Back up workspace? / 备份工作空间？ (y/n)", "y").lower() == "y"
    backup_config = ask("Back up OpenClaw config? / 备份 OpenClaw 配置？ (y/n)", "y").lower() == "y"
    backup_strategies = ask("Back up strategies? / 备份策略文件？ (y/n)", "y").lower() == "y"
    backup_skills = ask("Back up skills? / 备份技能包？ (y/n)", "y").lower() == "y"
    
    # Security / 安全
    print("\n── Security / 安全设置 ──")
    exclude_auth = ask("Exclude channel auth tokens from backup? / 排除通道认证 token？ (y/n)", "y").lower() == "y"

    config = {
        "webdav_url": webdav_url.rstrip("/"),
        "webdav_user": webdav_user,
        "webdav_pass": webdav_pass,
        "remote_dir": remote_dir,
        "max_backups": max_backups,
        "interval_days": interval_days,
        "workspace_path": workspace_path,
        "openclaw_dir": openclaw_dir,
        "backup_workspace": backup_workspace,
        "backup_config": backup_config,
        "backup_strategies": backup_strategies,
        "backup_skills": backup_skills,
        "exclude_channel_auth": exclude_auth,
    }

    # Save config / 保存配置
    config_file.write_text(json.dumps(config, indent=2, ensure_ascii=False))
    print(f"\n💾 Config saved to: {config_file}")

    # Test connection via netrc (no -u flag) / 通过 netrc 测试连接（不暴露密码）
    print("\n🔌 Testing WebDAV connection... / 测试 WebDAV 连接...")
    try:
        import tempfile, urllib.parse
        host = urllib.parse.urlparse(webdav_url).hostname
        with tempfile.NamedTemporaryFile(mode='w', suffix='_netrc', delete=False) as nf:
            nf.write(f"machine {host}\nlogin {webdav_user}\npassword {webdav_pass}\n")
            netrc_path = nf.name
        os.chmod(netrc_path, 0o600)
        result = subprocess.run(
            ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
             "--netrc-file", netrc_path,
             "-X", "PROPFIND", f"{webdav_url}/", "-H", "Depth: 0"],
            capture_output=True, text=True, timeout=10
        )
        os.unlink(netrc_path)
        code = result.stdout.strip()
        if code in ("207", "200"):
            print(f"✅ WebDAV OK (HTTP {code})")
        else:
            print(f"⚠️  WebDAV returned HTTP {code} — check URL and credentials")
            print(f"   检查 URL 和凭证是否正确")
    except Exception as e:
        print(f"⚠️  Connection test failed: {e}")
        try: os.unlink(netrc_path)
        except: pass

    # Encrypt config / 加密配置
    print("\n── Credential Security / 凭证安全 ──")
    do_encrypt = ask("Encrypt config with GPG? (recommended) / 使用 GPG 加密配置？（推荐） (y/n)", "y").lower() == "y"
    if do_encrypt:
        # Check GPG availability
        gpg_check = subprocess.run(["gpg", "--version"], capture_output=True)
        if gpg_check.returncode == 0:
            encrypt_config(config_file, encrypted_file)
        else:
            print("⚠️  GPG not found. Install gnupg to encrypt config. / 未找到 GPG，请安装 gnupg。")
            print("   Config remains unencrypted. / 配置保持未加密状态。")

    # Cron info / 定时任务信息
    print(f"\n── Cron Schedule / 定时任务 ──")
    cron_expr = f"0 3 */{interval_days} * *"
    print(f"📅 Recommended: {cron_expr} (every {interval_days} days at 03:00)")
    print(f"   建议：{cron_expr}（每 {interval_days} 天凌晨 3 点）")
    print(f"\n   Add via OpenClaw / 通过 OpenClaw 添加：")
    print(f'   openclaw cron add --name "🌑 Mjolnir Shadow" \\')
    print(f'     --schedule "{cron_expr}" --tz "Asia/Shanghai" \\')
    print(f'     --isolated --timeout 300 \\')
    print(f'     --message "bash {skill_dir}/scripts/backup.sh"')

    print(f"\n🌑 Setup complete! Run first backup / 配置完成！运行首次备份：")
    print(f"   bash {skill_dir}/scripts/backup.sh")

if __name__ == "__main__":
    main()


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