Files
MoviePilot/app/runtime/extensions/plugin/manager.py

1529 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import concurrent.futures
import inspect
import posixpath
import threading
import time
from contextlib import contextmanager
from datetime import datetime
from pathlib import Path
from typing import (
Any,
Callable,
ContextManager,
Dict,
List,
Optional,
Tuple,
Type,
Union,
cast,
)
from watchfiles import watch
from app.foundation.environment import is_free_threaded_runtime, is_gil_enabled
from app.foundation.singleton import Singleton
from app.runtime.events import EventHandlerBinding, eventmanager
from app.runtime.execution import run_in_threadpool_to_completion
from app.runtime.extensions.plugin.access import PluginAccessPolicy
from app.runtime.extensions.plugin.datadir import remove_plugin_data_directory
from app.runtime.extensions.plugin.dependency import (
PluginDependencyClassification,
PluginDependencyInstallResult,
)
from app.runtime.extensions.plugin.metadata import PluginMetadataMapper
from app.runtime.extensions.plugin.monitor import PluginChangeMonitor
from app.runtime.extensions.plugin.projection import PluginProjection
from app.runtime.extensions.plugin.runtime import PluginRuntime
from app.runtime.extensions.plugin.tools import PluginToolCatalog
from app.runtime.log import clear_plugin_instance_log_level as clear_instance_log_level_override
from app.runtime.log import logger
from app.runtime.observability import observe_compat_facade
from app.runtime.reload import ConfigReloadMixin
from app.runtime.settings import get_runtime_setting
from app.runtime.thread import ThreadHelper
from app.schemas.category import ClassificationFactValue, ClassificationFieldDefinition
from app.schemas.exception import PluginMutationRejectedError
from app.schemas.plugin import Plugin as _SchemaPlugin
from app.schemas.plugin import PluginDashboard as _SchemaPluginDashboard
from app.schemas.plugin import PluginInstance, PluginRuntimeStatus
from app.schemas.types import EventType
LegacyDiagnosticsConfigurator = Callable[..., None]
LegacyImportScanner = Callable[..., None]
LegacyPluginImportPreparer = Callable[..., None]
SiteAuthLevelProvider = Callable[[], int]
PluginCatalogFactory = Callable[[Callable[..., Any]], Any]
PluginRuntimeFactory = Callable[["PluginManager"], PluginRuntime]
PluginRouteRefresher = Callable[[str], None]
def _ignore_legacy_diagnostics(**_kwargs) -> None:
"""在启动组合根尚未注入兼容服务时保持插件加载可用。"""
def _ignore_plugin_resource_imports(**_kwargs) -> None:
"""未进入应用启动组合时不主动创建进程级宿主资源。"""
def _unavailable_site_auth_level() -> int:
"""站点能力尚未装配时返回未认证等级。"""
return 0
def _unavailable_plugin_catalog_factory(_mapper: Callable[..., Any]) -> Any:
"""在启动组合根尚未装配目录用例时拒绝隐式跨层构造。"""
raise RuntimeError("插件目录应用服务尚未由启动组合根装配")
def _unavailable_plugin_runtime_factory(_manager: "PluginManager") -> PluginRuntime:
"""拒绝在启动组合根尚未注入依赖图时隐式构造插件 Runtime。"""
raise RuntimeError("插件 Runtime 工厂尚未由启动组合根装配")
def _warn_if_plugin_enabled_gil(
*,
gil_enabled_before: bool,
plugin_id: Optional[str],
) -> None:
"""记录插件加载使 free-threaded 进程重新启用 GIL 的真实转换。"""
if (
not is_free_threaded_runtime()
or gil_enabled_before
or not is_gil_enabled()
):
return
logger.warning(
"加载插件%s后 free-threaded 运行时已启用 GIL,请检查原生扩展兼容性",
plugin_id or "集合",
)
def _unavailable_plugin_route_refresher(_plugin_id: str) -> None:
"""在 HTTP 组合尚未装配时拒绝发布不完整的热重载投影。"""
raise RuntimeError("插件动态路由刷新器尚未由启动组合根装配")
_legacy_diagnostics_configurator: LegacyDiagnosticsConfigurator = (
_ignore_legacy_diagnostics
)
_legacy_import_scanner: LegacyImportScanner = _ignore_legacy_diagnostics
_legacy_plugin_import_preparer: LegacyPluginImportPreparer = (
_ignore_plugin_resource_imports
)
_site_auth_level_provider: SiteAuthLevelProvider = _unavailable_site_auth_level
_plugin_catalog_factory: PluginCatalogFactory = _unavailable_plugin_catalog_factory
_plugin_runtime_factory: PluginRuntimeFactory = _unavailable_plugin_runtime_factory
_plugin_route_refresher: PluginRouteRefresher = _unavailable_plugin_route_refresher
def configure_plugin_legacy_import_services(
*,
diagnostics_configurator: LegacyDiagnosticsConfigurator,
import_scanner: LegacyImportScanner,
) -> None:
"""由启动组合根注入插件旧导入诊断服务,避免扩展层反向依赖兼容层。"""
global _legacy_diagnostics_configurator, _legacy_import_scanner
_legacy_diagnostics_configurator = diagnostics_configurator
_legacy_import_scanner = import_scanner
def configure_plugin_resource_import_preparer(
preparer: LegacyPluginImportPreparer,
) -> None:
"""注入旧插件导入前的宿主资源准备器。"""
global _legacy_plugin_import_preparer
_legacy_plugin_import_preparer = preparer
def configure_site_auth_level_provider(provider: SiteAuthLevelProvider) -> None:
"""由启动组合根注入站点认证等级,避免扩展运行时依赖应用服务。"""
global _site_auth_level_provider
_site_auth_level_provider = provider
def configure_plugin_catalog_factory(factory: PluginCatalogFactory) -> None:
"""由启动组合根注入插件目录应用服务工厂,消除 Runtime 反向依赖。"""
global _plugin_catalog_factory
_plugin_catalog_factory = factory
def configure_plugin_runtime_factory(factory: PluginRuntimeFactory) -> None:
"""由启动组合根注入完整插件 Runtime 工厂。"""
global _plugin_runtime_factory
_plugin_runtime_factory = factory
def reset_plugin_runtime_factory() -> None:
"""恢复未装配工厂,仅供隔离测试清理进程级依赖。"""
global _plugin_runtime_factory
_plugin_runtime_factory = _unavailable_plugin_runtime_factory
def configure_plugin_route_refresher(refresher: PluginRouteRefresher) -> None:
"""由启动组合根注入热重载后的动态路由投影刷新器。"""
global _plugin_route_refresher
_plugin_route_refresher = refresher
def _resolve_plugin_handler_instance(
owner_class: Type[Any],
) -> Optional[EventHandlerBinding]:
"""通过当前插件管理器单例解析实例,避免事件总线持有过期对象。"""
manager = cast(
Optional["PluginManager"],
PluginManager.get_existing_instance(),
)
if manager is None:
return None
return manager.resolve_event_handler_instance(owner_class)
@observe_compat_facade("PluginManager")
class PluginManager(ConfigReloadMixin, metaclass=Singleton):
"""插件管理器"""
CONFIG_WATCH = {"DEV", "PLUGIN_AUTO_RELOAD", "PLUGIN_LOCAL_REPO_PATHS"}
AGENT_TOOLS_BUILD_MAX_ATTEMPTS = 3
# 略长于 watchfiles 默认 debounce,吸收包写入完成后才交付的延迟批次。
MONITOR_SETTLE_SECONDS = 2.0
def __init__(self) -> None:
"""消费组合根注入的唯一 Runtime,并保留旧私有字段的对象身份。"""
self._monitor_suppression_lock = threading.Lock()
self._suppressed_monitor_plugins: Dict[str, int] = {}
self._monitor_suppressed_until: Dict[str, float] = {}
self._plugin_quiesce_lock = threading.RLock()
self._plugin_quiesce_future: Optional[
concurrent.futures.Future[bool]
] = None
self._plugin_service_quiesce_future: Optional[
concurrent.futures.Future[bool]
] = None
self._plugin_runtime_closed = False
self._plugin_runtime = _plugin_runtime_factory(self)
self._plugin_registry = self._plugin_runtime.registry
self._plugins = self._plugin_registry.classes
self._running_plugins = self._plugin_registry.running
self._plugin_instance_store = self._plugin_runtime.instances
self._plugin_config_store = self._plugin_runtime.configs
self._plugin_access = self._plugin_runtime.access
self._plugin_catalog_view = self._plugin_runtime.catalog
self._recent_local_sync = self._plugin_runtime.recent_local_sync
self._plugin_paths = self._plugin_runtime.paths
self._local_plugin_sync = self._plugin_runtime.local_sync
self._plugin_monitor = self._plugin_runtime.monitor
self._plugin_mutation_admission = self._plugin_runtime.admission
self._plugin_dependencies = self._plugin_runtime.dependencies
self._plugin_loader = self._plugin_runtime.loader
self._plugin_tool_catalog = self._plugin_runtime.tools
self._plugin_lifecycle = self._plugin_runtime.lifecycle
self._plugin_metadata = self._plugin_runtime.metadata
self._plugin_sync = self._plugin_runtime.sync
self._plugin_clone = self._plugin_runtime.clone
self._plugin_log_level = self._plugin_runtime.log_level
self._plugin_default_target = self._plugin_runtime.default_target
self._plugin_classification = self._plugin_runtime.classification
# 事件总线只通过通用解析器访问运行中的插件实例。
eventmanager.register_handler_instance_resolver(
"plugins",
_resolve_plugin_handler_instance,
)
def resolve_event_handler_instance(
self,
owner_class: Type[Any],
) -> Optional[EventHandlerBinding]:
"""为插件声明的事件方法解析当前运行实例。"""
plugin_id = owner_class.__name__
# 旧测试与部分扩展会替换私有映射来构造隔离运行态,解析器继续尊重该接缝。
if self._plugins.get(plugin_id) is not owner_class:
return None
plugin = self._running_plugins.get(plugin_id)
owner_name = plugin_id
if plugin and callable(getattr(plugin, "get_name", None)):
owner_name = plugin.get_name()
return EventHandlerBinding(
instance=plugin,
owner_name=owner_name,
run_sync_in_threadpool=True,
)
def init_config(self):
"""按最新系统配置完整重启插件。"""
try:
with self.mutation("配置热重载"):
# 停止已有插件
self.stop()
classification = self.classify_plugins()
self.apply_plugin_dependency_classification(classification)
for plugin_id in classification.ready:
self.start(plugin_id)
except PluginMutationRejectedError as error:
logger.warning(str(error))
def start(self, pid: Optional[str] = None) -> Dict[str, PluginRuntimeStatus]:
"""
启动加载插件
:param pid: 插件ID,为空加载所有插件
"""
try:
with self.mutation("启动插件"):
with self._plugin_quiesce_lock:
_legacy_diagnostics_configurator(
enabled=get_runtime_setting('DEBUG'),
emitter=logger.warning,
)
gil_enabled_before = is_gil_enabled()
try:
return self._plugin_lifecycle.start(pid)
finally:
_warn_if_plugin_enabled_gil(
gil_enabled_before=gil_enabled_before,
plugin_id=pid,
)
except PluginMutationRejectedError as error:
logger.warning(str(error))
if pid:
return {pid: PluginRuntimeStatus.LOAD_FAILED}
return {}
def init_plugin(self, plugin_id: str, conf: dict):
"""
初始化插件
:param plugin_id: 插件ID
:param conf: 插件配置
"""
try:
with self.mutation("初始化插件配置"):
with self._plugin_quiesce_lock:
self._plugin_lifecycle.initialize(plugin_id, conf)
except PluginMutationRejectedError as error:
logger.warning(str(error))
def clear_plugin_agent_tools_cache(self) -> None:
"""
清空插件智能体工具注册表缓存。
"""
self._plugin_tool_catalog.clear()
def get_plugin_agent_tools_revision(self) -> int:
"""
获取插件智能体工具注册表版本号。
"""
return self._plugin_tool_catalog.revision
@property
def _plugin_agent_tools_revision(self) -> int:
"""兼容读取旧私有字段,实际版本由独立工具目录持有。"""
return self._plugin_tool_catalog.revision
def stop(self, pid: Optional[str] = None) -> None:
"""
停止插件服务
:param pid: 插件ID,为空停止所有插件
"""
try:
with self.mutation("停止插件"):
with self._plugin_quiesce_lock:
self._plugin_lifecycle.stop(pid)
except PluginMutationRejectedError as error:
logger.warning(str(error))
def mutation(self, operation: str) -> ContextManager[None]:
"""为一个完整插件可变事务取得可跨异步边界传播的准入 lease。"""
return self._plugin_mutation_admission.hold(operation)
def reopen_plugins(self) -> bool:
"""为新应用生命周期解除运行时封口,仍活跃的 quiesce owner 禁止复用。"""
with self._plugin_quiesce_lock:
futures = (
self._plugin_quiesce_future,
self._plugin_service_quiesce_future,
)
if any(future is not None and not future.done() for future in futures):
logger.warning("插件后台服务仍在停止,无法开启新的应用生命周期")
return False
if self._plugin_runtime_closed and self._running_plugins:
logger.warning("上一应用生命周期仍持有插件实例,拒绝解除运行时封口")
return False
if not self._plugin_mutation_admission.reopen():
logger.warning("上一应用生命周期仍有插件可变事务,拒绝解除运行时封口")
return False
self._plugin_runtime_closed = False
return True
async def quiesce_plugins(self, timeout: float = 240.0) -> bool:
"""封口变更事务并停用插件 handler,超时后保留 Future ownership。"""
if self._plugin_mutation_admission.is_held():
logger.warning("插件可变事务不能等待自身收敛,拒绝在事务内执行停机")
return False
with self._plugin_quiesce_lock:
self._plugin_runtime_closed = True
self._plugin_mutation_admission.seal()
future = self._plugin_quiesce_future
if future is None or future.done():
future = ThreadHelper().submit(self._quiesce_after_mutations)
self._plugin_quiesce_future = future
try:
result = await asyncio.wait_for(
asyncio.shield(asyncio.wrap_future(future)),
timeout=max(0.0, timeout),
)
return bool(result)
except asyncio.TimeoutError:
logger.error(f"插件后台服务未在 {timeout:g} 秒内收敛")
return False
except Exception as error: # noqa: BLE001 Future 异常必须转为生命周期结果
logger.error(f"插件后台服务停止失败:{error}", exc_info=True)
return False
finally:
if future.done():
with self._plugin_quiesce_lock:
if self._plugin_quiesce_future is future:
self._plugin_quiesce_future = None
async def quiesce_plugin_services(self, timeout: float = 240.0) -> bool:
"""在事件结算后执行旧插件停机 hook,并有界等待同步 owner。"""
with self._plugin_quiesce_lock:
prepare_future = self._plugin_quiesce_future
if prepare_future is not None and not prepare_future.done():
logger.warning("插件事件入口仍在封口,拒绝提前关闭插件资源")
return False
if not self._plugin_runtime_closed:
logger.warning("插件运行时尚未封口,拒绝关闭插件资源")
return False
if self._plugin_mutation_admission.active_count:
logger.warning("插件可变事务仍在执行,拒绝关闭插件资源")
return False
future = self._plugin_service_quiesce_future
if future is None or future.done():
future = ThreadHelper().submit(
self._plugin_lifecycle.quiesce_services,
)
self._plugin_service_quiesce_future = future
try:
result = await asyncio.wait_for(
asyncio.shield(asyncio.wrap_future(future)),
timeout=max(0.0, timeout),
)
return bool(result)
except asyncio.TimeoutError:
logger.error(f"插件旧停机 hook 未在 {timeout:g} 秒内收敛")
return False
except Exception as error: # noqa: BLE001 Future 异常必须转为生命周期结果
logger.error(f"插件旧停机 hook 执行失败:{error}", exc_info=True)
return False
finally:
if future.done():
with self._plugin_quiesce_lock:
if self._plugin_service_quiesce_future is future:
self._plugin_service_quiesce_future = None
def finalize_plugins(self) -> bool:
"""确认 quiesce owner 已结束后禁用 handler 并卸载插件实例。"""
with self._plugin_quiesce_lock:
futures = (
self._plugin_quiesce_future,
self._plugin_service_quiesce_future,
)
if any(future is not None and not future.done() for future in futures):
logger.warning("插件后台服务仍在停止,拒绝释放运行实例")
return False
if self._plugin_mutation_admission.active_count:
logger.warning("插件可变事务仍在执行,拒绝释放运行实例")
return False
return self._plugin_lifecycle.finalize()
def _quiesce_after_mutations(self) -> bool:
"""等待已获准变更自然结束后,再停用插件事件入口。"""
self._plugin_mutation_admission.wait_until_idle()
return self._plugin_lifecycle.quiesce_handlers()
@property
def running_plugins(self) -> Dict[str, Any]:
"""
获取运行态插件列表
:return: 运行态插件列表
"""
return self._plugin_registry.running
@property
def plugins(self) -> Dict[str, Any]:
"""
获取插件列表
:return: 插件列表
"""
return self._plugin_registry.classes
def on_config_changed(self):
"""在插件监控配置变化后重建文件监控。"""
self.reload_monitor()
def get_reload_name(self) -> str:
"""返回配置重载日志使用的功能名称。"""
return "插件文件修改监测"
def start_monitor(self, *, reopen: bool = False) -> None:
"""按当前配置启动监控;新生命周期可显式解除既有封口。"""
if reopen and not self._plugin_monitor.reopen():
return
if (
not self.is_plugin_settling()
and (
get_runtime_setting('DEV')
or get_runtime_setting('PLUGIN_AUTO_RELOAD')
)
):
self._plugin_monitor.start()
def reload_monitor(self):
"""
重新加载插件文件修改监测
"""
self._plugin_monitor.reload(
enabled=(
not self.is_plugin_settling()
and (
get_runtime_setting('DEV')
or get_runtime_setting('PLUGIN_AUTO_RELOAD')
)
)
)
def stop_monitor(self, timeout: float = 5.0) -> bool:
"""停止插件文件监控,并返回线程是否在预算内真正退出。"""
return self._plugin_monitor.stop(timeout=timeout)
def close_monitor(self, timeout: float = 5.0) -> bool:
"""封口当前生命周期的文件监控,并返回线程是否真正退出。"""
return self._plugin_monitor.close(timeout=timeout)
def _run_file_watcher(self):
"""
运行 watchfiles 监视器的主循环。
"""
PluginChangeMonitor(
runtime_root=get_runtime_setting('ROOT_PATH') / "app" / "plugins",
local_roots=self._plugin_runtime.system().local_repo_paths,
stop_event=self._plugin_monitor.stop_event,
recent_sync=self._recent_local_sync,
federated_change=self._get_federated_plugin_change,
runtime_plugin=self._get_plugin_id_from_path,
monitor_suppressed=self.is_plugin_monitor_suppressed,
local_candidate=self._get_local_plugin_candidate_from_path,
sync_local=self._sync_local_plugin_if_installed,
reload_plugin=self._reload_plugin_tree_from_monitor,
dependency_manifest_status=(
self._plugin_runtime.system().dependency_manifest_status
),
watch=watch,
log=logger,
).run()
def _reload_plugin_tree_from_monitor(
self,
plugin_id: str,
) -> PluginRuntimeStatus:
"""重载源码树,并发布源插件及虚拟实例的动态路由投影。"""
with self.mutation("热重载插件路由"):
status = self.reload_plugin_tree(plugin_id)
reload_targets = self.get_plugin_reload_targets(plugin_id)
for reload_target in reload_targets:
_plugin_route_refresher(reload_target)
return status
def _get_federated_plugin_change(
self,
event_path: Path,
) -> Optional[Tuple[str, Optional[dict], bool]]:
"""
识别运行态 Vue 插件声明目录内的构建产物变化。
:return: 插件 ID、本地仓库候选和联邦入口是否完整;非联邦目录变化返回 None。
"""
return self._plugin_paths.federated_change(event_path)
def _get_plugin_id_from_path(self, event_path: Path) -> Optional[str]:
"""
根据文件路径解析出插件的ID。
:param event_path: 被修改文件的 Path 对象。
:return: 插件ID字符串,如果不是有效插件文件则返回 None。
"""
return self._plugin_paths.runtime_plugin(event_path)
def _get_local_plugin_candidate_from_path(self, event_path: Path) -> Optional[dict]:
"""
根据本地插件仓库路径解析具体插件候选,保留 plugins/plugins.v2 来源差异
"""
return self._plugin_paths.local_candidate(event_path)
def _sync_local_plugin_if_installed(self, pid: str, candidate: Optional[dict] = None) -> bool:
"""
已安装本地插件源码变化时,同步到运行目录
"""
try:
with self.mutation("同步本地插件源码"):
return self._local_plugin_sync.sync(pid, candidate)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
@contextmanager
def suppress_plugin_monitor(self, plugin_id: str):
"""在插件包写入及其文件事件收敛期间阻止监控重复重载。"""
with self.mutation("更新插件包"):
normalized_id = plugin_id.lower()
with self._monitor_suppression_lock:
self._suppressed_monitor_plugins[normalized_id] = (
self._suppressed_monitor_plugins.get(normalized_id, 0) + 1
)
try:
yield
finally:
with self._monitor_suppression_lock:
count = self._suppressed_monitor_plugins.get(normalized_id, 0)
if count <= 1:
self._suppressed_monitor_plugins.pop(normalized_id, None)
self._monitor_suppressed_until[normalized_id] = (
time.monotonic() + self.MONITOR_SETTLE_SECONDS
)
else:
self._suppressed_monitor_plugins[normalized_id] = count - 1
def is_plugin_monitor_suppressed(self, plugin_id: str) -> bool:
"""判断插件是否处于包写入或延迟文件事件收敛阶段。"""
with self._monitor_suppression_lock:
normalized_id = plugin_id.lower()
if self._suppressed_monitor_plugins.get(normalized_id, 0) > 0:
return True
suppressed_until = self._monitor_suppressed_until.get(normalized_id)
if suppressed_until is None:
return False
if time.monotonic() < suppressed_until:
return True
self._monitor_suppressed_until.pop(normalized_id, None)
return False
def remove_plugin(self, plugin_id: str):
"""
从内存中移除一个插件
:param plugin_id: 插件ID
"""
try:
with self.mutation("移除插件实例"):
with self._plugin_quiesce_lock:
self._plugin_lifecycle.stop(plugin_id)
self._plugin_registry.remove(plugin_id)
except PluginMutationRejectedError as error:
logger.warning(str(error))
def remove_plugin_package(self, plugin_id: str) -> bool:
"""把插件物理目录删除委托给唯一包文件事务 owner。"""
return self._plugin_runtime.system().remove_plugin_package(plugin_id)
def reload_plugin(self, plugin_id: str) -> PluginRuntimeStatus:
"""
将一个插件重新加载到内存
:param plugin_id: 插件ID
"""
try:
with self.mutation("重新加载插件"):
with self._plugin_quiesce_lock:
gil_enabled_before = is_gil_enabled()
try:
return self._plugin_lifecycle.reload(
plugin_id,
EventType.PluginReload,
)
finally:
_warn_if_plugin_enabled_gil(
gil_enabled_before=gil_enabled_before,
plugin_id=plugin_id,
)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return PluginRuntimeStatus.LOAD_FAILED
def reload_plugin_tree(self, plugin_id: str) -> PluginRuntimeStatus:
"""重载源码插件,并同步刷新所有引用该源码的虚拟实例。"""
try:
with self.mutation("重载插件实例树"):
with self._plugin_quiesce_lock:
source_plugin_id = self.get_plugin_source_id(plugin_id)
status = self.reload_plugin(source_plugin_id)
for instance in self._plugin_instance_store.for_source(source_plugin_id):
self.reload_plugin(instance.instance_id)
return status
except PluginMutationRejectedError as error:
logger.warning(str(error))
return PluginRuntimeStatus.LOAD_FAILED
def get_plugin_reload_targets(self, plugin_id: str) -> List[str]:
"""返回源码更新后需要刷新注册信息的源插件及其实例 ID。"""
source_plugin_id = self.get_plugin_source_id(plugin_id)
return [
source_plugin_id,
*(
instance.instance_id
for instance in self._plugin_instance_store.for_source(
source_plugin_id
)
),
]
def sync(
self,
startup_token: object | None = None,
*,
online_restore_plugins: set[str] | None = None,
) -> List[str]:
"""
安装本地不存在或需要更新的插件
"""
with self.mutation("同步插件包"):
return self._plugin_sync.sync(
startup_token,
online_restore_plugins=online_restore_plugins,
)
@staticmethod
def install_plugin_missing_dependencies() -> List[str]:
"""
安装插件中缺失或不兼容的依赖项
"""
manager = PluginManager()
with manager.mutation("安装插件依赖"):
return manager._plugin_dependencies.install_missing()
@staticmethod
def install_plugin_missing_dependencies_with_status() -> PluginDependencyInstallResult:
"""安装插件缺失依赖并返回缺失项及安装成功状态。"""
manager = PluginManager()
with manager.mutation("安装插件依赖"):
return manager._plugin_dependencies.install_missing_with_status()
@staticmethod
async def async_install_plugin_missing_dependencies_with_status() -> PluginDependencyInstallResult:
"""在异步启动链中恢复插件依赖并保留取消语义。"""
manager = PluginManager()
with manager.mutation("安装插件依赖"):
return await manager._plugin_dependencies.async_install_missing_with_status()
def classify_plugins(self) -> PluginDependencyClassification:
"""返回依赖 owner 计算的物理插件与虚拟实例分类。"""
return self._plugin_dependencies.classify_plugins()
def apply_plugin_dependency_classification(
self,
classification: PluginDependencyClassification,
) -> None:
"""把分类结果委托依赖 owner 写入唯一运行状态注册表。"""
self._plugin_dependencies.apply_classification(classification)
def set_plugin_settling(self, settling: bool) -> None:
"""更新启动后的插件恢复任务状态。"""
self._plugin_registry.set_settling(settling)
def get_plugin_runtime_statuses(self) -> Dict[str, PluginRuntimeStatus]:
"""返回插件运行状态快照。"""
return self._plugin_registry.runtime_status_snapshot()
def get_plugin_runtime_generation(self) -> int:
"""返回插件状态变化代次。"""
return self._plugin_registry.generation
def mark_plugin_restart_required(
self,
plugin_id: str,
distributions: tuple[str, ...],
) -> None:
"""记录已落盘但尚未由当前进程完整激活的原生依赖。"""
self._plugin_registry.mark_restart_required(plugin_id, distributions)
def get_plugin_restart_requirements(self) -> Dict[str, tuple[str, ...]]:
"""返回当前进程的插件原生依赖重启要求。"""
return self._plugin_registry.restart_required_snapshot()
def is_plugin_settling(self) -> bool:
"""返回插件源码和依赖是否仍在后台恢复。"""
return self._plugin_registry.settling
def get_plugin_config(self, pid: str) -> dict:
"""
获取插件配置
:param pid: 插件ID
"""
return self._plugin_config_store.read(pid)
def get_plugin_instances(self) -> dict[str, PluginInstance]:
"""返回全部有效虚拟插件实例描述。"""
return self._plugin_instance_store.all()
def get_plugin_instance(self, plugin_id: str) -> Optional[PluginInstance]:
"""返回指定虚拟插件实例描述,物理插件返回空。"""
return self._plugin_instance_store.get(plugin_id)
def get_plugin_source_id(self, plugin_id: str) -> str:
"""解析插件运行身份对应的源码身份,普通插件保持原值。"""
instance = self._plugin_instance_store.get(plugin_id)
return instance.source_plugin_id if instance else plugin_id
def get_plugin_source_instances(self, plugin_id: str) -> List[PluginInstance]:
"""返回直接引用指定源码插件的虚拟实例。"""
return self._plugin_instance_store.for_source(plugin_id)
def delete_plugin_instance(self, plugin_id: str) -> bool:
"""删除虚拟实例描述;调用方仍负责停止实例和清理业务数据。
同时清掉该实例的进程内日志等级覆盖:覆盖表按实例 ID 常驻进程,只删库里那
一行的话,同一进程内用相同后缀重建的分身会继承上一个分身的等级——界面显示
「跟随全局」,实际仍按旧等级输出,直到重启才恢复。
"""
try:
with self.mutation("删除插件实例描述"):
deleted = self._plugin_instance_store.delete(plugin_id)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
clear_instance_log_level_override(plugin_id)
return deleted
def delete_plugin_data_rows(self, instance_id: str) -> None:
"""只删该实例在插件数据表里的行,不碰它的自有数据库。
与 :meth:`delete_plugin_data` 的区别正在于此:那个方法把数据表与自有库捆在
一起删,而彻底清理要让用户逐项勾选,两者必须拆开。
"""
self._plugin_config_store.delete_data_rows(instance_id)
def destroy_plugin_own_database(self, instance_id: str) -> None:
"""销毁该实例的自有数据库并释放句柄。"""
self._plugin_config_store.destroy_database(instance_id)
def delete_plugin_data_directory(self, instance_id: str) -> bool:
"""删除该实例在插件数据目录下的整个目录,返回删除前它是否存在。
这个目录此前没有任何代码清理过:插件把落盘文件写在这里,卸载与重置都只动
数据库,目录会一直留着,重建同名实例时静默继承上一轮的文件。
:param instance_id: 实例 ID
:return: 删除前该目录是否存在
:raise ValueError: 标识越界,或目标目录解析后不在插件数据根之内
"""
return remove_plugin_data_directory(instance_id)
def purge_plugin_instance(self, instance_id: str) -> bool:
"""彻底删除一个分身实例的行连同其配置,返回删除前它是否存在。
调用方需自行持有 ``mutation`` 上下文;彻底清理的各个步骤必须落在同一个
lease 内,否则停机封口可能卡在两步之间,留下删了一半的实例。
本体的行删不掉:底层读取口只认分身,本体传进来一律返回未删除。这是有意的,
本体那一行还承载着该插件的启用状态与展示覆盖,删掉等于把装着的插件静默停用。
同时清掉该实例的进程内日志等级覆盖:覆盖表按实例 ID 常驻进程,只删库里那一
行的话,同一进程内用相同 ID 重建的分身会继承上一个分身的等级。
"""
deleted = self._plugin_instance_store.delete(instance_id)
clear_instance_log_level_override(instance_id)
return deleted
def is_plugin_clone(self, instance_id: str) -> bool:
"""判断该实例 ID 是否为分身而非源插件本体。"""
return self._plugin_instance_store.get(instance_id) is not None
def save_plugin_config(self, pid: str, conf: dict, force: bool = False) -> bool:
"""
保存插件配置
:param pid: 插件ID
:param conf: 配置
:param force: 强制保存
"""
try:
with self.mutation("保存插件配置"):
return self._plugin_config_store.write(pid, conf, force)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
async def async_save_plugin_config(
self, pid: str, conf: dict, force: bool = False
) -> bool:
"""
异步保存插件配置。
:param pid: 插件ID
:param conf: 配置
:param force: 强制保存
"""
try:
with self.mutation("保存插件配置"):
return await self._plugin_config_store.async_write(pid, conf, force)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
def delete_plugin_config(self, pid: str, force: bool = False) -> bool:
"""
删除插件配置
:param pid: 插件ID
:param force: 插件停止后仍允许按插件 ID 删除持久化配置
"""
try:
with self.mutation("删除插件配置"):
return self._plugin_config_store.delete(pid, force)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
def delete_plugin_data(self, pid: str, force: bool = False) -> bool:
"""
删除插件数据
:param pid: 插件ID
:param force: 插件停止后仍允许按插件 ID 删除持久化数据
"""
try:
with self.mutation("删除插件数据"):
return self._plugin_config_store.delete_data(pid, force)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False
def get_plugin_state(self, pid: str) -> bool:
"""
获取插件状态
:param pid: 插件ID
"""
plugin = self._plugin_registry.instance(pid)
return plugin.get_state() if plugin else False
def _plugin_projection(self) -> PluginProjection:
"""返回类型化 Runtime 持有的唯一能力投影。"""
return self._plugin_runtime.projection
def _plugin_catalog(self) -> Any:
"""兼容旧私有入口,返回绑定唯一元数据 owner 的目录服务。"""
return _plugin_catalog_factory(self._plugin_metadata.map)
def get_plugin_commands(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件命令
[{
"cmd": "/xx",
"event": EventType.xx,
"desc": "xxxx",
"data": {},
"pid": "",
}]
"""
return self._plugin_projection().commands(pid)
def get_plugin_apis(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件API
[{
"path": "/xx",
"endpoint": self.xxx,
"methods": ["GET", "POST"],
"summary": "API名称",
"description": "API说明",
"allow_anonymous": false
}]
"""
return self._plugin_projection().apis(pid)
def get_plugin_services(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件服务
[{
"id": "服务ID",
"name": "服务名称",
"trigger": "触发器:cron、interval、date、CronTrigger.from_crontab()",
"func": self.xxx,
"kwargs": {} # 定时器参数,
"func_kwargs": {} # 方法参数
}]
"""
return self._plugin_projection().services(pid)
def get_plugin_modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str, Any]]:
"""
获取插件模块
{
plugin_id: {
method: function
}
}
"""
return self._plugin_projection().modules(pid)
def get_media_sources(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""获取运行中插件声明的媒体数据源。"""
return self._plugin_projection().media_sources(pid)
def get_classification_fields(
self,
pid: Optional[str] = None,
) -> tuple[ClassificationFieldDefinition, ...]:
"""获取当前启用插件声明的扩展分类字段快照。"""
return self._plugin_classification.fields(pid)
def get_classification_facts(
self,
media: Any,
) -> dict[str, dict[str, ClassificationFactValue]]:
"""校验媒体识别结果携带的插件扩展事实并返回分类器输入。"""
return self._plugin_classification.facts(media)
def get_plugin_actions(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件动作
[{
"id": "动作ID",
"name": "动作名称",
"func": self.xxx,
"kwargs": {} # 需要附加传递的参数
}]
"""
return self._plugin_projection().actions(pid)
@staticmethod
def _copy_plugin_agent_tools(
tools_info: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
复制插件智能体工具注册信息,避免调用方修改缓存内容。
"""
return PluginToolCatalog.copy(tools_info)
def get_plugin_agent_tools(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件智能体工具
[{
"plugin_id": "插件ID",
"plugin_name": "插件名称",
"tools": [ToolClass1, ToolClass2, ...]
}]
"""
return self._plugin_tool_catalog.get(
self._running_plugins,
plugin_id=pid,
log=logger,
)
@staticmethod
def get_plugin_remote_entry(plugin_id: str, dist_path: str) -> str:
"""
获取插件的远程入口地址
:param plugin_id: 插件 ID
:param dist_path: 插件的分发路径
:return: 远程入口地址
"""
dist_path = dist_path.strip("/")
path = posixpath.join(
"plugin",
"file",
plugin_id.lower(),
dist_path,
"remoteEntry.js",
)
if not path.startswith("/"):
path = "/" + path
return path
def get_plugin_remotes(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
"""
获取插件联邦组件列表
"""
return self._plugin_projection().remotes(pid)
def get_plugin_auth_providers(self) -> List[Dict[str, Any]]:
"""
聚合插件声明的登录认证提供方。
:return: 插件认证入口列表
"""
return self._plugin_projection().auth_providers()
def get_plugin_sidebar_nav(self) -> List[Dict[str, Any]]:
"""
聚合所有已启用 Vue 插件的侧栏导航项(get_sidebar_nav)。
"""
return self._plugin_projection().sidebar()
def get_plugin_dashboard_meta(self) -> List[Dict[str, str]]:
"""
获取所有插件仪表盘元信息
"""
return self._plugin_projection().dashboard_metadata()
def get_plugin_dashboard(self, pid: str, key: str, user_agent: str = None) -> Optional[_SchemaPluginDashboard]:
"""
获取插件仪表盘
"""
return self._plugin_projection().dashboard(pid, key, user_agent)
def get_plugin_attr(self, pid: str, attr: str) -> Any:
"""
获取插件属性
:param pid: 插件ID
:param attr: 属性名
"""
plugin = self._plugin_registry.instance(pid)
if not plugin:
return None
if not hasattr(plugin, attr):
return None
return getattr(plugin, attr)
def run_plugin_method(self, pid: str, method: str, *args, **kwargs) -> Any:
"""
运行插件方法
:param pid: 插件ID
:param method: 方法名
:param args: 参数
:param kwargs: 关键字参数
"""
plugin = self._plugin_registry.instance(pid)
if not plugin:
return None
if not hasattr(plugin, method):
return None
return getattr(plugin, method)(*args, **kwargs)
async def async_run_plugin_method(self, pid: str, method: str, *args, **kwargs) -> Any:
"""
异步运行插件方法,同步实现经受控线程入口执行
:param pid: 插件ID
:param method: 方法名
:param args: 参数
:param kwargs: 关键字参数
"""
plugin = self._plugin_registry.instance(pid)
if not plugin:
return None
if not hasattr(plugin, method):
return None
method_func = getattr(plugin, method)
if inspect.iscoroutinefunction(method_func):
return await method_func(*args, **kwargs)
return await run_in_threadpool_to_completion(method_func, *args, **kwargs)
def get_plugin_ids(self) -> List[str]:
"""
获取所有插件ID
"""
return self._plugin_registry.plugin_ids()
def get_running_plugin_ids(self) -> List[str]:
"""
获取所有运行态插件ID
"""
return self._plugin_registry.running_ids()
def get_online_plugins(self, force: bool = False) -> List[_SchemaPlugin]:
"""
获取所有在线插件信息
"""
return self._plugin_catalog_view.online(force)
def get_local_plugins(self) -> List[_SchemaPlugin]:
"""
获取所有本地已下载的插件信息
"""
return self._plugin_catalog_view.local()
def get_installed_plugins(self) -> List[_SchemaPlugin]:
"""按安装清单返回插件,即使运行时尚未加载也保留卡片。"""
return self._plugin_catalog_view.installed()
def get_local_plugin_version(self, pid: str) -> Optional[str]:
"""
获取指定已安装插件的本地版本,不触发全部插件的状态、页面和权限计算。
插件类由运行期动态加载,旧插件可能未声明版本属性,因此缺失时返回 None。
"""
return self._plugin_catalog_view.local_version(pid)
def get_local_repo_plugins(self) -> List[_SchemaPlugin]:
"""
获取本地插件仓库目录中的插件信息
"""
return self._plugin_catalog_view.local_repository()
def is_plugin_exists(self, pid: str, version: str = None) -> bool:
"""
判断插件是否存在,并满足版本要求(有传入version时)
:param pid: 插件ID
:param version: 插件版本
"""
return self._plugin_catalog_view.exists(pid, version)
def get_plugins_from_market(self, market: str,
package_version: Optional[str] = None,
force: bool = False) -> Optional[List[_SchemaPlugin]]:
"""
从指定的市场获取插件信息
:param market: 市场的 URL 或标识
:param package_version: 首选插件版本 (如 "v2", "v3"),如果不指定则获取 v1 版本
:param force: 是否强制刷新(忽略缓存)
:return: 返回插件的列表,若获取失败返回 []
"""
return self._plugin_catalog_view.get_from_market(
market,
package_version,
force,
)
def process_plugins_list(self, higher_version_plugins: List[_SchemaPlugin],
base_version_plugins: List[_SchemaPlugin]) -> List[_SchemaPlugin]:
"""
处理插件列表:合并、去重、排序、保留最高版本
:param higher_version_plugins: 高版本插件列表
:param base_version_plugins: 基础版本插件列表
:return: 处理后的插件列表
"""
return self._plugin_catalog_view.merge(
higher_version_plugins,
base_version_plugins,
)
def _process_plugin_info(self, pid: str, plugin_info: dict, market: str,
installed_apps: List[str], add_time: int,
package_version: Optional[str] = None) -> Optional[_SchemaPlugin]:
"""
处理单个插件信息,创建 schemas.Plugin 对象
:param pid: 插件ID
:param plugin_info: 插件信息字典
:param market: 市场URL
:param installed_apps: 已安装插件列表
:param add_time: 添加顺序
:param package_version: 包版本
:return: 创建的插件对象,如果验证失败返回None
"""
return self._plugin_metadata.map(
plugin_id=pid,
plugin_info=plugin_info,
market=market,
installed_plugins=installed_apps,
add_time=add_time,
package_version=package_version,
)
@staticmethod
def _normalize_plugin_label(labels: Any) -> Optional[str]:
"""
规整插件市场标签字段,兼容旧字符串和新列表格式。
:param labels: 插件市场 package 中的 labels 字段
:return: 用空格拼接后的标签字符串,无法识别或为空时返回 None
"""
return PluginMetadataMapper.normalize_label(labels)
async def async_get_online_plugins(
self,
force: bool = False,
progress_callback: Optional[Callable[..., None]] = None,
) -> List[_SchemaPlugin]:
"""
异步获取所有在线插件信息
:param force: 是否强制刷新(忽略缓存)
:param progress_callback: 定时服务进度更新回调
"""
return await self._plugin_catalog_view.async_online(
force,
progress_callback,
)
async def async_get_online_plugin_candidates(
self,
force: bool = False,
) -> List[_SchemaPlugin]:
"""获取按仓库保留的在线插件候选,供来源准入和更新选择。"""
return await self._plugin_catalog_view.async_online_candidates(force)
async def async_get_plugins_from_market(self, market: str,
package_version: Optional[str] = None,
force: bool = False) -> Optional[List[_SchemaPlugin]]:
"""
异步从指定的市场获取插件信息
:param market: 市场的 URL 或标识
:param package_version: 首选插件版本 (如 "v2", "v3"),如果不指定则获取 v1 版本
:param force: 是否强制刷新(忽略缓存)
:return: 返回插件的列表,若获取失败返回 []
"""
return await self._plugin_catalog_view.async_get_from_market(
market,
package_version,
force,
)
def __set_and_check_auth_level(
self,
plugin: Union[_SchemaPlugin, Type[Any]],
source: Optional[Union[dict, Type[Any]]] = None,
) -> bool:
"""
设置并检查插件的认证级别
:param plugin: 插件对象或包含 auth_level 属性的对象
:param source: 可选的字典对象或类对象,可能包含 "level" 或 "auth_level" 键
:return: 如果插件的认证级别有效且当前环境的认证级别满足要求,返回 True,否则返回 False
"""
return self._plugin_access.check(plugin, source)
@staticmethod
def __get_plugin_private_key(plugin_id: str) -> Optional[str]:
"""
根据插件标识获取对应的私钥
:param plugin_id: 插件标识
:return: 对应的插件私钥,如果未找到则返回 None
"""
return PluginAccessPolicy.private_key(plugin_id)
def clone_plugin(self, plugin_id: str, suffix: Optional[str], name: str, description: str,
version: str = None, icon: str = None,
restore_previous: bool = True) -> Tuple[bool, str]:
"""
创建插件分身
:param plugin_id: 原插件ID
:param suffix: 分身后缀;留空时由运行时自动分配最小可用序号
:param name: 分身名称
:param description: 分身描述
:param version: 自定义版本号,分身始终跟随源插件版本,仅为旧客户端保留
:param icon: 自定义图标URL
:param restore_previous: 同后缀名下留有已停用的分身时是否沿用它的业务参数
:return: (是否成功, 成功时为分身实例ID/失败时为可读原因)
"""
try:
with self.mutation("创建插件分身"):
return self._plugin_clone.clone(
plugin_id=plugin_id,
suffix=suffix,
name=name,
description=description,
version=version,
icon=icon,
restore_previous=restore_previous,
)
except PluginMutationRejectedError as error:
logger.warning(str(error))
return False, str(error)
def get_restorable_plugin_instances(self, plugin_id: str) -> List[Dict[str, Any]]:
"""列出该插件名下已停用、设置仍留存可恢复的分身。
停用只把启用位置假,业务参数与展示信息都还在那一行上;用户在创建分身时挑一个
拿回来,比按后缀去猜哪个留有残留可靠得多。启用中的分身不在此列——它们的配置
正被使用,摆进恢复选择器只会让人误以为能把一个活着的实例再建一遍。
:param plugin_id: 源插件ID
:return: 每个可恢复分身的实例ID、后缀、展示信息与是否留有业务参数
"""
prefix_length = len(plugin_id)
results: List[Dict[str, Any]] = []
for instance in self._plugin_instance_store.for_source(plugin_id):
if instance.is_enabled:
continue
instance_id = instance.instance_id
results.append({
"instance_id": instance_id,
# 分身ID由源插件ID直接拼后缀而成,去掉前缀即还原用户当初填的后缀
"suffix": (
instance_id[prefix_length:]
if instance_id.startswith(plugin_id)
else instance_id
),
"plugin_name": instance.plugin_name,
"plugin_desc": instance.plugin_desc,
"has_config": self._plugin_config_store.has_config(instance_id),
})
return results
def get_plugin_instance_log_levels(self, plugin_id: str) -> List[Dict[str, Any]]:
"""
查询插件全部实例(含本体)当前的日志等级设置
:param plugin_id: 插件ID
:return: 每个实例的等级设置条目列表,首项固定是本体自身
:raise LookupError: 插件不存在,或 plugin_id 实为某个分身自身的实例ID
"""
return self._plugin_log_level.list_levels(plugin_id)
def set_plugin_instance_log_level(
self,
plugin_id: str,
instance_id: str,
level: str,
expires_at: Optional[datetime] = None,
) -> None:
"""
设置指定实例的日志等级覆盖,运行期立即生效并落盘
:param plugin_id: 插件ID
:param instance_id: 实例ID
:param level: 目标日志等级
:param expires_at: 覆盖失效时间,为空表示不过期
:raise LookupError: 插件不存在,或实例不存在/不归属该插件
:raise ValueError: level 不是受支持的等级名
"""
self._plugin_log_level.set_level(plugin_id, instance_id, level, expires_at)
def clear_plugin_instance_log_level(self, plugin_id: str, instance_id: str) -> None:
"""
清除指定实例的日志等级覆盖,立即回落全局等级,重复清除保持幂等
:param plugin_id: 插件ID
:param instance_id: 实例ID
:raise LookupError: 插件不存在,或实例不存在/不归属该插件
"""
self._plugin_log_level.clear_level(plugin_id, instance_id)
def mark_plugin_loadable(self, plugin_id: str) -> None:
"""
把源插件本体登记为应当装载,用于安装收尾
本体的装载判据在实例表的启用位上,安装清单只回答「包在不在磁盘上」。安装
完成时不登记这一行,插件靠定向重载当次能跑起来,重启后却不会再被加载。
:param plugin_id: 插件ID
"""
self._plugin_instance_store.enable_host(plugin_id)
def disable_plugin_host(self, plugin_id: str) -> bool:
"""
卸载收尾:停用源插件本体但保留其业务参数
业务参数是用户的数据,重装同名插件后应当还在;默认目标置位与日志等级覆盖
只对在册实例有意义,随停用一并清除,不会被下一次重装静默继承。
:param plugin_id: 插件ID
:return: 停用前它是否存在且处于启用状态
"""
changed = self._plugin_instance_store.disable_host(plugin_id)
clear_instance_log_level_override(plugin_id)
return changed
def set_plugin_instance_enabled(self, instance_id: str, enabled: bool) -> bool:
"""
启用或停用一个实例,本体与分身走同一个入口
启用位是「这份配置是否应当被实例化并启动」的唯一判据,本体与分身因而可以
共用一个开关:调用方只给实例ID,由本方法按它是分身还是本体分发。
停用不删任何设置:业务参数与展示信息原样留在那一行,再次启用即恢复;删行
才是彻底清理,不从这里走。
:param instance_id: 实例ID,等于插件ID时表示本体自身
:param enabled: 目标启用状态
:return: 该实例存在且状态确实发生了变化
:raise PluginMutationRejectedError: 当前处于停机准入窗口
"""
store = self._plugin_instance_store
is_clone = store.get(instance_id) is not None
with self.mutation(f"{'启用' if enabled else '停用'}插件实例 {instance_id}"):
if is_clone:
changed = store.enable(instance_id) if enabled else store.disable(instance_id)
elif enabled:
changed = store.enable_host(instance_id)
else:
changed = store.disable_host(instance_id)
if not changed:
return False
if enabled:
self.reload_plugin(instance_id)
else:
# 停用要把运行态一并摘掉,否则这一轮进程里它还在跑,重启才真的停下来
self.remove_plugin(instance_id)
clear_instance_log_level_override(instance_id)
return True
def resolve_plugin_call_target(self, plugin_id: str) -> str:
"""
确定按插件ID发起、未指定实例的调用应当落到哪个实例
:param plugin_id: 插件ID,也可以是调用方已经明确知道的具体实例ID
:return: 应当使用的实例ID
:raise LookupError: 该插件已有分身但未设置默认调用目标,或默认调用目标已停用
"""
return self._plugin_default_target.resolve(plugin_id)
def set_plugin_instance_default_target(self, plugin_id: str, instance_id: str) -> bool:
"""
设置指定插件实例为默认调用目标,并清除同插件的旧默认
:param plugin_id: 插件ID
:param instance_id: 实例ID
:return: 目标实例存在时为True,指定的非本体实例不归属该插件时为False
:raise LookupError: 插件不存在,或 plugin_id 实为某个分身自身的实例ID
"""
return self._plugin_default_target.set_target(plugin_id, instance_id)
def clear_plugin_instance_default_target(self, plugin_id: str, instance_id: str) -> None:
"""
清除指定插件实例的默认调用目标置位,仅当当前置位的正是该实例时才动作
:param plugin_id: 插件ID
:param instance_id: 实例ID
:raise LookupError: 插件不存在,或 plugin_id 实为某个分身自身的实例ID
"""
self._plugin_default_target.clear_target(plugin_id, instance_id)
def _modify_plugin_files(self, plugin_dir: Path, original_id: str, suffix: str,
name: str, description: str, version: str = None,
icon: str = None) -> Tuple[bool, str]:
"""
兼容旧内部调用,将分身文件改写委托给包适配器。
:param plugin_dir: 插件目录
:param original_id: 原插件ID
:param suffix: 分身后缀
:param name: 分身名称
:param description: 分身描述
:param version: 自定义版本号
:param icon: 自定义图标URL
:return: (是否成功, 错误信息)
"""
original_plugin_class = self._plugins.get(original_id)
if not original_plugin_class:
return False, f"无法获取原插件类 {original_id}"
return self._plugin_runtime.system().modify_plugin_files(
plugin_dir=plugin_dir,
original_class_name=original_plugin_class.__name__,
suffix=suffix,
name=name,
description=description,
version=version,
icon=icon,
)
@staticmethod
def _modify_python_file(file_path: Path, original_class_name: str,
clone_class_name: str, name: str, description: str,
version: str = None, icon: str = None) -> Tuple[bool, str]:
"""
兼容旧内部调用,将 Python 文件改写委托给包适配器。
"""
return PluginManager()._plugin_runtime.system().modify_python_file(
file_path=file_path,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
name=name,
description=description,
version=version,
icon=icon,
)
def _modify_federation_files(self, dist_dir: Path, original_class_name: str,
clone_class_name: str) -> Tuple[bool, str]:
"""
兼容旧内部调用,将联邦文件改写委托给包适配器。
"""
return self._plugin_runtime.system().modify_federation_files(
dist_dir=dist_dir,
original_class_name=original_class_name,
clone_class_name=clone_class_name,
)
@staticmethod
def _rename_federation_assets(dist_dir: Path, original_class_name: str, clone_class_name: str):
"""
兼容旧内部调用,将资源重命名委托给包适配器。
"""
PluginManager()._plugin_runtime.system().rename_federation_assets(
dist_dir,
original_class_name,
clone_class_name,
)