Compare commits

...

19 Commits

Author SHA1 Message Date
Soulter
7ff2d6c537 feat: 删除导入URL至知识库功能的相关组件 2025-08-12 22:36:01 +08:00
Soulter
2125197cab feat: 合并知识库的上传文件和 URL 标签页 2025-08-12 22:34:05 +08:00
RC-CHN
5bdd492ddb fix: 优化url转知识库错误处理 2025-07-31 15:55:02 +08:00
RC-CHN
347d3eb4a5 feat: 更新导入功能提示信息,添加上传状态通知 2025-07-31 15:40:43 +08:00
RC-CHN
1b0cf53a92 feat: 添加上传前提提示信息至导入url至知识库功能 2025-07-31 15:26:33 +08:00
RC-CHN
81b6f20451 Merge branch 'AstrBotDevs:master' into url2kb 2025-07-27 01:04:59 +08:00
Ruochen
6f104d713f perf: 在导入url的部分配置项未启用时隐藏暂不使用的下拉框选项 2025-07-25 09:37:14 +08:00
Ruochen
bc8799229a perf: 更新url导入选项添加默认值 2025-07-25 09:12:11 +08:00
Ruochen
917bd401dd fix: 优化导入结果处理,添加整体摘要和主题摘要的文件命名 2025-07-22 17:29:05 +08:00
Ruochen
2878d6000a feat: 添加从URL导入功能的组件 2025-07-22 17:01:59 +08:00
Ruochen
dda8dc2f83 feat:完成从url获取部分的UI 2025-07-22 15:26:07 +08:00
RC-CHN
5e7835c793 Merge branch 'AstrBotDevs:master' into master 2025-07-21 11:16:33 +08:00
Ruochen
55f5e256b2 perf:并行化sendMessage中的图片获取逻辑 2025-07-16 10:30:28 +08:00
Ruochen
75beb7e7b9 fix:释放blob URL以防止内存泄漏 2025-07-16 10:26:24 +08:00
Ruochen
89982cfbb5 feat:webchat文件上传按钮支持多选文件上传 2025-07-16 10:24:31 +08:00
Ruochen
a55f51678c perf:将文件输入的值重置为空字符串以提升浏览器兼容性 2025-07-16 10:02:58 +08:00
Ruochen
3bc3027fc7 perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致 2025-07-16 09:53:32 +08:00
Ruochen
0dd3a43462 fix:上传后清空value,允许触发change事件以多次上传同一张图片 2025-07-16 09:26:49 +08:00
Ruochen
d15ed94b9e feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片) 2025-07-16 09:24:41 +08:00
3 changed files with 483 additions and 88 deletions

View File

@@ -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"
}
}
}

View File

@@ -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": "所有分片上传成功"
}
}
}

View File

