diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index c4222e71..664a5b63 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -12,6 +12,7 @@ from starlette.responses import StreamingResponse from app import schemas from app.command import Command from app.core.config import settings +from app.core.event import eventmanager from app.core.plugin import PluginManager from app.core.security import ( resource_token_cookie, @@ -30,7 +31,8 @@ from app.helper.server import MoviePilotServerHelper from app.helper.plugin import PluginHelper from app.log import logger from app.scheduler import Scheduler -from app.schemas.types import SystemConfigKey +from app.schemas.event import PluginDataResetEventData +from app.schemas.types import ChainEventType, SystemConfigKey PROTECTED_ROUTES = {"/api/v1/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc"} PLUGIN_PREFIX = f"{settings.API_V1_STR}/plugin" @@ -530,6 +532,12 @@ def reset_plugin( 根据插件ID重置插件配置及数据 """ plugin_manager = PluginManager() + eventmanager.send_event( + ChainEventType.PluginDataReset, + PluginDataResetEventData(plugin_id=plugin_id, reset_config=True, reset_data=True), + ) + # 事件处理器需要运行中插件完成补偿;补偿后先停止插件,避免删除数据时仍有任务读写旧状态。 + plugin_manager.stop(plugin_id) # 删除配置 plugin_manager.delete_plugin_config(plugin_id) # 删除插件所有数据 diff --git a/app/schemas/event.py b/app/schemas/event.py index 39982970..aacb80fa 100644 --- a/app/schemas/event.py +++ b/app/schemas/event.py @@ -64,6 +64,19 @@ class ChainEventData(BaseEventData): pass +class PluginDataResetEventData(ChainEventData): + """ + PluginDataReset 事件的数据模型。 + + 在主程序清空某个插件配置或插件数据前发出,插件可在数据被删除前完成 + 自有状态补偿。事件处理器只应处理 plugin_id 与自身匹配的事件。 + """ + + plugin_id: str = Field(..., description="即将被重置的插件 ID") + reset_config: bool = Field(default=False, description="是否即将重置插件配置") + reset_data: bool = Field(default=False, description="是否即将重置插件数据") + + class AgentLLMProviderEventData(ChainEventData): """ Agent LLM 供应商选择事件数据。 diff --git a/app/schemas/types.py b/app/schemas/types.py index aa1f35d2..0c385fd0 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -163,6 +163,8 @@ EVENT_TYPE_NAMES = { # 同步链式事件 class ChainEventType(Enum): + # 插件数据重置前 + PluginDataReset = "plugin.data.reset" # 名称识别 NameRecognize = "name.recognize" # 认证验证 diff --git a/tests/test_plugin_endpoint.py b/tests/test_plugin_endpoint.py index 5f596f87..e7e8d8c9 100644 --- a/tests/test_plugin_endpoint.py +++ b/tests/test_plugin_endpoint.py @@ -3,7 +3,10 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch from app import schemas from app.api.endpoints.plugin import plugin_history +from app.api.endpoints.plugin import reset_plugin from app.api.endpoints.system import sync_plugin_market_from_wiki +from app.schemas.event import PluginDataResetEventData +from app.schemas.types import ChainEventType def test_plugin_history_merges_remote_metadata(): @@ -101,3 +104,51 @@ def test_sync_plugin_market_from_wiki_merges_and_deduplicates_repos(): "https://github.com/local/existing,https://github.com/wiki/new-repo", ) send_event.assert_awaited_once() + + +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)) + return True + + def delete_data(plugin_id): + calls.append(("delete_data", plugin_id)) + return True + + def stop_plugin(plugin_id): + calls.append(("stop", plugin_id)) + return True + + plugin_manager.stop.side_effect = stop_plugin + plugin_manager.delete_plugin_config.side_effect = delete_config + plugin_manager.delete_plugin_data.side_effect = delete_data + + with ( + patch("app.api.endpoints.plugin.PluginManager", return_value=plugin_manager), + patch("app.api.endpoints.plugin.eventmanager") as eventmanager, + patch("app.api.endpoints.plugin.reload_plugin") as reload_plugin_mock, + ): + eventmanager.send_event.side_effect = lambda etype, data: calls.append(("event", etype, data)) + result = reset_plugin("SubscribeAssistantEnhanced", None) + + assert result.success is True + assert len(calls) == 4 + event_call = calls[0] + assert event_call[0] == "event" + assert event_call[1] is ChainEventType.PluginDataReset + assert isinstance(event_call[2], PluginDataResetEventData) + assert event_call[2].plugin_id == "SubscribeAssistantEnhanced" + assert event_call[2].reset_config is True + assert event_call[2].reset_data is True + assert calls[1:] == [ + ("stop", "SubscribeAssistantEnhanced"), + ("delete_config", "SubscribeAssistantEnhanced"), + ("delete_data", "SubscribeAssistantEnhanced"), + ] + reload_plugin_mock.assert_called_once_with("SubscribeAssistantEnhanced")