refactor(plugins): comprehensive code review - ~35 fixes across 14 plugins

Phase 1 - Plugin code review (14/14 plugins):
- Security: 3x token leak in print→logger.debug, Bearer prefix handling
- Bug: bare except→specific exceptions, HorseState type safety, sync→async
- Critical: response_model undefined, route dead code, sync blocking event loop
- Quality: 11x print()→logger, variable name shadowing, consistent logging

Phase 2 - Deep analysis:
- Fix: payout int truncation→max(1, round(amount*odds))
- Fix: room_store get_lock race condition→dict.setdefault()
- Verify: data_manager f-string SQL is safe (uses ? placeholders)

Infrastructure: review reports generated for all plugins.
This commit is contained in:
2026-05-09 23:22:28 +08:00
parent 9a8cb3ad6d
commit c01338f496
43 changed files with 4233 additions and 3645 deletions

View File

@@ -1,69 +1,65 @@
"""Danding_QqPush 插件初始化模块"""
from nonebot import get_driver, get_bots
from nonebot.log import logger
from nonebot.plugin import PluginMetadata
from .config import Config
from .api import create_routes
from .sender import sender
__plugin_meta__ = PluginMetadata(
name="danding_qqpush",
description="通过外部 HTTP API 向 QQ 群定向推送通知",
usage="""
API 接口:
POST /danding/qqpush/{token}
请求参数:
{
"group_id": 123456789,
"qq": 987654321,
"text": "系统告警#数据库连接失败#请立即处理"
}
说明:
- text 中的 # 表示换行
- 消息会自动渲染为图片并发送到指定群
""",
config=Config,
)
# 加载配置
plugin_config = Config.model_validate(get_driver().config.dict())
def register_routes():
"""注册 FastAPI 路由"""
driver = get_driver()
# 创建并注册路由
routes = create_routes(plugin_config.Token, plugin_config)
driver.server_app.include_router(routes)
logger.info(f"[Danding_QqPush] API 路由已注册: /danding/qqpush/{plugin_config.Token}")
def init_bot():
"""初始化 Bot 实例"""
try:
bots = get_bots()
if bots:
# 获取第一个可用的 Bot
bot = list(bots.values())[0]
sender.set_bot(bot)
logger.info(f"[Danding_QqPush] Bot 连接: {bot.self_id}")
else:
logger.warning("[Danding_QqPush] 未找到可用的 Bot 实例")
except Exception as e:
logger.warning(f"[Danding_QqPush] 初始化 Bot 失败: {str(e)}")
# 插件加载时注册路由并初始化 Bot
try:
register_routes()
init_bot()
logger.info("[Danding_QqPush] 插件加载成功")
except Exception as e:
logger.error(f"[Danding_QqPush] 插件加载失败: {str(e)}")
"""Danding_QqPush 插件初始化模块"""
from nonebot import get_driver
from nonebot.log import logger
from nonebot.plugin import PluginMetadata
from .config import Config
from .api import create_routes
from .sender import sender
__plugin_meta__ = PluginMetadata(
name="danding_qqpush",
description="通过外部 HTTP API 向 QQ 群定向推送通知",
usage="""
API 接口:
POST /danding/qqpush/{token}
请求参数:
{
"group_id": 123456789,
"qq": 987654321,
"text": "系统告警#数据库连接失败#请立即处理"
}
说明:
- text 中的 # 表示换行
- 消息会自动渲染为图片并发送到指定群
""",
config=Config,
)
# 加载配置
plugin_config = Config.model_validate(get_driver().config.model_dump())
def register_routes():
"""注册 FastAPI 路由"""
driver = get_driver()
# 创建并注册路由
routes = create_routes(plugin_config.Token, plugin_config)
driver.server_app.include_router(routes)
logger.info(f"[Danding_QqPush] API 路由已注册: /danding/qqpush/{plugin_config.Token}")
# 插件加载时注册路由
try:
register_routes()
logger.info("[Danding_QqPush] 插件加载成功")
except Exception as e:
logger.error(f"[Danding_QqPush] 插件加载失败: {str(e)}")
# Bot 连接时自动初始化 sender
driver = get_driver()
@driver.on_bot_connect
async def _(bot):
"""Bot 连接时自动设置 sender"""
try:
sender.set_bot(bot)
logger.info(f"[Danding_QqPush] Bot 已连接: {bot.self_id}")
except Exception as e:
logger.error(f"[Danding_QqPush] Bot 连接初始化失败: {e}")

