Merge pull request #19 from Fairy-Oracle-Sanctuary/dev

优化codepy插件
This commit is contained in:
镀铬酸钾
2026-01-02 20:13:11 +08:00
committed by GitHub
4 changed files with 170 additions and 74 deletions

View File

@@ -1,6 +1,6 @@
from .command_manager import matcher from .command_manager import matcher
from .config_loader import global_config from .config_loader import global_config
from .plugin_manager import PluginDataManager from .plugin_manager import PluginDataManager
from .WS import WS from .ws import WS
__all__ = ["WS", "matcher", "global_config", "PluginDataManager"] __all__ = ["WS", "matcher", "global_config", "PluginDataManager"]

View File

@@ -36,13 +36,15 @@ class CommandManager:
Args: Args:
prefixes (Tuple[str, ...]): 一个包含所有合法命令前缀的元组。 prefixes (Tuple[str, ...]): 一个包含所有合法命令前缀的元组。
""" """
# --- 初始化所有处理器列表 ---
self.prefixes = prefixes self.prefixes = prefixes
self.commands: Dict[str, Callable] = {} # 存储消息指令处理器 self.commands: Dict[str, Callable] = {}
self.notice_handlers: List[Dict] = [] # 存储通知事件处理器 self.message_handlers: List[Callable] = []
self.request_handlers: List[Dict] = [] # 存储请求事件处理器 self.notice_handlers: List[Dict] = []
self.plugins: Dict[str, Dict[str, Any]] = {} # 存储已加载插件的元数据 self.request_handlers: List[Dict] = []
self.plugins: Dict[str, Dict[str, Any]] = {}
# --- 注册内置 help 指令 --- # --- 注册内置指令 ---
self.commands["help"] = self._help_command self.commands["help"] = self._help_command
self.plugins["core.help"] = { self.plugins["core.help"] = {
"name": "帮助", "name": "帮助",
@@ -50,6 +52,26 @@ class CommandManager:
"usage": "/help", "usage": "/help",
} }
def on_message(self) -> Callable:
"""
装饰器:用于注册一个通用的消息处理器。
被此装饰器注册的函数,会在每次收到消息时(在指令匹配前)被调用。
如果函数返回 True则表示该消息已被“消费”后续的指令匹配将不会进行。
Example:
@matcher.on_message()
async def code_input_handler(bot, event):
if is_waiting_for_code(event.user_id):
await process_code(event.raw_message)
return True # 消费事件
"""
def decorator(func: Callable) -> Callable:
self.message_handlers.append(func)
return func
return decorator
async def _help_command(self, bot, event): async def _help_command(self, bot, event):
""" """
内置的 `/help` 命令的实现。 内置的 `/help` 命令的实现。
@@ -164,21 +186,21 @@ class CommandManager:
async def handle_message(self, bot, event): async def handle_message(self, bot, event):
""" """
处理消息事件,解析并分发指令。 处理消息事件,优先执行通用处理器,然后解析并分发指令。
该方法会检查消息是否以已配置的命令前缀开头,如果是,则解析出
指令名称和参数,并调用对应的处理器。
Args:
bot: Bot 实例。
event: 消息事件对象。
""" """
# --- 1. 执行通用消息处理器 ---
for handler in self.message_handlers:
# 如果任何一个处理器返回 True则中断后续处理
consumed = await self._run_handler(handler, bot, event)
if consumed:
return
# --- 2. 检查并执行指令 ---
if not event.raw_message: if not event.raw_message:
return return
raw_text = event.raw_message.strip() raw_text = event.raw_message.strip()
# 1. 检查前缀
prefix_found = None prefix_found = None
for p in self.prefixes: for p in self.prefixes:
if raw_text.startswith(p): if raw_text.startswith(p):
@@ -188,7 +210,6 @@ class CommandManager:
if not prefix_found: if not prefix_found:
return return
# 2. 拆分指令和参数
full_cmd = raw_text[len(prefix_found) :].split() full_cmd = raw_text[len(prefix_found) :].split()
if not full_cmd: if not full_cmd:
return return
@@ -196,7 +217,6 @@ class CommandManager:
cmd_name = full_cmd[0] cmd_name = full_cmd[0]
args = full_cmd[1:] args = full_cmd[1:]
# 3. 查找并执行
if cmd_name in self.commands: if cmd_name in self.commands:
func = self.commands[cmd_name] func = self.commands[cmd_name]
await self._run_handler(func, bot, event, args) await self._run_handler(func, bot, event, args)
@@ -227,7 +247,7 @@ class CommandManager:
async def _run_handler(self, func: Callable, bot, event, args: List[str] = None): async def _run_handler(self, func: Callable, bot, event, args: List[str] = None):
""" """
智能执行事件处理器。 智能执行事件处理器,并返回事件是否被消费
该方法会检查目标处理器的函数签名,并根据签名动态地传入所需的参数 该方法会检查目标处理器的函数签名,并根据签名动态地传入所需的参数
(如 `bot`, `event`, `args`),实现了依赖注入。 (如 `bot`, `event`, `args`),实现了依赖注入。
@@ -237,7 +257,9 @@ class CommandManager:
bot: Bot 实例。 bot: Bot 实例。
event: 事件对象。 event: 事件对象。
args (List[str], optional): 指令参数列表(仅对消息事件有效)。 args (List[str], optional): 指令参数列表(仅对消息事件有效)。
Defaults to None.
Returns:
bool: 如果处理器函数返回 True则返回 True否则返回 False。
""" """
sig = inspect.signature(func) sig = inspect.signature(func)
params = sig.parameters params = sig.parameters
@@ -250,8 +272,9 @@ class CommandManager:
if "args" in params and args is not None: if "args" in params and args is not None:
kwargs["args"] = args kwargs["args"] = args
# 执行函数 # 执行函数并获取返回值
await func(**kwargs) result = await func(**kwargs)
return result is True
# --- 全局单例 --- # --- 全局单例 ---

