From 2764742b86aac1ca35111185d10f24ba7dee33a9 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:37:07 +0800 Subject: [PATCH] fix(plugin): reset stopped plugin config and data (#6031) --- app/api/endpoints/plugin.py | 4 ++-- app/core/plugin.py | 10 ++++---- tests/test_plugin_endpoint.py | 45 ++++++++++++++++++++++++++++++----- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 946b7a0f..c036da4c 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -615,9 +615,9 @@ def reset_plugin( # 事件处理器需要运行中插件完成补偿;补偿后先停止插件,避免删除数据时仍有任务读写旧状态。 plugin_manager.stop(plugin_id) # 删除配置 - plugin_manager.delete_plugin_config(plugin_id) + plugin_manager.delete_plugin_config(plugin_id, force=True) # 删除插件所有数据 - plugin_manager.delete_plugin_data(plugin_id) + plugin_manager.delete_plugin_data(plugin_id, force=True) # 重新加载插件 reload_plugin(plugin_id) return schemas.Response(success=True) diff --git a/app/core/plugin.py b/app/core/plugin.py index fbbe151a..d2f8079d 100644 --- a/app/core/plugin.py +++ b/app/core/plugin.py @@ -723,21 +723,23 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton): await SystemConfigOper().async_set(self._config_key % pid, conf) return True - def delete_plugin_config(self, pid: str) -> bool: + def delete_plugin_config(self, pid: str, force: bool = False) -> bool: """ 删除插件配置 :param pid: 插件ID + :param force: 插件停止后仍允许按插件 ID 删除持久化配置 """ - if not self._plugins.get(pid): + if not force and not self._plugins.get(pid): return False return SystemConfigOper().delete(self._config_key % pid) - def delete_plugin_data(self, pid: str) -> bool: + def delete_plugin_data(self, pid: str, force: bool = False) -> bool: """ 删除插件数据 :param pid: 插件ID + :param force: 插件停止后仍允许按插件 ID 删除持久化数据 """ - if not self._plugins.get(pid): + if not force and not self._plugins.get(pid): return False PluginDataOper().del_data(pid) return True diff --git a/tests/test_plugin_endpoint.py b/tests/test_plugin_endpoint.py index d5883ef6..ad784973 100644 --- a/tests/test_plugin_endpoint.py +++ b/tests/test_plugin_endpoint.py @@ -7,8 +7,10 @@ from app.api.endpoints.plugin import plugin_releases from app.api.endpoints.plugin import reset_plugin from app.api.endpoints.system import sync_plugin_market_from_wiki from app.core.config import settings +from app.core.plugin import PluginManager from app.schemas.event import PluginDataResetEventData from app.schemas.types import ChainEventType +from app.utils.singleton import Singleton def test_plugin_history_merges_remote_metadata(): @@ -279,12 +281,12 @@ def test_reset_plugin_sends_pre_reset_chain_event_before_deleting_data(): plugin_manager = MagicMock() calls = [] - def delete_config(plugin_id): - calls.append(("delete_config", plugin_id)) + def delete_config(plugin_id, force=False): + calls.append(("delete_config", plugin_id, force)) return True - def delete_data(plugin_id): - calls.append(("delete_data", plugin_id)) + def delete_data(plugin_id, force=False): + calls.append(("delete_data", plugin_id, force)) return True def stop_plugin(plugin_id): @@ -314,7 +316,38 @@ def test_reset_plugin_sends_pre_reset_chain_event_before_deleting_data(): assert event_call[2].reset_data is True assert calls[1:] == [ ("stop", "SubscribeAssistantEnhanced"), - ("delete_config", "SubscribeAssistantEnhanced"), - ("delete_data", "SubscribeAssistantEnhanced"), + ("delete_config", "SubscribeAssistantEnhanced", True), + ("delete_data", "SubscribeAssistantEnhanced", True), ] reload_plugin_mock.assert_called_once_with("SubscribeAssistantEnhanced") + + +def test_delete_plugin_config_can_force_delete_after_plugin_is_stopped(): + """ + 重置入口会先停止插件;配置删除需要能处理运行态注册已清理的插件 ID。 + """ + Singleton._instances.pop((PluginManager, (), frozenset()), None) + manager = PluginManager() + + with patch("app.core.plugin.SystemConfigOper") as system_config_oper: + system_config_oper.return_value.delete.return_value = True + assert manager.delete_plugin_config("DemoPlugin", force=True) is True + + system_config_oper.return_value.delete.assert_called_once_with("plugin.DemoPlugin") + Singleton._instances.pop((PluginManager, (), frozenset()), None) + + +def test_delete_plugin_data_can_force_delete_after_plugin_is_stopped(): + """ + 重置入口会先停止插件;插件数据删除不能依赖运行态注册仍存在。 + """ + Singleton._instances.pop((PluginManager, (), frozenset()), None) + manager = PluginManager() + calls = [] + + with patch("app.core.plugin.PluginDataOper") as plugin_data_oper: + plugin_data_oper.return_value.del_data.side_effect = lambda pid: calls.append(pid) + assert manager.delete_plugin_data("DemoPlugin", force=True) is True + + assert calls == ["DemoPlugin"] + Singleton._instances.pop((PluginManager, (), frozenset()), None)