This commit is contained in:
Windpicker-owo
2025-08-29 14:16:30 +08:00
27 changed files with 3178 additions and 623 deletions

View File

@@ -5,7 +5,9 @@
"""
import os
import sys
import time
import importlib
from pathlib import Path
from threading import Thread
from typing import Dict, Set, List, Optional, Tuple
@@ -27,7 +29,8 @@ class PluginFileHandler(FileSystemEventHandler):
self.hot_reload_manager = hot_reload_manager
self.pending_reloads: Set[str] = set() # 待重载的插件名称
self.last_reload_time: Dict[str, float] = {} # 上次重载时间
self.debounce_delay = 1.0 # 防抖延迟(秒)
self.debounce_delay = 2.0 # 增加防抖延迟到2秒确保文件写入完成
self.file_change_cache: Dict[str, float] = {} # 文件变化缓存
def on_modified(self, event):
"""文件修改事件"""
@@ -60,26 +63,42 @@ class PluginFileHandler(FileSystemEventHandler):
plugin_name, source_type = plugin_info
current_time = time.time()
last_time = self.last_reload_time.get(plugin_name, 0)
# 防抖处理,避免频繁重载
if current_time - last_time < self.debounce_delay:
# 文件变化缓存,避免重复处理同一文件的快速连续变化
file_cache_key = f"{file_path}_{change_type}"
last_file_time = self.file_change_cache.get(file_cache_key, 0)
if current_time - last_file_time < 0.5: # 0.5秒内的重复文件变化忽略
return
self.file_change_cache[file_cache_key] = current_time
# 插件级别的防抖处理
last_plugin_time = self.last_reload_time.get(plugin_name, 0)
if current_time - last_plugin_time < self.debounce_delay:
# 如果在防抖期内,更新待重载标记但不立即处理
self.pending_reloads.add(plugin_name)
return
file_name = Path(file_path).name
logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type}) [{source_type}]")
logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type}) [{source_type}] -> {plugin_name}")
# 如果是删除事件,处理关键文件删除
if change_type == "deleted":
# 解析实际的插件名称
actual_plugin_name = self.hot_reload_manager._resolve_plugin_name(plugin_name)
if file_name == "plugin.py":
if plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(plugin_name)
if actual_plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(actual_plugin_name)
else:
logger.info(f"🗑️ 插件主文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]")
return
elif file_name in ("manifest.toml", "_manifest.json"):
if plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(plugin_name)
if actual_plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} -> {actual_plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(actual_plugin_name)
else:
logger.info(f"🗑️ 插件配置文件被删除,但插件未加载: {plugin_name} -> {actual_plugin_name} [{source_type}]")
return
# 对于修改和创建事件,都进行重载
@@ -87,29 +106,43 @@ class PluginFileHandler(FileSystemEventHandler):
self.pending_reloads.add(plugin_name)
self.last_reload_time[plugin_name] = current_time
# 延迟重载,避免文件正在写入时重载
# 延迟重载,确保文件写入完成
reload_thread = Thread(
target=self._delayed_reload,
args=(plugin_name, source_type),
args=(plugin_name, source_type, current_time),
daemon=True
)
reload_thread.start()
except Exception as e:
logger.error(f"❌ 处理文件变化时发生错误: {e}")
logger.error(f"❌ 处理文件变化时发生错误: {e}", exc_info=True)
def _delayed_reload(self, plugin_name: str, source_type: str):
def _delayed_reload(self, plugin_name: str, source_type: str, trigger_time: float):
"""延迟重载插件"""
try:
# 等待文件写入完成
time.sleep(self.debounce_delay)
if plugin_name in self.pending_reloads:
self.pending_reloads.remove(plugin_name)
logger.info(f"🔄 延迟重载插件: {plugin_name} [{source_type}]")
self.hot_reload_manager._reload_plugin(plugin_name)
# 检查是否还需要重载(可能在等待期间有更新的变化)
if plugin_name not in self.pending_reloads:
return
# 检查是否有更新的重载请求
if self.last_reload_time.get(plugin_name, 0) > trigger_time:
return
self.pending_reloads.discard(plugin_name)
logger.info(f"🔄 开始延迟重载插件: {plugin_name} [{source_type}]")
# 执行深度重载
success = self.hot_reload_manager._deep_reload_plugin(plugin_name)
if success:
logger.info(f"✅ 插件重载成功: {plugin_name} [{source_type}]")
else:
logger.error(f"❌ 插件重载失败: {plugin_name} [{source_type}]")
except Exception as e:
logger.error(f"❌ 延迟重载插件 {plugin_name} 时发生错误: {e}")
logger.error(f"❌ 延迟重载插件 {plugin_name} 时发生错误: {e}", exc_info=True)
def _get_plugin_info_from_path(self, file_path: str) -> Optional[Tuple[str, str]]:
"""从文件路径获取插件信息
@@ -237,35 +270,131 @@ class PluginHotReloadManager:
logger.info("🛑 插件热重载已停止")
def _reload_plugin(self, plugin_name: str):
"""重载指定插件"""
"""重载指定插件(简单重载)"""
try:
logger.info(f"🔄 开始重载插件: {plugin_name}")
# 解析实际的插件名称
actual_plugin_name = self._resolve_plugin_name(plugin_name)
logger.info(f"🔄 开始简单重载插件: {plugin_name} -> {actual_plugin_name}")
if plugin_manager.reload_plugin(plugin_name):
logger.info(f"✅ 插件重载成功: {plugin_name}")
if plugin_manager.reload_plugin(actual_plugin_name):
logger.info(f"✅ 插件简单重载成功: {actual_plugin_name}")
return True
else:
logger.error(f"❌ 插件重载失败: {plugin_name}")
logger.error(f"❌ 插件简单重载失败: {actual_plugin_name}")
return False
except Exception as e:
logger.error(f"❌ 重载插件 {plugin_name} 时发生错误: {e}")
logger.error(f"❌ 重载插件 {plugin_name} 时发生错误: {e}", exc_info=True)
return False
def _resolve_plugin_name(self, folder_name: str) -> str:
"""
将文件夹名称解析为实际的插件名称
通过检查插件管理器中的路径映射来找到对应的插件名
"""
# 首先检查是否直接匹配
if folder_name in plugin_manager.plugin_classes:
logger.debug(f"🔍 直接匹配插件名: {folder_name}")
return folder_name
# 如果没有直接匹配,搜索路径映射,并优先返回在插件类中存在的名称
matched_plugins = []
for plugin_name, plugin_path in plugin_manager.plugin_paths.items():
# 检查路径是否包含该文件夹名
if folder_name in plugin_path:
matched_plugins.append((plugin_name, plugin_path))
# 在匹配的插件中,优先选择在插件类中存在的
for plugin_name, plugin_path in matched_plugins:
if plugin_name in plugin_manager.plugin_classes:
logger.debug(f"🔍 文件夹名 '{folder_name}' 映射到插件名 '{plugin_name}' (路径: {plugin_path})")
return plugin_name
# 如果还是没找到在插件类中存在的,返回第一个匹配项
if matched_plugins:
plugin_name, plugin_path = matched_plugins[0]
logger.warning(f"⚠️ 文件夹 '{folder_name}' 映射到 '{plugin_name}',但该插件类不存在")
return plugin_name
# 如果还是没找到,返回原文件夹名
logger.warning(f"⚠️ 无法找到文件夹 '{folder_name}' 对应的插件名,使用原名称")
return folder_name
def _deep_reload_plugin(self, plugin_name: str):
"""深度重载指定插件(清理模块缓存)"""
try:
# 解析实际的插件名称
actual_plugin_name = self._resolve_plugin_name(plugin_name)
logger.info(f"🔄 开始深度重载插件: {plugin_name} -> {actual_plugin_name}")
# 强制清理相关模块缓存
self._force_clear_plugin_modules(plugin_name)
# 使用插件管理器的强制重载功能
success = plugin_manager.force_reload_plugin(actual_plugin_name)
if success:
logger.info(f"✅ 插件深度重载成功: {actual_plugin_name}")
return True
else:
logger.error(f"❌ 插件深度重载失败,尝试简单重载: {actual_plugin_name}")
# 如果深度重载失败,尝试简单重载
return self._reload_plugin(actual_plugin_name)
except Exception as e:
logger.error(f"❌ 深度重载插件 {plugin_name} 时发生错误: {e}", exc_info=True)
# 出错时尝试简单重载
return self._reload_plugin(plugin_name)
def _force_clear_plugin_modules(self, plugin_name: str):
"""强制清理插件相关的模块缓存"""
import sys
# 找到所有相关的模块名
modules_to_remove = []
plugin_module_prefix = f"src.plugins.built_in.{plugin_name}"
for module_name in list(sys.modules.keys()):
if plugin_module_prefix in module_name:
modules_to_remove.append(module_name)
# 删除模块缓存
for module_name in modules_to_remove:
if module_name in sys.modules:
logger.debug(f"🗑️ 清理模块缓存: {module_name}")
del sys.modules[module_name]
def _force_reimport_plugin(self, plugin_name: str):
"""强制重新导入插件(委托给插件管理器)"""
try:
# 使用插件管理器的重载功能
success = plugin_manager.reload_plugin(plugin_name)
return success
except Exception as e:
logger.error(f"❌ 强制重新导入插件 {plugin_name} 时发生错误: {e}", exc_info=True)
return False
def _unload_plugin(self, plugin_name: str):
"""卸载指定插件"""
try:
logger.info(f"🗑️ 开始卸载插件: {plugin_name}")
if plugin_manager.unload_plugin(plugin_name):
logger.info(f"✅ 插件卸载成功: {plugin_name}")
return True
else:
logger.error(f"❌ 插件卸载失败: {plugin_name}")
return False
except Exception as e:
logger.error(f"❌ 卸载插件 {plugin_name} 时发生错误: {e}")
logger.error(f"❌ 卸载插件 {plugin_name} 时发生错误: {e}", exc_info=True)
return False
def reload_all_plugins(self):
"""重载所有插件"""
try:
logger.info("🔄 开始重载所有插件...")
logger.info("🔄 开始深度重载所有插件...")
# 获取当前已加载的插件列表
loaded_plugins = list(plugin_manager.loaded_plugins.keys())
@@ -274,15 +403,42 @@ class PluginHotReloadManager:
fail_count = 0
for plugin_name in loaded_plugins:
if plugin_manager.reload_plugin(plugin_name):
logger.info(f"🔄 重载插件: {plugin_name}")
if self._deep_reload_plugin(plugin_name):
success_count += 1
else:
fail_count += 1
logger.info(f"✅ 插件重载完成: 成功 {success_count} 个,失败 {fail_count}")
# 清理全局缓存
importlib.invalidate_caches()
except Exception as e:
logger.error(f"❌ 重载所有插件时发生错误: {e}")
logger.error(f"❌ 重载所有插件时发生错误: {e}", exc_info=True)
def force_reload_plugin(self, plugin_name: str):
"""手动强制重载指定插件(委托给插件管理器)"""
try:
logger.info(f"🔄 手动强制重载插件: {plugin_name}")
# 清理待重载列表中的该插件(避免重复重载)
for handler in self.file_handlers:
handler.pending_reloads.discard(plugin_name)
# 使用插件管理器的强制重载功能
success = plugin_manager.force_reload_plugin(plugin_name)
if success:
logger.info(f"✅ 手动强制重载成功: {plugin_name}")
else:
logger.error(f"❌ 手动强制重载失败: {plugin_name}")
return success
except Exception as e:
logger.error(f"❌ 手动强制重载插件 {plugin_name} 时发生错误: {e}", exc_info=True)
return False
def add_watch_directory(self, directory: str):
"""添加新的监听目录"""
@@ -321,14 +477,33 @@ class PluginHotReloadManager:
def get_status(self) -> dict:
"""获取热重载状态"""
pending_reloads = set()
if self.file_handlers:
for handler in self.file_handlers:
pending_reloads.update(handler.pending_reloads)
return {
"is_running": self.is_running,
"watch_directories": self.watch_directories,
"active_observers": len(self.observers),
"loaded_plugins": len(plugin_manager.loaded_plugins),
"failed_plugins": len(plugin_manager.failed_plugins),
"pending_reloads": list(pending_reloads),
"debounce_delay": self.file_handlers[0].debounce_delay if self.file_handlers else 0,
}
def clear_all_caches(self):
"""清理所有Python模块缓存"""
try:
logger.info("🧹 开始清理所有Python模块缓存...")
# 重新扫描所有插件目录,这会重新加载模块
plugin_manager.rescan_plugin_directory()
logger.info("✅ 模块缓存清理完成")
except Exception as e:
logger.error(f"❌ 清理模块缓存时发生错误: {e}", exc_info=True)
# 全局热重载管理器实例
hot_reload_manager = PluginHotReloadManager()

