mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-22 07:56:48 +00:00
Feature/feishu receive file (#1608)
* feat(feishu): add channel file materialization hook for inbound messages - Introduce Channel.receive_file(msg, thread_id) as a base method for file materialization; default is no-op. - Implement FeishuChannel.receive_file to download files/images from Feishu messages, save to sandbox, and inject virtual paths into msg.text. - Update ChannelManager to call receive_file for any channel if msg.files is present, enabling downstream model access to user-uploaded files. - No impact on Slack/Telegram or other channels (they inherit the default no-op). * style(backend): format code with ruff for lint compliance - Auto-formatted packages/harness/deerflow/agents/factory.py and tests/test_create_deerflow_agent.py using `ruff format` - Ensured both files conform to project linting standards - Fixes CI lint check failures caused by code style issues * fix(feishu): handle file write operation asynchronously to prevent blocking * fix(feishu): rename GetMessageResourceRequest to _GetMessageResourceRequest and remove redundant code * test(feishu): add tests for receive_file method and placeholder replacement * fix(manager): remove unnecessary type casting for channel retrieval * fix(feishu): update logging messages to reflect resource handling instead of image * fix(feishu): sanitize filename by replacing invalid characters in file uploads * fix(feishu): improve filename sanitization and reorder image key handling in message processing * fix(feishu): add thread lock to prevent filename conflicts during file downloads * fix(test): correct bad merge in test_feishu_parser.py * chore: run ruff and apply formatting cleanup fix(feishu): preserve rich-text attachment order and improve fallback filename handling
This commit is contained in:
@@ -1,11 +1,20 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.channels.commands import KNOWN_CHANNEL_COMMANDS
|
||||
from app.channels.feishu import FeishuChannel
|
||||
from app.channels.message_bus import MessageBus
|
||||
from app.channels.message_bus import InboundMessage, MessageBus
|
||||
|
||||
|
||||
def _run(coro):
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
return loop.run_until_complete(coro)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def test_feishu_on_message_plain_text():
|
||||
@@ -71,6 +80,64 @@ def test_feishu_on_message_rich_text():
|
||||
assert "\n\n" in parsed_text
|
||||
|
||||
|
||||
def test_feishu_receive_file_replaces_placeholders_in_order():
|
||||
async def go():
|
||||
bus = MessageBus()
|
||||
channel = FeishuChannel(bus, {"app_id": "test", "app_secret": "test"})
|
||||
|
||||
msg = InboundMessage(
|
||||
channel_name="feishu",
|
||||
chat_id="chat_1",
|
||||
user_id="user_1",
|
||||
text="before [image] middle [file] after",
|
||||
thread_ts="msg_1",
|
||||
files=[{"image_key": "img_key"}, {"file_key": "file_key"}],
|
||||
)
|
||||
|
||||
channel._receive_single_file = AsyncMock(side_effect=["/mnt/user-data/uploads/a.png", "/mnt/user-data/uploads/b.pdf"])
|
||||
|
||||
result = await channel.receive_file(msg, "thread_1")
|
||||
|
||||
assert result.text == "before /mnt/user-data/uploads/a.png middle /mnt/user-data/uploads/b.pdf after"
|
||||
|
||||
_run(go())
|
||||
|
||||
|
||||
def test_feishu_on_message_extracts_image_and_file_keys():
|
||||
bus = MessageBus()
|
||||
channel = FeishuChannel(bus, {"app_id": "test", "app_secret": "test"})
|
||||
|
||||
event = MagicMock()
|
||||
event.event.message.chat_id = "chat_1"
|
||||
event.event.message.message_id = "msg_1"
|
||||
event.event.message.root_id = None
|
||||
event.event.sender.sender_id.open_id = "user_1"
|
||||
|
||||
# Rich text with one image and one file element.
|
||||
event.event.message.content = json.dumps(
|
||||
{
|
||||
"content": [
|
||||
[
|
||||
{"tag": "text", "text": "See"},
|
||||
{"tag": "img", "image_key": "img_123"},
|
||||
{"tag": "file", "file_key": "file_456"},
|
||||
]
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
with pytest.MonkeyPatch.context() as m:
|
||||
mock_make_inbound = MagicMock()
|
||||
m.setattr(channel, "_make_inbound", mock_make_inbound)
|
||||
channel._on_message(event)
|
||||
|
||||
mock_make_inbound.assert_called_once()
|
||||
files = mock_make_inbound.call_args[1]["files"]
|
||||
assert files == [{"image_key": "img_123"}, {"file_key": "file_456"}]
|
||||
assert "[image]" in mock_make_inbound.call_args[1]["text"]
|
||||
assert "[file]" in mock_make_inbound.call_args[1]["text"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", sorted(KNOWN_CHANNEL_COMMANDS))
|
||||
def test_feishu_recognizes_all_known_slash_commands(command):
|
||||
"""Every entry in KNOWN_CHANNEL_COMMANDS must be classified as a command."""
|
||||
|
||||
Reference in New Issue
Block a user