Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions backend/app/channels/dingtalk.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
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
Expand Down Expand Up @@ -428,8 +428,16 @@ def _on_chatbot_message(self, message: Any) -> None:
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

Expand Down
65 changes: 65 additions & 0 deletions backend/tests/test_dingtalk_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,71 @@ async def go():

_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."""

Expand Down
Loading