mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-29 13:40:00 +08:00
fixed # 统一头像获取接口
This commit is contained in:
@@ -33,6 +33,11 @@ Windows双击主仓库根目录的`build_astrbot.cmd`,成功后控制台打印
|
||||
真寻账号。缺少稳定消息ID时拒绝写操作。字体不可用时尽可能退回文字。
|
||||
本地模拟和ZIP校验不等于真实平台发图成功,需要部署者实际验证。
|
||||
|
||||
QQ/OneBot(aiocqhttp)图片查询会读取发送者的腾讯头像,用于我的农场、
|
||||
农场详述和地块编号。头像请求最多等待2秒、限制2MiB,成功缓存5分钟,
|
||||
失败缓存30秒,最多缓存16项;失败时仍使用默认头像。不记录头像URL或
|
||||
QQ号。其他平台暂用默认头像,不将其用户ID当作QQ号查询。
|
||||
|
||||
官方接口参考:
|
||||
[插件入口](https://docs.astrbot.app/en/dev/star/guides/simple.html)、
|
||||
[插件配置](https://docs.astrbot.app/en/dev/star/guides/plugin-config.html)。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""QQ/OneBot头像读取;固定来源、有界缓存和可关闭的异步请求。"""
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
import re
|
||||
from time import monotonic
|
||||
|
||||
import aiohttp
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_BYTES = 2 * 1024 * 1024
|
||||
MAX_ENTRIES = 16
|
||||
FETCH_TIMEOUT = 2
|
||||
|
||||
|
||||
async def download_avatar(user_id):
|
||||
"""只访问固定腾讯HTTPS端点,不跟随重定向或读取用户提供的URL。"""
|
||||
if not re.fullmatch(r'[0-9]{1,20}', user_id):
|
||||
return None
|
||||
url = f'https://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100'
|
||||
timeout = aiohttp.ClientTimeout(total=FETCH_TIMEOUT)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(url, allow_redirects=False) as response:
|
||||
response.raise_for_status()
|
||||
if response.status != 200:
|
||||
return None
|
||||
if (response.content_length is not None
|
||||
and response.content_length > MAX_BYTES):
|
||||
return None
|
||||
content = bytearray()
|
||||
async for chunk in response.content.iter_chunked(65536):
|
||||
if len(content) + len(chunk) > MAX_BYTES:
|
||||
return None
|
||||
content.extend(chunk)
|
||||
return bytes(content) or None
|
||||
|
||||
|
||||
class AvatarCache:
|
||||
"""单事件循环使用;缓存最多16项,同一QQ的并发请求共享下载。"""
|
||||
|
||||
def __init__(self):
|
||||
self._cache = OrderedDict()
|
||||
self._pending = {}
|
||||
self._closed = False
|
||||
|
||||
async def get(self, platform, user_id):
|
||||
user_id = str(user_id)
|
||||
if (self._closed or platform != 'aiocqhttp'
|
||||
or not re.fullmatch(r'[0-9]{1,20}', user_id)):
|
||||
return None
|
||||
cached = self._cache.get(user_id)
|
||||
if cached and cached[0] > monotonic():
|
||||
self._cache.move_to_end(user_id)
|
||||
return cached[1]
|
||||
task = self._pending.get(user_id)
|
||||
if task is None:
|
||||
if len(self._pending) >= MAX_ENTRIES:
|
||||
return None
|
||||
task = asyncio.create_task(self._fetch(user_id))
|
||||
self._pending[user_id] = task
|
||||
return await asyncio.shield(task)
|
||||
|
||||
async def _fetch(self, user_id):
|
||||
try:
|
||||
content = await asyncio.wait_for(
|
||||
download_avatar(user_id), timeout=FETCH_TIMEOUT,
|
||||
)
|
||||
if (not isinstance(content, bytes) or not content
|
||||
or len(content) > MAX_BYTES):
|
||||
content = None
|
||||
except Exception:
|
||||
# 不记录QQ号、完整URL和异常中的潜在身份信息。
|
||||
logger.warning('farm astrbot avatar unavailable; using default')
|
||||
content = None
|
||||
finally:
|
||||
self._pending.pop(user_id, None)
|
||||
self._cache[user_id] = (
|
||||
monotonic() + (300 if content else 30), content,
|
||||
)
|
||||
self._cache.move_to_end(user_id)
|
||||
while len(self._cache) > MAX_ENTRIES:
|
||||
self._cache.popitem(last=False)
|
||||
return content
|
||||
|
||||
async def close(self):
|
||||
self._closed = True
|
||||
tasks = list(self._pending.values())
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
self._cache.clear()
|
||||
@@ -10,6 +10,7 @@ from astrbot.api.event import AstrMessageEvent, filter
|
||||
from astrbot.api.message_components import At, Image
|
||||
from astrbot.api.star import Context, Star, StarTools
|
||||
|
||||
from .avatars import AvatarCache
|
||||
from .runtime import COMMANDS, Runtime
|
||||
|
||||
|
||||
@@ -22,6 +23,7 @@ class FarmPlugin(Star):
|
||||
self.config = config
|
||||
self._lifecycle_lock = asyncio.Lock()
|
||||
self._closed = False
|
||||
self._avatars = AvatarCache()
|
||||
configured = config.get('data_dir', '')
|
||||
data_dir = (Path(configured).expanduser() if configured else
|
||||
StarTools.get_data_dir('astrbot_plugin_farm'))
|
||||
@@ -66,12 +68,17 @@ class FarmPlugin(Star):
|
||||
yield event.plain_result('无法取得稳定的平台或用户标识,未执行。')
|
||||
return
|
||||
bot_id = json.dumps([str(platform_id), str(event.get_self_id())])
|
||||
avatar = None
|
||||
if action in ('farm', 'details', 'plot_ids') and self.runtime.rendering:
|
||||
avatar = await self._avatars.get(
|
||||
event.get_platform_name(), event.get_sender_id(),
|
||||
)
|
||||
response = await self.runtime.execute(
|
||||
action, arguments, bot_id=bot_id,
|
||||
platform=event.get_platform_name(), user_id=event.get_sender_id(),
|
||||
message_id=getattr(event.message_obj, 'message_id', None),
|
||||
display_name=event.get_sender_name() or '农场玩家',
|
||||
target_user_id=target,
|
||||
target_user_id=target, avatar=avatar,
|
||||
)
|
||||
if isinstance(response, bytes):
|
||||
yield event.chain_result([Image.fromBytes(response)])
|
||||
@@ -83,5 +90,6 @@ class FarmPlugin(Star):
|
||||
if self._closed:
|
||||
return
|
||||
self._closed = True
|
||||
await self._avatars.close()
|
||||
await self.runtime.close()
|
||||
logger.info('farm astrbot plugin terminated')
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
Pillow>=10.4
|
||||
aiohttp>=3.9,<4
|
||||
|
||||
Reference in New Issue
Block a user