fix(feishu): fail open when bot open_id is unavailable

This commit is contained in:
matevip 2026-05-20 10:13:34 +08:00
parent 16b5d75d2c
commit 73ab31a13c
2 changed files with 66 additions and 6 deletions

View File

@ -441,8 +441,28 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
}
/**
* 获取机器人自身的 open_id懒加载并缓存
* 调用 /open-apis/bot/v3/info 接口失败时返回 null降级到放行保持兼容
* Package-private for testing: returns true when a group message should be dropped
* because {@code require_mention} is on, the bot was not mentioned, AND we know our
* own open_id (so we trust the negative answer).
*
* <p>When {@code botOpenId} is {@code null} the bot identity is unavailable either
* the {@code /open-apis/bot/v3/info} fetch hasn't succeeded yet, or it failed and is
* in the negative-cache window. In that case the gate falls open so a transient
* Feishu API outage doesn't silence the bot in every group it's in. The accompanying
* warn-level log makes the degraded mode visible.
*/
static boolean isGroupNonMentionDrop(boolean isGroup,
boolean requireMention,
boolean isBotMentioned,
String botOpenId) {
return isGroup && requireMention && !isBotMentioned && botOpenId != null;
}
/**
* Fetches the bot's own open_id once and caches it. Returns {@code null} when
* the fetch fails callers that consult this value (currently the
* {@code require_mention} gate) must treat {@code null} as "identity unknown"
* and fall open so a transient API outage doesn't silence the bot.
*/
private String getBotOpenId() {
if (botOpenId != null) return botOpenId;
@ -463,7 +483,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
log.info("[feishu] Bot open_id fetched and cached: {}", botOpenId);
}
} catch (Exception e) {
log.warn("[feishu] Failed to fetch bot open_id, require_mention will allow message: {}", e.getMessage());
log.warn("[feishu] Failed to fetch bot open_id; require_mention gate will fall open until next attempt: {}", e.getMessage());
}
return botOpenId;
}
@ -638,12 +658,19 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
private void handleFeishuMessage(String messageId, String messageType, String contentStr,
String chatId, String chatType, String senderOpenId,
String parentId, boolean isBotMentioned, Object rawPayload) {
// require_mention 群聊过滤群聊中必须 @机器人才响应
// require_mention 群聊过滤群聊中必须 @机器人才响应
// botOpenId null API 抖动 / 尚未拉取成功失败回退到放行
// 避免飞书 /open-apis/bot/v3/info 短暂不可用时整个群机器人变哑巴
boolean isGroup = "group".equals(chatType);
if (isGroup && getConfigBoolean("require_mention", false) && !isBotMentioned) {
log.debug("[feishu] require_mention=true but bot not mentioned, ignoring messageId={}", messageId);
boolean requireMention = getConfigBoolean("require_mention", false);
if (isGroupNonMentionDrop(isGroup, requireMention, isBotMentioned, botOpenId)) {
log.debug("[feishu] require_mention=true but bot not mentioned, dropping messageId={}", messageId);
return;
}
if (isGroup && requireMention && !isBotMentioned) {
// botOpenId is null here identity unknown, gate falls open.
log.warn("[feishu] require_mention=true but bot open_id unavailable; allowing messageId={}", messageId);
}
// 消息去重
if (messageId != null && !processedMessageIds.add(messageId)) {

View File

@ -104,6 +104,39 @@ class FeishuMentionTest {
assertFalse(FeishuChannelAdapter.webhookMentionsContainBot(List.of(mention), BOT_ID));
}
// ==================== isGroupNonMentionDrop (gate matrix) ====================
@Test
void gate_groupRequireMentionBotMentioned_passesThrough() {
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(true, true, true, BOT_ID));
}
@Test
void gate_groupRequireMentionBotNotMentioned_dropsWhenOpenIdKnown() {
assertTrue(FeishuChannelAdapter.isGroupNonMentionDrop(true, true, false, BOT_ID));
}
@Test
void gate_groupRequireMentionBotNotMentioned_failsOpenWhenOpenIdUnknown() {
// Bot identity unavailable (API outage / pending fetch) degrade to allow.
// This was the bug the original PR shipped with: gate dropped instead of fell open.
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(true, true, false, null));
}
@Test
void gate_p2pAlwaysPasses_regardlessOfRequireMention() {
// require_mention only applies to group chat; DMs are never gated.
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(false, true, false, BOT_ID));
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(false, true, false, null));
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(false, true, true, BOT_ID));
}
@Test
void gate_requireMentionDisabled_passesThrough() {
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(true, false, false, BOT_ID));
assertFalse(FeishuChannelAdapter.isGroupNonMentionDrop(true, false, false, null));
}
// ==================== helpers ====================
private static MentionEvent mentionEvent(String openId) {