Skill: zhao-zwl/quick-resurrection (version 1.0.6)

--- name: quick-resurrection 打包并恢复你的 OpenClaw Agent 工作区配置。换电脑或重装系统后，一个命令把整个团队迁移到新环境。执行前展示配置 diff 供用户确认，支持 --dry-run/--no-cron/--no-restart 细粒度控制。 metadata: openclaw: requires: bins: [openclaw] install: [] version: "1.0.6" permissions: - action: "修改 ~/.qclaw/openclaw.json（添加 agent 配置）" risk: "修改运行时配置，影响 agent spawn 行为" mitigation: "修改前展示完整 diff，用户确认后再写入；allowAgents 默认最小白名单，不用通配符" - action: "读写 ~/.qclaw/ 目录（复制 workspace、备份配置）" risk: "可能暴露 MEMORY.md/TOOLS.md 等敏感文件" mitigation: "打包时标注敏感文件，搬家包妥善保管" - action: "创建 cron 定时任务" risk: "定时任务可自动执行任意命令" mitigation: "执行前展示所有 cron 任务内容（含 payload），用户确认后自动创建" - action: "打包含敏感文件（MEMORY.md/TOOLS.md 可能含 API keys、账号密码）" risk: "搬家包泄露会暴露所有敏感配置和私人记忆" mitigation: "SKILL.md 明确警告不要分享搬家包；建议传输时加密" - action: "调用 openclaw CLI（list tasks、restart gateway 等）" risk: "依赖外部命令行工具" mitigation: "requires.bins 已声明 openclaw 依赖" --- # Quick Resurrection > **触发：** 用户说「我要搬家」「换电脑了」「重装系统」「帮我打包带走团队」等。 > > **版本：v3.0** — 审查确认 + diff 展示 + 细粒度控制（--dry-run/--no-cron/--no-restart）。 --- ## 快速开始 ### 打包（旧环境） ```bash cd ~/.qclaw/workspace/skills/quick-resurrection python3 pack.py ``` 生成搬家包 → 复制到新环境。 ### 迁移（新环境） ```bash # 审查模式：只看不做（推荐首次使用） python3 migrate.py /path/to/搬家包.zip --dry-run # 基本用法 python3 migrate.py /path/to/搬家包.zip # 跳过危险操作 python3 migrate.py /path/to/搬家包.zip --no-cron --no-restart ``` --- ## ⚠️ 重要提示 **打包内容含敏感信息。** 搬家包会包含 MEMORY.md（含个人记忆/偏好）、TOOLS.md（含 API keys、账号密码、服务器地址）等文件，可能含有 API keys、账号密码、内部流程等敏感内容。**不要公开分享搬家包**，传输时使用加密介质。 **安全使用建议：** 1. **务必先 `--dry-run`** — 在新环境运行前，先用 `python3 migrate.py 搬家包.zip --dry-run` 审查 diff，确认无误后再正式执行。 2. **敏感操作可跳过** — 使用 `--no-cron` 跳过 cron 创建、`--no-restart` 跳过 Gateway 重启，逐步验证。 3. **检查搬家包内容** — 解压后先检查 MEMORY.md、TOOLS.md、openclaw-agents.json、cron payload 是否包含不想迁移的敏感内容。 4. **确认备份存在** — 操作前确认 `~/.qclaw/backup/` 下有备份目录，如需回滚可恢复。 5. **检查绝对路径** — 验证 openclaw-agents.json 中的 workspace 路径指向的是目标环境而非旧环境的路径。 6. **低信任环境** — 如对搬家包来源不确定，先逐行审计 pack.py/migrate.py，或在隔离环境中测试。 --- ## 一、核心概念 ### 三要素激活原理 子代理正确激活必须同时满足三个条件： | 要素 | 参数 | 作用 | |------|------|------| | **身份** | `agentId` | 控制读哪个 SOUL.md（人格） | | **隔离** | `cwd` | 控制 workspace 根目录 | | **权限** | `allowAgents` | 白名单，允许 spawn | **关键发现（2026-04-21 实测验证）：** - `agentId` 控制人格，不控制 workspace - `cwd` 才控制 workspace 隔离 - 不设 cwd → 子代理永远读父代理的 workspace ### 通用化设计 | 场景 | pack.py 行为 | migrate.py 行为 | |------|-------------|----------------| | 有团队成员 | 打包团队成员 | 复制团队成员 | | 无团队成员 | 跳过 | 跳过，提示 | | 有 cron | 打包 | 创建（跳过已存在） | | 无 cron | 跳过 | 跳过，提示 | ### 备份与回滚 **搬家前自动备份：** - 目标：`~/.qclaw/backup/搬家备份_YYYYMMDD_HHMMSS/` - 内容：所有 workspace-* + openclaw.json **回滚方法：** ```bash cp -r ~/.qclaw/backup/搬家备份_xxx/* ~/.qclaw/ openclaw gateway restart ``` ### 配置合并策略 **核心原则：不覆盖新环境的其他配置。** - agents 配置：deep merge - hooks.allowedAgentIds：union merge（取并集） - 其他配置（channel、plugins 等）：原样保留 --- ## 二、脚本说明 ### 打包脚本 pack.py ```bash python3 pack.py ``` **自动检测内容：** 1. 从 openclaw.json 找到 active workspace（自动推断，不再硬编码） 2. 收集身份文件（SOUL/MEMORY/TOOLS 等） 3. 收集团队成员（有则打包，无则跳过） 4. 收集 skills（有则打包，无则跳过） 5. 获取 cron 任务配置 6. 动态生成 README.md **输出：** `~/一键搬家包/{Agent名称}搬家包_YYYYMMDD.zip` ### 迁移脚本 migrate.py ```bash # 方式A（推荐）：解压后直接运行 unzip 搬家包.zip cd 搬家包 python3 migrate.py # 方式B：zip 在当前目录 python3 migrate.py # 方式C：传入路径 python3 migrate.py /path/to/搬家包.zip ``` **执行步骤：** | Step | 内容 | |------|------| | 0 | 备份现有配置（自动） | | 0.5 | 选择如何处理 main agent（交互） | | 1 | 复制身份文件 | | 2 | 复制团队成员 | | 3 | 复制 skills | | 4 | 合并 agent 配置 | | 5 | 创建 cron 任务 | | 6 | 重启 Gateway | **Main Agent 三个选项（均有完整执行逻辑）：** ``` 1. 指向现有 agent（把当前 agent 变成你的主控） 2. 新建 main agent 实例（另起一个，保留当前配置） 3. 覆盖现有 main agent（⚠️ 替换现有主控） ``` ⚠️ **已废弃脚本（v2.0 不再使用）：** - `setup_config.py` — 配置更新已并入 migrate.py - `init.sh` — 创建 main agent 已并入 migrate.py --- ## 三、搬家包结构 ``` 搬家包/ ├── README.md ← 动态生成 ├── migrate.py ← v2.0 │ ├── 身份层/ ← Main Agent 核心文件 │ ├── SOUL.md │ ├── MEMORY.md │ ├── TOOLS.md │ ├── AGENTS.md │ ├── IDENTITY.md │ ├── USER.md │ └── memory/ ← 历史记录 │ ├── 团队成员层/ ← 自动检测，有则包含 │ ├── 成员A/ │ │ └── SOUL.md │ └── ... │ ├── skills/ ← 自动检测，有则包含 │ ├── openclaw-agents.json ← Agent 配置片段 ├── cron_jobs.json ← Cron 任务清单 └── 工作目录说明.md ← git 仓库提示 ``` --- ## 四、配置说明 ### openclaw.json 关键配置 ```json { "agents": { "defaults": { "subagents": { "allowAgents": ["成员A", "成员B"] } // 最小白名单，仅实际成员ID }, "list": [ { "id": "main", "name": "Main Agent" }, { "id": "成员A", "name": "成员A", "workspace": "...", "agentDir": "..." } ] }, "hooks": { "allowedAgentIds": ["成员A", "成员B", ...] } } ``` ### models.json（每个 agent 独立） ```json { "providers": { "qclaw": { "baseUrl": "http://127.0.0.1:19000/proxy/llm", "apiKey": "__QCLAW_AUTH_GATEWAY_MANAGED__", "api": "openai-completions", "models": [{ "id": "modelroute", "name": "modelroute", "input": ["text", "image"] }] } } } ``` --- ## 五、常见问题 | 问题 | 原因 | 解法 | |------|------|------| | `agentId is not allowed` | 缺 allowAgents 白名单 | migrate.py 自动补上最小白名单（仅实际成员ID） | | 子代理读错 SOUL.md | 没传 cwd 参数 | spawn 时必须传 agentId + cwd | | Gateway 重启后 spawn 报错 | channel 注册需时间 | 等 5-10 秒再 spawn | | 子代理空跑 | 任务描述不够具体 | 预写脚本让子代理执行 | | 搬家后配置丢失 | v1.0 用 replace 而非 merge | v2.0 已修复 | --- ## 六、验证清单 搬家完成后逐一检查： ```bash # Gateway 状态 openclaw gateway status # SOUL.md 是否存在 ls ~/.qclaw/workspace-*/SOUL.md # 团队成员 ls ~/.qclaw/workspace-*/ # 测试子代理激活 # 在 agent 对话中说："测试激活团队成员" # cron 任务 openclaw tasks list # 回滚（如需要） ls ~/.qclaw/backup/ cp -r ~/.qclaw/backup/搬家备份_xxx/* ~/.qclaw/ openclaw gateway restart ``` ```

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

## [2.0.0] - 2026-04-23

### Added
- `metadata.name` 字段（clawhub 识别必需）
- `SKILL.md` 精简重组，移除与 MATERIAL_PACKING.md 的重复内容

