feat: add plugin dependency pipeline and HTML rendering service

- Implemented a new dependency pipeline for plugins to manage Python package dependencies, including conflict detection and automatic installation of missing dependencies.
- Introduced an HTML rendering service that utilizes existing browsers to render HTML content as PNG images, with support for various configurations and error handling.
This commit is contained in:
DrSmoothl
2026-04-03 01:48:23 +08:00
parent fbc2fba6ff
commit 4ec06ece56
17 changed files with 2585 additions and 93 deletions

View File

@@ -3,6 +3,8 @@
验证协议层、传输层、RPC 通信链路的正确性。
"""
# pyright: reportArgumentType=false, reportAttributeAccessIssue=false, reportCallIssue=false, reportIndexIssue=false, reportMissingImports=false, reportOptionalMemberAccess=false
from pathlib import Path
from types import SimpleNamespace
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence
@@ -2891,7 +2893,7 @@ class TestIntegration:
assert instances[0].stopped is True
@pytest.mark.asyncio
async def test_handle_plugin_source_changes_only_reload_matching_supervisor(self, monkeypatch, tmp_path):
async def test_handle_plugin_source_changes_restarts_supervisors_after_dependency_sync(self, monkeypatch, tmp_path):
from src.config.file_watcher import FileChange
from src.plugin_runtime import integration as integration_module
import json
@@ -2915,7 +2917,6 @@ class TestIntegration:
def __init__(self, plugin_dirs, registered_plugins):
self._plugin_dirs = plugin_dirs
self._registered_plugins = registered_plugins
self.reload_reasons = []
self.config_updates = []
def get_loaded_plugin_ids(self):
@@ -2924,9 +2925,6 @@ class TestIntegration:
def get_loaded_plugin_versions(self):
return {plugin_id: "1.0.0" for plugin_id in self._registered_plugins}
async def reload_plugins(self, plugin_ids=None, reason="manual", external_available_plugins=None):
self.reload_reasons.append((plugin_ids, reason, external_available_plugins or {}))
async def notify_plugin_config_updated(self, plugin_id, config_data, config_version=""):
self.config_updates.append((plugin_id, config_data, config_version))
return True
@@ -2935,27 +2933,37 @@ class TestIntegration:
manager._started = True
manager._builtin_supervisor = FakeSupervisor([builtin_root], {"test.alpha": object()})
manager._third_party_supervisor = FakeSupervisor([thirdparty_root], {"test.beta": object()})
dependency_sync_calls = []
restart_calls = []
async def fake_sync(plugin_dirs: Sequence[Path]) -> Any:
"""记录依赖同步调用。"""
dependency_sync_calls.append(list(plugin_dirs))
return integration_module.DependencySyncState(
blocked_changed_plugin_ids={"test.beta"},
environment_changed=False,
)
async def fake_restart(reason: str) -> bool:
"""记录 Supervisor 重启调用。"""
restart_calls.append(reason)
return True
monkeypatch.setattr(manager, "_sync_plugin_dependencies", fake_sync)
monkeypatch.setattr(manager, "_restart_supervisors", fake_restart)
changes = [
FileChange(change_type=1, path=beta_dir / "plugin.py"),
]
refresh_calls = []
def fake_refresh() -> None:
refresh_calls.append(True)
manager._refresh_plugin_config_watch_subscriptions = fake_refresh
await manager._handle_plugin_source_changes(changes)
assert manager._builtin_supervisor.reload_reasons == []
assert manager._third_party_supervisor.reload_reasons == [
(["test.beta"], "file_watcher", {"test.alpha": "1.0.0"})
]
assert dependency_sync_calls == [[builtin_root, thirdparty_root]]
assert restart_calls == ["file_watcher_blocklist_changed"]
assert manager._builtin_supervisor.config_updates == []
assert manager._third_party_supervisor.config_updates == []
assert refresh_calls == [True]
@pytest.mark.asyncio
async def test_reload_plugins_globally_warns_and_skips_cross_supervisor_dependents(self, monkeypatch):