perf: knowledgebase delete

This commit is contained in:
Soulter
2025-05-23 15:50:10 +08:00
parent ad51381063
commit 43a45f18ce
2 changed files with 219 additions and 109 deletions
+1
View File
@@ -71,6 +71,7 @@ class AstrBotDashboard:
route, view_handler, methods, _ = api
if route == f"/{subpath}" and request.method in methods:
return await view_handler(*args, **kwargs)
return jsonify(Response().error("未找到该路由").__dict__)
async def auth_middleware(self):
if not request.path.startswith("/api"):
+218 -109
View File
@@ -2,14 +2,21 @@
<div class="flex-grow-1" style="display: flex; flex-direction: column; height: 100%;">
<div style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px; padding: 16px">
<!-- knowledge card -->
<div v-if="kbCollections.length == 0" class="d-flex align-center justify-center flex-column"
<div v-if="!installed" class="d-flex align-center justify-center flex-column"
style="flex-grow: 1; width: 100%; height: 100%;">
<h2>还没有安装知识库插件</h2>
<v-btn style="margin-top: 16px;" variant="tonal" color="primary"
@click="installPlugin" :loading="installing">
立即安装
</v-btn>
</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>
<v-btn style="margin-top: 16px;" variant="tonal" color="primary" @click="showCreateDialog = true">
创建知识库
</v-btn>
</div>
<div v-else>
<h2 class="mb-4">知识库列表</h2>
<v-btn class="mb-4" prepend-icon="mdi-plus" variant="tonal" color="primary"
@@ -27,10 +34,20 @@
</div>
<div class="kb-name">{{ kb.collection_name }}</div>
<div class="kb-count">{{ kb.count || 0 }} 条知识</div>
<div class="kb-actions">
<v-btn icon variant="text" size="small" color="error" @click.stop="confirmDelete(kb)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</div>
</div>
</div>
<div style="padding: 16px; text-align: center;">
<small style="color: #a3a3a3">Tips: 在聊天页面通过 /kb 指令了解如何使用</small>
</div>
</div>
</div>
<!-- 创建知识库对话框 -->
@@ -96,13 +113,13 @@
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<v-tabs v-model="activeTab">
<v-tab value="upload">上传文件</v-tab>
<v-tab value="search">搜索内容</v-tab>
</v-tabs>
<v-window v-model="activeTab" class="mt-4">
<!-- 上传文件标签页 -->
<v-window-item value="upload">
@@ -111,21 +128,14 @@
<h3>上传文件到知识库</h3>
<p class="text-subtitle-1">支持 txtpdfwordexcel 等多种格式</p>
</div>
<div class="upload-zone"
@dragover.prevent
@drop.prevent="onFileDrop"
@click="triggerFileInput">
<input
type="file"
ref="fileInput"
style="display: none"
@change="onFileSelected"
/>
<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>
</div>
<div class="selected-files mt-4" v-if="selectedFile">
<div type="info" variant="tonal" class="d-flex align-center">
<div>
@@ -136,71 +146,53 @@
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div class="text-center mt-4">
<v-btn
color="primary"
variant="elevated"
:loading="uploading"
:disabled="!selectedFile"
@click="uploadFile"
>
<v-btn color="primary" variant="elevated" :loading="uploading"
:disabled="!selectedFile" @click="uploadFile">
上传到知识库
</v-btn>
</div>
</div>
<div class="upload-progress mt-4" v-if="uploading">
<v-progress-linear
indeterminate
color="primary"
></v-progress-linear>
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</div>
</div>
</v-window-item>
<!-- 搜索内容标签页 -->
<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"
variant="outlined"
class="flex-grow-1 me-2"
@click:append="searchKnowledgeBase"
@keyup.enter="searchKnowledgeBase"
placeholder="输入关键词搜索知识库内容..."
hide-details
></v-text-field>
<v-select
v-model="topK"
:items="[3, 5, 10, 20]"
label="结果数量"
variant="outlined"
style="max-width: 120px;"
hide-details
></v-select>
<v-text-field v-model="searchQuery" label="搜索知识库内容" append-icon="mdi-magnify"
variant="outlined" class="flex-grow-1 me-2" @click:append="searchKnowledgeBase"
@keyup.enter="searchKnowledgeBase" placeholder="输入关键词搜索知识库内容..."
hide-details></v-text-field>
<v-select v-model="topK" :items="[3, 5, 10, 20]" label="结果数量" 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>
</div>
<div v-else-if="searchResults.length > 0">
<h3 class="mb-2">搜索结果</h3>
<v-card v-for="(result, index) in searchResults" :key="index"
class="mb-4 search-result-card" variant="outlined">
<v-card-text>
<div class="d-flex align-center mb-2">
<v-icon class="me-2" size="small" color="primary">mdi-file-document-outline</v-icon>
<span class="text-caption text-medium-emphasis">{{ result.metadata.source }}</span>
<v-icon class="me-2" size="small"
color="primary">mdi-file-document-outline</v-icon>
<span class="text-caption text-medium-emphasis">{{
result.metadata.source }}</span>
<v-spacer></v-spacer>
<v-chip v-if="result.score" size="small" color="primary" variant="tonal">
<v-chip v-if="result.score" size="small" color="primary"
variant="tonal">
相关度: {{ Math.round(result.score * 100) }}%
</v-chip>
</div>
@@ -208,7 +200,7 @@
</v-card-text>
</v-card>
</div>
<div v-else-if="searchPerformed">
<v-alert type="info" variant="tonal">
没有找到匹配的内容
@@ -222,6 +214,22 @@
</v-card>
</v-dialog>
<!-- 删除知识库确认对话框 -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h5">确认删除</v-card-title>
<v-card-text>
<p>您确定要删除知识库 <span class="font-weight-bold">{{ deleteTarget.collection_name }}</span> </p>
<p class="text-red">此操作不可逆所有知识库内容将被永久删除</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-card-actions>
</v-card>
</v-dialog>
<!-- 消息提示 -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color">
{{ snackbar.text }}
@@ -236,6 +244,8 @@ export default {
name: 'KnowledgeBase',
data() {
return {
installed: true,
installing: false,
kbCollections: [],
showCreateDialog: false,
showEmojiPicker: false,
@@ -287,13 +297,58 @@ export default {
searchResults: [],
searching: false,
searchPerformed: false,
topK: 5
topK: 5,
showDeleteDialog: false,
deleteTarget: {
collection_name: ''
},
deleting: false
}
},
mounted() {
this.getKBCollections();
this.checkPlugin();
},
methods: {
checkPlugin() {
axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
.then(response => {
if (response.data.status !== 'ok') {
this.showSnackbar('插件未安装或不可用', 'error');
}
if (response.data.data.length > 0) {
this.installed = true;
this.getKBCollections();
} else {
this.installed = false;
}
})
.catch(error => {
console.error('Error checking plugin:', error);
this.showSnackbar('检查插件失败', 'error');
})
},
installPlugin() {
this.installing = true;
axios.post('/api/plugin/install', {
url: "https://github.com/soulter/astrbot_plugin_knowledge_base",
proxy: localStorage.getItem('selectedGitHubProxy') || ""
})
.then(response => {
if (response.data.status === 'ok') {
this.checkPlugin();
} else {
this.showSnackbar(response.data.message || '安装失败', 'error');
}
})
.catch(error => {
console.error('Error installing plugin:', error);
this.showSnackbar('安装插件失败', 'error');
}).finally(() => {
this.installing = false;
});
},
getKBCollections() {
axios.get('/api/plug/alkaid/kb/collections')
.then(response => {
@@ -353,7 +408,7 @@ export default {
this.showContentDialog = true;
this.resetContentDialog();
},
resetContentDialog() {
this.activeTab = 'upload';
this.selectedFile = null;
@@ -361,28 +416,28 @@ export default {
this.searchResults = [];
this.searchPerformed = false;
},
triggerFileInput() {
this.$refs.fileInput.click();
},
onFileSelected(event) {
const files = event.target.files;
if (files.length > 0) {
this.selectedFile = files[0];
}
},
onFileDrop(event) {
const files = event.dataTransfer.files;
if (files.length > 0) {
this.selectedFile = files[0];
}
},
getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
switch (extension) {
case 'pdf':
return 'mdi-file-pdf-box';
@@ -401,53 +456,53 @@ export default {
return 'mdi-file-outline';
}
},
uploadFile() {
if (!this.selectedFile) {
this.showSnackbar('请先选择文件', 'warning');
return;
}
this.uploading = true;
const formData = new FormData();
formData.append('file', this.selectedFile);
formData.append('collection_name', this.currentKB.collection_name);
axios.post('/api/plug/alkaid/kb/collection/add_file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('文件上传成功');
this.selectedFile = null;
// 刷新知识库列表,获取更新的数量
this.getKBCollections();
} else {
this.showSnackbar(response.data.message || '上传失败', 'error');
}
})
.catch(error => {
console.error('Error uploading file:', error);
this.showSnackbar('文件上传失败', 'error');
})
.finally(() => {
this.uploading = false;
});
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('文件上传成功');
this.selectedFile = null;
// 刷新知识库列表,获取更新的数量
this.getKBCollections();
} else {
this.showSnackbar(response.data.message || '上传失败', 'error');
}
})
.catch(error => {
console.error('Error uploading file:', error);
this.showSnackbar('文件上传失败', 'error');
})
.finally(() => {
this.uploading = false;
});
},
searchKnowledgeBase() {
if (!this.searchQuery.trim()) {
this.showSnackbar('请输入搜索内容', 'warning');
return;
}
this.searching = true;
this.searchPerformed = true;
axios.get(`/api/plug/alkaid/kb/collection/search`, {
params: {
collection_name: this.currentKB.collection_name,
@@ -455,26 +510,26 @@ export default {
top_k: this.topK
}
})
.then(response => {
if (response.data.status === 'ok') {
this.searchResults = response.data.data || [];
if (this.searchResults.length === 0) {
this.showSnackbar('没有找到匹配的内容', 'info');
.then(response => {
if (response.data.status === 'ok') {
this.searchResults = response.data.data || [];
if (this.searchResults.length === 0) {
this.showSnackbar('没有找到匹配的内容', 'info');
}
} else {
this.showSnackbar(response.data.message || '搜索失败', 'error');
this.searchResults = [];
}
} else {
this.showSnackbar(response.data.message || '搜索失败', 'error');
})
.catch(error => {
console.error('Error searching knowledge base:', error);
this.showSnackbar('搜索知识库失败', 'error');
this.searchResults = [];
}
})
.catch(error => {
console.error('Error searching knowledge base:', error);
this.showSnackbar('搜索知识库失败', 'error');
this.searchResults = [];
})
.finally(() => {
this.searching = false;
});
})
.finally(() => {
this.searching = false;
});
},
showSnackbar(text, color = 'success') {
@@ -486,7 +541,43 @@ export default {
selectEmoji(emoji) {
this.newKB.emoji = emoji;
this.showEmojiPicker = false;
}
},
confirmDelete(kb) {
this.deleteTarget = kb;
this.showDeleteDialog = true;
},
deleteKnowledgeBase() {
if (!this.deleteTarget.collection_name) {
this.showSnackbar('删除目标不存在', 'error');
return;
}
this.deleting = true;
axios.get('/api/plug/alkaid/kb/collection/delete', {
params: {
collection_name: this.deleteTarget.collection_name
}
})
.then(response => {
if (response.data.status === 'ok') {
this.showSnackbar('知识库删除成功');
this.getKBCollections(); // 刷新列表
this.showDeleteDialog = false;
} else {
this.showSnackbar(response.data.message || '删除失败', 'error');
}
})
.catch(error => {
console.error('Error deleting knowledge base:', error);
this.showSnackbar('删除知识库失败', 'error');
})
.finally(() => {
this.deleting = false;
});
},
}
}
</script>
@@ -502,7 +593,7 @@ export default {
.kb-card {
height: 280px;
border-radius: 8px;
overflow: hidden;
overflow: hidden;
position: relative;
cursor: pointer;
display: flex;
@@ -638,4 +729,22 @@ export default {
background-color: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
.kb-actions {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 8px;
opacity: 0;
transition: opacity 0.2s ease;
}
.kb-card {
position: relative;
}
.kb-card:hover .kb-actions {
opacity: 1;
}
</style>