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懒加载并缓存 * Package-private for testing: returns true when a group message should be dropped
* 调用 /open-apis/bot/v3/info 接口失败时返回 null降级到放行保持兼容 * 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() { private String getBotOpenId() {
if (botOpenId != null) return botOpenId; if (botOpenId != null) return botOpenId;
@ -463,7 +483,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
log.info("[feishu] Bot open_id fetched and cached: {}", botOpenId); log.info("[feishu] Bot open_id fetched and cached: {}", botOpenId);
} }
} catch (Exception e) { } 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; return botOpenId;
} }
@ -638,12 +658,19 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
private void handleFeishuMessage(String messageId, String messageType, String contentStr, private void handleFeishuMessage(String messageId, String messageType, String contentStr,
String chatId, String chatType, String senderOpenId, String chatId, String chatType, String senderOpenId,
String parentId, boolean isBotMentioned, Object rawPayload) { String parentId, boolean isBotMentioned, Object rawPayload) {
// require_mention 群聊过滤群聊中必须 @机器人才响应 // require_mention 群聊过滤群聊中必须 @机器人才响应
// botOpenId null API 抖动 / 尚未拉取成功失败回退到放行
// 避免飞书 /open-apis/bot/v3/info 短暂不可用时整个群机器人变哑巴
boolean isGroup = "group".equals(chatType); boolean isGroup = "group".equals(chatType);
if (isGroup && getConfigBoolean("require_mention", false) && !isBotMentioned) { boolean requireMention = getConfigBoolean("require_mention", false);
log.debug("[feishu] require_mention=true but bot not mentioned, ignoring messageId={}", messageId); if (isGroupNonMentionDrop(isGroup, requireMention, isBotMentioned, botOpenId)) {
log.debug("[feishu] require_mention=true but bot not mentioned, dropping messageId={}", messageId);
return; 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)) { if (messageId != null && !processedMessageIds.add(messageId)) {

View File

@ -104,6 +104,39 @@ class FeishuMentionTest {
assertFalse(FeishuChannelAdapter.webhookMentionsContainBot(List.of(mention), BOT_ID)); 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 ==================== // ==================== helpers ====================
private static MentionEvent mentionEvent(String openId) { private static MentionEvent mentionEvent(String openId) {