fix(feishu): stop co-mentioned humans being learned as bot aliases

The mention alias-learning fed every identifier of every mention in a
delivery into the per-chat alias cache. A single delivery of "@bot @alice"
matched the bot by its global id and then learned alice's openId as a bot
alias, so every later "@alice" message was misdetected as @bot and the agent
replied to messages never addressed to it.

Only single-mention deliveries are unambiguous bot identities, so restrict
alias learning to them — a multi-mention delivery mixes the bot with
co-mentioned humans, and Feishu's dual-delivery alias form is itself a single
mention, so this is safe and keeps the learning feature working. Also cap the
per-chat alias set size. Adds a [bot, human] co-mention regression test.
This commit is contained in:
matevip 2026-06-09 09:35:35 +08:00
parent 74b2607e40
commit fcc2dd5ccf
2 changed files with 32 additions and 3 deletions

View File

@ -94,11 +94,17 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
*/ */
private final ConcurrentHashMap<String, Set<String>> chatBotAliases = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Set<String>> chatBotAliases = new ConcurrentHashMap<>();
/** Max learned aliases retained per chat, to bound memory on busy groups. */
private static final int CHAT_ALIAS_MAX = 64;
/** /**
* Per-messageId mention tracker(带 TTL)。 * Per-messageId mention tracker(带 TTL)。
* <p>飞书 SDK 经常对同一条消息双投递:一份 mentions 含 bot 的<em>全局身份</em>(来自 /bot/v3/info), * <p>飞书 SDK 经常对同一条消息双投递:一份 mentions 含 bot 的<em>全局身份</em>(来自 /bot/v3/info),
* 另一份含 bot 的<em>群内别名</em>。我们累积同一 messageId 下所有投递看到的 mention 标识, * 另一份含 bot 的<em>群内别名</em>。我们累积同一 messageId 下<em>单 mention</em>投递看到的标识,
* 一旦其中任何一份被识别为 @bot,就把累积的全部标识写入 {@link #chatBotAliases}。 * 一旦其中任何一份被识别为 @bot,就把累积的标识写入 {@link #chatBotAliases}。
* <p>只累积单 mention 投递是有意为之:多 mention 投递(如 {@code @bot @某人})会把 bot 与
* 被同时 @ 的人混在一起,无法区分,若整体学习会把人误学成 bot 别名,导致之后 @ 该人的消息
* 被误判为 @bot。而飞书双投递里 bot 别名那一份本身就是单 mention,所以这样既安全又不丢功能。
*/ */
private final ConcurrentHashMap<String, MentionTrack> mentionTracker = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, MentionTrack> mentionTracker = new ConcurrentHashMap<>();
@ -720,8 +726,14 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
MentionTrack track = null; MentionTrack track = null;
if (messageId != null) { if (messageId != null) {
track = mentionTracker.computeIfAbsent(messageId, k -> new MentionTrack()); track = mentionTracker.computeIfAbsent(messageId, k -> new MentionTrack());
// Only single-mention deliveries are unambiguous bot identities. A
// multi-mention delivery (e.g. @bot @alice) mixes the bot with
// co-mentioned humans that must NOT be learned as aliases; Feishu's
// dual-delivery alias form is itself a single mention, so this is safe.
if (mentions.length == 1) {
collectMentionIdentifiers(mentions, track.seenIds); collectMentionIdentifiers(mentions, track.seenIds);
} }
}
// 1. 直接匹配 bot 的全局身份 // 1. 直接匹配 bot 的全局身份
if (eventMentionsContainBot(mentions, botOpenId, botName)) { if (eventMentionsContainBot(mentions, botOpenId, botName)) {
@ -754,6 +766,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter implements Stre
private void learnFromTrack(String chatId, MentionTrack track) { private void learnFromTrack(String chatId, MentionTrack track) {
if (chatId == null || track == null || track.seenIds.isEmpty()) return; if (chatId == null || track == null || track.seenIds.isEmpty()) return;
Set<String> aliases = chatBotAliases.computeIfAbsent(chatId, k -> ConcurrentHashMap.newKeySet()); Set<String> aliases = chatBotAliases.computeIfAbsent(chatId, k -> ConcurrentHashMap.newKeySet());
if (aliases.size() >= CHAT_ALIAS_MAX) return;
int before = aliases.size(); int before = aliases.size();
aliases.addAll(track.seenIds); aliases.addAll(track.seenIds);
int added = aliases.size() - before; int added = aliases.size() - before;

View File

@ -132,6 +132,22 @@ class FeishuMentionTest {
new MentionEvent[]{mentionEvent(alias)}, chatB, "m3", BOT_ID, null)); new MentionEvent[]{mentionEvent(alias)}, chatB, "m3", BOT_ID, null));
} }
@Test
void learning_coMentionedHumanNotLearnedAsAlias() {
FeishuChannelAdapter adapter = newAdapter();
String human = "ou_human_alice";
String chatA = "oc_chatA";
// @bot @alice 在同一次投递:bot 用全局身份命中(返回 true),
// 但被同时 @ 的人不能被学成 bot 别名。
assertTrue(adapter.detectBotMentionWithLearning(
new MentionEvent[]{mentionEvent(BOT_ID), mentionEvent(human)}, chatA, "m_co", BOT_ID, null));
// 之后只 @ 那个人的消息,绝不能被误判为 @bot。
assertFalse(adapter.detectBotMentionWithLearning(
new MentionEvent[]{mentionEvent(human)}, chatA, "m_human", BOT_ID, null));
}
// ==================== mention tracker TTL ==================== // ==================== mention tracker TTL ====================
@Test @Test