mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
feat(telegram): support rich Agent messages
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""Agent Telegram Rich Message 契约测试。"""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.agent.orchestrator import MoviePilotAgent
|
||||
from app.agent.prompt import prompt_manager
|
||||
from app.agent.tools.impl.send_message import SendMessageInput, SendMessageTool
|
||||
from app.chain.agent import AgentChain
|
||||
from app.schemas.types import NotificationChannel
|
||||
|
||||
|
||||
def test_send_message_input_accepts_rich_message_only() -> None:
|
||||
"""Rich Message 本身应能构成完整的工具载荷。"""
|
||||
payload = SendMessageInput(rich_message="# 结果\n\n- 成功")
|
||||
|
||||
assert payload.message is None
|
||||
assert payload.rich_message == "# 结果\n\n- 成功"
|
||||
|
||||
|
||||
def test_send_message_tool_keeps_plain_fallback_for_rich_message() -> None:
|
||||
"""工具应同时保留跨渠道文本回退和 Telegram Rich Markdown。"""
|
||||
|
||||
async def _run():
|
||||
tool = SendMessageTool(session_id="session-1", user_id="10001")
|
||||
tool.set_message_attr(
|
||||
channel=NotificationChannel.Telegram.value,
|
||||
source="telegram-test",
|
||||
username="tester",
|
||||
)
|
||||
tool.set_agent_context(agent_context={})
|
||||
with patch(
|
||||
"app.agent.tools.base.ToolChain.async_post_message",
|
||||
new_callable=AsyncMock,
|
||||
) as async_post_message:
|
||||
result = await tool.run(rich_message="# 结果\n\n- **成功**")
|
||||
return result, async_post_message
|
||||
|
||||
result, async_post_message = asyncio.run(_run())
|
||||
message = async_post_message.await_args.args[0]
|
||||
|
||||
assert result == "消息已发送"
|
||||
assert message.text == "# 结果\n\n- **成功**"
|
||||
assert message.rich_message == "# 结果\n\n- **成功**"
|
||||
assert message.save_history is False
|
||||
|
||||
|
||||
def test_agent_prompt_prefers_rich_message_only_for_telegram() -> None:
|
||||
"""仅 Telegram 会话应收到 Rich Message 优先提示。"""
|
||||
telegram_prompt = prompt_manager.get_agent_prompt(
|
||||
channel=NotificationChannel.Telegram.value
|
||||
)
|
||||
wechat_prompt = prompt_manager.get_agent_prompt(
|
||||
channel=NotificationChannel.Wechat.value
|
||||
)
|
||||
|
||||
assert "`rich_message` argument" in telegram_prompt
|
||||
assert "GitHub-style Markdown" in telegram_prompt
|
||||
assert "`rich_message` argument" not in wechat_prompt
|
||||
|
||||
|
||||
def test_agent_direct_telegram_reply_uses_rich_message() -> None:
|
||||
"""Agent 常规 Telegram 回复也应自动携带 Rich Markdown。"""
|
||||
|
||||
async def _run():
|
||||
agent = MoviePilotAgent(session_id="telegram-session", user_id="10001")
|
||||
agent.channel = NotificationChannel.Telegram.value
|
||||
agent.source = "telegram-test"
|
||||
agent.username = "tester"
|
||||
with patch.object(
|
||||
AgentChain,
|
||||
"async_post_message",
|
||||
new_callable=AsyncMock,
|
||||
) as async_post_message:
|
||||
await agent.send_agent_message("# 结果\n\n- **完成**")
|
||||
return async_post_message
|
||||
|
||||
async_post_message = asyncio.run(_run())
|
||||
message = async_post_message.await_args.args[0]
|
||||
|
||||
assert message.text == "# 结果\n\n- **完成**"
|
||||
assert message.rich_message == "# 结果\n\n- **完成**"
|
||||
@@ -365,7 +365,10 @@ class TestAgentToolStreaming:
|
||||
|
||||
assert run_in_threadpool_mock.await_count == 1
|
||||
assert run_in_threadpool_mock.await_args.args[0].__name__ == "send_direct_message"
|
||||
assert run_in_threadpool_mock.await_args.args[1].mtype == MessageType.Agent
|
||||
notification = run_in_threadpool_mock.await_args.args[1]
|
||||
assert notification.mtype == MessageType.Agent
|
||||
assert notification.text == "hello"
|
||||
assert notification.rich_message == "hello"
|
||||
assert handler.has_sent_message
|
||||
|
||||
def test_flush_edits_message_via_threadpool(self):
|
||||
@@ -392,6 +395,12 @@ class TestAgentToolStreaming:
|
||||
|
||||
assert run_in_threadpool_mock.await_count == 1
|
||||
assert run_in_threadpool_mock.await_args.args[0].__name__ == "edit_message"
|
||||
assert (
|
||||
run_in_threadpool_mock.await_args.kwargs["metadata"][
|
||||
"telegram_rich_message"
|
||||
]
|
||||
== "hello world"
|
||||
)
|
||||
assert handler._sent_text == "hello world"
|
||||
|
||||
def test_stop_streaming_waits_inflight_initial_flush_before_final_edit(self):
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_modified_builtin_skills_have_incremented_versions() -> None:
|
||||
expected_versions = {
|
||||
"database-operation": "4",
|
||||
"moviepilot-api": "13",
|
||||
"moviepilot-cli": "7",
|
||||
"moviepilot-cli": "8",
|
||||
"moviepilot-update": "3",
|
||||
"organize-files": "3",
|
||||
"transfer-failed-retry": "4",
|
||||
|
||||
@@ -54,6 +54,25 @@ def test_send_msg_success(telegram):
|
||||
assert result and result.get("success")
|
||||
|
||||
|
||||
def test_edit_msg_with_rich_message(telegram):
|
||||
"""Telegram 流式编辑应继续使用 Rich Message 协议。"""
|
||||
telegram._bot.edit_message_text.return_value = SimpleNamespace(message_id=101)
|
||||
|
||||
result = telegram.edit_msg(
|
||||
chat_id="10001",
|
||||
message_id=101,
|
||||
text="# 旧回退",
|
||||
rich_message="# 流式结果\n\n- **完成**",
|
||||
)
|
||||
|
||||
assert result is True
|
||||
kwargs = telegram._bot.edit_message_text.call_args.kwargs
|
||||
assert kwargs["text"] is None
|
||||
assert kwargs["rich_message"].html == (
|
||||
'<h1>流式结果</h1><ul><li><b>完成</b></li></ul>'
|
||||
)
|
||||
|
||||
|
||||
def test_telegram_parser_preserves_reply_to_message_id():
|
||||
"""Telegram ForceReply 回复应保留来源消息和被回复消息的 message_id。"""
|
||||
module = TelegramModule()
|
||||
@@ -331,6 +350,50 @@ def test_send_msg_with_html_parse_mode_keeps_html(telegram):
|
||||
)
|
||||
|
||||
|
||||
def test_send_msg_uses_rich_message_api(telegram):
|
||||
"""Rich Markdown 应转换后通过 Telegram sendRichMessage 发送。"""
|
||||
telegram.bot.send_rich_message.return_value = SimpleNamespace(
|
||||
message_id=101,
|
||||
chat=SimpleNamespace(id=10001),
|
||||
)
|
||||
|
||||
result = telegram.send_msg(
|
||||
title="",
|
||||
rich_message=(
|
||||
"# 处理完成\n\n"
|
||||
"| 项目 | 结果 |\n"
|
||||
"| --- | --- |\n"
|
||||
"| 下载 | **成功** |"
|
||||
),
|
||||
buttons=[[{"text": "查看", "url": "https://example.com"}]],
|
||||
)
|
||||
|
||||
assert result == {"success": True, "message_id": 101, "chat_id": 10001}
|
||||
telegram.bot.send_message.assert_not_called()
|
||||
send_kwargs = telegram.bot.send_rich_message.call_args.kwargs
|
||||
assert send_kwargs["chat_id"] == "fake_chat_id"
|
||||
assert send_kwargs["rich_message"].markdown is None
|
||||
assert "<h1>处理完成</h1>" in send_kwargs["rich_message"].html
|
||||
assert "<table>" in send_kwargs["rich_message"].html
|
||||
assert send_kwargs["reply_markup"] is not None
|
||||
|
||||
|
||||
def test_send_msg_edits_rich_message(telegram):
|
||||
"""带原消息定位信息的 Rich Message 应使用富文本编辑接口。"""
|
||||
result = telegram.send_msg(
|
||||
title="",
|
||||
rich_message="# 更新结果\n\n- 已完成",
|
||||
original_message_id=101,
|
||||
original_chat_id="10001",
|
||||
)
|
||||
|
||||
assert result == {"success": True, "message_id": 101, "chat_id": "10001"}
|
||||
edit_kwargs = telegram.bot.edit_message_text.call_args.kwargs
|
||||
assert edit_kwargs["text"] is None
|
||||
assert edit_kwargs["message_id"] == 101
|
||||
assert "<h1>更新结果</h1>" in edit_kwargs["rich_message"].html
|
||||
|
||||
|
||||
def test_telegram_module_passes_parse_mode_to_client():
|
||||
"""模块发送通知时应透传消息指定的parse_mode"""
|
||||
module = TelegramModule()
|
||||
@@ -359,6 +422,59 @@ def test_telegram_module_passes_parse_mode_to_client():
|
||||
assert client.send_msg.call_args.kwargs["parse_mode"] == "HTML"
|
||||
|
||||
|
||||
def test_telegram_module_passes_rich_message_to_client():
|
||||
"""Telegram 模块应把消息模型中的 Rich Markdown 透传给客户端。"""
|
||||
module = TelegramModule()
|
||||
client = Mock()
|
||||
|
||||
with patch.object(
|
||||
module,
|
||||
"get_configs",
|
||||
return_value={"telegram-test": SimpleNamespace(name="telegram-test")},
|
||||
), patch.object(
|
||||
module, "check_message", return_value=True
|
||||
), patch.object(
|
||||
module, "get_instance", return_value=client
|
||||
):
|
||||
module.post_message(
|
||||
Message(
|
||||
channel=NotificationChannel.Telegram,
|
||||
source="telegram-test",
|
||||
rich_message="# 智能体回复\n\n- 已完成",
|
||||
)
|
||||
)
|
||||
|
||||
client.send_msg.assert_called_once()
|
||||
assert client.send_msg.call_args.kwargs["rich_message"].startswith("# 智能体回复")
|
||||
|
||||
|
||||
def test_telegram_module_passes_streaming_rich_message_to_edit_client():
|
||||
"""Telegram 模块应把流式 Rich Markdown 元数据透传给编辑客户端。"""
|
||||
module = TelegramModule()
|
||||
module._channel = NotificationChannel.Telegram
|
||||
client = Mock()
|
||||
client.edit_msg.return_value = True
|
||||
|
||||
with patch.object(
|
||||
module,
|
||||
"get_configs",
|
||||
return_value={"telegram-test": SimpleNamespace(name="telegram-test")},
|
||||
), patch.object(
|
||||
module, "get_instance", return_value=client
|
||||
):
|
||||
result = module.edit_message(
|
||||
channel=NotificationChannel.Telegram,
|
||||
source="telegram-test",
|
||||
message_id=101,
|
||||
chat_id="10001",
|
||||
text="# 普通回退",
|
||||
metadata={"telegram_rich_message": "# 流式富文本"},
|
||||
)
|
||||
|
||||
assert result is True
|
||||
assert client.edit_msg.call_args.kwargs["rich_message"] == "# 流式富文本"
|
||||
|
||||
|
||||
def test_telegram_module_plain_post_message_keeps_chat_without_editing_source_message():
|
||||
"""普通通知应保留原会话目标,同时避免把来源消息 ID 当成编辑目标。"""
|
||||
module = TelegramModule()
|
||||
|
||||
Reference in New Issue
Block a user