View File

@@ -1,142 +1,143 @@
"""API 接口模块 - FastAPI 路由定义"""
from fastapi import APIRouter, Request, HTTPException
from pydantic import BaseModel
from typing import Optional
from nonebot import get_driver, logger
from .config import Config
from .text_parser import TextParser
from .image_render import ImageRenderer
from .sender import sender
# 请求体模型
class PushRequest(BaseModel):
"""推送请求模型"""
group_id: int
"""接收消息的 QQ 群号"""
qq: int
"""被 @ 的 QQ 号"""
text: str
"""通知文本(# 表示换行)"""
# 响应模型
class PushResponse(BaseModel):
"""推送响应模型"""
success: bool
"""是否成功"""
message: str
"""响应消息"""
data: Optional[dict] = None
"""返回数据(如有)"""
# 创建路由器
router = APIRouter()
def create_routes(token: str, config: Config):
"""
创建 API 路由
Args:
token: 鉴权 Token
config: 配置对象
"""
@router.post(f"/danding/qqpush/{token}", response_model=PushResponse)
async def qqpush(request: Request, data: PushRequest):
"""
QQ 消息推送接口
Args:
request: FastAPI 请求对象
data: 推送请求数据
Returns:
推送结果
"""
try:
# 1. 验证参数
if not data.group_id:
raise HTTPException(status_code=400, detail="group_id 不能为空")
if not data.qq:
raise HTTPException(status_code=400, detail="qq 不能为空")
if not data.text or not isinstance(data.text, str):
raise HTTPException(status_code=400, detail="text 不能为空且必须是字符串")
# 2. 检查 Bot 是否在线
bot = sender.get_bot()
if not bot:
logger.error("Bot 实例未设置,无法发送消息")
raise HTTPException(
status_code=500,
detail="Bot 未连接,请检查机器人状态"
)
# 3. 文本处理
text_parser = TextParser(max_length=config.MaxTextLength)
if not text_parser.validate_text(data.text):
raise HTTPException(status_code=400, detail="文本内容无效")
parsed_text = text_parser.parse(data.text)
logger.info(f"解析文本: {parsed_text[:50]}..." if len(parsed_text) > 50 else parsed_text)
# 4. 生成图片
image_renderer = ImageRenderer(
width=config.ImageWidth,
font_size=config.ImageFontSize,
padding=config.ImagePadding,
line_spacing=config.ImageLineSpacing,
bg_color=config.ImageBgColor,
text_color=config.ImageTextColor,
font_paths=config.FontPaths
)
image_base64 = image_renderer.render_to_base64(parsed_text)
logger.info("图片生成成功")
# 5. 发送消息
send_result = await sender.send_to_group(
group_id=data.group_id,
qq=data.qq,
image_base64=image_base64
)
if not send_result["success"]:
logger.error(f"消息发送失败: {send_result['error']}")
raise HTTPException(
status_code=500,
detail=send_result["message"]
)
logger.info(f"消息发送成功 - 群: {data.group_id}, @: {data.qq}")
return PushResponse(
success=True,
message="推送成功",
data={
"group_id": data.group_id,
"qq": data.qq,
"message_id": send_result["data"].get("message_id")
}
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"推送接口异常: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"服务器内部错误: {str(e)}"
)
return router
"""API 接口模块 - FastAPI 路由定义"""
from fastapi import APIRouter, Request, HTTPException
from pydantic import BaseModel
import asyncio
from typing import Optional
from nonebot import get_driver, logger
from .config import Config
from .text_parser import TextParser
from .image_render import ImageRenderer
from .sender import sender
# 请求体模型
class PushRequest(BaseModel):
"""推送请求模型"""
group_id: int
"""接收消息的 QQ 群号"""
qq: int
"""被 @ 的 QQ 号"""
text: str
"""通知文本(# 表示换行)"""
# 响应模型
class PushResponse(BaseModel):
"""推送响应模型"""
success: bool
"""是否成功"""
message: str
"""响应消息"""
data: Optional[dict] = None
"""返回数据(如有)"""
# 创建路由器
router = APIRouter()
def create_routes(token: str, config: Config):
"""
创建 API 路由
Args:
token: 鉴权 Token
config: 配置对象
"""
@router.post(f"/danding/qqpush/{token}", response_model=PushResponse)
async def qqpush(request: Request, data: PushRequest):
"""
QQ 消息推送接口
Args:
request: FastAPI 请求对象
data: 推送请求数据
Returns:
推送结果
"""
try:
# 1. 验证参数
if not data.group_id:
raise HTTPException(status_code=400, detail="group_id 不能为空")
if not data.qq:
raise HTTPException(status_code=400, detail="qq 不能为空")
if not data.text or not isinstance(data.text, str):
raise HTTPException(status_code=400, detail="text 不能为空且必须是字符串")
# 2. 检查 Bot 是否在线
bot = sender.get_bot()
if not bot:
logger.error("Bot 实例未设置,无法发送消息")
raise HTTPException(
status_code=500,
detail="Bot 未连接,请检查机器人状态"
)
# 3. 文本处理
text_parser = TextParser(max_length=config.MaxTextLength)
if not text_parser.validate_text(data.text):
raise HTTPException(status_code=400, detail="文本内容无效")
parsed_text = text_parser.parse(data.text)
logger.info(f"解析文本: {parsed_text[:50]}..." if len(parsed_text) > 50 else parsed_text)
# 4. 生成图片
image_renderer = ImageRenderer(
width=config.ImageWidth,
font_size=config.ImageFontSize,
padding=config.ImagePadding,
line_spacing=config.ImageLineSpacing,
bg_color=config.ImageBgColor,
text_color=config.ImageTextColor,
font_paths=config.FontPaths
)
image_base64 = await asyncio.to_thread(image_renderer.render_to_base64, parsed_text)
logger.info("图片生成成功")
# 5. 发送消息
send_result = await sender.send_to_group(
group_id=data.group_id,
qq=data.qq,
image_base64=image_base64
)
if not send_result["success"]:
logger.error(f"消息发送失败: {send_result['error']}")
raise HTTPException(
status_code=500,
detail=send_result["message"]
)
logger.info(f"消息发送成功 - 群: {data.group_id}, @: {data.qq}")
return PushResponse(
success=True,
message="推送成功",
data={
"group_id": data.group_id,
"qq": data.qq,
"message_id": send_result["data"].get("message_id")
}
)
except HTTPException:
raise
except Exception as e:
logger.exception(f"推送接口异常: {str(e)}")
raise HTTPException(
status_code=500,
detail=f"服务器内部错误: {str(e)}"
)
return router

