Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/parsar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Optional business questions use the declared `ask_user` function. They appear in

1. On the card, choose the **Agent**, turn on **私聊** and/or **群聊**, then **保存并启用**. Enabling requires credentials, an Agent with an environment template and at least one of the two. With **群聊** on, the bot answers mentions in every group it has joined; adding the bot to a group is all that is needed. Who can add the bot is controlled by the app’s availability in the developer console.
2. In the developer console, enable the bot, long-connection event delivery and `im.message.receive_v1`, publish the app and add it to the target groups. Grant mention receipt and send-as-bot permissions; direct messages, message resources and reactions need their own scopes. A missing reaction permission does not discard a message.
3. Private chat keeps one conversation per sender until `/clear`. In groups, mentioning the bot starts a topic and each topic is one conversation shared by everyone who replies in it. Follow-ups without a mention arrive only when the app has the “read all group messages” scope (`im:message.group_msg`). **扫码创建** requests it; for **已有应用**, enable it in the Feishu developer console. A topic reply that mentions only other people is addressed to them and is ignored. The bot serves its own tenant only: messages from external contacts are ignored. Other bots and `@all` are ignored, as are groups while **群聊** is off and private chats while **私聊** is off. Changing the bound Agent affects new topics and new private conversations.
3. Private chat keeps one conversation per sender until `/clear`. In groups, mentioning the bot starts a topic and each topic is one conversation shared by its participants: whoever mentioned the bot in the topic is answered there without mentioning it again, others only when they mention it, and a reply that mentions only other people is ignored. Follow-ups without a mention arrive only when the app has the “read all group messages” scope (`im:message.group_msg`). **扫码创建** requests it; for **已有应用**, enable it in the Feishu developer console. A topic reply that mentions only other people is addressed to them and is ignored. The bot serves its own tenant only: messages from external contacts are ignored. Other bots and `@all` are ignored, as are groups while **群聊** is off and private chats while **私聊** is off. Changing the bound Agent affects new topics and new private conversations.
4. Text, rich posts, images, files, audio and video, quoted messages and merged forwards are normalized before dispatch. Attachment limits are eight resources and 50 MiB total; files reach the workspace through Core's Files API. Follow-up files wait until the conversation is idle. Quoted messages are read up to four levels; earlier group messages are not read.
5. Commands: `/clear` clears the context: it stops any running work and the next message starts a new conversation, in a private chat or in the same group topic. `/stop` stops the current task; it takes priority over queued follow-ups and recovers an uncertain creation before stopping it. `/answer <call_id> <text>` answers a pending business question, and a plain reply answers when only one is pending. `/help` lists the commands.

Expand Down
4 changes: 2 additions & 2 deletions example/parsar/README.zh.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: example/parsar/README.md
source_hash: 0a4d6a1ee7fb588c82d9f0accd102cdf16e8cd4f3306f54a3417640808032796
source_hash: f0705df7d832d4eb66c03ce1c80fb626f9befbc4af7d96715ebb1a81b5314814
---

# Parsar 工作空间
Expand Down Expand Up @@ -130,7 +130,7 @@ Parsar 会跟踪每一轮正在执行的对话,网页和飞书都一样。出

1. 在卡片上选择 **Agent**,打开 **私聊** 和/或 **群聊**,然后 **保存并启用**。启用需要应用凭证、已配置环境模板的 Agent,以及两者至少开启一项。打开 **群聊** 后,机器人会回复它所在的任何群里的 @ 消息,把机器人拉进群即可使用。谁能添加机器人由开发者后台中应用的可用范围控制。
2. 在开发者后台启用机器人、长连接事件推送和 `im.message.receive_v1`,发布应用并把它加入目标群。授予接收 @ 消息和以机器人身份发送消息的权限;私聊、消息资源和表情回复需要各自的权限。缺少表情权限不会丢弃消息。
3. 私聊为每个发送者保持一个对话,直到发送 `/clear`。群聊中 @ 机器人会开启一个话题,每个话题是一个对话,所有在其中回复的人共享。只有应用开通了“获取群组中所有消息”权限(`im:message.group_msg`),话题中不带 @ 的后续消息才会推送给机器人。**扫码创建** 会申请这个权限;**已有应用** 需要在飞书开发者后台开通。话题中只 @ 了其他人的回复是发给他们的,会被忽略。其他机器人和 机器人只服务自己所在的企业,外部联系人发来的消息会被忽略。`@所有人` 会被忽略;关闭 **群聊** 时忽略群消息,关闭 **私聊** 时忽略私聊。更换绑定的 Agent 只影响新话题和新的私聊对话。
3. 私聊为每个发送者保持一个对话,直到发送 `/clear`。群聊中 @ 机器人会开启一个话题,每个话题是一个对话,由参与者共享:在话题里 @ 过机器人的人,之后不 @ 它也会得到回复;其他人要 @ 它才会被理会;只 @ 了别人的回复会被忽略。只有应用开通了“获取群组中所有消息”权限(`im:message.group_msg`),话题中不带 @ 的后续消息才会推送给机器人。**扫码创建** 会申请这个权限;**已有应用** 需要在飞书开发者后台开通。话题中只 @ 了其他人的回复是发给他们的,会被忽略。其他机器人和 机器人只服务自己所在的企业,外部联系人发来的消息会被忽略。`@所有人` 会被忽略;关闭 **群聊** 时忽略群消息,关闭 **私聊** 时忽略私聊。更换绑定的 Agent 只影响新话题和新的私聊对话。
4. 文本、富文本、图片、文件、音视频、引用消息和合并转发会在投递前规范化。附件上限为 8 个、合计 50 MiB,文件通过 Core 的 Files API 写入工作区。后续消息中的文件会等到对话空闲后写入。引用消息最多读取四层;不读取更早的群消息。
5. 命令:`/clear` 清空上下文:停止正在执行的任务,下一条消息开始新对话,私聊和群聊话题都适用(群聊仍在同一个话题里)。`/stop` 停止当前任务,优先于排队中的追问,并会先确认不确定的创建再停止。`/answer <call_id> <回答>` 回答待处理的业务问题;只有一个待回答的问题时,直接回复即可。`/help` 列出这些命令。

