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,17 +1,6 @@
import base64
import datetime
import hashlib
import json
import os
from random import choice
from tkinter import Image
"""今日老婆 —— 入口(元数据 + 依赖)。"""
from PIL import *
from .utils import *
import httpx
from nonebot import on_command, logger
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot import on_command
from nonebot.plugin import PluginMetadata
__plugin_meta__ = PluginMetadata(
@@ -21,106 +10,4 @@ __plugin_meta__ = PluginMetadata(
type="application",
)
wife = on_command("今日老婆", aliases={"今日老婆"})
def get_member_list(all_list):
id_list = []
for member_list in all_list:
id_list.append(member_list['user_id'])
return id_list
async def download_avatar(user_id: str) -> bytes:
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
data = await download_url(url)
if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e":
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100"
data = await download_url(url)
return data
async def download_url(url: str) -> bytes:
async with httpx.AsyncClient() as client:
for i in range(3):
try:
resp = await client.get(url)
if resp.status_code != 200:
continue
return resp.content
except Exception as e:
logger.error(f"Error downloading {url}, retry {i}/3: {str(e)}")
async def get_wife_info(member_info, qq_id):
img = await download_avatar(qq_id)
avatar = Image.open(BytesIO(img)).convert('RGBA')
avatar = png_resize(avatar, new_width=145, new_height=145)
b_io = BytesIO()
avatar.save(b_io, format="PNG")
avatar_str = 'base64://' + base64.b64encode(b_io.getvalue()).decode()
member_name = (member_info["card"] or member_info["nickname"])
msg = (MessageSegment.text('你今天的群友老婆是:'), MessageSegment.image(avatar_str),
MessageSegment.text(f'{member_name}({qq_id})'))
return msg
def load_group_config(group_id: str) -> int:
filename = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json')
try:
with open(filename, encoding='utf8') as f:
config = json.load(f)
return config
except:
return None
def write_group_config(group_id: str, link_id: str, wife_id: str, date: str, config) -> int:
config_file = os.path.join(os.path.dirname(__file__), 'config', f'{group_id}.json')
if config is not None:
config[link_id] = [wife_id, date]
else:
config = {link_id: [wife_id, date]}
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False)
@wife.handle()
async def wife_handle(bot: Bot, ev: MessageEvent):
group_id = ev.group_id
user_id = ev.user_id
bot_id = ev.self_id
wife_id = None
today = str(datetime.date.today())
config = load_group_config(group_id)
# if priv.check_priv(ev, priv.SUPERUSER):
# wife_id = bot_id
if config is not None:
if str(user_id) in list(config):
if config[str(user_id)][1] == today:
wife_id = config[str(user_id)][0]
else:
del config[str(user_id)]
if wife_id is None:
all_list = await bot.get_group_member_list(group_id=group_id)
id_list = get_member_list(all_list)
id_list.remove(bot_id)
id_list.remove(user_id)
if config is not None:
for record_id in list(config):
if config[record_id][1] != today:
del config[record_id]
else:
try:
id_list.remove(int(config[record_id][0]))
except:
del config[record_id]
wife_id = choice(id_list)
write_group_config(group_id, user_id, wife_id, today, config)
member_info = await bot.get_group_member_info(group_id=group_id, user_id=wife_id)
result = await get_wife_info(member_info, wife_id)
await bot.send(ev, result, at_sender=True)
from .handlers import wife # noqa: E402,F401
@@ -0,0 +1,3 @@
"""今日老婆 —— Trigger 层。"""
from .wife import wife # noqa: F401
@@ -0,0 +1,78 @@
"""今日老婆 —— 触发器(Trigger + View)层。"""
from __future__ import annotations
import base64
import datetime
from io import BytesIO
from random import choice
from nonebot import on_command
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot.plugin import PluginMetadata
from PIL import Image
from ..services.store import get_member_list, load_group_config, write_group_config
from ..utils import download_avatar, png_resize
__plugin_meta__ = PluginMetadata(
name="今日老婆",
description="随机抓取群友作为老婆",
usage="发送【今日老婆】",
type="application",
)
wife = on_command("今日老婆", aliases={"今日老婆"})
async def get_wife_info(member_info, qq_id):
img = await download_avatar(qq_id)
avatar = Image.open(BytesIO(img)).convert("RGBA")
avatar = png_resize(avatar, new_width=145, new_height=145)
b_io = BytesIO()
avatar.save(b_io, format="PNG")
avatar_str = "base64://" + base64.b64encode(b_io.getvalue()).decode()
member_name = member_info["card"] or member_info["nickname"]
return (
MessageSegment.text("你今天的群友老婆是:"),
MessageSegment.image(avatar_str),
MessageSegment.text(f"{member_name}({qq_id})"),
)
@wife.handle()
async def wife_handle(bot: Bot, ev: MessageEvent):
group_id = ev.group_id
user_id = ev.user_id
bot_id = ev.self_id
wife_id = None
today = str(datetime.date.today())
config = load_group_config(group_id)
if config is not None:
if str(user_id) in list(config):
if config[str(user_id)][1] == today:
wife_id = config[str(user_id)][0]
else:
del config[str(user_id)]
if wife_id is None:
all_list = await bot.get_group_member_list(group_id=group_id)
id_list = get_member_list(all_list)
id_list.remove(bot_id)
id_list.remove(user_id)
if config is not None:
for record_id in list(config):
if config[record_id][1] != today:
del config[record_id]
else:
try:
id_list.remove(int(config[record_id][0]))
except Exception:
del config[record_id]
wife_id = choice(id_list)
write_group_config(group_id, user_id, wife_id, today, config)
member_info = await bot.get_group_member_info(group_id=group_id, user_id=wife_id)
result = await get_wife_info(member_info, wife_id)
await bot.send(ev, result, at_sender=True)
@@ -0,0 +1,3 @@
"""今日老婆 —— Service 层。"""
from .store import get_member_list, load_group_config, write_group_config # noqa: F401
@@ -0,0 +1,35 @@
"""今日老婆 —— 数据访问(Service 层)。"""
from __future__ import annotations
import json
import os
from pathlib import Path
_CONFIG_DIR = Path(__file__).resolve().parents[3] / "data" / "dailywife" / "config"
def get_member_list(all_list):
return [member["user_id"] for member in all_list]
def load_group_config(group_id: str) -> dict | None:
filename = _CONFIG_DIR / f"{group_id}.json"
try:
with open(filename, encoding="utf8") as f:
return json.load(f)
except Exception:
return None
def write_group_config(group_id: str, link_id: str, wife_id: str, date: str, config) -> None:
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
config_file = _CONFIG_DIR / f"{group_id}.json"
if config is not None:
config[link_id] = [wife_id, date]
else:
config = {link_id: [wife_id, date]}
tmp = config_file.with_suffix(".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False)
os.replace(tmp, config_file)
@@ -1,50 +0,0 @@
import hashlib
import json
from nonebot.log import logger as sv
import os
import random
import time
from io import BytesIO
import aiohttp
import qrcode
import requests
import requests.exceptions
from PIL import Image, ImageDraw, ImageFont
# PNG重绘大小
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=''):
"""
PNG缩放透明度处理
:param source_file: 源文件(Image.open())
:param new_width: 设置的宽度
:param new_height: 设置的高度
:param resample: 抗锯齿
:param ref_file: 参考文件
:return:
"""
img = source_file
img = img.convert("RGBA")
width, height = img.size
if ref_file != '':
imgRef = Image.open(ref_file)
new_width, new_height = imgRef.size
else:
if new_height == 0:
new_height = new_width * width / height
bands = img.split()
resample_map = {
"NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS
}
resample_method = resample_map.get(resample, Image.LANCZOS) # 默认使用 LANCZOS
bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands]
resized_file = Image.merge('RGBA', bands)
return resized_file
@@ -0,0 +1,3 @@
"""今日老婆 —— 纯工具层。"""
from .avatar import download_avatar, download_url, png_resize # noqa: F401
@@ -0,0 +1,56 @@
"""今日老婆 —— 头像工具(纯工具 + 网络请求)。"""
from __future__ import annotations
import hashlib
from io import BytesIO
import httpx
from PIL import Image
# PNG 重绘大小
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""):
img = source_file
img = img.convert("RGBA")
width, height = img.size
if ref_file != "":
imgRef = Image.open(ref_file)
new_width, new_height = imgRef.size
else:
if new_height == 0:
new_height = new_width * width / height
bands = img.split()
resample_map = {
"NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS,
}
resample_method = resample_map.get(resample, Image.LANCZOS)
bands = [b.resize((new_width, new_height), resample=resample_method) for b in bands]
return Image.merge("RGBA", bands)
async def download_url(url: str) -> bytes:
from nonebot import logger
async with httpx.AsyncClient() as client:
for i in range(3):
try:
resp = await client.get(url)
if resp.status_code != 200:
continue
return resp.content
except Exception as e:
logger.error(f"Error downloading {url}, retry {i}/3: {str(e)}")
return b""
async def download_avatar(user_id: str) -> bytes:
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
data = await download_url(url)
if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e":
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100"
data = await download_url(url)
return data