From 1ca1deb7f6d1ef8940079b0d50eeb75c3a2be412 Mon Sep 17 00:00:00 2001 From: wuli666 <421774554@qq.com> Date: Fri, 26 Jun 2026 18:37:51 +0800 Subject: [PATCH] fix(feishu): classify native voice messages as VOICE for auto-transcription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lark's native "audio" msg_type is an in-app voice recording — uploaded audio files arrive as "file"/"media". But _resolve_normalized_message_type resolved the "audio" preferred type to MessageType.AUDIO, which the gateway treats as a non-transcribed file attachment (run.py: AUDIO -> audio_file_paths, "never STT"; VOICE -> audio_paths, "always STT"). Result: a Feishu voice note reached the agent as an untranscribable audio attachment and was silently ignored — the user's spoken message never became text. Every other platform that receives native voice notes (Telegram, Discord, Slack, WhatsApp, Signal, Matrix, WeChat, WeCom, DingTalk, QQ, BlueBubbles, Mattermost, Yuanbao) classifies them as MessageType.VOICE. Feishu was the only one classifying them as AUDIO. This is the follow-up to #28993, which added native voice-note transcription for Discord + DingTalk but did not cover Feishu. Return MessageType.VOICE for the "audio" branch. The branch is reached only for Lark's top-level audio msg_type (set in the normalizer; file uploads map to "document"), so VOICE is unconditionally correct here — no risk of auto-transcribing an uploaded music/audio file. - plugins/platforms/feishu/adapter.py: _resolve_normalized_message_type audio branch returns VOICE instead of resolving to AUDIO via mime. - tests/gateway/test_feishu.py: test_extract_audio_message_downloads_and_caches asserted the old AUDIO behavior on a fixture literally named voice.ogg — updated to expect VOICE (the corrected classification). - tests/gateway/test_feishu_voice_message_type.py: new focused regression tests (audio->VOICE with and without mime; photo/document/text unaffected). Rebased onto current main: the Feishu platform moved from the single-file gateway/platforms/feishu.py into the plugins/platforms/feishu/ package; the fix applies to the same _resolve_normalized_message_type logic at its new home. Verified the new voice tests fail when the branch resolves to AUDIO and pass with VOICE, while the photo/document/text cases are unaffected either way. Note: classification is mock-tested here; the downstream STT pipeline is the shared, already-proven path (#28993). End-to-end verification on a live Feishu account would be a welcome confirmation. Refs #28993 (sibling-gap: Feishu was the platform left uncovered). --- plugins/platforms/feishu/adapter.py | 10 +++- tests/gateway/test_feishu.py | 6 ++- .../gateway/test_feishu_voice_message_type.py | 46 +++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/gateway/test_feishu_voice_message_type.py 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