From e16ef2969b1446162e19af7bdde1446674851e66 Mon Sep 17 00:00:00 2001 From: richardmilles <76790700+richardmilles@users.noreply.github.com> Date: Sat, 8 Aug 2026 15:14:22 +0200 Subject: [PATCH] fix(dingtalk): strip leading mentions before command classification (#4724) Group @bot /command messages are classified as commands like Feishu. --- backend/app/channels/dingtalk.py | 12 ++++- backend/tests/test_dingtalk_channel.py | 65 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/backend/app/channels/dingtalk.py b/backend/app/channels/dingtalk.py index 9d6f5ecc8..57cad0524 100644 --- a/backend/app/channels/dingtalk.py +++ b/backend/app/channels/dingtalk.py @@ -14,7 +14,7 @@ from typing import Any import httpx from app.channels.base import Channel -from app.channels.commands import is_known_channel_command +from app.channels.commands import is_known_channel_command, strip_leading_mentions from app.channels.connection_identity import attach_connection_identity from app.channels.message_bus import InboundMessage, InboundMessageType, MessageBus, OutboundMessage, ResolvedAttachment from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths @@ -428,8 +428,16 @@ class DingTalkChannel(Channel): len(files), ) - if _is_dingtalk_command(text): + # DingTalk group chats often deliver "@bot /new" with the mention left + # in the text (Slack/Discord strip their own bot mention upstream). + # Skip a leading mention only for the command path so ordinary chat + # keeps @mentions intact for the agent; the stripped form also flows + # into the inbound so ChannelManager._handle_command parses the bare + # command. Mirrors FeishuChannel. + command_text = strip_leading_mentions(text) + if _is_dingtalk_command(command_text): msg_type = InboundMessageType.COMMAND + text = command_text else: msg_type = InboundMessageType.CHAT diff --git a/backend/tests/test_dingtalk_channel.py b/backend/tests/test_dingtalk_channel.py index 098edbb9d..f9776830a 100644 --- a/backend/tests/test_dingtalk_channel.py +++ b/backend/tests/test_dingtalk_channel.py @@ -304,6 +304,71 @@ class TestOnChatbotMessage: _run(go()) + @pytest.mark.parametrize("text", ["@bot /new", "@_user_1 /help", "@bot /goal ship it"]) + def test_leading_mention_before_command_classifies_and_strips(self, text): + """DingTalk group chats leave "@bot /new" in the text; classify as COMMAND + and strip the mention so ChannelManager receives the bare command.""" + + async def go(): + bus = MessageBus() + bus.publish_inbound = AsyncMock() + channel = DingTalkChannel(bus, config={}) + channel._client_id = "test_key" + channel._main_loop = asyncio.get_event_loop() + channel._running = True + + msg = _make_chatbot_message( + text=text, + conversation_type=_CONVERSATION_TYPE_GROUP, + sender_staff_id="user_002", + conversation_id="conv_group_001", + message_id="msg_mention_cmd", + ) + + channel._send_running_reply = AsyncMock() + channel._on_chatbot_message(msg) + + await asyncio.sleep(0.1) + + bus.publish_inbound.assert_awaited_once() + inbound = bus.publish_inbound.await_args.args[0] + assert inbound.msg_type == InboundMessageType.COMMAND, f"{text!r} should be COMMAND" + assert not inbound.text.startswith("@"), "leading mention must be stripped for dispatch" + assert inbound.text.split(maxsplit=1)[0] in KNOWN_CHANNEL_COMMANDS + + _run(go()) + + def test_leading_mention_before_chat_keeps_mention(self): + """A mentioned non-command stays CHAT and keeps the mention for the agent.""" + + async def go(): + bus = MessageBus() + bus.publish_inbound = AsyncMock() + channel = DingTalkChannel(bus, config={}) + channel._client_id = "test_key" + channel._main_loop = asyncio.get_event_loop() + channel._running = True + + msg = _make_chatbot_message( + text="@bot please summarise this", + conversation_type=_CONVERSATION_TYPE_GROUP, + sender_staff_id="user_002", + conversation_id="conv_group_001", + message_id="msg_mention_chat", + ) + + channel._send_running_reply = AsyncMock() + channel._on_chatbot_message(msg) + + await asyncio.sleep(0.1) + + bus.publish_inbound.assert_awaited_once() + inbound = bus.publish_inbound.await_args.args[0] + assert inbound.msg_type == InboundMessageType.CHAT + assert inbound.text == "@bot please summarise this" + + _run(go()) + def test_group_message_integer_conversation_type_normalized(self): """SDK may deliver conversationType as int 2 — must still route as group."""