From 56ab7b3095b6afa8feb4730b73cc4ba5c529ac1b Mon Sep 17 00:00:00 2001 From: matevip Date: Wed, 20 May 2026 10:13:47 +0800 Subject: [PATCH] fix(feishu): fall back to text when Interactive Card payload exceeds Feishu limit --- .../channel/feishu/FeishuChannelAdapter.java | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/channel/feishu/FeishuChannelAdapter.java b/mateclaw-server/src/main/java/vip/mate/channel/feishu/FeishuChannelAdapter.java index a24a6e32..3ce0c688 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/feishu/FeishuChannelAdapter.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/feishu/FeishuChannelAdapter.java @@ -1411,12 +1411,15 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { FeishuCardFormatter.ContentFormat fmt = FeishuCardFormatter.detect(content); - if ("always".equals(cardFormat) || fmt != FeishuCardFormatter.ContentFormat.PLAIN_TEXT) { - sendCard(targetId, FeishuCardFormatter.render(content, fmt)); - } else { - splitTextForFeishu(content, MAX_TEXT_MESSAGE_CHARS) - .forEach(c -> sendOneTextChunk(targetId, c)); + boolean preferCard = "always".equals(cardFormat) || fmt != FeishuCardFormatter.ContentFormat.PLAIN_TEXT; + if (preferCard) { + boolean sent = sendCard(targetId, FeishuCardFormatter.render(content, fmt)); + if (sent) return; + // Card path bailed (oversized payload, null guard, or network error) — + // fall through to text so the user doesn't get nothing. } + splitTextForFeishu(content, MAX_TEXT_MESSAGE_CHARS) + .forEach(c -> sendOneTextChunk(targetId, c)); } private void sendOneTextChunk(String targetId, String content) { @@ -1447,19 +1450,45 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { } } - public void sendCard(String targetId, Map cardJson) { + /** + * Feishu rejects interactive messages whose {@code content} payload exceeds + * roughly 30 KB once stringified. We compare the serialized card against a + * slightly tighter ceiling and fall back to plain text rather than letting + * the request error out at the API. + */ + static final int MAX_CARD_CONTENT_BYTES = 30_000; + + /** + * Posts an Interactive Card. Returns {@code true} when the card was sent + * (HTTP 2xx), {@code false} when a pre-flight check rejected the call + * (null inputs, channel stopped, payload oversized) or the HTTP request + * itself failed — letting {@link #sendMessage(String, String)} fall back + * to plain text instead of going silent. + */ + public boolean sendCard(String targetId, Map cardJson) { if (httpClient == null) { log.warn("[feishu] Channel not started, cannot send card"); - return; + return false; + } + if (targetId == null || cardJson == null) { + log.warn("[feishu] sendCard called with null target or card"); + return false; } ensureTokenValid(); String apiBase = getApiBaseUrl(); String receiveIdType = targetId.startsWith("ou_") ? "open_id" : "chat_id"; try { + String cardContent = objectMapper.writeValueAsString(cardJson); + int cardBytes = cardContent.getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + if (cardBytes > MAX_CARD_CONTENT_BYTES) { + log.warn("[feishu] Card content {} bytes exceeds {} byte limit, falling back to text", + cardBytes, MAX_CARD_CONTENT_BYTES); + return false; + } String jsonBody = objectMapper.writeValueAsString(Map.of( "receive_id", targetId, "msg_type", "interactive", - "content", objectMapper.writeValueAsString(cardJson) + "content", cardContent )); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiBase + "/open-apis/im/v1/messages?receive_id_type=" + receiveIdType)) @@ -1470,11 +1499,13 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { log.warn("[feishu] Send card failed: status={}, body={}", response.statusCode(), response.body()); - } else { - log.debug("[feishu] Card sent to {} (type={})", targetId, receiveIdType); + return false; } + log.debug("[feishu] Card sent to {} (type={})", targetId, receiveIdType); + return true; } catch (Exception e) { log.error("[feishu] Failed to send card: {}", e.getMessage(), e); + return false; } } @@ -1483,6 +1514,10 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { log.warn("[feishu] Channel not started, cannot update card"); return; } + if (messageId == null || cardJson == null) { + log.warn("[feishu] updateCard called with null messageId or card"); + return; + } ensureTokenValid(); String apiBase = getApiBaseUrl(); try {