feat: 继续完成剩下的组件

- AlkaidPage_sigma.vue
- PlatformPage.vue
- LongTermMemory.vue
- KnowledgeBase.vue
This commit is contained in:
IGCrystal
2025-06-17 09:24:51 +08:00
parent 7c27520d57
commit 8275130f04
10 changed files with 556 additions and 182 deletions
@@ -22,5 +22,23 @@
"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"
}
}
@@ -29,5 +29,108 @@
"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"
}
}
@@ -32,6 +32,63 @@
"all": "All",
"category": "By Category",
"importance": "By Importance",
"dateRange": "By Date Range"
"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"
},
"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"
},
"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"
}
}
@@ -22,5 +22,23 @@
"beta": "测试版",
"stable": "稳定版",
"deprecated": "已弃用"
},
"sigma": {
"subtitle": "AstrBot 实验性项目",
"visualization": "可视化",
"filterUserId": "筛选用户 ID",
"filter": "筛选",
"resetFilter": "重置筛选",
"refreshGraph": "刷新图形",
"nodeDetails": "节点详情",
"id": "ID",
"type": "类型",
"name": "名称",
"userId": "用户ID",
"timestamp": "时间戳",
"graphStats": "图形统计",
"nodeCount": "节点数",
"edgeCount": "边数",
"inDevelopment": "功能开发中"
}
}
@@ -29,5 +29,108 @@
"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": "获取嵌入模型列表失败"
}
}
@@ -32,6 +32,63 @@
"all": "全部",
"category": "按分类",
"importance": "按重要程度",
"dateRange": "按时间范围"
"dateRange": "按时间范围",
"title": "筛选",
"userIdLabel": "筛选用户 ID",
"filterButton": "筛选",
"resetButton": "重置筛选",
"refreshButton": "刷新图形"
},
"search": {
"title": "搜索记忆",
"userIdLabel": "用户 ID",
"queryLabel": "输入关键词",
"searchButton": "搜索",
"resultsTitle": "搜索结果",
"noResults": "未找到相关记忆内容",
"similarity": "相关度"
},
"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": "无"
},
"messages": {
"searchQueryRequired": "请输入搜索关键词",
"searchSuccess": "找到 {count} 条相关记忆",
"searchNoResults": "未找到相关记忆内容",
"searchError": "搜索失败",
"addSuccess": "记忆数据添加成功!",
"addError": "添加记忆数据失败",
"factDetailsError": "获取记忆详情失败",
"metadataParseError": "无法解析元数据"
}
}
+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,
+5 -5
View File
@@ -177,11 +177,11 @@ export default {
// 安全访问翻译的计算属性
messages() {
return {
updateSuccess: "更新成功!",
addSuccess: "添加成功!",
deleteSuccess: "删除成功!",
statusUpdateSuccess: "状态更新成功!",
deleteConfirm: "确定要删除平台适配器"
updateSuccess: this.tm('features.platform.messages.updateSuccess'),
addSuccess: this.tm('features.platform.messages.addSuccess'),
deleteSuccess: this.tm('features.platform.messages.deleteSuccess'),
statusUpdateSuccess: this.tm('features.platform.messages.statusUpdateSuccess'),
deleteConfirm: this.tm('features.platform.messages.deleteConfirm')
};
}
},
+84 -77
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>还没有安装知识库插件
<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">知识库列表
<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 [];
});
},
+54 -49
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,
@@ -334,7 +339,7 @@ export default {
// 添加搜索记忆方法
searchMemory() {
if (!this.searchQuery.trim()) {
this.$toast.warning('请输入搜索关键词');
this.$toast.warning(this.tm('messages.searchQueryRequired'));
return;
}
@@ -367,17 +372,17 @@ export default {
});
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;
@@ -409,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;
@@ -540,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;
@@ -574,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);