View File

@@ -13,7 +13,7 @@ from watchdog.events import FileSystemEventHandler
# 初始化日志系统,必须在其他 core 模块导入之前执行 # 初始化日志系统,必须在其他 core 模块导入之前执行
from core.logger import logger from core.logger import logger
from core import WS from core.ws import WS
from core.plugin_manager import load_all_plugins from core.plugin_manager import load_all_plugins

View File

@@ -5,10 +5,11 @@ code_py插件
""" """
import asyncio import asyncio
import os import re
import sys import sys
import tempfile import tempfile
from typing import Tuple import os
from typing import Tuple, Set
from core.bot import Bot from core.bot import Bot
from core.command_manager import matcher from core.command_manager import matcher
@@ -17,73 +18,145 @@ from models import MessageEvent
__plugin_meta__ = { __plugin_meta__ = {
"name": "code_py", "name": "code_py",
"description": "提供执行python代码的功能", "description": "提供执行python代码的功能",
"usage": "/code py [python代码] - 执行python代码", "usage": "/code_py - 进入交互模式,等待输入代码块\n/code_py [单行代码] - 快速执行单行代码",
} }
# --- 安全配置:危险模块黑名单 ---
DANGEROUS_MODULES = [
"os", "sys", "subprocess", "shutil", "socket", "requests", "urllib",
"http", "ftplib", "telnetlib", "ctypes", "_thread", "multiprocessing",
"asyncio",
]
@matcher.command("code_py") # 编译后的正则表达式,用于分割语句
async def execute_python_code(bot: Bot, event: MessageEvent, args: list[str]): STATEMENT_SPLIT_PATTERN = re.compile(r'[;\n]')
if not args:
await event.reply("请提供要执行的Python代码。用法/code_py [python代码]") def is_code_safe(code: str) -> Tuple[bool, str]:
"""
检查代码中是否包含危险的模块导入。
"""
statements = STATEMENT_SPLIT_PATTERN.split(code)
for statement in statements:
statement = statement.strip()
if not statement: continue
parts = statement.split()
if not parts: continue
if parts[0] == 'from' and len(parts) > 1:
module_name = parts[1].strip()
if module_name in DANGEROUS_MODULES:
return False, f"检测到不允许的模块导入:'{module_name}'"
elif parts[0] == 'import' and len(parts) > 1:
modules_str = ' '.join(parts[1:])
imported_modules = [m.strip() for m in modules_str.split(',')]
for module_name in imported_modules:
actual_module_name = module_name.split()[0]
if actual_module_name in DANGEROUS_MODULES:
return False, f"检测到不允许的模块导入:'{actual_module_name}'"
return True, ""
async def run_code_in_subprocess(code_str: str, timeout: float = 10.0) -> Tuple[str, str]:
"""
在子进程中安全地执行Python代码。
"""
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as tf:
tf.write(code_str)
tf_path = tf.name
try:
proc = await asyncio.create_subprocess_exec(
sys.executable, tf_path,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
try:
out_bytes, err_bytes = await asyncio.wait_for(proc.communicate(), timeout=timeout)
except asyncio.TimeoutError:
proc.kill()
await proc.communicate()
return "", f"执行超时(>{timeout}s"
return out_bytes.decode(errors="ignore"), err_bytes.decode(errors="ignore")
finally:
try:
os.remove(tf_path)
except Exception:
pass
async def process_and_reply(bot: Bot, event: MessageEvent, code: str):
"""
核心处理逻辑:安全检查、执行代码并回复结果。
"""
safe, message = is_code_safe(code)
if not safe:
await event.reply(f"代码安全检查未通过:\n{message}")
return return
code = " ".join(args)
async def run_code_in_subprocess(
code_str: str, timeout: float = 5.0
) -> Tuple[str, str]:
# 这里用临时文件
with tempfile.NamedTemporaryFile(
"w", suffix=".py", delete=False, encoding="utf-8"
) as tf:
tf.write(code_str)
tf_path = tf.name
try:
proc = await asyncio.create_subprocess_exec(
sys.executable,
tf_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
try:
out_bytes, err_bytes = await asyncio.wait_for(
proc.communicate(), timeout=timeout
)
except asyncio.TimeoutError:
proc.kill()
await proc.communicate()
return "", f"执行超时(>{timeout}s"
return out_bytes.decode(errors="ignore"), err_bytes.decode(errors="ignore")
finally:
try:
os.remove(tf_path)
except Exception:
pass
try: try:
stdout, stderr = await run_code_in_subprocess(code, timeout=5.0) stdout, stderr = await run_code_in_subprocess(code, timeout=10.0)
except Exception as e: except Exception as e:
await event.reply(f"执行失败:{e}") await event.reply(f"执行失败:{e}")
return return
# 优先显示 stderr如果 stderr 为空则显示 stdout
resp = stderr.strip() or stdout.strip() or "(无输出)" resp = stderr.strip() or stdout.strip() or "(无输出)"
# 限制返回长度,避免过长消息
MAX = 1500 MAX = 1500
if len(resp) > MAX: if len(resp) > MAX:
resp = resp[:MAX] + "\n...输出被截断..." resp = resp[:MAX] + "\n...输出被截断..."
nodes = [ nodes = [
bot.build_forward_node(user_id=event.self_id, nickname="机器人", message=code), bot.build_forward_node(user_id=event.self_id, nickname="输入代码", message=code),
bot.build_forward_node( bot.build_forward_node(user_id=event.self_id, nickname="执行结果", message=resp),
user_id=event.self_id, nickname="机器人", message="执行结果:\n" + resp
),
] ]
try: try:
await bot.send_forwarded_messages(event, nodes) await bot.send_forwarded_messages(event, nodes)
except Exception as e: except Exception as e:
await event.reply(f"发送失败: {e}") await event.reply(f"结果发送失败: {e}\n\n{resp}")
# --- 交互式会话状态 ---
# 使用集合存储正在等待代码输入的用户标识
waiting_users: Set[str] = set()
def get_session_id(event: MessageEvent) -> str:
"""根据事件类型生成唯一的会话ID"""
if hasattr(event, 'group_id'):
# 群聊会话ID
return f"group_{event.group_id}-{event.user_id}"
else:
# 私聊会话ID
return f"private_{event.user_id}"
@matcher.command("code_py")
async def handle_code_command(bot: Bot, event: MessageEvent, args: list[str]):
# 模式一:快速执行单行代码
if args:
code = " ".join(args)
await process_and_reply(bot, event, code)
return
# 模式二:进入交互模式
session_id = get_session_id(event)
if session_id in waiting_users:
await event.reply("您已经有一个正在等待输入的code会话了请直接发送代码。")
return
waiting_users.add(session_id)
await event.reply("请在下一条消息中发送要执行的Python代码块。发送“取消”可退出")
@matcher.on_message()
async def handle_code_input(bot: Bot, event: MessageEvent):
session_id = get_session_id(event)
# 检查用户是否处于等待状态
if session_id in waiting_users:
# 从等待集合中移除,无论输入是什么
waiting_users.remove(session_id)
# 处理取消操作
if event.raw_message.strip() == "取消":
await event.reply("已取消输入。")
return True # 消费事件
# 执行代码
await process_and_reply(bot, event, event.raw_message)
return True # 消费事件,防止被其他指令匹配
# 如果用户不在等待状态,则不处理
return False