diff --git a/config/plugins/commands.yaml b/config/plugins/commands.yaml index af90530..fb274fb 100644 --- a/config/plugins/commands.yaml +++ b/config/plugins/commands.yaml @@ -9,10 +9,6 @@ commands: permissions: - framework.scaffold.plugin source: internal - echo: &id001 - description: echo input - permissions: [] - source: plugin.example_plugin help: description: 显示帮助信息 permissions: @@ -33,10 +29,6 @@ commands: permissions: - framework.permission.read source: internal - plugin_status: &id002 - description: show status - permissions: [] - source: plugin.example_plugin pm_plugin_status: description: '权限管理: 查看插件权限状态' permissions: @@ -92,9 +84,6 @@ commands: permissions: - framework.command.test source: internal -last_updated: 316707.28385033 -plugin_commands: - example_plugin: - echo: *id001 - plugin_status: *id002 -total_commands: 19 +last_updated: 1742.472300949 +plugin_commands: {} +total_commands: 17 diff --git a/services/plugin_service.py b/services/plugin_service.py index e6145d5..038c15c 100644 --- a/services/plugin_service.py +++ b/services/plugin_service.py @@ -5,7 +5,9 @@ import logging import asyncio import importlib.util import sys +import os import inspect +import time from pathlib import Path from typing import Dict, List, Any, Optional, Callable from dataclasses import dataclass @@ -49,22 +51,146 @@ class PluginService: """启动插件服务""" try: logger.info("启动插件服务") - + # 创建插件目录 self.plugins_dir.mkdir(exist_ok=True) - + # 自动加载插件 if self.config['plugins']['auto_load']: await self.load_all_plugins() - + await self.save_command_config() - + + # 启动 watchdog 热重载 + if self.config.get('plugins', {}).get('hot_reload', False): + self._loop = asyncio.get_running_loop() + self._start_watchdog() + self.is_running = True logger.info("插件服务启动完成") - + except Exception as e: logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True) raise + + # ── watchdog 热重载 ────────────────────────────────────── + + def _start_watchdog(self): + """启动 watchdog 监听 plugins/ 目录实现热重载""" + try: + from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler + + plugin_service = self # 闭包引用 + + class _PluginReloadHandler(FileSystemEventHandler): + """防抖 + 插件级重载""" + + def __init__(self): + self._debounce: Dict[str, float] = {} + self._debounce_sec = 1.0 # 1 秒内同插件只触发一次 + + def _plugin_name_from_path(self, event_path: str) -> Optional[str]: + """从事件路径提取插件名(plugins//...)""" + try: + rel = os.path.relpath(event_path, str(plugin_service.plugins_dir)) + parts = Path(rel).parts + if parts and not parts[0].startswith('.'): + return parts[0] + except ValueError: + pass + return None + + def _should_handle(self, plugin_name: str) -> bool: + """防抖 — 同插件在冷却时间内跳过""" + now = time.time() + last = self._debounce.get(plugin_name, 0) + if now - last < self._debounce_sec: + return False + self._debounce[plugin_name] = now + return True + + def on_modified(self, event): + if event.is_directory: + return + path = event.src_path + if not path.endswith(('.py', '.yaml', '.yml')): + return + plugin_name = self._plugin_name_from_path(path) + if not plugin_name: + return + if not self._should_handle(plugin_name): + return + logger.info( + f"🔁 检测到插件文件变更: {plugin_name} ({os.path.basename(path)})" + ) + asyncio.run_coroutine_threadsafe( + plugin_service._reload_plugin(plugin_name), + plugin_service._loop, + ) + + def on_created(self, event): + self.on_modified(event) + + self._watchdog_observer = Observer() + self._watchdog_observer.schedule( + _PluginReloadHandler(), + str(self.plugins_dir), + recursive=True, + ) + self._watchdog_observer.start() + logger.info("👁️ 插件热重载已启动 (watchdog)") + + except ImportError: + logger.warning( + "watchdog 未安装,插件热重载不可用。pip install watchdog" + ) + except Exception as e: + logger.warning(f"启动 watchdog 失败 (不影响框架): {e}") + + async def _reload_plugin(self, plugin_name: str): + """热重载单个插件 — 卸载后重新加载""" + plugin_dir = self.plugins_dir / plugin_name + if not plugin_dir.is_dir(): + logger.debug(f"插件目录已消失,跳过重载: {plugin_name}") + return + + if plugin_name in self.plugins: + logger.info(f" ⏳ 卸载旧版本: {plugin_name}") + await self.unload_plugin(plugin_name) + + await asyncio.sleep(0.2) # 给文件系统缓冲 + + success = await self.load_plugin(plugin_name) + if success: + # 重载后重新注册延迟路由 + try: + internet = self.service_manager.get_service("internet") + if internet and internet.is_running: + plugin = self.plugins.get(plugin_name) + if plugin and hasattr(plugin, 'network_bridge'): + await self._reinitialize_plugin_network(plugin, internet) + except Exception: + pass + logger.info(f" ✅ 热重载完成: {plugin_name}") + else: + logger.warning(f" ❌ 热重载失败: {plugin_name}") + + def _stop_watchdog(self): + """停止 watchdog observer""" + obs = getattr(self, '_watchdog_observer', None) + if obs and obs.is_alive(): + obs.stop() + obs.join(timeout=3) + logger.info("👁️ 插件热重载已停止") + + async def stop(self): + """停止插件服务""" + logger.info("关闭插件服务") + self._stop_watchdog() + for name in list(self.plugins.keys()): + await self.unload_plugin(name) + self.is_running = False async def load_all_plugins(self): """加载所有插件"""