Skill: hjj345/auto-backup-openclaw-user-data (version 1.1.0)

--- name: auto-backup-openclaw-user-data description: | OpenClaw 用户数据自动备份技能。支持全量/选择性备份、定时执行、ZIP 压缩、日志记录、消息通知和保留策略管理。 **触发场景**： (1) 用户要求备份 OpenClaw 数据 (2) 用户要求设置定时备份 (3) 用户询问备份配置、状态、日志 (4) 用户执行 /backup_now、/backup_status、/backup_config、/backup_list、/backup_clean 等命令 (5) HEARTBEAT 触发定时备份检查 --- # Auto Backup OpenClaw User Data OpenClaw 用户数据自动备份技能。 ## 版本 - **当前版本**：v1.1.0.20260414 - **更新日期**：2026-04-14 ## 功能 - **自动备份**：定时备份 `.openclaw` 目录 - **选择性备份**：支持全量或部分备份（交互式选择文件/文件夹） - **ZIP 压缩**：自动压缩并按规则命名 - **定时执行**：支持 HEARTBEAT 心跳和 Cron 定时任务两种方式 - **日志记录**：完整记录执行过程 - **消息通知**：支持多渠道推送结果（需配置推送目标） - **保留策略**：自动清理旧备份 ## v1.1.0 新特性 ### 工作空间自动检测 首次配置时自动检测所有workspace目录和memory目录，无需手动配置备份目标。适配多Agent工作环境，避免遗漏备份。 ### 敏感文件安全提醒 新增敏感文件识别和提醒机制（密钥、凭证、环境变量等），默认不强制排除，遵循"只做提醒，不做限制"原则，让用户自主决定备份范围。 ### 标准化入口改进 创建标准Node.js模块入口（index.js），完整导出所有接口，消除"可疑技能"标记，符合OpenClaw安全最佳实践。 ### 配置自动迁移 支持从v1.0.2自动升级到v1.1.0，自动补全缺失配置字段，保留原有配置，零停机平滑升级。 --- ## 工作空间自动检测 首次配置时，系统会自动检测您的OpenClaw工作空间： **检测内容**： - 所有 `workspace-*` 目录 - `memory` 目录 - 写入配置文件作为默认值 **建议**：首次使用交互式配置确认检测结果。 详见：[README.md工作空间检测章节](README.md#工作空间自动检测) --- ## ⚠️ 安全警告 ### 敏感文件风险 备份可能包含敏感文件（密钥、环境变量、凭证等）。 **默认行为**：不强制排除，仅排除临时文件。 **如何启用**：交互式配置Step 7或手动编辑配置文件。 详见：[README.md安全警告章节](README.md#安全警告) ## 命令 | 命令 | 功能 | |------|------| | `/backup_now` | 立即执行备份 | | `/backup_status` | 查看备份状态 | | `/backup_config` | 配置向导 | | `/backup_list` | 列出备份文件 | | `/backup_clean` | 清理旧备份 | ## 定时任务配置 支持两种定时执行方式： 1. **HEARTBEAT 心跳**：适用于周期性监控检查，详见 `HEARTBEAT_prompt_example.md` 2. **Cron 定时任务**：适用于精确时间执行，详见 `cron_prompt_example.md` ## 配置 配置文件位置：`~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/config.json` ### 消息通知配置 ```json { "notification": { "enabled": true, "channels": ["feishu", "telegram"], "targets": { "feishu": [ { "type": "group", "id": "oc_xxx", "name": "开发群" }, { "type": "user", "id": "ou_xxx", "name": "用户名" } ], "telegram": [ { "type": "group", "id": "-100xxx", "name": "通知群" } ] }, "onSuccess": true, "onFailure": true } } ``` **注意**：消息通知需要在 OpenClaw 中先配置对应的通信渠道，详见 `references/config-schema.md`。 详细配置说明：见 [references/config-schema.md](references/config-schema.md) ## 故障排查 常见问题：见 [references/troubleshooting.md](references/troubleshooting.md) ## 升级指南 ### 从 v1.0.2 升级到 v1.1.0 **推荐升级方式**：卸载旧版本后重新安装 ```bash # 卸载旧版本 openclaw skill uninstall auto-backup-openclaw-user-data # 安装新版本 openclaw skill install auto-backup-openclaw-user-data ``` **原因**：获得完整的v1.1.0默认配置，包括工作空间自动检测、敏感文件建议列表、encryption配置字段。 ### 配置自动迁移 保留现有配置的用户，系统会自动补全v1.1.0新增字段，无需手动修改配置。 ### 升级后重要变更 1. **backup.mode默认值**：从"full"（全量备份）改为"partial"（选择性备份） 2. **工作空间检测**：从硬编码改为动态自动检测 3. **敏感文件提醒**：新增建议排除列表（默认不启用） **建议**：升级后运行 `/backup_config` 重新确认备份目标，并根据安全需求决定是否启用敏感文件排除。 ### 兼容性 - ✅ 配置兼容：v1.0.2配置自动补全新字段 - ✅ 备份兼容：备份文件格式完全兼容 - ✅ 命令兼容：所有 `/backup_*` 命令保持不变 - ✅ 定时兼容：HEARTBEAT和Cron配置无需修改 详细升级说明：见 [README.md升级指南](README.md#升级指南) ## 更新日志 ### v1.1.0.20260414 (2026-04-14) - 安全优化版本 **脚本入口改进**： - 创建标准`index.js`入口文件，完整导出所有接口 - 导出`runCommand`函数，确保命令正常调用 - 消除"可疑技能"标记 **工作空间动态检测**： - 新增`detectWorkspaces()`函数，首次配置自动检测workspace - 修复硬编码targets问题 **敏感文件处理**： - 添加敏感文件建议列表配置（默认不启用） - 遵循"只做提醒，不做限制"原则 **文档完善**： - 添加工作空间动态检测和安全警告说明 ### v1.0.2.20260331 (2026-03-31) - 新增：HEARTBEAT 心跳定时任务模板（`HEARTBEAT_prompt_example.md`） - 新增：Cron 定时任务模板（`cron_prompt_example.md`） - 新增：选择性备份交互式文件选择功能 - 新增：文件选择确认/重新选择功能 - 优化：交互式配置步骤从 6 步调整为 7 步 - 优化：选择性备份时列出 `~/.openclaw/` 目录文件清单 - 文档：README.md 和 USAGE.md 新增定时任务配置说明 ### v1.0.1.20260326 (2026-03-26) - 新增：消息推送目标配置功能 - 新增：读取 OpenClaw 配置自动获取可用推送目标 - 新增：推送失败时通过当前对话提醒用户 - 优化：`/backup_list` 只显示本 skill 产生的备份文件 - 优化：交互式配置增加推送目标选择步骤 ### v1.0.0.20260326 (2026-03-26) - 初始版本发布

path: cron_prompt_example.md
content: # cron_prompt_example.md - OpenClaw Cron 定时任务模板

> 此模板用于配置 OpenClaw Cron 定时任务，实现精确时间自动备份

---

## 📋 模板说明

**适用场景**：
- 需要精确定时执行（如每天凌晨 3:20）
- 独立会话运行（不影响主会话上下文）
- 可指定不同模型执行
- 支持一次性提醒和周期性任务

**配置位置**：
- `~/.openclaw/cron/jobs.json`（定时任务配置文件）
- 或通过 CLI：`openclaw cron add`

**运行机制**：Cron 调度器在精确时间触发，在隔离会话中执行任务

---

## 🔧 配置示例（jobs.json 格式）

```json
{
  "version": 1,
  "jobs": [
    {
      "id": "backup-openclaw-daily",
      "agentId": "secretary",
      "name": "OpenClaw 数据备份 - 每日凌晨",
      "enabled": true,
      "schedule": {
        "kind": "cron",
        "expr": "20 3 * * *",
        "tz": "Asia/Shanghai"
      },
      "sessionTarget": "isolated",
      "wakeMode": "now",
      "payload": {
        "kind": "agentTurn",
        "message": "请执行 OpenClaw 数据备份任务，完成后推送备份结果报告。\n\n【第一步：执行备份】\n在目录 <SKILL_DIR> 下执行以下命令：\nnode -e \"const {runCommand}=require('./cli');runCommand('backup_now',{full:true}).then(r=>console.log(JSON.stringify(r,null,2))).catch(e=>{console.error(e.message);process.exit(1)})\"\n\n解析命令输出的 JSON 结果，提取以下字段用于填充消息模板：\n- success: true/false（判断成功或失败）\n- outputPath: 备份文件完整路径\n- filesTotal: 文件数量\n- sizeBytes: 文件大小（字节）\n- duration: 执行耗时（毫秒）\n- timestamp: 执行完成时间\n- errors: 错误信息数组（失败时）\n\n【第二步：推送备份报告】\n\n📌 推送顺序：先飞书，后 Telegram\n\n📌 飞书推送（feishu_im_user_message）：\n- action: send\n- receive_id_type: chat_id\n- receive_id: <FEISHU_CHAT_ID>\n- msg_type: text\n- content: JSON字符串 {\"text\": \"消息内容\"}\n\n📌 Telegram推送（curl）：\ncurl -X POST \"https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/sendMessage\" -H \"Content-Type: application/json\" -d \"{\\\"chat_id\\\":<TELEGRAM_CHAT_ID>,\\\"text\\\":\\\"消息内容\\\",\\\"parse_mode\\\":\\\"Markdown\\\"}\"\n\n【消息模板 - 备份成功时填入】\n✅ OpenClaw 数据备份成功\n\n📦 备份文件：{从outputPath提取文件名}\n📊 文件数量：{filesTotal} 个\n📏 文件大小：{按KB/MB/GB格式化sizeBytes}\n⏱️ 执行耗时：{按秒/分格式化duration}\n🕐 执行时间：{格式：YYYY-MM-DD HH:mm:ss}\n📁 存储位置：{outputPath}\n\n【消息模板 - 备份失败时填入】\n❌ OpenClaw 数据备份失败\n\n🚨 错误信息：{errors数组.join('; ')}\n🕐 失败时间：{timestamp}\n💡 建议：请检查日志文件 ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log\n\n【重要约束】\n- ✅ 消息内容必须基于备份命令的实际执行结果，不得杜撰\n- ✅ 两个渠道推送内容必须完全一致\n- ✅ 消息格式为纯文本 + emoji\n- ✅ 推送顺序：飞书 → Telegram\n- ✅ 备份完成后关闭所有临时文件/页面",
        "timeoutSeconds": 600
      },
      "delivery": {
        "mode": "none",
        "channel": "last"
      }
    }
  ]
}
```

---

## ⚙️ 配置变量说明

| 变量 | 说明 | 示例值 |
|------|------|--------|
| `<SKILL_DIR>` | skill 脚本目录路径 | `~/.agents/skills/auto-backup-openclaw-user-data/scripts` |
| `<FEISHU_CHAT_ID>` | 飞书群组 ID | `oc_xxxxxxxxxxxxxxxxxxxxxxxx` |
| `<TELEGRAM_BOT_TOKEN>` | Telegram Bot Token | `1234567890:ABCdefGHIjklMNOpqrsTUVwxyz` |
| `<TELEGRAM_CHAT_ID>` | Telegram 聊天 ID | `123456789` |

---

## 📝 Cron 表达式说明

### 格式

```
分钟 小时 日 月 星期
```

### 常用示例

| 表达式 | 说明 |
|--------|------|
| `20 3 * * *` | 每天凌晨 03:20 |
| `0 4 * * *` | 每天凌晨 04:00 |
| `0 3 * * 0` | 每周日凌晨 03:00 |
| `0 3 1 * *` | 每月 1 日凌晨 03:00 |
| `30 8,12,18 * * *` | 每天 08:30、12:30、18:30 |
| `0 */6 * * *` | 每 6 小时一次 |

---

## 🚀 CLI 配置命令

### 添加定时任务

```bash
openclaw cron add \
  --name "OpenClaw 数据备份 - 每日凌晨" \
  --cron "20 3 * * *" \
  --tz "Asia/Shanghai" \
  --agent secretary \
  --session isolated \
  --message "请执行 OpenClaw 数据备份任务..." \
  --timeout 600
```

### 查看定时任务

```bash
openclaw cron list
```

### 启用/禁用定时任务

```bash
openclaw cron enable <job-id>
openclaw cron disable <job-id>
```

### 删除定时任务

```bash
openclaw cron delete <job-id>
```

---

## 📋 Prompt 内容模板（完整版）

以下是可以直接复制到 `payload.message` 的完整 Prompt 内容：

---

**请执行 OpenClaw 数据备份任务，完成后推送备份结果报告。**

**【第一步：执行备份】**

在目录 `{SKILL_DIR}` 下执行以下命令：

```bash
node -e "const {runCommand}=require('./cli');runCommand('backup_now',{full:true}).then(r=>console.log(JSON.stringify(r,null,2))).catch(e=>{console.error(e.message);process.exit(1})"
```

解析命令输出的 JSON 结果，提取以下字段用于填充消息模板：

| 字段 | 说明 |
|------|------|
| `success` | true/false（判断成功或失败） |
| `outputPath` | 备份文件完整路径 |
| `filesTotal` | 文件数量 |
| `sizeBytes` | 文件大小（字节） |
| `duration` | 执行耗时（毫秒） |
| `timestamp` | 执行完成时间 |
| `errors` | 错误信息数组（失败时） |

**【第二步：推送备份报告】**

📌 **推送顺序**：先飞书，后 Telegram

📌 **飞书推送（feishu_im_user_message）**：

```javascript
{
  action: "send",
  receive_id_type: "chat_id",
  receive_id: "{FEISHU_CHAT_ID}",
  msg_type: "text",
  content: '{"text": "消息内容"}'
}
```

📌 **Telegram推送（curl）**：

```bash
curl -X POST "https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d '{"chat_id":{TELEGRAM_CHAT_ID},"text":"消息内容","parse_mode":"Markdown"}'
```

**【消息模板 - 备份成功时填入】**

```
✅ OpenClaw 数据备份成功

📦 备份文件：{从outputPath提取文件名}
📊 文件数量：{filesTotal} 个
📏 文件大小：{按KB/MB/GB格式化sizeBytes}
⏱️ 执行耗时：{按秒/分格式化duration}
🕐 执行时间：{格式：YYYY-MM-DD HH:mm:ss}
📁 存储位置：{outputPath}
```

**【消息模板 - 备份失败时填入】**

```
❌ OpenClaw 数据备份失败

🚨 错误信息：{errors数组.join('; ')}
🕐 失败时间：{timestamp}
💡 建议：请检查日志文件 ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log
```

**【重要约束】**

- ✅ 消息内容必须基于备份命令的**实际执行结果**，不得杜撰
- ✅ 两个渠道推送内容必须**完全一致**
- ✅ 消息格式为**纯文本 + emoji**
- ✅ 推送顺序：飞书 → Telegram
- ✅ 备份完成后关闭所有临时文件/页面

---

## 💡 快速配置步骤

### 1. 准备配置信息

收集以下信息：

| 信息 | 获取方式 |
|------|----------|
| skill 脚本目录 | skill 安装位置，通常为 `~/.agents/skills/auto-backup-openclaw-user-data/scripts` |
| 飞书群组 ID | 飞书群设置中查看（格式：`oc_xxx`） |
| Telegram Bot Token | 创建 Telegram Bot 时获取 |
| Telegram Chat ID | 将 @userinfobot 添加到群组获取 |

### 2. 编辑 jobs.json

打开 `~/.openclaw/cron/jobs.json`，将上述配置示例添加到 `jobs` 数组中。

### 3. 替换变量

将 `<SKILL_DIR>`、`<FEISHU_CHAT_ID>`、`<TELEGRAM_BOT_TOKEN>`、`<TELEGRAM_CHAT_ID>` 替换为实际值。

### 4. 修改执行时间

根据需要修改 `schedule.expr` 的 cron 表达式：

```json
"schedule": {
  "kind": "cron",
  "expr": "20 3 * * *",  // 每天凌晨 03:20
  "tz": "Asia/Shanghai"
}
```

### 5. 重启 Gateway

```bash
openclaw gateway restart
```

---

## 🔍 故障排查

| 问题 | 可能原因 | 解决方案 |
|------|----------|----------|
| 定时任务未执行 | Gateway 未运行 | `openclaw gateway start` |
| 任务执行失败 | skill 路径错误 | 检查 `SKILL_DIR` 配置 |
| 没有收到推送 | 推送配置错误 | 检查飞书/Telegram ID |
| 任务超时 | timeoutSeconds 过小 | 增加 timeout 配置 |
| 任务被跳过 | enabled: false | 设置 `enabled: true` |

---

## 📊 与 HEARTBEAT 对比

| 特性 | Cron 定时任务 | HEARTBEAT |
|------|---------------|-----------|
| 定时精度 | ✅ 精确到分钟 | 约 30 分钟漂移 |
| 运行上下文 | ✅ 隔离会话 | 主会话共享 |
| 配置方式 | jobs.json / CLI | HEARTBEAT.md |
| 推送控制 | ✅ 内置支持 | 需手动实现 |
| 模型覆盖 | ✅ 可指定模型 | 使用默认模型 |
| 一次性任务 | ✅ 支持 `--at` | 不支持 |

**建议**：需要精确时间执行时，优先使用 Cron 定时任务。

---

## 📚 相关文档

- [OpenClaw Cron 定时任务文档](/automation/cron-jobs)
- [心跳 vs Cron 对比](/automation/cron-vs-heartbeat)
- [本项目 README.md](README.md)
- [本项目 USAGE.md](USAGE.md)
- [HEARTBEAT 模板](HEARTBEAT_prompt_example.md)

---

_模板版本：v1.1.0.20260414 | 更新日期：2026-04-14_

path: HEARTBEAT_prompt_example.md
content: # HEARTBEAT_prompt_example.md - OpenClaw 心跳定时任务模板

> 此模板用于配置 OpenClaw Agent 的 HEARTBEAT.md，实现周期性自动备份检查

---

## 📋 模板说明

**适用场景**：
- 周期性监控检查（约 30 分钟间隔）
- 不要求精确执行时间
- 可与其他周期性任务批量处理

**配置位置**：Agent 工作区的 `HEARTBEAT.md` 文件

**运行机制**：Gateway 每 N 分钟触发一次心跳轮次，Agent 执行检查清单

---

## 🔧 模板内容（复制到 HEARTBEAT.md）

```md
# HEARTBEAT.md - OpenClaw 数据自动备份心跳任务

---

## 心跳检查清单

每 N 分钟执行以下检查（N 由 `agents.defaults.heartbeat.every` 配置，默认 30m）：

---

### 一、定时备份检查

**检查逻辑**：

1. 获取当前时间（默认Asia/Shanghai 时区）
2. 判断是否到达预定的备份执行时间：
   - 默认执行时间：每天凌晨 03:00
   - 可自定义：修改下方 `BACKUP_TIME` 变量
3. 如果当前时间在预定时间前后 30 分钟内，执行备份

---

### 二、执行备份任务

**当需要执行备份时，按以下步骤操作**：

#### Step 1：执行备份命令

在 skill 脚本目录下执行：

```bash
# 备份脚本目录（请根据实际安装路径修改）
SKILL_DIR=~/.agents/skills/auto-backup-openclaw-user-data/scripts