### Changed
- `SKILL.md`：全面精简（9768→4262字节），保留核心操作文档
- `README.md`：精简为快速入口文档
- `migrate.py`：修复选项1 bug（找workspace逻辑写错，candidates循环里写成死代码）

### Fixed
- `SKILL.md` 的 `metadata.name` 从 `team-resurrection` 修正为 `quick-resurrection`（与 clawhub slug 一致）

### Removed
- `MATERIAL_PACKING.md`：内容70%与SKILL.md重复，已废弃

---

## [1.0.0] - 2026-04-22

- 初始版本：pack.py + migrate.py
- 支持有/无团队的通用化检测
- Main Agent 三个选项（选项1/2 当时执行逻辑残缺）


path: MATERIAL_PACKING.md
content: # 一键搬家材料打包清单

> 搬家包完整内容记录，确保一键搬家不遗漏任何关键材料
> 
> **v2.0 更新**：backup/ 目录列入维护，新增 backup_existing() 自动备份逻辑。

---

## 一、搬家包结构

```
搬家包/
├── README.md                    ← 动态生成（v2.0自动推断内容）
├── migrate.py                   ← v2.0 一键执行脚本
│
├── 身份层/                      ← Main Agent（自动检测workspace）
│   ├── SOUL.md                  ← 人格定义（必须）
│   ├── MEMORY.md                ← 长期记忆（必须）
│   ├── TOOLS.md                 ← 工具配置（必须）
│   ├── AGENTS.md                ← 团队成员表（必须）
│   ├── IDENTITY.md              ← 身份标识（必须）
│   ├── USER.md                  ← 用户信息（必须）
│   └── memory/                  ← 历史记录
│
├── 团队成员层/                  ← 团队成员（自动检测，有则包含）
│   ├── 成员A/
│   │   └── SOUL.md
│   ├── 成员B/
│   │   └── SOUL.md
│   └── ...（其他成员）
│
├── skills/                      ← Skills（自动检测，有则包含）
│   ├── team-resurrection/       ← 本skill
│   ├── agent-creator/
│   ├── novel-writer-structure/
│   └── 短流创规则/
│
├── openclaw-agents.json         ← Agent配置片段
└── cron_tasks.json              ← Cron任务清单
```

---

## 二、脚本说明（v2.0）

### 核心脚本

| 脚本 | 功能 | 必需性 |
|------|------|--------|
| `pack.py` | 打包所有资料 | ✅ 必需 |
| `migrate.py` | 一键搬家执行 | ✅ 必需 |
| `setup_config.py` | ~~仅更新配置~~ | ❌ 已废弃 |
| `init.sh` | ~~创建main agent~~ | ❌ 已废弃 |

> ⚠️ v2.0 将 setup_config.py 和 init.sh 的功能全部并入 migrate.py，无需单独执行。

### pack.py（打包脚本）

**功能：** 自动检测 active workspace，收集所有资料，生成搬家包

**执行方式：**
```bash
cd skills/team-resurrection
python3 pack.py
```

**收集内容（v2.0 自动检测）：**
- Main Agent 信息（SOUL.md/MEMORY.md 等，自动推断 workspace）
- 团队成员信息（自动检测，有则打包）
- Skills（自动检测，有则打包）
- Cron 任务配置（自动获取）
- Agent 配置片段

**输出：** `~/一键搬家包/{Agent名称}搬家包_YYYYMMDD_HHMMSS.zip`

### migrate.py（一键执行脚本）

**功能：** 完整的一键搬家流程

**执行方式：**
```bash
unzip Agent搬家包.zip
python3 Agent搬家包/migrate.py
```

**执行步骤：**
1. 检测环境（是否有main agent）
2. **让用户选择如何处理main agent**（不强制）
3. 复制身份文件
4. 复制团队成员
5. 复制skills
6. 更新openclaw.json
7. 创建cron任务
8. 重启Gateway

**用户选择逻辑：**
```
如果有main agent：
  1. 指向现有agent（把当前 agent 指向新 workspace）
  2. 新建main agent实例（保留当前的main，创建新的 main agent）
  3. 覆盖现有main agent（⚠️ 当前的main会被替换）

如果没有main agent：
  1. 指向现有agent
  2. 新建main agent实例
```

### setup_config.py（配置更新脚本）

**功能：** 仅更新配置，不复制文件

**执行方式：**
```bash
python3 setup_config.py
python3 setup_config.py --skip-cron      # 跳过cron创建
python3 setup_config.py --skip-restart   # 跳过Gateway重启
```

**用途：** 如果文件已手动复制，只需更新配置时使用

### init.sh（Step 0脚本）

**功能：** 创建main agent实例

**执行方式：**
```bash
chmod +x init.sh
./init.sh
```

**用途：** 如果新环境没有main agent，且用户选择"新建main agent实例"时使用

---

## 三、身份层文件说明

| 文件 | 作用 | 是否必须 |
|------|------|----------|
| SOUL.md | 人格定义、说话风格、行为规矩 | ✅ 必须 |
| MEMORY.md | 长期记忆、重要事件、决策记录 | ✅ 必须 |
| TOOLS.md | 工具配置、Agent ID、飞书ID | ✅ 必须 |
| AGENTS.md | 团队成员表、职责分工 | ✅ 必须 |
| IDENTITY.md | 身份标识（名字、头像） | ✅ 必须 |
| USER.md | 用户信息（姓名、偏好） | ✅ 必须 |
| memory/ | 历史记录目录 | ⭕ 可选 |

---

## 四、团队成员层说明

**目录命名规范：** `中文名/`（不带旧版ID后缀）

| 成员 | 目录名 | Agent ID | 职责 |
|------|--------|----------|------|
| 成员A | 成员A/ | member-a | 团队成员 |
| 成员B | 成员B/ | member-b | 团队成员 |
| 成员C | 成员C/ | member-c | 团队成员 |
| 成员D | 成员D/ | member-d | 团队成员 |
| 成员I | 成员I/ | member-i | 团队成员 |
| 成员E | 成员E/ | member-e | 团队成员 |
| 成员F | 成员F/ | member-f | 团队成员 |
| 成员G | 成员G/ | member-g | 团队成员 |
| 成员H | 成员H/ | member-h | 团队成员 |

**每个成员目录包含：**
- SOUL.md（人格定义）
- MEMORY.md（长期记忆）
- TOOLS.md（可选）

---

## 五、配置文件说明

### openclaw-agents.json

**位置：** 搬家包根目录

**作用：** Agent配置片段，用于`config.patch`更新

**关键配置：**
```json
{
  "agents": {
    "defaults": {
      "maxConcurrent": 10,
      "subagents": {
        "allowAgents": ["*"]
      }
    },
    "list": [
      {"id": "main", "name": "QClaw"},
      {"id": "member-a", "name": "成员A", "workspace": "...", "agentDir": "..."}
    ]
  },
  "hooks": {
    "allowedAgentIds": ["member-a", "member-b", ...]
  }
}
```

### cron_tasks.json

**位置：** 搬家包根目录

**作用：** Cron任务模板

**任务清单：**
| 任务名 | 频率 | 用途 |
|--------|------|------|
| team-daily-{name} | 每天01:00 | 激活成员 |
| main 每日自迭代 | 每天22:30 | 自我优化 |
| 短流创进度催促 | 每30分钟 | 检查未定稿作品 |

---

## 六、一键搬家完整流程

### 用户视角

```
用户（旧环境）：我要搬家
Agent：好的，开始打包...
       [执行 pack.py]
       打包完成：~/一键搬家包/xxx.zip

用户（新环境）：[把包丢给agent] 这是一键搬家包，帮我搬家
Agent：好的，开始执行...
       [解压 + 执行 migrate.py]
       
       请选择如何处理 Main Agent：
         1. 指向现有agent
         2. 新建main agent实例
         3. 覆盖现有main agent
       
       用户选择：2
       
       复制身份文件 ✓
       复制团队成员 ✓
       复制skills ✓
       更新配置 ✓
       创建cron ✓
       重启Gateway ✓
       ✅ 搬家完成！
```

### 手动执行

```bash
# Step 1: 打包（旧环境）
python3 skills/team-resurrection/pack.py

# Step 2: 复制搬家包到新环境

# Step 3: 执行搬家（新环境）
unzip Agent搬家包.zip
python3 Agent搬家包/migrate.py
```

---

## 七、通用化设计

### 核心原则

**自动适配，无需手动配置：**
- pack.py 会自动检测是否有团队成员
- migrate.py 会自动检测搬家包里是否有团队成员
- README.md 会根据实际情况动态生成

### 自动检测逻辑

#### pack.py（打包脚本）

```python
# Step 2: 收集团队成员信息（可选）
agents = get_agent_info()

# 检测是否有团队成员
has_team = False
if agents:
    for agent in agents:
        if agent.get('id') != 'main':
            has_team = True
            break

if not has_team:
    info("未检测到团队成员，跳过此步骤")
else:
    # 收集团队成员信息...
```

**结果：**
- 有团队成员 → 生成"团队成员层"目录
- 没有团队成员 → 不生成"团队成员层"目录

#### migrate.py（执行脚本）

```python
def copy_team_members(package_dir):
    info("检查是否有团队成员...")
    
    team_dir = package_dir / "团队成员层"
    if not team_dir.exists():
        info("未检测到团队成员，跳过此步骤")
        return True
    
    # 复制团队成员...
```

**结果：**
- 有"团队成员层"目录 → 复制团队成员
- 没有"团队成员层"目录 → 跳过，输出友好提示

### 适用场景

| 场景 | pack.py行为 | migrate.py行为 |
|------|-------------|----------------|
| 有团队的agent | 打包团队成员 | 复制团队成员 |
| 没有团队的agent | 跳过，不打包 | 跳过，不复制 |

---

## 八、验证清单

搬家完成后，逐一检查：

