群组消息处理
Hashee 群聊(H2G)是 Agent 与多个用户在同一会话里交互的场景。
群聊有自己的加密协议(hashee-group-v1.2,独立于 Layer 4 Ratchet)和路由规则。
这一页讲 SDK 在群里的行为差异、@mention 的工作方式、以及 V1 当前的 tool call 限制。
技术真源:packages/public/protocol/src/aad.ts 中 GROUP_E2E_VERSION_V1_2,
和 .claude/rules/security-e2ee.md §3.1 中 hashee-group-v1.2 AAD 字段。
群聊加密 vs 一对一
| 维度 | H2A / H2H 一对一 | H2G 群聊 |
|---|---|---|
| 加密协议 | Layer 1-5 完整栈 | hashee-group-v1.2 (Layer 1 + per-recipient wrap) |
| Forward secrecy | ✓ Double Ratchet | ✗(用 group key 轮换替代) |
| Per-message CEK | ✓ | ✗ 用 group key 直接加密 |
| Per-recipient wrap | ✓ wrap CEK | ✓ wrap group key(在 keyVersion 变更时) |
| Tool call | ✓ 完整支持 | ✗ V1 禁用(tool_not_supported_in_group) |
群聊加密的核心是 group key:一个 256-bit DEK,所有成员共享,
用每个成员的 X25519 公钥分别 wrap。AAD 绑定 (conversation_id, group_key_version, "hashee-group-v1.2"),
保证密文不能跨群、不能跨 key version 重放。
SDK 接收群消息
agent.addMessageHandler(async (msg) => { if (msg.conversation_type === "group") { console.log(`[group] from ${msg.sender_id} in conv ${msg.conversation_id}: ${msg.payload?.text}`); handleGroupMessage(msg); } else { handleDirectMessage(msg); }});InboundMessage 在群消息时多两个字段:
conversation_type: "group"group_metadata:含group_id,group_key_version,members[](成员 user_id 列表)
@Mention 路由
群里 Agent 通常只响应被 @ 的消息——这是 UX 默认期望,避免 Agent 在群里”自言自语”。
agent.addMessageHandler(async (msg) => { if (msg.conversation_type !== "group") return; if (!msg.payload?.text) return;
const mentioned = msg.payload.mentions?.includes(MY_AGENT_ID); if (!mentioned) return; // 不是 @ 我,忽略
// 去掉 mention 占位符再喂给 LLM 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], });});
function stripMention(text: string, agentId: string) { return text.replace(new RegExp(`@<U:${agentId}>\\s?`, "g"), "").trim();}主动给群发消息
await agent.send(groupConversationId, { type: "text", text: "🚨 系统告警:API 错误率上升 @<U:01906abc-...> @<U:01906def-...>", mentions: ["01906abc-...", "01906def-..."],});被 @ 的成员会收到额外推送通知(即使群被设置为”消息免打扰”)。
Group Key 轮换
Group key 在以下情况自动轮换(由后端协调,SDK 透明处理):
| 触发 | 行为 |
|---|---|
| 成员加入 | 不立即轮换(防止”加群即知道历史明文”是预期行为) |
| 成员退出 / 被踢 | 立即轮换(被踢成员不应该看到后续消息) |
| 管理员手动触发 | 立即轮换(怀疑泄露场景) |
| 长时间未轮换(默认 90 天) | 自动轮换 |
轮换流程:
- 任一在线客户端(包括 Agent)生成新 group key v(n+1)。
- 用每个剩余成员的 X25519 公钥 wrap 新 key。
- 通过
system.group_key_rotated事件广播给所有成员。 - 后续消息用新 key 加密,AAD 中带
keyVersion: n+1。
SDK 自动监听 system.group_key_rotated、解 wrap、缓存新 key。业务代码无感知。
Group Key 备份
如果 Agent 在群 key 轮换时离线,下次上线 SDK 会主动 fetch wrap 列表:
SDK 上线 → status: connected ↓ GET /agents/:id/groups/:gid/key-wraps?since=<last_seen_version> ↓ 后端返回 [{version: n+1, wrap}, ...] ↓ SDK 用 X25519 私钥逐一 unwrap → 缓存 → 解最新历史消息群里禁用的能力
- Tool call:sendArtifact
subtype: "tool_call"在群里返回denied: tool_not_supported_in_group。原因:多人在场广播 tool prompt 会破坏隐私。 - Direct stream:
createStreamSession在群里行为同 send(一次性而非分帧推送), 因为群成员加载顺序不可控,UX 上”边输入边渲染”对其他人是干扰。 - Recall after 2min:群里撤回窗口缩短到 1 分钟(默认)。
入群 / 离群事件
agent.addEventHandler((event) => { if (event.type === "group.member_joined") { const { group_id, user_id } = event.payload; console.log(`[group ${group_id}] new member: ${user_id}`); // 业务侧:欢迎消息?同步用户档案? } if (event.type === "group.member_left") { const { group_id, user_id, reason } = event.payload; // reason: "self_leave" | "kicked" | "deleted" } if (event.type === "group.dismissed") { const { group_id } = event.payload; // 群被解散,清理本地相关数据 }});Agent 加群 / 退群
Agent 不能”主动加群”——必须群成员邀请。当用户在群里”邀请 Agent”,
Agent 会收到 group.invited 事件 + 一个 group.join_token:
agent.addEventHandler(async (event) => { if (event.type !== "group.invited") return; const { group_id, inviter_user_id, join_token } = event.payload;
// 业务侧:决定是否接受 (基于策略,如"只接受 owner 邀请") const accept = await myPolicy.shouldAcceptGroupInvite(inviter_user_id, group_id); if (accept) { await agent.acceptGroupInvite(join_token); } else { await agent.declineGroupInvite(join_token, "policy"); }});主动退群:
await agent.leaveGroup(groupConversationId);退群会触发后端 group key 轮换(针对剩余成员)。
群消息上限
| 项 | 上限 |
|---|---|
| 单群最大成员 | 500(含 Agent) |
| 一个 Agent 加入的群数 | 50 |
| 群历史消息查询 | 一次最多 100 条 |
演示视频
视频文字版逐节描述
- 00:00 – 00:10 — Hashee app 显示一个群”研发讨论”含 4 个成员:用户 A、B、C 和 DemoBot。群内有几条历史消息。
- 00:10 – 00:25 — 用户 A 发”今天天气怎么样”——群里没有 @ Agent,DemoBot 终端 日志显示”inbound but not mentioned, skip”,Hashee app 中 Agent 没回复。
- 00:25 – 00:40 — 用户 B 发 “@DemoBot 帮我查下 https://example.com”。
DemoBot 终端日志
mentioned by user B; processing。Agent 调 LLM → 回复"@<U:user_b> 这个网站...";Hashee app 中可见 user B 头像旁的 mention 高亮。 - 00:40 – 00:55 — 用户 C 加入群(Owner 触发):DemoBot 收到
group.member_joined事件,但 group key 不立即轮换;后续消息正常解密。 - 00:55 – 01:10 — 用户 B 被踢:DemoBot 收到
group.member_left reason=kicked, 紧接system.group_key_rotated keyVersion: 2。SDK 透明处理;下条消息用 v2 加密+解密成功。 - 01:10 – 01:25 — 演示 tool call 限制:Agent 试图 sendArtifact tool_call
到群里,立即收到
denied: tool_not_supported_in_group。Agent 改为发文本提示 用户”请打开私聊执行此操作”。