Files
DanDingNoneBot/danding_bot/plugins/chatai/__init__.py
Mr.Xia c01338f496 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.
2026-05-09 23:22:28 +08:00

186 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import random
import os
import sys
import subprocess
from nonebot import on_message, get_plugin_config, get_driver, logger
from nonebot.adapters.onebot.v11 import MessageEvent, Bot, MessageSegment
from nonebot.plugin import PluginMetadata
from nonebot.exception import FinishedException
from openai import OpenAI
from .config import Config
from .screenshot import markdown_to_image
import pyppeteer
# 插件元信息
__plugin_meta__ = PluginMetadata(
name="chatai",
description="一个对接 DeepSeek 的聊天 AI 插件",
usage="发送以 * 开头的消息AI 会回复你,两分钟后自动撤销",
config=Config,
)
# 获取插件配置
plugin_config = get_plugin_config(Config)
# 全局浏览器实例
_browser: pyppeteer.browser.Browser | None = None
_browser_lock = asyncio.Lock()
# OpenAI 客户端(延迟初始化)
_ai_client: OpenAI | None = None
# 撤回任务引用集合防止被GC回收
_recall_tasks: set[asyncio.Task] = set()
# 注册消息事件处理器
message_handler = on_message(priority=50, block=True)
# 确保输出目录存在
os.makedirs("data/chatai", exist_ok=True)
# 获取 NoneBot 驱动器
driver = get_driver()
def _force_kill_chrome():
"""强制终止残留 Chrome 进程"""
try:
if sys.platform == "win32":
subprocess.run(
["taskkill", "/F", "/IM", "chrome.exe"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
else:
subprocess.run(
["pkill", "-9", "-f", "chrome"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
except Exception:
pass
@driver.on_startup
async def startup_cleanup():
"""启动时清理残留Chrome进程"""
_force_kill_chrome()
@driver.on_shutdown
async def close_browser():
"""在 NoneBot 关闭时关闭浏览器"""
global _browser
async with _browser_lock:
if _browser is not None:
try:
await _browser.close()
except Exception as e:
logger.warning(f"关闭浏览器异常: {e}")
_browser = None
_force_kill_chrome()
async def init_browser() -> "pyppeteer.browser.Browser":
"""初始化或复用浏览器实例"""
global _browser
async with _browser_lock:
if _browser is None or not _browser.process:
try:
_browser = await pyppeteer.launch(
headless=True,
args=["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
)
logger.info("chatai: 浏览器实例已创建")
except Exception as e:
logger.error(f"chatai: 浏览器启动失败: {e}")
raise
return _browser
def _get_ai_client() -> OpenAI:
"""获取或创建 OpenAI 客户端(单例)"""
global _ai_client
if _ai_client is None:
_ai_client = OpenAI(
api_key=plugin_config.deepseek_token,
base_url="https://api.siliconflow.cn/v1",
)
return _ai_client
async def call_ai_api(message: str) -> str:
"""调用 AI 接口"""
client = _get_ai_client()
response = client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3",
messages=[
{"role": "system", "content": (
"你是一个活泼可爱的群助手。请在回复中使用丰富的 Emoji 表情"
"(如 😊 😄 🎉 ✨ 💖 等)。你的语气要俏皮活泼,像在和朋友聊天一样自然。"
"在回答问题时要保持专业性的同时,也要让回复显得生动有趣。"
"每条回复都必须包含至少2-3个 Emoji 表情。"
"如果你需要展示代码片段,请确保代码中不包含任何颜文字或表情符号,"
"保持代码的专业性和可读性。"
)},
{"role": "user", "content": message},
],
stream=False,
)
return response.choices[0].message.content or ""
@message_handler.handle()
async def handle_message(event: MessageEvent, bot: Bot):
user_message = event.get_plaintext().strip()
if not user_message.startswith("*"):
return
user_message = user_message[1:].strip()
if not user_message:
await asyncio.sleep(random.uniform(2, 3))
await message_handler.finish("请输入有效内容哦~")
try:
browser = await init_browser()
response = await call_ai_api(user_message)
if response:
await asyncio.sleep(random.uniform(2, 3))
# 使用事件ID+时间戳避免并发路径冲突
image_path = f"data/chatai/output_{event.message_id}.png"
await markdown_to_image(response, image_path, browser)
sent_message = await bot.send(
event, MessageSegment.image(f"file:///{os.path.abspath(image_path)}")
)
# 清理临时图片文件
try:
os.remove(image_path)
except OSError:
pass
# 保存task引用防止GC回收
task = asyncio.create_task(
_delete_message_after_delay(bot, sent_message["message_id"])
)
_recall_tasks.add(task)
task.add_done_callback(_recall_tasks.discard)
except FinishedException:
raise
except Exception as e:
logger.error(f"chatai处理失败: user_id={event.user_id} error={e}")
await asyncio.sleep(random.uniform(2, 3))
await message_handler.finish(f"出错了: {e}")
async def _delete_message_after_delay(bot: Bot, message_id: int):
"""两分钟后撤回消息"""
await asyncio.sleep(120)
try:
await bot.delete_msg(message_id=message_id)
except Exception as e:
logger.debug(f"chatai撤回消息失败(可忽略): msg_id={message_id} error={e}")