mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-28 16:20:56 +08:00
* ✨ feat(hook): 增强认证钩子和运行时缓存管理 ``` ♻ refactor(hook): 移除未使用的配置项并优化缓存设置 移除 AUTH_HOOKS_CONCURRENCY_LIMIT 配置项,该配置项不再使用 ✨feat(auth_ban): 简化缓存配置并添加实体参数支持 将 BAN_CACHE_TTL 相关配置从动态配置改为常量定义, 移除复杂的 TTL 值转换逻辑,并为 auth_ban 函数添加可选的 entity 参数以支持外部传入实体信息 ♻ refactor(auth_limit): 移除未使用的配置依赖 移除 AUTH_LIMIT_NOTICE_CD 配置项,直接使用常量值 2 作为限制通知冷却时间 📦 依赖更新: update playwright dependency to version 1.57.0 in pyproject.toml and requirements.txt ``` * ✨ feat(auth_checker): 增强插件模块处理和预过滤逻辑,支持用户插件兼容性 * ✨ feat(http_utils): 添加内容缓存机制以优化HTTP响应处理 * ✨ feat(cache): 添加群组插件设置视图缓存类型并更新相关逻辑 * ✨ feat(renderer): 优化渲染引擎,增加内存缓存管理和HTML文档处理逻辑 * ✨ feat(sign_in): 添加好感度排行和好感度总排行命令 * ✨ feat(renderer): 增强浏览器实例管理和模板预处理,支持历史 include 语法兼容 * ✨ feat(renderer): 优化 Playwright 环境检查逻辑,增加结果缓存以提高性能 * ✨ feat(renderer): 增强模板文件渲染策略,优化资产加载路径处理 * 🚨 auto fix by pre-commit hooks * ✨ feat(log): 增加日志内容安全序列化,避免超长 base64 等污染日志 ✨ feat(log_sanitizer): 添加对嵌入超长 base64/data URI 的清理功能 ✨ feat(auth_checker): 添加 Alconna 快捷方式缓存检查,优化路由匹配逻辑 * feat(renderer): 添加渲染结果内存缓存功能以优化性能 feat(help): 实现帮助菜单图像缓存机制 feat(sign_in): 更新HTML卡片生成以支持动画禁用和剪裁 feat(superuser): 在启动时预热超级用户帮助缓存 feat(theme): 优化主题管理器的资源解析缓存 * 🚨 auto fix by pre-commit hooks * feat(renderer): 增加全页面视口最大宽度限制并优化内容尺寸计算 --------- Co-authored-by: ATTomatoo <1126160939@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: HibiKier <45528451+HibiKier@users.noreply.github.com>
339 lines
9.3 KiB
Python
339 lines
9.3 KiB
Python
from datetime import datetime
|
|
import os
|
|
from pathlib import Path
|
|
import random
|
|
|
|
import aiofiles
|
|
import nonebot
|
|
from nonebot.drivers import Driver
|
|
from nonebot_plugin_uninfo import Uninfo
|
|
|
|
from zhenxun import ui
|
|
from zhenxun.configs.config import BotConfig, Config
|
|
from zhenxun.configs.path_config import THEMES_PATH
|
|
from zhenxun.models.sign_user import SignUser
|
|
from zhenxun.services import avatar_service
|
|
from zhenxun.utils.manager.priority_manager import PriorityLifecycle
|
|
from zhenxun.utils.platform import PlatformUtils
|
|
|
|
from .config import (
|
|
SIGN_TODAY_CARD_PATH,
|
|
level2attitude,
|
|
lik2level,
|
|
lik2relation,
|
|
)
|
|
|
|
assert (
|
|
len(level2attitude) == len(lik2level) == len(lik2relation)
|
|
), "好感度态度、等级、关系长度不匹配!"
|
|
|
|
AVA_URL = "http://q1.qlogo.cn/g?b=qq&nk={}&s=160"
|
|
|
|
driver: Driver = nonebot.get_driver()
|
|
|
|
base_config = Config.get("sign_in")
|
|
|
|
|
|
MORNING_MESSAGE = [
|
|
"早上好,希望今天是美好的一天!",
|
|
"醒了吗,今天也要元气满满哦!",
|
|
"早上好呀,今天也要开心哦!",
|
|
"早安,愿你拥有美好的一天!",
|
|
]
|
|
|
|
LG_MESSAGE = [
|
|
"今天要早点休息哦~",
|
|
"可不要熬夜到太晚呀",
|
|
"请尽早休息吧!",
|
|
"不要熬夜啦!",
|
|
]
|
|
|
|
|
|
def _get_sign_template_files() -> list[Path]:
|
|
theme_name = str(Config.get_config("UI", "THEME", "default") or "default")
|
|
files: list[Path] = []
|
|
theme_candidates = [theme_name]
|
|
if theme_name != "default":
|
|
theme_candidates.append("default")
|
|
|
|
for candidate in theme_candidates:
|
|
base = THEMES_PATH / candidate / "pages" / "builtin" / "sign"
|
|
for file_name in ("main.html", "style.css", "manifest.json"):
|
|
file_path = base / file_name
|
|
if file_path.exists():
|
|
files.append(file_path)
|
|
return files
|
|
|
|
|
|
def _is_sign_card_cache_stale(card_file: Path) -> bool:
|
|
if not card_file.exists():
|
|
return False
|
|
try:
|
|
card_mtime = card_file.stat().st_mtime
|
|
except OSError:
|
|
return True
|
|
|
|
for template_file in _get_sign_template_files():
|
|
try:
|
|
if template_file.stat().st_mtime > card_mtime:
|
|
return True
|
|
except OSError:
|
|
continue
|
|
return False
|
|
|
|
|
|
@PriorityLifecycle.on_startup(priority=5)
|
|
async def init_image():
|
|
SIGN_TODAY_CARD_PATH.mkdir(exist_ok=True, parents=True)
|
|
clear_sign_data_pic()
|
|
|
|
|
|
async def get_card(
|
|
user: SignUser,
|
|
session: Uninfo,
|
|
nickname: str,
|
|
add_impression: float,
|
|
gold: int | None,
|
|
gift: str,
|
|
is_double: bool = False,
|
|
is_card_view: bool = False,
|
|
) -> Path:
|
|
"""获取好感度卡片
|
|
|
|
参数:
|
|
user: SignUser
|
|
session: Uninfo
|
|
nickname: 用户昵称
|
|
impression: 新增的好感度
|
|
gold: 金币
|
|
gift: 礼物
|
|
is_double: 是否触发双倍.
|
|
is_card_view: 是否展示好感度卡片.
|
|
|
|
返回:
|
|
Path: 卡片路径
|
|
"""
|
|
user_id = user.user_id
|
|
date = datetime.now().date()
|
|
_type = "view" if is_card_view else "sign"
|
|
file_name = f"{user_id}_{_type}_{date}.png"
|
|
card_file = SIGN_TODAY_CARD_PATH / file_name
|
|
|
|
if card_file.exists():
|
|
if not _is_sign_card_cache_stale(card_file):
|
|
return card_file
|
|
card_file.unlink(missing_ok=True)
|
|
|
|
if add_impression == -1:
|
|
view_name = f"{user_id}_view_{date}.png"
|
|
view_card_file = SIGN_TODAY_CARD_PATH / view_name
|
|
if view_card_file.exists():
|
|
if not _is_sign_card_cache_stale(view_card_file):
|
|
return view_card_file
|
|
view_card_file.unlink(missing_ok=True)
|
|
is_card_view = True
|
|
|
|
return await _generate_html_card(
|
|
user, session, nickname, add_impression, gold, gift, is_double, is_card_view
|
|
)
|
|
|
|
|
|
def get_level_and_next_impression(impression: float) -> tuple[int, int | float, int]:
|
|
"""获取当前好感等级与下一等级的差距
|
|
|
|
参数:
|
|
impression: 好感度
|
|
|
|
返回:
|
|
tuple[int, int, int]: 好感度等级,下一等级好感度要求,已达到的好感度要求
|
|
"""
|
|
|
|
keys = list(lik2level.keys())
|
|
level_int, next_impression, previous_impression = (
|
|
int(lik2level[keys[-1]]),
|
|
keys[-2],
|
|
keys[-1],
|
|
)
|
|
for i in range(len(keys)):
|
|
if impression >= keys[i]:
|
|
level_int, next_impression, previous_impression = (
|
|
int(lik2level[keys[i]]),
|
|
keys[i - 1],
|
|
keys[i],
|
|
)
|
|
if i == 0:
|
|
next_impression = impression
|
|
break
|
|
return level_int, next_impression, previous_impression
|
|
|
|
|
|
def clear_sign_data_pic():
|
|
"""
|
|
清空当前签到图片数据
|
|
"""
|
|
date = datetime.now().date()
|
|
for file in os.listdir(SIGN_TODAY_CARD_PATH):
|
|
if str(date) not in file:
|
|
os.remove(SIGN_TODAY_CARD_PATH / file)
|
|
|
|
|
|
async def _generate_html_card(
|
|
user: SignUser,
|
|
session: Uninfo,
|
|
nickname: str,
|
|
add_impression: float,
|
|
gold: int | None,
|
|
gift: str,
|
|
is_double: bool = False,
|
|
is_card_view: bool = False,
|
|
) -> Path:
|
|
"""使用渲染服务生成签到卡片
|
|
|
|
参数:
|
|
user: SignUser
|
|
session: Uninfo
|
|
nickname: 用户昵称
|
|
add_impression: 新增的好感度
|
|
gold: 金币
|
|
gift: 礼物
|
|
is_double: 是否触发双倍.
|
|
is_card_view: 是否为卡片视图.
|
|
|
|
返回:
|
|
Path: 卡片路径
|
|
"""
|
|
now = datetime.now()
|
|
date = now.date()
|
|
_type = "view" if is_card_view else "sign"
|
|
file_name = f"{user.user_id}_{_type}_{date}.png"
|
|
card_file = SIGN_TODAY_CARD_PATH / file_name
|
|
|
|
if card_file.exists():
|
|
if not _is_sign_card_cache_stale(card_file):
|
|
return card_file
|
|
card_file.unlink(missing_ok=True)
|
|
|
|
impression = float(user.impression)
|
|
user_console = await user.user_console
|
|
if user_console and user_console.uid is not None:
|
|
uid = f"{user_console.uid}".rjust(12, "0")
|
|
uid_formatted = f"{uid[:4]} {uid[4:8]} {uid[8:]}"
|
|
else:
|
|
uid_formatted = "XXXX XXXX XXXX"
|
|
|
|
level, next_impression, previous_impression = get_level_and_next_impression(
|
|
impression
|
|
)
|
|
|
|
attitude = f"对你的态度: {level2attitude.get(str(level), '未知')}"
|
|
interpolation_val = max(0, next_impression - impression)
|
|
interpolation = f"{interpolation_val:.2f}"
|
|
|
|
denominator = next_impression - previous_impression
|
|
progress = (
|
|
100.0
|
|
if denominator == 0
|
|
else min(100.0, ((impression - previous_impression) / denominator) * 100)
|
|
)
|
|
|
|
hour = now.hour
|
|
if 6 < hour < 10:
|
|
message = random.choice(MORNING_MESSAGE)
|
|
elif 0 <= hour < 6:
|
|
message = random.choice(LG_MESSAGE)
|
|
else:
|
|
message = f"{BotConfig.self_nickname}希望你开心!"
|
|
bot_message = f"{BotConfig.self_nickname}说: {message}"
|
|
|
|
temperature = random.randint(1, 40)
|
|
weather_icon_name = f"{random.randint(0, 11)}.png"
|
|
tag_icon_name = f"{random.randint(0, 5)}.png"
|
|
|
|
font_size = 45
|
|
if len(nickname) > 6:
|
|
font_size = 27
|
|
|
|
avatar_path = await avatar_service.get_avatar_path(
|
|
PlatformUtils.get_platform(session), user.user_id
|
|
)
|
|
user_info = {
|
|
"nickname": nickname,
|
|
"uid_str": uid_formatted,
|
|
"avatar_url": avatar_path.as_uri() if avatar_path else "",
|
|
"sign_count": user.sign_count,
|
|
"font_size": font_size,
|
|
}
|
|
|
|
favorability_info = {
|
|
"current": impression,
|
|
"level": level,
|
|
"level_text": f"{level} [{lik2relation.get(str(level), '未知')}]",
|
|
"heart2": [1 for _ in range(level)],
|
|
"heart1": [1 for _ in range(len(lik2level) - level - 1)],
|
|
"next_level_at": next_impression,
|
|
"previous_level_at": previous_impression,
|
|
}
|
|
|
|
reward_info = None
|
|
rank = None
|
|
total_gold = None
|
|
|
|
if is_card_view:
|
|
value_list = (
|
|
await SignUser.annotate()
|
|
.order_by("-impression")
|
|
.values_list("user_id", flat=True)
|
|
)
|
|
rank = value_list.index(user.user_id) + 1 if user.user_id in value_list else 0
|
|
total_gold = user_console.gold if user_console else 0
|
|
|
|
reward_info = {
|
|
"impression_added": 0,
|
|
"gold_added": 0,
|
|
"gift_received": "",
|
|
"is_double": False,
|
|
}
|
|
|
|
else:
|
|
reward_info = {
|
|
"impression_added": add_impression,
|
|
"gold_added": gold or 0,
|
|
"gift_received": gift,
|
|
"is_double": is_double,
|
|
}
|
|
|
|
page_info = {
|
|
"date_str": str(now.replace(microsecond=0)),
|
|
"weather_icon_name": weather_icon_name,
|
|
"temperature": temperature,
|
|
"tag_icon_name": tag_icon_name,
|
|
}
|
|
|
|
card_data = {
|
|
"is_card_view": is_card_view,
|
|
"user": user_info,
|
|
"favorability": favorability_info,
|
|
"reward": reward_info,
|
|
"page": page_info,
|
|
"bot_message": bot_message,
|
|
"attitude": attitude,
|
|
"interpolation": interpolation,
|
|
"progress": progress,
|
|
"rank": rank,
|
|
"total_gold": total_gold,
|
|
}
|
|
|
|
image_bytes = await ui.render_template(
|
|
"pages/builtin/sign",
|
|
data=card_data,
|
|
clip_selector=".wrapper",
|
|
clip_padding=8,
|
|
disable_animations=True,
|
|
screenshot_scale="css",
|
|
)
|
|
|
|
async with aiofiles.open(card_file, "wb") as f:
|
|
await f.write(image_bytes)
|
|
|
|
return card_file
|