feat(dingtalk): prefix group replies with sender mention

In group chats, prefix the outbound markdown reply with an H1 naming the sender (# @<nick>) so the addressed user can spot it in a busy group. Private replies are sent verbatim.

Visual only: DingTalk markdown robot messages do not push real @ notifications (that would require staffId plumbing and a different message type). sender_name is read from OutboundMessage.metadata, which the agent loop already propagates from inbound metadata.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
李明振
2026-07-27 02:33:41 +08:00
committed by Xubin Ren
co-authored by Claude
parent 14e692e40d
commit 205889f9e0
2 changed files with 60 additions and 2 deletions
+9 -2
View File
@@ -713,8 +713,15 @@ class DingTalkChannel(BaseChannel):
if not token:
raise RuntimeError("DingTalk access token unavailable")
if msg.content and msg.content.strip():
if not await self._send_markdown_text(token, msg.chat_id, msg.content.strip()):
content = msg.content.strip() if msg.content else ""
if content:
# In group chats, prefix the reply with a markdown header naming the
# sender so the addressed user can spot the reply. Visual only —
# DingTalk's markdown robot messages do not push real @ notifications.
sender_name = msg.metadata.get("sender_name") if msg.metadata else None
if msg.chat_id.startswith("group:") and sender_name:
content = f"# @{sender_name}\n\n{content}"
if not await self._send_markdown_text(token, msg.chat_id, content):
raise RuntimeError("DingTalk text message was not delivered")
for media_ref in msg.media or []:
@@ -1,4 +1,5 @@
import asyncio
import json
import zipfile
from io import BytesIO
from types import SimpleNamespace
@@ -252,6 +253,56 @@ async def test_group_send_uses_group_messages_api() -> None:
assert call["json"]["msgKey"] == "sampleMarkdown"
@pytest.mark.asyncio
async def test_group_send_prepends_sender_mention(monkeypatch) -> None:
"""Group replies are prefixed with a markdown header naming the sender."""
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
channel = DingTalkChannel(config, MessageBus())
channel._http = _FakeHttp()
async def _fake_token() -> str:
return "token"
monkeypatch.setattr(channel, "_get_access_token", _fake_token)
await channel.send(
OutboundMessage(
channel="dingtalk",
chat_id="group:conv123",
content="hello",
metadata={"sender_name": "Alice"},
)
)
sent_text = json.loads(channel._http.calls[0]["json"]["msgParam"])["text"]
assert sent_text == "# @Alice\n\nhello"
@pytest.mark.asyncio
async def test_private_send_does_not_prepend_mention(monkeypatch) -> None:
"""Private replies are sent verbatim, without the sender header."""
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
channel = DingTalkChannel(config, MessageBus())
channel._http = _FakeHttp()
async def _fake_token() -> str:
return "token"
monkeypatch.setattr(channel, "_get_access_token", _fake_token)
await channel.send(
OutboundMessage(
channel="dingtalk",
chat_id="user1", # private chat: no "group:" prefix
content="hello",
metadata={"sender_name": "Alice"},
)
)
sent_text = json.loads(channel._http.calls[0]["json"]["msgParam"])["text"]
assert sent_text == "hello"
@pytest.mark.asyncio
async def test_handler_uses_voice_recognition_text_when_text_is_empty(monkeypatch) -> None:
bus = MessageBus()