feat(v0.2.2): 插件热重载 — watchdog 监听 plugins/ 自动重载
- PluginService.start() 中启动 watchdog Observer - 监听 plugins/ 下 .py/.yaml/.yml 文件变更 - 1秒防抖,避免重复触发 - 变更后自动卸载→重新加载对应插件 - 插件重载后自动重连网络路由 - PluginService.stop() 清理 observer - 配置项 hot_reload: true/false 控制 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+131
-5
@@ -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/<name>/...)"""
|
||||
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):
|
||||
"""加载所有插件"""
|
||||
|
||||
Reference in New Issue
Block a user