"""插件运行目录、本地仓和联邦产物路径解析。""" from __future__ import annotations import ast from collections.abc import Callable, Mapping from pathlib import Path from typing import Any, Optional from app.runtime.extensions.plugin.system import PluginSystemServices class PluginPathResolver: """把文件事件解析为插件 ID、本地候选和联邦入口状态。""" def __init__( self, *, runtime_root: Path, running: Callable[[], Mapping[str, Any]], system: Callable[[], PluginSystemServices], strict_system_version: Callable[[], bool], log: Any, ) -> None: """保存运行目录和插件市场路径解析端口。""" self._runtime_root = runtime_root.resolve() self._running = running self._system = system self._strict_system_version = strict_system_version self._logger = log def federated_change( self, event_path: Path, ) -> Optional[tuple[str, Optional[dict], bool]]: """识别联邦构建产物变化并确认入口文件已完整生成。""" try: event_path = event_path.resolve() candidate = self.local_candidate(event_path) if candidate: plugin_id = candidate.get("id") plugin_dir = Path(candidate.get("path")).resolve() else: if not event_path.is_relative_to(self._runtime_root): return None relative_parts = event_path.relative_to(self._runtime_root).parts if not relative_parts: return None plugin_dir = self._runtime_root / relative_parts[0] plugin_id = next( ( item for item in self._running() if item.lower() == relative_parts[0].lower() ), None, ) if not plugin_id: return None plugin = self._running().get(plugin_id) if not plugin: return None render_mode, dist_path = plugin.get_render_mode() if render_mode != "vue" or not isinstance(dist_path, str) or not dist_path: return None relative_dist_path = Path(dist_path) if ( relative_dist_path.is_absolute() or ".." in relative_dist_path.parts or "\\" in dist_path ): return None plugin_dir = plugin_dir.resolve() dist_dir = (plugin_dir / relative_dist_path).resolve() if ( dist_dir == plugin_dir or not dist_dir.is_relative_to(plugin_dir) or not event_path.is_relative_to(dist_dir) ): return None remote_entry = dist_dir / "remoteEntry.js" ready = remote_entry.is_file() and remote_entry.resolve().is_relative_to( plugin_dir ) return plugin_id, candidate, ready except Exception as error: self._logger.error(f"识别插件联邦构建产物变化时出错: {error}") return None def runtime_plugin(self, event_path: Path) -> Optional[str]: """从运行目录中的插件 ``__init__.py`` AST 解析插件类名。""" try: event_path = event_path.resolve() if not event_path.is_relative_to(self._runtime_root): return None parts = event_path.relative_to(self._runtime_root).parts if not parts: return None init_file = self._runtime_root / parts[0] / "__init__.py" if not init_file.exists(): return None tree = ast.parse( init_file.read_text(encoding="utf-8", errors="replace") ) for node in ast.walk(tree): if not isinstance(node, ast.ClassDef): continue if any( isinstance(base, ast.Name) and base.id == "_PluginBase" for base in node.bases ): return node.name return None except Exception as error: self._logger.error(f"从路径解析插件 ID 时出错: {error}") return None def local_candidate(self, event_path: Path) -> Optional[dict]: """按 ``plugins``、``plugins.v2``、``plugins.v3`` 目录解析候选。""" try: event_path = event_path.resolve() for repo_path in self._system().local_repo_paths(): if not repo_path.exists() or not repo_path.is_dir(): continue if not event_path.is_relative_to(repo_path): continue parts = event_path.relative_to(repo_path).parts if len(parts) < 2: continue if parts[0] == "plugins": package_version = "" elif parts[0].startswith("plugins."): package_version = parts[0].split(".", 1)[1] else: continue return self._system().local_candidate( parts[1], package_version=package_version, repo_path=repo_path, strict_compat=False, strict_system_version=self._strict_system_version(), ) return None except Exception as error: self._logger.error(f"从本地插件仓路径解析候选时出错: {error}") return None