refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
@@ -1,12 +1,9 @@
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import to_me
|
||||
"""打卡记鹿 —— 入口(元数据 + 依赖)。"""
|
||||
|
||||
from .data_proc import *
|
||||
from .img_generator import *
|
||||
import io
|
||||
import base64
|
||||
from nonebot import require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
require("nonebot_plugin_orm")
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="打卡记鹿",
|
||||
@@ -16,54 +13,4 @@ __plugin_meta__ = PluginMetadata(
|
||||
type="application",
|
||||
)
|
||||
|
||||
record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me())
|
||||
query = on_command("查询记录", aliases={"查卡"})
|
||||
|
||||
|
||||
@record.handle()
|
||||
async def do_job(bot: Bot, ev: MessageEvent):
|
||||
# 获取当前日期的年份和月份
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
# 获取发信人信息
|
||||
uid = str(ev.user_id)
|
||||
message_id = ev.message_id
|
||||
# 添加一条记录
|
||||
await record_month(uid)
|
||||
# 获取记录
|
||||
img = await draw_calendar_grid_image(uid, year, month)
|
||||
text_msg = MessageSegment.text("这是您的打卡记鹿:\n")
|
||||
image_msg = MessageSegment.image(pic2b64(img))
|
||||
mix_msg = (text_msg,)
|
||||
mix_msg += image_msg
|
||||
reply_msg = (MessageSegment.reply(message_id),)
|
||||
reply_msg += mix_msg
|
||||
await record.finish(reply_msg)
|
||||
|
||||
|
||||
def pic2b64(pic: Image) -> str:
|
||||
buf = io.BytesIO()
|
||||
pic.save(buf, format='PNG')
|
||||
base64_str = base64.b64encode(buf.getvalue()).decode()
|
||||
return 'base64://' + base64_str
|
||||
|
||||
|
||||
@query.handle()
|
||||
async def query_job(bot: Bot, ev: MessageEvent):
|
||||
# 获取当前日期的年份和月份
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
# 获取发信人信息
|
||||
uid = str(ev.user_id)
|
||||
message_id = ev.message_id
|
||||
# 获取记录
|
||||
img_file = await draw_calendar_grid_image(uid, year, month)
|
||||
text_msg = MessageSegment.text("这是您的记鹿:\n")
|
||||
image_msg = MessageSegment.image(pic2b64(img_file))
|
||||
mix_msg = (text_msg,)
|
||||
mix_msg += image_msg
|
||||
reply_msg = (MessageSegment.reply(message_id),)
|
||||
reply_msg += mix_msg
|
||||
await record.finish(reply_msg)
|
||||
from .handlers import query, record # noqa: E402,F401
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""打卡记鹿 —— Trigger 层。"""
|
||||
|
||||
from .checkin import query, record # noqa: F401
|
||||
@@ -0,0 +1,58 @@
|
||||
"""打卡记鹿 —— 触发器(Trigger + View)层。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot import on_command
|
||||
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
|
||||
from nonebot.rule import to_me
|
||||
from PIL import Image
|
||||
|
||||
from ..services.checkin import checkin, review
|
||||
|
||||
record = on_command("打卡记鹿", aliases={"打卡"}, rule=to_me())
|
||||
query = on_command("查询记录", aliases={"查卡"})
|
||||
|
||||
|
||||
def pic2b64(pic: Image) -> str:
|
||||
buf = io.BytesIO()
|
||||
pic.save(buf, format="PNG")
|
||||
base64_str = base64.b64encode(buf.getvalue()).decode()
|
||||
return "base64://" + base64_str
|
||||
|
||||
|
||||
@record.handle()
|
||||
async def do_job(bot: Bot, ev: MessageEvent):
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
uid = str(ev.user_id)
|
||||
message_id = ev.message_id
|
||||
img = await checkin(uid, year, month)
|
||||
text_msg = MessageSegment.text("这是您的打卡记鹿:\n")
|
||||
image_msg = MessageSegment.image(pic2b64(img))
|
||||
mix_msg = (text_msg,)
|
||||
mix_msg += image_msg
|
||||
reply_msg = (MessageSegment.reply(message_id),)
|
||||
reply_msg += mix_msg
|
||||
await record.finish(reply_msg)
|
||||
|
||||
|
||||
@query.handle()
|
||||
async def query_job(bot: Bot, ev: MessageEvent):
|
||||
now = datetime.now()
|
||||
year = now.year
|
||||
month = now.month
|
||||
uid = str(ev.user_id)
|
||||
message_id = ev.message_id
|
||||
img_file = await review(uid, year, month)
|
||||
text_msg = MessageSegment.text("这是您的记鹿:\n")
|
||||
image_msg = MessageSegment.image(pic2b64(img_file))
|
||||
mix_msg = (text_msg,)
|
||||
mix_msg += image_msg
|
||||
reply_msg = (MessageSegment.reply(message_id),)
|
||||
reply_msg += mix_msg
|
||||
await record.finish(reply_msg)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""打卡记鹿 —— 数据模型(Model 层)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_orm")
|
||||
|
||||
from nonebot_plugin_orm import Model
|
||||
from sqlalchemy import BigInteger, Date, Integer
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class CheckInRecord(Model):
|
||||
"""打卡记录,一行一次打卡,同一天可多次"""
|
||||
|
||||
__tablename__ = "deer_pipe_checkin"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
uid: Mapped[int] = mapped_column(BigInteger, index=True)
|
||||
date: Mapped[datetime.date] = mapped_column(Date, index=True)
|
||||
@@ -1,25 +1,19 @@
|
||||
"""打卡记鹿 —— 数据访问(Model/Repository 层)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from nonebot import require
|
||||
|
||||
require("nonebot_plugin_orm")
|
||||
|
||||
from nonebot_plugin_orm import Model, get_session
|
||||
from sqlalchemy import BigInteger, Date, Integer, select
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from nonebot_plugin_orm import get_session
|
||||
from sqlalchemy import select
|
||||
|
||||
from .models import CheckInRecord
|
||||
|
||||
|
||||
class CheckInRecord(Model):
|
||||
"""打卡记录,一行一次打卡,同一天可多次"""
|
||||
|
||||
__tablename__ = "deer_pipe_checkin"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
uid: Mapped[int] = mapped_column(BigInteger, index=True)
|
||||
date: Mapped[datetime.date] = mapped_column(Date, index=True)
|
||||
|
||||
|
||||
# 记录信息
|
||||
async def record_month(uid):
|
||||
now = datetime.now()
|
||||
async with get_session() as session:
|
||||
@@ -27,7 +21,6 @@ async def record_month(uid):
|
||||
await session.commit()
|
||||
|
||||
|
||||
# 读取当月信息(返回当月所有打卡的日,同一天多次打卡会有重复项)
|
||||
async def get_records(uid):
|
||||
now = datetime.now()
|
||||
month_start = datetime(now.year, now.month, 1).date()
|
||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,3 @@
|
||||
"""打卡记鹿 —— Service 层。"""
|
||||
|
||||
from .checkin import checkin, review # noqa: F401
|
||||
@@ -0,0 +1,19 @@
|
||||
"""打卡记鹿 —— 业务编排(Service 层)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from ..repository import record_month
|
||||
from ..utils.render import draw_calendar_grid_image
|
||||
|
||||
|
||||
async def checkin(uid: str, year: int, month: int) -> Image.Image:
|
||||
"""记录一次打卡并返回当月日历图。"""
|
||||
await record_month(uid)
|
||||
return await draw_calendar_grid_image(uid, year, month)
|
||||
|
||||
|
||||
async def review(uid: str, year: int, month: int) -> Image.Image:
|
||||
"""仅查询(不新增记录)并返回当月日历图。"""
|
||||
return await draw_calendar_grid_image(uid, year, month)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""打卡记鹿 —— 渲染工具。"""
|
||||
|
||||
from .render import draw_calendar_grid_image # noqa: F401
|
||||
@@ -1,121 +1,87 @@
|
||||
import calendar
|
||||
from collections import Counter
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
from .data_proc import *
|
||||
import random
|
||||
"""打卡记鹿 —— 渲染工具(utils 层)。"""
|
||||
|
||||
filepath = os.path.dirname(__file__).replace("\\", "/")
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import os
|
||||
import random
|
||||
from collections import Counter
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from nonebot.log import logger
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps
|
||||
|
||||
from ..repository import get_records
|
||||
|
||||
# 资源统一放插件 res/ 下
|
||||
filepath = os.path.join(os.path.dirname(os.path.dirname(__file__)), "res").replace("\\", "/")
|
||||
|
||||
|
||||
async def draw_calendar_grid_image(uid, year, month):
|
||||
# 获取该月的第一天是星期几,以及这个月有多少天
|
||||
first_weekday, num_days = calendar.monthrange(year, month)
|
||||
|
||||
# 调整第一天的索引,使周日对应0,周六对应6
|
||||
first_weekday = (first_weekday + 1) % 7
|
||||
|
||||
# 设置单元格尺寸和内边距
|
||||
cell_size = 50
|
||||
cell_padding = 5
|
||||
|
||||
# 计算当前月份的行数
|
||||
num_rows = (num_days + first_weekday + 6) // 7 + 1 # +1 行用于留空
|
||||
|
||||
# 计算图片宽度和高度
|
||||
num_rows = (num_days + first_weekday + 6) // 7 + 1
|
||||
image_width = 7 * (cell_size + cell_padding) - cell_padding
|
||||
image_height = num_rows * (cell_size + cell_padding) - cell_padding
|
||||
|
||||
# 创建图像对象
|
||||
img = Image.new("RGBA", (image_width, image_height), "white")
|
||||
draw = ImageDraw.Draw(img)
|
||||
font_path = filepath + "/font/SourceHanSansCN-Medium.otf"
|
||||
print(font_path)
|
||||
logger.debug(f"打卡字体路径: {font_path}")
|
||||
font_normal = ImageFont.truetype(font=font_path, size=18)
|
||||
font_large = ImageFont.truetype(font=font_path, size=24)
|
||||
|
||||
# 绘制网格线
|
||||
for i in range(7):
|
||||
x = i * (cell_size + cell_padding)
|
||||
draw.line([(x, 55), (x, image_height)], fill="black", width=1)
|
||||
for j in range(0, image_height + cell_size, cell_size + cell_padding):
|
||||
draw.line([(0, j), (image_width, j)], fill="black", width=1)
|
||||
|
||||
# 小🦌
|
||||
deer_pipe_path = filepath + "/img/deer_pipe.jpg"
|
||||
deer_pipe_img = Image.open(deer_pipe_path)
|
||||
deer_pipe_img = deer_pipe_img.resize((54, 50))
|
||||
|
||||
# 画标题
|
||||
title = f"{month}月打卡记鹿"
|
||||
text_length = draw.textlength(text=title, font=font_large)
|
||||
img_xy = (int(((image_width - text_length + 55) / 2) - 55), 5)
|
||||
title_pos = ((image_width - text_length + 55) / 2, 10)
|
||||
img.paste(deer_pipe_img, img_xy)
|
||||
draw.text(title_pos, title, fill="black", font=font_large)
|
||||
|
||||
# 获取🦌信息(同一天多次打卡会得到重复项)
|
||||
day_counts = Counter(await get_records(uid))
|
||||
# 填充日期
|
||||
date = datetime(year, month, 1)
|
||||
for day in range(1, num_days + 1):
|
||||
weekday = (first_weekday + day - 1) % 7
|
||||
row = (first_weekday + day - 1) // 7 + 1 # +1 行用于留空
|
||||
row = (first_weekday + day - 1) // 7 + 1
|
||||
x = weekday * (cell_size + cell_padding) + cell_padding
|
||||
y = row * (cell_size + cell_padding) + cell_padding
|
||||
img.paste(deer_pipe_img, (x - 4, y))
|
||||
draw.text((x+20, y - 2), f"{str(day)}", fill="black", font=font_normal)
|
||||
# 🦌是天的标记,打卡的PNG印记才是要多次绘制的对象,横向错开
|
||||
draw.text((x + 20, y - 2), f"{str(day)}", fill="black", font=font_normal)
|
||||
for i in range(day_counts.get(day, 0)):
|
||||
im = get_random_right()
|
||||
img = image_paste(im, img, (x - 4 + i * 10, y))
|
||||
|
||||
# 保存图片
|
||||
# img.save(output_image_path)
|
||||
|
||||
# 显示图片
|
||||
# img.show()
|
||||
img = ImageOps.expand(img, border=5, fill="black")
|
||||
img = ImageOps.expand(img, border=5, fill="white")
|
||||
return img
|
||||
|
||||
|
||||
# 全局变量,用于存储已经选择过的文件名
|
||||
selected_images = []
|
||||
|
||||
|
||||
def get_random_right():
|
||||
global selected_images
|
||||
path = filepath + "/img/right"
|
||||
# 过滤掉已经选择过的文件名
|
||||
im_name = [name for name in os.listdir(path) if name not in selected_images]
|
||||
|
||||
# 如果所有文件都已经选择过,重新初始化已选择列表
|
||||
if not im_name:
|
||||
selected_images = []
|
||||
im_name = os.listdir(path)
|
||||
|
||||
index = random.randint(0, len(im_name) - 1)
|
||||
im = im_name[index]
|
||||
selected_images.append(im) # 将选择的文件名添加到已选择列表中
|
||||
|
||||
selected_images.append(im)
|
||||
im_path = os.path.join(path, im)
|
||||
im_file = Image.open(im_path)
|
||||
im_file = im_file.resize((50, 50))
|
||||
return im_file
|
||||
|
||||
|
||||
# 图片粘贴
|
||||
def image_paste(paste_image, under_image, pos):
|
||||
"""
|
||||
:param paste_image: 需要粘贴的图片
|
||||
:param under_image: 底图
|
||||
:param pos: 位置(x,y)坐标
|
||||
:return: 返回图片
|
||||
"""
|
||||
# 获取需要贴入图片的透明通道
|
||||
r, g, b, alpha = paste_image.split()
|
||||
# 粘贴时将alpha值传递至mask属性
|
||||
under_image.paste(paste_image, pos, alpha)
|
||||
return under_image
|
||||