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:
|
permissions:
|
||||||
- framework.scaffold.plugin
|
- framework.scaffold.plugin
|
||||||
source: internal
|
source: internal
|
||||||
echo: &id001
|
|
||||||
description: echo input
|
|
||||||
permissions: []
|
|
||||||
source: plugin.example_plugin
|
|
||||||
help:
|
help:
|
||||||
description: 显示帮助信息
|
description: 显示帮助信息
|
||||||
permissions:
|
permissions:
|
||||||
@@ -33,10 +29,6 @@ commands:
|
|||||||
permissions:
|
permissions:
|
||||||
- framework.permission.read
|
- framework.permission.read
|
||||||
source: internal
|
source: internal
|
||||||
plugin_status: &id002
|
|
||||||
description: show status
|
|
||||||
permissions: []
|
|
||||||
source: plugin.example_plugin
|
|
||||||
pm_plugin_status:
|
pm_plugin_status:
|
||||||
description: '权限管理: 查看插件权限状态'
|
description: '权限管理: 查看插件权限状态'
|
||||||
permissions:
|
permissions:
|
||||||
@@ -92,9 +84,6 @@ commands:
|
|||||||
permissions:
|
permissions:
|
||||||
- framework.command.test
|
- framework.command.test
|
||||||
source: internal
|
source: internal
|
||||||
last_updated: 316707.28385033
|
last_updated: 1742.472300949
|
||||||
plugin_commands:
|
plugin_commands: {}
|
||||||
example_plugin:
|
total_commands: 17
|
||||||
echo: *id001
|
|
||||||
plugin_status: *id002
|
|
||||||
total_commands: 19
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
import inspect
|
import inspect
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Any, Optional, Callable
|
from typing import Dict, List, Any, Optional, Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -59,6 +61,11 @@ class PluginService:
|
|||||||
|
|
||||||
await self.save_command_config()
|
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
|
self.is_running = True
|
||||||
logger.info("插件服务启动完成")
|
logger.info("插件服务启动完成")
|
||||||
|
|
||||||
@@ -66,6 +73,125 @@ class PluginService:
|
|||||||
logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True)
|
logger.error(f"启动插件服务时出错: {str(e)}", exc_info=True)
|
||||||
raise
|
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):
|
async def load_all_plugins(self):
|
||||||
"""加载所有插件"""
|
"""加载所有插件"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user