跳转到内容

群组消息处理

Hashee 群聊(H2G)是 Agent 与多个用户在同一会话里交互的场景。 群聊有自己的加密协议(hashee-group-v1.2,独立于 Layer 4 Ratchet)和路由规则。 这一页讲 SDK 在群里的行为差异、@mention 的工作方式、以及 V1 当前的 tool call 限制。

技术真源:packages/public/protocol/src/aad.tsGROUP_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 天)自动轮换

轮换流程:

  1. 任一在线客户端(包括 Agent)生成新 group key v(n+1)。
  2. 用每个剩余成员的 X25519 公钥 wrap 新 key。
  3. 通过 system.group_key_rotated 事件广播给所有成员。
  4. 后续消息用新 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 streamcreateStreamSession 在群里行为同 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 改为发文本提示 用户”请打开私聊执行此操作”。

下一步