跳转到内容

群里 @mention 路由

群聊里 Agent 不能”自言自语”——只能在被 @ 时回复(否则刷屏)。但有时候 “被 @” 需要细分:是问问题?是命令?是从群转到私聊处理?

4 种典型路由场景

场景触发Agent 做
群里普通 @@Bot 帮我查一下 X在群里回复
群里命令@Bot /help@Bot /vote ...群里回复或开 artifact
群里转私聊@Bot 这事我们私聊吧在群里说”好的,私聊见”+ 主动开私聊 H2A
群主 / admin 操作@Bot 把群规则设为 X(仅 admin 可触发)校验权限后操作

完整代码

import { HasheeAgent } from "@hasheeai/agent-sdk-ts";
const agent = await HasheeAgent.init({
agentId: process.env.HASHEE_AGENT_ID!,
token: process.env.HASHEE_AGENT_TOKEN!,
baseUrl: "https://api.hashee.ai",
connectionMode: "websocket",
});
const MY_ID = process.env.HASHEE_AGENT_ID!;
agent.addMessageHandler(async (msg) => {
if (msg.payload?.type !== "text") return;
// === H2A 私聊:直接处理 ===
if (msg.conversation_type === "h2a") {
return handleDM(msg);
}
// === 群里:检查是否被 @ ===
if (msg.conversation_type !== "group") return;
const myMentioned = msg.mentions?.includes(MY_ID);
const mentionAll = msg.mention_all;
if (!myMentioned && !mentionAll) return; // 不是 @ 我,忽略
const text = stripMention(msg.payload.text, MY_ID);
// === 命令? ===
if (text.startsWith("/")) {
return handleCommand(msg, text);
}
// === 转私聊关键词? ===
if (/(私聊|私下|DM)/.test(text)) {
return moveToDM(msg, text);
}
// === 普通问答 ===
return handleGroupQuestion(msg, text);
});
// === 1. 群里命令 ===
async function handleCommand(msg: any, text: string) {
const [cmd, ...args] = text.slice(1).split(/\s+/);
// /admin-only 系列
if (cmd === "set-rule") {
const isAdmin = await checkAdmin(msg.conversation_id, msg.sender_id);
if (!isAdmin) {
await agent.send(msg.conversation_id, {
type: "text",
text: `@<U:${msg.sender_id}> 抱歉,只有群主 / 管理员能改群规则。`,
mentions: [msg.sender_id],
});
return;
}
await setRule(msg.conversation_id, args.join(" "));
return;
}
// /help
if (cmd === "help") {
await agent.sendArtifact(msg.conversation_id, {
artifact: {
subtype: "info",
title: "Agent 命令",
payload: {
body: `- \`/help\` — 显示本帮助
- \`/vote 标题 选项1 选项2\` — 发起投票
- \`/summary\` — 上周群讨论摘要
- \`/set-rule <文本>\` — 设置群规则(仅 admin)`,
},
},
});
return;
}
// 兜底
await agent.send(msg.conversation_id, {
type: "text",
text: `@<U:${msg.sender_id}> 不认识命令 \`/${cmd}\`,输入 \`@${MY_ID} /help\` 看用法`,
mentions: [msg.sender_id],
});
}
// === 2. 群里转私聊 ===
async function moveToDM(msg: any, text: string) {
// 1. 群里礼貌回应
await agent.send(msg.conversation_id, {
type: "text",
text: `@<U:${msg.sender_id}> 好的,我私聊给你。`,
mentions: [msg.sender_id],
});
// 2. 主动开 H2A 私聊(如果该 user 还没建立和我的 H2A 关系,需先等他加我为联系人)
// V1:找到该 user 与我的 H2A conversation_id(如已存在)
const dmConvId = await lookupOrCreateDmConv(msg.sender_id);
if (!dmConvId) {
// user 还没加我;提示他加联系人
await agent.send(msg.conversation_id, {
type: "text",
text: `@<U:${msg.sender_id}> 你先在搜索栏添加我为联系人,我才能给你发私聊。`,
mentions: [msg.sender_id],
});
return;
}
// 3. 发首条私聊消息
await agent.send(dmConvId, {
type: "text",
text: `刚才你在群里说"${text.slice(0, 100)}"——我们这里继续聊吧。`,
});
}
async function lookupOrCreateDmConv(userId: string): Promise<string | null> {
// 业务侧:从 relation.established 事件持久化 user_id → conversation_id 映射
// 不在则返回 null(user 没和你建 H2A)
return null; // 略:替换为真实查询
}
// === 3. 群里普通问答 ===
async function handleGroupQuestion(msg: any, text: string) {
await agent.typing(msg.conversation_id);
const reply = await callLLM(text);
await agent.send(msg.conversation_id, {
type: "text",
text: `@<U:${msg.sender_id}> ${reply}`,
mentions: [msg.sender_id],
});
}
// === 4. H2A 私聊 ===
async function handleDM(msg: any) {
const reply = await callLLM(msg.payload.text);
await agent.send(msg.conversation_id, { type: "text", text: reply });
}
// === 工具 ===
function stripMention(text: string, agentId: string): string {
return text.replace(new RegExp(`@<U:${agentId}>\\s?`, "g"), "").trim();
}
async function checkAdmin(groupId: string, userId: string): Promise<boolean> {
// 业务侧:从 group.member_joined 事件 + role 字段持久化
return false; // 略
}
async function setRule(groupId: string, rule: string) {
// 业务侧操作
}
async function callLLM(text: string): Promise<string> {
// 替换为真实 LLM 调用
return `Echo: ${text}`;
}
agent.addStatusHandler((s) => console.log(`[status] ${s}`));
console.log("[group-router] up");

群事件订阅

agent.addEventHandler(async (event) => {
switch (event.type) {
case "group.member_joined":
// 业务侧:欢迎新成员
const { group_id, user_id } = event.payload;
await agent.send(group_id, {
type: "text",
text: `欢迎 @<U:${user_id}>!发 \`@${MY_ID} /help\` 看我能做什么。`,
mentions: [user_id],
});
break;
case "group.member_left":
// 清理该 user 在本群的业务数据
break;
case "group.dismissed":
// 群被解散,清理整个 group_id 的数据
break;
}
});

V1 限制 + 注意事项

  • 群里 tool_call 禁用(spec 限制) — 高敏感工具调用必须转私聊
  • 流式输出在大群(>200 成员)禁用 — 用一次性 send 替代
  • 撤回窗口 1 分钟(比私聊 2 分钟短)
  • @mention 必须用 @<U:user_id> 占位符 + mentions 数组双重声明
  • 不响应 mention_all (@everyone) 除非真有 broadcast 用途 — 否则刷屏

升级路径

想做改哪
接 group key 轮换通知监听 system.group_key_rotated(SDK 透明处理,业务通常无需感知)
跨群协调(如汇总 5 个项目群周报到管理员群)业务侧维护”群分组”映射 + 跨群 send
群成员投票决定群规则团队投票卡 配方
多个 Agent 协作(如 PM bot + Tech bot 在同一群)各 Agent 自己识别 @;用不同 agent_id;不要共用

相关页面