fix(feishu): split long messages instead of silent truncate

This commit is contained in:
matevip 2026-05-02 15:40:11 +08:00
parent eaacb3a78f
commit fcab47ffb9

View File

@ -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<String> 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<String> splitTextForFeishu(String content, int maxChars) {
if (content == null || content.isEmpty()) {
return List.of();
}
if (content.length() <= maxChars) {
return List.of(content);
}
List<String> 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<MessageContentPart> parts) {
if (httpClient == null) {