feat: 一大堆更新,修了一堆bug加了新功能
Some checks failed
Auto Deploy NeoBot (FRP + SSH 密码登录) / deploy-to-server (push) Has been cancelled

1. 新增反馈插件、复读插件、戳一戳插件
2. 修复了配置、线程安全、SQL校验等多处bug
3. 重构插件加载系统,支持验证插件+热加载
4. 修复大量测试用例问题,修复了76个测试挂逼的问题
5. 调整了broadcast插件的发送间隔
6. 优化了性能统计的函数命名逻辑
7. 修复了furry插件的注释和函数名错误
8. 重构了输入校验的逻辑顺序
9. 配置文件新增了默认值处理
This commit is contained in:
2026-05-15 06:25:40 +08:00
parent f0c63136bf
commit 67d01392e4
25 changed files with 726 additions and 1154 deletions

View File

@@ -152,7 +152,7 @@ class ConfigModel(BaseModel):
mysql: MySQLModel
docker: DockerModel
image_manager: ImageManagerModel
reverse_ws: ReverseWSModel
reverse_ws: ReverseWSModel = Field(default_factory=ReverseWSModel)
threading: ThreadingModel = Field(default_factory=ThreadingModel)
bilibili: BilibiliModel = Field(default_factory=BilibiliModel)
local_file_server: LocalFileServerModel = Field(default_factory=LocalFileServerModel)

View File

@@ -0,0 +1,56 @@
[
{
"id": 1,
"user_id": 2212335563,
"nickname": "十四",
"content": "什么时候出个今日老公",
"time": 1778722380,
"time_str": "2026-05-14 09:33:00",
"done": false
},
{
"id": 2,
"user_id": 2221577113,
"nickname": "鍍鉻酸鉀",
"content": "什么时候出个发打码的勾八功能",
"time": 1778722573,
"time_str": "2026-05-14 09:36:13",
"done": false
},
{
"id": 3,
"user_id": 2212335563,
"nickname": "十四",
"content": "加一个今日老公功能",
"time": 1778722684,
"time_str": "2026-05-14 09:38:04",
"done": false
},
{
"id": 4,
"user_id": 2212335563,
"nickname": "十四",
"content": "加一个今日老婆功能",
"time": 1778722721,
"time_str": "2026-05-14 09:38:41",
"done": false
},
{
"id": 5,
"user_id": 2221577113,
"nickname": "鍍鉻酸鉀",
"content": "1",
"time": 1778723275,
"time_str": "2026-05-14 09:47:55",
"done": false
},
{
"id": 6,
"user_id": 3067550242,
"nickname": "斑鸠",
"content": "我这有个不用的API 你要不要",
"time": 1778727344,
"time_str": "2026-05-14 10:55:44",
"done": false
}
]

View File

