fix(qqbot): add INTERACTION intent and expose video/file cached paths
1. Add INTERACTION intent bit (1<<26) to _send_identify, fixing approval button clicks not being received (INTERACTION_CREATE events were never dispatched by the gateway) 2. Include local cached path in video/file attachment descriptions so the LLM can reference files for re-sending to users 3. Add unit tests (TestIdentifyIntents, TestProcessAttachmentsPathExposure)
This commit is contained in:
@@ -705,9 +705,8 @@ class QQAdapter(BasePlatformAdapter):
|
|||||||
"token": f"QQBot {token}",
|
"token": f"QQBot {token}",
|
||||||
"intents": (1 << 25)
|
"intents": (1 << 25)
|
||||||
| (1 << 30)
|
| (1 << 30)
|
||||||
| (
|
| (1 << 12)
|
||||||
1 << 12
|
| (1 << 26), # C2C_GROUP_AT_MESSAGES + PUBLIC_GUILD_MESSAGES + DIRECT_MESSAGE + INTERACTION
|
||||||
), # C2C_GROUP_AT_MESSAGES + PUBLIC_GUILD_MESSAGES + DIRECT_MESSAGE
|
|
||||||
"shard": [0, 1],
|
"shard": [0, 1],
|
||||||
"properties": {
|
"properties": {
|
||||||
"$os": "macOS",
|
"$os": "macOS",
|
||||||
@@ -1620,11 +1619,15 @@ class QQAdapter(BasePlatformAdapter):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("[%s] Failed to cache image: %s", self._log_tag, exc)
|
logger.debug("[%s] Failed to cache image: %s", self._log_tag, exc)
|
||||||
else:
|
else:
|
||||||
# Other attachments (video, file, etc.): record as text.
|
# Other attachments (video, file, etc.): download and record with path.
|
||||||
try:
|
try:
|
||||||
cached_path = await self._download_and_cache(url, ct)
|
cached_path = await self._download_and_cache(url, ct)
|
||||||
if cached_path:
|
if cached_path:
|
||||||
other_attachments.append(f"[Attachment: {filename or ct}]")
|
name = filename or ct
|
||||||
|
if ct.startswith("video/"):
|
||||||
|
other_attachments.append(f"[video: {name} ({cached_path})]")
|
||||||
|
else:
|
||||||
|
other_attachments.append(f"[file: {name} ({cached_path})]")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("[%s] Failed to cache attachment: %s", self._log_tag, exc)
|
logger.debug("[%s] Failed to cache attachment: %s", self._log_tag, exc)
|
||||||
|
|
||||||
|
|||||||
@@ -1810,3 +1810,207 @@ class TestSendUpdatePrompt:
|
|||||||
|
|
||||||
adapter.send_with_keyboard = fake_swk # type: ignore[assignment]
|
adapter.send_with_keyboard = fake_swk # type: ignore[assignment]
|
||||||
await adapter.send_update_prompt(chat_id="u", prompt="ok?")
|
await adapter.send_update_prompt(chat_id="u", prompt="ok?")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _send_identify includes INTERACTION intent
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestIdentifyIntents:
|
||||||
|
"""Verify the WebSocket identify payload includes the INTERACTION intent bit."""
|
||||||
|
|
||||||
|
def _make_adapter(self):
|
||||||
|
from gateway.platforms.qqbot.adapter import QQAdapter
|
||||||
|
return QQAdapter(_make_config(app_id="a", client_secret="b"))
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_intents_include_interaction_bit(self):
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
# Mock token retrieval and WebSocket
|
||||||
|
adapter._access_token = "fake_token"
|
||||||
|
adapter._token_expires_at = 9999999999.0
|
||||||
|
|
||||||
|
sent_payloads = []
|
||||||
|
|
||||||
|
class FakeWS:
|
||||||
|
closed = False
|
||||||
|
|
||||||
|
async def send_json(self, payload):
|
||||||
|
sent_payloads.append(payload)
|
||||||
|
|
||||||
|
adapter._ws = FakeWS()
|
||||||
|
await adapter._send_identify()
|
||||||
|
|
||||||
|
assert len(sent_payloads) == 1
|
||||||
|
intents = sent_payloads[0]["d"]["intents"]
|
||||||
|
|
||||||
|
# Verify all expected intent bits are present
|
||||||
|
assert intents & (1 << 25), "GROUP_MESSAGES (1<<25) missing"
|
||||||
|
assert intents & (1 << 30), "GUILD_AT_MESSAGE (1<<30) missing"
|
||||||
|
assert intents & (1 << 12), "DIRECT_MESSAGES (1<<12) missing"
|
||||||
|
assert intents & (1 << 26), "INTERACTION (1<<26) missing"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _process_attachments: video/file path exposure
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestProcessAttachmentsPathExposure:
|
||||||
|
"""Verify that video and file attachments include the cached local path."""
|
||||||
|
|
||||||
|
def _make_adapter(self):
|
||||||
|
from gateway.platforms.qqbot.adapter import QQAdapter
|
||||||
|
return QQAdapter(_make_config(app_id="a", client_secret="b"))
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_video_attachment_includes_path(self):
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
# Mock _download_and_cache to return a known path
|
||||||
|
async def fake_download(url, ct):
|
||||||
|
return "/tmp/cache/video_abc123.mp4"
|
||||||
|
|
||||||
|
adapter._download_and_cache = fake_download # type: ignore[assignment]
|
||||||
|
|
||||||
|
attachments = [
|
||||||
|
{
|
||||||
|
"content_type": "video/mp4",
|
||||||
|
"url": "https://multimedia.nt.qq.com.cn/download/video123",
|
||||||
|
"filename": "my_video.mp4",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = await adapter._process_attachments(attachments)
|
||||||
|
|
||||||
|
assert result["image_urls"] == []
|
||||||
|
assert result["voice_transcripts"] == []
|
||||||
|
info = result["attachment_info"]
|
||||||
|
assert "[video:" in info
|
||||||
|
assert "my_video.mp4" in info
|
||||||
|
assert "/tmp/cache/video_abc123.mp4" in info
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_file_attachment_includes_path(self):
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
async def fake_download(url, ct):
|
||||||
|
return "/tmp/cache/doc_abc123_report.pdf"
|
||||||
|
|
||||||
|
adapter._download_and_cache = fake_download # type: ignore[assignment]
|
||||||
|
|
||||||
|
attachments = [
|
||||||
|
{
|
||||||
|
"content_type": "application/pdf",
|
||||||
|
"url": "https://multimedia.nt.qq.com.cn/download/file456",
|
||||||
|
"filename": "report.pdf",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = await adapter._process_attachments(attachments)
|
||||||
|
|
||||||
|
info = result["attachment_info"]
|
||||||
|
assert "[file:" in info
|
||||||
|
assert "report.pdf" in info
|
||||||
|
assert "/tmp/cache/doc_abc123_report.pdf" in info
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_video_without_filename_falls_back_to_content_type(self):
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
async def fake_download(url, ct):
|
||||||
|
return "/tmp/cache/video_xyz.mp4"
|
||||||
|
|
||||||
|
adapter._download_and_cache = fake_download # type: ignore[assignment]
|
||||||
|
|
||||||
|
attachments = [
|
||||||
|
{
|
||||||
|
"content_type": "video/mp4",
|
||||||
|
"url": "https://cdn.qq.com/vid",
|
||||||
|
"filename": "",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = await adapter._process_attachments(attachments)
|
||||||
|
|
||||||
|
info = result["attachment_info"]
|
||||||
|
assert "[video: video/mp4" in info
|
||||||
|
assert "/tmp/cache/video_xyz.mp4" in info
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_failure_produces_no_attachment_info(self):
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
async def fake_download(url, ct):
|
||||||
|
return None
|
||||||
|
|
||||||
|
adapter._download_and_cache = fake_download # type: ignore[assignment]
|
||||||
|
|
||||||
|
attachments = [
|
||||||
|
{
|
||||||
|
"content_type": "video/mp4",
|
||||||
|
"url": "https://cdn.qq.com/vid",
|
||||||
|
"filename": "vid.mp4",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
result = await adapter._process_attachments(attachments)
|
||||||
|
assert result["attachment_info"] == ""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_quoted_video_includes_path_in_quote_block(self):
|
||||||
|
"""Quoted video attachments should surface the cached path in the quote block."""
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
async def fake_process(atts):
|
||||||
|
# Simulate the fixed _process_attachments for a video attachment.
|
||||||
|
return {
|
||||||
|
"image_urls": [],
|
||||||
|
"image_media_types": [],
|
||||||
|
"voice_transcripts": [],
|
||||||
|
"attachment_info": "[video: clip.mp4 (/tmp/cache/clip.mp4)]",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter._process_attachments = fake_process # type: ignore[assignment]
|
||||||
|
|
||||||
|
d = {
|
||||||
|
"message_type": 103,
|
||||||
|
"msg_elements": [{
|
||||||
|
"content": "看看这个视频",
|
||||||
|
"attachments": [
|
||||||
|
{"content_type": "video/mp4",
|
||||||
|
"url": "https://qq-cdn/clip.mp4",
|
||||||
|
"filename": "clip.mp4"}
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
out = await adapter._process_quoted_context(d)
|
||||||
|
assert "[Quoted message]:" in out["quote_block"]
|
||||||
|
assert "/tmp/cache/clip.mp4" in out["quote_block"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_quoted_file_includes_path_in_quote_block(self):
|
||||||
|
"""Quoted file attachments should surface the cached path in the quote block."""
|
||||||
|
adapter = self._make_adapter()
|
||||||
|
|
||||||
|
async def fake_process(atts):
|
||||||
|
return {
|
||||||
|
"image_urls": [],
|
||||||
|
"image_media_types": [],
|
||||||
|
"voice_transcripts": [],
|
||||||
|
"attachment_info": "[file: report.pdf (/tmp/cache/report.pdf)]",
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter._process_attachments = fake_process # type: ignore[assignment]
|
||||||
|
|
||||||
|
d = {
|
||||||
|
"message_type": 103,
|
||||||
|
"msg_elements": [{
|
||||||
|
"content": "",
|
||||||
|
"attachments": [
|
||||||
|
{"content_type": "application/pdf",
|
||||||
|
"url": "https://qq-cdn/report.pdf",
|
||||||
|
"filename": "report.pdf"}
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
out = await adapter._process_quoted_context(d)
|
||||||
|
assert "[Quoted message]:" in out["quote_block"]
|
||||||
|
assert "/tmp/cache/report.pdf" in out["quote_block"]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user