Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions gateway/platforms/qqbot/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1923,15 +1923,15 @@ async def _stt_voice_attachment(
)
return None

# 4. Call STT API
# 4. Call STT API and always clean up the temp WAV afterward.
logger.debug("[%s] STT: calling ASR on %s", self._log_tag, wav_path)
transcript = await self._call_stt(wav_path)

# 5. Cleanup temp file
try:
os.unlink(wav_path)
except OSError:
pass
transcript = await self._call_stt(wav_path)
finally:
try:
os.unlink(wav_path)
except OSError:
pass

if transcript:
logger.debug("[%s] STT success: %r", self._log_tag, transcript[:100])
Expand Down
68 changes: 68 additions & 0 deletions tests/gateway/test_qqbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from types import SimpleNamespace
from unittest import mock

import httpx
import pytest

from gateway.config import PlatformConfig
Expand Down Expand Up @@ -191,6 +192,73 @@ def test_connect_uses_redirect_guard_hook(self):
assert kwargs.get("event_hooks", {}).get("response") == [_ssrf_redirect_guard]


# ---------------------------------------------------------------------------
# Voice attachment temp-file cleanup
# ---------------------------------------------------------------------------

class TestVoiceAttachmentTempCleanup:
def _make_adapter(self, **extra):
from gateway.platforms.qqbot import QQAdapter
return QQAdapter(_make_config(**extra))

def _setup_download_mocks(self, adapter, content=b"RIFFmock-wav-audio-data"):
response = mock.Mock()
response.content = content
response.headers = {"content-type": "audio/wav"}
response.raise_for_status = mock.Mock()

adapter._http_client = mock.AsyncMock()
adapter._http_client.get = mock.AsyncMock(return_value=response)

def test_temp_wav_cleaned_up_on_stt_failure(self):
adapter = self._make_adapter(app_id="a", client_secret="b")
self._setup_download_mocks(adapter)
seen = {}

async def _raise_transport_error(path):
seen["wav_path"] = path
raise httpx.TransportError("boom")

with mock.patch("tools.url_safety.is_safe_url", return_value=True):
adapter._call_stt = mock.AsyncMock(side_effect=_raise_transport_error)
transcript = asyncio.run(
adapter._stt_voice_attachment(
"https://cdn.qq.com/voice.silk",
"audio/silk",
"voice.silk",
voice_wav_url="https://cdn.qq.com/voice.wav",
)
)

assert transcript is None
assert "wav_path" in seen
assert not os.path.exists(seen["wav_path"])

def test_temp_wav_cleaned_up_on_stt_success(self):
adapter = self._make_adapter(app_id="a", client_secret="b")
self._setup_download_mocks(adapter)
seen = {}

async def _return_transcript(path):
seen["wav_path"] = path
return "hello from qq voice"

with mock.patch("tools.url_safety.is_safe_url", return_value=True):
adapter._call_stt = mock.AsyncMock(side_effect=_return_transcript)
transcript = asyncio.run(
adapter._stt_voice_attachment(
"https://cdn.qq.com/voice.silk",
"audio/silk",
"voice.silk",
voice_wav_url="https://cdn.qq.com/voice.wav",
)
)

assert transcript == "hello from qq voice"
assert "wav_path" in seen
assert not os.path.exists(seen["wav_path"])


# ---------------------------------------------------------------------------
# WebSocket proxy handling
# ---------------------------------------------------------------------------
Expand Down
Loading