
1. 一条事件日志为什么能同时装下人类、Agent 和 Git如果你正在做多主体协作系统大概率遇到过这个别扭的局面人类的消息走一套表Agent 的执行记录走另一套日志Git 提交又是第三套数据结构。三套东西各自有 ID、各自有权限、各自有审计等到要回答“这个改动到底是谁在什么时候基于什么上下文做的”时你得写三段查询再把结果拼起来拼完还不一定对得上。block/buzz 这个项目给出的答案很干脆别分三套全部塞进同一条签名事件流。人类发的消息、Agent 执行的动作、Git 的补丁提交在协议层面是同一种数据结构——Nostr 事件。区别只有一个签名用的密钥对不同。身份是密钥对权限是“你是不是这个频道的成员”审计是“这条事件有没有合法签名、有没有进哈希链”。这篇不聊它的产品定位只聚焦一件事这条签名事件日志到底长什么样字段怎么配怎么在本地把它解析出来并回放验证。适合已经在写 Agent 协作、事件溯源、或者多主体审计系统的同学。读完你应该能自己搭一个最小事件流把人类操作、Agent 行为、Git 提交统一编码进去并跑通验签和回放。我试过把这套结构套到一个内部小工具上最大的感受是一旦身份和事件统一了后面加任何新主体新的 Agent、CI 机器人、定时任务都只是“再生成一对密钥”不用改表结构也不用加权限位。2. 前置TaoToken 在这条链路里扮演什么角色要复现事件解析和回放你需要一个能签发密钥、能调用模型做事件语义解析的入口。TaoToken 在这里的作用是提供统一的 API 接入层你用它拿 API Key然后在本地脚本里调用模型把原始事件 JSON 翻译成可读的操作描述或者让 Agent 根据事件流决定下一步动作。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。注意区分官网带推广参数API 地址是纯接口地址写代码时用后者。具体到本篇的场景你会用到两类能力一类是模型对话用来做事件内容的语义解析和回放时的自然语言摘要入口在模型对话页面另一类是 API Key 管理所有请求都要带 Key入口在 API Keys 页面。如果你打算让 Agent 长期挂在事件流上做自动响应那更适合用 Coding Plan因为它是面向持续编码和 Agent 场景的订阅形态比按次调用更稳。接入文档在 doc 页面里面有完整的请求格式和鉴权说明。Claude Code 这类工具的用户可以看 ClaudeCodeAnthropic 对应的接入说明。控制台在 console 页面用来查看调用量和余额。一句话TaoToken 不参与事件签名本身签名是你本地密钥对的事它负责的是事件流的“语义层”——把机器可读的事件翻译成人类可读的操作以及让 Agent 基于事件流做决策。3. 可复制的事件字段配置骨架下面这套骨架是我实际跑通过的字段命名尽量贴近 Nostr 的 NIP-01 规范同时留了扩展位给 Agent 和 Git 场景。3.1 基础事件结构一条事件的核心字段就这几个顺序不能乱因为事件 ID 是对固定顺序的字段数组做哈希{ id: 事件内容的 SHA-256hex 编码, pubkey: 作者公钥hex 编码, created_at: 1730000000, kind: 9, tags: [ [e, 被回复事件 id], [p, 被提及人公钥], [t, agent-task] ], content: {\text\:\hello world\}, sig: 对 id 的 Schnorr 签名hex 编码 }几个关键点必须说清楚。id不是随便生成的 UUID而是对[0, pubkey, created_at, kind, tags, content]这个数组做规范化 JSON 序列化后取 SHA-256。sig不参与哈希它签名的是id。这意味着内容改一个字节id就变签名就失效。kind是功能分派的核心。1 是普通笔记7 是反应9 是群聊消息22242 是认证事件。buzz 自己占了 40000–49999 段做扩展比如工作流执行、任务请求、画布操作。你自定义新功能时选一个没被占用的 kind 就行旧客户端收到不认识的 kind 会忽略不会崩。3.2 人类操作、Agent 行为、Git 提交的编码差异三类主体共用同一结构差异体现在kind、tags和content的约定上主体类型典型 kindtags 约定content 约定人类消息9e/p 标记回复和提及文本或富文本 JSONAgent 行为40000t 标记任务类型e 关联触发事件结构化执行结果Git 提交NIP-34 段e 关联仓库公告p 关联审查人补丁元数据Git 这块走的是 NIP-34补丁集、仓库公告、状态变更都是签名事件。配好git-sign-nostr和git-credential-nostr之后git 操作用 nostr 密钥签名提 PR 等于发一条事件CI 结果等于回一条事件Agent 首轮审查又是另一条事件。合并决策和全部证据躺在同一个频道里可搜索、可验签。3.3 哈希链审计字段如果你要做防篡改审计在事件之外再加一条链记录每条链到前一条的哈希{ seq: 1024, ts: 1730000000, event_id: 对应事件 id, kind: 9, actor_pubkey: 操作者公钥, action: message.create, channel_id: 16 字节频道 id无则填 16 个零字节, metadata: {source: agent, task: review}, prev_hash: 前一条链哈希, hash: 本条链哈希 }哈希输入的确定性是重点数值统一大端字节序metadata用有序映射序列化保证键排序确定channel_id为空时填固定长度的零字节。任何一处不确定性都会让链在跨机器验证时断掉。创世条目用 64 个零作为prev_hash。4. 验证请求与成功结果配好字段之后下一步是跑通“提交事件 → 验签 → 回放”这条链路。4.1 本地生成密钥对并签名用 Python 快速验证签名逻辑依赖coincurve或secp256k1库import hashlib import json import time from coincurve import PrivateKey def canonical_event(pubkey, created_at, kind, tags, content): return json.dumps([0, pubkey, created_at, kind, tags, content], separators(,, :), ensure_asciiFalse) def compute_event_id(pubkey, created_at, kind, tags, content): serialized canonical_event(pubkey, created_at, kind, tags, content) return hashlib.sha256(serialized.encode(utf-8)).hexdigest() priv PrivateKey() pubkey priv.public_key.format(compressedTrue)[1:].hex() created_at int(time.time()) kind 9 tags [[t, agent-task]] content json.dumps({text: agent started review}, ensure_asciiFalse) event_id compute_event_id(pubkey, created_at, kind, tags, content) sig priv.sign_schnorr(bytes.fromhex(event_id)).hex() event { id: event_id, pubkey: pubkey, created_at: created_at, kind: kind, tags: tags, content: content, sig: sig } print(json.dumps(event, ensure_asciiFalse, indent2))跑完你会拿到一条完整事件。注意canonical_event里的separators参数它去掉了 JSON 序列化时的空格这是保证不同实现算出同一个id的关键。4.2 用 TaoToken 做事件语义解析拿到事件之后把content丢给模型做解析让它输出人类可读的操作描述。请求走 API 地址curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet, messages: [ {role: system, content: 你是事件流解析器把 JSON 事件翻译成一句操作描述。}, {role: user, content: {\kind\:9,\content\:\{\\\text\\\:\\\agent started review\\\}\,\tags\:[[\t\,\agent-task\]]}} ] }成功返回的choices[0].message.content应该是一句类似“Agent 在 agent-task 任务下开始执行审查”的描述。这一步的意义在于事件流本身是机器可读的但审计和回放需要人类可读模型在这里做的是翻译层。4.3 回放验证回放的核心是重算id并验签两步都对才认为事件合法def verify_event(event): recomputed compute_event_id( event[pubkey], event[created_at], event[kind], event[tags], event[content] ) if recomputed ! event[id]: return False, id mismatch pub PublicKey(bytes.fromhex(02 event[pubkey])) ok pub.verify_schnorr( bytes.fromhex(event[sig]), bytes.fromhex(event[id]) ) return ok, ok if ok else sig invalid回放时按created_at排序逐条验签再把kind分派到不同处理器9 走消息渲染40000 走 Agent 行为渲染NIP-34 段走 Git 操作渲染。这样一条事件流就能同时回放出人类对话、Agent 动作和代码变更三条时间线。5. 本篇常见错排查事件 ID 对不上。九成是 JSON 序列化不一致。检查separators是否去掉了空格检查ensure_ascii是否统一检查字段顺序是否严格是[0, pubkey, created_at, kind, tags, content]。少一个空格、多一个转义哈希就完全不同。验签失败但 ID 正确。检查公钥格式。Schnorr 验签用的是 x-only 公钥如果你拿的是压缩公钥33 字节带前缀要去掉第一个字节。签名是 64 字节不是 DER 编码。Agent 事件和人类事件混在一起分不清。别靠pubkey硬编码判断用tags里的标记位。约定一个[t, agent]或[t, human]回放时按 tag 分派。身份是密钥对但角色是标签两者解耦。Git 事件收不到。NIP-34 相关的事件类型在 buzz 里标注为正在接线如果你用的是早期版本Git 事件可能还没进主流程。先确认版本再检查git-credential-nostr是否配置正确。哈希链验证断在中间。检查metadata的序列化是否用了有序映射。Python 的dict在 3.7 之后保序但如果你从数据库读出来再序列化顺序可能变。用sort_keysTrue或者显式构造有序结构。调用模型返回 401。检查 API Key 是否带对了前缀检查请求地址用的是https://taotoken.net/api而不是官网地址。官网地址带推广参数不适合直接做接口调用。回放顺序错乱。created_at是秒级时间戳同一秒内多条事件会并列。加一个seq字段做次级排序或者用事件 ID 做稳定排序。6. 把这条事件流用起来如果你只想快速验证最小路径是本地生成密钥对 → 构造一条 kind 9 事件 → 重算 ID 验签 → 用 TaoToken 的模型对话接口把 content 翻译成描述。这条链路跑通你就理解了“统一事件日志”的核心。如果你要做长期 Agent 协作建议把 API Key 管理好用 API Keys 页面生成专用 Key然后按接入文档把 Agent 挂到事件流上。持续编码场景用 Coding Plan 更合适因为 Agent 需要长期在线响应事件按次调用在成本和稳定性上都不划算。真正值得带走的是那个结构判断当系统里混着人类和 Agent 两类主体时别给 Agent 开后门加特权让它们和人类共享同一套身份模型、同一个事件日志、同样的审计约束。权限问题退化成“这个密钥在不在成员表里”审计问题退化成“查日志加验签”。复杂度没有消失但被压缩进了三个正交的机制里后面加任何新主体都只是再生成一对密钥的事。