refactor: 收口 V3 分层架构与插件兼容边界

This commit is contained in:
jxxghp
2026-08-18 13:22:02 +08:00
parent cca99bd421
commit 8472bcff43
274 changed files with 10730 additions and 6130 deletions
+220
View File
@@ -0,0 +1,220 @@
"""插件运行目录与本地仓库的文件变化监控。"""
from __future__ import annotations
import time
import threading
from collections.abc import Callable
from pathlib import Path
from typing import Any, Optional
FederatedChangeResolver = Callable[[Path], Optional[tuple[str, Optional[dict], bool]]]
RuntimePluginResolver = Callable[[Path], Optional[str]]
LocalCandidateResolver = Callable[[Path], Optional[dict]]
LocalPluginSync = Callable[[str, Optional[dict]], bool]
PluginReloader = Callable[[str], Any]
WatchFunction = Callable[..., Any]
class PluginMonitorController:
"""独立管理插件文件监控线程的启动、停止和重建。"""
def __init__(self, *, runner: Callable[[], None], log: Any) -> None:
"""保存监控循环入口和日志端口,线程状态仅由本组件持有。"""
self._runner = runner
self._logger = log
self._thread: Optional[threading.Thread] = None
self._stop_event = threading.Event()
@property
def stop_event(self) -> threading.Event:
"""返回供 watchfiles 监听的停止事件。"""
return self._stop_event
def reload(self, enabled: bool) -> None:
"""按当前配置停止旧线程,并在启用时创建新线程。"""
self.stop()
if enabled:
self.start()
def start(self) -> None:
"""启动唯一的守护监控线程。"""
if self._thread and self._thread.is_alive():
self._logger.info("插件文件修改监测已经在运行中...")
return
self._logger.info("开始监测插件文件修改...")
self._stop_event.clear()
self._thread = threading.Thread(target=self._runner, daemon=True)
self._thread.start()
def stop(self) -> None:
"""请求监控线程退出,并在限定时间内等待其清理。"""
if not self._thread or not self._thread.is_alive():
self._logger.info("未启用插件文件修改监测,无需停止")
return
self._logger.info("正在停止插件文件修改监测...")
self._stop_event.set()
self._thread.join(timeout=5)
if self._thread.is_alive():
self._logger.warning("插件文件修改监测线程在5秒内未能正常停止。")
self._thread = None
self._logger.info("插件文件修改监测停止完成")
class PluginChangeMonitor:
"""把文件变化归并为本地同步和运行态重载动作。"""
def __init__(
self,
*,
runtime_root: Path,
local_roots: Callable[[], list[Path]],
stop_event: Any,
recent_sync: dict[str, float],
federated_change: FederatedChangeResolver,
runtime_plugin: RuntimePluginResolver,
local_candidate: LocalCandidateResolver,
sync_local: LocalPluginSync,
reload_plugin: PluginReloader,
watch: WatchFunction,
log: Any,
) -> None:
"""保存监控路径、变化解析器和副作用回调。"""
self._runtime_root = runtime_root
self._local_roots = local_roots
self._stop_event = stop_event
self._recent_sync = recent_sync
self._federated_change = federated_change
self._runtime_plugin = runtime_plugin
self._local_candidate = local_candidate
self._sync_local = sync_local
self._reload_plugin = reload_plugin
self._watch = watch
self._logger = log
def run(self) -> None:
"""运行 watchfiles 主循环并按批次同步、重载插件。"""
plugin_paths = [str(self._runtime_root)]
plugin_paths.extend(
str(path)
for path in self._local_roots()
if path.exists() and path.is_dir()
)
self._logger.info(">>> 监控线程已启动,准备进入watch循环...")
for changes in self._watch(
*plugin_paths,
stop_event=self._stop_event,
rust_timeout=1000,
yield_on_timeout=True,
):
if not changes:
continue
self._process_changes(changes)
def _process_changes(self, changes: Any) -> None:
"""把一批文件事件归并为最多一次同步和一次重载。"""
plugins_to_reload = set()
local_plugins_to_sync = {}
for _change_type, path_str in changes:
event_path = Path(path_str)
if "__pycache__" in event_path.parts:
continue
if event_path.name == "requirements.txt":
self._handle_requirements_change(event_path)
continue
federated_change = self._federated_change(event_path)
if federated_change:
plugin_id, candidate, remote_entry_ready = federated_change
if candidate and remote_entry_ready:
if candidate.get("compatible") is False:
self._logger.info(
f"检测到本地插件 {plugin_id} 联邦构建产物变化,"
f"但跳过同步:{candidate.get('skip_reason')}"
)
elif plugin_id not in local_plugins_to_sync:
local_plugins_to_sync[plugin_id] = (
candidate,
event_path,
False,
)
continue
if event_path.suffix != ".py":
continue
runtime_plugin_id = self._runtime_plugin(event_path)
candidate = (
self._local_candidate(event_path)
if not runtime_plugin_id
else None
)
if runtime_plugin_id:
last_sync_time = self._recent_sync.get(runtime_plugin_id)
if last_sync_time and time.time() - last_sync_time < 2:
continue
plugins_to_reload.add(runtime_plugin_id)
elif candidate:
if candidate.get("compatible") is False:
package_version = candidate.get("package_version")
source_root = (
f"plugins.{package_version}"
if package_version
else "plugins"
)
self._logger.info(
f"检测到本地插件 {candidate.get('id')} 文件变化,"
f"来源:{source_root},文件:{event_path}"
f"但跳过同步:{candidate.get('skip_reason')}"
)
continue
local_plugins_to_sync[candidate.get("id")] = (
candidate,
event_path,
True,
)
for plugin_id, (candidate, event_path, should_reload) in (
local_plugins_to_sync.items()
):
package_version = candidate.get("package_version")
source_root = (
f"plugins.{package_version}" if package_version else "plugins"
)
change_name = "Python 文件" if should_reload else "联邦构建产物"
self._logger.info(
f"检测到本地插件 {plugin_id} {change_name}变化,"
f"来源:{source_root},文件:{event_path}"
)
if self._sync_local(plugin_id, candidate) and should_reload:
plugins_to_reload.add(plugin_id)
if not plugins_to_reload:
return
self._logger.info(
f"检测到插件文件变化,准备重载: {list(plugins_to_reload)}"
)
for plugin_id in plugins_to_reload:
try:
self._reload_plugin(plugin_id)
except Exception as err:
self._logger.error(
f"插件 {plugin_id} 热重载失败: {err}",
exc_info=True,
)
def _handle_requirements_change(self, event_path: Path) -> None:
"""记录依赖文件变化,但不在监控线程中隐式安装依赖。"""
candidate = self._local_candidate(event_path)
if not candidate:
return
if candidate.get("compatible") is False:
self._logger.info(
f"检测到本地插件 {candidate.get('id')} 依赖文件变化,"
f"但跳过处理:{candidate.get('skip_reason')}"
)
return
self._logger.warning(
f"检测到本地插件 {candidate.get('id')} 依赖文件变化,"
"请重新安装本地插件以安装依赖"
)