@@ -2,12 +2,13 @@
插件管理器模块
负责扫描、加载和管理 `plugins` 目录下的所有插件。
支持固定验证插件列表 + 热加载模式。
"""
import importlib
import os
import pkgutil
import sys
from typing import Set
from typing import Dict, Set
from .command_manager import CommandManager
from ..utils.exceptions import SyncHandlerError, PluginLoadError, PluginReloadError, PluginNotFoundError
@@ -15,11 +16,13 @@ from ..utils.logger import logger, ModuleLogger
from ..utils.singleton import Singleton
from .command_manager import matcher as command_manager
# 确保logger在模块级别可见
__all__ = ['PluginManager', 'logger']
# 确保logger在模块级别可见
__all__ = ['PluginManager', 'logger']
# 插件来源类型
PLUGIN_SOURCE_VERIFIED = "verified" # 固定验证插件
PLUGIN_SOURCE_HOT = "hot" # 热加载插件
PLUGIN_SOURCE_UNKNOWN = "unknown" # 未知来源
class PluginManager(Singleton):
@@ -32,22 +35,21 @@ class PluginManager(Singleton):
:param command_manager: CommandManager 的实例
"""
# 检查是否已经初始化
if hasattr(self, '_initialized') and self._initialized:
return
# 只有首次初始化时才执行
self._initialized = True
# 始终创建 logger 和 loaded_plugins
self.logger = ModuleLogger("PluginManager")
self.loaded_plugins: Set[str] = set()
self.verified_plugins: Set[str] = set()
self.hot_loaded_plugins: Set[str] = set()
self.plugin_sources: Dict[str, str] = {}
if command_manager:
self._command_manager = command_manager
else:
self._command_manager = None
@property
def command_manager(self):
"""
@@ -60,33 +62,48 @@ class PluginManager(Singleton):
def load_all_plugins(self) -> None:
"""
扫描并加载 `plugins` 目录下的所有插件。
加载流程:
1. 导入 neobot.plugins 包(触发 __init__.py 中的验证插件 + 热加载)
2. 扫描目录,加载启动后新增的插件
3. 追踪每个插件的来源类型
"""
# 使用 pathlib 获取更可靠的路径
# 当前文件src/neobot/core/managers/plugin_manager.py
# 目标src/neobot/plugins/
current_dir = os.path.dirname(os.path.abspath(__file__))
# 回退三级到项目根目录 (core/managers -> core -> neobot -> src)
root_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
plugin_dir = os.path.join(root_dir, "neobot", "plugins")
# 使用完整的包名neobot.plugins
package_name = "neobot.plugins"
if not os.path.exists(plugin_dir):
self.logger.error(f"插件目录不存在:{plugin_dir}")
return
# 获取验证插件列表(从 __init__.py 导入)
try:
plugins_pkg = importlib.import_module(package_name)
verified_list = getattr(plugins_pkg, "VERIFIED_PLUGINS", ())
except Exception as e:
self.logger.warning(f"无法获取验证插件列表: {e}")
verified_list = ()
self.logger.info(f"正在从 {package_name} 加载插件 (路径:{plugin_dir})...")
for _, module_name, is_pkg in pkgutil.iter_modules([plugin_dir]):
full_module_name = f"{package_name}.{module_name}"
if module_name.startswith("_"):
continue
action = "加载" # 初始化默认值
full_module_name = f"{package_name}.{module_name}"
is_verified = module_name in verified_list
action = "加载"
try:
if full_module_name in self.loaded_plugins:
self.command_manager.unload_plugin(full_module_name)
module = importlib.reload(sys.modules[full_module_name])
action = "重载"
elif full_module_name in sys.modules:
# __init__.py 已导入此模块,标记即可
module = sys.modules[full_module_name]
action = "跳过" if not is_verified else "加载"
else:
module = importlib.import_module(full_module_name)
action = "加载"
@@ -94,11 +111,23 @@ class PluginManager(Singleton):
if hasattr(module, "__plugin_meta__"):
meta = getattr(module, "__plugin_meta__")
self.command_manager.plugins[full_module_name] = meta
self.loaded_plugins.add(full_module_name)
self.plugin_sources[full_module_name] = (
PLUGIN_SOURCE_VERIFIED if is_verified else PLUGIN_SOURCE_HOT
)
if is_verified:
self.verified_plugins.add(full_module_name)
else:
self.hot_loaded_plugins.add(full_module_name)
type_str = "" if is_pkg else "文件"
self.logger.success(f" [{type_str}] 成功{action}: {module_name}")
source_tag = "[验证]" if is_verified else "[热加载]"
if action != "跳过":
self.logger.success(f" {source_tag} [{type_str}] 成功{action}: {module_name}")
else:
self.logger.debug(f" {source_tag} [{type_str}] 已加载: {module_name}")
except SyncHandlerError as e:
error = PluginLoadError(
plugin_name=module_name,
@@ -122,7 +151,7 @@ class PluginManager(Singleton):
"""
if full_module_name not in self.loaded_plugins:
self.logger.warning(f"尝试重载一个未被加载的插件: {full_module_name},将按首次加载处理。")
if full_module_name not in sys.modules:
reload_error = PluginNotFoundError(
plugin_name=full_module_name,
@@ -135,11 +164,11 @@ class PluginManager(Singleton):
try:
self.command_manager.unload_plugin(full_module_name)
module = importlib.reload(sys.modules[full_module_name])
if hasattr(module, "__plugin_meta__"):
meta = getattr(module, "__plugin_meta__")
self.command_manager.plugins[full_module_name] = meta
self.logger.success(f"插件 {full_module_name} 已成功重载。")
except SyncHandlerError as e:
error = PluginReloadError(
@@ -158,5 +187,41 @@ class PluginManager(Singleton):
self.logger.exception(f"重载插件 {full_module_name} 时发生错误: {error.message}")
self.logger.log_custom_exception(error)
def get_plugin_source(self, full_module_name: str) -> str:
"""
获取插件的来源类型
Args:
full_module_name: 插件的完整模块名
Returns:
str: PLUGIN_SOURCE_VERIFIED / PLUGIN_SOURCE_HOT / PLUGIN_SOURCE_UNKNOWN
"""
return self.plugin_sources.get(full_module_name, PLUGIN_SOURCE_UNKNOWN)
def is_verified_plugin(self, full_module_name: str) -> bool:
"""
判断插件是否为已验证的固定插件
Args:
full_module_name: 插件的完整模块名
Returns:
bool: 是否为验证插件
"""
return full_module_name in self.verified_plugins
def is_hot_loaded_plugin(self, full_module_name: str) -> bool:
"""
判断插件是否为热加载插件
Args:
full_module_name: 插件的完整模块名
Returns:
bool: 是否为热加载插件
"""
return full_module_name in self.hot_loaded_plugins
plugin_manager = PluginManager(command_manager=command_manager)

View File

@@ -56,6 +56,7 @@ class ThreadManager:
# 每个客户端的线程池(用于反向 WebSocket
self._client_executors: Dict[str, ThreadPoolExecutor] = {}
self._client_executor_locks: Dict[str, threading.Lock] = {}
self._client_init_lock = threading.Lock()
# 线程安全的事件循环(用于跨线程调用)
self._event_loops: Dict[str, asyncio.AbstractEventLoop] = {}
@@ -142,7 +143,7 @@ class ThreadManager:
ThreadPoolExecutor 实例
"""
if client_id not in self._client_executors:
with threading.Lock():
with self._client_init_lock:
if client_id not in self._client_executors:
executor = ThreadPoolExecutor(
max_workers=global_config.threading.client_max_workers,

View File

@@ -81,35 +81,24 @@ class InputValidator:
self.nine_digit_pattern = re.compile(r'^\d{9}$') # 用于城市代码验证
def validate_sql_input(self, input_str: str, allow_safe_keywords: bool = False) -> bool:
"""
验证 SQL 输入是否安全
Args:
input_str: 输入字符串
allow_safe_keywords: 是否允许安全的 SQL 关键字
Returns:
bool: 是否安全
"""
if not input_str:
return True
input_lower = input_str.lower()
# 检查 SQL 注入模式(使用预编译的正则表达式)
if allow_safe_keywords:
dangerous_operations = ['drop', 'delete', 'truncate', 'alter', 'create', 'exec']
for op in dangerous_operations:
if re.search(r'\b' + re.escape(op) + r'\b', input_lower):
self.logger.warning(f"检测到危险 SQL 操作: {op}")
return False
return True
for pattern in self.sql_injection_patterns:
if pattern.search(input_lower):
self.logger.warning(f"检测到可能的 SQL 注入: {input_str}")
return False
# 如果允许安全关键字,检查是否包含危险操作
if allow_safe_keywords:
dangerous_operations = ['drop', 'delete', 'truncate', 'alter', 'create', 'exec']
for op in dangerous_operations:
if op in input_lower:
self.logger.warning(f"检测到危险 SQL 操作: {op}")
return False
return True
def validate_xss_input(self, input_str: str) -> bool:
@@ -320,9 +309,8 @@ class InputValidator:
sanitized = html.escape(html_str)
# 移除危险的属性
sanitized = re.sub(r'on\w+\s*=', 'data-', sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r'on(\w+)\s*=', r'data-\1=', sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r'javascript:', 'data:', sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r'data:', 'data:', sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r'vbscript:', 'data:', sanitized, flags=re.IGNORECASE)
return sanitized

View File

@@ -122,7 +122,7 @@ def timeit(func: Optional[Callable] = None, *, log_level: int = logging.INFO, co
装饰后的函数
"""
def decorator(func: Callable) -> Callable:
func_name = func.__qualname__
func_name = func.__name__
is_coroutine = inspect.iscoroutinefunction(func)
if is_coroutine: