diff --git a/onebot_adapter/README.md b/onebot_adapter/README.md index a6b4228..f78e5e4 100644 --- a/onebot_adapter/README.md +++ b/onebot_adapter/README.md @@ -70,6 +70,7 @@ if ctx: | `send_private_msg` | `user_id`、`message` | `data.message_id` | | `delete_msg` | `message_id` | 当前实现依赖消息 ID,失败会返回 `retcode=1` | | `get_login_info` | 无 | `user_id`、`nickname` | +| `get_group_info` | `group_id` | 实时群名称、当前成员数和成员上限 | | `get_group_list` | 无 | 已缓存群列表 | | `get_friend_list` | 无 | 已缓存用户列表 | | `get_stranger_info` | `user_id` | 最小用户信息 | diff --git a/onebot_adapter/action_registry.py b/onebot_adapter/action_registry.py index 482489a..830cd43 100644 --- a/onebot_adapter/action_registry.py +++ b/onebot_adapter/action_registry.py @@ -13,6 +13,7 @@ CanSendRecordAction, DeleteMessageAction, GetFriendListAction, + GetGroupInfoAction, GetGroupListAction, GetGroupMemberInfoAction, GetGroupMemberListAction, @@ -71,6 +72,7 @@ def create_default(cls, ctx: ActionContext) -> ActionRegistry: 'send_private_msg': SendMessageAction(ctx, force_type='private'), 'delete_msg': DeleteMessageAction(ctx), 'get_login_info': GetLoginInfoAction(ctx), + 'get_group_info': GetGroupInfoAction(ctx), 'get_group_list': GetGroupListAction(ctx), 'get_friend_list': GetFriendListAction(ctx), 'get_stranger_info': GetStrangerInfoAction(ctx), diff --git a/onebot_adapter/actions/__init__.py b/onebot_adapter/actions/__init__.py index 6d51f36..c470ee7 100644 --- a/onebot_adapter/actions/__init__.py +++ b/onebot_adapter/actions/__init__.py @@ -6,6 +6,7 @@ - SendMessageAction: send_msg / send_group_msg / send_private_msg - DeleteMessageAction: delete_msg - GetLoginInfoAction: get_login_info + - GetGroupInfoAction: get_group_info - GetGroupListAction: get_group_list - GetFriendListAction: get_friend_list - GetStrangerInfoAction: get_stranger_info @@ -23,6 +24,7 @@ from modules.onebot_adapter.actions.can_send_record import CanSendRecordAction from modules.onebot_adapter.actions.delete_msg import DeleteMessageAction from modules.onebot_adapter.actions.get_friend_list import GetFriendListAction +from modules.onebot_adapter.actions.get_group_info import GetGroupInfoAction from modules.onebot_adapter.actions.get_group_list import GetGroupListAction from modules.onebot_adapter.actions.get_group_member_info import ( GetGroupMemberInfoAction, @@ -43,6 +45,7 @@ 'CanSendRecordAction', 'DeleteMessageAction', 'GetFriendListAction', + 'GetGroupInfoAction', 'GetGroupListAction', 'GetGroupMemberInfoAction', 'GetGroupMemberListAction', diff --git a/onebot_adapter/actions/get_group_info.py b/onebot_adapter/actions/get_group_info.py new file mode 100644 index 0000000..06c627c --- /dev/null +++ b/onebot_adapter/actions/get_group_info.py @@ -0,0 +1,78 @@ +"""get_group_info - query and return real group information.""" + +from __future__ import annotations + +from typing import Any + +from modules.onebot_adapter.base_action import BaseAction + + +class GetGroupInfoAction(BaseAction): + """Convert QQ group information to the OneBot 11 response shape.""" + + async def execute(self, params: dict[str, Any], echo: str | None = None) -> dict[str, Any]: + group_id = params.get('group_id', 0) + self._ctx.log.info(f'get_group_info 请求: group_id={group_id}') + + group_openid = await self._resolve_group_openid(group_id) + if not group_openid: + self._ctx.log.warning(f'get_group_info 参数错误: 无效 group_id={group_id}') + return self._fail('缺少有效的 group_id', echo=echo) + + sender = self._ctx.get_sender() + if not sender: + self._ctx.log.warning( + f'get_group_info 查询失败: group_id={group_openid}, ' + 'reason=无可用的消息发送器' + ) + return self._fail('无可用的消息发送器', echo=echo) + + group, error = await sender.get_group_info(group_openid, return_error=True) + if group is None: + self._ctx.log.warning( + f'get_group_info 查询失败: group_id={group_openid}, response={error}' + ) + return self._platform_fail(error, echo) + + member_count = self._to_non_negative_int( + group.get('group_member_num', group.get('member_count', 0)) + ) + max_member_count = self._to_non_negative_int( + group.get('max_group_member_num', group.get('max_member_count', 0)) + ) + result = { + 'group_id': group_id, + 'group_name': str(group.get('group_name') or ''), + 'member_count': member_count, + 'max_member_count': max_member_count, + } + self._ctx.log.info( + f'get_group_info 成功: group_id={group_openid}, member_count={member_count}' + ) + return self._ok(result, echo=echo) + + async def _resolve_group_openid(self, raw_id: Any) -> str | None: + value = str(raw_id or '').strip() + if not value: + return None + if not value.isdigit(): + return value + if not self._ctx.id_mapper: + return None + return await self._ctx.id_mapper.to_openid_by_type(int(value), 'group') + + def _platform_fail(self, response: Any, echo: str | None) -> dict[str, Any]: + data = response if isinstance(response, dict) else {} + message = str(data.get('message') or data.get('msg') or response or '查询群信息失败') + try: + retcode = int(data.get('code') or data.get('err_code') or 1) + except (TypeError, ValueError): + retcode = 1 + return self._fail(message, echo=echo, retcode=retcode) + + @staticmethod + def _to_non_negative_int(value: Any) -> int: + try: + return max(0, int(value or 0)) + except (TypeError, ValueError): + return 0 diff --git a/onebot_adapter/adapter.py b/onebot_adapter/adapter.py index 9f3993c..f526604 100644 --- a/onebot_adapter/adapter.py +++ b/onebot_adapter/adapter.py @@ -19,6 +19,8 @@ from __future__ import annotations +import asyncio +import time from typing import TYPE_CHECKING, Any from modules.onebot_adapter.action_context import ActionContext @@ -58,6 +60,9 @@ class OneBotAdapter: - 事件格式转换 → lib/event_converter.py """ + _GROUP_NAME_CACHE_TTL = 300 + _EMPTY_GROUP_NAME_CACHE_TTL = 3600 + # --- instance variables (declared for type checker) --- _mctx: ModuleContext log: Any @@ -87,6 +92,8 @@ def __init__(self, module_ctx: ModuleContext) -> None: # 运行时状态 self._bm: BotManager | None = None # BotManager 引用 + self._group_name_cache: dict[tuple[str, str], tuple[float, str]] = {} + self._group_name_locks: dict[tuple[str, str], asyncio.Lock] = {} # ==================== 生命周期 ==================== @@ -348,6 +355,49 @@ async def _stop_network(self) -> None: # ==================== 事件处理 (Observer) ==================== + async def _get_cached_group_name(self, event: Event, bot: Any) -> str: + """优先读取已保存的群名,未命中时查询一次群资料。""" + group_id = str(event.group_id or '') + if not event.is_group or not group_id: + return '' + + cache_key = (str(event.appid or ''), group_id) + now = time.monotonic() + cached = self._group_name_cache.get(cache_key) + if cached and cached[0] > now: + return cached[1] + + lock = self._group_name_locks.setdefault(cache_key, asyncio.Lock()) + async with lock: + now = time.monotonic() + cached = self._group_name_cache.get(cache_key) + if cached and cached[0] > now: + return cached[1] + + group_name = '' + sender = getattr(bot, 'sender', None) + get_group_record = getattr(sender, 'get_group_record', None) + if get_group_record is not None: + try: + record = await get_group_record(group_id) + if isinstance(record, dict): + group_name = str(record.get('group_name') or '') + except Exception as e: + self.log.debug(f'读取 OneBot 上报群名失败: group_id={group_id}, error={e}') + + get_group_info = getattr(sender, 'get_group_info', None) + if not group_name and get_group_info is not None: + try: + group_info = await get_group_info(group_id) + if isinstance(group_info, dict): + group_name = str(group_info.get('group_name') or '') + except Exception as e: + self.log.debug(f'刷新 OneBot 上报群名失败: group_id={group_id}, error={e}') + + ttl = self._GROUP_NAME_CACHE_TTL if group_name else self._EMPTY_GROUP_NAME_CACHE_TTL + self._group_name_cache[cache_key] = (now + ttl, group_name) + return group_name + async def _on_raw_event(self, event: Event, bot: Any) -> None: """on_raw_event 回调 — 将事件转为 OneBot 格式推送到 WS 客户端 @@ -391,7 +441,13 @@ async def _on_raw_event(self, event: Event, bot: Any) -> None: if event.is_lifecycle: ob_event = await convert_lifecycle_event(event, self.id_mapper, self_qq) else: - ob_event = await convert_message_event(event, self.id_mapper, self_qq) + group_name = await self._get_cached_group_name(event, bot) + ob_event = await convert_message_event( + event, + self.id_mapper, + self_qq, + group_name=group_name, + ) if ob_event: if has_ws: diff --git a/onebot_adapter/lib/event_converter.py b/onebot_adapter/lib/event_converter.py index c09bdd7..6963c63 100644 --- a/onebot_adapter/lib/event_converter.py +++ b/onebot_adapter/lib/event_converter.py @@ -85,7 +85,13 @@ def _segments_to_raw(segments: list[dict]) -> str: return ''.join(parts) -async def convert_message_event(event, id_mapper, self_qq: int) -> dict | None: +async def convert_message_event( + event, + id_mapper, + self_qq: int, + *, + group_name: str = '', +) -> dict | None: """将 Elaina Event 转换为 OneBot 11 message 事件""" et = event.event_type if et not in ( @@ -157,6 +163,7 @@ async def convert_message_event(event, id_mapper, self_qq: int) -> dict | None: if is_group: ob_event['group_id'] = qq_group + ob_event['group_name'] = str(group_name or '') ob_event['sender']['card'] = '' ob_event['sender']['role'] = event.member_role ob_event['anonymous'] = None diff --git a/onebot_adapter/payload/message_sender_service.py b/onebot_adapter/payload/message_sender_service.py index bd23807..b15f7f6 100644 --- a/onebot_adapter/payload/message_sender_service.py +++ b/onebot_adapter/payload/message_sender_service.py @@ -17,7 +17,6 @@ from core.message._http import MessageType from core.message.sender import MessageSender -from modules.onebot_adapter.payload.segment_parser import ParsedMessage from modules.onebot_adapter.payload.payload_converter import PayloadConverter from modules.onebot_adapter.payload.segment_parser import ParsedMessage @@ -140,6 +139,7 @@ async def _send_media( media_data, media_type, ctn, + file_name=parsed.file_name, target_group_id=group_id, target_user_id=user_id, **kwargs,