@@ -12,7 +12,9 @@
: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>
<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%;">
@@ -75,13 +77,16 @@
<v-form @submit.prevent="submitCreateForm">
<v-text-field variant="outlined" v-model="newKB.name" :label="tm('createDialog.nameLabel')" 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="tm('createDialog.descriptionLabel')" variant="outlined" :placeholder="tm('createDialog.descriptionPlaceholder')"
<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="tm('createDialog.embeddingModelLabel')" variant="outlined" class="mt-2">
:item-props="embeddingModelProps" :label="tm('createDialog.embeddingModelLabel')"
variant="outlined" class="mt-2">
</v-select>
<small>{{ tm('createDialog.tips') }}</small>
@@ -89,8 +94,10 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<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-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>
@@ -114,7 +121,8 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">{{ tm('emojiPicker.close') }}</v-btn>
<v-btn color="primary" variant="text" @click="showEmojiPicker = false">{{ tm('emojiPicker.close')
}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -134,89 +142,177 @@
<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>
{{ tm('contentDialog.embeddingModel') }}: {{ 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>
{{ tm('contentDialog.vectorDimension') }}: {{ 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">{{ tm('contentDialog.tabs.upload') }}</v-tab>
<v-tab value="import">导入数据</v-tab>
<v-tab value="search">{{ tm('contentDialog.tabs.search') }}</v-tab>
</v-tabs>
<v-window v-model="activeTab" class="mt-4">
<!-- 上传文件标签页 -->
<v-window-item value="upload">
<div class="upload-container pa-4">
<div class="text-center mb-4">
<h3>{{ tm('upload.title') }}</h3>
<p class="text-subtitle-1">{{ tm('upload.subtitle') }}</p>
<!-- 导入数据标签页 -->
<v-window-item value="import">
<div class="import-container pa-4">
<div class="mb-8">
<h2>导入数据</h2>
<p class="text-subtitle-1">选择数据源并导入内容到知识库</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">{{ tm('upload.dropzone') }}</p>
</div>
<!-- 数据源选择下拉列表 -->
<v-select
v-model="dataSource"
:items="dataSourceOptions"
:label="'数据源选择'"
variant="outlined"
item-title="title"
item-value="value"
prepend-inner-icon="mdi-database"
></v-select>
<!-- 优化后的分片长度和重叠长度设置 -->
<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">{{ 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">
mdi-information-outline
</v-icon>
</template>
<span>
{{ 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="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="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>
</v-card-text>
</v-card>
<div class="selected-files mt-4" v-if="selectedFile">
<div type="info" variant="tonal" class="d-flex align-center">
<div>
<v-icon class="me-2">{{ getFileIcon(selectedFile.name) }}</v-icon>
<span style="font-weight: 1000;">{{ selectedFile.name }}</span>
</div>
<v-btn size="small" color="error" variant="text" @click="selectedFile = null">
<v-icon>mdi-close</v-icon>
</v-btn>
<!-- 从文件导入 -->
<div v-if="dataSource === 'file'" class="mt-4">
<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">{{ tm('upload.dropzone') }}</p>
</div>
<div class="text-center mt-4">
<v-btn color="primary" variant="elevated" :loading="uploading"
:disabled="!selectedFile" @click="uploadFile">
{{ tm('upload.upload') }}
</v-btn>
<!-- 分片长度和重叠长度设置 -->
<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">{{
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">
mdi-information-outline
</v-icon>
</template>
<span>
{{ 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="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="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>
</v-card-text>
</v-card>
<div class="selected-files mt-4" v-if="selectedFile">
<div type="info" variant="tonal" class="d-flex align-center">
<div>
<v-icon class="me-2">{{ getFileIcon(selectedFile.name) }}</v-icon>
<span style="font-weight: 1000;">{{ selectedFile.name }}</span>
</div>
<v-btn size="small" color="error" variant="text" @click="selectedFile = null">
<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">
{{ tm('upload.upload') }}
</v-btn>
</div>
</div>
<div class="upload-progress mt-4" v-if="uploading">
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</div>
</div>
<div class="upload-progress mt-4" v-if="uploading">
<v-progress-linear indeterminate color="primary"></v-progress-linear>
<!-- 从URL导入 -->
<div v-if="dataSource === 'url'" class="from-url-container">
<v-alert type="info" variant="tonal" class="mb-4" border>
{{ tm('importFromUrl.preRequisite') }}
</v-alert>
<v-text-field v-model="importUrl" :label="tm('importFromUrl.urlLabel')"
:placeholder="tm('importFromUrl.urlPlaceholder')" variant="outlined" class="mb-4" hide-details></v-text-field>
<v-card class="mb-4" 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-cog-outline</v-icon>
<span class="text-subtitle-1 font-weight-bold">{{ tm('importFromUrl.optionsTitle') }}</span>
<v-tooltip location="top">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" class="ml-2" size="small" color="grey">mdi-information-outline</v-icon>
</template>
<span>{{ tm('importFromUrl.tooltip') }}</span>
</v-tooltip>
</v-card-title>
<v-card-text class="pa-4 pt-2">
<v-row>
<v-col cols="12" md="6">
<v-switch hide-details v-model="importOptions.use_llm_repair" :label="tm('importFromUrl.useLlmRepairLabel')"
color="primary" inset></v-switch>
</v-col>
<v-col cols="12" md="6">
<v-switch v-model="importOptions.use_clustering_summary" hide-details
:label="tm('importFromUrl.useClusteringSummaryLabel')" color="primary" inset></v-switch>
</v-col>
<v-row class="pa-4">
<!-- Optional Repair Selector -->
<v-col v-if="importOptions.use_llm_repair" :md="optionalSelectorColWidth" cols="12">
<v-select v-model="importOptions.repair_llm_provider_id" :items="llmProviderConfigs" item-value="id"
:item-props="llmModelProps" :label="tm('importFromUrl.repairLlmProviderIdLabel')" variant="outlined"
clearable hide-details></v-select>
</v-col>
<!-- Optional Summary Selector -->
<v-col v-if="importOptions.use_clustering_summary" :md="optionalSelectorColWidth" cols="12">
<v-select v-model="importOptions.summarize_llm_provider_id" :items="llmProviderConfigs" item-value="id"
:item-props="llmModelProps" :label="tm('importFromUrl.summarizeLlmProviderIdLabel')" variant="outlined"
clearable hide-details></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select v-model="importOptions.embedding_provider_id" :items="embeddingProviderConfigs" item-value="id"
:item-props="embeddingModelProps" :label="tm('importFromUrl.embeddingProviderIdLabel')"
variant="outlined" clearable hide-details></v-select>
</v-col>
<v-col cols="12" md="3">
<v-text-field v-model="importOptions.chunk_size" :label="tm('importFromUrl.chunkSizeLabel')" type="number"
variant="outlined" clearable hide-details></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field v-model="importOptions.chunk_overlap" :label="tm('importFromUrl.chunkOverlapLabel')"
type="number" variant="outlined" clearable hide-details></v-text-field>
</v-col>
</v-row>
</v-row>
</v-card-text>
</v-card>
<div class="text-center">
<v-btn color="primary" variant="elevated" :loading="importing" :disabled="!importUrl" @click="startImportFromUrl">
{{ tm('importFromUrl.startImport') }}
</v-btn>
</div>
</div>
</div>
</v-window-item>
@@ -225,12 +321,13 @@
<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="tm('search.queryLabel')" append-icon="mdi-magnify"
variant="outlined" class="flex-grow-1 me-2" @click:append="searchKnowledgeBase"
@keyup.enter="searchKnowledgeBase" :placeholder="tm('search.queryPlaceholder')"
hide-details></v-text-field>
<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="tm('search.queryPlaceholder')" hide-details></v-text-field>
<v-select v-model="topK" :items="[3, 5, 10, 20]" :label="tm('search.resultCountLabel')" 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>
@@ -253,7 +350,8 @@
<v-spacer></v-spacer>
<v-chip v-if="result.score" size="small" color="primary"
variant="tonal">
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100) }}%
{{ tm('search.relevance') }}: {{ Math.round(result.score * 100)
}}%
</v-chip>
</div>
<div class="search-content">{{ result.content }}</div>
@@ -284,8 +382,11 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<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-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>
@@ -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);
}
}
},
}
</script>
@@ -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;
}
</style>