From 40e80f43f5d2d5fbe1d60ed89c9ad080a59499fe Mon Sep 17 00:00:00 2001 From: InfinityPacer Date: Sun, 11 Oct 2026 02:43:16 +0800 Subject: [PATCH] fix(plugin): advance runtime generation when a plugin is enabled or disabled Co-Authored-By: Claude Opus 5.5 --- app/runtime/extensions/plugin/lifecycle.py | 8 +++++ app/runtime/extensions/plugin/registry.py | 9 ++++++ app/runtime/extensions/plugin/runtime.py | 1 + tests/test_plugin_agent_pets.py | 12 +++++++ ...plugin_instance_log_context_entrypoints.py | 31 ++++++++++++++++++- 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/app/runtime/extensions/plugin/lifecycle.py b/app/runtime/extensions/plugin/lifecycle.py index af2d81d06..c1316849a 100644 --- a/app/runtime/extensions/plugin/lifecycle.py +++ b/app/runtime/extensions/plugin/lifecycle.py @@ -83,6 +83,7 @@ class PluginLifecycle: refresh_classification: Callable[[str, Any], None] | None = None, remove_classification: Callable[[str], None] | None = None, gil_fallback_recorder: GilFallbackRecorder = ignore_gil_fallback, + enabled_state_recorder: Callable[[str], None] | None = None, ) -> None: """保存注册表、加载器、数据库和事件端口。""" self._classes = classes @@ -107,6 +108,10 @@ class PluginLifecycle: lambda _plugin_id: None ) self._gil_fallback_recorder = gil_fallback_recorder + # 启用状态变化会改变侧栏、仪表盘和助手形象等前端投影,需要推进前端刷新代次 + self._enabled_state_recorder = enabled_state_recorder or ( + lambda _plugin_id: None + ) self._lifecycle_lock = threading.RLock() self._quiesced_hooks: dict[str, set[str]] = {} @@ -239,6 +244,7 @@ class PluginLifecycle: plugin = self._running.get(plugin_id) if not plugin: return + was_enabled = bool(plugin.get_state()) self._remove_classification(plugin_id) try: with bind_plugin_instance(plugin_id): @@ -254,6 +260,8 @@ class PluginLifecycle: self._remove_classification(plugin_id) raise self._clear_tools() + if enabled != was_enabled: + self._enabled_state_recorder(plugin_id) def _refresh_classification_safely( self, diff --git a/app/runtime/extensions/plugin/registry.py b/app/runtime/extensions/plugin/registry.py index 9ba04e63d..fb0abf113 100644 --- a/app/runtime/extensions/plugin/registry.py +++ b/app/runtime/extensions/plugin/registry.py @@ -101,6 +101,15 @@ class PluginRegistry: self._gil_fallback_plugins[plugin_id] = True self._generation += 1 + def mark_enabled_state_changed(self, plugin_id: str) -> None: + """记录插件配置切换了启用状态,推进前端刷新代次。 + + 启停只改实例自身的启用位,不经过运行状态表,但侧栏、仪表盘和助手形象等 + 前端投影都按启用位过滤,读取方需要据此重新拉取。 + """ + del plugin_id + self._generation += 1 + def gil_fallback_snapshot(self) -> list[str]: """返回当前进程内导致 GIL 回退的插件 ID,保持记录顺序。""" return list(self._gil_fallback_plugins) diff --git a/app/runtime/extensions/plugin/runtime.py b/app/runtime/extensions/plugin/runtime.py index 910d3bef1..fc276d60e 100644 --- a/app/runtime/extensions/plugin/runtime.py +++ b/app/runtime/extensions/plugin/runtime.py @@ -244,6 +244,7 @@ def build_plugin_runtime( refresh_classification=refresh_classification, remove_classification=classification.remove, gil_fallback_recorder=registry.mark_gil_fallback, + enabled_state_recorder=registry.mark_enabled_state_changed, ) metadata = PluginMetadataMapper( plugin_instance=registry.instance, diff --git a/tests/test_plugin_agent_pets.py b/tests/test_plugin_agent_pets.py index 0bf14f069..1d569fd31 100644 --- a/tests/test_plugin_agent_pets.py +++ b/tests/test_plugin_agent_pets.py @@ -366,3 +366,15 @@ def test_user_config_endpoint_reports_oversized_agent_pet_state(monkeypatch): assert "16384" in rejected.message assert accepted.success is True assert repository.writes == [("alice", "AgentPet", "builtin")] + + +def test_registry_advances_generation_on_enabled_state_change(): + """启停切换推进前端刷新代次,前端据此重新读取形象声明。""" + from app.runtime.extensions.plugin.registry import PluginRegistry + + registry = PluginRegistry() + before = registry.generation + + registry.mark_enabled_state_changed("PetDemo") + + assert registry.generation == before + 1 diff --git a/tests/test_plugin_instance_log_context_entrypoints.py b/tests/test_plugin_instance_log_context_entrypoints.py index decb5f87f..1c13aed94 100644 --- a/tests/test_plugin_instance_log_context_entrypoints.py +++ b/tests/test_plugin_instance_log_context_entrypoints.py @@ -30,7 +30,7 @@ from app.scheduler.reconcile import SchedulerReconcileOwner # --------------------------------------------------------------------------- -def _lifecycle(*, plugins, running=None): +def _lifecycle(*, plugins, running=None, enabled_state_recorder=None): """构造隔离外部事件和模块清理的生命周期实例。""" classes: dict = {} running = running if running is not None else {} @@ -50,6 +50,7 @@ def _lifecycle(*, plugins, running=None): database=lambda: PluginDatabase(), log=MagicMock(), event_sender=MagicMock(), + enabled_state_recorder=enabled_state_recorder, ) return lifecycle, classes, running @@ -334,3 +335,31 @@ def test_projection_api_endpoint_binds_owning_instance_without_any_caller_contex assert plugin.seen == ["DemoPluginWork"] assert current_plugin_instance_id() is None + + +def test_lifecycle_initialize_records_enabled_state_change(): + """配置切换启用状态时通知注册表推进前端刷新代次,状态不变时不打扰读取方。""" + recorded: list[str] = [] + + class _Plugin: + enabled = True + + def init_plugin(self, config: dict) -> None: + self.enabled = bool(config.get("enabled")) + + def get_state(self) -> bool: + return self.enabled + + plugin = _Plugin() + lifecycle, _classes, _running = _lifecycle( + plugins=[], + running={"DemoPluginWork": plugin}, + enabled_state_recorder=recorded.append, + ) + + lifecycle.initialize("DemoPluginWork", {"enabled": True}) + assert recorded == [] + + lifecycle.initialize("DemoPluginWork", {"enabled": False}) + lifecycle.initialize("DemoPluginWork", {"enabled": True}) + assert recorded == ["DemoPluginWork", "DemoPluginWork"]