From 61aac9c80cd827536f67749c8f280675958314c9 Mon Sep 17 00:00:00 2001
From: RC-CHN <67079377+RC-CHN@users.noreply.github.com>
Date: Thu, 14 Aug 2025 14:01:11 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87?=
=?UTF-8?q?=E8=A7=A3=E6=9E=90URL=20=E7=9A=84=E6=96=B9=E5=BC=8F=E5=AF=BC?=
=?UTF-8?q?=E5=85=A5=E7=BD=91=E9=A1=B5=E6=95=B0=E6=8D=AE=E5=88=B0=E7=9F=A5?=
=?UTF-8?q?=E8=AF=86=E5=BA=93=20(#2280)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片)
* fix:上传后清空value,允许触发change事件以多次上传同一张图片
* perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致
* perf:将文件输入的值重置为空字符串以提升浏览器兼容性
* feat:webchat文件上传按钮支持多选文件上传
* fix:释放blob URL以防止内存泄漏
* perf:并行化sendMessage中的图片获取逻辑
* feat:完成从url获取部分的UI
* feat: 添加从URL导入功能的组件
* fix: 优化导入结果处理,添加整体摘要和主题摘要的文件命名
* perf: 更新url导入选项添加默认值
* perf: 在导入url的部分配置项未启用时隐藏暂不使用的下拉框选项
* feat: 添加上传前提提示信息至导入url至知识库功能
* feat: 更新导入功能提示信息,添加上传状态通知
* fix: 优化url转知识库错误处理
* feat: 合并知识库的上传文件和 URL 标签页
* feat: 删除导入URL至知识库功能的相关组件
---------
Co-authored-by: Soulter <905617992@qq.com>
---
.../en-US/features/alkaid/knowledge-base.json | 26 +-
.../zh-CN/features/alkaid/knowledge-base.json | 26 +-
dashboard/src/views/alkaid/KnowledgeBase.vue | 519 +++++++++++++++---
3 files changed, 483 insertions(+), 88 deletions(-)
diff --git a/dashboard/src/i18n/locales/en-US/features/alkaid/knowledge-base.json b/dashboard/src/i18n/locales/en-US/features/alkaid/knowledge-base.json
index 17452504a..c985229af 100644
--- a/dashboard/src/i18n/locales/en-US/features/alkaid/knowledge-base.json
+++ b/dashboard/src/i18n/locales/en-US/features/alkaid/knowledge-base.json
@@ -75,7 +75,8 @@
"usage": "Usage: Enter \"/kb use {name}\" in the chat page",
"tabs": {
"upload": "Upload Files",
- "search": "Search Content"
+ "search": "Search Content",
+ "fromURL": "From URL"
}
},
"upload": {
@@ -132,5 +133,26 @@
"deleteFailed": "Deletion failed",
"deleteKnowledgeBaseFailed": "Failed to delete knowledge base",
"getEmbeddingModelListFailed": "Failed to get embedding model list"
+ },
+ "importFromUrl": {
+ "title": "Import from URL",
+ "urlLabel": "Web Page URL",
+ "urlPlaceholder": "Enter the URL of the web page to extract knowledge from",
+ "optionsTitle": "Import Options",
+ "tooltip": "These options control how text is extracted and processed from the URL content.\nLeave blank to use the plugin's default settings.\nEnabling LLM text repair and summary may take a long time.",
+ "useLlmRepairLabel": "Enable LLM Text Repair",
+ "useClusteringSummaryLabel": "Enable Clustering Summary",
+ "repairLlmProviderIdLabel": "Text Repair Model",
+ "summarizeLlmProviderIdLabel": "Summarize Model",
+ "embeddingProviderIdLabel": "Embedding Model",
+ "chunkSizeLabel": "Chunk Size",
+ "chunkOverlapLabel": "Chunk Overlap",
+ "startImport": "Start Import",
+ "importing": "Importing...",
+ "importSuccess": "Import Successful",
+ "importFailed": "Import Failed",
+ "uploadingChunks": "Content extracted successfully, uploading chunks...",
+ "preRequisite": "Hint: Please go to the plugin market to install astrbot_plugin_url_2_knowledge_base and follow the instructions in the plugin documentation to complete the playwright installation before using this feature.",
+ "allChunksUploaded": "All chunks uploaded successfully"
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/dashboard/src/i18n/locales/zh-CN/features/alkaid/knowledge-base.json b/dashboard/src/i18n/locales/zh-CN/features/alkaid/knowledge-base.json
index 8e0a53e9e..2e1c8c81a 100644
--- a/dashboard/src/i18n/locales/zh-CN/features/alkaid/knowledge-base.json
+++ b/dashboard/src/i18n/locales/zh-CN/features/alkaid/knowledge-base.json
@@ -75,7 +75,8 @@
"usage": "使用方式: 在聊天页中输入 \"/kb use {name}\"",
"tabs": {
"upload": "上传文件",
- "search": "搜索内容"
+ "search": "搜索内容",
+ "fromURL": "从URL导入"
}
},
"upload": {
@@ -132,5 +133,26 @@
"deleteFailed": "删除失败",
"deleteKnowledgeBaseFailed": "删除知识库失败",
"getEmbeddingModelListFailed": "获取嵌入模型列表失败"
+ },
+ "importFromUrl": {
+ "title": "从 URL 导入",
+ "urlLabel": "网页 URL",
+ "urlPlaceholder": "请输入要提取知识的网页地址",
+ "optionsTitle": "导入选项",
+ "tooltip": "这些选项控制如何从URL内容中提取和处理文本。\n留空将使用插件的默认设置。\n启用LLM文本修复和摘要后可能花费时间较长。",
+ "useLlmRepairLabel": "启用LLM文本修复",
+ "useClusteringSummaryLabel": "启用聚类摘要",
+ "repairLlmProviderIdLabel": "文本修复模型",
+ "summarizeLlmProviderIdLabel": "摘要模型",
+ "embeddingProviderIdLabel": "嵌入模型",
+ "chunkSizeLabel": "分片长度",
+ "chunkOverlapLabel": "重叠长度",
+ "startImport": "开始导入",
+ "importing": "正在导入...",
+ "importSuccess": "导入成功",
+ "importFailed": "导入失败",
+ "uploadingChunks": "内容提取成功,正在上传分片...",
+ "preRequisite": "提示:请先前往插件市场安装 astrbot_plugin_url_2_knowledge_base 并根据插件文档内的指示完成 playwright 安装后才可使用本功能",
+ "allChunksUploaded": "所有分片上传成功"
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/dashboard/src/views/alkaid/KnowledgeBase.vue b/dashboard/src/views/alkaid/KnowledgeBase.vue
index 637da2575..cf606e3a9 100644
--- a/dashboard/src/views/alkaid/KnowledgeBase.vue
+++ b/dashboard/src/views/alkaid/KnowledgeBase.vue
@@ -12,7 +12,9 @@
:loading="installing">
{{ tm('notInstalled.install') }}
-
+
@@ -75,13 +77,16 @@
-
+
-
+ :item-props="embeddingModelProps" :label="tm('createDialog.embeddingModelLabel')"
+ variant="outlined" class="mt-2">
{{ tm('createDialog.tips') }}
@@ -89,8 +94,10 @@
- {{ tm('createDialog.cancel') }}
- {{ tm('createDialog.create') }}
+ {{ tm('createDialog.cancel')
+ }}
+ {{ tm('createDialog.create')
+ }}
@@ -114,7 +121,8 @@
- {{ tm('emojiPicker.close') }}
+ {{ tm('emojiPicker.close')
+ }}
@@ -134,89 +142,177 @@
mdi-database
- {{ tm('contentDialog.embeddingModel') }}: {{ currentKB._embedding_provider_config.embedding_model }}
+ {{ tm('contentDialog.embeddingModel') }}: {{
+ currentKB._embedding_provider_config.embedding_model }}
mdi-vector-point
- {{ tm('contentDialog.vectorDimension') }}: {{ currentKB._embedding_provider_config.embedding_dimensions }}
+ {{ tm('contentDialog.vectorDimension') }}: {{
+ currentKB._embedding_provider_config.embedding_dimensions }}
💡 使用方式: 在聊天页中输入 “/kb use {{ currentKB.collection_name }}”
- {{ tm('contentDialog.tabs.upload') }}
+ 导入数据
{{ tm('contentDialog.tabs.search') }}
-
-
-
-
-
{{ tm('upload.title') }}
-
{{ tm('upload.subtitle') }}
+
+
+
+
-
-
-
mdi-cloud-upload
-
{{ tm('upload.dropzone') }}
-
+
+
-
-
-
- mdi-puzzle-outline
- {{ tm('upload.chunkSettings.title') }}
-
-
-
- mdi-information-outline
-
-
-
- {{ tm('upload.chunkSettings.tooltip') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ getFileIcon(selectedFile.name) }}
- {{ selectedFile.name }}
-
-
- mdi-close
-
+
+
+
+
+
mdi-cloud-upload
+
{{ tm('upload.dropzone') }}
-
-
- {{ tm('upload.upload') }}
-
+
+
+
+ mdi-puzzle-outline
+ {{
+ tm('upload.chunkSettings.title') }}
+
+
+
+ mdi-information-outline
+
+
+
+ {{ tm('upload.chunkSettings.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getFileIcon(selectedFile.name) }}
+ {{ selectedFile.name }}
+
+
+ mdi-close
+
+
+
+
+
+ {{ tm('upload.upload') }}
+
+
+
+
+
+
-
-
+
+
+
+ {{ tm('importFromUrl.preRequisite') }}
+
+
+
+
+
+ mdi-cog-outline
+ {{ tm('importFromUrl.optionsTitle') }}
+
+
+ mdi-information-outline
+
+ {{ tm('importFromUrl.tooltip') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('importFromUrl.startImport') }}
+
+
@@ -225,12 +321,13 @@
-
+
-
@@ -253,7 +350,8 @@
- {{ tm('search.relevance') }}: {{ Math.round(result.score * 100) }}%
+ {{ tm('search.relevance') }}: {{ Math.round(result.score * 100)
+ }}%
{{ result.content }}
@@ -284,8 +382,11 @@
- {{ tm('deleteDialog.cancel') }}
- {{ tm('deleteDialog.delete') }}
+ {{
+ tm('deleteDialog.cancel')
+ }}
+ {{
+ tm('deleteDialog.delete') }}
@@ -360,7 +461,12 @@ export default {
collection_name: '',
emoji: ''
},
- activeTab: 'upload',
+ activeTab: 'import',
+ dataSource: 'file',
+ dataSourceOptions: [
+ { title: '从文件', value: 'file', icon: 'mdi-file-upload' },
+ { title: '从URL', value: 'url', icon: 'mdi-web' }
+ ],
selectedFile: null,
chunkSize: null,
overlap: null,
@@ -375,20 +481,78 @@ export default {
collection_name: ''
},
deleting: false,
- embeddingProviderConfigs: []
+ embeddingProviderConfigs: [],
+ llmProviderConfigs: [],
+ // URL导入相关数据
+ importUrl: '',
+ importOptions: {
+ use_llm_repair: true,
+ use_clustering_summary: false,
+ repair_llm_provider_id: null,
+ summarize_llm_provider_id: null,
+ embedding_provider_id: null,
+ chunk_size: 300,
+ chunk_overlap: 50,
+ },
+ importing: false,
+ pollingInterval: null,
+ }
+ },
+ computed: {
+ optionalSelectorColWidth() {
+ const repairOn = this.importOptions.use_llm_repair;
+ const summaryOn = this.importOptions.use_clustering_summary;
+ if (repairOn && summaryOn) {
+ return 6; // Both on, each takes half
+ }
+ return 12; // Only one is on, it takes full width
+ }
+ },
+ watch: {
+ llmProviderConfigs: {
+ handler(newVal) {
+ if (newVal && newVal.length > 0) {
+ if (!this.importOptions.repair_llm_provider_id) {
+ this.importOptions.repair_llm_provider_id = newVal[0].id;
+ }
+ if (!this.importOptions.summarize_llm_provider_id) {
+ this.importOptions.summarize_llm_provider_id = newVal[0].id;
+ }
+ }
+ },
+ immediate: true,
+ deep: true
+ },
+ embeddingProviderConfigs: {
+ handler(newVal) {
+ if (newVal && newVal.length > 0) {
+ if (!this.importOptions.embedding_provider_id) {
+ this.importOptions.embedding_provider_id = newVal[0].id;
+ }
+ }
+ },
+ immediate: true,
+ deep: true
}
},
mounted() {
this.checkPlugin();
this.getEmbeddingProviderList();
+ this.getLlmProviderList();
},
methods: {
+ llmModelProps(providerConfig) {
+ return {
+ title: providerConfig.llm_model || providerConfig.id,
+ subtitle: `Provider ID: ${providerConfig.id}`,
+ }
+ },
embeddingModelProps(providerConfig) {
return {
title: providerConfig.embedding_model,
- subtitle: this.tm('createDialog.providerInfo', {
- id: providerConfig.id,
- dimensions: providerConfig.embedding_dimensions
+ subtitle: this.tm('createDialog.providerInfo', {
+ id: providerConfig.id,
+ dimensions: providerConfig.embedding_dimensions
}),
}
},
@@ -500,7 +664,8 @@ export default {
},
resetContentDialog() {
- this.activeTab = 'upload';
+ this.activeTab = 'import';
+ this.dataSource = 'file';
this.selectedFile = null;
this.searchQuery = '';
this.searchResults = [];
@@ -508,6 +673,13 @@ export default {
// 重置分片长度和重叠长度参数
this.chunkSize = null;
this.overlap = null;
+ // 重置URL导入相关数据
+ this.importUrl = '';
+ this.importing = false;
+ if (this.pollingInterval) {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ }
},
triggerFileInput() {
@@ -704,8 +876,178 @@ export default {
openUrl(url) {
window.open(url, '_blank');
+ },
+
+ getLlmProviderList() {
+ axios.get('/api/config/provider/list', {
+ params: {
+ provider_type: 'chat_completion'
+ }
+ })
+ .then(response => {
+ if (response.data.status === 'ok') {
+ this.llmProviderConfigs = response.data.data || [];
+ } else {
+ this.showSnackbar(response.data.message || 'Failed to get LLM provider list', 'error');
+ }
+ })
+ .catch(error => {
+ console.error('Error fetching LLM providers:', error);
+ this.showSnackbar('Failed to get LLM provider list', 'error');
+ });
+ },
+
+ // URL导入相关方法
+ async startImportFromUrl() {
+ if (!this.importUrl) {
+ this.showSnackbar('Please enter a URL', 'warning');
+ return;
+ }
+
+ this.importing = true;
+
+ try {
+ const payload = {
+ url: this.importUrl,
+ ...Object.fromEntries(Object.entries(this.importOptions).filter(([_, v]) => v !== '' && v !== null && v !== undefined))
+ };
+
+ console.log('Starting URL import with payload:', JSON.stringify(payload, null, 2));
+ const addTaskResponse = await axios.post('/api/plug/url_2_kb/add', payload);
+
+ if (!addTaskResponse.data.task_id) {
+ throw new Error(addTaskResponse.data.message || 'Failed to start import task: No task_id received.');
+ }
+
+ const taskId = addTaskResponse.data.task_id;
+ this.pollTaskStatus(taskId);
+
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || error.message || 'An unknown error occurred.';
+ this.showSnackbar(`Error: ${errorMessage}`, 'error');
+ this.importing = false;
+ }
+ },
+
+ pollTaskStatus(taskId) {
+ this.pollingInterval = setInterval(async () => {
+ try {
+ const statusResponse = await axios.post(`/api/plug/url_2_kb/status`, { task_id: taskId });
+
+ const taskData = statusResponse.data;
+ const taskStatus = taskData.status;
+
+ if (taskStatus === 'completed') {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ this.showSnackbar(this.tm('importFromUrl.uploadingChunks'), 'info');
+ this.handleImportResult(taskData);
+ } else if (taskStatus === 'failed') {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ const failureReason = taskData.result || 'Unknown reason.';
+ this.showSnackbar(`${this.tm('importFromUrl.importFailed')}: ${failureReason}`, 'error');
+ this.importing = false;
+ }
+ } catch (error) {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ const errorMessage = error.response?.data?.message || error.message || 'An unknown error occurred during polling.';
+ this.showSnackbar(`Polling Error: ${errorMessage}`, 'error');
+ this.importing = false;
+ }
+ }, 3000);
+ },
+
+ async handleImportResult(data) {
+ const chunks = [];
+ const result = data.result;
+
+ // 1. Handle overall summary
+ if (result.overall_summary) {
+ chunks.push({ content: result.overall_summary, filename: 'overall_summary.txt' });
+ }
+
+ // 2. Handle topic summaries
+ if (result.topics && result.topics.length > 0) {
+ result.topics.forEach(topic => {
+ if (topic.topic_summary) {
+ chunks.push({ content: topic.topic_summary, filename: `topic_${topic.topic_id}_summary.txt` });
+ }
+ });
+ }
+
+ // 3. Handle noise points
+ if (result.noise_points && result.noise_points.length > 0) {
+ result.noise_points.forEach((point, index) => {
+ const content = typeof point === 'object' && point.text ? point.text : point;
+ chunks.push({ content: content, filename: `noise_${index + 1}.txt` });
+ });
+ }
+
+ if (chunks.length === 0) {
+ this.showSnackbar('URL processed, but no text chunks were extracted.', 'info');
+ this.importing = false;
+ return;
+ }
+
+ let successCount = 0;
+ let failCount = 0;
+
+ for (let i = 0; i < chunks.length; i++) {
+ const chunk = chunks[i];
+ try {
+ await this.uploadChunkAsFile(chunk.content, chunk.filename);
+ successCount++;
+ } catch (error) {
+ failCount++;
+ }
+ }
+
+ if (failCount === 0) {
+ this.showSnackbar(this.tm('importFromUrl.allChunksUploaded'), 'success');
+ } else if (successCount > 0) {
+ this.showSnackbar('Import partially complete. See console for details.', 'warning');
+ } else {
+ this.showSnackbar('Import failed. No chunks were uploaded.', 'error');
+ }
+
+ this.importing = false;
+ this.getKBCollections();
+ },
+
+ async uploadChunkAsFile(content, filename) {
+ const blob = new Blob([content], { type: 'text/plain' });
+ const file = new File([blob], filename, { type: 'text/plain' });
+
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('collection_name', this.currentKB.collection_name);
+
+ if (this.importOptions.chunk_size && this.importOptions.chunk_size > 0) {
+ formData.append('chunk_size', this.importOptions.chunk_size);
+ }
+ if (this.importOptions.chunk_overlap && this.importOptions.chunk_overlap >= 0) {
+ formData.append('chunk_overlap', this.importOptions.chunk_overlap);
+ }
+
+ const response = await axios.post('/api/plug/alkaid/kb/collection/add_file', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+
+ if (response.data.status !== 'ok') {
+ throw new Error(response.data.message || 'Chunk upload failed');
+ }
+ return response.data;
+ },
+ },
+ beforeUnmount() {
+ if (this.pollingInterval) {
+ clearInterval(this.pollingInterval);
}
- }
+ },
}
@@ -898,4 +1240,13 @@ export default {
.chunk-field:focus-within :deep(.v-field__prepend-inner) {
opacity: 1;
}
+
+.import-container,
+.from-url-container {
+ min-height: 400px;
+}
+
+.data-source-select :deep(.v-field__prepend-inner) {
+ padding-right: 12px;
+}