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..0d04601d6c 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", + "EmbeddingProvider", + "RerankProvider", + "TTSProvider", +] diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index 20c5a7947d..cea163ac81 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 resources of the provider adapter""" + config_metadata: dict | None = None + """the config metadata of the provider adapter""" @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..736d90950e 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -519,13 +519,30 @@ async def get_provider_template(self): } } ) - config_schema = { - "provider": provider_metadata["provider_group"]["metadata"]["provider"] + provider_i18n_translations = {} + provider_schema = provider_metadata["provider_group"]["metadata"]["provider"] + config_schema = {"provider": provider_schema} + + provider_default_tmpl = config_schema["provider"]["config_template"] + provider_schema_wrapper = { + "provider_group": {"metadata": {"provider": provider_schema}} } + for provider in provider_registry: + if provider.default_config_tmpl: + provider_default_tmpl[provider.type] = copy.deepcopy( + provider.default_config_tmpl + ) + if provider.config_metadata: + self._inject_provider_metadata_with_i18n( + provider, + provider_schema_wrapper, + provider_i18n_translations, + ) data = { "config_schema": config_schema, "providers": astrbot_config["provider"], "provider_sources": astrbot_config["provider_sources"], + "provider_i18n_translations": provider_i18n_translations, } return Response().ok(data=data).__dict__ @@ -1411,6 +1428,33 @@ async def _register_platform_logo(self, platform, platform_default_tmpl) -> None f"Unexpected error registering logo for platform {platform.name}: {e}", ) + def _rewrite_metadata_i18n_keys( + self, metadata: dict, i18n_prefix: str, field_path: str = "" + ): + """Rewrite metadata text fields to dynamic i18n keys recursively.""" + for field_key, field_value in metadata.items(): + if not isinstance(field_value, dict): + continue + + current_path = f"{field_path}.{field_key}" if field_path else field_key + for key in ("description", "hint", "labels", "name"): + if key in field_value: + field_value[key] = f"{i18n_prefix}.{current_path}.{key}" + + if "items" in field_value and isinstance(field_value["items"], dict): + self._rewrite_metadata_i18n_keys( + field_value["items"], i18n_prefix, current_path + ) + + if "template_schema" in field_value and isinstance( + field_value["template_schema"], dict + ): + self._rewrite_metadata_i18n_keys( + field_value["template_schema"], + i18n_prefix, + f"{current_path}.template_schema", + ) + def _inject_platform_metadata_with_i18n( self, platform, metadata, platform_i18n_translations: dict ): @@ -1426,15 +1470,33 @@ def _inject_platform_metadata_with_i18n( "platform_group", {} ).setdefault("platform", {})[platform.name] = lang_data - for field_key, field_value in platform_items_to_inject.items(): - for key in ("description", "hint", "labels"): - if key in field_value: - field_value[key] = f"{i18n_prefix}.{field_key}.{key}" + self._rewrite_metadata_i18n_keys(platform_items_to_inject, i18n_prefix) metadata["platform_group"]["metadata"]["platform"]["items"].update( platform_items_to_inject ) + def _inject_provider_metadata_with_i18n( + self, provider, metadata, provider_i18n_translations: dict + ): + """Inject provider config metadata and rewrite dynamic i18n keys.""" + metadata["provider_group"]["metadata"]["provider"].setdefault("items", {}) + provider_items_to_inject = copy.deepcopy(provider.config_metadata) + + if provider.i18n_resources: + i18n_prefix = f"provider_group.provider.{provider.type}" + + for lang, lang_data in provider.i18n_resources.items(): + provider_i18n_translations.setdefault(lang, {}).setdefault( + "provider_group", {} + ).setdefault("provider", {})[provider.type] = lang_data + + self._rewrite_metadata_i18n_keys(provider_items_to_inject, i18n_prefix) + + metadata["provider_group"]["metadata"]["provider"]["items"].update( + provider_items_to_inject + ) + async def _get_astrbot_config(self): config = self.config metadata = copy.deepcopy(CONFIG_METADATA_2) @@ -1487,14 +1549,22 @@ async def _get_astrbot_config(self): provider_default_tmpl = metadata["provider_group"]["metadata"]["provider"][ "config_template" ] + provider_i18n_translations = {} for provider in provider_registry: if provider.default_config_tmpl: - provider_default_tmpl[provider.type] = provider.default_config_tmpl + provider_default_tmpl[provider.type] = copy.deepcopy( + provider.default_config_tmpl + ) + if provider.config_metadata: + self._inject_provider_metadata_with_i18n( + provider, metadata, provider_i18n_translations + ) return { "metadata": metadata, "config": config, "platform_i18n_translations": platform_i18n_translations, + "provider_i18n_translations": provider_i18n_translations, } async def _get_plugin_config(self, plugin_name: str): diff --git a/dashboard/src/composables/useProviderSources.ts b/dashboard/src/composables/useProviderSources.ts index 2891d8976c..2bf299882c 100644 --- a/dashboard/src/composables/useProviderSources.ts +++ b/dashboard/src/composables/useProviderSources.ts @@ -1,8 +1,9 @@ -import { ref, computed, onMounted, nextTick, watch } from 'vue' +import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue' import axios from 'axios' import { getProviderIcon } from '@/utils/providerUtils' import { askForConfirmation as askForConfirmationDialog, useConfirmDialog } from '@/utils/confirmDialog' import { normalizeTextInput } from '@/utils/inputValue' +import { mergeDynamicTranslations } from '@/i18n/composables' export interface UseProviderSourcesOptions { defaultTab?: string @@ -45,6 +46,16 @@ export function useProviderSources(options: UseProviderSourcesOptions) { return askForConfirmationDialog(message, confirmDialog) } + function applyProviderDynamicI18n(providerI18nTranslations?: Record) { + if (providerI18nTranslations && typeof providerI18nTranslations === 'object') { + mergeDynamicTranslations('features.config-metadata', providerI18nTranslations) + } + } + + function handleLocaleChange() { + void loadProviderTemplate() + } + // ===== State ===== const config = ref>({}) const metadata = ref>({}) @@ -628,6 +639,7 @@ export function useProviderSources(options: UseProviderSourcesOptions) { try { const response = await axios.get('/api/config/provider/template') if (response.data.status === 'ok') { + applyProviderDynamicI18n(response.data.data.provider_i18n_translations) configSchema.value = response.data.data.config_schema || {} if (configSchema.value.provider?.config_template) { providerTemplates.value = configSchema.value.provider.config_template @@ -645,9 +657,14 @@ export function useProviderSources(options: UseProviderSourcesOptions) { } onMounted(async () => { + window.addEventListener('astrbot-locale-changed', handleLocaleChange) await loadProviderTemplate() }) + onBeforeUnmount(() => { + window.removeEventListener('astrbot-locale-changed', handleLocaleChange) + }) + return { // state config,