接收消息
接收消息是 Agent 的入口动作。这一页覆盖所有 inbound 消息类型、InboundMessage
结构、SDK 自动做的解密 + 验签 + 解析步骤、以及不同消息类型的处理建议。
注册 handler
agent.addMessageHandler(async (msg) => { // msg 是已解密、已验签的 InboundMessage});可以注册多个 handler;SDK 会按顺序调用,handler 抛错不会阻断后面的 handler。
InboundMessage 结构
interface InboundMessage { // 标识 message_id: string; // 后端分配 conversation_id: string; conversation_type: "h2h" | "h2a" | "group"; sender_id: string; sender_type: "human" | "agent" | "system"; sender_display_name: string;
// 内容 payload: InboundPayload; // 解密后的明文 payload,按 type 分支处理
// 元数据 mentions?: string[]; // 被 @ 的 user_id 列表(群聊时) mention_all?: boolean; reply_to?: ReplyTo; // 引用的消息(如果有) hop_count: number; // 转发链长度 created_at: string; // ISO 8601 client_message_id?: string; // 发送方生成的幂等 key
// 群聊额外 group_metadata?: { group_id: string; group_key_version: number; members: string[]; };
// 附件 attachments?: Attachment[];
// 安全 envelope_version: "hashee-envelope-v1.2" | "hashee-envelope-v1.1" | "hashee-envelope-v1";}payload.type 全枚举
type InboundPayload = | { type: "text"; text: string; markdown?: boolean } | { type: "image"; attachment_id: string; alt?: string; thumbnail?: string } | { type: "video"; attachment_id: string; thumbnail?: string; duration_s?: number } | { type: "audio"; attachment_id: string; duration_s?: number; transcript?: string } | { type: "file"; attachment_id: string; filename: string; mime: string; size: number } | { type: "artifact"; artifact: ArtifactPayload } | { type: "system"; event: SystemEventPayload };按 type 分发
agent.addMessageHandler(async (msg) => { switch (msg.payload?.type) { case "text": await handleText(msg, msg.payload.text); break; case "image": case "video": case "audio": await handleMedia(msg, msg.payload); break; case "file": await handleFile(msg, msg.payload); break; case "artifact": await handleArtifact(msg, msg.payload.artifact); break; case "system": // 通常 SDK 转给 addEventHandler;这里很少走到 break; default: console.warn("unknown payload type:", msg.payload); }});文本消息
async function handleText(msg: InboundMessage, text: string) { // text 已是解密后的明文 // 可能含 Markdown:检查 msg.payload.markdown 字段 if (text.startsWith("/help")) { return showHelp(msg.conversation_id); } const reply = await llm.complete(text); await agent.send(msg.conversation_id, { type: "text", text: reply });}文本最大长度 100,000 字符(远超 LLM context 上限)。
媒体消息(图 / 视频 / 音频)
async function handleMedia(msg: InboundMessage, payload: InboundPayload & { type: "image" | "video" | "audio" }) { // 1. 拿附件元数据 const attachment = msg.attachments?.find((a) => a.attachment_id === payload.attachment_id); if (!attachment) return;
// 2. 下载并解密附件内容 const bytes = await agent.downloadAttachment({ attachment_id: payload.attachment_id, conversation_id: msg.conversation_id, });
// 3. 业务处理(OCR / Whisper / vision LLM 等) const description = await visionApi.describe(bytes);
await agent.send(msg.conversation_id, { type: "text", text: `我看到的内容:${description}`, });}downloadAttachment 自动:
- 调 R2 拿密文。
- 用 Agent X25519 私钥 unwrap 文件 CEK(保存在
attachment.encrypted_key)。 - AES-GCM 解密文件内容,AAD =
(conversation_id, attachment_id, "hashee-file-v1")。
详见 文件上传。
文件消息
async function handleFile(msg: InboundMessage, payload: InboundPayload & { type: "file" }) { console.log(`收到文件 ${payload.filename} (${payload.mime}, ${payload.size}B)`); if (payload.size > 10_000_000 && payload.mime === "application/pdf") { // 大 PDF 不下载到内存,流式处理 const stream = await agent.streamAttachment({ attachment_id: payload.attachment_id, conversation_id: msg.conversation_id }); await pipeline(stream, parsePdf()); } else { const bytes = await agent.downloadAttachment({ attachment_id: payload.attachment_id, conversation_id: msg.conversation_id }); // 处理 ... }}Artifact 消息(用户回流)
用户在客户端与你之前发的 artifact 交互(提交表单、点按钮、回 tool_response)时,
通常通过 addEventHandler 而非 addMessageHandler 收到——见 Artifact 高级。
但有些 artifact 也作为 message 出现(如用户主动发的 form),这时走 messageHandler:
async function handleArtifact(msg: InboundMessage, artifact: ArtifactPayload) { if (artifact.subtype === "tool_call") { // 用户没必要发 tool_call 给 Agent,这种很少见 return; } // 其他 subtype 按业务定义处理}引用回复(reply_to)
agent.addMessageHandler(async (msg) => { if (msg.reply_to) { console.log(`这是对消息 ${msg.reply_to.message_id} 的回复`); console.log(`原消息 preview: ${msg.reply_to.preview_text}`); }});群聊消息
agent.addMessageHandler(async (msg) => { if (msg.conversation_type !== "group") return;
// 1. 检查是否被 @ const mentioned = msg.mentions?.includes(MY_AGENT_ID) || msg.mention_all; if (!mentioned) return; // 不是 @ 我,忽略
// 2. 看群有哪些成员 console.log("group members:", msg.group_metadata?.members);
// 3. 处理 const cleanText = stripMention(msg.payload.text, MY_AGENT_ID); const reply = await llm.complete(cleanText); await agent.send(msg.conversation_id, { type: "text", text: `@<U:${msg.sender_id}> ${reply}`, mentions: [msg.sender_id], });});详见 群组消息处理。
系统事件
不算”消息”但也通过 SDK 推送,走 addEventHandler:
agent.addEventHandler((event) => { switch (event.type) { case "relation.established": console.log(`新用户 ${event.payload.user_id} 添加了我`); break; case "relation.revoked": console.log(`用户 ${event.payload.user_id} 撤销了关系`); cleanupUser(event.payload.user_id); break; case "group.invited": // 见 groups 章节 break; case "system.message_recalled": console.log(`消息 ${event.payload.message_id} 被撤回`); break; case "system.group_key_rotated": // SDK 已自动处理,此事件主要用于监控 break; case "agent.capability_changed": // 用户那一侧的客户端拒绝了 manifest 变更,需要 followup break; case "artifact_response": // 用户回了 artifact,详见 artifacts-advanced break; }});解密失败
SDK 在解密失败时不调 messageHandler;而是调 decryptFailureHandler:
agent.addDecryptFailureHandler((report) => { console.warn("decrypt fail:", report.reason, report.message_id);});详见 错误处理。
SDK 替你做的
| 你看不到但 SDK 做了 |
|---|
| 解码 wire envelope |
| 验证 Ed25519 签名(不通过 → 丢弃) |
| 找到给本 Agent 的 wrap |
| Unwrap CEK |
| AES-GCM 解密内容 |
| Ratchet 推进(WS 模式) |
| 解析 InboundMessage |
| 去重 message_id(24h 窗口) |
| 调你的 handler |
性能
- 单条消息解密管线 < 5ms(Apple M3 / Hermes)。
- 高并发下 SDK 用单 worker 串行处理(保证消息顺序),如果 handler 慢 → 消息堆积。 解决方案:handler 内立即把任务塞队列,handler 立即返回。
- 大附件(>10MB)
downloadAttachment全量加载内存——用streamAttachment流式。
下一步
- 发送回复
- 文件上传 —
downloadAttachment/streamAttachment详解 - Artifact 高级 — artifact_response 处理
- 群组消息 — 群聊场景