mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
1417 lines
50 KiB
Python
1417 lines
50 KiB
Python
import asyncio
|
|
import base64
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
from urllib.parse import quote
|
|
|
|
from telebot import apihelper
|
|
|
|
from app.agent.tools.impl.send_message import SendMessageInput, SendMessageTool
|
|
from app.agent.tools.impl.send_local_file import SendLocalFileInput
|
|
from app.agent import MoviePilotAgent, AgentChain
|
|
from app.agent.llm import AgentCapabilityManager
|
|
from app.chain.message import MessageChain
|
|
from app.runtime.config import global_vars, settings
|
|
from app.agent.llm import LLMHelper
|
|
from app.modules.discord import DiscordModule
|
|
from app.modules.qqbot import QQBotModule
|
|
from app.modules.qqbot.qqbot import QQBot
|
|
from app.modules.slack import SlackModule
|
|
from app.modules.telegram.telegram import Telegram
|
|
from app.modules.telegram import TelegramModule
|
|
from app.modules.synologychat import SynologyChatModule
|
|
from app.modules.vocechat import VoceChatModule
|
|
from app.modules.wechat import WechatModule
|
|
from app.modules.wechat.wechatbot import WeChatBot
|
|
from app.schemas import IncomingMessage, Message
|
|
from app.schemas.types import NotificationChannel, MessageType
|
|
|
|
|
|
class AgentImageSupportTest(unittest.TestCase):
|
|
def test_telegram_extract_audio_refs_returns_prefixed_file_ids(self):
|
|
audio_refs = TelegramModule._extract_audio_refs(
|
|
{
|
|
"voice": {"file_id": "voice-1"},
|
|
"audio": {"file_id": "audio-1"},
|
|
}
|
|
)
|
|
|
|
self.assertEqual(
|
|
audio_refs,
|
|
["tg://voice_file_id/voice-1", "tg://audio_file_id/audio-1"],
|
|
)
|
|
|
|
def test_telegram_extract_images_returns_prefixed_file_ids(self):
|
|
images = TelegramModule._extract_images(
|
|
{
|
|
"photo": [{"file_id": "small"}, {"file_id": "large"}],
|
|
"document": {
|
|
"file_id": "doc-image",
|
|
"mime_type": "image/png",
|
|
"file_name": "poster.png",
|
|
},
|
|
}
|
|
)
|
|
|
|
self.assertEqual([image.ref for image in images], ["tg://file_id/large", "tg://file_id/doc-image"])
|
|
self.assertEqual(images[0].mime_type, "image/jpeg")
|
|
self.assertEqual(images[1].mime_type, "image/png")
|
|
self.assertEqual(images[1].name, "poster.png")
|
|
|
|
def test_telegram_message_parser_accepts_double_encoded_body(self):
|
|
module = TelegramModule()
|
|
body = json.dumps(
|
|
json.dumps(
|
|
{
|
|
"message": {
|
|
"from": {"id": 10001, "username": "tester"},
|
|
"chat": {"id": 10001, "type": "private"},
|
|
"photo": [{"file_id": "small"}, {"file_id": "large"}],
|
|
}
|
|
}
|
|
)
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="telegram-test", config={}),
|
|
), patch.object(
|
|
module,
|
|
"get_instance",
|
|
return_value=SimpleNamespace(bot_username=None),
|
|
):
|
|
message = module.message_parser(
|
|
source="telegram-test", body=body, form={}, args={}
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual([image.ref for image in message.images], ["tg://file_id/large"])
|
|
|
|
def test_telegram_forward_payload_uses_dict_not_json_string(self):
|
|
payload = Telegram._serialize_update_payload(
|
|
SimpleNamespace(
|
|
to_dict=lambda: {
|
|
"text": "hi",
|
|
"photo": [{"file_id": "image-1"}],
|
|
}
|
|
)
|
|
)
|
|
|
|
self.assertEqual(
|
|
payload,
|
|
{"text": "hi", "photo": [{"file_id": "image-1"}]},
|
|
)
|
|
|
|
def test_telegram_download_file_uses_configured_file_url(self):
|
|
telegram = Telegram.__new__(Telegram)
|
|
telegram._bot = Mock()
|
|
telegram._telegram_token = "token-123"
|
|
telegram._bot.get_file.return_value = SimpleNamespace(file_path="photos/a.jpg")
|
|
|
|
old_file_url = apihelper.FILE_URL
|
|
old_proxy = apihelper.proxy
|
|
apihelper.FILE_URL = "https://tg-proxy.example/file/bot{0}/{1}"
|
|
apihelper.proxy = {"https": "http://127.0.0.1:7890"}
|
|
|
|
try:
|
|
with patch(
|
|
"app.modules.telegram.telegram.RequestUtils.get_res",
|
|
return_value=SimpleNamespace(content=b"image-bytes"),
|
|
) as get_res:
|
|
content = telegram.download_file("file-id-1")
|
|
finally:
|
|
apihelper.FILE_URL = old_file_url
|
|
apihelper.proxy = old_proxy
|
|
|
|
self.assertEqual(content, b"image-bytes")
|
|
get_res.assert_called_once_with(
|
|
"https://tg-proxy.example/file/bottoken-123/photos/a.jpg"
|
|
)
|
|
|
|
def test_process_allows_image_only_message(self):
|
|
chain = MessageChain()
|
|
message = IncomingMessage(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
images=["tg://file_id/image-1"],
|
|
)
|
|
|
|
with patch.object(chain, "message_parser", return_value=message), patch.object(
|
|
chain, "handle_message"
|
|
) as handle_message:
|
|
chain.process(body="{}", form={}, args={"source": "telegram-test"})
|
|
|
|
handle_kwargs = handle_message.call_args.kwargs
|
|
self.assertEqual(handle_kwargs["text"], "")
|
|
self.assertEqual([image.ref for image in handle_kwargs["images"]], ["tg://file_id/image-1"])
|
|
|
|
def test_process_allows_audio_only_message(self):
|
|
chain = MessageChain()
|
|
message = IncomingMessage(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
audio_refs=["tg://voice_file_id/voice-1"],
|
|
)
|
|
|
|
with patch.object(chain, "message_parser", return_value=message), patch.object(
|
|
chain, "handle_message"
|
|
) as handle_message:
|
|
chain.process(body="{}", form={}, args={"source": "telegram-test"})
|
|
|
|
handle_kwargs = handle_message.call_args.kwargs
|
|
self.assertEqual(handle_kwargs["text"], "")
|
|
self.assertEqual(handle_kwargs["audio_refs"], ["tg://voice_file_id/voice-1"])
|
|
|
|
def test_process_allows_file_only_message(self):
|
|
chain = MessageChain()
|
|
message = IncomingMessage(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
files=[
|
|
IncomingMessage.MessageAttachment(
|
|
ref="tg://document_file_id/doc-1",
|
|
name="note.txt",
|
|
mime_type="text/plain",
|
|
size=12,
|
|
)
|
|
],
|
|
)
|
|
|
|
with patch.object(chain, "message_parser", return_value=message), patch.object(
|
|
chain, "handle_message"
|
|
) as handle_message:
|
|
chain.process(body="{}", form={}, args={"source": "telegram-test"})
|
|
|
|
handle_kwargs = handle_message.call_args.kwargs
|
|
self.assertEqual(handle_kwargs["text"], "")
|
|
self.assertEqual(handle_kwargs["files"][0].ref, "tg://document_file_id/doc-1")
|
|
|
|
def test_image_message_routes_to_agent_even_when_global_agent_is_disabled(self):
|
|
chain = MessageChain()
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
ai_agent_enable=True,
|
|
ai_agent_global=False,
|
|
)
|
|
|
|
with patch.object(chain, "load_cache", return_value={}), patch.object(
|
|
chain.messagehelper, "put"
|
|
), patch.object(chain.messageoper, "add"), patch.object(
|
|
chain, "_handle_ai_message"
|
|
) as handle_ai_message, patch.object(
|
|
settings, "AI_AGENT_ENABLE", True
|
|
), patch.object(
|
|
settings, "AI_AGENT_GLOBAL", False
|
|
):
|
|
chain.handle_message(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="",
|
|
images=["tg://file_id/image-1"],
|
|
)
|
|
|
|
handle_ai_message.assert_called_once()
|
|
|
|
def test_audio_message_routes_to_agent_without_forcing_voice_reply(self):
|
|
chain = MessageChain()
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
ai_agent_enable=True,
|
|
ai_agent_global=False,
|
|
)
|
|
|
|
with patch.object(chain, "load_cache", return_value={}), patch.object(
|
|
chain, "_transcribe_audio_refs", return_value="帮我推荐一部电影"
|
|
), patch.object(chain.messagehelper, "put"), patch.object(
|
|
chain.messageoper, "add"
|
|
), patch.object(chain, "_handle_ai_message") as handle_ai_message:
|
|
with patch.object(settings, "AI_AGENT_ENABLE", True), patch.object(
|
|
settings, "AI_AGENT_GLOBAL", False
|
|
):
|
|
chain.handle_message(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="",
|
|
audio_refs=["tg://voice_file_id/voice-1"],
|
|
)
|
|
|
|
handle_ai_message.assert_called_once()
|
|
self.assertEqual(handle_ai_message.call_args.kwargs["text"], "帮我推荐一部电影")
|
|
self.assertTrue(handle_ai_message.call_args.kwargs["has_audio_input"])
|
|
self.assertNotIn("reply_with_voice", handle_ai_message.call_args.kwargs)
|
|
|
|
def test_file_message_routes_to_agent_even_when_global_agent_is_disabled(self):
|
|
chain = MessageChain()
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
ai_agent_enable=True,
|
|
ai_agent_global=False,
|
|
)
|
|
|
|
with patch.object(chain, "load_cache", return_value={}), patch.object(
|
|
chain.messagehelper, "put"
|
|
), patch.object(chain.messageoper, "add"), patch.object(
|
|
chain, "_handle_ai_message"
|
|
) as handle_ai_message, patch.object(
|
|
settings, "AI_AGENT_ENABLE", True
|
|
), patch.object(
|
|
settings, "AI_AGENT_GLOBAL", False
|
|
):
|
|
chain.handle_message(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="",
|
|
files=[
|
|
IncomingMessage.MessageAttachment(
|
|
ref="tg://document_file_id/doc-1",
|
|
name="report.txt",
|
|
mime_type="text/plain",
|
|
)
|
|
],
|
|
)
|
|
|
|
handle_ai_message.assert_called_once()
|
|
self.assertEqual(handle_ai_message.call_args.kwargs["files"][0].name, "report.txt")
|
|
|
|
def test_transcribe_audio_refs_supports_new_channel_refs(self):
|
|
chain = MessageChain()
|
|
audio_refs = [
|
|
"slack://file/" + quote("https://files.slack.com/test.mp3", safe=""),
|
|
"discord://file/" + quote("https://cdn.discordapp.com/voice.ogg", safe=""),
|
|
"qq://file/" + quote("https://example.com/qq-voice.ogg", safe=""),
|
|
"vocechat://file/%2Fuploads%2Fvoice.ogg",
|
|
"synology://file/" + quote("https://example.com/synology-voice.wav", safe=""),
|
|
"feishu://file/om_audio/file_audio/voice.opus",
|
|
]
|
|
|
|
with patch(
|
|
"app.chain.message.is_audio_input_available", return_value=True
|
|
), patch.object(
|
|
chain,
|
|
"run_module",
|
|
side_effect=[b"slack", b"discord", b"qq", b"vocechat", b"synology", b"feishu"],
|
|
) as run_module, patch(
|
|
"app.chain.message.transcribe_audio",
|
|
side_effect=[
|
|
"slack text",
|
|
"discord text",
|
|
"qq text",
|
|
"vocechat text",
|
|
"synology text",
|
|
"feishu text",
|
|
],
|
|
) as transcribe_bytes:
|
|
result = chain._transcribe_audio_refs(
|
|
audio_refs=audio_refs,
|
|
channel=NotificationChannel.Slack,
|
|
source="mixed-source",
|
|
)
|
|
|
|
self.assertEqual(
|
|
result,
|
|
"slack text\ndiscord text\nqq text\nvocechat text\nsynology text\nfeishu text",
|
|
)
|
|
self.assertEqual(
|
|
[call.args[0] for call in run_module.call_args_list],
|
|
[
|
|
"download_slack_file_bytes",
|
|
"download_discord_file_bytes",
|
|
"download_qq_file_bytes",
|
|
"download_vocechat_file_bytes",
|
|
"download_synologychat_file_bytes",
|
|
"download_feishu_file_bytes",
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[call.kwargs["filename"] for call in transcribe_bytes.call_args_list],
|
|
[
|
|
"test.mp3",
|
|
"voice.ogg",
|
|
"qq-voice.ogg",
|
|
"voice.ogg",
|
|
"synology-voice.wav",
|
|
"voice.opus",
|
|
],
|
|
)
|
|
|
|
def test_agent_send_agent_message_keeps_default_text_reply(self):
|
|
agent = MoviePilotAgent(
|
|
session_id="session-1",
|
|
user_id="user-1",
|
|
channel=NotificationChannel.Telegram.value,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(
|
|
AgentChain, "async_post_message", new_callable=AsyncMock
|
|
) as async_post_message:
|
|
import asyncio
|
|
|
|
asyncio.run(agent.send_agent_message("这是语音回复"))
|
|
|
|
notification = async_post_message.await_args.args[0]
|
|
self.assertIsNone(notification.voice_path)
|
|
self.assertIsNone(notification.voice_caption)
|
|
self.assertEqual(notification.text, "这是语音回复")
|
|
|
|
def test_agent_process_wraps_request_as_structured_json(self):
|
|
agent = MoviePilotAgent(
|
|
session_id="session-1",
|
|
user_id="user-1",
|
|
channel=NotificationChannel.Telegram.value,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch(
|
|
"app.agent.memory.memory_manager.get_agent_messages", return_value=[]
|
|
), patch.object(agent, "_execute_agent", new_callable=AsyncMock) as execute_agent:
|
|
import asyncio
|
|
|
|
asyncio.run(
|
|
agent.process(
|
|
"帮我总结这个文件",
|
|
files=[
|
|
{
|
|
"name": "report.txt",
|
|
"local_path": "/tmp/report.txt",
|
|
"status": "ready",
|
|
}
|
|
],
|
|
)
|
|
)
|
|
|
|
messages = execute_agent.await_args.args[0]
|
|
human_message = messages[-1]
|
|
content = human_message.content
|
|
self.assertIsInstance(content, list)
|
|
payload = json.loads(content[0]["text"])
|
|
self.assertEqual(payload["message"], "帮我总结这个文件")
|
|
self.assertEqual(payload["input"]["mode"], "text")
|
|
self.assertFalse(payload["input"]["transcribed"])
|
|
self.assertEqual(payload["files"][0]["local_path"], "/tmp/report.txt")
|
|
|
|
def test_agent_process_marks_voice_input_in_structured_json(self):
|
|
"""语音输入应在结构化消息中标记为转写来源。"""
|
|
agent = MoviePilotAgent(
|
|
session_id="session-1",
|
|
user_id="user-1",
|
|
channel=NotificationChannel.Telegram.value,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch(
|
|
"app.agent.memory.memory_manager.get_agent_messages", return_value=[]
|
|
), patch.object(agent, "_execute_agent", new_callable=AsyncMock) as execute_agent:
|
|
asyncio.run(
|
|
agent.process(
|
|
"帮我推荐一部电影",
|
|
has_audio_input=True,
|
|
)
|
|
)
|
|
|
|
messages = execute_agent.await_args.args[0]
|
|
payload = json.loads(messages[-1].content[0]["text"])
|
|
self.assertEqual(payload["message"], "帮我推荐一部电影")
|
|
self.assertEqual(payload["input"]["mode"], "voice")
|
|
self.assertTrue(payload["input"]["transcribed"])
|
|
|
|
def test_llm_supports_image_input_respects_explicit_override(self):
|
|
with patch.object(settings, "LLM_SUPPORT_IMAGE_INPUT", False):
|
|
self.assertFalse(LLMHelper.supports_image_input())
|
|
|
|
def test_llm_supports_image_input_uses_boolean_setting(self):
|
|
with patch.object(settings, "LLM_SUPPORT_IMAGE_INPUT", True):
|
|
self.assertTrue(LLMHelper.supports_image_input())
|
|
|
|
with patch.object(settings, "LLM_SUPPORT_IMAGE_INPUT", False):
|
|
self.assertFalse(LLMHelper.supports_image_input())
|
|
|
|
def test_handle_ai_message_routes_images_to_files_when_image_input_disabled(self):
|
|
chain = MessageChain()
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
ai_agent_enable=True,
|
|
)
|
|
|
|
loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})
|
|
with patch.object(global_vars, "CURRENT_EVENT_LOOP", loop), patch.object(
|
|
settings, "AI_AGENT_ENABLE", True
|
|
), patch.object(
|
|
settings, "LLM_SUPPORT_IMAGE_INPUT", False
|
|
), patch(
|
|
"app.chain.message.supports_image_input", return_value=False
|
|
), patch.object(chain, "_get_or_create_session_id", return_value="session-1"), patch.object(
|
|
chain, "_download_attachments_to_data_urls"
|
|
) as download_images, patch.object(
|
|
chain,
|
|
"_prepare_agent_files",
|
|
return_value=[
|
|
{
|
|
"name": "image_1.jpg",
|
|
"mime_type": "image/jpeg",
|
|
"local_path": "/tmp/image_1.jpg",
|
|
"status": "ready",
|
|
}
|
|
],
|
|
) as prepare_files, patch(
|
|
"app.chain.message.get_running_agent_manager"
|
|
) as get_running_manager, patch(
|
|
"app.chain.message.asyncio.run_coroutine_threadsafe",
|
|
side_effect=lambda coro, _loop: coro.close(),
|
|
) as run_coroutine_threadsafe:
|
|
process_message = AsyncMock()
|
|
get_running_manager.return_value.process_message = process_message
|
|
chain._handle_ai_message(
|
|
text="/ai 帮我看看这张图",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
images=["tg://file_id/image-1"],
|
|
)
|
|
|
|
download_images.assert_not_called()
|
|
prepare_files.assert_called_once()
|
|
attachments = prepare_files.call_args.kwargs["files"]
|
|
self.assertEqual(attachments[0].ref, "tg://file_id/image-1")
|
|
self.assertEqual(attachments[0].mime_type, "image/jpeg")
|
|
run_coroutine_threadsafe.assert_called_once()
|
|
self.assertEqual(process_message.call_args.kwargs["images"], None)
|
|
self.assertEqual(
|
|
process_message.call_args.kwargs["files"][0]["local_path"],
|
|
"/tmp/image_1.jpg",
|
|
)
|
|
|
|
def test_handle_ai_message_forwards_voice_input_to_agent_manager(self):
|
|
"""AI消息入队时应保留语音输入标记。"""
|
|
chain = MessageChain()
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
ai_agent_enable=True,
|
|
)
|
|
|
|
loop = Mock(**{"is_running.return_value": True, "is_closed.return_value": False})
|
|
with patch.object(global_vars, "CURRENT_EVENT_LOOP", loop), patch.object(
|
|
settings, "AI_AGENT_ENABLE", True
|
|
), patch.object(
|
|
chain, "_get_or_create_session_id", return_value="session-1"
|
|
), patch(
|
|
"app.chain.message.get_running_agent_manager"
|
|
) as get_running_manager, patch(
|
|
"app.chain.message.asyncio.run_coroutine_threadsafe",
|
|
side_effect=lambda coro, _loop: coro.close(),
|
|
):
|
|
process_message = AsyncMock()
|
|
get_running_manager.return_value.process_message = process_message
|
|
chain._handle_ai_message(
|
|
text="帮我推荐一部电影",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
has_audio_input=True,
|
|
)
|
|
|
|
self.assertTrue(process_message.call_args.kwargs["has_audio_input"])
|
|
|
|
def test_slack_images_use_authenticated_data_url_download(self):
|
|
chain = MessageChain()
|
|
|
|
with patch.object(
|
|
chain,
|
|
"run_module",
|
|
return_value="data:image/png;base64,abc123",
|
|
) as run_module:
|
|
images = chain._download_attachments_to_data_urls(
|
|
attachments=["https://files.slack.com/files-pri/T1-F1/test.png"],
|
|
channel=NotificationChannel.Slack,
|
|
source="slack-test",
|
|
)
|
|
|
|
self.assertEqual(images, ["data:image/png;base64,abc123"])
|
|
run_module.assert_called_once_with(
|
|
"download_slack_file_to_data_url",
|
|
file_url="https://files.slack.com/files-pri/T1-F1/test.png",
|
|
source="slack-test",
|
|
)
|
|
|
|
def test_slack_module_download_file_to_data_url(self):
|
|
module = SlackModule()
|
|
client = Mock()
|
|
client.download_file.return_value = (b"png-binary", "image/png")
|
|
|
|
with patch.object(
|
|
module, "get_config", return_value=SimpleNamespace(name="slack-test")
|
|
), patch.object(module, "get_instance", return_value=client):
|
|
data_url = module.download_slack_file_to_data_url(
|
|
"https://files.slack.com/files-pri/T1-F1/test.png",
|
|
"slack-test",
|
|
)
|
|
|
|
self.assertEqual(
|
|
data_url,
|
|
f"data:image/png;base64,{base64.b64encode(b'png-binary').decode()}",
|
|
)
|
|
|
|
def test_slack_extract_audio_refs_returns_private_file_refs(self):
|
|
audio_refs = SlackModule._extract_audio_refs(
|
|
{
|
|
"files": [
|
|
{
|
|
"type": "audio",
|
|
"filetype": "mp3",
|
|
"mimetype": "audio/mpeg",
|
|
"url_private": "https://files.slack.com/files-pri/T1-F1/test.mp3",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
self.assertEqual(
|
|
audio_refs,
|
|
[
|
|
"slack://file/"
|
|
+ quote("https://files.slack.com/files-pri/T1-F1/test.mp3", safe="")
|
|
],
|
|
)
|
|
|
|
def test_send_message_input_accepts_image_only_payload(self):
|
|
payload = SendMessageInput(
|
|
image_url="https://example.com/poster.png",
|
|
)
|
|
|
|
self.assertEqual(payload.image_url, "https://example.com/poster.png")
|
|
|
|
def test_send_message_tool_uses_regular_notification_type(self):
|
|
"""发送消息工具应按普通通知消息登记。"""
|
|
|
|
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",
|
|
)
|
|
|
|
with patch(
|
|
"app.agent.tools.base.ToolChain.async_post_message",
|
|
new_callable=AsyncMock,
|
|
) as async_post_message:
|
|
result = await tool.run(
|
|
message="处理完成",
|
|
title="智能体通知",
|
|
image_url="https://example.com/poster.png",
|
|
)
|
|
return result, async_post_message
|
|
|
|
result, async_post_message = asyncio.run(_run())
|
|
notification = async_post_message.await_args.args[0]
|
|
|
|
self.assertEqual(result, "消息已发送")
|
|
self.assertEqual(notification.mtype, MessageType.Other)
|
|
self.assertEqual(notification.channel, NotificationChannel.Telegram)
|
|
self.assertEqual(notification.source, "telegram-test")
|
|
self.assertEqual(notification.title, "智能体通知")
|
|
self.assertEqual(notification.text, "处理完成")
|
|
self.assertEqual(notification.image, "https://example.com/poster.png")
|
|
self.assertIsNone(notification.parse_mode)
|
|
|
|
def test_send_message_tool_ignores_parse_mode_argument(self):
|
|
"""发送消息工具不再支持由 Agent 指定 Telegram parse_mode。"""
|
|
|
|
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",
|
|
)
|
|
|
|
with patch(
|
|
"app.agent.tools.base.ToolChain.async_post_message",
|
|
new_callable=AsyncMock,
|
|
) as async_post_message:
|
|
result = await tool.run(
|
|
message="<b>处理完成</b>",
|
|
parse_mode="HTML",
|
|
)
|
|
return result, async_post_message
|
|
|
|
result, async_post_message = asyncio.run(_run())
|
|
notification = async_post_message.await_args.args[0]
|
|
|
|
self.assertEqual(result, "消息已发送")
|
|
self.assertEqual(notification.text, "<b>处理完成</b>")
|
|
self.assertIsNone(notification.parse_mode)
|
|
|
|
def test_send_message_tool_marks_reply_sent_after_dispatch(self):
|
|
"""发送消息工具成功发送后应终止本轮回复。"""
|
|
|
|
async def _run():
|
|
tool = SendMessageTool(session_id="session-1", user_id="10001")
|
|
agent_context = {}
|
|
tool.set_agent_context(agent_context)
|
|
tool.set_message_attr(
|
|
channel=NotificationChannel.Telegram.value,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch(
|
|
"app.agent.tools.base.ToolChain.async_post_message",
|
|
new_callable=AsyncMock,
|
|
):
|
|
result = await tool.run(message="处理完成")
|
|
return result, agent_context
|
|
|
|
result, agent_context = asyncio.run(_run())
|
|
|
|
self.assertEqual(result, "消息已发送")
|
|
self.assertTrue(agent_context["user_reply_sent"])
|
|
self.assertEqual(agent_context["reply_mode"], "send_message")
|
|
|
|
def test_send_local_file_input_accepts_file_payload(self):
|
|
payload = SendLocalFileInput(
|
|
file_path="/tmp/report.txt",
|
|
message="请下载查看",
|
|
)
|
|
|
|
self.assertEqual(payload.file_path, "/tmp/report.txt")
|
|
|
|
def test_discord_extract_images_supports_attachment_content_type(self):
|
|
images = DiscordModule._extract_images(
|
|
{
|
|
"attachments": [
|
|
{
|
|
"content_type": "image/png",
|
|
"url": "https://cdn.discordapp.com/test.png",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
self.assertEqual([image.ref for image in images], ["https://cdn.discordapp.com/test.png"])
|
|
self.assertEqual(images[0].mime_type, "image/png")
|
|
|
|
def test_discord_extract_audio_refs_supports_attachment_content_type(self):
|
|
audio_refs = DiscordModule._extract_audio_refs(
|
|
{
|
|
"attachments": [
|
|
{
|
|
"content_type": "audio/ogg",
|
|
"filename": "voice.ogg",
|
|
"url": "https://cdn.discordapp.com/voice.ogg",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
self.assertEqual(
|
|
audio_refs,
|
|
[
|
|
"discord://file/"
|
|
+ quote("https://cdn.discordapp.com/voice.ogg", safe="")
|
|
],
|
|
)
|
|
|
|
def test_discord_extract_files_supports_non_media_attachment(self):
|
|
files = DiscordModule._extract_files(
|
|
{
|
|
"attachments": [
|
|
{
|
|
"content_type": "application/pdf",
|
|
"filename": "guide.pdf",
|
|
"url": "https://cdn.discordapp.com/guide.pdf",
|
|
"size": 1024,
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
self.assertEqual(files[0].name, "guide.pdf")
|
|
self.assertEqual(
|
|
files[0].ref,
|
|
"discord://file/" + quote("https://cdn.discordapp.com/guide.pdf", safe=""),
|
|
)
|
|
|
|
def test_discord_send_direct_message_returns_chat_id(self):
|
|
module = DiscordModule()
|
|
client = Mock()
|
|
client.send_msg.return_value = (
|
|
True,
|
|
{"message_id": "discord-msg-1", "chat_id": "discord-chat-1"},
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_configs",
|
|
return_value={"discord-test": SimpleNamespace(name="discord-test")},
|
|
), patch.object(
|
|
module, "check_message", return_value=True
|
|
), patch.object(
|
|
module, "get_instance", return_value=client
|
|
):
|
|
response = module.send_direct_message(
|
|
Message(title="hi", userid="user-1")
|
|
)
|
|
|
|
self.assertIsNotNone(response)
|
|
self.assertEqual(response.message_id, "discord-msg-1")
|
|
self.assertEqual(response.chat_id, "discord-chat-1")
|
|
|
|
def test_download_images_routes_wechat_refs_to_module_downloader(self):
|
|
chain = MessageChain()
|
|
|
|
with patch.object(
|
|
chain,
|
|
"run_module",
|
|
return_value="data:image/png;base64,wechat123",
|
|
) as run_module:
|
|
images = chain._download_attachments_to_data_urls(
|
|
attachments=["wxwork://media_id/media-1"],
|
|
channel=NotificationChannel.Wechat,
|
|
source="wechat-test",
|
|
)
|
|
|
|
self.assertEqual(images, ["data:image/png;base64,wechat123"])
|
|
run_module.assert_called_once_with(
|
|
"download_wechat_image_to_data_url",
|
|
image_ref="wxwork://media_id/media-1",
|
|
source="wechat-test",
|
|
)
|
|
|
|
def test_download_images_routes_feishu_refs_to_module_downloader(self):
|
|
chain = MessageChain()
|
|
|
|
with patch.object(
|
|
chain,
|
|
"run_module",
|
|
return_value="data:image/png;base64,feishu123",
|
|
) as run_module:
|
|
data_urls = chain._download_attachments_to_data_urls(
|
|
attachments=[
|
|
IncomingMessage.MessageImage(
|
|
ref="feishu://image/img_v2_xxx",
|
|
mime_type="image/png",
|
|
)
|
|
],
|
|
channel=NotificationChannel.Feishu,
|
|
source="feishu-test",
|
|
)
|
|
|
|
self.assertEqual(data_urls, ["data:image/png;base64,feishu123"])
|
|
run_module.assert_called_once_with(
|
|
"download_feishu_image_to_data_url",
|
|
image_ref="feishu://image/img_v2_xxx",
|
|
source="feishu-test",
|
|
)
|
|
|
|
def test_download_message_file_bytes_supports_feishu_refs(self):
|
|
chain = MessageChain()
|
|
|
|
with patch.object(chain, "run_module", return_value=b"feishu-file") as run_module:
|
|
content = chain._download_message_file_bytes(
|
|
file_ref="feishu://file/file_xxx/report.pdf",
|
|
channel=NotificationChannel.Feishu,
|
|
source="feishu-test",
|
|
)
|
|
|
|
self.assertEqual(content, b"feishu-file")
|
|
run_module.assert_called_once_with(
|
|
"download_feishu_file_bytes",
|
|
file_ref="feishu://file/file_xxx/report.pdf",
|
|
source="feishu-test",
|
|
)
|
|
|
|
def test_wechat_message_parser_extracts_image_media_id(self):
|
|
module = WechatModule()
|
|
xml_message = b"""
|
|
<xml>
|
|
<FromUserName><![CDATA[user-1]]></FromUserName>
|
|
<MsgType><![CDATA[image]]></MsgType>
|
|
<PicUrl><![CDATA[https://example.com/image.png]]></PicUrl>
|
|
<MediaId><![CDATA[media-1]]></MediaId>
|
|
</xml>
|
|
"""
|
|
crypt = Mock()
|
|
crypt.DecryptMsg.return_value = (0, xml_message)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="wechat-test",
|
|
config={
|
|
"WECHAT_TOKEN": "token",
|
|
"WECHAT_ENCODING_AESKEY": "encoding",
|
|
"WECHAT_CORPID": "corpid",
|
|
},
|
|
),
|
|
), patch.object(
|
|
module, "get_instance", return_value=SimpleNamespace(send_msg=Mock())
|
|
), patch(
|
|
"app.modules.wechat.WXBizMsgCrypt",
|
|
return_value=crypt,
|
|
):
|
|
message = module.message_parser(
|
|
source="wechat-test",
|
|
body=b"encrypted",
|
|
form={},
|
|
args={"msg_signature": "sig", "timestamp": "1", "nonce": "n"},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual([image.ref for image in message.images], ["wxwork://media_id/media-1"])
|
|
|
|
def test_wechat_message_parser_extracts_file_media_id(self):
|
|
module = WechatModule()
|
|
xml_message = b"""
|
|
<xml>
|
|
<FromUserName><![CDATA[user-1]]></FromUserName>
|
|
<MsgType><![CDATA[file]]></MsgType>
|
|
<MediaId><![CDATA[file-media-1]]></MediaId>
|
|
<FileName><![CDATA[manual.pdf]]></FileName>
|
|
</xml>
|
|
"""
|
|
crypt = Mock()
|
|
crypt.DecryptMsg.return_value = (0, xml_message)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="wechat-test",
|
|
config={
|
|
"WECHAT_TOKEN": "token",
|
|
"WECHAT_ENCODING_AESKEY": "encoding",
|
|
"WECHAT_CORPID": "corpid",
|
|
},
|
|
),
|
|
), patch.object(
|
|
module, "get_instance", return_value=SimpleNamespace(send_msg=Mock())
|
|
), patch(
|
|
"app.modules.wechat.WXBizMsgCrypt",
|
|
return_value=crypt,
|
|
):
|
|
message = module.message_parser(
|
|
source="wechat-test",
|
|
body=b"encrypted",
|
|
form={},
|
|
args={"msg_signature": "sig", "timestamp": "1", "nonce": "n"},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(message.files[0].ref, "wxwork://file_media_id/file-media-1")
|
|
|
|
def test_wechat_bot_parser_accepts_image_only_payload(self):
|
|
module = WechatModule()
|
|
body = json.dumps(
|
|
{
|
|
"body": {
|
|
"from": {"userid": "wxbot-user"},
|
|
"msgtype": "image",
|
|
"image": {
|
|
"download_url": "https://example.com/encrypted-image",
|
|
"aeskey": "YWJjZGVmZw",
|
|
},
|
|
}
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="wechat-bot-test", config={"WECHAT_MODE": "bot"}
|
|
),
|
|
), patch.object(
|
|
module, "get_instance", return_value=SimpleNamespace(send_msg=Mock())
|
|
):
|
|
message = module.message_parser(
|
|
source="wechat-bot-test",
|
|
body=body,
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertTrue(message.images[0].ref.startswith("wxbot://image/"))
|
|
|
|
def test_wechat_bot_handles_image_only_callback(self):
|
|
bot = WeChatBot.__new__(WeChatBot)
|
|
bot._config_name = "wechat-bot-test"
|
|
bot._admins = []
|
|
bot.send_msg = Mock()
|
|
bot._remember_target = Mock()
|
|
bot._forward_to_message_chain = Mock()
|
|
|
|
payload = {
|
|
"body": {
|
|
"from": {"userid": "wxbot-user"},
|
|
"msgtype": "image",
|
|
"image": {
|
|
"download_url": "https://example.com/encrypted-image",
|
|
"aeskey": "YWJjZGVmZw",
|
|
},
|
|
}
|
|
}
|
|
|
|
bot._handle_callback_message(payload)
|
|
|
|
bot._remember_target.assert_called_once_with("wxbot-user")
|
|
bot._forward_to_message_chain.assert_called_once_with(payload)
|
|
|
|
def test_vocechat_message_parser_extracts_image_file_payload(self):
|
|
module = VoceChatModule()
|
|
body = json.dumps(
|
|
{
|
|
"detail": {
|
|
"type": "normal",
|
|
"content_type": "vocechat/file",
|
|
"content": "/uploads/poster.png",
|
|
"properties": {"content_type": "image/png"},
|
|
},
|
|
"from_uid": 7910,
|
|
"target": {"gid": 2},
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="vocechat-test", config={"channel_id": "2"}
|
|
),
|
|
):
|
|
message = module.message_parser(
|
|
source="vocechat-test",
|
|
body=body,
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(
|
|
[image.ref for image in message.images],
|
|
["vocechat://file/%2Fuploads%2Fposter.png"],
|
|
)
|
|
self.assertEqual(message.images[0].mime_type, "image/png")
|
|
|
|
def test_vocechat_message_parser_extracts_audio_file_payload(self):
|
|
module = VoceChatModule()
|
|
body = json.dumps(
|
|
{
|
|
"detail": {
|
|
"type": "normal",
|
|
"content_type": "vocechat/file",
|
|
"content": "/uploads/voice.ogg",
|
|
"properties": {"content_type": "audio/ogg"},
|
|
},
|
|
"from_uid": 7910,
|
|
"target": {"gid": 2},
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="vocechat-test", config={"channel_id": "2"}
|
|
),
|
|
):
|
|
message = module.message_parser(
|
|
source="vocechat-test",
|
|
body=body,
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(
|
|
message.audio_refs,
|
|
["vocechat://file/%2Fuploads%2Fvoice.ogg"],
|
|
)
|
|
|
|
def test_vocechat_message_parser_extracts_generic_file_payload(self):
|
|
module = VoceChatModule()
|
|
body = json.dumps(
|
|
{
|
|
"detail": {
|
|
"type": "normal",
|
|
"content_type": "vocechat/file",
|
|
"content": "/uploads/manual.pdf",
|
|
"properties": {"content_type": "application/pdf"},
|
|
},
|
|
"from_uid": 7910,
|
|
"target": {"gid": 2},
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(
|
|
name="vocechat-test", config={"channel_id": "2"}
|
|
),
|
|
):
|
|
message = module.message_parser(
|
|
source="vocechat-test",
|
|
body=body,
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(message.files[0].ref, "vocechat://file/%2Fuploads%2Fmanual.pdf")
|
|
|
|
def test_vocechat_post_message_passes_image_and_correct_target(self):
|
|
module = VoceChatModule()
|
|
client = Mock()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_configs",
|
|
return_value={"vocechat-test": SimpleNamespace(name="vocechat-test")},
|
|
), patch.object(
|
|
module, "check_message", return_value=True
|
|
), patch.object(
|
|
module, "get_instance", return_value=client
|
|
):
|
|
module.post_message(
|
|
Message(
|
|
title="poster",
|
|
image="https://example.com/poster.png",
|
|
targets={"vocechat_userid": "UID#100"},
|
|
)
|
|
)
|
|
|
|
client.send_msg.assert_called_once_with(
|
|
title="poster",
|
|
text=None,
|
|
image="https://example.com/poster.png",
|
|
userid="UID#100",
|
|
link=None,
|
|
)
|
|
|
|
def test_slack_post_message_passes_local_file(self):
|
|
module = SlackModule()
|
|
client = Mock()
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
file_path = Path(tempdir) / "guide.pdf"
|
|
file_path.write_bytes(b"pdf")
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_configs",
|
|
return_value={"slack-test": SimpleNamespace(name="slack-test")},
|
|
), patch.object(
|
|
module, "check_message", return_value=True
|
|
), patch.object(
|
|
module, "get_instance", return_value=client
|
|
):
|
|
module.post_message(
|
|
Message(
|
|
title="手册",
|
|
text="请下载",
|
|
file_path=str(file_path),
|
|
file_name="guide.pdf",
|
|
userid="U123",
|
|
)
|
|
)
|
|
|
|
client.send_file.assert_called_once()
|
|
|
|
def test_discord_post_message_passes_local_file(self):
|
|
module = DiscordModule()
|
|
client = Mock()
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
file_path = Path(tempdir) / "guide.pdf"
|
|
file_path.write_bytes(b"pdf")
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_configs",
|
|
return_value={
|
|
"discord-test": SimpleNamespace(
|
|
name="discord-test",
|
|
type="discord",
|
|
enabled=True,
|
|
)
|
|
},
|
|
), patch.object(
|
|
module, "check_message", return_value=True
|
|
), patch.object(
|
|
module, "get_instance", return_value=client
|
|
):
|
|
module.post_message(
|
|
Message(
|
|
title="手册",
|
|
text="请下载",
|
|
file_path=str(file_path),
|
|
file_name="guide.pdf",
|
|
userid="user-1",
|
|
)
|
|
)
|
|
|
|
client.send_file.assert_called_once()
|
|
|
|
def test_qq_message_parser_accepts_image_only_attachment(self):
|
|
module = QQBotModule()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="qq-test", config={}),
|
|
):
|
|
message = module.message_parser(
|
|
source="qq-test",
|
|
body={
|
|
"type": "C2C_MESSAGE_CREATE",
|
|
"author": {"user_openid": "qq-user"},
|
|
"attachments": [
|
|
{
|
|
"content_type": "image/png",
|
|
"url": "https://example.com/qq-image.png",
|
|
}
|
|
],
|
|
},
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual([image.ref for image in message.images], ["https://example.com/qq-image.png"])
|
|
self.assertEqual(message.images[0].mime_type, "image/png")
|
|
|
|
def test_qq_markdown_image_size_preserves_poster_ratio(self):
|
|
with patch.object(QQBot, "_get_image_size", return_value=(1000, 1500)):
|
|
content, use_markdown = QQBot._format_message_markdown(
|
|
title="poster",
|
|
image="https://example.com/poster.jpg",
|
|
)
|
|
|
|
self.assertTrue(use_markdown)
|
|
self.assertIn("", content)
|
|
|
|
def test_qq_markdown_image_uses_poster_ratio_fallback(self):
|
|
with patch.object(QQBot, "_get_image_size", return_value=None):
|
|
content, use_markdown = QQBot._format_message_markdown(
|
|
image="https://example.com/poster.jpg",
|
|
)
|
|
|
|
self.assertTrue(use_markdown)
|
|
self.assertEqual(content, "")
|
|
|
|
def test_qq_message_parser_accepts_audio_only_attachment(self):
|
|
module = QQBotModule()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="qq-test", config={}),
|
|
):
|
|
message = module.message_parser(
|
|
source="qq-test",
|
|
body={
|
|
"type": "C2C_MESSAGE_CREATE",
|
|
"author": {"user_openid": "qq-user"},
|
|
"attachments": [
|
|
{
|
|
"content_type": "audio/ogg",
|
|
"filename": "voice.ogg",
|
|
"url": "https://example.com/qq-voice.ogg",
|
|
}
|
|
],
|
|
},
|
|
form={},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(
|
|
message.audio_refs,
|
|
["qq://file/" + quote("https://example.com/qq-voice.ogg", safe="")],
|
|
)
|
|
|
|
def test_synology_message_parser_accepts_image_only_form(self):
|
|
module = SynologyChatModule()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="synology-test", config={}),
|
|
), patch.object(
|
|
module,
|
|
"get_instance",
|
|
return_value=SimpleNamespace(check_token=lambda token: token == "token-1"),
|
|
):
|
|
message = module.message_parser(
|
|
source="synology-test",
|
|
body={},
|
|
form={
|
|
"token": "token-1",
|
|
"user_id": "42",
|
|
"username": "tester",
|
|
"file_url": "https://example.com/image.png",
|
|
},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual([image.ref for image in message.images], ["https://example.com/image.png"])
|
|
|
|
def test_synology_message_parser_accepts_audio_only_form(self):
|
|
module = SynologyChatModule()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="synology-test", config={}),
|
|
), patch.object(
|
|
module,
|
|
"get_instance",
|
|
return_value=SimpleNamespace(check_token=lambda token: token == "token-1"),
|
|
):
|
|
message = module.message_parser(
|
|
source="synology-test",
|
|
body={},
|
|
form={
|
|
"token": "token-1",
|
|
"user_id": "42",
|
|
"username": "tester",
|
|
"attachments": json.dumps(
|
|
[
|
|
{
|
|
"url": "https://example.com/voice.ogg",
|
|
"content_type": "audio/ogg",
|
|
}
|
|
]
|
|
),
|
|
},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(
|
|
message.audio_refs,
|
|
["synology://file/" + quote("https://example.com/voice.ogg", safe="")],
|
|
)
|
|
|
|
def test_synology_message_parser_accepts_generic_file_attachment(self):
|
|
module = SynologyChatModule()
|
|
|
|
with patch.object(
|
|
module,
|
|
"get_config",
|
|
return_value=SimpleNamespace(name="synology-test", config={}),
|
|
), patch.object(
|
|
module,
|
|
"get_instance",
|
|
return_value=SimpleNamespace(check_token=lambda token: token == "token-1"),
|
|
):
|
|
message = module.message_parser(
|
|
source="synology-test",
|
|
body={},
|
|
form={
|
|
"token": "token-1",
|
|
"user_id": "42",
|
|
"username": "tester",
|
|
"attachments": json.dumps(
|
|
[
|
|
{
|
|
"url": "https://example.com/manual.pdf",
|
|
"content_type": "application/pdf",
|
|
"filename": "manual.pdf",
|
|
}
|
|
]
|
|
),
|
|
},
|
|
args={},
|
|
)
|
|
|
|
self.assertIsNotNone(message)
|
|
self.assertEqual(
|
|
message.files[0].ref,
|
|
"synology://file/" + quote("https://example.com/manual.pdf", safe=""),
|
|
)
|
|
|
|
def test_prepare_agent_files_saves_local_file(self):
|
|
chain = MessageChain()
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
chain.runtime_config = replace(
|
|
chain.runtime_config,
|
|
temporary_path=Path(tempdir),
|
|
)
|
|
with patch.object(
|
|
chain,
|
|
"_download_message_file_bytes",
|
|
return_value="你好,MoviePilot".encode("utf-8"),
|
|
):
|
|
prepared = chain._prepare_agent_files(
|
|
session_id="session-1",
|
|
files=[
|
|
IncomingMessage.MessageAttachment(
|
|
ref="tg://document_file_id/doc-1",
|
|
name="note.txt",
|
|
mime_type="text/plain",
|
|
)
|
|
],
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
)
|
|
|
|
self.assertEqual(prepared[0]["status"], "ready")
|
|
self.assertTrue(Path(prepared[0]["local_path"]).exists())
|
|
|
|
def test_telegram_post_message_passes_file_to_client(self):
|
|
module = TelegramModule()
|
|
client = Mock()
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
file_path = Path(tempdir) / "report.txt"
|
|
file_path.write_text("hello", encoding="utf-8")
|
|
|
|
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(
|
|
title="报告",
|
|
text="请下载",
|
|
file_path=str(file_path),
|
|
file_name="report.txt",
|
|
userid="user-1",
|
|
)
|
|
)
|
|
|
|
client.send_file.assert_called_once()
|