feat(v0.5.1): 插件进程隔离 — 集成到 PluginService

- PluginService._should_isolate() 检查全局配置 + 插件级配置
- _load_plugin_isolated() 使用 IsolatedPlugin (multiprocessing.Process)
- unload_plugin() 兼容隔离/非隔离两种实例
- 配置: plugins.isolation (全局默认) + settings.isolation (插件级)
- 插件崩溃不影响框架主进程

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-13 11:07:30 +08:00
parent 37d1110f2b
commit 7ce495de2a
2 changed files with 97 additions and 21 deletions
+2 -1
View File
@@ -218,7 +218,8 @@ class InitService:
'plugins': {
'auto_load': True,
'hot_reload': True,
'max_retry_count': 3
'max_retry_count': 3,
'isolation': False, # 默认不隔离,插件可在 settings.isolation 中声明
},
'auto_start_scripts': {
'enabled': True,
+95 -20
View File
@@ -192,36 +192,105 @@ class PluginService:
await self.unload_plugin(name)
self.is_running = False
def _should_isolate(self, plugin_config: dict) -> bool:
"""检查插件是否应使用进程隔离模式"""
global_isolation = self.config.get('plugins', {}).get('isolation', False)
plugin_isolation = plugin_config.get('settings', {}).get('isolation', None)
if plugin_isolation is not None:
return bool(plugin_isolation)
return global_isolation
async def load_all_plugins(self):
"""加载所有插件"""
try:
logger.debug("开始加载所有插件")
if not self.plugins_dir.exists():
logger.warning("插件目录不存在,跳过加载")
return
loaded_count = 0
error_count = 0
isolated_count = 0
# 遍历插件目录
for plugin_dir in self.plugins_dir.iterdir():
if plugin_dir.is_dir():
try:
success = await self.load_plugin(plugin_dir.name)
if success:
loaded_count += 1
# 先读配置判断是否需要隔离
config_file = plugin_dir / "config.yaml"
plugin_config = {}
if config_file.exists():
with open(config_file, 'r') as f:
plugin_config = yaml.safe_load(f) or {}
if self._should_isolate(plugin_config):
success = await self._load_plugin_isolated(plugin_dir.name)
if success:
isolated_count += 1
loaded_count += 1
else:
error_count += 1
else:
error_count += 1
success = await self.load_plugin(plugin_dir.name)
if success:
loaded_count += 1
else:
error_count += 1
except Exception as e:
logger.error(f"加载插件 {plugin_dir.name} 时出错: {str(e)}", exc_info=True)
error_count += 1
logger.info(f"插件加载完成: 成功 {loaded_count}, 失败 {error_count}")
logger.info(
f"插件加载完成: 成功 {loaded_count} (其中隔离 {isolated_count}), 失败 {error_count}"
)
except Exception as e:
logger.error(f"加载所有插件时出错: {str(e)}", exc_info=True)
raise
async def _load_plugin_isolated(self, plugin_name: str) -> bool:
"""在独立子进程中加载插件(进程隔离模式)"""
try:
from services.process_isolated import IsolatedPlugin
plugin_path = self.plugins_dir / plugin_name
main_module = plugin_path / "__init__.py"
if not main_module.exists():
logger.error(f"隔离插件主模块不存在: {main_module}")
return False
config_file = plugin_path / "config.yaml"
plugin_config = {}
if config_file.exists():
with open(config_file) as f:
plugin_config = yaml.safe_load(f) or {}
iso = IsolatedPlugin(
plugin_name,
str(main_module),
plugin_config,
)
self.plugins[plugin_name] = iso
self.plugin_info[plugin_name] = PluginInfo(
name=plugin_config.get('name', plugin_name),
version=plugin_config.get('version', '0.1.0'),
description=plugin_config.get('description', ''),
author=plugin_config.get('author', ''),
enabled=True,
loaded=True,
error_count=0,
permissions=[],
plugin_path=plugin_path,
commands={},
)
logger.info(f"🔒 隔离插件加载成功: {plugin_name} (PID={iso.pid})")
return True
except Exception as e:
logger.error(f"加载隔离插件 {plugin_name} 失败: {e}")
return False
async def load_plugin(self, plugin_name: str) -> bool:
"""加载单个插件 - 支持异步权限处理"""
@@ -454,17 +523,18 @@ class PluginService:
"""卸载插件"""
try:
logger.debug(f"开始卸载插件: {plugin_name}")
if plugin_name not in self.plugins:
logger.warning(f"插件未加载: {plugin_name}")
return False
plugin_instance = self.plugins[plugin_name]
plugin_info = self.plugin_info[plugin_name]
# 注销插件命令
await self._unregister_plugin_commands(plugin_name)
# 注销插件命令 (隔离插件可能没有注册命令)
if not self._is_isolated(plugin_instance):
await self._unregister_plugin_commands(plugin_name)
# 调用插件的清理方法
try:
if hasattr(plugin_instance, 'shutdown'):
@@ -474,23 +544,28 @@ class PluginService:
plugin_instance.shutdown()
except Exception as e:
logger.error(f"插件清理时出错 {plugin_name}: {str(e)}", exc_info=True)
# 从模块缓存中移除
module_name = f"plugins.{plugin_name}"
if module_name in sys.modules:
del sys.modules[module_name]
# 移除插件实例和信息
del self.plugins[plugin_name]
plugin_info.loaded = False
plugin_info.enabled = False
logger.info(f"插件卸载成功: {plugin_name}")
return True
except Exception as e:
logger.error(f"卸载插件 {plugin_name} 时出错: {str(e)}", exc_info=True)
return False
@staticmethod
def _is_isolated(plugin_instance) -> bool:
from services.process_isolated import IsolatedPlugin
return isinstance(plugin_instance, IsolatedPlugin)
async def _unregister_plugin_commands(self, plugin_name: str):
"""注销插件命令"""