View File

@@ -1,42 +1,40 @@
"""Danding_QqPush 插件配置模块"""
from pydantic import BaseModel
class Config(BaseModel):
"""插件配置"""
Token: str = "danding-8HkL9xQ2"
"""API 访问 Token用于鉴权"""
# 图片生成配置
ImageWidth: int = 800
"""生成的图片宽度(像素)"""
ImageFontSize: int = 24
"""字体大小(像素)"""
ImagePadding: int = 30
"""图片内边距(像素)"""
ImageLineSpacing: float = 1.4
"""行距倍数"""
ImageBgColor: tuple = (252, 252, 252)
"""图片背景颜色 (R, G, B)"""
ImageTextColor: tuple = (0, 0, 0)
"""文本颜色 (R, G, B)"""
# 文本处理配置
MaxTextLength: int = 2000
"""最大文本长度(字符数),超过将截断"""
# 字体路径配置
FontPaths: list = [
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
"/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc",
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
]
"""字体文件路径列表"""
"""Danding_QqPush 插件配置模块"""
from pydantic import BaseModel
class Config(BaseModel):
"""插件配置"""
Token: str = ""
"""API 访问 Token用于鉴权(必须在 .env 中配置 DANDING_QQPUSH_TOKEN"""
# 图片生成配置
ImageWidth: int = 800
"""生成的图片宽度(像素)"""
ImageFontSize: int = 24
"""字体大小(像素)"""
ImagePadding: int = 30
"""图片内边距(像素)"""
ImageLineSpacing: float = 1.4
"""行距倍数"""
ImageBgColor: tuple = (252, 252, 252)
"""图片背景颜色 (R, G, B)"""
ImageTextColor: tuple = (0, 0, 0)
"""文本颜色 (R, G, B)"""
# 文本处理配置
MaxTextLength: int = 2000
"""最大文本长度(字符数),超过将截断"""
# 字体路径配置
FontPaths: list = ("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
"/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc",
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",)
"""字体文件路径列表"""

View File

@@ -1,148 +1,148 @@
"""消息发送模块 - 负责向 QQ 群发送消息"""
from typing import Optional
from nonebot import get_bots
from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
class MessageSender:
"""消息发送器"""
def __init__(self):
"""初始化消息发送器"""
self.bot: Optional[Bot] = None
def set_bot(self, bot: Bot):
"""
设置 Bot 实例
Args:
bot: OneBot V11 Bot 实例
"""
self.bot = bot
def get_bot(self) -> Optional[Bot]:
"""
获取 Bot 实例
Returns:
Bot 实例,如果未设置则尝试从全局获取
"""
if self.bot:
return self.bot
# 尝试从全局获取 Bot
try:
bots = get_bots()
if bots:
bot = list(bots.values())[0]
self.bot = bot
return bot
except Exception:
pass
return None
async def send_to_group(
self,
group_id: int,
qq: int,
image_base64: str
) -> dict:
"""
向指定群发送消息(@用户 + 图片)
Args:
group_id: 群号
qq: 要 @ 的 QQ 号
image_base64: 图片的 base64 编码格式base64://...
Returns:
发送结果字典
Raises:
ValueError: Bot 未设置
Exception: 发送失败
"""
bot = self.get_bot()
if not bot:
raise ValueError("Bot 实例未设置,无法发送消息")
try:
# 构造消息:@用户 + 图片
message = Message()
message.append(MessageSegment.at(qq))
message.append(MessageSegment.image(image_base64))
# 发送群消息,添加 __qqpush_source 标记供 auto_recall 识别
result = await bot.call_api(
"send_group_msg",
group_id=group_id,
message=message,
__qqpush_source="danding_qqpush" # 添加标记
)
return {
"success": True,
"data": result,
"message": "消息发送成功"
}
except Exception as e:
# 捕获异常并返回错误信息
return {
"success": False,
"error": str(e),
"message": f"消息发送失败: {str(e)}"
}
async def send_text_to_group(
self,
group_id: int,
qq: int,
text: str
) -> dict:
"""
向指定群发送纯文本消息(@用户 + 文本)
Args:
group_id: 群号
qq: 要 @ 的 QQ 号
text: 文本内容
Returns:
发送结果字典
"""
bot = self.get_bot()
if not bot:
raise ValueError("Bot 实例未设置,无法发送消息")
try:
# 构造消息:@用户 + 文本
message = Message()
message.append(MessageSegment.at(qq))
message.append(MessageSegment.text(text))
# 发送群消息,添加 __qqpush_source 标记供 auto_recall 识别
result = await bot.call_api(
"send_group_msg",
group_id=group_id,
message=message,
__qqpush_source="danding_qqpush" # 添加标记
)
return {
"success": True,
"data": result,
"message": "消息发送成功"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"消息发送失败: {str(e)}"
}
# 全局消息发送器实例
sender = MessageSender()
"""消息发送模块 - 负责向 QQ 群发送消息"""
from typing import Optional
from nonebot import get_bots, logger
from nonebot.adapters.onebot.v11 import Bot, Message, MessageSegment
class MessageSender:
"""消息发送器"""
def __init__(self):
"""初始化消息发送器"""
self.bot: Optional[Bot] = None
def set_bot(self, bot: Bot):
"""
设置 Bot 实例
Args:
bot: OneBot V11 Bot 实例
"""
self.bot = bot
def get_bot(self) -> Optional[Bot]:
"""
获取 Bot 实例
Returns:
Bot 实例,如果未设置则尝试从全局获取
"""
if self.bot:
return self.bot
# 尝试从全局获取 Bot
try:
bots = get_bots()
if bots:
bot = list(bots.values())[0]
self.bot = bot
return bot
except Exception as e:
logger.warning(f"[QqPush] 获取全局Bot失败: {e}")
return None
async def send_to_group(
self,
group_id: int,
qq: int,
image_base64: str
) -> dict:
"""
向指定群发送消息(@用户 + 图片)
Args:
group_id: 群号
qq: 要 @ 的 QQ 号
image_base64: 图片的 base64 编码格式base64://...
Returns:
发送结果字典
Raises:
ValueError: Bot 未设置
Exception: 发送失败
"""
bot = self.get_bot()
if not bot:
raise ValueError("Bot 实例未设置,无法发送消息")
try:
# 构造消息:@用户 + 图片
message = Message()
message.append(MessageSegment.at(qq))
message.append(MessageSegment.image(image_base64))
# 发送群消息,添加 __qqpush_source 标记供 auto_recall 识别
result = await bot.call_api(
"send_group_msg",
group_id=group_id,
message=message,
__qqpush_source="danding_qqpush" # 添加标记
)
return {
"success": True,
"data": result,
"message": "消息发送成功"
}
except Exception as e:
# 捕获异常并返回错误信息
return {
"success": False,
"error": str(e),
"message": f"消息发送失败: {str(e)}"
}
async def send_text_to_group(
self,
group_id: int,
qq: int,
text: str
) -> dict:
"""
向指定群发送纯文本消息(@用户 + 文本)
Args:
group_id: 群号
qq: 要 @ 的 QQ 号
text: 文本内容
Returns:
发送结果字典
"""
bot = self.get_bot()
if not bot:
raise ValueError("Bot 实例未设置,无法发送消息")
try:
# 构造消息:@用户 + 文本
message = Message()
message.append(MessageSegment.at(qq))
message.append(MessageSegment.text(text))
# 发送群消息,添加 __qqpush_source 标记供 auto_recall 识别
result = await bot.call_api(
"send_group_msg",
group_id=group_id,
message=message,
__qqpush_source="danding_qqpush" # 添加标记
)
return {
"success": True,
"data": result,
"message": "消息发送成功"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"消息发送失败: {str(e)}"
}
# 全局消息发送器实例
sender = MessageSender()

View File

@@ -1,52 +1,52 @@
"""工具函数模块"""
import secrets
import string
def generate_token(length: int = 16, prefix: str = "danding-") -> str:
"""
生成随机 Token
Args:
length: 随机部分长度
prefix: Token 前缀
Returns:
生成的 Token
"""
# 生成随机字符串(字母和数字)
alphabet = string.ascii_letters + string.digits
random_part = ''.join(secrets.choice(alphabet) for _ in range(length))
return f"{prefix}{random_part}"
def validate_token(token: str, expected_token: str) -> bool:
"""
验证 Token 是否正确
Args:
token: 待验证的 Token
expected_token: 期望的 Token
Returns:
是否匹配
"""
if not token or not expected_token:
return False
return token == expected_token
def format_log_message(message: str, level: str = "INFO") -> str:
"""
格式化日志消息
Args:
message: 原始消息
level: 日志级别
Returns:
格式化后的消息
"""
return f"[Danding_QqPush] [{level}] {message}"
"""工具函数模块"""
import secrets
import string
def generate_token(length: int = 16, prefix: str = "danding-") -> str:
"""
生成随机 Token
Args:
length: 随机部分长度
prefix: Token 前缀
Returns:
生成的 Token
"""
# 生成随机字符串(字母和数字)
alphabet = string.ascii_letters + string.digits
random_part = ''.join(secrets.choice(alphabet) for _ in range(length))
return f"{prefix}{random_part}"
def validate_token(token: str, expected_token: str) -> bool:
"""
验证 Token 是否正确
Args:
token: 待验证的 Token
expected_token: 期望的 Token
Returns:
是否匹配
"""
if not token or not expected_token:
return False
return secrets.compare_digest(token.encode(), expected_token.encode())
def format_log_message(message: str, level: str = "INFO") -> str:
"""
格式化日志消息
Args:
message: 原始消息
level: 日志级别
Returns:
格式化后的消息
"""
return f"[Danding_QqPush] [{level}] {message}"