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') }} - - - - {{ 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') }} + + + + {{ tm('upload.chunkSettings.tooltip') }} + + + + +
+ + + +
+
+
+ +
+
+
+ {{ getFileIcon(selectedFile.name) }} + {{ selectedFile.name }} +
+ + mdi-close + +
+ +
+ + {{ tm('upload.upload') }} + +
+
+ +
+
-
- + +
+ + {{ tm('importFromUrl.preRequisite') }} + + + + + + mdi-cog-outline + {{ tm('importFromUrl.optionsTitle') }} + + + {{ 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; +}