fix: include JSON cards in group context (#9655)

* fix: include JSON cards in group context

* fix: allow JSON cards to trigger active replies
This commit is contained in:
PeiPei233
2026-08-14 00:33:48 +08:00
committed by GitHub
parent 27689db58c
commit de33730454
3 changed files with 129 additions and 6 deletions
@@ -1,5 +1,6 @@
import asyncio
import datetime
import json
import random
import uuid
from collections import defaultdict, deque
@@ -14,6 +15,7 @@ from astrbot.api.message_components import (
File,
Forward,
Image,
Json,
Plain,
Record,
Reply,
@@ -217,6 +219,37 @@ class GroupChatContext:
logger.error(f"获取图片描述失败: {e}")
else:
parts.append(" [Image]")
elif isinstance(comp, Json):
card_data = comp.data
if isinstance(card_data, dict) and isinstance(
card_data.get("data"), str
):
try:
nested_data = json.loads(card_data["data"])
if isinstance(nested_data, dict):
card_data = nested_data
except json.JSONDecodeError:
pass
detail = {}
if isinstance(card_data, dict):
meta = card_data.get("meta")
if isinstance(meta, dict):
candidate = meta.get("detail_1") or meta.get("news")
if isinstance(candidate, dict):
detail = candidate
fields = []
for label, value in (
("Title", detail.get("title")),
("Description", detail.get("desc")),
("URL", detail.get("qqdocurl") or detail.get("jumpUrl")),
):
if isinstance(value, str) and value.strip():
normalized = " ".join(value.split())
fields.append(f"{label}: {_truncate_reply_text(normalized)}")
suffix = f": {'; '.join(fields)}" if fields else ""
parts.append(f" [Shared Card{suffix}]")
elif isinstance(comp, At):
is_at_self = str(comp.qq) in (
event.get_self_id(),
+5 -5
View File
@@ -6,7 +6,7 @@ from sys import maxsize
import astrbot.api.message_components as Comp
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, filter
from astrbot.api.message_components import Image, Plain
from astrbot.api.message_components import Image, Json, Plain
from astrbot.api.provider import LLMResponse, ProviderRequest
from astrbot.core import logger
from astrbot.core.message.message_event_result import MessageChain
@@ -197,10 +197,10 @@ class Main(star.Star):
async def on_message(self, event: AstrMessageEvent):
"""群聊上下文感知"""
message_components = _iter_message_components(event)
has_image_or_plain = False
has_context_content = False
for comp in message_components:
if isinstance(comp, Plain) or isinstance(comp, Image):
has_image_or_plain = True
if isinstance(comp, Plain | Image | Json):
has_context_content = True
break
group_context_enabled = False
@@ -210,7 +210,7 @@ class Main(star.Star):
except BaseException as e:
logger.error(f"group chat context: {e}")
if group_context_enabled and self.group_chat_context and has_image_or_plain:
if group_context_enabled and self.group_chat_context and has_context_content:
need_active = await self.group_chat_context.need_active_reply(event)
group_icl_enable = self.context.get_config(umo=event.unified_msg_origin)[
+91 -1
View File
@@ -1,10 +1,12 @@
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import pytest
from astrbot.api.message_components import Plain
from astrbot.api.message_components import Json, Plain
from astrbot.api.provider import LLMResponse
from astrbot.builtin_stars.astrbot.group_chat_context import GroupChatContext
from astrbot.builtin_stars.astrbot.main import Main
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.platform.message_type import MessageType
@@ -149,6 +151,30 @@ async def test_on_message_does_not_clear_group_context_on_first_enabled_message(
main.group_chat_context.remove_session.assert_not_called()
@pytest.mark.asyncio
async def test_on_message_records_json_card_and_checks_active_reply():
main = Main.__new__(Main)
main.context = MagicMock()
main.context.get_config.return_value = {
"provider_ltm_settings": {
"group_icl_enable": True,
"active_reply": {"enable": False},
},
}
main.group_chat_context = SimpleNamespace(
need_active_reply=AsyncMock(return_value=False),
handle_message=AsyncMock(),
)
event = make_event()
event.message_obj.message = [Json(data={"meta": {"news": {"title": "News"}}})]
async for _ in main.on_message(event):
pass
main.group_chat_context.need_active_reply.assert_awaited_once_with(event)
main.group_chat_context.handle_message.assert_awaited_once_with(event)
@pytest.mark.asyncio
async def test_on_message_skips_recording_when_command_handler_matched():
"""A slash-command message (handlers_parsed_params non-empty) must not be
@@ -210,3 +236,67 @@ async def test_llm_response_persists_final_complete_chain():
sender_name="bot",
max_messages=700,
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("card_data", "expected"),
[
(
{
"meta": {
"detail_1": {
"title": "WeChat AI models",
"desc": "AI learning\nwith examples",
"qqdocurl": "https://example.com/detail",
}
}
},
" [Shared Card: Title: WeChat AI models; Description: AI learning "
"with examples; URL: https://example.com/detail]",
),
(
{
"data": json.dumps(
{
"meta": {
"news": {
"title": "Wrapped card",
"jumpUrl": "https://example.com/news",
}
}
}
)
},
" [Shared Card: Title: Wrapped card; URL: https://example.com/news]",
),
({"app": "com.example.unknown"}, " [Shared Card]"),
],
)
async def test_format_message_summarizes_json_card(card_data, expected):
context = GroupChatContext(MagicMock(), MagicMock())
event = MagicMock()
event.message_obj = SimpleNamespace(sender=SimpleNamespace(nickname="Alice"))
event.get_messages.return_value = [Json(data=card_data)]
formatted = await context._format_message(event, {})
assert formatted.endswith(expected)
@pytest.mark.asyncio
async def test_format_message_truncates_long_json_card_fields():
context = GroupChatContext(MagicMock(), MagicMock())
event = MagicMock()
event.message_obj = SimpleNamespace(sender=SimpleNamespace(nickname="Alice"))
event.get_messages.return_value = [
Json(
data={
"meta": {"news": {"desc": "a" * 201}},
}
)
]
formatted = await context._format_message(event, {})
assert f"Description: {'a' * 200}...]" in formatted