Skip to content

Commit 3abb1d6

Browse files
wangyuyan-agent超渡法師
andauthored
fix(gateway): prevent feishu bot self-echo message loop (#705)
* fix(gateway): prevent feishu bot self-echo message loop Feishu WebSocket pushes the bot's own sent messages back as im.message.receive_v1 events with sender_type='user', bypassing all existing filters. This causes an infinite reply loop. Fix: send_text_message now returns the message_id from the API response and inserts it into the dedupe cache. When the echo event arrives via WebSocket, the dedupe check catches it. Discord Discussion: https://discord.com/channels/1491295327620169908/1500160821567684660 * fix(gateway): add warn log for invalid JSON on feishu 200 response Replace silent unwrap_or_default() with explicit match that logs a warning when the Feishu API returns 200 but the response body is not valid JSON. This makes the self-echo dedupe skip visible in production logs. * test(gateway): add tests for invalid JSON and missing message_id in feishu send - send_text_message_invalid_json_returns_none: 200 + garbage body → None - send_text_message_missing_message_id_returns_none: 200 + valid JSON but no data.message_id → None --------- Co-authored-by: wangyuyan-agent <265828726+wangyuyan-agent@users.noreply.github.com> Co-authored-by: 超渡法師 <chaodu@openab.dev>
1 parent 9638590 commit 3abb1d6

1 file changed

Lines changed: 62 additions & 13 deletions

File tree

gateway/src/adapters/feishu.rs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -831,14 +831,14 @@ async fn resolve_user_name(
831831
// ---------------------------------------------------------------------------
832832

833833
/// Send a text message to a feishu chat_id.
834-
/// Returns true on success.
834+
/// Returns the sent message_id on success (for self-echo dedupe), None on failure.
835835
pub async fn send_text_message(
836836
client: &reqwest::Client,
837837
api_base: &str,
838838
token: &str,
839839
chat_id: &str,
840840
text: &str,
841-
) -> bool {
841+
) -> Option<String> {
842842
let url = format!(
843843
"{}/open-apis/im/v1/messages?receive_id_type=chat_id",
844844
api_base
@@ -860,18 +860,28 @@ pub async fn send_text_message(
860860
{
861861
Ok(resp) => {
862862
if resp.status().is_success() {
863-
info!(chat_id = %chat_id, "feishu message sent");
864-
true
863+
let msg_id = match resp.json::<serde_json::Value>().await {
864+
Ok(body) => body
865+
.pointer("/data/message_id")
866+
.and_then(|v| v.as_str())
867+
.map(|s| s.to_string()),
868+
Err(e) => {
869+
warn!(chat_id = %chat_id, err = %e, "feishu 200 response not valid JSON, self-echo dedupe will be skipped");
870+
None
871+
}
872+
};
873+
info!(chat_id = %chat_id, message_id = ?msg_id, "feishu message sent");
874+
msg_id
865875
} else {
866876
let status = resp.status();
867877
let text = resp.text().await.unwrap_or_default();
868878
tracing::error!(status = %status, body = %text, "feishu send message failed");
869-
false
879+
None
870880
}
871881
}
872882
Err(e) => {
873883
tracing::error!(err = %e, "feishu send message request failed");
874-
false
884+
None
875885
}
876886
}
877887
}
@@ -1002,12 +1012,17 @@ pub async fn handle_reply(
10021012
let text = &reply.content.text;
10031013
let limit = adapter.config.message_limit;
10041014

1005-
// Split long messages
1015+
// Split long messages; store sent message_ids in dedupe to prevent
1016+
// self-echo (Feishu pushes bot's own messages back via WebSocket)
10061017
if text.len() <= limit {
1007-
send_text_message(&adapter.client, &api_base, &token, &reply.channel.id, text).await;
1018+
if let Some(msg_id) = send_text_message(&adapter.client, &api_base, &token, &reply.channel.id, text).await {
1019+
adapter.dedupe.is_duplicate(&msg_id);
1020+
}
10081021
} else {
10091022
for chunk in split_text(text, limit) {
1010-
send_text_message(&adapter.client, &api_base, &token, &reply.channel.id, chunk).await;
1023+
if let Some(msg_id) = send_text_message(&adapter.client, &api_base, &token, &reply.channel.id, chunk).await {
1024+
adapter.dedupe.is_duplicate(&msg_id);
1025+
}
10111026
}
10121027
}
10131028
}
@@ -1400,14 +1415,15 @@ mod tests {
14001415
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
14011416
"code": 0,
14021417
"msg": "success",
1418+
"data": {"message_id": "om_test123"}
14031419
})))
14041420
.expect(1)
14051421
.mount(&server)
14061422
.await;
14071423

14081424
let client = reqwest::Client::new();
1409-
let ok = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1410-
assert!(ok);
1425+
let msg_id = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1426+
assert_eq!(msg_id.as_deref(), Some("om_test123"));
14111427
}
14121428

14131429
#[tokio::test]
@@ -1421,8 +1437,41 @@ mod tests {
14211437
.await;
14221438

14231439
let client = reqwest::Client::new();
1424-
let ok = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1425-
assert!(!ok);
1440+
let msg_id = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1441+
assert!(msg_id.is_none());
1442+
}
1443+
1444+
#[tokio::test]
1445+
async fn send_text_message_invalid_json_returns_none() {
1446+
let server = MockServer::start().await;
1447+
Mock::given(method("POST"))
1448+
.and(path("/open-apis/im/v1/messages"))
1449+
.respond_with(ResponseTemplate::new(200).set_body_string("not json"))
1450+
.expect(1)
1451+
.mount(&server)
1452+
.await;
1453+
1454+
let client = reqwest::Client::new();
1455+
let msg_id = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1456+
assert!(msg_id.is_none());
1457+
}
1458+
1459+
#[tokio::test]
1460+
async fn send_text_message_missing_message_id_returns_none() {
1461+
let server = MockServer::start().await;
1462+
Mock::given(method("POST"))
1463+
.and(path("/open-apis/im/v1/messages"))
1464+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
1465+
"code": 0,
1466+
"msg": "success",
1467+
})))
1468+
.expect(1)
1469+
.mount(&server)
1470+
.await;
1471+
1472+
let client = reqwest::Client::new();
1473+
let msg_id = send_text_message(&client, &server.uri(), "t-tok", "oc_chat1", "hello").await;
1474+
assert!(msg_id.is_none());
14261475
}
14271476

14281477
// --- Split text tests ---

0 commit comments

Comments
 (0)