### 基础检查

- [ ] `openclaw gateway status` 显示 running
- [ ] `cat ~/.qclaw/workspace-xxx/SOUL.md | head -20` 显示正确人格
- [ ] `ls ~/.qclaw/workspace-xxx/*/SOUL.md` 显示团队成员文件

### 配置检查

- [ ] openclaw.json 包含所有 agent 配置
- [ ] `agents.defaults.subagents.allowAgents: ["*"]` 已设置
- [ ] 每个 agent 的 workspace 目录存在
- [ ] 每个 workspace 包含 SOUL.md

### 功能检查

- [ ] Gateway 已重启并等待5-10秒
- [ ] 测试 spawn 至少 2 个 agent，确认人格正确
- [ ] Cron 任务已创建（如需要）
- [ ] Skills 已安装（如需要）

---

## 附录：版本变更记录（v2.0）

### v2.0（2026-04-22）

**P0 修复：**
- ✅ pack.py 不再硬编码 workspace-xxx，改为 `detect_active_workspace()` 自动检测
- ✅ migrate.py 新增 `backup_existing()`，搬家前自动备份到 `~/.qclaw/backup/搬家备份_时间戳/`
- ✅ migrate.py 新增 `deep_merge()`，config merge 而非 replace，保护新环境其他配置
- ✅ `prompt_main_agent_handling()` 三个选项逻辑补全（v1.0 选项1/2 只打印警告不执行）

**P1 修复：**
- ✅ 删除了 init.sh/setup_config.py 的"可选执行"误导文档
- ✅ migrate.py 新增 `find_package_dir()`，支持解压后直接 `cd` 进目录运行
- ✅ cron 创建区分"已存在/创建/失败"三类状态

**相关文件：**
- `pack.py` → v2.0，12751 bytes
- `migrate.py` → v2.0，19240 bytes
- `SKILL.md` → v2.0，9768 bytes



path: README.md
content: # Quick Resurrection

打包并恢复你的 OpenClaw Agent 工作区配置。
换电脑或重装系统后，一个命令把整个团队迁移到新环境。
执行前展示配置内容供用户确认，确认后全自动执行。

---

## ⚠️ 重要提示

**打包内容含敏感信息。** 搬家包会包含 MEMORY.md、TOOLS.md 等文件，可能含有 API keys、账号密码、内部流程等敏感内容。请妥善保管搬家包，不要随意分享。

---

## 特性

- **零门槛**：一条命令完成打包/迁移
- **安全优先**：自动备份，config 合并不覆盖
- **通用适配**：支持 Main Agent + 任意数量子代理
- **跨平台**：Mac / Windows / Linux 均可用

## 快速开始

### 打包（旧环境）

```bash
cd ~/.qclaw/workspace/skills/quick-resurrection
python3 pack.py
```

### 迁移（新环境）

```bash
python3 migrate.py
```

按提示完成，全程引导式。

## 核心功能

| 功能 | 说明 |
|------|------|
| 自动检测 workspace | 读 openclaw.json，自动推断 active workspace |
| 自动备份 | 搬家前备份到 `~/.qclaw/backup/` |
| 配置合并 | deep merge，保护新环境原有配置 |
| 通用打包 | 有团队打包团队，无团队跳过 |
| 跨平台 | Mac / Windows / Linux |

---

## 权限说明

本 skill 需要以下权限：

| 操作 | 文件 | 说明 |
|------|------|------|
| 读取/修改 | `~/.qclaw/openclaw.json` | 添加/合并 agent 配置 |
| 读写文件系统 | `~/.qclaw/` | 复制 workspace、备份配置 |
| 创建 | cron 定时任务 | 定时任务迁移 |
| 执行 | `openclaw gateway restart` | 重启 Gateway 使配置生效 |

**安全措施：**
- 所有操作前自动备份到 `~/.qclaw/backup/`
- 执行前**展示所有变更内容**（含 cron payload），用户确认后全自动执行
- allowAgents 使用**最小白名单**（仅实际成员ID），不使用通配符 `["*"]`

---

## 版本

- **v3.0**（2026-04-24）— 新增--dry-run/--no-cron/--no-restart，merge_config写入前展示diff，agents.list追加不替换，Zip Slip防御，审查展示cron payload

path: cron_tasks.json
content: {
  "description": "Cron 任务清单示例（通用模板）",
  "note": "这是模板文件，实际 cron 由 migrate.py 根据此模板在新环境创建",
  "tasks": [
    {
      "name": "main 每日自迭代",
      "schedule": "cron 0 22 * * * Asia/Shanghai",
      "sessionTarget": "isolated",
      "payload": {
        "kind": "agentTurn",
        "message": "执行每日自迭代：读 memory/、读团队 SOUL/SKILL、更新 MEMORY.md、commit push。"
      }
    }
  ]
}

path: examples/README.md
content: # 使用场景示例

## 场景一：换电脑后恢复团队

### Step 1：旧电脑上打包

用户在旧电脑上对 Agent 说：

> 我要搬家

Agent 执行 `pack.py`，输出：

```
检测到团队成员：
  - xiaoce（小策）
  - laomo（老墨）
  - dabai（大白）
  - ...

打包完成：~/Agent搬家包_20260422.zip
请将压缩包复制到新环境
```

### Step 2：新电脑上迁移

用户把压缩包放到新电脑，对 Agent 说：

> 这是搬家包，帮我搬家

Agent 执行 `migrate.py`，按提示引导用户：
- 解压文件
- 选择 Main Agent 处理方式
- 选择子代理处理方式
- 配置合并（不覆盖已有）

### Step 3：验证

迁移完成后，验证：
- Agent 能正常唤醒
- cron 任务已恢复
- 成员配置正确


path: migrate.py
content: #!/usr/bin/env python3
"""
一键搬家执行脚本 v2.2（审查确认后全自动执行）
功能：检测环境 → 备份 → 合并配置 → 复制文件 → 完成搬家

v3.0 变更：
- 新增 --dry-run / --no-cron / --no-restart 细粒度控制
- merge_config 写入前展示 openclaw.json 的 diff（before → after）
- agents.list 按 agent.id 追加/更新，不整体替换
- 审查步骤移到修改配置之前
- copy_team_members 使用配置中的实际 workspace 路径
- Zip Slip 路径穿越防御
"""

import os
import sys
import json
import shutil
import subprocess
import time
import zipfile
import argparse
import difflib
from datetime import datetime
from pathlib import Path

# =============================================
# 颜色输出
# =============================================
GREEN  = '\033[0;32m'
RED    = '\033[0;31m'
YELLOW = '\033[1;33m'
BLUE   = '\033[0;34m'
NC     = '\033[0m'

def log(msg):   print(f"{GREEN}[✓]{NC} {msg}")
def err(msg):   print(f"{RED}[✗]{NC} {msg}")
def warn(msg):  print(f"{YELLOW}[!]{NC} {msg}")
def info(msg):  print(f"{BLUE}[i]{NC} {msg}")

def run_cmd(cmd):
    """执行命令，返回 (stdout, returncode)。仅用于只读操作。"""
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout.strip(), result.returncode

# =============================================
# 备份模块
# =============================================

BACKUP_DIR = Path.home() / ".qclaw" / "backup"

def backup_existing():
    """
    搬家前备份现有配置到 ~/.qclaw/backup/
    每次搬家创建带时间戳的子目录
    """
    info("检查是否需要备份...")
    
    targets = []
    qclaw_base = Path.home() / ".qclaw"
    
    if not qclaw_base.exists():
        info("  ~/.qclaw/ 不存在，无需备份")
        return
    
    # 检查 workspace
    for d in qclaw_base.iterdir():
        if d.is_dir() and d.name.startswith("workspace-"):
            targets.append(d)
    
    # 检查 openclaw.json
    config = Path.home() / ".qclaw" / "openclaw.json"
    if config.exists():
        targets.append(config)
    
    if not targets:
        info("  未检测到现有配置，无需备份")
        return
    
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_sub = BACKUP_DIR / f"搬家备份_{timestamp}"
    backup_sub.mkdir(parents=True, exist_ok=True)
    
    info(f"  备份现有配置到：{backup_sub}")
    count = 0
    for t in targets:
        try:
            dst = backup_sub / t.name
            shutil.copytree(t, dst, dirs_exist_ok=True)
            count += 1
        except Exception as e:
            warn(f"  备份失败 {t.name}：{e}")
    
    log(f"  已备份 {count} 项")

# =============================================
# 配置合并模块
# =============================================

def load_json(path):
    with open(path, 'r', encoding='utf-8') as f:
        return json.load(f)

def deep_merge(base, patch):
    """
    深度合并两个 dict。
    - patch 中有而 base 中无的键：直接添加
    - 两者都有且都是 dict：递归合并
    - 两者都有但类型不同：patch 覆盖 base
    """
    result = base.copy()
    for k, v in patch.items():
        if k in result and isinstance(result[k], dict) and isinstance(v, dict):
            result[k] = deep_merge(result[k], v)
        else:
            result[k] = v
    return result

