diff --git a/plugins/platforms/feishu/adapter.py b/plugins/platforms/feishu/adapter.py index eb1f5df3a56..f3756a8589b 100644 --- a/plugins/platforms/feishu/adapter.py +++ b/plugins/platforms/feishu/adapter.py @@ -3874,7 +3874,15 @@ class FeishuAdapter(BasePlatformAdapter): if preferred == "photo": return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.PHOTO) if preferred == "audio": - return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.AUDIO) + # Lark's native "audio" msg_type is an in-app voice recording, not + # an uploaded audio file (those arrive as "file"/"media" and are + # normalized to "document"). Classify it as VOICE so the gateway + # auto-transcribes it (Opus → STT) the same way + # Discord/DingTalk/Telegram/etc. do — otherwise a Feishu voice note + # reaches the agent as an untranscribable AUDIO attachment and is + # silently ignored. Follow-up to #28993, which added native + # voice-note transcription for Discord + DingTalk. + return MessageType.VOICE if preferred == "document": return self._resolve_media_message_type(media_types[0] if media_types else "", default=MessageType.DOCUMENT) return MessageType.TEXT diff --git a/tests/gateway/test_feishu.py b/tests/gateway/test_feishu.py index 0e411b8cf60..d887bc94f38 100644 --- a/tests/gateway/test_feishu.py +++ b/tests/gateway/test_feishu.py @@ -1490,7 +1490,11 @@ class TestAdapterBehavior(unittest.TestCase): text, msg_type, media_urls, media_types, _mentions = asyncio.run(adapter._extract_message_content(message)) self.assertEqual(text, "") - self.assertEqual(msg_type.value, "audio") + # Lark "audio" msg_type is a native voice recording (the fixture is + # literally voice.ogg) — it must classify as VOICE so the gateway + # auto-transcribes it, not AUDIO (a non-transcribed file attachment). + # See the #28993 follow-up fix in _resolve_normalized_message_type. + self.assertEqual(msg_type.value, "voice") self.assertEqual(media_urls, ["/tmp/feishu-audio.ogg"]) self.assertEqual(media_types, ["audio/ogg"]) diff --git a/tests/gateway/test_feishu_voice_message_type.py b/tests/gateway/test_feishu_voice_message_type.py new file mode 100644 index 00000000000..c6e6723c562 --- /dev/null +++ b/tests/gateway/test_feishu_voice_message_type.py @@ -0,0 +1,46 @@ +"""Regression tests for Feishu native voice-note classification. + +Lark's native ``audio`` msg_type is an in-app voice recording (uploaded +audio files arrive as ``file``/``media`` and normalize to ``document``). It +must be classified as MessageType.VOICE so the gateway auto-transcribes it +(Opus → STT), the same way Discord/DingTalk/Telegram do. Before the fix it +resolved to MessageType.AUDIO, which the gateway treats as a non-transcribed +file attachment — so a Feishu voice note silently reached the agent as +untranscribable audio. Follow-up to #28993 (Discord + DingTalk). +""" + +from gateway.platforms.base import MessageType +from plugins.platforms.feishu.adapter import FeishuAdapter, FeishuNormalizedMessage + + +def _resolve(preferred: str, media_types): + """Call _resolve_normalized_message_type without a full adapter init. + + The method only reads normalized.preferred_message_type and delegates to + the static _resolve_media_message_type — no instance state — so we bypass + __init__ (which needs Lark credentials/config) via __new__. + """ + adapter = FeishuAdapter.__new__(FeishuAdapter) + normalized = FeishuNormalizedMessage( + raw_type=preferred, + text_content="", + preferred_message_type=preferred, + ) + return adapter._resolve_normalized_message_type(normalized, media_types) + + +def test_native_voice_audio_is_classified_as_voice(): + """Lark audio msg_type (voice recording) → VOICE, so it gets transcribed.""" + assert _resolve("audio", ["audio/opus"]) is MessageType.VOICE + + +def test_native_voice_audio_without_media_type_is_voice(): + """A voice note with no resolved mime still classifies as VOICE.""" + assert _resolve("audio", []) is MessageType.VOICE + + +def test_photo_and_document_unaffected(): + """The fix is scoped to the audio branch — other types are unchanged.""" + assert _resolve("photo", ["image/png"]) is MessageType.PHOTO + assert _resolve("document", ["application/pdf"]) is MessageType.DOCUMENT + assert _resolve("text", []) is MessageType.TEXT