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 abe5defb..f9a44412 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 @@ -1184,16 +1184,38 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { // ==================== 消息发送 ==================== + /** + * Conservative per-message char ceiling. Feishu's documented limit is on + * the encoded JSON body (~150KB), but UTF-8 Chinese is 3 bytes/char and + * JSON escape adds further overhead, so a 4000-char chunk is comfortably + * under any realistic limit and also gives readable IM chunking instead + * of one wall of text. Split at paragraph / line boundaries when possible. + */ + static final int MAX_TEXT_MESSAGE_CHARS = 4000; + @Override public void sendMessage(String targetId, String content) { if (httpClient == null) { log.warn("[feishu] Channel not started, cannot send message"); return; } + if (content == null) { + return; + } ensureTokenValid(); - String apiBase = getApiBaseUrl(); + List chunks = splitTextForFeishu(content, MAX_TEXT_MESSAGE_CHARS); + if (chunks.size() > 1) { + log.info("[feishu] Splitting message into {} chunks ({} chars total) before send", + chunks.size(), content.length()); + } + for (String chunk : chunks) { + sendOneTextChunk(targetId, chunk); + } + } + private void sendOneTextChunk(String targetId, String content) { + String apiBase = getApiBaseUrl(); try { String jsonBody = objectMapper.writeValueAsString(Map.of( "receive_id", targetId, @@ -1212,7 +1234,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { if (response.statusCode() != 200) { log.warn("[feishu] Send message failed: status={}, body={}", response.statusCode(), response.body()); } else { - log.debug("[feishu] Message sent to chat_id={}", targetId); + log.debug("[feishu] Message sent to chat_id={} ({} chars)", targetId, content.length()); } } catch (Exception e) { @@ -1220,6 +1242,52 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter { } } + /** + * Split a possibly-oversized message into chunks no larger than + * {@code maxChars}, preferring paragraph (\n\n) then line (\n) then + * whitespace boundaries. Hard-cuts as a last resort so we never silently + * drop content. + */ + static List splitTextForFeishu(String content, int maxChars) { + if (content == null || content.isEmpty()) { + return List.of(); + } + if (content.length() <= maxChars) { + return List.of(content); + } + List chunks = new ArrayList<>(); + int idx = 0; + int n = content.length(); + while (idx < n) { + int end = Math.min(idx + maxChars, n); + if (end < n) { + int boundary = -1; + int paragraphCut = content.lastIndexOf("\n\n", end); + if (paragraphCut > idx + maxChars / 2) { + boundary = paragraphCut + 2; + } + if (boundary < 0) { + int lineCut = content.lastIndexOf('\n', end); + if (lineCut > idx + maxChars / 2) { + boundary = lineCut + 1; + } + } + if (boundary < 0) { + int spaceCut = content.lastIndexOf(' ', end); + if (spaceCut > idx + maxChars / 2) { + boundary = spaceCut + 1; + } + } + if (boundary > idx) { + end = boundary; + } + } + chunks.add(content.substring(idx, end)); + idx = end; + } + return chunks; + } + @Override public void sendContentParts(String targetId, List parts) { if (httpClient == null) {