diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index ada7cb063..521703fdb 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -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, diff --git a/src/plugin_system/core/plugin_hot_reload.py b/src/plugin_system/core/plugin_hot_reload.py index b28634a7b..8d33b1f80 100644 --- a/src/plugin_system/core/plugin_hot_reload.py +++ b/src/plugin_system/core/plugin_hot_reload.py @@ -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), } diff --git a/src/plugins/built_in/maizone/scheduler.py b/src/plugins/built_in/maizone/scheduler.py index 98f0182de..b2c65a028 100644 --- a/src/plugins/built_in/maizone/scheduler.py +++ b/src/plugins/built_in/maizone/scheduler.py @@ -86,7 +86,7 @@ class ScheduleManager: logger.debug("当前时间没有日程活动") # 每5分钟检查一次,避免频繁检查 - await asyncio.sleep(60) + await asyncio.sleep(300) except asyncio.CancelledError: logger.info("定时任务循环被取消")