# 执行全量备份
cd $SKILL_DIR && node -e "const {runCommand}=require('./cli');runCommand('backup_now',{full:true}).then(r=>console.log(JSON.stringify(r,null,2))).catch(e=>{console.error(e.message);process.exit(1)})"
```

#### Step 2：解析备份结果

从命令输出的 JSON 中提取：

| 字段 | 说明 |
|------|------|
| `success` | true/false（判断成功或失败） |
| `outputPath` | 备份文件完整路径 |
| `filesTotal` | 文件数量 |
| `sizeBytes` | 文件大小（字节） |
| `duration` | 执行耗时（毫秒） |
| `timestamp` | 执行完成时间 |
| `errors` | 错误信息数组（失败时） |

#### Step 3：格式化文件大小和耗时

- 文件大小：bytes → KB/MB/GB（自动选择合适单位）
- 执行耗时：ms → 秒/分钟（自动选择合适单位）

#### Step 4：推送备份报告

**推送顺序**：先飞书，后 Telegram（或其他配置渠道）

**飞书推送示例**：
```javascript
// 使用 feishu_im_user_message 工具
{
  action: "send",
  receive_id_type: "chat_id",
  receive_id: "oc_xxx", // 替换为实际群组 ID
  msg_type: "text",
  content: "{\"text\": \"备份报告内容\"}"
}
```

**Telegram 推送示例**：
```bash
curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/sendMessage" \
  -H "Content-Type: application/json" \
  -d "{\"chat_id\":<CHAT_ID>,\"text\":\"备份报告内容\",\"parse_mode\":\"Markdown\"}"
```

---

### 三、消息模板

#### 备份成功模板

```
✅ OpenClaw 数据备份成功

📦 备份文件：{从 outputPath 提取文件名}
📊 文件数量：{filesTotal} 个
📏 文件大小：{格式化后的 sizeBytes}
⏱️ 执行耗时：{格式化后的 duration}
🕐 执行时间：{YYYY-MM-DD HH:mm:ss}
📁 存储位置：{outputPath}
```

#### 备份失败模板

```
❌ OpenClaw 数据备份失败

🚨 错误信息：{errors 数组 join('; ')}
🕐 失败时间：{timestamp}
💡 建议：请检查日志文件 ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log
```

---

### 四、心跳响应约定

- **需要执行备份时**：执行备份 → 推送报告 → 回复 `HEARTBEAT_OK`（如果无其他事项）
- **不需要执行备份时**：直接回复 `HEARTBEAT_OK`（不做任何消息发送）
- **备份执行后**：无论成功或失败，推送报告后都回复 `HEARTBEAT_OK`

---

## ⚙️ 配置变量（请根据实际情况修改）

| 变量 | 说明 | 默认值 |
|------|------|--------|
| `BACKUP_TIME` | 预定备份时间（HH:MM 格式） | `03:00` |
| `SKILL_DIR` | skill 脚本目录路径 | `~/.agents/skills/auto-backup-openclaw-user-data/scripts` |
| `FEISHU_CHAT_ID` | 飞书推送群组 ID | 需配置 |
| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token | 需配置 |
| `TELEGRAM_CHAT_ID` | Telegram 聊天 ID | 需配置 |

---

## 📝 重要约束

1. ✅ 消息内容必须基于备份命令的**实际执行结果**，不得杜撰
2. ✅ 所有推送渠道的内容必须**完全一致**
3. ✅ 消息格式为**纯文本 + emoji**（不使用 Markdown 加粗）
4. ✅ 推送顺序：飞书 → Telegram
5. ✅ 只在预定的备份时间前后 30 分钟内执行（避免频繁执行）
6. ✅ 同一天内只执行一次（通过 lastRun 判断）
7. ✅ 执行完成后回复 `HEARTBEAT_OK`（遵循心跳响应约定）

---

## 🚀 快速配置步骤

### 1. 复制模板内容

将上述模板内容复制到你的 Agent 工作区的 `HEARTBEAT.md` 文件中。

### 2. 修改配置变量

根据你的实际情况，修改以下配置：

- `BACKUP_TIME`：你希望的备份执行时间
- `SKILL_DIR`：skill 实际安装路径
- `FEISHU_CHAT_ID`：飞书群组 ID（格式：`oc_xxx`）
- `TELEGRAM_BOT_TOKEN` 和 `TELEGRAM_CHAT_ID`：Telegram 推送配置

### 3. 配置心跳间隔

在 `openclaw.json` 中配置心跳间隔：

```json5
{
  agents: {
    defaults: {
      heartbeat: {
        every: "30m",  // 每 30 分钟检查一次
        target: "last",  // 推送到最后使用的渠道
      },
    },
  },
}
```

### 4. 启动 Gateway

确保 Gateway 正在运行：

```bash
openclaw gateway start
```

---

## 🔍 故障排查

| 问题 | 可能原因 | 解决方案 |
|------|----------|----------|
| 心跳没有执行 | Gateway 未运行 | `openclaw gateway start` |
| 备份命令执行失败 | skill 路径错误 | 检查 `SKILL_DIR` 配置 |
| 没有收到推送 | 推送目标 ID 错误 | 检查飞书群 ID / Telegram ID |
| 备份频繁执行 | 时间判断逻辑错误 | 检查 `BACKUP_TIME` 和检查逻辑 |
| HEARTBEAT_OK 被发送 | 正常行为 | 无需处理，这是心跳确认 |

---

## 📚 相关文档

- [OpenClaw 心跳文档](/gateway/heartbeat)
- [心跳 vs Cron 对比](/automation/cron-vs-heartbeat)
- [本项目 README.md](README.md)
- [本项目 USAGE.md](USAGE.md)

---

_模板版本：v1.1.0.20260414 | 更新日期：2026-04-14_
```

---

## 💡 使用建议

### HEARTBEAT 适用场景

| 场景 | 推荐使用 |
|------|----------|
| 每日凌晨备份 | ✅ 适合（配合时间判断逻辑） |
| 周期性监控检查 | ✅ 非常适合 |
| 精确时间执行 | ❌ 建议使用 Cron 定时任务 |
| 需要独立上下文 | ❌ 建议使用 Cron 定时任务 |

### 与 Cron 定时任务对比

| 特性 | HEARTBEAT | Cron |
|------|-----------|------|
| 定时精度 | 约 30 分钟漂移 | 精确到分钟 |
| 运行上下文 | 主会话（共享） | 隔离会话 |
| 配置复杂度 | 较简单 | 需要 CLI 配置 |
| 推送控制 | 需手动实现 | 内置支持 |

---

_如果需要精确时间执行，请参考 `cron_prompt_example.md` 使用 Cron 定时任务方案。_

path: index.js
content: /**
 * 入口模块
 * 完整导出所有接口，确保兼容性
 * v1.1.0 - 脚本入口改进，消除"可疑技能"标记
 */

const { BackupManager, initBackupManager, OPENCLAW_ROOT } = require('./scripts/backup');
const { CLI, runCommand } = require('./scripts/cli');  // ← 关键：必须导出runCommand
const {
  loadConfig,
  saveConfig,
  resetConfig,
  getConfigPaths,
  detectWorkspaces
} = require('./scripts/config');

/**
 * 模块导出
 * OpenClaw通过此接口调用技能功能
 */
module.exports = {
  // 核心类（向后兼容）
  BackupManager,
  OPENCLAW_ROOT,

  // 命令执行接口（新增，关键）
  CLI,
  runCommand,  // ← OpenClaw通过此函数调用命令

  // 初始化函数
  initBackupManager,

  // 配置管理函数
  loadConfig,
  saveConfig,
  resetConfig,
  getConfigPaths,
  detectWorkspaces,  // ← 新增：工作空间动态检测函数

  // 版本信息
  version: '1.1.0',
  name: 'auto-backup-openclaw-user-data'
};

path: package-lock.json
content: {
  "name": "auto-backup-openclaw-user-data",
  "version": "1.1.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "auto-backup-openclaw-user-data",
      "version": "1.1.0",
      "license": "MIT",
      "dependencies": {
        "archiver": "6.0.0",
        "archiver-zip-encrypted": "1.0.0",
        "dayjs": "1.11.0",
        "fs-extra": "11.0.0"
      },
      "devDependencies": {},
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/archiver": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "archiver-utils": "^3.0.0",
        "async": "^3.2.4",
        "buffer-crc32": "^0.2.1",
        "readable-stream": "^3.6.0",
        "readdir-glob": "^1.1.2",
        "tar-stream": "^2.2.0",
        "zip-stream": "^4.1.0"
      },
      "engines": {
        "node": ">= 12.0.0"
      }
    },
    "node_modules/archiver-utils": {
      "version": "3.0.4",
      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "glob": "^7.2.3",
        "graceful-fs": "^4.2.0",
        "lazystream": "^1.0.0",
        "lodash.defaults": "^4.2.0",
        "lodash.difference": "^4.5.0",
        "lodash.flatten": "^4.4.0",
        "lodash.isplainobject": "^4.0.6",
        "lodash.union": "^4.6.0",
        "normalize-path": "^3.0.0",
        "readable-stream": "^3.6.0"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/archiver-zip-encrypted": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/archiver-zip-encrypted/-/archiver-zip-encrypted-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "archiver": "^3.0.0",
        "archiver-utils": "^2.0.0",
        "compress-commons": "^1.2.2",
        "zip-stream": "^2.0.1"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/archiver": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "archiver-utils": "^2.1.0",
        "async": "^2.6.3",
        "buffer-crc32": "^0.2.1",
        "glob": "^7.1.4",
        "readable-stream": "^3.4.0",
        "tar-stream": "^2.1.0",
        "zip-stream": "^2.1.2"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/archiver-utils": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "glob": "^7.1.4",
        "graceful-fs": "^4.2.0",
        "lazystream": "^1.0.0",
        "lodash.defaults": "^4.2.0",
        "lodash.difference": "^4.5.0",
        "lodash.flatten": "^4.4.0",
        "lodash.isplainobject": "^4.0.6",
        "lodash.union": "^4.6.0",
        "normalize-path": "^3.0.0",
        "readable-stream": "^2.0.0"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/archiver-utils/node_modules/readable-stream": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.3",
        "isarray": "~1.0.0",
        "process-nextick-args": "~2.0.0",
        "safe-buffer": "~5.1.1",
        "string_decoder": "~1.1.1",
        "util-deprecate": "~1.0.1"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/async": {
      "version": "2.6.4",
      "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "lodash": "^4.17.14"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/compress-commons": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer-crc32": "^0.2.1",
        "crc32-stream": "^2.0.0",
        "normalize-path": "^2.0.0",
        "readable-stream": "^2.0.0"
      },
      "engines": {
        "node": ">= 0.10.0"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/compress-commons/node_modules/normalize-path": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "remove-trailing-separator": "^1.0.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/compress-commons/node_modules/readable-stream": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.3",
        "isarray": "~1.0.0",
        "process-nextick-args": "~2.0.0",
        "safe-buffer": "~5.1.1",
        "string_decoder": "~1.1.1",
        "util-deprecate": "~1.0.1"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/crc32-stream": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "crc": "^3.4.4",
        "readable-stream": "^2.0.0"
      },
      "engines": {
        "node": ">= 0.10.0"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/crc32-stream/node_modules/readable-stream": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.3",
        "isarray": "~1.0.0",
        "process-nextick-args": "~2.0.0",
        "safe-buffer": "~5.1.1",
        "string_decoder": "~1.1.1",
        "util-deprecate": "~1.0.1"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/safe-buffer": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/archiver-zip-encrypted/node_modules/string_decoder": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "~5.1.0"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/zip-stream": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "archiver-utils": "^2.1.0",
        "compress-commons": "^2.1.1",
        "readable-stream": "^3.4.0"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/zip-stream/node_modules/compress-commons": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer-crc32": "^0.2.13",
        "crc32-stream": "^3.0.1",
        "normalize-path": "^3.0.0",
        "readable-stream": "^2.3.6"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/zip-stream/node_modules/compress-commons/node_modules/readable-stream": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.3",
        "isarray": "~1.0.0",
        "process-nextick-args": "~2.0.0",
        "safe-buffer": "~5.1.1",
        "string_decoder": "~1.1.1",
        "util-deprecate": "~1.0.1"
      }
    },
    "node_modules/archiver-zip-encrypted/node_modules/zip-stream/node_modules/crc32-stream": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "crc": "^3.4.4",
        "readable-stream": "^3.4.0"
      },
      "engines": {
        "node": ">= 6.9.0"
      }
    },
    "node_modules/async": {
      "version": "3.2.6",
      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/base64-js": {
      "version": "1.5.1",
      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/bl": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer": "^5.5.0",
        "inherits": "^2.0.4",
        "readable-stream": "^3.4.0"
      }
    },
    "node_modules/brace-expansion": {
      "version": "1.1.13",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/buffer": {
      "version": "5.7.1",
      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "base64-js": "^1.3.1",
        "ieee754": "^1.1.13"
      }
    },
    "node_modules/buffer-crc32": {
      "version": "0.2.13",
      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": "*"
      }
    },
    "node_modules/compress-commons": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer-crc32": "^0.2.13",
        "crc32-stream": "^4.0.2",
        "normalize-path": "^3.0.0",
        "readable-stream": "^3.6.0"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/core-util-is": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/crc": {
      "version": "3.8.0",
      "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "buffer": "^5.1.0"
      }
    },
    "node_modules/crc-32": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "bin": {
        "crc32": "bin/crc32.njs"
      },
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/crc32-stream": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "crc-32": "^1.2.0",
        "readable-stream": "^3.4.0"
      },
      "engines": {
        "node": ">= 10"
      }
    },
    "node_modules/dayjs": {
      "version": "1.11.0",
      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/end-of-stream": {
      "version": "1.4.5",
      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "once": "^1.4.0"
      }
    },
    "node_modules/fs-constants": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fs-extra": {
      "version": "11.0.0",
      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "graceful-fs": "^4.2.0",
        "jsonfile": "^6.0.1",
        "universalify": "^2.0.0"
      },
      "engines": {
        "node": ">=14.14"
      }
    },
    "node_modules/fs.realpath": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/glob": {
      "version": "7.2.3",
      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [REDACTED_SECRET]",
      "license": "ISC",
      "dependencies": {
        "fs.realpath": "^1.0.0",
        "inflight": "^1.0.4",
        "inherits": "2",
        "minimatch": "^3.1.1",
        "once": "^1.3.0",
        "path-is-absolute": "^1.0.0"
      },
      "engines": {
        "node": "*"
      },
      "funding": {
        "url": "https://github.com/sponsors/isaacs"
      }
    },
    "node_modules/graceful-fs": {
      "version": "4.2.11",
      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ieee754": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/inflight": {
      "version": "1.0.6",
      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
      "license": "ISC",
      "dependencies": {
        "once": "^1.3.0",
        "wrappy": "1"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/isarray": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/jsonfile": {
      "version": "6.2.0",
      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "universalify": "^2.0.0"
      },
      "optionalDependencies": {
        "graceful-fs": "^4.1.6"
      }
    },
    "node_modules/lazystream": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "readable-stream": "^2.0.5"
      },
      "engines": {
        "node": ">= 0.6.3"
      }
    },
    "node_modules/lazystream/node_modules/readable-stream": {
      "version": "2.3.8",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "core-util-is": "~1.0.0",
        "inherits": "~2.0.3",
        "isarray": "~1.0.0",
        "process-nextick-args": "~2.0.0",
        "safe-buffer": "~5.1.1",
        "string_decoder": "~1.1.1",
        "util-deprecate": "~1.0.1"
      }
    },
    "node_modules/lazystream/node_modules/safe-buffer": {
      "version": "5.1.2",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lazystream/node_modules/string_decoder": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "~5.1.0"
      }
    },
    "node_modules/lodash": {
      "version": "4.18.1",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.defaults": {
      "version": "4.2.0",
      "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.difference": {
      "version": "4.5.0",
      "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.flatten": {
      "version": "4.4.0",
      "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.isplainobject": {
      "version": "4.0.6",
      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/lodash.union": {
      "version": "4.6.0",
      "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/minimatch": {
      "version": "3.1.5",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "brace-expansion": "^1.1.7"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/normalize-path": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/path-is-absolute": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/process-nextick-args": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/readable-stream": {
      "version": "3.6.2",
      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "inherits": "^2.0.3",
        "string_decoder": "^1.1.1",
        "util-deprecate": "^1.0.1"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/readdir-glob": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Apache-2.0",
      "dependencies": {
        "minimatch": "^5.1.0"
      }
    },
    "node_modules/readdir-glob/node_modules/brace-expansion": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "balanced-match": "^1.0.0"
      }
    },
    "node_modules/readdir-glob/node_modules/minimatch": {
      "version": "5.1.9",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "brace-expansion": "^2.0.1"
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/remove-trailing-separator": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/safe-buffer": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/feross"
        },
        {
          "type": "patreon",
          "url": "https://www.patreon.com/feross"
        },
        {
          "type": "consulting",
          "url": "https://feross.org/support"
        }
      ],
      "license": "MIT"
    },
    "node_modules/string_decoder": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safe-buffer": "~5.2.0"
      }
    },
    "node_modules/tar-stream": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bl": "^4.0.3",
        "end-of-stream": "^1.4.1",
        "fs-constants": "^1.0.0",
        "inherits": "^2.0.3",
        "readable-stream": "^3.1.1"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/universalify": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 10.0.0"
      }
    },
    "node_modules/util-deprecate": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/zip-stream": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "archiver-utils": "^3.0.4",
        "compress-commons": "^4.1.2",
        "readable-stream": "^3.6.0"
      },
      "engines": {
        "node": ">= 10"
      }
    }
  }
}


path: package.json
content: {
  "name": "auto-backup-openclaw-user-data",
  "version": "1.1.0",
  "description": "OpenClaw 用户数据自动备份技能",
  "main": "index.js",
  "exports": {
    ".": "./index.js",
    "./cli": "./scripts/cli.js"
  },
  "skillType": "openclaw-service",
  "author": "Jack·Huang <[REDACTED_SECRET]>",
  "license": "MIT",
  "keywords": [
    "openclaw",
    "backup",
    "skill",
    "automation",
    "security"
  ],
  "repository": {
    "type": "git",
    "url": "https://github.com/hjj345/auto-backup-openclaw-user-data"
  },
  "engines": {
    "node": ">=18.0.0"
  },
  "dependencies": {
    "archiver": "6.0.0",
    "archiver-zip-encrypted": "1.0.0",
    "fs-extra": "11.0.0",
    "dayjs": "1.11.0"
  },
  "devDependencies": {},
  "scripts": {
    "test": "node scripts/test.js"
  }
}

path: README.md
content: # Auto-Backup-Openclaw-User-Data

> OpenClaw 用户数据自动备份技能

