Skip to content
Closed
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
13 changes: 12 additions & 1 deletion astrbot/api/provider/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -15,4 +22,8 @@
"ProviderRequest",
"ProviderType",
"STTProvider",
"TTSProvider",
"EmbeddingProvider",
"RerankProvider",
"register_provider_adapter",
]
17 changes: 15 additions & 2 deletions astrbot/core/provider/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
4 changes: 4 additions & 0 deletions astrbot/core/provider/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/provider/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
"""用于注册平台适配器的带参装饰器"""

Expand Down Expand Up @@ -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
Expand Down
110 changes: 91 additions & 19 deletions astrbot/dashboard/routes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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}",
)
Comment on lines +1495 to +1499

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve provider metadata fallback when i18n locale is absent

This rewrites dynamic provider metadata to i18n keys whenever i18n_resources is present, but it does not keep a readable fallback for locales that the plugin does not provide. In that case (for example, plugin only ships zh-CN while UI is en-US), the frontend receives key paths without corresponding translations and renders raw strings like provider_group.provider.xxx.field.hint, making provider-source forms hard to use. Keep original metadata text as fallback (or gate key-rewriting on available locale data) so partially localized providers remain usable.

Useful? React with 👍 / 👎.

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)
Expand Down Expand Up @@ -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):
Expand Down
5 changes: 3 additions & 2 deletions dashboard/src/components/chat/ProviderConfigDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

<!-- 基础配置 -->
<div class="mb-4">
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="configSchema"
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="providerSourceSchema"
metadataKey="provider" :is-editing="true" />
</div>

Expand All @@ -54,7 +54,7 @@
</v-expansion-panel-title>
<v-expansion-panel-text>
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig"
:metadata="configSchema" metadataKey="provider" :is-editing="true" />
:metadata="providerSourceSchema" metadataKey="provider" :is-editing="true" />
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
Expand Down Expand Up @@ -174,6 +174,7 @@ const {
testingProviders,
isSourceModified,
configSchema,
providerSourceSchema,
manualModelId,
modelSearch,
availableSourceTypes,
Expand Down
45 changes: 40 additions & 5 deletions dashboard/src/components/shared/AstrBotConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,21 @@ const translateIfKey = (value) => {
}

const filteredIterable = computed(() => {
if (!props.iterable) return {}
const { hint, ...rest } = props.iterable
return rest
const result = {}
const metadataItems = props.metadata?.[props.metadataKey]?.items || {}
const iterable = props.iterable || {}

for (const key of Object.keys(metadataItems)) {
if (key === 'hint') continue
result[key] = iterable[key]
}
Comment on lines +50 to +53

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Render only intended keys in AstrBotConfig forms

This loop now prioritizes all metadata keys instead of the keys actually present in iterable, which breaks callers that intentionally pass a subset object (for example basicSourceConfig/advancedSourceConfig in the provider source UI). Because provider metadata contains fields for many provider types, the form will render unrelated inputs and can push users into editing or saving irrelevant settings; previously only configured keys were shown. Restricting rendered keys to the provided iterable (or making metadata-driven expansion opt-in) avoids this regression.

Useful? React with 👍 / 👎.


for (const [key, value] of Object.entries(iterable)) {
if (key === 'hint' || key in result) continue
result[key] = value
}

return result
})

const providerHint = computed(() => {
Expand Down Expand Up @@ -155,6 +167,28 @@ function getItemPath(key) {
return props.pathPrefix ? `${props.pathPrefix}.${key}` : key
}

function ensureStructuredValue(key, itemMeta) {
if (!props.iterable || typeof props.iterable !== 'object') {
return undefined
}

if (props.iterable[key] !== undefined && props.iterable[key] !== null) {
return props.iterable[key]
}

if (itemMeta?.type === 'object' || itemMeta?.type === 'dict') {
props.iterable[key] = {}
return props.iterable[key]
}

if (itemMeta?.type === 'list' || itemMeta?.type === 'template_list') {
props.iterable[key] = []
return props.iterable[key]
}

return props.iterable[key]
}
Comment on lines +170 to +190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

ensureStructuredValue 函数直接修改了 iterable 这个 prop。在 Vue 中,直接修改 prop 是一种反模式,可能会导致不可预期的行为,并让组件的逻辑变得难以理解。Prop 应当被视为不可变数据。

一个更好的实践是使用一个由 prop 初始化的本地响应式状态,或者让组件通过 emit 事件来通知父组件进行更改。但考虑到这个组件的递归特性,这样做可能会很复杂。一个更简单的、避免直接修改 prop 的修复方法是,确保父组件总是传递一个结构完整的对象,即使这意味着需要在父组件中初始化空对象/数组。

当前实现虽然能工作,但比较脆弱,未来可能引入 bug。


function hasVisibleItemsAfter(items, currentIndex) {
const itemEntries = Object.entries(items)

Expand Down Expand Up @@ -204,7 +238,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
<v-expand-transition>
<AstrBotConfig
:metadata="metadata[metadataKey].items"
:iterable="iterable[key]"
:iterable="ensureStructuredValue(key, metadata[metadataKey].items[key])"
:metadataKey="key"
:pluginName="pluginName"
:pathPrefix="getItemPath(key)"
Expand All @@ -231,7 +265,8 @@ function hasVisibleItemsAfter(items, currentIndex) {
</v-list-item-subtitle>
</div>
<TemplateListEditor
v-model="iterable[key]"
:model-value="ensureStructuredValue(key, metadata[metadataKey].items[key])"
@update:model-value="iterable[key] = $event"
:templates="metadata[metadataKey].items[key]?.templates || {}"
class="config-field"
/>
Expand Down
Loading
Loading