From 4bcfd9fa7be1198e91f38c185178d0010062152b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:55:20 +0000 Subject: [PATCH 1/6] Initial plan From f0a921d22fd365671029d38a793c643e3fffd490 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:00:38 +0000 Subject: [PATCH 2/6] Fix provider duplication issue by making config properties dynamic Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com> --- astrbot/core/provider/manager.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 5fc5a4b5e..533f1a58b 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -26,11 +26,6 @@ def __init__( ): self.persona_mgr = persona_mgr self.acm = acm - config = acm.confs["default"] - self.providers_config: list = config["provider"] - self.provider_settings: dict = config["provider_settings"] - self.provider_stt_settings: dict = config.get("provider_stt_settings", {}) - self.provider_tts_settings: dict = config.get("provider_tts_settings", {}) # 人格相关属性,v4.0.0 版本后被废弃,推荐使用 PersonaManager self.default_persona_name = persona_mgr.default_persona @@ -75,6 +70,30 @@ def selected_default_persona(self): """动态获取最新的默认选中 persona。已弃用,请使用 context.persona_mgr.get_default_persona_v3()""" return self.persona_mgr.selected_default_persona_v3 + @property + def providers_config(self) -> list: + """动态获取最新的 provider 配置列表""" + config = self.acm.confs["default"] + return config["provider"] + + @property + def provider_settings(self) -> dict: + """动态获取最新的 provider_settings 配置""" + config = self.acm.confs["default"] + return config["provider_settings"] + + @property + def provider_stt_settings(self) -> dict: + """动态获取最新的 provider_stt_settings 配置""" + config = self.acm.confs["default"] + return config.get("provider_stt_settings", {}) + + @property + def provider_tts_settings(self) -> dict: + """动态获取最新的 provider_tts_settings 配置""" + config = self.acm.confs["default"] + return config.get("provider_tts_settings", {}) + async def set_provider( self, provider_id: str, From 967f4b81d13043d7621e17b98364a5ca9d1bf9a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:03:34 +0000 Subject: [PATCH 3/6] Add tests for dynamic provider config properties Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com> --- tests/test_provider_manager.py | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/test_provider_manager.py diff --git a/tests/test_provider_manager.py b/tests/test_provider_manager.py new file mode 100644 index 000000000..2ce1a2392 --- /dev/null +++ b/tests/test_provider_manager.py @@ -0,0 +1,131 @@ +"""Tests for ProviderManager dynamic config properties.""" + +from unittest.mock import MagicMock + +import pytest + +from astrbot.core.astrbot_config_mgr import AstrBotConfigManager +from astrbot.core.config.astrbot_config import AstrBotConfig +from astrbot.core.db.sqlite import SQLiteDatabase +from astrbot.core.persona_mgr import PersonaManager +from astrbot.core.provider.manager import ProviderManager + + +@pytest.fixture +def provider_manager_fixture(tmp_path): + """Provides a ProviderManager instance for testing dynamic config properties.""" + # Create temporary database + temp_db_path = tmp_path / "test_db.db" + db = SQLiteDatabase(str(temp_db_path)) + + # Create mock config manager with initial config + acm = MagicMock(spec=AstrBotConfigManager) + config = AstrBotConfig() + config["provider"] = [ + {"id": "provider1", "type": "openai_chat_completion", "enable": True} + ] + config["provider_settings"] = {"default_provider_id": "provider1"} + config["provider_stt_settings"] = {"provider_id": "stt1"} + config["provider_tts_settings"] = {"provider_id": "tts1"} + acm.confs = {"default": config} + + # Create mock persona manager + persona_mgr = MagicMock(spec=PersonaManager) + persona_mgr.default_persona = "default_persona" + persona_mgr.persona_v3_config = [] + persona_mgr.personas_v3 = [] + persona_mgr.selected_default_persona_v3 = None + + # Create ProviderManager instance + manager = ProviderManager(acm, db, persona_mgr) + return manager, acm, config + + +def test_providers_config_is_dynamic(provider_manager_fixture): + """Test that providers_config property dynamically reflects config changes.""" + manager, acm, config = provider_manager_fixture + + # Initial state + assert len(manager.providers_config) == 1 + assert manager.providers_config[0]["id"] == "provider1" + + # Modify the config by adding a new provider + config["provider"].append( + {"id": "provider2", "type": "openai_chat_completion", "enable": True} + ) + + # Verify the property reflects the change + assert len(manager.providers_config) == 2 + assert manager.providers_config[1]["id"] == "provider2" + + +def test_provider_settings_is_dynamic(provider_manager_fixture): + """Test that provider_settings property dynamically reflects config changes.""" + manager, acm, config = provider_manager_fixture + + # Initial state + assert manager.provider_settings["default_provider_id"] == "provider1" + + # Modify the config + config["provider_settings"]["default_provider_id"] = "provider2" + + # Verify the property reflects the change + assert manager.provider_settings["default_provider_id"] == "provider2" + + +def test_provider_stt_settings_is_dynamic(provider_manager_fixture): + """Test that provider_stt_settings property dynamically reflects config changes.""" + manager, acm, config = provider_manager_fixture + + # Initial state + assert manager.provider_stt_settings["provider_id"] == "stt1" + + # Modify the config + config["provider_stt_settings"]["provider_id"] = "stt2" + + # Verify the property reflects the change + assert manager.provider_stt_settings["provider_id"] == "stt2" + + +def test_provider_tts_settings_is_dynamic(provider_manager_fixture): + """Test that provider_tts_settings property dynamically reflects config changes.""" + manager, acm, config = provider_manager_fixture + + # Initial state + assert manager.provider_tts_settings["provider_id"] == "tts1" + + # Modify the config + config["provider_tts_settings"]["provider_id"] = "tts2" + + # Verify the property reflects the change + assert manager.provider_tts_settings["provider_id"] == "tts2" + + +def test_multiple_provider_additions(provider_manager_fixture): + """Test that multiple provider additions are properly reflected. + + This test simulates the bug scenario where adding providers would + not be reflected in the ProviderManager's providers_config. + """ + manager, acm, config = provider_manager_fixture + + # Initial state: one provider + assert len(manager.providers_config) == 1 + + # Simulate adding multiple providers as the WebUI would do + new_providers = [ + {"id": "deepseek-st", "type": "openai_chat_completion", "enable": True}, + {"id": "deepseek-st_copy", "type": "openai_chat_completion", "enable": True}, + {"id": "deepseek-st_copy2", "type": "openai_chat_completion", "enable": True}, + ] + + for provider in new_providers: + config["provider"].append(provider) + + # Verify all providers are visible through the property + assert len(manager.providers_config) == 4 + provider_ids = [p["id"] for p in manager.providers_config] + assert "provider1" in provider_ids + assert "deepseek-st" in provider_ids + assert "deepseek-st_copy" in provider_ids + assert "deepseek-st_copy2" in provider_ids From 1686e6ee7608ec3351412aa9f2c88d96039fb600 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sun, 2 Nov 2025 02:24:19 +0800 Subject: [PATCH 4/6] Update astrbot/core/provider/manager.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- astrbot/core/provider/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 533f1a58b..da71549dc 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -80,7 +80,7 @@ def providers_config(self) -> list: def provider_settings(self) -> dict: """动态获取最新的 provider_settings 配置""" config = self.acm.confs["default"] - return config["provider_settings"] + return config.get("provider_settings", {}) @property def provider_stt_settings(self) -> dict: From ffbed369e338913fc25e01a81b9ca91cdecf7e43 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Sun, 2 Nov 2025 02:31:14 +0800 Subject: [PATCH 5/6] Update astrbot/core/provider/manager.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- astrbot/core/provider/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index da71549dc..dc851bdaa 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -74,7 +74,7 @@ def selected_default_persona(self): def providers_config(self) -> list: """动态获取最新的 provider 配置列表""" config = self.acm.confs["default"] - return config["provider"] + return config.get("provider", []) @property def provider_settings(self) -> dict: From 06c4a110060c26830c6a32df80e320f1815f72d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:13:31 +0000 Subject: [PATCH 6/6] Extract repeated config access to _default_config property Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com> --- astrbot/core/provider/manager.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index dc851bdaa..6d9c9739c 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -70,29 +70,30 @@ def selected_default_persona(self): """动态获取最新的默认选中 persona。已弃用,请使用 context.persona_mgr.get_default_persona_v3()""" return self.persona_mgr.selected_default_persona_v3 + @property + def _default_config(self) -> dict: + """私有属性,获取默认配置""" + return self.acm.confs["default"] + @property def providers_config(self) -> list: """动态获取最新的 provider 配置列表""" - config = self.acm.confs["default"] - return config.get("provider", []) + return self._default_config.get("provider", []) @property def provider_settings(self) -> dict: """动态获取最新的 provider_settings 配置""" - config = self.acm.confs["default"] - return config.get("provider_settings", {}) + return self._default_config.get("provider_settings", {}) @property def provider_stt_settings(self) -> dict: """动态获取最新的 provider_stt_settings 配置""" - config = self.acm.confs["default"] - return config.get("provider_stt_settings", {}) + return self._default_config.get("provider_stt_settings", {}) @property def provider_tts_settings(self) -> dict: """动态获取最新的 provider_tts_settings 配置""" - config = self.acm.confs["default"] - return config.get("provider_tts_settings", {}) + return self._default_config.get("provider_tts_settings", {}) async def set_provider( self,