def merge_config(package_dir):
    """
    将搬家包中的 agents 配置与现有 openclaw.json 深度合并。
    只合并 agents 和 hooks 相关字段，保留其他所有配置。
    """
    info("合并 agent 配置（保护新环境原有配置）...")
    
    agents_file = package_dir / "openclaw-agents.json"
    if not agents_file.exists():
        warn("  找不到 openclaw-agents.json，跳过配置更新")
        return True
    
    agents_patch = load_json(agents_file)
    
    config_path = Path.home() / ".qclaw" / "openclaw.json"
    
    if config_path.exists():
        existing = load_json(config_path)
        info(f"  现有配置 keys：{list(existing.keys())}")
    else:
        existing = {}
        warn("  openclaw.json 不存在，将从零创建")
    
    # 深度合并 agents 和 hooks
    merged = existing.copy()
    
    if 'agents' in agents_patch:
        if 'agents' not in merged:
            merged['agents'] = {}
        
        # agents.defaults: deep merge（保留现有模型等配置）
        if 'defaults' in agents_patch['agents']:
            if 'defaults' not in merged['agents']:
                merged['agents']['defaults'] = {}
            merged['agents']['defaults'] = deep_merge(
                merged['agents']['defaults'], agents_patch['agents']['defaults'])
        
        # agents.list: 按 agent.id 追加/更新，不整体替换（保护新环境其他 agent）
        if 'list' in agents_patch['agents']:
            if 'list' not in merged['agents']:
                merged['agents']['list'] = []
            existing_ids = {a.get('id') for a in merged['agents']['list']}
            for agent in agents_patch['agents']['list']:
                aid = agent.get('id')
                if aid in existing_ids:
                    # 更新现有 agent（替换整个条目）
                    merged['agents']['list'] = [
                        agent if a.get('id') == aid else a
                        for a in merged['agents']['list']
                    ]
                else:
                    # 追加新 agent
                    merged['agents']['list'].append(agent)
            added = len(agents_patch['agents']['list'])
            log(f"  agents.list: 追加/更新 {added} 个 agent（保留现有 {len(existing_ids)} 个）")
        
        # allowAgents: 如果搬家包含 ["*"]，转换为最小白名单（仅实际成员ID）
        defaults = merged.get('agents', {}).get('defaults', {})
        subagents = defaults.get('subagents', {})
        if subagents.get('allowAgents') == ["*"]:
            all_agent_ids = [a.get('id') for a in merged.get('agents', {}).get('list', [])
                             if a.get('id') and a.get('id') != 'main']
            subagents['allowAgents'] = all_agent_ids
            log(f'  allowAgents 已转为最小白名单（{len(all_agent_ids)} 个成员ID）')
        
        log("  agents 配置已合并")
    
    if 'hooks' in agents_patch:
        if 'hooks' not in merged:
            merged['hooks'] = {}
        # hooks.allowedAgentIds 需要合并（追加而非覆盖）
        existing_ids = set(merged['hooks'].get('allowedAgentIds', []))
        patch_ids = set(agents_patch['hooks'].get('allowedAgentIds', []))
        merged['hooks']['allowedAgentIds'] = list(existing_ids | patch_ids)
        log("  hooks 配置已合并")
    
    # 展示 diff（before → after）
    if config_path.exists():
        existing_text = json.dumps(existing, indent=2, ensure_ascii=False).splitlines(keepends=True)
        merged_text = json.dumps(merged, indent=2, ensure_ascii=False).splitlines(keepends=True)
        diff = list(difflib.unified_diff(existing_text, merged_text,
                                          fromfile="现有 openclaw.json", tofile="合并后",
                                          n=3))
        if diff:
            info("  openclaw.json 变更 diff：")
            for line in diff[:60]:
                line = line.rstrip("\n")
                if line.startswith("+") and not line.startswith("+++"):
                    print(f"    {GREEN}{line}{NC}")
                elif line.startswith("-") and not line.startswith("---"):
                    print(f"    {RED}{line}{NC}")
                elif line.startswith("@@"):
                    print(f"    {YELLOW}{line}{NC}")
                else:
                    print(f"    {line}")
            if len(diff) > 60:
                info(f"    ...（共 {len(diff)} 行 diff，已截断）")
        else:
            info("  openclaw.json 无变更")
    else:
        info("  将创建新的 openclaw.json")

    # 写回
    with open(config_path, 'w', encoding='utf-8') as f:
        json.dump(merged, f, indent=2, ensure_ascii=False)
    
    log(f"  openclaw.json 已更新（合并后）")
    return True

# =============================================
# 主流程步骤
# =============================================

def find_package_dir():
    """
    找到搬家包目录。
    优先顺序：
    1. 命令行参数传入（zip 路径或目录路径）
    2. 当前目录本身就是包目录（解压后直接 cd 进去）
    3. 当前目录下有 搬家包_* 目录
    4. 当前目录下有 *.zip
    """
    # 检查命令行参数
    if len(sys.argv) > 1:
        arg = Path(sys.argv[1])
        if arg.is_file() and arg.suffix == '.zip':
            extract_dir = arg.with_suffix("")
            if not extract_dir.exists():
                info(f"  解压：{arg.name}")
                with zipfile.ZipFile(arg, 'r') as zip_ref:
                    # 防御 Zip Slip：检查解压路径不超出目标目录
                    for member in zip_ref.namelist():
                        member_path = (arg.parent / member).resolve()
                        if not str(member_path).startswith(str(arg.parent.resolve())):
                            err(f"  安全警告：zip 包含路径穿越文件 {member}，中止解压")
                            return None
                    zip_ref.extractall(arg.parent)
            if (extract_dir / "身份层").exists():
                return extract_dir
        elif arg.is_dir() and (arg / "身份层").exists():
            return arg

    # 如果目录里有人身份层，说明当前就在包目录里
    if Path("身份层").exists():
        return Path.cwd()
    
    # 找搬家包_* 子目录
    for d in Path.cwd().iterdir():
        if d.is_dir() and d.name.startswith("搬家包_") and (d / "身份层").exists():
            return d
    
    # 找 zip 文件并解压
    zips = list(Path.cwd().glob("*.zip")) + list(Path.cwd().glob("搬家*.zip"))
    for zf in zips:
        info(f"  发现搬家包：{zf.name}")
        extract_dir = zf.with_suffix("")  # 去掉 .zip
        if extract_dir.exists():
            warn(f"  解压目录已存在，跳过解压")
        else:
            info(f"  解压到：{extract_dir}")
            with zipfile.ZipFile(zf, 'r') as zip_ref:
                # 防御 Zip Slip
                for member in zip_ref.namelist():
                    member_path = (Path.cwd() / member).resolve()
                    if not str(member_path).startswith(str(Path.cwd().resolve())):
                        err(f"  安全警告：zip 包含路径穿越文件 {member}，跳过此包")
                        continue
                zip_ref.extractall(Path.cwd())
        if (extract_dir / "身份层").exists():
            return extract_dir
    
    return None

def copy_identity_files(package_dir):
    """复制身份文件（backup 已在前面做过）"""
    info("复制身份文件...")
    
    identity_dir = package_dir / "身份层"
    if not identity_dir.exists():
        err("  找不到身份层目录")
        return False
    
    workspace = Path.home() / ".qclaw"
    workspace.mkdir(parents=True, exist_ok=True)
    
    # 找到 main agent workspace（从 agents 配置中读）
    config_path = workspace / "openclaw.json"
    main_ws = None
    
    if config_path.exists():
        config = load_json(config_path)
        for agent in config.get('agents', {}).get('list', []):
            if agent.get('id') == 'main':
                main_ws = agent.get('workspace', '')
                break
    
    # fallback：找 workspace-agent-* 目录
    if not main_ws:
        candidates = [d for d in workspace.iterdir()
                      if d.is_dir() and d.name.startswith("workspace-")]
        if candidates:
            # 直接取第一个候选 workspace（由 openclaw.json 的 main agent 配置决定）
            main_ws = str(candidates[0])
    
    if not main_ws:
        # 最后一个 fallback
        main_ws = str(workspace / "workspace-main")
    
    ws_path = Path(main_ws)
    ws_path.mkdir(parents=True, exist_ok=True)
    
    copied = 0
    for fname in ["SOUL.md", "MEMORY.md", "TOOLS.md", "AGENTS.md", "IDENTITY.md", "USER.md"]:
        src = identity_dir / fname
        if src.exists():
            shutil.copy2(src, ws_path / fname)
            copied += 1
    
    memory_src = identity_dir / "memory"
    if memory_src.exists():
        memory_dst = ws_path / "memory"
        memory_dst.mkdir(parents=True, exist_ok=True)
        for f in memory_src.glob("*.md"):
            shutil.copy2(f, memory_dst / f.name)
        copied += 1
    
    log(f"  已复制 {copied} 项到 {ws_path.name}/")
    return True

def copy_team_members(package_dir):
    """复制团队成员（自动检测）"""
    info("检查团队成员...")
    
    team_dir = package_dir / "团队成员层"
    if not team_dir.exists():
        info("  未检测到团队成员，跳过")
        return True
    
    workspace_base = Path.home() / ".qclaw"
    
    # 找到 main workspace 父目录
    main_ws = None
    config_path = workspace_base / "openclaw.json"
    if config_path.exists():
        config = load_json(config_path)
        for agent in config.get('agents', {}).get('list', []):
            if agent.get('id') == 'main':
                main_ws = agent.get('workspace', '')
                break
    
    if not main_ws:
        candidates = [d for d in workspace_base.iterdir()
                      if d.is_dir() and d.name.startswith("workspace-")]
        if candidates:
            main_ws = str(candidates[0])
    
    if not main_ws:
        info("  未找到 main workspace，跳过")
        return True
    
    main_ws_path = Path(main_ws)
    
    # 读 openclaw-agents.json 获取每个成员的实际 workspace 路径
    agents_file = package_dir / "openclaw-agents.json"
    member_workspaces = {}
    if agents_file.exists():
        try:
            agents_data = load_json(agents_file)
            for a in agents_data.get('agents', {}).get('list', []):
                if a.get('id') != 'main' and a.get('workspace'):
                    member_workspaces[a.get('name', a.get('id'))] = a['workspace']
        except:
            pass
    
    count = 0
    for member_dir in team_dir.iterdir():
        if member_dir.is_dir():
            # 优先使用 openclaw-agents.json 中配置的 workspace 路径
            member_name = member_dir.name
            if member_name in member_workspaces:
                dst = Path(member_workspaces[member_name])
            else:
                dst = main_ws_path / member_name
            dst.mkdir(parents=True, exist_ok=True)
            for fname in ["SOUL.md", "MEMORY.md", "TOOLS.md"]:
                src = member_dir / fname
                if src.exists():
                    shutil.copy2(src, dst / fname)
            count += 1
    
    log(f"  已复制 {count} 个团队成员")
    return True

def copy_skills(package_dir):
    """复制 skills"""
    info("复制 skills...")
    
    skills_src = package_dir / "skills"
    if not skills_src.exists():
        warn("  未检测到 skills")
        return True
    
    workspace_base = Path.home() / ".qclaw"
    
    # 找 main workspace
    main_ws = None
    config_path = workspace_base / "openclaw.json"
    if config_path.exists():
        config = load_json(config_path)
        for agent in config.get('agents', {}).get('list', []):
            if agent.get('id') == 'main':
                main_ws = agent.get('workspace', '')
                break
    
    if not main_ws:
        candidates = [d for d in workspace_base.iterdir()
                      if d.is_dir() and d.name.startswith("workspace-")]
        if candidates:
            main_ws = str(candidates[0])
    
    if not main_ws:
        info("  未找到 main workspace，跳过")
        return True
    
    ws_path = Path(main_ws)
    
    skills_dst = ws_path / "skills"
    skills_dst.mkdir(parents=True, exist_ok=True)
    
    count = 0
    for skill_dir in skills_src.iterdir():
        if skill_dir.is_dir():
            dst = skills_dst / skill_dir.name
            shutil.copytree(skill_dir, dst, dirs_exist_ok=True)
            count += 1
    
    log(f"  已复制 {count} 个 skills")
    return True

def create_cron_tasks(package_dir):
    """从 cron_jobs.json 重建定时任务"""
    info("重建 cron 任务...")
    
    cron_file = package_dir / "cron_jobs.json"
    if not cron_file.exists():
        info("  未检测到 cron 配置，跳过")
        return True
    
    jobs = load_json(cron_file)
    if not jobs:
        info("  cron_jobs.json 为空，跳过")
        return True
    
    # 读取当前已有任务名
    cron_dir = Path.home() / ".qclaw" / "cron" / "jobs.json"
    existing_names = set()
    if cron_dir.exists():
        try:
            existing_jobs = json.loads(cron_dir.read_text(encoding="utf-8")).get("jobs", [])
            existing_names = {j.get("name", "") for j in existing_jobs}
        except:
            pass
    
    created = skipped = failed = 0
    for job in jobs:
        job_name = job.get("name", "unnamed")
        if job_name in existing_names:
            skipped += 1
            info(f"  跳过已存在：{job_name}")
            continue
        
        # 构造 openclaw tasks add --job JSON（去掉运行时 state 字段）
        job_copy = {k: v for k, v in job.items()
                    if k not in ("id", "createdAtMs", "updatedAtMs", "state")}
        # 确保 delivery 有 sessionTarget
        if "sessionTarget" not in job_copy:
            job_copy["sessionTarget"] = "isolated"
        
        job_json = json.dumps(job_copy, ensure_ascii=False)
        result = subprocess.run(
            ["openclaw", "tasks", "add", "--job", job_json],
            capture_output=True, text=True
        )
        output = result.stdout.strip()
        code = result.returncode
        
        if code == 0:
            log(f"  创建：{job_name}")
            created += 1
        else:
            err(f"  创建失败：{job_name} - {output[:100]}")
            failed += 1
    
    info(f"  结果：{created}个创建，{skipped}个跳过，{failed}个失败")
    return True

def restart_gateway():
    """重启 Gateway（用户已在审查步骤确认）"""
    info("重启 Gateway...")
    result = subprocess.run(
        ["openclaw", "gateway", "restart"],
        capture_output=True, text=True
    )
    output = result.stdout.strip()
    code = result.returncode
    
    if code == 0:
        log("  Gateway 已重启")
        info("  等待5秒让 channel 注册...")
        time.sleep(5)
    else:
        warn("  Gateway 重启命令可能失败")
        warn("  请手动执行：openclaw gateway restart")
    
    return True

# =============================================
# 用户交互
# =============================================

