Merge remote-tracking branch 'origin/master' into dev/4.11.x

# Conflicts:
#	pyproject.toml
#	uv.lock
#	web/src/app/home/components/dynamic-form/DynamicFormItemComponent.tsx
This commit is contained in:
Junyan Qin
2026-07-03 20:46:19 +08:00
52 changed files with 4826 additions and 739 deletions
@@ -1,5 +1,7 @@
from __future__ import annotations
import asyncio
import time
import typing
import aiocqhttp
@@ -10,6 +12,103 @@ from langbot_plugin.api.entities.builtin.platform import entities as platform_en
from langbot_plugin.api.entities.builtin.platform import events as platform_events
_GROUP_NAME_CACHE_TTL_SECONDS = 3600
_GROUP_NAME_NEGATIVE_CACHE_TTL_SECONDS = 60
_GROUP_NAME_LOOKUP_TIMEOUT_SECONDS = 2
_GROUP_MEMBER_INFO_CACHE_TTL_SECONDS = 86400
_GROUP_MEMBER_INFO_NEGATIVE_CACHE_TTL_SECONDS = 600
_GROUP_MEMBER_INFO_LOOKUP_TIMEOUT_SECONDS = 2
_group_name_cache: dict[typing.Union[int, str], tuple[str, float]] = {}
_group_name_negative_cache: dict[typing.Union[int, str], float] = {}
_group_member_info_cache: dict[tuple[typing.Union[int, str], typing.Union[int, str]], tuple[dict, float]] = {}
_group_member_info_negative_cache: dict[tuple[typing.Union[int, str], typing.Union[int, str]], float] = {}
def _get_field(data: dict, key: str, default: str = '') -> str:
value = data.get(key)
if value is None:
return default
return str(value)
def _get_group_member_name(sender: dict) -> str:
return _get_field(sender, 'card') or _get_field(sender, 'nickname') or _get_field(sender, 'user_id')
def _get_group_name_placeholder(group_id: typing.Union[int, str]) -> str:
return f'Group {group_id}'
async def _get_group_name(group_id: typing.Union[int, str], bot: aiocqhttp.CQHttp | None = None) -> str:
now = time.monotonic()
if group_id in _group_name_cache:
group_name, expires_at = _group_name_cache[group_id]
if expires_at > now:
return group_name
del _group_name_cache[group_id]
if group_id in _group_name_negative_cache:
expires_at = _group_name_negative_cache[group_id]
if expires_at > now:
return ''
del _group_name_negative_cache[group_id]
if bot is None:
return ''
try:
group_info = await asyncio.wait_for(
bot.get_group_info(group_id=group_id),
timeout=_GROUP_NAME_LOOKUP_TIMEOUT_SECONDS,
)
except Exception:
_group_name_negative_cache[group_id] = now + _GROUP_NAME_NEGATIVE_CACHE_TTL_SECONDS
return ''
group_name = _get_field(group_info, 'group_name') if isinstance(group_info, dict) else ''
if group_name:
_group_name_cache[group_id] = (group_name, now + _GROUP_NAME_CACHE_TTL_SECONDS)
_group_name_negative_cache.pop(group_id, None)
else:
_group_name_negative_cache[group_id] = now + _GROUP_NAME_NEGATIVE_CACHE_TTL_SECONDS
return group_name
async def _get_group_member_info(
group_id: typing.Union[int, str],
user_id: typing.Union[int, str],
bot: aiocqhttp.CQHttp | None = None,
) -> dict:
now = time.monotonic()
cache_key = (group_id, user_id)
if cache_key in _group_member_info_cache:
member_info, expires_at = _group_member_info_cache[cache_key]
if expires_at > now:
return member_info
del _group_member_info_cache[cache_key]
if cache_key in _group_member_info_negative_cache:
expires_at = _group_member_info_negative_cache[cache_key]
if expires_at > now:
return {}
del _group_member_info_negative_cache[cache_key]
if bot is None:
return {}
try:
member_info = await asyncio.wait_for(
bot.get_group_member_info(group_id=group_id, user_id=user_id),
timeout=_GROUP_MEMBER_INFO_LOOKUP_TIMEOUT_SECONDS,
)
except Exception:
_group_member_info_negative_cache[cache_key] = now + _GROUP_MEMBER_INFO_NEGATIVE_CACHE_TTL_SECONDS
return {}
if isinstance(member_info, dict) and member_info:
_group_member_info_cache[cache_key] = (
member_info,
now + _GROUP_MEMBER_INFO_CACHE_TTL_SECONDS,
)
_group_member_info_negative_cache.pop(cache_key, None)
return member_info
_group_member_info_negative_cache[cache_key] = now + _GROUP_MEMBER_INFO_NEGATIVE_CACHE_TTL_SECONDS
return {}
class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
@staticmethod
async def yiri2target(event: platform_events.Event, bot_account_id: int | str | None = None):
@@ -25,9 +124,9 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
if event_type == 'message':
return await AiocqhttpEventConverter.message_to_eba(event, bot)
if event_type == 'notice':
return AiocqhttpEventConverter.notice_to_eba(event, bot_user_id)
return await AiocqhttpEventConverter.notice_to_eba(event, bot, bot_user_id)
if event_type == 'request':
return AiocqhttpEventConverter.request_to_eba(event)
return await AiocqhttpEventConverter.request_to_eba(event, bot)
if event_type == 'meta_event':
return AiocqhttpEventConverter.platform_specific(event, f'meta.{getattr(event, "detail_type", "")}')
return None
@@ -60,14 +159,14 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
if message_type == 'group':
chat_type = platform_entities.ChatType.GROUP
chat_id = getattr(event, 'group_id', '')
group = AiocqhttpEventConverter.group_from_event(event)
group = await AiocqhttpEventConverter.group_from_event(event, bot)
return platform_events.MessageReceivedEvent(
type='message.received',
adapter_name='aiocqhttp',
message_id=getattr(event, 'message_id', ''),
message_chain=message_chain,
sender=AiocqhttpEventConverter.user_from_sender(event),
sender=await AiocqhttpEventConverter.user_from_sender(event, bot),
chat_type=chat_type,
chat_id=chat_id,
group=group,
@@ -76,8 +175,9 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
)
@staticmethod
def notice_to_eba(
async def notice_to_eba(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
bot_user_id: int | str | None = None,
) -> platform_events.EBAEvent:
notice_type = getattr(event, 'notice_type', getattr(event, 'detail_type', ''))
@@ -91,12 +191,14 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
if notice_type == 'group_recall'
else platform_entities.ChatType.PRIVATE,
chat_id=getattr(event, 'group_id', getattr(event, 'user_id', '')),
group=AiocqhttpEventConverter.group_from_event(event) if notice_type == 'group_recall' else None,
group=await AiocqhttpEventConverter.group_from_event(event, bot)
if notice_type == 'group_recall'
else None,
timestamp=float(getattr(event, 'time', 0) or 0),
source_platform_object=event,
)
if notice_type == 'group_increase':
group = AiocqhttpEventConverter.group_from_event(event)
group = await AiocqhttpEventConverter.group_from_event(event, bot)
user = AiocqhttpEventConverter.user(getattr(event, 'user_id', ''))
inviter_id = getattr(event, 'operator_id', None)
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
@@ -119,7 +221,7 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
source_platform_object=event,
)
if notice_type == 'group_decrease':
group = AiocqhttpEventConverter.group_from_event(event)
group = await AiocqhttpEventConverter.group_from_event(event, bot)
operator = AiocqhttpEventConverter.user(getattr(event, 'operator_id', None))
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
return platform_events.BotRemovedFromGroupEvent(
@@ -141,7 +243,7 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
source_platform_object=event,
)
if notice_type == 'group_ban':
group = AiocqhttpEventConverter.group_from_event(event)
group = await AiocqhttpEventConverter.group_from_event(event, bot)
duration = int(getattr(event, 'duration', 0) or 0)
operator = AiocqhttpEventConverter.user(getattr(event, 'operator_id', None))
if AiocqhttpEventConverter._is_bot_user(getattr(event, 'user_id', None), bot_user_id, event):
@@ -179,7 +281,10 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
return AiocqhttpEventConverter.platform_specific(event, f'notice.{notice_type}')
@staticmethod
def request_to_eba(event: aiocqhttp.Event) -> platform_events.EBAEvent:
async def request_to_eba(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_events.EBAEvent:
request_type = getattr(event, 'request_type', getattr(event, 'detail_type', ''))
if request_type == 'friend':
return platform_events.FriendRequestReceivedEvent(
@@ -195,7 +300,7 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
return platform_events.BotInvitedToGroupEvent(
type='bot.invited_to_group',
adapter_name='aiocqhttp',
group=AiocqhttpEventConverter.group_from_event(event),
group=await AiocqhttpEventConverter.group_from_event(event, bot),
inviter=AiocqhttpEventConverter.user(getattr(event, 'user_id', '')),
request_id=getattr(event, 'flag', ''),
timestamp=float(getattr(event, 'time', 0) or 0),
@@ -204,13 +309,28 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
return AiocqhttpEventConverter.platform_specific(event, f'request.{request_type}')
@staticmethod
def user_from_sender(event: aiocqhttp.Event) -> platform_entities.User:
async def user_from_sender(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_entities.User:
sender = getattr(event, 'sender', {}) or {}
nickname = sender.get('card') or sender.get('nickname') or ''
user_id = sender.get('user_id', getattr(event, 'user_id', ''))
has_sender_display_name = bool(_get_field(sender, 'card') or _get_field(sender, 'nickname'))
nickname = _get_group_member_name(sender)
remark = sender.get('remark')
if (
getattr(event, 'message_type', getattr(event, 'detail_type', 'private')) == 'group'
and user_id
and (not has_sender_display_name or not remark)
):
member_info = await _get_group_member_info(getattr(event, 'group_id', ''), user_id, bot)
remark = _get_field(member_info, 'card') or _get_field(member_info, 'remark') or None
if not has_sender_display_name:
nickname = _get_group_member_name(member_info) or nickname
return platform_entities.User(
id=sender.get('user_id', getattr(event, 'user_id', '')),
id=user_id,
nickname=nickname,
remark=sender.get('remark'),
remark=remark,
)
@staticmethod
@@ -220,10 +340,19 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
return platform_entities.User(id=user_id, nickname=nickname)
@staticmethod
def group_from_event(event: aiocqhttp.Event) -> platform_entities.UserGroup:
async def group_from_event(
event: aiocqhttp.Event,
bot: aiocqhttp.CQHttp | None = None,
) -> platform_entities.UserGroup:
group_id = getattr(event, 'group_id', '')
group_name = getattr(event, 'group_name', '') or ''
if group_id and not group_name:
group_name = await _get_group_name(group_id, bot)
if group_id and not group_name:
group_name = _get_group_name_placeholder(group_id)
return platform_entities.UserGroup(
id=getattr(event, 'group_id', ''),
name=getattr(event, 'group_name', '') or '',
id=group_id,
name=group_name,
member_count=getattr(event, 'member_count', None),
)