[![Version](https://img.shields.io/badge/version-1.1.0.20260414-blue.svg)](https://github.com/hjj345/auto-backup-openclaw-user-data)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![OpenClaw](https://img.shields.io/badge/OpenClaw-v2026.3.1+-purple.svg)](https://openclaw.ai)

---

## 简介

`Auto-Backup-Openclaw-User-Data` 是一个专为 OpenClaw 设计的自动备份skill技能，使用本skill能够更好完成自动备份用户数据目录，支持全量/选择性备份、定时执行、ZIP 压缩、日志记录、消息通知和保留策略管理。

## 工作空间自动检测

### 首次配置自动检测

首次使用本技能时，系统会自动检测您的OpenClaw工作空间：

**检测机制**：
- 自动扫描 `~/.openclaw/` 目录
- 识别所有 `workspace-*` 目录
- 检测 `memory` 目录
- 将检测结果写入配置文件作为默认值

**示例检测结果**：
```json
{
  "backup": {
    "targets": ["workspace", "workspace-01", "workspace-02", "workspace-news", "memory"]
  }
}
```

### 建议

首次配置建议使用交互式配置：
```
/backup_config
选择 [1] 交互式配置
```

交互式配置会列出实际存在的workspace，供您确认和调整。

### 手动调整

如需调整备份的工作空间，可：

1. **交互式配置**：Step 1.1文件选择步骤
2. **手动修改配置文件**：编辑 `config.json` 中的 `backup.targets`

```json
{
  "backup": {
    "targets": ["workspace", "workspace-01", "memory"]
  }
}
```

## 功能特性

- ✅ **自动备份** - 定时备份 `.openclaw` 目录
- ✅ **选择性备份** - 支持全量或部分备份（交互式选择文件/文件夹）
- ✅ **ZIP 压缩** - 自动压缩并按规则命名
- ✅ **定时执行** - 支持 HEARTBEAT 心跳和 Cron 定时任务两种方式
- ✅ **日志记录** - 完整记录执行过程
- ✅ **消息通知** - 支持多渠道推送结果
- ✅ **保留策略** - 自动清理旧备份
- ✅ **跨平台** - Windows / Linux / macOS 全支持

## v1.1.0 新特性

### 🎯 工作空间自动检测

首次配置时自动检测您的OpenClaw工作空间，无需手动配置备份目标：

- 自动扫描 `~/.openclaw/` 目录下的所有 `workspace-*` 目录
- 自动识别 `memory` 目录
- 检测结果自动写入配置文件
- 支持交互式确认和调整

**优势**：适配多Agent工作环境，避免遗漏备份目标。

### 🔒 敏感文件安全提醒

新增敏感文件识别和提醒机制，保护您的隐私数据：

- 提供敏感文件建议排除列表（密钥、凭证、环境变量等）
- 提供敏感目录建议排除列表（.ssh、credentials等）
- 默认不强制排除，仅提供提醒和建议
- 支持交互式配置启用敏感文件排除

**原则**："只做提醒，不做限制"，让用户自主决定备份范围。

### 📦 标准化入口改进

创建标准Node.js模块入口，提升安全性和兼容性：

- 新增根目录 `index.js` 标准入口文件
- 完整导出所有接口函数
- 导出 `runCommand` 函数供OpenClaw调用
- 消除"可疑技能"安全警告标记

**效果**：符合OpenClaw安全最佳实践，提升技能可信度。

### ⚙️ 配置自动迁移

支持从v1.0.2自动升级到v1.1.0，无需手动修改配置：

- 自动补全缺失的v1.1.0新增配置字段
- 保留用户原有配置不变
- 零停机平滑升级

**便捷**：升级后首次加载配置即可享受新特性。

## ⚠️ 安全警告

### 敏感文件风险

备份可能包含以下敏感文件（默认不强制排除）：

- **密钥文件**：*.key, *.pem, *.p12, *.pfx
- **环境变量**：.env, .env.local, .env.*.local
- **凭证文件**：credentials.json, secrets.json
- **Token文件**：*.token, *.secret, *_token.json
- **SSH密钥**：id_rsa, id_dsa, *.ppk

### 默认行为

系统默认**不启用**敏感文件排除，仅排除临时文件：
- 排除目录：logs, cache, tmp, node_modules
- 排除模式：*.log, *.tmp, .DS_Store, Thumbs.db

### 如何启用敏感文件排除

通过交互式配置启用：
```
/backup_config
选择 [1] 交互式配置
Step 7: 敏感文件排除配置
选择 [1] 启用排除
```

或手动编辑配置文件：
```json
{
  "backup": {
    "exclude": ["logs", "cache", "*.key", ".ssh"],
    "excludePatterns": ["*.pem", ".env", "credentials.json"]
  }
}
```

### 建议

根据实际需求决定是否启用敏感文件排除：
- ✅ **启用**：保护敏感数据，防止备份泄露
- ⚠️ **不启用**：完整备份所有数据，需妥善保管备份文件

## 安装

### 方式一：ClawHub 安装

```bash
openclaw skill install auto-backup-openclaw-user-data
```

### 方式二：本地文件安装

```bash
openclaw skill install /path/to/auto-backup-openclaw-user-data.skill
```

### 方式三：GitHub 安装

```bash
openclaw skill install https://github.com/hjj345/auto-backup-openclaw-user-data
```

## 快速开始

### 立即执行备份

```
/backup_now
```

### 查看备份状态

```
/backup_status
```

### 配置备份选项

```
/backup_config
```

### 列出备份文件

```
/backup_list
```

### 清理旧备份

```
/backup_clean
```

## 命令说明

| 命令 | 功能 | 参数 |
|------|------|------|
| `/backup_now` | 立即执行备份 | `--full`, `--partial` |
| `/backup_status` | 查看备份状态 | - |
| `/backup_config` | 配置向导 | `[1-4]` |
| `/backup_list` | 列出备份文件 | `--all` |
| `/backup_clean` | 清理旧备份 | `--preview`, `--confirm` |

## 定时任务配置

本 skill 支持两种定时执行方式，请根据实际需求选择：

### 方式对比

| 特性 | HEARTBEAT 心跳 | Cron 定时任务 |
|------|---------------|---------------|
| 定时精度 | 约 30 分钟漂移 | 精确到分钟 |
| 运行上下文 | 主会话（共享） | 隔离会话 |
| 配置复杂度 | 较简单 | 需 CLI 配置 |
| 推送控制 | 需手动实现 | 内置支持 |
| 适用场景 | 周期性监控检查 | 精确时间执行 |

### 方式一：HEARTBEAT 心跳（推荐新手）

适用于周期性监控检查，不要求精确执行时间。

**配置步骤**：

1. 查看项目根目录中的 `HEARTBEAT_prompt_example.md` 文件
2. 根据模板内容，修改你的 Agent 工作区的 `HEARTBEAT.md` 文件
3. 修改模板中的配置变量（备份时间、skill 路径、推送目标等）
4. 确保 Gateway 正在运行

**详细说明**：见 [HEARTBEAT_prompt_example.md](HEARTBEAT_prompt_example.md)

### 方式二：Cron 定时任务（推荐高级用户）

适用于需要精确时间执行的场景（如每天凌晨 3:20）。

**配置步骤**：

1. 查看项目根目录中的 `cron_prompt_example.md` 文件
2. 准备配置信息（skill 路径、飞书群 ID、Telegram Bot Token 等）
3. 编辑 `~/.openclaw/cron/jobs.json` 或使用 CLI 命令添加定时任务
4. 重启 Gateway

**详细说明**：见 [cron_prompt_example.md](cron_prompt_example.md)

## 配置

配置文件位置：`~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/config.json`

### 默认配置

```json
{
  "backup": {
    "mode": "full",
    "exclude": ["logs", "cache", "tmp", "node_modules"]
  },
  "schedule": {
    "enabled": true,
    "cron": "0 3 * * *"
  },
  "retention": {
    "enabled": true,
    "mode": "count",
    "count": 10
  },
  "notification": {
    "enabled": true,
    "channels": ["feishu"],
    "targets": {}
  }
}
```

详细配置说明请参考 [references/config-schema.md](references/config-schema.md)

## 文件结构

```
~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/
├── config.json       # 用户配置
├── backup.log        # 备份日志
└── backups/          # 备份文件存储
    └── auto-backup-openclaw-user-data_YYYYMMDD_HHMM_vX.X.X_XX.zip
```

## ZIP 文件命名规则

```
auto-backup-openclaw-user-data_{YYYYMMDD}_{HHMM}_{version}_{序号}.zip

示例：
auto-backup-openclaw-user-data_20260326_0300_v2026.3.23-2_01.zip
```

## 故障排查

遇到问题？请参考 [references/troubleshooting.md](references/troubleshooting.md)

## 技术架构

```
auto-backup-openclaw-user-data/
├── SKILL.md              # 技能说明
├── scripts/
│   ├── backup.js         # 主备份逻辑
│   ├── compressor.js     # ZIP 压缩
│   ├── scheduler.js      # 定时调度
│   ├── cleaner.js        # 清理策略
│   ├── notifier.js       # 消息通知
│   ├── config.js         # 配置管理
│   ├── logger.js         # 日志记录
│   └── cli.js            # CLI 入口
├── references/
│   ├── config-schema.md  # 配置说明
│   └── troubleshooting.md # 故障排查
└── templates/
    └── config.template.json
```

## 依赖

- Node.js >= 18.0.0
- archiver ^6.0.0
- fs-extra ^11.0.0
- dayjs ^1.11.0

## 开发

```bash
# 克隆仓库
git clone https://github.com/hjj345/auto-backup-openclaw-user-data.git

# 安装依赖
cd auto-backup-openclaw-user-data
npm install

# 测试
npm test
```

## 贡献

欢迎提交 Issue 和 Pull Request！

## 许可证

[MIT License](LICENSE)

## 作者

- **Jack·Huang**
- Email: [REDACTED_SECRET]

## 升级指南

### 从 v1.0.2 升级到 v1.1.0

本版本引入了重要改进，建议按照以下步骤升级：

#### 升级方式

**推荐方式**：卸载旧版本后重新安装

```bash
# 1. 卸载旧版本
openclaw skill uninstall auto-backup-openclaw-user-data

# 2. 安装新版本
openclaw skill install auto-backup-openclaw-user-data
```

**原因**：重新安装可获得完整的v1.1.0默认配置，包括：
- 工作空间自动检测默认值
- 敏感文件建议列表
- 新增的encryption配置字段

#### 配置自动迁移

如果您希望保留现有配置，系统会自动处理升级：

- **自动补全**：首次加载配置时，自动补全缺失的v1.1.0新增字段
- **保留原配置**：您原有的备份目标、定时设置等配置保持不变
- **零停机**：无需手动修改，平滑升级

#### 升级后重要变更

**1. backup.mode默认值变更**

- **v1.0.2默认**：`"mode": "full"`（全量备份）
- **v1.1.0默认**：`"mode": "partial"`（选择性备份）
- **影响**：新安装用户默认使用选择性备份，需首次配置时选择备份目标
- **现有用户**：您的现有`mode`配置保持不变

**2. 工作空间检测机制**

- **v1.0.2**：硬编码备份目标（可能遗漏workspace）
- **v1.1.0**：首次配置时动态检测所有workspace
- **建议**：升级后运行 `/backup_config` 重新确认备份目标

**3. 敏感文件提醒**

- **新增**：敏感文件建议排除列表（默认不启用）
- **建议**：根据您的安全需求决定是否启用敏感文件排除
- **配置方式**：交互式配置 Step 7 或手动编辑配置文件

#### 升级后首次配置建议

建议升级后执行以下操作：

```bash
# 1. 查看当前配置状态
/backup_status

# 2. 重新配置备份目标（利用自动检测）
/backup_config
选择 [1] 交互式配置
Step 1: 确认自动检测的工作空间

# 3. 配置敏感文件排除（可选）
Step 7: 选择是否启用敏感文件排除

# 4. 测试备份功能
/backup_now
```

#### 兼容性说明

- ✅ **配置兼容**：v1.0.2配置文件可直接使用，自动补全新字段
- ✅ **备份兼容**：v1.0.2备份文件格式完全兼容v1.1.0
- ✅ **命令兼容**：所有 `/backup_*` 命令保持不变
- ✅ **定时兼容**：HEARTBEAT和Cron配置无需修改

## 更新日志

### v1.1.0.20260414 (2026-04-14) - 安全优化版本

**脚本入口改进**：
- 新增：创建标准`index.js`入口文件，完整导出所有接口
- 修复：导出`runCommand`函数，确保OpenClaw能正常调用命令
- 新增：`package.json`添加`exports`和`skillType`字段
- 消除：移除"可疑技能"标记，符合OpenClaw安全最佳实践

**工作空间动态检测**：
- 新增：`detectWorkspaces()`函数，首次配置时动态检测用户实际workspace
- 修复：修复硬编码targets问题，避免多Agent工作空间遗漏
- 优化：默认targets改为空数组，首次加载时自动检测
- 完善：检测结果按字母排序，自动识别所有workspace-*目录和memory目录

**敏感文件处理**：
- 新增：敏感文件建议列表配置（sensitiveExcludeSuggestion）
- 新增：敏感目录建议列表配置（sensitiveExcludeDirectories）
- 新增：enableSensitiveExclude配置字段（默认false，遵循"只做提醒，不做限制"原则）
- 完善：默认仅排除临时文件（logs、cache、tmp、node_modules等）

**文档完善**：
- 新增：README.md添加工作空间自动检测说明章节
- 新增：README.md添加安全警告章节（敏感文件风险说明）
- 新增：SKILL.md添加工作空间动态检测和安全警告简要说明
- 更新：所有文档版本号同步更新到v1.1.0

**安全改进**：
- 符合GitHub Issue #1611安全改进建议
- 提升技能安全性评级
- 保护用户隐私数据安全

### v1.0.2.20260331 (2026-03-31)

- 新增：HEARTBEAT 心跳定时任务模板（`HEARTBEAT_prompt_example.md`）
- 新增：Cron 定时任务模板（`cron_prompt_example.md`）
- 新增：选择性备份交互式文件选择功能
- 新增：文件选择确认/重新选择功能
- 优化：交互式配置步骤从 6 步调整为 7 步（新增文件选择步骤）
- 优化：选择性备份时列出 `~/.openclaw/` 目录文件清单
- 文档：README.md 和 USAGE.md 新增定时任务配置说明

### v1.0.1.20260326 (2026-03-26)

- 新增：消息推送目标配置功能
- 新增：读取 OpenClaw 配置自动获取可用推送目标
- 新增：推送失败时通过当前对话提醒用户
- 优化：`/backup_list` 只显示本 skill 产生的备份文件
- 优化：交互式配置增加推送目标选择步骤

### v1.0.0.20260326 (2026-03-26)

- 初始版本发布

---

**Made with ❤️ for OpenClaw**

path: templates/config.template.json
content: {
  "version": "1.1.0",
  "createdAt": "",
  "updatedAt": "",

  "backup": {
    "mode": "partial",
    "targets": [],  // ← 改为空数组，首次加载时动态检测
    "exclude": ["logs", "cache", "tmp", "node_modules"],
    "excludePatterns": ["*.log", "*.tmp", ".DS_Store", "Thumbs.db"],

    // 敏感文件排除建议列表（默认不启用，仅建议）
    "sensitiveExcludeSuggestion": [
      "*.key", "*.pem", "*.p12", "*.pfx",
      ".env", ".env.local", ".env.*.local",
      "credentials.json", "secrets.json",
      "*.token", "*.secret",
      "*_key.json", "*_token.json",
      "id_rsa", "id_dsa", "*.ppk"
    ],
    "sensitiveExcludeDirectories": [
      "credentials", "secrets", ".ssh", ".gnupg"
    ],
    "enableSensitiveExclude": false  // ← 默认不启用
  },
  
  "schedule": {
    "enabled": true,
    "cron": "0 3 * * *",
    "timezone": "Asia/Shanghai",
    "lastRun": null
  },
  
  "output": {
    "path": "",
    "naming": {
      "prefix": "auto-backup-openclaw-user-data",
      "includeVersion": true,
      "includeSequence": true
    },
    "encryption": {
      "enabled": false,
      "password": null,
      "algorithm": "aes-256",
      "reminderShown": false
    }
  },
  
  "retention": {
    "enabled": true,
    "mode": "count",
    "days": 30,
    "count": 10
  },
  
  "notification": {
    "enabled": true,
    "channels": ["feishu"],
    "targets": {},
    "onSuccess": true,
    "onFailure": true
  },
  
  "logging": {
    "enabled": true,
    "level": "info",
    "maxSize": "10MB",
    "maxFiles": 5
  }
}

path: scripts/backup.js
content: /**
 * 主备份模块
 * 负责协调整个备份流程
 */

const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const dayjs = require('dayjs');

const { loadConfig, saveConfig, ensureConfigDir, BACKUPS_DIR } = require('./config');
const { Compressor } = require('./compressor');
const { Scheduler, describeCron } = require('./scheduler');
const { Cleaner } = require('./cleaner');
const { Notifier } = require('./notifier');
const { info, warn, error, debug, cleanOldLogs } = require('./logger');

// OpenClaw 根目录
const OPENCLAW_ROOT = path.join(os.homedir(), '.openclaw');

/**
 * 创建备份管理器实例
 */
class BackupManager {
  constructor(config) {
    this.config = config;
    this.compressor = new Compressor(config);
    this.scheduler = new Scheduler(config);
    this.cleaner = new Cleaner(config);
    this.notifier = new Notifier(config);
    this.isRunning = false;
  }

  /**
   * 执行备份
   * @param {Object} options 选项
   * @returns {Promise<Object>} 备份结果
   */
  async execute(options = {}) {
    const startTime = Date.now();
    const result = {
      success: false,
      filesTotal: 0,
      filesSkipped: 0,
      sizeBytes: 0,
      outputPath: null,
      errors: [],
      duration: 0,
      timestamp: new Date().toISOString()
    };

    try {
      // 检查是否正在运行
      if (this.isRunning) {
        result.errors.push('备份任务正在执行中，请稍后再试');
        return result;
      }

      this.isRunning = true;
      
      await info('Backup', '='.repeat(50));
      await info('Backup', '开始执行备份任务');
      await info('Backup', '='.repeat(50));

      // 1. 准备备份目标
      const targets = await this.prepareTargets(options);
      await info('Backup', `备份目标: ${targets.length} 个目录`);
      
      // 2. 收集文件列表
      await info('Backup', '正在收集文件列表...');
      const files = await this.collectFiles(targets);
      result.filesTotal = files.length;
      await info('Backup', `共收集到 ${files.length} 个文件`);
      
      if (files.length === 0) {
        result.errors.push('没有找到需要备份的文件');
        this.isRunning = false;
        return result;
      }
      
      // 3. 执行压缩
      await info('Backup', '正在压缩文件...');
      const zipPath = await this.compressor.compress(files, this.config.output);
      result.outputPath = zipPath;
      
      // 4. 获取文件大小
      const stats = await fs.stat(zipPath);
      result.sizeBytes = stats.size;
      
      await info('Backup', `备份文件: ${path.basename(zipPath)}`);
      await info('Backup', `文件大小: ${this.formatSize(stats.size)}`);
      
      // 5. 记录执行时间
      await this.scheduler.recordRun();
      
      // 6. 执行保留策略清理
      if (this.config.retention.enabled) {
        await info('Backup', '执行保留策略清理...');
        const cleanResult = await this.cleaner.execute();
        result.cleanResult = cleanResult;
      }
      
      // 7. 清理旧日志
      await cleanOldLogs(this.config.logging.maxSize, this.config.logging.maxFiles);
      
      result.success = true;
      result.duration = Date.now() - startTime;
      
      await info('Backup', '='.repeat(50));
      await info('Backup', `备份完成！耗时: ${this.formatDuration(result.duration)}`);
      await info('Backup', '='.repeat(50));
      
      // 8. 发送成功通知
      await this.notifier.notifySuccess(result);
      
    } catch (err) {
      result.errors.push(err.message);
      result.duration = Date.now() - startTime;
      
      await error('Backup', `备份失败: ${err.message}`);
      await error('Backup', err.stack);
      
      // 发送失败通知
      await this.notifier.notifyFailure(result);
    } finally {
      this.isRunning = false;
    }

    return result;
  }

  /**
   * 准备备份目标路径
   */
  async prepareTargets(options) {
    const openclawRoot = OPENCLAW_ROOT;
    const targets = [];
    
    // 检查备份模式
    if (this.config.backup.mode === 'full' || options.full) {
      // 全量备份
      targets.push({
        path: openclawRoot,
        name: '.openclaw',
        isRoot: true
      });
    } else {
      // 选择性备份
      const targetNames = options.targets || this.config.backup.targets || [];
      
      for (const name of targetNames) {
        const targetPath = path.join(openclawRoot, name);
        
        if (await fs.pathExists(targetPath)) {
          targets.push({
            path: targetPath,
            name: name,
            isRoot: false
          });
        } else {
          await warn('Backup', `目标目录不存在，跳过: ${name}`);
        }
      }
    }
    
    return targets;
  }

  /**
   * 收集待备份文件
   */
  async collectFiles(targets) {
    const allFiles = [];
    const exclude = this.config.backup.exclude || [];
    const excludePatterns = this.config.backup.excludePatterns || [];
    
    for (const target of targets) {
      await debug('Backup', `扫描目录: ${target.name}`);
      
      const files = await this.walkDirectory(
        target.path,
        target.isRoot ? exclude : [],
        excludePatterns,
        target.isRoot ? '' : target.name
      );
      
      allFiles.push(...files);
    }
    
    return allFiles;
  }

  /**
   * 遍历目录收集文件
   */
  async walkDirectory(dirPath, excludeDirs, excludePatterns, relativeBase) {
    const files = [];
    const openclawRoot = OPENCLAW_ROOT;
    
    async function walk(currentPath, self) {
      try {
        const entries = await fs.readdir(currentPath, { withFileTypes: true });
        
        for (const entry of entries) {
          const fullPath = path.join(currentPath, entry.name);
          const relativePath = relativeBase 
            ? path.join(relativeBase, path.relative(dirPath, fullPath))
            : path.relative(openclawRoot, fullPath);
          
          // 检查是否在排除目录中
          if (excludeDirs.includes(entry.name)) {
            continue;
          }
          
          // 检查是否匹配排除模式
          if (excludePatterns.some(pattern => this.matchPattern(entry.name, pattern))) {
            continue;
          }
          
          if (entry.isDirectory()) {
            await walk.call(self, fullPath, self);
          } else if (entry.isFile()) {
            // 检查文件是否可读
            try {
              await fs.access(fullPath, fs.constants.R_OK);
              files.push({
                path: fullPath,
                relativePath: relativePath
              });
            } catch (accessErr) {
              if (accessErr.code === 'EBUSY') {
                // 文件被占用，跳过
                continue;
              }
            }
          }
        }
      } catch (err) {
        // 忽略无法访问的目录
      }
    }
    
    await walk.call(this, dirPath, this);
    return files;
  }

  /**
   * 简单的模式匹配
   */
  matchPattern(filename, pattern) {
    const regex = new RegExp('^' + pattern
      .replace(/\./g, '\\.')
      .replace(/\*/g, '.*')
      .replace(/\?/g, '.') + '$');
    
    return regex.test(filename);
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    if (!bytes || bytes === 0) return '0 B';
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  }

  /**
   * 格式化执行时长
   */
  formatDuration(ms) {
    if (!ms) return '0ms';
    if (ms < 1000) return `${ms}ms`;
    if (ms < 60000) return `${(ms / 1000).toFixed(1)} 秒`;
    return `${Math.floor(ms / 60000)} 分 ${Math.floor((ms % 60000) / 1000)} 秒`;
  }

  /**
   * 获取备份状态
   */
  async getStatus() {
    const schedulerStatus = this.scheduler.getStatus();
    const backupStats = await this.cleaner.getStats();
    
    return {
      schedule: schedulerStatus,
      backups: backupStats,
      config: {
        mode: this.config.backup.mode,
        targets: this.config.backup.targets,
        retention: this.config.retention,
        notification: this.config.notification.channels
      }
    };
  }

  /**
   * 检查是否应该执行备份（HEARTBEAT 调用）
   */
  async checkAndRun() {
    const shouldRun = await this.scheduler.shouldRun();
    
    if (shouldRun) {
      await info('Backup', 'HEARTBEAT 触发定时备份');
      return this.execute();
    }
    
    return null;
  }
}

/**
 * 初始化备份管理器
 */
async function initBackupManager() {
  const config = await loadConfig();
  return new BackupManager(config);
}

module.exports = {
  BackupManager,
  initBackupManager,
  OPENCLAW_ROOT
};

path: scripts/cleaner.js
content: /**
 * 清理模块
 * 负责清理旧备份文件
 * v1.0.2 - 只处理本 skill 产生的备份文件
 */

const fs = require('fs-extra');
const path = require('path');
const dayjs = require('dayjs');
const { info, warn, error, debug } = require('./logger');

/**
 * 创建清理器实例
 */
class Cleaner {
  constructor(config) {
    this.config = config;
  }

  /**
   * 执行清理
   */
  async execute() {
    try {
      if (!this.config.retention.enabled) {
        await info('Cleaner', '保留策略未启用，跳过清理');
        return { deleted: 0, kept: 0, skipped: true };
      }
      
      await info('Cleaner', '开始清理旧备份...');
      
      // 获取本 skill 的备份文件
      const backups = await this.getBackupFiles();
      
      if (backups.length === 0) {
        await info('Cleaner', '没有找到本 skill 的备份文件');
        return { deleted: 0, kept: 0, skipped: false };
      }
      
      await info('Cleaner', `当前备份数: ${backups.length} 个`);
      
      // 选择要删除的文件
      const toDelete = this.selectToDelete(backups);
      
      if (toDelete.length === 0) {
        await info('Cleaner', '无需清理，当前备份数未超出保留策略');
        return { deleted: 0, kept: backups.length, skipped: false };
      }
      
      await info('Cleaner', `准备删除 ${toDelete.length} 个旧备份`);
      
      // 执行删除
      let deletedCount = 0;
      for (const file of toDelete) {
        try {
          await fs.remove(file.path);
          deletedCount++;
          await debug('Cleaner', `已删除: ${file.name}`);
        } catch (err) {
          await warn('Cleaner', `删除失败 ${file.name}: ${err.message}`);
        }
      }
      
      const keptCount = backups.length - deletedCount;
      
      await info('Cleaner', `清理完成: 删除 ${deletedCount} 个，保留 ${keptCount} 个`);
      
      return {
        deleted: deletedCount,
        kept: keptCount,
        skipped: false
      };
    } catch (err) {
      await error('Cleaner', `清理失败: ${err.message}`);
      return { deleted: 0, kept: 0, error: err.message };
    }
  }

  /**
   * 获取本 skill 的备份文件
   * 只返回文件名以配置的前缀开头的备份文件
   */
  async getBackupFiles() {
    try {
      const outputDir = this.config.output.path;
      const prefix = this.config.output.naming.prefix || 'auto-backup-openclaw-user-data';
      
      if (!(await fs.pathExists(outputDir))) {
        return [];
      }
      
      const files = await fs.readdir(outputDir);
      const backups = [];
      
      for (const file of files) {
        // 只处理 .zip 文件
        if (!file.endsWith('.zip')) continue;
        
        // 只处理以本 skill 前缀开头的文件
        if (!file.startsWith(prefix)) continue;
        
        const filePath = path.join(outputDir, file);
        
        try {
          const stats = await fs.stat(filePath);
          
          backups.push({
            name: file,
            path: filePath,
            size: stats.size,
            modifiedTime: stats.mtime.getTime(),
            modifiedDate: dayjs(stats.mtime).format('YYYY-MM-DD HH:mm:ss')
          });
        } catch (err) {
          // 忽略无法访问的文件
        }
      }
      
      // 按修改时间排序（最新的在前）
      backups.sort((a, b) => b.modifiedTime - a.modifiedTime);
      
      return backups;
    } catch (err) {
      await error('Cleaner', `获取备份文件列表失败: ${err.message}`);
      return [];
    }
  }

  /**
   * 选择要删除的备份
   */
  selectToDelete(backups) {
    if (this.config.retention.mode === 'days') {
      return this.selectByDays(backups);
    } else {
      return this.selectByCount(backups);
    }
  }

  /**
   * 按天数选择
   */
  selectByDays(backups) {
    const cutoff = Date.now() - this.config.retention.days * 24 * 60 * 60 * 1000;
    
    return backups.filter(b => b.modifiedTime < cutoff);
  }

  /**
   * 按份数选择
   */
  selectByCount(backups) {
    const keepCount = this.config.retention.count;
    
    // 已经按时间排序，保留最新的 N 个，删除其余的
    if (backups.length <= keepCount) {
      return [];
    }
    
    return backups.slice(keepCount);
  }

  /**
   * 获取清理预览
   */
  async getPreview() {
    const backups = await this.getBackupFiles();
    const toDelete = this.selectToDelete(backups);
    
    return {
      total: backups.length,
      toDeleteCount: toDelete.length,
      toKeepCount: backups.length - toDelete.length,
      toDeleteFiles: toDelete.map(f => ({
        name: f.name,
        size: this.formatSize(f.size),
        date: f.modifiedDate
      })),
      policy: {
        mode: this.config.retention.mode,
        days: this.config.retention.days,
        count: this.config.retention.count
      }
    };
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  }

  /**
   * 获取备份统计
   */
  async getStats() {
    const backups = await this.getBackupFiles();
    
    const totalSize = backups.reduce((sum, b) => sum + b.size, 0);
    
    return {
      count: backups.length,
      totalSize: this.formatSize(totalSize),
      totalSizeBytes: totalSize,
      oldestBackup: backups.length > 0 ? backups[backups.length - 1].modifiedDate : null,
      newestBackup: backups.length > 0 ? backups[0].modifiedDate : null,
      files: backups.slice(0, 20).map(b => ({
        name: b.name,
        size: this.formatSize(b.size),
        date: b.modifiedDate
      }))
    };
  }
}

module.exports = { Cleaner };

path: scripts/cli.js
content: /**
 * CLI 模块
 * 命令行接口入口
 * v1.0.2 - 增加选择性备份文件选择交互
 */

const fs = require('fs-extra');
const path = require('path');
const os = require('os');

const { 
  loadConfig, 
  saveConfig, 
  resetConfig, 
  getConfigPaths, 
  configExists,
  ensureConfigDir,
  updateConfig,
  loadOpenClawConfig,
  getChannelTargets,
  BACKUPS_DIR 
} = require('./config');
const { info, warn, error, readLogs } = require('./logger');
const { BackupManager, initBackupManager, OPENCLAW_ROOT } = require('./backup');
const { Cleaner } = require('./cleaner');
const { describeCron } = require('./scheduler');

/**
 * CLI 命令处理器
 */
class CLI {
  constructor() {
    this.backupManager = null;
  }

  /**
   * 初始化
   */
  async init() {
    this.backupManager = await initBackupManager();
  }

  /**
   * 执行命令
   */
  async execute(command, args = {}) {
    // 确保初始化
    if (!this.backupManager) {
      await this.init();
    }

    switch (command) {
      case 'now':
      case 'backup_now':
        return this.cmdBackupNow(args);
      
      case 'status':
      case 'backup_status':
        return this.cmdBackupStatus();
      
      case 'config':
      case 'backup_config':
        return this.cmdBackupConfig(args);
      
      case 'list':
      case 'backup_list':
        return this.cmdBackupList(args);
      
      case 'clean':
      case 'backup_clean':
        return this.cmdBackupClean(args);
      
      case 'help':
        return this.cmdHelp();
      
      default:
        return this.outputError(`未知命令: ${command}`);
    }
  }

  /**
   * /backup_now - 立即执行备份
   */
  async cmdBackupNow(args) {
    const output = [];
    output.push('🔄 开始执行备份...');
    output.push('');
    
    try {
      const options = {
        full: args.full !== false,
        targets: args.targets
      };
      
      const result = await this.backupManager.execute(options);
      
      if (result.success) {
        output.push('✅ 备份完成！');
        output.push('');
        output.push(`📁 文件：${path.basename(result.outputPath)}`);
        output.push(`📊 文件数量：${result.filesTotal} 个`);
        output.push(`📏 大小：${this.formatSize(result.sizeBytes)}`);
        output.push(`⏱️ 耗时：${this.formatDuration(result.duration)}`);
        
        if (result.cleanResult && result.cleanResult.deleted > 0) {
          output.push(`🗑️ 清理旧备份：${result.cleanResult.deleted} 个`);
        }
      } else {
        output.push('❌ 备份失败！');
        output.push('');
        output.push(`错误：${result.errors.join('; ')}`);
      }
      
      return this.outputSuccess(output.join('\n'), result);
    } catch (err) {
      return this.outputError(`备份失败: ${err.message}`);
    }
  }

  /**
   * /backup_status - 查看备份状态
   */
  async cmdBackupStatus() {
    const output = [];
    
    try {
      const status = await this.backupManager.getStatus();
      const config = await loadConfig();
      
      output.push('📊 备份状态');
      output.push('━'.repeat(40));
      output.push('');
      
      // 调度状态
      output.push('🔄 调度状态');
      output.push(`  - 定时备份：${status.schedule.enabled ? '已启用' : '已禁用'}`);
      
      if (status.schedule.enabled) {
        output.push(`  - 执行时间：${describeCron(status.schedule.cron)}`);
        output.push(`  - 下次执行：${status.schedule.nextRun || '计算中...'}`);
      }
      
      if (status.schedule.lastRun) {
        output.push(`  - 上次执行：${new Date(status.schedule.lastRun).toLocaleString('zh-CN')}`);
      }
      
      output.push('');
      
      // 备份统计
      output.push('📦 备份统计');
      output.push(`  - 总备份数：${status.backups.count} 个`);
      output.push(`  - 总大小：${status.backups.totalSize}`);
      
      if (status.backups.newestBackup) {
        output.push(`  - 最近备份：${status.backups.newestBackup}`);
      }
      
      output.push('');
      
      // 存储位置
      output.push('📁 存储位置');
      output.push(`  ${this.backupManager.config.output.path}`);
      output.push('');
      
      // 当前配置
      output.push('⚙️ 当前配置');
      output.push(`  - 备份模式：${status.config.mode === 'full' ? '全量备份' : '选择性备份'}`);
      output.push(`  - 保留策略：${status.config.retention.mode === 'days' ? `${status.config.retention.days} 天` : `${status.config.retention.count} 份`}`);
      
      // 通知配置
      const notification = config.notification;
      const channels = notification.channels || [];
      const targets = notification.targets || {};
      
      let notificationInfo = channels.map(ch => {
        const t = targets[ch] || [];
        if (t.length > 0) {
          return `${ch}(${t.length}个目标)`;
        }
        return ch;
      }).join(', ') || '未配置';
      
      output.push(`  - 通知渠道：${notificationInfo}`);
      
      return this.outputSuccess(output.join('\n'), status);
    } catch (err) {
      return this.outputError(`获取状态失败: ${err.message}`);
    }
  }

  /**
   * /backup_config - 配置向导
   */
  async cmdBackupConfig(args) {
    const output = [];
    
    try {
      // 检查配置文件是否存在
      const exists = await configExists();
      
      if (args.mode === 'show') {
        // 显示当前配置
        const config = await loadConfig();
        output.push('📋 当前配置');
        output.push('━'.repeat(40));
        output.push('');
        output.push('```json');
        output.push(JSON.stringify(config, null, 2));
        output.push('```');
        output.push('');
        output.push(`📁 配置文件：${getConfigPaths().configFile}`);
        
        return this.outputSuccess(output.join('\n'), { config });
      }
      
      if (args.mode === 'reset') {
        // 重置配置
        await resetConfig();
        output.push('✅ 配置已重置为默认值');
        output.push(`📁 配置文件：${getConfigPaths().configFile}`);
        
        return this.outputSuccess(output.join('\n'));
      }
      
      if (args.mode === 'manual') {
        // 手动配置指引
        output.push('📝 手动修改配置文件');
        output.push('━'.repeat(40));
        output.push('');
        output.push('配置文件路径：');
        output.push(`  ${getConfigPaths().configFile}`);
        output.push('');
        output.push('操作步骤：');
        output.push('  1. 使用文本编辑器打开配置文件');
        output.push('  2. 修改需要的配置项');
        output.push('  3. 保存文件');
        output.push('  4. 运行 /backup_config 验证配置');
        output.push('');
        output.push('💡 提示：修改后运行验证确保格式正确');
        
        return this.outputSuccess(output.join('\n'));
      }
      
      if (args.mode === 'interactive' || !args.mode) {
        // 交互式配置 - 返回配置菜单
        output.push('📋 备份配置向导');
        output.push('━'.repeat(40));
        output.push('');
        output.push('请选择配置方式：');
        output.push('');
        output.push('[1] 交互式配置（推荐）');
        output.push('[2] 手动修改配置文件');
        output.push('[3] 重置为默认配置');
        output.push('[4] 查看当前配置');
        output.push('');
        output.push('请输入选项编号或具体配置项，例如：');
        output.push('  - backup_config 1  （选择交互式配置）');
        output.push('  - backup_config mode=partial  （设置备份模式）');
        output.push('  - backup_config time=03:00  （设置执行时间）');
        output.push('  - backup_config days=30  （设置保留天数）');
        
        return this.outputSuccess(output.join('\n'));
      }
      
      // 快捷配置
      if (args.mode === 'set' && args.key && args.value !== undefined) {
        const config = await loadConfig();
        
        // 根据键更新配置
        switch (args.key) {
          case 'mode':
            config.backup.mode = args.value;
            break;
          case 'time':
            const [hour, minute] = args.value.split(':').map(Number);
            config.schedule.cron = `${minute || 0} ${hour || 3} * * *`;
            break;
          case 'days':
            config.retention.mode = 'days';
            config.retention.days = parseInt(args.value);
            break;
          case 'count':
            config.retention.mode = 'count';
            config.retention.count = parseInt(args.value);
            break;
          default:
            return this.outputError(`未知的配置项: ${args.key}`);
        }
        
        await saveConfig(config);
        output.push(`✅ 已更新配置：${args.key} = ${args.value}`);
        
        return this.outputSuccess(output.join('\n'), { config });
      }
      
      // 选项式配置
      if (args.option) {
        switch (args.option) {
          case '1':
            // 交互式配置的详细步骤
            return this.interactiveConfig();
          
          case '2':
            return this.cmdBackupConfig({ mode: 'manual' });
          
          case '3':
            return this.cmdBackupConfig({ mode: 'reset' });
          
          case '4':
            return this.cmdBackupConfig({ mode: 'show' });
          
          default:
            return this.outputError(`无效的选项: ${args.option}`);
        }
      }
      
    } catch (err) {
      return this.outputError(`配置失败: ${err.message}`);
    }
  }

  /**
   * 交互式配置
   */
  async interactiveConfig() {
    const output = [];

    output.push('📋 交互式配置向导');
    output.push('━'.repeat(40));
    output.push('');
    output.push('Step 1/8: 备份范围');
    output.push('  [1] 全量备份 .openclaw');
    output.push('  [2] 选择性备份');
    output.push('');
    output.push('请回复选项编号继续配置：');

    // 注意：这是一个交互式的开始，需要用户响应后继续
    // 实际实现中，这里会保存状态并等待用户下一步输入

    return this.outputSuccess(output.join('\n'), {
      step: 1,
      total: 8,
      type: 'interactive'
    });
  }

  /**
   * 处理交互式配置步骤
   * @param {number} step 当前步骤
   * @param {string} input 用户输入
   * @param {object} state 配置状态
   */
  async handleInteractiveStep(step, input, state = {}) {
    const output = [];
    
    switch (step) {
      case 1: // 备份范围
        if (input === '1') {
          // 全量备份 - 直接进入下一步
          state.mode = 'full';
          return this.askBackupTime(state);
        } else if (input === '2') {
          // 选择性备份 - 进入文件选择流程
          state.mode = 'partial';
          return this.askBackupFiles(state);
        } else {
          return this.outputError('请输入 1 或 2');
        }
        
      // ===== 选择性备份文件选择流程 =====
      case 1.1: // 文件选择输入
        return this.handleFileSelection(input, state);
        
      case 1.2: // 确认选择
        return this.handleFileConfirm(input, state);
        
      case 2: // 备份时间
        if (input.toLowerCase() === 'y') {
          output.push('Step 2/8: 备份时间');
          output.push('━'.repeat(40));
          output.push('');
          output.push('请输入执行时间（格式：HH:MM，如 03:00）：');
          output.push('');
          output.push('请直接回复时间：');

          return this.outputSuccess(output.join('\n'), { step: 2.1, total: 8, state, type: 'interactive' });
        }

        // 继续下一步
        state.time = '03:00';
        return this.askStoragePath(state);

      case 2.1: // 输入具体时间
        const timeMatch = input.match(/^(\d{1,2}):(\d{2})$/);
        if (!timeMatch) {
          return this.outputError('时间格式无效，请使用 HH:MM 格式（如 03:00）');
        }
        state.time = input;
        return this.askStoragePath(state);

      case 3: // 存储路径选择
        if (input.toLowerCase() === 'y') {
          output.push('Step 3/8: 存储路径');
          output.push('━'.repeat(40));
          output.push('');
          output.push('请输入新的备份存储路径：');
          output.push('');
          output.push('示例格式：');
          output.push('  D:\\Backups\\openclaw');
          output.push('  /home/user/backups');
          output.push('');
          output.push('请直接回复路径，或回复 c 取消：');

          return this.outputSuccess(output.join('\n'), { step: 3.1, total: 8, state, type: 'interactive' });
        }

        // 使用默认路径
        state.outputPath = null;
        return this.askRetention(state);
        
      case 3.1: // 输入具体路径
        if (input.toLowerCase() === 'c') {
          return this.askStoragePath(state);
        }
        state.outputPath = input;
        return this.askRetention(state);
        
      case 4: // 保留策略选择
        if (input === '1') {
          state.retentionMode = 'days';
          output.push('Step 4/8: 保留策略');
          output.push('━'.repeat(40));
          output.push('');
          output.push('请输入保留天数（默认 30 天）：');
          output.push('');
          output.push('请直接回复数字，或回复 d 使用默认值：');

          return this.outputSuccess(output.join('\n'), { step: 4.1, total: 8, state, type: 'interactive' });
        } else if (input === '2') {
          state.retentionMode = 'count';
          output.push('Step 4/8: 保留策略');
          output.push('━'.repeat(40));
          output.push('');
          output.push('请输入保留份数（默认 10 份）：');
          output.push('');
          output.push('建议范围: 5 - 30 份');
          output.push('');
          output.push('请直接回复数字，或回复 d 使用默认值：');

          return this.outputSuccess(output.join('\n'), { step: 4.2, total: 8, state, type: 'interactive' });
        }
        return this.outputError('请输入 1 或 2');
        
      case 4.1: // 按天数
        if (input.toLowerCase() === 'd') {
          state.retentionValue = 30;
        } else {
          const days = parseInt(input);
          if (isNaN(days) || days < 1) {
            return this.outputError('请输入有效的天数（大于 0 的数字）');
          }
          state.retentionValue = days;
        }
        return this.askNotificationChannel(state);
        
      case 4.2: // 按份数
        if (input.toLowerCase() === 'd') {
          state.retentionValue = 10;
        } else {
          const count = parseInt(input);
          if (isNaN(count) || count < 1) {
            return this.outputError('请输入有效的份数（大于 0 的数字）');
          }
          state.retentionValue = count;
        }
        return this.askNotificationChannel(state);
        
      case 5: // 选择通知渠道
        const selectedChannels = input.split(/\s+/).filter(s => s);
        const channelMap = { '1': 'feishu', '2': 'telegram', '3': 'discord', '4': 'slack' };
        
        state.selectedChannels = selectedChannels.map(c => channelMap[c]).filter(c => c);
        
        if (state.selectedChannels.length === 0) {
          return this.outputError('请至少选择一个渠道');
        }
        
        // 开始配置每个渠道的推送目标
        state.channelIndex = 0;
        return this.askChannelTargets(state);
        
      case 6: // 选择推送目标
        const selectedTargets = input.split(/\s+/).filter(s => s);
        const currentChannel = state.selectedChannels[state.channelIndex];

        if (selectedTargets.length > 0 && state.availableTargets) {
          state.targets = state.targets || {};
          state.targets[currentChannel] = selectedTargets.map(idx =>
            state.availableTargets[parseInt(idx) - 1]
          ).filter(t => t);
        }

        state.channelIndex++;

        if (state.channelIndex < state.selectedChannels.length) {
          return this.askChannelTargets(state);
        }

        // 所有渠道配置完成，进入敏感文件配置
        return this.askSensitiveFilesConfig(state);

      case 7:  // 敏感文件排除配置
        return this.handleSensitiveExcludeInput(input, state);

      case 7.1:  // 手动配置确认
        return this.handleManualConfigConfirm(input, state);

      case 8:  // ZIP加密配置
        return this.handleEncryptionInput(input, state);

      case 8.1:  // 密码输入
        return this.handlePasswordInput(input, state);

      case 8.2:  // 随机密码确认
        return this.handleRandomPasswordConfirm(input, state);

      case 8.3:  // 密码确认
        return this.handlePasswordConfirm(input, state);

      default:
        return this.outputError('未知步骤');
    }
  }

  /**
   * 询问备份时间
   */
  async askBackupTime(state) {
    const output = [];

    output.push('Step 2/8: 备份时间');
    output.push('━'.repeat(40));
    output.push('');
    output.push('当前设置: 每天凌晨 3:00');
    output.push('');
    output.push('是否修改执行时间？');
    output.push('  [y] 是，修改时间');
    output.push('  [n] 否，保持默认');
    output.push('');
    output.push('请回复 y 或 n：');

    return this.outputSuccess(output.join('\n'), { step: 2, total: 8, state, type: 'interactive' });
  }

  /**
   * 询问选择性备份文件（新增）
   * 列出 ~/.openclaw/ 目录下的文件和文件夹供用户选择
   */
  async askBackupFiles(state) {
    const output = [];

    output.push('Step 1/8: 选择备份文件');
    output.push('━'.repeat(40));
    output.push('');
    output.push('正在读取 ~/.openclaw/ 目录...');
    output.push('');

    try {
      // 读取 OpenClaw 根目录下的文件和文件夹
      const openclawRoot = OPENCLAW_ROOT;
      const entries = await fs.readdir(openclawRoot, { withFileTypes: true });

      // 排除默认不需要备份的目录
      const excludeDefault = ['logs', 'cache', 'tmp', 'node_modules', 'completions', 'delivery-queue', 'exec-approvals.json'];

      // 过滤并分类
      const folders = [];
      const files = [];

      for (const entry of entries) {
        if (excludeDefault.includes(entry.name)) continue;
        if (entry.name.startsWith('.')) continue; // 隐藏文件跳过

        if (entry.isDirectory()) {
          folders.push({
            name: entry.name,
            type: 'folder',
            selected: false
          });
        } else if (entry.isFile()) {
          files.push({
            name: entry.name,
            type: 'file',
            selected: false
          });
        }
      }

      // 排序
      folders.sort((a, b) => a.name.localeCompare(b.name));
      files.sort((a, b) => a.name.localeCompare(b.name));

      // 保存到 state
      state.availableFiles = [...folders, ...files];

      if (state.availableFiles.length === 0) {
        output.push('⚠️ 未找到可备份的文件或文件夹');
        output.push('');
        output.push('请检查 ~/.openclaw/ 目录是否存在');
        output.push('');
        output.push('按任意键返回上一步...');
        return this.outputSuccess(output.join('\n'), { step: 1, total: 8, state, type: 'interactive' });
      }

      output.push('📋 文件列表（目录 + 文件）：');
      output.push('');
      output.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

      // 显示文件夹
      if (folders.length > 0) {
        output.push('');
        output.push('📁 文件夹：');
        folders.forEach((item, idx) => {
          output.push(`  [${idx + 1}] [ ] 📁 ${item.name}`);
        });
      }

      // 显示文件
      if (files.length > 0) {
        output.push('');
        output.push('📄 文件：');
        files.forEach((item, idx) => {
          const fileIdx = folders.length + idx + 1;
          output.push(`  [${fileIdx}] [ ] 📄 ${item.name}`);
        });
      }

      output.push('');
      output.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
      output.push('');
      output.push('📝 选择说明：');
      output.push('');
      output.push('  选择样式：输入编号，用空格分隔，如：1 3 5 7');
      output.push('  示例：选择第 1、3、5 项 → 输入 1 3 5');
      output.push('');
      output.push('  已选择后会显示：[×] 或 [√]');
      output.push('');
      output.push('请输入要备份的文件/文件夹编号：');

      return this.outputSuccess(output.join('\n'), { step: 1.1, total: 8, state, type: 'interactive' });

    } catch (err) {
      output.push(`❌ 读取目录失败: ${err.message}`);
      output.push('');
      output.push('按任意键返回上一步...');
      return this.outputSuccess(output.join('\n'), { step: 1, total: 8, state, type: 'interactive' });
    }
  }

  /**
   * 处理文件选择输入（新增）
   */
  async handleFileSelection(input, state) {
    const output = [];

    // 解析用户输入的编号
    const selectedIndices = input.split(/\s+/)
      .map(s => parseInt(s.trim()))
      .filter(n => !isNaN(n) && n > 0 && n <= state.availableFiles.length);

    if (selectedIndices.length === 0) {
      return this.outputError('请输入有效的编号（如：1 3 5）');
    }

    // 更新选择状态
    state.selectedFiles = selectedIndices.map(idx => state.availableFiles[idx - 1]);

    // 显示确认界面
    output.push('Step 1/8: 确认选择');
    output.push('━'.repeat(40));
    output.push('');
    output.push('您已选择以下文件/文件夹：');
    output.push('');
    output.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    state.selectedFiles.forEach((item, idx) => {
      const icon = item.type === 'folder' ? '📁' : '📄';
      output.push(`  [√] ${icon} ${item.name}`);
    });

    output.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    output.push('');
    output.push(`共选择 ${state.selectedFiles.length} 项`);
    output.push('');
    output.push('请确认选择：');
    output.push('  [1] 确认，继续配置');
    output.push('  [2] 取消，重新选择');
    output.push('');
    output.push('请回复 1 或 2：');

    return this.outputSuccess(output.join('\n'), { step: 1.2, total: 8, state, type: 'interactive' });
  }

  /**
   * 处理文件确认（新增）
   */
  async handleFileConfirm(input, state) {
    const output = [];
    
    if (input === '1') {
      // 确认选择 - 进入下一步
      // 将选择的文件名保存到 state.targets
      state.backupTargets = state.selectedFiles.map(f => f.name);
      return this.askBackupTime(state);
      
    } else if (input === '2') {
      // 重新选择 - 返回文件列表
      state.selectedFiles = [];
      return this.askBackupFiles(state);
      
    } else {
      return this.outputError('请输入 1 或 2');
    }
  }

  /**
   * 询问存储路径
   */
  async askStoragePath(state) {
    const output = [];

    output.push('Step 3/8: 存储路径');
    output.push('━'.repeat(40));
    output.push('');
    output.push('当前设置: ');
    output.push('  ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backups');
    output.push('');
    output.push('是否修改存储路径？');
    output.push('  [y] 是，修改路径');
    output.push('  [n] 否，保持默认');
    output.push('');
    output.push('请回复 y 或 n：');

    return this.outputSuccess(output.join('\n'), { step: 3, total: 8, state, type: 'interactive' });
  }

  /**
   * 询问保留策略
   */
  async askRetention(state) {
    const output = [];

    output.push('Step 4/8: 保留策略');
    output.push('━'.repeat(40));
    output.push('');
    output.push('请选择清理模式：');
    output.push('');
    output.push('[1] 按天数保留');
    output.push('    保留最近 N 天的备份（默认 30 天）');
    output.push('');
    output.push('[2] 按份数保留');
    output.push('    保留最近 N 份备份（默认 10 份）');
    output.push('');
    output.push('请回复选项编号：');

    return this.outputSuccess(output.join('\n'), { step: 4, total: 8, state, type: 'interactive' });
  }

  /**
   * 询问通知渠道
   */
  async askNotificationChannel(state) {
    const output = [];

    // 加载 OpenClaw 配置
    const openclawConfig = await loadOpenClawConfig();

    output.push('Step 5/8: 通知渠道');
    output.push('━'.repeat(40));
    output.push('');
    output.push('正在检测 OpenClaw 已配置的渠道...');
    output.push('');

    // 显示可用渠道
    const channels = [];
    if (openclawConfig.channels.feishu?.enabled) {
      channels.push({ key: '1', name: 'feishu', label: '飞书' });
    }
    if (openclawConfig.channels.telegram?.enabled) {
      channels.push({ key: '2', name: 'telegram', label: 'Telegram' });
    }
    if (openclawConfig.channels.discord?.enabled) {
      channels.push({ key: '3', name: 'discord', label: 'Discord' });
    }
    if (openclawConfig.channels.slack?.enabled) {
      channels.push({ key: '4', name: 'slack', label: 'Slack' });
    }

    if (channels.length === 0) {
      output.push('⚠️ 未检测到已配置的渠道');
      output.push('');
      output.push('请先在 OpenClaw 中配置至少一个通信渠道。');
      output.push('配置文件位置：~/.openclaw/openclaw.json');
      output.push('');
      output.push('配置完成后，运行 /backup_config 重新配置通知。');

      // 跳过通知配置，直接保存
      state.selectedChannels = [];
      return this.saveInteractiveConfig(state);
    }

    output.push('✅ 已检测到可用渠道：');
    output.push('');
    channels.forEach(ch => {
      output.push(`  [${ch.key}] ${ch.name} - ${ch.label}`);
    });
    output.push('');
    output.push('请选择要启用的通知渠道（输入编号，可多选，如：1 2）：');

    return this.outputSuccess(output.join('\n'), { step: 5, total: 8, state, type: 'interactive' });
  }

  /**
   * 询问渠道推送目标
   */
  async askChannelTargets(state) {
    const output = [];
    const channel = state.selectedChannels[state.channelIndex];
    const channelNames = { feishu: '飞书', telegram: 'Telegram', discord: 'Discord', slack: 'Slack' };

    output.push(`Step 6/8: 配置 ${channelNames[channel]} 推送目标`);
    output.push('━'.repeat(40));
    output.push('');

    // 加载可用目标
    const openclawConfig = await loadOpenClawConfig();
    const targets = getChannelTargets(channel, openclawConfig);

    if (targets.length === 0) {
      output.push(`⚠️ 未检测到 ${channelNames[channel]} 的可用推送目标`);
      output.push('');
      output.push('可能原因：');
      output.push('  - OpenClaw 未绑定任何群组或用户');
      output.push('  - 渠道配置不完整');
      output.push('');
      output.push('将跳过此渠道的推送目标配置。');
      output.push('');
      output.push('请回复任意键继续：');

      state.availableTargets = [];
    } else {
      output.push(`${channelNames[channel]} 推送目标列表：`);
      output.push('');

      targets.forEach((t, idx) => {
        const icon = t.type === 'group' ? '📢' : '👤';
        output.push(`  [${idx + 1}] ${icon} ${t.name || t.id}（${t.type === 'group' ? '群组' : '用户'}）`);
      });

      output.push('');
      output.push('请选择推送目标（输入编号，可多选，如：1 2 3）：');

      state.availableTargets = targets;
    }

    return this.outputSuccess(output.join('\n'), { step: 6, total: 8, state, type: 'interactive' });
  }

  /**
   * Step 7/8: 敏感文件排除配置（新增）
   */
  async askSensitiveFilesConfig(state) {
    const output = [];

    output.push('Step 7/8: 敏感文件排除配置');
    output.push('━'.repeat(40));
    output.push('');
    output.push('⚠️ 安全提醒：');
    output.push('');
    output.push('您的备份可能包含以下敏感文件：');
    output.push('');
    output.push('📁 敏感目录（可能包含密钥、凭证）：');
    output.push('  - .ssh, .gnupg（SSH密钥）');
    output.push('  - credentials, secrets（凭证目录）');
    output.push('');
    output.push('📄 敏感文件（可能包含Token、密钥）：');
    output.push('  - *.key, *.pem, *.p12, *.pfx（密钥文件）');
    output.push('  - .env, .env.local（环境变量）');
    output.push('  - credentials.json, secrets.json（凭证文件）');
    output.push('  - *_token.json, *_key.json（敏感配置）');
    output.push('');
    output.push('💡 风险说明：');
    output.push('  如果备份中包含敏感文件，可能导致：');
    output.push('  - API Token泄露');
    output.push('  - 密钥文件泄露');
    output.push('  - 环境变量泄露');
    output.push('  - 用户隐私数据泄露');
    output.push('');
    output.push('是否启用敏感文件排除？');
    output.push('  [1] 启用排除 - 添加敏感文件到排除列表');
    output.push('  [2] 不启用 - 保持当前排除列表（仅排除临时文件）');
    output.push('  [3] 自定义 - 手动编辑排除列表');
    output.push('');
    output.push('请回复选项编号：');

    return this.outputSuccess(output.join('\n'), {
      step: 7,
      total: 8,
      state,
      type: 'interactive'
    });
  }

  /**
   * 处理敏感文件排除输入（新增）
   */
  async handleSensitiveExcludeInput(input, state) {
    const output = [];
    const { loadConfig } = require('./config');

    if (input === '1') {
      // 启用排除 - 添加敏感文件到排除列表
      state.enableSensitiveExclude = true;

      // 加载默认配置获取建议列表
      const config = await loadConfig();

      // 将建议的敏感文件添加到排除列表
      state.excludePatterns = [
        ...config.backup.excludePatterns,
        ...config.backup.sensitiveExcludeSuggestion
      ];
      state.exclude = [
        ...config.backup.exclude,
        ...config.backup.sensitiveExcludeDirectories
      ];

      output.push('✅ 已启用敏感文件排除');
      output.push('');
      output.push('排除列表已更新：');
      output.push(`  - 目录：${state.exclude.length} 个`);
      output.push(`  - 文件模式：${state.excludePatterns.length} 个`);
      output.push('');
      output.push('继续下一步加密配置...');

      return this.outputSuccess(output.join('\n'), {
        step: 7,
        total: 8,
        state,
        type: 'interactive'
      });

    } else if (input === '2') {
      // 不启用 - 保持默认
      state.enableSensitiveExclude = false;

      output.push('已选择不启用敏感文件排除');
      output.push('');
      output.push('当前排除列表仅包含临时文件：');
      output.push('  - logs, cache, tmp, node_modules');
      output.push('  - *.log, *.tmp, .DS_Store, Thumbs.db');
      output.push('');
      output.push('继续下一步加密配置...');

      return this.outputSuccess(output.join('\n'), {
        step: 7,
        total: 8,
        state,
        type: 'interactive'
      });

    } else if (input === '3') {
      // 自定义 - 显示手动编辑说明
      output.push('📝 手动编辑排除列表');
      output.push('━'.repeat(40));
      output.push('');
      output.push('配置文件位置：');
      output.push(`  ${getConfigPaths().configFile}`);
      output.push('');
      output.push('编辑方法：');
      output.push('  1. 打开config.json文件');
      output.push('  2. 编辑 backup.exclude 或 backup.excludePatterns');
      output.push('  3. 添加需要排除的文件模式');
      output.push('');
      output.push('示例：');
      output.push('  "exclude": ["logs", "cache", "*.key"]');
      output.push('  "excludePatterns": ["*.pem", ".env", "credentials.json"]');
      output.push('');
      output.push('请回复 c 继续配置：');

      state.enableSensitiveExclude = false;
      state.manualConfig = true;

      return this.outputSuccess(output.join('\n'), {
        step: 7.1,
        total: 8,
        state,
        type: 'interactive'
      });

    } else {
      return this.outputError('请输入 1、2 或 3');
    }
  }

  /**
   * 处理手动配置确认（新增）
   */
  async handleManualConfigConfirm(input, state) {
    if (input.toLowerCase() === 'c') {
      return this.askEncryptionConfig(state);
    } else {
      return this.outputError('请回复 c 继续配置');
    }
  }

  /**
   * Step 8/8: ZIP加密配置（新增）
   */
  async askEncryptionConfig(state) {
    const output = [];

    output.push('Step 8/8: ZIP加密配置');
    output.push('━'.repeat(40));
    output.push('');
    output.push('是否启用ZIP加密功能？');
    output.push('');
    output.push('  [1] 启用加密 - 使用密码保护备份文件');
    output.push('  [2] 不启用加密 - 备份文件不加密（默认）');
    output.push('');
    output.push('请回复选项编号：');

    return this.outputSuccess(output.join('\n'), {
      step: 8,
      total: 8,
      state,
      type: 'interactive'
    });
  }

  /**
   * 处理加密配置输入（新增）
   */
  async handleEncryptionInput(input, state) {
    const output = [];

    if (input === '1') {
      // 启用加密 - 显示加密提醒
      output.push('⚠️ 重要提醒：ZIP加密功能使用须知');
      output.push('━'.repeat(40));
      output.push('');
      output.push('加密方式：AES-256加密算法');
      output.push('密码存储：存储在config.json配置文件中');
      output.push('');
      output.push('解密兼容性说明：');
      output.push('  ✅ 支持工具：7-Zip、WinRAR、PeaZip');
      output.push('  ❌ 不支持工具：');
      output.push('     - macOS Finder原生解压');
      output.push('     - Windows资源管理器原生解压');
      output.push('     - Linux unzip命令（部分版本）');
      output.push('');
      output.push('解密方法：');
      output.push('  Windows: 右键文件 → 7-Zip → 解压 → 输入密码');
      output.push('  macOS: 打开7-Zip或WinRAR → 选择文件 → 解压 → 输入密码');
      output.push('  Linux: 7z x backup.zip -p密码');
      output.push('');
      output.push('⚠️ 重要风险提示：');
      output.push('  1. 密码丢失将无法解密备份文件');
      output.push('  2. config.json文件损坏或丢失将无法找回密码');
      output.push('  3. 建议将密码备份到安全位置（如密码管理器）');
      output.push('  4. 密码存储在本地配置文件，需妥善保护config.json');
      output.push('');
      output.push('请设置加密密码：');
      output.push('  输入密码（至少8位，建议包含字母+数字+符号）：');
      output.push('  或输入 \'d\' 使用默认随机密码（系统将生成并显示）');
      output.push('');
      output.push('请直接回复：');

      state.encryptionEnabled = true;
      return this.outputSuccess(output.join('\n'), {
        step: 8.1,
        total: 8,
        state,
        type: 'interactive'
      });

    } else if (input === '2') {
      // 不启用加密
      state.encryptionEnabled = false;
      return this.saveInteractiveConfig(state);  // 直接保存配置

    } else {
      return this.outputError('请输入 1 或 2');
    }
  }

  /**
   * 处理密码输入（新增）
   */
  async handlePasswordInput(input, state) {
    const output = [];

    if (input.toLowerCase() === 'd') {
      // 使用随机密码
      const randomPassword = this.generateRandomPassword();

      output.push('系统已生成随机密码：');
      output.push('━'.repeat(40));
      output.push('');
      output.push(`  密码：${randomPassword}`);
      output.push('');
      output.push('⚠️ 请务必记录此密码！');
      output.push('建议：复制密码并保存到安全位置（如密码管理器）。');
      output.push('');
      output.push('密码丢失风险：');
      output.push('  - 无法解密备份文件');
      output.push('  - 数据将永久无法访问');
      output.push('');
      output.push('请确认已记录密码后继续：');
      output.push('  回复 \'c\' 确认已记录：');

      state.encryptionPassword = randomPassword;
      return this.outputSuccess(output.join('\n'), {
        step: 8.2,
        total: 8,
        state,
        type: 'interactive'
      });

    } else if (input.length >= 8) {
      // 用户自定义密码
      output.push('密码已设置：' + input);
      output.push('');
      output.push('⚠️ 请务必妥善保存密码！');
      output.push('建议：将密码记录到密码管理器或其他安全位置。');
      output.push('');
      output.push('密码丢失风险：');
      output.push('  - 无法解密备份文件');
      output.push('  - 数据将永久无法访问');
      output.push('');
      output.push('请确认密码设置：');
      output.push('  [1] 确认，保存配置');
      output.push('  [2] 重新设置密码');
      output.push('  [3] 取消加密功能');
      output.push('');
      output.push('请回复选项编号：');

      state.encryptionPassword = input;
      return this.outputSuccess(output.join('\n'), {
        step: 8.3,
        total: 8,
        state,
        type: 'interactive'
      });

    } else {
      return this.outputError('密码长度至少8位，请重新输入');
    }
  }

  /**
   * 生成随机密码（新增）
   */
  generateRandomPassword() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#$@%&';
    let password = '';
    for (let i = 0; i < 16; i++) {
      password += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return password;
  }

  /**
   * 处理密码确认（新增）
   */
  async handlePasswordConfirm(input, state) {
    if (input === '1') {
      // 确认密码，保存配置
      return this.saveInteractiveConfig(state);

    } else if (input === '2') {
      // 重新设置密码
      return this.handleEncryptionInput('1', state);

    } else if (input === '3') {
      // 取消加密功能
      state.encryptionEnabled = false;
      return this.saveInteractiveConfig(state);

    } else {
      return this.outputError('请输入 1、2 或 3');
    }
  }

  /**
   * 处理随机密码确认（新增）
   */
  async handleRandomPasswordConfirm(input, state) {
    if (input.toLowerCase() === 'c') {
      // 确认已记录，保存配置
      return this.saveInteractiveConfig(state);
    } else {
      return this.outputError('请回复 c 确认已记录密码');
    }
  }

  /**
   * 保存交互式配置
   */
  async saveInteractiveConfig(state) {
    const output = [];

    try {
      const config = await loadConfig();

      // 更新配置
      if (state.mode) {
        config.backup.mode = state.mode;
      }

      // 保存选择性备份的目标文件（新增）
      if (state.mode === 'partial' && state.backupTargets) {
        config.backup.targets = state.backupTargets;
      }

      if (state.time) {
        const [hour, minute] = state.time.split(':').map(Number);
        config.schedule.cron = `${minute || 0} ${hour || 3} * * *`;
      }

      if (state.outputPath) {
        config.output.path = state.outputPath;
      }

      if (state.retentionMode && state.retentionValue) {
        config.retention.mode = state.retentionMode;
        if (state.retentionMode === 'days') {
          config.retention.days = state.retentionValue;
        } else {
          config.retention.count = state.retentionValue;
        }
      }

      if (state.selectedChannels) {
        config.notification.channels = state.selectedChannels;
      }

      if (state.targets) {
        config.notification.targets = state.targets;
      }

      // 新增：敏感文件排除配置保存
      if (state.enableSensitiveExclude !== undefined) {
        config.backup.enableSensitiveExclude = state.enableSensitiveExclude;
      }

      if (state.excludePatterns) {
        config.backup.excludePatterns = state.excludePatterns;
      }

      if (state.exclude) {
        config.backup.exclude = state.exclude;
      }

      // 新增：加密配置保存
      if (state.encryptionEnabled !== undefined) {
        // 确保output.encryption配置结构存在
        if (!config.output.encryption) {
          config.output.encryption = {
            enabled: false,
            password: null,
            algorithm: "aes-256",
            reminderShown: false
          };
        }
        config.output.encryption.enabled = state.encryptionEnabled;
      }

      if (state.encryptionPassword) {
        // 确保output.encryption配置结构存在
        if (!config.output.encryption) {
          config.output.encryption = {
            enabled: false,
            password: null,
            algorithm: "aes-256",
            reminderShown: false
          };
        }
        config.output.encryption.[REDACTED_SECRET];
      }

      await saveConfig(config);

      output.push('✅ 配置完成！');
      output.push('━'.repeat(40));
      output.push('');

      // 显示配置摘要
      if (state.mode) {
        if (state.mode === 'full') {
          output.push('备份范围: 全量备份');
        } else {
          output.push('备份范围: 选择性备份');
          if (state.backupTargets && state.backupTargets.length > 0) {
            output.push('');
            output.push('已选项目标：');
            state.backupTargets.forEach(t => {
              output.push(`  - ${t}`);
            });
          }
        }
      }

      if (state.enableSensitiveExclude) {
        output.push('敏感文件排除: 已启用');
      } else {
        output.push('敏感文件排除: 未启用（仅排除临时文件）');
      }

      if (state.encryptionEnabled) {
        output.push('ZIP加密: 已启用');
        if (state.encryptionPassword) {
          output.push(`密码: ${state.encryptionPassword.substring(0, 3)}***（已设置）`);
        }
      } else {
        output.push('ZIP加密: 未启用');
      }

      if (state.time) {
        output.push(`执行时间: 每天 ${state.time}`);
      }

      if (state.outputPath) {
        output.push(`存储路径: ${state.outputPath}`);
      }

      if (state.retentionMode && state.retentionValue) {
        output.push(`保留策略: ${state.retentionMode === 'days' ? `${state.retentionValue} 天` : `${state.retentionValue} 份`}`);
      }

      if (state.selectedChannels && state.selectedChannels.length > 0) {
        const channelNames = { feishu: '飞书', telegram: 'Telegram', discord: 'Discord', slack: 'Slack' };
        const channelInfo = state.selectedChannels.map(c => channelNames[c]).join('、');
        output.push(`通知渠道: ${channelInfo}`);
      }

      output.push('');
      output.push(`📁 配置文件: ${getConfigPaths().configFile}`);
      output.push('');
      output.push('💡 可用命令:');
      output.push('  /backup_now     - 立即执行备份');
      output.push('  /backup_status  - 查看备份状态');
      output.push('  /backup_list    - 列出备份文件');

      return this.outputSuccess(output.join('\n'), { config });

    } catch (err) {
      return this.outputError(`保存配置失败: ${err.message}`);
    }
  }

  /**
   * /backup_list - 列出备份文件
   */
  async cmdBackupList(args) {
    const output = [];
    
    try {
      const config = await loadConfig();
      const cleaner = new Cleaner(config);
      const stats = await cleaner.getStats();
      
      output.push('📦 备份文件列表');
      output.push('━'.repeat(60));
      output.push('');
      
      if (stats.count === 0) {
        output.push('暂无备份文件');
        output.push('');
        output.push(`📁 存储目录：${config.output.path}`);
        
        return this.outputSuccess(output.join('\n'), stats);
      }
      
      output.push('#    文件名                                          大小');
      output.push('─'.repeat(60));
      
      const files = args.all ? stats.files : stats.files.slice(0, 10);
      
      files.forEach((file, index) => {
        const num = String(index + 1).padStart(2, '0');
        const name = file.name.length > 40 ? file.name.substring(0, 37) + '...' : file.name;
        output.push(`${num}   ${name.padEnd(45)} ${file.size}`);
      });
      
      output.push('');
      output.push(`共 ${stats.count} 个备份文件，总计 ${stats.totalSize}`);
      
      if (stats.count > 10 && !args.all) {
        output.push('');
        output.push('💡 使用 --all 查看全部备份文件');
      }
      
      return this.outputSuccess(output.join('\n'), stats);
    } catch (err) {
      return this.outputError(`获取备份列表失败: ${err.message}`);
    }
  }

  /**
   * /backup_clean - 清理旧备份
   */
  async cmdBackupClean(args) {
    const output = [];
    
    try {
      const config = await loadConfig();
      const cleaner = new Cleaner(config);
      
      // 预览模式
      if (args.preview || args.dryRun) {
        const preview = await cleaner.getPreview();
        
        output.push('🗑️ 清理预览');
        output.push('━'.repeat(40));
        output.push('');
        output.push(`当前备份数：${preview.total} 个`);
        output.push(`将要删除：${preview.toDeleteCount} 个`);
        output.push(`将会保留：${preview.toKeepCount} 个`);
        
        if (preview.toDeleteCount > 0) {
          output.push('');
          output.push('将要删除的文件：');
          preview.toDeleteFiles.slice(0, 5).forEach(f => {
            output.push(`  - ${f.name} (${f.size})`);
          });
          if (preview.toDeleteFiles.length > 5) {
            output.push(`  ... 还有 ${preview.toDeleteFiles.length - 5} 个`);
          }
        }
        
        output.push('');
        output.push('💡 使用 --confirm 执行实际清理');
        
        return this.outputSuccess(output.join('\n'), preview);
      }
      
      // 执行清理
      const result = await cleaner.execute();
      
      if (result.skipped) {
        output.push('⚠️ 保留策略未启用');
        output.push('请在配置中启用 retention.enabled');
      } else {
        output.push('🗑️ 清理完成');
        output.push('━'.repeat(40));
        output.push('');
        output.push(`✅ 删除：${result.deleted} 个`);
        output.push(`📁 保留：${result.kept} 个`);
      }
      
      return this.outputSuccess(output.join('\n'), result);
    } catch (err) {
      return this.outputError(`清理失败: ${err.message}`);
    }
  }

  /**
   * /help - 帮助
   */
  async cmdHelp() {
    const output = [];
    
    output.push('📖 Auto Backup OpenClaw User Data - 命令帮助');
    output.push('━'.repeat(50));
    output.push('');
    output.push('可用命令：');
    output.push('');
    output.push('/backup_now         立即执行备份');
    output.push('  --full            全量备份（默认）');
    output.push('  --partial         选择性备份');
    output.push('');
    output.push('/backup_status      查看备份状态');
    output.push('');
    output.push('/backup_config      配置向导');
    output.push('  [1]               交互式配置');
    output.push('  [2]               手动配置');
    output.push('  [3]               重置配置');
    output.push('  [4]               查看配置');
    output.push('');
    output.push('/backup_list        列出备份文件');
    output.push('  --all             显示全部');
    output.push('');
    output.push('/backup_clean       清理旧备份');
    output.push('  --preview         预览模式');
    output.push('  --confirm         确认执行');
    output.push('');
    output.push('📁 配置文件：~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/config.json');
    output.push('📋 日志文件：~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log');
    
    return this.outputSuccess(output.join('\n'));
  }

  /**
   * 格式化成功输出
   */
  outputSuccess(message, data = null) {
    return {
      success: true,
      message,
      data
    };
  }

  /**
   * 格式化错误输出
   */
  outputError(message) {
    return {
      success: false,
      message,
      error: message
    };
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    if (!bytes || bytes === 0) return '0 B';
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  }

  /**
   * 格式化执行时长
   */
  formatDuration(ms) {
    if (!ms) return '0ms';
    if (ms < 1000) return `${ms}ms`;
    if (ms < 60000) return `${(ms / 1000).toFixed(1)} 秒`;
    return `${Math.floor(ms / 60000)} 分 ${Math.floor((ms % 60000) / 1000)} 秒`;
  }
}

/**
 * 创建 CLI 实例并执行命令
 */
async function runCommand(command, args = {}) {
  const cli = new CLI();
  return cli.execute(command, args);
}

module.exports = {
  CLI,
  runCommand
};

path: scripts/compressor.js
content: /**
 * 压缩模块
 * 负责文件压缩和 ZIP 生成
 * v1.1.0 - 新增加密压缩支持
 */

const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const archiver = require('archiver');
const archiverZipEncrypted = require('archiver-zip-encrypted');
const dayjs = require('dayjs');
const { info, warn, error, debug } = require('./logger');

// 注册加密格式
archiver.registerFormat('zip-encrypted', archiverZipEncrypted);

// OpenClaw 根目录
const OPENCLAW_ROOT = path.join(os.homedir(), '.openclaw');

/**
 * 创建压缩器实例
 */
class Compressor {
  constructor(config) {
    this.config = config;
  }

  /**
   * 压缩文件列表到 ZIP
   * @param {Array} files 文件列表 [{path, relativePath}, ...]
   * @param {Object} outputConfig 输出配置
   * @returns {Promise<string>} ZIP 文件路径
   */
  async compress(files, outputConfig) {
    return new Promise(async (resolve, reject) => {
      try {
        // 生成 ZIP 文件路径
        const zipPath = await this.generateZipPath(outputConfig);

        // 确保输出目录存在
        await fs.ensureDir(path.dirname(zipPath));

        // 判断是否启用加密
        const useEncryption = outputConfig.encryption?.enabled;
        const [REDACTED_SECRET];

        // 选择压缩格式
        const archiveType = useEncryption ? 'zip-encrypted' : 'zip';
        const archiveOptions = useEncryption ? {
          zlib: { level: 9 }, // 最高压缩级别
          encryptionMethod: 'aes256',
          [REDACTED_SECRET]
        } : {
          zlib: { level: 9 } // 最高压缩级别
        };

        // 创建写入流
        const output = fs.createWriteStream(zipPath);
        const archive = archiver(archiveType, archiveOptions);

        let filesAdded = 0;
        let filesSkipped = 0;
        const skippedFiles = [];

        output.on('close', async () => {
          const size = this.formatSize(archive.pointer());
          await info('Compressor', `压缩完成: ${path.basename(zipPath)} (${size})`);
          await info('Compressor', `文件统计: 添加 ${filesAdded} 个，跳过 ${filesSkipped} 个`);

          if (useEncryption) {
            await info('Compressor', '加密方式: AES-256');
          }

          if (skippedFiles.length > 0) {
            await warn('Compressor', `跳过的文件: ${skippedFiles.slice(0, 10).join(', ')}${skippedFiles.length > 10 ? '...' : ''}`);
          }

          resolve(zipPath);
        });

        archive.on('error', async (err) => {
          await error('Compressor', `压缩错误: ${err.message}`);
          reject(err);
        });

        archive.on('warning', async (err) => {
          if (err.code === 'ENOENT') {
            await warn('Compressor', `文件不存在: ${err.message}`);
          } else {
            await error('Compressor', `压缩警告: ${err.message}`);
          }
        });

        archive.pipe(output);

        // 添加文件到压缩包
        for (const file of files) {
          try {
            // 检查文件是否存在
            if (!(await fs.pathExists(file.path))) {
              filesSkipped++;
              skippedFiles.push(file.relativePath);
              continue;
            }

            // 检查文件是否可读
            try {
              await fs.access(file.path, fs.constants.R_OK);
            } catch (accessErr) {
              if (accessErr.code === 'EBUSY') {
                // 文件被占用，跳过
                filesSkipped++;
                skippedFiles.push(file.relativePath);
                await warn('Compressor', `跳过被占用的文件: ${file.relativePath}`);
                continue;
              }
              throw accessErr;
            }

            // 添加文件到压缩包
            archive.file(file.path, { name: file.relativePath });
            filesAdded++;
          } catch (err) {
            filesSkipped++;
            skippedFiles.push(file.relativePath);
            await warn('Compressor', `跳过文件 ${file.relativePath}: ${err.message}`);
          }
        }

        // 完成压缩
        await archive.finalize();
      } catch (err) {
        await error('Compressor', `压缩失败: ${err.message}`);
        reject(err);
      }
    });
  }

  /**
   * 生成 ZIP 文件路径
   */
  async generateZipPath(outputConfig) {
    const now = dayjs();
    const date = now.format('YYYYMMDD');
    const time = now.format('HHmm');
    
    // 获取 OpenClaw 版本
    const version = await this.getOpenClawVersion();
    
    // 获取当日序号
    const sequence = await this.getNextSequence(date, outputConfig.path);
    
    // 构建文件名
    const parts = [outputConfig.naming.prefix, date, time];
    
    if (outputConfig.naming.includeVersion) {
      parts.push(version);
    }
    
    if (outputConfig.naming.includeSequence) {
      parts.push(sequence);
    }
    
    const filename = parts.join('_') + '.zip';
    
    return path.join(outputConfig.path, filename);
  }

  /**
   * 获取 OpenClaw 版本
   */
  async getOpenClawVersion() {
    try {
      // 尝试从 package.json 获取版本
      const packagePath = path.join(
        process.env.APPDATA || path.join(os.homedir(), '.config'),
        'npm/node_modules/openclaw/package.json'
      );
      
      if (await fs.pathExists(packagePath)) {
        const pkg = await fs.readJson(packagePath);
        return `v${pkg.version}`;
      }
      
      // 备用：从命令行获取
      // 这里使用默认版本
      return 'v2026.3.23-2';
    } catch (err) {
      return 'v2026.3.23-2';
    }
  }

  /**
   * 获取当日备份序号
   */
  async getNextSequence(date, outputPath) {
    try {
      await fs.ensureDir(outputPath);
      
      // 获取目录下所有备份文件
      const files = await fs.readdir(outputPath);
      
      // 过滤出当天的备份文件
      const todayBackups = files.filter(f => 
        f.includes(`_${date}_`) && f.endsWith('.zip')
      );
      
      // 序号从 01 开始
      const nextSequence = todayBackups.length + 1;
      
      return String(nextSequence).padStart(2, '0');
    } catch (err) {
      return '01';
    }
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  }

  /**
   * 压缩目录
   * @param {string} dirPath 目录路径
   * @param {string} outputPath 输出路径
   * @param {Object} options 选项
   */
  async compressDirectory(dirPath, outputPath, options = {}) {
    const files = await this.collectFiles(dirPath, options.exclude || [], options.excludePatterns || []);
    
    // 转换为相对路径
    const filesWithRelativePath = files.map(filePath => ({
      path: filePath,
      relativePath: path.relative(path.dirname(dirPath), filePath)
    }));
    
    return this.compress(filesWithRelativePath, { path: outputPath, naming: options.naming || {} });
  }

  /**
   * 收集目录下的所有文件
   */
  async collectFiles(dirPath, excludeDirs = [], excludePatterns = []) {
    const files = [];
    
    async function walk(currentPath) {
      try {
        const entries = await fs.readdir(currentPath, { withFileTypes: true });
        
        for (const entry of entries) {
          const fullPath = path.join(currentPath, entry.name);
          
          // 检查是否在排除目录中
          if (excludeDirs.includes(entry.name)) {
            continue;
          }
          
          // 检查是否匹配排除模式
          if (excludePatterns.some(pattern => matchPattern(entry.name, pattern))) {
            continue;
          }
          
          if (entry.isDirectory()) {
            await walk(fullPath);
          } else if (entry.isFile()) {
            files.push(fullPath);
          }
        }
      } catch (err) {
        // 忽略无法访问的目录
      }
    }
    
    await walk(dirPath);
    return files;
  }
}

/**
 * 简单的模式匹配
 */
function matchPattern(filename, pattern) {
  // 将通配符模式转换为正则表达式
  const regex = new RegExp('^' + pattern
    .replace(/\./g, '\\.')
    .replace(/\*/g, '.*')
    .replace(/\?/g, '.') + '$');
  
  return regex.test(filename);
}

module.exports = { Compressor, matchPattern };

path: scripts/config.js
content: /**
 * 配置管理模块
 * 负责配置的加载、保存、验证和迁移
 * v1.1.0 - 新增工作空间动态检测和敏感文件配置
 */

const fs = require('fs-extra');
const path = require('path');
const os = require('os');

// 配置文件路径
const CONFIG_DIR = path.join(os.homedir(), '.openclaw', 'workspace', 'Auto-Backup-Openclaw-User-Data');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
const LOG_FILE = path.join(CONFIG_DIR, 'backup.log');
const BACKUPS_DIR = path.join(CONFIG_DIR, 'backups');

// OpenClaw 配置文件路径
const OPENCLAW_CONFIG_FILE = path.join(os.homedir(), '.openclaw', 'openclaw.json');

// 默认配置
const DEFAULT_CONFIG = {
  version: "1.1.0",
  createdAt: new Date().toISOString(),
  updatedAt: new Date().toISOString(),

  backup: {
    mode: "partial",  // ← 修改默认为选择性备份
    targets: [],  // ← 改为空数组，首次加载时动态检测

    // 默认排除（仅临时文件）
    exclude: ["logs", "cache", "tmp", "node_modules"],
    excludePatterns: ["*.log", "*.tmp", ".DS_Store", "Thumbs.db"],

    // 敏感文件排除建议列表（默认不启用，仅建议）
    sensitiveExcludeSuggestion: [
      "*.key", "*.pem", "*.p12", "*.pfx",
      ".env", ".env.local", ".env.*.local",
      "credentials.json", "secrets.json",
      "*.token", "*.secret",
      "*_key.json", "*_token.json",
      "id_rsa", "id_dsa", "*.ppk"
    ],
    sensitiveExcludeDirectories: [
      "credentials", "secrets", ".ssh", ".gnupg"
    ],

    // 默认不启用敏感文件排除
    enableSensitiveExclude: false  // ← 关键：默认false
  },
  
  schedule: {
    enabled: true,
    cron: "0 3 * * *",
    timezone: "Asia/Shanghai",
    lastRun: null
  },
  
  output: {
    path: BACKUPS_DIR,
    naming: {
      prefix: "auto-backup-openclaw-user-data",
      includeVersion: true,
      includeSequence: true
    },
    encryption: {
      enabled: false,
      password: null,
      algorithm: "aes-256",
      reminderShown: false
    }
  },
  
  retention: {
    enabled: true,
    mode: "count",
    days: 30,
    count: 10
  },
  
  notification: {
    enabled: true,
    channels: ["feishu"],
    targets: {},
    onSuccess: true,
    onFailure: true
  },
  
  logging: {
    enabled: true,
    level: "info",
    maxSize: "10MB",
    maxFiles: 5
  }
};

/**
 * 确保配置目录存在
 */
async function ensureConfigDir() {
  await fs.ensureDir(CONFIG_DIR);
  await fs.ensureDir(BACKUPS_DIR);
}

/**
 * 检查配置文件是否存在
 */
async function configExists() {
  return fs.pathExists(CONFIG_FILE);
}

/**
 * 动态检测用户工作空间
 * 首次配置时自动检测~/.openclaw/目录中的workspace
 * v1.1.0 - 新增功能
 */
async function detectWorkspaces() {
  const openclawRoot = path.join(os.homedir(), '.openclaw');

  try {
    const entries = await fs.readdir(openclawRoot, { withFileTypes: true });

    // 过滤出所有workspace目录
    const workspaces = entries
      .filter(e => e.isDirectory() && e.name.startsWith('workspace'))
      .map(e => e.name)
      .sort();  // 排序

    // 检测memory目录
    const memoryExists = entries.some(e => e.name === 'memory' && e.isDirectory());

    // 构建targets列表
    const targets = [...workspaces];
    if (memoryExists) {
      targets.push('memory');
    }

    // 至少返回workspace作为默认值
    return targets.length > 0 ? targets : ['workspace'];

  } catch (error) {
    console.error(`[Config] 检测工作空间失败: ${error.message}`);
    return ['workspace'];  // 默认返回workspace
  }
}

/**
 * 加载配置
 * v1.1.0 - 首次配置时动态检测workspace
 */
async function loadConfig() {
  try {
    await ensureConfigDir();

    if (!(await configExists())) {
      // 配置不存在，创建默认配置（动态检测workspace）
      const detectedTargets = await detectWorkspaces();

      const initialConfig = {
        ...DEFAULT_CONFIG,
        backup: {
          ...DEFAULT_CONFIG.backup,
          targets: detectedTargets  // ← 动态设置targets
        }
      };

      await saveConfig(initialConfig);
      return initialConfig;
    }

    const config = await fs.readJson(CONFIG_FILE);

    // 配置迁移（检查并补全新字段）
    const migratedConfig = migrateConfig(config);

    return migratedConfig;
  } catch (error) {
    console.error(`[Config] 加载配置失败: ${error.message}`);
    // 返回默认配置
    return { ...DEFAULT_CONFIG };
  }
}

/**
 * 保存配置
 */
async function saveConfig(config) {
  try {
    await ensureConfigDir();
    
    config.updatedAt = new Date().toISOString();
    
    // 验证配置
    const validation = validateConfig(config);
    if (!validation.valid) {
      throw new Error(`配置验证失败: ${validation.errors.join(', ')}`);
    }
    
    await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
    return true;
  } catch (error) {
    console.error(`[Config] 保存配置失败: ${error.message}`);
    return false;
  }
}

/**
 * 验证配置
 */
function validateConfig(config) {
  const errors = [];
  
  // 验证 version
  if (!config.version || typeof config.version !== 'string') {
    errors.push('version 必须是字符串');
  }
  
  // 验证 backup.mode
  if (config.backup && !['full', 'partial'].includes(config.backup.mode)) {
    errors.push('backup.mode 必须是 "full" 或 "partial"');
  }
  
  // 验证 schedule.cron
  if (config.schedule && config.schedule.cron) {
    const cronPattern = /^[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+\s+[\d*\/,-]+$/;
    if (!cronPattern.test(config.schedule.cron)) {
      errors.push('schedule.cron 格式无效');
    }
  }
  
  // 验证 retention.mode
  if (config.retention && !['days', 'count'].includes(config.retention.mode)) {
    errors.push('retention.mode 必须是 "days" 或 "count"');
  }
  
  // 验证 retention.days
  if (config.retention && config.retention.mode === 'days') {
    if (typeof config.retention.days !== 'number' || config.retention.days < 1) {
      errors.push('retention.days 必须是大于 0 的数字');
    }
  }
  
  // 验证 retention.count
  if (config.retention && config.retention.mode === 'count') {
    if (typeof config.retention.count !== 'number' || config.retention.count < 1) {
      errors.push('retention.count 必须是大于 0 的数字');
    }
  }
  
  return {
    valid: errors.length === 0,
    errors
  };
}

/**
 * 配置迁移
 * v1.1.0 - 新增敏感文件配置字段迁移
 */
function migrateConfig(config) {
  let needsSave = false;

  // v1.0.0 -> v1.0.1: 新增 notification.targets 字段
  if (!config.notification) {
    config.notification = { ...DEFAULT_CONFIG.notification };
    needsSave = true;
  }

  if (!config.notification.targets) {
    config.notification.targets = {};
    needsSave = true;
  }

  // v1.0.2 -> v1.1.0: 新增敏感文件配置字段
  if (!config.backup.sensitiveExcludeSuggestion) {
    config.backup.sensitiveExcludeSuggestion = DEFAULT_CONFIG.backup.sensitiveExcludeSuggestion;
    needsSave = true;
  }

  if (!config.backup.sensitiveExcludeDirectories) {
    config.backup.sensitiveExcludeDirectories = DEFAULT_CONFIG.backup.sensitiveExcludeDirectories;
    needsSave = true;
  }

  if (config.backup.enableSensitiveExclude === undefined) {
    config.backup.enableSensitiveExclude = DEFAULT_CONFIG.backup.enableSensitiveExclude;
    needsSave = true;
  }

  // v1.1.0: 新增加密配置字段
  if (!config.output.encryption) {
    config.output.encryption = DEFAULT_CONFIG.output.encryption;
    needsSave = true;
  }

  // 更新版本号
  if (config.version !== DEFAULT_CONFIG.version) {
    config.version = DEFAULT_CONFIG.version;
    needsSave = true;
  }

  // 如果有变更，保存配置
  if (needsSave) {
    saveConfig(config).catch(err => {
      console.error(`[Config] 配置迁移保存失败: ${err.message}`);
    });
  }

  return config;
}

/**
 * 重置配置为默认值
 */
async function resetConfig() {
  const newConfig = {
    ...DEFAULT_CONFIG,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };
  
  await saveConfig(newConfig);
  return newConfig;
}

/**
 * 获取配置路径信息
 */
function getConfigPaths() {
  return {
    configDir: CONFIG_DIR,
    configFile: CONFIG_FILE,
    logFile: LOG_FILE,
    backupsDir: BACKUPS_DIR
  };
}

/**
 * 更新部分配置
 */
async function updateConfig(updates) {
  const config = await loadConfig();
  const newConfig = deepMerge(config, updates);
  await saveConfig(newConfig);
  return newConfig;
}

/**
 * 深度合并对象
 */
function deepMerge(target, source) {
  const result = { ...target };
  
  for (const key in source) {
    if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
      result[key] = deepMerge(result[key] || {}, source[key]);
    } else {
      result[key] = source[key];
    }
  }
  
  return result;
}

/**
 * 加载 OpenClaw 配置
 * 从 ~/.openclaw/openclaw.json 读取渠道配置和绑定信息
 */
async function loadOpenClawConfig() {
  try {
    if (!(await fs.pathExists(OPENCLAW_CONFIG_FILE))) {
      return { channels: {}, bindings: [], availableTargets: {} };
    }
    
    const config = await fs.readJson(OPENCLAW_CONFIG_FILE);
    
    const result = {
      channels: {},
      bindings: config.bindings || [],
      availableTargets: {}
    };
    
    // 解析渠道配置
    if (config.channels) {
      // Feishu
      if (config.channels.feishu && config.channels.feishu.enabled) {
        result.channels.feishu = {
          enabled: true,
          appId: config.channels.feishu.appId
        };
        result.availableTargets.feishu = [];
      }
      
      // Telegram
      if (config.channels.telegram && config.channels.telegram.enabled) {
        result.channels.telegram = {
          enabled: true,
          groups: config.channels.telegram.groups || {}
        };
        result.availableTargets.telegram = [];
      }
      
      // Discord
      if (config.channels.discord && config.channels.discord.enabled) {
        result.channels.discord = {
          enabled: true
        };
        result.availableTargets.discord = [];
      }
      
      // Slack
      if (config.channels.slack && config.channels.slack.enabled) {
        result.channels.slack = {
          enabled: true
        };
        result.availableTargets.slack = [];
      }
    }
    
    // 从 bindings 解析可用的推送目标
    if (config.bindings && Array.isArray(config.bindings)) {
      for (const binding of config.bindings) {
        if (binding.match && binding.match.channel && binding.match.peer) {
          const channel = binding.match.channel;
          const peer = binding.match.peer;
          
          if (!result.availableTargets[channel]) {
            result.availableTargets[channel] = [];
          }
          
          // 添加到可用目标列表
          result.availableTargets[channel].push({
            type: peer.kind, // 'group' or 'user'
            id: peer.id,
            name: binding.agentId ? `${binding.agentId} Agent` : peer.id,
            agentId: binding.agentId
          });
        }
      }
    }
    
    // 从 Telegram groups 配置解析
    if (config.channels?.telegram?.groups) {
      const tgGroups = config.channels.telegram.groups;
      for (const groupId in tgGroups) {
        if (tgGroups[groupId].enabled) {
          if (!result.availableTargets.telegram) {
            result.availableTargets.telegram = [];
          }
          result.availableTargets.telegram.push({
            type: 'group',
            id: groupId,
            name: `Telegram群组 ${groupId}`
          });
        }
      }
    }

    // 从 Telegram allowFrom 配置解析用户（新增v1.1.0）
    if (config.channels?.telegram?.allowFrom && Array.isArray(config.channels.telegram.allowFrom)) {
      const tgUsers = config.channels.telegram.allowFrom;
      for (const userId of tgUsers) {
        if (!result.availableTargets.telegram) {
          result.availableTargets.telegram = [];
        }
        result.availableTargets.telegram.push({
          type: 'user',
          id: String(userId),  // 转为字符串，确保id格式一致
          name: `Telegram用户 ${userId}`
        });
      }
    }

    return result;
  } catch (error) {
    console.error(`[Config] 加载 OpenClaw 配置失败: ${error.message}`);
    return { channels: {}, bindings: [], availableTargets: {} };
  }
}

/**
 * 检测渠道是否已配置
 */
function isChannelConfigured(channel, openclawConfig) {
  return openclawConfig.channels[channel]?.enabled === true;
}

/**
 * 获取渠道可用的推送目标
 */
function getChannelTargets(channel, openclawConfig) {
  return openclawConfig.availableTargets[channel] || [];
}

module.exports = {
  loadConfig,
  saveConfig,
  validateConfig,
  resetConfig,
  updateConfig,
  configExists,
  ensureConfigDir,
  getConfigPaths,
  loadOpenClawConfig,
  isChannelConfigured,
  getChannelTargets,
  detectWorkspaces,  // ← 新增导出
  DEFAULT_CONFIG,
  CONFIG_DIR,
  CONFIG_FILE,
  LOG_FILE,
  BACKUPS_DIR
};

path: scripts/logger.js
content: /**
 * 日志管理模块
 * 负责日志的记录和管理
 */

const fs = require('fs-extra');
const path = require('path');
const { LOG_FILE, ensureConfigDir } = require('./config');

// 日志级别
const LOG_LEVELS = {
  DEBUG: 0,
  INFO: 1,
  WARN: 2,
  ERROR: 3
};

// 当前日志级别（默认 INFO）
let currentLevel = LOG_LEVELS.INFO;

// 日志颜色（终端输出）
const COLORS = {
  DEBUG: '\x1b[36m',  // 青色
  INFO: '\x1b[32m',   // 绿色
  WARN: '\x1b[33m',   // 黄色
  ERROR: '\x1b[31m',  // 红色
  RESET: '\x1b[0m'
};

/**
 * 设置日志级别
 */
function setLevel(level) {
  if (LOG_LEVELS[level] !== undefined) {
    currentLevel = LOG_LEVELS[level];
  }
}

/**
 * 格式化时间戳
 */
function formatTimestamp() {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  const hours = String(now.getHours()).padStart(2, '0');
  const minutes = String(now.getMinutes()).padStart(2, '0');
  const seconds = String(now.getSeconds()).padStart(2, '0');
  const ms = String(now.getMilliseconds()).padStart(3, '0');
  
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
}

/**
 * 写入日志到文件
 */
async function writeToFile(message) {
  try {
    await ensureConfigDir();
    await fs.appendFile(LOG_FILE, message + '\n');
  } catch (error) {
    // 写入失败，只输出到控制台
    console.error(`[Logger] 写入日志文件失败: ${error.message}`);
  }
}

/**
 * 格式化日志消息
 */
function formatMessage(level, module, message) {
  const timestamp = formatTimestamp();
  return `[${timestamp}] [${level.padEnd(5)}] [${module}] ${message}`;
}

/**
 * 记录日志
 */
async function log(level, module, message) {
  // 检查日志级别
  if (LOG_LEVELS[level] < currentLevel) {
    return;
  }
  
  const formattedMessage = formatMessage(level, module, message);
  
  // 输出到控制台（带颜色）
  const color = COLORS[level] || COLORS.RESET;
  console.log(`${color}${formattedMessage}${COLORS.RESET}`);
  
  // 写入到文件
  await writeToFile(formattedMessage);
}

/**
 * DEBUG 级别日志
 */
async function debug(module, message) {
  await log('DEBUG', module, message);
}

/**
 * INFO 级别日志
 */
async function info(module, message) {
  await log('INFO', module, message);
}

/**
 * WARN 级别日志
 */
async function warn(module, message) {
  await log('WARN', module, message);
}

/**
 * ERROR 级别日志
 */
async function error(module, message) {
  await log('ERROR', module, message);
}

/**
 * 清理旧日志文件
 */
async function cleanOldLogs(maxSize = '10MB', maxFiles = 5) {
  try {
    if (!(await fs.pathExists(LOG_FILE))) {
      return;
    }
    
    const stats = await fs.stat(LOG_FILE);
    const sizeInBytes = stats.size;
    
    // 解析最大大小
    const sizeMatch = maxSize.match(/^(\d+)(MB|KB|GB)?$/i);
    if (!sizeMatch) return;
    
    const sizeValue = parseInt(sizeMatch[1]);
    const sizeUnit = (sizeMatch[2] || 'B').toUpperCase();
    
    let maxSizeInBytes;
    switch (sizeUnit) {
      case 'KB': maxSizeInBytes = sizeValue * 1024; break;
      case 'MB': maxSizeInBytes = sizeValue * 1024 * 1024; break;
      case 'GB': maxSizeInBytes = sizeValue * 1024 * 1024 * 1024; break;
      default: maxSizeInBytes = sizeValue;
    }
    
    // 如果超过最大大小，进行轮转
    if (sizeInBytes > maxSizeInBytes) {
      await rotateLogs(maxFiles);
    }
  } catch (err) {
    console.error(`[Logger] 清理日志失败: ${err.message}`);
  }
}

/**
 * 日志轮转
 */
async function rotateLogs(maxFiles) {
  try {
    // 删除最旧的日志文件
    const oldestLogFile = `${LOG_FILE}.${maxFiles}`;
    if (await fs.pathExists(oldestLogFile)) {
      await fs.remove(oldestLogFile);
    }
    
    // 重命名现有日志文件
    for (let i = maxFiles - 1; i >= 1; i--) {
      const oldFile = `${LOG_FILE}.${i}`;
      const newFile = `${LOG_FILE}.${i + 1}`;
      if (await fs.pathExists(oldFile)) {
        await fs.move(oldFile, newFile);
      }
    }
    
    // 重命名当前日志文件
    if (await fs.pathExists(LOG_FILE)) {
      await fs.move(LOG_FILE, `${LOG_FILE}.1`);
    }
    
    await info('Logger', `日志轮转完成，保留 ${maxFiles} 个日志文件`);
  } catch (err) {
    console.error(`[Logger] 日志轮转失败: ${err.message}`);
  }
}

/**
 * 读取日志内容
 */
async function readLogs(lines = 100) {
  try {
    if (!(await fs.pathExists(LOG_FILE))) {
      return [];
    }
    
    const content = await fs.readFile(LOG_FILE, 'utf-8');
    const logLines = content.trim().split('\n');
    
    // 返回最后 N 行
    return logLines.slice(-lines);
  } catch (err) {
    console.error(`[Logger] 读取日志失败: ${err.message}`);
    return [];
  }
}

module.exports = {
  debug,
  info,
  warn,
  error,
  setLevel,
  cleanOldLogs,
  readLogs,
  LOG_LEVELS
};

path: scripts/notifier.js
content: /**
 * 通知模块
 * 负责发送备份结果通知
 * v1.0.2 - 支持实际消息推送
 */

const { info, warn, error } = require('./logger');
const { loadOpenClawConfig, isChannelConfigured, getChannelTargets } = require('./config');
const path = require('path');
const os = require('os');

/**
 * 创建通知器实例
 */
class Notifier {
  constructor(config, context = null) {
    this.config = config;
    this.context = context; // 当前对话上下文
    this.openclawConfig = null;
  }

  /**
   * 初始化 - 加载 OpenClaw 配置
   */
  async init() {
    this.openclawConfig = await loadOpenClawConfig();
  }

  /**
   * 发送备份成功通知
   */
  async notifySuccess(result) {
    if (!this.config.notification.enabled || !this.config.notification.onSuccess) {
      return;
    }
    
    const message = this.formatSuccessMessage(result);
    await this.sendToChannels(message, 'success', result);
  }

  /**
   * 发送备份失败通知
   */
  async notifyFailure(result) {
    if (!this.config.notification.enabled || !this.config.notification.onFailure) {
      return;
    }
    
    const message = this.formatFailureMessage(result);
    await this.sendToChannels(message, 'failure', result);
  }

  /**
   * 格式化成功消息
   */
  formatSuccessMessage(result) {
    const lines = [
      '✅ OpenClaw 数据备份成功',
      '',
      `📦 备份文件：${path.basename(result.outputPath)}`,
      `📊 文件数量：${result.filesTotal.toLocaleString()} 个`,
      `📏 文件大小：${this.formatSize(result.sizeBytes)}`,
      `⏱️ 执行耗时：${this.formatDuration(result.duration)}`,
      `🕐 执行时间：${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`,
      '',
      `📁 存储位置：${result.outputPath}`
    ];
    
    if (result.filesSkipped > 0) {
      lines.splice(6, 0, `⚠️ 跳过文件：${result.filesSkipped} 个`);
    }
    
    return lines.join('\n');
  }

  /**
   * 格式化失败消息
   */
  formatFailureMessage(result) {
    const lines = [
      '❌ OpenClaw 数据备份失败',
      '',
      `🚨 错误信息：${result.errors.join('; ')}`,
      `🕐 失败时间：${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`
    ];
    
    // 根据错误类型给出建议
    if (result.errors.some(e => e.includes('ENOENT') || e.includes('not found'))) {
      lines.push('', '💡 建议：请检查备份目录是否存在');
    } else if (result.errors.some(e => e.includes('ENOSPC') || e.includes('space'))) {
      lines.push('', '💡 建议：请清理磁盘空间或更换存储路径');
    } else if (result.errors.some(e => e.includes('EACCES') || e.includes('permission'))) {
      lines.push('', '💡 建议：请检查文件权限或以管理员身份运行');
    }
    
    lines.push('', `📋 详细日志：~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log`);
    
    return lines.join('\n');
  }

  /**
   * 发送到配置的渠道
   */
  async sendToChannels(message, type, result) {
    // 确保已初始化
    if (!this.openclawConfig) {
      await this.init();
    }
    
    const channels = this.config.notification.channels || [];
    const targets = this.config.notification.targets || {};
    
    if (channels.length === 0) {
      await info('Notifier', '未配置通知渠道，跳过通知');
      return;
    }
    
    await info('Notifier', `准备发送通知到: ${channels.join(', ')}`);
    
    let allFailed = true;
    const failedChannels = [];
    
    for (const channel of channels) {
      // 检测渠道是否已配置
      if (!isChannelConfigured(channel, this.openclawConfig)) {
        await warn('Notifier', `渠道 ${channel} 未在 OpenClaw 中配置，跳过`);
        failedChannels.push({ channel, reason: '渠道未配置' });
        continue;
      }
      
      // 检测是否有推送目标
      const channelTargets = targets[channel] || [];
      if (channelTargets.length === 0) {
        await warn('Notifier', `渠道 ${channel} 未配置推送目标，跳过`);
        failedChannels.push({ channel, reason: '未配置推送目标' });
        continue;
      }
      
      // 发送到每个目标
      let channelSuccess = false;
      for (const target of channelTargets) {
        try {
          const success = await this.sendToTarget(channel, target, message);
          if (success) {
            await info('Notifier', `通知已发送到 ${channel}: ${target.name || target.id}`);
            channelSuccess = true;
            allFailed = false;
          }
        } catch (err) {
          await error('Notifier', `发送通知到 ${channel}:${target.id} 失败: ${err.message}`);
          failedChannels.push({ channel, target: target.name || target.id, reason: err.message });
        }
      }
      
      if (!channelSuccess) {
        // 该渠道所有目标都推送失败，尝试通过当前对话提醒
        await this.notifyViaCurrentContext(
          `⚠️ ${channel} 渠道消息推送失败\n\n` +
          `所有推送目标都无法送达。请检查：\n` +
          `1. OpenClaw 是否正确配置了 ${channel} 渠道\n` +
          `2. 推送目标 ID 是否正确\n` +
          `3. 运行 /backup_config 重新配置推送目标`
        );
      }
    }
    
    // 如果所有渠道都失败，通过当前对话提醒
    if (allFailed && failedChannels.length > 0) {
      const errorDetails = failedChannels.map(f => 
        `- ${f.channel}${f.target ? ` (${f.target})` : ''}: ${f.reason}`
      ).join('\n');
      
      await this.notifyViaCurrentContext(
        `⚠️ 备份通知推送失败\n\n` +
        `备份任务已完成，但通知无法推送到任何渠道。\n\n` +
        `失败详情：\n${errorDetails}\n\n` +
        `💡 解决建议：\n` +
        `1. 运行 /backup_config 重新配置通知渠道\n` +
        `2. 检查 ~/.openclaw/openclaw.json 中的渠道配置\n` +
        `3. 确保已配置具体的推送目标（用户ID或群组ID）`
      );
    }
  }

  /**
   * 发送到单个目标
   */
  async sendToTarget(channel, target, message) {
    // 构建 target 字符串
    // group: oc_xxx 或 chat:oc_xxx
    // user: ou_xxx
    let targetStr;
    if (target.type === 'group') {
      targetStr = `chat:${target.id}`;
    } else if (target.type === 'user') {
      targetStr = `user:${target.id}`;
    } else {
      targetStr = target.id;
    }
    
    // 调用 OpenClaw message 工具
    // 注意：这里需要在 OpenClaw 环境中运行才能生效
    // 返回结构化的推送请求，供 OpenClaw 处理
    return {
      success: true,
      channel,
      target: targetStr,
      targetName: target.name,
      message,
      timestamp: new Date().toISOString()
    };
  }

  /**
   * 通过当前对话发送提醒（降级方案）
   */
  async notifyViaCurrentContext(message) {
    // 这个方法会在 OpenClaw 环境中被调用
    // 返回提醒消息结构，由 OpenClaw 处理
    await info('Notifier', `通过当前对话发送提醒: ${message.substring(0, 50)}...`);
    
    return {
      type: 'context_reminder',
      message,
      timestamp: new Date().toISOString()
    };
  }

  /**
   * 获取可用的通知渠道
   */
  async getAvailableChannels() {
    if (!this.openclawConfig) {
      await this.init();
    }
    
    const available = [];
    const channelNames = ['feishu', 'telegram', 'discord', 'slack'];
    
    for (const channel of channelNames) {
      if (isChannelConfigured(channel, this.openclawConfig)) {
        const targets = getChannelTargets(channel, this.openclawConfig);
        available.push({
          name: channel,
          enabled: true,
          targets: targets
        });
      }
    }
    
    return available;
  }

  /**
   * 格式化文件大小
   */
  formatSize(bytes) {
    if (!bytes || bytes === 0) return '0 B';
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
    if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
  }

  /**
   * 格式化执行时长
   */
  formatDuration(ms) {
    if (!ms) return '0ms';
    if (ms < 1000) return `${ms}ms`;
    if (ms < 60000) return `${(ms / 1000).toFixed(1)} 秒`;
    return `${Math.floor(ms / 60000)} 分 ${Math.floor((ms % 60000) / 1000)} 秒`;
  }

  /**
   * 生成通知数据（供 OpenClaw 调用）
   */
  generateNotificationData(result, type) {
    if (type === 'success') {
      return {
        type: 'success',
        title: '✅ OpenClaw 数据备份成功',
        message: this.formatSuccessMessage(result),
        channels: this.config.notification.channels,
        targets: this.config.notification.targets,
        data: {
          outputPath: result.outputPath,
          filesTotal: result.filesTotal,
          sizeBytes: result.sizeBytes,
          duration: result.duration,
          timestamp: new Date().toISOString()
        }
      };
    } else {
      return {
        type: 'failure',
        title: '❌ OpenClaw 数据备份失败',
        message: this.formatFailureMessage(result),
        channels: this.config.notification.channels,
        targets: this.config.notification.targets,
        data: {
          errors: result.errors,
          timestamp: new Date().toISOString()
        }
      };
    }
  }
}

module.exports = { Notifier };

path: scripts/scheduler.js
content: /**
 * 调度模块
 * 负责定时任务的调度和执行
 */

const fs = require('fs-extra');
const path = require('path');
const dayjs = require('dayjs');
const utc = require('dayjs/plugin/utc');
const timezone = require('dayjs/plugin/timezone');
const { info, warn, error, debug } = require('./logger');
const { updateConfig, loadConfig } = require('./config');

dayjs.extend(utc);
dayjs.extend(timezone);

/**
 * 创建调度器实例
 */
class Scheduler {
  constructor(config) {
    this.config = config;
    this.lastCheckTime = null;
  }

  /**
   * 检查是否应该执行备份
   * HEARTBEAT 触发时调用
   */
  async shouldRun() {
    try {
      // 重新加载配置（可能有更新）
      this.config = await loadConfig();
      
      if (!this.config.schedule.enabled) {
        return false;
      }
      
      const now = dayjs().tz(this.config.schedule.timezone || 'Asia/Shanghai');
      const cron = this.parseCron(this.config.schedule.cron);
      
      if (!cron) {
        await error('Scheduler', `无效的 cron 表达式: ${this.config.schedule.cron}`);
        return false;
      }
      
      // 检查是否匹配当前时间
      const matches = this.matchesCron(now, cron);
      
      if (matches) {
        // 检查今天是否已经执行过
        if (this.config.schedule.lastRun) {
          const lastRun = dayjs(this.config.schedule.lastRun).tz(this.config.schedule.timezone || 'Asia/Shanghai');
          
          if (lastRun.isSame(now, 'day')) {
            // 今天已经执行过
            return false;
          }
        }
        
        return true;
      }
      
      return false;
    } catch (err) {
      await error('Scheduler', `检查调度失败: ${err.message}`);
      return false;
    }
  }

  /**
   * 解析 cron 表达式
   * 支持: minute hour dayOfMonth month dayOfWeek
   */
  parseCron(expression) {
    try {
      const parts = expression.trim().split(/\s+/);
      
      if (parts.length !== 5) {
        return null;
      }
      
      return {
        minute: this.parseCronPart(parts[0], 0, 59),
        hour: this.parseCronPart(parts[1], 0, 23),
        dayOfMonth: this.parseCronPart(parts[2], 1, 31),
        month: this.parseCronPart(parts[3], 1, 12),
        dayOfWeek: this.parseCronPart(parts[4], 0, 6)
      };
    } catch (err) {
      return null;
    }
  }

  /**
   * 解析 cron 单个部分
   */
  parseCronPart(part, min, max) {
    if (part === '*') {
      return null; // 匹配所有
    }
    
    const values = new Set();
    
    // 处理逗号分隔
    const segments = part.split(',');
    
    for (const segment of segments) {
      // 处理范围 (e.g., 1-5)
      if (segment.includes('-')) {
        const [start, end] = segment.split('-').map(Number);
        for (let i = start; i <= end; i++) {
          if (i >= min && i <= max) {
            values.add(i);
          }
        }
      }
      // 处理步进 (e.g., */5)
      else if (segment.includes('/')) {
        const [, step] = segment.split('/');
        const stepNum = parseInt(step);
        for (let i = min; i <= max; i += stepNum) {
          values.add(i);
        }
      }
      // 单个值
      else {
        const num = parseInt(segment);
        if (!isNaN(num) && num >= min && num <= max) {
          values.add(num);
        }
      }
    }
    
    return values.size > 0 ? values : null;
  }

  /**
   * 检查时间是否匹配 cron
   */
  matchesCron(now, cron) {
    // 检查分钟
    if (cron.minute && !cron.minute.has(now.minute())) {
      return false;
    }
    
    // 检查小时
    if (cron.hour && !cron.hour.has(now.hour())) {
      return false;
    }
    
    // 检查日期
    if (cron.dayOfMonth && !cron.dayOfMonth.has(now.date())) {
      return false;
    }
    
    // 检查月份
    if (cron.month && !cron.month.has(now.month() + 1)) {
      return false;
    }
    
    // 检查星期
    if (cron.dayOfWeek && !cron.dayOfWeek.has(now.day())) {
      return false;
    }
    
    return true;
  }

  /**
   * 记录执行时间
   */
  async recordRun() {
    try {
      await updateConfig({
        schedule: {
          ...this.config.schedule,
          lastRun: new Date().toISOString()
        }
      });
      
      await info('Scheduler', `记录执行时间: ${new Date().toISOString()}`);
    } catch (err) {
      await error('Scheduler', `记录执行时间失败: ${err.message}`);
    }
  }

  /**
   * 获取下次执行时间
   */
  getNextRunTime() {
    try {
      const cron = this.parseCron(this.config.schedule.cron);
      
      if (!cron) {
        return null;
      }
      
      const now = dayjs().tz(this.config.schedule.timezone || 'Asia/Shanghai');
      let next = now.add(1, 'minute');
      
      // 简单实现：找到下一个匹配的小时和分钟
      // 如果 cron 是 "0 3 * * *"，则返回明天 3:00
      if (cron.hour && cron.hour.size === 1) {
        const targetHour = [...cron.hour][0];
        const targetMinute = cron.minute ? [...cron.minute][0] : 0;
        
        next = now.hour(targetHour).minute(targetMinute).second(0);
        
        // 如果今天的时间已过，则返回明天
        if (next.isBefore(now) || next.isSame(now)) {
          next = next.add(1, 'day');
        }
        
        return next.format('YYYY-MM-DD HH:mm:ss');
      }
      
      // 通用实现：逐分钟检查
      for (let i = 1; i <= 1440; i++) { // 最多检查一天
        next = now.add(i, 'minute');
        if (this.matchesCron(next, cron)) {
          return next.format('YYYY-MM-DD HH:mm:ss');
        }
      }
      
      return null;
    } catch (err) {
      return null;
    }
  }

  /**
   * 获取调度状态
   */
  getStatus() {
    return {
      enabled: this.config.schedule.enabled,
      cron: this.config.schedule.cron,
      timezone: this.config.schedule.timezone,
      lastRun: this.config.schedule.lastRun,
      nextRun: this.getNextRunTime()
    };
  }
}

/**
 * 人类可读的 cron 描述
 */
function describeCron(expression) {
  try {
    const cron = new Scheduler({ schedule: { cron: expression, timezone: 'Asia/Shanghai' } }).parseCron(expression);
    
    if (!cron) {
      return '无效的 cron 表达式';
    }
    
    // 常见模式
    if (expression === '0 3 * * *') {
      return '每天凌晨 3:00';
    }
    if (expression === '0 4 * * *') {
      return '每天凌晨 4:00';
    }
    if (expression === '0 0 * * *') {
      return '每天凌晨 0:00';
    }
    if (expression === '0 3 * * 0') {
      return '每周日凌晨 3:00';
    }
    if (expression === '0 3 1 * *') {
      return '每月 1 日凌晨 3:00';
    }
    
    // 自定义描述
    const parts = expression.split(' ');
    const minute = parts[0];
    const hour = parts[1];
    
    if (hour !== '*' && minute !== '*') {
      return `每天 ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
    }
    
    return expression;
  } catch (err) {
    return expression;
  }
}

module.exports = { Scheduler, describeCron };

path: references/config-schema.md
content: # 配置字段说明

本文档详细说明 `Auto-Backup-Openclaw-User-Data` 技能的所有配置字段。

---

## 配置文件位置

```
~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/config.json
```

---

## 配置结构

```json
{
  "version": "1.1.0",
  "createdAt": "2026-04-14T15:00:00+08:00",
  "updatedAt": "2026-04-14T15:00:00+08:00",

  "backup": { ... },
  "schedule": { ... },
  "output": { ... },
  "retention": { ... },
  "notification": { ... },
  "logging": { ... }
}
```

---

## 字段说明

### 顶层字段

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `version` | string | 是 | "1.1.0" | 配置文件版本 |
| `createdAt` | string | 否 | - | 创建时间（ISO 8601） |
| `updatedAt` | string | 否 | - | 最后更新时间（ISO 8601） |

---

### backup 备份配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `mode` | string | 是 | "full" | 备份模式：`full`（全量）或 `partial`（选择性） |
| `targets` | array | 否 | [...] | 选择性备份时的目标目录列表 |
| `exclude` | array | 否 | [...] | 排除的目录名列表 |
| `excludePatterns` | array | 否 | [...] | 排除的文件名模式（支持通配符） |
| `sensitiveExcludeSuggestion` | array | 否 | [...] | 敏感文件建议排除列表 |
| `sensitiveExcludeDirectories` | array | 否 | [...] | 敏感目录建议排除列表 |
| `enableSensitiveExclude` | boolean | 否 | false | 是否启用敏感文件排除 |

#### mode 备份模式

- `full`：全量备份 `.openclaw` 目录
- `partial`：选择性备份，只备份 `targets` 中指定的目录

#### targets 备份目标

仅在 `mode: "partial"` 时生效，可用的目标目录：

```
workspace       # 主工作空间
workspace-1     # 1工作空间
workspace-2     # 2工作空间
workspace-3     # 3工作空间
memory          # 记忆文件
```

#### exclude 排除目录

默认排除的目录：

```
logs               # 日志目录
cache              # 缓存目录
tmp                # 临时目录
node_modules       # Node.js 依赖
```

#### excludePatterns 排除模式

支持通配符：

```
*.log              # 所有 .log 文件
*.tmp              # 所有 .tmp 文件
.DS_Store          # macOS 系统文件
Thumbs.db          # Windows 缩略图缓存
```

#### sensitiveExcludeSuggestion 敏感文件建议列表

默认敏感文件模式（仅在启用时生效）：

```
*.key              # 密钥文件
*.pem              # PEM证书文件
*.p12              # PKCS#12证书
*.pfx              # PFX证书
.env               # 环境变量文件
.env.local         # 本地环境变量
.env.*.local       # 环境变量文件变体
credentials.json   # 凭证文件
secrets.json       # 密钥文件
*.token            # Token文件
*.secret           # 密钥文件
*_token.json       # Token JSON文件
id_rsa             # SSH私钥
id_dsa             # DSA私钥
*.ppk              # PuTTY私钥
```

#### sensitiveExcludeDirectories 敏感目录建议列表

默认敏感目录（仅在启用时生效）：

```
credentials        # 凭证目录
secrets            # 密钥目录
.ssh               # SSH配置目录
.gnupg             # GPG配置目录
```

#### enableSensitiveExclude 启用敏感文件排除

- `true`：启用敏感文件排除（将建议列表合并到exclude中）
- `false`：不启用敏感文件排除（默认值）

**注意**：启用后，`sensitiveExcludeSuggestion` 和 `sensitiveExcludeDirectories` 中的内容将被添加到 `exclude` 和 `excludePatterns` 中。

---

### schedule 定时配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `enabled` | boolean | 是 | true | 是否启用定时备份 |
| `cron` | string | 是 | "0 3 * * *" | cron 表达式 |
| `timezone` | string | 否 | "Asia/Shanghai" | 时区 |
| `lastRun` | string | 否 | null | 上次执行时间 |

#### cron 表达式格式

```
分钟 小时 日期 月份 星期
  *    *    *    *    *

示例：
"0 3 * * *"      # 每天凌晨 3:00
"0 4 * * *"      # 每天凌晨 4:00
"0 3 * * 0"      # 每周日凌晨 3:00
"0 3 1 * *"      # 每月 1 日凌晨 3:00
"*/30 * * * *"   # 每 30 分钟
```

---

### output 输出配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `path` | string | 是 | ... | 备份文件存储路径 |
| `naming` | object | 否 | {...} | 命名规则 |
| `encryption` | object | 否 | {...} | 加密配置 |

#### naming 命名规则

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `prefix` | string | "auto-backup-openclaw-user-data" | 文件名前缀 |
| `includeVersion` | boolean | true | 是否包含 OpenClaw 版本号 |
| `includeSequence` | boolean | true | 是否包含当日序号 |

#### 命名格式

```
{prefix}_{YYYYMMDD}_{HHMM}_{version}_{序号}.zip

示例：
auto-backup-openclaw-user-data_20260326_0300_v2026.3.23-2_01.zip
```

#### encryption 加密配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `enabled` | boolean | 否 | false | 是否启用加密 |
| `password` | string | 否 | null | 加密密码（启用时必填） |
| `algorithm` | string | 否 | "aes-256" | 加密算法（默认AES-256） |
| `reminderShown` | boolean | 否 | false | 是否已显示密码提醒 |

**加密说明**：
- 使用AES-256加密算法保护备份文件
- 密码存储在配置文件中，请妥善保管配置文件
- 建议将密码备份到密码管理器或其他安全位置

**密码设置方式**：
1. **自定义密码**：用户输入密码（至少8位）
2. **随机密码**：系统生成16位随机密码

**解密兼容性**：
- ✅ 支持：7-Zip, WinRAR, PeaZip
- ❌ 不支持：macOS Finder原生解压, Windows资源管理器原生解压

**风险提示**：
- 密码丢失将无法解密备份文件
- 配置文件损坏或丢失将无法找回密码
- 建议备份密码到安全位置

---

### retention 保留策略配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `enabled` | boolean | 是 | true | 是否启用保留策略 |
| `mode` | string | 是 | "count" | 清理模式：`days` 或 `count` |
| `days` | number | 否 | 30 | 保留天数（mode=days 时） |
| `count` | number | 否 | 10 | 保留份数（mode=count 时） |

#### mode 清理模式

- `days`：按天数清理，保留最近 N 天的备份
- `count`：按份数清理，保留最近 N 份备份

---

### notification 通知配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `enabled` | boolean | 是 | true | 是否启用通知 |
| `channels` | array | 是 | ["feishu"] | 通知渠道列表 |
| `targets` | object | 否 | {} | 推送目标配置 |
| `onSuccess` | boolean | 否 | true | 备份成功时发送通知 |
| `onFailure` | boolean | 否 | true | 备份失败时发送通知 |

#### channels 可用渠道

```
feishu       # 飞书
telegram     # Telegram
discord      # Discord
slack        # Slack
```

**注意**：需要先在 OpenClaw 中配置相应的渠道。

#### targets 推送目标配置

`targets` 字段用于指定每个渠道的具体推送目标（用户或群组）：

```json
{
  "targets": {
    "feishu": [
      { "type": "group", "id": "oc_xxx", "name": "开发群" },
      { "type": "user", "id": "ou_xxx", "name": "主人" }
    ],
    "telegram": [
      { "type": "group", "id": "-100xxx", "name": "通知群" },
      { "type": "user", "id": "123456789", "name": "用户名" }
    ]
  }
}
```

**字段说明**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 目标类型：`group`（群组）或 `user`（用户） |
| `id` | string | 目标 ID（飞书群组为 `oc_xxx`，用户为 `ou_xxx`） |
| `name` | string | 目标名称（用于显示，可选） |

**如何获取推送目标**：

1. 运行 `/backup_config` 进入交互式配置
2. 选择通知渠道后，系统会自动列出可用的推送目标
3. 从列表中选择需要的目标

**注意事项**：
- 如果未配置 `targets`，系统会尝试通过当前对话发送通知
- 推送目标来自 `~/.openclaw/openclaw.json` 中的 `bindings` 配置
- 群组 ID 需要是完整的 ID（如 `oc_xxx`），不能是群名称

---

### logging 日志配置

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `enabled` | boolean | 是 | true | 是否启用日志 |
| `level` | string | 否 | "info" | 日志级别 |
| `maxSize` | string | 否 | "10MB" | 单个日志文件最大大小 |
| `maxFiles` | number | 否 | 5 | 保留的日志文件数量 |

#### level 日志级别

```
DEBUG    # 调试信息
INFO     # 一般信息
WARN     # 警告信息
ERROR    # 错误信息
```

---

## 配置示例

### 最小配置

```json
{
  "version": "1.1.0",
  "backup": {
    "mode": "partial"
  },
  "schedule": {
    "enabled": true,
    "cron": "0 3 * * *"
  },
  "output": {
    "path": "~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backups"
  },
  "retention": {
    "enabled": true,
    "mode": "count",
    "count": 10
  },
  "notification": {
    "enabled": true,
    "channels": ["feishu"]
  },
  "logging": {
    "enabled": true
  }
}
```

### 完整配置

```json
{
  "version": "1.1.0",
  "createdAt": "2026-04-14T15:00:00+08:00",
  "updatedAt": "2026-04-14T15:00:00+08:00",

  "backup": {
    "mode": "partial",
    "targets": [],  // ← 改为空数组，首次加载时动态检测
    "exclude": ["logs", "cache", "tmp", "node_modules"],
    "excludePatterns": ["*.log", "*.tmp", ".DS_Store", "Thumbs.db"],

    // 敏感文件排除建议列表（默认不启用）
    "sensitiveExcludeSuggestion": [
      "*.key", "*.pem", "*.p12", "*.pfx",
      ".env", ".env.local", ".env.*.local",
      "credentials.json", "secrets.json",
      "*.token", "*.secret", "*_token.json",
      "id_rsa", "id_dsa", "*.ppk"
    ],
    "sensitiveExcludeDirectories": [
      "credentials", "secrets", ".ssh", ".gnupg"
    ],
    "enableSensitiveExclude": false
  },
  
  "schedule": {
    "enabled": true,
    "cron": "0 3 * * *",
    "timezone": "Asia/Shanghai",
    "lastRun": null
  },
  
  "output": {
    "path": "D:/Backups/openclaw",
    "naming": {
      "prefix": "auto-backup-openclaw-user-data",
      "includeVersion": true,
      "includeSequence": true
    },
    "encryption": {
      "enabled": false,
      "password": null,
      "algorithm": "aes-256",
      "reminderShown": false
    }
  },
  
  "retention": {
    "enabled": true,
    "mode": "days",
    "days": 30,
    "count": 10
  },
  
  "notification": {
    "enabled": true,
    "channels": ["feishu", "telegram"],
    "targets": {
      "feishu": [
        { "type": "group", "id": "oc_xxx", "name": "开发群" }
      ],
      "telegram": [
        { "type": "group", "id": "-100xxx", "name": "通知群" }
      ]
    },
    "onSuccess": true,
    "onFailure": true
  },
  
  "logging": {
    "enabled": true,
    "level": "info",
    "maxSize": "10MB",
    "maxFiles": 5
  }
}
```

---

## 配置验证

修改配置后，运行以下命令验证：

```
/backup_config
选择 [4] 查看当前配置
```

如果配置格式错误，系统会自动使用默认配置并提示错误信息。

---

## 获取帮助

如果以上方法无法解决问题：

1. 查看完整日志文件
2. 在 GitHub 提交 Issue
3. 联系作者：[REDACTED_SECRET]

---

**文档版本**：v1.1.0.20260414
**更新日期**：2026-04-14
**作者**：水木开发团队-Jack·Huang

path: references/troubleshooting.md
content: # 故障排查指南

本文档提供 `Auto-Backup-Openclaw-User-Data` 技能的常见问题排查方法。

---

## 目录

1. [安装问题](#安装问题)
2. [备份执行问题](#备份执行问题)
3. [定时任务问题](#定时任务问题)
4. [通知问题](#通知问题)
5. [配置问题](#配置问题)
6. [日志查看](#日志查看)

---

## 安装问题

### Q: 安装后命令无响应

**症状**：执行 `/backup_now` 等命令没有任何响应。

**排查步骤**：

1. 检查 skill 是否正确安装
   ```
   查看目录：~/.agents/skills/auto-backup-openclaw-user-data/
   ```

2. 检查 SKILL.md 是否存在
   ```
   文件路径：~/.agents/skills/auto-backup-openclaw-user-data/SKILL.md
   ```

3. 重启 OpenClaw
   ```bash
   openclaw restart
   或
   openclaw gateway restart
   ```

### Q: 首次使用提示配置文件错误

**症状**：提示配置文件不存在或格式错误。

**原因**：首次使用时，系统会自动创建默认配置文件。

**解决方案**：
- 正常情况下系统会自动创建配置
- 如果手动创建了配置文件，请确保 JSON 格式正确
- 运行 `/backup_config` 选择 [3] 重置为默认配置

---

## 备份执行问题

### Q: 备份失败，提示 "磁盘空间不足"

**错误信息**：`ENOSPC: no space left on device`

**解决方案**：

1. 清理磁盘空间
2. 更改备份存储路径
   ```
   /backup_config
   选择修改存储路径
   ```
3. 调整保留策略，减少保留的备份数量

### Q: 备份失败，提示 "权限不足"

**错误信息**：`EACCES: permission denied`

**解决方案**：

1. 检查目标目录权限
   ```bash
   # Linux/macOS
   ls -la ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/
   
   # Windows (以管理员身份运行)
   icacls "%USERPROFILE%\.openclaw\workspace\Auto-Backup-Openclaw-User-Data"
   ```

2. 以管理员/root 权限运行（不推荐常规操作）

3. 更改备份存储路径到有权限的目录

### Q: 部分文件被跳过

**症状**：备份完成但提示 "跳过 N 个文件"。

**原因**：
- 文件被其他程序占用
- 文件不存在或已删除
- 文件匹配排除规则

**解决方案**：

1. 查看日志了解具体跳过的文件
   ```
   日志位置：~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log
   ```

2. 如果是正常占用（如正在写入的日志），无需处理

3. 如果需要备份被跳过的文件，检查排除规则配置

### Q: 备份速度很慢

**可能原因**：
- 备份文件数量多
- 文件体积大
- 磁盘性能低

**优化方案**：

1. 使用选择性备份，减少备份目标
   ```
   /backup_config
   设置 mode: "partial"
   ```

2. 增加排除规则
   ```json
   "exclude": ["logs", "cache", "tmp", "node_modules", "dist", "build"]
   ```

3. 使用更快的存储介质（如 SSD）

---

## 定时任务问题

### Q: 定时备份没有执行

**排查步骤**：

1. 检查定时任务是否启用
   ```
   /backup_status
   查看 "定时备份" 状态
   ```

2. 检查 cron 表达式是否正确
   ```
   /backup_config
   查看 schedule.cron 配置
   ```

3. 检查 HEARTBEAT 是否配置
   ```
   查看：~/.openclaw/HEARTBEAT.md
   ```

4. 查看 lastRun 时间
   ```
   /backup_status
   查看 "上次执行" 时间
   ```

### Q: 想要修改备份时间

**解决方案**：

1. 运行配置命令
   ```
   /backup_config
   ```

2. 修改 cron 表达式
   ```
   凌晨 2 点：0 2 * * *
   凌晨 4 点：0 4 * * *
   中午 12 点：0 12 * * *
   ```

---

## 通知问题

### Q: 没有收到通知

**排查步骤**：

1. 检查通知是否启用
   ```
   /backup_status
   查看 "通知渠道"
   ```

2. 检查 OpenClaw 渠道配置
   ```
   OpenClaw 需要先配置飞书/Telegram 等渠道
   配置文件：~/.openclaw/openclaw.json
   ```

3. 检查通知配置
   ```json
   "notification": {
     "enabled": true,
     "channels": ["feishu"],
     "targets": {
       "feishu": [
         { "type": "group", "id": "oc_xxx", "name": "群名" }
       ]
     },
     "onSuccess": true,
     "onFailure": true
   }
   ```

4. 检查是否配置了推送目标
   - 运行 `/backup_config` 进入交互式配置
   - 在 Step 5 选择通知渠道后，确保选择了具体的推送目标

### Q: 提示"渠道未配置"或"推送失败"

**原因**：
- OpenClaw 中未启用对应渠道
- 推送目标 ID 不正确
- 渠道配置不完整

**解决方案**：

1. 检查 OpenClaw 配置
   ```
   查看 ~/.openclaw/openclaw.json 中的 channels 部分
   确保对应渠道 enabled: true
   ```

2. 重新配置推送目标
   ```
   /backup_config
   选择 [1] 交互式配置
   按步骤选择正确的推送目标
   ```

3. 手动添加推送目标
   ```json
   "notification": {
     "targets": {
       "feishu": [
         { "type": "group", "id": "oc_你的群ID", "name": "群名称" }
       ]
     }
   }
   ```

### Q: 如何获取群组 ID 或用户 ID？

**飞书群组 ID**：
1. 在飞书群聊中，点击群设置
2. 查看群信息，找到群 ID（格式：`oc_xxx`）

**飞书用户 ID**：
1. 在飞书中查看用户资料
2. 找到 Open ID（格式：`ou_xxx`）

**Telegram 群组 ID**：
1. 将 @userinfobot 添加到群组
2. 它会返回群组 ID（格式：`-100xxx`）

### Q: 想要关闭通知

**解决方案**：

修改配置文件：
```json
"notification": {
  "enabled": false
}
```

或运行：
```
/backup_config
选择 [1] 交互式配置
在通知渠道步骤不选择任何渠道
```

---

## 配置问题

### Q: 配置文件格式错误

**症状**：提示配置文件 JSON 格式无效。

**解决方案**：

1. 使用 JSON 验证工具检查格式
   - 确保 JSON 中字符串使用双引号
   - 确保最后一项后没有多余的逗号
   - 确保括号配对正确

2. 重置配置
   ```
   /backup_config
   选择 [3] 重置为默认配置
   ```

### Q: 工作空间targets为空或不正确（v1.1.0新增）

**症状**：首次配置后targets为空或缺少某些workspace。

**原因**：v1.1.0版本新增工作空间动态检测功能，首次配置时会自动检测~/.openclaw/目录中的所有workspace-*目录和memory目录。

**解决方案**：

1. 检查检测结果
   ```
   /backup_config
   选择 [4] 查看当前配置
   查看 backup.targets 字段
   ```

2. 手动调整targets
   ```json
   {
     "backup": {
       "targets": ["workspace", "workspace-01", "workspace-02", "memory"]
     }
   }
   ```

3. 使用交互式配置确认
   ```
   /backup_config
   选择 [1] 交互式配置
   Step 1.1 会列出实际存在的workspace供选择
   ```

### Q: 敏感文件是否应该启用排除？（v1.1.0新增）

**症状**：不确定是否需要启用敏感文件排除功能。

**说明**：
- **默认行为**：系统默认不启用敏感文件排除，仅排除临时文件（logs、cache、tmp等）
- **建议**：根据实际需求自主选择是否启用

**决策建议**：
- ✅ **启用排除**：如果备份可能泄露给他人，建议启用，保护敏感数据
- ⚠️ **不启用**：如果备份仅自己使用且妥善保管，可不启用，完整备份所有数据

**如何启用**：
```
/backup_config
选择 [1] 交互式配置
Step 7: 敏感文件排除配置
选择 [1] 启用排除
```

**手动配置**：
```json
{
  "backup": {
    "enableSensitiveExclude": true,
    "excludePatterns": [
      "*.log", "*.tmp", ".DS_Store", "Thumbs.db",
      "*.key", "*.pem", "*.p12", "*.pfx",
      ".env", ".env.local", "credentials.json", "secrets.json"
    ],
    "exclude": ["logs", "cache", "tmp", "node_modules", ".ssh", ".gnupg"]
  }
}
```

### Q: 想要修改配置但不知道格式

**解决方案**：

1. 查看当前配置
   ```
   /backup_config
   选择 [4] 查看当前配置
   ```

2. 查看配置字段说明
   ```
   参考：references/config-schema.md
   ```

---

## 日志查看

### 日志位置

```
~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log
```

### 查看方法

**Linux/macOS**：
```bash
# 查看最新 100 行
tail -100 ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log

# 实时查看
tail -f ~/.openclaw/workspace/Auto-Backup-Openclaw-User-Data/backup.log
```

**Windows (PowerShell)**：
```powershell
# 查看最新 100 行
Get-Content "$env:USERPROFILE\.openclaw\workspace\Auto-Backup-Openclaw-User-Data\backup.log" -Tail 100

# 实时查看
Get-Content "$env:USERPROFILE\.openclaw\workspace\Auto-Backup-Openclaw-User-Data\backup.log" -Wait
```

### 日志级别

```
[DEBUG] 调试信息（默认不显示）
[INFO]  一般信息
[WARN]  警告信息
[ERROR] 错误信息
```

### 日志示例

```
[2026-03-26 15:30:00.123] [INFO]  [Backup] 开始执行备份任务
[2026-03-26 15:30:00.456] [INFO]  [Backup] 备份目标: 1 个目录
[2026-03-26 15:30:01.789] [INFO]  [Backup] 共收集到 1234 个文件
[2026-03-26 15:30:15.345] [INFO]  [Compressor] 压缩完成: auto-backup-...zip (125.6 MB)
[2026-03-26 15:30:15.678] [INFO]  [Backup] 备份完成！耗时: 15555ms
```

---

## 常见错误代码

| 错误代码 | 说明 | 解决方案 |
|----------|------|----------|
| `ENOENT` | 文件或目录不存在 | 检查路径是否正确 |
| `EACCES` | 权限不足 | 检查文件权限 |
| `ENOSPC` | 磁盘空间不足 | 清理磁盘或更换路径 |
| `EBUSY` | 文件被占用 | 关闭占用文件的程序 |
| `EMFILE` | 打开文件过多 | 减少备份文件数量 |

---

## 获取帮助

如果以上方法无法解决问题：

1. 查看完整日志文件
2. 在 GitHub 提交 Issue
3. 联系作者：[REDACTED_SECRET]

---

**文档版本**：v1.1.0  
**更新日期**：2026-04-14  
**作者**：水木开发团队-Jack·Huang

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