mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2025-12-14 13:42:56 +08:00
Some checks failed
检查bot是否运行正常 / bot check (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Has been cancelled
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Has been cancelled
Sequential Lint and Type Check / ruff-call (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Force Sync to Aliyun / sync (push) Has been cancelled
Update Version / update-version (push) Has been cancelled
Sequential Lint and Type Check / pyright-call (push) Has been cancelled
* ♻️ refactor: 统一图片渲染架构并引入通用UI组件系统 🎨 **渲染服务重构** - 统一图片渲染入口,引入主题系统支持 - 优化Jinja2环境管理,支持主题覆盖和插件命名空间 - 新增UI缓存机制和主题重载功能 ✨ **通用UI组件系统** - 新增 zhenxun.ui 模块,提供数据模型和构建器 - 引入BaseBuilder基类,支持链式调用 - 新增多种UI构建器:InfoCard, Markdown, Table, Chart, Layout等 - 新增通用组件:Divider, Badge, ProgressBar, UserInfoBlock 🔄 **插件迁移** - 迁移9个内置插件至新渲染系统 - 移除各插件中分散的图片生成工具 - 优化数据处理和渲染逻辑 💥 **Breaking Changes** - 移除旧的图片渲染接口和模板路径 - TEMPLATE_PATH 更名为 THEMES_PATH - 插件需适配新的RendererService和zhenxun.ui模块 * ✅ test(check): 更新自检插件测试中的渲染服务模拟 * ♻️ refactor(renderer): 将缓存文件名哈希算法切换到 SHA256 * ♻️ refactor(shop): 移除商店HTML图片生成模块 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
290 lines
7.7 KiB
Python
290 lines
7.7 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
|
|
import pytz
|
|
|
|
from zhenxun.configs.config import BotConfig, Config
|
|
from zhenxun.models.sign_log import SignLog
|
|
from zhenxun.models.sign_user import SignUser
|
|
from zhenxun.services import renderer_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 = [
|
|
"今天要早点休息哦~",
|
|
"可不要熬夜到太晚呀",
|
|
"请尽早休息吧!",
|
|
"不要熬夜啦!",
|
|
]
|
|
|
|
|
|
@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():
|
|
return card_file
|
|
|
|
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():
|
|
return view_card_file
|
|
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():
|
|
return card_file
|
|
|
|
impression = float(user.impression)
|
|
user_console = await user.user_console
|
|
uid_str = (
|
|
f"{user_console.uid:08}"
|
|
if user_console and user_console.uid is not None
|
|
else "XXXXXXXX"
|
|
)
|
|
uid_formatted = f"{uid_str[:4]} {uid_str[4:]}"
|
|
|
|
level, next_impression, previous_impression = get_level_and_next_impression(
|
|
impression
|
|
)
|
|
|
|
attitude = 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:
|
|
bot_message = random.choice(MORNING_MESSAGE)
|
|
elif 0 <= hour < 6:
|
|
bot_message = random.choice(LG_MESSAGE)
|
|
else:
|
|
bot_message = f"{BotConfig.self_nickname}希望你开心!"
|
|
|
|
temperature = random.randint(1, 40)
|
|
weather_icon_name = f"{random.randint(0, 11)}.png"
|
|
tag_icon_name = f"{random.randint(0, 5)}.png"
|
|
|
|
user_info = {
|
|
"nickname": nickname,
|
|
"uid_str": uid_formatted,
|
|
"avatar_url": PlatformUtils.get_user_avatar_url(
|
|
user.user_id, PlatformUtils.get_platform(session), session.self_id
|
|
)
|
|
or "",
|
|
"sign_count": user.sign_count,
|
|
}
|
|
|
|
favorability_info = {
|
|
"current": impression,
|
|
"level": level,
|
|
"next_level_at": next_impression,
|
|
"previous_level_at": previous_impression,
|
|
}
|
|
|
|
reward_info = None
|
|
rank = None
|
|
total_gold = None
|
|
last_sign_date_str = 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
|
|
|
|
last_log = (
|
|
await SignLog.filter(user_id=user.user_id).order_by("-create_time").first()
|
|
)
|
|
last_date = "从未"
|
|
if last_log:
|
|
last_date = str(
|
|
last_log.create_time.astimezone(pytz.timezone("Asia/Shanghai")).date()
|
|
)
|
|
last_sign_date_str = f"上次签到:{last_date}"
|
|
|
|
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,
|
|
"last_sign_date_str": last_sign_date_str,
|
|
}
|
|
|
|
image_bytes = await renderer_service.render("pages/builtin/sign", data=card_data)
|
|
|
|
async with aiofiles.open(card_file, "wb") as f:
|
|
await f.write(image_bytes)
|
|
|
|
return card_file
|