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:
2026-04-04 22:56:27 +08:00
parent e5bd492d74
commit a3fe4d24fc
35 changed files with 8501 additions and 0 deletions

View 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",
]

View 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

View 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)

View 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