diff --git a/nanobot/channels/qq.py b/nanobot/channels/qq.py index 6c58049..4809fd3 100644 --- a/nanobot/channels/qq.py +++ b/nanobot/channels/qq.py @@ -13,16 +13,17 @@ from nanobot.config.schema import QQConfig try: import botpy - from botpy.message import C2CMessage + from botpy.message import C2CMessage, GroupMessage QQ_AVAILABLE = True except ImportError: QQ_AVAILABLE = False botpy = None C2CMessage = None + GroupMessage = None if TYPE_CHECKING: - from botpy.message import C2CMessage + from botpy.message import C2CMessage, GroupMessage def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]": @@ -38,10 +39,13 @@ def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]": logger.info("QQ bot ready: {}", self.robot.name) async def on_c2c_message_create(self, message: "C2CMessage"): - await channel._on_message(message) + await channel._on_message(message, is_group=False) + + async def on_group_at_message_create(self, message: "GroupMessage"): + await channel._on_message(message, is_group=True) async def on_direct_message_create(self, message): - await channel._on_message(message) + await channel._on_message(message, is_group=False) return _Bot @@ -57,6 +61,7 @@ class QQChannel(BaseChannel): self._client: "botpy.Client | None" = None self._processed_ids: deque = deque(maxlen=1000) self._msg_seq: int = 1 # 消息序列号,避免被 QQ API 去重 + self._chat_type_cache: dict[str, str] = {} async def start(self) -> None: """Start the QQ bot.""" @@ -71,8 +76,7 @@ class QQChannel(BaseChannel): self._running = True BotClass = _make_bot_class(self) self._client = BotClass() - - logger.info("QQ bot started (C2C private message)") + logger.info("QQ bot started (C2C & Group supported)") await self._run_bot() async def _run_bot(self) -> None: @@ -101,20 +105,31 @@ class QQChannel(BaseChannel): if not self._client: logger.warning("QQ client not initialized") return + try: msg_id = msg.metadata.get("message_id") - self._msg_seq += 1 # 递增序列号 - await self._client.api.post_c2c_message( - openid=msg.chat_id, - msg_type=0, - content=msg.content, - msg_id=msg_id, - msg_seq=self._msg_seq, # 添加序列号避免去重 - ) + self._msg_seq += 1 + msg_type = self._chat_type_cache.get(msg.chat_id, "c2c") + if msg_type == "group": + await self._client.api.post_group_message( + group_openid=msg.chat_id, + msg_type=0, + content=msg.content, + msg_id=msg_id, + msg_seq=self._msg_seq, + ) + else: + await self._client.api.post_c2c_message( + openid=msg.chat_id, + msg_type=0, + content=msg.content, + msg_id=msg_id, + msg_seq=self._msg_seq, + ) except Exception as e: logger.error("Error sending QQ message: {}", e) - async def _on_message(self, data: "C2CMessage") -> None: + async def _on_message(self, data: "C2CMessage | GroupMessage", is_group: bool = False) -> None: """Handle incoming message from QQ.""" try: # Dedup by message ID @@ -122,18 +137,24 @@ class QQChannel(BaseChannel): return self._processed_ids.append(data.id) - author = data.author - user_id = str(getattr(author, 'id', None) or getattr(author, 'user_openid', 'unknown')) content = (data.content or "").strip() if not content: return + if is_group: + chat_id = data.group_openid + user_id = data.author.member_openid + self._chat_type_cache[chat_id] = "group" + else: + chat_id = str(getattr(data.author, 'id', None) or getattr(data.author, 'user_openid', 'unknown')) + user_id = chat_id + self._chat_type_cache[chat_id] = "c2c" + await self._handle_message( sender_id=user_id, - chat_id=user_id, + chat_id=chat_id, content=content, metadata={"message_id": data.id}, ) except Exception: logger.exception("Error handling QQ message") - diff --git a/tests/test_qq_channel.py b/tests/test_qq_channel.py new file mode 100644 index 0000000..90b4e60 --- /dev/null +++ b/tests/test_qq_channel.py @@ -0,0 +1,66 @@ +from types import SimpleNamespace + +import pytest + +from nanobot.bus.events import OutboundMessage +from nanobot.bus.queue import MessageBus +from nanobot.channels.qq import QQChannel +from nanobot.config.schema import QQConfig + + +class _FakeApi: + def __init__(self) -> None: + self.c2c_calls: list[dict] = [] + self.group_calls: list[dict] = [] + + async def post_c2c_message(self, **kwargs) -> None: + self.c2c_calls.append(kwargs) + + async def post_group_message(self, **kwargs) -> None: + self.group_calls.append(kwargs) + + +class _FakeClient: + def __init__(self) -> None: + self.api = _FakeApi() + + +@pytest.mark.asyncio +async def test_on_group_message_routes_to_group_chat_id() -> None: + channel = QQChannel(QQConfig(app_id="app", secret="secret", allow_from=["user1"]), MessageBus()) + + data = SimpleNamespace( + id="msg1", + content="hello", + group_openid="group123", + author=SimpleNamespace(member_openid="user1"), + ) + + await channel._on_message(data, is_group=True) + + msg = await channel.bus.consume_inbound() + assert msg.sender_id == "user1" + assert msg.chat_id == "group123" + + +@pytest.mark.asyncio +async def test_send_group_message_uses_group_api_with_msg_seq() -> None: + channel = QQChannel(QQConfig(app_id="app", secret="secret", allow_from=["*"]), MessageBus()) + channel._client = _FakeClient() + channel._chat_type_cache["group123"] = "group" + + await channel.send( + OutboundMessage( + channel="qq", + chat_id="group123", + content="hello", + metadata={"message_id": "msg1"}, + ) + ) + + assert len(channel._client.api.group_calls) == 1 + call = channel._client.api.group_calls[0] + assert call["group_openid"] == "group123" + assert call["msg_id"] == "msg1" + assert call["msg_seq"] == 2 + assert not channel._client.api.c2c_calls