Skip to content
Merged
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
11 changes: 11 additions & 0 deletions astrbot/core/platform/platform_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,14 @@ class PlatformMetadata:

module_path: str | None = None
"""注册该适配器的模块路径,用于插件热重载时清理"""
i18n_resources: dict[str, dict] | None = None
"""国际化资源数据,如 {"zh-CN": {...}, "en-US": {...}}
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
"""

config_metadata: dict | None = None
"""配置项元数据,用于 WebUI 生成表单。对应 config_metadata.json 的内容
参考 https://github.com/AstrBotDevs/AstrBot/pull/5045
"""
5 changes: 5 additions & 0 deletions astrbot/core/platform/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ def register_platform_adapter(
adapter_display_name: str | None = None,
logo_path: str | None = None,
support_streaming_message: bool = True,
i18n_resources: dict[str, dict] | None = None,
config_metadata: dict | None = None,
):
"""用于注册平台适配器的带参装饰器。

default_config_tmpl 指定了平台适配器的默认配置模板。用户填写好后将会作为 platform_config 传入你的 Platform 类的实现类。
logo_path 指定了平台适配器的 logo 文件路径,是相对于插件目录的路径。
config_metadata 指定了配置项的元数据,用于 WebUI 生成表单。如果不指定,WebUI 将会把配置项渲染为原始的键值对编辑框。
"""

def decorator(cls):
Expand Down Expand Up @@ -49,6 +52,8 @@ def decorator(cls):
logo_path=logo_path,
support_streaming_message=support_streaming_message,
module_path=module_path,
i18n_resources=i18n_resources,
config_metadata=config_metadata,
)
platform_registry.append(pm)
platform_cls_map[adapter_name] = cls
Expand Down
44 changes: 42 additions & 2 deletions astrbot/dashboard/routes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,30 @@ async def _register_platform_logo(self, platform, platform_default_tmpl) -> None
f"Unexpected error registering logo for platform {platform.name}: {e}",
)

def _inject_platform_metadata_with_i18n(
self, platform, metadata, platform_i18n_translations: dict
):
"""将配置元数据注入到 metadata 中并处理国际化键转换。"""
metadata["platform_group"]["metadata"]["platform"].setdefault("items", {})
platform_items_to_inject = copy.deepcopy(platform.config_metadata)

if platform.i18n_resources:
i18n_prefix = f"platform_group.platform.{platform.name}"

for lang, lang_data in platform.i18n_resources.items():
platform_i18n_translations.setdefault(lang, {}).setdefault(
"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}"

metadata["platform_group"]["metadata"]["platform"]["items"].update(
platform_items_to_inject
)

async def _get_astrbot_config(self):
config = self.config
metadata = copy.deepcopy(CONFIG_METADATA_2)
Expand All @@ -1311,11 +1335,23 @@ async def _get_astrbot_config(self):
"config_template"
]

# 收集平台的 i18n 翻译数据
platform_i18n_translations = {}

# 收集需要注册logo的平台
logo_registration_tasks = []
for platform in platform_registry:
if platform.default_config_tmpl:
platform_default_tmpl[platform.name] = platform.default_config_tmpl
platform_default_tmpl[platform.name] = copy.deepcopy(
platform.default_config_tmpl
)

# 注入配置元数据(在 convert_to_i18n_keys 之后,使用国际化键)
if platform.config_metadata:
self._inject_platform_metadata_with_i18n(
platform, metadata, platform_i18n_translations
)

# 收集logo注册任务
if platform.logo_path:
logo_registration_tasks.append(
Expand All @@ -1334,7 +1370,11 @@ async def _get_astrbot_config(self):
if provider.default_config_tmpl:
provider_default_tmpl[provider.type] = provider.default_config_tmpl

return {"metadata": metadata, "config": config}
return {
"metadata": metadata,
"config": config,
"platform_i18n_translations": platform_i18n_translations,
}

async def _get_plugin_config(self, plugin_name: str):
ret: dict = {"metadata": None, "config": None}
Expand Down
9 changes: 8 additions & 1 deletion dashboard/src/components/platform/AddNewPlatform.vue
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,14 @@ export default {
}
},
methods: {
getPlatformIcon,
getPlatformIcon(platformType) {
// Check for plugin-provided logo_token first
const template = this.platformTemplates?.[platformType];
if (template && template.logo_token) {
return `/api/file/${template.logo_token}`;
}
return getPlatformIcon(platformType);
},
getPlatformDescription,
resetForm() {
this.selectedPlatformType = null;
Expand Down
45 changes: 45 additions & 0 deletions dashboard/src/i18n/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ export function useI18n() {

// 保存到localStorage
localStorage.setItem('astrbot-locale', newLocale);

// 触发自定义事件,通知相关页面重新加载配置数据
// 这是因为插件适配器的 i18n 数据是通过后端 API 注入的,
// 需要根据 Accept-Language 头重新获取
window.dispatchEvent(new CustomEvent('astrbot-locale-changed', {
detail: { locale: newLocale }
}));
}
};

Expand Down Expand Up @@ -171,6 +178,44 @@ export function useLanguageSwitcher() {
};
}

