perf: knowledgebase delete
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -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">支持 txt、pdf、word、excel 等多种格式</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>
|
||||
|
||||
Reference in New Issue
Block a user