# NanoClaw v1 → v2 — 变更内容 NanoClaw v1(你一直在运行的 `~/nanoclaw` 检出)与 v2(本次重写)之间的宏观差异。这不是迁移指南——那是 `bash migrate-v2.sh` 和 `/migrate-from-v1` 技能的角色。本文档是**词汇表**:当某些东西被移动或重命名时,在这里找到它。 在接触迁移代码或将定制向前移植之前,请阅读本文档。 --- ## 一句话总结 v1 是一个 Node 进程,带有一个 SQLite 文件和原生频道适配器。v2 是一个生成每个 session(会话)Docker 容器的宿主机,将状态拆分到中央库 + 每个 session 的 DB 对中,通过显式实体模型路由,并将频道作为技能从兄弟分支安装。 --- ## 实体模型——最大的变化 **v1:** 一张扁平的 `registered_groups(jid, name, folder, trigger_pattern, requires_trigger, is_main, channel_name)` 表。一个 group 目录是 agent 身份的单位。一个聊天(JID)连接到恰好一个目录,`trigger_pattern` 是路由器应用于每条入站消息的不透明正则表达式。 **v2:** 三张表,中间有一个有意的多对多关系: ``` agent_groups ─┐ ├─ messaging_group_agents ─┬─ messaging_groups │ (engage_mode, │ (channel_type, │ engage_pattern, │ platform_id, │ sender_scope, │ unknown_sender_policy) │ ignored_message_policy, │ session_mode, priority) ``` 后果: - **一个 agent 可以在多个聊天上应答,一个聊天可以扇出到多个 agent。** v1 两者都做不到。 - **没有 `is_main` 标志。** 权限现在通过 `user_roles`(owner/admin,全局或限定范围)显式化。见下文。 - **没有 `trigger_pattern` 正则表达式。** 替换为四个正交列。自动迁移和 `/migrate-from-v1` 技能使用的映射规则: - v1 `trigger_pattern` 非空 → v2 `engage_mode='pattern'`、`engage_pattern = ` - v1 `requires_trigger=0` 或 pattern 为 `.`/`.*` → v2 `engage_mode='pattern'`、`engage_pattern='.'`("始终"变体) - 无 pattern 且需要触发器 → v2 `engage_mode='mention'` - `sender_scope` 和 `ignored_message_policy` 是新的;默认值 `all` / `drop` - **JID 分解。** v1 的 `jid` 列存储 `dc:12345` / `tg:67890`。v2 将其拆分为 `channel_type` + `platform_id`。具体来说:`dc:12345` 变为 `channel_type='discord'`、`platform_id='discord:12345'`。前缀别名(`dc` → `discord`、`tg` → `telegram`、`wa` → `whatsapp`)在 `setup/migrate-v2/shared.ts` 中。 - **v1 中 `channel_name` 不可靠。** 许多行的该列为空;实际频道只能从 JID 前缀猜测。v2 的 `channel_type` 始终是显式的。 --- ## 中央库 vs. Session DB **v1:** 一个 SQLite 文件位于 `store/messages.db`。每个聊天、消息、注册 group、计划任务和 session 都存在那里。宿主机和任何 agent 进程都打开同一个文件。 **v2:** 三种 DB 形态。 1. `data/v2.db`——**中央库**。所有非 per-session 的东西:users、roles、agent groups、messaging groups、连线、pending approvals、user DMs、schema 迁移。 2. `data/v2-sessions//inbound.db`——**宿主机写入,容器读取**。`messages_in`、路由、destinations、pending questions、processing_ack。计划任务存于此处(见"调度")。 3. `data/v2-sessions//outbound.db`——**容器写入,宿主机读取**。`messages_out`、session_state。 每个文件有且仅有一个写入者。无跨挂载锁竞争。心跳是对 `/workspace/.heartbeat` 的文件 touch,而非 DB 更新。宿主机使用偶数 `seq` 号,容器使用奇数。 消息历史(v1 `messages` 表、v1 `chats` 表)**不被迁移**。迁移将运行上重要的状态向前复制(agent、频道、连线、计划任务、group 目录),留下聊天日志。 --- ## 调度 **v1:** 在 `store/messages.db` 中有专门的 `scheduled_tasks` 表,拥有自己的列(`schedule_type`、`schedule_value`、`next_run`、`last_run`、`context_mode`、`script`、`status`)。一个独立的类似 cron 的调度进程从中读取。 **v2:** 计划任务是 session `inbound.db` 中带有 `kind='task'` 的 **`messages_in` 行**。相关列: - `process_after`(ISO8601)——宿主机 sweep 在 `datetime(process_after) <= datetime('now')` 时唤醒容器 - `recurrence`——cron 字符串;`NULL` = 单次执行 - `series_id`——将重复事件分组;首次插入时设置为 task id - `status`——`pending` | `processing` | `completed` | `failed` | `paused` 公共 API 是 `src/modules/scheduling/db.ts` 中的 `insertTask()`。重复执行通过 `cron-parser` 在用户时区计算(见 `src/modules/scheduling/recurrence.ts`)。迁移将 v1 的 `schedule_type`+`schedule_value` 对映射为单个 cron 字符串,然后调用 `insertTask()`。 任务可以在 session 唤醒之前存在——宿主机 sweep 在首次到期时创建/唤醒容器。 --- ## 凭据 **v1:** `.env`——明文环境变量。`DISCORD_BOT_TOKEN`、`ANTHROPIC_API_KEY` 等。宿主机直接读取它们并传递给任何需要它们的代码。 **v2:** OneCLI Agent Vault。一个位于 `http://127.0.0.1:10254` 的独立本地服务持有秘密。Agent 被*限定*到特定秘密,vault 在批准后将它们注入到离开容器的 API 请求中。容器永远看不到原始秘密值。 注意:自动创建的 agent 默认为 `selective` 秘密模式——没有附加秘密,即使匹配的秘密存在于 vault 中。关于修复方案(`onecli agents set-secret-mode --mode all`),见根 CLAUDE.md 中"auto-created agents start in selective secret mode"部分。 **自动迁移做了什么:** 将每个 v1 `.env` 键逐字复制到 v2 `.env`,永不覆盖现有的 v2 键。OneCLI vault 迁移是一个单独的步骤,由 `/init-onecli` 技能处理,该技能知道如何从 `.env` 中拉取。 --- ## 频道适配器 **v1:** 在 `src/channels/` 中导入的原生适配器(例如直接使用 `discord.js`)。安装一个频道意味着编辑代码、添加依赖并设置环境变量。 **v2:** 频道适配器存在于兄弟 `channels` 分支上。每个 `/add-` 技能: 1. `git fetch origin channels` 2. `git show channels:src/channels/.ts > src/channels/.ts` 3. 将 `import './.js';` 追加到 `src/channels/index.ts` 4. `pnpm install @chat-adapter/@` 5. `pnpm run build` 幂等——重新运行为无操作。固定版本保持供应链诚信。自动迁移检测 v1 中哪些频道已被连接(通过不同的 `channel_name` / JID 前缀),并为每个频道运行匹配的 `setup/install-.sh`。v1 中没有 v2 技能的频道(目前少见,随着 v2 的完善将更常见)被记录在交接文件中,由 `/migrate-from-v1` 技能向用户提出。 **`.env` 之外的频道认证。** 某些频道在磁盘上存储 session 状态(Baileys WhatsApp 密钥库、Matrix 同步状态、iMessage tokens)。`channel-auth` 步骤有一个按频道的注册表(`setup/migrate-v2/shared.ts: CHANNEL_AUTH_REGISTRY`),知道除了 env 键之外还要复制哪些文件 glob。 --- ## 权限——从隐式到显式 **v1:** `registered_groups.is_main = 1` 标记一个 group 为特权级别。没有 `users` 表。权限是约定而非强制执行。 **v2:** 显式表。 - `users(id = ":", kind, display_name)`——每个消息平台标识符一行 - `user_roles(user_id, role ∈ {owner, admin}, agent_group_id nullable, granted_by, granted_at)`——owner 始终全局;admin 可以是全局或限定范围 - `agent_group_members(user_id, agent_group_id, ...)`——用于 `sender_scope='known'` 关卡的"已知"成员资格 Owner 在 `/migrate-from-v1` 技能的访谈阶段("哪个 handle 是你?")被种子。自动迁移不会猜测——v1 没有这方面的真源。 **默认访问——"任何人都可以与 bot 通话" vs "仅已知用户"。** v1 隐式存储(通过触发器 regex + `is_main`)。v2 通过 `messaging_groups.unknown_sender_policy ∈ {'strict', 'request_approval', 'public'}` 暴露它。技能询问用户 v1 以哪种模式运行,并相应地翻转已迁移的 messaging group。 --- ## 磁盘上的 Group 目录 **v1:** `groups//CLAUDE.md` 和可选的 `logs/`。`CLAUDE.md` 是一个纯指令文件,group 特定。 **v2:** 每个 group 仍然位于 `groups//`,但形态更丰富: - `CLAUDE.md`——**在容器启动时组合**,来自 `.claude-shared.md`(到全局的 symlink)+ `.claude-fragments/*.md`(模块片段)+ `CLAUDE.local.md`。**不要直接编辑 `CLAUDE.md`。** - `CLAUDE.local.md`——每个 group 的内容。迁移将 v1 的旧 `CLAUDE.md` 写到这里。 - `container.json`——可选的每个 group 的容器配置(apt 依赖、env、挂载)。v1 的 `registered_groups.container_config` JSON 接近但不完全相同——迁移将 v1 的 payload 存储在 `groups//.v1-container-config.json` 中供技能协调,而不是静默映射。 - `.claude-fragments/` 和 `.claude-shared.md` 在宿主机首次接触到该 group 时由 `initGroupFilesystem()` 安装,因此迁移只需写入 `CLAUDE.local.md`,将脚手架工作留给宿主机。 --- ## 宿主机进程 vs. 容器 **v1:** 单个 Node 进程。"Agent"与路由器是同一个进程。 **v2:** 顶层的 Node 宿主机,每个 session 一个 Bun 运行时的 Docker 容器。它们仅通过两个 session DB 通信。无共享模块,无 IPC,无 stdin 管道。如果你编写了从 agent 进入宿主机内部(或反之)的自定义代码,那个接口已不复存在——移植它是 `/migrate-from-v1` 技能的话题,而非机械复制。 锁文件:宿主机使用 `pnpm-lock.yaml`,agent-runner 使用 `bun.lock`。宿主机侧 `minimumReleaseAge: 4320`(3 天供应链等待);agent-runner 没有发布年龄限制。 --- ## 自我修改和 MCP 工具 **v1:** 如果你添加了 MCP 服务器或自我修改管道,通常是直接编辑长期运行的进程。 **v2:** - MCP 服务器通过 `container/agent-runner/src/mcp-tools/*.ts` 注册并按 session 加载。还有 `install_packages` 和 `add_mcp_server` 自我修改工具,在重建容器镜像之前经过管理员审批流程(`src/modules/self-mod/apply.ts`)。 - 你在 v1 中编写的自定义 MCP 工具可以清晰地映射到 v2 的工具注册表,但导入路径、运行时(Bun vs Node)和 SQL 辅助函数差异(`bun:sqlite` 使用 `$name` 前缀的参数)可能需要调整。技能将引导你完成这些。 --- ## 已消失或无法映射的内容 - **`scheduled_tasks` 作为一个单独的表的模式**——已移入 session `inbound.db` 的 `kind='task'` 下。迁移移植活跃行;非活跃/已完成的行导出到 `logs/setup-migration/inactive-tasks.json` 供参考。 - **`messages` / `chats` 表(聊天历史)**——不迁移。如需要,保留在 v1 检出中。 - **`router_state`(键/值)**——不迁移。v2 状态存于上述显式表中。 - **`sessions`(v1 group→session_id)**——v1 session 不映射;v2 session 以 `(agent_group_id, messaging_group_id, thread_id)` 为键并按需创建。 - **对旧 `store/messages.db` 的原始访问**——v1 DB 被保留在原位且不被触碰。如果迁移出错可以重新运行(对于 agent/频道/连线,迁移子步是幂等的;目录使用 rsync 语义)。 --- ## 迁移范围——代码所在位置 - `migrate-v2.sh`——入口点:从 v2 检出运行 `bash migrate-v2.sh`。 - `setup/migrate-v2/*.ts`——各个迁移步骤(env、db、groups、sessions、tasks、channel-auth、select-channels、switchover-prompt)。 - `setup/migrate-v2/shared.ts`——JID 解析、触发器映射、频道认证注册表。 - `logs/setup-migration/handoff.json`——由 `migrate-v2.sh` 写入,由 `/migrate-from-v1` 技能读取。 - `logs/migrate-steps/*.log`——每个步骤的原始 stdout。 - `.claude/skills/migrate-from-v1/SKILL.md`——用于 owner 种子、CLAUDE.md 清理、容器配置验证、fork 移植的 Claude 技能。 - `migrate-v2-reset.sh`——开发辅助工具,清除 v2 状态以重新测试。 - 完整开发指南见 [docs/migration-dev.md](migration-dev.md)。