Skip to content

Commit aec76ac

Browse files
lishuceoclaude
andauthored
fix: 多 bot 话题中 @ 某 bot 时其他 bot 不再抢答 (#262)
* fix: 话题创建者 bot 在 @ 他人时不再抢答(open_id 跨 app 隔离) 多 bot 共处一个话题时,用户 @ 某个 bot,其他 bot 也会响应。 根因:飞书 open_id 按 app 隔离,同一 bot 在不同 app 下 open_id 不同。 thread_bypass 的闸用 `!anyBotMentioned`(是否 @ 了已知 bot)判定,而 getAllBotOpenIds() 只存各 bot 用自己 app 拉到的「自视 open_id」。当用户 @ 了另一个 bot 时,本 bot 的 app 收到的那条 mention 是「对方在本 app 视角 下的 open_id」,不在 knownBotIds 里 → anyBotMentioned 漏判 → 话题创建者 bot 误以为没人被 @ 而走 bypass 抢答。 修复:改用「是否 @ 了非自己」作为不 bypass 的判据。每个 bot 唯一能可靠 识别的就是自己的 open_id —— 只要消息 @ 了除本 bot 外的任何对象,就说明在 叫别人,不 bypass。同时给单 bot 路径补上此前缺失的同款 mention 闸。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: 覆盖话题创建者 bot 在 @ 他人时不 bypass 抢答 - 多 bot: 创建者 bot 看到 @ 了跨 app open_id 未识别的对象 → 不 bypass - 单 bot: 创建者 bot 看到 @ 了另一独立服务 bot → 不 bypass 两个用例在修复前均会失败(走 thread_bypass 抢答),修复后通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 88bf472 commit aec76ac

2 files changed

Lines changed: 50 additions & 4 deletions

File tree

‎src/__tests__/mention-gate.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,16 @@ describe('resolveMentionGate', () => {
218218
it('allows non-group chat types without @mention', async () => {
219219
expect(await resolveMentionGate({ ...baseInput, chatType: 'supergroup' })).toBe('non_group');
220220
});
221+
222+
it('blocks thread bypass when another (separate-service) bot is @mentioned', async () => {
223+
// 单 bot 模式:DevBot 是话题创建者,但用户 @ 了群里另一个独立服务的 bot。
224+
// 旧逻辑单 bot 路径无 mention 闸 → bypass 放行抢答;修复后 @非自己 → 不 bypass。
225+
mockGetThreadSession.mockReturnValue({ userId: 'ou_user_1', createdAt: new Date().toISOString() });
226+
const otherBotMention = { id: { open_id: 'ou_separate_service_bot' } };
227+
expect(await resolveMentionGate({
228+
...baseInput, threadId: 'omt_123', mentions: [otherBotMention],
229+
})).toBeUndefined();
230+
});
221231
});
222232

223233
// ── 多 bot 模式 ──
@@ -321,6 +331,24 @@ describe('resolveMentionGate', () => {
321331
...baseInput, mentions: [otherBotMention], threadId: 'omt_existing_topic',
322332
})).toBeUndefined();
323333
});
334+
335+
it('blocks thread_bypass when @mentioned party is unrecognized as bot (cross-app open_id)', async () => {
336+
// 真实 bug 复现:本 bot(dev) 是话题创建者,用户在话题里 @ 了另一个 bot。
337+
// 飞书 open_id 按 app 隔离,被 @ 的 bot 在本 app 视角下的 open_id 不在 knownBotIds 里,
338+
// 旧逻辑 anyBotMentioned=false → 命中 thread_bypass 抢答。
339+
// 修复后:只要 @ 了"非自己",就不 bypass。
340+
mockGetThreadSession.mockImplementation((_: string, agentId?: string) => {
341+
if (agentId === 'dev') return { userId: 'ou_user_1', createdAt: '2026-01-01T00:00:00Z' };
342+
return undefined;
343+
});
344+
const foreignBotMention = { id: { open_id: 'ou_other_bot_foreign_scope' } };
345+
expect(await resolveMentionGate({
346+
...baseInput,
347+
mentions: [foreignBotMention],
348+
threadId: 'omt_existing_topic',
349+
text: '@张全栈 接下来做啥',
350+
})).toBeUndefined();
351+
});
324352
});
325353

326354
// ── 边界情况 ──

‎src/feishu/event-handler.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -566,11 +566,20 @@ async function resolveMentionGate(input: MentionGateInput): Promise<string | und
566566
// 补充 chatBotRegistry 中跨 app bot open_id
567567
const registryBotIds = chatBotRegistry.getBots(chatId).map(b => b.openId);
568568
const knownBotIds = new Set([...allBotOpenIds, ...registryBotIds]);
569-
const anyBotMentioned = mentions.some(m => knownBotIds.has(m.id.open_id ?? ''));
570569

571-
// 话题内 thread bypass:话题创建者 bot 无需 @mention
572-
// 走共享 evaluateThreadBypass —— session 创建者 + 单人话题直接放行;多人话题保守要求 @
573-
if (threadId && !anyBotMentioned && isThreadCreatorAgent(threadId, agentId)) {
570+
// 话题内 thread bypass:话题创建者 bot 无需 @mention 也可继续对话。
571+
// 但只要用户 @ 了"除本 bot 以外的任何对象",就说明在叫别人 —— 不 bypass。
572+
//
573+
// 为什么以"是否 @ 了非自己"为准,而不是"是否 @ 了已知 bot":
574+
// 飞书 open_id 按 app 隔离,同一个 bot 在不同 app 下 open_id 不同。
575+
// getAllBotOpenIds() 存的是各 bot 用自己 app 拉到的「自视 open_id」,
576+
// 当用户 @ 了另一个 bot 时,本 bot 的 app 收到的那条 mention 是「对方在本 app 视角下的 open_id」,
577+
// 不在 knownBotIds 里 → anyBotMentioned 漏判 → 话题创建者 bot 误以为没人被 @ 而抢答。
578+
// 每个 bot 唯一能确信的就是自己的 open_id,故改用「@ 了非自己」作为不 bypass 的判据。
579+
const mentionsOtherParty = mentions.some(
580+
(m) => !!m.id.open_id && m.id.open_id !== botOpenId,
581+
);
582+
if (threadId && !mentionsOtherParty && isThreadCreatorAgent(threadId, agentId)) {
574583
const result = await evaluateThreadBypass(threadBypassDeps, {
575584
threadId, chatId, agentId, senderUserId: userId, messageId,
576585
});
@@ -599,6 +608,15 @@ async function resolveMentionGate(input: MentionGateInput): Promise<string | und
599608

600609
// 群聊未 @mention:仅话题内 session 创建者可放行(共享判定逻辑,与多 bot 一致)
601610
if (!threadId) return undefined;
611+
612+
// 若消息 @ 了"除本 bot 以外的任何对象"(哪怕是另一个独立服务的 bot),说明在叫别人,不 bypass。
613+
// 走到这里说明本 bot 未被 @(@自己已在上方 mentionedBot 分支返回),故任一带 open_id 的 mention 都是「@别人」。
614+
const selfOpenId = feishuClient.botOpenId ?? '';
615+
const mentionsOtherParty = mentions.some(
616+
(m) => !!m.id.open_id && m.id.open_id !== selfOpenId,
617+
);
618+
if (mentionsOtherParty) return undefined;
619+
602620
const result = await evaluateThreadBypass(threadBypassDeps, {
603621
threadId, chatId, senderUserId: userId, messageId,
604622
});

0 commit comments

Comments
 (0)