Merge pull request #1822 from IGCrystal/branch-1

 feat(WebUI): complete dashboard internationalization system refactor
This commit is contained in:
Soulter
2025-06-22 22:22:33 +08:00
committed by GitHub
100 changed files with 6020 additions and 1041 deletions
+3 -2
View File
@@ -31,6 +31,7 @@
"vee-validate": "4.11.3",
"vite-plugin-vuetify": "1.0.2",
"vue": "3.3.4",
"vue-i18n": "^11.1.5",
"vue-router": "4.2.4",
"vue3-apexcharts": "1.4.4",
"vue3-print-nb": "0.1.4",
@@ -41,11 +42,11 @@
"@mdi/font": "7.2.96",
"@rushstack/eslint-patch": "1.3.3",
"@types/chance": "1.1.3",
"@types/node": "20.5.7",
"@types/node": "^20.5.7",
"@vitejs/plugin-vue": "4.3.3",
"@vue/eslint-config-prettier": "8.0.0",
"@vue/eslint-config-typescript": "11.0.3",
"@vue/tsconfig": "0.4.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "8.48.0",
"eslint-plugin-vue": "9.17.0",
"prettier": "3.0.2",
+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) => {
@@ -107,7 +107,7 @@ function saveEditedContent() {
color="primary"
class="editor-fullscreen-btn"
@click="openEditorDialog(key, iterable, metadata[metadataKey].items[key]?.editor_theme, metadata[metadataKey].items[key]?.editor_language)"
title="全屏编辑"
:title="t('core.common.editor.fullscreen')"
>
<v-icon>mdi-fullscreen</v-icon>
</v-btn>
@@ -288,10 +288,10 @@ function saveEditedContent() {
<v-btn icon @click="dialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title>编辑内容 - {{ currentEditingKey }}</v-toolbar-title>
<v-toolbar-title>{{ t('core.common.editor.editingTitle') }} - {{ currentEditingKey }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn variant="text" @click="saveEditedContent">保存</v-btn>
<v-btn variant="text" @click="saveEditedContent">{{ t('core.common.save') }}</v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-text class="pa-0">
@@ -309,12 +309,17 @@ function saveEditedContent() {
<script>
import ListConfigItem from './ListConfigItem.vue';
import { useI18n } from '@/i18n/composables';
export default {
name: 'AstrBotConfig',
components: {
ListConfigItem
},
setup() {
const { t } = useI18n();
return { t };
},
props: {
metadata: {
type: Object,
@@ -51,7 +51,7 @@ export default {
props: {
historyNum: {
type: String,
default: -1
default: "-1"
},
showLevelBtns: {
type: Boolean,
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed, inject } from 'vue';
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
const props = defineProps({
extension: {
@@ -31,6 +32,9 @@ const emit = defineEmits([
const reveal = ref(false);
// 国际化
const { tm } = useModuleI18n('features/extension');
// 操作函数
const configure = () => {
emit('configure', props.extension);
@@ -47,13 +51,13 @@ const reloadExtension = () => {
const $confirm = inject("$confirm");
const uninstallExtension = async () => {
if (typeof $confirm !== "function") {
console.error("$confirm 未正确注册");
console.error(tm("card.errors.confirmNotRegistered"));
return;
}
const confirmed = await $confirm({
title: "删除确认",
message: "你确定要删除当前插件吗?",
title: tm("dialogs.uninstall.title"),
message: tm("dialogs.uninstall.message"),
});
if (confirmed) {
@@ -90,13 +94,13 @@ const viewReadme = () => {
<template v-slot:activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps" color="warning" class="ml-2" icon="mdi-update" size="small"></v-icon>
</template>
<span>有新版本可用: {{ extension.online_version }}</span>
<span>{{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}</span>
</v-tooltip>
<v-tooltip location="top" v-if="!extension.activated && !marketMode">
<template v-slot:activator="{ props: tooltipProps }">
<v-icon v-bind="tooltipProps" color="error" class="ml-2" icon="mdi-cancel" size="small"></v-icon>
</template>
<span>该插件已经被禁用</span>
<span>{{ tm("card.status.disabled") }}</span>
</v-tooltip>
</p>
@@ -111,7 +115,7 @@ const viewReadme = () => {
</v-chip>
<v-chip color="primary" label size="small" class="ml-2" v-if="extension.handlers?.length">
<v-icon icon="mdi-cogs" start></v-icon>
{{ extension.handlers?.length }}个行为
{{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
</v-chip>
</div>
@@ -127,16 +131,16 @@ const viewReadme = () => {
borderRadius: '8px',
objectFit: 'cover',
objectPosition: 'center'
}" alt="logo" />
}" :alt="tm('card.alt.logo')" />
</div>
</v-card-text>
<v-card-actions style="margin-left: 0px; gap: 2px;">
<v-btn color="teal-accent-4" text="查看文档" variant="text" @click="viewReadme"></v-btn>
<v-btn v-if="!marketMode" color="teal-accent-4" text="操作" variant="text" @click="reveal = true"></v-btn>
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" text="安装" variant="text"
<v-btn color="teal-accent-4" :text="tm('buttons.viewDocs')" variant="text" @click="viewReadme"></v-btn>
<v-btn v-if="!marketMode" color="teal-accent-4" :text="tm('buttons.actions')" variant="text" @click="reveal = true"></v-btn>
<v-btn v-if="marketMode && !extension?.installed" color="teal-accent-4" :text="tm('buttons.install')" variant="text"
@click="emit('install', extension)"></v-btn>
<v-btn v-if="marketMode && extension?.installed" color="teal-accent-4" text="已安装" variant="text" disabled></v-btn>
<v-btn v-if="marketMode && extension?.installed" color="teal-accent-4" :text="tm('status.installed')" variant="text" disabled></v-btn>
</v-card-actions>
<v-expand-transition v-if="!marketMode">
@@ -145,7 +149,7 @@ const viewReadme = () => {
<v-card-text style="overflow-y: auto;">
<div class="d-flex align-center mb-4">
<img v-if="extension.logo" :src="extension.logo"
style="height: 50px; width: 50px; border-radius: 8px; margin-right: 16px;" alt="扩展图标" />
style="height: 50px; width: 50px; border-radius: 8px; margin-right: 16px;" :alt="tm('card.alt.extensionIcon')" />
<h3>{{ extension.name }}</h3>
</div>
@@ -159,39 +163,39 @@ const viewReadme = () => {
}">
<v-btn prepend-icon="mdi-cog" color="primary" variant="tonal" @click="configure"
:block="$vuetify.display.xs">
插件配置
{{ tm("card.actions.pluginConfig") }}
</v-btn>
<v-btn prepend-icon="mdi-delete" color="error" variant="tonal" @click="uninstallExtension"
:block="$vuetify.display.xs">
卸载插件
{{ tm("card.actions.uninstallPlugin") }}
</v-btn>
<v-btn prepend-icon="mdi-reload" color="primary" variant="tonal" @click="reloadExtension"
:block="$vuetify.display.xs">
重载插件
{{ tm("card.actions.reloadPlugin") }}
</v-btn>
<v-btn :prepend-icon="extension.activated ? 'mdi-cancel' : 'mdi-check-circle'"
:color="extension.activated ? 'error' : 'success'" variant="tonal" @click="toggleActivation"
:block="$vuetify.display.xs">
{{ extension.activated ? '禁用' : '启用' }}插件
{{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{ tm("card.actions.togglePlugin") }}
</v-btn>
<v-btn prepend-icon="mdi-cogs" color="info" variant="tonal" @click="viewHandlers"
:block="$vuetify.display.xs">
查看行为 ({{ extension.handlers.length }})
{{ tm("card.actions.viewHandlers") }} ({{ extension.handlers.length }})
</v-btn>
<v-btn prepend-icon="mdi-update" color="primary" variant="tonal" :disabled="!extension?.has_update "
@click="updateExtension" :block="$vuetify.display.xs">
更新到 {{ extension.online_version || extension.version }}
{{ tm("card.actions.updateTo") }} {{ extension.online_version || extension.version }}
</v-btn>
</div>
</v-card-text>
<v-card-actions class="pt-0 d-flex justify-center">
<v-btn color="teal-accent-4" text="返回" variant="text" @click="reveal = false"></v-btn>
<v-btn color="teal-accent-4" :text="tm('buttons.back')" variant="text" @click="reveal = false"></v-btn>
</v-card-actions>
</v-card>
</v-expand-transition>
@@ -3,7 +3,7 @@
<v-row v-if="items.length === 0">
<v-col cols="12" class="text-center pa-8">
<v-icon size="64" color="grey-lighten-1">{{ emptyIcon }}</v-icon>
<p class="text-grey mt-4">{{ emptyText }}</p>
<p class="text-grey mt-4">{{ displayEmptyText }}</p>
</v-col>
</v-row>
@@ -24,7 +24,7 @@
@update:model-value="toggleEnabled(item)"
></v-switch>
</template>
<span>{{ getItemEnabled(item) ? '已启用' : '已禁用' }}</span>
<span>{{ getItemEnabled(item) ? t('core.common.itemCard.enabled') : t('core.common.itemCard.disabled') }}</span>
</v-tooltip>
</v-card-title>
@@ -43,7 +43,7 @@
prepend-icon="mdi-delete"
@click="$emit('delete', item)"
>
删除
{{ t('core.common.itemCard.delete') }}
</v-btn>
<v-btn
variant="text"
@@ -52,7 +52,7 @@
prepend-icon="mdi-pencil"
@click="$emit('edit', item)"
>
编辑
{{ t('core.common.itemCard.edit') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -62,8 +62,14 @@
</template>
<script>
import { useI18n } from '@/i18n/composables';
export default {
name: 'ItemCardGrid',
setup() {
const { t } = useI18n();
return { t };
},
props: {
items: {
type: Array,
@@ -83,10 +89,15 @@ export default {
},
emptyText: {
type: String,
default: '暂无数据'
default: null
}
},
emits: ['toggle-enabled', 'delete', 'edit'],
computed: {
displayEmptyText() {
return this.emptyText || this.t('core.common.itemCard.noData');
}
},
methods: {
getItemTitle(item) {
return item[this.titleField];
@@ -0,0 +1,158 @@
<template>
<v-menu offset="12" location="bottom center">
<template v-slot:activator="{ props: activatorProps }">
<v-btn
v-bind="activatorProps"
:variant="(props.variant === 'header' || props.variant === 'chatbox') ? 'flat' : 'text'"
:color="(props.variant === 'header' || props.variant === 'chatbox') ? 'var(--v-theme-surface)' : undefined"
:rounded="(props.variant === 'header' || props.variant === 'chatbox') ? 'sm' : undefined"
icon
size="small"
:class="['language-switcher', `language-switcher--${props.variant}`, (props.variant === 'header' || props.variant === 'chatbox') ? 'action-btn' : '']"
>
<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-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, useLanguageSwitcher } from '@/i18n/composables'
import { useCustomizerStore } from '@/stores/customizer'
import type { Locale } from '@/i18n/types'
// 定义props来控制样式变体
const props = withDefaults(defineProps<{
variant?: 'default' | 'header' | 'chatbox'
}>(), {
variant: 'default'
})
// 使用新的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 = async (langCode: string) => {
await switchLanguage(langCode as Locale)
}
</script>
<style scoped>
.language-flag {
font-size: 16px;
margin-right: 8px;
}
/* 默认变体样式 - 圆形按钮用于登录页 */
.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,不需要额外样式 */
}
/* ChatBox变体样式 - 与Header保持一致 */
.language-switcher--chatbox {
/* 继承action-btn样式,与工具栏主题按钮保持一致 */
}
/* 深色模式下的悬停效果(仅对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>
@@ -37,11 +37,11 @@
</v-list-item>
</v-list>
<div style="display: flex; align-items: center;">
<v-text-field v-model="newItem" label="添加新项,按回车确认添加" @keyup.enter="addItem" clearable dense hide-details
<v-text-field v-model="newItem" :label="t('core.common.list.addItemPlaceholder')" @keyup.enter="addItem" clearable dense hide-details
variant="outlined" density="compact"></v-text-field>
<v-btn @click="addItem" text variant="tonal">
<v-icon>mdi-plus</v-icon>
添加
{{ t('core.common.list.addButton') }}
</v-btn>
</div>
@@ -49,8 +49,14 @@
</template>
<script>
import { useI18n } from '@/i18n/composables';
export default {
name: 'ListConfigItem',
setup() {
const { t } = useI18n();
return { t };
},
props: {
value: {
type: Array,
+53 -4
View File
@@ -5,10 +5,13 @@
<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 || t('core.header.logoTitle'))"
></h2>
<!-- 父子组件传递css变量可能会出错暂时使用十六进制颜色值 -->
<h4 :style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000aa' : '#ffffffcc'}"
class="hint-text">{{ subtitle }}</h4>
class="hint-text">{{ subtitle || t('core.header.accountDialog.title') }}</h4>
</div>
</div>
</div>
@@ -16,14 +19,27 @@
<script setup lang="ts">
import { useCustomizerStore } from "@/stores/customizer";
import { useI18n } from '@/i18n/composables';
const { t } = useI18n();
const props = withDefaults(defineProps<{
title?: string;
subtitle?: string;
}>(), {
title: 'AstrBot 仪表盘',
subtitle: '欢迎使用'
title: '', // 默认为空,组件会使用翻译值
subtitle: ''
})
// 智能格式化标题,在小屏幕上允许在合适位置换行
const formatTitle = (title: string) => {
// 如果标题包含 "AstrBot" 和其他文字,在它们之间添加换行机会
if (title.includes('AstrBot ') || title.includes('AstrBot')) {
// 处理 "AstrBot 仪表盘" 或 "AstrBot Dashboard" 等格式
return title.replace(/(AstrBot)\s+(.+)/, '$1<wbr> $2');
}
return title;
}
</script>
<style scoped>
@@ -40,6 +56,8 @@ const props = withDefaults(defineProps<{
align-items: center;
gap: 20px;
padding: 10px;
max-width: 100%;
overflow: visible;
}
.logo-image {
@@ -60,6 +78,8 @@ const props = withDefaults(defineProps<{
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 0;
flex: 1;
}
.logo-text h2 {
@@ -67,6 +87,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 +103,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>
@@ -1,9 +1,10 @@
<script setup>
import { ref, watch, onMounted } from 'vue';
import { ref, watch, onMounted, computed } from 'vue';
import axios from 'axios';
import { marked } from 'marked';
import hljs from 'highlight.js';
import 'highlight.js/styles/github.css';
import { useI18n } from '@/i18n/composables';
const props = defineProps({
show: {
@@ -22,6 +23,9 @@ const props = defineProps({
const emit = defineEmits(['update:show']);
// 国际化
const { t } = useI18n();
const content = ref(null);
const error = ref(null);
const loading = ref(false);
@@ -54,10 +58,10 @@ async function fetchReadme() {
if (res.data.status === 'ok') {
content.value = res.data.data.content;
} else {
error.value = res.data.message || '获取README失败';
error.value = res.data.message || t('core.common.readme.errors.fetchFailed');
}
} catch (err) {
error.value = err.message || '获取README时发生错误';
error.value = err.message || t('core.common.readme.errors.fetchError');
} finally {
loading.value = false;
}
@@ -99,13 +103,23 @@ function renderMarkdown(content) {
function refreshReadme() {
fetchReadme();
}
// 计算属性处理双向绑定
const _show = computed({
get() {
return props.show;
},
set(value) {
emit('update:show', value);
}
});
</script>
<template>
<v-dialog v-model="_show" width="800" persistent>
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<span class="text-h5">插件说明文档</span>
<span class="text-h5">{{ t('core.common.readme.title') }}</span>
<v-btn icon @click="$emit('update:show', false)">
<v-icon>mdi-close</v-icon>
</v-btn>
@@ -119,21 +133,21 @@ function refreshReadme() {
prepend-icon="mdi-github"
@click="openRepoInNewTab()"
>
GitHub中查看仓库
{{ t('core.common.readme.buttons.viewOnGithub') }}
</v-btn>
<v-btn
color="secondary"
prepend-icon="mdi-refresh"
@click="refreshReadme()"
>
刷新文档
{{ t('core.common.readme.buttons.refresh') }}
</v-btn>
</div>
<!-- 加载中 -->
<div v-if="loading" class="d-flex flex-column align-center justify-center" style="height: 100%;">
<v-progress-circular indeterminate color="primary" size="64" class="mb-4"></v-progress-circular>
<p class="text-body-1 text-center">正在加载README文档...</p>
<p class="text-body-1 text-center">{{ t('core.common.readme.loading') }}</p>
</div>
<!-- 内容显示 -->
@@ -148,14 +162,14 @@ function refreshReadme() {
<!-- 无内容提示 -->
<div v-else class="d-flex flex-column align-center justify-center" style="height: 100%;">
<v-icon size="64" color="warning" class="mb-4">mdi-file-question-outline</v-icon>
<p class="text-body-1 text-center mb-4">该插件未提供文档链接或GitHub仓库地址<br>请查看插件市场或联系插件作者获取更多信息</p>
<p class="text-body-1 text-center mb-4">{{ t('core.common.readme.empty.title') }}<br>{{ t('core.common.readme.empty.subtitle') }}</p>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="tonal" @click="$emit('update:show', false)">
关闭
{{ t('core.common.close') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -1,7 +1,7 @@
<template>
<v-dialog v-model="visible" persistent max-width="400">
<v-card>
<v-card-title>正在等待 AstrBot 重启...</v-card-title>
<v-card-title>{{ t('core.common.restart.waiting') }}</v-card-title>
<v-card-text>
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</v-card-text>
@@ -11,12 +11,16 @@
<script>
import axios from 'axios'
import { useCommonStore } from '@/stores/common';
import { useI18n } from '@/i18n/composables';
export default {
name: 'WaitingForRestart',
setup() {
const { t } = useI18n();
return { t };
},
data() {
return {
visible: false,
@@ -47,7 +51,7 @@ export default {
}, 1000)
} else {
if (this.cnt == 10) {
this.status = '拉取状态达到最大次数,请手动检查。'
this.status = this.t('core.common.restart.maxRetriesReached')
}
this.cnt = 0
setTimeout(() => {
+165
View File
@@ -0,0 +1,165 @@
import { ref, computed } from 'vue';
import { translations as staticTranslations } from './translations';
import type { Locale } from './types';
// 全局状态
const currentLocale = ref<Locale>('zh-CN');
const translations = ref<Record<string, any>>({});
/**
* 初始化i18n系统
*/
export async function initI18n(locale: Locale = 'zh-CN') {
currentLocale.value = locale;
// 加载静态翻译数据
loadTranslations(locale);
}
/**
* 加载翻译数据(现在从静态导入获取)
*/
function loadTranslations(locale: Locale) {
try {
const data = staticTranslations[locale];
if (data) {
translations.value = data;
} else {
console.warn(`Translations not found for locale: ${locale}`);
// 回退到中文
if (locale !== 'zh-CN') {
console.log('Falling back to zh-CN');
translations.value = staticTranslations['zh-CN'];
}
}
} catch (error) {
console.error(`Failed to load translations for ${locale}:`, error);
// 回退到中文
if (locale !== 'zh-CN') {
console.log('Falling back to zh-CN');
translations.value = staticTranslations['zh-CN'];
}
}
}
/**
* 主要的翻译函数组合
*/
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 `[MISSING: ${key}]`;
}
}
if (typeof value !== 'string') {
console.warn(`Translation value is not string: ${key}`, value);
// 返回带括号的键名,便于在开发时识别类型错误的翻译
return `[INVALID: ${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;
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);
}
+293
View File
@@ -0,0 +1,293 @@
/**
* 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 {
// 使用动态import加载JSON文件,兼容构建和开发环境
const modulePath = `../locales/${locale}/${moduleInfo.path}`;
const module = await import(/* @vite-ignore */ modulePath);
const data = module.default || module;
// 缓存结果
this.cache.set(cacheKey, data);
// 更新模块信息
moduleInfo.loaded = true;
moduleInfo.data = data;
return data;
} catch (error) {
console.error(`加载模块 ${moduleName} 失败:`, error);
// 回退方案:尝试使用fetch(开发环境)
try {
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 (fetchError) {
console.error(`回退fetch加载也失败:`, fetchError);
return {};
}
}
}
/**
* 通用模块加载器 - 减少重复代码,提高可维护性
*/
private async loadModules(
locale: string,
prefix: string,
overrideList: string[] = []
): Promise<any> {
// 使用覆盖列表或从注册表中筛选符合前缀的模块名
const moduleNames = overrideList.length > 0
? overrideList
: Array.from(this.moduleRegistry.keys()).filter(key => key.startsWith(prefix));
const results = await Promise.all(
moduleNames.map(module => this.loadModule(locale, module))
);
return this.mergeModules(results, moduleNames);
}
/**
* 加载核心模块(最高优先级)
*/
async loadCoreModules(locale: string): Promise<any> {
return this.loadModules(locale, 'core');
}
/**
* 加载功能模块
*/
async loadFeatureModules(locale: string, features?: string[]): Promise<any> {
return this.loadModules(locale, 'features', features || []);
}
/**
* 加载消息模块
*/
async loadMessageModules(locale: string): Promise<any> {
return this.loadModules(locale, 'messages');
}
/**
* 加载所有模块
*/
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 = {};
const pathRegistry = new Map<string, string>();
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];
const fullPath = nameParts.join('.');
if (current[finalKey] && pathRegistry.has(fullPath)) {
const existingModule = pathRegistry.get(fullPath);
console.warn(`⚠️ I18n模块路径冲突: "${fullPath}" 已被模块 "${existingModule}" 占用,模块 "${moduleName}" 可能会覆盖部分键值`);
}
// 记录路径和模块名的映射
pathRegistry.set(fullPath, moduleName);
// 设置最终值(保持原有的浅合并行为)
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);
}
}
@@ -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,76 @@
{
"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",
"locale": "en-US",
"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"
},
"restart": {
"waiting": "Waiting for AstrBot to restart...",
"maxRetriesReached": "Maximum retry attempts reached, please check manually."
},
"readme": {
"title": "Extension Documentation",
"buttons": {
"viewOnGithub": "View Repository on GitHub",
"refresh": "Refresh Documentation"
},
"loading": "Loading README documentation...",
"errors": {
"fetchFailed": "Failed to fetch README",
"fetchError": "Error occurred while fetching README"
},
"empty": {
"title": "This extension does not provide documentation link or GitHub repository address.",
"subtitle": "Please check the extension marketplace or contact the extension author for more information."
}
},
"editor": {
"fullscreen": "Fullscreen Edit",
"editingTitle": "Editing Content"
},
"list": {
"addItemPlaceholder": "Add new item, press Enter to confirm",
"addButton": "Add"
},
"itemCard": {
"enabled": "Enabled",
"disabled": "Disabled",
"delete": "Delete",
"edit": "Edit",
"noData": "No data available"
}
}
@@ -0,0 +1,85 @@
{
"logoTitle": "AstrBot Dashboard",
"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,44 @@
{
"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"
},
"sigma": {
"subtitle": "AstrBot Experimental Project",
"visualization": "Visualization",
"filterUserId": "Filter User ID",
"filter": "Filter",
"resetFilter": "Reset Filter",
"refreshGraph": "Refresh Graph",
"nodeDetails": "Node Details",
"id": "ID",
"type": "Type",
"name": "Name",
"userId": "User ID",
"timestamp": "Timestamp",
"graphStats": "Graph Statistics",
"nodeCount": "Node Count",
"edgeCount": "Edge Count",
"inDevelopment": "Under Development"
}
}
@@ -0,0 +1,136 @@
{
"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"
},
"notInstalled": {
"title": "Knowledge base plugin is not installed yet",
"install": "Install now"
},
"empty": {
"title": "No knowledge base yet, create one now! 🙂",
"create": "Create Knowledge Base"
},
"list": {
"title": "Knowledge Base List",
"create": "Create Knowledge Base",
"config": "Configure",
"knowledgeCount": "knowledge items",
"tips": "Tips: Learn how to use through /kb command in chat page!"
},
"createDialog": {
"title": "Create New Knowledge Base",
"nameLabel": "Knowledge Base Name",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Brief description of the knowledge base...",
"embeddingModelLabel": "Embedding Model",
"providerInfo": "Provider ID: {id} | Embedding Model Dimensions: {dimensions}",
"tips": "Tips: Once you choose an embedding model for a knowledge base, please do not modify the provider's model or vector dimension information, otherwise it will seriously affect the recall rate of the knowledge base or even cause errors.",
"cancel": "Cancel",
"create": "Create"
},
"emojiPicker": {
"title": "Select Emoji",
"close": "Close",
"categories": {
"emotions": "Smileys and Emotions",
"animals": "Animals and Nature",
"food": "Food and Drink",
"activities": "Activities and Objects",
"travel": "Travel and Places",
"symbols": "Symbols and Flags"
}
},
"contentDialog": {
"title": "Knowledge Base Management",
"embeddingModel": "Embedding Model",
"vectorDimension": "Vector Dimension",
"usage": "Usage: Enter \"/kb use {name}\" in the chat page",
"tabs": {
"upload": "Upload Files",
"search": "Search Content"
}
},
"upload": {
"title": "Upload Files to Knowledge Base",
"subtitle": "Supports txt, pdf, word, excel and other formats",
"dropzone": "Drag and drop files here or click to upload",
"chunkSettings": {
"title": "Chunk Settings",
"tooltip": "Chunk size determines the size of each text block, overlap length determines the overlap between adjacent text blocks.\nSmaller chunks are more precise but increase quantity, appropriate overlap can improve retrieval accuracy.",
"chunkSizeLabel": "Chunk Size",
"chunkSizeHint": "Control the size of each text block, leave empty to use default value",
"overlapLabel": "Overlap Length",
"overlapHint": "Control the overlap between adjacent text blocks, leave empty to use default value"
},
"upload": "Upload File",
"uploading": "Uploading..."
},
"search": {
"queryLabel": "Search Knowledge Base Content",
"queryPlaceholder": "Enter keywords to search knowledge base content...",
"resultCountLabel": "Result Count",
"searching": "Searching...",
"resultsTitle": "Search Results",
"relevance": "Relevance",
"noResults": "No matching content found"
},
"deleteDialog": {
"title": "Confirm Delete",
"confirmText": "Are you sure you want to delete knowledge base {name}?",
"warning": "This operation is irreversible, all knowledge base content will be permanently deleted.",
"cancel": "Cancel",
"delete": "Delete"
},
"messages": {
"pluginNotAvailable": "Plugin not installed or unavailable",
"checkPluginFailed": "Failed to check plugin",
"installFailed": "Installation failed",
"installPluginFailed": "Failed to install plugin",
"getKnowledgeBaseListFailed": "Failed to get knowledge base list",
"knowledgeBaseCreated": "Knowledge base created successfully",
"createFailed": "Creation failed",
"createKnowledgeBaseFailed": "Failed to create knowledge base",
"pleaseEnterKnowledgeBaseName": "Please enter knowledge base name",
"pleaseSelectFile": "Please select a file first",
"operationSuccess": "Operation successful: {message}",
"uploadFailed": "Upload failed",
"fileUploadFailed": "File upload failed",
"pleaseEnterSearchContent": "Please enter search content",
"noMatchingContent": "No matching content found",
"searchFailed": "Search failed",
"searchKnowledgeBaseFailed": "Failed to search knowledge base",
"deleteTargetNotExists": "Delete target does not exist",
"knowledgeBaseDeleted": "Knowledge base deleted successfully",
"deleteFailed": "Deletion failed",
"deleteKnowledgeBaseFailed": "Failed to delete knowledge base",
"getEmbeddingModelListFailed": "Failed to get embedding model list"
}
}
@@ -0,0 +1,97 @@
{
"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",
"title": "Filters",
"userIdLabel": "Filter by User ID",
"filterButton": "Filter",
"resetButton": "Reset Filter",
"refreshButton": "Refresh Graph"
},
"search": {
"title": "Search Memory",
"userIdLabel": "User ID",
"queryLabel": "Enter keywords",
"searchButton": "Search",
"resultsTitle": "Search Results",
"noResults": "No relevant memory content found",
"similarity": "Relevance",
"noTextContent": "No text content"
},
"addMemory": {
"title": "Add Memory Data",
"textLabel": "Enter text content",
"userIdLabel": "User ID",
"summarizeLabel": "Need summary",
"addButton": "Add Data"
},
"nodeDetails": {
"title": "Node Details",
"id": "ID",
"type": "Type",
"name": "Name",
"userId": "User ID",
"timestamp": "Timestamp"
},
"graphStats": {
"title": "Graph Statistics",
"nodeCount": "Node Count",
"edgeCount": "Edge Count"
},
"factDialog": {
"title": "Memory Fact",
"id": "ID",
"docId": "Document ID",
"createdAt": "Created At",
"updatedAt": "Updated At",
"metadata": "Metadata",
"metadataKey": "Key",
"metadataValue": "Value",
"loading": "Loading...",
"close": "Close",
"noValue": "None",
"unknown": "Unknown"
},
"messages": {
"searchQueryRequired": "Please enter search keywords",
"searchSuccess": "Found {count} relevant memories",
"searchNoResults": "No relevant memory content found",
"searchError": "Search failed",
"addSuccess": "Memory data added successfully!",
"addError": "Failed to add memory data",
"factDetailsError": "Failed to get memory details",
"metadataParseError": "Unable to parse metadata",
"relationNoMemoryData": "This relation has no associated memory data"
}
}
@@ -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,75 @@
{
"title": "Let's Chat!",
"subtitle": "Chat with AI Assistant",
"input": {
"placeholder": "Start typing...",
"send": "Send",
"clear": "Clear",
"upload": "Upload File",
"voice": "Voice Input",
"recordingPrompt": "Recording, please speak...",
"chatPrompt": "Let's chat!"
},
"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"
},
"connection": {
"title": "Connection Status Notice",
"message": "The system detected that the chat connection needs to be re-established.",
"reasons": "This may be due to:",
"reasonWindowResize": "Switching chat window size (normal behavior)",
"reasonMultipleTabs": "Opening chat pages in other tabs",
"reasonNetworkIssue": "Temporary network interruption",
"notice": "Note: To ensure proper message delivery, the system only allows one active chat connection at a time. If you're using chat in multiple tabs, please keep only one page open.",
"understand": "Got it",
"status": {
"reconnecting": "Reconnecting...",
"reconnected": "Chat connection re-established",
"failed": "Connection failed, please refresh the page"
}
}
}
@@ -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,64 @@
{
"title": "Dashboard",
"subtitle": "Real-time monitoring and statistics",
"lastUpdate": "Last updated",
"status": {
"loading": "Loading...",
"dataError": "Failed to fetch data",
"noticeError": "Failed to fetch notice",
"online": "Online",
"uptime": "Uptime",
"memoryUsage": "Memory Usage"
},
"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,160 @@
{
"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",
"actions": "Actions",
"back": "Back"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled",
"system": "System",
"loading": "Loading...",
"installed": "Installed",
"unknown": "Unknown"
},
"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"
},
"uninstall": {
"title": "Confirm Deletion",
"message": "Are you sure you want to delete this extension?"
}
},
"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"
},
"card": {
"actions": {
"pluginConfig": "Extension Config",
"uninstallPlugin": "Uninstall Extension",
"reloadPlugin": "Reload Extension",
"togglePlugin": "Extension",
"viewHandlers": "View Handlers",
"updateTo": "Update to"
},
"status": {
"hasUpdate": "New version available",
"disabled": "This extension is disabled",
"handlersCount": " handlers"
},
"alt": {
"logo": "logo",
"extensionIcon": "extension icon"
},
"errors": {
"confirmNotRegistered": "$confirm not properly registered"
}
}
}
@@ -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"
}
@@ -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,76 @@
{
"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": "语言",
"locale": "zh-CN",
"type": "输入",
"press": "按",
"longPress": "长按",
"yes": "是",
"no": "否",
"dialog": {
"confirmTitle": "确认操作",
"confirmMessage": "你确定要执行此操作吗?",
"confirmButton": "确定",
"cancelButton": "取消"
},
"restart": {
"waiting": "正在等待 AstrBot 重启...",
"maxRetriesReached": "拉取状态达到最大次数,请手动检查。"
},
"readme": {
"title": "插件说明文档",
"buttons": {
"viewOnGithub": "在GitHub中查看仓库",
"refresh": "刷新文档"
},
"loading": "正在加载README文档...",
"errors": {
"fetchFailed": "获取README失败",
"fetchError": "获取README时发生错误"
},
"empty": {
"title": "该插件未提供文档链接或GitHub仓库地址。",
"subtitle": "请查看插件市场或联系插件作者获取更多信息。"
}
},
"editor": {
"fullscreen": "全屏编辑",
"editingTitle": "编辑内容"
},
"list": {
"addItemPlaceholder": "添加新项,按回车确认添加",
"addButton": "添加"
},
"itemCard": {
"enabled": "已启用",
"disabled": "已禁用",
"delete": "删除",
"edit": "编辑",
"noData": "暂无数据"
}
}
@@ -0,0 +1,85 @@
{
"logoTitle": "AstrBot 仪表盘",
"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,44 @@
{
"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": "已弃用"
},
"sigma": {
"subtitle": "AstrBot 实验性项目",
"visualization": "可视化",
"filterUserId": "筛选用户 ID",
"filter": "筛选",
"resetFilter": "重置筛选",
"refreshGraph": "刷新图形",
"nodeDetails": "节点详情",
"id": "ID",
"type": "类型",
"name": "名称",
"userId": "用户ID",
"timestamp": "时间戳",
"graphStats": "图形统计",
"nodeCount": "节点数",
"edgeCount": "边数",
"inDevelopment": "功能开发中"
}
}
@@ -0,0 +1,136 @@
{
"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": "重新索引"
},
"notInstalled": {
"title": "还没有安装知识库插件",
"install": "立即安装"
},
"empty": {
"title": "还没有知识库,快创建一个吧!🙂",
"create": "创建知识库"
},
"list": {
"title": "知识库列表",
"create": "创建知识库",
"config": "配置",
"knowledgeCount": "条知识",
"tips": "Tips: 在聊天页面通过 /kb 指令了解如何使用!"
},
"createDialog": {
"title": "创建新知识库",
"nameLabel": "知识库名称",
"descriptionLabel": "描述",
"descriptionPlaceholder": "知识库的简短描述...",
"embeddingModelLabel": "Embedding(嵌入)模型",
"providerInfo": "提供商 ID: {id} | 嵌入模型维度: {dimensions}",
"tips": "Tips: 一旦选择了一个知识库的嵌入模型,请不要再修改该提供商的模型或者向量维度信息,否则将严重影响该知识库的召回率甚至报错。",
"cancel": "取消",
"create": "创建"
},
"emojiPicker": {
"title": "选择表情",
"close": "关闭",
"categories": {
"emotions": "笑脸和情感",
"animals": "动物和自然",
"food": "食物和饮料",
"activities": "活动和物品",
"travel": "旅行和地点",
"symbols": "符号和旗帜"
}
},
"contentDialog": {
"title": "知识库管理",
"embeddingModel": "嵌入模型",
"vectorDimension": "向量维度",
"usage": "使用方式: 在聊天页中输入 \"/kb use {name}\"",
"tabs": {
"upload": "上传文件",
"search": "搜索内容"
}
},
"upload": {
"title": "上传文件到知识库",
"subtitle": "支持 txt、pdf、word、excel 等多种格式",
"dropzone": "拖放文件到这里或点击上传",
"chunkSettings": {
"title": "分片设置",
"tooltip": "分片长度决定每块文本的大小,重叠长度决定相邻文本块之间的重叠程度。\n较小的分片更精确但会增加数量,适当的重叠可提高检索准确性。",
"chunkSizeLabel": "分片长度",
"chunkSizeHint": "控制每个文本块大小,留空使用默认值",
"overlapLabel": "重叠长度",
"overlapHint": "控制相邻文本块重叠度,留空使用默认值"
},
"upload": "上传文件",
"uploading": "正在上传..."
},
"search": {
"queryLabel": "搜索知识库内容",
"queryPlaceholder": "输入关键词搜索知识库内容...",
"resultCountLabel": "结果数量",
"searching": "正在搜索...",
"resultsTitle": "搜索结果",
"relevance": "相关度",
"noResults": "没有找到匹配的内容"
},
"deleteDialog": {
"title": "确认删除",
"confirmText": "您确定要删除知识库 {name} 吗?",
"warning": "此操作不可逆,所有知识库内容将被永久删除。",
"cancel": "取消",
"delete": "删除"
},
"messages": {
"pluginNotAvailable": "插件未安装或不可用",
"checkPluginFailed": "检查插件失败",
"installFailed": "安装失败",
"installPluginFailed": "安装插件失败",
"getKnowledgeBaseListFailed": "获取知识库列表失败",
"knowledgeBaseCreated": "知识库创建成功",
"createFailed": "创建失败",
"createKnowledgeBaseFailed": "创建知识库失败",
"pleaseEnterKnowledgeBaseName": "请输入知识库名称",
"pleaseSelectFile": "请先选择文件",
"operationSuccess": "操作成功: {message}",
"uploadFailed": "上传失败",
"fileUploadFailed": "文件上传失败",
"pleaseEnterSearchContent": "请输入搜索内容",
"noMatchingContent": "没有找到匹配的内容",
"searchFailed": "搜索失败",
"searchKnowledgeBaseFailed": "搜索知识库失败",
"deleteTargetNotExists": "删除目标不存在",
"knowledgeBaseDeleted": "知识库删除成功",
"deleteFailed": "删除失败",
"deleteKnowledgeBaseFailed": "删除知识库失败",
"getEmbeddingModelListFailed": "获取嵌入模型列表失败"
}
}
@@ -0,0 +1,97 @@
{
"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": "按时间范围",
"title": "筛选",
"userIdLabel": "筛选用户 ID",
"filterButton": "筛选",
"resetButton": "重置筛选",
"refreshButton": "刷新图形"
},
"search": {
"title": "搜索记忆",
"userIdLabel": "用户 ID",
"queryLabel": "输入关键词",
"searchButton": "搜索",
"resultsTitle": "搜索结果",
"noResults": "未找到相关记忆内容",
"similarity": "相关度",
"noTextContent": "无文本内容"
},
"addMemory": {
"title": "添加记忆数据",
"textLabel": "输入文本内容",
"userIdLabel": "用户 ID",
"summarizeLabel": "需要摘要",
"addButton": "添加数据"
},
"nodeDetails": {
"title": "节点详情",
"id": "ID",
"type": "类型",
"name": "名称",
"userId": "用户ID",
"timestamp": "时间戳"
},
"graphStats": {
"title": "图形统计",
"nodeCount": "节点数",
"edgeCount": "边数"
},
"factDialog": {
"title": "记忆事实",
"id": "ID",
"docId": "文档ID",
"createdAt": "创建时间",
"updatedAt": "更新时间",
"metadata": "元数据",
"metadataKey": "键",
"metadataValue": "值",
"loading": "加载中...",
"close": "关闭",
"noValue": "无",
"unknown": "未知"
},
"messages": {
"searchQueryRequired": "请输入搜索关键词",
"searchSuccess": "找到 {count} 条相关记忆",
"searchNoResults": "未找到相关记忆内容",
"searchError": "搜索失败",
"addSuccess": "记忆数据添加成功!",
"addError": "添加记忆数据失败",
"factDetailsError": "获取记忆详情失败",
"metadataParseError": "无法解析元数据",
"relationNoMemoryData": "该关系没有关联的记忆数据"
}
}
@@ -0,0 +1,13 @@
{
"login": "登录",
"username": "用户名",
"password": "密码",
"logo": {
"title": "AstrBot 仪表盘",
"subtitle": "欢迎使用"
},
"theme": {
"switchToDark": "切换到深色主题",
"switchToLight": "切换到浅色主题"
}
}
@@ -0,0 +1,4 @@
{
"messageCount": "消息条数",
"time": "时间"
}
@@ -0,0 +1,75 @@
{
"title": "聊天吧!",
"subtitle": "与AI助手进行对话",
"input": {
"placeholder": "开始输入...",
"send": "发送",
"clear": "清空",
"upload": "上传文件",
"voice": "语音输入",
"recordingPrompt": "录音中,请说话...",
"chatPrompt": "聊天吧!"
},
"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": "粘贴图片"
},
"connection": {
"title": "连接状态提醒",
"message": "系统检测到聊天连接需要重新建立。",
"reasons": "这可能是因为:",
"reasonWindowResize": "切换了聊天窗口大小(正常现象)",
"reasonMultipleTabs": "在其他标签页中打开了聊天页面",
"reasonNetworkIssue": "网络连接临时中断",
"notice": "注意:为了确保消息正确接收,系统只允许同时保持一个聊天连接。如果您在多个标签页中使用聊天功能,建议只保留一个页面。",
"understand": "我知道了",
"status": {
"reconnecting": "正在重新连接...",
"reconnected": "聊天连接已重新建立",
"failed": "连接失败,请刷新页面重试"
}
}
}
@@ -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,64 @@
{
"title": "控制台",
"subtitle": "实时监控和统计数据",
"lastUpdate": "最后更新",
"status": {
"loading": "加载中...",
"dataError": "获取数据失败",
"noticeError": "获取公告失败",
"online": "在线",
"uptime": "运行时间",
"memoryUsage": "内存占用"
},
"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,160 @@
{
"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": "取消",
"actions": "操作",
"back": "返回"
},
"status": {
"enabled": "启用",
"disabled": "禁用",
"system": "系统",
"loading": "加载中...",
"installed": "已安装",
"unknown": "未知"
},
"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": "日志"
},
"uninstall": {
"title": "删除确认",
"message": "你确定要删除当前插件吗?"
}
},
"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": "输入插件仓库链接"
},
"card": {
"actions": {
"pluginConfig": "插件配置",
"uninstallPlugin": "卸载插件",
"reloadPlugin": "重载插件",
"togglePlugin": "插件",
"viewHandlers": "查看行为",
"updateTo": "更新到"
},
"status": {
"hasUpdate": "有新版本可用",
"disabled": "该插件已经被禁用",
"handlersCount": "个行为"
},
"alt": {
"logo": "logo",
"extensionIcon": "扩展图标"
},
"errors": {
"confirmNotRegistered": "$confirm 未正确注册"
}
}
}
@@ -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;
}
+132
View File
@@ -0,0 +1,132 @@
// 静态导入所有翻译文件
// 这种方式确保构建时所有翻译都会被正确打包
// 中文翻译
import zhCNCommon from './locales/zh-CN/core/common.json';
import zhCNActions from './locales/zh-CN/core/actions.json';
import zhCNStatus from './locales/zh-CN/core/status.json';
import zhCNNavigation from './locales/zh-CN/core/navigation.json';
import zhCNHeader from './locales/zh-CN/core/header.json';
import zhCNChat from './locales/zh-CN/features/chat.json';
import zhCNExtension from './locales/zh-CN/features/extension.json';
import zhCNConversation from './locales/zh-CN/features/conversation.json';
import zhCNToolUse from './locales/zh-CN/features/tool-use.json';
import zhCNProvider from './locales/zh-CN/features/provider.json';
import zhCNPlatform from './locales/zh-CN/features/platform.json';
import zhCNConfig from './locales/zh-CN/features/config.json';
import zhCNConsole from './locales/zh-CN/features/console.json';
import zhCNAbout from './locales/zh-CN/features/about.json';
import zhCNSettings from './locales/zh-CN/features/settings.json';
import zhCNAuth from './locales/zh-CN/features/auth.json';
import zhCNChart from './locales/zh-CN/features/chart.json';
import zhCNDashboard from './locales/zh-CN/features/dashboard.json';
import zhCNAlkaidIndex from './locales/zh-CN/features/alkaid/index.json';
import zhCNAlkaidKnowledgeBase from './locales/zh-CN/features/alkaid/knowledge-base.json';
import zhCNAlkaidMemory from './locales/zh-CN/features/alkaid/memory.json';
import zhCNErrors from './locales/zh-CN/messages/errors.json';
import zhCNSuccess from './locales/zh-CN/messages/success.json';
import zhCNValidation from './locales/zh-CN/messages/validation.json';
// 英文翻译
import enUSCommon from './locales/en-US/core/common.json';
import enUSActions from './locales/en-US/core/actions.json';
import enUSStatus from './locales/en-US/core/status.json';
import enUSNavigation from './locales/en-US/core/navigation.json';
import enUSHeader from './locales/en-US/core/header.json';
import enUSChat from './locales/en-US/features/chat.json';
import enUSExtension from './locales/en-US/features/extension.json';
import enUSConversation from './locales/en-US/features/conversation.json';
import enUSToolUse from './locales/en-US/features/tool-use.json';
import enUSProvider from './locales/en-US/features/provider.json';
import enUSPlatform from './locales/en-US/features/platform.json';
import enUSConfig from './locales/en-US/features/config.json';
import enUSConsole from './locales/en-US/features/console.json';
import enUSAbout from './locales/en-US/features/about.json';
import enUSSettings from './locales/en-US/features/settings.json';
import enUSAuth from './locales/en-US/features/auth.json';
import enUSChart from './locales/en-US/features/chart.json';
import enUSDashboard from './locales/en-US/features/dashboard.json';
import enUSAlkaidIndex from './locales/en-US/features/alkaid/index.json';
import enUSAlkaidKnowledgeBase from './locales/en-US/features/alkaid/knowledge-base.json';
import enUSAlkaidMemory from './locales/en-US/features/alkaid/memory.json';
import enUSErrors from './locales/en-US/messages/errors.json';
import enUSSuccess from './locales/en-US/messages/success.json';
import enUSValidation from './locales/en-US/messages/validation.json';
// 组装翻译对象
export const translations = {
'zh-CN': {
core: {
common: zhCNCommon,
actions: zhCNActions,
status: zhCNStatus,
navigation: zhCNNavigation,
header: zhCNHeader
},
features: {
chat: zhCNChat,
extension: zhCNExtension,
conversation: zhCNConversation,
tooluse: zhCNToolUse,
provider: zhCNProvider,
platform: zhCNPlatform,
config: zhCNConfig,
console: zhCNConsole,
about: zhCNAbout,
settings: zhCNSettings,
auth: zhCNAuth,
chart: zhCNChart,
dashboard: zhCNDashboard,
alkaid: {
index: zhCNAlkaidIndex,
'knowledge-base': zhCNAlkaidKnowledgeBase,
memory: zhCNAlkaidMemory
}
},
messages: {
errors: zhCNErrors,
success: zhCNSuccess,
validation: zhCNValidation
}
},
'en-US': {
core: {
common: enUSCommon,
actions: enUSActions,
status: enUSStatus,
navigation: enUSNavigation,
header: enUSHeader
},
features: {
chat: enUSChat,
extension: enUSExtension,
conversation: enUSConversation,
tooluse: enUSToolUse,
provider: enUSProvider,
platform: enUSPlatform,
config: enUSConfig,
console: enUSConsole,
about: enUSAbout,
settings: enUSSettings,
auth: enUSAuth,
chart: enUSChart,
dashboard: enUSDashboard,
alkaid: {
index: enUSAlkaidIndex,
'knowledge-base': enUSAlkaidKnowledgeBase,
memory: enUSAlkaidMemory
}
},
messages: {
errors: enUSErrors,
success: enUSSuccess,
validation: enUSValidation
}
}
};
export type TranslationData = typeof translations;
+129
View File
@@ -0,0 +1,129 @@
/**
* I18n TypeScript Type Definitions - Auto-generated from JSON
* 国际化类型定义,从JSON文件自动推断,确保类型安全且自动同步
*/
// 直接导入已经组织好的翻译数据
import { translations } from './translations';
// 导出翻译数据常量,供类型推断使用
export const translationData = translations;
// 从实际的翻译数据推断完整的翻译结构类型
export type TranslationSchema = typeof translations[keyof typeof translations];
// TypeScript 助手:递归提取嵌套键路径
type NestedKeyOf<T> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? `${K}` | `${K}.${NestedKeyOf<T[K]>}`
: `${K}`
}[keyof T & string]
: never;
// 自动推断的翻译键联合类型 - 包含所有有效的点分隔键路径
export type TranslationKey = NestedKeyOf<TranslationSchema>;
// 语言环境类型 - 从实际的翻译数据键推断
export type Locale = keyof typeof translations;
// 翻译函数类型
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;
};
}
// Vue I18n 模块增强 - 为了避免编译时的模块查找问题,暂时注释掉
// 这些类型定义在运行时仍然有效,但不会在编译时产生错误
/*
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$t: (key: TranslationKey, params?: Record<string, string | number>) => string;
}
}
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
};
}
}
@@ -3,12 +3,15 @@ import {ref, computed} from 'vue';
import {useCustomizerStore} from '@/stores/customizer';
import axios from 'axios';
import Logo from '@/components/shared/Logo.vue';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
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);
@@ -31,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);
@@ -103,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 = '';
})
@@ -132,21 +135,21 @@ 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;
}
dashboardHasNewVersion.value = res.data.data.dashboard_has_new_version;
})
.catch((err) => {
if (err.response.status == 401) {
if (err.response && err.response.status == 401) {
console.log("401");
const authStore = useAuthStore();
authStore.logout();
@@ -191,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,
@@ -214,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;
@@ -273,13 +276,16 @@ 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 variant="header" />
<!-- 主题切换按钮 -->
<v-btn size="small" @click="toggleDarkMode();" class="action-btn"
color="var(--v-theme-surface)" variant="flat" rounded="sm">
@@ -293,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>
@@ -318,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">
@@ -335,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>
@@ -363,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>
@@ -376,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>
@@ -419,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>
@@ -430,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="t('core.header.logoTitle')" :subtitle="t('core.header.accountDialog.title')"></logo>
</div>
<v-alert
v-if="accountWarning"
@@ -445,7 +451,7 @@ commonStore.getStartTime();
border="start"
class="mb-4"
>
<strong>安全提醒:</strong> 请修改默认密码以确保账户安全
<strong>{{ t('core.header.accountDialog.securityWarning') }}</strong>
</v-alert>
<v-alert
@@ -473,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
@@ -488,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>
@@ -502,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>
@@ -528,7 +534,7 @@ commonStore.getStartTime();
@click="dialog = false"
:disabled="accountEditStatus.loading"
>
取消
{{ t('core.header.accountDialog.actions.cancel') }}
</v-btn>
<v-btn
color="primary"
@@ -537,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;">{{ 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);
@@ -167,14 +170,14 @@ function endDrag() {
</template>
</v-list>
<div class="sidebar-footer" v-if="!customizer.mini_sidebar">
<v-btn style="margin-bottom: 8px;" size="small" variant="primary" to="/settings">
🔧 设置
<v-btn style="margin-bottom: 8px;" size="small" variant="tonal" color="primary" to="/settings">
🔧 {{ t('core.navigation.settings') }}
</v-btn>
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="toggleIframe">
官方文档
{{ t('core.navigation.documentation') }}
</v-btn>
<v-btn style="margin-bottom: 8px;" size="small" variant="plain" @click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
GitHub
{{ t('core.navigation.github') }}
</v-btn>
</div>
</div>
@@ -189,13 +192,13 @@ 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;">拖拽</span>
<span style="margin-left: 8px;">{{ t('core.navigation.drag') }}</span>
</div>
<div style="display: flex; gap: 8px;">
<!-- 跳转按钮 -->
<v-btn
icon
@click.stop="openIframeLink"
@click.stop="openIframeLink('https://astrbot.app')"
@mousedown.stop
style="border-radius: 8px; border: 1px solid #ccc;"
>
@@ -218,4 +221,4 @@ function endDrag() {
style="width: 100%; height: calc(100% - 56px); border: none; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;"
></iframe>
</div>
</template>
</template>
@@ -14,59 +14,62 @@ export interface menu {
subCaption?: string;
}
// 注意:这个文件现在包含i18n键值而不是直接的文本
// 在组件中使用时需要通过t()函数进行翻译
// 所有键名都使用 core.navigation.* 格式
const sidebarItem: menu[] = [
{
title: '统计',
title: 'core.navigation.dashboard',
icon: 'mdi-view-dashboard',
to: '/dashboard/default'
},
{
title: '消息平台',
title: 'core.navigation.platforms',
icon: 'mdi-message-processing',
to: '/platforms',
},
{
title: '服务提供商',
title: 'core.navigation.providers',
icon: 'mdi-creation',
to: '/providers',
},
{
title: 'MCP',
title: 'core.navigation.toolUse',
icon: 'mdi-function-variant',
to: '/tool-use'
},
{
title: '配置文件',
title: 'core.navigation.config',
icon: 'mdi-cog',
to: '/config',
},
{
title: '插件',
title: 'core.navigation.extension',
icon: 'mdi-puzzle',
to: '/extension'
},
{
title: '聊天',
title: 'core.navigation.chat',
icon: 'mdi-chat',
to: '/chat'
},
{
title: '对话数据',
title: 'core.navigation.conversation',
icon: 'mdi-database',
to: '/conversation'
},
{
title: '控制台',
title: 'core.navigation.console',
icon: 'mdi-console',
to: '/console'
},
{
title: 'Alkaid',
title: 'core.navigation.alkaid',
icon: 'mdi-test-tube',
to: '/alkaid'
},
{
title: '关于',
title: 'core.navigation.about',
icon: 'mdi-information',
to: '/about'
},
+26 -8
View File
@@ -4,6 +4,7 @@ import App from './App.vue';
import { router } from './router';
import vuetify from './plugins/vuetify';
import confirmPlugin from './plugins/confirmPlugin';
import { setupI18n } from './i18n/composables';
import '@/scss/style.scss';
import VueApexCharts from 'vue3-apexcharts';
@@ -11,14 +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(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) => {
+3 -3
View File
@@ -1,11 +1,11 @@
import { createRouter, createWebHistory } from 'vue-router';
import { createRouter, createWebHashHistory } from 'vue-router';
import MainRoutes from './MainRoutes';
import AuthRoutes from './AuthRoutes';
import ChatBoxRoutes from './ChatBoxRoutes';
import { useAuthStore } from '@/stores/auth';
export const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
MainRoutes,
AuthRoutes,
@@ -26,7 +26,7 @@ router.beforeEach(async (to, from, next) => {
const authRequired = !publicPages.includes(to.path);
const auth: AuthStore = useAuthStore();
// 如果用户已登录且试图访问登录页面,则重定向到首页或之前尝试访问的页面
// 如果用户已登录且试图访问登录页面,则重定向到首页
if (to.path === '/auth/login' && auth.has_token()) {
return next(auth.returnUrl || '/');
}
@@ -0,0 +1,141 @@
/* 自定义滚动条样式 - 紫色主题 */
/* 全局滚动条样式 */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: rgba(160, 60, 254, 0.75);
border-radius: 5px;
transition: all 0.3s ease;
border: 1px solid rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(147, 51, 234, 0.85);
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(147, 51, 234, 0.3);
}
::-webkit-scrollbar-thumb:active {
background: rgba(147, 51, 234, 0.95);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* 深色主题滚动条样式 */
.v-theme--PurpleThemeDark {
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(192, 132, 252, 0.75);
border: 1px solid rgba(0, 0, 0, 0.2);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(192, 132, 252, 0.85);
box-shadow: 0 2px 8px rgba(192, 132, 252, 0.4);
}
::-webkit-scrollbar-thumb:active {
background: rgba(192, 132, 252, 0.95);
}
}
/* 细滚动条变体 */
.thin-scrollbar {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background: rgba(147, 51, 234, 0.75);
border: none;
}
}
.v-theme--PurpleThemeDark .thin-scrollbar {
::-webkit-scrollbar-thumb {
background: rgba(192, 132, 252, 0.75);
}
}
/* 聊天区域滚动条 */
.chat-scrollbar {
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(147, 51, 234, 0.08);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(147, 51, 234, 0.75);
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(147, 51, 234, 0.85);
}
}
.v-theme--PurpleThemeDark .chat-scrollbar {
::-webkit-scrollbar-track {
background: rgba(192, 132, 252, 0.08);
}
::-webkit-scrollbar-thumb {
background: rgba(192, 132, 252, 0.75);
border: 1px solid rgba(0, 0, 0, 0.1);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(192, 132, 252, 0.85);
}
}
/* 隐藏滚动条变体 */
.hidden-scrollbar {
::-webkit-scrollbar {
width: 0px;
height: 0px;
}
scrollbar-width: none;
-ms-overflow-style: none;
}
/* Firefox 兼容性 */
* {
scrollbar-width: thin;
scrollbar-color: rgba(147, 51, 234, 0.75) rgba(0, 0, 0, 0.05);
}
.v-theme--PurpleThemeDark * {
scrollbar-color: rgba(192, 132, 252, 0.75) rgba(255, 255, 255, 0.05);
}
/* 平滑滚动 */
html {
scroll-behavior: smooth;
}
/* 移动端触摸滚动优化 */
* {
-webkit-overflow-scrolling: touch;
}
+1
View File
@@ -12,5 +12,6 @@
@import './components/VShadow';
@import './components/VTextField';
@import './components/VTabs';
@import './components/VScrollbar';
@import './pages/dashboards';
-1
View File
@@ -155,7 +155,6 @@ export const useCommonStore = defineStore({
return data;
})
.catch((err) => {
this.toast("获取插件市场数据失败: " + err, "error");
return Promise.reject(err);
});
},
+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
+24 -7
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,18 +37,35 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'AlkaidPage',
components: {},
setup() {
const { tm } = useModuleI18n('features/alkaid/index');
return { tm };
},
data() {
return {}
},
methods: {
navigateTo(tab) {
this.$router.push(`/alkaid/${tab}`);
try {
if (this.$router && typeof this.$router.push === 'function') {
this.$router.push(`/alkaid/${tab}`);
}
} catch (error) {
console.warn('Navigation error:', error);
}
},
isActive(tab) {
return this.$route.path.includes(`/alkaid/${tab}`);
try {
return this.$route && this.$route.path.includes(`/alkaid/${tab}`);
} catch (error) {
console.warn('Route check error:', error);
return false;
}
}
},
mounted() {
+55 -49
View File
@@ -11,7 +11,7 @@ import ForceSupervisor from "graphology-layout-force/worker";
<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 实验性项目</small>
<small style="color: #a3a3a3;">{{ tm('features.alkaid.index.sigma.subtitle') }}</small>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
@@ -35,24 +35,24 @@ import ForceSupervisor from "graphology-layout-force/worker";
<div id="graph-control-panel"
style="min-width: 450px; border: 1px solid #eee; border-radius: 8px; padding: 16px; margin-left: 16px;">
<div>
<span style="color: #333333;">可视化</span>
<span style="color: #333333;">{{ tm('features.alkaid.index.sigma.visualization') }}</span>
<div style="margin-top: 8px;">
<v-autocomplete v-model="searchUserId" :items="userIdList" variant="outlined"
label="筛选用户 ID"></v-autocomplete>
<v-btn color="primary" @click="onNodeSelect" variant="tonal" style="margin-top: 8px;">
<v-icon start>mdi-magnify</v-icon>
筛选
</v-btn>
<v-btn color="secondary" @click="resetFilter" variant="tonal"
style="margin-top: 8px; margin-left: 8px;">
<v-icon start>mdi-filter-remove</v-icon>
重置筛选
</v-btn>
<v-autocomplete v-model="searchUserId" :items="userIdList" variant="outlined"
:label="tm('features.alkaid.index.sigma.filterUserId')"></v-autocomplete>
<v-btn color="primary" @click="onNodeSelect" variant="tonal" style="margin-top: 8px;">
<v-icon start>mdi-magnify</v-icon>
{{ tm('features.alkaid.index.sigma.filter') }}
</v-btn>
<v-btn color="secondary" @click="resetFilter" variant="tonal"
style="margin-top: 8px; margin-left: 8px;">
<v-icon start>mdi-filter-remove</v-icon>
{{ tm('features.alkaid.index.sigma.resetFilter') }}
</v-btn>
</div>
<div style="margin-top: 16px;">
<v-btn color="primary" @click="refreshGraph" variant="tonal">
<v-icon start>mdi-refresh</v-icon>
刷新图形
{{ tm('features.alkaid.index.sigma.refreshGraph') }}
</v-btn>
</div>
</div>
@@ -60,56 +60,56 @@ import ForceSupervisor from "graphology-layout-force/worker";
<v-divider class="my-4"></v-divider>
<div v-if="selectedNode" class="mt-4">
<h3>节点详情</h3>
<h3>{{ tm('features.alkaid.index.sigma.nodeDetails') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div v-if="selectedNode.id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">ID:</span>
<span>{{ selectedNode.id }}</span>
<div v-if="selectedNode.id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.id') }}:</span>
<span>{{ selectedNode.id }}</span>
</div>
</div>
</div>
<div v-if="selectedNode._label">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">类型:</span>
<span>{{ selectedNode._label }}</span>
<div v-if="selectedNode._label">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.type') }}:</span>
<span>{{ selectedNode._label }}</span>
</div>
</div>
</div>
<div v-if="selectedNode.name">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">名称:</span>
<span>{{ selectedNode.name }}</span>
<div v-if="selectedNode.name">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.name') }}:</span>
<span>{{ selectedNode.name }}</span>
</div>
</div>
</div>
<div v-if="selectedNode.user_id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">用户ID:</span>
<span>{{ selectedNode.user_id }}</span>
<div v-if="selectedNode.user_id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.userId') }}:</span>
<span>{{ selectedNode.user_id }}</span>
</div>
</div>
</div>
<div v-if="selectedNode.ts">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">时间戳:</span>
<span>{{ selectedNode.ts }}</span>
<div v-if="selectedNode.ts">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.timestamp') }}:</span>
<span>{{ selectedNode.ts }}</span>
</div>
</div>
</div>
<div v-if="selectedNode.type">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">类型:</span>
<span>{{ selectedNode.type }}</span>
<div v-if="selectedNode.type">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.type') }}:</span>
<span>{{ selectedNode.type }}</span>
</div>
</div>
</div>
</v-card>
</div>
<div v-if="graphStats" class="mt-4">
<h3>图形统计</h3>
<h3>{{ tm('features.alkaid.index.sigma.graphStats') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">节点数:</span>
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.nodeCount') }}:</span>
<span>{{ graphStats.nodeCount }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">边数:</span>
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.edgeCount') }}:</span>
<span>{{ graphStats.edgeCount }}</span>
</div>
</v-card>
@@ -121,7 +121,7 @@ import ForceSupervisor from "graphology-layout-force/worker";
<div class="d-flex align-center justify-center"
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
<v-icon size="64" color="grey-lighten-1">mdi-tools</v-icon>
<p class="text-h6 text-grey ml-4">功能开发中</p>
<p class="text-h6 text-grey ml-4">{{ tm('features.alkaid.index.sigma.inDevelopment') }}</p>
</div>
</div>
@@ -134,12 +134,18 @@ import ForceSupervisor from "graphology-layout-force/worker";
import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'AlkaidPage',
components: {
AstrBotConfig,
WaitingForRestart
},
setup() {
const { tm } = useModuleI18n('features/alkaid/index');
return { tm };
},
data() {
return {
renderer: null,
+1 -1
View File
@@ -9,7 +9,7 @@ const customizer = useCustomizerStore();
<div
style="height: 100%; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<div id="container">
<ChatPage chatbox-mode="true"></ChatPage>
<ChatPage :chatbox-mode="true"></ChatPage>
</div>
</div>
</v-app>
+445 -177
View File
@@ -1,23 +1,3 @@
<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">
<v-card-text class="chat-page-container">
@@ -25,7 +5,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 +21,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 +37,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 +54,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 +65,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 +74,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 +83,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 +99,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 +111,25 @@ 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="t('core.common.language')" v-if="chatboxMode">
<template v-slot:activator="{ props }">
<LanguageSwitcher variant="chatbox" />
</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 +137,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 +155,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>
<code>Ctrl</code>
<span>录制语音 🎤</span>
<span>{{ t('core.common.longPress') }}</span>
<code>Ctrl + B</code>
<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 +191,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 +216,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 +225,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 +233,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 +255,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" />
@@ -279,29 +265,129 @@ const props = defineProps({
</div>
</div>
</v-card-text>
</v-card>
</v-card>
<!-- 编辑对话标题对话框 -->
<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> <!-- 连接冲突提示对话框 -->
<v-dialog v-model="connectionConflictDialog" max-width="600" persistent>
<v-card class="rounded-lg">
<v-toolbar color="primary" density="comfortable" flat>
<v-icon color="white" class="ml-4 mr-2">mdi-information-outline</v-icon>
<v-toolbar-title class="text-white">{{ tm('connection.title') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="connectionConflictDialog = false" variant="text" color="white">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="pa-6">
<div class="text-body-1 mb-4">
{{ tm('connection.message') }}
</div>
<v-alert
type="info"
variant="tonal"
class="mb-4"
icon="mdi-lightbulb-outline"
>
<div class="text-body-2 mb-2">
<strong>{{ tm('connection.reasons') }}</strong>
</div>
<ul class="ml-4">
<li class="mb-1">{{ tm('connection.reasonWindowResize') }}</li>
<li class="mb-1">{{ tm('connection.reasonMultipleTabs') }}</li>
<li class="mb-1">{{ tm('connection.reasonNetworkIssue') }}</li>
</ul>
</v-alert>
<v-alert
type="warning"
variant="tonal"
icon="mdi-alert-circle-outline"
class="mb-0"
>
<div class="text-body-2">
{{ tm('connection.notice') }}
</div>
</v-alert>
</v-card-text>
<v-card-actions class="px-6 pb-4">
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="elevated"
@click="connectionConflictDialog = false"
class="px-6"
>
{{ tm('connection.understand') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 连接状态消息提示 -->
<v-snackbar
v-model="connectionStatusSnackbar"
:color="connectionStatusColor"
:timeout="4000"
location="top"
>
<v-icon class="mr-2">
{{ connectionStatusColor === 'success' ? 'mdi-check-circle' :
connectionStatusColor === 'warning' ? 'mdi-alert-circle' : 'mdi-information' }}
</v-icon>
{{ connectionStatusMessage }}
</v-snackbar>
</template>
<script>
import { router } from '@/router';
import axios from 'axios';
import { marked } from 'marked';
import { ref } from 'vue';
import { useCustomizerStore } from '@/stores/customizer';
import { useI18n, useModuleI18n } from '@/i18n/composables';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
marked.setOptions({
breaks: true
});
export default {
name: 'ChatPage',
components: {
LanguageSwitcher
},
props: {
chatboxMode: {
type: Boolean,
default: false
}
}, setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
return {
t,
tm,
router,
marked,
ref
};
},
data() {
return {
@@ -313,7 +399,7 @@ export default {
stagedImagesUrl: [], // 用于存储图片的blob URL数组
loadingChat: false,
inputFieldLabel: '聊天吧!',
inputFieldLabel: '',
isRecording: false,
audioChunks: [],
@@ -324,8 +410,11 @@ export default {
statusText: '',
eventSource: null,
eventSourceReader: null,
sseReconnecting: false, // 添加重连状态标志
// Ctrl键长按相关变量
// // Ctrl键长按相关变量
ctrlKeyDown: false,
ctrlKeyTimer: null,
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
@@ -342,9 +431,13 @@ export default {
sidebarHovered: false,
sidebarHoverTimer: null,
sidebarHoverExpanded: false,
sidebarHoverDelay: 100, // 悬停延迟,单位毫秒
pendingCid: null, // Store pending conversation ID for route handling
sidebarHoverDelay: 100, // 悬停延迟,单位毫秒
pendingCid: null, // Store pending conversation ID for route handling
// 连接状态提示相关
connectionConflictDialog: false,
connectionStatusSnackbar: false,
connectionStatusMessage: '',
connectionStatusColor: 'info',
}
},
@@ -363,9 +456,16 @@ export default {
// Watch for route changes to handle direct navigation to /chat/<cid>
'$route': {
immediate: true,
handler(to) {
console.log('Route changed:', to.path);
// Check if the route matches /chat/<cid> pattern
handler(to, from) {
console.log('Route changed:', to.path, 'from:', from?.path); // 如果是从不同的路由模式切换(chat <-> chatbox),重新建立SSE连接
if (from &&
((from.path.startsWith('/chat') && to.path.startsWith('/chatbox')) ||
(from.path.startsWith('/chatbox') && to.path.startsWith('/chat')))) {
console.log('Route mode changed, reconnecting SSE...');
this.reconnectSSE();
}
// Check if the route matches /chat/<cid> or /chatbox/<cid> pattern
if (to.path.startsWith('/chat/') || to.path.startsWith('/chatbox/')) {
const pathCid = to.path.split('/')[2];
console.log('Path CID:', pathCid);
@@ -401,6 +501,8 @@ export default {
mounted() {
// Theme is now handled globally by the customizer store.
// 设置输入框标签
this.inputFieldLabel = this.tm('input.chatPrompt');
this.startListeningEvent();
this.checkStatus();
this.getConversations();
@@ -409,7 +511,10 @@ export default {
inputField.addEventListener('keydown', function (e) {
if (e.keyCode == 13 && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
// 检查是否有内容可发送
if (this.canSendMessage()) {
this.sendMessage();
}
}
}.bind(this));
@@ -424,11 +529,8 @@ export default {
},
beforeUnmount() {
if (this.eventSource) {
this.eventSource.cancel();
console.log('SSE连接已断开');
}
this.disconnectSSE();
// 移除keyup事件监听
document.removeEventListener('keyup', this.handleInputKeyUp);
@@ -439,9 +541,20 @@ export default {
// Cleanup blob URLs
this.cleanupMediaCache();
},
},
methods: {
// 显示连接冲突对话框
showConnectionConflictDialog() {
this.connectionConflictDialog = true;
},
// 显示连接状态消息
showConnectionStatus(message, color = 'info') {
this.connectionStatusMessage = message;
this.connectionStatusColor = color;
this.connectionStatusSnackbar = true;
},
toggleTheme() {
const customizer = useCustomizerStore();
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
@@ -537,108 +650,246 @@ export default {
}
},
async startListeningEvent() {
const response = await fetch('/api/chat/listen', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
// 断开SSE连接
disconnectSSE() {
if (this.eventSourceReader) {
try {
this.eventSourceReader.cancel();
console.log('SSE Reader cancelled');
} catch (error) {
console.warn('Error cancelling SSE reader:', error);
}
})
this.eventSourceReader = null;
}
if (this.eventSource) {
try {
this.eventSource.cancel();
console.log('SSE连接已断开');
} catch (error) {
console.warn('Error cancelling SSE:', error);
}
this.eventSource = null;
}
},
if (!response.ok) {
console.error('SSE连接失败:', response.statusText);
// 重新连接SSE
async reconnectSSE() {
if (this.sseReconnecting) {
console.log('SSE reconnection already in progress');
return;
}
this.sseReconnecting = true;
console.log('Reconnecting SSE...');
this.disconnectSSE();
// 等待更长时间确保后端连接完全清理
await new Promise(resolve => setTimeout(resolve, 1000));
this.startListeningEvent();
},
const reader = response.body.getReader();
const decoder = new TextDecoder();
this.eventSource = reader
let in_streaming = false
let message_obj = null
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('SSE连接关闭');
break;
}
const chunk = decoder.decode(value, { stream: true });
// 可能有多行
let lines = chunk.split('\n\n');
console.log('SSE数据:', lines);
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (!line) {
continue;
}
console.log(line)
// data: {"type": "plain", "data": "helloworld"}
let chunk_json = JSON.parse(line.replace('data: ', ''));
if (chunk_json.type === 'heartbeat') {
continue; // 心跳包
}
if (chunk_json.type === 'error') {
console.error('Error received:', chunk_json.data);
continue;
}
if (chunk_json.type === 'image') {
let img = chunk_json.data.replace('[IMAGE]', '');
const imageUrl = await this.getMediaFile(img);
let bot_resp = {
type: 'bot',
message: `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
async startListeningEvent() {
// 确保之前的连接已断开
this.disconnectSSE();
// 如果正在重连过程中,等待一下
if (this.sseReconnecting) {
await new Promise(resolve => setTimeout(resolve, 500));
}
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try {
console.log(`尝试建立SSE连接 (${retryCount + 1}/${maxRetries})`);
const response = await fetch('/api/chat/listen', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'record') {
let audio = chunk_json.data.replace('[RECORD]', '');
const audioUrl = await this.getMediaFile(audio);
let bot_resp = {
type: 'bot',
message: `<audio controls class="audio-player">
<source src="${audioUrl}" type="audio/wav">
您的浏览器不支持音频播放。
</audio>`
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'plain') {
if (!in_streaming) {
message_obj = {
type: 'bot',
message: ref(chunk_json.data),
});
if (!response.ok) {
throw new Error(`SSE连接失败: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
this.eventSource = reader;
this.eventSourceReader = reader;
this.sseReconnecting = false;
let in_streaming = false;
let message_obj = null;
console.log('SSE连接已建立');
// 显示连接成功状态
if (retryCount > 0) {
this.showConnectionStatus(this.tm('connection.status.reconnected'), 'success');
}
while (true) {
try {
const { done, value } = await reader.read();
if (done) {
console.log('SSE连接正常关闭');
break;
}
this.messages.push(message_obj);
in_streaming = true;
} else {
message_obj.message.value += chunk_json.data;
const chunk = decoder.decode(value, { stream: true });
// 可能有多行
let lines = chunk.split('\n\n');
console.log('SSE数据:', lines);
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (!line) {
continue;
}
console.log(line); // 处理后端错误响应格式
if (line.startsWith('{"status":"error"')) {
try {
const errorObj = JSON.parse(line);
if (errorObj.message === 'Already connected') {
console.log('检测到连接冲突显示提示对话框...');
this.showConnectionConflictDialog();
throw new Error('CONNECTION_CONFLICT');
}
console.error('后端错误:', errorObj.message);
continue;
} catch (parseError) {
if (parseError.message === 'CONNECTION_CONFLICT') {
throw parseError;
}
console.warn('解析错误响应失败:', line);
continue;
}
}
// data: {"type": "plain", "data": "helloworld"}
let chunk_json;
try {
chunk_json = JSON.parse(line.replace('data: ', ''));
} catch (parseError) {
console.warn('JSON解析失败:', line, parseError);
continue;
}
// 检查解析后的数据是否有效
if (!chunk_json || typeof chunk_json !== 'object') {
console.warn('无效的数据对象:', chunk_json);
continue;
}
// 检查是否有type字段
if (!chunk_json.hasOwnProperty('type')) {
console.warn('数据缺少type字段:', chunk_json);
continue;
}
if (chunk_json.type === 'heartbeat') {
continue; // 心跳包
}
if (chunk_json.type === 'error') {
console.error('Error received:', chunk_json.data);
continue;
}
if (chunk_json.type === 'image') {
let img = chunk_json.data.replace('[IMAGE]', '');
const imageUrl = await this.getMediaFile(img);
let bot_resp = {
type: 'bot',
message: `<img src="${imageUrl}" style="max-width: 80%; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);"/>`
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'record') {
let audio = chunk_json.data.replace('[RECORD]', '');
const audioUrl = await this.getMediaFile(audio);
let bot_resp = {
type: 'bot',
message: `<audio controls class="audio-player">
<source src="${audioUrl}" type="audio/wav">
${this.t('messages.errors.browser.audioNotSupported')}
</audio>`
}
this.messages.push(bot_resp);
} else if (chunk_json.type === 'plain') {
if (!in_streaming) {
message_obj = {
type: 'bot',
message: this.ref(chunk_json.data),
}
this.messages.push(message_obj);
in_streaming = true;
} else {
message_obj.message.value += chunk_json.data;
}
} else if (chunk_json.type === 'end') {
in_streaming = false;
continue;
} else if (chunk_json.type === 'update_title') {
// 更新对话标题
const conversation = this.conversations.find(c => c.cid === chunk_json.cid);
if (conversation) {
conversation.title = chunk_json.data;
}
} else {
console.warn('未知数据类型:', chunk_json.type);
}
this.scrollToBottom();
}
} catch (readError) {
if (readError.name === 'AbortError') {
console.log('SSE连接被取消');
break;
}
if (readError.message === 'CONNECTION_CONFLICT') {
throw readError;
}
console.error('SSE读取错误:', readError);
break;
}
} else if (chunk_json.type === 'end') {
in_streaming = false;
continue;
} else if (chunk_json.type === 'update_title') {
// 更新对话标题
const conversation = this.conversations.find(c => c.cid === chunk_json.cid);
if (conversation) {
conversation.title = chunk_json.data;
}
} else {
console.warn('未知数据类型:', chunk_json.type);
}
this.scrollToBottom();
// 如果成功连接并正常结束,跳出重试循环
break;
} catch (error) {
console.error(`SSE连接错误 (尝试 ${retryCount + 1}):`, error);
retryCount++;
if (error.message === 'CONNECTION_CONFLICT' && retryCount < maxRetries) {
console.log(`连接冲突,等待 ${2000 * retryCount}ms 后重试...`);
this.showConnectionStatus(`${this.tm('connection.status.reconnecting')} (${retryCount}/${maxRetries})`, 'warning');
await new Promise(resolve => setTimeout(resolve, 2000 * retryCount));
continue;
}
if (retryCount >= maxRetries) {
console.error('SSE连接重试次数已达上限');
this.showConnectionStatus(this.tm('connection.status.failed'), 'error');
this.sseReconnecting = false;
break;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
} finally {
this.eventSource = null;
this.eventSourceReader = null;
}
}
this.sseReconnecting = false;
},
removeAudio() {
@@ -662,12 +913,12 @@ export default {
};
this.mediaRecorder.start();
this.isRecording = true;
this.inputFieldLabel = "录音中,请说话...";
this.inputFieldLabel = this.tm('input.recordingPrompt');
},
async stopRecording() {
this.isRecording = false;
this.inputFieldLabel = "聊天吧!";
this.inputFieldLabel = this.tm('input.chatPrompt');
this.mediaRecorder.stop();
this.mediaRecorder.onstop = async () => {
const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
@@ -756,9 +1007,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]}`);
}
}
@@ -777,7 +1028,7 @@ export default {
const audioUrl = await this.getMediaFile(audio);
message[i].message = `<audio controls class="audio-player">
<source src="${audioUrl}" type="audio/wav">
您的浏览器不支持音频播放。
${this.t('messages.errors.browser.audioNotSupported')}
</audio>`
}
if (message[i].image_url && message[i].image_url.length > 0) {
@@ -800,9 +1051,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 +1067,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');
}
},
@@ -833,7 +1084,9 @@ export default {
second: '2-digit',
hour12: false
};
return date.toLocaleString('zh-CN', options).replace(/\//g, '-').replace(/, /g, ' ');
// 使用当前语言环境的locale
const locale = this.t('core.common.locale') || 'zh-CN';
return date.toLocaleString(locale, options).replace(/\//g, '-').replace(/, /g, ' ');
},
deleteConversation(cid) {
@@ -846,7 +1099,20 @@ export default {
});
},
// 检查是否可以发送消息
canSendMessage() {
return (this.prompt && this.prompt.trim()) ||
this.stagedImagesName.length > 0 ||
this.stagedAudioUrl;
},
async sendMessage() {
// 检查是否有内容可发送
if (!this.canSendMessage()) {
console.log('没有内容可发送');
return;
}
if (this.currCid == '') {
const cid = await this.newConversation();
// URL is already updated in newConversation method
@@ -855,7 +1121,7 @@ export default {
// Create a message object with actual URLs for display
const userMessage = {
type: 'user',
message: this.prompt,
message: this.prompt.trim(), // 使用 trim() 去除前后空格
image_url: [],
audio_url: null
};
@@ -894,20 +1160,22 @@ export default {
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: JSON.stringify({
message: this.prompt,
message: this.prompt.trim(), // 确保发送的消息已去除前后空格
conversation_id: this.currCid,
image_url: this.stagedImagesName, // Already contains just filenames
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : [] // Already contains just filename
image_url: this.stagedImagesName,
audio_url: this.stagedAudioUrl ? [this.stagedAudioUrl] : []
})
})
.then(response => {
this.prompt = '';
this.stagedImagesName = [];
this.stagedImagesUrl = [];
this.stagedAudioUrl = "";
this.loadingChat = false;
})
.catch(err => {
console.error(err);
this.loadingChat = false;
});
},
scrollToBottom() {
@@ -915,10 +1183,11 @@ export default {
const container = this.$refs.messageContainer;
container.scrollTop = container.scrollHeight;
});
},
},
handleInputKeyDown(e) {
if (e.keyCode === 17) { // Ctrl键
if (e.ctrlKey && e.keyCode === 66) { // Ctrl+B组合
e.preventDefault(); // 防止默认行为
// 防止重复触发
if (this.ctrlKeyDown) return;
@@ -931,10 +1200,9 @@ export default {
}
}, this.ctrlKeyLongPressThreshold);
}
},
},
handleInputKeyUp(e) {
if (e.keyCode === 17) { // Ctrl键
if (e.keyCode === 66) { // B键释放
this.ctrlKeyDown = false;
// 清除定时器
+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: this.tm('messages.loadError'),
saveSuccess: this.tm('messages.saveSuccess'),
saveError: this.tm('messages.saveError'),
configApplied: this.tm('messages.configApplied'),
configApplyError: this.tm('messages.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');
// 使markedMarkdown
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>
+146 -115
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: [],
@@ -25,12 +28,12 @@ const extension_config = reactive({
config: {}
});
const pluginMarketData = ref([]);
const loadingDialog = reactive({
show: false,
title: "加载中...",
statusCode: 0, // 0: loading, 1: success, 2: error,
result: ""
});
const loadingDialog = reactive({
show: false,
title: "",
statusCode: 0, // 0: loading, 1: success, 2: error,
result: ""
});
const showPluginInfoDialog = ref(false);
const selectedPlugin = ref({});
const curr_namespace = ref("");
@@ -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 = "";
};
@@ -175,8 +178,8 @@ const checkUpdate = () => {
if (matchedPlugin) {
extension.online_version = matchedPlugin.version;
extension.has_update = extension.version !== matchedPlugin.version &&
matchedPlugin.version !== "未知";
extension.has_update = extension.version !== matchedPlugin.version &&
matchedPlugin.version !== tm('status.unknown');
} else {
extension.has_update = false;
}
@@ -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") {
@@ -201,6 +204,7 @@ const uninstallExtension = async (extension_name) => {
};
const updateExtension = async (extension_name) => {
loadingDialog.title = tm('status.loading');
loadingDialog.show = true;
try {
const res = await axios.post('/api/plugin/update', {
@@ -216,14 +220,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 +305,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 +334,7 @@ const getPlatformEnableConfig = async () => {
//
if (platformEnableData.platforms.length === 0) {
toast("未添加任何平台适配器,请先在平台管理中添加平台", "warning");
toast(tm('dialogs.platformConfig.noAdaptersDesc'), "warning");
} else {
//
platformEnableData.platforms.forEach(platform => {
@@ -349,7 +353,7 @@ const getPlatformEnableConfig = async () => {
platformEnableDialog.value = true;
} catch (err) {
toast("获取平台插件配置失败: " + err, "error");
toast(tm('messages.getPlatformConfigFailed') + " " + err, "error");
} finally {
loadingPlatformData.value = false;
}
@@ -371,7 +375,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 +456,19 @@ 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.title = tm('status.loading');
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 +495,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 +518,7 @@ const newExtension = async () => {
});
}).catch((err) => {
loading_.value = false;
toast("安装插件失败: " + err, "error");
toast(tm('messages.installFailed') + " " + err, "error");
onLoadingDialogResult(2, err, -1);
});
}
@@ -537,7 +542,7 @@ onMounted(async () => {
checkAlreadyInstalled();
checkUpdate();
} catch (err) {
console.error("获取插件市场数据失败:", err);
toast(tm('messages.getMarketDataFailed') + " " + err, "error");
}
});
@@ -555,42 +560,64 @@ 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>
<!-- 标签页 -->
<v-card-text>
<div class="d-flex align-center mb-2" style="justify-content: space-between;">
<v-tabs v-model="activeTab" color="primary" class="mb-4">
<!-- 标签栏和搜索栏 - 响应式布局 -->
<div class="mb-4">
<!-- 标签栏 -->
<v-tabs v-model="activeTab" color="primary" class="mb-3">
<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
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"
variant="solo-filled" flat hide-details single-line></v-text-field>
<!-- 搜索栏 - 在移动端时独占一行 -->
<v-row class="mb-2">
<v-col cols="12" sm="6" md="4" lg="3">
<v-text-field
v-if="activeTab == 'market'"
v-model="marketSearch"
density="compact"
: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
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>
</v-col>
</v-row>
</div>
<!-- 已安装插件标签页内容 -->
<v-tab-item v-show="activeTab === 'installed'">
<v-row class="mb-4">
<v-col cols="12" sm="6" md="6" class="d-flex align-center">
<v-col cols="12" class="d-flex align-center flex-wrap ga-2">
<v-btn-group variant="outlined" density="comfortable" color="primary">
<v-btn @click="isListView = false" :color="!isListView ? 'primary' : undefined"
:variant="!isListView ? 'flat' : 'outlined'">
@@ -602,14 +629,15 @@ onMounted(async () => {
</v-btn>
</v-btn-group>
<v-btn class="ml-2" @click="toggleShowReserved" prepend-icon="mdi-eye-settings-outline"
:color="showReserved ? 'primary' : undefined" :variant="showReserved ? 'flat' : 'outlined'">
{{ showReserved ? '隐藏系统插件' : '显示系统插件' }}
<v-btn @click="toggleShowReserved" prepend-icon="mdi-eye-settings-outline"
:color="showReserved ? 'primary' : undefined" :variant="showReserved ? 'flat' : 'outlined'"
class="flex-shrink-0">
{{ 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">
平台命令配置
<v-btn prepend-icon="mdi-tune-vertical" color="primary" variant="outlined"
@click="getPlatformEnableConfig" class="flex-shrink-0">
{{ tm('buttons.platformConfig') }}
</v-btn>
</v-col>
@@ -625,15 +653,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 +678,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 +687,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 +702,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 +714,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 +723,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 +770,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 +783,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 +809,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 +821,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 +888,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 +897,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 +905,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 +913,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 +934,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 +959,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 +974,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 +984,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 +1014,16 @@ onMounted(async () => {
</div>
<div style="margin-top: 32px;">
<h3>日志</h3>
<ConsoleDisplayer historyNum="10" style="height: 200px; margin-top: 16px;"></ConsoleDisplayer>
<h3>{{ tm('dialogs.loading.logs') }}</h3>
<ConsoleDisplayer historyNum="10" style="height: 200px; margin-top: 16px; margin-bottom: 24px;"></ConsoleDisplayer>
</div>
</v-card-text>
<v-card-actions>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<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 +1031,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 +1056,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: this.tm('messages.updateSuccess'),
addSuccess: this.tm('messages.addSuccess'),
deleteSuccess: this.tm('messages.deleteSuccess'),
statusUpdateSuccess: this.tm('messages.statusUpdateSuccess'),
deleteConfirm: this.tm('messages.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);
+77 -49
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>
@@ -333,6 +333,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',
@@ -342,6 +343,10 @@ export default {
ConsoleDisplayer,
ItemCardGrid
},
setup() {
const { tm } = useModuleI18n('features/provider');
return { tm };
},
data() {
return {
config_data: {},
@@ -416,6 +421,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') {
@@ -454,9 +488,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) });
}
},
@@ -508,21 +542,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 });
},
//
@@ -633,10 +661,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);
});
@@ -651,7 +679,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);
@@ -665,7 +693,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);
});
},
@@ -675,7 +703,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; //
@@ -703,7 +731,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 }));
}
}
}
+86 -79
View File
@@ -4,35 +4,35 @@
<!-- knowledge card -->
<div v-if="!installed" class="d-flex align-center justify-center flex-column"
style="flex-grow: 1; width: 100%; height: 100%;">
<h2>还没有安装知识库插件
<v-icon v-class="ml - 2" size="small" color="grey"
<h2>{{ tm('notInstalled.title') }}
<v-icon class="ml-2" size="small" color="grey"
@click="openUrl('https://astrbot.app/use/knowledge-base.html')">mdi-information-outline</v-icon>
</h2>
<v-btn style="margin-top: 16px;" variant="tonal" color="primary" @click="installPlugin"
:loading="installing">
立即安装
{{ tm('notInstalled.install') }}
</v-btn>
<ConsoleDisplayer v-show="installing" style="background-color: #fff; max-height: 300px; margin-top: 16px; max-width: 100%" :show-level-btns="false"></ConsoleDisplayer>
</div>
<div v-else-if="kbCollections.length == 0" class="d-flex align-center justify-center flex-column"
style="flex-grow: 1; width: 100%; height: 100%;">
<h2>还没有知识库快创建一个吧🙂</h2>
<h2>{{ tm('empty.title') }}</h2>
<v-btn style="margin-top: 16px;" variant="tonal" color="primary" @click="showCreateDialog = true">
创建知识库
{{ tm('empty.create') }}
</v-btn>
</div>
<div v-else>
<h2 class="mb-4">知识库列表
<v-icon v-class="ml - 2" size="x-small" color="grey"
<h2 class="mb-4">{{ tm('list.title') }}
<v-icon class="ml-2" size="x-small" color="grey"
@click="openUrl('https://astrbot.app/use/knowledge-base.html')">mdi-information-outline</v-icon>
</h2>
<v-btn class="mb-4" prepend-icon="mdi-plus" variant="tonal" color="primary"
@click="showCreateDialog = true">
创建知识库
{{ tm('list.create') }}
</v-btn>
<v-btn class="mb-4 ml-4" prepend-icon="mdi-cog" variant="tonal" color="success"
@click="$router.push('/extension?open_config=astrbot_plugin_knowledge_base')">
配置
{{ tm('list.config') }}
</v-btn>
<div class="kb-grid">
@@ -44,7 +44,7 @@
<span class="kb-emoji">{{ kb.emoji || '🙂' }}</span>
</div>
<div class="kb-name">{{ kb.collection_name }}</div>
<div class="kb-count">{{ kb.count || 0 }} 条知识</div>
<div class="kb-count">{{ kb.count || 0 }} {{ tm('list.knowledgeCount') }}</div>
<div class="kb-actions">
<v-btn icon variant="text" size="small" color="error" @click.stop="confirmDelete(kb)">
<v-icon>mdi-delete</v-icon>
@@ -54,7 +54,7 @@
</div>
</div>
<div style="padding: 16px; text-align: center;">
<small style="color: #a3a3a3">Tips: 在聊天页面通过 /kb 指令了解如何使用</small>
<small style="color: #a3a3a3">{{ tm('list.tips') }}</small>
</div>
</div>
@@ -64,7 +64,7 @@
<!-- 创建知识库对话框 -->
<v-dialog v-model="showCreateDialog" max-width="500px">
<v-card>
<v-card-title class="text-h4">创建新知识库</v-card-title>
<v-card-title class="text-h4">{{ tm('createDialog.title') }}</v-card-title>
<v-card-text>
<div style="width: 100%; display: flex; align-items: center; justify-content: center;">
@@ -75,22 +75,22 @@
<v-form @submit.prevent="submitCreateForm">
<v-text-field variant="outlined" v-model="newKB.name" label="知识库名称" required></v-text-field>
<v-text-field variant="outlined" v-model="newKB.name" :label="tm('createDialog.nameLabel')" required></v-text-field>
<v-textarea v-model="newKB.description" label="描述" variant="outlined" placeholder="知识库的简短描述..."
<v-textarea v-model="newKB.description" :label="tm('createDialog.descriptionLabel')" variant="outlined" :placeholder="tm('createDialog.descriptionPlaceholder')"
rows="3"></v-textarea>
<v-select v-model="newKB.embedding_provider_id" :items="embeddingProviderConfigs"
:item-props="embeddingModelProps" label="Embedding(嵌入)模型" variant="outlined" class="mt-2">
:item-props="embeddingModelProps" :label="tm('createDialog.embeddingModelLabel')" variant="outlined" class="mt-2">
</v-select>
<small>Tips: 一旦选择了一个知识库的嵌入模型请不要再修改该提供商的模型或者向量维度信息否则将严重影响该知识库的召回率甚至报错</small>
<small>{{ tm('createDialog.tips') }}</small>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="error" variant="text" @click="showCreateDialog = false">取消</v-btn>
<v-btn color="primary" variant="text" @click="submitCreateForm">创建</v-btn>
<v-btn color="error" variant="text" @click="showCreateDialog = false">{{ tm('createDialog.cancel') }}</v-btn>
<v-btn color="primary" variant="text" @click="submitCreateForm">{{ tm('createDialog.create') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -98,11 +98,11 @@
<!-- 表情选择器对话框 -->
<v-dialog v-model="showEmojiPicker" max-width="400px">
<v-card>
<v-card-title class="text-h6">选择表情</v-card-title>
<v-card-title class="text-h6">{{ tm('emojiPicker.title') }}</v-card-title>
<v-card-text>
<div class="emoji-picker">
<div v-for="(category, catIndex) in emojiCategories" :key="catIndex" class="mb-4">
<div class="text-subtitle-2 mb-2">{{ category.name }}</div>
<div class="text-subtitle-2 mb-2">{{ tm(`emojiPicker.categories.${category.key}`) }}</div>
<div class="emoji-grid">
<div v-for="(emoji, emojiIndex) in category.emojis" :key="emojiIndex" class="emoji-item"
@click="selectEmoji(emoji)">
@@ -114,7 +114,7 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">关闭</v-btn>
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">{{ tm('emojiPicker.close') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -124,7 +124,7 @@
<v-card>
<v-card-title class="d-flex align-center">
<div class="me-2 emoji-sm">{{ currentKB.emoji || '🙂' }}</div>
<span>{{ currentKB.collection_name }} - 知识库管理</span>
<span>{{ currentKB.collection_name }} - {{ tm('contentDialog.title') }}</span>
<v-spacer></v-spacer>
<v-btn variant="plain" icon @click="showContentDialog = false">
<v-icon>mdi-close</v-icon>
@@ -134,19 +134,19 @@
<div v-if="currentKB._embedding_provider_config" class="px-6 py-2">
<v-chip class="mr-2" color="primary" variant="tonal" size="small" rounded="sm">
<v-icon start size="small">mdi-database</v-icon>
嵌入模型: {{ currentKB._embedding_provider_config.embedding_model }}
{{ tm('contentDialog.embeddingModel') }}: {{ currentKB._embedding_provider_config.embedding_model }}
</v-chip>
<v-chip color="secondary" variant="tonal" size="small" rounded="sm">
<v-icon start size="small">mdi-vector-point</v-icon>
向量维度: {{ currentKB._embedding_provider_config.embedding_dimensions }}
{{ tm('contentDialog.vectorDimension') }}: {{ currentKB._embedding_provider_config.embedding_dimensions }}
</v-chip>
<small style="margin-left: 8px;">💡 使用方式: 在聊天页中输入 /kb use {{ currentKB.collection_name }}</small>
</div>
<v-card-text>
<v-tabs v-model="activeTab">
<v-tab value="upload">上传文件</v-tab>
<v-tab value="search">搜索内容</v-tab>
<v-tab value="upload">{{ tm('contentDialog.tabs.upload') }}</v-tab>
<v-tab value="search">{{ tm('contentDialog.tabs.search') }}</v-tab>
</v-tabs>
<v-window v-model="activeTab" class="mt-4">
@@ -154,22 +154,22 @@
<v-window-item value="upload">
<div class="upload-container pa-4">
<div class="text-center mb-4">
<h3>上传文件到知识库</h3>
<p class="text-subtitle-1">支持 txtpdfwordexcel 等多种格式</p>
<h3>{{ tm('upload.title') }}</h3>
<p class="text-subtitle-1">{{ tm('upload.subtitle') }}</p>
</div>
<div class="upload-zone" @dragover.prevent @drop.prevent="onFileDrop"
@click="triggerFileInput">
<input type="file" ref="fileInput" style="display: none" @change="onFileSelected" />
<v-icon size="48" color="primary">mdi-cloud-upload</v-icon>
<p class="mt-2">拖放文件到这里或点击上传</p>
<p class="mt-2">{{ tm('upload.dropzone') }}</p>
</div>
<!-- 优化后的分片长度和重叠长度设置 -->
<v-card class="mt-4 chunk-settings-card" variant="outlined" color="grey-lighten-4">
<v-card-title class="pa-4 pb-0 d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-puzzle-outline</v-icon>
<span class="text-subtitle-1 font-weight-bold">分片设置</span>
<span class="text-subtitle-1 font-weight-bold">{{ tm('upload.chunkSettings.title') }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" class="ml-2" size="small" color="grey">
@@ -177,20 +177,19 @@
</v-icon>
</template>
<span>
分片长度决定每块文本的大小重叠长度决定相邻文本块之间的重叠程度<br>
较小的分片更精确但会增加数量适当的重叠可提高检索准确性
{{ tm('upload.chunkSettings.tooltip') }}
</span>
</v-tooltip>
</v-card-title>
<v-card-text class="pa-4 pt-2">
<div class="d-flex flex-wrap" style="gap: 8px">
<v-text-field v-model="chunkSize" label="分片长度" type="number"
hint="控制每个文本块大小,留空使用默认值" persistent-hint variant="outlined"
<v-text-field v-model="chunkSize" :label="tm('upload.chunkSettings.chunkSizeLabel')" type="number"
:hint="tm('upload.chunkSettings.chunkSizeHint')" persistent-hint variant="outlined"
density="comfortable" class="flex-grow-1 chunk-field"
prepend-inner-icon="mdi-text-box-outline" min="50"></v-text-field>
<v-text-field v-model="overlap" label="重叠长度" type="number"
hint="控制相邻文本块重叠度,留空使用默认值" persistent-hint variant="outlined"
<v-text-field v-model="overlap" :label="tm('upload.chunkSettings.overlapLabel')" type="number"
:hint="tm('upload.chunkSettings.overlapHint')" persistent-hint variant="outlined"
density="comfortable" class="flex-grow-1 chunk-field"
prepend-inner-icon="mdi-vector-intersection" min="0"></v-text-field>
</div>
@@ -211,7 +210,7 @@
<div class="text-center mt-4">
<v-btn color="primary" variant="elevated" :loading="uploading"
:disabled="!selectedFile" @click="uploadFile">
上传到知识库
{{ tm('upload.upload') }}
</v-btn>
</div>
</div>
@@ -226,23 +225,23 @@
<v-window-item value="search">
<div class="search-container pa-4">
<v-form @submit.prevent="searchKnowledgeBase" class="d-flex align-center">
<v-text-field v-model="searchQuery" label="搜索知识库内容" append-icon="mdi-magnify"
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')" append-icon="mdi-magnify"
variant="outlined" class="flex-grow-1 me-2" @click:append="searchKnowledgeBase"
@keyup.enter="searchKnowledgeBase" placeholder="输入关键词搜索知识库内容..."
@keyup.enter="searchKnowledgeBase" :placeholder="tm('search.queryPlaceholder')"
hide-details></v-text-field>
<v-select v-model="topK" :items="[3, 5, 10, 20]" label="结果数量" variant="outlined"
<v-select v-model="topK" :items="[3, 5, 10, 20]" :label="tm('search.resultCountLabel')" variant="outlined"
style="max-width: 120px;" hide-details></v-select>
</v-form>
<div class="search-results mt-4">
<div v-if="searching">
<v-progress-linear indeterminate color="primary"></v-progress-linear>
<p class="text-center mt-4">正在搜索...</p>
<p class="text-center mt-4">{{ tm('search.searching') }}</p>
</div>
<div v-else-if="searchResults.length > 0">
<h3 class="mb-2">搜索结果</h3>
<h3 class="mb-2">{{ tm('search.resultsTitle') }}</h3>
<v-card v-for="(result, index) in searchResults" :key="index"
class="mb-4 search-result-card" variant="outlined">
<v-card-text>
@@ -254,7 +253,7 @@
<v-spacer></v-spacer>
<v-chip v-if="result.score" size="small" color="primary"
variant="tonal">
相关度: {{ Math.round(result.score * 100) }}%
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100) }}%
</v-chip>
</div>
<div class="search-content">{{ result.content }}</div>
@@ -264,7 +263,7 @@
<div v-else-if="searchPerformed">
<v-alert type="info" variant="tonal">
没有找到匹配的内容
{{ tm('search.noResults') }}
</v-alert>
</div>
</div>
@@ -278,15 +277,15 @@
<!-- 删除知识库确认对话框 -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h5">确认删除</v-card-title>
<v-card-title class="text-h5">{{ tm('deleteDialog.title') }}</v-card-title>
<v-card-text>
<p>您确定要删除知识库 <span class="font-weight-bold">{{ deleteTarget.collection_name }}</span> </p>
<p class="text-red">此操作不可逆所有知识库内容将被永久删除</p>
<p>{{ tm('deleteDialog.confirmText', { name: deleteTarget.collection_name }) }}</p>
<p class="text-red">{{ tm('deleteDialog.warning') }}</p>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="showDeleteDialog = false">取消</v-btn>
<v-btn color="error" variant="text" @click="deleteKnowledgeBase" :loading="deleting">删除</v-btn>
<v-btn color="grey-darken-1" variant="text" @click="showDeleteDialog = false">{{ tm('deleteDialog.cancel') }}</v-btn>
<v-btn color="error" variant="text" @click="deleteKnowledgeBase" :loading="deleting">{{ tm('deleteDialog.delete') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -301,12 +300,17 @@
<script>
import axios from 'axios';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'KnowledgeBase',
components: {
ConsoleDisplayer,
},
setup() {
const { tm } = useModuleI18n('features/alkaid/knowledge-base');
return { tm };
},
data() {
return {
installed: true,
@@ -327,27 +331,27 @@ export default {
},
emojiCategories: [
{
name: '笑脸和情感',
key: 'emotions',
emojis: ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘']
},
{
name: '动物和自然',
key: 'animals',
emojis: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵']
},
{
name: '食物和饮料',
key: 'food',
emojis: ['🍏', '🍎', '🍐', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥']
},
{
name: '活动和物品',
key: 'activities',
emojis: ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥅', '🏒', '🏑', '🥍']
},
{
name: '旅行和地点',
key: 'travel',
emojis: ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🚚', '🚛', '🚜', '🛴', '🚲']
},
{
name: '符号和旗帜',
key: 'symbols',
emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗']
}
],
@@ -382,14 +386,17 @@ export default {
embeddingModelProps(providerConfig) {
return {
title: providerConfig.embedding_model,
subtitle: `提供商 ID: ${providerConfig.id} | 嵌入模型维度: ${providerConfig.embedding_dimensions}`,
subtitle: this.tm('createDialog.providerInfo', {
id: providerConfig.id,
dimensions: providerConfig.embedding_dimensions
}),
}
},
checkPlugin() {
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
.then(response => {
if (response.data.status !== 'ok') {
this.showSnackbar('插件未安装或不可用', 'error');
this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error');
}
if (response.data.data.length > 0) {
this.installed = true;
@@ -400,7 +407,7 @@ export default {
})
.catch(error => {
console.error('Error checking plugin:', error);
this.showSnackbar('检查插件失败', 'error');
this.showSnackbar(this.tm('messages.checkPluginFailed'), 'error');
})
},
@@ -414,12 +421,12 @@ export default {
if (response.data.status === 'ok') {
this.checkPlugin();
} else {
this.showSnackbar(response.data.message || '安装失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.installFailed'), 'error');
}
})
.catch(error => {
console.error('Error installing plugin:', error);
this.showSnackbar('安装插件失败', 'error');
this.showSnackbar(this.tm('messages.installPluginFailed'), 'error');
}).finally(() => {
this.installing = false;
});
@@ -432,7 +439,7 @@ export default {
})
.catch(error => {
console.error('Error fetching knowledge base collections:', error);
this.showSnackbar('获取知识库列表失败', 'error');
this.showSnackbar(this.tm('messages.getKnowledgeBaseListFailed'), 'error');
});
},
@@ -449,23 +456,23 @@ export default {
})
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('知识库创建成功');
this.showSnackbar(this.tm('messages.knowledgeBaseCreated'));
this.getKBCollections();
this.showCreateDialog = false;
this.resetNewKB();
} else {
this.showSnackbar(response.data.message || '创建失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.createFailed'), 'error');
}
})
.catch(error => {
console.error('Error creating knowledge base collection:', error);
this.showSnackbar('创建知识库失败', 'error');
this.showSnackbar(this.tm('messages.createKnowledgeBaseFailed'), 'error');
});
},
submitCreateForm() {
if (!this.newKB.name) {
this.showSnackbar('请输入知识库名称', 'warning');
this.showSnackbar(this.tm('messages.pleaseEnterKnowledgeBaseName'), 'warning');
return;
}
this.createCollection(
@@ -545,7 +552,7 @@ export default {
uploadFile() {
if (!this.selectedFile) {
this.showSnackbar('请先选择文件', 'warning');
this.showSnackbar(this.tm('messages.pleaseSelectFile'), 'warning');
return;
}
@@ -571,18 +578,18 @@ export default {
})
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('操作成功: ' + response.data.message);
this.showSnackbar(this.tm('messages.operationSuccess', { message: response.data.message }));
this.selectedFile = null;
//
this.getKBCollections();
} else {
this.showSnackbar(response.data.message || '上传失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.uploadFailed'), 'error');
}
})
.catch(error => {
console.error('Error uploading file:', error);
this.showSnackbar('文件上传失败', 'error');
this.showSnackbar(this.tm('messages.fileUploadFailed'), 'error');
})
.finally(() => {
this.uploading = false;
@@ -591,7 +598,7 @@ export default {
searchKnowledgeBase() {
if (!this.searchQuery.trim()) {
this.showSnackbar('请输入搜索内容', 'warning');
this.showSnackbar(this.tm('messages.pleaseEnterSearchContent'), 'warning');
return;
}
@@ -610,16 +617,16 @@ export default {
this.searchResults = response.data.data || [];
if (this.searchResults.length === 0) {
this.showSnackbar('没有找到匹配的内容', 'info');
this.showSnackbar(this.tm('messages.noMatchingContent'), 'info');
}
} else {
this.showSnackbar(response.data.message || '搜索失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.searchFailed'), 'error');
this.searchResults = [];
}
})
.catch(error => {
console.error('Error searching knowledge base:', error);
this.showSnackbar('搜索知识库失败', 'error');
this.showSnackbar(this.tm('messages.searchKnowledgeBaseFailed'), 'error');
this.searchResults = [];
})
.finally(() => {
@@ -645,7 +652,7 @@ export default {
deleteKnowledgeBase() {
if (!this.deleteTarget.collection_name) {
this.showSnackbar('删除目标不存在', 'error');
this.showSnackbar(this.tm('messages.deleteTargetNotExists'), 'error');
return;
}
@@ -658,16 +665,16 @@ export default {
})
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('知识库删除成功');
this.showSnackbar(this.tm('messages.knowledgeBaseDeleted'));
this.getKBCollections(); //
this.showDeleteDialog = false;
} else {
this.showSnackbar(response.data.message || '删除失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.deleteFailed'), 'error');
}
})
.catch(error => {
console.error('Error deleting knowledge base:', error);
this.showSnackbar('删除知识库失败', 'error');
this.showSnackbar(this.tm('messages.deleteKnowledgeBaseFailed'), 'error');
})
.finally(() => {
this.deleting = false;
@@ -684,13 +691,13 @@ export default {
if (response.data.status === 'ok') {
this.embeddingProviderConfigs = response.data.data || [];
} else {
this.showSnackbar(response.data.message || '获取嵌入模型列表失败', 'error');
this.showSnackbar(response.data.message || this.tm('messages.getEmbeddingModelListFailed'), 'error');
return [];
}
})
.catch(error => {
console.error('Error fetching embedding providers:', error);
this.showSnackbar('获取嵌入模型列表失败', 'error');
this.showSnackbar(this.tm('messages.getEmbeddingModelListFailed'), 'error');
return [];
});
},
+111 -61
View File
@@ -11,60 +11,60 @@
style="min-width: 450px; border: 1px solid #eee; border-radius: 8px; padding: 16px; padding-bottom: 0px; margin-left: 16px; max-height: calc(100% - 40px);">
<div>
<!-- <span style="color: #333333;">可视化</span> -->
<h3>筛选</h3>
<h3>{{ tm('filters.title') }}</h3>
<div style="margin-top: 8px;">
<v-autocomplete v-model="searchUserId" density="compact" :items="userIdList" variant="outlined"
label="筛选用户 ID"></v-autocomplete>
:label="tm('filters.userIdLabel')"></v-autocomplete>
</div>
<div style="display: flex; gap: 8px;">
<v-btn color="primary" @click="onNodeSelect" variant="tonal">
<v-icon start>mdi-magnify</v-icon>
筛选
{{ tm('filters.filterButton') }}
</v-btn>
<v-btn color="secondary" @click="resetFilter" variant="tonal">
<v-icon start>mdi-filter-remove</v-icon>
重置筛选
{{ tm('filters.resetButton') }}
</v-btn>
<v-btn color="primary" @click="refreshGraph" variant="tonal">
<v-icon start>mdi-refresh</v-icon>
刷新图形
{{ tm('filters.refreshButton') }}
</v-btn>
</div>
</div>
<!-- 新增搜索记忆功能 -->
<div class="mt-4">
<h3>搜索记忆</h3>
<h3>{{ tm('search.title') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div>
<v-text-field v-model="searchMemoryUserId" label="用户 ID" variant="outlined" density="compact" hide-details
<v-text-field v-model="searchMemoryUserId" :label="tm('search.userIdLabel')" variant="outlined" density="compact" hide-details
class="mb-2"></v-text-field>
<v-text-field v-model="searchQuery" label="输入关键词" variant="outlined" density="compact" hide-details
<v-text-field v-model="searchQuery" :label="tm('search.queryLabel')" variant="outlined" density="compact" hide-details
@keyup.enter="searchMemory" class="mb-2"></v-text-field>
<v-btn color="info" @click="searchMemory" :loading="isSearching" variant="tonal">
<v-icon start>mdi-text-search</v-icon>
搜索
{{ tm('search.searchButton') }}
</v-btn>
</div>
<!-- 新增搜索结果展示区域 -->
<div v-if="searchResults.length > 0" class="mt-3">
<v-divider class="mb-3"></v-divider>
<div class="text-subtitle-1 mb-2">搜索结果 ({{ searchResults.length }})</div>
<div class="text-subtitle-1 mb-2">{{ tm('search.resultsTitle') }} ({{ searchResults.length }})</div>
<v-expansion-panels variant="accordion">
<v-expansion-panel v-for="(result, index) in searchResults" :key="index">
<v-expansion-panel-title>
<div>
<span class="text-truncate d-inline-block" style="max-width: 300px;">{{ result.text.substring(0, 30)
}}...</span>
<span class="ms-2 text-caption text-grey">(相关度: {{ (result.score * 100).toFixed(1) }}%)</span>
<span class="ms-2 text-caption text-grey">({{ tm('search.similarity') }}: {{ (result.score * 100).toFixed(1) }}%)</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div>
<div class="mb-2 text-body-1">{{ result.text }}</div>
<div class="d-flex">
<span class="text-caption text-grey">文档ID: {{ result.doc_id }}</span>
<span class="text-caption text-grey">{{ tm('factDialog.docId') }}: {{ result.doc_id }}</span>
</div>
</div>
</v-expansion-panel-text>
@@ -72,68 +72,68 @@
</v-expansion-panels>
</div>
<div v-else-if="hasSearched" class="mt-3 text-center text-body-1 text-grey">
未找到相关记忆内容
{{ tm('search.noResults') }}
</div>
</v-card>
</div>
<!-- 新增添加记忆数据的表单 -->
<div class="mt-4">
<h3>添加记忆数据</h3>
<h3>{{ tm('addMemory.title') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<v-form @submit.prevent="addMemoryData">
<v-textarea v-model="newMemoryText" label="输入文本内容" variant="outlined" rows="4" hide-details
<v-textarea v-model="newMemoryText" :label="tm('addMemory.textLabel')" variant="outlined" rows="4" hide-details
class="mb-2"></v-textarea>
<v-text-field v-model="newMemoryUserId" label="用户 ID" variant="outlined" density="compact"
<v-text-field v-model="newMemoryUserId" :label="tm('addMemory.userIdLabel')" variant="outlined" density="compact"
hide-details></v-text-field>
<v-switch v-model="needSummarize" color="primary" label="需要摘要" hide-details></v-switch>
<v-switch v-model="needSummarize" color="primary" :label="tm('addMemory.summarizeLabel')" hide-details></v-switch>
<v-btn color="success" type="submit" :loading="isSubmitting" :disabled="!newMemoryText || !newMemoryUserId">
<v-icon start>mdi-plus</v-icon>
添加数据
{{ tm('addMemory.addButton') }}
</v-btn>
</v-form>
</v-card>
</div>
<div v-if="selectedNode" class="mt-4">
<h3>节点详情</h3>
<h3>{{ tm('nodeDetails.title') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div v-if="selectedNode.id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">ID:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.id') }}:</span>
<span>{{ selectedNode.id }}</span>
</div>
</div>
<div v-if="selectedNode._label">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">类型:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.type') }}:</span>
<span>{{ selectedNode._label }}</span>
</div>
</div>
<div v-if="selectedNode.name">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">名称:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.name') }}:</span>
<span>{{ selectedNode.name }}</span>
</div>
</div>
<div v-if="selectedNode.user_id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">用户ID:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.userId') }}:</span>
<span>{{ selectedNode.user_id }}</span>
</div>
</div>
<div v-if="selectedNode.ts">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">时间戳:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.timestamp') }}:</span>
<span>{{ selectedNode.ts }}</span>
</div>
</div>
<div v-if="selectedNode.type">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">类型:</span>
<span class="text-subtitle-2">{{ tm('nodeDetails.type') }}:</span>
<span>{{ selectedNode.type }}</span>
</div>
</div>
@@ -141,14 +141,14 @@
</div>
<div v-if="graphStats" class="mt-4">
<h3>图形统计</h3>
<h3>{{ tm('graphStats.title') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">节点数:</span>
<span class="text-subtitle-2">{{ tm('graphStats.nodeCount') }}:</span>
<span>{{ graphStats.nodeCount }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">边数:</span>
<span class="text-subtitle-2">{{ tm('graphStats.edgeCount') }}:</span>
<span>{{ graphStats.edgeCount }}</span>
</div>
</v-card>
@@ -158,7 +158,7 @@
<v-card class="fact-detail-card">
<v-card-title class="d-flex align-center bg-primary text-white px-4 py-3">
<v-icon class="mr-2" color="white">mdi-memory</v-icon>
记忆事实
{{ tm('factDialog.title') }}
<v-spacer></v-spacer>
<v-btn icon variant="text" color="white" @click="showFactDialog = false">
<v-icon>mdi-close</v-icon>
@@ -175,14 +175,14 @@
<v-col cols="6">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="primary" class="mr-2">mdi-identifier</v-icon>
<div class="text-subtitle-2">ID</div>
<div class="text-subtitle-2">{{ tm('factDialog.id') }}</div>
</div>
<div class="text-body-2 text-grey pa-1">{{ selectedEdgeFactData.id }}</div>
</v-col>
<v-col cols="6">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="primary" class="mr-2">mdi-file-document-outline</v-icon>
<div class="text-subtitle-2">文档ID</div>
<div class="text-subtitle-2">{{ tm('factDialog.docId') }}</div>
</div>
<div class="text-body-2 text-grey pa-1">{{ selectedEdgeFactData.doc_id }}</div>
</v-col>
@@ -193,14 +193,14 @@
<v-col cols="6">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="primary" class="mr-2">mdi-calendar-plus</v-icon>
<div class="text-subtitle-2">创建时间</div>
<div class="text-subtitle-2">{{ tm('factDialog.createdAt') }}</div>
</div>
<div class="text-body-2 text-grey pa-1">{{ formatTime(selectedEdgeFactData.created_at) }}</div>
</v-col>
<v-col cols="6">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="primary" class="mr-2">mdi-calendar-edit</v-icon>
<div class="text-subtitle-2">更新时间</div>
<div class="text-subtitle-2">{{ tm('factDialog.updatedAt') }}</div>
</div>
<div class="text-body-2 text-grey pa-1">{{ formatTime(selectedEdgeFactData.updated_at) }}</div>
</v-col>
@@ -210,14 +210,14 @@
<div v-if="parsedMetadata && Object.keys(parsedMetadata).length > 0" class="mt-4">
<div class="d-flex align-center mb-2">
<v-icon size="small" color="primary" class="mr-2">mdi-database-cog</v-icon>
<div class="text-subtitle-2">元数据</div>
<div class="text-subtitle-2">{{ tm('factDialog.metadata') }}</div>
</div>
<v-card variant="outlined" class="metadata-table">
<v-table density="compact" hover>
<thead>
<tr>
<th class="text-left"></th>
<th class="text-left"></th>
<th class="text-left">{{ tm('factDialog.metadataKey') }}</th>
<th class="text-left">{{ tm('factDialog.metadataValue') }}</th>
</tr>
</thead>
<tbody>
@@ -233,7 +233,7 @@
<div v-else class="text-center py-6">
<v-progress-circular indeterminate color="primary" size="50" width="5"></v-progress-circular>
<div class="mt-3 text-body-1">加载中...</div>
<div class="mt-3 text-body-1">{{ tm('factDialog.loading') }}</div>
</div>
</v-card-text>
@@ -241,7 +241,7 @@
<v-card-actions class="pa-4" v-if="selectedEdgeFactData">
<v-btn block color="primary" variant="tonal" @click="showFactDialog = false">
关闭
{{ tm('factDialog.close') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -253,9 +253,14 @@
<script>
import axios from 'axios';
import * as d3 from "d3"; // npm install d3
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'LongTermMemory',
setup() {
const { tm } = useModuleI18n('features/alkaid/memory');
return { tm };
},
data() {
return {
simulation: null,
@@ -310,15 +315,31 @@ export default {
this.ltmGetUserIds();
},
beforeUnmount() {
// D3仿
if (this.simulation) {
this.simulation.stop();
}
// DOM
if (this.svg) {
try {
this.svg.remove();
} catch (e) {
console.warn('Error removing SVG:', e);
}
}
//
this.nodes = [];
this.links = [];
this.userIdList = [];
this.searchResults = [];
},
methods: {
//
searchMemory() {
if (!this.searchQuery.trim()) {
this.$toast.warning('请输入搜索关键词');
this.$toast.warning(this.tm('messages.searchQueryRequired'));
return;
}
@@ -345,23 +366,23 @@ export default {
this.searchResults = Object.keys(data).map(doc_id => {
return {
doc_id: doc_id,
text: data[doc_id].text || '无文本内容',
text: data[doc_id].text || this.tm('search.noTextContent'),
score: data[doc_id].score || 0
};
});
if (this.searchResults.length === 0) {
this.$toast.info('未找到相关记忆内容');
this.$toast.info(this.tm('messages.searchNoResults'));
} else {
this.$toast.success(`找到 ${this.searchResults.length} 条相关记忆`);
this.$toast.success(this.tm('messages.searchSuccess', { count: this.searchResults.length }));
}
} else {
this.$toast.error('搜索失败: ' + response.data.message);
this.$toast.error(this.tm('messages.searchError') + ': ' + response.data.message);
}
})
.catch(error => {
console.error('搜索记忆数据失败:', error);
this.$toast.error('搜索失败: ' + (error.response?.data?.message || error.message));
this.$toast.error(this.tm('messages.searchError') + ': ' + (error.response?.data?.message || error.message));
})
.finally(() => {
this.isSearching = false;
@@ -393,11 +414,11 @@ export default {
// this.needSummarize = false;
//
this.$toast.success('记忆数据添加成功!');
this.$toast.success(this.tm('messages.addSuccess'));
})
.catch(error => {
console.error('添加记忆数据失败:', error);
this.$toast.error('添加记忆数据失败: ' + (error.response?.data?.message || error.message));
this.$toast.error(this.tm('messages.addError') + ': ' + (error.response?.data?.message || error.message));
})
.finally(() => {
this.isSubmitting = false;
@@ -410,8 +431,10 @@ export default {
axios.get('/api/plug/alkaid/ltm/graph', { params })
.then(response => {
let nodesRaw = response.data.data.nodes;
let edgesRaw = response.data.data.edges;
const data = response.data.data || {};
// data
let nodesRaw = data && Array.isArray(data.nodes) ? data.nodes : [];
let edgesRaw = data && Array.isArray(data.edges) ? data.edges : [];
this.node_data = nodesRaw;
this.edge_data = edgesRaw;
@@ -453,6 +476,11 @@ export default {
})
.catch(error => {
console.error('Error fetching graph data:', error);
//
this.nodes = [];
this.links = [];
this.node_data = [];
this.edge_data = [];
})
.finally(() => {
this.isLoading = false;
@@ -462,10 +490,13 @@ export default {
ltmGetUserIds() {
axios.get('/api/plug/alkaid/ltm/user_ids')
.then(response => {
this.userIdList = response.data.data;
//
const data = response.data.data;
this.userIdList = Array.isArray(data) ? data : [];
})
.catch(error => {
console.error('Error fetching user IDs:', error);
this.userIdList = []; //
});
},
@@ -514,12 +545,12 @@ export default {
this.parsedMetadata = this.parseMetadata(this.selectedEdgeFactData.metadata);
this.showFactDialog = true;
} else {
this.$toast.error('获取记忆详情失败: ' + response.data.message);
this.$toast.error(this.tm('messages.factDetailsError') + ': ' + response.data.message);
}
})
.catch(error => {
console.error('获取记忆详情失败:', error);
this.$toast.error('获取记忆详情失败: ' + (error.response?.data?.message || error.message));
this.$toast.error(this.tm('messages.factDetailsError') + ': ' + (error.response?.data?.message || error.message));
})
.finally(() => {
this.isLoadingFactData = false;
@@ -548,13 +579,13 @@ export default {
return { value: String(metadata) };
} catch (e) {
console.error('解析元数据出错:', e);
return { error: '无法解析元数据' };
return { error: this.tm('messages.metadataParseError') };
}
},
//
formatMetadataValue(value) {
if (value === null || value === undefined) return '无';
if (value === null || value === undefined) return this.tm('factDialog.noValue');
if (typeof value === 'object') {
return JSON.stringify(value);
@@ -565,7 +596,7 @@ export default {
//
formatTime(timestamp) {
if (!timestamp) return '未知';
if (!timestamp) return this.tm('factDialog.unknown');
try {
return new Date(timestamp).toLocaleString();
} catch (e) {
@@ -575,10 +606,20 @@ export default {
initD3Graph() {
const container = document.getElementById("graph-container");
if (!container) return;
d3.select("#graph-container svg").remove();
const width = container.clientWidth;
const height = container.clientHeight;
if (!container) {
console.warn('Graph container not found');
return;
}
// SVG
try {
d3.select("#graph-container svg").remove();
} catch (e) {
console.warn('Error removing existing SVG:', e);
}
const width = container.clientWidth || 800;
const height = container.clientHeight || 600;
const svg = d3.select("#graph-container")
.append("svg")
.attr("width", "100%")
@@ -608,9 +649,18 @@ export default {
},
updateD3Graph() {
if (!this.svg || !this.simulation) return;
if (!this.svg || !this.simulation || !this.g) {
console.warn('D3 elements not ready for update');
return;
}
const g = this.g;
g.selectAll("*").remove();
try {
g.selectAll("*").remove();
} catch (e) {
console.warn('Error clearing D3 graph:', e);
return;
}
//
g.append("defs").append("marker")
@@ -659,7 +709,7 @@ export default {
this.selectedEdgeFactId = factId;
this.getFactDetails(factId);
} else {
this.$toast.info('该关系没有关联的记忆数据');
this.$toast.info(this.tm('messages.relationNoMemoryData'));
}
});
+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">前面的世界以后再来探索吧</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: 24px !important; opacity: 0.7 !important; align-self: center !important; border-color: rgba(94, 53, 177, 0.4) !important;"></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: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></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%;
@@ -283,16 +399,65 @@ onMounted(() => {
}
.custom-divider {
border-color: rgba(94, 53, 177, 0.1) !important;
border-color: rgba(94, 53, 177, 0.3) !important;
opacity: 1;
}
.login-page-container-dark .custom-divider {
border-color: rgba(114, 46, 209, 0.08) !important;
border-color: rgba(180, 148, 246, 0.4) !important;
}
.loginBox {
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>
@@ -8,15 +8,15 @@
</div>
<div class="stat-content">
<div class="stat-title">运行时间</div>
<h3 class="uptime-value">{{ stat.running || '加载中...' }}</h3>
<div class="stat-title">{{ tm('features.dashboard.status.uptime') }}</div>
<h3 class="uptime-value">{{ stat.running || tm('features.dashboard.status.loading') }}</h3>
</div>
<v-spacer></v-spacer>
<div class="uptime-status">
<v-icon icon="mdi-circle" size="10" color="success" class="blink-animation"></v-icon>
<span class="status-text">在线</span>
<span class="status-text">{{ tm('features.dashboard.status.online') }}</span>
</div>
</div>
</v-card-text>
@@ -30,7 +30,7 @@
</div>
<div class="stat-content">
<div class="stat-title">内存占用</div>
<div class="stat-title">{{ tm('features.dashboard.status.memoryUsage') }}</div>
<div class="memory-values">
<h3 class="memory-value">{{ stat.memory?.process || 0 }} <span class="memory-unit">MiB</span></h3>
<span class="memory-separator">/</span>
@@ -53,13 +53,19 @@
</template>
<script>
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'OnlineTime',
setup() {
const { tm } = useModuleI18n('features/dashboard');
return { tm };
},
props: ['stat'],
data: () => ({
stat: {
memory: { process: 0, system: 0 },
running: "加载中...",
running: "",
},
}),
computed: {
@@ -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;