fix(plugin): advance runtime generation when a plugin is enabled or disabled

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
InfinityPacer
2026-10-11 02:43:16 +08:00
committed by InfinityPacer
parent ab729be4ed
commit 40e80f43f5
5 changed files with 60 additions and 1 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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"]