Block 开源 buzz:多 Agent 通信平台的移动端为何重写协议

2026-08-05

本文基于 buzz 仓库 commit 8342dfc(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/block/buzz 最新代码与文档为准。

buzz 移动端最值得看的不是 Flutter,而是它证明了一件事:当你把协议放在产品前面,新增一个客户端不需要新增一套后端,但每多一种语言,你就要把同一套事实重抄一遍——而且没人能替你保证三份抄本一致。

buzz 是 Block 开源的多 Agent 通信平台,仓库地址 https://github.com/block/buzz ,许可证 Apache-2.0(Copyright 2026 Block, Inc.)。它不是”蜂鸣器”也不是什么热度产品,是一个人和 Agent 待在同一批频道里、消息形态完全相同的协作系统。仓库 README 这样定位自己:人和 Agent 一起干活的工作区,跑在你自己拥有的中继上。这里的**中继(relay)**需要先解释一句:在 Nostr 这类协议里,客户端之间不直接连,消息发给一台叫中继的服务器,它负责存事件、并按订阅条件推给订阅了的人。buzz 的中继实现在 crates/buzz-relay

站内已有几篇相邻的文章,分工不同:Hermes 接入聊天平台的做法讲的是怎么把 Agent 塞进别人家的 IM;跨平台扩展三体对比比较的是扩展机制的取向;结构化输出不稳的排查处理的是模型侧的输出可靠性。这篇只谈一件事——同一份协议规范落到第三种语言里,工程上会发生什么。

一、先看清楚这个仓库是几套实现

全仓有 3704 个受版本控制的文件(其中 bin/ 下 41 个是 hermit 工具链软链接),crates/ 下 28 个 Rust crate 共 413 个文件,desktop/ 2359 个文件,mobile/ 467 个,web/ 65 个,admin-web/ 14 个。数字本身不重要,重要的是它们分属三种语言:中继和后端是 Rust,桌面与两个 Web 面板是 TypeScript,移动端是 Dart/Flutter。

mobile/lib 下有 250 个 .dart 文件,mobile/test 下 103 个 _test.dartlib/features/ 有 10 个功能目录:activity、channels、forum、home、invites、pairing、profile、pulse、search、settings。这里就有第一个要提醒的事实:mobile/README.md 的 Architecture 段落还写着 features/ 下只有一个”Placeholder home surface”,而磁盘上早就不是这样了。文档也是需要同步的一份实现,它掉队得最快。

移动端不是壳。它自己解析 Nostr 事件、自己做 NIP-44 加解密、自己走 NIP-42 认证握手、自己实现设备配对的密钥协商。换句话说,中继那边定义的协议,Dart 这边是从零重写的第三份。

组成部分它负责什么对应仓库位置你什么时候会碰到它
kind 号常量表定义每种事件是什么(消息、反应、删除、Agent 观察帧…)Rust 侧 crates/buzz-core/src/kind.rs;Dart 侧 mobile/lib/shared/relay/nostr_models.dart新增任何一种事件类型时
中继连接与认证WebSocket 收发帧、响应 AUTH 挑战mobile/lib/shared/relay/relay_socket.dart登录失败、掉线重连排查
加解密与签名ECDH、HKDF、NIP-44 v2、Agent 归属验签mobile/lib/shared/crypto/ecdh.dart / hkdf.dart / nip44.dart / nip_oa.dart私聊、观察帧、判断某个 pubkey 是不是 Agent
设备配对扫码建立会话、双端比对短码mobile/lib/features/pairing/pairing_crypto.dart,规范在 crates/buzz-core/src/pairing/NIP-AB.md首次登录、换设备
身份存储把私钥与工作区凭据落到设备安全存储mobile/lib/shared/community/community_storage.dart登出、迁移、丢设备
应用装配主题、启动顺序、认证态分叉mobile/lib/main.dartmobile/lib/app.dart改启动流程、加全局 Provider

二、协议先行省了什么

省的是后端。移动端要拿到频道消息,不需要一个”移动端 API”;它连的是同一台中继,订阅的是同一批事件。桌面端能看到的东西,手机端只要把 kind 认全、把解密做对,就自然能看到。

这条路能走通,靠的是事件本身自带可验证性。Nostr 里一条消息就是一个 JSON 事件:有 kind(无符号整数,标明这是什么类型)、content、tags,以及作者用私钥做的签名。crates/buzz-core/src/event.rs 里有个测试把事件的 sig 字段整个换成 128 个 0,然后断言 verify_id() 仍然通过、verify_signature() 失败——这说明事件 id 是内容的哈希,签名是另一件事。id 对得上只能证明内容没被改;要证明作者是谁,必须单独验签。任何一个客户端,无论用什么语言写,只要能算哈希、能验 Schnorr 签名,就有资格独立判断一条消息真伪,不需要信任服务端的转述。

这也是 Agent 与人能共用一张网的前提。在 buzz 里,Agent 不是一个”机器人账号标志位”,而是自己有一对密钥。它是谁的、被谁授权,靠一条挂在 kind:0 资料事件上的 auth 标签证明。mobile/lib/shared/crypto/nip_oa.dart 完整实现了这个校验:标签形如 ["auth", "<owner-pubkey-hex>", "<conditions>", "<sig-hex>"],被签的原文是 "nostr:agent-auth:" + agent_pubkey_hex + ":" + conditions,用 BIP-340 Schnorr 验签。代码里明确拒绝自证——owner == agent 直接跳过,也就是说没人能自己给自己盖”我属于某某”的章。文件注释写明它对应桌面端 desktop/src-tauri 里的 profile_valid_oa_owner_pubkey

这就是协议先行的红利:手机端判断”这个发言的是不是 Agent、归谁管”,用的是密码学结论,不是服务端返回的一个布尔字段。

三、代价:同一份事实抄第三遍

打开 mobile/lib/shared/relay/nostr_models.dart,第 7 行的注释是这样一句:Keep in sync with desktop/src/shared/constants/kinds.ts。往下是 33 个 kind 常量——note、reaction、auth、agentObserverFrame、jobRequest 到 jobError 一整组、forumPost、huddle 四件套等等。

注意同步方向。Rust 侧的 crates/buzz-core/src/kind.rs 开头自称是”Buzz kind 号的权威来源”,而 Dart 这份注释指向的却是桌面端的 TypeScript 常量文件。同一个事实在仓库里有三份副本,同步关系还不是一条直线。根目录的 AGENTS.md 把这条写成了明文规矩(CLAUDE.md 是同样内容的另一份):移动端的事件 kind 必须与 desktop/src/shared/constants/kinds.ts 保持一致。没有代码生成,靠人和规矩。

漂移的痕迹到处都是。在 mobile/lib 下不区分大小写 grep “mirrors desktop”,能命中 35 处、分布在 23 个文件里——从 inbox 分类优先级(features/activity/inbox_item.dart 注释里的 categoryPriority)、@ 提及候选的排序,到表情爆开动画的生成逻辑(shared/emoji/emoji_burst.dart 对齐桌面端的 spawnPickerEmojiBurst),都写着”对齐桌面端的某个函数”。这些不是协议,是产品行为一致性,同样只能靠注释维系。

代价还体现在验证面上。协议规范这边,buzz 自己在 docs/nips/ 写了 15 份 NIP 规范 md 加 2 份 fixtures json,另有 crates/buzz-core/src/pairing/NIP-AB.md 是第 16 份(NIP 就是 Nostr 生态里给协议提案编号的规范文档)。Rust 侧甚至有 crates/buzz-conformance,把运行时轨迹拿去和 docs/spec/MultiTenantRelay.tla 这份形式化模型比对——所谓形式化验证,是先用数学化的方式写下系统允许的状态迁移,再检查真实实现有没有走出这个范围;该 crate 的 lib.rs 自己写得很清楚,这不是证明,只能检查你实际跑过的执行路径。而 Dart 侧没有对应物:mobile/test/shared/crypto/ 目录下只有 nip_oa_test.dart 一个文件,nip44.dart 那 169 行的 v2 实现在这个目录里没有独立的测试文件与之对应——它只在 mobile/test/features/channels/agent_activity/observer_subscription_test.dart 这类上层测试里被间接跑到。这个区别要紧:上层测试验的是”观察帧这条链路能通”,而跨端互操作要的是”同一个密文,Dart 加、TypeScript 解也能对上”,后者需要固定测试向量,光靠自己加自己解是测不出字节序错位的——你和自己永远一致。

四、移动端自己实现的那几件硬事

NIP-44 v2 加密mobile/lib/shared/crypto/nip44.dart):会话密钥由 HKDF-Extract(salt="nip44-v2", ikm=ecdh_shared_secret) 得到,再用 HKDF-Expand 展开 76 字节,切成 32 字节 ChaCha 密钥、12 字节 nonce、32 字节 HMAC 密钥;密文包体是 版本(0x02) || nonce(32) || ciphertext || mac(32) 再 base64,HMAC-SHA256 覆盖的是 nonce || ciphertext。明文长度限制 1 到 65535 字节。底层用 pointycastle。这里每一步的顺序都不能自由发挥,错一个字节顺序,桌面端就解不开。

设备配对mobile/lib/features/pairing/pairing_crypto.dart,规范 NIP-AB.md):扫的二维码是 nostrpair://<pubkey>?secret=<hex>&relay=<url>&v=1 形式,代码里有 2048 字符上限。会话 id 由 HKDF 以 nostr-pair-session-id 为 info 导出;防中间人的手段是 SAS——两端各自从 ECDH 共享密钥导出同一串字节,取前 4 字节大端整数对 1000000 取模,得到一个 6 位数字给人肉眼比对,两边数字一致才继续。还有一个 128 字节的 transcript(会话 id、双方公钥、SAS 输入拼起来)再做一次 HKDF,把整个会话参数绑死。

中继认证mobile/lib/shared/relay/relay_socket.dart):中继下发 AUTH 挑战后,客户端把 nsec 解成 hex 私钥,构造一个 kind 22242 的事件,tags 里放 ["relay", <ws地址>]["challenge", <挑战串>],签名后以 ["AUTH", event] 发回,然后等一个 event id 匹配的 OK 帧。被拒时走 classifyRelayAuthFailure 分类。这是 NIP-42 那套。

身份存储与密钥自持:私钥以 nsec(bech32 编码的私钥)形式,连同工作区信息一起存进 flutter_secure_storagecommunity_storage.dart,键 buzz_communities,另有 buzz_nsec 等旧键的迁移路径)。auth_provider.dart 启动时会验:Nip19 解码后前缀必须是 nsec、数据长度必须是 64,不合法就把这条工作区删掉、换下一条,全都不合法就回到配对页。这里必须说清楚:密钥自持意味着服务端手里没有你的私钥副本,丢了私钥就是丢了身份,没有”找回账号”这条路,登出(signOut)在代码里就是把当前这条工作区记录从安全存储里删掉,若还有别的工作区就切到下一条,一条不剩才回配对页。