View File

@@ -2,6 +2,7 @@ import asyncio
import os
import traceback
import sys
import importlib
from typing import Dict, List, Optional, Tuple, Type, Any
from importlib.util import spec_from_file_location, module_from_spec
@@ -289,11 +290,11 @@ class PluginManager:
Args:
plugin_file: 插件文件路径
plugin_name: 插件名称
plugin_dir: 插件目录路径
"""
# 生成模块名
# 生成模块名和插件信息
plugin_path = Path(plugin_file)
plugin_dir = plugin_path.parent # 插件目录
plugin_name = plugin_dir.name # 插件名称
module_name = ".".join(plugin_path.parent.parts)
try:
@@ -307,13 +308,13 @@ class PluginManager:
module.__package__ = module_name # 设置模块包名
spec.loader.exec_module(module)
logger.debug(f"插件模块加载成功: {plugin_file}")
logger.debug(f"插件模块加载成功: {plugin_file} -> {plugin_name} ({plugin_dir})")
return True
except Exception as e:
error_msg = f"加载插件模块 {plugin_file} 失败: {e}"
logger.error(error_msg)
self.failed_plugins[module_name] = error_msg
self.failed_plugins[plugin_name if 'plugin_name' in locals() else module_name] = error_msg
return False
# == 兼容性检查 ==
@@ -527,6 +528,10 @@ class PluginManager:
# 从已加载插件中移除
del self.loaded_plugins[plugin_name]
# 从插件类注册表中移除
if plugin_name in self.plugin_classes:
del self.plugin_classes[plugin_name]
# 从失败列表中移除(如果存在)
if plugin_name in self.failed_plugins:
del self.failed_plugins[plugin_name]
@@ -535,7 +540,7 @@ class PluginManager:
return True
except Exception as e:
logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}")
logger.error(f"❌ 插件卸载失败: {plugin_name} - {str(e)}", exc_info=True)
return False
def reload_plugin(self, plugin_name: str) -> bool:
@@ -548,55 +553,54 @@ class PluginManager:
bool: 重载是否成功
"""
try:
# 先卸载插件
logger.info(f"🔄 开始重载插件: {plugin_name}")
# 卸载插件
if plugin_name in self.loaded_plugins:
self.unload_plugin(plugin_name)
if not self.unload_plugin(plugin_name):
logger.warning(f"⚠️ 插件卸载失败,继续重载: {plugin_name}")
# 清除Python模块缓存
plugin_path = self.plugin_paths.get(plugin_name)
if plugin_path:
plugin_file = os.path.join(plugin_path, "plugin.py")
if os.path.exists(plugin_file):
# 从sys.modules中移除相关模块
modules_to_remove = []
plugin_module_prefix = ".".join(Path(plugin_file).parent.parts)
# 重新扫描插件目录
self.rescan_plugin_directory()
for module_name in sys.modules:
if module_name.startswith(plugin_module_prefix):
modules_to_remove.append(module_name)
for module_name in modules_to_remove:
del sys.modules[module_name]
# 从插件类注册表中移除
if plugin_name in self.plugin_classes:
del self.plugin_classes[plugin_name]
# 重新加载插件模块
if self._load_plugin_module_file(plugin_file):
# 重新加载插件实例
success, _ = self.load_registered_plugin_classes(plugin_name)
if success:
logger.info(f"🔄 插件重载成功: {plugin_name}")
return True
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败")
return False
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 模块加载失败")
return False
# 重新加载插件实例
if plugin_name in self.plugin_classes:
success, _ = self.load_registered_plugin_classes(plugin_name)
if success:
logger.info(f"✅ 插件重载成功: {plugin_name}")
return True
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件文件不存在")
logger.error(f"❌ 插件重载失败: {plugin_name} - 实例化失败")
return False
else:
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件路径未知")
logger.error(f"❌ 插件重载失败: {plugin_name} - 插件类未找到")
return False
except Exception as e:
logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}")
logger.debug("详细错误信息: ", exc_info=True)
logger.error(f"❌ 插件重载失败: {plugin_name} - {str(e)}", exc_info=True)
return False
def force_reload_plugin(self, plugin_name: str) -> bool:
"""强制重载插件(使用简化的方法)
Args:
plugin_name: 插件名称
Returns:
bool: 重载是否成功
"""
return self.reload_plugin(plugin_name)
def clear_all_plugin_caches(self):
"""清理所有插件相关的模块缓存(简化版)"""
try:
logger.info("🧹 清理模块缓存...")
# 清理importlib缓存
importlib.invalidate_caches()
logger.info("🧹 模块缓存清理完成")
except Exception as e:
logger.error(f"❌ 清理模块缓存时发生错误: {e}", exc_info=True)
# 全局插件管理器实例
plugin_manager = PluginManager()