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

@@ -29,6 +29,7 @@ import tomlkit
from src.common.logger import get_console_handler, get_logger, initialize_logging
from src.plugin_runtime import (
ENV_BLOCKED_PLUGIN_REASONS,
ENV_EXTERNAL_PLUGIN_IDS,
ENV_HOST_VERSION,
ENV_IPC_ADDRESS,
@@ -196,6 +197,7 @@ class PluginRunner:
session_token: str,
plugin_dirs: List[str],
external_available_plugins: Optional[Dict[str, str]] = None,
blocked_plugin_reasons: Optional[Dict[str, str]] = None,
) -> None:
"""初始化 Runner。
@@ -204,6 +206,7 @@ class PluginRunner:
session_token: 握手用会话令牌。
plugin_dirs: 当前 Runner 负责扫描的插件目录列表。
external_available_plugins: 视为已满足的外部依赖插件版本映射。
blocked_plugin_reasons: 需要拒绝加载的插件及原因映射。
"""
self._host_address: str = host_address
self._session_token: str = session_token
@@ -213,9 +216,15 @@ class PluginRunner:
for plugin_id, plugin_version in (external_available_plugins or {}).items()
if str(plugin_id or "").strip() and str(plugin_version or "").strip()
}
self._blocked_plugin_reasons: Dict[str, str] = {
str(plugin_id or "").strip(): str(reason or "").strip()
for plugin_id, reason in (blocked_plugin_reasons or {}).items()
if str(plugin_id or "").strip() and str(reason or "").strip()
}
self._rpc_client: RPCClient = RPCClient(host_address, session_token)
self._loader: PluginLoader = PluginLoader(host_version=os.getenv(ENV_HOST_VERSION, ""))
self._loader.set_blocked_plugin_reasons(self._blocked_plugin_reasons)
self._start_time: float = time.monotonic()
self._shutting_down: bool = False
self._reload_lock: asyncio.Lock = asyncio.Lock()
@@ -1639,6 +1648,7 @@ class PluginRunner:
async def _async_main() -> None:
"""异步主入口"""
blocked_plugin_reasons_raw = os.environ.get(ENV_BLOCKED_PLUGIN_REASONS, "")
host_address = os.environ.pop(ENV_IPC_ADDRESS, "")
external_plugin_ids_raw = os.environ.get(ENV_EXTERNAL_PLUGIN_IDS, "")
session_token = os.environ.pop(ENV_SESSION_TOKEN, "")
@@ -1658,13 +1668,30 @@ async def _async_main() -> None:
logger.warning("外部依赖插件版本映射格式非法,已回退为空映射")
external_plugin_ids = {}
try:
blocked_plugin_reasons = json.loads(blocked_plugin_reasons_raw) if blocked_plugin_reasons_raw else {}
except json.JSONDecodeError:
logger.warning("解析阻止加载插件原因映射失败,已回退为空映射")
blocked_plugin_reasons = {}
if not isinstance(blocked_plugin_reasons, dict):
logger.warning("阻止加载插件原因映射格式非法,已回退为空映射")
blocked_plugin_reasons = {}
runner_kwargs: Dict[str, Any] = {
"external_available_plugins": {
str(plugin_id): str(plugin_version) for plugin_id, plugin_version in external_plugin_ids.items()
}
}
if blocked_plugin_reasons:
runner_kwargs["blocked_plugin_reasons"] = {
str(plugin_id): str(reason) for plugin_id, reason in blocked_plugin_reasons.items()
}
runner = PluginRunner(
host_address,
session_token,
plugin_dirs,
external_available_plugins={
str(plugin_id): str(plugin_version) for plugin_id, plugin_version in external_plugin_ids.items()
},
**runner_kwargs,
)
# 注册信号处理