feat: 完成仪表板国际化系统重构

 核心特性:
- 实现模块化i18n架构,支持22个功能模块
- 完成中英双语翻译文件(44个翻译文件)
- 新增懒加载翻译模块,提升性能
- 类型安全的翻译键值验证系统

🌐 国际化覆盖:
- 所有主要页面(15+)完成国际化
- 导航侧边栏、顶栏、共享组件全部支持
- 仪表板统计组件完整国际化
- 登录页面及认证流程完整国际化

🎨 UI/UX 优化:
- 统一顶栏按钮样式(语言切换+主题切换)
- 移动端登录页采用全屏设计
- Logo组件智能换行支持中英文
- 响应式语言切换组件

📱 移动端适配:
- 登录卡片移动端全屏布局
- 悬浮工具栏底部固定定位
- 触摸友好的交互设计
- 多设备响应式支持

🔧 技术改进:
- 模块化翻译文件结构 (core/*, features/*)
- 懒加载机制减少初始包体积
- TypeScript类型定义完整
- 翻译键值自动验证
This commit is contained in:
IGCrystal
2025-06-16 13:53:33 +08:00
parent 60b2ff0a7a
commit 0f95f62aa1
86 changed files with 4594 additions and 921 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"marked": "^15.0.7",
"pinia": "2.1.6",
"remixicon": "3.5.0",
"vue-i18n": "^9.8.0",
"vue-i18n": "^11.1.5",
"vee-validate": "4.11.3",
"vite-plugin-vuetify": "1.0.2",
"vue": "3.3.4",
+7 -4
View File
@@ -5,8 +5,8 @@
<v-card-text>{{ message }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="gray" @click="handleCancel">取消</v-btn>
<v-btn color="red" @click="handleConfirm">确定</v-btn>
<v-btn color="gray" @click="handleCancel">{{ t('core.common.dialog.cancelButton') }}</v-btn>
<v-btn color="red" @click="handleConfirm">{{ t('core.common.dialog.confirmButton') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -14,6 +14,9 @@
<script setup>
import { ref } from "vue";
import { useI18n } from '@/i18n/composables';
const { t } = useI18n();
const isOpen = ref(false);
const title = ref("");
@@ -21,8 +24,8 @@ const message = ref("");
let resolvePromise = null; // ✅ 确保 Promise 句柄可用
const open = (options) => {
title.value = options.title || "确认操作";
message.value = options.message || "你确定要执行此操作吗?";
title.value = options.title || t('core.common.dialog.confirmTitle');
message.value = options.message || t('core.common.dialog.confirmMessage');
isOpen.value = true;
return new Promise((resolve) => {
@@ -1,59 +1,77 @@
<template>
<v-menu>
<template v-slot:activator="{ props }">
<v-menu offset="12" location="bottom center">
<template v-slot:activator="{ props: activatorProps }">
<v-btn
v-bind="props"
variant="text"
v-bind="activatorProps"
:variant="props.variant === 'header' ? 'flat' : 'text'"
:color="props.variant === 'header' ? 'var(--v-theme-surface)' : undefined"
:rounded="props.variant === 'header' ? 'sm' : undefined"
icon
size="small"
class="language-switcher"
:class="['language-switcher', `language-switcher--${props.variant}`, props.variant === 'header' ? 'action-btn' : '']"
>
<v-icon>mdi-translate</v-icon>
<v-tooltip activator="parent" location="bottom">
{{ $t('common.language') }}
<v-icon
size="18"
:color="props.variant === 'default' ? (useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa') : undefined"
>
mdi-translate
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t('core.common.language') }}
</v-tooltip>
</v-btn>
</template>
<v-list density="compact" min-width="140">
<v-list-item
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
@click="changeLanguage(lang.code)"
:class="{ 'v-list-item--active': currentLocale === lang.code }"
>
<template v-slot:prepend>
<span class="language-flag">{{ lang.flag }}</span>
</template>
<v-list-item-title>{{ lang.name }}</v-list-item-title>
<template v-slot:append v-if="currentLocale === lang.code">
<v-icon color="primary" size="small">mdi-check</v-icon>
</template>
</v-list-item>
</v-list>
<v-card class="language-dropdown" elevation="8" rounded="lg">
<v-list density="compact" class="pa-1">
<v-list-item
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
@click="changeLanguage(lang.code)"
:class="{ 'v-list-item--active': currentLocale === lang.code, 'language-item-selected': currentLocale === lang.code }"
class="language-item"
rounded="md"
>
<template v-slot:prepend>
<span class="language-flag">{{ lang.flag }}</span>
</template>
<v-list-item-title>{{ lang.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useI18n, useLanguageSwitcher } from '@/i18n/composables'
import { useCustomizerStore } from '@/stores/customizer'
import type { Locale } from '@/i18n/types'
const { locale } = useI18n()
// 定义props来控制样式变体
const props = withDefaults(defineProps<{
variant?: 'default' | 'header'
}>(), {
variant: 'default'
})
const languages = [
{ code: 'zh-CN', name: '简体中文', flag: '🇨🇳' },
{ code: 'en-US', name: 'English', flag: '🇺🇸' }
]
// 使用新的i18n系统
const { t } = useI18n()
const { languageOptions, currentLanguage, switchLanguage, locale } = useLanguageSwitcher()
const languages = computed(() =>
languageOptions.value.map(lang => ({
code: lang.value,
name: lang.label,
flag: lang.flag
}))
)
const currentLocale = computed(() => locale.value)
const changeLanguage = (langCode: string) => {
locale.value = langCode
localStorage.setItem('locale', langCode)
// 可选:刷新页面以确保所有文本都更新
// window.location.reload()
const changeLanguage = async (langCode: string) => {
await switchLanguage(langCode as Locale)
}
</script>
@@ -63,7 +81,73 @@ const changeLanguage = (langCode: string) => {
margin-right: 8px;
}
.language-switcher {
/* 默认变体样式 - 圆形按钮用于登录页 */
.language-switcher--default {
margin: 0 4px;
transition: all 0.3s ease;
border-radius: 50% !important;
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
}
.language-switcher--default:hover {
transform: scale(1.05);
background: rgba(94, 53, 177, 0.08) !important;
}
/* Header变体样式 - 完全继承Vuetify和action-btn的默认样式 */
.language-switcher--header {
/* action-btn类已经处理了margin-right: 6px,不需要额外样式 */
}
/* 深色模式下的悬停效果(仅对default变体) */
:deep(.v-theme--PurpleThemeDark) .language-switcher--default:hover {
background: rgba(114, 46, 209, 0.12) !important;
}
.language-dropdown {
min-width: 100px;
width: fit-content;
border: 1px solid rgba(94, 53, 177, 0.15) !important;
background: #f8f6fc !important;
backdrop-filter: blur(10px);
}
/* 深色模式下的下拉框样式 */
:deep(.v-theme--PurpleThemeDark) .language-dropdown {
background: #2a2733 !important;
border: 1px solid rgba(110, 60, 180, 0.692) !important;
}
.language-item {
margin: 2px 0;
transition: all 0.2s ease;
}
.language-item:hover {
background: rgba(94, 53, 177, 0.08) !important;
}
.language-item-selected {
background: rgba(94, 53, 177, 0.15) !important;
font-weight: 500;
}
.language-item-selected:hover {
background: rgba(94, 53, 177, 0.2) !important;
}
/* 深色模式下的列表项悬停效果 */
:deep(.v-theme--PurpleThemeDark) .language-item:hover {
background: rgba(114, 46, 209, 0.12) !important;
}
:deep(.v-theme--PurpleThemeDark) .language-item-selected {
background: rgba(114, 46, 209, 0.2) !important;
}
:deep(.v-theme--PurpleThemeDark) .language-item-selected:hover {
background: rgba(114, 46, 209, 0.25) !important;
}
</style>
+47 -1
View File
@@ -5,7 +5,10 @@
<img width="110" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
</div>
<div class="logo-text">
<h2 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'}">{{ title }}</h2>
<h2
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'}"
v-html="formatTitle(title)"
></h2>
<!-- 父子组件传递css变量可能会出错暂时使用十六进制颜色值 -->
<h4 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000aa' : '#ffffffcc'}"
class="hint-text">{{ subtitle }}</h4>
@@ -24,6 +27,16 @@ const props = withDefaults(defineProps<{
title: 'AstrBot 仪表盘',
subtitle: '欢迎使用'
})
// 格式化标题,在小屏幕上允许在合适位置换行
const formatTitle = (title: string) => {
if (title === 'AstrBot 仪表盘') {
return 'AstrBot<wbr> 仪表盘'
} else if (title === 'AstrBot Dashboard') {
return 'AstrBot<wbr> Dashboard'
}
return title
}
</script>
<style scoped>
@@ -40,6 +53,8 @@ const props = withDefaults(defineProps<{
align-items: center;
gap: 20px;
padding: 10px;
max-width: 100%;
overflow: visible;
}
.logo-image {
@@ -60,6 +75,8 @@ const props = withDefaults(defineProps<{
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
flex: 1;
}
.logo-text h2 {
@@ -67,6 +84,15 @@ const props = withDefaults(defineProps<{
font-size: 1.8rem;
font-weight: 600;
letter-spacing: 0.5px;
white-space: nowrap;
min-width: fit-content;
}
/* 在小屏幕上允许在指定位置换行 */
@media (max-width: 420px) {
.logo-text h2 {
line-height: 1.3;
}
}
.logo-text h4 {
@@ -74,5 +100,25 @@ const props = withDefaults(defineProps<{
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.3px;
white-space: nowrap;
}
/* 响应式处理 */
@media (max-width: 520px) {
.logo-content {
gap: 15px;
}
.logo-text h2 {
font-size: 1.6rem;
}
.logo-text h4 {
font-size: 0.9rem;
}
.logo-image img {
width: 90px;
}
}
</style>
+164
View File
@@ -0,0 +1,164 @@
import { ref, computed, watchEffect } from 'vue';
import { I18nLoader } from './loader';
import type { Locale } from './types';
// 全局状态
const currentLocale = ref<Locale>('zh-CN');
const loader = new I18nLoader();
const translations = ref<Record<string, any>>({});
// 加载器实例
let loaderInstance: I18nLoader | null = null;
/**
* 初始化i18n系统
*/
export async function initI18n(locale: Locale = 'zh-CN') {
loaderInstance = new I18nLoader();
currentLocale.value = locale;
// 加载初始翻译
await loadTranslations(locale);
}
/**
* 加载翻译数据
*/
async function loadTranslations(locale: Locale) {
if (!loaderInstance) {
throw new Error('I18n not initialized. Call initI18n() first.');
}
try {
const data = await loaderInstance.loadLocale(locale);
translations.value = data;
} catch (error) {
console.error(`Failed to load translations for ${locale}:`, error);
// 回退到中文
if (locale !== 'zh-CN') {
console.log('Falling back to zh-CN');
const fallbackData = await loaderInstance.loadLocale('zh-CN');
translations.value = fallbackData;
}
}
}
/**
* 主要的翻译函数组合
*/
export function useI18n() {
// 翻译函数
const t = (key: string, params?: Record<string, string | number>): string => {
const keys = key.split('.');
let value: any = translations.value;
// 遍历键路径
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
console.warn(`Translation key not found: ${key}`);
return key; // 返回键名作为回退
}
}
if (typeof value !== 'string') {
console.warn(`Translation value is not string: ${key}`);
return key;
}
// 此时value确定是string类型
let result: string = value;
// 处理参数插值
if (params) {
result = result.replace(/\{(\w+)\}/g, (match: string, paramKey: string) => {
return params[paramKey]?.toString() || match;
});
}
return result;
};
// 切换语言
const setLocale = async (newLocale: Locale) => {
if (newLocale !== currentLocale.value) {
currentLocale.value = newLocale;
await loadTranslations(newLocale);
// 保存到localStorage
localStorage.setItem('astrbot-locale', newLocale);
}
};
// 获取当前语言
const locale = computed(() => currentLocale.value);
// 获取可用语言列表
const availableLocales: Locale[] = ['zh-CN', 'en-US'];
// 检查是否已加载
const isLoaded = computed(() => Object.keys(translations.value).length > 0);
return {
t,
locale,
setLocale,
availableLocales,
isLoaded
};
}
/**
* 模块特定的翻译函数
*/
export function useModuleI18n(moduleName: string) {
const { t } = useI18n();
const tm = (key: string, params?: Record<string, string | number>): string => {
// 将斜杠转换为点号以匹配嵌套对象结构
const normalizedModuleName = moduleName.replace(/\//g, '.');
return t(`${normalizedModuleName}.${key}`, params);
};
return { tm };
}
/**
* 语言切换器组合函数
*/
export function useLanguageSwitcher() {
const { locale, setLocale, availableLocales } = useI18n();
const languageOptions = computed(() => [
{ value: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
{ value: 'en-US', label: 'English', flag: '🇺🇸' }
]);
const currentLanguage = computed(() => {
return languageOptions.value.find(lang => lang.value === locale.value);
});
const switchLanguage = async (newLocale: Locale) => {
await setLocale(newLocale);
};
return {
locale,
languageOptions,
currentLanguage,
switchLanguage,
availableLocales
};
}
// 初始化函数(在应用启动时调用)
export async function setupI18n() {
// 从localStorage获取保存的语言设置
const savedLocale = localStorage.getItem('astrbot-locale') as Locale;
const initialLocale = savedLocale && ['zh-CN', 'en-US'].includes(savedLocale)
? savedLocale
: 'zh-CN';
await initI18n(initialLocale);
}
-20
View File
@@ -1,20 +0,0 @@
import { createI18n } from 'vue-i18n'
import zhCN from './locales/zh-CN.json'
import enUS from './locales/en-US.json'
// 从localStorage获取用户选择的语言,默认为中文
const savedLocale = localStorage.getItem('locale') || 'zh-CN'
// 创建i18n实例
const i18n = createI18n({
legacy: false, // 使用Composition API模式
locale: savedLocale, // 设置地区
fallbackLocale: 'zh-CN', // 设置备用语言
messages: {
'zh-CN': zhCN,
'en-US': enUS
},
globalInjection: true // 全局注入
})
export default i18n
+288
View File
@@ -0,0 +1,288 @@
/**
* Dynamic I18n Loader
* 动态国际化加载器,支持按需加载和缓存机制
*/
export interface LoaderCache {
[key: string]: any;
}
export interface ModuleInfo {
name: string;
path: string;
loaded: boolean;
data?: any;
}
export class I18nLoader {
private cache: Map<string, any> = new Map();
private moduleRegistry: Map<string, ModuleInfo> = new Map();
constructor() {
this.registerModules();
}
/**
* 注册所有可用的翻译模块
*/
private registerModules(): void {
const modules = [
// 核心模块
{ name: 'core/common', path: 'core/common.json' },
{ name: 'core/actions', path: 'core/actions.json' },
{ name: 'core/status', path: 'core/status.json' },
{ name: 'core/navigation', path: 'core/navigation.json' },
{ name: 'core/header', path: 'core/header.json' },
// 功能模块
{ name: 'features/chat', path: 'features/chat.json' },
{ name: 'features/extension', path: 'features/extension.json' },
{ name: 'features/conversation', path: 'features/conversation.json' },
{ name: 'features/tooluse', path: 'features/tool-use.json' },
{ name: 'features/provider', path: 'features/provider.json' },
{ name: 'features/platform', path: 'features/platform.json' },
{ name: 'features/config', path: 'features/config.json' },
{ name: 'features/console', path: 'features/console.json' },
{ name: 'features/about', path: 'features/about.json' },
{ name: 'features/settings', path: 'features/settings.json' },
{ name: 'features/auth', path: 'features/auth.json' },
{ name: 'features/chart', path: 'features/chart.json' },
{ name: 'features/dashboard', path: 'features/dashboard.json' },
{ name: 'features/alkaid/index', path: 'features/alkaid/index.json' },
{ name: 'features/alkaid/knowledge-base', path: 'features/alkaid/knowledge-base.json' },
{ name: 'features/alkaid/memory', path: 'features/alkaid/memory.json' },
// 消息模块
{ name: 'messages/errors', path: 'messages/errors.json' },
{ name: 'messages/success', path: 'messages/success.json' },
{ name: 'messages/validation', path: 'messages/validation.json' }
];
modules.forEach(module => {
this.moduleRegistry.set(module.name, {
name: module.name,
path: module.path,
loaded: false
});
});
}
/**
* 加载单个模块
*/
async loadModule(locale: string, moduleName: string): Promise<any> {
const cacheKey = `${locale}:${moduleName}`;
// 检查缓存
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
const moduleInfo = this.moduleRegistry.get(moduleName);
if (!moduleInfo) {
console.warn(`模块 ${moduleName} 未注册`);
return {};
}
try {
// 使用fetch方式加载JSON文件
const modulePath = `/src/i18n/locales/${locale}/${moduleInfo.path}`;
const response = await fetch(modulePath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// 缓存结果
this.cache.set(cacheKey, data);
// 更新模块信息
moduleInfo.loaded = true;
moduleInfo.data = data;
return data;
} catch (error) {
console.error(`加载模块 ${moduleName} 失败:`, error);
return {};
}
}
/**
* 加载核心模块(最高优先级)
*/
async loadCoreModules(locale: string): Promise<any> {
const coreModules = [
'core/common',
'core/actions',
'core/status',
'core/navigation',
'core/header'
];
const results = await Promise.all(
coreModules.map(module => this.loadModule(locale, module))
);
return this.mergeModules(results, coreModules);
}
/**
* 加载功能模块
*/
async loadFeatureModules(locale: string, features?: string[]): Promise<any> {
const featureModules = features || [
'features/chat',
'features/extension',
'features/conversation',
'features/tooluse',
'features/provider',
'features/platform',
'features/config',
'features/console',
'features/about',
'features/settings',
'features/auth',
'features/chart',
'features/dashboard',
'features/alkaid/index',
'features/alkaid/knowledge-base',
'features/alkaid/memory'
];
const results = await Promise.all(
featureModules.map(module => this.loadModule(locale, module))
);
return this.mergeModules(results, featureModules);
}
/**
* 加载消息模块
*/
async loadMessageModules(locale: string): Promise<any> {
const messageModules = [
'messages/errors',
'messages/success',
'messages/validation'
];
const results = await Promise.all(
messageModules.map(module => this.loadModule(locale, module))
);
return this.mergeModules(results, messageModules);
}
/**
* 加载所有模块
*/
async loadAllModules(locale: string): Promise<any> {
const [core, features, messages] = await Promise.all([
this.loadCoreModules(locale),
this.loadFeatureModules(locale),
this.loadMessageModules(locale)
]);
return {
...core,
...features,
...messages
};
}
/**
* 加载完整语言包(所有模块合并)
*/
async loadLocale(locale: string): Promise<any> {
return this.loadAllModules(locale);
}
/**
* 合并多个模块数据
*/
private mergeModules(modules: any[], moduleNames: string[]): any {
const result: any = {};
modules.forEach((module, index) => {
const moduleName = moduleNames[index];
const nameParts = moduleName.split('/');
// 构建嵌套对象结构(对所有模块统一处理)
let current = result;
for (let i = 0; i < nameParts.length - 1; i++) {
if (!current[nameParts[i]]) {
current[nameParts[i]] = {};
}
current = current[nameParts[i]];
}
// 设置最终值
const finalKey = nameParts[nameParts.length - 1];
current[finalKey] = { ...current[finalKey], ...module };
});
return result;
}
/**
* 预加载关键模块
*/
async preloadEssentials(locale: string): Promise<void> {
const essentials = [
'core/common',
'core/navigation',
'features/chat'
];
await Promise.all(
essentials.map(module => this.loadModule(locale, module))
);
}
/**
* 清理缓存
*/
clearCache(locale?: string): void {
if (locale) {
// 清理特定语言的缓存
const keys = Array.from(this.cache.keys()).filter((key: string) => key.startsWith(`${locale}:`));
keys.forEach((key: string) => this.cache.delete(key));
} else {
// 清理所有缓存
this.cache.clear();
}
}
/**
* 获取加载状态
*/
getLoadingStatus(): { total: number; loaded: number; modules: ModuleInfo[] } {
const modules = Array.from(this.moduleRegistry.values());
const loaded = modules.filter(m => m.loaded).length;
return {
total: modules.length,
loaded,
modules
};
}
/**
* 热重载模块
*/
async reloadModule(locale: string, moduleName: string): Promise<any> {
const cacheKey = `${locale}:${moduleName}`;
this.cache.delete(cacheKey);
const moduleInfo = this.moduleRegistry.get(moduleName);
if (moduleInfo) {
moduleInfo.loaded = false;
}
return this.loadModule(locale, moduleName);
}
}
-108
View File
@@ -1,108 +0,0 @@
{
"sidebar": {
"dashboard": "Dashboard",
"platforms": "Platforms",
"providers": "Providers",
"toolUse": "MCP",
"config": "Config",
"extension": "Extensions",
"extensionMarketplace": "Extension Market",
"chat": "Chat",
"conversation": "Conversations",
"console": "Console",
"alkaid": "Alkaid",
"about": "About",
"settings": "Settings",
"documentation": "Documentation",
"github": "GitHub",
"drag": "Drag"
},
"common": {
"save": "Save",
"cancel": "Cancel",
"close": "Close",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"confirm": "Confirm",
"loading": "Loading...",
"success": "Success",
"error": "Error",
"warning": "Warning",
"info": "Info",
"name": "Name",
"description": "Description",
"author": "Author",
"status": "Status",
"actions": "Actions",
"enable": "Enable",
"disable": "Disable",
"enabled": "Enabled",
"disabled": "Disabled",
"reload": "Reload",
"configure": "Configure",
"install": "Install",
"uninstall": "Uninstall",
"update": "Update",
"language": "Language"
},
"extension": {
"title": "Installed Extensions",
"subtitle": "Manage all installed extensions",
"showSystemPlugins": "Show System Plugins",
"hideSystemPlugins": "Hide System Plugins",
"platformCommandConfig": "Platform Command Config",
"noPlugins": "No plugins available",
"tryInstallOrShowSystem": "Try installing plugins or show system plugins",
"configDialog": {
"title": "Extension Configuration",
"noConfig": "This extension has no configuration"
},
"platformConfig": {
"title": "Platform Command Availability Configuration",
"description": "Set the availability of each plugin on different platforms, check to enable",
"noPlatforms": "No platform adapters found",
"addPlatformFirst": "Please add and configure platform adapters in Platform Management first, then set plugin platform availability",
"goToPlatformManagement": "Go to Platform Management"
}
},
"extensionMarketplace": {
"title": "Extension Market",
"installPlugin": "Install Extension",
"fromGitHub": "Download from GitHub",
"fromLocal": "Upload .zip file from local",
"repoUrl": "Repository URL",
"selectFile": "Select File",
"pluginDevelopmentDoc": "Plugin Development Documentation",
"submitPluginRepo": "Submit Plugin Repository"
},
"platform": {
"title": "Platform Adapter Management",
"subtitle": "Manage robot platform adapters to connect to different chat platforms",
"adapters": "Platform Adapters",
"addAdapter": "Add Adapter"
},
"provider": {
"title": "Service Providers",
"tabTypes": {
"chat_completion": "Chat Completion",
"speech_to_text": "Speech to Text",
"text_to_speech": "Text to Speech",
"embedding": "Embedding"
},
"openaiDescription": "{type} service provider. Also supports all OpenAI API compatible model providers.",
"defaultDescription": "{type} service provider"
},
"auth": {
"login": "Login",
"username": "Username",
"password": "Password"
},
"chart": {
"messageCount": "Message Count",
"time": "Time"
},
"alkaid": {
"comingSoon": "The world ahead, let's explore it later!"
}
}
@@ -0,0 +1,22 @@
{
"create": "Create",
"read": "Read",
"update": "Update",
"delete": "Delete",
"search": "Search",
"filter": "Filter",
"sort": "Sort",
"export": "Export",
"import": "Import",
"backup": "Backup",
"restore": "Restore",
"copy": "Copy",
"paste": "Paste",
"cut": "Cut",
"undo": "Undo",
"redo": "Redo",
"refresh": "Refresh",
"submit": "Submit",
"reset": "Reset",
"clear": "Clear"
}
@@ -0,0 +1,40 @@
{
"save": "Save",
"cancel": "Cancel",
"close": "Close",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"confirm": "Confirm",
"loading": "Loading...",
"success": "Success",
"error": "Error",
"warning": "Warning",
"info": "Info",
"name": "Name",
"description": "Description",
"author": "Author",
"status": "Status",
"actions": "Actions",
"enable": "Enable",
"disable": "Disable",
"enabled": "Enabled",
"disabled": "Disabled",
"reload": "Reload",
"configure": "Configure",
"install": "Install",
"uninstall": "Uninstall",
"update": "Update",
"language": "Language",
"type": "Type",
"press": "Press",
"longPress": "Long press",
"yes": "Yes",
"no": "No",
"dialog": {
"confirmTitle": "Confirm Action",
"confirmMessage": "Are you sure you want to perform this action?",
"confirmButton": "Confirm",
"cancelButton": "Cancel"
}
}
@@ -0,0 +1,84 @@
{
"version": {
"hasNewVersion": "AstrBot has a new version!",
"dashboardHasNewVersion": "WebUI has a new version!"
},
"buttons": {
"update": "Update",
"account": "Account",
"theme": {
"light": "Light Mode",
"dark": "Dark Mode"
}
},
"updateDialog": {
"title": "Update AstrBot",
"currentVersion": "Current Version",
"status": {
"checking": "Checking for updates...",
"switching": "Switching version...",
"updating": "Updating..."
},
"tabs": {
"release": "😊 Release",
"dev": "🧐 Development (master branch)"
},
"updateToLatest": "Update to Latest Version",
"tip": "💡 TIP: Switching to an older version or a specific version will not re-download the dashboard files, which may cause some data display errors. You can find the corresponding dashboard files dist.zip at",
"tipLink": "here",
"tipContinue": ", extract and replace the data/dist folder. Of course, the frontend source code is in the dashboard directory, you can also build it yourself using npm install and npm build.",
"dockerTip": "The `Update to Latest Version` button will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
"dockerTipLink": "watchtower",
"dockerTipContinue": "to automatically monitor and pull.",
"table": {
"tag": "Tag",
"publishDate": "Publish Date",
"content": "Content",
"sourceUrl": "Source URL",
"actions": "Actions",
"sha": "SHA",
"date": "Date",
"message": "Message",
"view": "View",
"switch": "Switch"
},
"manualInput": {
"title": "Manual Input Version or Commit SHA",
"placeholder": "Enter version number or commit hash from master branch.",
"hint": "e.g. v3.3.16 (without SHA) or 42e5ec5d80b93b6bfe8b566754d45ffac4c3fe0b",
"linkText": "View master branch commit history (click copy on the right to copy)",
"confirm": "Confirm Switch"
},
"dashboardUpdate": {
"title": "Update Dashboard to Latest Version Only",
"currentVersion": "Current Version",
"hasNewVersion": "New version available!",
"isLatest": "Already the latest version.",
"downloadAndUpdate": "Download and Update"
}
},
"accountDialog": {
"title": "Modify Account",
"securityWarning": "Security Reminder: Please change the default password to ensure account security",
"form": {
"currentPassword": "Current Password",
"newPassword": "New Password",
"newUsername": "New Username (Optional)",
"passwordHint": "Password must be at least 8 characters",
"usernameHint": "Leave blank to keep current username",
"defaultCredentials": "Default username and password are both astrbot"
},
"validation": {
"passwordRequired": "Please enter password",
"passwordMinLength": "Password must be at least 8 characters",
"usernameMinLength": "Username must be at least 3 characters"
},
"actions": {
"save": "Save Changes",
"cancel": "Cancel"
},
"messages": {
"updateFailed": "Update failed, please try again"
}
}
}
@@ -0,0 +1,18 @@
{
"dashboard": "Dashboard",
"platforms": "Platforms",
"providers": "Providers",
"toolUse": "MCP Tools",
"config": "Config",
"extension": "Extensions",
"extensionMarketplace": "Extension Market",
"chat": "Chat",
"conversation": "Conversations",
"console": "Console",
"alkaid": "Alkaid Lab",
"about": "About",
"settings": "Settings",
"documentation": "Documentation",
"github": "GitHub",
"drag": "Drag"
}
@@ -0,0 +1,22 @@
{
"loading": "Loading",
"success": "Success",
"error": "Error",
"warning": "Warning",
"info": "Info",
"pending": "Pending",
"processing": "Processing",
"completed": "Completed",
"failed": "Failed",
"cancelled": "Cancelled",
"timeout": "Timeout",
"connecting": "Connecting",
"connected": "Connected",
"disconnected": "Disconnected",
"online": "Online",
"offline": "Offline",
"active": "Active",
"inactive": "Inactive",
"ready": "Ready",
"busy": "Busy"
}
@@ -0,0 +1,17 @@
{
"hero": {
"title": "AstrBot",
"subtitle": "A project out of interests and loves ❤️",
"starButton": "Star this project! 🌟",
"issueButton": "Submit Issue"
},
"contributors": {
"title": "Contributors",
"description": "This project is maintained by many open source community members. Thanks to every contributor for their dedication!",
"viewLink": "View AstrBot Contributors"
},
"stats": {
"title": "Global Deployment",
"license": "AstrBot is open source under AGPL v3 license"
}
}
@@ -0,0 +1,26 @@
{
"title": "Alkaid Laboratory",
"subtitle": "Explore cutting-edge AI features",
"comingSoon": "The world ahead, let's explore it later!",
"page": {
"title": "The Alkaid Project.",
"subtitle": "AstrBot Alpha Project",
"navigation": {
"knowledgeBase": "Knowledge Base",
"longTermMemory": "Long-term Memory",
"other": "..."
}
},
"features": {
"knowledgeBase": "Knowledge Base",
"longTermMemory": "Long-term Memory",
"advancedChat": "Advanced Chat",
"multiModal": "Multi-modal Interaction"
},
"status": {
"experimental": "Experimental",
"beta": "Beta",
"stable": "Stable",
"deprecated": "Deprecated"
}
}
@@ -0,0 +1,33 @@
{
"title": "Knowledge Base",
"subtitle": "Manage and query knowledge base content",
"upload": {
"title": "Upload Documents",
"selectFiles": "Select Files",
"supportedFormats": "Supported Formats",
"dragDrop": "Drag files here",
"processing": "Processing...",
"success": "Upload Successful",
"error": "Upload Failed"
},
"search": {
"placeholder": "Search knowledge base...",
"results": "Search Results",
"noResults": "No relevant content found",
"searching": "Searching..."
},
"documents": {
"title": "Document List",
"name": "Document Name",
"size": "Size",
"uploadTime": "Upload Time",
"status": "Status",
"actions": "Actions"
},
"management": {
"delete": "Delete",
"preview": "Preview",
"download": "Download",
"reindex": "Reindex"
}
}
@@ -0,0 +1,37 @@
{
"title": "Long-term Memory",
"subtitle": "AI assistant's long-term memory management",
"memories": {
"title": "Memory List",
"content": "Memory Content",
"importance": "Importance Level",
"createTime": "Create Time",
"lastAccess": "Last Access",
"category": "Category"
},
"categories": {
"personal": "Personal Information",
"preferences": "Preference Settings",
"conversations": "Conversation History",
"facts": "Factual Information",
"skills": "Skill Knowledge"
},
"importance": {
"high": "High",
"medium": "Medium",
"low": "Low"
},
"actions": {
"view": "View Details",
"edit": "Edit",
"delete": "Delete",
"pin": "Pin",
"unpin": "Unpin"
},
"filters": {
"all": "All",
"category": "By Category",
"importance": "By Importance",
"dateRange": "By Date Range"
}
}
@@ -0,0 +1,13 @@
{
"login": "Login",
"username": "Username",
"password": "Password",
"logo": {
"title": "AstrBot Dashboard",
"subtitle": "Welcome"
},
"theme": {
"switchToDark": "Switch to Dark Theme",
"switchToLight": "Switch to Light Theme"
}
}
@@ -0,0 +1,4 @@
{
"messageCount": "Message Count",
"time": "Time"
}
@@ -0,0 +1,59 @@
{
"title": "Let's Chat!",
"subtitle": "Chat with AI Assistant",
"input": {
"placeholder": "Start typing...",
"send": "Send",
"clear": "Clear",
"upload": "Upload File",
"voice": "Voice Input"
},
"message": {
"user": "User",
"assistant": "Assistant",
"system": "System",
"error": "Error Message",
"loading": "Thinking..."
},
"voice": {
"start": "Start Recording",
"stop": "Stop Recording",
"recording": "New Recording",
"processing": "Processing...",
"error": "Recording Failed"
},
"welcome": {
"title": "Welcome to AstrBot",
"subtitle": "Your Intelligent Chat Assistant",
"quickActions": "Quick Actions",
"examples": "Example Questions"
},
"actions": {
"copy": "Copy",
"regenerate": "Regenerate",
"like": "Like",
"dislike": "Dislike",
"share": "Share",
"newChat": "New Chat",
"deleteChat": "Delete this conversation",
"editTitle": "Edit Title",
"fullscreen": "Fullscreen Mode",
"exitFullscreen": "Exit Fullscreen"
},
"conversation": {
"newConversation": "New Conversation",
"noHistory": "No conversation history",
"systemStatus": "System Status",
"llmService": "LLM Service",
"speechToText": "Speech to Text"
},
"modes": {
"darkMode": "Switch to Dark Mode",
"lightMode": "Switch to Light Mode"
},
"shortcuts": {
"help": "Get Help",
"voiceRecord": "Record Voice",
"pasteImage": "Paste Image"
}
}
@@ -0,0 +1,62 @@
{
"title": "Configuration",
"subtitle": "Manage system configuration and settings",
"editor": {
"visual": "Visual Editor",
"code": "Code Editor",
"revertCode": "Revert to Previous Code",
"applyConfig": "Apply This Configuration",
"applyTip": "`Apply This Configuration` will stage and apply the configuration to the visual editor. To save, you need to click the save button in the bottom right corner."
},
"actions": {
"save": "Save Configuration",
"delete": "Delete This Item",
"add": "Add",
"reset": "Reset to Default",
"export": "Export Configuration",
"import": "Import Configuration",
"validate": "Validate Configuration"
},
"help": {
"documentation": "Official Documentation",
"support": "Join Group for Help",
"helpText": "Don't understand the configuration? Please see {documentation} or {support}.",
"helpPrefix": "Don't understand the configuration? Please see",
"helpMiddle": "or",
"helpSuffix": "."
},
"messages": {
"configApplied": "Configuration successfully applied. To save, you need to click the save button in the bottom right corner.",
"configApplyError": "Configuration not applied, JSON format error.",
"saveSuccess": "Configuration saved successfully",
"saveError": "Failed to save configuration",
"loadError": "Failed to load configuration"
},
"sections": {
"general": "General Settings",
"advanced": "Advanced Settings",
"security": "Security Settings",
"appearance": "Appearance Settings",
"notification": "Notification Settings"
},
"general": {
"botName": "Bot Name",
"language": "Interface Language",
"timezone": "Timezone",
"autoSave": "Auto Save",
"debugMode": "Debug Mode"
},
"advanced": {
"logLevel": "Log Level",
"maxConnections": "Max Connections",
"timeout": "Timeout",
"retryAttempts": "Retry Attempts",
"cacheSize": "Cache Size"
},
"security": {
"apiKey": "API Key",
"allowedHosts": "Allowed Hosts",
"rateLimit": "Rate Limit",
"encryption": "Encryption Settings"
}
}
@@ -0,0 +1,15 @@
{
"title": "Console",
"autoScroll": {
"enabled": "Auto-scroll enabled",
"disabled": "Auto-scroll disabled"
},
"pipInstall": {
"button": "Install pip Package",
"dialogTitle": "Install Pip Package",
"packageLabel": "*Package name, e.g. llmtuner",
"mirrorLabel": "Force PyPI repository URL (optional)",
"mirrorHint": "Force PyPI repository URL > Config item `PyPI Repository Address`",
"installButton": "Install"
}
}
@@ -0,0 +1,77 @@
{
"title": "Conversation Management",
"subtitle": "Manage and view user conversation history",
"filters": {
"title": "Filter Conditions",
"platform": "Platform",
"type": "Type",
"search": "Search Keywords",
"reset": "Reset"
},
"history": {
"title": "Conversation History",
"refresh": "Refresh"
},
"table": {
"headers": {
"title": "Conversation Title",
"platform": "Platform",
"type": "Type",
"sessionId": "ID",
"createdAt": "Created At",
"updatedAt": "Updated At",
"actions": "Actions"
}
},
"actions": {
"view": "View",
"edit": "Edit",
"delete": "Delete"
},
"messageTypes": {
"group": "Group Chat",
"friend": "Private Chat",
"unknown": "Unknown"
},
"status": {
"noTitle": "Untitled Conversation",
"unknown": "Unknown",
"noData": "No conversation records",
"emptyContent": "Conversation content is empty",
"audioNotSupported": "Your browser does not support audio playback."
},
"dialogs": {
"view": {
"title": "Conversation Details",
"editMode": "Edit Conversation",
"previewMode": "Preview Mode",
"saveChanges": "Save Changes",
"close": "Close",
"confirmClose": "You have unsaved changes, are you sure you want to close?"
},
"edit": {
"title": "Edit Conversation Information",
"titleLabel": "Conversation Title",
"titlePlaceholder": "Enter conversation title",
"cancel": "Cancel",
"save": "Save"
},
"delete": {
"title": "Confirm Delete",
"message": "Are you sure you want to delete conversation {title}? This action cannot be undone.",
"cancel": "Cancel",
"confirm": "Delete"
}
},
"messages": {
"fetchError": "Failed to fetch conversation list",
"saveSuccess": "Save successful",
"saveError": "Save failed",
"deleteSuccess": "Delete successful",
"deleteError": "Delete failed",
"historyError": "Failed to fetch conversation history",
"historySaveSuccess": "Conversation history saved successfully",
"historySaveError": "Failed to save conversation history",
"invalidJson": "Invalid JSON format"
}
}
@@ -0,0 +1,61 @@
{
"title": "Dashboard",
"subtitle": "Real-time monitoring and statistics",
"lastUpdate": "Last updated",
"status": {
"loading": "Loading...",
"dataError": "Failed to fetch data",
"noticeError": "Failed to fetch notice"
},
"stats": {
"totalMessage": {
"title": "Total Messages",
"subtitle": "Total messages sent from all platforms"
},
"onlinePlatform": {
"title": "Platforms",
"subtitle": "Number of connected platforms"
},
"runningTime": {
"title": "Uptime",
"subtitle": "System uptime duration"
},
"memoryUsage": {
"title": "Memory Usage",
"subtitle": "System memory usage status",
"cpuLoad": "CPU Load",
"status": {
"good": "Good",
"normal": "Normal",
"high": "High"
}
}
},
"charts": {
"messageTrend": {
"title": "Message Trend Analysis",
"subtitle": "Track message count changes over time",
"totalMessages": "Total Messages",
"dailyAverage": "Daily Average",
"growthRate": "Growth Rate",
"timeLabel": "Time",
"messageCount": "Message Count",
"timeRanges": {
"1day": "Past 1 Day",
"3days": "Past 3 Days",
"1week": "Past 1 Week",
"1month": "Past 1 Month"
}
},
"platformStat": {
"title": "Platform Message Statistics",
"subtitle": "Message count distribution by platform",
"total": "Total",
"noData": "No platform data available",
"messageUnit": "msgs",
"platformCount": "Platforms",
"mostActive": "Most Active",
"totalPercentage": "Total Percentage"
}
}
}
@@ -0,0 +1,131 @@
{
"title": "Extension Management",
"subtitle": "Manage and configure system extensions",
"tabs": {
"installed": "Installed",
"market": "Extension Market"
},
"search": {
"placeholder": "Search extensions...",
"marketPlaceholder": "Search market extensions..."
},
"views": {
"card": "Card View",
"list": "List View"
},
"buttons": {
"showSystemPlugins": "Show System Extensions",
"hideSystemPlugins": "Hide System Extensions",
"platformConfig": "Platform Command Config",
"install": "Install",
"uninstall": "Uninstall",
"update": "Update",
"reload": "Reload",
"enable": "Enable",
"disable": "Disable",
"configure": "Configure",
"viewInfo": "Handlers",
"viewDocs": "Documentation",
"close": "Close",
"save": "Save",
"saveAndClose": "Save and Close",
"cancel": "Cancel"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"system": "System",
"loading": "Loading...",
"installed": "Installed"
},
"tooltips": {
"enable": "Click to Enable",
"disable": "Click to Disable",
"reload": "Reload",
"configure": "Configure",
"viewInfo": "Handlers",
"viewDocs": "Documentation",
"update": "Update",
"uninstall": "Uninstall"
},
"table": {
"headers": {
"name": "Name",
"description": "Description",
"version": "Version",
"author": "Author",
"status": "Status",
"actions": "Actions",
"stars": "Stars",
"lastUpdate": "Last Update",
"tags": "Tags",
"eventType": "Event Type",
"specificType": "Specific Type",
"trigger": "Trigger"
}
},
"empty": {
"noPlugins": "No Extensions",
"noPluginsDesc": "Try installing extensions or showing system extensions"
},
"market": {
"recommended": "🥳 Recommended",
"allPlugins": "📦 All Extensions",
"showFullName": "Full Name",
"devDocs": "Extension Development Docs",
"submitRepo": "Submit Extension Repository"
},
"dialogs": {
"error": {
"title": "Error Information",
"checkConsole": "Please check console for details"
},
"platformConfig": {
"title": "Platform Command Availability Configuration",
"description": "Set the availability of each extension on different platforms, check to enable",
"noAdapters": "No Platform Adapters Found",
"noAdaptersDesc": "Please add and configure platform adapters in Platform Management first, then set extension platform availability",
"goPlatforms": "Go to Platform Management",
"selectAll": "Select All",
"selectAllNormal": "Select All Normal Extensions",
"selectAllSystem": "Select All System Extensions",
"selectNone": "Select None",
"toggleAll": "Toggle All"
},
"config": {
"title": "Extension Configuration",
"noConfig": "This extension has no configuration"
},
"loading": {
"title": "Loading...",
"logs": "Logs"
}
},
"messages": {
"uninstalling": "Uninstalling",
"refreshing": "Refreshing extension list...",
"refreshSuccess": "Extension list refreshed!",
"refreshFailed": "Error occurred while refreshing extension list",
"reloadSuccess": "Reload successful",
"updateSuccess": "Update successful!",
"addSuccess": "Add successful!",
"saveSuccess": "Save successful!",
"deleteSuccess": "Delete successful!",
"installing": "Installing extension from file",
"installingFromUrl": "Installing extension from URL...",
"installFailed": "Extension installation failed:",
"getPlatformConfigFailed": "Failed to get platform extension config:",
"savePlatformConfigFailed": "Failed to save platform extension config:",
"getMarketDataFailed": "Failed to get extension market data:",
"hasUpdate": "New version available:",
"confirmDelete": "Are you sure you want to delete this extension?",
"fillUrlOrFile": "Please fill in extension URL or upload extension file",
"dontFillBoth": "Please don't fill in both extension URL and upload file"
},
"upload": {
"fromFile": "Install from File",
"fromUrl": "Install from URL",
"selectFile": "Select File",
"enterUrl": "Enter extension repository URL"
}
}
@@ -0,0 +1,40 @@
{
"title": "Platform Adapter Management",
"subtitle": "Manage bot platform adapters to connect to different chat platforms",
"adapters": "Platform Adapters",
"addAdapter": "Add Adapter",
"emptyText": "No platform adapters yet, click Add Adapter to create one",
"details": {
"adapterType": "Adapter Type",
"token": "Token",
"description": "Description"
},
"logs": {
"title": "Platform Logs",
"expand": "Expand",
"collapse": "Collapse"
},
"dialog": {
"add": "Add",
"edit": "Edit",
"adapter": "Platform Adapter",
"refresh": "Refresh",
"cancel": "Cancel",
"save": "Save"
},
"messages": {
"updateSuccess": "Update successful!",
"addSuccess": "Add successful!",
"deleteSuccess": "Delete successful!",
"statusUpdateSuccess": "Status update successful!",
"deleteConfirm": "Are you sure you want to delete platform adapter"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"connecting": "Connecting",
"connected": "Connected",
"disconnected": "Disconnected",
"error": "Error"
}
}
@@ -0,0 +1,82 @@
{
"title": "Service Provider Management",
"subtitle": "Manage model service providers",
"providers": {
"title": "Service Providers",
"settings": "Settings",
"addProvider": "Add Provider",
"providerType": "Provider Type",
"tabs": {
"all": "All",
"chatCompletion": "Chat Completion",
"speechToText": "Speech to Text",
"textToSpeech": "Text to Speech",
"embedding": "Embedding"
},
"empty": {
"all": "No service providers available, click Add Provider to add one",
"typed": "No {type} type service providers available, click Add Provider to add one"
},
"description": {
"openai": "{type} service provider. Also supports all OpenAI API compatible model providers.",
"default": "{type} service provider"
}
},
"availability": {
"title": "Provider Availability",
"subtitle": "Determined by testing model conversation availability, may incur API costs",
"refresh": "Refresh Status",
"noData": "Click \"Refresh Status\" button to get service provider availability",
"available": "Available",
"unavailable": "Unavailable",
"errorMessage": "Error Message"
},
"logs": {
"title": "Service Logs",
"expand": "Expand",
"collapse": "Collapse"
},
"dialogs": {
"addProvider": {
"title": "Service Provider",
"tabs": {
"basic": "Basic",
"speechToText": "Speech to Text",
"textToSpeech": "Text to Speech",
"embedding": "Embedding"
},
"noTemplates": "No {type} type provider templates available"
},
"config": {
"addTitle": "Add",
"editTitle": "Edit",
"provider": "Service Provider",
"cancel": "Cancel",
"save": "Save"
},
"settings": {
"title": "Service Provider Settings",
"sessionSeparation": {
"title": "Enable Provider Session Isolation",
"description": "Different sessions can independently select text generation, TTS, STT and other service providers."
},
"close": "Close"
}
},
"messages": {
"success": {
"update": "Updated successfully!",
"add": "Added successfully!",
"delete": "Deleted successfully!",
"statusUpdate": "Status updated successfully!",
"sessionSeparation": "Session isolation settings updated"
},
"error": {
"sessionSeparation": "Failed to get session isolation configuration",
"fetchStatus": "Failed to get service provider status"
},
"confirm": {
"delete": "Are you sure you want to delete service provider {id}?"
}
}
}
@@ -0,0 +1,18 @@
{
"network": {
"title": "Network",
"githubProxy": {
"title": "GitHub Proxy Address",
"subtitle": "Set the GitHub proxy address used when downloading plugins or updating AstrBot. This is effective in mainland China's network environment. Can be customized, input takes effect in real time. All addresses do not guarantee stability. If errors occur when updating plugins/projects, please first check if the proxy address is working properly.",
"label": "Select GitHub Proxy Address"
}
},
"system": {
"title": "System",
"restart": {
"title": "Restart",
"subtitle": "Restart AstrBot",
"button": "Restart"
}
}
}
@@ -0,0 +1,114 @@
{
"title": "Function Tool Management",
"subtitle": "Manage MCP servers and view available function tools",
"tooltip": {
"info": "What are Function Calling and MCP?",
"marketplace": "Browse and install MCP servers from the community",
"serverConfig": "MCP server (stdio) configuration supports the following fields:\ncommand: Command name (e.g. python or uv)\nargs: Command arguments array (e.g. [\"run\", \"server.py\"])\nenv: Environment variables object (e.g. {\"api_key\": \"abc\"})\ncwd: Working directory path (e.g. /path/to/server)\nencoding: Output encoding (default utf-8)\nencoding_error_handler: The text encoding error handler. Defaults to strict.\nOther fields please refer to MCP documentation\n⚠️ If you deploy AstrBot using Docker, make sure to install MCP servers in the data directory mounted by AstrBot"
},
"tabs": {
"local": "Local Servers",
"marketplace": "MCP Marketplace"
},
"mcpServers": {
"title": "MCP Servers",
"buttons": {
"refresh": "Refresh",
"add": "Add Server",
"useTemplate": "Use Template"
},
"empty": "No MCP servers available, click Add Server to add one",
"status": {
"noTools": "No available tools",
"availableTools": "Available tools",
"configSummary": "Config: {keys}",
"noConfig": "No configuration set"
}
},
"functionTools": {
"title": "Function Tools",
"buttons": {
"expand": "Expand",
"collapse": "Collapse"
},
"search": "Search function tools",
"empty": "No function tools available",
"description": "Function Description",
"parameters": "Parameter List",
"noParameters": "This tool has no parameters",
"table": {
"paramName": "Parameter Name",
"type": "Type",
"description": "Description",
"required": "Required"
}
},
"marketplace": {
"title": "MCP Server Marketplace",
"search": "Search servers",
"buttons": {
"refresh": "Refresh",
"detail": "Details",
"import": "Import"
},
"loading": "Loading MCP server marketplace...",
"empty": "No MCP servers available",
"status": {
"availableTools": "Available tools ({count})",
"noToolsInfo": "No tool information available"
}
},
"dialogs": {
"addServer": {
"title": "Add MCP Server",
"editTitle": "Edit MCP Server",
"fields": {
"name": "Server Name",
"nameRequired": "Name is required",
"enable": "Enable Server",
"config": "Server Configuration"
},
"configNotes": {
"note1": "1. Some MCP servers may require filling in `API_KEY` or `TOKEN` information in env according to their requirements, please check if filled.",
"note2": "2. When url parameter is specified in configuration: if `transport` parameter is also specified as `streamable_http`, Streamable HTTP is used, otherwise SSE connection is used."
},
"errors": {
"configEmpty": "Configuration cannot be empty",
"jsonFormat": "JSON format error: {error}",
"jsonParse": "JSON parse error: {error}"
},
"buttons": {
"cancel": "Cancel",
"save": "Save"
}
},
"serverDetail": {
"title": "Server Details",
"installConfig": "Installation Configuration",
"availableTools": "Available Tools",
"buttons": {
"close": "Close",
"importConfig": "Import Configuration"
}
},
"confirmDelete": "Are you sure you want to delete server {name}?"
},
"messages": {
"getServersError": "Failed to get MCP server list: {error}",
"getToolsError": "Failed to get function tools list: {error}",
"saveSuccess": "Save successful!",
"saveError": "Save failed: {error}",
"deleteSuccess": "Delete successful!",
"deleteError": "Delete failed: {error}",
"updateSuccess": "Update successful!",
"updateError": "Update failed: {error}",
"getMarketError": "Failed to get MCP marketplace server list: {error}",
"importError": {
"noConfig": "This server has no available configuration",
"invalidFormat": "Server configuration format is incorrect",
"failed": "Import configuration failed: {error}"
},
"configParseError": "Configuration parse error: {error}",
"noAvailableConfig": "No available configuration"
}
}
@@ -0,0 +1,39 @@
{
"network": {
"timeout": "Network request timeout, please try again later",
"connection": "Network connection failed, please check your network",
"server": "Server error, please contact technical support",
"unavailable": "Service temporarily unavailable",
"forbidden": "Access denied"
},
"validation": {
"required": "This field is required",
"invalid": "Invalid input format",
"tooLong": "Input is too long",
"tooShort": "Input is too short",
"email": "Please enter a valid email address",
"url": "Please enter a valid URL",
"number": "Please enter a valid number"
},
"auth": {
"unauthorized": "Unauthorized access, please login again",
"forbidden": "Insufficient permissions to perform this operation",
"tokenExpired": "Login expired, please login again",
"invalidCredentials": "Invalid username or password"
},
"file": {
"uploadFailed": "File upload failed",
"invalidFormat": "Unsupported file format",
"tooLarge": "File size exceeds limit",
"notFound": "File not found"
},
"operation": {
"failed": "Operation failed",
"cancelled": "Operation cancelled",
"notSupported": "Operation not supported",
"conflict": "Operation conflict, please try again later"
},
"browser": {
"audioNotSupported": "Your browser does not support audio playback."
}
}
@@ -0,0 +1,23 @@
{
"operation": {
"saved": "Save Successful",
"created": "Create Successful",
"updated": "Update Successful",
"deleted": "Delete Successful",
"uploaded": "Upload Successful",
"downloaded": "Download Successful",
"imported": "Import Successful",
"exported": "Export Successful",
"copied": "Copy Successful",
"sent": "Send Successful"
},
"connection": {
"connected": "Connection Successful",
"authenticated": "Login Successful",
"synchronized": "Synchronization Successful"
},
"validation": {
"valid": "Validation Passed",
"completed": "Operation Completed"
}
}
@@ -0,0 +1,24 @@
{
"required": "This field is required",
"email": "Please enter a valid email address",
"url": "Please enter a valid URL",
"number": "Please enter a valid number",
"min": "Minimum value is {min}",
"max": "Maximum value is {max}",
"minLength": "Minimum length is {length} characters",
"maxLength": "Maximum length is {length} characters",
"pattern": "Invalid format",
"unique": "This value already exists",
"confirm": "The two entries do not match",
"fileSize": "File size cannot exceed {size}MB",
"fileType": "Unsupported file type",
"required_field": "Please fill in the required field",
"invalid_format": "Invalid format",
"password_too_short": "Password must be at least 8 characters",
"password_too_weak": "Password is too weak",
"invalid_phone": "Please enter a valid phone number",
"invalid_date": "Please enter a valid date",
"date_range": "Invalid date range",
"upload_failed": "File upload failed",
"network_error": "Network connection error, please try again"
}
-108
View File
@@ -1,108 +0,0 @@
{
"sidebar": {
"dashboard": "统计",
"platforms": "消息平台",
"providers": "服务提供商",
"toolUse": "MCP",
"config": "配置文件",
"extension": "插件管理",
"extensionMarketplace": "插件市场",
"chat": "聊天",
"conversation": "对话数据库",
"console": "控制台",
"alkaid": "Alkaid",
"about": "关于",
"settings": "设置",
"documentation": "官方文档",
"github": "GitHub",
"drag": "拖拽"
},
"common": {
"save": "保存",
"cancel": "取消",
"close": "关闭",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"confirm": "确认",
"loading": "加载中...",
"success": "成功",
"error": "错误",
"warning": "警告",
"info": "信息",
"name": "名称",
"description": "描述",
"author": "作者",
"status": "状态",
"actions": "操作",
"enable": "启用",
"disable": "禁用",
"enabled": "已启用",
"disabled": "已禁用",
"reload": "重载",
"configure": "配置",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
"language": "语言"
},
"extension": {
"title": "已安装的插件",
"subtitle": "管理已经安装的所有插件",
"showSystemPlugins": "显示系统插件",
"hideSystemPlugins": "隐藏系统插件",
"platformCommandConfig": "平台命令配置",
"noPlugins": "暂无插件",
"tryInstallOrShowSystem": "尝试安装插件或者显示系统插件",
"configDialog": {
"title": "插件配置",
"noConfig": "这个插件没有配置"
},
"platformConfig": {
"title": "平台命令可用性配置",
"description": "设置每个插件在不同平台上的可用性,勾选表示启用",
"noPlatforms": "未找到平台适配器",
"addPlatformFirst": "请先在 平台管理 中添加并配置平台适配器,然后再设置插件的平台可用性",
"goToPlatformManagement": "前往平台管理"
}
},
"extensionMarketplace": {
"title": "插件市场",
"installPlugin": "安装插件",
"fromGitHub": "从 GitHub 上在线下载",
"fromLocal": "从本机上传 .zip 压缩包",
"repoUrl": "仓库链接",
"selectFile": "选择文件",
"pluginDevelopmentDoc": "插件开发文档",
"submitPluginRepo": "提交插件仓库"
},
"platform": {
"title": "平台适配器管理",
"subtitle": "管理机器人的平台适配器,连接到不同的聊天平台",
"adapters": "平台适配器",
"addAdapter": "新增适配器"
},
"provider": {
"title": "服务提供商",
"tabTypes": {
"chat_completion": "基本对话",
"speech_to_text": "语音转文本",
"text_to_speech": "文本转语音",
"embedding": "Embedding"
},
"openaiDescription": "{type} 服务提供商。同时也支持所有兼容 OpenAI API 的模型提供商。",
"defaultDescription": "{type} 服务提供商"
},
"auth": {
"login": "登录",
"username": "用户名",
"password": "密码"
},
"chart": {
"messageCount": "消息条数",
"time": "时间"
},
"alkaid": {
"comingSoon": "前面的世界,以后再来探索吧!"
}
}
@@ -0,0 +1,22 @@
{
"create": "创建",
"read": "读取",
"update": "更新",
"delete": "删除",
"search": "搜索",
"filter": "筛选",
"sort": "排序",
"export": "导出",
"import": "导入",
"backup": "备份",
"restore": "恢复",
"copy": "复制",
"paste": "粘贴",
"cut": "剪切",
"undo": "撤销",
"redo": "重做",
"refresh": "刷新",
"submit": "提交",
"reset": "重置",
"clear": "清空"
}
@@ -0,0 +1,40 @@
{
"save": "保存",
"cancel": "取消",
"close": "关闭",
"delete": "删除",
"edit": "编辑",
"add": "添加",
"confirm": "确认",
"loading": "加载中...",
"success": "成功",
"error": "错误",
"warning": "警告",
"info": "信息",
"name": "名称",
"description": "描述",
"author": "作者",
"status": "状态",
"actions": "操作",
"enable": "启用",
"disable": "禁用",
"enabled": "已启用",
"disabled": "已禁用",
"reload": "重载",
"configure": "配置",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
"language": "语言",
"type": "输入",
"press": "按",
"longPress": "长按",
"yes": "是",
"no": "否",
"dialog": {
"confirmTitle": "确认操作",
"confirmMessage": "你确定要执行此操作吗?",
"confirmButton": "确定",
"cancelButton": "取消"
}
}
@@ -0,0 +1,84 @@
{
"version": {
"hasNewVersion": "AstrBot 有新版本!",
"dashboardHasNewVersion": "WebUI 有新版本!"
},
"buttons": {
"update": "更新",
"account": "账户",
"theme": {
"light": "浅色模式",
"dark": "深色模式"
}
},
"updateDialog": {
"title": "更新 AstrBot",
"currentVersion": "当前版本",
"status": {
"checking": "正在检查更新...",
"switching": "正在切换版本...",
"updating": "正在更新..."
},
"tabs": {
"release": "😊 正式版",
"dev": "🧐 开发版(master 分支)"
},
"updateToLatest": "更新到最新版本",
"tip": "💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件,这可能会造成部分数据显示错误。您可在",
"tipLink": "此处",
"tipContinue": "找到对应的面板文件 dist.zip,解压后替换 data/dist 文件夹即可。当然,前端源代码在 dashboard 目录下,你也可以自己使用 npm install 和 npm build 构建。",
"dockerTip": "`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
"dockerTipLink": "watchtower",
"dockerTipContinue": "来自动监控拉取。",
"table": {
"tag": "标签",
"publishDate": "发布时间",
"content": "内容",
"sourceUrl": "源码地址",
"actions": "操作",
"sha": "SHA",
"date": "日期",
"message": "信息",
"view": "查看",
"switch": "切换"
},
"manualInput": {
"title": "手动输入版本号或 Commit SHA",
"placeholder": "输入版本号或 master 分支下的 commit hash。",
"hint": "如 v3.3.16 (不带 SHA) 或 42e5ec5d80b93b6bfe8b566754d45ffac4c3fe0b",
"linkText": "查看 master 分支提交记录(点击右边的 copy 即可复制)",
"confirm": "确定切换"
},
"dashboardUpdate": {
"title": "单独更新管理面板到最新版本",
"currentVersion": "当前版本",
"hasNewVersion": "有新版本!",
"isLatest": "已经是最新版本了。",
"downloadAndUpdate": "下载并更新"
}
},
"accountDialog": {
"title": "修改账户",
"securityWarning": "安全提醒: 请修改默认密码以确保账户安全",
"form": {
"currentPassword": "当前密码",
"newPassword": "新密码",
"newUsername": "新用户名 (可选)",
"passwordHint": "密码长度至少 8 位",
"usernameHint": "留空表示不修改用户名",
"defaultCredentials": "默认用户名和密码均为 astrbot"
},
"validation": {
"passwordRequired": "请输入密码",
"passwordMinLength": "密码长度至少 8 位",
"usernameMinLength": "用户名长度至少3位"
},
"actions": {
"save": "保存修改",
"cancel": "取消"
},
"messages": {
"updateFailed": "修改失败,请重试"
}
}
}
@@ -0,0 +1,18 @@
{
"dashboard": "统计",
"platforms": "消息平台",
"providers": "服务提供商",
"toolUse": "MCP",
"config": "配置文件",
"extension": "插件管理",
"extensionMarketplace": "插件市场",
"chat": "聊天",
"conversation": "对话数据库",
"console": "控制台",
"alkaid": "Alkaid",
"about": "关于",
"settings": "设置",
"documentation": "官方文档",
"github": "GitHub",
"drag": "拖拽"
}
@@ -0,0 +1,22 @@
{
"loading": "加载中",
"success": "成功",
"error": "错误",
"warning": "警告",
"info": "信息",
"pending": "等待中",
"processing": "处理中",
"completed": "已完成",
"failed": "失败",
"cancelled": "已取消",
"timeout": "超时",
"connecting": "连接中",
"connected": "已连接",
"disconnected": "已断开",
"online": "在线",
"offline": "离线",
"active": "活跃",
"inactive": "非活跃",
"ready": "就绪",
"busy": "忙碌"
}
@@ -0,0 +1,17 @@
{
"hero": {
"title": "AstrBot",
"subtitle": "A project out of interests and loves ❤️",
"starButton": "Star 这个项目! 🌟",
"issueButton": "提交 Issue"
},
"contributors": {
"title": "贡献者",
"description": "本项目由众多开源社区成员共同维护。感谢每一位贡献者的付出!",
"viewLink": "查看 AstrBot 贡献者"
},
"stats": {
"title": "全球部署",
"license": "AstrBot 采用 AGPL v3 协议开源"
}
}
@@ -0,0 +1,26 @@
{
"title": "Alkaid实验室",
"subtitle": "探索前沿AI功能",
"comingSoon": "前面的世界,以后再来探索吧!",
"page": {
"title": "The Alkaid Project.",
"subtitle": "AstrBot Alpha 项目",
"navigation": {
"knowledgeBase": "知识库",
"longTermMemory": "长期记忆层",
"other": "..."
}
},
"features": {
"knowledgeBase": "知识库",
"longTermMemory": "长期记忆",
"advancedChat": "高级对话",
"multiModal": "多模态交互"
},
"status": {
"experimental": "实验性",
"beta": "测试版",
"stable": "稳定版",
"deprecated": "已弃用"
}
}
@@ -0,0 +1,33 @@
{
"title": "知识库",
"subtitle": "管理和查询知识库内容",
"upload": {
"title": "上传文档",
"selectFiles": "选择文件",
"supportedFormats": "支持的格式",
"dragDrop": "拖拽文件到此处",
"processing": "处理中...",
"success": "上传成功",
"error": "上传失败"
},
"search": {
"placeholder": "搜索知识库...",
"results": "搜索结果",
"noResults": "未找到相关内容",
"searching": "搜索中..."
},
"documents": {
"title": "文档列表",
"name": "文档名称",
"size": "大小",
"uploadTime": "上传时间",
"status": "状态",
"actions": "操作"
},
"management": {
"delete": "删除",
"preview": "预览",
"download": "下载",
"reindex": "重新索引"
}
}
@@ -0,0 +1,37 @@
{
"title": "长期记忆",
"subtitle": "AI助手的长期记忆管理",
"memories": {
"title": "记忆列表",
"content": "记忆内容",
"importance": "重要程度",
"createTime": "创建时间",
"lastAccess": "最后访问",
"category": "分类"
},
"categories": {
"personal": "个人信息",
"preferences": "偏好设置",
"conversations": "对话历史",
"facts": "事实信息",
"skills": "技能知识"
},
"importance": {
"high": "高",
"medium": "中",
"low": "低"
},
"actions": {
"view": "查看详情",
"edit": "编辑",
"delete": "删除",
"pin": "置顶",
"unpin": "取消置顶"
},
"filters": {
"all": "全部",
"category": "按分类",
"importance": "按重要程度",
"dateRange": "按时间范围"
}
}
@@ -0,0 +1,13 @@
{
"login": "登录",
"username": "用户名",
"password": "密码",
"logo": {
"title": "AstrBot 仪表盘",
"subtitle": "欢迎使用"
},
"theme": {
"switchToDark": "切换到深色主题",
"switchToLight": "切换到浅色主题"
}
}
@@ -0,0 +1,4 @@
{
"messageCount": "消息条数",
"time": "时间"
}
@@ -0,0 +1,59 @@
{
"title": "聊天吧!",
"subtitle": "与AI助手进行对话",
"input": {
"placeholder": "开始输入...",
"send": "发送",
"clear": "清空",
"upload": "上传文件",
"voice": "语音输入"
},
"message": {
"user": "用户",
"assistant": "助手",
"system": "系统",
"error": "错误消息",
"loading": "思考中..."
},
"voice": {
"start": "开始录音",
"stop": "停止录音",
"recording": "新录音",
"processing": "处理中...",
"error": "录音失败"
},
"welcome": {
"title": "欢迎使用 AstrBot",
"subtitle": "您的智能对话助手",
"quickActions": "快速操作",
"examples": "示例问题"
},
"actions": {
"copy": "复制",
"regenerate": "重新生成",
"like": "点赞",
"dislike": "踩",
"share": "分享",
"newChat": "创建对话",
"deleteChat": "删除此对话",
"editTitle": "编辑标题",
"fullscreen": "全屏模式",
"exitFullscreen": "退出全屏"
},
"conversation": {
"newConversation": "新对话",
"noHistory": "暂无对话历史",
"systemStatus": "系统状态",
"llmService": "LLM 服务",
"speechToText": "语音转文本"
},
"modes": {
"darkMode": "切换到夜间模式",
"lightMode": "切换到日间模式"
},
"shortcuts": {
"help": "获取帮助",
"voiceRecord": "录制语音",
"pasteImage": "粘贴图片"
}
}
@@ -0,0 +1,62 @@
{
"title": "配置文件",
"subtitle": "管理系统配置和设置",
"editor": {
"visual": "可视化编辑",
"code": "代码编辑",
"revertCode": "回到更改前的代码",
"applyConfig": "应用此配置",
"applyTip": "`应用此配置` 将配置暂存并应用到可视化。如要保存,需再点击右下角保存按钮。"
},
"actions": {
"save": "保存配置",
"delete": "删除这项",
"add": "添加",
"reset": "重置为默认",
"export": "导出配置",
"import": "导入配置",
"validate": "验证配置"
},
"help": {
"documentation": "官方文档",
"support": "加群询问",
"helpText": "不了解配置?请见 {documentation} 或 {support}。",
"helpPrefix": "不了解配置?请见",
"helpMiddle": "或",
"helpSuffix": "。"
},
"messages": {
"configApplied": "配置成功应用。如要保存,需再点击右下角保存按钮。",
"configApplyError": "配置未应用,Json 格式错误。",
"saveSuccess": "配置保存成功",
"saveError": "配置保存失败",
"loadError": "配置加载失败"
},
"sections": {
"general": "常规设置",
"advanced": "高级设置",
"security": "安全设置",
"appearance": "外观设置",
"notification": "通知设置"
},
"general": {
"botName": "机器人名称",
"language": "界面语言",
"timezone": "时区",
"autoSave": "自动保存",
"debugMode": "调试模式"
},
"advanced": {
"logLevel": "日志级别",
"maxConnections": "最大连接数",
"timeout": "超时时间",
"retryAttempts": "重试次数",
"cacheSize": "缓存大小"
},
"security": {
"apiKey": "API密钥",
"allowedHosts": "允许的主机",
"rateLimit": "频率限制",
"encryption": "加密设置"
}
}
@@ -0,0 +1,15 @@
{
"title": "控制台",
"autoScroll": {
"enabled": "自动滚动已开启",
"disabled": "自动滚动已关闭"
},
"pipInstall": {
"button": "安装 pip 库",
"dialogTitle": "安装 Pip 库",
"packageLabel": "*库名,如 llmtuner",
"mirrorLabel": "强制 PyPI 软件仓库链接(可选)",
"mirrorHint": "强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`",
"installButton": "安装"
}
}
@@ -0,0 +1,77 @@
{
"title": "对话管理",
"subtitle": "管理和查看用户对话历史记录",
"filters": {
"title": "筛选条件",
"platform": "平台",
"type": "类型",
"search": "搜索关键词",
"reset": "重置"
},
"history": {
"title": "对话历史",
"refresh": "刷新"
},
"table": {
"headers": {
"title": "对话标题",
"platform": "平台",
"type": "类型",
"sessionId": "ID",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"actions": "操作"
}
},
"actions": {
"view": "查看",
"edit": "编辑",
"delete": "删除"
},
"messageTypes": {
"group": "群聊",
"friend": "私聊",
"unknown": "未知"
},
"status": {
"noTitle": "无标题对话",
"unknown": "未知",
"noData": "暂无对话记录",
"emptyContent": "对话内容为空",
"audioNotSupported": "您的浏览器不支持音频播放。"
},
"dialogs": {
"view": {
"title": "对话详情",
"editMode": "编辑对话",
"previewMode": "预览模式",
"saveChanges": "保存修改",
"close": "关闭",
"confirmClose": "您有未保存的更改,确定要关闭吗?"
},
"edit": {
"title": "编辑对话信息",
"titleLabel": "对话标题",
"titlePlaceholder": "输入对话标题",
"cancel": "取消",
"save": "保存"
},
"delete": {
"title": "确认删除",
"message": "确定要删除对话 {title} 吗?此操作不可恢复。",
"cancel": "取消",
"confirm": "删除"
}
},
"messages": {
"fetchError": "获取对话列表失败",
"saveSuccess": "保存成功",
"saveError": "保存失败",
"deleteSuccess": "删除成功",
"deleteError": "删除失败",
"historyError": "获取对话历史失败",
"historySaveSuccess": "对话历史保存成功",
"historySaveError": "对话历史保存失败",
"invalidJson": "JSON格式无效"
}
}
@@ -0,0 +1,61 @@
{
"title": "控制台",
"subtitle": "实时监控和统计数据",
"lastUpdate": "最后更新",
"status": {
"loading": "加载中...",
"dataError": "获取数据失败",
"noticeError": "获取公告失败"
},
"stats": {
"totalMessage": {
"title": "消息总数",
"subtitle": "所有平台发送的消息总计"
},
"onlinePlatform": {
"title": "消息平台",
"subtitle": "已连接的消息平台数量"
},
"runningTime": {
"title": "运行时间",
"subtitle": "系统已运行时长"
},
"memoryUsage": {
"title": "内存占用",
"subtitle": "系统内存使用情况",
"cpuLoad": "CPU 负载",
"status": {
"good": "良好",
"normal": "正常",
"high": "偏高"
}
}
},
"charts": {
"messageTrend": {
"title": "消息趋势分析",
"subtitle": "跟踪消息数量随时间的变化",
"totalMessages": "总消息数",
"dailyAverage": "平均每天",
"growthRate": "增长率",
"timeLabel": "时间",
"messageCount": "消息条数",
"timeRanges": {
"1day": "过去 1 天",
"3days": "过去 3 天",
"1week": "过去 7 天",
"1month": "过去 30 天"
}
},
"platformStat": {
"title": "平台消息统计",
"subtitle": "各平台消息数量分布",
"total": "总计",
"noData": "暂无平台数据",
"messageUnit": "条",
"platformCount": "平台数",
"mostActive": "最活跃",
"totalPercentage": "总消息占比"
}
}
}
@@ -0,0 +1,131 @@
{
"title": "插件管理",
"subtitle": "管理和配置系统插件",
"tabs": {
"installed": "已安装",
"market": "插件市场"
},
"search": {
"placeholder": "搜索插件...",
"marketPlaceholder": "搜索市场插件..."
},
"views": {
"card": "卡片视图",
"list": "列表视图"
},
"buttons": {
"showSystemPlugins": "显示系统插件",
"hideSystemPlugins": "隐藏系统插件",
"platformConfig": "平台命令配置",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
"reload": "重载",
"enable": "启用",
"disable": "禁用",
"configure": "配置",
"viewInfo": "行为",
"viewDocs": "文档",
"close": "关闭",
"save": "保存",
"saveAndClose": "保存并关闭",
"cancel": "取消"
},
"status": {
"enabled": "启用",
"disabled": "禁用",
"system": "系统",
"loading": "加载中...",
"installed": "已安装"
},
"tooltips": {
"enable": "点击启用",
"disable": "点击禁用",
"reload": "重载",
"configure": "配置",
"viewInfo": "行为",
"viewDocs": "文档",
"update": "更新",
"uninstall": "卸载"
},
"table": {
"headers": {
"name": "名称",
"description": "描述",
"version": "版本",
"author": "作者",
"status": "状态",
"actions": "操作",
"stars": "Star数",
"lastUpdate": "最近更新",
"tags": "标签",
"eventType": "行为类型",
"specificType": "具体类型",
"trigger": "触发方式"
}
},
"empty": {
"noPlugins": "暂无插件",
"noPluginsDesc": "尝试安装插件或者显示系统插件"
},
"market": {
"recommended": "🥳 推荐",
"allPlugins": "📦 全部插件",
"showFullName": "完整名称",
"devDocs": "插件开发文档",
"submitRepo": "提交插件仓库"
},
"dialogs": {
"error": {
"title": "错误信息",
"checkConsole": "详情请检查控制台"
},
"platformConfig": {
"title": "平台命令可用性配置",
"description": "设置每个插件在不同平台上的可用性,勾选表示启用",
"noAdapters": "未找到平台适配器",
"noAdaptersDesc": "请先在 平台管理 中添加并配置平台适配器,然后再设置插件的平台可用性",
"goPlatforms": "前往平台管理",
"selectAll": "全选",
"selectAllNormal": "全选普通插件",
"selectAllSystem": "全选系统插件",
"selectNone": "全不选",
"toggleAll": "反选"
},
"config": {
"title": "插件配置",
"noConfig": "这个插件没有配置"
},
"loading": {
"title": "加载中...",
"logs": "日志"
}
},
"messages": {
"uninstalling": "正在卸载",
"refreshing": "正在刷新插件列表...",
"refreshSuccess": "插件列表已刷新!",
"refreshFailed": "刷新插件列表时发生错误",
"reloadSuccess": "重载成功",
"updateSuccess": "更新成功!",
"addSuccess": "添加成功!",
"saveSuccess": "保存成功!",
"deleteSuccess": "删除成功!",
"installing": "正在从文件安装插件",
"installingFromUrl": "正在从链接安装插件...",
"installFailed": "安装插件失败:",
"getPlatformConfigFailed": "获取平台插件配置失败:",
"savePlatformConfigFailed": "保存平台插件配置失败:",
"getMarketDataFailed": "获取插件市场数据失败:",
"hasUpdate": "有新版本:",
"confirmDelete": "确定要删除插件吗?",
"fillUrlOrFile": "请填写插件链接或上传插件文件",
"dontFillBoth": "请不要同时填写插件链接和上传插件文件"
},
"upload": {
"fromFile": "从文件安装",
"fromUrl": "从链接安装",
"selectFile": "选择文件",
"enterUrl": "输入插件仓库链接"
}
}
@@ -0,0 +1,40 @@
{
"title": "平台适配器管理",
"subtitle": "管理机器人的平台适配器,连接到不同的聊天平台",
"adapters": "平台适配器",
"addAdapter": "新增适配器",
"emptyText": "暂无平台适配器,点击 新增适配器 添加",
"details": {
"adapterType": "适配器类型",
"token": "Token",
"description": "描述"
},
"logs": {
"title": "平台日志",
"expand": "展开",
"collapse": "收起"
},
"dialog": {
"add": "新增",
"edit": "编辑",
"adapter": "平台适配器",
"refresh": "刷新",
"cancel": "取消",
"save": "保存"
},
"messages": {
"updateSuccess": "更新成功!",
"addSuccess": "添加成功!",
"deleteSuccess": "删除成功!",
"statusUpdateSuccess": "状态更新成功!",
"deleteConfirm": "确定要删除平台适配器"
},
"status": {
"enabled": "已启用",
"disabled": "已禁用",
"connecting": "连接中",
"connected": "已连接",
"disconnected": "已断开",
"error": "错误"
}
}
@@ -0,0 +1,82 @@
{
"title": "服务提供商管理",
"subtitle": "管理模型服务提供商",
"providers": {
"title": "服务提供商",
"settings": "设置",
"addProvider": "新增服务提供商",
"providerType": "提供商类型",
"tabs": {
"all": "全部",
"chatCompletion": "基本对话",
"speechToText": "语音转文字",
"textToSpeech": "文字转语音",
"embedding": "Embedding"
},
"empty": {
"all": "暂无服务提供商,点击 新增服务提供商 添加",
"typed": "暂无{type}类型的服务提供商,点击 新增服务提供商 添加"
},
"description": {
"openai": "{type} 服务提供商。同时也支持所有兼容 OpenAI API 的模型提供商。",
"default": "{type} 服务提供商"
}
},
"availability": {
"title": "服务提供商可用性",
"subtitle": "通过测试模型对话可用性判断,可能产生API费用",
"refresh": "刷新状态",
"noData": "点击\"刷新状态\"按钮获取服务提供商可用性",
"available": "可用",
"unavailable": "不可用",
"errorMessage": "错误信息"
},
"logs": {
"title": "服务日志",
"expand": "展开",
"collapse": "收起"
},
"dialogs": {
"addProvider": {
"title": "服务提供商",
"tabs": {
"basic": "基本",
"speechToText": "语音转文字",
"textToSpeech": "文字转语音",
"embedding": "Embedding"
},
"noTemplates": "暂无{type}类型的提供商模板"
},
"config": {
"addTitle": "新增",
"editTitle": "编辑",
"provider": "服务提供商",
"cancel": "取消",
"save": "保存"
},
"settings": {
"title": "服务提供商设置",
"sessionSeparation": {
"title": "启用提供商会话隔离",
"description": "不同会话将可独立选择文本生成、TTS、STT 等服务提供商。"
},
"close": "关闭"
}
},
"messages": {
"success": {
"update": "更新成功!",
"add": "添加成功!",
"delete": "删除成功!",
"statusUpdate": "状态更新成功!",
"sessionSeparation": "会话隔离设置已更新"
},
"error": {
"sessionSeparation": "获取会话隔离配置失败",
"fetchStatus": "获取服务提供商状态失败"
},
"confirm": {
"delete": "确定要删除服务提供商 {id} 吗?"
}
}
}
@@ -0,0 +1,18 @@
{
"network": {
"title": "网络",
"githubProxy": {
"title": "GitHub 加速地址",
"subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。",
"label": "选择 GitHub 加速地址"
}
},
"system": {
"title": "系统",
"restart": {
"title": "重启",
"subtitle": "重启 AstrBot",
"button": "重启"
}
}
}
@@ -0,0 +1,114 @@
{
"title": "函数工具管理",
"subtitle": "管理 MCP 服务器和查看可用的函数工具",
"tooltip": {
"info": "函数调用和 MCP 是什么?",
"marketplace": "浏览和安装来自社区的 MCP 服务器",
"serverConfig": "MCP 服务器(stdio)配置支持以下字段:\ncommand: 命令名称 (例如 python 或 uv)\nargs: 命令参数数组 (例如 [\"run\", \"server.py\"])\nenv: 环境变量对象 (例如 {\"api_key\": \"abc\"})\ncwd: 工作目录路径 (例如 /path/to/server)\nencoding: 输出编码 (默认 utf-8)\nencoding_error_handler: The text encoding error handler. Defaults to strict.\n其他字段请参考 MCP 文档\n⚠️ 如果您使用 Docker 部署 AstrBot, 请务必将 MCP 服务器装在 AstrBot 挂载好的 data 目录下"
},
"tabs": {
"local": "本地服务器",
"marketplace": "MCP 市场"
},
"mcpServers": {
"title": "MCP 服务器",
"buttons": {
"refresh": "刷新",
"add": "新增服务器",
"useTemplate": "使用模板"
},
"empty": "暂无 MCP 服务器,点击 新增服务器 添加",
"status": {
"noTools": "无可用工具",
"availableTools": "可用工具",
"configSummary": "配置: {keys}",
"noConfig": "未设置配置"
}
},
"functionTools": {
"title": "函数工具",
"buttons": {
"expand": "展开",
"collapse": "收起"
},
"search": "搜索函数工具",
"empty": "没有可用的函数工具",
"description": "功能描述",
"parameters": "参数列表",
"noParameters": "此工具没有参数",
"table": {
"paramName": "参数名",
"type": "类型",
"description": "描述",
"required": "必填"
}
},
"marketplace": {
"title": "MCP 服务器市场",
"search": "搜索服务器",
"buttons": {
"refresh": "刷新",
"detail": "详情",
"import": "导入"
},
"loading": "正在加载 MCP 服务器市场...",
"empty": "暂无可用的 MCP 服务器",
"status": {
"availableTools": "可用工具 ({count})",
"noToolsInfo": "无可用工具信息"
}
},
"dialogs": {
"addServer": {
"title": "新增 MCP 服务器",
"editTitle": "编辑 MCP 服务器",
"fields": {
"name": "服务器名称",
"nameRequired": "名称是必填项",
"enable": "启用服务器",
"config": "服务器配置"
},
"configNotes": {
"note1": "1. 某些 MCP 服务器可能需要按照其要求在 env 中填充 `API_KEY` 或 `TOKEN` 等信息,请注意检查是否填写。",
"note2": "2. 当配置中指定 url 参数时:如果还同时指定 `transport` 参数的值为 `streamable_http`,则使用 Steamable HTTP,否则使用 SSE 连接。"
},
"errors": {
"configEmpty": "配置不能为空",
"jsonFormat": "JSON 格式错误: {error}",
"jsonParse": "JSON 解析错误: {error}"
},
"buttons": {
"cancel": "取消",
"save": "保存"
}
},
"serverDetail": {
"title": "服务器详情",
"installConfig": "安装配置",
"availableTools": "可用工具",
"buttons": {
"close": "关闭",
"importConfig": "导入配置"
}
},
"confirmDelete": "确定要删除服务器 {name} 吗?"
},
"messages": {
"getServersError": "获取 MCP 服务器列表失败: {error}",
"getToolsError": "获取函数工具列表失败: {error}",
"saveSuccess": "保存成功!",
"saveError": "保存失败: {error}",
"deleteSuccess": "删除成功!",
"deleteError": "删除失败: {error}",
"updateSuccess": "更新成功!",
"updateError": "更新失败: {error}",
"getMarketError": "获取 MCP 市场服务器列表失败: {error}",
"importError": {
"noConfig": "此服务器没有可用配置",
"invalidFormat": "服务器配置格式不正确",
"failed": "导入配置失败: {error}"
},
"configParseError": "配置解析错误: {error}",
"noAvailableConfig": "无可用配置"
}
}
@@ -0,0 +1,39 @@
{
"network": {
"timeout": "网络请求超时,请稍后重试",
"connection": "网络连接失败,请检查网络状态",
"server": "服务器错误,请联系技术支持",
"unavailable": "服务暂不可用",
"forbidden": "访问被拒绝"
},
"validation": {
"required": "此字段为必填项",
"invalid": "输入格式不正确",
"tooLong": "输入内容过长",
"tooShort": "输入内容过短",
"email": "请输入有效的邮箱地址",
"url": "请输入有效的URL地址",
"number": "请输入有效的数字"
},
"auth": {
"unauthorized": "未授权访问,请重新登录",
"forbidden": "权限不足,无法执行此操作",
"tokenExpired": "登录已过期,请重新登录",
"invalidCredentials": "用户名或密码错误"
},
"file": {
"uploadFailed": "文件上传失败",
"invalidFormat": "不支持的文件格式",
"tooLarge": "文件大小超出限制",
"notFound": "文件未找到"
},
"operation": {
"failed": "操作失败",
"cancelled": "操作已取消",
"notSupported": "不支持此操作",
"conflict": "操作冲突,请稍后重试"
},
"browser": {
"audioNotSupported": "您的浏览器不支持音频播放。"
}
}
@@ -0,0 +1,23 @@
{
"operation": {
"saved": "保存成功",
"created": "创建成功",
"updated": "更新成功",
"deleted": "删除成功",
"uploaded": "上传成功",
"downloaded": "下载成功",
"imported": "导入成功",
"exported": "导出成功",
"copied": "复制成功",
"sent": "发送成功"
},
"connection": {
"connected": "连接成功",
"authenticated": "登录成功",
"synchronized": "同步成功"
},
"validation": {
"valid": "验证通过",
"completed": "操作完成"
}
}
@@ -0,0 +1,24 @@
{
"required": "此字段为必填项",
"email": "请输入有效的邮箱地址",
"url": "请输入有效的URL地址",
"number": "请输入有效的数字",
"min": "最小值为 {min}",
"max": "最大值为 {max}",
"minLength": "至少需要 {length} 个字符",
"maxLength": "最多允许 {length} 个字符",
"pattern": "格式不正确",
"unique": "该值已存在",
"confirm": "两次输入不一致",
"fileSize": "文件大小不能超过 {size}MB",
"fileType": "不支持的文件类型",
"required_field": "请填写必填字段",
"invalid_format": "格式无效",
"password_too_short": "密码至少需要8个字符",
"password_too_weak": "密码强度太弱",
"invalid_phone": "请输入有效的手机号码",
"invalid_date": "请输入有效的日期",
"date_range": "日期范围无效",
"upload_failed": "文件上传失败",
"network_error": "网络连接错误,请重试"
}
+33
View File
@@ -0,0 +1,33 @@
// 导出核心组件
export { I18nValidator } from '../validator';
export { I18nLoader } from '../loader';
export type * from '../types';
// 实用工具函数
export function generateMissingKeys(
sourceTranslations: Record<string, any>,
targetTranslations: Record<string, any>
): string[] {
const missing: string[] = [];
function traverse(source: any, target: any, path: string = '') {
for (const key in source) {
const currentPath = path ? `${path}.${key}` : key;
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) {
missing.push(currentPath);
} else {
traverse(source[key], target[key], currentPath);
}
} else {
if (!(key in target)) {
missing.push(currentPath);
}
}
}
}
traverse(sourceTranslations, targetTranslations);
return missing;
}
+366
View File
@@ -0,0 +1,366 @@
/**
* I18n TypeScript Type Definitions
* 国际化类型定义,确保类型安全
*/
// 核心模块类型定义
export interface CoreTranslations {
common: {
save: string;
cancel: string;
close: string;
delete: string;
edit: string;
add: string;
confirm: string;
loading: string;
success: string;
error: string;
warning: string;
info: string;
name: string;
description: string;
author: string;
status: string;
actions: string;
enable: string;
disable: string;
enabled: string;
disabled: string;
reload: string;
configure: string;
install: string;
uninstall: string;
update: string;
language: string;
};
actions: {
create: string;
read: string;
update: string;
delete: string;
search: string;
filter: string;
sort: string;
export: string;
import: string;
backup: string;
restore: string;
};
status: {
loading: string;
success: string;
error: string;
warning: string;
info: string;
pending: string;
processing: string;
completed: string;
failed: string;
cancelled: string;
};
navigation: {
dashboard: string;
platforms: string;
providers: string;
toolUse: string;
config: string;
extension: string;
chat: string;
conversation: string;
console: string;
alkaid: string;
about: string;
settings: string;
documentation: string;
github: string;
drag: string;
};
}
// 功能模块类型定义
export interface FeatureTranslations {
chat: {
title: string;
subtitle: string;
input: {
placeholder: string;
send: string;
clear: string;
};
message: {
user: string;
assistant: string;
system: string;
};
voice: {
start: string;
stop: string;
recording: string;
};
};
extension: {
title: string;
subtitle: string;
showSystemPlugins: string;
hideSystemPlugins: string;
platformCommandConfig: string;
noPlugins: string;
tryInstallOrShowSystem: string;
configDialog: {
title: string;
noConfig: string;
};
platformConfig: {
title: string;
description: string;
noPlatforms: string;
addPlatformFirst: string;
goToPlatformManagement: string;
};
marketplace: {
title: string;
installPlugin: string;
fromGitHub: string;
fromLocal: string;
repoUrl: string;
selectFile: string;
pluginDevelopmentDoc: string;
submitPluginRepo: string;
};
};
conversation: {
title: string;
subtitle: string;
table: {
id: string;
platform: string;
user: string;
message: string;
time: string;
actions: string;
};
filter: {
platform: string;
user: string;
dateRange: string;
};
export: {
title: string;
format: string;
range: string;
};
};
provider: {
title: string;
tabTypes: {
chat_completion: string;
speech_to_text: string;
text_to_speech: string;
embedding: string;
};
openaiDescription: string;
defaultDescription: string;
};
platform: {
title: string;
subtitle: string;
adapters: string;
addAdapter: string;
};
config: {
title: string;
subtitle: string;
sections: {
general: string;
advanced: string;
security: string;
};
};
console: {
title: string;
subtitle: string;
clear: string;
download: string;
};
about: {
title: string;
version: string;
author: string;
license: string;
repository: string;
};
alkaid: {
comingSoon: string;
knowledgeBase: {
title: string;
subtitle: string;
};
memory: {
title: string;
subtitle: string;
};
};
}
// 消息模块类型定义
export interface MessageTranslations {
errors: {
network: {
timeout: string;
connection: string;
server: string;
};
validation: {
required: string;
invalid: string;
tooLong: string;
tooShort: string;
};
auth: {
unauthorized: string;
forbidden: string;
tokenExpired: string;
};
};
success: {
save: {
completed: string;
config: string;
settings: string;
};
action: {
created: string;
updated: string;
deleted: string;
};
};
validation: {
required: string;
email: string;
url: string;
number: string;
min: string;
max: string;
};
}
// 完整的翻译类型
export interface TranslationSchema extends CoreTranslations, FeatureTranslations, MessageTranslations {}
// 翻译键类型
export type TranslationKey =
// Core keys
| `core.common.${keyof CoreTranslations['common']}`
| `core.actions.${keyof CoreTranslations['actions']}`
| `core.status.${keyof CoreTranslations['status']}`
| `core.navigation.${keyof CoreTranslations['navigation']}`
// Feature keys
| `features.chat.${keyof FeatureTranslations['chat'] | `input.${keyof FeatureTranslations['chat']['input']}` | `message.${keyof FeatureTranslations['chat']['message']}` | `voice.${keyof FeatureTranslations['chat']['voice']}`}`
| `features.extension.${keyof FeatureTranslations['extension'] | `configDialog.${keyof FeatureTranslations['extension']['configDialog']}` | `platformConfig.${keyof FeatureTranslations['extension']['platformConfig']}` | `marketplace.${keyof FeatureTranslations['extension']['marketplace']}`}`
| `features.conversation.${keyof FeatureTranslations['conversation'] | `table.${keyof FeatureTranslations['conversation']['table']}` | `filter.${keyof FeatureTranslations['conversation']['filter']}` | `export.${keyof FeatureTranslations['conversation']['export']}`}`
| `features.provider.${keyof FeatureTranslations['provider'] | `tabTypes.${keyof FeatureTranslations['provider']['tabTypes']}`}`
| `features.platform.${keyof FeatureTranslations['platform']}`
| `features.config.${keyof FeatureTranslations['config'] | `sections.${keyof FeatureTranslations['config']['sections']}`}`
| `features.console.${keyof FeatureTranslations['console']}`
| `features.about.${keyof FeatureTranslations['about']}`
| `features.alkaid.${keyof FeatureTranslations['alkaid'] | `knowledgeBase.${keyof FeatureTranslations['alkaid']['knowledgeBase']}` | `memory.${keyof FeatureTranslations['alkaid']['memory']}`}`
// Message keys
| `messages.errors.${keyof MessageTranslations['errors'] | `network.${keyof MessageTranslations['errors']['network']}` | `validation.${keyof MessageTranslations['errors']['validation']}` | `auth.${keyof MessageTranslations['errors']['auth']}`}`
| `messages.success.${keyof MessageTranslations['success'] | `save.${keyof MessageTranslations['success']['save']}` | `action.${keyof MessageTranslations['success']['action']}`}`
| `messages.validation.${keyof MessageTranslations['validation']}`;
// 语言环境类型
export type Locale = 'zh-CN' | 'en-US';
// 翻译函数类型
export type TranslationFunction = {
(key: TranslationKey): string;
(key: TranslationKey, params: Record<string, string | number>): string;
};
// 模块加载状态
export interface ModuleLoadingState {
core: boolean;
features: boolean;
messages: boolean;
}
// 翻译配置
export interface I18nConfig {
locale: Locale;
fallbackLocale: Locale;
lazy: boolean;
preload: string[];
caching: boolean;
devMode: boolean;
}
// 验证结果
export interface ValidationResult {
isValid: boolean;
missingKeys: string[];
extraKeys: string[];
errors: ValidationError[];
}
export interface ValidationError {
type: 'missing' | 'extra' | 'type_mismatch' | 'empty_value';
key: string;
message: string;
severity: 'error' | 'warning';
}
// 使用情况报告
export interface UsageReport {
unusedKeys: string[];
undefinedKeys: string[];
coverage: number;
totalKeys: number;
usedKeys: number;
}
// 翻译统计信息
export interface TranslationStats {
modules: {
[moduleName: string]: {
keys: number;
coverage: number;
lastUpdated: string;
};
};
locales: {
[locale: string]: {
totalKeys: number;
translatedKeys: number;
coverage: number;
};
};
overall: {
totalKeys: number;
averageCoverage: number;
lastSync: string;
};
}
// 开发工具类型
export interface DevToolsData {
currentLocale: Locale;
loadedModules: string[];
cacheStats: {
size: number;
hits: number;
misses: number;
};
performance: {
loadTime: number;
renderTime: number;
};
}
// 导出类型声明模块
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends TranslationSchema {}
}
+441
View File
@@ -0,0 +1,441 @@
/**
* I18n Validator
* 国际化验证器,用于检查翻译完整性、使用情况分析和错误检测
*/
import type { ValidationResult, ValidationError, UsageReport, TranslationStats } from './types';
export class I18nValidator {
private baseLocale: string = 'zh-CN';
private supportedLocales: string[] = ['zh-CN', 'en-US'];
/**
* 验证翻译完整性
*/
validateCompleteness(localeData: Record<string, any>): ValidationResult {
const errors: ValidationError[] = [];
const missingKeys: string[] = [];
const extraKeys: string[] = [];
// 获取基准语言数据
const baseData = localeData[this.baseLocale];
if (!baseData) {
errors.push({
type: 'missing',
key: this.baseLocale,
message: `基准语言 ${this.baseLocale} 数据缺失`,
severity: 'error'
});
return { isValid: false, missingKeys, extraKeys, errors };
}
// 获取所有键
const baseKeys = this.getAllKeys(baseData);
// 验证每种语言
for (const locale of this.supportedLocales) {
if (locale === this.baseLocale) continue;
const targetData = localeData[locale];
if (!targetData) {
errors.push({
type: 'missing',
key: locale,
message: `语言 ${locale} 数据缺失`,
severity: 'error'
});
continue;
}
const targetKeys = this.getAllKeys(targetData);
// 检查缺失的键
const missing = baseKeys.filter(key => !targetKeys.includes(key));
missingKeys.push(...missing.map(key => `${locale}.${key}`));
// 检查多余的键
const extra = targetKeys.filter(key => !baseKeys.includes(key));
extraKeys.push(...extra.map(key => `${locale}.${key}`));
// 添加详细错误信息
missing.forEach(key => {
errors.push({
type: 'missing',
key: `${locale}.${key}`,
message: `${locale} 中缺失键: ${key}`,
severity: 'error'
});
});
extra.forEach(key => {
errors.push({
type: 'extra',
key: `${locale}.${key}`,
message: `${locale} 中存在多余键: ${key}`,
severity: 'warning'
});
});
}
return {
isValid: errors.filter(e => e.severity === 'error').length === 0,
missingKeys,
extraKeys,
errors
};
}
/**
* 验证翻译值的有效性
*/
validateValues(localeData: Record<string, any>): ValidationError[] {
const errors: ValidationError[] = [];
for (const [locale, data] of Object.entries(localeData)) {
this.validateNestedValues(data, locale, '', errors);
}
return errors;
}
/**
* 递归验证嵌套值
*/
private validateNestedValues(
obj: any,
locale: string,
parentKey: string,
errors: ValidationError[]
): void {
for (const [key, value] of Object.entries(obj)) {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
if (typeof value === 'object' && value !== null) {
this.validateNestedValues(value, locale, fullKey, errors);
} else if (typeof value === 'string') {
// 检查空值
if (!value.trim()) {
errors.push({
type: 'empty_value',
key: `${locale}.${fullKey}`,
message: `空翻译值: ${locale}.${fullKey}`,
severity: 'warning'
});
}
// 检查插值占位符
const placeholders = value.match(/\{[^}]+\}/g) || [];
for (const placeholder of placeholders) {
if (!/^{[a-zA-Z_][a-zA-Z0-9_]*}$/.test(placeholder)) {
errors.push({
type: 'type_mismatch',
key: `${locale}.${fullKey}`,
message: `无效的插值占位符: ${placeholder} in ${locale}.${fullKey}`,
severity: 'warning'
});
}
}
} else {
errors.push({
type: 'type_mismatch',
key: `${locale}.${fullKey}`,
message: `翻译值应为字符串,实际为: ${typeof value}`,
severity: 'error'
});
}
}
}
/**
* 分析翻译使用情况
*/
validateUsage(translationKeys: string[], usedKeys: string[]): UsageReport {
const unusedKeys = translationKeys.filter(key => !usedKeys.includes(key));
const undefinedKeys = usedKeys.filter(key => !translationKeys.includes(key));
return {
unusedKeys,
undefinedKeys,
coverage: (usedKeys.length / translationKeys.length) * 100,
totalKeys: translationKeys.length,
usedKeys: usedKeys.length
};
}
/**
* 生成翻译统计信息
*/
generateStats(localeData: Record<string, any>): TranslationStats {
const stats: TranslationStats = {
modules: {},
locales: {},
overall: {
totalKeys: 0,
averageCoverage: 0,
lastSync: new Date().toISOString()
}
};
// 分析每种语言
for (const [locale, data] of Object.entries(localeData)) {
const keys = this.getAllKeys(data);
const translatedKeys = keys.filter(key => {
const value = this.getValueByKey(data, key);
return typeof value === 'string' && value.trim() !== '';
});
stats.locales[locale] = {
totalKeys: keys.length,
translatedKeys: translatedKeys.length,
coverage: (translatedKeys.length / keys.length) * 100
};
// 分析模块
this.analyzeModules(data, locale, stats.modules);
}
// 计算总体统计
const locales = Object.values(stats.locales);
stats.overall.totalKeys = Math.max(...locales.map(l => l.totalKeys));
stats.overall.averageCoverage = locales.reduce((sum, l) => sum + l.coverage, 0) / locales.length;
return stats;
}
/**
* 分析模块统计
*/
private analyzeModules(data: any, locale: string, modules: TranslationStats['modules']): void {
for (const [moduleName, moduleData] of Object.entries(data)) {
if (typeof moduleData === 'object' && moduleData !== null) {
const moduleKey = `${locale}.${moduleName}`;
const keys = this.getAllKeys(moduleData);
const translatedKeys = keys.filter(key => {
const value = this.getValueByKey(moduleData, key);
return typeof value === 'string' && value.trim() !== '';
});
if (!modules[moduleKey]) {
modules[moduleKey] = {
keys: 0,
coverage: 0,
lastUpdated: new Date().toISOString()
};
}
modules[moduleKey].keys = keys.length;
modules[moduleKey].coverage = (translatedKeys.length / keys.length) * 100;
}
}
}
/**
* 获取对象的所有键路径
*/
private getAllKeys(obj: any, prefix: string = ''): string[] {
const keys: string[] = [];
for (const [key, value] of Object.entries(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null) {
keys.push(...this.getAllKeys(value, fullKey));
} else {
keys.push(fullKey);
}
}
return keys;
}
/**
* 根据键路径获取值
*/
private getValueByKey(obj: any, keyPath: string): any {
return keyPath.split('.').reduce((current, key) => {
return current && current[key];
}, obj);
}
/**
* 检查插值一致性
*/
validateInterpolation(localeData: Record<string, any>): ValidationError[] {
const errors: ValidationError[] = [];
const baseData = localeData[this.baseLocale];
if (!baseData) return errors;
const baseKeys = this.getAllKeys(baseData);
for (const key of baseKeys) {
const baseValue = this.getValueByKey(baseData, key);
if (typeof baseValue !== 'string') continue;
const basePlaceholders = (baseValue.match(/\{[^}]+\}/g) || []).sort();
for (const locale of this.supportedLocales) {
if (locale === this.baseLocale) continue;
const targetData = localeData[locale];
if (!targetData) continue;
const targetValue = this.getValueByKey(targetData, key);
if (typeof targetValue !== 'string') continue;
const targetPlaceholders = (targetValue.match(/\{[^}]+\}/g) || []).sort();
if (JSON.stringify(basePlaceholders) !== JSON.stringify(targetPlaceholders)) {
errors.push({
type: 'type_mismatch',
key: `${locale}.${key}`,
message: `插值占位符不匹配: ${locale}.${key},期望 ${basePlaceholders.join(', ')},实际 ${targetPlaceholders.join(', ')}`,
severity: 'error'
});
}
}
}
return errors;
}
/**
* 验证键命名规范
*/
validateKeyNaming(localeData: Record<string, any>): ValidationError[] {
const errors: ValidationError[] = [];
const keyNamingPattern = /^[a-z][a-zA-Z0-9]*$/;
for (const [locale, data] of Object.entries(localeData)) {
this.validateKeyNamingRecursive(data, locale, '', keyNamingPattern, errors);
}
return errors;
}
/**
* 递归验证键命名
*/
private validateKeyNamingRecursive(
obj: any,
locale: string,
parentKey: string,
pattern: RegExp,
errors: ValidationError[]
): void {
for (const key of Object.keys(obj)) {
const fullKey = parentKey ? `${parentKey}.${key}` : key;
if (!pattern.test(key)) {
errors.push({
type: 'type_mismatch',
key: `${locale}.${fullKey}`,
message: `键名不符合命名规范: ${key},应使用小驼峰命名`,
severity: 'warning'
});
}
if (typeof obj[key] === 'object' && obj[key] !== null) {
this.validateKeyNamingRecursive(obj[key], locale, fullKey, pattern, errors);
}
}
}
/**
* 验证多个语言包
*/
async validateLocales(locales: string[]): Promise<{
summary: {
totalLocales: number;
totalKeys: number;
missingKeys: number;
emptyValues: number;
invalidInterpolations: number;
completeness: number;
};
details: ValidationResult[];
recommendations: string[];
}> {
const results: ValidationResult[] = [];
for (const locale of locales) {
try {
// 这里应该从实际的翻译文件中加载,暂时创建基本结构
const localeData = { [locale]: {} };
const result = this.validateCompleteness(localeData);
results.push(result);
} catch (error) {
console.error(`验证语言包 ${locale} 时出错:`, error);
// 创建错误结果
const errorResult: ValidationResult = {
isValid: false,
missingKeys: [],
extraKeys: [],
errors: [
{
type: 'missing',
key: locale,
message: error instanceof Error ? error.message : '未知错误',
severity: 'error'
}
]
};
results.push(errorResult);
}
}
// 生成汇总报告
const totalKeys = results.length * 100; // 估算的总键数
const missingKeys = results.reduce((sum, r) => sum + r.missingKeys.length, 0);
return {
summary: {
totalLocales: results.length,
totalKeys,
missingKeys,
emptyValues: 0, // 暂时设为0
invalidInterpolations: 0, // 暂时设为0
completeness: totalKeys > 0 ? ((totalKeys - missingKeys) / totalKeys) * 100 : 100
},
details: results,
recommendations: [
'建议优先翻译核心模块的缺失键',
'检查所有空值并提供适当的翻译',
'确保插值占位符在所有语言中保持一致'
]
};
}
/**
* 生成验证报告
*/
generateReport(localeData: Record<string, any>, usedKeys: string[] = []): {
completeness: ValidationResult;
values: ValidationError[];
interpolation: ValidationError[];
naming: ValidationError[];
usage: UsageReport | null;
stats: TranslationStats;
} {
const completeness = this.validateCompleteness(localeData);
const values = this.validateValues(localeData);
const interpolation = this.validateInterpolation(localeData);
const naming = this.validateKeyNaming(localeData);
const stats = this.generateStats(localeData);
let usage: UsageReport | null = null;
if (usedKeys.length > 0) {
const allKeys = this.getAllKeys(localeData[this.baseLocale] || {});
usage = this.validateUsage(allKeys, usedKeys);
}
return {
completeness,
values,
interpolation,
naming,
usage,
stats
};
}
}
@@ -8,8 +8,10 @@ import {md5} from 'js-md5';
import {useAuthStore} from '@/stores/auth';
import {useCommonStore} from '@/stores/common';
import {marked} from 'marked';
import { useI18n } from '@/i18n/composables';
const customizer = useCustomizerStore();
const { t } = useI18n();
let dialog = ref(false);
let accountWarning = ref(false)
let updateStatusDialog = ref(false);
@@ -32,23 +34,23 @@ let installLoading = ref(false);
let tab = ref(0);
let releasesHeader = [
{title: '标签', key: 'tag_name'},
{title: '发布时间', key: 'published_at'},
{title: '内容', key: 'body'},
{title: '源码地址', key: 'zipball_url'},
{title: '操作', key: 'switch'}
];
const releasesHeader = computed(() => [
{title: t('core.header.updateDialog.table.tag'), key: 'tag_name'},
{title: t('core.header.updateDialog.table.publishDate'), key: 'published_at'},
{title: t('core.header.updateDialog.table.content'), key: 'body'},
{title: t('core.header.updateDialog.table.sourceUrl'), key: 'zipball_url'},
{title: t('core.header.updateDialog.table.actions'), key: 'switch'}
]);
// Form validation
const formValid = ref(true);
const passwordRules = [
(v: string) => !!v || '请输入密码',
(v: string) => v.length >= 8 || '密码长度至少 8 位'
];
const usernameRules = [
(v: string) => !v || v.length >= 3 || '用户名长度至少3位'
];
const passwordRules = computed(() => [
(v: string) => !!v || t('core.header.accountDialog.validation.passwordRequired'),
(v: string) => v.length >= 8 || t('core.header.accountDialog.validation.passwordMinLength')
]);
const usernameRules = computed(() => [
(v: string) => !v || v.length >= 3 || t('core.header.accountDialog.validation.usernameMinLength')
]);
// 显示密码相关
const showPassword = ref(false);
@@ -104,7 +106,7 @@ function accountEdit() {
.catch((err) => {
console.log(err);
accountEditStatus.value.error = true;
accountEditStatus.value.message = typeof err === 'string' ? err : '修改失败,请重试';
accountEditStatus.value.message = typeof err === 'string' ? err : t('core.header.accountDialog.messages.updateFailed');
password.value = '';
newPassword.value = '';
})
@@ -133,14 +135,14 @@ function getVersion() {
}
function checkUpdate() {
updateStatus.value = '正在检查更新...';
updateStatus.value = t('core.header.updateDialog.status.checking');
axios.get('/api/update/check')
.then((res) => {
hasNewVersion.value = res.data.data.has_new_version;
if (res.data.data.has_new_version) {
releaseMessage.value = res.data.message;
updateStatus.value = '有新版本!';
updateStatus.value = t('core.header.version.hasNewVersion');
} else {
updateStatus.value = res.data.message;
}
@@ -192,7 +194,7 @@ function getDevCommits() {
}
function switchVersion(version: string) {
updateStatus.value = '正在切换版本...';
updateStatus.value = t('core.header.updateDialog.status.switching');
installLoading.value = true;
axios.post('/api/update/do', {
version: version,
@@ -215,7 +217,7 @@ function switchVersion(version: string) {
}
function updateDashboard() {
updateStatus.value = '正在更新...';
updateStatus.value = t('core.header.updateDialog.status.updating');
axios.post('/api/update/dashboard')
.then((res) => {
updateStatus.value = res.data.message;
@@ -274,15 +276,15 @@ commonStore.getStartTime();
<!-- 版本提示信息 - 在手机上隐藏 -->
<div class="mr-4 hidden-xs">
<small v-if="hasNewVersion">
AstrBot 有新版本
{{ t('core.header.version.hasNewVersion') }}
</small>
<small v-else-if="dashboardHasNewVersion">
WebUI 有新版本
{{ t('core.header.version.dashboardHasNewVersion') }}
</small>
</div>
<!-- 语言切换器 -->
<LanguageSwitcher />
<LanguageSwitcher variant="header" />
<!-- 主题切换按钮 -->
<v-btn size="small" @click="toggleDarkMode();" class="action-btn"
@@ -297,12 +299,12 @@ commonStore.getStartTime();
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
<v-icon class="hidden-sm-and-up">mdi-update</v-icon>
<span class="hidden-xs">更新</span>
<span class="hidden-xs">{{ t('core.header.buttons.update') }}</span>
</v-btn>
</template>
<v-card>
<v-card-title class="mobile-card-title">
<span class="text-h5">更新 AstrBot</span>
<span class="text-h5">{{ t('core.header.updateDialog.title') }}</span>
<v-btn v-if="$vuetify.display.xs" icon @click="updateStatusDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
@@ -322,16 +324,14 @@ commonStore.getStartTime();
</div>
<div class="mb-4 mt-4">
<small>💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件这可能会造成部分数据显示错误您可在 <a
href="https://github.com/Soulter/AstrBot/releases">此处</a>
找到对应的面板文件 dist.zip解压后替换 data/dist 文件夹即可当然前端源代码在 dashboard 目录下你也可以自己使用
npm install npm build
构建</small>
<small>{{ t('core.header.updateDialog.tip') }} <a
href="https://github.com/Soulter/AstrBot/releases">{{ t('core.header.updateDialog.tipLink') }}</a>
{{ t('core.header.updateDialog.tipContinue') }}</small>
</div>
<v-tabs v-model="tab">
<v-tab value="0">😊 正式版</v-tab>
<v-tab value="1">🧐 开发版(master 分支)</v-tab>
<v-tab value="0">{{ t('core.header.updateDialog.tabs.release') }}</v-tab>
<v-tab value="1">{{ t('core.header.updateDialog.tabs.dev') }}</v-tab>
</v-tabs>
<v-tabs-window v-model="tab">
@@ -339,25 +339,24 @@ commonStore.getStartTime();
<v-tabs-window-item key="0" v-show="tab == 0">
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
:disabled="!hasNewVersion">
更新到最新版本
{{ t('core.header.updateDialog.updateToLatest') }}
</v-btn>
<div class="mb-4">
<small>`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板如果您正在使用 Docker
部署也可以重新拉取镜像或者使用 <a
href="https://containrrr.dev/watchtower/usage-overview/">watchtower</a> 来自动监控拉取</small>
<small>{{ t('core.header.updateDialog.dockerTip') }} <a
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
</div>
<v-data-table :headers="releasesHeader" :items="releases" item-key="name">
<template v-slot:item.body="{ item }: { item: { body: string } }">
<v-tooltip :text="item.body">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="small">查看</v-btn>
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
</template>
</v-tooltip>
</template>
<template v-slot:item.switch="{ item }: { item: { tag_name: string } }">
<v-btn @click="switchVersion(item.tag_name)" rounded="xl" variant="plain" color="primary">
切换
{{ t('core.header.updateDialog.table.switch') }}
</v-btn>
</template>
</v-data-table>
@@ -367,11 +366,16 @@ commonStore.getStartTime();
<v-tabs-window-item key="1" v-show="tab == 1">
<div style="margin-top: 16px;">
<v-data-table
:headers="[{ title: 'SHA', key: 'sha' }, { title: '日期', key: 'date' }, { title: '信息', key: 'message' }, { title: '操作', key: 'switch' }]"
:headers="[
{ title: t('core.header.updateDialog.table.sha'), key: 'sha' },
{ title: t('core.header.updateDialog.table.date'), key: 'date' },
{ title: t('core.header.updateDialog.table.message'), key: 'message' },
{ title: t('core.header.updateDialog.table.actions'), key: 'switch' }
]"
:items="devCommits" item-key="sha">
<template v-slot:item.switch="{ item }: { item: { sha: string } }">
<v-btn @click="switchVersion(item.sha)" rounded="xl" variant="plain" color="primary">
切换
{{ t('core.header.updateDialog.table.switch') }}
</v-btn>
</template>
</v-data-table>
@@ -380,42 +384,40 @@ commonStore.getStartTime();
</v-tabs-window>
<h3 class="mb-4">手动输入版本号或 Commit SHA</h3>
<h3 class="mb-4">{{ t('core.header.updateDialog.manualInput.title') }}</h3>
<v-text-field label="输入版本号或 master 分支下的 commit hash。" v-model="version" required
<v-text-field :label="t('core.header.updateDialog.manualInput.placeholder')" v-model="version" required
variant="outlined"></v-text-field>
<div class="mb-4">
<small> v3.3.16 (不带 SHA) 42e5ec5d80b93b6bfe8b566754d45ffac4c3fe0b</small>
<small>{{ t('core.header.updateDialog.manualInput.hint') }}</small>
<br>
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>查看 master 分支提交记录点击右边的
copy
即可复制</small></a>
<a href="https://github.com/Soulter/AstrBot/commits/master"><small>{{ t('core.header.updateDialog.manualInput.linkText') }}</small></a>
</div>
<v-btn color="error" style="border-radius: 10px;" @click="switchVersion(version)">
确定切换
{{ t('core.header.updateDialog.manualInput.confirm') }}
</v-btn>
<v-divider class="mt-4 mb-4"></v-divider>
<div style="margin-top: 16px;">
<h3 class="mb-4">单独更新管理面板到最新版本</h3>
<h3 class="mb-4">{{ t('core.header.updateDialog.dashboardUpdate.title') }}</h3>
<div class="mb-4">
<small>当前版本 {{ dashboardCurrentVersion }}</small>
<small>{{ t('core.header.updateDialog.dashboardUpdate.currentVersion') }} {{ dashboardCurrentVersion }}</small>
<br>
</div>
<div class="mb-4">
<p v-if="dashboardHasNewVersion">
有新版本
{{ t('core.header.updateDialog.dashboardUpdate.hasNewVersion') }}
</p>
<p v-else="dashboardHasNewVersion">
已经是最新版本了
{{ t('core.header.updateDialog.dashboardUpdate.isLatest') }}
</p>
</div>
<v-btn color="primary" style="border-radius: 10px;" @click="updateDashboard()"
:disabled="!dashboardHasNewVersion">
下载并更新
{{ t('core.header.updateDialog.dashboardUpdate.downloadAndUpdate') }}
</v-btn>
</div>
</v-container>
@@ -423,7 +425,7 @@ commonStore.getStartTime();
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="updateStatusDialog = false">
关闭
{{ t('core.common.close') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -434,13 +436,13 @@ commonStore.getStartTime();
<template v-slot:activator="{ props }">
<v-btn size="small" class="action-btn mr-4" color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
<v-icon>mdi-account</v-icon>
<span class="hidden-xs ml-1">账户</span>
<span class="hidden-xs ml-1">{{ t('core.header.buttons.account') }}</span>
</v-btn>
</template>
<v-card class="account-dialog">
<v-card-text class="py-6">
<div class="d-flex flex-column align-center mb-6">
<logo title="AstrBot 仪表盘" subtitle="修改账户"></logo>
<logo title="AstrBot 仪表盘" :subtitle="t('core.header.accountDialog.title')"></logo>
</div>
<v-alert
v-if="accountWarning"
@@ -449,7 +451,7 @@ commonStore.getStartTime();
border="start"
class="mb-4"
>
<strong>安全提醒:</strong> 请修改默认密码以确保账户安全
<strong>{{ t('core.header.accountDialog.securityWarning') }}</strong>
</v-alert>
<v-alert
@@ -477,7 +479,7 @@ commonStore.getStartTime();
v-model="password"
:append-inner-icon="showPassword ? 'mdi-eye-off' : 'mdi-eye'"
:type="showPassword ? 'text' : 'password'"
label="当前密码"
:label="t('core.header.accountDialog.form.currentPassword')"
variant="outlined"
required
clearable
@@ -492,13 +494,13 @@ commonStore.getStartTime();
:append-inner-icon="showNewPassword ? 'mdi-eye-off' : 'mdi-eye'"
:type="showNewPassword ? 'text' : 'password'"
:rules="passwordRules"
label="新密码"
:label="t('core.header.accountDialog.form.newPassword')"
variant="outlined"
required
clearable
@click:append-inner="showNewPassword = !showNewPassword"
prepend-inner-icon="mdi-lock-plus-outline"
hint="密码长度至少 8 位"
:hint="t('core.header.accountDialog.form.passwordHint')"
persistent-hint
class="mb-4"
></v-text-field>
@@ -506,18 +508,18 @@ commonStore.getStartTime();
<v-text-field
v-model="newUsername"
:rules="usernameRules"
label="新用户名 (可选)"
:label="t('core.header.accountDialog.form.newUsername')"
variant="outlined"
clearable
prepend-inner-icon="mdi-account-edit-outline"
hint="留空表示不修改用户名"
:hint="t('core.header.accountDialog.form.usernameHint')"
persistent-hint
class="mb-3"
></v-text-field>
</v-form>
<div class="text-caption text-medium-emphasis mt-2">
默认用户名和密码均为 astrbot
{{ t('core.header.accountDialog.form.defaultCredentials') }}
</div>
</v-card-text>
@@ -532,7 +534,7 @@ commonStore.getStartTime();
@click="dialog = false"
:disabled="accountEditStatus.loading"
>
取消
{{ t('core.header.accountDialog.actions.cancel') }}
</v-btn>
<v-btn
color="primary"
@@ -541,7 +543,7 @@ commonStore.getStartTime();
:disabled="!formValid"
prepend-icon="mdi-content-save"
>
保存修改
{{ t('core.header.accountDialog.actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -1,6 +1,8 @@
<script setup>
import { useI18n } from '@/i18n/composables';
const props = defineProps({ item: Object, level: Number });
const { t } = useI18n();
</script>
<template>
@@ -16,7 +18,7 @@ const props = defineProps({ item: Object, level: Number });
<template v-slot:prepend>
<v-icon v-if="item.icon" :size="item.iconSize" class="hide-menu" :icon="item.icon"></v-icon>
</template>
<v-list-item-title style="font-size: 14px;">{{ $t(item.title) }}</v-list-item-title>
<v-list-item-title style="font-size: 14px;">{{ t(item.title) }}</v-list-item-title>
<v-list-item-subtitle v-if="item.subCaption" class="text-caption mt-n1 hide-menu">
{{ item.subCaption }}
</v-list-item-subtitle>
@@ -2,9 +2,12 @@
import { ref, shallowRef, onMounted } from 'vue';
import axios from 'axios';
import { useCustomizerStore } from '../../../stores/customizer';
import { useI18n } from '@/i18n/composables';
import sidebarItems from './sidebarItem';
import NavItem from './NavItem.vue';
const { t } = useI18n();
const customizer = useCustomizerStore();
const sidebarMenu = shallowRef(sidebarItems);
@@ -168,13 +171,13 @@ function endDrag() {
</v-list>
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
<v-btn style="margin-bottom: 8px;" size="small" variant="tonal" color="primary" to="/settings">
🔧 {{ $t('sidebar.settings') }}
🔧 {{ t('core.navigation.settings') }}
</v-btn>
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="toggleIframe">
{{ $t('sidebar.documentation') }}
{{ t('core.navigation.documentation') }}
</v-btn>
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
{{ $t('sidebar.github') }}
{{ t('core.navigation.github') }}
</v-btn>
</div>
</div>
@@ -189,7 +192,7 @@ function endDrag() {
<div :style="dragHeaderStyle" @mousedown="onMouseDown" @touchstart="onTouchStart">
<div style="display: flex; align-items: center;">
<v-icon icon="mdi-cursor-move" />
<span style="margin-left: 8px;">{{ $t('sidebar.drag') }}</span>
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
</div>
<div style="display: flex; gap: 8px;">
<!-- 跳转按钮 -->
@@ -15,60 +15,61 @@ export interface menu {
}
// 注意:这个文件现在包含i18n键值而不是直接的文本
// 在组件中使用时需要通过$t()函数进行翻译
// 在组件中使用时需要通过t()函数进行翻译
// 所有键名都使用 core.navigation.* 格式
const sidebarItem: menu[] = [
{
title: 'sidebar.dashboard',
title: 'core.navigation.dashboard',
icon: 'mdi-view-dashboard',
to: '/dashboard/default'
},
{
title: 'sidebar.platforms',
title: 'core.navigation.platforms',
icon: 'mdi-message-processing',
to: '/platforms',
},
{
title: 'sidebar.providers',
title: 'core.navigation.providers',
icon: 'mdi-creation',
to: '/providers',
},
{
title: 'sidebar.toolUse',
title: 'core.navigation.toolUse',
icon: 'mdi-function-variant',
to: '/tool-use'
},
{
title: 'sidebar.config',
title: 'core.navigation.config',
icon: 'mdi-cog',
to: '/config',
},
{
title: 'sidebar.extension',
title: 'core.navigation.extension',
icon: 'mdi-puzzle',
to: '/extension'
},
{
title: 'sidebar.chat',
title: 'core.navigation.chat',
icon: 'mdi-chat',
to: '/chat'
},
{
title: 'sidebar.conversation',
title: 'core.navigation.conversation',
icon: 'mdi-database',
to: '/conversation'
},
{
title: 'sidebar.console',
title: 'core.navigation.console',
icon: 'mdi-console',
to: '/console'
},
{
title: 'sidebar.alkaid',
title: 'core.navigation.alkaid',
icon: 'mdi-test-tube',
to: '/alkaid'
},
{
title: 'sidebar.about',
title: 'core.navigation.about',
icon: 'mdi-information',
to: '/about'
},
+26 -10
View File
@@ -4,7 +4,7 @@ import App from './App.vue';
import { router } from './router';
import vuetify from './plugins/vuetify';
import confirmPlugin from './plugins/confirmPlugin';
import i18n from './i18n';
import { setupI18n } from './i18n/composables';
import '@/scss/style.scss';
import VueApexCharts from 'vue3-apexcharts';
@@ -12,15 +12,31 @@ import print from 'vue3-print-nb';
import { loader } from '@guolao/vue-monaco-editor'
import axios from 'axios';
const app = createApp(App);
app.use(router);
app.use(createPinia());
app.use(i18n);
app.use(print);
app.use(VueApexCharts);
app.use(vuetify);
app.use(confirmPlugin);
app.mount('#app');
// 初始化新的i18n系统,等待完成后再挂载应用
setupI18n().then(() => {
console.log('🌍 新i18n系统初始化完成');
const app = createApp(App);
app.use(router);
app.use(createPinia());
app.use(print);
app.use(VueApexCharts);
app.use(vuetify);
app.use(confirmPlugin);
app.mount('#app');
}).catch(error => {
console.error('❌ 新i18n系统初始化失败:', error);
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
const app = createApp(App);
app.use(router);
app.use(createPinia());
app.use(print);
app.use(VueApexCharts);
app.use(vuetify);
app.use(confirmPlugin);
app.mount('#app');
});
axios.interceptors.request.use((config) => {
+14 -9
View File
@@ -10,16 +10,16 @@
<img v-if="selectedLogo == 1" width="280" src="@/assets/images/logo-normal.svg" alt="AstrBot Logo" class="fade-in">
</div>
<div class="title-container">
<h1 class="text-h2 font-weight-bold">AstrBot</h1>
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">A project out of interests and loves </p>
<h1 class="text-h2 font-weight-bold">{{ tm('hero.title') }}</h1>
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">{{ tm('hero.subtitle') }}</p>
<div class="action-buttons">
<v-btn @click="open('https://github.com/Soulter/AstrBot')"
color="primary" variant="elevated" prepend-icon="mdi-star">
Star 这个项目! 🌟
{{ tm('hero.starButton') }}
</v-btn>
<v-btn class="ml-4" @click="open('https://github.com/Soulter/AstrBot/issues')"
color="secondary" variant="elevated" prepend-icon="mdi-comment-question">
提交 Issue
{{ tm('hero.issueButton') }}
</v-btn>
</div>
</div>
@@ -31,12 +31,12 @@
<v-container>
<v-row justify="center" align="center">
<v-col cols="12" md="6" class="pr-md-8 contributors-info">
<h2 class="text-h4 font-weight-medium">贡献者</h2>
<h2 class="text-h4 font-weight-medium">{{ tm('contributors.title') }}</h2>
<p class="mb-4 text-body-1" style="color: var(--v-theme-secondaryText);">
本项目由众多开源社区成员共同维护感谢每一位贡献者的付出
{{ tm('contributors.description') }}
</p>
<p class="text-body-1" style="color: var(--v-theme-secondaryText);">
<a href="https://github.com/Soulter/AstrBot/graphs/contributors" class="text-decoration-none custom-link">查看 AstrBot 贡献者</a>
<a href="https://github.com/Soulter/AstrBot/graphs/contributors" class="text-decoration-none custom-link">{{ tm('contributors.viewLink') }}</a>
</p>
</v-col>
<v-col cols="12" md="6">
@@ -60,11 +60,11 @@
<v-container>
<v-row justify="center" align="center" class="flex-md-row-reverse">
<v-col cols="12" md="6" class="pl-md-8 stats-info">
<h2 class="text-h4 font-weight-medium">全球部署</h2>
<h2 class="text-h4 font-weight-medium">{{ tm('stats.title') }}</h2>
<div class="license-container mt-8">
<img v-bind="props" src="https://www.gnu.org/graphics/agplv3-with-text-100x42.png" style="cursor: pointer;"/>
<p class="text-caption mt-2" style="color: var(--v-theme-secondaryText);">AstrBot 采用 AGPL v3 协议开源</p>
<p class="text-caption mt-2" style="color: var(--v-theme-secondaryText);">{{ tm('stats.license') }}</p>
</div>
</v-col>
<v-col cols="12" md="6">
@@ -89,9 +89,14 @@
<script>
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'AboutPage',
setup() {
const { tm } = useModuleI18n('features/about');
return { tm };
},
data() {
return {
selectedLogo: 0
+11 -5
View File
@@ -3,8 +3,8 @@
<v-card-text class="pa-4" style="height: 100%;">
<v-container fluid class="d-flex flex-column" style="height: 100%;">
<div style="margin-bottom: 32px;">
<h1 class="gradient-text">The Alkaid Project.</h1>
<small style="color: #a3a3a3;">AstrBot Alpha 项目</small>
<h1 class="gradient-text">{{ tm('page.title') }}</h1>
<small style="color: #a3a3a3;">{{ tm('page.subtitle') }}</small>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
@@ -12,19 +12,19 @@
:color="isActive('knowledge-base') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('knowledge-base')">
<v-icon start>mdi-text-box-search</v-icon>
知识库
{{ tm('page.navigation.knowledgeBase') }}
</v-btn>
<v-btn size="large" :variant="isActive('long-term-memory') ? 'flat' : 'tonal'"
:color="isActive('long-term-memory') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('long-term-memory')">
<v-icon start>mdi-dots-hexagon</v-icon>
长期记忆层
{{ tm('page.navigation.longTermMemory') }}
</v-btn>
<v-btn size="large" :variant="isActive('other') ? 'flat' : 'tonal'"
:color="isActive('other') ? '#9b72cb' : ''" rounded="lg"
@click="navigateTo('other')">
<v-icon start>mdi-tools</v-icon>
...
{{ tm('page.navigation.other') }}
</v-btn>
</div>
@@ -37,9 +37,15 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'AlkaidPage',
components: {},
setup() {
const { tm } = useModuleI18n('features/alkaid/index');
return { tm };
},
data() {
return {}
},
+62 -52
View File
@@ -1,22 +1,4 @@
<script setup>
import { router } from '@/router';
import axios from 'axios';
import { marked } from 'marked';
import { ref } from 'vue';
import { defineProps } from 'vue';
import { useCustomizerStore } from '@/stores/customizer';
marked.setOptions({
breaks: true
});
const props = defineProps({
chatboxMode: {
type: Boolean,
default: false
}
});
</script>
<template>
<v-card class="chat-page-card">
@@ -25,7 +7,7 @@ const props = defineProps({
<div class="sidebar-panel" :class="{ 'sidebar-collapsed': sidebarCollapsed }"
@mouseenter="handleSidebarMouseEnter" @mouseleave="handleSidebarMouseLeave">
<div style="display: flex; align-items: center; justify-content: center; padding: 16px; padding-bottom: 0px;" v-if="props.chatboxMode">
<div style="display: flex; align-items: center; justify-content: center; padding: 16px; padding-bottom: 0px;" v-if="chatboxMode">
<img width="50" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
<span v-if="!sidebarCollapsed" style="font-weight: 1000; font-size: 26px; margin-left: 8px;" class="text-secondary">AstrBot</span>
</div>
@@ -41,7 +23,7 @@ const props = defineProps({
<div style="padding: 16px; padding-top: 8px;">
<v-btn block variant="text" class="new-chat-btn" @click="newC" :disabled="!currCid"
v-if="!sidebarCollapsed" prepend-icon="mdi-plus" style="box-shadow: 0 1px 2px rgba(0,0,0,0.1); background-color: transparent !important; border-radius: 4px;">创建对话</v-btn>
v-if="!sidebarCollapsed" prepend-icon="mdi-plus" style="box-shadow: 0 1px 2px rgba(0,0,0,0.1); background-color: transparent !important; border-radius: 4px;">{{ tm('actions.newChat') }}</v-btn>
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed"
elevation="0"></v-btn>
</div>
@@ -57,7 +39,7 @@ const props = defineProps({
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
rounded="lg" class="conversation-item" active-color="secondary">
<v-list-item-title v-if="!sidebarCollapsed" class="conversation-title">{{ item.title
|| '新对话' }}</v-list-item-title>
|| tm('conversation.newConversation') }}</v-list-item-title>
<!-- <v-list-item-subtitle v-if="!sidebarCollapsed" class="timestamp">{{
formatDate(item.updated_at)
}}</v-list-item-subtitle> -->
@@ -74,7 +56,7 @@ const props = defineProps({
<div class="no-conversations" v-if="conversations.length === 0">
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
<div class="no-conversations-text" v-if="!sidebarCollapsed || sidebarHoverExpanded">
暂无对话历史</div>
{{ tm('conversation.noHistory') }}</div>
</div>
</v-fade-transition>
</div>
@@ -85,7 +67,7 @@ const props = defineProps({
<div style="padding: 16px;" :class="{ 'fade-in': sidebarHoverExpanded }"
v-if="!sidebarCollapsed">
<div class="sidebar-section-title">
系统状态
{{ tm('conversation.systemStatus') }}
</div>
<div class="status-chips">
<v-chip class="status-chip" :color="status?.llm_enabled ? 'primary' : 'grey-lighten-2'"
@@ -94,7 +76,7 @@ const props = defineProps({
<v-icon :icon="status?.llm_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
size="x-small"></v-icon>
</template>
<span>LLM 服务</span>
<span>{{ tm('conversation.llmService') }}</span>
</v-chip>
<v-chip class="status-chip" :color="status?.stt_enabled ? 'success' : 'grey-lighten-2'"
@@ -103,7 +85,7 @@ const props = defineProps({
<v-icon :icon="status?.stt_enabled ? 'mdi-check-circle' : 'mdi-alert-circle'"
size="x-small"></v-icon>
</template>
<span>语音转文本</span>
<span>{{ tm('conversation.speechToText') }}</span>
</v-chip>
</div>
@@ -119,7 +101,7 @@ const props = defineProps({
<v-btn variant="outlined" rounded="sm" class="delete-chat-btn"
@click="deleteConversation(currCid)" color="error" density="comfortable" size="small">
<v-icon start size="small">mdi-delete</v-icon>
删除此对话
{{ tm('actions.deleteChat') }}
</v-btn>
</div>
</transition>
@@ -131,19 +113,19 @@ const props = defineProps({
<div class="conversation-header fade-in">
<div class="conversation-header-content" v-if="currCid && getCurrentConversation">
<h2 class="conversation-header-title">{{ getCurrentConversation.title || '新对话' }}</h2>
<h2 class="conversation-header-title">{{ getCurrentConversation.title || tm('conversation.newConversation') }}</h2>
<div class="conversation-header-time">{{ formatDate(getCurrentConversation.updated_at) }}</div>
</div>
<div class="conversation-header-actions">
<!-- router 推送到 /chatbox -->
<v-tooltip text="全屏模式" v-if="!props.chatboxMode">
<v-tooltip :text="tm('actions.fullscreen')" v-if="!chatboxMode">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" @click="router.push(currCid ? `/chatbox/${currCid}` : '/chatbox')"
class="fullscreen-icon">mdi-fullscreen</v-icon>
</template>
</v-tooltip>
<!-- 主题切换按钮 -->
<v-tooltip :text="isDark ? '切换到日间模式' : '切换到夜间模式'" v-if="props.chatboxMode">
<v-tooltip :text="isDark ? tm('modes.lightMode') : tm('modes.darkMode')" v-if="chatboxMode">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon @click="toggleTheme" class="theme-toggle-icon" variant="text">
<v-icon>{{ isDark ? 'mdi-weather-night' : 'mdi-white-balance-sunny' }}</v-icon>
@@ -151,7 +133,7 @@ const props = defineProps({
</template>
</v-tooltip>
<!-- router 推送到 /chat -->
<v-tooltip text="退出全屏" v-if="props.chatboxMode">
<v-tooltip :text="tm('actions.exitFullscreen')" v-if="chatboxMode">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" @click="router.push(currCid ? `/chat/${currCid}` : '/chat')"
class="fullscreen-icon">mdi-fullscreen-exit</v-icon>
@@ -169,19 +151,19 @@ const props = defineProps({
<span class="bot-name">AstrBot ⭐</span>
</div>
<div class="welcome-hint">
<span>输入</span>
<span>{{ t('core.common.type') }}</span>
<code>help</code>
<span>获取帮助 😊</span>
<span>{{ tm('shortcuts.help') }} 😊</span>
</div>
<div class="welcome-hint">
<span>长按</span>
<span>{{ t('core.common.longPress') }}</span>
<code>Ctrl</code>
<span>录制语音 🎤</span>
<span>{{ tm('shortcuts.voiceRecord') }} 🎤</span>
</div>
<div class="welcome-hint">
<span></span>
<span>{{ t('core.common.press') }}</span>
<code>Ctrl + V</code>
<span>粘贴图片 🏞️</span>
<span>{{ tm('shortcuts.pasteImage') }} 🏞️</span>
</div>
</div>
@@ -205,7 +187,7 @@ const props = defineProps({
<div class="audio-attachment" v-if="msg.audio_url && msg.audio_url.length > 0">
<audio controls class="audio-player">
<source :src="msg.audio_url" type="audio/wav">
您的浏览器不支持音频播放。
{{ t('messages.errors.browser.audioNotSupported') }}
</audio>
</div>
</div>
@@ -230,7 +212,7 @@ const props = defineProps({
<!-- 输入区域 -->
<div class="input-area fade-in">
<v-text-field autocomplete="off" id="input-field" variant="outlined" v-model="prompt"
:label="inputFieldLabel" placeholder="开始输入..." :loading="loadingChat"
:label="inputFieldLabel" :placeholder="tm('input.placeholder')" :loading="loadingChat"
clear-icon="mdi-close-circle" clearable @click:clear="clearMessage" class="message-input"
@keydown="handleInputKeyDown" hide-details>
<template v-slot:loader>
@@ -239,7 +221,7 @@ const props = defineProps({
</template>
<template v-slot:append>
<v-tooltip text="发送">
<v-tooltip :text="tm('input.send')">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="sendMessage" class="send-btn" icon="mdi-send"
variant="text" color="deep-purple"
@@ -247,7 +229,7 @@ const props = defineProps({
</template>
</v-tooltip>
<v-tooltip text="语音输入">
<v-tooltip :text="tm('input.voice')">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="isRecording ? stopRecording() : startRecording()"
class="record-btn"
@@ -269,7 +251,7 @@ const props = defineProps({
<div v-if="stagedAudioUrl" class="audio-preview">
<v-chip color="deep-purple-lighten-4" class="audio-chip">
<v-icon start icon="mdi-microphone" size="small"></v-icon>
新录音
{{ tm('voice.recording') }}
</v-chip>
<v-btn @click="removeAudio" class="remove-attachment-btn" icon="mdi-close" size="small"
color="error" variant="text" />
@@ -284,25 +266,51 @@ const props = defineProps({
<!-- 编辑对话标题对话框 -->
<v-dialog v-model="editTitleDialog" max-width="400">
<v-card>
<v-card-title class="dialog-title">编辑对话标题</v-card-title>
<v-card-title class="dialog-title">{{ tm('actions.editTitle') }}</v-card-title>
<v-card-text>
<v-text-field v-model="editingTitle" label="对话标题" variant="outlined" hide-details class="mt-2"
<v-text-field v-model="editingTitle" :label="tm('conversation.newConversation')" variant="outlined" hide-details class="mt-2"
@keyup.enter="saveTitle" autofocus />
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="editTitleDialog = false" color="grey-darken-1">取消</v-btn>
<v-btn text @click="saveTitle" color="primary">保存</v-btn>
<v-btn text @click="editTitleDialog = false" color="grey-darken-1">{{ t('core.common.cancel') }}</v-btn>
<v-btn text @click="saveTitle" color="primary">{{ t('core.common.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { router } from '@/router';
import axios from 'axios';
import { marked } from 'marked';
import { useCustomizerStore } from '@/stores/customizer';
import { useI18n, useModuleI18n } from '@/i18n/composables';
marked.setOptions({
breaks: true
});
export default {
name: 'ChatPage',
components: {
},
props: {
chatboxMode: {
type: Boolean,
default: false
}
},
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
return {
t,
tm,
router
};
},
data() {
return {
prompt: '',
@@ -313,7 +321,7 @@ export default {
stagedImagesUrl: [], // 用于存储图片的blob URL数组
loadingChat: false,
inputFieldLabel: '聊天吧!',
inputFieldLabel: '',
isRecording: false,
audioChunks: [],
@@ -401,6 +409,8 @@ export default {
mounted() {
// Theme is now handled globally by the customizer store.
// 设置输入框标签
this.inputFieldLabel = this.tm('title');
this.startListeningEvent();
this.checkStatus();
this.getConversations();
@@ -756,9 +766,9 @@ export default {
// Update the URL to reflect the selected conversation
if (this.$route.path !== `/chat/${cid[0]}` && this.$route.path !== `/chatbox/${cid[0]}`) {
if (this.$route.path.startsWith('/chatbox')) {
router.push(`/chatbox/${cid[0]}`);
this.$router.push(`/chatbox/${cid[0]}`);
} else {
router.push(`/chat/${cid[0]}`);
this.$router.push(`/chat/${cid[0]}`);
}
}
@@ -800,9 +810,9 @@ export default {
this.currCid = cid;
// Update the URL to reflect the new conversation
if (this.$route.path.startsWith('/chatbox')) {
router.push(`/chatbox/${cid}`);
this.$router.push(`/chatbox/${cid}`);
} else {
router.push(`/chat/${cid}`);
this.$router.push(`/chat/${cid}`);
}
this.getConversations();
return cid;
@@ -816,9 +826,9 @@ export default {
this.currCid = '';
this.messages = [];
if (this.$route.path.startsWith('/chatbox')) {
router.push('/chatbox');
this.$router.push('/chatbox');
} else {
router.push('/chat');
this.$router.push('/chat');
}
},
+46 -23
View File
@@ -1,10 +1,4 @@
<script setup>
import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import config from '@/config';
</script>
<template>
<v-card style="margin-bottom: 16px;">
@@ -17,11 +11,10 @@ import config from '@/config';
<v-btn icon="mdi-code-json" style="width: 80px;" :color="editorTab === 1 ? 'primary' : ''"
@click="configToString(); editorTab = 1;"></v-btn>
</v-btn-group>
<v-btn v-if="editorTab === 1" style="margin-left: 16px;" size="small" @click="configToString()">回到更改前的代码</v-btn>
<v-btn v-if="editorTab === 1" style="margin-left: 16px;" size="small" @click="configToString()">{{ tm('editor.revertCode') }}</v-btn>
<v-btn v-if="editorTab === 1 && config_data_has_changed" style="margin-left: 16px;" size="small"
@click="applyStrConfig()">应用此配置</v-btn>
<small v-if="editorTab === 1" style="margin-left: 16px;">💡 `应用此配置` 将配置暂存并应用到可视化如要保存<span
style="font-weight: 1000;"></span>点击右下角保存按钮</small>
@click="applyStrConfig()">{{ tm('editor.applyConfig') }}</v-btn>
<small v-if="editorTab === 1" style="margin-left: 16px;">💡 {{ tm('editor.applyTip') }}</small>
</div>
</v-card-text>
@@ -76,7 +69,7 @@ import config from '@/config';
v-show="config_template_tab === index" :key="index" :value="index">
<div style="padding: 16px;">
<v-btn variant="tonal" rounded="xl" color="error" @click="deleteItem(key2, index)">
删除这项
{{ tm('actions.delete') }}
</v-btn>
<AstrBotConfig :metadata="metadata[key]['metadata']" :iterable="config_item" :metadataKey="key2">
@@ -106,9 +99,11 @@ import config from '@/config';
<div style="margin-left: 16px; padding-bottom: 16px">
<small>不了解配置请见 <a href="https://astrbot.app/">官方文档</a>
<a
href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft">加群询问</a></small>
<small>{{ tm('help.helpPrefix') }}
<a href="https://astrbot.app/" target="_blank">{{ tm('help.documentation') }}</a>
{{ tm('help.helpMiddle') }}
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft" target="_blank">{{ tm('help.support') }}</a>{{ tm('help.helpSuffix') }}
</small>
</div>
</v-tabs-window>
@@ -134,6 +129,12 @@ import config from '@/config';
<script>
import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import config from '@/config';
import { useI18n, useModuleI18n } from '@/i18n/composables';
export default {
name: 'ConfigPage',
@@ -142,6 +143,28 @@ export default {
VueMonacoEditor,
WaitingForRestart
},
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/config');
return {
t,
tm
};
},
computed: {
// 安全访问翻译的计算属性
messages() {
return {
loadError: "配置加载失败",
saveSuccess: "配置保存成功",
saveError: "配置保存失败",
configApplied: "配置应用成功",
configApplyError: "配置应用失败"
};
}
},
watch: {
config_data_str: function (val) {
this.config_data_has_changed = true;
@@ -181,26 +204,26 @@ export default {
this.provider_config_tmpl = res.data.data.provider_config_tmpl;
this.adapter_config_tmpl = res.data.data.adapter_config_tmpl;
}).catch((err) => {
save_message = err;
save_message_snack = true;
save_message_success = "error";
this.save_message = this.messages.loadError;
this.save_message_snack = true;
this.save_message_success = "error";
});
},
updateConfig() {
if (!this.fetched) return;
axios.post('/api/config/astrbot/update', this.config_data).then((res) => {
if (res.data.status === "ok") {
this.save_message = res.data.message;
this.save_message = res.data.message || this.messages.saveSuccess;
this.save_message_snack = true;
this.save_message_success = "success";
this.$refs.wfr.check();
} else {
this.save_message = res.data.message;
this.save_message = res.data.message || this.messages.saveError;
this.save_message_snack = true;
this.save_message_success = "error";
}
}).catch((err) => {
this.save_message = err;
this.save_message = this.messages.saveError;
this.save_message_snack = true;
this.save_message_success = "error";
});
@@ -214,11 +237,11 @@ export default {
this.config_data = JSON.parse(this.config_data_str);
this.config_data_has_changed = false;
this.save_message_success = "success";
this.save_message = "配置成功应用。如要保存,需再点击右下角保存按钮。";
this.save_message = this.messages.configApplied;
this.save_message_snack = true;
} catch (e) {
this.save_message_success = "error";
this.save_message = "配置未应用,Json 格式错误。";
this.save_message = this.messages.configApplyError;
this.save_message_snack = true;
}
},
+10 -8
View File
@@ -1,34 +1,36 @@
<script setup>
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import { useModuleI18n } from '@/i18n/composables';
import axios from 'axios';
const { tm } = useModuleI18n('features/console');
</script>
<template>
<div style="height: 100%;">
<div
style="background-color: var(--v-theme-surface); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px; display: flex; flex-direction: row; align-items: center; justify-content: space-between;">
<h4>控制台</h4>
<h4>{{ tm('title') }}</h4>
<div class="d-flex align-center">
<v-switch
v-model="autoScrollDisabled"
:label="autoScrollDisabled ? '自动滚动已关闭' : '自动滚动已开启'"
:label="autoScrollDisabled ? tm('autoScroll.disabled') : tm('autoScroll.enabled')"
hide-details
density="compact"
style="margin-right: 16px;"
></v-switch>
<v-dialog v-model="pipDialog" width="400">
<template v-slot:activator="{ props }">
<v-btn variant="plain" v-bind="props">安装 pip </v-btn>
<v-btn variant="plain" v-bind="props">{{ tm('pipInstall.button') }}</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">安装 Pip </span>
<span class="text-h5">{{ tm('pipInstall.dialogTitle') }}</span>
</v-card-title>
<v-card-text>
<v-text-field v-model="pipInstallPayload.package" label="*库名,如 llmtuner" variant="outlined"></v-text-field>
<v-text-field v-model="pipInstallPayload.mirror" label="强制 PyPI 软件仓库链接(可选)" variant="outlined"></v-text-field>
<small>强制 PyPI 软件仓库链接 > 配置项 `PyPI 软件仓库地址`</small>
<v-text-field v-model="pipInstallPayload.package" :label="tm('pipInstall.packageLabel')" variant="outlined"></v-text-field>
<v-text-field v-model="pipInstallPayload.mirror" :label="tm('pipInstall.mirrorLabel')" variant="outlined"></v-text-field>
<small>{{ tm('pipInstall.mirrorHint') }}</small>
<div>
<small>{{ status }}</small>
</div>
@@ -37,7 +39,7 @@ import axios from 'axios';
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="pipInstall" :loading="loading">
安装
{{ tm('pipInstall.installButton') }}
</v-btn>
</v-card-actions>
</v-card>
+84 -72
View File
@@ -5,10 +5,10 @@
<v-row>
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-2">
<v-icon size="x-large" color="primary" class="me-2">mdi-chat-processing</v-icon>对话管理
<v-icon size="x-large" color="primary" class="me-2">mdi-chat-processing</v-icon>{{ tm('title') }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-4">
管理和查看用户对话历史记录
{{ tm('subtitle') }}
</p>
</v-col>
</v-row>
@@ -17,10 +17,10 @@
<v-card class="mb-4" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-filter-variant</v-icon>
<span class="text-h6">筛选条件</span>
<span class="text-h6">{{ tm('filters.title') }}</span>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="resetFilters" class="ml-2">
<v-icon class="mr-1">mdi-refresh</v-icon>重置
<v-icon class="mr-1">mdi-refresh</v-icon>{{ tm('filters.reset') }}
</v-btn>
</v-card-title>
@@ -29,7 +29,7 @@
<v-card-text class="py-4">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select v-model="platformFilter" label="平台" :items="availablePlatforms" chips multiple
<v-select v-model="platformFilter" :label="tm('filters.platform')" :items="availablePlatforms" chips multiple
clearable variant="outlined" density="compact" hide-details>
<template v-slot:selection="{ item }">
<v-chip size="small" :color="getPlatformColor(item.value)" label>
@@ -40,7 +40,7 @@
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select v-model="messageTypeFilter" label="类型" :items="messageTypeItems" chips multiple
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems" chips multiple
clearable variant="outlined" density="compact" hide-details>
<template v-slot:selection="{ item }">
<v-chip size="small" :color="getMessageTypeColor(item.value)" variant="outlined"
@@ -52,7 +52,7 @@
</v-col>
<v-col cols="12" sm="12" md="4">
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify" label="搜索关键词" hide-details
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify" :label="tm('filters.search')" hide-details
density="compact" variant="outlined" clearable></v-text-field>
</v-col>
</v-row>
@@ -63,32 +63,32 @@
<v-card class="mb-6" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-message</v-icon>
<span class="text-h6">对话历史</span>
<span class="text-h6">{{ tm('history.title') }}</span>
<v-chip color="info" size="small" class="ml-2">{{ pagination.total || 0 }}</v-chip>
<v-spacer></v-spacer>
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
:loading="loading">
刷新
{{ tm('history.refresh') }}
</v-btn>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-0">
<v-data-table :headers="headers" :items="conversations" :loading="loading" density="comfortable"
<v-data-table :headers="tableHeaders" :items="conversations" :loading="loading" density="comfortable"
hide-default-footer items-per-page="10" class="elevation-0"
:items-per-page="pagination.page_size" :items-per-page-options="[10, 20, 50, 100]"
@update:options="handleTableOptions">
<template v-slot:item.title="{ item }">
<div class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-chat</v-icon>
<span>{{ item.title || '无标题对话' }}</span>
<span>{{ item.title || tm('status.noTitle') }}</span>
</div>
</template>
<template v-slot:item.platform="{ item }">
<v-chip :color="getPlatformColor(item.sessionInfo.platform)" size="small" label>
{{ item.sessionInfo.platform || '未知' }}
{{ item.sessionInfo.platform || tm('status.unknown') }}
</v-chip>
</template>
@@ -100,7 +100,7 @@
</template>
<template v-slot:item.sessionId="{ item }">
<span>{{ item.sessionInfo.sessionId || '未知' }}</span>
<span>{{ item.sessionInfo.sessionId || tm('status.unknown') }}</span>
</template>
<template v-slot:item.created_at="{ item }">
@@ -115,15 +115,15 @@
<div class="actions-wrapper">
<v-btn color="primary" variant="flat" size="small" class="action-button"
@click="viewConversation(item)">
<v-icon class="mr-1">mdi-eye</v-icon>查看
<v-icon class="mr-1">mdi-eye</v-icon>{{ tm('actions.view') }}
</v-btn>
<v-btn color="warning" variant="flat" size="small" class="action-button"
@click="editConversation(item)">
<v-icon class="mr-1">mdi-pencil</v-icon>编辑
<v-icon class="mr-1">mdi-pencil</v-icon>{{ tm('actions.edit') }}
</v-btn>
<v-btn color="error" variant="flat" size="small" class="action-button"
@click="confirmDeleteConversation(item)">
<v-icon class="mr-1">mdi-delete</v-icon>删除
<v-icon class="mr-1">mdi-delete</v-icon>{{ tm('actions.delete') }}
</v-btn>
</div>
</template>
@@ -131,7 +131,7 @@
<template v-slot:no-data>
<div class="d-flex flex-column align-center py-6">
<v-icon size="64" color="grey lighten-1">mdi-chat-remove</v-icon>
<span class="text-subtitle-1 text-disabled mt-3">暂无对话记录</span>
<span class="text-subtitle-1 text-disabled mt-3">{{ tm('status.noData') }}</span>
</div>
</template>
</v-data-table>
@@ -150,7 +150,7 @@
<v-card class="conversation-detail-card">
<v-card-title class="bg-primary text-white py-3 d-flex align-center">
<v-icon color="white" class="me-2">mdi-eye</v-icon>
<span class="text-truncate">{{ selectedConversation?.title || '无标题对话' }}</span>
<span class="text-truncate">{{ selectedConversation?.title || tm('status.noTitle') }}</span>
<v-spacer></v-spacer>
<div class="d-flex align-center" v-if="selectedConversation?.sessionInfo">
@@ -170,12 +170,12 @@
<v-btn color="secondary" variant="tonal" size="small" class="mr-2"
@click="isEditingHistory = !isEditingHistory">
<v-icon class="mr-1">{{ isEditingHistory ? 'mdi-eye' : 'mdi-pencil' }}</v-icon>
{{ isEditingHistory ? '预览模式' : '编辑对话' }}
{{ isEditingHistory ? tm('dialogs.view.previewMode') : tm('dialogs.view.editMode') }}
</v-btn>
<v-btn v-if="isEditingHistory" color="success" variant="tonal" size="small"
:loading="savingHistory" @click="saveHistoryChanges">
<v-icon class="mr-1">mdi-content-save</v-icon>
保存修改
{{ tm('dialogs.view.saveChanges') }}
</v-btn>
</div>
@@ -196,7 +196,7 @@
<!-- 空对话提示 -->
<div v-if="conversationHistory.length === 0" class="text-center py-5">
<v-icon size="48" color="grey">mdi-chat-remove</v-icon>
<p class="text-disabled mt-2">对话内容为空</p>
<p class="text-disabled mt-2">{{ tm('status.emptyContent') }}</p>
</div>
<!-- 消息列表 -->
@@ -219,7 +219,7 @@
<div class="audio-attachment" v-if="msg.audio_url">
<audio controls class="audio-player">
<source :src="msg.audio_url" type="audio/wav">
您的浏览器不支持音频播放
{{ tm('status.audioNotSupported') }}
</audio>
</div>
</div>
@@ -247,7 +247,7 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeHistoryDialog">
关闭
{{ tm('dialogs.view.close') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -258,12 +258,12 @@
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">mdi-pencil</v-icon>
<span>编辑对话信息</span>
<span>{{ tm('dialogs.edit.title') }}</span>
</v-card-title>
<v-card-text class="py-4">
<v-form ref="form" v-model="valid">
<v-text-field v-model="editedItem.title" label="对话标题" placeholder="输入对话标题" variant="outlined"
<v-text-field v-model="editedItem.title" :label="tm('dialogs.edit.titleLabel')" :placeholder="tm('dialogs.edit.titlePlaceholder')" variant="outlined"
density="comfortable" class="mb-3"></v-text-field>
</v-form>
</v-card-text>
@@ -273,10 +273,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="dialogEdit = false" :disabled="loading">
取消
{{ tm('dialogs.edit.cancel') }}
</v-btn>
<v-btn color="primary" @click="saveConversation" :loading="loading">
保存
{{ tm('dialogs.edit.save') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -287,11 +287,11 @@
<v-card>
<v-card-title class="bg-error text-white py-3">
<v-icon color="white" class="me-2">mdi-alert</v-icon>
<span>确认删除</span>
<span>{{ tm('dialogs.delete.title') }}</span>
</v-card-title>
<v-card-text class="py-4">
<p>确定要删除对话 <strong>{{ selectedConversation?.title || '无标题对话' }}</strong> 此操作不可恢复</p>
<p>{{ tm('dialogs.delete.message', { title: selectedConversation?.title || tm('status.noTitle') }) }}</p>
</v-card-text>
<v-divider></v-divider>
@@ -299,10 +299,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="dialogDelete = false" :disabled="loading">
取消
{{ tm('dialogs.delete.cancel') }}
</v-btn>
<v-btn color="error" @click="deleteConversation" :loading="loading">
删除
{{ tm('dialogs.delete.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -320,6 +320,7 @@ import axios from 'axios';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
import { marked } from 'marked';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
marked.setOptions({
breaks: true
@@ -331,20 +332,23 @@ export default {
VueMonacoEditor
},
setup() {
const { t, locale } = useI18n();
const { tm } = useModuleI18n('features/conversation');
return {
t,
tm,
locale
};
},
data() {
return {
// 表格数据
conversations: [],
search: '',
headers: [
{ title: '对话标题', key: 'title', sortable: true },
{ title: '平台', key: 'platform', sortable: true, width: '120px' },
{ title: '类型', key: 'messageType', sortable: true, width: '100px' },
{ title: 'ID', key: 'sessionId', sortable: true, width: '100px' },
{ title: '创建时间', key: 'created_at', sortable: true, width: '180px' },
{ title: '更新时间', key: 'updated_at', sortable: true, width: '180px' },
{ title: '操作', key: 'actions', sortable: false, align: 'center', width: '240px' }
],
headers: [],
// 筛选条件
platformFilter: [],
@@ -443,6 +447,19 @@ export default {
},
computed: {
// 动态表头
tableHeaders() {
return [
{ title: this.tm('table.headers.title'), key: 'title', sortable: true },
{ title: this.tm('table.headers.platform'), key: 'platform', sortable: true, width: '120px' },
{ title: this.tm('table.headers.type'), key: 'messageType', sortable: true, width: '100px' },
{ title: this.tm('table.headers.sessionId'), key: 'sessionId', sortable: true, width: '100px' },
{ title: this.tm('table.headers.createdAt'), key: 'created_at', sortable: true, width: '180px' },
{ title: this.tm('table.headers.updatedAt'), key: 'updated_at', sortable: true, width: '180px' },
{ title: this.tm('table.headers.actions'), key: 'actions', sortable: false, align: 'center', width: '240px' }
];
},
// 可用平台列表
availablePlatforms() {
const platforms = []
@@ -462,8 +479,8 @@ export default {
// 可用消息类型列表
messageTypeItems() {
return [
{ title: '群聊', value: 'GroupMessage' },
{ title: '私聊', value: 'FriendMessage' },
{ title: this.tm('messageTypes.group'), value: 'GroupMessage' },
{ title: this.tm('messageTypes.friend'), value: 'FriendMessage' },
];
},
@@ -492,7 +509,7 @@ export default {
this.fetchConversations();
},
_methods: {
methods: {
// Monaco编辑器挂载后的回调
onMonacoMounted(editor) {
this.monacoEditor = editor;
@@ -572,9 +589,9 @@ export default {
// 获取消息类型的显示文本
getMessageTypeDisplay(messageType) {
const typeMap = {
'GroupMessage': '群聊',
'FriendMessage': '私聊',
'default': '未知'
'GroupMessage': this.tm('messageTypes.group'),
'FriendMessage': this.tm('messageTypes.friend'),
'default': this.tm('messageTypes.unknown')
};
return typeMap[messageType] || typeMap.default;
@@ -620,7 +637,7 @@ export default {
if (!data || !data.conversations) {
console.error('API 返回数据格式不符合预期:', data);
this.showErrorMessage('API 返回数据格式不符合预期');
this.showErrorMessage(this.tm('messages.fetchError'));
return;
}
@@ -643,7 +660,7 @@ export default {
console.warn('API 响应中没有分页信息');
}
} else {
this.showErrorMessage(response.data.message || '获取对话列表失败');
this.showErrorMessage(response.data.message || this.tm('messages.fetchError'));
}
} catch (error) {
console.error('获取对话列表出错:', error);
@@ -651,7 +668,7 @@ export default {
console.error('错误响应数据:', error.response.data);
console.error('错误状态码:', error.response.status);
}
this.showErrorMessage(error.response?.data?.message || error.message || '获取对话列表失败');
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.fetchError'));
} finally {
// this.loading = false;
setTimeout(() => {
@@ -685,11 +702,11 @@ export default {
}
this.dialogView = true;
} else {
this.showErrorMessage(response.data.message || '获取对话详情失败');
this.showErrorMessage(response.data.message || this.tm('messages.historyError'));
}
} catch (error) {
console.error('获取对话详情出错:', error);
this.showErrorMessage(error.response?.data?.message || error.message || '获取对话详情失败');
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.historyError'));
} finally {
this.loading = false;
}
@@ -707,7 +724,7 @@ export default {
try {
historyJson = JSON.parse(this.editedHistory);
} catch (e) {
this.showErrorMessage('JSON格式错误,请检查您的输入');
this.showErrorMessage(this.tm('messages.invalidJson'));
return;
}
@@ -719,14 +736,14 @@ export default {
if (response.data.status === "ok") {
this.conversationHistory = historyJson;
this.showSuccessMessage('对话历史更新成功');
this.showSuccessMessage(this.tm('messages.historySaveSuccess'));
this.isEditingHistory = false;
} else {
this.showErrorMessage(response.data.message || '更新对话历史失败');
this.showErrorMessage(response.data.message || this.tm('messages.historySaveError'));
}
} catch (error) {
console.error('更新对话历史出错:', error);
this.showErrorMessage(error.response?.data?.message || error.message || '更新对话历史失败');
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.historySaveError'));
} finally {
this.savingHistory = false;
}
@@ -735,7 +752,7 @@ export default {
// 关闭对话历史对话框
closeHistoryDialog() {
if (this.isEditingHistory) {
if (confirm('您有未保存的更改,确定要关闭吗?')) {
if (confirm(this.tm('dialogs.view.confirmClose'))) {
this.dialogView = false;
}
} else {
@@ -772,15 +789,15 @@ export default {
}
this.dialogEdit = false;
this.showSuccessMessage('对话信息更新成功');
this.showSuccessMessage(this.tm('messages.saveSuccess'));
// 刷新数据
this.fetchConversations();
} else {
this.showErrorMessage(response.data.message || '更新对话信息失败');
this.showErrorMessage(response.data.message || this.tm('messages.saveError'));
}
} catch (error) {
this.showErrorMessage(error.response?.data?.message || error.message || '更新对话信息失败');
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.saveError'));
} finally {
this.loading = false;
}
@@ -810,12 +827,12 @@ export default {
}
this.dialogDelete = false;
this.showSuccessMessage('对话删除成功');
this.showSuccessMessage(this.tm('messages.deleteSuccess'));
} else {
this.showErrorMessage(response.data.message || '删除对话失败');
this.showErrorMessage(response.data.message || this.tm('messages.deleteError'));
}
} catch (error) {
this.showErrorMessage(error.response?.data?.message || error.message || '删除对话失败');
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.deleteError'));
} finally {
this.loading = false;
}
@@ -823,10 +840,11 @@ export default {
// 格式化时间戳
formatTimestamp(timestamp) {
if (!timestamp) return '未知时间';
if (!timestamp) return this.tm('status.unknown');
const date = new Date(timestamp * 1000);
return new Intl.DateTimeFormat('zh-CN', {
const locale = this.locale || 'zh-CN';
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -860,7 +878,7 @@ export default {
} else if (typeof content === 'string') {
// 处理字符串内容
final_content = content;
} else if (!final_content) return '空消息';
} else if (!final_content) return this.tm('status.emptyContent');
// 使用marked处理Markdown格式
return marked(final_content);
},
@@ -878,13 +896,7 @@ export default {
this.messageType = 'error';
this.showMessage = true;
}
},
get methods() {
return this._methods;
},
set methods(value) {
this._methods = value;
},
}
}
</script>
+96 -93
View File
@@ -5,11 +5,14 @@ import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ReadmeDialog from '@/components/shared/ReadmeDialog.vue';
import axios from 'axios';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import { ref, computed, onMounted, reactive } from 'vue';
const commonStore = useCommonStore();
const { t } = useI18n();
const { tm } = useModuleI18n('features/extension');
const activeTab = ref('installed');
const extension_data = reactive({
data: [],
@@ -62,34 +65,34 @@ const showPluginFullName = ref(false);
const marketSearch = ref("");
const filterKeys = ['name', 'desc', 'author'];
const plugin_handler_info_headers = [
{ title: '行为类型', key: 'event_type_h' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '具体类型', key: 'type' },
{ title: '触发方式', key: 'cmd' },
];
const plugin_handler_info_headers = computed(() => [
{ title: tm('table.headers.eventType'), key: 'event_type_h' },
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
{ title: tm('table.headers.specificType'), key: 'type' },
{ title: tm('table.headers.trigger'), key: 'cmd' },
]);
// 插件表格的表头定义
const pluginHeaders = [
{ title: '名称', key: 'name', width: '200px' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '版本', key: 'version', width: '100px' },
{ title: '作者', key: 'author', width: '100px' },
{ title: '状态', key: 'status', width: '80px' },
{ title: '操作', key: 'actions', sortable: false, width: '220px' }
];
const pluginHeaders = computed(() => [
{ title: tm('table.headers.name'), key: 'name', width: '200px' },
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
{ title: tm('table.headers.version'), key: 'version', width: '100px' },
{ title: tm('table.headers.author'), key: 'author', width: '100px' },
{ title: tm('table.headers.status'), key: 'status', width: '80px' },
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '220px' }
]);
// 插件市场表头
const pluginMarketHeaders = [
{ title: '名称', key: 'name', maxWidth: '200px' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '作者', key: 'author', maxWidth: '90px' },
{ title: 'Star', key: 'stars', maxWidth: '80px' },
{ title: '最近更新', key: 'updated_at', maxWidth: '100px' },
{ title: '标签', key: 'tags', maxWidth: '100px' },
{ title: '操作', key: 'actions', sortable: false }
];
const pluginMarketHeaders = computed(() => [
{ title: tm('table.headers.name'), key: 'name', maxWidth: '200px' },
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
{ title: tm('table.headers.author'), key: 'author', maxWidth: '90px' },
{ title: tm('table.headers.stars'), key: 'stars', maxWidth: '80px' },
{ title: tm('table.headers.lastUpdate'), key: 'updated_at', maxWidth: '100px' },
{ title: tm('table.headers.tags'), key: 'tags', maxWidth: '100px' },
{ title: tm('table.headers.actions'), key: 'actions', sortable: false }
]);
// 过滤要显示的插件
@@ -131,7 +134,7 @@ const toast = (message, success) => {
const resetLoadingDialog = () => {
loadingDialog.show = false;
loadingDialog.title = "加载中...";
loadingDialog.title = tm('dialogs.loading.title');
loadingDialog.statusCode = 0;
loadingDialog.result = "";
};
@@ -185,7 +188,7 @@ const checkUpdate = () => {
};
const uninstallExtension = async (extension_name) => {
toast("正在卸载" + extension_name, "primary");
toast(tm('messages.uninstalling') + " " + extension_name, "primary");
try {
const res = await axios.post('/api/plugin/uninstall', { name: extension_name });
if (res.data.status === "error") {
@@ -216,14 +219,14 @@ const updateExtension = async (extension_name) => {
Object.assign(extension_data, res.data);
onLoadingDialogResult(1, res.data.message);
setTimeout(async () => {
toast(`正在刷新插件列表...`, "info", 2000);
toast(tm('messages.refreshing'), "info", 2000);
try {
await getExtensions();
toast("插件列表已刷新!", "success");
toast(tm('messages.refreshSuccess'), "success");
} catch (error) {
const errorMsg = error.response?.data?.message || error.message || String(error);
toast(`刷新插件列表时发生错误: ${errorMsg}`, "error");
toast(`${tm('messages.refreshFailed')}: ${errorMsg}`, "error");
}
}, 1000);
} catch (err) {
@@ -301,7 +304,7 @@ const reloadPlugin = async (plugin_name) => {
toast(res.data.message, "error");
return;
}
toast("重载成功", "success");
toast(tm('messages.reloadSuccess'), "success");
getExtensions();
} catch (err) {
toast(err, "error");
@@ -330,7 +333,7 @@ const getPlatformEnableConfig = async () => {
// 如果没有平台,给出提示但仍显示对话框
if (platformEnableData.platforms.length === 0) {
toast("未添加任何平台适配器,请先在平台管理中添加平台", "warning");
toast(tm('dialogs.platformConfig.noAdaptersDesc'), "warning");
} else {
// 确保每个平台都有一个配置对象
platformEnableData.platforms.forEach(platform => {
@@ -349,7 +352,7 @@ const getPlatformEnableConfig = async () => {
platformEnableDialog.value = true;
} catch (err) {
toast("获取平台插件配置失败: " + err, "error");
toast(tm('messages.getPlatformConfigFailed') + " " + err, "error");
} finally {
loadingPlatformData.value = false;
}
@@ -371,7 +374,7 @@ const savePlatformEnableConfig = async () => {
toast(res.data.message, "success");
platformEnableDialog.value = false;
} catch (err) {
toast("保存平台插件配置失败: " + err, "error");
toast(tm('messages.savePlatformConfigFailed') + " " + err, "error");
} finally {
loadingPlatformData.value = false;
}
@@ -452,18 +455,18 @@ const checkAlreadyInstalled = () => {
const newExtension = async () => {
if (extension_url.value === "" && upload_file.value === null) {
toast("请填写插件链接或上传插件文件", "error");
toast(tm('messages.fillUrlOrFile'), "error");
return;
}
if (extension_url.value !== "" && upload_file.value !== null) {
toast("请不要同时填写插件链接和上传插件文件", "error");
toast(tm('messages.dontFillBoth'), "error");
return;
}
loading_.value = true;
loadingDialog.show = true;
if (upload_file.value !== null) {
toast("正在从文件安装插件", "primary");
toast(tm('messages.installing'), "primary");
const formData = new FormData();
formData.append('file', upload_file.value);
axios.post('/api/plugin/install-upload', formData, {
@@ -490,7 +493,7 @@ const newExtension = async () => {
onLoadingDialogResult(2, err, -1);
});
} else {
toast("正在从链接 " + extension_url.value + " 安装插件...", "primary");
toast(tm('messages.installingFromUrl') + " " + extension_url.value, "primary");
axios.post('/api/plugin/install',
{
url: extension_url.value,
@@ -513,7 +516,7 @@ const newExtension = async () => {
});
}).catch((err) => {
loading_.value = false;
toast("安装插件失败: " + err, "error");
toast(tm('messages.installFailed') + " " + err, "error");
onLoadingDialogResult(2, err, -1);
});
}
@@ -537,7 +540,7 @@ onMounted(async () => {
checkAlreadyInstalled();
checkUpdate();
} catch (err) {
console.error("获取插件市场数据失败:", err);
console.error(tm('messages.getMarketDataFailed'), err);
}
});
@@ -555,10 +558,10 @@ onMounted(async () => {
</div>
</template>
<v-card-title class="text-h4 font-weight-bold">
AstrBot 插件
{{ tm('title') }}
</v-card-title>
<v-card-subtitle class="text-subtitle-1 mt-1 text-medium-emphasis">
管理安装 AstrBot 插件
{{ tm('subtitle') }}
</v-card-subtitle>
</v-card-item>
@@ -569,19 +572,19 @@ onMounted(async () => {
<v-tabs v-model="activeTab" color="primary" class="mb-4">
<v-tab value="installed">
<v-icon class="mr-2">mdi-puzzle</v-icon>
已安装插件
{{ tm('tabs.installed') }}
</v-tab>
<v-tab value="market">
<v-icon class="mr-2">mdi-store</v-icon>
插件市场
{{ tm('tabs.market') }}
</v-tab>
</v-tabs>
<v-text-field v-if="activeTab == 'market'" style="max-width: 300px;" v-model="marketSearch" density="compact"
label="Search" prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details
:label="tm('search.marketPlaceholder')" prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details
single-line></v-text-field>
<v-text-field v-else style="max-width: 300px;" v-model="pluginSearch" density="compact" label="Search" prepend-inner-icon="mdi-magnify"
<v-text-field v-else style="max-width: 300px;" v-model="pluginSearch" density="compact" :label="tm('search.placeholder')" prepend-inner-icon="mdi-magnify"
variant="solo-filled" flat hide-details single-line></v-text-field>
</div>
@@ -604,12 +607,12 @@ onMounted(async () => {
<v-btn class="ml-2" @click="toggleShowReserved" prepend-icon="mdi-eye-settings-outline"
:color="showReserved ? 'primary' : undefined" :variant="showReserved ? 'flat' : 'outlined'">
{{ showReserved ? '隐藏系统插件' : '显示系统插件' }}
{{ showReserved ? tm('buttons.hideSystemPlugins') : tm('buttons.showSystemPlugins') }}
</v-btn>
<v-btn class="ml-2" prepend-icon="mdi-tune-vertical" color="primary" variant="outlined"
@click="getPlatformEnableConfig">
平台命令配置
{{ tm('buttons.platformConfig') }}
</v-btn>
</v-col>
@@ -625,15 +628,15 @@ onMounted(async () => {
<v-card class="rounded-lg">
<v-card-title class="headline d-flex align-center">
<v-icon color="error" class="mr-2">mdi-alert-circle</v-icon>
错误信息
{{ tm('dialogs.error.title') }}
</v-card-title>
<v-card-text>
<p class="text-body-1">{{ extension_data.message }}</p>
<p class="text-caption mt-2">详情请检查控制台</p>
<p class="text-caption mt-2">{{ tm('dialogs.error.checkConsole') }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" @click="isActive.value = false">关闭</v-btn>
<v-btn color="primary" @click="isActive.value = false">{{ tm('buttons.close') }}</v-btn>
</v-card-actions>
</v-card>
</template>
@@ -650,7 +653,7 @@ onMounted(async () => {
<template v-slot:loader>
<v-row class="py-8 d-flex align-center justify-center">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<span class="ml-2">加载中...</span>
<span class="ml-2">{{ tm('status.loading') }}</span>
</v-row>
</template>
@@ -659,7 +662,7 @@ onMounted(async () => {
<div>
<div class="text-subtitle-1 font-weight-medium">{{ item.name }}</div>
<div v-if="item.reserved" class="d-flex align-center mt-1">
<v-chip color="primary" size="x-small" class="font-weight-medium">系统</v-chip>
<v-chip color="primary" size="x-small" class="font-weight-medium">{{ tm('status.system') }}</v-chip>
</div>
</div>
</div>
@@ -674,7 +677,7 @@ onMounted(async () => {
<span class="text-body-2">{{ item.version }}</span>
<v-icon v-if="item.has_update" color="warning" size="small" class="ml-1">mdi-alert</v-icon>
<v-tooltip v-if="item.has_update" activator="parent">
<span>有新版本: {{ item.online_version }}</span>
<span>{{ tm('messages.hasUpdate') }} {{ item.online_version }}</span>
</v-tooltip>
</div>
</template>
@@ -686,7 +689,7 @@ onMounted(async () => {
<template v-slot:item.status="{ item }">
<v-chip :color="item.activated ? 'success' : 'error'" size="small" class="font-weight-medium"
:variant="item.activated ? 'flat' : 'outlined'">
{{ item.activated ? '启用' : '禁用' }}
{{ item.activated ? tm('status.enabled') : tm('status.disabled') }}
</v-chip>
</template>
@@ -695,43 +698,43 @@ onMounted(async () => {
<v-btn-group density="comfortable" variant="text" color="primary">
<v-btn v-if="!item.activated" icon size="small" color="success" @click="pluginOn(item)">
<v-icon>mdi-play</v-icon>
<v-tooltip activator="parent" location="top">点击启用</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.enable') }}</v-tooltip>
</v-btn>
<v-btn v-else icon size="small" color="error" @click="pluginOff(item)">
<v-icon>mdi-pause</v-icon>
<v-tooltip activator="parent" location="top">点击禁用</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.disable') }}</v-tooltip>
</v-btn>
<v-btn icon size="small" color="info" @click="reloadPlugin(item.name)">
<v-icon>mdi-refresh</v-icon>
<v-tooltip activator="parent" location="top">重载</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.reload') }}</v-tooltip>
</v-btn>
<v-btn icon size="small" @click="openExtensionConfig(item.name)">
<v-icon>mdi-cog</v-icon>
<v-tooltip activator="parent" location="top">配置</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.configure') }}</v-tooltip>
</v-btn>
<v-btn icon size="small" @click="showPluginInfo(item)">
<v-icon>mdi-information</v-icon>
<v-tooltip activator="parent" location="top">行为</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewInfo') }}</v-tooltip>
</v-btn>
<v-btn v-if="item.repo" icon size="small" @click="viewReadme(item)">
<v-icon>mdi-book-open-page-variant</v-icon>
<v-tooltip activator="parent" location="top">文档</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.viewDocs') }}</v-tooltip>
</v-btn>
<v-btn icon size="small" color="warning" @click="updateExtension(item.name)"
:v-show="item.has_update">
<v-icon>mdi-update</v-icon>
<v-tooltip activator="parent" location="top">更新</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.update') }}</v-tooltip>
</v-btn>
<v-btn icon size="small" color="error" @click="uninstallExtension(item.name)"
:disabled="item.reserved">
<v-icon>mdi-delete</v-icon>
<v-tooltip activator="parent" location="top">卸载</v-tooltip>
<v-tooltip activator="parent" location="top">{{ tm('tooltips.uninstall') }}</v-tooltip>
</v-btn>
</v-btn-group>
@@ -742,8 +745,8 @@ onMounted(async () => {
<template v-slot:no-data>
<div class="text-center pa-8">
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
<div class="text-h5 mb-2">暂无插件</div>
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</div>
<div class="text-h5 mb-2">{{ tm('empty.noPlugins') }}</div>
<div class="text-body-1 mb-4">{{ tm('empty.noPluginsDesc') }}</div>
</div>
</template>
</v-data-table>
@@ -755,8 +758,8 @@ onMounted(async () => {
<v-row v-if="filteredPlugins.length === 0" class="text-center">
<v-col cols="12" class="pa-8">
<v-icon size="64" color="info" class="mb-4">mdi-puzzle-outline</v-icon>
<div class="text-h5 mb-2">暂无插件</div>
<div class="text-body-1 mb-4">尝试安装插件或者显示系统插件</div>
<div class="text-h5 mb-2">{{ tm('empty.noPlugins') }}</div>
<div class="text-body-1 mb-4">{{ tm('empty.noPluginsDesc') }}</div>
</v-col>
</v-row>
@@ -781,7 +784,7 @@ onMounted(async () => {
<!-- <small style="color: var(--v-theme-secondaryText);">每个插件都是作者无偿提供的的劳动成果如果您喜欢某个插件 Star</small> -->
<div v-if="pinnedPlugins.length > 0" class="mt-4">
<h2>🥳 推荐</h2>
<h2>{{ tm('market.recommended') }}</h2>
<v-row style="margin-top: 8px;">
<v-col cols="12" md="6" lg="6" v-for="plugin in pinnedPlugins" :key="plugin.name">
<ExtensionCard :extension="plugin" class="h-120 rounded-lg" market-mode="true" :highlight="true"
@@ -793,9 +796,9 @@ onMounted(async () => {
<div class="mt-4">
<div class="d-flex align-center mb-2" style="justify-content: space-between;">
<h2>📦 全部插件</h2>
<v-switch v-model="showPluginFullName" label="完整名称" hide-details density="compact"
style="margin-left: 12px" />
<h2>{{ tm('market.allPlugins') }}</h2>
<v-switch v-model="showPluginFullName" :label="tm('market.showFullName')" hide-details density="compact"
style="margin-left: 12px" />
</div>
<v-col cols="12" md="12" style="padding: 0px;">
@@ -860,8 +863,8 @@ onMounted(async () => {
</v-col>
<v-col v-if="activeTab === 'market'" style="margin-bottom: 16px;" cols="12" md="12">
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">提交插件仓库</a></small>
<small><a href="https://astrbot.app/dev/plugin.html">{{ tm('market.devDocs') }}</a></small> |
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">{{ tm('market.submitRepo') }}</a></small>
</v-col>
</v-row>
@@ -869,7 +872,7 @@ onMounted(async () => {
<v-dialog v-model="platformEnableDialog" max-width="900" persistent>
<v-card class="rounded-lg">
<v-toolbar color="primary" density="comfortable" flat>
<v-toolbar-title class="text-white">平台命令可用性配置</v-toolbar-title>
<v-toolbar-title class="text-white">{{ tm('dialogs.platformConfig.title') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="platformEnableDialog = false" variant="text" color="white">
<v-icon>mdi-close</v-icon>
@@ -877,7 +880,7 @@ onMounted(async () => {
</v-toolbar>
<v-card-text class="pt-4">
<p class="text-body-2 mb-4">设置每个插件在不同平台上的可用性勾选表示启用</p>
<p class="text-body-2 mb-4">{{ tm('dialogs.platformConfig.description') }}</p>
<v-overlay :model-value="loadingPlatformData" class="align-center justify-center" persistent>
<v-progress-circular color="primary" indeterminate size="64"></v-progress-circular>
@@ -885,16 +888,16 @@ onMounted(async () => {
<div v-if="platformEnableData.platforms.length === 0" class="text-center pa-8">
<v-icon icon="mdi-alert" color="warning" size="64" class="mb-4"></v-icon>
<div class="text-h5 mb-2">未找到平台适配器</div>
<div class="text-body-1 mb-4">请先在 <strong>平台管理</strong> 中添加并配置平台适配器然后再设置插件的平台可用性</div>
<v-btn color="primary" to="/platforms" variant="elevated">前往平台管理</v-btn>
<div class="text-h5 mb-2">{{ tm('dialogs.platformConfig.noAdapters') }}</div>
<div class="text-body-1 mb-4">{{ tm('dialogs.platformConfig.noAdaptersDesc') }}</div>
<v-btn color="primary" to="/platforms" variant="elevated">{{ tm('dialogs.platformConfig.goPlatforms') }}</v-btn>
</div>
<v-sheet v-else class="rounded-lg overflow-hidden">
<v-table hover class="elevation-1">
<thead>
<tr>
<th class="text-left">插件名称</th>
<th class="text-left">{{ tm('table.headers.name') }}</th>
<th v-for="platform in platformEnableData.platforms" :key="platform.name">
<div class="d-flex align-center">
{{ platform.display_name }}
@@ -906,19 +909,19 @@ onMounted(async () => {
</template>
<v-list>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, true)">
<v-list-item-title>全选</v-list-item-title>
<v-list-item-title>{{ tm('dialogs.platformConfig.selectAll') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, true, false)">
<v-list-item-title>全选普通插件</v-list-item-title>
<v-list-item-title>{{ tm('dialogs.platformConfig.selectAllNormal') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, true, true)">
<v-list-item-title>全选系统插件</v-list-item-title>
<v-list-item-title>{{ tm('dialogs.platformConfig.selectAllSystem') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="selectAllPluginsForPlatform(platform.name, false)">
<v-list-item-title>全不选</v-list-item-title>
<v-list-item-title>{{ tm('dialogs.platformConfig.selectNone') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="toggleAllPluginsForPlatform(platform.name)">
<v-list-item-title>反选</v-list-item-title>
<v-list-item-title>{{ tm('dialogs.platformConfig.toggleAll') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
@@ -931,7 +934,7 @@ onMounted(async () => {
<td>
<div class="d-flex align-center">
{{ plugin.name }}
<v-chip v-if="plugin.reserved" color="primary" size="x-small" class="ml-2">系统</v-chip>
<v-chip v-if="plugin.reserved" color="primary" size="x-small" class="ml-2">{{ tm('status.system') }}</v-chip>
</div>
<div class="text-caption text-grey">{{ plugin.desc }}</div>
</td>
@@ -946,9 +949,9 @@ onMounted(async () => {
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" text @click="platformEnableDialog = false">关闭</v-btn>
<v-btn color="grey" text @click="platformEnableDialog = false">{{ tm('buttons.close') }}</v-btn>
<v-btn v-if="platformEnableData.platforms.length > 0" color="primary"
@click="savePlatformEnableConfig">保存</v-btn>
@click="savePlatformEnableConfig">{{ tm('buttons.save') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -956,16 +959,16 @@ onMounted(async () => {
<!-- 配置对话框 -->
<v-dialog v-model="configDialog" width="1000">
<v-card>
<v-card-title class="text-h5">插件配置</v-card-title>
<v-card-title class="text-h5">{{ tm('dialogs.config.title') }}</v-card-title>
<v-card-text>
<AstrBotConfig v-if="extension_config.metadata" :metadata="extension_config.metadata"
:iterable="extension_config.config" :metadataKey="curr_namespace" />
<p v-else>这个插件没有配置</p>
<p v-else>{{ tm('dialogs.config.noConfig') }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="updateConfig">保存并关闭</v-btn>
<v-btn color="blue-darken-1" variant="text" @click="configDialog = false">关闭</v-btn>
<v-btn color="blue-darken-1" variant="text" @click="updateConfig">{{ tm('buttons.saveAndClose') }}</v-btn>
<v-btn color="blue-darken-1" variant="text" @click="configDialog = false">{{ tm('buttons.close') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -986,13 +989,13 @@ onMounted(async () => {
</div>
<div style="margin-top: 32px;">
<h3>日志</h3>
<h3>{{ tm('dialogs.loading.logs') }}</h3>
<ConsoleDisplayer historyNum="10" style="height: 200px; margin-top: 16px;"></ConsoleDisplayer>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="resetLoadingDialog">关闭</v-btn>
<v-btn color="blue-darken-1" variant="text" @click="resetLoadingDialog">{{ tm('buttons.close') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -1000,7 +1003,7 @@ onMounted(async () => {
<!-- 插件信息对话框 -->
<v-dialog v-model="showPluginInfoDialog" width="1200">
<v-card>
<v-card-title class="text-h5">{{ selectedPlugin.name }} 插件行为</v-card-title>
<v-card-title class="text-h5">{{ selectedPlugin.name }} {{ tm('buttons.viewInfo') }}</v-card-title>
<v-card-text>
<v-data-table style="font-size: 17px;" :headers="plugin_handler_info_headers" :items="selectedPlugin.handlers"
item-key="name">
@@ -1025,7 +1028,7 @@ onMounted(async () => {
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="showPluginInfoDialog = false">关闭</v-btn>
<v-btn color="blue-darken-1" variant="text" @click="showPluginInfoDialog = false">{{ tm('buttons.close') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
+40 -18
View File
@@ -5,10 +5,10 @@
<v-row>
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-2">
<v-icon size="x-large" color="primary" class="me-2">mdi-connection</v-icon>平台适配器管理
<v-icon size="x-large" color="primary" class="me-2">mdi-connection</v-icon>{{ tm('title') }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-4">
管理机器人的平台适配器连接到不同的聊天平台
{{ tm('subtitle') }}
</p>
</v-col>
</v-row>
@@ -17,13 +17,13 @@
<v-card class="mb-6" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-apps</v-icon>
<span class="text-h6">平台适配器</span>
<span class="text-h6">{{ tm('adapters') }}</span>
<v-chip color="info" size="small" class="ml-2">{{ config_data.platform?.length || 0 }}</v-chip>
<v-spacer></v-spacer>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" v-bind="props">
新增适配器
{{ tm('addAdapter') }}
</v-btn>
</template>
<v-list @update:selected="addFromDefaultConfigTmpl($event)">
@@ -47,7 +47,7 @@
title-field="id"
enabled-field="enable"
empty-icon="mdi-connection"
empty-text="暂无平台适配器点击 新增适配器 添加"
:empty-text="tm('emptyText')"
@toggle-enabled="platformStatusChange"
@delete="deletePlatform"
@edit="editPlatform"
@@ -56,13 +56,13 @@
<div class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-tag</v-icon>
<span class="text-caption text-medium-emphasis">
适配器类型:
{{ tm('details.adapterType') }}:
<v-chip size="x-small" color="primary" class="ml-1">{{ item.type }}</v-chip>
</span>
</div>
<div v-if="item.token" class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-key</v-icon>
<span class="text-caption text-medium-emphasis">Token: </span>
<span class="text-caption text-medium-emphasis">{{ tm('details.token') }}: </span>
</div>
<div v-if="item.description" class="d-flex align-center">
<v-icon size="small" color="grey" class="me-2">mdi-information-outline</v-icon>
@@ -77,10 +77,10 @@
<v-card elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-console-line</v-icon>
<span class="text-h6">平台日志</span>
<span class="text-h6">{{ tm('logs.title') }}</span>
<v-spacer></v-spacer>
<v-btn variant="text" color="primary" @click="showConsole = !showConsole">
{{ showConsole ? '收起' : '展开' }}
{{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
<v-icon>{{ showConsole ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</v-card-title>
@@ -100,7 +100,7 @@
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<span>{{ updatingMode ? '编辑' : '新增' }} {{ newSelectedPlatformName }} 平台适配器</span>
<span>{{ updatingMode ? tm('dialog.edit') : tm('dialog.add') }} {{ newSelectedPlatformName }} {{ tm('dialog.adapter') }}</span>
</v-card-title>
<v-card-text class="py-4">
@@ -113,7 +113,7 @@
<v-col cols="12" md="4" class="d-flex flex-column align-end">
<v-btn :loading="iframeLoading" @click="refreshIframe" variant="tonal" color="primary">
<v-icon>mdi-refresh</v-icon>
刷新
{{ tm('dialog.refresh') }}
</v-btn>
<iframe v-show="!iframeLoading"
:src="store.getTutorialLink(newSelectedPlatformConfig.type)"
@@ -128,10 +128,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showPlatformCfg = false" :disabled="loading">
取消
{{ tm('dialog.cancel') }}
</v-btn>
<v-btn color="primary" @click="newPlatform" :loading="loading">
保存
{{ tm('dialog.save') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -154,6 +154,7 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
import { useCommonStore } from '@/stores/common';
import { useI18n, useModuleI18n } from '@/i18n/composables';
export default {
name: 'PlatformPage',
@@ -163,6 +164,27 @@ export default {
ConsoleDisplayer,
ItemCardGrid
},
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/platform');
return {
t,
tm
};
},
computed: {
// 安全访问翻译的计算属性
messages() {
return {
updateSuccess: "更新成功!",
addSuccess: "添加成功!",
deleteSuccess: "删除成功!",
statusUpdateSuccess: "状态更新成功!",
deleteConfirm: "确定要删除平台适配器"
};
}
},
data() {
return {
config_data: {},
@@ -235,7 +257,7 @@ export default {
this.showPlatformCfg = false;
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || "更新成功!");
this.showSuccess(res.data.message || this.messages.updateSuccess);
}).catch((err) => {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
@@ -246,7 +268,7 @@ export default {
this.loading = false;
this.showPlatformCfg = false;
this.getConfig();
this.showSuccess(res.data.message || "添加成功!");
this.showSuccess(res.data.message || this.messages.addSuccess);
}).catch((err) => {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
@@ -255,11 +277,11 @@ export default {
},
deletePlatform(platform) {
if (confirm(`确定要删除平台适配器 ${platform.id} 吗?`)) {
if (confirm(`${this.messages.deleteConfirm} ${platform.id} 吗?`)) {
axios.post('/api/config/platform/delete', { id: platform.id }).then((res) => {
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || "删除成功!");
this.showSuccess(res.data.message || this.messages.deleteSuccess);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
});
@@ -275,7 +297,7 @@ export default {
}).then((res) => {
this.getConfig();
this.$refs.wfr.check();
this.showSuccess(res.data.message || "状态更新成功!");
this.showSuccess(res.data.message || this.messages.statusUpdateSuccess);
}).catch((err) => {
platform.enable = !platform.enable; // 发生错误时回滚状态
this.showError(err.response?.data?.message || err.message);
+79 -51
View File
@@ -5,10 +5,10 @@
<v-row>
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-2">
<v-icon size="x-large" color="primary" class="me-2">mdi-creation</v-icon>服务提供商管理
<v-icon size="x-large" color="primary" class="me-2">mdi-creation</v-icon>{{ tm('title') }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-4">
管理模型服务提供商
{{ tm('subtitle') }}
</p>
</v-col>
</v-row>
@@ -17,14 +17,14 @@
<v-card class="mb-6" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-api</v-icon>
<span class="text-h6">服务提供商</span>
<span class="text-h6">{{ tm('providers.title') }}</span>
<v-chip color="info" size="small" class="ml-2">{{ config_data.provider?.length || 0 }}</v-chip>
<v-spacer></v-spacer>
<v-btn color="success" prepend-icon="mdi-cog" variant="tonal" class="me-2" @click="showSettingsDialog = true">
设置
{{ tm('providers.settings') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-plus" variant="tonal" @click="showAddProviderDialog = true">
新增服务提供商
{{ tm('providers.addProvider') }}
</v-btn>
</v-card-title>
@@ -35,23 +35,23 @@
<v-tabs v-model="activeProviderTypeTab" bg-color="transparent">
<v-tab value="all" class="font-weight-medium px-3">
<v-icon start>mdi-filter-variant</v-icon>
全部
{{ tm('providers.tabs.all') }}
</v-tab>
<v-tab value="chat_completion" class="font-weight-medium px-3">
<v-icon start>mdi-message-text</v-icon>
基本对话
{{ tm('providers.tabs.chatCompletion') }}
</v-tab>
<v-tab value="speech_to_text" class="font-weight-medium px-3">
<v-icon start>mdi-microphone-message</v-icon>
语音转文字
{{ tm('providers.tabs.speechToText') }}
</v-tab>
<v-tab value="text_to_speech" class="font-weight-medium px-3">
<v-icon start>mdi-volume-high</v-icon>
文字转语音
{{ tm('providers.tabs.textToSpeech') }}
</v-tab>
<v-tab value="embedding" class="font-weight-medium px-3">
<v-icon start>mdi-code-json</v-icon>
Embedding
{{ tm('providers.tabs.embedding') }}
</v-tab>
</v-tabs>
</v-card-text>
@@ -71,7 +71,7 @@
<div class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-tag</v-icon>
<span class="text-caption text-medium-emphasis">
提供商类型:
{{ tm('providers.providerType') }}:
<v-chip size="x-small" color="primary" class="ml-1">{{ item.type }}</v-chip>
</span>
</div>
@@ -94,22 +94,22 @@
<v-card class="mb-6" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-heart-pulse</v-icon>
<span class="text-h6">供应商可用性</span>
<span class="text-h6">{{ tm('availability.title') }}</span>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" :loading="loadingStatus" @click="fetchProviderStatus">
<v-icon left>mdi-refresh</v-icon>
刷新状态
{{ tm('availability.refresh') }}
</v-btn>
</v-card-title>
<v-card-subtitle class="px-4 py-1 text-caption text-medium-emphasis">
通过测试模型对话可用性判断可能产生API费用
{{ tm('availability.subtitle') }}
</v-card-subtitle>
<v-divider></v-divider>
<v-card-text class="px-4 py-3">
<v-alert v-if="providerStatuses.length === 0" type="info" variant="tonal">
点击"刷新状态"按钮获取供应商可用性
{{ tm('availability.noData') }}
</v-alert>
<v-container v-else class="pa-0">
@@ -122,11 +122,11 @@
</v-icon>
<span class="font-weight-bold">{{ status.id }}</span>
<v-chip :color="status.status === 'available' ? 'success' : 'error'" size="small" class="ml-2">
{{ status.status === 'available' ? '可用' : '不可用' }}
{{ status.status === 'available' ? tm('availability.available') : tm('availability.unavailable') }}
</v-chip>
</v-card-item>
<v-card-text v-if="status.status === 'unavailable'" class="text-caption text-medium-emphasis">
<span class="font-weight-bold">错误信息:</span> {{ status.error }}
<span class="font-weight-bold">{{ tm('availability.errorMessage') }}:</span> {{ status.error }}
</v-card-text>
</v-card>
</v-col>
@@ -139,10 +139,10 @@
<v-card elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-console-line</v-icon>
<span class="text-h6">服务日志</span>
<span class="text-h6">{{ tm('logs.title') }}</span>
<v-spacer></v-spacer>
<v-btn variant="text" color="primary" @click="showConsole = !showConsole">
{{ showConsole ? '收起' : '展开' }}
{{ showConsole ? tm('logs.collapse') : tm('logs.expand') }}
<v-icon>{{ showConsole ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</v-card-title>
@@ -162,7 +162,7 @@
<v-card class="provider-selection-dialog">
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-plus-circle</v-icon>
<span>服务提供商</span>
<span>{{ tm('dialogs.addProvider.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showAddProviderDialog = false">
<v-icon>mdi-close</v-icon>
@@ -173,19 +173,19 @@
<v-tabs v-model="activeProviderTab" grow slider-color="primary" bg-color="background">
<v-tab value="chat_completion" class="font-weight-medium px-3">
<v-icon start>mdi-message-text</v-icon>
基本
{{ tm('dialogs.addProvider.tabs.basic') }}
</v-tab>
<v-tab value="speech_to_text" class="font-weight-medium px-3">
<v-icon start>mdi-microphone-message</v-icon>
语音转文字
{{ tm('dialogs.addProvider.tabs.speechToText') }}
</v-tab>
<v-tab value="text_to_speech" class="font-weight-medium px-3">
<v-icon start>mdi-volume-high</v-icon>
文字转语音
{{ tm('dialogs.addProvider.tabs.textToSpeech') }}
</v-tab>
<v-tab value="embedding" class="font-weight-medium px-3">
<v-icon start>mdi-code-json</v-icon>
Embedding
{{ tm('dialogs.addProvider.tabs.embedding') }}
</v-tab>
</v-tabs>
@@ -216,7 +216,7 @@
</v-col>
<v-col v-if="Object.keys(getTemplatesByType(tabType)).length === 0" cols="12">
<v-alert type="info" variant="tonal">
暂无{{ getTabTypeName(tabType) }}类型的提供商模板
{{ tm('dialogs.addProvider.noTemplates', { type: getTabTypeName(tabType) }) }}
</v-alert>
</v-col>
</v-row>
@@ -231,7 +231,7 @@
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">{{ updatingMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<span>{{ updatingMode ? '编辑' : '新增' }} {{ newSelectedProviderName }} 服务提供商</span>
<span>{{ updatingMode ? tm('dialogs.config.editTitle') : tm('dialogs.config.addTitle') }} {{ newSelectedProviderName }} {{ tm('dialogs.config.provider') }}</span>
</v-card-title>
<v-card-text class="py-4">
@@ -247,10 +247,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showProviderCfg = false" :disabled="loading">
取消
{{ tm('dialogs.config.cancel') }}
</v-btn>
<v-btn color="primary" @click="newProvider" :loading="loading">
保存
{{ tm('dialogs.config.save') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -261,7 +261,7 @@
<v-card>
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
<v-icon color="white" class="me-2">mdi-cog</v-icon>
<span>服务提供商设置</span>
<span>{{ tm('dialogs.settings.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showSettingsDialog = false">
<v-icon>mdi-close</v-icon>
@@ -281,8 +281,8 @@
>
<template v-slot:label>
<div>
<div class="text-subtitle-1">启用提供商会话隔离</div>
<div class="text-caption text-medium-emphasis">不同会话将可独立选择文本生成TTSSTT 等服务提供商</div>
<div class="text-subtitle-1">{{ tm('dialogs.settings.sessionSeparation.title') }}</div>
<div class="text-caption text-medium-emphasis">{{ tm('dialogs.settings.sessionSeparation.description') }}</div>
</div>
</template>
</v-switch>
@@ -293,7 +293,7 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showSettingsDialog = false">
关闭
{{ tm('dialogs.settings.close') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -315,6 +315,7 @@ import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'ProviderPage',
@@ -324,6 +325,10 @@ export default {
ConsoleDisplayer,
ItemCardGrid
},
setup() {
const { tm } = useModuleI18n('features/provider');
return { tm };
},
data() {
return {
config_data: {},
@@ -384,6 +389,35 @@ export default {
},
computed: {
// 翻译消息的计算属性
messages() {
return {
emptyText: {
all: this.tm('providers.empty.all'),
typed: this.tm('providers.empty.typed')
},
tabTypes: {
'chat_completion': this.tm('providers.tabs.chatCompletion'),
'speech_to_text': this.tm('providers.tabs.speechToText'),
'text_to_speech': this.tm('providers.tabs.textToSpeech'),
'embedding': this.tm('providers.tabs.embedding')
},
success: {
update: this.tm('messages.success.update'),
add: this.tm('messages.success.add'),
delete: this.tm('messages.success.delete'),
statusUpdate: this.tm('messages.success.statusUpdate'),
sessionSeparation: this.tm('messages.success.sessionSeparation')
},
error: {
sessionSeparation: this.tm('messages.error.sessionSeparation')
},
confirm: {
delete: this.tm('messages.confirm.delete')
}
};
},
// 根据选择的标签过滤提供商列表
filteredProviders() {
if (!this.config_data.provider || this.activeProviderTypeTab === 'all') {
@@ -422,9 +456,9 @@ export default {
// 获取空列表文本
getEmptyText() {
if (this.activeProviderTypeTab === 'all') {
return "暂无服务提供商,点击 新增服务提供商 添加";
return this.messages.emptyText.all;
} else {
return `暂无${this.getTabTypeName(this.activeProviderTypeTab)}类型的服务提供商,点击 新增服务提供商 添加`;
return this.tm('providers.empty.typed', { type: this.getTabTypeName(this.activeProviderTypeTab) });
}
},
@@ -476,21 +510,15 @@ export default {
// 获取Tab类型的中文名称
getTabTypeName(tabType) {
const names = {
'chat_completion': '基本对话',
'speech_to_text': '语音转文本',
'text_to_speech': '文本转语音',
'embedding': 'Embedding'
};
return names[tabType] || tabType;
return this.messages.tabTypes[tabType] || tabType;
},
// 获取提供商简介
getProviderDescription(template, name) {
if (name == 'OpenAI') {
return `${template.type} 服务提供商。同时也支持所有兼容 OpenAI API 的模型提供商。`;
return this.tm('providers.description.openai', { type: template.type });
}
return `${template.type} 服务提供商`;
return this.tm('providers.description.default', { type: template.type });
},
// 选择提供商模板
@@ -573,7 +601,7 @@ export default {
this.loading = false;
this.showProviderCfg = false;
this.getConfig();
this.showSuccess(res.data.message || "更新成功!");
this.showSuccess(res.data.message || this.messages.success.update);
}).catch((err) => {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
@@ -584,7 +612,7 @@ export default {
this.loading = false;
this.showProviderCfg = false;
this.getConfig();
this.showSuccess(res.data.message || "添加成功!");
this.showSuccess(res.data.message || this.messages.success.add);
}).catch((err) => {
this.loading = false;
this.showError(err.response?.data?.message || err.message);
@@ -593,10 +621,10 @@ export default {
},
deleteProvider(provider) {
if (confirm(`确定要删除服务提供商 ${provider.id} 吗?`)) {
if (confirm(this.tm('messages.confirm.delete', { id: provider.id }))) {
axios.post('/api/config/provider/delete', { id: provider.id }).then((res) => {
this.getConfig();
this.showSuccess(res.data.message || "删除成功!");
this.showSuccess(res.data.message || this.messages.success.delete);
}).catch((err) => {
this.showError(err.response?.data?.message || err.message);
});
@@ -611,7 +639,7 @@ export default {
config: provider
}).then((res) => {
this.getConfig();
this.showSuccess(res.data.message || "状态更新成功!");
this.showSuccess(res.data.message || this.messages.success.statusUpdate);
}).catch((err) => {
provider.enable = !provider.enable; // 发生错误时回滚状态
this.showError(err.response?.data?.message || err.message);
@@ -625,7 +653,7 @@ export default {
this.sessionSeparationEnabled = res.data.data.enable;
}
}).catch((err) => {
this.showError(err.response?.data?.message || "获取会话隔离配置失败");
this.showError(err.response?.data?.message || this.messages.error.sessionSeparation);
});
},
@@ -635,7 +663,7 @@ export default {
axios.post('/api/config/provider/set_session_seperate', {
enable: this.sessionSeparationEnabled
}).then((res) => {
this.showSuccess(res.data.message || "会话隔离设置已更新");
this.showSuccess(res.data.message || this.messages.success.sessionSeparation);
this.sessionSettingLoading = false;
}).catch((err) => {
this.sessionSeparationEnabled = !this.sessionSeparationEnabled; // 发生错误时回滚状态
@@ -663,7 +691,7 @@ export default {
if (res.data && res.data.status === 'ok') {
this.providerStatuses = res.data.data || [];
} else {
this.showError(res.data?.message || "获取供应商状态失败");
this.showError(res.data?.message || this.tm('messages.error.fetchStatus'));
}
this.loadingStatus = false;
}).catch((err) => {
+11 -6
View File
@@ -3,19 +3,19 @@
<div style="background-color: var(--v-theme-surface, #fff); padding: 8px; padding-left: 16px; border-radius: 8px; margin-bottom: 16px;">
<v-list lines="two">
<v-list-subheader>网络</v-list-subheader>
<v-list-subheader>{{ tm('network.title') }}</v-list-subheader>
<v-list-item subtitle="设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。" title="GitHub 加速地址">
<v-list-item :subtitle="tm('network.githubProxy.subtitle')" :title="tm('network.githubProxy.title')">
<v-combobox variant="outlined" style="width: 100%; margin-top: 16px;" v-model="selectedGitHubProxy" :items="githubProxies"
label="选择 GitHub 加速地址">
:label="tm('network.githubProxy.label')">
</v-combobox>
</v-list-item>
<v-list-subheader>系统</v-list-subheader>
<v-list-subheader>{{ tm('system.title') }}</v-list-subheader>
<v-list-item subtitle="重启 AstrBot" title="重启">
<v-btn style="margin-top: 16px;" color="error" @click="restartAstrBot">重启</v-btn>
<v-list-item :subtitle="tm('system.restart.subtitle')" :title="tm('system.restart.title')">
<v-btn style="margin-top: 16px;" color="error" @click="restartAstrBot">{{ tm('system.restart.button') }}</v-btn>
</v-list-item>
@@ -33,11 +33,16 @@
<script>
import axios from 'axios';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
components: {
WaitingForRestart,
},
setup() {
const { tm } = useModuleI18n('features/settings');
return { tm };
},
data() {
return {
githubProxies: [
+80 -83
View File
@@ -5,10 +5,10 @@
<v-row>
<v-col cols="12">
<h1 class="text-h4 font-weight-bold mb-2">
<v-icon size="x-large" color="primary" class="me-2">mdi-function-variant</v-icon>函数工具管理
<v-icon size="x-large" color="primary" class="me-2">mdi-function-variant</v-icon>{{ tm('title') }}
</h1>
<p class="text-subtitle-1 text-medium-emphasis mb-4 d-flex align-center">
管理 MCP 服务器和查看可用的函数工具
{{ tm('subtitle') }}
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="small" color="primary" class="ms-1 cursor-pointer"
@@ -16,7 +16,7 @@
mdi-information
</v-icon>
</template>
<span>函数调用和 MCP 是什么</span>
<span>{{ tm('tooltip.info') }}</span>
</v-tooltip>
</p>
</v-col>
@@ -26,13 +26,13 @@
<v-tabs v-model="activeTab" color="primary" class="mb-4" show-arrows>
<v-tab value="local" class="font-weight-medium">
<v-icon start>mdi-server</v-icon>
本地服务器
{{ tm('tabs.local') }}
</v-tab>
<v-tab value="marketplace" class="font-weight-medium">
<v-icon start>mdi-store</v-icon>
MCP 市场
{{ tm('tabs.marketplace') }}
<v-tooltip location="top" activator="parent">
<span>浏览和安装来自社区的 MCP 服务器</span>
<span>{{ tm('tooltip.marketplace') }}</span>
</v-tooltip>
</v-tab>
</v-tabs>
@@ -44,14 +44,14 @@
<v-card class="mb-6" elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-server</v-icon>
<span class="text-h6">MCP 服务器</span>
<span class="text-h6">{{ tm('mcpServers.title') }}</span>
<v-spacer></v-spacer>
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="getServers" :loading="loading">
刷新
{{ tm('mcpServers.buttons.refresh') }}
</v-btn>
<v-btn color="primary" style="margin-left: 8px;" prepend-icon="mdi-plus" variant="tonal"
@click="showMcpServerDialog = true">
新增服务器
{{ tm('mcpServers.buttons.add') }}
</v-btn>
</v-card-title>
@@ -60,7 +60,7 @@
<v-card-text class="px-4 py-3">
<item-card-grid :items="mcpServers || []" title-field="name" enabled-field="active"
empty-icon="mdi-server-off" empty-text="暂无 MCP 服务器点击 新增服务器 添加" @toggle-enabled="updateServerStatus"
empty-icon="mdi-server-off" :empty-text="tm('mcpServers.empty')" @toggle-enabled="updateServerStatus"
@delete="deleteServer" @edit="editServer">
<template v-slot:item-details="{ item }">
@@ -74,7 +74,7 @@
<div v-if="item.tools && item.tools.length > 0">
<div class="d-flex align-center mb-1">
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
<span class="text-caption text-medium-emphasis">可用工具 ({{ item.tools.length }})</span>
<span class="text-caption text-medium-emphasis">{{ tm('mcpServers.status.availableTools') }} ({{ item.tools.length }})</span>
</div>
<v-chip-group class="tool-chips">
<v-chip v-for="(tool, idx) in item.tools" :key="idx" size="x-small" density="compact" color="info"
@@ -85,7 +85,7 @@
</div>
<div v-else class="text-caption text-medium-emphasis mt-2">
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
无可用工具
{{ tm('mcpServers.status.noTools') }}
</div>
</template>
@@ -98,11 +98,11 @@
<v-card elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-function</v-icon>
<span class="text-h6">函数工具</span>
<span class="text-h6">{{ tm('functionTools.title') }}</span>
<v-chip color="info" size="small" class="ml-2">{{ tools.length }}</v-chip>
<v-spacer></v-spacer>
<v-btn variant="text" color="primary" @click="showTools = !showTools">
{{ showTools ? '收起' : '展开' }}
{{ showTools ? tm('functionTools.buttons.collapse') : tm('functionTools.buttons.expand') }}
<v-icon>{{ showTools ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</v-card-title>
@@ -113,11 +113,11 @@
<v-card-text class="pa-3" v-if="showTools">
<div v-if="tools.length === 0" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">mdi-api-off</v-icon>
<p class="text-grey mt-4">没有可用的函数工具</p>
<p class="text-grey mt-4">{{ tm('functionTools.empty') }}</p>
</div>
<div v-else>
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" label="搜索函数工具" variant="outlined"
<v-text-field v-model="toolSearch" prepend-inner-icon="mdi-magnify" :label="tm('functionTools.search')" variant="outlined"
density="compact" class="mb-4" hide-details clearable></v-text-field>
<v-expansion-panels v-model="openedPanel" multiple>
@@ -147,22 +147,22 @@
<v-card-text>
<p class="text-body-1 font-weight-medium mb-3">
<v-icon color="primary" size="small" class="me-1">mdi-information</v-icon>
功能描述
{{ tm('functionTools.description') }}
</p>
<p class="text-body-2 ml-6 mb-4">{{ tool.function.description }}</p>
<template v-if="tool.function.parameters && tool.function.parameters.properties">
<p class="text-body-1 font-weight-medium mb-3">
<v-icon color="primary" size="small" class="me-1">mdi-code-json</v-icon>
参数列表
{{ tm('functionTools.parameters') }}
</p>
<v-table density="compact" class="params-table mt-1">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>{{ tm('functionTools.table.paramName') }}</th>
<th>{{ tm('functionTools.table.type') }}</th>
<th>{{ tm('functionTools.table.description') }}</th>
</tr>
</thead>
<tbody>
@@ -181,7 +181,7 @@
</template>
<div v-else class="text-center pa-4 text-medium-emphasis">
<v-icon size="large" color="grey-lighten-1">mdi-code-brackets</v-icon>
<p>此工具没有参数</p>
<p>{{ tm('functionTools.noParameters') }}</p>
</div>
</v-card-text>
</v-card>
@@ -199,14 +199,14 @@
<v-card elevation="2">
<v-card-title class="d-flex align-center py-3 px-4">
<v-icon color="primary" class="me-2">mdi-store</v-icon>
<span class="text-h6">MCP 服务器市场</span>
<span class="text-h6">{{ tm('marketplace.title') }}</span>
<v-spacer></v-spacer>
<v-text-field v-model="marketplaceSearch" prepend-inner-icon="mdi-magnify" label="搜索服务器"
<v-text-field v-model="marketplaceSearch" prepend-inner-icon="mdi-magnify" :label="tm('marketplace.search')"
variant="outlined" density="compact" hide-details class="mx-2" style="max-width: 300px" clearable
@update:model-value="searchMarketplaceServers"></v-text-field>
<v-btn color="primary" prepend-icon="mdi-refresh" variant="text" @click="fetchMarketplaceServers(1)"
:loading="marketplaceLoading">
刷新
{{ tm('marketplace.buttons.refresh') }}
</v-btn>
</v-card-title>
@@ -216,13 +216,13 @@
<!-- 加载中 -->
<div v-if="marketplaceLoading" class="text-center pa-8">
<v-progress-circular indeterminate color="primary" size="64"></v-progress-circular>
<p class="text-grey mt-4">正在加载 MCP 服务器市场...</p>
<p class="text-grey mt-4">{{ tm('marketplace.loading') }}</p>
</div>
<!-- 无数据 -->
<div v-else-if="filteredMarketplaceServers.length === 0" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">mdi-store-off</v-icon>
<p class="text-grey mt-4">暂无可用的 MCP 服务器</p>
<p class="text-grey mt-4">{{ tm('marketplace.empty') }}</p>
</div>
<v-row v-else>
@@ -241,7 +241,7 @@
<div class="d-flex align-center mb-2">
<v-icon size="small" color="grey" class="me-2">mdi-tools</v-icon>
<span class="text-caption text-medium-emphasis">
可用工具 ({{ server.tools ? server.tools.length : 0 }})
{{ tm('marketplace.status.availableTools', { count: server.tools ? server.tools.length : 0 }) }}
</span>
</div>
@@ -253,7 +253,7 @@
</v-chip-group>
<div v-else class="text-caption text-medium-emphasis mb-2">
<v-icon size="small" color="warning" class="me-1">mdi-alert-circle</v-icon>
无可用工具信息
{{ tm('marketplace.status.noToolsInfo') }}
</div>
</v-card-text>
@@ -263,11 +263,11 @@
<v-spacer></v-spacer>
<v-btn variant="text" size="small" color="info" prepend-icon="mdi-information-outline"
@click="showServerDetail(server)">
详情
{{ tm('marketplace.buttons.detail') }}
</v-btn>
<v-btn variant="text" size="small" color="primary" prepend-icon="mdi-plus"
@click="importServerConfig(server)">
导入
{{ tm('marketplace.buttons.import') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -290,44 +290,34 @@
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">{{ isEditMode ? 'mdi-pencil' : 'mdi-plus' }}</v-icon>
<span>{{ isEditMode ? '编辑' : '新增' }} MCP 服务器</span>
<span>{{ isEditMode ? tm('dialogs.addServer.editTitle') : tm('dialogs.addServer.title') }}</span>
</v-card-title>
<v-card-text class="py-4">
<v-form @submit.prevent="saveServer" ref="form">
<v-text-field v-model="currentServer.name" label="服务器名称" variant="outlined" :rules="[v => !!v || '名称是必填项']"
<v-text-field v-model="currentServer.name" :label="tm('dialogs.addServer.fields.name')" variant="outlined" :rules="[v => !!v || tm('dialogs.addServer.fields.nameRequired')]"
required class="mb-3"></v-text-field>
<v-switch v-model="currentServer.active" label="启用服务器" color="primary" hide-details class="mb-3"></v-switch>
<v-switch v-model="currentServer.active" :label="tm('dialogs.addServer.fields.enable')" color="primary" hide-details class="mb-3"></v-switch>
<div class="mb-2 d-flex align-center">
<span class="text-subtitle-1">服务器配置</span>
<span class="text-subtitle-1">{{ tm('dialogs.addServer.fields.config') }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" class="ms-2" size="small" color="primary">mdi-information</v-icon>
</template>
<div>
<p class="mb-1">MCP 服务器(stdio)配置支持以下字段:</p>
<p class="mb-1"><code>command</code>: 命令名称 (例如 python uv)</p>
<p class="mb-1"><code>args</code>: 命令参数数组 (例如 ["run", "server.py"])</p>
<p class="mb-1"><code>env</code>: 环境变量对象 (例如 {"api_key": "abc"})</p>
<p class="mb-1"><code>cwd</code>: 工作目录路径 (例如 /path/to/server)</p>
<p class="mb-1"><code>encoding</code>: 输出编码 (默认 utf-8)</p>
<p class="mb-1"><code>encoding_error_handler</code>: The text encoding error handler. Defaults to
strict.
</p>
<p class="mb-1">其他字段请参考 MCP 文档</p>
<p class="mb-1"> 如果您使用 Docker 部署 AstrBot, 请务必将 MCP 服务器装在 AstrBot 挂载好的 data 目录下</p>
<div style="white-space: pre-line;">
{{ tm('tooltip.serverConfig') }}
</div>
</v-tooltip>
<v-spacer></v-spacer>
<v-btn size="small" color="info" variant="text" @click="setConfigTemplate" class="me-1">
使用模板
{{ tm('mcpServers.buttons.useTemplate') }}
</v-btn>
</div>
<small>1. 某些 MCP 服务器可能需要按照其要求在 env 中填充 `API_KEY` `TOKEN` 等信息请注意检查是否填写</small>
<small>{{ tm('dialogs.addServer.configNotes.note1') }}</small>
<br>
<small>2. 当配置中指定 url 参数时如果还同时指定 `transport` 参数的值为 `streamable_http`则使用 Steamable HTTP否则使用 SSE 连接</small>
<small>{{ tm('dialogs.addServer.configNotes.note2') }}</small>
<div class="monaco-container">
<VueMonacoEditor v-model:value="serverConfigJson" theme="vs-dark" language="json" :options="{
@@ -355,10 +345,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeServerDialog" :disabled="loading">
取消
{{ tm('dialogs.addServer.buttons.cancel') }}
</v-btn>
<v-btn color="primary" @click="saveServer" :loading="loading" :disabled="!isServerFormValid">
保存
{{ tm('dialogs.addServer.buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -369,7 +359,7 @@
<v-card>
<v-card-title class="bg-primary text-white py-3">
<v-icon color="white" class="me-2">mdi-information-outline</v-icon>
<span>服务器详情</span>
<span>{{ tm('dialogs.serverDetail.title') }}</span>
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showServerDetailDialog = false">
<v-icon>mdi-close</v-icon>
@@ -380,7 +370,7 @@
<h2 class="text-h5 mb-3">{{ selectedMarketplaceServer.name }}</h2>
<div class="mb-4">
<h3 class="text-subtitle-1 font-weight-bold mb-2">安装配置</h3>
<h3 class="text-subtitle-1 font-weight-bold mb-2">{{ tm('dialogs.serverDetail.installConfig') }}</h3>
<div class="monaco-container" style="height: 200px">
<VueMonacoEditor v-model:value="selectedServerConfigDisplay" theme="vs-dark" language="json" :options="{
readOnly: true,
@@ -397,7 +387,7 @@
<div v-if="selectedMarketplaceServer.tools && selectedMarketplaceServer.tools.length > 0">
<h3 class="text-subtitle-1 font-weight-bold mb-2">
可用工具
{{ tm('dialogs.serverDetail.availableTools') }}
<v-chip color="info" size="small" class="ml-1">{{ selectedMarketplaceServer.tools.length }}</v-chip>
</h3>
@@ -414,14 +404,14 @@
<p class="mb-3">{{ tool.description }}</p>
<template v-if="tool.inputSchema && tool.inputSchema.properties">
<h4 class="text-subtitle-2 mb-2">参数列表</h4>
<h4 class="text-subtitle-2 mb-2">{{ tm('functionTools.parameters') }}</h4>
<v-table density="compact">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>必填</th>
<th>描述</th>
<th>{{ tm('functionTools.table.paramName') }}</th>
<th>{{ tm('functionTools.table.type') }}</th>
<th>{{ tm('functionTools.table.required') }}</th>
<th>{{ tm('functionTools.table.description') }}</th>
</tr>
</thead>
<tbody>
@@ -437,7 +427,7 @@
color="error" size="small">
mdi-check
</v-icon>
<span v-else></span>
<span v-else>{{ t('core.common.no') }}</span>
</td>
<td>{{ param.description }}</td>
</tr>
@@ -455,10 +445,10 @@
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="showServerDetailDialog = false">
关闭
{{ tm('dialogs.serverDetail.buttons.close') }}
</v-btn>
<v-btn color="primary" prepend-icon="mdi-plus" @click="importServerConfig(selectedMarketplaceServer)">
导入配置
{{ tm('dialogs.serverDetail.buttons.importConfig') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -477,6 +467,8 @@ import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import { VueMonacoEditor } from '@guolao/vue-monaco-editor';
import ItemCardGrid from '@/components/shared/ItemCardGrid.vue';
import { useI18n, useModuleI18n } from '@/i18n/composables';
export default {
name: 'ToolUsePage',
components: {
@@ -484,6 +476,11 @@ export default {
VueMonacoEditor,
ItemCardGrid
},
setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/tooluse');
return { t, tm };
},
data() {
return {
refreshInterval: null,
@@ -551,10 +548,10 @@ export default {
);
if (configKeys.length > 0) {
return `配置: ${configKeys.join(', ')}`;
return this.tm('mcpServers.status.configSummary', { keys: configKeys.join(', ') });
}
return '未设置配置';
return this.tm('mcpServers.status.noConfig');
}
},
@@ -612,7 +609,7 @@ export default {
this.mcpServers = response.data.data || [];
})
.catch(error => {
this.showError("获取 MCP 服务器列表失败: " + error.message);
this.showError(this.tm('messages.getServersError', { error: error.message }));
}).finally(() => {
setTimeout(() => {
this.loading = false;
@@ -626,14 +623,14 @@ export default {
this.tools = response.data.data || [];
})
.catch(error => {
this.showError("获取函数工具列表失败: " + error.message);
this.showError(this.tm('messages.getToolsError', { error: error.message }));
});
},
validateJson() {
try {
if (!this.serverConfigJson.trim()) {
this.jsonError = '配置不能为空';
this.jsonError = this.tm('dialogs.addServer.errors.configEmpty');
return false;
}
@@ -641,7 +638,7 @@ export default {
this.jsonError = null;
return true;
} catch (e) {
this.jsonError = `JSON 格式错误: ${e.message}`;
this.jsonError = this.tm('dialogs.addServer.errors.jsonFormat', { error: e.message });
return false;
}
},
@@ -683,30 +680,30 @@ export default {
this.showMcpServerDialog = false;
this.getServers();
this.getTools();
this.showSuccess(response.data.message || "保存成功!");
this.showSuccess(response.data.message || this.tm('messages.saveSuccess'));
this.resetForm();
})
.catch(error => {
this.loading = false;
this.showError("保存失败: " + (error.response?.data?.message || error.message));
this.showError(this.tm('messages.saveError', { error: error.response?.data?.message || error.message }));
});
} catch (e) {
this.loading = false;
this.showError(`JSON 解析错误: ${e.message}`);
this.showError(this.tm('dialogs.addServer.errors.jsonParse', { error: e.message }));
}
},
deleteServer(server) {
let serverName = server.name || server;
if (confirm(`确定要删除服务器 ${serverName} 吗?`)) {
if (confirm(this.tm('dialogs.confirmDelete', { name: serverName }))) {
axios.post('/api/tools/mcp/delete', { name: serverName })
.then(response => {
this.getServers();
this.getTools();
this.showSuccess(response.data.message || "删除成功!");
this.showSuccess(response.data.message || this.tm('messages.deleteSuccess'));
})
.catch(error => {
this.showError("删除失败: " + (error.response?.data?.message || error.message));
this.showError(this.tm('messages.deleteError', { error: error.response?.data?.message || error.message }));
});
}
},
@@ -745,10 +742,10 @@ export default {
axios.post('/api/tools/mcp/update', server)
.then(response => {
this.getServers();
this.showSuccess(response.data.message || "更新成功!");
this.showSuccess(response.data.message || this.tm('messages.updateSuccess'));
})
.catch(error => {
this.showError("更新失败: " + (error.response?.data?.message || error.message));
this.showError(this.tm('messages.updateError', { error: error.response?.data?.message || error.message }));
// 回滚状态
server.active = !server.active;
});
@@ -816,7 +813,7 @@ export default {
this.marketplaceLoading = false;
})
.catch(error => {
this.showError("获取 MCP 市场服务器列表失败: " + error.message);
this.showError(this.tm('messages.getMarketError', { error: error.message }));
this.marketplaceLoading = false;
});
},
@@ -843,10 +840,10 @@ export default {
const configs = JSON.parse(server.config);
this.selectedServerConfigDisplay = JSON.stringify(configs[0], null, 2);
} else {
this.selectedServerConfigDisplay = '// 无可用配置';
this.selectedServerConfigDisplay = '// ' + this.tm('messages.noAvailableConfig');
}
} catch (e) {
this.selectedServerConfigDisplay = '// 配置解析错误: ' + e.message;
this.selectedServerConfigDisplay = '// ' + this.tm('messages.configParseError', { error: e.message });
}
this.showServerDetailDialog = true;
@@ -857,13 +854,13 @@ export default {
try {
// 解析服务器配置
if (!server.config) {
this.showError('此服务器没有可用配置');
this.showError(this.tm('messages.importError.noConfig'));
return;
}
const configs = JSON.parse(server.config);
if (!configs || !configs[0] || !configs[0].mcpServers) {
this.showError('服务器配置格式不正确');
this.showError(this.tm('messages.importError.invalidFormat'));
return;
}
@@ -889,7 +886,7 @@ export default {
this.showMcpServerDialog = true;
} catch (e) {
this.showError('导入配置失败: ' + e.message);
this.showError(this.tm('messages.importError.failed', { error: e.message }));
}
}
}
+5 -5
View File
@@ -3,13 +3,13 @@
<div class="d-flex align-center justify-center"
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
<span size="64">🌍</span>
<p class="text-h6 text-grey ml-4">{{ $t('alkaid.comingSoon') }}</p>
<p class="text-h6 text-grey ml-4">{{ tm('comingSoon') }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'OtherFeatures'
}
<script setup>
import { useModuleI18n } from '@/i18n/composables';
const { tm } = useModuleI18n('features/alkaid/index');
</script>
@@ -1,15 +1,18 @@
<script setup lang="ts">
import AuthLogin from '../authForms/AuthLogin.vue';
import Logo from '@/components/shared/Logo.vue';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
import { onMounted, ref } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
const cardVisible = ref(false);
const router = useRouter();
const authStore = useAuthStore();
const customizer = useCustomizerStore();
const { tm: t } = useModuleI18n('features/auth');
//
function toggleTheme() {
@@ -36,76 +39,146 @@ onMounted(() => {
<div v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="login-page-container">
<div class="login-background"></div>
<!-- 主题切换按钮 -->
<div class="theme-toggle-container">
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="flat"
size="small"
color="primary"
elevation="2"
<div class="login-container">
<!-- 桌面端卡片样式 -->
<v-card
v-if="!$vuetify.display.xs"
variant="outlined"
class="login-card"
:class="{ 'card-visible': cardVisible }"
>
<v-icon size="20">mdi-weather-night</v-icon>
<v-tooltip activator="parent" location="left">
切换到深色主题
</v-tooltip>
</v-btn>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
<!-- 移动端全屏样式 -->
<div
v-else
class="mobile-login-container"
:class="{ 'mobile-visible': cardVisible }"
>
<div class="mobile-content">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</div>
</div>
<!-- 悬浮式圆角工具栏 -->
<v-card
class="floating-toolbar"
:class="{ 'toolbar-visible': cardVisible }"
elevation="8"
rounded="xl"
>
<v-card-text class="pa-2">
<div class="d-flex align-center gap-1">
<LanguageSwitcher />
<v-divider vertical class="mx-1" style="height: 20px; opacity: 0.3;"></v-divider>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
>
mdi-weather-night
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t('theme.switchToDark') }}
</v-tooltip>
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
<v-card
variant="outlined"
class="login-card"
:class="{ 'card-visible': cardVisible }"
>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
</div>
<div v-else class="login-page-container-dark">
<div class="login-background-dark"></div>
<!-- 主题切换按钮 -->
<div class="theme-toggle-container">
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="flat"
size="small"
color="secondary"
elevation="2"
>
<v-icon size="20">mdi-white-balance-sunny</v-icon>
<v-tooltip activator="parent" location="left">
切换到浅色主题
</v-tooltip>
</v-btn>
</div>
<v-card
<div class="login-container">
<!-- 桌面端卡片样式 -->
<v-card
v-if="!$vuetify.display.xs"
variant="outlined"
class="login-card"
:class="{ 'card-visible': cardVisible }"
>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo />
>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
<!-- 移动端全屏样式 -->
<div
v-else
class="mobile-login-container"
:class="{ 'mobile-visible': cardVisible }"
>
<div class="mobile-content">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
</div>
<!-- 悬浮式圆角工具栏 -->
<v-card
class="floating-toolbar"
:class="{ 'toolbar-visible': cardVisible }"
elevation="8"
rounded="xl"
>
<v-card-text class="pa-2">
<div class="d-flex align-center gap-1">
<LanguageSwitcher />
<v-divider vertical class="mx-1" style="height: 20px; opacity: 0.3;"></v-divider>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
>
mdi-white-balance-sunny
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t('theme.switchToLight') }}
</v-tooltip>
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</div>
</template>
@@ -193,21 +266,64 @@ onMounted(() => {
}
}
.theme-toggle-container {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
.login-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.floating-toolbar {
background: #f8f6fc !important;
border: 1px solid rgba(94, 53, 177, 0.15) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
backdrop-filter: blur(10px);
transform: translateY(20px);
opacity: 0;
transition: transform 0.6s ease 0.2s, opacity 0.6s ease 0.2s, border-color 0.3s ease, box-shadow 0.3s ease;
min-width: auto !important;
width: fit-content;
&.toolbar-visible {
transform: translateY(0);
opacity: 1;
}
&:hover {
transform: translateY(-2px);
border-color: rgba(158, 126, 222, 0.99) !important;
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
}
}
.login-page-container-dark .floating-toolbar {
background: #2a2733 !important;
border: 1px solid rgba(110, 60, 180, 0.692) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
&:hover {
border-color: rgba(160, 118, 219, 0.782) !important;
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
}
}
.theme-toggle-btn {
transition: all 0.3s ease;
border-radius: 50% !important;
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
&:hover {
transform: scale(1.1);
transform: scale(1.05);
background: rgba(94, 53, 177, 0.08) !important;
}
}
.login-page-container-dark .theme-toggle-btn:hover {
background: rgba(114, 46, 209, 0.12) !important;
}
.login-card {
max-width: 520px;
width: 90%;
@@ -295,4 +411,53 @@ onMounted(() => {
max-width: 475px;
margin: 0 auto;
}
/* 移动端全屏登录样式 */
.mobile-login-container {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
transform: translateY(20px);
opacity: 0;
transition: transform 0.5s ease, opacity 0.5s ease;
z-index: 1;
&.mobile-visible {
transform: translateY(0);
opacity: 1;
}
}
.mobile-content {
width: 100%;
max-width: 400px;
padding: 40px 20px;
}
/* 移动端调整工具栏位置 */
@media (max-width: 599px) {
.floating-toolbar {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(20px);
z-index: 1000;
&.toolbar-visible {
transform: translateX(-50%) translateY(0);
}
&:hover {
transform: translateX(-50%) translateY(-2px);
}
}
.login-container {
gap: 0;
}
}
</style>
@@ -4,6 +4,9 @@ import { useAuthStore } from '@/stores/auth';
import { Form } from 'vee-validate';
import md5 from 'js-md5';
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
const { tm: t } = useModuleI18n('features/auth');
const valid = ref(false);
const show1 = ref(false);
@@ -40,7 +43,7 @@ async function validate(values: any, { setErrors }: any) {
<Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
<v-text-field
v-model="username"
label="用户名"
:label="t('username')"
class="mb-6 input-field"
required
density="comfortable"
@@ -53,7 +56,7 @@ async function validate(values: any, { setErrors }: any) {
<v-text-field
v-model="password"
label="密码"
:label="t('password')"
required
density="comfortable"
variant="outlined"
@@ -79,7 +82,7 @@ async function validate(values: any, { setErrors }: any) {
elevation="2"
>
<span class="login-btn-text">登录</span>
<span class="login-btn-text">{{ t('login') }}</span>
</v-btn>
<div v-if="errors.apiError" class="mt-4 error-container">
@@ -1,8 +1,8 @@
<template>
<div class="dashboard-container">
<div class="dashboard-header">
<h1 class="dashboard-title">控制台</h1>
<div class="dashboard-subtitle">实时监控和统计数据</div>
<h1 class="dashboard-title">{{ t('title') }}</h1>
<div class="dashboard-subtitle">{{ t('subtitle') }}</div>
</div>
<v-slide-y-transition>
@@ -58,7 +58,7 @@
</v-row>
<div class="dashboard-footer">
<v-chip size="small" color="primary" variant="flat" prepend-icon="mdi-refresh">
最后更新: {{ lastUpdated }}
{{ t('lastUpdate') }}: {{ lastUpdated }}
</v-chip>
<v-btn
icon="mdi-refresh"
@@ -82,6 +82,7 @@ import MemoryUsage from './components/MemoryUsage.vue';
import MessageStat from './components/MessageStat.vue';
import PlatformStat from './components/PlatformStat.vue';
import axios from 'axios';
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'DefaultDashboard',
@@ -93,17 +94,24 @@ export default {
MessageStat,
PlatformStat,
},
data: () => ({
stat: {},
noticeTitle: '',
noticeContent: '',
noticeType: '',
lastUpdated: '加载中...',
refreshInterval: null,
isRefreshing: false
}),
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
data() {
return {
stat: {},
noticeTitle: '',
noticeContent: '',
noticeType: '',
lastUpdated: '',
refreshInterval: null,
isRefreshing: false
};
},
mounted() {
this.lastUpdated = this.t('status.loading');
this.fetchData();
this.fetchNotice();
@@ -129,7 +137,7 @@ export default {
this.lastUpdated = new Date().toLocaleTimeString();
console.log('Dashboard data:', this.stat);
} catch (error) {
console.error('获取数据失败:', error);
console.error(this.t('status.dataError'), error);
} finally {
this.isRefreshing = false;
}
@@ -145,7 +153,7 @@ export default {
this.noticeType = data['dashboard-notice'].type;
}
}).catch(error => {
console.error('获取公告失败:', error);
console.error(this.t('status.noticeError'), error);
});
}
}
@@ -7,7 +7,7 @@
</div>
<div class="stat-content">
<div class="stat-title">内存占用</div>
<div class="stat-title">{{ t('stats.memoryUsage.title') }}</div>
<div class="stat-value-wrapper">
<h2 class="stat-value">{{ stat.memory?.process || 0 }} <span class="memory-unit">MiB / {{ stat.memory?.system || 0 }} MiB</span></h2>
<v-chip :color="memoryStatus.color" size="x-small" class="status-chip">
@@ -19,7 +19,7 @@
<div class="metrics-container">
<div class="metric-item">
<div class="metric-label">CPU 负载</div>
<div class="metric-label">{{ t('stats.memoryUsage.cpuLoad') }}</div>
<div class="metric-value">{{ stat.cpu_percent || '0' }}%</div>
</div>
</div>
@@ -28,9 +28,15 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'MemoryUsage',
props: ['stat'],
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
computed: {
memoryPercentage() {
if (!this.stat.memory || !this.stat.memory.process || !this.stat.memory.system) return 0;
@@ -39,11 +45,11 @@ export default {
memoryStatus() {
const percentage = this.memoryPercentage;
if (percentage < 30) {
return { color: 'success', label: '良好' };
return { color: 'success', label: this.t('stats.memoryUsage.status.good') };
} else if (percentage < 70) {
return { color: 'warning', label: '正常' };
return { color: 'warning', label: this.t('stats.memoryUsage.status.normal') };
} else {
return { color: 'error', label: '偏高' };
return { color: 'error', label: this.t('stats.memoryUsage.status.high') };
}
}
}
@@ -3,8 +3,8 @@
<v-card-text>
<div class="chart-header">
<div>
<div class="chart-title">消息趋势分析</div>
<div class="chart-subtitle">跟踪消息数量随时间的变化</div>
<div class="chart-title">{{ t('charts.messageTrend.title') }}</div>
<div class="chart-subtitle">{{ t('charts.messageTrend.subtitle') }}</div>
</div>
<v-select
@@ -32,17 +32,17 @@
<div class="chart-stats">
<div class="stat-box">
<div class="stat-label">总消息数</div>
<div class="stat-label">{{ t('charts.messageTrend.totalMessages') }}</div>
<div class="stat-number">{{ totalMessages }}</div>
</div>
<div class="stat-box">
<div class="stat-label">平均每天</div>
<div class="stat-label">{{ t('charts.messageTrend.dailyAverage') }}</div>
<div class="stat-number">{{ dailyAverage }}</div>
</div>
<div class="stat-box" :class="{'trend-up': growthRate > 0, 'trend-down': growthRate < 0}">
<div class="stat-label">增长率</div>
<div class="stat-label">{{ t('charts.messageTrend.growthRate') }}</div>
<div class="stat-number">
<v-icon size="small" :icon="growthRate > 0 ? 'mdi-arrow-up' : 'mdi-arrow-down'"></v-icon>
{{ Math.abs(growthRate) }}%
@@ -53,7 +53,7 @@
<div class="chart-container">
<div v-if="loading" class="loading-overlay">
<v-progress-circular indeterminate color="primary"></v-progress-circular>
<div class="loading-text">加载中...</div>
<div class="loading-text">{{ t('status.loading') }}</div>
</div>
<apexchart
type="area"
@@ -70,22 +70,23 @@
<script>
import axios from 'axios';
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'MessageStat',
props: ['stat'],
data: () => ({
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
data() {
return {
totalMessages: '0',
dailyAverage: '0',
growthRate: 0,
loading: false,
selectedTimeRange: { label: '过去 1 天', value: 86400 },
timeRanges: [
{ label: '过去 1 天', value: 86400 },
{ label: '过去 3 天', value: 259200 },
{ label: '过去 7 天', value: 604800 },
{ label: '过去 30 天', value: 2592000 },
],
selectedTimeRange: null,
timeRanges: [],
chartOptions: {
chart: {
@@ -136,14 +137,14 @@ export default {
},
y: {
title: {
formatter: () => '消息条数 '
formatter: () => ''
}
},
},
xaxis: {
type: 'datetime',
title: {
text: '时间'
text: ''
},
labels: {
formatter: function (value) {
@@ -161,7 +162,7 @@ export default {
},
yaxis: {
title: {
text: '消息条数'
text: ''
},
min: function(min) {
return min < 10 ? 0 : Math.floor(min * 0.8);
@@ -185,15 +186,31 @@ export default {
chartSeries: [
{
name: '消息条数',
name: '',
data: []
}
],
messageTimeSeries: []
}),
};
},
mounted() {
//
this.timeRanges = [
{ label: this.t('charts.messageTrend.timeRanges.1day'), value: 86400 },
{ label: this.t('charts.messageTrend.timeRanges.3days'), value: 259200 },
{ label: this.t('charts.messageTrend.timeRanges.1week'), value: 604800 },
{ label: this.t('charts.messageTrend.timeRanges.1month'), value: 2592000 },
];
this.selectedTimeRange = this.timeRanges[0];
//
this.chartOptions.tooltip.y.title.formatter = () => this.t('charts.messageTrend.messageCount') + ' ';
this.chartOptions.xaxis.title.text = this.t('charts.messageTrend.timeLabel');
this.chartOptions.yaxis.title.text = this.t('charts.messageTrend.messageCount');
this.chartSeries[0].name = this.t('charts.messageTrend.messageCount');
//
this.fetchMessageSeries();
},
@@ -215,7 +232,7 @@ export default {
this.processTimeSeriesData();
}
} catch (error) {
console.error('获取消息趋势数据失败:', error);
console.error(this.t('status.dataError'), error);
} finally {
this.loading = false;
}
@@ -7,11 +7,11 @@
</div>
<div class="stat-content">
<div class="stat-title">消息平台</div>
<div class="stat-title">{{ t('stats.onlinePlatform.title') }}</div>
<div class="stat-value-wrapper">
<h2 class="stat-value">{{ stat.platform_count || 0 }}</h2>
</div>
<div class="stat-subtitle">已连接的消息平台数量</div>
<div class="stat-subtitle">{{ t('stats.onlinePlatform.subtitle') }}</div>
</div>
</div>
</v-card-text>
@@ -19,9 +19,15 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'OnlinePlatform',
props: ['stat']
props: ['stat'],
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
}
};
</script>
@@ -3,8 +3,8 @@
<v-card-text>
<div class="platform-header">
<div>
<div class="platform-title">平台消息统计</div>
<div class="platform-subtitle">各平台消息数量分布</div>
<div class="platform-title">{{ t('charts.platformStat.title') }}</div>
<div class="platform-subtitle">{{ t('charts.platformStat.subtitle') }}</div>
</div>
</div>
@@ -27,7 +27,7 @@
<template v-slot:append>
<div class="platform-count">
<span class="count-value">{{ platform.count }}</span>
<span class="count-label"></span>
<span class="count-label">{{ t('charts.platformStat.messageUnit') }}</span>
</div>
</template>
</v-list-item>
@@ -35,17 +35,17 @@
<div class="platform-stats-summary">
<div class="platform-stat-item">
<div class="stat-label">平台数</div>
<div class="stat-label">{{ t('charts.platformStat.platformCount') }}</div>
<div class="stat-value">{{ platforms.length }}</div>
</div>
<v-divider vertical></v-divider>
<div class="platform-stat-item">
<div class="stat-label">最活跃</div>
<div class="stat-label">{{ t('charts.platformStat.mostActive') }}</div>
<div class="stat-value">{{ mostActivePlatform }}</div>
</div>
<v-divider vertical></v-divider>
<div class="platform-stat-item">
<div class="stat-label">总消息占比</div>
<div class="stat-label">{{ t('charts.platformStat.totalPercentage') }}</div>
<div class="stat-value">{{ topPlatformPercentage }}%</div>
</div>
</div>
@@ -65,19 +65,27 @@
<div v-else class="no-data">
<v-icon icon="mdi-information-outline" size="40" color="grey-lighten-1"></v-icon>
<div class="no-data-text">暂无平台数据</div>
<div class="no-data-text">{{ t('charts.platformStat.noData') }}</div>
</div>
</v-card-text>
</v-card>
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'PlatformStat',
props: ['stat'],
data: () => ({
platforms: []
}),
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
data() {
return {
platforms: []
};
},
computed: {
sortedPlatforms() {
return [...this.platforms].sort((a, b) => b.count - a.count);
@@ -7,11 +7,11 @@
</div>
<div class="stat-content">
<div class="stat-title">运行时间</div>
<div class="stat-title">{{ t('stats.runningTime.title') }}</div>
<div class="stat-value-wrapper">
<h2 class="stat-value">{{ formattedTime }}</h2>
</div>
<div class="stat-subtitle">AstrBot 运行时间</div>
<div class="stat-subtitle">{{ t('stats.runningTime.subtitle') }}</div>
</div>
</div>
</v-card-text>
@@ -19,12 +19,18 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'RunningTime',
props: ['stat'],
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
computed: {
formattedTime() {
return this.stat?.running || '加载中...';
return this.stat?.running || this.t('status.loading');
}
}
};
@@ -7,14 +7,14 @@
</div>
<div class="stat-content">
<div class="stat-title">消息总数</div>
<div class="stat-title">{{ t('stats.totalMessage.title') }}</div>
<div class="stat-value-wrapper">
<h2 class="stat-value">{{ formattedCount }}</h2>
<v-chip v-if="stat.daily_increase" class="trend-chip" size="x-small" color="success">
+{{ stat.daily_increase }}
</v-chip>
</div>
<div class="stat-subtitle">所有平台发送的消息总计</div>
<div class="stat-subtitle">{{ t('stats.totalMessage.subtitle') }}</div>
</div>
</div>
</v-card-text>
@@ -22,9 +22,15 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'TotalMessage',
props: ['stat'],
setup() {
const { tm: t } = useModuleI18n('features/dashboard');
return { t };
},
computed: {
formattedCount() {
const count = this.stat?.message_count;