mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +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.httpClient = null;
|
||||||
this.tenantAccessToken = null;
|
this.tenantAccessToken = null;
|
||||||
this.botOpenId = null;
|
// Reset bot-open-id state under the same lock getBotOpenId uses, so a
|
||||||
this.botOpenIdLastFailureMs = 0L;
|
// 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.processedMessageIds.clear();
|
||||||
this.nicknameCache.clear();
|
this.nicknameCache.clear();
|
||||||
this.quotedMessageCache.clear();
|
this.quotedMessageCache.clear();
|
||||||
@ -1339,39 +1344,43 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
HttpResponse<InputStream> response = httpClient.send(request,
|
HttpResponse<InputStream> response = httpClient.send(request,
|
||||||
HttpResponse.BodyHandlers.ofInputStream());
|
HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
// BodyHandlers.ofInputStream does NOT auto-close the response body —
|
||||||
log.debug("[feishu] Download resource failed: status={}", response.statusCode());
|
// callers must consume or close it on every path, including non-2xx
|
||||||
return null;
|
// 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.
|
||||||
// 构建目标目录
|
|
||||||
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);
|
|
||||||
|
|
||||||
try (InputStream is = response.body()) {
|
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) {
|
} catch (Exception e) {
|
||||||
log.debug("[feishu] Download resource failed: {}", e.getMessage());
|
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
|
* 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
|
* 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) {
|
private void sendOneTextChunk(String targetId, String content) {
|
||||||
String apiBase = getApiBaseUrl();
|
String apiBase = getApiBaseUrl();
|
||||||
|
String receiveIdType = resolveReceiveIdType(targetId);
|
||||||
try {
|
try {
|
||||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||||
"receive_id", targetId,
|
"receive_id", targetId,
|
||||||
@ -1434,7 +1461,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
));
|
));
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
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("Content-Type", "application/json; charset=utf-8")
|
||||||
.header("Authorization", "Bearer " + tenantAccessToken)
|
.header("Authorization", "Bearer " + tenantAccessToken)
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||||
@ -1444,7 +1471,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
log.warn("[feishu] Send message failed: status={}, body={}", response.statusCode(), response.body());
|
log.warn("[feishu] Send message failed: status={}, body={}", response.statusCode(), response.body());
|
||||||
} else {
|
} 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) {
|
} catch (Exception e) {
|
||||||
@ -1466,6 +1493,9 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
* (null inputs, channel stopped, payload oversized) or the HTTP request
|
* (null inputs, channel stopped, payload oversized) or the HTTP request
|
||||||
* itself failed — letting {@link #sendMessage(String, String)} fall back
|
* itself failed — letting {@link #sendMessage(String, String)} fall back
|
||||||
* to plain text instead of going silent.
|
* 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) {
|
public boolean sendCard(String targetId, Map<String, Object> cardJson) {
|
||||||
if (httpClient == null) {
|
if (httpClient == null) {
|
||||||
@ -1478,7 +1508,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
}
|
}
|
||||||
ensureTokenValid();
|
ensureTokenValid();
|
||||||
String apiBase = getApiBaseUrl();
|
String apiBase = getApiBaseUrl();
|
||||||
String receiveIdType = targetId.startsWith("ou_") ? "open_id" : "chat_id";
|
String receiveIdType = resolveReceiveIdType(targetId);
|
||||||
try {
|
try {
|
||||||
String cardContent = objectMapper.writeValueAsString(cardJson);
|
String cardContent = objectMapper.writeValueAsString(cardJson);
|
||||||
int cardBytes = cardContent.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
|
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 apiBase = getApiBaseUrl();
|
||||||
|
String receiveIdType = resolveReceiveIdType(targetId);
|
||||||
try {
|
try {
|
||||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||||
"receive_id", chatId,
|
"receive_id", targetId,
|
||||||
"msg_type", msgType,
|
"msg_type", msgType,
|
||||||
"content", objectMapper.writeValueAsString(content)
|
"content", objectMapper.writeValueAsString(content)
|
||||||
));
|
));
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
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("Content-Type", "application/json; charset=utf-8")
|
||||||
.header("Authorization", "Bearer " + tenantAccessToken)
|
.header("Authorization", "Bearer " + tenantAccessToken)
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||||
@ -1673,14 +1704,7 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
|
|||||||
|
|
||||||
ensureTokenValid();
|
ensureTokenValid();
|
||||||
String apiBase = getApiBaseUrl();
|
String apiBase = getApiBaseUrl();
|
||||||
|
String receiveIdType = resolveReceiveIdType(targetId);
|
||||||
// 根据 targetId 前缀判断 receive_id_type
|
|
||||||
String receiveIdType;
|
|
||||||
if (targetId.startsWith("ou_")) {
|
|
||||||
receiveIdType = "open_id";
|
|
||||||
} else {
|
|
||||||
receiveIdType = "chat_id";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
String jsonBody = objectMapper.writeValueAsString(Map.of(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user