feat: 一大堆更新,修了一堆bug加了新功能
Some checks failed
Auto Deploy NeoBot (FRP + SSH 密码登录) / deploy-to-server (push) Has been cancelled
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:
@@ -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)
|
||||
|
||||
56
src/neobot/core/data/feedback.json
Normal file
56
src/neobot/core/data/feedback.json
Normal 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
|
||||
}
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user