Agent 实时观察:kind 24200 的 observer 帧走 NIP-44 解密(features/channels/agent_activity/observer_subscription.dart),每个 Agent 最多保留 800 条帧。也就是说手机上看 Agent 干活是有窗口的,不是完整历史。

五、边界与代价:它明确不管什么

  • 不管把三份实现自动对齐。kind 表是手抄的,行为一致性靠注释,仓库里没有从单一来源生成 Rust/TS/Dart 常量的机制。你加一种事件类型,就要自己走完三处。
  • 不管让移动端脱离中继工作。README 描述的当前形态是单中继:一个中继 URL 就选定一个 community。客户端再独立,也要连上那台机器。
  • 不管替你保管密钥。前面说过,私钥在设备安全存储里,没有托管方,也就没有找回。
  • 不管给 Dart 侧同等的验证强度。TLA+ 模型、conformance 校验器都在 Rust 侧;移动端能跑的检查就是 dart formatflutter analyzeflutter testmobile/README.md 的 Checks 段)。而 AGENTS.md 还明确规定:Agent 只允许跑这三样,flutter run / build / clean / upgrade 一律禁止。让 Agent 改移动端代码时,它能自证的范围比改 Rust 时窄得多。
  • 不管帮你把发布签名藏起来。Android 正式包要求上传密钥的四个参数全部由环境变量提供(BUZZ_ANDROID_UPLOAD_KEYSTORE_PATH 等),且 README 明写 keystore 路径必须是绝对路径、文件必须留在仓库之外;走中心签名服务时置 BUZZ_ANDROID_RELEASE_SIGNING=external,该模式产出未签名包,并且检测到任何 BUZZ_ANDROID_UPLOAD_* 变量就拒绝运行。这套约束的含义是:签名材料的暴露面由你的 CI 决定,仓库只负责拒绝把它带进来。
  • 另外提醒一句:根目录那 8 份 VISION*.md 写的是愿景,不是已实现的功能清单,读的时候别把两者混起来。

