mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-30 06:41:51 +00:00
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:
parent
66d81f9e14
commit
bbd77d165c
2 changed files with 212 additions and 5 deletions
|
|
@ -705,9 +705,8 @@ class QQAdapter(BasePlatformAdapter):
|
|||
"token": f"QQBot {token}",
|
||||
"intents": (1 << 25)
|
||||
| (1 << 30)
|
||||
| (
|
||||
1 << 12
|
||||
), # C2C_GROUP_AT_MESSAGES + PUBLIC_GUILD_MESSAGES + DIRECT_MESSAGE
|
||||
| (1 << 12)
|
||||
| (1 << 26), # C2C_GROUP_AT_MESSAGES + PUBLIC_GUILD_MESSAGES + DIRECT_MESSAGE + INTERACTION
|
||||
"shard": [0, 1],
|
||||
"properties": {
|
||||
"$os": "macOS",
|
||||
|
|
@ -1620,11 +1619,15 @@ class QQAdapter(BasePlatformAdapter):
|
|||
except Exception as exc:
|
||||
logger.debug("[%s] Failed to cache image: %s", self._log_tag, exc)
|
||||
else:
|
||||
# Other attachments (video, file, etc.): record as text.
|
||||
# Other attachments (video, file, etc.): download and record with path.
|
||||
try:
|
||||
cached_path = await self._download_and_cache(url, ct)
|
||||
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:
|
||||
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]
|
||||
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"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue