From fa939dfbe67f2d4f27fafb0d9bd0412f6d0ad07b Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 13 May 2026 06:56:03 +0800 Subject: [PATCH] fix(agent): reply Feishu agent streams with cards --- app/agent/__init__.py | 16 +++++++++++++++ app/agent/callback/__init__.py | 10 ++++++++++ app/api/endpoints/openai.py | 4 ++++ app/chain/message.py | 8 ++++++++ app/modules/feishu/feishu.py | 32 +++++++++++++++++++----------- tests/test_agent_tool_streaming.py | 27 +++++++++++++++++++++++++ tests/test_feishu.py | 24 ++++++++++++++++++++++ 7 files changed, 109 insertions(+), 12 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 08c96f7b..cd2a2759 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -179,6 +179,8 @@ class MoviePilotAgent: channel: str = None, source: str = None, username: str = None, + original_message_id: Optional[str] = None, + original_chat_id: Optional[str] = None, replay_mode: ReplyMode = ReplyMode.DISPATCH, persist_output_message: bool = True, allow_message_tools: bool = True, @@ -189,6 +191,8 @@ class MoviePilotAgent: self.channel = channel self.source = source self.username = username + self.original_message_id = original_message_id + self.original_chat_id = original_chat_id self.reply_mode = replay_mode self.persist_output_message = persist_output_message self.allow_message_tools = allow_message_tools @@ -619,6 +623,8 @@ class MoviePilotAgent: source=self.source, user_id=self.user_id, username=self.username, + original_message_id=self.original_message_id, + original_chat_id=self.original_chat_id, ) # 流式运行智能体,token 直接推送到 stream_handler @@ -794,6 +800,8 @@ class _MessageTask: channel: Optional[str] = None source: Optional[str] = None username: Optional[str] = None + original_message_id: Optional[str] = None + original_chat_id: Optional[str] = None reply_mode: ReplyMode = ReplyMode.DISPATCH @@ -878,6 +886,8 @@ class AgentManager: channel: str = None, source: str = None, username: str = None, + original_message_id: Optional[str] = None, + original_chat_id: Optional[str] = None, reply_mode: ReplyMode = ReplyMode.DISPATCH, ) -> str: """ @@ -893,6 +903,8 @@ class AgentManager: channel=channel, source=source, username=username, + original_message_id=original_message_id, + original_chat_id=original_chat_id, reply_mode=reply_mode, ) @@ -980,6 +992,8 @@ class AgentManager: channel=task.channel, source=task.source, username=task.username, + original_message_id=task.original_message_id, + original_chat_id=task.original_chat_id, replay_mode=task.reply_mode, ) self.active_agents[session_id] = agent @@ -992,6 +1006,8 @@ class AgentManager: agent.source = task.source if task.username: agent.username = task.username + agent.original_message_id = task.original_message_id + agent.original_chat_id = task.original_chat_id agent.reply_mode = task.reply_mode return await agent.process(task.message, images=task.images, files=task.files) diff --git a/app/agent/callback/__init__.py b/app/agent/callback/__init__.py index b2a378d6..71c30744 100644 --- a/app/agent/callback/__init__.py +++ b/app/agent/callback/__init__.py @@ -61,6 +61,8 @@ class StreamingHandler: self._source: Optional[str] = None self._user_id: Optional[str] = None self._username: Optional[str] = None + self._original_message_id: Optional[str] = None + self._original_chat_id: Optional[str] = None self._title: str = "" self._allow_dispatch_without_context = False # 非啰嗦模式下的待输出工具统计,等下一段文本到来时再统一补一句摘要 @@ -147,6 +149,8 @@ class StreamingHandler: source: Optional[str] = None, user_id: Optional[str] = None, username: Optional[str] = None, + original_message_id: Optional[str] = None, + original_chat_id: Optional[str] = None, title: str = "", ): """ @@ -163,6 +167,8 @@ class StreamingHandler: self._source = source self._user_id = user_id self._username = username + self._original_message_id = original_message_id + self._original_chat_id = original_chat_id self._title = title self._streaming_enabled = True @@ -472,6 +478,8 @@ class StreamingHandler: mtype=NotificationType.Agent, userid=self._user_id, username=self._username, + original_message_id=self._original_message_id, + original_chat_id=self._original_chat_id, title=self._title, text=current_text, ), @@ -515,6 +523,8 @@ class StreamingHandler: mtype=NotificationType.Agent, userid=self._user_id, username=self._username, + original_message_id=self._original_message_id, + original_chat_id=self._original_chat_id, title=self._title, text=current_text, ), diff --git a/app/api/endpoints/openai.py b/app/api/endpoints/openai.py index 6eb12e46..21a35b82 100644 --- a/app/api/endpoints/openai.py +++ b/app/api/endpoints/openai.py @@ -85,12 +85,16 @@ class _OpenAIStreamingHandler(StreamingHandler): source: Optional[str] = None, user_id: Optional[str] = None, username: Optional[str] = None, + original_message_id: Optional[str] = None, + original_chat_id: Optional[str] = None, title: str = "", ): self._channel = channel self._source = source self._user_id = user_id self._username = username + self._original_message_id = original_message_id + self._original_chat_id = original_chat_id self._title = title self._streaming_enabled = True self._sent_text = "" diff --git a/app/chain/message.py b/app/chain/message.py index 1da5072a..62ba34d5 100644 --- a/app/chain/message.py +++ b/app/chain/message.py @@ -269,6 +269,8 @@ class MessageChain(ChainBase): source=source, userid=userid, username=username, + original_message_id=original_message_id, + original_chat_id=original_chat_id, images=images, files=files, ) @@ -284,6 +286,8 @@ class MessageChain(ChainBase): source=source, userid=userid, username=username, + original_message_id=original_message_id, + original_chat_id=original_chat_id, images=images, files=files, ) @@ -1055,6 +1059,8 @@ class MessageChain(ChainBase): source: str, userid: Union[str, int], username: str, + original_message_id: Optional[Union[str, int]] = None, + original_chat_id: Optional[str] = None, images: Optional[List[CommingMessage.MessageImage]] = None, files: Optional[List[CommingMessage.MessageAttachment]] = None, session_id: Optional[str] = None, @@ -1168,6 +1174,8 @@ class MessageChain(ChainBase): channel=channel.value if channel else None, source=source, username=username, + original_message_id=str(original_message_id) if original_message_id else None, + original_chat_id=original_chat_id, ), global_vars.loop, ) diff --git a/app/modules/feishu/feishu.py b/app/modules/feishu/feishu.py index a4a9b48a..f4a60ac6 100644 --- a/app/modules/feishu/feishu.py +++ b/app/modules/feishu/feishu.py @@ -652,21 +652,29 @@ class Feishu: userid: Optional[str] = None, chat_id: Optional[str] = None, receive_id_type: Optional[str] = None, + original_message_id: Optional[str] = None, ) -> Optional[dict]: card_id = self._create_streaming_card(title=title, text=text) if not card_id: return None - receive_id, resolved_receive_id_type = self._resolve_target( - userid=userid, - chat_id=chat_id, - receive_id_type=receive_id_type, - ) - result = self._send_message( - receive_id, - resolved_receive_id_type, - "interactive", - {"type": "card", "data": {"card_id": card_id}}, - ) + if original_message_id: + result = self._reply_message( + message_id=original_message_id, + msg_type="interactive", + content={"type": "card", "data": {"card_id": card_id}}, + ) + else: + receive_id, resolved_receive_id_type = self._resolve_target( + userid=userid, + chat_id=chat_id, + receive_id_type=receive_id_type, + ) + result = self._send_message( + receive_id, + resolved_receive_id_type, + "interactive", + {"type": "card", "data": {"card_id": card_id}}, + ) if not result: return None result["metadata"] = { @@ -1111,7 +1119,6 @@ class Feishu: message.mtype == NotificationType.Agent and not message.buttons and not message.link - and not original_message_id ) if is_streaming_agent_text: try: @@ -1121,6 +1128,7 @@ class Feishu: userid=userid, chat_id=chat_id, receive_id_type=receive_id_type, + original_message_id=original_message_id, ) except Exception as err: logger.error(f"飞书流式卡片发送失败:{err}") diff --git a/tests/test_agent_tool_streaming.py b/tests/test_agent_tool_streaming.py index 2e8f9631..11fe9dc6 100644 --- a/tests/test_agent_tool_streaming.py +++ b/tests/test_agent_tool_streaming.py @@ -263,6 +263,33 @@ class TestAgentToolStreaming(unittest.TestCase): ) self.assertTrue(handler.has_sent_message) + def test_flush_passes_original_message_context_to_send_direct_message(self): + handler = StreamingHandler() + handler._channel = MessageChannel.Feishu.value + handler._source = "feishu-main" + handler._user_id = "ou_user" + handler._username = "tester" + handler._original_message_id = "om_origin" + handler._original_chat_id = "oc_origin" + handler._streaming_enabled = True + handler.emit("hello") + + with patch( + "app.agent.callback.run_in_threadpool", new_callable=AsyncMock + ) as run_in_threadpool_mock: + run_in_threadpool_mock.return_value = MessageResponse( + message_id="om_stream", + chat_id="oc_origin", + source="feishu-main", + success=True, + ) + + asyncio.run(handler._flush()) + + notification = run_in_threadpool_mock.await_args.args[1] + self.assertEqual(notification.original_message_id, "om_origin") + self.assertEqual(notification.original_chat_id, "oc_origin") + def test_verbose_background_tool_call_does_not_post_message(self): async def _run(): tool = DummyTool(session_id="session-1", user_id="10001") diff --git a/tests/test_feishu.py b/tests/test_feishu.py index b869fa14..2b2ebd8d 100644 --- a/tests/test_feishu.py +++ b/tests/test_feishu.py @@ -301,6 +301,30 @@ class TestFeishu(unittest.TestCase): self.assertEqual(message_request.request_body.msg_type, "interactive") self.assertEqual(json.loads(message_request.request_body.content)["data"]["card_id"], "card_stream") + def test_send_notification_replies_with_streaming_card_for_agent_text(self): + client = self._build_client() + client._api_client, message_api = self._build_message_api( + reply_response=self._success_response(message_id="om_reply", chat_id="oc_stream"), + card_create_response=self._card_create_success_response("card_stream"), + ) + + result = client.send_notification( + Notification( + mtype=NotificationType.Agent, + title="MoviePilot助手", + text="第一帧内容", + ), + userid="ou_user_stream", + original_message_id="om_origin", + ) + + self.assertTrue(result["success"]) + message_api.create.assert_not_called() + reply_request = message_api.reply.call_args.args[0] + self.assertEqual(reply_request.message_id, "om_origin") + self.assertEqual(reply_request.request_body.msg_type, "interactive") + self.assertEqual(json.loads(reply_request.request_body.content)["data"]["card_id"], "card_stream") + def test_edit_message_uses_cardkit_content_for_streaming_card(self): client = self._build_client() client._api_client, message_api = self._build_message_api(