feat: 新增通知图片测试命令
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"""API 接口模块 - FastAPI 路由定义"""
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
"""API 接口模块 - FastAPI 路由定义"""
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
from nonebot import logger
|
||||
|
||||
|
||||
from .config import Config
|
||||
from .article_render import render_article_to_base64
|
||||
from .text_parser import TextParser
|
||||
@@ -14,32 +14,32 @@ from .utils import validate_token
|
||||
|
||||
# Module-level singleton: load font once, reuse across requests
|
||||
_renderer: Optional['ImageRenderer'] = None
|
||||
|
||||
def _get_renderer(config: Config) -> 'ImageRenderer':
|
||||
global _renderer
|
||||
if _renderer is None:
|
||||
_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,
|
||||
)
|
||||
return _renderer
|
||||
|
||||
|
||||
|
||||
# 请求体模型
|
||||
class PushRequest(BaseModel):
|
||||
"""推送请求模型"""
|
||||
group_id: int
|
||||
"""接收消息的 QQ 群号"""
|
||||
|
||||
|
||||
def _get_renderer(config: Config) -> 'ImageRenderer':
|
||||
global _renderer
|
||||
if _renderer is None:
|
||||
_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,
|
||||
)
|
||||
return _renderer
|
||||
|
||||
|
||||
|
||||
# 请求体模型
|
||||
class PushRequest(BaseModel):
|
||||
"""推送请求模型"""
|
||||
group_id: int
|
||||
"""接收消息的 QQ 群号"""
|
||||
|
||||
qq: Optional[int] = None
|
||||
"""可选的被 @ QQ 号;未传时仅发送群消息"""
|
||||
|
||||
|
||||
text: str
|
||||
"""通知文本(# 表示换行)"""
|
||||
|
||||
@@ -51,67 +51,67 @@ class PushRequest(BaseModel):
|
||||
|
||||
published_at: Optional[str] = None
|
||||
"""公告发布时间"""
|
||||
|
||||
|
||||
class PrivatePushRequest(BaseModel):
|
||||
"""私聊推送请求模型"""
|
||||
user_id: int
|
||||
"""接收私聊消息的好友 QQ 号"""
|
||||
|
||||
text: Optional[str] = None
|
||||
"""通知文本(# 表示换行)"""
|
||||
|
||||
text_only: bool = False
|
||||
"""True=发送纯文本;False(默认)=渲染为图片发送"""
|
||||
|
||||
html: Optional[str] = None
|
||||
"""可选的完整公告 HTML;传入时使用虚拟浏览器截图"""
|
||||
|
||||
title: Optional[str] = None
|
||||
"""公告标题"""
|
||||
|
||||
published_at: Optional[str] = None
|
||||
"""公告发布时间"""
|
||||
|
||||
|
||||
|
||||
# 响应模型
|
||||
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: 配置对象
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class PrivatePushRequest(BaseModel):
|
||||
"""私聊推送请求模型"""
|
||||
user_id: int
|
||||
"""接收私聊消息的好友 QQ 号"""
|
||||
|
||||
text: Optional[str] = None
|
||||
"""通知文本(# 表示换行)"""
|
||||
|
||||
text_only: bool = False
|
||||
"""True=发送纯文本;False(默认)=渲染为图片发送"""
|
||||
|
||||
html: Optional[str] = None
|
||||
"""可选的完整公告 HTML;传入时使用虚拟浏览器截图"""
|
||||
|
||||
title: Optional[str] = None
|
||||
"""公告标题"""
|
||||
|
||||
published_at: Optional[str] = None
|
||||
"""公告发布时间"""
|
||||
|
||||
|
||||
|
||||
# 响应模型
|
||||
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("/danding/qqpush/{request_token}", response_model=PushResponse)
|
||||
async def qqpush(request_token: str, request: Request, data: PushRequest):
|
||||
"""
|
||||
QQ 消息推送接口
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
data: 推送请求数据
|
||||
|
||||
Returns:
|
||||
推送结果
|
||||
"""
|
||||
QQ 消息推送接口
|
||||
|
||||
Args:
|
||||
request: FastAPI 请求对象
|
||||
data: 推送请求数据
|
||||
|
||||
Returns:
|
||||
推送结果
|
||||
"""
|
||||
try:
|
||||
if not validate_token(request_token, token):
|
||||
@@ -120,27 +120,27 @@ def create_routes(token: str, config: Config):
|
||||
# 1. 验证参数
|
||||
if not data.group_id:
|
||||
raise HTTPException(status_code=400, detail="group_id 不能为空")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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. 原始公告使用 Chromium 截图,普通通知保持既有 Pillow 渲染。
|
||||
if data.html:
|
||||
image_base64 = await render_article_to_base64(
|
||||
@@ -154,128 +154,132 @@ def create_routes(token: str, config: Config):
|
||||
qq=data.qq,
|
||||
)
|
||||
else:
|
||||
image_base64 = await asyncio.to_thread(_get_renderer(config).render_to_base64, parsed_text)
|
||||
image_base64 = await asyncio.to_thread(
|
||||
_get_renderer(config).render_to_base64, parsed_text, scene="group"
|
||||
)
|
||||
send_result = await sender.send_to_group(
|
||||
group_id=data.group_id,
|
||||
image_base64=image_base64,
|
||||
qq=data.qq,
|
||||
)
|
||||
logger.info("图片生成成功")
|
||||
|
||||
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"推送接口异常: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="服务器内部错误"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/danding/private/{request_token}", response_model=PushResponse)
|
||||
async def private_push(request_token: str, request: Request, data: PrivatePushRequest):
|
||||
"""
|
||||
好友私聊推送接口
|
||||
|
||||
仅可向好友发送私聊消息;目标不在好友列表或发送失败时返回错误。
|
||||
"""
|
||||
try:
|
||||
if not validate_token(request_token, token):
|
||||
raise HTTPException(status_code=403, detail="Token 验证失败")
|
||||
|
||||
# 1. 验证参数
|
||||
if not data.user_id:
|
||||
raise HTTPException(status_code=400, detail="user_id 不能为空")
|
||||
|
||||
if not (data.text or data.html):
|
||||
raise HTTPException(status_code=400, detail="text 和 html 不能同时为空")
|
||||
|
||||
# 2. 检查 Bot 是否在线
|
||||
bot = sender.get_bot()
|
||||
if not bot:
|
||||
logger.error("Bot 实例未设置,无法发送消息")
|
||||
raise HTTPException(status_code=500, detail="Bot 未连接,请检查机器人状态")
|
||||
|
||||
# 3. 检查目标是否为好友
|
||||
try:
|
||||
is_friend = await sender.is_friend(data.user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"查询好友列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"查询好友列表失败: {e}")
|
||||
|
||||
if not is_friend:
|
||||
logger.warning(f"目标用户不是好友: {data.user_id}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"用户 {data.user_id} 不在好友列表中,无法发送私聊消息"
|
||||
)
|
||||
|
||||
# 4. 发送消息
|
||||
if data.text_only and data.text:
|
||||
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)
|
||||
send_result = await sender.send_text_to_private(data.user_id, parsed_text)
|
||||
elif data.html:
|
||||
image_base64 = await render_article_to_base64(
|
||||
data.html,
|
||||
data.title or "蛋定助手通知",
|
||||
data.published_at,
|
||||
)
|
||||
send_result = await sender.send_to_private(data.user_id, image_base64)
|
||||
else:
|
||||
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)
|
||||
image_base64 = await asyncio.to_thread(_get_renderer(config).render_to_base64, parsed_text)
|
||||
send_result = await sender.send_to_private(data.user_id, image_base64)
|
||||
|
||||
logger.info("私聊文本已准备发送" if data.text_only else "私聊图片生成成功")
|
||||
|
||||
if not send_result["success"]:
|
||||
logger.error(f"私聊消息发送失败: {send_result.get('error')}")
|
||||
raise HTTPException(status_code=500, detail=send_result["message"])
|
||||
|
||||
logger.info(f"私聊消息发送成功 - 好友: {data.user_id}")
|
||||
|
||||
return PushResponse(
|
||||
success=True,
|
||||
message="私聊推送成功",
|
||||
data={
|
||||
"user_id": data.user_id,
|
||||
"message_id": send_result["data"].get("message_id")
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"私聊推送接口异常: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="服务器内部错误"
|
||||
)
|
||||
return router
|
||||
|
||||
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"推送接口异常: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="服务器内部错误"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/danding/private/{request_token}", response_model=PushResponse)
|
||||
async def private_push(request_token: str, request: Request, data: PrivatePushRequest):
|
||||
"""
|
||||
好友私聊推送接口
|
||||
|
||||
仅可向好友发送私聊消息;目标不在好友列表或发送失败时返回错误。
|
||||
"""
|
||||
try:
|
||||
if not validate_token(request_token, token):
|
||||
raise HTTPException(status_code=403, detail="Token 验证失败")
|
||||
|
||||
# 1. 验证参数
|
||||
if not data.user_id:
|
||||
raise HTTPException(status_code=400, detail="user_id 不能为空")
|
||||
|
||||
if not (data.text or data.html):
|
||||
raise HTTPException(status_code=400, detail="text 和 html 不能同时为空")
|
||||
|
||||
# 2. 检查 Bot 是否在线
|
||||
bot = sender.get_bot()
|
||||
if not bot:
|
||||
logger.error("Bot 实例未设置,无法发送消息")
|
||||
raise HTTPException(status_code=500, detail="Bot 未连接,请检查机器人状态")
|
||||
|
||||
# 3. 检查目标是否为好友
|
||||
try:
|
||||
is_friend = await sender.is_friend(data.user_id)
|
||||
except Exception as e:
|
||||
logger.error(f"查询好友列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"查询好友列表失败: {e}")
|
||||
|
||||
if not is_friend:
|
||||
logger.warning(f"目标用户不是好友: {data.user_id}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"用户 {data.user_id} 不在好友列表中,无法发送私聊消息"
|
||||
)
|
||||
|
||||
# 4. 发送消息
|
||||
if data.text_only and data.text:
|
||||
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)
|
||||
send_result = await sender.send_text_to_private(data.user_id, parsed_text)
|
||||
elif data.html:
|
||||
image_base64 = await render_article_to_base64(
|
||||
data.html,
|
||||
data.title or "蛋定助手通知",
|
||||
data.published_at,
|
||||
)
|
||||
send_result = await sender.send_to_private(data.user_id, image_base64)
|
||||
else:
|
||||
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)
|
||||
image_base64 = await asyncio.to_thread(
|
||||
_get_renderer(config).render_to_base64, parsed_text, scene="private"
|
||||
)
|
||||
send_result = await sender.send_to_private(data.user_id, image_base64)
|
||||
|
||||
logger.info("私聊文本已准备发送" if data.text_only else "私聊图片生成成功")
|
||||
|
||||
if not send_result["success"]:
|
||||
logger.error(f"私聊消息发送失败: {send_result.get('error')}")
|
||||
raise HTTPException(status_code=500, detail=send_result["message"])
|
||||
|
||||
logger.info(f"私聊消息发送成功 - 好友: {data.user_id}")
|
||||
|
||||
return PushResponse(
|
||||
success=True,
|
||||
message="私聊推送成功",
|
||||
data={
|
||||
"user_id": data.user_id,
|
||||
"message_id": send_result["data"].get("message_id")
|
||||
}
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"私聊推送接口异常: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="服务器内部错误"
|
||||
)
|
||||
return router
|
||||
|
||||
Reference in New Issue
Block a user