mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-08-30 17:33:24 +08:00
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:
@@ -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(),
|
||||
|
||||
@@ -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)[
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user