# v1 → v2 迁移 — 开发指南 如何测试、开发和调试迁移流程。 ## 快速入门 ```bash # 完整周期:重置 → 迁移 → Claude 完成 bash migrate-v2-reset.sh && bash migrate-v2.sh ``` ## 架构 两部分迁移: 1. **`migrate-v2.sh`**——确定性 bash 脚本。处理前提条件、DB 种子、文件拷贝、频道安装、容器构建、服务切换。写入 `logs/setup-migration/handoff.json`,然后 `exec` 到 Claude 中。 2. **`/migrate-from-v1` 技能**——由 Claude 驱动。读取交接文件,种子 owner/roles,清理 CLAUDE.local.md,验证容器配置,移植 fork 定制。 ## 文件布局 ``` migrate-v2.sh # 入口点 migrate-v2-reset.sh # 清除 v2 状态以重新测试 setup/migrate-v2/ env.ts # 阶段 1a:合并 .env db.ts # 阶段 1b:种子 v2 DB groups.ts # 阶段 1c:拷贝 group 目录 + container.json sessions.ts # 阶段 1d:拷贝 session 并设置续接 tasks.ts # 阶段 1e:移植计划任务 channel-auth.ts # 阶段 2b:拷贝频道认证状态 select-channels.ts # 阶段 2a:clack 多选 switchover-prompt.ts # 服务切换提示 setup/migrate-v2/shared.ts # 共享辅助函数(JID 解析、触发器映射等) .claude/skills/migrate-from-v1/ # Claude 技能 logs/setup-migration/handoff.json # 由 migrate-v2.sh 写入,由技能读取 logs/migrate-steps/*.log # 每个步骤的原始输出 ``` ## 开发循环 ```bash # 重置 v2 到干净状态(保留 node_modules) bash migrate-v2-reset.sh # 以非交互式频道选择运行迁移 NANOCLAW_CHANNELS="telegram" bash migrate-v2.sh # 或以交互方式运行(clack 多选) bash migrate-v2.sh ``` `migrate-v2-reset.sh` 清除:`data/`、`logs/`、`.env`、`groups/`(恢复 git 跟踪的)、`container/skills/`(恢复 git 跟踪的)、`src/channels/`(恢复 git 跟踪的)。 它**不**清除 `node_modules/`(重新安装成本高昂)。 ## 测试单个步骤 每个步骤是一个独立的 TypeScript 文件: ```bash # 运行单个步骤(在 pnpm install 之后) pnpm exec tsx setup/migrate-v2/env.ts /path/to/v1 pnpm exec tsx setup/migrate-v2/db.ts /path/to/v1 pnpm exec tsx setup/migrate-v2/groups.ts /path/to/v1 pnpm exec tsx setup/migrate-v2/sessions.ts /path/to/v1 pnpm exec tsx setup/migrate-v2/tasks.ts /path/to/v1 pnpm exec tsx setup/migrate-v2/channel-auth.ts /path/to/v1 telegram discord ``` 每个步骤向 stdout 打印 `OK:
`、`SKIPPED:` 或错误。成功/跳过时退出 0,失败时退出非零值。 ## 调试 ### 检查已迁移的内容 ```bash # Agent groups sqlite3 data/v2.db "SELECT * FROM agent_groups" # Messaging groups + 连线 sqlite3 data/v2.db "SELECT mg.id, mg.channel_type, mg.platform_id, mg.unknown_sender_policy, mga.engage_mode, mga.engage_pattern FROM messaging_groups mg JOIN messaging_group_agents mga ON mga.messaging_group_id = mg.id" # Sessions sqlite3 data/v2.db "SELECT * FROM sessions" # 用户和角色 sqlite3 data/v2.db "SELECT * FROM users" sqlite3 data/v2.db "SELECT * FROM user_roles" # Session 续接(哪个 Claude Code session 将被恢复) AG_ID=$(sqlite3 data/v2.db "SELECT id FROM agent_groups LIMIT 1") SESS_ID=$(sqlite3 data/v2.db "SELECT id FROM sessions LIMIT 1") sqlite3 data/v2-sessions/$AG_ID/$SESS_ID/outbound.db "SELECT * FROM session_state" # 计划任务 sqlite3 data/v2-sessions/$AG_ID/$SESS_ID/inbound.db "SELECT id, kind, recurrence, status FROM messages_in WHERE kind='task'" ``` ### 检查交接文件 ```bash python3 -m json.tool logs/setup-migration/handoff.json ``` ### 常见问题 **切换后 bot 不响应:** 1. 检查两个服务都没有运行:`systemctl --user list-units 'nanoclaw*'` 2. 检查错误日志:`tail logs/nanoclaw.error.log` 3. 检查发送者策略:`sqlite3 data/v2.db "SELECT unknown_sender_policy FROM messaging_groups"`——在 owner 被种子之前必须是 `public` 4. 检查触发模式:`sqlite3 data/v2.db "SELECT engage_mode, engage_pattern FROM messaging_group_agents"`——应为 `pattern` / `.` 用于响应所有内容 **Session 未从 v1 续接:** 1. 检查是否设置了续接:见上方的"Session 续接"查询 2. 检查 JSONL 是否在正确路径下:`ls data/v2-sessions//.claude-shared/projects/-workspace-agent/` 3. v1 session JSONL 应从 `-workspace-group/` 拷贝到 `-workspace-agent/`(v2 容器 CWD 为 `/workspace/agent`) **服务切换回退不起作用:** 1. v2 服务名称为 `nanoclaw-v2-`——找到它:`systemctl --user list-units 'nanoclaw*'` 2. 手动停止:`systemctl --user stop && systemctl --user disable ` 3. 重启 v1:`systemctl --user start nanoclaw` ### 步骤日志 每个步骤将原始输出写入 `logs/migrate-steps/.log`。当某个步骤失败时读取这些日志: ```bash cat logs/migrate-steps/1b-db.log cat logs/migrate-steps/1d-sessions.log ``` ## 关键决策 - `unknown_sender_policy` 在迁移期间设置为 `public`,以便 bot 立即响应。`/migrate-from-v1` 技能在种子 owner 后收紧此设置。 - v1 中 `requires_trigger=0` 优先于非空的 `trigger_pattern`——它意味着"响应所有内容"。 - v1 `container_config.additionalMounts` 直接写入 v2 `container.json`(相同格式)。 - v1 Claude Code session 从 `-workspace-group/` 拷贝到 `-workspace-agent/`,session ID 以 `continuation:claude` 写入 `outbound.db`,以便 agent-runner 恢复同一对话。 - 结尾的 `exec claude "/migrate-from-v1"` 替换了 bash 进程——`write_handoff` 在 `exec` 之前显式调用,因为 EXIT 陷阱不会在 `exec` 时触发。