Files
AstrBot/tests/test_platform_audio_media_resolver.py
Weilong Liao 7c366a708b fix: unify media reference handling (#8764)
* fix: unify media reference handling

* fix: accept bare base64 record media refs

* chore: update agents.md

* fix: unify file URI handling across media components and utilities

* fix: unify media reference type handling with MediaRefStr alias

* Potential fix for pull request finding 'CodeQL / Incomplete URL substring sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Update astrbot/core/platform/sources/discord/discord_platform_adapter.py

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: unify media handling and improve base64 decoding across components

* fix: simplify client_kwargs type definition and enhance media message handling in platform adapter

* fix: unify media utility documentation and enhance function descriptions

* perf: drop "pilk" requirement, improve audio outbound for tencent-related IM apps which using silk

* fix: unify Tencent Silk audio handling and enhance media resolver functionality

---

- Centralize media reference materialization and base64 resolution for local paths, http(s), base64://, data URIs, and legacy bare base64 payloads.
- Normalize incoming Record audio to wav and Image media to temporary jpg during preprocess, with event-scoped cleanup.
- Reuse the shared media resolver across OpenAI, Gemini, Anthropic, MiMo, DeerFlow, STT, and platform media paths while sanitizing logs and cleaning temporary conversion outputs.
- Ensure generated TTS audio is tracked for cleanup after the event finishes.

fix #8676
fix #8543
fix #7588
fix #7580
fix #8030
fix #8034
fix #7461
fix #7565
fix #6509
fix #7144
fix #7795



---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-06-14 10:37:16 +08:00

167 lines
5.0 KiB
Python

import pytest
from astrbot.api.event import MessageChain
from astrbot.api.message_components import Record
from astrbot.core.platform.sources.lark import lark_adapter
from astrbot.core.platform.sources.lark.lark_adapter import LarkPlatformAdapter
from astrbot.core.platform.sources.line import line_adapter
from astrbot.core.platform.sources.line.line_adapter import LinePlatformAdapter
from astrbot.core.platform.sources.misskey import misskey_utils
from astrbot.core.platform.sources.misskey.misskey_utils import create_file_component
from astrbot.core.platform.sources.qqofficial import (
qqofficial_message_event,
qqofficial_platform_adapter,
)
from astrbot.core.platform.sources.qqofficial.qqofficial_message_event import (
QQOfficialMessageEvent,
)
from astrbot.core.platform.sources.qqofficial.qqofficial_platform_adapter import (
QQOfficialPlatformAdapter,
)
WAV_PATH = "/tmp/astrbot-platform-audio.wav"
class FakeMediaResolver:
calls = []
def __init__(self, media_ref: str, **kwargs) -> None:
self.media_ref = media_ref
self.kwargs = kwargs
self.calls.append((media_ref, kwargs))
async def to_path(self, **kwargs) -> str:
self.calls[-1] = (*self.calls[-1], kwargs)
return WAV_PATH
def _patch_resolver(monkeypatch, module) -> None:
FakeMediaResolver.calls = []
monkeypatch.setattr(module, "MediaResolver", FakeMediaResolver)
@pytest.mark.asyncio
async def test_line_audio_component_uses_media_resolver_for_external_url(monkeypatch):
_patch_resolver(monkeypatch, line_adapter)
adapter = LinePlatformAdapter.__new__(LinePlatformAdapter)
record = await adapter._build_audio_component(
"msg-1",
{
"contentProvider": {
"type": "external",
"originalContentUrl": "https://example.test/voice.m4a",
}
},
)
assert isinstance(record, Record)
assert record.file == WAV_PATH
assert record.url == WAV_PATH
assert FakeMediaResolver.calls == [
(
"https://example.test/voice.m4a",
{"media_type": "audio", "default_suffix": ".wav"},
{"target_format": "wav"},
)
]
@pytest.mark.asyncio
async def test_lark_audio_component_uses_media_resolver_after_download(monkeypatch):
_patch_resolver(monkeypatch, lark_adapter)
adapter = LarkPlatformAdapter.__new__(LarkPlatformAdapter)
async def fake_download_file_resource_to_temp(**kwargs):
assert kwargs["message_type"] == "audio"
return "/tmp/lark-source.opus"
monkeypatch.setattr(
adapter,
"_download_file_resource_to_temp",
fake_download_file_resource_to_temp,
)
records = await adapter._parse_message_components(
message_id="msg-1",
message_type="audio",
content={"file_key": "file-key"},
at_map={},
)
assert len(records) == 1
assert isinstance(records[0], Record)
assert records[0].file == WAV_PATH
assert records[0].url == WAV_PATH
assert FakeMediaResolver.calls == [
(
"/tmp/lark-source.opus",
{"media_type": "audio", "default_suffix": ".wav"},
{"target_format": "wav"},
)
]
@pytest.mark.asyncio
async def test_qqofficial_audio_attachment_uses_media_resolver(monkeypatch):
_patch_resolver(monkeypatch, qqofficial_platform_adapter)
record = await QQOfficialPlatformAdapter._prepare_audio_attachment(
"https://example.test/voice.amr",
"voice.amr",
)
assert isinstance(record, Record)
assert record.file == WAV_PATH
assert record.url == WAV_PATH
assert FakeMediaResolver.calls == [
(
"https://example.test/voice.amr",
{"media_type": "audio", "default_suffix": ".amr"},
{"target_format": "wav"},
)
]
@pytest.mark.asyncio
async def test_qqofficial_send_record_resolves_to_tencent_silk(monkeypatch):
_patch_resolver(monkeypatch, qqofficial_message_event)
parsed = await QQOfficialMessageEvent._parse_to_qqofficial(
MessageChain([Record(file="voice.amr", url="https://example.test/voice.amr")])
)
assert parsed[3] == WAV_PATH
assert FakeMediaResolver.calls == [
(
"https://example.test/voice.amr",
{"media_type": "audio", "default_suffix": ".wav"},
{"target_format": "tencent_silk"},
)
]
@pytest.mark.asyncio
async def test_misskey_audio_file_component_uses_media_resolver(monkeypatch):
_patch_resolver(monkeypatch, misskey_utils)
record, part_text = await create_file_component(
{
"url": "https://example.test/voice.ogg",
"name": "voice.ogg",
"type": "audio/ogg",
}
)
assert isinstance(record, Record)
assert record.file == WAV_PATH
assert record.url == WAV_PATH
assert part_text == "音频[voice.ogg]"
assert FakeMediaResolver.calls == [
(
"https://example.test/voice.ogg",
{"media_type": "audio", "default_suffix": ".wav"},
{"target_format": "wav"},
)
]