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:
@@ -1,50 +0,0 @@
|
||||
from nonebot.adapters.onebot.v11 import Bot, Message
|
||||
from nonebot import logger
|
||||
|
||||
|
||||
class Reply:
|
||||
def __init__(self):
|
||||
self.message: str = ''
|
||||
self.user_id: int = -1
|
||||
self.user_card: str = ''
|
||||
self.time: int = -1
|
||||
|
||||
|
||||
async def extract_reply(event, bot: Bot) -> Reply:
|
||||
"""
|
||||
提取回复内容
|
||||
"""
|
||||
logger.info(event['group_id'])
|
||||
reply_data = Reply()
|
||||
if event['reply'] and event['reply']['message_id']: # 待优化
|
||||
reply = event['reply']
|
||||
sender = reply['sender']
|
||||
mes: Message = reply['message']
|
||||
print(mes)
|
||||
msg = mes[0]['data']['text']
|
||||
if msg:
|
||||
reply_data.message = msg
|
||||
meminfo = await get_member_info(bot=bot, user_id=sender['user_id'], group_id=event['group_id'])
|
||||
reply_data.user_card = (meminfo["card"] or meminfo["nickname"])
|
||||
reply_data.time = reply['time']
|
||||
reply_data.user_id = reply['sender']['user_id']
|
||||
else:
|
||||
reply_data.message = -1
|
||||
else:
|
||||
mes: Message = event["message"]
|
||||
print(mes)
|
||||
msg = mes[0]['data']['text'].replace("maq","")
|
||||
if msg:
|
||||
reply_data.message = msg
|
||||
meminfo = await get_member_info(bot=bot, user_id=event['user_id'], group_id=event['group_id'])
|
||||
reply_data.user_card = (meminfo["card"] or meminfo["nickname"])
|
||||
reply_data.time = event['time']
|
||||
reply_data.user_id = event['user_id']
|
||||
else:
|
||||
reply_data.message = -1
|
||||
return reply_data
|
||||
|
||||
|
||||
async def get_member_info(bot: Bot, group_id: int, user_id: int):
|
||||
member_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id)
|
||||
return member_info
|
||||
@@ -1,13 +1,9 @@
|
||||
from nonebot import logger, on_message, require
|
||||
"""语录 —— 入口(元数据 + 依赖)。"""
|
||||
|
||||
from nonebot import require
|
||||
from nonebot.plugin import PluginMetadata
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
from nonebot.adapters.onebot.v11 import Bot, Event
|
||||
from nonebot.plugin import PluginMetadata
|
||||
from nonebot.rule import keyword
|
||||
|
||||
from .Reply import extract_reply
|
||||
from .make_a_qoute import generate
|
||||
|
||||
__plugin_meta__ = PluginMetadata(
|
||||
name="语录",
|
||||
@@ -16,21 +12,4 @@ __plugin_meta__ = PluginMetadata(
|
||||
type="application",
|
||||
)
|
||||
|
||||
maq = on_message(keyword("maq"))
|
||||
|
||||
|
||||
@maq.handle()
|
||||
async def maq_handle(bot: Bot, ev: Event):
|
||||
event = ev.dict()
|
||||
try:
|
||||
if event.get("message_type") != "group":
|
||||
return
|
||||
reply = await extract_reply(event, bot)
|
||||
if reply.message == -1:
|
||||
await maq.finish("请引用文字消息")
|
||||
logger.info(f"{reply.user_id}, {reply.user_card}, {reply.message}")
|
||||
image = await generate(reply)
|
||||
await UniMessage.image(raw=image.getvalue()).send()
|
||||
except (KeyError, TypeError) as e:
|
||||
logger.error(e)
|
||||
await maq.finish(str(e))
|
||||
from .handlers import maq # noqa: E402,F401
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""语录 —— Trigger 层。"""
|
||||
|
||||
from .quote import maq # noqa: F401
|
||||
@@ -0,0 +1,32 @@
|
||||
"""语录 —— 触发器(Trigger)层。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nonebot import logger, on_message, require
|
||||
from nonebot.adapters.onebot.v11 import Bot, Event
|
||||
from nonebot.rule import keyword
|
||||
from nonebot_plugin_alconna import UniMessage
|
||||
|
||||
from ..services.generate import generate
|
||||
from ..utils.reply import extract_reply
|
||||
|
||||
require("nonebot_plugin_alconna")
|
||||
|
||||
maq = on_message(keyword("maq"))
|
||||
|
||||
|
||||
@maq.handle()
|
||||
async def maq_handle(bot: Bot, ev: Event):
|
||||
event = ev.dict()
|
||||
try:
|
||||
if event.get("message_type") != "group":
|
||||
return
|
||||
reply = await extract_reply(event, bot)
|
||||
if reply.message == -1:
|
||||
await maq.finish("请引用文字消息")
|
||||
logger.info(f"{reply.user_id}, {reply.user_card}, {reply.message}")
|
||||
image = await generate(reply)
|
||||
await UniMessage.image(raw=image.getvalue()).send()
|
||||
except (KeyError, TypeError) as e:
|
||||
logger.error(e)
|
||||
await maq.finish(str(e))
|
||||
@@ -0,0 +1,3 @@
|
||||
"""语录 —— Service 层。"""
|
||||
|
||||
from .generate import generate # noqa: F401
|
||||
+15
-55
@@ -1,3 +1,7 @@
|
||||
"""语录 —— 图片生成(Service 层)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
@@ -11,16 +15,15 @@ from PIL import Image, ImageDraw, ImageFont
|
||||
from pilmoji import Pilmoji
|
||||
from pilmoji.source import TwemojiEmojiSource
|
||||
|
||||
from .Reply import Reply
|
||||
from ..utils.reply import Reply
|
||||
|
||||
# --- 常量 ---
|
||||
_FONT_DIR = Path(__file__).resolve().parent / "data" / "font"
|
||||
FONT_SMALL = 24 # 用户卡片 / 时间
|
||||
FONT_TIME = 18 # 时间
|
||||
FONT_TEXT_MAX = 32 # 正文最大字号
|
||||
FONT_TEXT_MIN = 12 # 正文最小字号(低于此则截断)
|
||||
# 字体资源在插件 res/font 下
|
||||
_FONT_DIR = Path(__file__).resolve().parents[1] / "res" / "font"
|
||||
FONT_SMALL = 24
|
||||
FONT_TIME = 18
|
||||
FONT_TEXT_MAX = 32
|
||||
FONT_TEXT_MIN = 12
|
||||
|
||||
# --- 字体懒加载 ---
|
||||
_font_cache: dict = {}
|
||||
|
||||
|
||||
@@ -46,10 +49,6 @@ def _auto_fit(
|
||||
max_width: float,
|
||||
max_height: float,
|
||||
) -> tuple[list[str], ImageFont.FreeTypeFont, int, int]:
|
||||
"""
|
||||
自动缩放字号使文本适合 max_width × max_height 区域。
|
||||
返回 (行列表, 字体, 字号, 行间距)。
|
||||
"""
|
||||
for size in range(FONT_TEXT_MAX, FONT_TEXT_MIN - 1, -2):
|
||||
font = _get_font("SourceHanSansCN-Medium.otf", size)
|
||||
chars_per_line = max(1, int(max_width / (size * 0.92)))
|
||||
@@ -60,8 +59,6 @@ def _auto_fit(
|
||||
draw.textlength(line, font) <= max_width for line in lines
|
||||
):
|
||||
return lines, font, size, spacing
|
||||
|
||||
# 最小字号兜底:允许截断
|
||||
font = _get_font("SourceHanSansCN-Medium.otf", FONT_TEXT_MIN)
|
||||
chars_per_line = max(1, int(max_width / (FONT_TEXT_MIN * 0.92)))
|
||||
lines = wrap_text(text, chars_per_line)
|
||||
@@ -70,87 +67,54 @@ def _auto_fit(
|
||||
|
||||
|
||||
async def generate(reply: Reply) -> BytesIO:
|
||||
"""
|
||||
生成图片 — 正文根据长度自动缩放字号,无需截断
|
||||
"""
|
||||
image = Image.new('RGBA', (1280, 640), (0, 0, 0, 255))
|
||||
image = Image.new("RGBA", (1280, 640), (0, 0, 0, 255))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
x_start = 601.1
|
||||
x_end = 1241.1
|
||||
max_width = x_end - x_start # ≈ 640px
|
||||
max_height = 400 # 为用户卡片留出空间
|
||||
|
||||
# 自动缩放
|
||||
max_width = x_end - x_start
|
||||
max_height = 400
|
||||
text_wrapped, text_font, font_size, line_spacing = _auto_fit(
|
||||
reply.message, draw, max_width, max_height,
|
||||
)
|
||||
|
||||
# 垂直居中
|
||||
total_h = len(text_wrapped) * (font_size + line_spacing) - line_spacing
|
||||
y_min = 100 - font_size
|
||||
y_max = 512
|
||||
y = int((y_max - y_min) / 2 - total_h / 2)
|
||||
|
||||
# --- Pilmoji ---
|
||||
with Pilmoji(image, source=TwemojiEmojiSource, emoji_position_offset=(0, 10)) as emoji:
|
||||
for line in text_wrapped:
|
||||
text_size = draw.textlength(line, text_font)
|
||||
x = int(x_start + (max_width - text_size) / 2)
|
||||
emoji.text((x, y), line, font=text_font, fill=(255, 255, 255, 255), embedded_color=True)
|
||||
y += font_size + line_spacing
|
||||
|
||||
# --- 头像 ---
|
||||
img_content = await download_avatar(str(reply.user_id))
|
||||
avatar = Image.open(img_content).resize((640, 640))
|
||||
avatar = alpha_gradient(avatar, 320, 640)
|
||||
image.paste(avatar, (0, 0), avatar)
|
||||
|
||||
# --- 用户卡片 ---
|
||||
x_center = 921.1
|
||||
card_size = draw.textlength("- " + reply.user_card, _font_small())
|
||||
x_card_start = x_center - card_size / 2
|
||||
draw.text((x_card_start, y), "- " + reply.user_card, font=_font_small(), fill=(169, 172, 184, 255))
|
||||
|
||||
# --- 时间 ---
|
||||
fmt_time = strftime("%Y.%m.%d %H:%M", localtime(reply.time))
|
||||
time_size = draw.textlength(fmt_time, _font_small())
|
||||
x_time_start = x_end - time_size
|
||||
draw.text((x_time_start, 640 - FONT_SMALL - 20), fmt_time, font=_font_time(), fill=(169, 172, 184, 255))
|
||||
|
||||
# --- 输出 ---
|
||||
bytes_io = BytesIO()
|
||||
image.convert('RGB').save(bytes_io, format="JPEG")
|
||||
image.convert("RGB").save(bytes_io, format="JPEG")
|
||||
return bytes_io
|
||||
|
||||
|
||||
def wrap_text(text: str, width: int) -> list[str]:
|
||||
"""
|
||||
按宽度自动换行,支持中日韩文本。
|
||||
先按换行符分行,再用 cjk_textwrap.wrap 按宽度折行。
|
||||
"""
|
||||
return [wrapped for line in text.splitlines() for wrapped in cjk_textwrap.wrap(line, width)]
|
||||
|
||||
|
||||
def alpha_gradient(image, x_s: int, x_e: int):
|
||||
"""
|
||||
为图像添加从 x_s 到 x_e 的左右渐变透明度遮罩。
|
||||
使用 numpy 加速,避免逐像素 putpixel 的低效操作。
|
||||
"""
|
||||
image = image.convert("RGBA")
|
||||
w, h = image.size
|
||||
|
||||
# 默认全不透明
|
||||
gradient = np.full((h, w), 255, dtype=np.uint8)
|
||||
|
||||
# 生成渐变列
|
||||
xs = np.linspace(0, 1, x_e - x_s, endpoint=False)
|
||||
alphas = 255 - (np.sin(xs * np.pi - np.pi / 2) / 2 + 0.5) * 255
|
||||
alphas = alphas.astype(np.uint8)
|
||||
|
||||
# 应用到对应范围
|
||||
gradient[:, x_s:x_e] = alphas[np.newaxis, :]
|
||||
|
||||
mask = Image.fromarray(gradient, mode="L")
|
||||
image.putalpha(mask)
|
||||
return image
|
||||
@@ -166,10 +130,6 @@ async def download_avatar(user_id: str) -> BytesIO:
|
||||
|
||||
|
||||
async def download_url(url: str) -> bytes:
|
||||
"""
|
||||
下载指定 URL 内容,带 3 次重试和超时。
|
||||
如果失败,返回空字节串 b""。
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
for i in range(3):
|
||||
try:
|
||||
@@ -0,0 +1,3 @@
|
||||
"""语录 —— 纯工具层。"""
|
||||
|
||||
from .reply import Reply, extract_reply, get_member_info # noqa: F401
|
||||
@@ -0,0 +1,54 @@
|
||||
"""语录 —— 回复提取工具(utils 层)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nonebot import logger
|
||||
from nonebot.adapters.onebot.v11 import Bot, Message
|
||||
|
||||
|
||||
class Reply:
|
||||
def __init__(self):
|
||||
self.message: str = ""
|
||||
self.user_id: int = -1
|
||||
self.user_card: str = ""
|
||||
self.time: int = -1
|
||||
|
||||
|
||||
async def extract_reply(event, bot: Bot) -> Reply:
|
||||
"""
|
||||
提取回复内容
|
||||
"""
|
||||
logger.info(event["group_id"])
|
||||
reply_data = Reply()
|
||||
if event["reply"] and event["reply"]["message_id"]: # 待优化
|
||||
reply = event["reply"]
|
||||
sender = reply["sender"]
|
||||
mes: Message = reply["message"]
|
||||
logger.info(mes)
|
||||
msg = mes[0]["data"]["text"]
|
||||
if msg:
|
||||
reply_data.message = msg
|
||||
meminfo = await get_member_info(bot=bot, user_id=sender["user_id"], group_id=event["group_id"])
|
||||
reply_data.user_card = meminfo["card"] or meminfo["nickname"]
|
||||
reply_data.time = reply["time"]
|
||||
reply_data.user_id = reply["sender"]["user_id"]
|
||||
else:
|
||||
reply_data.message = -1
|
||||
else:
|
||||
mes: Message = event["message"]
|
||||
logger.info(mes)
|
||||
msg = mes[0]["data"]["text"].replace("maq", "")
|
||||
if msg:
|
||||
reply_data.message = msg
|
||||
meminfo = await get_member_info(bot=bot, user_id=event["user_id"], group_id=event["group_id"])
|
||||
reply_data.user_card = meminfo["card"] or meminfo["nickname"]
|
||||
reply_data.time = event["time"]
|
||||
reply_data.user_id = event["user_id"]
|
||||
else:
|
||||
reply_data.message = -1
|
||||
return reply_data
|
||||
|
||||
|
||||
async def get_member_info(bot: Bot, group_id: int, user_id: int):
|
||||
member_info = await bot.get_group_member_info(group_id=group_id, user_id=user_id)
|
||||
return member_info
|
||||
Reference in New Issue
Block a user