Expand Down
17 changes: 14 additions & 3 deletions example/parsar/server/feishu.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -724,14 +724,19 @@ export function feishuWorker(store, core, product, makeChannel, configId) {
out.sent_message_id === message.replyToMessageId,
)),
);
// A topic's participants are the people who mentioned the bot in it.
const participates = (thread, senderId) =>
thread.sender_id === senderId || (thread.participants || []).includes(senderId);
const receive = (appId, message) => {
const topic = message.chatType === "group" && threadFor(appId, message);
if (
!allowed(appId, message.chatType) ||
!["group", "p2p"].includes(message.chatType) ||
(message.chatType === "group" &&
!message.mentionedBot &&
// A topic reply that mentions only other people is addressed to them.
(message.mentions?.length || !threadFor(appId, message))) ||
// Without a mention, only a participant's reply in the topic reaches the
// bot, and not one that mentions only other people.
(message.mentions?.length || !topic || !participates(topic, message.senderId))) ||
!message.senderId ||
message.mentionAll
)
Expand Down Expand Up @@ -774,7 +779,13 @@ export function feishuWorker(store, core, product, makeChannel, configId) {
thread?.id || stableId(`${appId}:${message.chatId}:${root}`);
store.transaction(() => {
if (thread)
store.put("feishu_threads", { ...thread, settled_at: undefined });
store.put("feishu_threads", {
...thread,
settled_at: undefined,
...(message.chatType === "group" && !participates(thread, message.senderId) && {
participants: [...(thread.participants || []), message.senderId],
}),
});
if (!thread)
store.put("feishu_threads", {
id: threadId,
Expand Down
21 changes: 20 additions & 1 deletion example/parsar/tests/feishu.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1310,7 +1310,8 @@ test("group topics are shared by their participants and private chats stay per s
messageId: "om_colleague",
rootId: "om_first",
senderId: "ou_colleague",
mentionedBot: false,
// A colleague joins the topic's conversation by mentioning the bot.
mentionedBot: true,
content: "Add the payment step",
});
await w.tick();
Expand Down Expand Up @@ -2336,3 +2337,21 @@ test("moving a steered Turn's card survives a failed edit and an unconfirmed sen
assert.ok(kept.moved);
assert.match(kept.payload, /已并入下方回复/);
});

test("in a group topic only people who mentioned the bot are answered without a mention", async (t) => {
const f = await feishu(t);
const worker = f.create();
const say = (messageId, senderId, extra = {}) =>
worker.receive("cli_0123456789abcdef", { ...f.message, messageId, senderId, rootId: "om_first", mentionedBot: false, content: messageId, ...extra });
const reached = (messageId) => f.store.list("feishu_inbound").some((row) => row.message_id === messageId);
worker.receive("cli_0123456789abcdef", f.message);
say("a_follow_up", "ou_user");
say("b_unmentioned", "ou_b");
say("a_to_b", "ou_user", { mentions: [{ id: "ou_b" }] });
assert.ok(reached("a_follow_up"), "the person who opened the topic keeps talking without a mention");
assert.ok(!reached("b_unmentioned"), "someone who never mentioned the bot is ignored");
assert.ok(!reached("a_to_b"), "a reply that mentions only someone else is ignored");
say("b_mentions", "ou_b", { mentionedBot: true });
say("b_follow_up", "ou_b");
assert.ok(reached("b_mentions") && reached("b_follow_up"), "mentioning the bot once makes B a participant");
});
Loading