Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,18 @@ async def initialize(self) -> None:
# 初始化插件管理器
self.plugin_manager = PluginManager(self.star_context, self.astrbot_config)

# 扫描、注册插件、实例化插件类
await self.plugin_manager.reload()

# 根据配置实例化各个 Provider
self._default_chat_provider_warning_emitted = False
await self.provider_manager.initialize()
# Discover plugin-provided Provider adapters, initialize configured
# Providers, activate plugin instances, then load any adapters registered
# from their initialize() methods.
await self.plugin_manager.initialize_plugins(
before_plugin_activation=self.provider_manager.initialize,
after_plugin_activation=(
self.provider_manager.initialize_pending_registered_providers
),
)

# Check after plugin activation so the warning reflects final state.
self._warn_about_unset_default_chat_provider()

await self.kb_manager.initialize()
Expand Down
167 changes: 167 additions & 0 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,91 @@ async def _init_mcp_clients_bg() -> None:
name="provider-manager:mcp-init",
)

async def initialize_pending_registered_providers(self) -> None:
"""Load configured Providers registered during plugin activation.

This pass is intentionally incremental: an existing Provider instance is
never reloaded or initialized twice. A failure in one newly registered
adapter does not prevent other pending Providers from loading.

Returns:
None.
"""
loaded_provider_ids = []
for provider_config in self.providers_config:
provider_id = provider_config.get("id")
provider_type = provider_config.get("type")
if (
not provider_config.get("enable", False)
or not isinstance(provider_id, str)
or not provider_id
or provider_id in self.inst_map
or not isinstance(provider_type, str)
or provider_type not in provider_cls_map
or provider_config.get("provider_type") == "agent_runner"
):
continue

try:
await self.load_provider(provider_config)
except Exception as e:
logger.error(traceback.format_exc())
logger.error(e)
continue
if provider_id in self.inst_map:
loaded_provider_ids.append(provider_id)

if not loaded_provider_ids:
return

selected_provider_id = await sp.get_async(
key="curr_provider",
default=self.default_chat_provider_id,
scope="global",
scope_id="global",
)
selected_stt_provider_id = await sp.get_async(
key="curr_provider_stt",
default=self.provider_stt_settings.get("provider_id"),
scope="global",
scope_id="global",
)
selected_tts_provider_id = await sp.get_async(
key="curr_provider_tts",
default=self.provider_tts_settings.get("provider_id"),
scope="global",
scope_id="global",
)

selected_provider = (
self.inst_map.get(selected_provider_id)
if isinstance(selected_provider_id, str)
else None
)
if isinstance(selected_provider, Provider):
self.curr_provider_inst = selected_provider

selected_stt_provider = (
self.inst_map.get(selected_stt_provider_id)
if isinstance(selected_stt_provider_id, str)
else None
)
if isinstance(selected_stt_provider, STTProvider):
self.curr_stt_provider_inst = selected_stt_provider

selected_tts_provider = (
self.inst_map.get(selected_tts_provider_id)
if isinstance(selected_tts_provider_id, str)
else None
)
if isinstance(selected_tts_provider, TTSProvider):
self.curr_tts_provider_inst = selected_tts_provider

logger.info(
"Initialized Providers registered during plugin activation: %s",
loaded_provider_ids,
)

def dynamic_import_provider(self, type: str) -> None:
"""动态导入提供商适配器模块

Expand Down Expand Up @@ -865,6 +950,88 @@ async def reload(self, provider_config: dict) -> None:
def get_insts(self):
return self.provider_insts

@staticmethod
def _provider_adapter_type(provider_inst: Providers) -> str | None:
"""Return the configured adapter type for a Provider instance.

Args:
provider_inst: Provider instance whose configuration is inspected.

Returns:
Configured adapter type, or ``None`` when unavailable.
"""
provider_config = getattr(provider_inst, "provider_config", None)
provider_type = (
provider_config.get("type") if isinstance(provider_config, dict) else None
)
return provider_type if isinstance(provider_type, str) else None

def _restore_current_provider_fallbacks(self) -> None:
"""Select available current Providers after stale instances are removed.

Returns:
None.
"""
if self.curr_provider_inst not in self.provider_insts:
self.curr_provider_inst = (
self.provider_insts[0] if self.provider_insts else None
)
if self.curr_stt_provider_inst not in self.stt_provider_insts:
self.curr_stt_provider_inst = (
self.stt_provider_insts[0] if self.stt_provider_insts else None
)
if self.curr_tts_provider_inst not in self.tts_provider_insts:
self.curr_tts_provider_inst = (
self.tts_provider_insts[0] if self.tts_provider_insts else None
)

async def terminate_unregistered_providers(self) -> list[str]:
"""Terminate instances whose adapter registration was rolled back.

Returns:
IDs of Provider instances removed from the runtime.
"""
stale_provider_ids = [
provider_id
for provider_id, provider_inst in list(self.inst_map.items())
if (
(provider_type := self._provider_adapter_type(provider_inst))
is not None
and provider_type not in provider_cls_map
)
]

for provider_id in stale_provider_ids:
provider_inst = self.inst_map.get(provider_id)
try:
await self.terminate_provider(provider_id)
except Exception:
logger.exception(
"Failed to terminate rolled-back Provider adapter %s cleanly.",
provider_id,
)
finally:
if provider_inst is not None:
for provider_instances in (
self.provider_insts,
self.stt_provider_insts,
self.tts_provider_insts,
self.embedding_provider_insts,
self.rerank_provider_insts,
):
if provider_inst in provider_instances:
provider_instances.remove(provider_inst)
if provider_inst == self.curr_provider_inst:
self.curr_provider_inst = None
if provider_inst == self.curr_stt_provider_inst:
self.curr_stt_provider_inst = None
if provider_inst == self.curr_tts_provider_inst:
self.curr_tts_provider_inst = None
self.inst_map.pop(provider_id, None)

self._restore_current_provider_fallbacks()
return stale_provider_ids

async def terminate_provider(self, provider_id: str) -> None:
if provider_id in self.inst_map:
logger.info(
Expand Down
31 changes: 31 additions & 0 deletions astrbot/core/provider/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,34 @@ def decorator(cls):
return cls

return decorator


def unregister_provider_adapters_by_module(module_path_prefix: str) -> list[str]:
"""Unregister provider adapters declared by one plugin module tree.

Args:
module_path_prefix: Complete plugin module prefix to unregister.

Returns:
Adapter type names removed from the registries.
"""
to_remove = []
for metadata in provider_registry:
module_path = getattr(metadata.cls_type, "__module__", None)
if module_path and (
module_path == module_path_prefix
or module_path.startswith(f"{module_path_prefix}.")
):
to_remove.append(metadata)

for metadata in to_remove:
provider_registry.remove(metadata)
if provider_cls_map.get(metadata.type) is metadata:
provider_cls_map.pop(metadata.type, None)
logger.debug(
"Model provider unregistered: %s (from module %s)",
metadata.type,
getattr(metadata.cls_type, "__module__", None),
)

return [metadata.type for metadata in to_remove]
Loading
Loading