优化插件热重载管理,支持多个监听目录

This commit is contained in:
minecraft1024a
2025-08-14 13:43:22 +08:00
parent 5f7f68f640
commit ee66fbe827
3 changed files with 152 additions and 56 deletions

View File

@@ -16,6 +16,7 @@ from src.chat.utils.chat_message_builder import (
build_readable_messages_with_id,
get_raw_msg_before_timestamp_with_chat,
)
from src.plugin_system.apis.message_api import filter_mai_messages
from src.chat.utils.utils import get_chat_type_and_target_info
from src.chat.planner_actions.action_manager import ActionManager
from src.chat.message_receive.chat_stream import get_chat_manager
@@ -269,6 +270,9 @@ class ActionPlanner:
timestamp=time.time(),
limit=int(global_config.chat.max_context_size * 0.6),
)
# 过滤掉bot自己的消息避免planner把bot消息当作新消息处理
message_list_before_now = filter_mai_messages(message_list_before_now)
chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_before_now,

View File

@@ -8,7 +8,7 @@ import os
import time
from pathlib import Path
from threading import Thread
from typing import Dict, Set
from typing import Dict, Set, List, Optional, Tuple
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
@@ -31,27 +31,34 @@ class PluginFileHandler(FileSystemEventHandler):
def on_modified(self, event):
"""文件修改事件"""
if not event.is_directory and (event.src_path.endswith('.py') or event.src_path.endswith('.toml')):
self._handle_file_change(event.src_path, "modified")
if not event.is_directory:
file_path = str(event.src_path)
if file_path.endswith(('.py', '.toml')):
self._handle_file_change(file_path, "modified")
def on_created(self, event):
"""文件创建事件"""
if not event.is_directory and (event.src_path.endswith('.py') or event.src_path.endswith('.toml')):
self._handle_file_change(event.src_path, "created")
if not event.is_directory:
file_path = str(event.src_path)
if file_path.endswith(('.py', '.toml')):
self._handle_file_change(file_path, "created")
def on_deleted(self, event):
"""文件删除事件"""
if not event.is_directory and (event.src_path.endswith('.py') or event.src_path.endswith('.toml')):
self._handle_file_change(event.src_path, "deleted")
if not event.is_directory:
file_path = str(event.src_path)
if file_path.endswith(('.py', '.toml')):
self._handle_file_change(file_path, "deleted")
def _handle_file_change(self, file_path: str, change_type: str):
"""处理文件变化"""
try:
# 获取插件名称
plugin_name = self._get_plugin_name_from_path(file_path)
if not plugin_name:
plugin_info = self._get_plugin_info_from_path(file_path)
if not plugin_info:
return
plugin_name, source_type = plugin_info
current_time = time.time()
last_time = self.last_reload_time.get(plugin_name, 0)
@@ -60,18 +67,18 @@ class PluginFileHandler(FileSystemEventHandler):
return
file_name = Path(file_path).name
logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type})")
logger.info(f"📁 检测到插件文件变化: {file_name} ({change_type}) [{source_type}]")
# 如果是删除事件,处理关键文件删除
if change_type == "deleted":
if file_name == "plugin.py":
if plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name}")
logger.info(f"🗑️ 插件主文件被删除,卸载插件: {plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(plugin_name)
return
elif file_name == "manifest.toml":
elif file_name in ("manifest.toml", "_manifest.json"):
if plugin_name in plugin_manager.loaded_plugins:
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name}")
logger.info(f"🗑️ 插件配置文件被删除,卸载插件: {plugin_name} [{source_type}]")
self.hot_reload_manager._unload_plugin(plugin_name)
return
@@ -83,7 +90,7 @@ class PluginFileHandler(FileSystemEventHandler):
# 延迟重载,避免文件正在写入时重载
reload_thread = Thread(
target=self._delayed_reload,
args=(plugin_name,),
args=(plugin_name, source_type),
daemon=True
)
reload_thread.start()
@@ -91,58 +98,84 @@ class PluginFileHandler(FileSystemEventHandler):
except Exception as e:
logger.error(f"❌ 处理文件变化时发生错误: {e}")
def _delayed_reload(self, plugin_name: str):
def _delayed_reload(self, plugin_name: str, source_type: str):
"""延迟重载插件"""
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)
except Exception as e:
logger.error(f"❌ 延迟重载插件 {plugin_name} 时发生错误: {e}")
def _get_plugin_name_from_path(self, file_path: str) -> str:
"""从文件路径获取插件名称"""
def _get_plugin_info_from_path(self, file_path: str) -> Optional[Tuple[str, str]]:
"""从文件路径获取插件信息
Returns:
tuple[插件名称, 源类型] 或 None
"""
try:
path = Path(file_path)
# 检查是否在监听的插件目录中
plugin_root = Path(self.hot_reload_manager.watch_directory)
if not path.is_relative_to(plugin_root):
return ""
# 检查是否在任何一个监听的插件目录中
for watch_dir in self.hot_reload_manager.watch_directories:
plugin_root = Path(watch_dir)
if path.is_relative_to(plugin_root):
# 确定源类型
if "src" in str(plugin_root):
source_type = "built-in"
else:
source_type = "external"
# 获取插件目录名(插件名)
relative_path = path.relative_to(plugin_root)
if len(relative_path.parts) == 0:
continue
plugin_name = relative_path.parts[0]
# 获取插件目录名(插件名)
relative_path = path.relative_to(plugin_root)
plugin_name = relative_path.parts[0]
# 确认这是一个有效的插件目录
plugin_dir = plugin_root / plugin_name
if plugin_dir.is_dir():
# 检查是否有插件主文件或配置文件
has_plugin_py = (plugin_dir / "plugin.py").exists()
has_manifest = ((plugin_dir / "manifest.toml").exists() or
(plugin_dir / "_manifest.json").exists())
if has_plugin_py or has_manifest:
return plugin_name, source_type
# 确认这是一个有效的插件目录(检查是否有 plugin.py 或 manifest.toml
plugin_dir = plugin_root / plugin_name
if plugin_dir.is_dir() and ((plugin_dir / "plugin.py").exists() or (plugin_dir / "manifest.toml").exists()):
return plugin_name
return ""
return None
except Exception:
return ""
return None
class PluginHotReloadManager:
"""插件热重载管理器"""
def __init__(self, watch_directory: str = None):
print("fuck")
print(os.getcwd())
self.watch_directory = os.path.join(os.getcwd(), "plugins")
self.observer = None
self.file_handler = None
def __init__(self, watch_directories: Optional[List[str]] = None):
if watch_directories is None:
# 默认监听两个目录:根目录下的 plugins 和 src 下的插件目录
self.watch_directories = [
os.path.join(os.getcwd(), "plugins"), # 外部插件目录
os.path.join(os.getcwd(), "src", "plugins", "built_in") # 内置插件目录
]
else:
self.watch_directories = watch_directories
self.observers = []
self.file_handlers = []
self.is_running = False
# 确保监听目录存在
if not os.path.exists(self.watch_directory):
os.makedirs(self.watch_directory, exist_ok=True)
logger.info(f"创建插件监听目录: {self.watch_directory}")
for watch_dir in self.watch_directories:
if not os.path.exists(watch_dir):
os.makedirs(watch_dir, exist_ok=True)
logger.info(f"📁 创建插件监听目录: {watch_dir}")
def start(self):
"""启动热重载监听"""
@@ -151,34 +184,57 @@ class PluginHotReloadManager:
return
try:
self.observer = Observer()
self.file_handler = PluginFileHandler(self)
# 为每个监听目录创建独立的观察者
for watch_dir in self.watch_directories:
observer = Observer()
file_handler = PluginFileHandler(self)
observer.schedule(
file_handler,
watch_dir,
recursive=True
)
observer.start()
self.observers.append(observer)
self.file_handlers.append(file_handler)
self.observer.schedule(
self.file_handler,
self.watch_directory,
recursive=True
)
self.observer.start()
self.is_running = True
logger.info("🚀 插件热重载已启动,监听目录: plugins")
# 打印监听目录信息
dir_info = []
for watch_dir in self.watch_directories:
if "src" in watch_dir:
dir_info.append(f"{watch_dir} (内置插件)")
else:
dir_info.append(f"{watch_dir} (外部插件)")
logger.info(f"🚀 插件热重载已启动,监听目录:")
for info in dir_info:
logger.info(f" 📂 {info}")
except Exception as e:
logger.error(f"❌ 启动插件热重载失败: {e}")
self.stop() # 清理已创建的观察者
self.is_running = False
def stop(self):
"""停止热重载监听"""
if not self.is_running:
if not self.is_running and not self.observers:
return
if self.observer:
self.observer.stop()
self.observer.join()
# 停止所有观察者
for observer in self.observers:
try:
observer.stop()
observer.join()
except Exception as e:
logger.error(f"❌ 停止观察者时发生错误: {e}")
self.observers.clear()
self.file_handlers.clear()
self.is_running = False
logger.info("🛑 插件热重载已停止")
def _reload_plugin(self, plugin_name: str):
"""重载指定插件"""
@@ -228,11 +284,47 @@ class PluginHotReloadManager:
except Exception as e:
logger.error(f"❌ 重载所有插件时发生错误: {e}")
def add_watch_directory(self, directory: str):
"""添加新的监听目录"""
if directory in self.watch_directories:
logger.info(f"目录 {directory} 已在监听列表中")
return
# 确保目录存在
if not os.path.exists(directory):
os.makedirs(directory, exist_ok=True)
logger.info(f"📁 创建插件监听目录: {directory}")
self.watch_directories.append(directory)
# 如果热重载正在运行,为新目录创建观察者
if self.is_running:
try:
observer = Observer()
file_handler = PluginFileHandler(self)
observer.schedule(
file_handler,
directory,
recursive=True
)
observer.start()
self.observers.append(observer)
self.file_handlers.append(file_handler)
logger.info(f"📂 已添加新的监听目录: {directory}")
except Exception as e:
logger.error(f"❌ 添加监听目录 {directory} 失败: {e}")
self.watch_directories.remove(directory)
def get_status(self) -> dict:
"""获取热重载状态"""
return {
"is_running": self.is_running,
"watch_directory": self.watch_directory,
"watch_directories": self.watch_directories,
"active_observers": len(self.observers),
"loaded_plugins": len(plugin_manager.loaded_plugins),
"failed_plugins": len(plugin_manager.failed_plugins),
}

View File

@@ -86,7 +86,7 @@ class ScheduleManager:
logger.debug("当前时间没有日程活动")
# 每5分钟检查一次避免频繁检查
await asyncio.sleep(60)
await asyncio.sleep(300)
except asyncio.CancelledError:
logger.info("定时任务循环被取消")