Buzz 非 AI 机器人开发实战:基于 Countdown Bot 示例打通 NIP-42 认证与频道消息接入
【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz
本指南以开源仓库 buzz 中 examples/countdown-bot 示例为骨架,讲解如何在 Buzz(基于 Nostr 协议的蜂巢式通信平台)上构建一个不需要任何 AI/LLM 能力的纯算法机器人。通过阅读本文,你将掌握 Buzz 参与者的最小协议要求、两种中继认证路径(独立身份与 NIP-OA 所有者背书)、频道自加入机制,以及如何用一条命令在本地把机器人跑起来。
示例概览:一个"故意无聊"的算法机器人
Countdown Bot 是仓库中的一个刻意保持简单、不依赖任何模型推理的机器人示例。它只做一件事:监听一个 Buzz 频道,并对简单命令给出确定性回复:
| 输入 | 输出 |
|---|---|
!countdown 5 | 5 4 3 2 1 🚀 |
!fib 8 | 13 8 5 3 2 1 1 0 |
@Countdown Bot fib 8 | 13 8 5 3 2 1 1 0 |
这段行为在 examples/countdown-bot/src/main.rs 的单元测试中有明确固化,例如command_reply("!countdown 5")必须等于"5 4 3 2 1 🚀"、command_reply("!fib 101")必须返回越界提示"Please use a number from 1 to 100."。
它存在的意义不是功能本身,而是证明一条关键结论:Buzz 的参与者不必是 LLM Agent。任何能够持有 Nostr 密钥、应答 NIP-42 认证、发布 kind0个人资料、订阅事件并发布 kind9频道消息的进程,都可以成为 Buzz 机器人。
成为 Buzz 机器人的最小协议要求
根据 README 的界定,一个进程要成为 Buzz 参与者,只需满足以下协议能力:
- 持有 Nostr 密钥:拥有
nsec或十六进制私钥,用于给事件签名; - 应答 NIP-42 AUTH 挑战:在 WebSocket 握手后完成中继的认证握手;
- 发布 kind
0个人资料:让机器人拥有可识别的名称、头像与简介; - 订阅频道事件:通过 REQ 订阅目标频道下的 kind
9消息; - 发布 kind
9频道消息:以频道成员身份向频道写入回复。
这五项能力对应了 main.rs 中主流程的四个阶段:
let mut ws = connect_and_authenticate(&config).await?; publish_profile(&mut ws, &config).await?; announce_channel_membership(&mut ws, &config).await?; subscribe_to_channel(&mut ws, &config.channel_id).await?;随后进入一个tokio::select!事件循环,一边处理Ctrl-C信号优雅退出,一边消费中继推送的文本消息。示例刻意选择了"直接 WebSocket + NIP-42"而非 MCP 通道,目的正如 README 所注:让整条协议路径可以在单个小文件中完整审查,不被 SDK 的抽象层掩盖。
启动阶段的源码解析
1. 发布 kind0个人资料
机器人启动后首先发布一份名为Countdown Bot的 kind0资料,其中包含一个内嵌的 SVG 时钟图标(以data:image/svg+xmlData URL 形式放入picture字段)。源码通过buzz-sdk的 build_profile 构造事件:
let builder = buzz_sdk::builders::build_profile( Some(BOT_DISPLAY_NAME), // "Countdown Bot" Some(BOT_NAME), // "countdown-bot" Some(BOT_ICON_DATA_URL), // 内嵌 SVG 时钟图标 Some(BOT_ABOUT), // 机器人简介 None, // nip05 )?;build_profile只把传入的Some字段写入 JSON 对象,因此机器人可以按需只暴露display_name、name、picture、about中的任意组合。
2. 以 kind:9000 自加入频道(成员身份公告)
接着机器人以"尽力而为"(best-effort)的方式发布一个 NIP-29kind:9000自加入事件,携带role=bot标签。对应源码 announce_channel_membership:
let builder = EventBuilder::new(Kind::Custom(9000), "").tags([ Tag::parse(["h", config.channel_id.as_str()])?, Tag::parse(["p", &config.bot_keys.public_key().to_hex()])?, Tag::parse(["role", "bot"])?, ]);这里的三类标签与 SDK 中 build_add_member(NIP-29 add-member)的约定一致:h指向频道 UUID,p指向被加入成员的公钥,role声明成员角色。正是这则成员事件让机器人出现在频道成员列表、并进入 Buzz 的 @ 提及自动补全中。如果自加入失败(例如私有频道没有写入权限),代码只打印告警而不中断运行——README 明确提示:私有频道需要由 owner/admin 先把机器人公钥加入频道成员,它才能被提及、读取和写入。
3. 订阅频道 kind9消息
订阅过滤器只关注 kind9且h标签等于目标频道 UUID 的事件:
let filter = Filter::new().kind(Kind::Custom(9)).custom_tag( SingleLetterTag::lowercase(Alphabet::H), channel_id.to_string(), );这对应了 README 中"发布 kind9频道消息"这一核心交互面——Buzz 的频道消息正是通过带h标签的 kind9事件承载的。
4. 消息处理与防回环
收到事件后,maybe_reply 先做两道过滤:
- 忽略自己的消息:
event.pubkey == config.bot_keys.public_key()时直接返回,避免机器人回复自己的回复形成死循环; - 忽略历史事件:
event.created_at < started_at(启动时刻)之前的事件不处理,避免订阅回放触发旧消息批量回复。
通过过滤后,机器人用buzz_sdk::builders::build_message构造 kind9回复,并把被回复者的 pubkey 作为 p 标签提及传入(详见下文"提及命令"一节)。
认证路径一:独立机器人身份(standalone)
当机器人应当以自己独立的中继身份被接纳时,使用standalone模式。此时机器人只用自己的密钥完成 NIP-42 认证,不借助任何其他身份:
BUZZ_RELAY_URL=ws://localhost:3000 \ BUZZ_CHANNEL_ID=<channel-uuid> \ BUZZ_BOT_PRIVATE_KEY=<bot-nsec-or-hex-secret> \ BUZZ_BOT_AUTH_MODE=standalone \ cargo run --manifest-path examples/countdown-bot/Cargo.toml在关闭(closed)或白名单(allowlisted)中继上,需要先把机器人公钥加入中继成员,或加入配置的公钥白名单,再启动机器人。这条路径不复用任何所有者的访问权限——吊销机器人即从成员中移除该机器人公钥。
对应源码中,Config::from_env(main.rs)在auth_mode == "standalone"时将owner_auth_tag置为None,认证事件退化为标准的 NIP-42 AUTH:
EventBuilder::auth(challenge, relay_url).sign_with_keys(&config.bot_keys)?认证路径二:所有者背书机器人身份(owner-attested)
在owner-attested模式下,机器人仍然用自己密钥签名消息,但它的 NIP-42AUTH事件额外携带一个由所有者密钥签名的 NIP-OAauth标签。这一路径复用了 Buzz Agent 在 owner/agent OAuth 流程后获得的同一套"所有者背书"凭证机制:中继允许机器人连接,是因为它的所有者是中继成员,而不必把机器人密钥做成持久中继成员。
方式 A:启动时动态生成 auth 标签
BUZZ_RELAY_URL=ws://localhost:3000 \ BUZZ_CHANNEL_ID=<channel-uuid> \ BUZZ_BOT_PRIVATE_KEY=<bot-nsec-or-hex-secret> \ BUZZ_OWNER_PRIVATE_KEY=<owner-or-agent-nsec-or-hex-secret> \ BUZZ_BOT_AUTH_MODE=owner-attested \ cargo run --manifest-path examples/countdown-bot/Cargo.toml配置解析时,若未提供BUZZ_AUTH_TAG,代码会调用 SDK 的 compute_auth_tag 现场生成背书:
buzz_sdk::nip_oa::compute_auth_tag(&owner_keys, &bot_keys.public_key(), "")?然后立即用 verify_auth_tag 自检,确认标签与机器人公钥匹配后打印背书所有者,再交给 parse_auth_tag 解析为可嵌入 AUTH 事件的Tag(该解析只做结构校验、不做密码学运算,是 MCP 启动等快速路径使用的入口)。
方式 B:预计算并显式传入标签
也可以在外部预先算好标签,直接注入环境变量:
BUZZ_AUTH_TAG='["auth","<owner-pubkey>","","<sig>"]' \ BUZZ_BOT_AUTH_MODE=owner-attested \ # plus BUZZ_RELAY_URL, BUZZ_CHANNEL_ID, BUZZ_BOT_PRIVATE_KEY cargo run --manifest-path examples/countdown-bot/Cargo.toml预计算可以使用 SDK 提供的独立示例命令 compute_auth_tag:
cargo run --release --example compute_auth_tag -- <owner_secret_hex> <agent_pubkey_hex> [conditions]NIP-OA 背书原理
NIP-OA(Owner Attestation)的auth标签是长度为 4 的 JSON 数组:
["auth", "<owner-pubkey-hex>", "<conditions>", "<sig-hex>"]签名构造规则(见 crates/buzz-sdk/src/nip_oa.rs):
preimage = "nostr:agent-auth:" || agent_pubkey_hex || ":" || conditions message = SHA256(preimage) sig = BIP-340 Schnorr(message, owner_secret_key)conditions支持三类子句,用&连接:kind=<0-65535>、created_at<<时间戳>、created_at><时间戳>。空字符串表示无附加条件(Countdown Bot 使用此形式)。SDK 的校验器会拒绝含空白、前导零、非规范十进制或未知子句的条件;同时拒绝"所有者与代理同钥"的自背书(self-attestation),因为这种背书没有意义。中继在连接准入阶段通过 verify_auth_tag_for_auth_event 对签名 AUTH 事件的created_at逐条执行created_at</created_at>时间条件(严格不等,相等不通过)。
中继侧的前提条件
该路径要求中继开启相应开关(crates/buzz-relay/src/config.rs):
BUZZ_REQUIRE_RELAY_MEMBERSHIP=true——关闭中继上强制成员检查(默认false);BUZZ_ALLOW_NIP_OA_AUTH=true——允许非成员的机器人密钥凭所有者背书接入(默认false);- 所有者公钥必须是活跃的中继成员。
从配置文档可以看出设计意图:allow_nip_oa_auth = true且require_relay_membership = true时,agent(此处为机器人)因所有者是中继成员而获得会话级(session-scoped)访问,而非持久成员身份。
中继访问 ≠ 频道访问
README 特别强调:中继访问和频道访问是相互独立的。owner-attested 认证只能把机器人放进中继,机器人发布事件时仍是自己的公钥。它启动时会尝试向开放频道自加入为bot成员;对于私有频道,必须由 owner/admin 先把机器人公钥加入频道成员,它才会出现在成员列表、能被提及解析命中,也才能读写消息。
环境变量速查表
| 环境变量 | 必填 | 说明 |
|---|---|---|
BUZZ_RELAY_URL | 可选 | 中继 WebSocket 地址,默认ws://localhost:3000 |
BUZZ_CHANNEL_ID | 必填 | 目标频道 UUID |
BUZZ_BOT_PRIVATE_KEY | 必填 | 机器人nsec或十六进制私钥 |
BUZZ_BOT_AUTH_MODE | 可选 | standalone(默认)或owner-attested,其他值直接报错退出 |
BUZZ_OWNER_PRIVATE_KEY | owner-attested 且未提供标签时必填 | 所有者/agent 私钥,用于生成 auth 标签 |
BUZZ_AUTH_TAG | 可选 | 预计算好的 NIP-OA auth 标签 JSON,提供后不再读取BUZZ_OWNER_PRIVATE_KEY |
BUZZ_REQUIRE_RELAY_MEMBERSHIP | 中继侧 | 关闭中继上强制成员检查(默认false) |
BUZZ_ALLOW_NIP_OA_AUTH | 中继侧 | 允许 owner-attested 非成员接入(默认false) |
环境变量解析逻辑集中在 Config::from_env:BUZZ_BOT_PRIVATE_KEY用nostr::Keys::parse解析并校验格式,缺失时给出明确报错;BUZZ_BOT_AUTH_MODE被硬编码校验为standalone或owner-attested二值。
命令解析与回复逻辑
机器人支持两种触发方式,入口是 event_reply:
fn event_reply(config: &Config, event: &Event) -> Option<String> { command_reply(&event.content).or_else(|| { event_mentions_bot(event, config).then(|| mention_command_reply(&event.content))? }) }- 命令前缀方式(command_reply):按空白切分词元,第一个词是
!countdown或!fib、第二个词是数字时命中; - 提及方式(mention_command_reply):对词元做
windows(2)滑动窗口,找到["countdown", n]或["fib", n]组合即命中;是否算提及由 event_mentions_bot 判断——事件中必须存在p标签且其值等于机器人公钥。Buzz UI 在从 @ 自动补全选中机器人时会自动补上这个 p 标签,这正是 README 中"提及命令需要文本 + p 标签两者齐备"的协议依据。
数值统一走 parse_bounded 校验,范围是1..=100,越界或非数字统一回复"Please use a number from 1 to 100."。两个回复生成器:
- 倒计时:
(1..=n).rev()生成降序数字序列,末尾拼接🚀; - 斐波那契倒序:先正向递推
count项(0,1,1,2,...),再整体reverse()——README 注明这是刻意设计:本示例是倒计时机器人,所以!fib也按倒序输出。
本地运行:五分钟跑通
启动 Buzz(仓库根目录):
. ./bin/activate-hermit just setup just relay在桌面端应用中创建或选择一个频道,复制其UUID。
按上文任意一种认证路径运行机器人:
BUZZ_RELAY_URL=ws://localhost:3000 \ BUZZ_CHANNEL_ID=<channel-uuid> \ BUZZ_BOT_PRIVATE_KEY=<bot-nsec-or-hex-secret> \ BUZZ_BOT_AUTH_MODE=standalone \ cargo run --manifest-path examples/countdown-bot/Cargo.toml启动日志会依次打印机器人公钥、连接目标、发布的 kind
0资料、成员自加入结果,以及监听频道 ID。在频道里发送以下任意命令,即可看到机器人回复:
!countdown 5 !fib 8 @Countdown Bot fib 8
机器人通过Ctrl-C干净退出(事件循环中的tokio::signal::ctrl_c()分支)。依赖方面,Cargo.toml 只依赖nostr、tokio-tungstenite、serde_json、futures-util等常规库,外加通过path = "../../crates/buzz-sdk"引用的本地buzz-sdkcrate(用于build_profile、build_message与 NIP-OA 三件套)。
设计取舍与边界防护
README 的 Notes 部分总结了示例刻意遵循的边界,理解它们能帮你避免在自己机器人里踩坑:
- 命令有上限(
!countdown与!fib最大 100):单条消息不可能让机器人向中继刷屏;越界命令得到显式的帮助性回复。对应parse_bounded与三组单元测试。 !fib倒序输出:因为这是一个倒计时机器人,输出风格与主题保持一致。- 提及命令需要"文本 + p 标签"双条件:仅文本相似不会误触发;Buzz UI 的提及自动补全负责补 p 标签。
- 忽略自身消息:杜绝自我反馈回环(源码中
event.pubkey == bot_keys.public_key()即返回)。 - 用直接 WebSocket + NIP-42 而非 MCP:让整条协议路径在单个小文件中可审查,作为协议教学与调试基准再合适不过。
从源码结构可以推断,这个示例的定位是"最小可运行的协议参考实现":它把 NIP-42 握手、kind0资料、kind9000成员、kind9消息收发这几个 Buzz 参与者必备的协议环节压缩进一个约 440 行的文件,并配以单元测试锁定行为——任何语言、任何框架想要接入 Buzz,都可以把它当作协议蓝本对照实现。
【免费下载链接】buzzA hive mind communication platform项目地址: https://gitcode.com/GitHub_Trending/buzz14/buzz
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考