mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-30 17:33:24 +08:00
fix(dingtalk): handle command errors and rich-text mentions (#9389)
This commit is contained in:
@@ -189,9 +189,11 @@ class WakingCheckStage(Stage):
|
||||
break
|
||||
except Exception as e:
|
||||
await event.send(
|
||||
MessageEventResult().message(
|
||||
MessageEventResult()
|
||||
.message(
|
||||
f"插件 {star_map[handler.handler_module_path].name}: {e}",
|
||||
),
|
||||
)
|
||||
.use_markdown(False),
|
||||
)
|
||||
event.stop_event()
|
||||
passed = False
|
||||
|
||||
@@ -182,12 +182,15 @@ class DingtalkPlatformAdapter(Platform):
|
||||
abm.message_id = cast(str, message.message_id)
|
||||
abm.raw_message = message
|
||||
|
||||
leading_at_is_self = False
|
||||
if abm.type == MessageType.GROUP_MESSAGE:
|
||||
# 处理所有被 @ 的用户(包括机器人自己,因 at_users 已包含)
|
||||
if message.at_users:
|
||||
for user in message.at_users:
|
||||
for index, user in enumerate(message.at_users):
|
||||
if id := self._id_to_sid(user.dingtalk_id):
|
||||
abm.message.append(At(qq=id))
|
||||
if index == 0 and id == abm.self_id:
|
||||
leading_at_is_self = True
|
||||
abm.group_id = message.conversation_id
|
||||
abm.session_id = abm.group_id
|
||||
else:
|
||||
@@ -232,10 +235,18 @@ class DingtalkPlatformAdapter(Platform):
|
||||
)
|
||||
contents: list[dict] = cast(list[dict], rtc.rich_text_list)
|
||||
plain_parts: list[str] = []
|
||||
for content in contents:
|
||||
for index, content in enumerate(contents):
|
||||
if "text" in content:
|
||||
plain_text = cast(str, content.get("text") or "")
|
||||
if plain_text:
|
||||
# HarmonyOS repeats the leading bot mention as a text
|
||||
# segment even though atUsers already represents it.
|
||||
if (
|
||||
index == 0
|
||||
and leading_at_is_self
|
||||
and plain_text.lstrip().startswith("@")
|
||||
):
|
||||
continue
|
||||
plain_parts.append(plain_text)
|
||||
abm.message.append(Plain(plain_text))
|
||||
elif "type" in content and content["type"] == "picture":
|
||||
@@ -577,13 +588,17 @@ class DingtalkPlatformAdapter(Platform):
|
||||
text = segment.text.strip()
|
||||
if not text and not at_str:
|
||||
continue
|
||||
await send_message(
|
||||
msg_key="sampleMarkdown",
|
||||
msg_param={
|
||||
"title": "AstrBot",
|
||||
"text": f"{at_str} {text}".strip(),
|
||||
},
|
||||
)
|
||||
text = f"{at_str} {text}".strip()
|
||||
if message_chain.use_markdown_ is False:
|
||||
await send_message(
|
||||
msg_key="sampleText",
|
||||
msg_param={"content": text},
|
||||
)
|
||||
else:
|
||||
await send_message(
|
||||
msg_key="sampleMarkdown",
|
||||
msg_param={"title": "AstrBot", "text": text},
|
||||
)
|
||||
elif isinstance(segment, Image):
|
||||
photo_url = segment.file or segment.url or ""
|
||||
if photo_url.startswith(("http://", "https://")):
|
||||
|
||||
@@ -65,7 +65,7 @@ class CommandFilter(HandlerFilter):
|
||||
|
||||
def init_handler_md(self, handle_md: StarHandlerMetadata) -> None:
|
||||
self.handler_md = handle_md
|
||||
signature = inspect.signature(self.handler_md.handler)
|
||||
signature = inspect.signature(self.handler_md.handler, eval_str=True)
|
||||
self.handler_params = {} # 参数名 -> 参数类型,如果有默认值则为默认值
|
||||
idx = 0
|
||||
for k, v in signature.parameters.items():
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from astrbot.core.star.filter.command import CommandFilter
|
||||
|
||||
|
||||
async def postponed_annotations_handler(
|
||||
self,
|
||||
event,
|
||||
machine: str,
|
||||
retries: int = 1,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def test_command_filter_resolves_postponed_annotations():
|
||||
command_filter = CommandFilter(
|
||||
"probe",
|
||||
handler_md=SimpleNamespace(handler=postponed_annotations_handler),
|
||||
)
|
||||
|
||||
assert command_filter.handler_params == {"machine": str, "retries": 1}
|
||||
assert command_filter.validate_and_convert_params(
|
||||
["server-1", "2"],
|
||||
command_filter.handler_params,
|
||||
) == {"machine": "server-1", "retries": 2}
|
||||
|
||||
|
||||
def test_command_filter_rejects_missing_postponed_required_param():
|
||||
command_filter = CommandFilter(
|
||||
"probe",
|
||||
handler_md=SimpleNamespace(handler=postponed_annotations_handler),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="必要参数缺失"):
|
||||
command_filter.validate_and_convert_params([], command_filter.handler_params)
|
||||
@@ -1,8 +1,11 @@
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
import dingtalk_stream
|
||||
import pytest
|
||||
|
||||
from astrbot.api.message_components import At, Plain
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.platform.sources.dingtalk import dingtalk_adapter
|
||||
from astrbot.core.platform.sources.dingtalk.dingtalk_adapter import (
|
||||
DINGTALK_RECONNECT_INITIAL_DELAY,
|
||||
@@ -12,6 +15,29 @@ from astrbot.core.platform.sources.dingtalk.dingtalk_adapter import (
|
||||
)
|
||||
|
||||
|
||||
def _dingtalk_group_message(**payload) -> dingtalk_stream.ChatbotMessage:
|
||||
"""Build a DingTalk group callback message for adapter tests.
|
||||
|
||||
Args:
|
||||
**payload: Callback fields that vary between test cases.
|
||||
|
||||
Returns:
|
||||
A parsed DingTalk chatbot message.
|
||||
"""
|
||||
return dingtalk_stream.ChatbotMessage.from_dict(
|
||||
{
|
||||
"conversationId": "conversation",
|
||||
"conversationType": "2",
|
||||
"createAt": 1_700_000_000_000,
|
||||
"msgId": "message",
|
||||
"senderId": "sender",
|
||||
"senderNick": "sender",
|
||||
"chatbotUserId": "bot",
|
||||
**payload,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_dingtalk_reconnect_delay_uses_exponential_backoff():
|
||||
assert [_dingtalk_reconnect_delay(i) for i in range(1, 5)] == [
|
||||
10,
|
||||
@@ -76,3 +102,120 @@ async def test_dingtalk_reconnect_delay_wakes_on_terminate(monkeypatch):
|
||||
await adapter.terminate()
|
||||
run_task.cancel()
|
||||
await asyncio.gather(run_task, return_exceptions=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("use_markdown", "expected_key", "expected_param"),
|
||||
[
|
||||
(None, "sampleMarkdown", {"title": "AstrBot", "text": "first\nsecond"}),
|
||||
(False, "sampleText", {"content": "first\nsecond"}),
|
||||
],
|
||||
)
|
||||
async def test_dingtalk_text_respects_markdown_mode(
|
||||
use_markdown,
|
||||
expected_key,
|
||||
expected_param,
|
||||
):
|
||||
sent = []
|
||||
adapter = DingtalkPlatformAdapter.__new__(DingtalkPlatformAdapter)
|
||||
|
||||
async def capture_message(open_conversation_id, robot_code, msg_key, msg_param):
|
||||
sent.append((open_conversation_id, robot_code, msg_key, msg_param))
|
||||
|
||||
adapter._send_group_message = capture_message
|
||||
chain = MessageChain().message("first\nsecond").use_markdown(use_markdown)
|
||||
|
||||
await adapter._send_message_chain("group", "conversation", "robot", chain)
|
||||
|
||||
assert sent == [("conversation", "robot", expected_key, expected_param)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
{
|
||||
"atUsers": [{"dingtalkId": "bot"}],
|
||||
"isInAtList": True,
|
||||
"msgtype": "text",
|
||||
"text": {"content": " /server"},
|
||||
},
|
||||
{
|
||||
"atUsers": [{"dingtalkId": "bot"}],
|
||||
"isInAtList": True,
|
||||
"msgtype": "richText",
|
||||
"content": {
|
||||
"richText": [
|
||||
{"text": "@ExampleBot"},
|
||||
{"text": "/server"},
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_dingtalk_self_mention_produces_consistent_command_text(payload):
|
||||
adapter = DingtalkPlatformAdapter.__new__(DingtalkPlatformAdapter)
|
||||
|
||||
result = await adapter.convert_msg(_dingtalk_group_message(**payload))
|
||||
|
||||
assert result.message_str == "/server"
|
||||
assert len(result.message) == 2
|
||||
assert isinstance(result.message[0], At)
|
||||
assert result.message[0].qq == "bot"
|
||||
assert isinstance(result.message[1], Plain)
|
||||
assert result.message[1].text == "/server"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dingtalk_rich_text_preserves_non_self_mention_text():
|
||||
adapter = DingtalkPlatformAdapter.__new__(DingtalkPlatformAdapter)
|
||||
message = _dingtalk_group_message(
|
||||
atUsers=[{"dingtalkId": "another-user"}],
|
||||
isInAtList=False,
|
||||
msgtype="richText",
|
||||
content={
|
||||
"richText": [
|
||||
{"text": "@AnotherUser"},
|
||||
{"text": "/server"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
result = await adapter.convert_msg(message)
|
||||
|
||||
assert result.message_str == "@AnotherUser/server"
|
||||
assert len(result.message) == 3
|
||||
assert isinstance(result.message[0], At)
|
||||
assert result.message[0].qq == "another-user"
|
||||
assert isinstance(result.message[1], Plain)
|
||||
assert result.message[1].text == "@AnotherUser"
|
||||
assert isinstance(result.message[2], Plain)
|
||||
assert result.message[2].text == "/server"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dingtalk_rich_text_preserves_other_leading_mention():
|
||||
adapter = DingtalkPlatformAdapter.__new__(DingtalkPlatformAdapter)
|
||||
message = _dingtalk_group_message(
|
||||
atUsers=[{"dingtalkId": "another-user"}, {"dingtalkId": "bot"}],
|
||||
isInAtList=True,
|
||||
msgtype="richText",
|
||||
content={
|
||||
"richText": [
|
||||
{"text": "@AnotherUser"},
|
||||
{"text": "@ExampleBot"},
|
||||
{"text": "/server"},
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
result = await adapter.convert_msg(message)
|
||||
|
||||
assert result.message_str == "@AnotherUser@ExampleBot/server"
|
||||
assert isinstance(result.message[0], At)
|
||||
assert result.message[0].qq == "another-user"
|
||||
assert isinstance(result.message[1], At)
|
||||
assert result.message[1].qq == "bot"
|
||||
assert isinstance(result.message[2], Plain)
|
||||
assert result.message[2].text == "@AnotherUser"
|
||||
|
||||
Reference in New Issue
Block a user