diff --git a/astrbot/core/star/star_manager.py b/astrbot/core/star/star_manager.py index 8404c237d..38bdc61d7 100644 --- a/astrbot/core/star/star_manager.py +++ b/astrbot/core/star/star_manager.py @@ -1295,8 +1295,7 @@ class PluginManager: star_registry.append(metadata) # 禁用/启用插件 - if metadata.module_path in inactivated_plugins: - metadata.activated = False + metadata.activated = metadata.module_path not in inactivated_plugins # Plugin logo path if os.path.exists(logo_path): @@ -1934,7 +1933,12 @@ class PluginManager: for func_tool in self._iter_plugin_llm_tools(plugin.module_path): func_tool.active = func_tool.name not in inactivated_llm_tools - await self.reload(plugin_name) + success, error = await self.reload(plugin_name) + if not success: + raise Exception(error or f"插件 {plugin_name} 启用失败。") + current_plugin = self.context.get_registered_star(plugin_name) + if current_plugin: + current_plugin.activated = True async def install_plugin_from_file( self, zip_file_path: str, ignore_version_check: bool = False diff --git a/dashboard/src/views/extension/useExtensionPage.js b/dashboard/src/views/extension/useExtensionPage.js index a358cebb9..cb5984385 100644 --- a/dashboard/src/views/extension/useExtensionPage.js +++ b/dashboard/src/views/extension/useExtensionPage.js @@ -1043,9 +1043,12 @@ export const useExtensionPage = () => { }; const pluginOn = async (extension) => { + const previousActivated = extension.activated; + extension.activated = true; try { const res = await pluginApi.setEnabled(extension.name, true); if (res.data.status === "error") { + extension.activated = previousActivated; toast(res.data.message, "error"); return; } @@ -1054,20 +1057,25 @@ export const useExtensionPage = () => { await checkAndPromptConflicts(); } catch (err) { + extension.activated = previousActivated; toast(err, "error"); } }; const pluginOff = async (extension) => { + const previousActivated = extension.activated; + extension.activated = false; try { const res = await pluginApi.setEnabled(extension.name, false); if (res.data.status === "error") { + extension.activated = previousActivated; toast(res.data.message, "error"); return; } toast(res.data.message, "success"); - getExtensions(); + await getExtensions(); } catch (err) { + extension.activated = previousActivated; toast(err, "error"); } }; diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index 13d256b7f..e51f9f35a 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -2,6 +2,7 @@ import asyncio import json import os from pathlib import Path +from types import ModuleType from typing import Any, cast import pytest @@ -1981,6 +1982,89 @@ async def test_uninstall_failed_plugin_without_plugin_id_in_record( # --- reload + deactivated plugin regression tests --- +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("inactivated_plugins", "expected_activated"), + [ + ([], True), + (["data.plugins.demo_plugin.main"], False), + ], +) +async def test_load_syncs_existing_metadata_activation_from_preferences( + plugin_manager_pm: PluginManager, + monkeypatch, + inactivated_plugins: list[str], + expected_activated: bool, +): + """Existing plugin metadata activation follows persisted preferences.""" + _clear_star_runtime_state() + plugin_name = "demo_plugin" + module_path = f"data.plugins.{plugin_name}.main" + metadata = star_manager_module.StarMetadata( + name=plugin_name, + author="AstrBot Team", + desc="Demo plugin", + version="1.0.0", + root_dir_name=plugin_name, + module_path=module_path, + activated=False, + ) + star_manager_module.star_map[module_path] = metadata + star_manager_module.star_registry.append(metadata) + preferences = { + "inactivated_plugins": inactivated_plugins, + "inactivated_llm_tools": [], + "alter_cmd": {}, + } + + async def mock_global_get(key, default=None): + return preferences.get(key, default) + + async def mock_import_plugin_with_dependency_recovery( + path, + module_str, + root_dir_name, + requirements_path, + *, + reserved=False, + ): + del module_str, root_dir_name, requirements_path, reserved + assert path == module_path + return ModuleType(module_path) + + async def mock_sync_command_configs(): + return None + + monkeypatch.setattr(star_manager_module.sp, "global_get", mock_global_get) + monkeypatch.setattr( + plugin_manager_pm, + "_get_plugin_modules", + lambda: [{"pname": plugin_name, "module": "main"}], + ) + monkeypatch.setattr( + plugin_manager_pm, + "_import_plugin_with_dependency_recovery", + mock_import_plugin_with_dependency_recovery, + ) + monkeypatch.setattr(plugin_manager_pm, "_load_plugin_metadata", lambda **_: None) + monkeypatch.setattr( + star_manager_module, + "sync_command_configs", + mock_sync_command_configs, + ) + + try: + success, error = await plugin_manager_pm.load( + specified_module_path=module_path, + ) + + assert success is True + assert error is None + assert metadata.activated is expected_activated + finally: + _clear_star_runtime_state() + + @pytest.mark.asyncio async def test_reload_deactivated_plugin_preserves_tools( plugin_manager_pm: PluginManager, monkeypatch @@ -2156,9 +2240,6 @@ async def test_turn_on_plugin_after_deactivated_reload_reactivates_tools( async def mock_reload(plugin_name_arg): assert plugin_name_arg == plugin_name - # Simulate what load() does: re-register with activated=True - # since it's no longer in inactivated_plugins - plugin.activated = True return True, None monkeypatch.setattr(star_manager_module.sp, "global_get", mock_global_get)