ForcePilot/backend/package/yuxi/channel/plugins/dependency.py
Kris dd1857221c feat(channel/plugins): 新增渠道插件系统完整实现
实现了渠道插件的全生命周期管理能力,包括:
1. 插件清单的加载、解析与存储
2. 依赖校验与拓扑排序安装/卸载
3. 插件发现与安全沙箱执行环境
4. 插件注册中心与目录管理能力
2026-05-21 10:27:58 +08:00

132 lines
4.6 KiB
Python

from __future__ import annotations
import logging
from collections import defaultdict, deque
from typing import Any
from .manifest import ChannelPluginManifest
logger = logging.getLogger(__name__)
class DependencyGraph:
"""渠道插件依赖图 — 检测循环依赖并计算拓扑排序安装顺序。"""
def __init__(self):
self._nodes: dict[str, ChannelPluginManifest] = {}
self._edges: dict[str, set[str]] = defaultdict(set)
self._reverse_edges: dict[str, set[str]] = defaultdict(set)
def add(self, manifest: ChannelPluginManifest) -> None:
self._nodes[manifest.id] = manifest
for dep in manifest.dependencies:
dep_id = dep if isinstance(dep, str) else dep.get("id", "")
if dep_id:
self._edges[manifest.id].add(dep_id)
self._reverse_edges[dep_id].add(manifest.id)
def remove(self, plugin_id: str) -> None:
if plugin_id not in self._nodes:
return
del self._nodes[plugin_id]
for dep_id in list(self._edges.get(plugin_id, [])):
self._reverse_edges[dep_id].discard(plugin_id)
self._edges.pop(plugin_id, None)
for other_id, deps in list(self._edges.items()):
if plugin_id in deps:
deps.discard(plugin_id)
self._reverse_edges[plugin_id].discard(other_id)
def get_dependencies(self, plugin_id: str) -> set[str]:
return set(self._edges.get(plugin_id, []))
def get_dependents(self, plugin_id: str) -> set[str]:
return set(self._reverse_edges.get(plugin_id, []))
def has_cycle(self) -> tuple[bool, list[str]]:
"""检测是否存在循环依赖,返回 (是否有环, 环中的节点列表)。"""
visited: set[str] = set()
rec_stack: set[str] = set()
cycle: list[str] = []
def dfs(node: str, path: list[str]) -> bool:
visited.add(node)
rec_stack.add(node)
path.append(node)
for neighbor in self._edges.get(node, set()):
if neighbor not in visited:
if dfs(neighbor, path):
return True
elif neighbor in rec_stack:
cycle_start = path.index(neighbor)
cycle.extend(path[cycle_start:])
return True
path.pop()
rec_stack.remove(node)
return False
for node in self._nodes:
if node not in visited:
if dfs(node, []):
return True, cycle
return False, []
def topological_sort(self) -> list[str]:
"""返回拓扑排序后的插件 ID 列表。如果存在循环依赖,抛出 ValueError。"""
has_cycle, cycle = self.has_cycle()
if has_cycle:
raise ValueError(f"Circular dependency detected: {' -> '.join(cycle)}")
in_degree = {node: 0 for node in self._nodes}
for node, deps in self._edges.items():
for dep in deps:
if dep in in_degree:
in_degree[node] += 1
queue = deque(node for node, degree in in_degree.items() if degree == 0)
result = []
while queue:
node = queue.popleft()
result.append(node)
for dependent in self._reverse_edges.get(node, set()):
if dependent in in_degree:
in_degree[dependent] -= 1
if in_degree[dependent] == 0:
queue.append(dependent)
if len(result) != len(self._nodes):
raise ValueError("Dependency graph has unresolved dependencies")
return result
def install_order(self) -> list[str]:
"""获取安装顺序(依赖优先)。"""
return self.topological_sort()
def uninstall_order(self) -> list[str]:
"""获取卸载顺序(反向拓扑排序,被依赖者优先)。"""
return list(reversed(self.topological_sort()))
def get_subgraph(self, plugin_id: str) -> "DependencyGraph":
"""获取包含指定插件及其所有依赖的子图。"""
subgraph = DependencyGraph()
visited: set[str] = set()
def visit(node: str) -> None:
if node in visited or node not in self._nodes:
return
visited.add(node)
subgraph.add(self._nodes[node])
for dep in self._edges.get(node, set()):
visit(dep)
visit(plugin_id)
return subgraph
def to_dict(self) -> dict[str, Any]:
return {
"nodes": list(self._nodes.keys()),
"edges": {k: list(v) for k, v in self._edges.items()},
}