From 9221f6e74afa311f01c161b91a56a9565ade5f90 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 11 Aug 2026 19:48:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20=E5=B7=A5=E5=85=B7=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E6=B6=88=E6=81=AF=E5=9B=9E=E5=A1=AB=E5=8E=9F=E4=BC=9A?= =?UTF-8?q?=E8=AF=9DID=EF=BC=8C=E9=81=BF=E5=85=8D=E7=BE=A4=E8=81=8A@?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BA=E6=97=B6=E6=8C=89=E9=92=AE=E7=AD=89?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E6=B6=88=E6=81=AF=E5=8F=91=E5=88=B0=E7=A7=81?= =?UTF-8?q?=E8=81=8A=20(#6267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/agent/__init__.py | 3 + app/agent/tools/base.py | 10 ++ tests/test_agent_tool_message_target.py | 127 ++++++++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 tests/test_agent_tool_message_target.py diff --git a/app/agent/__init__.py b/app/agent/__init__.py index dd44e3e6d..d797a3f2d 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -722,6 +722,9 @@ class MoviePilotAgent: "reply_mode": None, "should_dispatch_reply": should_dispatch_reply, "is_admin": await self._is_system_admin_context(), + # 工具回调消息需要发回原会话(群聊@机器人时按钮选择等卡片不能发到私聊), + # 后台任务无渠道上下文时置空,交由通知链广播。 + "original_chat_id": None if self.is_background else self.original_chat_id, } def _should_stream(self) -> bool: diff --git a/app/agent/tools/base.py b/app/agent/tools/base.py index 0adbde982..bed361aed 100644 --- a/app/agent/tools/base.py +++ b/app/agent/tools/base.py @@ -658,6 +658,16 @@ class MoviePilotTool(BaseTool, metaclass=ABCMeta): "original_chat_id": None, } ) + elif not notification.original_chat_id: + # 工具回调消息默认回填当前会话的原会话 ID, + # 保证群聊 @ 机器人时按钮选择、消息发送等交互消息回复到原群,而不是私聊窗口。 + original_chat_id = str( + self._agent_context.get("original_chat_id") or "" + ).strip() or None + if original_chat_id: + notification = notification.model_copy( + update={"original_chat_id": original_chat_id} + ) await ToolChain().async_post_message(notification) diff --git a/tests/test_agent_tool_message_target.py b/tests/test_agent_tool_message_target.py new file mode 100644 index 000000000..114e2eb66 --- /dev/null +++ b/tests/test_agent_tool_message_target.py @@ -0,0 +1,127 @@ +"""Agent 工具回调消息回复目标(original_chat_id 回填)的测试。""" + +import asyncio +from unittest.mock import AsyncMock, patch + +from app.agent.tools.impl.ask_user_choice import ( + AskUserChoiceTool, + UserChoiceOptionInput, +) +from app.agent.tools.impl.send_message import SendMessageTool +from app.schemas import Notification +from app.schemas.types import MessageChannel + + +def _run_choice_tool(agent_context: dict, channel: str, source: str) -> Notification: + """运行按钮选择工具并返回其发送的通知。""" + tool = AskUserChoiceTool(session_id="session-1", user_id="ou_xxx") + tool.set_message_attr( + channel=channel, + source=source, + username="tester", + ) + tool.set_agent_context(agent_context=agent_context) + + with patch( + "app.agent.tools.base.ToolChain.async_post_message", + new=AsyncMock(), + ) as async_post_message: + asyncio.run( + tool.run( + message="请选择", + options=[UserChoiceOptionInput(label="继续", value="继续")], + ) + ) + + assert async_post_message.await_count == 1 + return async_post_message.await_args.args[0] + + +def test_choice_tool_backfills_original_chat_id_from_session_context(): + """群聊场景下按钮选择通知应回填会话上下文中的 original_chat_id。""" + notification = _run_choice_tool( + agent_context={"original_chat_id": "oc_group_123"}, + channel=MessageChannel.Feishu.value, + source="feishu-test", + ) + + assert notification.original_chat_id == "oc_group_123" + assert notification.userid == "ou_xxx" + + +def test_choice_tool_keeps_explicit_original_chat_id(): + """按钮选择通知已显式携带原会话 ID 时不应被上下文覆盖。""" + notification = _run_choice_tool( + agent_context={"original_chat_id": "oc_group_zzz"}, + channel=MessageChannel.Telegram.value, + source="telegram-test", + ) + + assert notification.original_chat_id == "oc_group_zzz" + + +def test_choice_tool_no_context_does_not_backfill(): + """会话上下文未携带原会话 ID 时,通知保持原有发送目标。""" + notification = _run_choice_tool( + agent_context={}, + channel=MessageChannel.Telegram.value, + source="telegram-test", + ) + + assert notification.original_chat_id is None + + +def test_background_tool_clears_original_chat_id(): + """无渠道上下文的后台任务应清空渠道定位信息交由消息链广播。""" + tool = SendMessageTool(session_id="session-1", user_id="ou_xxx") + tool.set_agent_context(agent_context={"original_chat_id": "oc_group_123"}) + + with patch( + "app.agent.tools.base.ToolChain.async_post_message", + new=AsyncMock(), + ) as async_post_message: + asyncio.run(tool.send_tool_message("后台任务执行完成")) + + notification = async_post_message.await_args.args[0] + assert notification.original_chat_id is None + assert notification.channel is None + assert notification.userid is None + + +def test_send_tool_message_backfills_original_chat_id(): + """send_tool_message 工具消息同样应回填原会话 ID。""" + tool = SendMessageTool(session_id="session-1", user_id="ou_xxx") + tool.set_message_attr( + channel=MessageChannel.Feishu.value, + source="feishu-test", + username="tester", + ) + tool.set_agent_context(agent_context={"original_chat_id": "oc_group_123"}) + + with patch( + "app.agent.tools.base.ToolChain.async_post_message", + new=AsyncMock(), + ) as async_post_message: + asyncio.run(tool.send_tool_message("正在执行操作")) + + notification = async_post_message.await_args.args[0] + assert notification.original_chat_id == "oc_group_123" + + +def test_tool_context_includes_original_chat_id(): + """工具共享上下文应携带当前会话的原会话 ID。""" + from app.agent import MoviePilotAgent + + agent = MoviePilotAgent( + session_id="session-1", + user_id="ou_xxx", + channel=MessageChannel.Feishu.value, + source="feishu-test", + username="tester", + original_chat_id="oc_group_123", + ) + + with patch.object(MoviePilotAgent, "_is_system_admin_context", return_value=False): + context = asyncio.run(agent._build_tool_context(should_dispatch_reply=True)) + + assert context["original_chat_id"] == "oc_group_123" \ No newline at end of file