fix(agent): reply Feishu agent streams with cards

This commit is contained in:
jxxghp
2026-05-13 06:56:03 +08:00
parent 77aa65bfdc
commit fa939dfbe6
7 changed files with 109 additions and 12 deletions

View File

@@ -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)

View File

@@ -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,
),

View File

@@ -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 = ""

View File

@@ -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,
)

View File

@@ -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}")

View File

@@ -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")

View File

@@ -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(