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()}, }