| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244 |
- <template>
- <el-dialog
- v-model="drawerVisible"
- :title="editingId ? '编辑知识库' : '新建知识库'"
- @open="handleOpen"
- class="knowledge-modal"
- fullscreen
- >
- <el-scrollbar class="modal-wrap">
- <el-form
- ref="formRef"
- :model="form"
- :rules="rules"
- label-position="left"
- label-width="120px"
- class="knowledge-form"
- >
- <el-tabs v-model="activeTab" tab-position="left" class="settings-tabs">
- <el-tab-pane label="基础信息" name="basic">
- <div class="tab-intro">设置知识库的名称、类型和描述信息</div>
- <div class="collapse-body">
- <el-form-item label="知识库类型" prop="type">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.type"
- class="selection-segmented"
- :options="knowledgeTypeOptions"
- @change="handleKnowledgeBaseTypeChange"
- />
- <div class="field-tip">
- FAQ 类型适合结构化问答数据;文档型支持文件解析与分块。
- </div>
- </div>
- </el-form-item>
- <div v-if="isDocumentType" class="subsection ml-120px mb-16px">
- <div class="subsection-title">索引策略</div>
- <div class="subsection-tip">
- 配置文档上传后的处理管道。关键词和向量检索为 RAG 必需项,系统会保持开启。
- </div>
- <div class="index-strategy-list">
- <el-form-item label="知识图谱启用">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.graph_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">开启后可使用图谱关系辅助检索。</div>
- </div>
- </el-form-item>
- <el-form-item label="关键字启用">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.keyword_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- disabled
- />
- <div class="field-tip">RAG 检索必须开启,系统会自动保持为开启状态。</div>
- </div>
- </el-form-item>
- <el-form-item label="向量启用">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.vector_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- disabled
- />
- <div class="field-tip">RAG 检索必须开启,系统会自动保持为开启状态。</div>
- </div>
- </el-form-item>
- <el-form-item label="维基启用">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.wiki_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">开启后可从 Wiki 内容中补充知识来源。</div>
- </div>
- </el-form-item>
- </div>
- </div>
- <el-form-item label="知识库名称" prop="name">
- <el-input v-model="form.name" placeholder="请输入知识库名称" />
- </el-form-item>
- <el-form-item label="知识库描述">
- <el-input
- v-model="form.description"
- type="textarea"
- :rows="3"
- placeholder="请输入知识库描述"
- />
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane label="模型配置" name="model">
- <div class="tab-intro">为知识库选择合适的 AI 模型</div>
- <div class="collapse-body">
- <el-form-item label="LLM大语言模型" prop="summary_model_id">
- <el-select v-model="form.summary_model_id" placeholder="请选择">
- <el-option
- v-for="item in summaryModels"
- :key="item.id"
- :label="buildModelLabel(item)"
- :value="item.id"
- />
- </el-select>
- <div class="field-tip">用于总结和摘要的大语言模型。</div>
- </el-form-item>
- <el-form-item label="Embedding 嵌入模型" prop="embedding_model_id">
- <el-select v-model="form.embedding_model_id" placeholder="请选择">
- <el-option
- v-for="item in embeddingModels"
- :key="item.id"
- :label="buildModelLabel(item)"
- :value="item.id"
- />
- </el-select>
- <div class="field-tip">用于文本向量化的嵌入模型。</div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane label="向量存储" name="vectorStore">
- <div class="tab-intro">从全局向量存储配置中选择, 可为空, 默认/为空:系统默认。</div>
- <div class="collapse-body">
- <el-form-item label="向量存储">
- <el-select
- v-model="form.vector_store_id"
- placeholder="系统默认"
- clearable
- :disabled="!!editingId"
- >
- <el-option
- v-for="item in vectorStoreOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- <div class="field-tip">
- 创建后不可更改。如需迁移,请创建一个绑定到目标存储的新 KB 并重新索引。
- </div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="解析引擎" name="parser">
- <div class="tab-intro">为不同文件类型选择文档解析引擎</div>
- <div class="collapse-body">
- <div class="field-tip compact-tip">未配置的文件类型将使用内置解析引擎。</div>
- <div class="parser-rule-list">
- <div
- v-for="rule in form.parser_engine_rules"
- :key="rule.file_types.join(',')"
- class="parser-rule-item"
- >
- <div class="parser-rule-item__types">{{ rule.file_types.join(', ') }}</div>
- <el-select v-model="rule.engine" placeholder="请选择">
- <el-option
- v-for="item in parserEngineOptions"
- :key="item"
- :label="item"
- :value="item"
- />
- </el-select>
- </div>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="图像处理" name="image">
- <div class="tab-intro">配置图像内容理解能力</div>
- <div class="collapse-body">
- <el-form-item label="多模态功能">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.vlm_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">启用图片等多模态内容的理解能力。</div>
- </div>
- </el-form-item>
- <el-form-item v-if="form.vlm_enabled" label="VLLM视觉模型">
- <el-select v-model="form.vlm_model_id" placeholder="请选择">
- <el-option
- v-for="item in vlmModels"
- :key="item.id"
- :label="buildModelLabel(item)"
- :value="item.id"
- />
- </el-select>
- <div class="field-tip">用于多模态理解的视觉语言模型(必选)。</div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="音频处理" name="audio">
- <div class="tab-intro">配置语音识别(ASR)</div>
- <div class="collapse-body">
- <div class="field-tip compact-tip">
- 启用后可上传音频文件并整段转写为文本(常见格式:mp3、wav、m4a、flac、ogg
- 等)。暂不支持视频上传。
- </div>
- <el-form-item label="音频语音识别">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.asr_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">
- 启用后可上传音频到知识库,系统自动将语音转写为文本并参与解析与检索。
- </div>
- </div>
- </el-form-item>
- <el-form-item v-if="form.asr_enabled" label="ASR模型">
- <el-select v-model="form.asr_model_id" placeholder="请选择">
- <el-option
- v-for="item in asrModels"
- :key="item.id"
- :label="buildModelLabel(item)"
- :value="item.id"
- />
- </el-select>
- <div class="field-tip">用于音频中语音转文本的识别模型(如 OpenAI Whisper)。</div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="存储引擎" name="storage">
- <div class="tab-intro">选择文档上传使用的文件存储引擎</div>
- <div class="collapse-body">
- <el-form-item label="存储引擎">
- <el-select v-model="form.storage_provider" placeholder="请选择">
- <el-option
- v-for="item in storageProviderOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- <div class="field-tip">
- 选择该知识库使用的存储引擎,需在全局设置中已配置对应引擎。
- </div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="分块设置" name="chunk">
- <div class="tab-intro">配置文档分块参数,优化检索效果</div>
- <div class="collapse-body">
- <el-form-item label="分块策略">
- <div class="slider-wrap">
- <div class="field-tip mb-4px mt-0">
- 选择文档的分块方式。自动模式会分析每个文档的结构并选择最佳策略。
- </div>
- <el-segmented
- v-model="form.chunk_strategy"
- class="selection-segmented"
- :options="chunkStrategyOptions"
- />
- <!-- <el-select
- v-model="form.chunk_strategy"
- filterable
- default-first-option
- placeholder="请选择"
- :options="chunkStrategyOptions"
- /> -->
- <div class="field-tip">
- {{ chunkSrategyTip?.[form.chunk_strategy] }}
- </div>
- </div>
- </el-form-item>
- <el-form-item label="分块大小">
- <div class="slider-wrap">
- <el-slider
- v-model="form.chunk_size"
- :min="100"
- :max="4000"
- :step="50"
- show-input
- />
- <div class="field-tip">控制每个文档分块的字符数(100-4000)。</div>
- </div>
- </el-form-item>
- <el-form-item label="分块重叠">
- <div class="slider-wrap">
- <el-slider
- v-model="form.chunk_overlap"
- :min="0"
- :max="500"
- :step="10"
- show-input
- />
- <div class="field-tip">相邻文档块之间的重叠字符数(0-500)。</div>
- </div>
- </el-form-item>
- <el-form-item label="分隔符">
- <el-select
- v-model="form.separators"
- multiple
- allow-create
- filterable
- default-first-option
- placeholder="请输入或选择分隔符"
- >
- <el-option
- v-for="item in separatorOptions"
- :key="item"
- :label="`${item.label}(${item.displayValue})`"
- :value="item.value"
- />
- </el-select>
- <div class="field-tip">文档分块时使用的分隔符。</div>
- </el-form-item>
- <el-form-item label="父子分块">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.enable_parent_child"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">
- 启用两级父子分块策略。大的父块提供上下文,小的子块用于向量匹配检索。
- </div>
- </div>
- </el-form-item>
- <div v-if="form.enable_parent_child" class="subsection ml-120px">
- <div class="subsection-title">父子分块参数</div>
- <el-form-item label="父块大小">
- <div class="slider-wrap">
- <el-slider
- v-model="form.parent_chunk_size"
- :min="512"
- :max="8192"
- :step="128"
- show-input
- />
- <div class="field-tip">提供上下文的父块字符数(512-8192)。</div>
- </div>
- </el-form-item>
- <el-form-item label="子块大小">
- <div class="slider-wrap">
- <el-slider
- v-model="form.child_chunk_size"
- :min="64"
- :max="2048"
- :step="64"
- show-input
- />
- <div class="field-tip">用于向量匹配的子块字符数(64-2048)。</div>
- </div>
- </el-form-item>
- </div>
- <el-collapse class="chunk-advanced-collapse mt-12px" expand-icon-position="left">
- <el-collapse-item title="高级" name="advanced">
- <div class="chunk-advanced-panel">
- <el-form-item label="Token 上限">
- <div class="slider-wrap">
- <el-input-number
- v-model="form.token_limit"
- :min="0"
- :max="8192"
- :step="1"
- style="width: 100%"
- />
- <div class="field-tip">
- 每个分块的硬性 Token 上限(0-8192)。0 = 关闭(仅按字符数)。当嵌入模型
- Token 上限较小时启用:MiniLM (256 tok) 用 200,BGE/Cohere (512 tok) 用
- 400。现代嵌入器(OpenAI、Voyage、Jina-v3)支持 >2000 tokens,保持 0
- 即可。
- </div>
- </div>
- </el-form-item>
- <el-form-item label="语言提示">
- <div class="switch-wrap">
- <el-select
- v-model="form.languages"
- multiple
- clearable
- collapse-tags
- collapse-tags-tooltip
- placeholder="选择语言提示"
- >
- <el-option
- v-for="item in languageOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value"
- />
- </el-select>
- <div class="field-tip">
- 限制启发式模式只识别选定的语言(DE/EN/ZH)。留空 =
- 自动检测。同质化语料库可显式设置以避免跨语言误匹配。
- </div>
- </div>
- </el-form-item>
- </div>
- </el-collapse-item>
- </el-collapse>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="isDocumentType" label="高级设置" name="advanced">
- <div class="tab-intro">配置问题生成等高级功能</div>
- <div class="collapse-body">
- <el-form-item label="AI 问题生成">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.question_generation_enabled"
- class="selection-segmented"
- :options="booleanSegmentedOptions"
- />
- <div class="field-tip">
- 解析文档时调用大模型为每个分块生成相关问题,提高检索召回率。启用后会增加文档解析耗时。
- </div>
- </div>
- </el-form-item>
- <el-form-item v-if="form.question_generation_enabled" label="生成问题数量">
- <el-input-number
- v-model="form.question_count"
- :min="1"
- :max="10"
- style="width: 100%"
- />
- <div class="field-tip">每个文档分块生成的问题数量(1-10)。</div>
- </el-form-item>
- </div>
- </el-tab-pane>
- <el-tab-pane v-if="!isDocumentType" label="FAQ设置" name="faq">
- <div class="tab-intro">设置 FAQ 知识库的索引策略和问答组织方式</div>
- <div class="collapse-body">
- <el-form-item label="索引方式">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.faq_index_mode"
- class="selection-segmented"
- :options="faqIndexModeOptions"
- />
- <div class="field-tip">仅索引问题可提升精度,索引问答可提高召回率。</div>
- </div>
- </el-form-item>
- <el-form-item label="问题索引方式">
- <div class="switch-wrap">
- <el-segmented
- v-model="form.faq_question_index_mode"
- class="selection-segmented"
- :options="faqQuestionIndexModeOptions"
- />
- <div class="field-tip">
- 合并索引:标准问和相似问合并索引;分别索引:标准问和每个相似问独立索引,检索更精确但需要更多存储。
- </div>
- </div>
- </el-form-item>
- </div>
- </el-tab-pane>
- </el-tabs>
- </el-form>
- </el-scrollbar>
- <template #footer>
- <div class="drawer-footer">
- <el-button @click="drawerVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="submitForm">保存</el-button>
- </div>
- </template>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, reactive, ref } from 'vue'
- import { ElMessage } from 'element-plus'
- import { aiModel, knowledge, storageProvider, vector } from '@repo/api-service'
- import type { KnowledgeBaseForm, KnowledgeModelOption, ParserEngineRule } from '../types'
- const emit = defineEmits<{ (e: 'refresh'): void }>()
- const submitLoading = ref(false)
- const drawerVisible = ref(false)
- const editingId = ref('')
- const activeTab = ref('basic')
- const modelList = ref<KnowledgeModelOption[]>([])
- const formRef = ref()
- const DEFAULT_PARSER_RULES: ParserEngineRule[] = [
- { engine: 'builtin', file_types: ['pdf'] },
- { engine: 'builtin', file_types: ['docx', 'doc'] },
- { engine: 'markitdown', file_types: ['pptx', 'ppt'] },
- { engine: 'builtin', file_types: ['xlsx', 'xls'] },
- { engine: 'simple', file_types: ['csv'] },
- { engine: 'builtin', file_types: ['md', 'markdown'] },
- { engine: 'simple', file_types: ['txt'] },
- { engine: 'simple', file_types: ['json'] },
- { engine: 'builtin', file_types: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'webp'] },
- { engine: 'simple', file_types: ['mp3', 'wav', 'm4a', 'flac', 'ogg'] }
- ]
- const separatorOptions = [
- { label: '双换行', value: '\n\n', displayValue: '\\n\\n' },
- { label: '单换行', value: '\n', displayValue: '\\n' },
- { label: '中文句号', value: '。', displayValue: '。' },
- { label: '感叹号', value: '!', displayValue: '!' },
- { label: '问号', value: '?', displayValue: '?' },
- { label: '英文分号', value: ';', displayValue: ';' },
- { label: '中文分号', value: ';', displayValue: ';' },
- { label: '空格', value: ' ', displayValue: ' ' }
- ]
- // 默认不选空格
- const DEFAULT_SEPARATORS = separatorOptions.map((item) => item.value).filter((item) => item !== ' ')
- const parserEngineOptions = ['builtin', 'markitdown', 'simple']
- const languageOptions = [
- { label: '中文', value: 'zh' },
- { label: '英语', value: 'en' },
- { label: '德语', value: 'de' }
- ]
- const storageProviderOptions = ref<{ label: string; value: string }[]>([])
- const vectorStoreOptions = ref<{ label: string; value: string }[]>([])
- const knowledgeTypeOptions = [
- { label: '文档', value: 'document' },
- { label: '问答', value: 'faq' }
- ]
- const booleanSegmentedOptions = [
- { label: '开启', value: true },
- { label: '关闭', value: false }
- ]
- const faqIndexModeOptions = [
- { label: '仅索引问题', value: 'question_only' },
- { label: '索引问答', value: 'question_answer' }
- ]
- const faqQuestionIndexModeOptions = [
- { label: '合并索引', value: 'combined' },
- { label: '分别索引', value: 'separate' }
- ]
- function cloneParserRules(rules?: ParserEngineRule[]) {
- return (rules || DEFAULT_PARSER_RULES).map((rule) => ({
- engine: rule.engine,
- file_types: [...rule.file_types]
- }))
- }
- const createDefaultForm = (): KnowledgeBaseForm => ({
- name: '',
- description: '',
- type: 'document',
- embedding_model_id: '',
- summary_model_id: '',
- faq_index_mode: 'question_only',
- faq_question_index_mode: 'separate',
- vlm_enabled: true,
- vlm_model_id: '',
- asr_enabled: false,
- asr_model_id: '',
- asr_language: '',
- graph_enabled: false,
- keyword_enabled: true,
- vector_enabled: true,
- wiki_enabled: false,
- question_generation_enabled: true,
- question_count: 3,
- chunk_strategy: 'auto',
- chunk_size: 512,
- chunk_overlap: 100,
- separators: [...DEFAULT_SEPARATORS],
- parser_engine_rules: cloneParserRules(),
- enable_parent_child: true,
- parent_chunk_size: 4096,
- child_chunk_size: 384,
- token_limit: 0,
- languages: [],
- storage_provider: 'local',
- vector_store_id: '',
- wiki_extraction_granularity: 'standard',
- wiki_max_pages_per_ingest: 0,
- wiki_synthesis_model_id: ''
- })
- const chunkStrategyOptions = [
- {
- label: '自动',
- value: 'auto'
- },
- {
- label: '按标题切分',
- value: 'heading'
- },
- {
- label: '结构感知',
- value: 'heuristic'
- },
- {
- label: '按长度切分',
- value: 'legacy'
- }
- ]
- const chunkSrategyTip = {
- auto: '文档分析器根据内容结构自动在「按标题切分」「结构感知」「按长度切分」之间选择。',
- heading:
- '在 Markdown 标题(#、##、###)边界处切分,每块自动带上所在标题路径。适合结构清晰的 Markdown 文档。',
- heuristic:
- '识别分页符、编号章节、多语言章节标记(DE/EN/ZH)、全大写标题等结构信号进行切分。适合没有 Markdown 标题的 PDF / 扫描件。',
- legacy: '忽略结构,仅按字符数和分隔符递归切分——原始行为。当上述策略对你的内容效果不佳时使用。'
- }
- const form = reactive<KnowledgeBaseForm>(createDefaultForm())
- const embeddingModels = computed(() => modelList.value.filter((item) => item.type === 'Embedding'))
- const summaryModels = computed(() => modelList.value.filter((item) => item.type === 'KnowledgeQA'))
- const vlmModels = computed(() => modelList.value.filter((item) => item.type === 'VLLM'))
- const asrModels = computed(() =>
- modelList.value.filter((item) => ['ASR', 'Asr', 'asr'].includes(item.type))
- )
- const isDocumentType = computed(() => form.type === 'document')
- async function fetchModels() {
- const res = await aiModel.postModelPageList({
- keyword: '',
- type: '',
- source: '',
- pageIndex: 1,
- pageSize: 200
- })
- if (res?.isSuccess) {
- modelList.value = (res.result?.model || []) as KnowledgeModelOption[]
- applyModelDefaults()
- }
- }
- async function fetchStorageProviders() {
- const res = await storageProvider.postAiStorageProviderEngines({})
- if (res?.isSuccess) {
- storageProviderOptions.value = (res.result || [])
- .filter((item) => {
- return item.allowed && item.available
- })
- .map((item) => ({ label: item.name, value: item.name }))
- applyModelDefaults()
- }
- }
- async function fetchVectorStores() {
- try {
- const res = await vector.postPageList({ keyword: '', pageIndex: 1, pageSize: 200 })
- if (res?.isSuccess && res.result) {
- const data = (res.result as any)?.model
- const items = Array.isArray(data) ? data : data?.list || data?.items || data?.data || []
- vectorStoreOptions.value = items.map((item: any) => ({
- label: item.name || item.display_name || item.id,
- value: item.id
- }))
- }
- } catch {
- // silent
- }
- }
- function applyModelDefaults() {
- if (!form.embedding_model_id && embeddingModels.value.length) {
- form.embedding_model_id = embeddingModels.value[0]!.id
- }
- if (!form.summary_model_id && summaryModels.value.length) {
- form.summary_model_id = summaryModels.value[0]!.id
- }
- if (!form.wiki_synthesis_model_id && summaryModels.value.length) {
- form.wiki_synthesis_model_id = summaryModels.value[0]!.id
- }
- if (vlmModels.value.length) {
- if (!form.vlm_model_id) form.vlm_model_id = vlmModels.value[0]!.id
- } else {
- form.vlm_enabled = false
- form.vlm_model_id = ''
- }
- if (asrModels.value.length) {
- if (!form.asr_model_id) form.asr_model_id = asrModels.value[0]!.id
- } else {
- form.asr_enabled = false
- form.asr_model_id = ''
- form.asr_language = ''
- }
- }
- async function hydrateSelectedModels() {
- const selectedIds = [
- form.embedding_model_id,
- form.summary_model_id,
- form.vlm_model_id,
- form.asr_model_id,
- form.wiki_synthesis_model_id
- ].filter(Boolean)
- const missingIds = Array.from(new Set(selectedIds)).filter(
- (id) => !modelList.value.some((item) => item.id === id)
- )
- if (!missingIds.length) return
- const results = await Promise.all(
- missingIds.map((id) => aiModel.postModelInfo({ id }).catch(() => null))
- )
- const hydratedModels = results
- .filter((res): res is NonNullable<typeof res> => !!res?.isSuccess && !!res.result)
- .map((res) => res.result as KnowledgeModelOption)
- if (hydratedModels.length) {
- modelList.value = [...modelList.value, ...hydratedModels].filter(
- (item, index, list) => list.findIndex((candidate) => candidate.id === item.id) === index
- )
- }
- }
- const rules = {
- name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
- type: [{ required: true, message: '请选择知识库类型', trigger: 'change' }],
- embedding_model_id: [{ required: true, message: '请选择 Embedding 模型', trigger: 'change' }],
- summary_model_id: [{ required: true, message: '请选择摘要模型', trigger: 'change' }]
- }
- function buildModelLabel(item: KnowledgeModelOption) {
- const title = item.title?.trim()
- return title ? `${title} (${item.name})` : item.name
- }
- function handleKnowledgeBaseTypeChange(type: KnowledgeBaseForm['type']) {
- activeTab.value = 'basic'
- if (type === 'faq') {
- form.graph_enabled = false
- form.keyword_enabled = true
- form.vector_enabled = true
- form.wiki_enabled = false
- form.vlm_enabled = false
- form.asr_enabled = false
- form.question_generation_enabled = false
- form.faq_index_mode = 'question_only'
- form.faq_question_index_mode = 'separate'
- return
- }
- form.graph_enabled = false
- form.keyword_enabled = true
- form.vector_enabled = true
- form.wiki_enabled = false
- form.question_generation_enabled = true
- form.question_count = 3
- form.chunk_size = 512
- form.chunk_overlap = 100
- form.separators = [...DEFAULT_SEPARATORS]
- form.parser_engine_rules = cloneParserRules()
- form.enable_parent_child = true
- form.parent_chunk_size = 4096
- form.child_chunk_size = 384
- form.token_limit = 0
- form.languages = []
- form.storage_provider = 'local'
- form.vector_store_id = ''
- form.wiki_extraction_granularity = 'standard'
- form.wiki_max_pages_per_ingest = 0
- applyModelDefaults()
- }
- function resetForm() {
- Object.assign(form, createDefaultForm())
- activeTab.value = 'basic'
- applyModelDefaults()
- }
- function handleOpen() {
- activeTab.value = 'basic'
- }
- function openCreateDrawer() {
- editingId.value = ''
- resetForm()
- drawerVisible.value = true
- }
- async function openEditDrawer(id: string) {
- editingId.value = id
- resetForm()
- const res = await knowledge.postAiKnowledgeBaseInfo({ id })
- if (!res?.isSuccess) return
- const detail = res.result
- const chunkingConfig = (detail.chunking_config || {}) as {
- strategy?: KnowledgeBaseForm['chunk_strategy']
- chunk_size?: number
- chunk_overlap?: number
- separators?: string[]
- parser_engine_rules?: ParserEngineRule[]
- enable_parent_child?: boolean
- parent_chunk_size?: number
- child_chunk_size?: number
- tokenLimit?: number
- languages?: string[]
- }
- Object.assign(form, {
- name: detail.name,
- description: detail.description || '',
- type: (detail.type || 'document') as KnowledgeBaseForm['type'],
- embedding_model_id: detail.embedding_model_id || '',
- summary_model_id: detail.summary_model_id || '',
- faq_index_mode: detail.faq_config?.index_mode || 'question_only',
- faq_question_index_mode: detail.faq_config?.question_index_mode || 'separate',
- vlm_enabled: detail.vlm_config?.enabled ?? false,
- vlm_model_id: detail.vlm_config?.model_id || '',
- asr_enabled: detail.asr_config?.enabled ?? false,
- asr_model_id: detail.asr_config?.model_id || '',
- asr_language: detail.asr_config?.language || '',
- graph_enabled: detail.indexing_strategy?.graph_enabled ?? false,
- keyword_enabled: true,
- vector_enabled: true,
- wiki_enabled: detail.indexing_strategy?.wiki_enabled ?? false,
- question_generation_enabled: detail.question_generation_config?.enabled ?? true,
- question_count: detail.question_generation_config?.question_count ?? 3,
- chunk_strategy: chunkingConfig.strategy ?? 'auto',
- chunk_size: chunkingConfig.chunk_size ?? 512,
- chunk_overlap: chunkingConfig.chunk_overlap ?? 100,
- separators: chunkingConfig.separators?.length
- ? [...chunkingConfig.separators]
- : [...DEFAULT_SEPARATORS],
- parser_engine_rules: cloneParserRules(chunkingConfig.parser_engine_rules),
- enable_parent_child: chunkingConfig.enable_parent_child ?? true,
- parent_chunk_size: chunkingConfig.parent_chunk_size ?? 4096,
- child_chunk_size: chunkingConfig.child_chunk_size ?? 384,
- token_limit: chunkingConfig.tokenLimit ?? 0,
- languages: chunkingConfig.languages?.length ? [...chunkingConfig.languages] : [],
- storage_provider:
- detail.storage_provider_config?.provider || detail.storage_config?.provider || 'local',
- vector_store_id: detail.vector_store_id || '',
- wiki_extraction_granularity: detail.wiki_config?.extraction_granularity || 'standard',
- wiki_max_pages_per_ingest: detail.wiki_config?.max_pages_per_ingest ?? 0,
- wiki_synthesis_model_id: detail.wiki_config?.synthesis_model_id || ''
- })
- await hydrateSelectedModels()
- applyModelDefaults()
- drawerVisible.value = true
- }
- function buildCommonPayload() {
- const keywordEnabled = true
- const vectorEnabled = true
- const wikiEnabled = form.wiki_enabled
- const vlmEnabled = vectorEnabled && form.vlm_enabled
- const asrEnabled = vectorEnabled && form.asr_enabled
- const questionGenerationEnabled = vectorEnabled && form.question_generation_enabled
- return {
- name: form.name,
- description: form.description,
- indexing_strategy: {
- graph_enabled: form.graph_enabled,
- keyword_enabled: keywordEnabled,
- vector_enabled: vectorEnabled,
- wiki_enabled: wikiEnabled
- },
- embedding_model_id: form.embedding_model_id,
- summary_model_id: form.summary_model_id,
- wiki_config: {
- extraction_granularity: form.wiki_extraction_granularity,
- max_pages_per_ingest: form.wiki_max_pages_per_ingest,
- synthesis_model_id: wikiEnabled
- ? form.wiki_synthesis_model_id || form.summary_model_id
- : form.summary_model_id
- },
- chunking_config: {
- strategy: form.chunk_strategy,
- chunk_size: form.chunk_size,
- chunk_overlap: form.chunk_overlap,
- separators: form.separators.length ? form.separators : [...DEFAULT_SEPARATORS],
- parser_engine_rules: cloneParserRules(form.parser_engine_rules),
- enable_parent_child: form.enable_parent_child,
- parent_chunk_size: form.parent_chunk_size,
- child_chunk_size: form.child_chunk_size,
- tokenLimit: form.token_limit,
- languages: [...form.languages]
- },
- vlm_config: {
- model_id: vlmEnabled ? form.vlm_model_id : '',
- enabled: vlmEnabled
- },
- asr_config: {
- model_id: asrEnabled ? form.asr_model_id : '',
- language: asrEnabled ? form.asr_language : '',
- enabled: asrEnabled
- },
- storage_provider_config: {
- provider: form.storage_provider
- },
- storage_config: {
- provider: form.storage_provider
- },
- question_generation_config: {
- enabled: questionGenerationEnabled,
- question_count: form.question_count
- },
- vector_store_id: form.vector_store_id || undefined
- }
- }
- function buildPayload() {
- const commonPayload = buildCommonPayload()
- if (form.type === 'faq') {
- return {
- ...commonPayload,
- type: form.type,
- faq_config: {
- index_mode: form.faq_index_mode,
- question_index_mode: form.faq_question_index_mode
- }
- }
- }
- return {
- ...commonPayload,
- type: form.type
- }
- }
- async function formValidate(formInstance: any) {
- return formInstance
- ?.validate()
- .then(() => true)
- .catch(() => false)
- }
- async function submitForm() {
- const valid = await formValidate(formRef.value)
- if (!valid) return
- submitLoading.value = true
- try {
- const payload = buildPayload()
- if (editingId.value) {
- await knowledge.postAiKnowledgeBaseUpdate({
- id: editingId.value,
- ...(payload as any)
- } as any)
- ElMessage.success('知识库已更新')
- } else {
- await knowledge.postAiKnowledgeBaseCreate(payload as any)
- ElMessage.success('知识库已创建')
- }
- drawerVisible.value = false
- emit('refresh')
- } catch {
- ElMessage.error('保存失败')
- } finally {
- submitLoading.value = false
- }
- }
- defineExpose({
- openCreateDrawer,
- openEditDrawer,
- resetForm,
- close: () => {
- resetForm()
- drawerVisible.value = false
- }
- })
- onMounted(async () => {
- await fetchModels()
- await fetchStorageProviders()
- await fetchVectorStores()
- resetForm()
- })
- </script>
- <style lang="less">
- .knowledge-modal {
- --agent-surface: #ffffff;
- --agent-surface-soft: #f7f8fc;
- --agent-border: rgba(148, 163, 184, 0.22);
- --agent-border-strong: rgba(148, 163, 184, 0.38);
- --agent-text: #0f172a;
- --agent-text-soft: #475569;
- --agent-accent: #14532d;
- --agent-shadow: 0 20px 45px rgba(15, 23, 42, 0.08);
- height: 100vh;
- display: flex;
- flex-direction: column;
- background:
- radial-gradient(circle at top left, rgba(20, 83, 45, 0.08), transparent 30%),
- radial-gradient(circle at top right, rgba(15, 23, 42, 0.05), transparent 24%), #eff3f8;
- .el-dialog__body {
- flex: 1;
- overflow: auto;
- padding: 24px;
- }
- .el-dialog__header {
- padding: 20px 24px 0;
- }
- .el-dialog__title {
- font-size: 22px;
- font-weight: 700;
- color: var(--agent-text);
- letter-spacing: -0.02em;
- }
- .el-dialog__footer {
- padding: 0 24px 24px;
- }
- .modal-wrap {
- margin: 0 auto;
- width: min(1360px, 100%);
- }
- .knowledge-form {
- max-width: 100%;
- }
- :deep(.settings-tabs > .el-tabs__header) {
- margin-right: 20px;
- }
- :deep(.settings-tabs > .el-tabs__content) {
- overflow: visible;
- }
- :deep(.settings-tabs .el-tabs__item) {
- height: auto;
- padding: 14px 18px;
- line-height: 1.4;
- border-radius: 12px;
- }
- :deep(.settings-tabs .el-tabs__item.is-active) {
- background: rgba(20, 83, 45, 0.1);
- color: var(--agent-accent);
- }
- :deep(.settings-tabs .el-tab-pane) {
- min-height: calc(100vh - 220px);
- border-radius: 20px;
- border: 1px solid rgba(255, 255, 255, 0.65);
- background:
- linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.92)),
- var(--agent-surface);
- box-shadow: var(--agent-shadow);
- }
- .tab-intro {
- padding: 0 20px;
- font-size: 16px;
- color: var(--text-strong);
- }
- .collapse-body {
- padding: 20px;
- }
- .switch-wrap,
- .slider-wrap {
- width: 100%;
- }
- .switch-wrap {
- .field-tip {
- margin-top: 0;
- }
- }
- .selection-segmented {
- align-self: flex-start;
- width: fit-content;
- max-width: 100%;
- margin-bottom: 8px;
- }
- .selection-segmented :deep(.el-segmented) {
- width: fit-content;
- max-width: 100%;
- }
- .selection-segmented :deep(.el-segmented__group) {
- width: auto;
- }
- .field-tip {
- margin-top: 8px;
- font-size: 13px;
- line-height: 1.7;
- color: var(--agent-text-soft);
- }
- .compact-tip {
- margin-bottom: 16px;
- }
- .index-strategy-list {
- display: grid;
- gap: 10px;
- margin-top: 16px;
- }
- .form-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 0 12px;
- }
- .subsection {
- margin-top: 12px;
- padding: 16px;
- border: 1px solid var(--agent-border);
- border-radius: 14px;
- background: var(--bg-container);
- }
- .subsection-title {
- margin-bottom: 14px;
- font-size: 14px;
- font-weight: 600;
- color: var(--agent-text);
- }
- .subsection-tip {
- margin: -4px 0 0;
- font-size: 13px;
- line-height: 1.7;
- color: var(--agent-text-soft);
- }
- .chunk-advanced-collapse {
- margin-left: 120px;
- }
- .chunk-advanced-panel {
- padding-top: 12px;
- }
- .parser-rule-list {
- display: grid;
- gap: 12px;
- }
- .parser-rule-item {
- display: grid;
- grid-template-columns: minmax(0, 1fr) 220px;
- gap: 12px;
- align-items: center;
- }
- .parser-rule-item__types {
- padding: 10px 14px;
- border: 1px solid var(--agent-border);
- border-radius: 12px;
- background: var(--bg-container);
- color: var(--agent-text);
- line-height: 1.5;
- }
- .separator-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 10px;
- }
- .separator-chip {
- display: inline-flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- width: 100%;
- padding: 10px 12px;
- border: 1px solid var(--agent-border);
- border-radius: 10px;
- background: var(--bg-container);
- color: var(--agent-text);
- font-size: 13px;
- text-align: left;
- cursor: pointer;
- transition: all 0.2s ease;
- }
- .separator-chip:hover {
- border-color: var(--agent-border-strong);
- background: var(--bg-container);
- }
- .separator-chip--active {
- border-color: rgba(20, 83, 45, 0.35);
- background: rgba(20, 83, 45, 0.08);
- color: var(--agent-accent);
- }
- .separator-chip__value {
- font-size: 12px;
- color: var(--agent-text-soft);
- }
- .separator-chip__close {
- flex-shrink: 0;
- font-size: 16px;
- line-height: 1;
- opacity: 0.6;
- }
- .drawer-footer {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- padding-top: 6px;
- }
- @media (max-width: 768px) {
- .el-dialog__body {
- padding: 16px;
- }
- :deep(.settings-tabs) {
- display: block;
- }
- :deep(.settings-tabs > .el-tabs__header) {
- margin-right: 0;
- margin-bottom: 16px;
- }
- .form-grid,
- .parser-rule-item,
- .separator-grid {
- grid-template-columns: 1fr;
- }
- }
- }
- </style>
|