mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(qqbot): keep extension fallback for voice detection, skip only for explicit file uploads
Refined the fix: instead of removing extension-based fallback entirely, only skip it when content_type is explicitly 'file' (or image/video). Empty or unknown content_types still fall back to extension matching as a defensive measure. - content_type='voice' or 'audio/*' → True (API signal) - content_type='file' → False (file transfer, never voice) - content_type='' → extension fallback (defensive) - content_type=unknown → extension fallback (defensive) Added _looks_like_voice() module-level helper and comprehensive tests.
This commit is contained in:
parent
3122dc1744
commit
e7cc10111a
2 changed files with 41 additions and 12 deletions
|
|
@ -151,6 +151,15 @@ def _coerce_list(value: Any) -> List[str]:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _looks_like_voice(filename: str) -> bool:
|
||||
"""Return True if *filename* has a known voice/audio extension."""
|
||||
fn = filename.strip().lower()
|
||||
return any(
|
||||
fn.endswith(ext)
|
||||
for ext in (".silk", ".amr", ".mp3", ".wav", ".ogg", ".m4a", ".aac", ".speex", ".flac")
|
||||
)
|
||||
|
||||
|
||||
class QQAdapter(BasePlatformAdapter):
|
||||
"""QQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API."""
|
||||
|
||||
|
|
@ -1809,15 +1818,25 @@ class QQAdapter(BasePlatformAdapter):
|
|||
def _is_voice_content_type(content_type: str, filename: str) -> bool:
|
||||
"""Check if an attachment is a voice/audio message.
|
||||
|
||||
Only trust ``content_type`` — the QQ Bot API explicitly distinguishes
|
||||
voice messages (``content_type="voice"``) from file uploads
|
||||
(``content_type="file"``). Filename-extension sniffing is intentionally
|
||||
omitted: files sent via QQ's file-transfer feature can have audio
|
||||
extensions (e.g. ``.wav``, ``.mp3``) but must **not** be routed through
|
||||
The QQ Bot API explicitly sets ``content_type="voice"`` for voice
|
||||
messages and ``content_type="file"`` for file uploads. When
|
||||
``content_type`` is a known non-voice type (``"file"``, ``"image/*"``,
|
||||
``"video/*"``), we trust it unconditionally and **skip** extension
|
||||
sniffing — files sent via QQ's file-transfer feature can have audio
|
||||
extensions (e.g. ``.wav``, ``.mp3``) but must not be routed through
|
||||
the STT pipeline.
|
||||
|
||||
When ``content_type`` is empty or unrecognised we fall back to
|
||||
filename-extension matching as a defensive measure.
|
||||
"""
|
||||
ct = content_type.strip().lower()
|
||||
return ct == "voice" or ct.startswith("audio/")
|
||||
if ct == "voice" or ct.startswith("audio/"):
|
||||
return True
|
||||
# Explicit non-voice types — never treat as voice regardless of extension.
|
||||
if ct in ("file", "") or ct.startswith("image/") or ct.startswith("video/"):
|
||||
return ct == "" and _looks_like_voice(filename)
|
||||
# Unknown content_type — defensive extension fallback.
|
||||
return _looks_like_voice(filename)
|
||||
|
||||
def _qq_media_headers(self) -> Dict[str, str]:
|
||||
"""Return Authorization headers for QQ multimedia CDN downloads.
|
||||
|
|
|
|||
|
|
@ -139,22 +139,32 @@ class TestIsVoiceContentType:
|
|||
def test_audio_content_type(self):
|
||||
assert self._fn("audio/mp3", "file.mp3") is True
|
||||
|
||||
def test_voice_extension_ignored_when_content_type_empty(self):
|
||||
"""content_type='' with audio extension → False (no extension sniffing)."""
|
||||
assert self._fn("", "file.silk") is False
|
||||
def test_voice_extension_fallback_when_content_type_empty(self):
|
||||
"""content_type='' with audio extension → True (extension fallback)."""
|
||||
assert self._fn("", "file.silk") is True
|
||||
|
||||
def test_non_voice(self):
|
||||
assert self._fn("image/jpeg", "photo.jpg") is False
|
||||
|
||||
def test_audio_extension_amr_ignored_when_content_type_empty(self):
|
||||
"""content_type='' with .amr extension → False (no extension sniffing)."""
|
||||
assert self._fn("", "recording.amr") is False
|
||||
def test_audio_extension_amr_fallback_when_content_type_empty(self):
|
||||
"""content_type='' with .amr extension → True (extension fallback)."""
|
||||
assert self._fn("", "recording.amr") is True
|
||||
|
||||
def test_file_upload_with_audio_extension(self):
|
||||
"""File upload with audio extension must NOT be treated as voice."""
|
||||
assert self._fn("file", "song.mp3") is False
|
||||
assert self._fn("file", "audio-30251.instrumental..wav") is False
|
||||
|
||||
def test_file_upload_never_voice(self):
|
||||
"""content_type='file' is never voice, regardless of extension."""
|
||||
assert self._fn("file", "recording.silk") is False
|
||||
assert self._fn("file", "voice.amr") is False
|
||||
|
||||
def test_unknown_content_type_extension_fallback(self):
|
||||
"""Unknown content_type falls back to extension matching."""
|
||||
assert self._fn("unknown/type", "voice.ogg") is True
|
||||
assert self._fn("unknown/type", "data.json") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Voice attachment SSRF protection
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue