refactor(plugin): share runtime dependency owners

This commit is contained in:
jxxghp
2026-08-30 12:12:01 +08:00
parent 26a3b1cda4
commit a350bae0d0
6 changed files with 104 additions and 58 deletions

View File

@@ -47,7 +47,46 @@ _installed_plugins_provider: InstalledPluginsProvider = _empty_installed_plugins
_plugin_install_gateway: PluginInstallGateway = _unconfigured_plugin_install_gateway
_async_plugin_install_gateway: AsyncPluginInstallGateway = _unconfigured_async_plugin_install_gateway
_plugin_market_transport = PluginMarketTransport()
_plugin_runtime_health = PluginRuntimeHealth()
_plugin_runtime_health: Optional[PluginRuntimeHealth] = None
_plugin_dependency_installer: Optional[PluginDependencyInstaller] = None
def configure_plugin_runtime_owners(
*,
health: PluginRuntimeHealth,
dependency: PluginDependencyInstaller,
) -> None:
"""发布组合根构造的插件运行健康与依赖安装唯一 owner。"""
global _plugin_runtime_health, _plugin_dependency_installer
_plugin_runtime_health = health
_plugin_dependency_installer = dependency
def reset_plugin_runtime_owners() -> None:
"""撤销当前 lifespan 发布的插件运行健康与依赖安装 owner。"""
global _plugin_runtime_health, _plugin_dependency_installer
_plugin_runtime_health = None
_plugin_dependency_installer = None
def _plugin_runtime_health_owner() -> PluginRuntimeHealth:
"""返回已发布 owner;旧入口提前调用时惰性保留兼容行为。"""
global _plugin_runtime_health
if _plugin_runtime_health is None:
_plugin_runtime_health = PluginRuntimeHealth()
return _plugin_runtime_health
def _plugin_dependency_owner() -> PluginDependencyInstaller:
"""返回已发布 owner;旧入口提前调用时只构造一份惰性实例。"""
global _plugin_dependency_installer
if _plugin_dependency_installer is None:
_plugin_dependency_installer = PluginDependencyInstaller(
_plugin_runtime_health_owner(),
installed_plugins_provider=lambda: _installed_plugins_provider(),
plugin_dir=PLUGIN_DIR,
)
return _plugin_dependency_installer
def configure_installed_plugins_provider(provider: InstalledPluginsProvider) -> None:
@@ -144,23 +183,17 @@ class PluginHelper(metaclass=WeakSingleton):
find_links_dirs: Optional[List[Path]] = None,
) -> Tuple[bool, str]:
"""把兼容入口委托给系统运行环境 owner。"""
return _plugin_runtime_health.install_packages_with_fallback(
return _plugin_runtime_health_owner().install_packages_with_fallback(
dependency_files, find_links_dirs
)
def find_missing_dependencies(self) -> List[str]:
"""把兼容入口委托给依赖聚合 owner。"""
return PluginDependencyInstaller(
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
plugin_dir=PLUGIN_DIR,
).find_missing()
return _plugin_dependency_owner().find_missing()
def install_dependencies(self, dependencies: List[str]) -> Tuple[bool, str]:
"""把兼容入口委托给依赖聚合 owner。"""
return PluginDependencyInstaller(
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
plugin_dir=PLUGIN_DIR,
).install(dependencies)
return _plugin_dependency_owner().install(dependencies)
@classmethod
async def async_install_packages_with_fallback(
@@ -168,7 +201,7 @@ class PluginHelper(metaclass=WeakSingleton):
find_links_dirs: Optional[List[Path]] = None,
) -> Tuple[bool, str]:
"""把异步兼容入口委托给系统运行环境 owner。"""
return await _plugin_runtime_health.async_install_packages_with_fallback(
return await _plugin_runtime_health_owner().async_install_packages_with_fallback(
dependency_files, find_links_dirs
)
@@ -176,17 +209,11 @@ class PluginHelper(metaclass=WeakSingleton):
self, dependencies: List[str],
) -> Tuple[bool, str]:
"""把异步兼容入口委托给依赖聚合 owner。"""
return await PluginDependencyInstaller(
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
plugin_dir=PLUGIN_DIR,
).async_install(dependencies)
return await _plugin_dependency_owner().async_install(dependencies)
async def async_find_missing_dependencies(self) -> List[str]:
"""把异步兼容入口委托给依赖聚合 owner。"""
return await PluginDependencyInstaller(
_plugin_runtime_health, installed_plugins_provider=_installed_plugins_provider,
plugin_dir=PLUGIN_DIR,
).async_find_missing()
return await _plugin_dependency_owner().async_find_missing()
async def async_install(
self, pid: str, repo_url: str, package_version: Optional[str] = None,

View File

@@ -24,6 +24,7 @@ class PluginMarketComposition:
transport: PluginMarketTransport
client: PluginMarketClient
package: PluginPackageManager
health: PluginRuntimeHealth
dependency: PluginDependencyInstaller
@@ -40,6 +41,7 @@ def compose_plugin_market(
plugin_root = root_path / "app" / "plugins"
transport = PluginMarketTransport.get_existing_instance() or PluginMarketTransport()
client = PluginMarketClient(transport)
health = PluginRuntimeHealth()
composition = PluginMarketComposition(
transport=transport,
client=client,
@@ -48,10 +50,11 @@ def compose_plugin_market(
plugin_root=plugin_root,
),
dependency=PluginDependencyInstaller(
PluginRuntimeHealth(),
health,
installed_plugins_provider=installed_plugins_provider,
plugin_dir=plugin_root,
),
health=health,
)
_market_client = client
return composition

View File

@@ -9,6 +9,8 @@ from app.adapters.external.market import (
LOCAL_REPO_PREFIX,
configure_installed_plugins_provider,
configure_plugin_install_gateway,
configure_plugin_runtime_owners,
reset_plugin_runtime_owners,
)
from app.adapters.external.plugin.client import (
VERSION_BACKWARD_COMPATIBLE_FLAGS,
@@ -190,6 +192,10 @@ def configure_plugin_services() -> None:
market_transport = market_composition.transport
market_client = market_composition.client
package_manager = market_composition.package
configure_plugin_runtime_owners(
health=market_composition.health,
dependency=market_composition.dependency,
)
plugin_manager = get_plugin_manager()
inventory_reader = PluginCandidateInventoryReader(
market_loader=market_client.get_plugin_index_result,
@@ -775,4 +781,5 @@ def stop_plugins() -> bool:
reset_plugin_catalog_query()
reset_plugin_release_service()
reset_plugin_rating_service()
reset_plugin_runtime_owners()
reset_plugin_market_composition()

View File

@@ -227,13 +227,17 @@ re-exported from the package root, SDK or Compat.
Complexity and concurrency governance covers the complete canonical execution
surface rather than only public methods. `scripts/architecture/complexity.py --v2`
ratchets private and nested methods, class/file hotspots, and the
`app/scheduler/` package; its generated baseline is an evidence ledger, not a
permission to add another oversized owner. `scripts/architecture/concurrency.py`
enumerates native Thread, Timer, thread/process executors, Process,
asyncio task/thread helpers, and executor hand-off calls across host roots. Each
fact must have a stable lexical owner and remain in the baseline with no new
call site or owner drift. `app/plugins/**`, `app/sdk/**`, and
uses complete AST child traversal to ratchet private/dunder/nested methods,
class/file hotspots, and the `app/scheduler/` package, including owners nested
under `Match`, `TryStar`, and other control-flow nodes. Its generated baseline
is an evidence ledger, not permission to add another oversized owner.
`scripts/architecture/concurrency.py` resolves canonical imports and aliases for
native Thread, Timer, thread/process executors, Process, TaskGroup, event-loop
task submission, asyncio task/thread helpers, and executor hand-off calls. It
scans the complete `app/**` host tree and aggregates each canonical target by
stable lexical owner and call count, so physical line movement is not semantic
drift and multiple same-line calls cannot collapse. New owners/count growth and
stale baselines after count reduction all block CI. `app/plugins/**`, `app/sdk/**`, and
`app/runtime/compat/**` are excluded because their compatibility/runtime
contracts are governed by their own exact manifests.
@@ -1030,6 +1034,7 @@ driven workflow registration.
| `app/adapters/system/plugin/package.py` | `PluginPackageManager`, the unique install, checkpoint, backup, restore and physical-delete owner |
| `app/adapters/system/plugin/dependency.py` | `PluginDependencyInstaller`, the dependency manifest, missing-package inspection and installation owner |
| `app/adapters/system/plugin/health.py` | `PluginRuntimeHealth`, the runtime-environment guard, pre/post-install check and recovery owner |
| `app/startup/composition/plugin.py` | Constructs one market transport/client, package manager, runtime-health owner and dependency installer per lifespan; `PluginHelper` compatibility calls consume those same published owners |
| `app/runtime/extensions/resource.py` | Data-only managed-resource registry and sync/async lifecycle adapters |
| `app/runtime/resources.py` | Lightweight acquisition, state observation and shutdown facade |
| `app/foundation/reflection.py` | Generic reflection and Python module discovery |

View File

@@ -2392,6 +2392,7 @@ def test_plugin_market_construction_is_owned_by_startup_composition():
"PluginMarketTransport",
"PluginPackageSourceClient",
"PluginPackageManager",
"PluginRuntimeHealth",
"PluginDependencyInstaller",
}
assert forbidden_names.isdisjoint(initializer_imports)
@@ -2417,6 +2418,7 @@ def test_plugin_market_construction_is_owned_by_startup_composition():
assert composition_calls.count("PluginMarketTransport") == 1
assert composition_calls.count("PluginPackageSourceClient") == 1
assert composition_calls.count("PluginPackageManager") == 1
assert composition_calls.count("PluginRuntimeHealth") == 1
assert composition_calls.count("PluginDependencyInstaller") == 1
@@ -2428,6 +2430,28 @@ def test_plugin_market_transport_is_process_shared_across_compat_and_composition
assert PluginMarketTransport.get_existing_instance() is _plugin_market_transport
def test_plugin_runtime_owners_are_shared_with_compat_facade():
"""兼容门面必须消费组合根发布的健康检查与依赖安装 owner。"""
from app.adapters.external.market import (
_plugin_dependency_owner,
_plugin_runtime_health_owner,
configure_plugin_runtime_owners,
reset_plugin_runtime_owners,
)
health = object()
dependency = object()
configure_plugin_runtime_owners(
health=health, # type: ignore[arg-type]
dependency=dependency, # type: ignore[arg-type]
)
try:
assert _plugin_runtime_health_owner() is health
assert _plugin_dependency_owner() is dependency
finally:
reset_plugin_runtime_owners()
def test_domain_initializer_delegates_construction_to_composition():
"""领域 initializer 只负责生命周期调用,不得直接构造 Adapter 或领域服务。"""
initializer_path = APP_ROOT / "startup" / "initializers" / "domain.py"

View File

@@ -712,36 +712,25 @@ async def test_agent_install_uses_application_gateway(
def test_startup_composition_configures_external_helper_gateway(monkeypatch) -> None:
"""启动组合根必须装配独立 owner,并向公开 Helper 发布同一 Gateway。"""
transport = Mock()
source = Mock()
market_client = Mock()
package_manager = Mock()
health = Mock()
dependency = Mock()
gateway_calls = []
application_calls = []
transport_consumers = []
gateway = Mock()
sync_runner = Mock(return_value=(True, "installed"))
async_runner = AsyncMock(return_value=(True, "installed"))
monkeypatch.setattr(
plugins_initializer,
"PluginMarketTransport",
lambda: transport,
)
monkeypatch.setattr(
plugins_initializer,
"PluginMarketClient",
lambda injected: transport_consumers.append(("market", injected)) or Mock(),
)
monkeypatch.setattr(
plugins_initializer,
"PluginPackageSourceClient",
lambda injected: transport_consumers.append(("package", injected)) or source,
)
monkeypatch.setattr(
plugins_initializer,
"PluginPackageManager",
lambda **kwargs: (
transport_consumers.append(("package-owner", kwargs["source"]))
or package_manager
"compose_plugin_market",
lambda **_kwargs: SimpleNamespace(
transport=transport,
client=market_client,
package=package_manager,
health=health,
dependency=dependency,
),
)
monkeypatch.setattr(
@@ -796,11 +785,6 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
"_run_plugin_install_async",
async_runner,
)
monkeypatch.setattr(
plugins_initializer,
"PluginDependencyInstaller",
lambda *_args, **_kwargs: Mock(),
)
for name in (
"configure_plugin_legacy_import_services",
"configure_plugin_resource_import_preparer",
@@ -809,6 +793,7 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
"configure_plugin_catalog_factory",
"configure_plugin_runtime_factory",
"configure_plugin_route_refresher",
"configure_plugin_runtime_owners",
"configure_plugin_system",
"configure_plugin_storage",
):
@@ -827,11 +812,6 @@ def test_startup_composition_configures_external_helper_gateway(monkeypatch) ->
plugins_initializer.configure_plugin_services()
assert transport_consumers == [
("market", transport),
("package", transport),
("package-owner", source),
]
assert application_calls == [gateway]
assert len(gateway_calls) == 1
assert callable(gateway_calls[0]["install"])