mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(feishu): three small follow-ups from the post-merge audit
This commit is contained in:
parent
2e4f88c612
commit
eb827d8cc5
@ -187,8 +187,13 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
|
||||
this.httpClient = null;
|
||||
this.tenantAccessToken = null;
|
||||
this.botOpenId = null;
|
||||
this.botOpenIdLastFailureMs = 0L;
|
||||
// Reset bot-open-id state under the same lock getBotOpenId uses, so a
|
||||
// dispatch thread mid-fetch sees a consistent (cleared) view rather than
|
||||
// a torn write that could re-cache a stale id.
|
||||
synchronized (botOpenIdLock) {
|
||||
this.botOpenId = null;
|
||||
this.botOpenIdLastFailureMs = 0L;
|
||||
}
|
||||
this.processedMessageIds.clear();
|
||||
this.nicknameCache.clear();
|
||||
this.quotedMessageCache.clear();
|
||||
@ -1339,39 +1344,43 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
HttpResponse<InputStream> response = httpClient.send(request,
|
||||
HttpResponse.BodyHandlers.ofInputStream());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
log.debug("[feishu] Download resource failed: status={}", response.statusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建目标目录
|
||||
Path mediaDir = Path.of(System.getProperty("user.home"), ".mateclaw", "media", "feishu");
|
||||
Files.createDirectories(mediaDir);
|
||||
|
||||
// 安全文件名
|
||||
String safeKey = fileKey.replaceAll("[^a-zA-Z0-9_]", "");
|
||||
if (safeKey.isEmpty()) safeKey = "file";
|
||||
|
||||
// 推断扩展名
|
||||
String ext = "bin";
|
||||
String contentType = response.headers().firstValue("Content-Type").orElse("");
|
||||
if (contentType.contains("jpeg") || contentType.contains("jpg")) ext = "jpg";
|
||||
else if (contentType.contains("png")) ext = "png";
|
||||
else if (contentType.contains("gif")) ext = "gif";
|
||||
else if (contentType.contains("webp")) ext = "webp";
|
||||
else if (contentType.contains("pdf")) ext = "pdf";
|
||||
else if (fileNameHint != null && fileNameHint.contains(".")) {
|
||||
ext = fileNameHint.substring(fileNameHint.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
Path filePath = mediaDir.resolve(messageId + "_" + safeKey + "." + ext);
|
||||
|
||||
// BodyHandlers.ofInputStream does NOT auto-close the response body —
|
||||
// callers must consume or close it on every path, including non-2xx
|
||||
// and early-return-after-content-type. Use try-with-resources around
|
||||
// the entire post-send block so failed downloads don't leak file
|
||||
// descriptors during sustained traffic.
|
||||
try (InputStream is = response.body()) {
|
||||
Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
if (response.statusCode() != 200) {
|
||||
log.debug("[feishu] Download resource failed: status={}", response.statusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("[feishu] Downloaded resource to: {}", filePath);
|
||||
return filePath.toAbsolutePath().toString();
|
||||
// 构建目标目录
|
||||
Path mediaDir = Path.of(System.getProperty("user.home"), ".mateclaw", "media", "feishu");
|
||||
Files.createDirectories(mediaDir);
|
||||
|
||||
// 安全文件名
|
||||
String safeKey = fileKey.replaceAll("[^a-zA-Z0-9_]", "");
|
||||
if (safeKey.isEmpty()) safeKey = "file";
|
||||
|
||||
// 推断扩展名
|
||||
String ext = "bin";
|
||||
String contentType = response.headers().firstValue("Content-Type").orElse("");
|
||||
if (contentType.contains("jpeg") || contentType.contains("jpg")) ext = "jpg";
|
||||
else if (contentType.contains("png")) ext = "png";
|
||||
else if (contentType.contains("gif")) ext = "gif";
|
||||
else if (contentType.contains("webp")) ext = "webp";
|
||||
else if (contentType.contains("pdf")) ext = "pdf";
|
||||
else if (fileNameHint != null && fileNameHint.contains(".")) {
|
||||
ext = fileNameHint.substring(fileNameHint.lastIndexOf('.') + 1);
|
||||
}
|
||||
|
||||
Path filePath = mediaDir.resolve(messageId + "_" + safeKey + "." + ext);
|
||||
Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
log.debug("[feishu] Downloaded resource to: {}", filePath);
|
||||
return filePath.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("[feishu] Download resource failed: {}", e.getMessage());
|
||||
@ -1381,6 +1390,23 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
|
||||
// ==================== 消息发送 ====================
|
||||
|
||||
/**
|
||||
* Maps a Feishu target identifier to the corresponding
|
||||
* {@code receive_id_type} query parameter expected by the Open API.
|
||||
* <p>
|
||||
* Targets prefixed with {@code "ou_"} are user open_ids; everything else
|
||||
* (group chat_ids start with {@code "oc_"}, but any non-{@code ou_} value
|
||||
* is treated as a chat_id by default) is sent with
|
||||
* {@code receive_id_type=chat_id}. Keeping this routing in one place
|
||||
* keeps {@link #sendOneTextChunk}, {@link #sendCard},
|
||||
* {@link #sendFeishuMedia} and {@link #proactiveSend} from drifting —
|
||||
* silently misrouting the text-fallback after a failed card send to an
|
||||
* individual was the previous regression.
|
||||
*/
|
||||
private static String resolveReceiveIdType(String targetId) {
|
||||
return targetId != null && targetId.startsWith("ou_") ? "open_id" : "chat_id";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -1426,6 +1452,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
|
||||
private void sendOneTextChunk(String targetId, String content) {
|
||||
String apiBase = getApiBaseUrl();
|
||||
String receiveIdType = resolveReceiveIdType(targetId);
|
||||
try {
|
||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||
"receive_id", targetId,
|
||||
@ -1434,7 +1461,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
));
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiBase + "/open-apis/im/v1/messages?receive_id_type=chat_id"))
|
||||
.uri(URI.create(apiBase + "/open-apis/im/v1/messages?receive_id_type=" + receiveIdType))
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Authorization", "Bearer " + tenantAccessToken)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
@ -1444,7 +1471,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={} ({} chars)", targetId, content.length());
|
||||
log.debug("[feishu] Message sent to {}={} ({} chars)", receiveIdType, targetId, content.length());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@ -1466,6 +1493,9 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
* (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.
|
||||
*
|
||||
* <p>{@code targetId} format follows {@link #resolveReceiveIdType}:
|
||||
* {@code "ou_"} prefix → open_id (1:1), anything else → chat_id (group).
|
||||
*/
|
||||
public boolean sendCard(String targetId, Map<String, Object> cardJson) {
|
||||
if (httpClient == null) {
|
||||
@ -1478,7 +1508,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
}
|
||||
ensureTokenValid();
|
||||
String apiBase = getApiBaseUrl();
|
||||
String receiveIdType = targetId.startsWith("ou_") ? "open_id" : "chat_id";
|
||||
String receiveIdType = resolveReceiveIdType(targetId);
|
||||
try {
|
||||
String cardContent = objectMapper.writeValueAsString(cardJson);
|
||||
int cardBytes = cardContent.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
|
||||
@ -1624,17 +1654,18 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendFeishuMedia(String chatId, String msgType, Map<String, Object> content) {
|
||||
private void sendFeishuMedia(String targetId, String msgType, Map<String, Object> content) {
|
||||
String apiBase = getApiBaseUrl();
|
||||
String receiveIdType = resolveReceiveIdType(targetId);
|
||||
try {
|
||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||
"receive_id", chatId,
|
||||
"receive_id", targetId,
|
||||
"msg_type", msgType,
|
||||
"content", objectMapper.writeValueAsString(content)
|
||||
));
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiBase + "/open-apis/im/v1/messages?receive_id_type=chat_id"))
|
||||
.uri(URI.create(apiBase + "/open-apis/im/v1/messages?receive_id_type=" + receiveIdType))
|
||||
.header("Content-Type", "application/json; charset=utf-8")
|
||||
.header("Authorization", "Bearer " + tenantAccessToken)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
@ -1673,14 +1704,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
||||
|
||||
ensureTokenValid();
|
||||
String apiBase = getApiBaseUrl();
|
||||
|
||||
// 根据 targetId 前缀判断 receive_id_type
|
||||
String receiveIdType;
|
||||
if (targetId.startsWith("ou_")) {
|
||||
receiveIdType = "open_id";
|
||||
} else {
|
||||
receiveIdType = "chat_id";
|
||||
}
|
||||
String receiveIdType = resolveReceiveIdType(targetId);
|
||||
|
||||
try {
|
||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user