From deda21f8faa27ec9bacf6ed44387e66da0e6f2a6 Mon Sep 17 00:00:00 2001 From: sf Date: Thu, 13 Aug 2026 16:01:28 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(onebot):=20=E6=96=B0=E5=A2=9E=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E7=BE=A4=E4=BF=A1=E6=81=AF=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因:发布版 OneBot 适配器只有缓存群列表,缺少标准 get_group_info 动作,调用方无法按群获取实时名称、成员数和容量信息。 新增:注册 get_group_info 动作,支持 QQ 群号映射和 OpenID 直传;调用平台群资料接口并转换为 OneBot 标准响应。 修复:对无效群号、异常计数字段和平台错误做校验与错误码透传,并补充 README 动作说明。 --- onebot_adapter/README.md | 1 + onebot_adapter/action_registry.py | 2 + onebot_adapter/actions/__init__.py | 3 + onebot_adapter/actions/get_group_info.py | 78 ++++++++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 onebot_adapter/actions/get_group_info.py 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 From 4a373ec18e8dacc2f9d9e537133a2d5cddd6e7c0 Mon Sep 17 00:00:00 2001 From: sf Date: Thu, 13 Aug 2026 16:02:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(onebot):=20=E5=9C=A8=E7=BE=A4=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E4=BA=8B=E4=BB=B6=E4=B8=AD=E4=B8=8A=E6=8A=A5=E7=BE=A4?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因:发布版 OneBot 群消息事件只有 group_id,消费端需要额外查询才能展示群名,逐条实时查询又会增加平台请求。 新增:群事件附带 group_name;优先读取已保存群记录,缺失时刷新群资料,并按应用与群维度缓存结果。 更改:成功结果缓存 5 分钟,空结果缓存 1 小时;并发刷新通过锁合并,查询失败不影响事件上报。 --- onebot_adapter/adapter.py | 58 ++++++++++++++++++++++++++- onebot_adapter/lib/event_converter.py | 9 ++++- 2 files changed, 65 insertions(+), 2 deletions(-) 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 From 6e6ed8dd406db0304307e4d9102e9a84b69a3c56 Mon Sep 17 00:00:00 2001 From: sf Date: Thu, 13 Aug 2026 16:04:12 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(onebot):=20=E5=8F=91=E9=80=81=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=97=B6=E4=BF=9D=E7=95=99=E5=8E=9F=E5=A7=8B=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原因:发布版文件消息段虽然已经解析 name 或 file_name,但媒体发送服务没有向上传链路继续传递,附件会丢失调用方指定的名称和扩展名。 修复:将 ParsedMessage.file_name 传入统一媒体发送接口,使普通上传、URL 上传和分片上传沿用原始文件名;同时移除该文件中的重复类型导入。 --- onebot_adapter/payload/message_sender_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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,