def prompt_main_agent_handling():
    """
    让用户选择如何处理 main agent。
    这次做完整逻辑：选项1/2/3 都有实际执行内容。
    """
    workspace_base = Path.home() / ".qclaw"
    config_path = workspace_base / "openclaw.json"
    
    has_main = False
    if config_path.exists():
        config = load_json(config_path)
        agents = config.get('agents', {}).get('list', [])
        has_main = any(a.get('id') == 'main' for a in agents)
    
    print()
    print("请选择如何处理 Main Agent：")
    print()
    print("  1. 指向现有 agent（把当前 agent 变成你的主控）")
    print("  2. 新建 main agent 实例（另起一个，保留当前配置）")
    if has_main:
        print("  3. 覆盖现有 main agent（⚠️ 替换现有主控）")
    print()
    
    choices = ["1", "2"]
    if has_main:
        choices.append("3")
    
    while True:
        choice = input("请输入选项（" + "/".join(choices) + "）：").strip()
        if choice in choices:
            break
        err("无效选项，请重新输入")
    
    # ---- 选项 1：指向现有 agent ----
    if choice == "1":
        info("将当前 agent 配置为 main...")
        
        if not config_path.exists():
            err("  openclaw.json 不存在，无法配置")
            return False
        
        config = load_json(config_path)
        
        # 找当前 agent 的 workspace
        candidates = [d for d in workspace_base.iterdir()
                      if d.is_dir() and d.name.startswith("workspace-")]
        
        if not candidates:
            warn("  未能找到任何 workspace 目录")
            warn("  请手动在 openclaw.json 中设置 main agent 的 workspace")
            return False
        
        main_ws = str(candidates[0])
        
        if main_ws:
            log(f"  找到 workspace：{main_ws}")
            
            # 把这个 agent 设为 main（添加或更新 agents.list）
            agents_list = config.get('agents', {}).get('list', [])
            
            # 移除已有的 main
            agents_list = [a for a in agents_list if a.get('id') != 'main']
            
            # 添加 main
            agents_list.insert(0, {
                "id": "main",
                "workspace": main_ws,
                "name": "Main Agent"
            })
            
            config['agents']['list'] = agents_list
            
            with open(config_path, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=2, ensure_ascii=False)
            
            log("  已将当前 agent 设为 main")
        else:
            warn("  未能自动找到 agent workspace")
            warn("  请手动在 openclaw.json 中设置 main agent 的 workspace")
        
        return True
    
    # ---- 选项 2：新建 main agent 实例 ----
    elif choice == "2":
        info("创建新的 main agent 实例...")
        
        # 创建目录结构
        new_ws = workspace_base / "workspace-new-main"
        new_ws.mkdir(parents=True, exist_ok=True)
        (new_ws / "memory").mkdir(exist_ok=True)
        (new_ws / "skills").mkdir(exist_ok=True)
        
        # 创建 agentDir
        agent_dir = workspace_base / "agents" / "main" / "agent"
        agent_dir.mkdir(parents=True, exist_ok=True)
        
        # 创建 models.json
        models_config = {
            "providers": {
                "qclaw": {
                    "baseUrl": "http://127.0.0.1:19000/proxy/llm",
                    "apiKey": "__QCLAW_AUTH_GATEWAY_MANAGED__",
                    "api": "openai-completions",
                    "models": [
                        {"id": "modelroute", "name": "modelroute", "input": ["text", "image"]}
                    ]
                }
            }
        }
        with open(agent_dir / "models.json", 'w', encoding='utf-8') as f:
            json.dump(models_config, f, indent=2)
        
        # 更新 openclaw.json：添加 main agent
        if not config_path.exists():
            config = {}
        else:
            config = load_json(config_path)
        
        if 'agents' not in config:
            config['agents'] = {}
        if 'list' not in config['agents']:
            config['agents']['list'] = []
        
        # 移除已有的 main
        config['agents']['list'] = [a for a in config['agents']['list']
                                     if a.get('id') != 'main']
        # 添加新 main
        config['agents']['list'].insert(0, {
            "id": "main",
            "workspace": str(new_ws),
            "name": "Main Agent",
            "agentDir": str(agent_dir)
        })
        
        with open(config_path, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
        
        log(f"  已创建 main agent：{new_ws}")
        return True
    
    # ---- 选项 3：覆盖 ----
    elif choice == "3":
        print()
        confirm = input("⚠️ 确认覆盖现有 main agent？输入 'yes' 继续：").strip()
        if confirm.lower() != 'yes':
            err("取消操作")
            return False
        
        info("将覆盖现有 main agent...")
        # 更新 main agent 的 workspace 路径（从搬家包读取）
        agents_file = None
        # 找搬家包目录
        for d in [Path.cwd()] + list(Path.cwd().iterdir()):
            if d.is_dir() and (d / "openclaw-agents.json").exists():
                agents_file = d / "openclaw-agents.json"
                break
        
        if agents_file:
            try:
                agents_patch = load_json(agents_file)
                patch_main = next(
                    (a for a in agents_patch.get('agents', {}).get('list', [])
                     if a.get('id') == 'main'), None)
                if patch_main and patch_main.get('workspace'):
                    new_ws = patch_main['workspace']
                    # 更新 openclaw.json 中 main agent 的 workspace
                    config = load_json(config_path)
                    for a in config.get('agents', {}).get('list', []):
                        if a.get('id') == 'main':
                            a['workspace'] = new_ws
                            break
                    with open(config_path, 'w', encoding='utf-8') as f:
                        json.dump(config, f, indent=2, ensure_ascii=False)
                    log(f"  main agent workspace 已更新为：{new_ws}")
            except Exception as e:
                warn(f"  无法更新 main workspace：{e}")
        
        return True
    
    return True

# =============================================
# 主流程
# =============================================

def review_and_confirm(package_dir, args=None):
    """
    搬家前审查步骤——在执行任何敏感操作之前，
    展示即将写入的配置内容，由用户确认后继续。
    
    这是安全关键步骤，不允许跳过。
    """
    print()
    print("🔍 Step 0.8: 搬家包内容审查")
    print("─" * 40)
    print()
    print("以下内容将从搬家包写入你的系统，请仔细核对：")
    print()
    
    # ---- 1. agents 配置 ----
    agents_file = package_dir / "openclaw-agents.json"
    if agents_file.exists():
        print("【1/3】Agent 配置（将写入 ~/.qclaw/openclaw.json）：")
        agents_patch = load_json(agents_file)
        
        agents_list = agents_patch.get('agents', {}).get('list', [])
        if agents_list:
            print(f"  将追加/更新 {len(agents_list)} 个 agent（不删除现有 agent）：")
            for a in agents_list:
                aid = a.get('id', '?')
                name = a.get('name', '?')
                ws = a.get('workspace', '?')
                print(f"    · {aid}（{name}）→ {ws}")
        
        defaults = agents_patch.get('agents', {}).get('defaults', {})
        allow = defaults.get('subagents', {}).get('allowAgents', [])
        if allow == ["*"]:
            print()
            print('  ⚠️ 注意：搬家包设置了 allowAgents 为 ["*"]')
            print("    安装时会改为最小白名单（仅实际成员ID）。")
            print("    如确实需要通配符，请安装后手动修改。")
        
        hooks_ids = agents_patch.get('hooks', {}).get('allowedAgentIds', [])
        if hooks_ids:
            print()
            print(f"  hooks.allowedAgentIds 将追加：{hooks_ids}")
    else:
        print("【1/3】Agent 配置：无")
    
    print()
    
    # ---- 2. cron 任务 ----
    cron_file = package_dir / "cron_jobs.json"
    if cron_file.exists():
        cron_tasks = load_json(cron_file)
        print(f"【2/3】Cron 定时任务（将创建 {len(cron_tasks)} 个）：")
        for t in cron_tasks:
            tname = t.get('name', 'unnamed')
            schedule = t.get('schedule', '?')
            payload_preview = str(t.get('payload', ''))[:80]
            print(f"    · {tname} — {schedule}")
            if payload_preview:
                print(f"      payload: {payload_preview}...")
    else:
        print("【2/3】Cron 任务：无")
    
    print()
    
    # ---- 3. 文件清单 ----
    print("【3/3】将从搬家包复制的文件：")
    
    identity_dir = package_dir / "身份层"
    if identity_dir.exists():
        identity_files = list(identity_dir.glob("*.md"))
        print(f"  身份文件（→ main workspace）：{len(identity_files)} 个")
        for f in identity_files:
            print(f"    · {f.name}")
        if (identity_dir / "MEMORY.md").exists():
            print("    ⚠️ 含 MEMORY.md（可能含私人信息）")
        if (identity_dir / "TOOLS.md").exists():
            print("    ⚠️ 含 TOOLS.md（可能含 API keys）")
    
    team_dir = package_dir / "团队成员层"
    if team_dir.exists():
        members = [d for d in team_dir.iterdir() if d.is_dir()]
        print(f"  团队成员：{len(members)} 个")
        for m_item in members:
            print(f"    · {m_item.name}/")
    
    skills_src = package_dir / "skills"
    if skills_src.exists():
        skills = [d for d in skills_src.iterdir() if d.is_dir()]
        print(f"  Skills：{len(skills)} 个")
        for s_item in skills:
            print(f"    · {s_item.name}/")
    
    print()
    print("─" * 40)
    print()
    
    if args and args.dry_run:
        print("🔍 DRY RUN 模式：以下操作只展示不执行")
    else:
        print("⚠️ 确认后将执行：复制文件 → 合并配置（写入前展示diff）" + (" → 创建 cron" if not (args and args.no_cron) else "（跳过cron）") + (" → 重启 Gateway" if not (args and args.no_restart) else "（跳过重启）"))
    
    while True:
        print("是否确认执行以上操作？")
        print()
        print("  y  — 确认，继续搬家")
        print("  n  — 取消，中止操作")
        print()
        choice = input("请输入（y/n）：").strip().lower()
        if choice == 'y':
            print()
            log("用户已确认，继续执行...")
            return True
        elif choice == 'n':
            print()
            err("用户取消，搬家中止")
            return False
        else:
            err("无效选项，请输入 y 或 n")


def parse_args():
    parser = argparse.ArgumentParser(description="一键搬家执行脚本 v3.0")
    parser.add_argument("package", nargs="?", help="搬家包 zip 或目录路径")
    parser.add_argument("--dry-run", action="store_true", help="只展示将要执行的操作和diff，不实际执行")
    parser.add_argument("--no-cron", action="store_true", help="跳过 cron 任务创建")
    parser.add_argument("--no-restart", action="store_true", help="跳过 Gateway 重启")
    return parser.parse_args()

def main():
    args = parse_args()

    print()
    print("=" * 60)
    print("  一键搬家执行脚本 v3.0（审查确认 + diff 展示 + 细粒度控制）")
    if args.dry_run:
        print("  🔍 DRY RUN 模式——只展示不执行")
    print("=" * 60)
    print()

    # ---- 找到搬家包 ----
    info("定位搬家包...")
    package_dir = find_package_dir()

    if not package_dir:
        err("找不到搬家包！")
        print()
        print("用法：python3 migrate.py <搬家包路径> [--dry-run] [--no-cron] [--no-restart]")
        return

    log(f"  搬家包目录：{package_dir.name}/")
    print()

    # ---- 搬家包审查（必须在修改配置之前） ----
    if not review_and_confirm(package_dir, args):
        return

    # ---- dry-run: 展示 merge diff 但不写入 ----
    if args.dry_run:
        print()
        info("DRY RUN: 展示配置合并 diff（不写入文件）...")
        agents_file = package_dir / "openclaw-agents.json"
        if agents_file.exists():
            agents_patch = load_json(agents_file)
            config_path = Path.home() / ".qclaw" / "openclaw.json"
            existing = load_json(config_path) if config_path.exists() else {}
            merged = existing.copy()
            if 'agents' in agents_patch:
                if 'agents' not in merged:
                    merged['agents'] = {}
                if 'defaults' in agents_patch['agents']:
                    if 'defaults' not in merged['agents']:
                        merged['agents']['defaults'] = {}
                    merged['agents']['defaults'] = deep_merge(
                        merged['agents']['defaults'], agents_patch['agents']['defaults'])
                if 'list' in agents_patch['agents']:
                    if 'list' not in merged['agents']:
                        merged['agents']['list'] = []
                    existing_ids = {a.get('id') for a in merged['agents']['list']}
                    for agent in agents_patch['agents']['list']:
                        aid = agent.get('id')
                        if aid in existing_ids:
                            merged['agents']['list'] = [
                                agent if a.get('id') == aid else a
                                for a in merged['agents']['list']
                            ]
                        else:
                            merged['agents']['list'].append(agent)
            if 'hooks' in agents_patch:
                if 'hooks' not in merged:
                    merged['hooks'] = {}
                existing_ids = set(merged['hooks'].get('allowedAgentIds', []))
                patch_ids = set(agents_patch['hooks'].get('allowedAgentIds', []))
                merged['hooks']['allowedAgentIds'] = list(existing_ids | patch_ids)

            existing_text = json.dumps(existing, indent=2, ensure_ascii=False).splitlines(keepends=True)
            merged_text = json.dumps(merged, indent=2, ensure_ascii=False).splitlines(keepends=True)
            diff = list(difflib.unified_diff(existing_text, merged_text,
                                              fromfile="现有 openclaw.json", tofile="合并后", n=3))
            if diff:
                info("  openclaw.json 变更 diff：")
                for line in diff[:80]:
                    line = line.rstrip("\n")
                    if line.startswith("+") and not line.startswith("+++"):
                        print(f"    {GREEN}{line}{NC}")
                    elif line.startswith("-") and not line.startswith("---"):
                        print(f"    {RED}{line}{NC}")
                    elif line.startswith("@@"):
                        print(f"    {YELLOW}{line}{NC}")
                    else:
                        print(f"    {line}")
            else:
                info("  openclaw.json 无变更")

        print()
        print("=" * 60)
        print("  🔍 DRY RUN 完成——以上操作未实际执行")
        print("  去掉 --dry-run 参数后重新运行以执行搬家")
        print("=" * 60)
        return

    # ---- 备份 ----
    print("🔒 Step 0: 备份现有配置...")
    backup_existing()
    print()

    # ---- 用户选择 main agent 处理方式 ----
    print("🔧 Step 0.5: Main Agent 配置...")
    if not prompt_main_agent_handling():
        err("Main Agent 配置失败，中止搬家")
        return
    print()

    # ---- 主流程 ----
    steps = [
        ("Step 1: 复制身份文件",     copy_identity_files),
        ("Step 2: 复制团队成员",     copy_team_members),
        ("Step 3: 复制 skills",       copy_skills),
        ("Step 4: 合并 agent 配置（写入前展示diff）",  merge_config),
    ]

    if not args.no_cron:
        steps.append(("Step 5: 创建 cron 任务", create_cron_tasks))
    else:
        info("跳过 cron 任务创建（--no-cron）")

    if not args.no_restart:
        steps.append(("Step 6: 重启 Gateway", restart_gateway))
    else:
        info("跳过 Gateway 重启（--no-restart），请手动执行：openclaw gateway restart")

    for label, fn in steps:
        print(f"{label}...")
        result = fn(package_dir)
        if not result:
            err(f"{label} 失败")
        print()

    # ---- 完成 ----
    print("=" * 60)
    print("  ✅ 一键搬家完成！")
    print("=" * 60)
    print()
    print("📋 验证步骤：")
    print()
    print("  1. 检查 SOUL.md：")
    print("     ls ~/.qclaw/workspace-*/SOUL.md")
    print()
    print("  2. 测试子代理激活：")
    print("     在 agent 对话中说：测试激活团队成员")
    print()
    if args.no_restart:
        print("  3. 手动重启 Gateway：")
        print("     openclaw gateway restart")
        print()
    print("  检查 cron 任务：")
    print("     openclaw tasks list")
    print()
    print("  如需回滚：")
    backup_list = sorted(BACKUP_DIR.iterdir(), key=lambda d: d.name)
    if backup_list:
        latest = backup_list[-1].name
        print(f"     cp -r {BACKUP_DIR / latest}/* ~/.qclaw/")
    print()
    print("=" * 60)
    print()


if __name__ == "__main__":
    main()


path: openclaw-agents.json
content: {
  "description": "Agent 配置示例（通用模板）",
  "note": "这是模板文件，实际配置由 migrate.py 在新环境 deep merge",
  "agents": {
    "list": [
      {
        "id": "member-a",
        "name": "成员A",
        "workspace": "/Users/xxx/.qclaw/workspace-xxx/成员A",
        "agentDir": "/Users/xxx/.qclaw/agents/member-a/agent"
      },
      {
        "id": "member-b",
        "name": "成员B",
        "workspace": "/Users/xxx/.qclaw/workspace-xxx/成员B",
        "agentDir": "/Users/xxx/.qclaw/agents/member-b/agent"
      },
      {
        "id": "member-c",
        "name": "成员C",
        "workspace": "/Users/xxx/.qclaw/workspace-xxx/成员C",
        "agentDir": "/Users/xxx/.qclaw/agents/member-c/agent"
      }
    ],
    "defaults": {
      "subagents": {
        "allowAgents": [
          "*"
        ]
      }
    }
  }
}

path: pack.py
content: #!/usr/bin/env python3
"""
一键搬家打包脚本 v2.0(通用版)
功能:自动收集所有资料,生成搬家包

修复:
- v2.0: 改为自动检测当前 active workspace，不再硬编码具体路径
"""

import os
import json
import shutil
import subprocess
from datetime import datetime
from pathlib import Path

# =============================================
# 颜色输出
# =============================================
GREEN = '\033[0;32m'
RED = '\033[0;31m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'

def log(msg): print(f"{GREEN}[✓]{NC} {msg}")
def err(msg): print(f"{RED}[✗]{NC} {msg}")
def warn(msg): print(f"{YELLOW}[!]{NC} {msg}")
def info(msg): print(f"{BLUE}[i]{NC} {msg}")

def run_cmd(cmd):
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
    return result.stdout.strip(), result.returncode

# =============================================
# 核心:自动检测当前 active workspace
# =============================================

def detect_active_workspace():
    """
    自动检测当前 agent 的 active workspace。

    策略:
    1. 读取 ~/.qclaw/openclaw.json 的 agents.list
    2. 找到 main agent 的 workspace 路径
    3. 验证该目录包含 SOUL.md

    返回:Path 对象 或 None
    """
    config_path = Path.home() / ".qclaw" / "openclaw.json"

    if not config_path.exists():
        warn("找不到 openclaw.json,尝试从当前工作目录推断")
        # fallback:从 cwd 推断
        cwd = Path.cwd()
        if (cwd / "SOUL.md").exists():
            return cwd
        return None

    with open(config_path, 'r', encoding='utf-8') as f:
        config = json.load(f)

    agents = config.get('agents', {}).get('list', [])

    # 找 main agent
    for agent in agents:
        if agent.get('id') == 'main':
            workspace = agent.get('workspace', '')
            if workspace:
                wp = Path(workspace)
                if wp.exists() and (wp / "SOUL.md").exists():
                    return wp

    # fallback：从agents.list推断主agent workspace
    # 策略：收集所有workspace路径的父级链，找包含SOUL.md的最深公共父目录
    workspaces = [Path(a['workspace']) for a in agents
                  if a.get('workspace') and Path(a.get('workspace', '')).exists()]
    if workspaces:
        # 收集所有workspace的所有祖先目录（到home为止）
        candidate_dirs = set()
        home = Path.home()
        for ws in workspaces:
            for parent in ws.parents:
                if parent == home or not str(parent).startswith(str(home)):
                    break
                candidate_dirs.add(parent)
        # 在候选目录中找包含SOUL.md+MEMORY.md的，选最深的（最接近workspace）
        for candidate in sorted(candidate_dirs, key=lambda x: len(x.parts), reverse=True):
            if (candidate / 'SOUL.md').exists() and (candidate / 'MEMORY.md').exists():
                return candidate

    # fallback：遍历 workspace- 开头的目录，找包含 SOUL.md + MEMORY.md 的
    workspace_base = Path.home() / ".qclaw"
    candidates = []
    for d in workspace_base.iterdir():
        if not (d.is_dir() and d.name.startswith("workspace-")):
            continue
        has_identity = (d / "SOUL.md").exists() and (d / "MEMORY.md").exists()
        subagent_count = sum(1 for sub in d.iterdir() if sub.is_dir() and (sub / "SOUL.md").exists())
        candidates.append((d, has_identity, subagent_count))
    candidates.sort(key=lambda x: (x[1], x[2]), reverse=True)
    if candidates:
        return candidates[0][0]

    # fallback:找当前 cwd 的父链
    cwd = Path.cwd()
    for parent in [cwd] + list(cwd.parents):
        if (parent / "SOUL.md").exists() and (parent / "MEMORY.md").exists():
            return parent

    return None

def get_agent_name(workspace):
    """从 workspace 推断 agent 名称"""
    soul = workspace / "SOUL.md"
    identity = workspace / "IDENTITY.md"

    # 优先从 IDENTITY.md 的 Name 字段读取
    if identity.exists():
        with open(identity, 'r', encoding='utf-8') as f:
            for line in f.read().split('\n'):
                if line.startswith('- **Name:**'):
                    name = line.split('**Name:**')[1].strip()
                    if name:
                        return name

    # 从 SOUL.md 的第一个 # 标题读取
    if soul.exists():
        with open(soul, 'r', encoding='utf-8') as f:
            for line in f.read().split('\n'):
                if line.startswith('# '):
                    title = line[2:].strip()
                    # 去掉 SOUL.md 标记
                    title = title.replace('SOUL.md', '').replace('—', '').strip()
                    if title:
                        return title

    return workspace.name  # 默认用目录名

# =============================================
# 主流程
# =============================================

def get_agent_info():
    """获取 agent 配置信息"""
    config_path = Path.home() / ".qclaw" / "openclaw.json"
    if not config_path.exists():
        return []
    with open(config_path, 'r', encoding='utf-8') as f:
        config = json.load(f)
    return config.get('agents', {}).get('list', [])

def get_cron_tasks():
    """从 ~/.qclaw/cron/jobs.json 读取 cron 任务定义"""
    cron_file = Path.home() / ".qclaw" / "cron" / "jobs.json"
    if not cron_file.exists():
        return []
    try:
        data = json.loads(cron_file.read_text(encoding="utf-8"))
        jobs = data.get("jobs", [])
        # 只取 enabled=True 的任务
        return [j for j in jobs if j.get("enabled", True)]
    except Exception:
        return []

def main():
    print()
    print("=" * 60)
    print("  一键搬家打包脚本 v2.0")
    print(f"  时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("=" * 60)
    print()

    # ---- 自动检测 active workspace ----
    info("检测当前 active workspace...")
    main_workspace = detect_active_workspace()

    if not main_workspace:
        err("无法自动检测 active workspace")
        err("请手动设置 MAIN_WORKSPACE 环境变量,或确保 openclaw.json 中有 main agent 配置")
        print()
        print("手动指定示例:")
        print("  export MAIN_WORKSPACE=/Users/xxx/.qclaw/workspace-agent-xxx")
        print("  python3 pack.py")
        return

    agent_name = get_agent_name(main_workspace)
    log(f"检测到 workspace:{main_workspace}")
    log(f"Agent 名称:{agent_name}")
    print()

    # 创建输出目录
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    package_name = f"{agent_name}搬家包_{timestamp}"
    OUTPUT_DIR = Path.home() / "一键搬家包"
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    package_dir = OUTPUT_DIR / package_name
    package_dir.mkdir(parents=True, exist_ok=True)
    info(f"输出目录:{package_dir}")
    print()

    # ------------------------------------------
    # Step 1: 收集 Main Agent 信息
    # ------------------------------------------
    print("📦 Step 1: 收集 Main Agent 信息...")

    main_dir = package_dir / "身份层"
    main_dir.mkdir(exist_ok=True)

    core_files = ["SOUL.md", "MEMORY.md", "TOOLS.md", "AGENTS.md", "IDENTITY.md", "USER.md", "SKILLS.md"]
    for filename in core_files:
        src = main_workspace / filename
        if src.exists():
            shutil.copy2(src, main_dir / filename)
            log(f"  {filename}")

    memory_src = main_workspace / "memory"
    if memory_src.exists():
        memory_dst = main_dir / "memory"
        memory_dst.mkdir(exist_ok=True)
        for f in memory_src.glob("*.md"):
            shutil.copy2(f, memory_dst / f.name)
        log(f"  memory/")

    print()

    # ------------------------------------------
    # Step 2: 收集团队成员(自动检测)
    # ------------------------------------------
    print("👥 Step 2: 收集团队成员...")

    agents = get_agent_info()
    team_members = []

    # 自动检测是否有团队成员
    has_team = False
    if agents:
        for agent in agents:
            if agent.get('id') != 'main':
                has_team = True
                break

    if not has_team:
        info("  未检测到团队成员,跳过")
    else:
        for agent in agents:
            agent_id = agent.get('id', '')
            if agent_id == 'main':
                continue

            name = agent.get('name', agent_id)
            workspace = agent.get('workspace', '')

            if workspace and Path(workspace).exists():
                team_dir = package_dir / "团队成员层" / name
                team_dir.mkdir(parents=True, exist_ok=True)

                for filename in ["SOUL.md", "MEMORY.md", "TOOLS.md"]:
                    src = Path(workspace) / filename
                    if src.exists():
                        shutil.copy2(src, team_dir / filename)

                team_members.append({"id": agent_id, "name": name, "workspace": workspace})
                log(f"  {name} ({agent_id})")

    info(f"  共 {len(team_members)} 个团队成员")
    print()

    # ------------------------------------------
    # Step 3: 收集 Skills
    # ------------------------------------------
    print("🔧 Step 3: 收集 Skills...")

    skills_src = main_workspace / "skills"
    skill_count = 0
    if skills_src.exists():
        skills_dst = package_dir / "skills"
        skills_dst.mkdir(exist_ok=True)

        for skill_dir in skills_src.iterdir():
            if skill_dir.is_dir() and (skill_dir / "SKILL.md").exists():
                dst = skills_dst / skill_dir.name
                shutil.copytree(skill_dir, dst, dirs_exist_ok=True)
                log(f"  {skill_dir.name}/")
                skill_count += 1

    if skill_count == 0:
        warn("  未检测到 skills")
    print()

    # ------------------------------------------
    # Step 4: 收集 Cron 任务配置
    # ------------------------------------------
    print("⏰ Step 4: 收集 Cron 任务配置...")

    cron_tasks = get_cron_tasks()
    if cron_tasks:
        # 存完整 jobs 定义，migrate 时直接重建
        with open(package_dir / "cron_jobs.json", 'w', encoding='utf-8') as f:
            json.dump(cron_tasks, f, indent=2, ensure_ascii=False)

        log(f"  cron_jobs.json ({len(cron_tasks)} 个任务)")
    else:
        warn("  未能获取 cron 任务列表")
    print()

    # ------------------------------------------
    # Step 4.5: 检测工作目录
    # ------------------------------------------
    print("📁 Step 4.5: 检测工作目录...")

    # 扫描常见工作目录
    common_work_dirs = [
        Path.home() / "tales",
        Path.home() / "work",
        Path.home() / "projects",
        Path.home() / "workspace",
    ]

    found_work_dirs = []
    for d in common_work_dirs:
        if d.exists() and d.is_dir():
            # 检查是否 git 仓库
            if (d / ".git").exists():
                found_work_dirs.append(str(d))
                log(f"  {d} (git仓库)")

    # 生成工作目录提示文件
    work_dir_note = f"""# 工作目录

打包时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

## 检测到的 git 工作目录

{chr(10).join(f'- {d}' for d in found_work_dirs) if found_work_dirs else '(未检测到常见工作目录)'}

## 搬家后请手动处理

1. 将上述目录的 git remote 设置为新环境的地址
2. 如果是新 clone，确保 .git/config 中的 user.name 和 user.email 正确
3. 建议: 在新环境执行一次 `git status` 确认仓库正常

## 提示

tales/ 目录是团队的创作成果仓库。
建议把它放在固定位置，并在新环境 clone 后更新路径配置。
"""

    with open(package_dir / "工作目录说明.md", 'w', encoding='utf-8') as f:
        f.write(work_dir_note)
    log("  工作目录说明.md")
    print()

    # ------------------------------------------
    # Step 5: 生成 Agent 配置片段
    # ------------------------------------------
    print("📝 Step 5: 生成 Agent 配置片段...")

    if agents:
        agents_config = {
            "agents": {
                "defaults": {
                    "model": {"primary": "qclaw/modelroute"},
                    "maxConcurrent": 10,
                    "subagents": {"allowAgents": [a["id"] for a in agents if a.get("id") != "main"]}
                },
                "list": agents
            },
            "hooks": {
                "allowedAgentIds": [a['id'] for a in agents if a['id'] != 'main']
            }
        }

        with open(package_dir / "openclaw-agents.json", 'w', encoding='utf-8') as f:
            json.dump(agents_config, f, indent=2, ensure_ascii=False)

        log("  openclaw-agents.json")
    print()

    # ------------------------------------------
    # Step 6: 生成 README.md
    # ------------------------------------------
    print("📄 Step 6: 生成 README.md...")

    team_tree = ""
    if has_team:
        team_tree = f"""
├── 团队成员层/          ← {len(team_members)} 个团队成员
│   ├── 小A/
│   └── 小B/
│   └── ..."""

    readme_content = f"""# 一键搬家包

**生成时间:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
**Agent:** {agent_name}
**Workspace:** {main_workspace}
"""

    if has_team:
        readme_content += f"**团队成员:** {len(team_members)} 人\n\n成员列表:\n"
        for m in team_members:
            readme_content += f"- **{m['name']}** (`{m['id']}`)\n"

    readme_content += f"""
---

## 包内容

```
搬家包/
├── 身份层/              ← Main Agent 核心文件
│   ├── SOUL.md
│   ├── MEMORY.md
│   ├── TOOLS.md
│   ├── AGENTS.md
│   ├── IDENTITY.md
│   ├── USER.md
│   └── memory/{team_tree}
│
├── skills/              ← Skills({skill_count}个)
│
├── openclaw-agents.json ← Agent 配置片段
├── cron_jobs.json       ← Cron 任务配置
├── 工作目录说明.md      ← git工作目录提示
│
├── README.md            ← 本文件
└── migrate.py           ← 一键执行脚本
```

---

## 使用方法

### 自动执行(推荐)

把这个包丢给 agent,说:

```
这是一键搬家包,帮我搬家
```

agent 会自动执行:
1. 备份现有配置
2. 合并 agent 配置(不覆盖其他配置)
3. 复制身份文件 + 团队成员 + skills
4. 创建 cron 任务
5. 重启 Gateway

### 手动执行

```bash
unzip 搬家包.zip
cd 搬家包
python3 migrate.py
```

---

## 注意事项

1. 搬家前会**自动备份**现有配置到 `~/.qclaw/backup/`
2. agent 配置会与现有配置**合并**,不会覆盖其他配置
3. 完成后请重启一次 Gateway(如果未自动重启)

---

**生成脚本:** skills/quick-resurrection/pack.py v2.0
"""

    with open(package_dir / "README.md", 'w', encoding='utf-8') as f:
        f.write(readme_content)

    # 把 migrate.py 也复制进包(从 skill 目录复制)
    skill_dir = main_workspace / "skills" / "quick-resurrection"
    migrate_src = skill_dir / "migrate.py"
    if migrate_src.exists():
        shutil.copy2(migrate_src, package_dir / "migrate.py")
        log("  migrate.py")

    log("  README.md")
    print()

    # ------------------------------------------
    # Step 7: 打包
    # ------------------------------------------
    print("📦 Step 7: 打包...")

    zip_file = str(package_dir) + ".zip"
    shutil.make_archive(str(package_dir), 'zip', package_dir)

    zip_size = os.path.getsize(zip_file)
    size_mb = zip_size / (1024 * 1024)

    log(f"  打包完成:{zip_file}")
    info(f"  包大小:{size_mb:.2f} MB")

    shutil.rmtree(package_dir)

    print()

    # 完成报告
    print("=" * 60)
    print("  打包完成")
    print("=" * 60)
    print()
    print(f"✅ 输出文件:{zip_file}")
    print(f"✅ 包大小:{size_mb:.2f} MB")
    print()
    print("📋 包内容:")
    print(f"   - Main Agent({agent_name})")
    print(f"   - 团队成员:{len(team_members)} 人")
    print(f"   - Skills:{skill_count} 个")
    print(f"   - Cron任务:{len(cron_tasks)} 个")
    if found_work_dirs:
        print(f"   - 工作目录:{len(found_work_dirs)} 个(含git仓库)")
    print()
    print("📋 后续步骤:")
    print()
    print("   1. 把搬家包复制到新环境")
    print("   2. 把搬家包丢给 agent,说:'这是一键搬家包,帮我搬家'")
    print()
    print("=" * 60)
    print()

if __name__ == "__main__":
    main()


path: templates/backup-template.md
content: # 团队搬家备份清单

## 基本信息
- 打包日期：
- 操作系统：
- Agent 版本：

## 需要打包的内容
- [ ] SOUL.md
- [ ] SKILL.md（自定义 skill）
- [ ] MEMORY.md
- [ ] USER.md
- [ ] TOOLS.md
- [ ] AGENTS.md
- [ ] current_tasks.md
- [ ] memory/ 目录
- [ ] skills/ 目录
- [ ] cron_tasks.json
- [ ] openclaw-agents.json

## 注意事项
- 打包前删除 __pycache__
- 敏感信息（API Key）请手动处理
- 跨平台迁移注意路径差异（Mac/Windows/Linux）


path: templates/migrate-checklist.md
content: # 迁移完成后检查清单

## 环境检查
- [ ] Python3 已安装：`python3 --version`
- [ ] OpenClaw 已安装：`openclaw --version`
- [ ] SSH Key 已配置（如使用 Git 同步）

## Agent 检查
- [ ] Main Agent 能正常唤醒
- [ ] 子代理能通过 sessions_spawn 激活
- [ ] 所有 SOUL.md 已复制到位

## 定时任务检查
- [ ] cron 任务已注册
- [ ] 定时任务执行正常

## 功能验证
- [ ] 能正常对话
- [ ] 子代理能正常派出
- [ ] 文件读写正常


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