六、上手与避坑清单

  • 只改一处 kind 就提交。会踩是因为编译器不会报错——Dart 那 33 个常量少一个,手机端只是把这类事件当未知类型静默丢掉,桌面端却一切正常,问题要等用户截图才暴露。怎么避:改 kind 时同时过 crates/buzz-core/src/kind.rsdesktop/src/shared/constants/kinds.tsmobile/lib/shared/relay/nostr_models.dart 三处,把这条写进你自己的检查清单。
  • 照着 mobile/README.md 的架构图找代码。会踩是因为那段还停留在只有 home 占位页的时期。怎么避:以 mobile/lib/ 的真实目录树和 AGENTS.md 的 Mobile App 段为准,README 当背景读。
  • 在 git worktree 里开发,然后困惑登录态。会踩是因为 debug 构建的应用标识是按 worktree 目录名生成的(iOS com.buzz.buzzMobile.<slug>、Android xyz.block.buzz.mobile.<slug>),跟着目录走而不是跟着分支走。怎么避:知道这是设计——一个 worktree 恒定一个安装、切分支不掉登录态;直接用 Xcode / Android Studio / flutter run 的人,每次切分支手动跑一次 scripts/mobile-worktree-overrides.sh 刷新显示标签;残留的安装用 just mobile-clean 清。
  • 把 nsec 当普通 token 处理。会踩是因为它在 JSON 里就是一个字符串字段,很容易顺手打日志、丢剪贴板、塞进错误上报。怎么避:把它当私钥本体对待——它签出去的每一条事件都代表这个身份,泄露不可撤销,登出只是本地删除、不会作废任何已签消息。
  • 顺手”优化”NIP-44 的实现细节。会踩是因为 Dart 这份是独立实现,本地跑通不代表跨端能解。怎么避:填充方式、密钥切分顺序、HMAC 覆盖范围(nonce || ciphertext)一个都别动;真要改,先准备好与桌面端、中继互解的固定向量。
  • 给配对二维码的版本号加 fallback。会踩是因为”未知就当成 1”看起来很稳妥。怎么避:NIP-AB.md 明确要求遇到不认识的 v 值必须拒绝并提示用户升级,只有 v 参数缺省时才默认为 1,这两种情况不能混。
  • 指望 Agent 观察面板回放全部历史。会踩是因为它看起来像日志。怎么避:记住每个 Agent 保留 800 条帧的上限,要留证据就落到别处。
  • 写超长文件。会踩是因为页面文件容易越滚越大。怎么避:mobile/scripts/check-file-sizes.mjs 有 1000 行硬上限,AGENTS.md 明确写了触发时要拆文件、不许提高上限也不许加豁免。

收个尾

如果你要用一句话总结 buzz 移动端给出的经验:协议先行让”再做一个客户端”变成一个纯客户端问题,但它把一致性成本从接口层挪到了人和纪律上——挪走了,不等于消失了。

接手时可以按这个顺序读:先 mobile/lib/shared/relay/nostr_models.dart 认全事件类型,再 relay_socket.dart 看连接与认证怎么起来,然后 shared/crypto/ 四个文件搞清加解密与 Agent 归属校验,最后 app.dart 看认证态如何分叉出配对页和主页。自检就三条:新增一种事件时,你能立刻说出要改哪三个文件吗?私钥在哪个存储里、丢了会怎样,你能对用户讲清楚吗?你改的这块,flutter test 覆盖得到吗?

想再往外看一层协议取向的差异,可以接着读Agent 协议生态对比

本篇属于一个把开源多 Agent 通信平台 buzz逐层拆开讲的系列,整体地图见 buzz 是什么:Block 开源的多 Agent 通信平台全景图;沿着这条线往下,还可以看 Block 开源多 Agent 通信平台 buzz 桌面端与中继分工拆解Block 开源多 Agent 平台 buzz:一张图上传的四道关

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。