feat(agents): Phase 7-10 hook system, plugins, skills, orchestration
Phase 7: Built-in Hooks (audit_log, dangerous_confirmation, security_scan) Phase 8: Plugin system (PluginManager, PluginSandbox, PluginManifest) Phase 9: Skills registry (SkillRegistry, local/plugin/MCP loaders) Phase 10: TeamLeader, RemoteTransport, BackgroundTaskManager
This commit is contained in:
12
backend/app/agents/plugins/__init__.py
Normal file
12
backend/app/agents/plugins/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""插件系统 - Phase 8"""
|
||||
|
||||
from app.agents.plugins.manager import PluginManager, get_plugin_manager
|
||||
from app.agents.plugins.manifest import PluginManifest
|
||||
from app.agents.plugins.sandbox import PluginSandbox
|
||||
|
||||
__all__ = [
|
||||
"PluginManager",
|
||||
"PluginManifest",
|
||||
"PluginSandbox",
|
||||
"get_plugin_manager",
|
||||
]
|
||||
207
backend/app/agents/plugins/manager.py
Normal file
207
backend/app/agents/plugins/manager.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""插件管理器 - Phase 8.2"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from app.agents.plugins.manifest import PluginManifest
|
||||
from app.agents.plugins.sandbox import PluginSandbox
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""插件管理器
|
||||
|
||||
负责插件的安装、卸载、启用、禁用和生命周期管理。
|
||||
"""
|
||||
|
||||
def __init__(self, plugins_dir: str | None = None):
|
||||
"""
|
||||
Args:
|
||||
plugins_dir: 插件目录,None 则使用默认目录
|
||||
"""
|
||||
if plugins_dir is None:
|
||||
plugins_dir = os.path.join(os.path.dirname(__file__), "..", "..", "..", "plugins")
|
||||
self.plugins_dir = plugins_dir
|
||||
self._plugins: dict[str, PluginManifest] = {}
|
||||
self._enabled: dict[str, bool] = {}
|
||||
self._modules: dict[str, Any] = {}
|
||||
self._sandbox = PluginSandbox()
|
||||
|
||||
def install(self, plugin_path: str) -> bool:
|
||||
"""安装插件
|
||||
|
||||
Args:
|
||||
plugin_path: 插件目录路径或 manifest.json 所在目录
|
||||
|
||||
Returns:
|
||||
是否安装成功
|
||||
"""
|
||||
try:
|
||||
manifest_path = os.path.join(plugin_path, "manifest.json")
|
||||
|
||||
if not os.path.exists(manifest_path):
|
||||
return False
|
||||
|
||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||
import json
|
||||
|
||||
data = json.load(f)
|
||||
|
||||
manifest = PluginManifest.from_dict(data)
|
||||
|
||||
# 验证 manifest
|
||||
if not self._validate_manifest(manifest, plugin_path):
|
||||
return False
|
||||
|
||||
# 复制插件到 plugins_dir
|
||||
target_dir = os.path.join(self.plugins_dir, manifest.id)
|
||||
os.makedirs(os.path.dirname(target_dir), exist_ok=True)
|
||||
|
||||
# 保存 manifest
|
||||
with open(os.path.join(target_dir, "manifest.json"), "w", encoding="utf-8") as f:
|
||||
json.dump(manifest.to_dict(), f, indent=2, ensure_ascii=False)
|
||||
|
||||
# 注册插件
|
||||
self._plugins[manifest.id] = manifest
|
||||
self._enabled[manifest.id] = True
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def uninstall(self, plugin_id: str) -> bool:
|
||||
"""卸载插件
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID
|
||||
|
||||
Returns:
|
||||
是否卸载成功
|
||||
"""
|
||||
if plugin_id not in self._plugins:
|
||||
return False
|
||||
|
||||
# 禁用插件
|
||||
self.disable(plugin_id)
|
||||
|
||||
# 移除模块
|
||||
if plugin_id in self._modules:
|
||||
del self._modules[plugin_id]
|
||||
|
||||
# 移除插件
|
||||
del self._plugins[plugin_id]
|
||||
del self._enabled[plugin_id]
|
||||
|
||||
# 删除目录
|
||||
plugin_dir = os.path.join(self.plugins_dir, plugin_id)
|
||||
if os.path.exists(plugin_dir):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(plugin_dir)
|
||||
|
||||
return True
|
||||
|
||||
def enable(self, plugin_id: str) -> bool:
|
||||
"""启用插件
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID
|
||||
|
||||
Returns:
|
||||
是否启用成功
|
||||
"""
|
||||
if plugin_id not in self._plugins:
|
||||
return False
|
||||
|
||||
self._enabled[plugin_id] = True
|
||||
return True
|
||||
|
||||
def disable(self, plugin_id: str) -> bool:
|
||||
"""禁用插件
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID
|
||||
|
||||
Returns:
|
||||
是否禁用成功
|
||||
"""
|
||||
if plugin_id not in self._plugins:
|
||||
return False
|
||||
|
||||
self._enabled[plugin_id] = False
|
||||
return True
|
||||
|
||||
def reload(self, plugin_id: str) -> bool:
|
||||
"""重新加载插件
|
||||
|
||||
Args:
|
||||
plugin_id: 插件 ID
|
||||
|
||||
Returns:
|
||||
是否重新加载成功
|
||||
"""
|
||||
if plugin_id not in self._plugins:
|
||||
return False
|
||||
|
||||
# 卸载模块
|
||||
if plugin_id in self._modules:
|
||||
del self._modules[plugin_id]
|
||||
|
||||
# 重新加载
|
||||
return self._load_plugin_module(plugin_id)
|
||||
|
||||
def list_plugins(self) -> list[PluginManifest]:
|
||||
"""列出所有插件"""
|
||||
return list(self._plugins.values())
|
||||
|
||||
def get_plugin(self, plugin_id: str) -> PluginManifest | None:
|
||||
"""获取插件清单"""
|
||||
return self._plugins.get(plugin_id)
|
||||
|
||||
def is_enabled(self, plugin_id: str) -> bool:
|
||||
"""检查插件是否启用"""
|
||||
return self._enabled.get(plugin_id, False)
|
||||
|
||||
def _validate_manifest(self, manifest: PluginManifest, plugin_path: str) -> bool:
|
||||
"""验证 manifest"""
|
||||
# 检查主入口文件是否存在
|
||||
main_path = os.path.join(plugin_path, manifest.main)
|
||||
if not os.path.exists(main_path):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _load_plugin_module(self, plugin_id: str) -> bool:
|
||||
"""加载插件模块"""
|
||||
plugin_dir = os.path.join(self.plugins_dir, plugin_id)
|
||||
manifest = self._plugins.get(plugin_id)
|
||||
if not manifest:
|
||||
return False
|
||||
|
||||
try:
|
||||
main_path = os.path.join(plugin_dir, manifest.main)
|
||||
spec = importlib.util.spec_from_file_location(plugin_id, main_path)
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[plugin_id] = module
|
||||
spec.loader.exec_module(module)
|
||||
self._modules[plugin_id] = module
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# 全局单例
|
||||
_manager: PluginManager | None = None
|
||||
|
||||
|
||||
def get_plugin_manager() -> PluginManager:
|
||||
"""获取全局插件管理器"""
|
||||
global _manager
|
||||
if _manager is None:
|
||||
_manager = PluginManager()
|
||||
return _manager
|
||||
73
backend/app/agents/plugins/manifest.py
Normal file
73
backend/app/agents/plugins/manifest.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""插件清单定义 - Phase 8.1"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginManifest:
|
||||
"""插件清单
|
||||
|
||||
定义插件的元数据和接口。
|
||||
"""
|
||||
|
||||
id: str # 唯一标识
|
||||
name: str # 显示名称
|
||||
version: str # 版本号
|
||||
description: str # 描述
|
||||
author: str = "" # 作者
|
||||
homepage: str = "" # 主页
|
||||
license: str = "MIT" # 许可证
|
||||
|
||||
# 插件类型
|
||||
plugin_type: str = "tool" # tool, hook, skill, all
|
||||
|
||||
# 入口点
|
||||
main: str = "index.py" # 主入口文件
|
||||
hooks: list[str] = field(default_factory=list) # 提供的 Hook 列表
|
||||
tools: list[str] = field(default_factory=list) # 提供的工具列表
|
||||
skills: list[str] = field(default_factory=list) # 提供的 Skills 列表
|
||||
|
||||
# 依赖
|
||||
dependencies: dict[str, str] = field(default_factory=dict) # pip 依赖
|
||||
peer_dependencies: dict[str, str] = field(default_factory=dict) # 对等依赖
|
||||
|
||||
# 权限要求
|
||||
permissions: list[str] = field(default_factory=list) # 需要的权限
|
||||
allowed_paths: list[str] = field(default_factory=list) # 允许访问的路径
|
||||
denied_paths: list[str] = field(default_factory=list) # 禁止访问的路径
|
||||
|
||||
# 网络权限
|
||||
network_allowed: bool = False # 是否允许网络访问
|
||||
allowed_hosts: list[str] = field(default_factory=list) # 允许访问的 host
|
||||
|
||||
# 配置
|
||||
config_schema: dict[str, Any] = field(default_factory=dict) # 配置 schema
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"version": self.version,
|
||||
"description": self.description,
|
||||
"author": self.author,
|
||||
"homepage": self.homepage,
|
||||
"license": self.license,
|
||||
"plugin_type": self.plugin_type,
|
||||
"main": self.main,
|
||||
"hooks": self.hooks,
|
||||
"tools": self.tools,
|
||||
"skills": self.skills,
|
||||
"dependencies": self.dependencies,
|
||||
"peer_dependencies": self.peer_dependencies,
|
||||
"permissions": self.permissions,
|
||||
"allowed_paths": self.allowed_paths,
|
||||
"denied_paths": self.denied_paths,
|
||||
"network_allowed": self.network_allowed,
|
||||
"allowed_hosts": self.allowed_hosts,
|
||||
"config_schema": self.config_schema,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "PluginManifest":
|
||||
return cls(**data)
|
||||
111
backend/app/agents/plugins/sandbox.py
Normal file
111
backend/app/agents/plugins/sandbox.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""插件沙箱隔离 - Phase 8.3"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
class PluginSandbox:
|
||||
"""插件沙箱
|
||||
|
||||
提供插件执行隔离环境。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._allowed_paths: set[str] = set()
|
||||
self._denied_paths: set[str] = set()
|
||||
self._network_allowed: bool = False
|
||||
self._allowed_hosts: set[str] = set()
|
||||
|
||||
def set_file_permissions(
|
||||
self,
|
||||
allowed_paths: list[str] | None = None,
|
||||
denied_paths: list[str] | None = None,
|
||||
) -> None:
|
||||
"""设置文件访问权限
|
||||
|
||||
Args:
|
||||
allowed_paths: 允许访问的路径列表
|
||||
denied_paths: 禁止访问的路径列表
|
||||
"""
|
||||
self._allowed_paths = set(allowed_paths or [])
|
||||
self._denied_paths = set(denied_paths or [])
|
||||
|
||||
def set_network_permissions(
|
||||
self, allowed: bool, allowed_hosts: list[str] | None = None
|
||||
) -> None:
|
||||
"""设置网络访问权限
|
||||
|
||||
Args:
|
||||
allowed: 是否允许网络访问
|
||||
allowed_hosts: 允许访问的 host 列表
|
||||
"""
|
||||
self._network_allowed = allowed
|
||||
self._allowed_hosts = set(allowed_hosts or [])
|
||||
|
||||
def check_file_access(self, path: str) -> bool:
|
||||
"""检查文件访问权限
|
||||
|
||||
Args:
|
||||
path: 文件路径
|
||||
|
||||
Returns:
|
||||
是否允许访问
|
||||
"""
|
||||
# 如果有允许列表,只允许访问列表中的路径
|
||||
if self._allowed_paths:
|
||||
return path in self._allowed_paths or any(
|
||||
path.startswith(allowed) for allowed in self._allowed_paths
|
||||
)
|
||||
|
||||
# 如果有禁止列表,禁止访问列表中的路径
|
||||
if self._denied_paths:
|
||||
return not any(path.startswith(denied) for denied in self._denied_paths)
|
||||
|
||||
# 没有限制
|
||||
return True
|
||||
|
||||
def check_network_access(self, host: str) -> bool:
|
||||
"""检查网络访问权限
|
||||
|
||||
Args:
|
||||
host: 主机地址
|
||||
|
||||
Returns:
|
||||
是否允许访问
|
||||
"""
|
||||
if not self._network_allowed:
|
||||
return False
|
||||
|
||||
if self._allowed_hosts:
|
||||
return host in self._allowed_hosts or any(
|
||||
host.endswith(allowed) for allowed in self._allowed_hosts
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def execute_in_sandbox(self, func: Any, *args, **kwargs) -> Any:
|
||||
"""在沙箱中执行函数
|
||||
|
||||
Args:
|
||||
func: 要执行的函数
|
||||
*args: 位置参数
|
||||
**kwargs: 关键字参数
|
||||
|
||||
Returns:
|
||||
函数返回值
|
||||
"""
|
||||
# 保存当前状态
|
||||
old_allowed_paths = self._allowed_paths.copy()
|
||||
old_denied_paths = self._denied_paths.copy()
|
||||
old_network_allowed = self._network_allowed
|
||||
old_allowed_hosts = self._allowed_hosts.copy()
|
||||
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
finally:
|
||||
# 恢复状态
|
||||
self._allowed_paths = old_allowed_paths
|
||||
self._denied_paths = old_denied_paths
|
||||
self._network_allowed = old_network_allowed
|
||||
self._allowed_hosts = old_allowed_hosts
|
||||
Reference in New Issue
Block a user