173 lines
12 KiB
Markdown
173 lines
12 KiB
Markdown
# 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 = <the regex>`
|
||
- 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/<session_id>/inbound.db`——**宿主机写入,容器读取**。`messages_in`、路由、destinations、pending questions、processing_ack。计划任务存于此处(见"调度")。
|
||
3. `data/v2-sessions/<session_id>/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-<channel>` 技能:
|
||
1. `git fetch origin channels`
|
||
2. `git show channels:src/channels/<name>.ts > src/channels/<name>.ts`
|
||
3. 将 `import './<name>.js';` 追加到 `src/channels/index.ts`
|
||
4. `pnpm install @chat-adapter/<name>@<pinned>`
|
||
5. `pnpm run build`
|
||
|
||
幂等——重新运行为无操作。固定版本保持供应链诚信。自动迁移检测 v1 中哪些频道已被连接(通过不同的 `channel_name` / JID 前缀),并为每个频道运行匹配的 `setup/install-<channel>.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 = "<channel_type>:<handle>", 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/<folder>/CLAUDE.md` 和可选的 `logs/`。`CLAUDE.md` 是一个纯指令文件,group 特定。
|
||
|
||
**v2:** 每个 group 仍然位于 `groups/<folder>/`,但形态更丰富:
|
||
- `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/<folder>/.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)。
|