跳转到内容

接收消息

接收消息是 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 自动:

  1. 调 R2 拿密文。
  2. 用 Agent X25519 私钥 unwrap 文件 CEK(保存在 attachment.encrypted_key)。
  3. 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 流式。

下一步