/**
* 将动态翻译数据(如插件提供的 i18n)合并到当前翻译中。
* @param modulePath 模块路径,如 'features.config-metadata'
* @param allLocaleData 所有语言的翻译数据,如 { "zh-CN": {...}, "en-US": {...} }
*/
export function mergeDynamicTranslations(modulePath: string, allLocaleData: Record<string, any>) {
const locale = currentLocale.value;
const localeData = allLocaleData[locale];
if (!localeData || typeof localeData !== 'object') return;

const pathParts = modulePath.split('.');
let target: any = translations.value;
for (const part of pathParts) {
if (!(part in target) || typeof target[part] !== 'object') {
target[part] = {};
}
target = target[part];
}

deepMerge(target, localeData);

// 触发响应式更新
translations.value = { ...translations.value };
}

function deepMerge(target: Record<string, any>, source: Record<string, any>) {
for (const key of Object.keys(source)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!(key in target) || typeof target[key] !== 'object') {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}

// 初始化函数(在应用启动时调用)
export async function setupI18n() {
// 从localStorage获取保存的语言设置
Expand Down
8 changes: 8 additions & 0 deletions dashboard/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ axios.interceptors.request.use((config) => {
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
const locale = localStorage.getItem('astrbot-locale');
if (locale) {
config.headers['Accept-Language'] = locale;
}
return config;
});

Expand All @@ -98,6 +102,10 @@ window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}`);
}
const locale = localStorage.getItem('astrbot-locale');
if (locale && !headers.has('Accept-Language')) {
headers.set('Accept-Language', locale);
}
return _origFetch(input, { ...init, headers });
};

Expand Down
18 changes: 18 additions & 0 deletions dashboard/src/views/ConfigPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,26 @@ export default {
this.getConfigInfoList(targetConfigId);
// 初始化配置类型状态
this.configType = this.isSystemConfig ? 'system' : 'normal';

// 监听语言切换事件,重新加载配置以获取插件的 i18n 数据
window.addEventListener('astrbot-locale-changed', this.handleLocaleChange);
},

beforeUnmount() {
// 移除语言切换事件监听器
window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange);
},
methods: {
// 处理语言切换事件,重新加载配置以获取插件的 i18n 数据
handleLocaleChange() {
// 重新加载当前配置
if (this.selectedConfigID) {
this.getConfig(this.selectedConfigID);
} else if (this.isSystemConfig) {
this.getConfig();
}
},

getConfigInfoList(abconf_id) {
// 获取配置列表
axios.get('/api/config/abconfs').then((res) => {
Expand Down
18 changes: 17 additions & 1 deletion dashboard/src/views/ExtensionPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useCommonStore } from "@/stores/common";
import { useI18n, useModuleI18n } from "@/i18n/composables";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";

import { ref, computed, onMounted, reactive, watch } from "vue";
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
import { useRoute, useRouter } from "vue-router";

const commonStore = useCommonStore();
Expand Down Expand Up @@ -1054,6 +1054,22 @@ onMounted(async () => {
}
});

// 处理语言切换事件,重新加载插件配置以获取插件的 i18n 数据
const handleLocaleChange = () => {
// 如果配置对话框是打开的,重新加载当前插件的配置
if (configDialog.value && currentConfigPlugin.value) {
openExtensionConfig(currentConfigPlugin.value);
}
};

// 监听语言切换事件
window.addEventListener("astrbot-locale-changed", handleLocaleChange);

// 清理事件监听器
onUnmounted(() => {
window.removeEventListener("astrbot-locale-changed", handleLocaleChange);
});

// 搜索防抖处理
let searchDebounceTimer = null;
watch(marketSearch, (newVal) => {
Expand Down
18 changes: 17 additions & 1 deletion dashboard/src/views/PlatformPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ItemCard from '@/components/shared/ItemCard.vue';
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { useI18n, useModuleI18n, mergeDynamicTranslations } from '@/i18n/composables';
import { getPlatformIcon, getTutorialLink } from '@/utils/platformUtils';
import {
askForConfirmation as askForConfirmationDialog,
Expand Down Expand Up @@ -280,15 +280,25 @@ export default {
this.statsRefreshInterval = setInterval(() => {
this.getPlatformStats();
}, 10000);

// 监听语言切换事件,重新加载配置以获取插件的 i18n 数据
window.addEventListener('astrbot-locale-changed', this.handleLocaleChange);
},

beforeUnmount() {
if (this.statsRefreshInterval) {
clearInterval(this.statsRefreshInterval);
}
// 移除语言切换事件监听器
window.removeEventListener('astrbot-locale-changed', this.handleLocaleChange);
},

methods: {
// 处理语言切换事件,重新加载配置以获取插件的 i18n 数据
handleLocaleChange() {
this.getConfig();
},

// 从工具函数导入
getPlatformIcon(platform_id) {
// 首先检查是否有来自插件的 logo_token
Expand All @@ -305,6 +315,12 @@ export default {
this.config_data = res.data.data.config;
this.fetched = true
this.metadata = res.data.data.metadata;

// 将插件平台适配器的 i18n 翻译注入到前端 i18n 系统中
const platformI18n = res.data.data.platform_i18n_translations;
if (platformI18n && typeof platformI18n === 'object') {
mergeDynamicTranslations('features.config-metadata', platformI18n);
}
}).catch((err) => {
this.showError(err);
});
Expand Down