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

This commit is contained in:
minecraft1024a
2025-08-14 13:43:22 +08:00
committed by Windpicker-owo
parent ebf6fc5c20
commit bf9e1e60c4
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, build_readable_messages_with_id,
get_raw_msg_before_timestamp_with_chat, 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.utils.utils import get_chat_type_and_target_info
from src.chat.planner_actions.action_manager import ActionManager from src.chat.planner_actions.action_manager import ActionManager
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
@@ -292,6 +293,9 @@ class ActionPlanner:
limit=int(global_config.chat.max_context_size * 0.6), 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( chat_content_block, message_id_list = build_readable_messages_with_id(
messages=message_list_before_now, messages=message_list_before_now,
timestamp_mode="normal", timestamp_mode="normal",

View File

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

View File

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