Skip to content

Commit 53dbb25

Browse files
chaodu-agent超渡法師
andauthored
fix(discord): send_message for streaming replies with bot mentions (#1112)
* fix(discord): use send_message for streaming replies with bot mentions When a streaming response contains <@uid> mentions, Discord's MESSAGE_UPDATE (edit) does not trigger mention notifications for the mentioned bot. Switch to delete placeholder + send_message (MESSAGE_CREATE) so the mentioned bot receives the gateway event and can respond. Fixes #1110 * fix: address review feedback — support <@!UID> and add platform guard - Support nickname-style mentions (<@!UID>) in contains_bot_mention() - Add platform == "discord" check so the delete+send path only applies to Discord, not Slack or other adapters - Fix misleading comment about '!' being for role mentions * fix: also detect role mentions (<@&ROLE_ID>) for send_message path Role mentions (e.g. <@&1496247626675257384>) should also trigger MESSAGE_CREATE so all bots with that role receive the gateway event. * test: add unit tests for contains_bot_mention Covers user mentions (<@uid>), nickname mentions (<@!UID>), role mentions (<@&ROLE_ID>), and negative cases. --------- Co-authored-by: 超渡法師 <chaodu@openab.dev>
1 parent 94686cc commit 53dbb25

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

src/adapter.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,25 @@ impl AdapterRouter {
915915
tracing::warn!(error = ?e, "delete placeholder failed; placeholder will remain visible");
916916
}
917917
}
918+
} else if adapter.platform() == "discord"
919+
&& contains_bot_mention(&final_content)
920+
{
921+
// Discord-specific: bot mention detected. Delete placeholder
922+
// and send as new message so Discord emits MESSAGE_CREATE —
923+
// otherwise the mentioned bot won't receive the gateway
924+
// event since MESSAGE_UPDATE skips notifications (#1110).
925+
let mut send_ok = false;
926+
if let Some(first) = chunks.first() {
927+
if adapter.send_message(&thread_channel, first).await.is_ok() {
928+
send_ok = true;
929+
}
930+
}
931+
for chunk in chunks.iter().skip(1) {
932+
let _ = adapter.send_message(&thread_channel, chunk).await;
933+
}
934+
if send_ok {
935+
let _ = adapter.delete_message(&msg).await;
936+
}
918937
} else {
919938
// Normal streaming: edit first chunk into placeholder, send rest
920939
if let Some(first) = chunks.first() {
@@ -953,6 +972,38 @@ impl AdapterRouter {
953972
}
954973
}
955974

975+
/// Returns true if `content` contains a Discord user/bot mention (`<@123>`, `<@!123>`)
976+
/// or a role mention (`<@&123>`).
977+
/// Used to detect cross-bot mentions so the streaming path can switch from
978+
/// edit (MESSAGE_UPDATE, no mention notification) to delete+send (MESSAGE_CREATE).
979+
fn contains_bot_mention(content: &str) -> bool {
980+
let mut i = 0;
981+
let bytes = content.as_bytes();
982+
while i + 2 < bytes.len() {
983+
if bytes[i] == b'<' && bytes[i + 1] == b'@' {
984+
// Skip optional '!' (nickname mention) or '&' (role mention)
985+
let start = if i + 2 < bytes.len()
986+
&& (bytes[i + 2] == b'!' || bytes[i + 2] == b'&')
987+
{
988+
i + 3
989+
} else {
990+
i + 2
991+
};
992+
if start < bytes.len() && bytes[start].is_ascii_digit() {
993+
if let Some(end) = content[start..].find('>') {
994+
if content[start..start + end].chars().all(|c| c.is_ascii_digit()) {
995+
return true;
996+
}
997+
}
998+
}
999+
i = start;
1000+
} else {
1001+
i += 1;
1002+
}
1003+
}
1004+
false
1005+
}
1006+
9561007
/// Flatten a tool-call title into a single line safe for inline-code spans.
9571008
fn sanitize_title(title: &str) -> String {
9581009
title
@@ -1259,6 +1310,34 @@ mod tests {
12591310
let out = compose_display(&tools, "response text", false, ToolDisplay::None);
12601311
assert_eq!(out, "response text");
12611312
}
1313+
1314+
#[test]
1315+
fn contains_bot_mention_user() {
1316+
assert!(contains_bot_mention("hello <@1234567890> world"));
1317+
}
1318+
1319+
#[test]
1320+
fn contains_bot_mention_nickname() {
1321+
assert!(contains_bot_mention("hey <@!9876543210>"));
1322+
}
1323+
1324+
#[test]
1325+
fn contains_bot_mention_role() {
1326+
assert!(contains_bot_mention("calling <@&1496247626675257384>"));
1327+
}
1328+
1329+
#[test]
1330+
fn contains_bot_mention_no_match() {
1331+
assert!(!contains_bot_mention("hello world"));
1332+
assert!(!contains_bot_mention("email user@example.com"));
1333+
assert!(!contains_bot_mention("<@not_a_number>"));
1334+
assert!(!contains_bot_mention("<#123456>")); // channel mention
1335+
}
1336+
1337+
#[test]
1338+
fn contains_bot_mention_embedded() {
1339+
assert!(contains_bot_mention("請問 <@1501788608439386172> 1+1=?"));
1340+
}
12621341
}
12631342

12641343
#[cfg(test)]

0 commit comments

Comments
 (0)