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>
This commit is contained in:
2026-09-03 00:44:38 +08:00
co-authored by Claude
parent 25bc30611f
commit 9371a28e35
2389 changed files with 5100 additions and 10127 deletions
@@ -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