feat(qq): support group at messages without regressing msg_seq deduplication or startup behavior
This commit is contained in:
@@ -13,16 +13,17 @@ from nanobot.config.schema import QQConfig
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import botpy
|
import botpy
|
||||||
from botpy.message import C2CMessage
|
from botpy.message import C2CMessage, GroupMessage
|
||||||
|
|
||||||
QQ_AVAILABLE = True
|
QQ_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
QQ_AVAILABLE = False
|
QQ_AVAILABLE = False
|
||||||
botpy = None
|
botpy = None
|
||||||
C2CMessage = None
|
C2CMessage = None
|
||||||
|
GroupMessage = None
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from botpy.message import C2CMessage
|
from botpy.message import C2CMessage, GroupMessage
|
||||||
|
|
||||||
|
|
||||||
def _make_bot_class(channel: "QQChannel") -> "type[botpy.Client]":
|
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)
|
logger.info("QQ bot ready: {}", self.robot.name)
|
||||||
|
|
||||||
async def on_c2c_message_create(self, message: "C2CMessage"):
|
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):
|
async def on_direct_message_create(self, message):
|
||||||
await channel._on_message(message)
|
await channel._on_message(message, is_group=False)
|
||||||
|
|
||||||
return _Bot
|
return _Bot
|
||||||
|
|
||||||
@@ -57,6 +61,7 @@ class QQChannel(BaseChannel):
|
|||||||
self._client: "botpy.Client | None" = None
|
self._client: "botpy.Client | None" = None
|
||||||
self._processed_ids: deque = deque(maxlen=1000)
|
self._processed_ids: deque = deque(maxlen=1000)
|
||||||
self._msg_seq: int = 1 # 消息序列号,避免被 QQ API 去重
|
self._msg_seq: int = 1 # 消息序列号,避免被 QQ API 去重
|
||||||
|
self._chat_type_cache: dict[str, str] = {}
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
"""Start the QQ bot."""
|
"""Start the QQ bot."""
|
||||||
@@ -71,8 +76,7 @@ class QQChannel(BaseChannel):
|
|||||||
self._running = True
|
self._running = True
|
||||||
BotClass = _make_bot_class(self)
|
BotClass = _make_bot_class(self)
|
||||||
self._client = BotClass()
|
self._client = BotClass()
|
||||||
|
logger.info("QQ bot started (C2C & Group supported)")
|
||||||
logger.info("QQ bot started (C2C private message)")
|
|
||||||
await self._run_bot()
|
await self._run_bot()
|
||||||
|
|
||||||
async def _run_bot(self) -> None:
|
async def _run_bot(self) -> None:
|
||||||
@@ -101,20 +105,31 @@ class QQChannel(BaseChannel):
|
|||||||
if not self._client:
|
if not self._client:
|
||||||
logger.warning("QQ client not initialized")
|
logger.warning("QQ client not initialized")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg_id = msg.metadata.get("message_id")
|
msg_id = msg.metadata.get("message_id")
|
||||||
self._msg_seq += 1 # 递增序列号
|
self._msg_seq += 1
|
||||||
await self._client.api.post_c2c_message(
|
msg_type = self._chat_type_cache.get(msg.chat_id, "c2c")
|
||||||
openid=msg.chat_id,
|
if msg_type == "group":
|
||||||
msg_type=0,
|
await self._client.api.post_group_message(
|
||||||
content=msg.content,
|
group_openid=msg.chat_id,
|
||||||
msg_id=msg_id,
|
msg_type=0,
|
||||||
msg_seq=self._msg_seq, # 添加序列号避免去重
|
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:
|
except Exception as e:
|
||||||
logger.error("Error sending QQ message: {}", 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."""
|
"""Handle incoming message from QQ."""
|
||||||
try:
|
try:
|
||||||
# Dedup by message ID
|
# Dedup by message ID
|
||||||
@@ -122,18 +137,24 @@ class QQChannel(BaseChannel):
|
|||||||
return
|
return
|
||||||
self._processed_ids.append(data.id)
|
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()
|
content = (data.content or "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
return
|
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(
|
await self._handle_message(
|
||||||
sender_id=user_id,
|
sender_id=user_id,
|
||||||
chat_id=user_id,
|
chat_id=chat_id,
|
||||||
content=content,
|
content=content,
|
||||||
metadata={"message_id": data.id},
|
metadata={"message_id": data.id},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Error handling QQ message")
|
logger.exception("Error handling QQ message")
|
||||||
|
|
||||||
|
|||||||
66
tests/test_qq_channel.py
Normal file
66
tests/test_qq_channel.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user