diff --git a/astrbot/api/provider/__init__.py b/astrbot/api/provider/__init__.py index f62b340f8d..817e8c812d 100644 --- a/astrbot/api/provider/__init__.py +++ b/astrbot/api/provider/__init__.py @@ -1,11 +1,18 @@ from astrbot.core.db.po import Personality -from astrbot.core.provider import Provider, STTProvider +from astrbot.core.provider import ( + EmbeddingProvider, + Provider, + RerankProvider, + STTProvider, + TTSProvider, +) from astrbot.core.provider.entities import ( LLMResponse, ProviderMetaData, ProviderRequest, ProviderType, ) +from astrbot.core.provider.register import register_provider_adapter __all__ = [ "LLMResponse", @@ -15,4 +22,8 @@ "ProviderRequest", "ProviderType", "STTProvider", + "TTSProvider", + "EmbeddingProvider", + "RerankProvider", + "register_provider_adapter", ] diff --git a/astrbot/core/provider/__init__.py b/astrbot/core/provider/__init__.py index 812e021715..22a19afdd9 100644 --- a/astrbot/core/provider/__init__.py +++ b/astrbot/core/provider/__init__.py @@ -1,4 +1,17 @@ from .entities import ProviderMetaData -from .provider import Provider, STTProvider +from .provider import ( + EmbeddingProvider, + Provider, + RerankProvider, + STTProvider, + TTSProvider, +) -__all__ = ["Provider", "ProviderMetaData", "STTProvider"] +__all__ = [ + "Provider", + "ProviderMetaData", + "STTProvider", + "TTSProvider", + "EmbeddingProvider", + "RerankProvider", +] diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 20c5a7947d..7a59c8121c 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -58,6 +58,10 @@ class ProviderMetaData(ProviderMeta): """the default configuration template of the provider adapter""" provider_display_name: str | None = None """the display name of the provider shown in the WebUI configuration page; if empty, the type is used""" + i18n_resources: dict[str, dict] | None = None + """the i18n resource data of the provider adapter, such as {"zh-CN": {...}, "en-US": {...}}""" + config_metadata: dict | None = None + """the configuration metadata used by WebUI to generate provider source forms""" @dataclass diff --git a/astrbot/core/provider/register.py b/astrbot/core/provider/register.py index 3ad83784ec..62059d70a4 100644 --- a/astrbot/core/provider/register.py +++ b/astrbot/core/provider/register.py @@ -17,6 +17,8 @@ def register_provider_adapter( provider_type: ProviderType = ProviderType.CHAT_COMPLETION, default_config_tmpl: dict | None = None, provider_display_name: str | None = None, + i18n_resources: dict[str, dict] | None = None, + config_metadata: dict | None = None, ): """用于注册平台适配器的带参装饰器""" @@ -44,6 +46,8 @@ def decorator(cls): cls_type=cls, default_config_tmpl=default_config_tmpl, provider_display_name=provider_display_name, + i18n_resources=i18n_resources, + config_metadata=config_metadata, ) provider_registry.append(pm) provider_cls_map[provider_type_name] = pm diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bcd7e075c7..406ce524b2 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -508,24 +508,15 @@ async def update_provider_source(self): return Response().ok(message="更新 provider source 成功").__dict__ async def get_provider_template(self): - provider_metadata = ConfigMetadataI18n.convert_to_i18n_keys( - { - "provider_group": { - "metadata": { - "provider": CONFIG_METADATA_2["provider_group"]["metadata"][ - "provider" - ] - } - } - } + config_schema, provider_i18n_translations, provider_type_metadata = ( + self._build_provider_schema_with_i18n() ) - config_schema = { - "provider": provider_metadata["provider_group"]["metadata"]["provider"] - } data = { "config_schema": config_schema, "providers": astrbot_config["provider"], "provider_sources": astrbot_config["provider_sources"], + "provider_i18n_translations": provider_i18n_translations, + "provider_type_metadata": provider_type_metadata, } return Response().ok(data=data).__dict__ @@ -1435,6 +1426,87 @@ def _inject_platform_metadata_with_i18n( platform_items_to_inject ) + def _apply_dynamic_i18n_keys(self, items: dict, i18n_prefix: str): + """Replace configurable text fields with dynamic i18n keys recursively.""" + for field_key, field_value in items.items(): + if not isinstance(field_value, dict): + continue + + field_prefix = f"{i18n_prefix}.{field_key}" + for key in ("description", "hint", "labels", "name"): + if key in field_value: + field_value[key] = f"{field_prefix}.{key}" + + if isinstance(field_value.get("items"), dict): + self._apply_dynamic_i18n_keys(field_value["items"], field_prefix) + + if isinstance(field_value.get("template_schema"), dict): + self._apply_dynamic_i18n_keys( + field_value["template_schema"], + f"{field_prefix}.template_schema", + ) + + def _collect_provider_i18n_translations( + self, provider, provider_i18n_translations: dict + ): + """Collect runtime i18n resources for a provider adapter.""" + if not provider.i18n_resources: + return + + for lang, lang_data in provider.i18n_resources.items(): + provider_i18n_translations.setdefault(lang, {}).setdefault( + "provider_group", {} + ).setdefault("provider", {})[provider.type] = lang_data + + def _build_provider_schema_with_i18n(self) -> tuple[dict, dict, dict]: + provider_metadata = ConfigMetadataI18n.convert_to_i18n_keys( + { + "provider_group": { + "metadata": { + "provider": copy.deepcopy( + CONFIG_METADATA_2["provider_group"]["metadata"]["provider"] + ) + } + } + } + ) + provider_i18n_translations = {} + provider_type_metadata = {} + + provider_default_tmpl = provider_metadata["provider_group"]["metadata"][ + "provider" + ]["config_template"] + for provider in provider_registry: + self._collect_provider_i18n_translations( + provider, provider_i18n_translations + ) + + if provider.default_config_tmpl: + provider_default_tmpl[provider.type] = copy.deepcopy( + provider.default_config_tmpl + ) + display_name = provider.provider_display_name or provider.type + if provider.i18n_resources: + display_name = f"provider_group.provider.{provider.type}.name" + provider_default_tmpl[provider.type]["_display_name"] = display_name + + if provider.config_metadata: + provider_items_to_inject = copy.deepcopy(provider.config_metadata) + if provider.i18n_resources: + self._apply_dynamic_i18n_keys( + provider_items_to_inject, + f"provider_group.provider.{provider.type}", + ) + provider_type_metadata[provider.type] = { + "items": provider_items_to_inject + } + + return ( + {"provider": provider_metadata["provider_group"]["metadata"]["provider"]}, + provider_i18n_translations, + provider_type_metadata, + ) + async def _get_astrbot_config(self): config = self.config metadata = copy.deepcopy(CONFIG_METADATA_2) @@ -1484,17 +1556,17 @@ async def _get_astrbot_config(self): await asyncio.gather(*logo_registration_tasks, return_exceptions=True) # 服务提供商的默认配置模板注入 - provider_default_tmpl = metadata["provider_group"]["metadata"]["provider"][ - "config_template" - ] - for provider in provider_registry: - if provider.default_config_tmpl: - provider_default_tmpl[provider.type] = provider.default_config_tmpl + provider_schema, provider_i18n_translations, provider_type_metadata = ( + self._build_provider_schema_with_i18n() + ) + metadata["provider_group"]["metadata"]["provider"] = provider_schema["provider"] return { "metadata": metadata, "config": config, "platform_i18n_translations": platform_i18n_translations, + "provider_i18n_translations": provider_i18n_translations, + "provider_type_metadata": provider_type_metadata, } async def _get_plugin_config(self, plugin_name: str): diff --git a/dashboard/src/components/chat/ProviderConfigDialog.vue b/dashboard/src/components/chat/ProviderConfigDialog.vue index 51ff37677f..f4c05728bf 100644 --- a/dashboard/src/components/chat/ProviderConfigDialog.vue +++ b/dashboard/src/components/chat/ProviderConfigDialog.vue @@ -42,7 +42,7 @@