mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-13 07:26:45 +00:00
fix: 支持飞书语音消息识别
This commit is contained in:
@@ -1272,6 +1272,13 @@ class MessageChain(ChainBase):
|
||||
)
|
||||
elif audio_ref.startswith("wxbot://voice"):
|
||||
continue
|
||||
elif audio_ref.startswith("feishu://file/"):
|
||||
content = self.run_module(
|
||||
"download_feishu_file_bytes", file_ref=audio_ref, source=source
|
||||
)
|
||||
filename = self._guess_audio_filename(
|
||||
audio_ref, default="input.opus"
|
||||
)
|
||||
elif audio_ref.startswith("http"):
|
||||
resp = RequestUtils(timeout=30).get_res(audio_ref)
|
||||
content = resp.content if resp and resp.content else None
|
||||
@@ -1339,11 +1346,11 @@ class MessageChain(ChainBase):
|
||||
"""
|
||||
下载可直接提供给 LLM 的附件内容,并统一转换为 data URL。
|
||||
"""
|
||||
attachments = CommingMessage.MessageImage.normalize_list(attachments)
|
||||
if not attachments:
|
||||
normalized_attachments = CommingMessage.MessageImage.normalize_list(attachments) or []
|
||||
if not normalized_attachments:
|
||||
return None
|
||||
data_urls = []
|
||||
for attachment in attachments:
|
||||
for attachment in normalized_attachments:
|
||||
attachment_ref = attachment.ref
|
||||
try:
|
||||
before_count = len(data_urls)
|
||||
|
||||
@@ -301,11 +301,32 @@ class FeishuModule(_ModuleBase, _MessageBase[Feishu]):
|
||||
client = self.get_instance(client_config.name)
|
||||
if not client:
|
||||
return None
|
||||
parts = file_ref.replace("feishu://file/", "", 1).split("/", 1)
|
||||
file_key = parts[0].strip() if parts else ""
|
||||
parts = [
|
||||
part.strip()
|
||||
for part in file_ref.replace("feishu://file/", "", 1).split("/")
|
||||
if part.strip()
|
||||
]
|
||||
file_key = ""
|
||||
downloaded = None
|
||||
if len(parts) >= 2 and parts[0].startswith("om_"):
|
||||
message_id, file_key = parts[0], parts[1]
|
||||
downloaded = client.download_message_resource_bytes(
|
||||
message_id=message_id,
|
||||
file_key=file_key,
|
||||
resource_type="audio",
|
||||
)
|
||||
if not downloaded:
|
||||
downloaded = client.download_message_resource_bytes(
|
||||
message_id=message_id,
|
||||
file_key=file_key,
|
||||
resource_type="file",
|
||||
)
|
||||
else:
|
||||
file_key = parts[0] if parts else ""
|
||||
if not file_key:
|
||||
return None
|
||||
downloaded = client.download_file_bytes(file_key)
|
||||
if not downloaded:
|
||||
downloaded = client.download_file_bytes(file_key)
|
||||
if not downloaded:
|
||||
return None
|
||||
content, _, _ = downloaded
|
||||
|
||||
@@ -235,13 +235,16 @@ class Feishu:
|
||||
elif message_type in {"audio", "media", "file"}:
|
||||
file_key = str(content.get("file_key") or "").strip()
|
||||
file_name = str(content.get("file_name") or "").strip() or None
|
||||
message_id = str(getattr(message, "message_id", None) or "").strip()
|
||||
if file_key:
|
||||
if message_type == "audio":
|
||||
audio_refs = [f"feishu://file/{file_key}/{file_name or 'audio.opus'}"]
|
||||
resource_path = f"{message_id}/{file_key}" if message_id else file_key
|
||||
audio_refs = [f"feishu://file/{resource_path}/{file_name or 'audio.opus'}"]
|
||||
else:
|
||||
resource_path = f"{message_id}/{file_key}" if message_id else file_key
|
||||
files = [
|
||||
CommingMessage.MessageAttachment(
|
||||
ref=f"feishu://file/{file_key}/{file_name or 'attachment'}",
|
||||
ref=f"feishu://file/{resource_path}/{file_name or 'attachment'}",
|
||||
name=file_name,
|
||||
)
|
||||
]
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from urllib.parse import quote
|
||||
@@ -14,6 +15,7 @@ from app.agent import MoviePilotAgent, AgentChain
|
||||
from app.chain.message import MessageChain
|
||||
from app.core.config import settings
|
||||
from app.agent.llm import LLMHelper
|
||||
from app.helper.voice import VoiceHelper
|
||||
from app.modules.discord import DiscordModule
|
||||
from app.modules.qqbot import QQBotModule
|
||||
from app.modules.slack import SlackModule
|
||||
@@ -278,16 +280,24 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
"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.object(VoiceHelper, "is_available", return_value=True), patch.object(
|
||||
chain,
|
||||
"run_module",
|
||||
side_effect=[b"slack", b"discord", b"qq", b"vocechat", b"synology"],
|
||||
side_effect=[b"slack", b"discord", b"qq", b"vocechat", b"synology", b"feishu"],
|
||||
) as run_module, patch.object(
|
||||
VoiceHelper,
|
||||
"transcribe_bytes",
|
||||
side_effect=["slack text", "discord text", "qq text", "vocechat text", "synology text"],
|
||||
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,
|
||||
@@ -297,7 +307,7 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
"slack text\ndiscord text\nqq text\nvocechat text\nsynology text",
|
||||
"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],
|
||||
@@ -307,6 +317,7 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
"download_qq_file_bytes",
|
||||
"download_vocechat_file_bytes",
|
||||
"download_synologychat_file_bytes",
|
||||
"download_feishu_file_bytes",
|
||||
],
|
||||
)
|
||||
self.assertEqual(
|
||||
@@ -317,6 +328,7 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
"qq-voice.ogg",
|
||||
"voice.ogg",
|
||||
"synology-voice.wav",
|
||||
"voice.opus",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -393,7 +405,7 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
with patch.object(settings, "AI_AGENT_ENABLE", True), patch.object(
|
||||
settings, "LLM_SUPPORT_IMAGE_INPUT", False
|
||||
), patch.object(chain, "_get_or_create_session_id", return_value="session-1"), patch.object(
|
||||
chain, "_download_images_to_base64"
|
||||
chain, "_download_attachments_to_data_urls"
|
||||
) as download_images, patch.object(
|
||||
chain,
|
||||
"_prepare_agent_files",
|
||||
@@ -439,8 +451,8 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
"run_module",
|
||||
return_value="data:image/png;base64,abc123",
|
||||
) as run_module:
|
||||
images = chain._download_images_to_base64(
|
||||
images=["https://files.slack.com/files-pri/T1-F1/test.png"],
|
||||
images = chain._download_attachments_to_data_urls(
|
||||
attachments=["https://files.slack.com/files-pri/T1-F1/test.png"],
|
||||
channel=MessageChannel.Slack,
|
||||
source="slack-test",
|
||||
)
|
||||
@@ -598,8 +610,8 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
"run_module",
|
||||
return_value="data:image/png;base64,wechat123",
|
||||
) as run_module:
|
||||
images = chain._download_images_to_base64(
|
||||
images=["wxwork://media_id/media-1"],
|
||||
images = chain._download_attachments_to_data_urls(
|
||||
attachments=["wxwork://media_id/media-1"],
|
||||
channel=MessageChannel.Wechat,
|
||||
source="wechat-test",
|
||||
)
|
||||
@@ -964,7 +976,13 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
with patch.object(
|
||||
module,
|
||||
"get_configs",
|
||||
return_value={"discord-test": SimpleNamespace(name="discord-test")},
|
||||
return_value={
|
||||
"discord-test": SimpleNamespace(
|
||||
name="discord-test",
|
||||
type="discord",
|
||||
enabled=True,
|
||||
)
|
||||
},
|
||||
), patch.object(
|
||||
module, "check_message", return_value=True
|
||||
), patch.object(
|
||||
@@ -1145,8 +1163,9 @@ class AgentImageSupportTest(unittest.TestCase):
|
||||
|
||||
def test_prepare_agent_files_saves_local_file(self):
|
||||
chain = MessageChain()
|
||||
with tempfile.TemporaryDirectory() as tempdir, patch.object(
|
||||
settings, "TEMP_PATH", Path(tempdir)
|
||||
with tempfile.TemporaryDirectory() as tempdir, patch(
|
||||
"app.chain.message.settings",
|
||||
SimpleNamespace(TEMP_PATH=Path(tempdir)),
|
||||
), patch.object(
|
||||
chain,
|
||||
"_download_message_file_bytes",
|
||||
|
||||
@@ -576,6 +576,24 @@ class TestFeishu(unittest.TestCase):
|
||||
payload = forward.call_args.args[0]
|
||||
self.assertEqual(payload["images"][0]["ref"], "feishu://image/om_img_evt/img_v2_evt")
|
||||
|
||||
def test_on_message_wraps_feishu_audio_ref_with_message_id(self):
|
||||
client = self._build_client()
|
||||
message = SimpleNamespace(
|
||||
message_id="om_audio_evt",
|
||||
chat_id="oc_chat_evt",
|
||||
chat_type="p2p",
|
||||
message_type="audio",
|
||||
content=json.dumps({"file_key": "file_audio_evt", "file_name": "voice.opus"}),
|
||||
)
|
||||
sender = SimpleNamespace(sender_id=SimpleNamespace(open_id="ou_user_evt", user_id=None))
|
||||
event = SimpleNamespace(sender=sender, message=message)
|
||||
|
||||
with patch.object(client, "_forward_to_message_chain") as forward:
|
||||
client._on_message(SimpleNamespace(event=event))
|
||||
|
||||
payload = forward.call_args.args[0]
|
||||
self.assertEqual(payload["audio_refs"], ["feishu://file/om_audio_evt/file_audio_evt/voice.opus"])
|
||||
|
||||
def test_feishu_channel_capabilities_enable_images_and_files(self):
|
||||
self.assertTrue(
|
||||
ChannelCapabilityManager.supports_capability(
|
||||
@@ -784,14 +802,24 @@ class TestFeishu(unittest.TestCase):
|
||||
):
|
||||
data_url = module.download_feishu_image_to_data_url("feishu://image/om_msg/img_v2_xxx", "feishu-main")
|
||||
file_bytes = module.download_feishu_file_bytes("feishu://file/file_xxx/note.txt", "feishu-main")
|
||||
audio_bytes = module.download_feishu_file_bytes(
|
||||
"feishu://file/om_audio/file_audio/voice.opus",
|
||||
"feishu-main",
|
||||
)
|
||||
|
||||
self.assertTrue(data_url.startswith("data:image/png;base64,"))
|
||||
self.assertEqual(file_bytes, b"file")
|
||||
client.download_message_resource_bytes.assert_called_once_with(
|
||||
self.assertEqual(audio_bytes, b"image")
|
||||
client.download_message_resource_bytes.assert_any_call(
|
||||
message_id="om_msg",
|
||||
file_key="img_v2_xxx",
|
||||
resource_type="image",
|
||||
)
|
||||
client.download_message_resource_bytes.assert_any_call(
|
||||
message_id="om_audio",
|
||||
file_key="file_audio",
|
||||
resource_type="audio",
|
||||
)
|
||||
|
||||
def test_module_message_reaction_helpers_delegate_to_client(self):
|
||||
module = FeishuModule()
|
||||
|
||||
Reference in New Issue
Block a user