<template><div class="page-content"><div class="mb-5 flex items-start justify-between"><div><h2 class="text-xl font-semibold">知识库</h2><p class="text-g-500 mt-1 text-sm">PostgreSQL 文本分块与全文检索基线，无需 MinIO/S3；Retriever 接口可后续替换为向量检索</p></div><ElButton type="primary" @click="openCreate">新建知识库</ElButton></div><ElAlert class="mb-4" type="info" :closable="false" title="正文限制 2 MiB 并直接保存在 PostgreSQL；调整分块参数只影响后续入库文档。"/><ElTable v-loading="loading" :data="items" row-key="id"><ElTableColumn prop="name" label="名称" min-width="180"/><ElTableColumn prop="retrieval_mode" label="检索器" width="140"/><ElTableColumn label="分块" width="130"><template #default="{row}">{{row.chunk_size}} / {{row.chunk_overlap}}</template></ElTableColumn><ElTableColumn prop="document_count" label="文档" width="80"/><ElTableColumn prop="chunk_count" label="切片" width="80"/><ElTableColumn label="状态" width="90"><template #default="{row}"><ElTag :type="row.enabled?'success':'info'">{{row.enabled?'启用':'停用'}}</ElTag></template></ElTableColumn><ElTableColumn label="操作" width="250" fixed="right"><template #default="{row}"><ElButton link type="primary" @click="openDocuments(row)">文档</ElButton><ElButton link type="primary" @click="openSearch(row)">检索</ElButton><ElButton link type="primary" @click="openEdit(row)">编辑</ElButton><ElButton link type="danger" @click="remove(row)">删除</ElButton></template></ElTableColumn></ElTable>
<ElDialog v-model="visible" :title="editing?'编辑知识库':'新建知识库'" width="680px"><ElForm label-width="110px"><ElFormItem label="名称"><ElInput v-model="form.name"/></ElFormItem><ElFormItem label="描述"><ElInput v-model="form.description" type="textarea"/></ElFormItem><div class="grid grid-cols-2 gap-x-4"><ElFormItem label="切片字符数"><ElInputNumber v-model="form.chunk_size" :min="200" :max="8000"/></ElFormItem><ElFormItem label="重叠字符数"><ElInputNumber v-model="form.chunk_overlap" :min="0" :max="form.chunk_size/2"/></ElFormItem></div><ElFormItem label="部门范围"><ElSelect v-model="form.department_ids" multiple allow-create filterable class="w-full" placeholder="留空表示全部部门"/></ElFormItem><ElFormItem label="启用"><ElSwitch v-model="form.enabled"/></ElFormItem></ElForm><template #footer><ElButton @click="visible=false">取消</ElButton><ElButton type="primary" @click="save">保存</ElButton></template></ElDialog>
<ElDrawer v-model="documentsVisible" :title="`${selected?.name||''} · 文档`" size="65%"><ElForm label-width="90px"><ElFormItem label="标题"><ElInput v-model="documentForm.title"/></ElFormItem><ElFormItem label="来源 URI"><ElInput v-model="documentForm.source_uri" placeholder="可选，仅作来源标记"/></ElFormItem><ElFormItem label="正文"><ElInput v-model="documentForm.content" type="textarea" :rows="8" show-word-limit maxlength="2097152"/></ElFormItem><ElFormItem><ElButton type="primary" @click="addDocument">入库并切片</ElButton></ElFormItem></ElForm><ElTable :data="documents"><ElTableColumn prop="title" label="标题"/><ElTableColumn prop="char_count" label="字符" width="90"/><ElTableColumn prop="chunk_count" label="切片" width="80"/><ElTableColumn prop="status" label="状态" width="90"/><ElTableColumn label="操作" width="80"><template #default="{row}"><ElButton link type="danger" @click="removeDocument(row)">删除</ElButton></template></ElTableColumn></ElTable></ElDrawer>
<ElDialog v-model="searchVisible" :title="`${selected?.name||''} · 检索测试`" width="760px"><div class="mb-4 flex gap-2"><ElInput v-model="query" placeholder="输入检索词" @keyup.enter="runSearch"/><ElInputNumber v-model="topK" :min="1" :max="20"/><ElButton type="primary" @click="runSearch">检索</ElButton></div><ElCard v-for="hit in hits" :key="hit.chunk_id" class="mb-3"><template #header>{{hit.document_title}} · #{{hit.chunk_index}} · {{hit.score.toFixed(3)}}</template><div class="whitespace-pre-wrap text-sm">{{hit.content}}</div></ElCard><ElEmpty v-if="!hits.length" description="暂无命中"/></ElDialog></div></template>
<script setup lang="ts">import{KnowledgeBase,KnowledgeBaseInput,KnowledgeDocument,SearchHit,addKnowledgeDocument,createKnowledgeBase,deleteKnowledgeBase,deleteKnowledgeDocument,fetchKnowledgeBases,fetchKnowledgeDocuments,searchKnowledge,updateKnowledgeBase}from'@/api/workbench';import{ElMessage,ElMessageBox}from'element-plus';const loading=ref(false),visible=ref(false),documentsVisible=ref(false),searchVisible=ref(false),editing=ref('');const items=ref<KnowledgeBase[]>([]),documents=ref<KnowledgeDocument[]>([]),hits=ref<SearchHit[]>([]),selected=ref<KnowledgeBase>(),query=ref(''),topK=ref(4);const blank=():KnowledgeBaseInput=>({name:'',description:'',retrieval_mode:'postgres_fts',chunk_size:800,chunk_overlap:100,department_ids:[],enabled:true});const form=reactive<KnowledgeBaseInput>(blank()),documentForm=reactive({title:'',source_type:'text',source_uri:'',content:''});async function load(){loading.value=true;try{items.value=await fetchKnowledgeBases()}finally{loading.value=false}}function openCreate(){editing.value='';Object.assign(form,blank());visible.value=true}function openEdit(row:KnowledgeBase){editing.value=row.id;Object.assign(form,{name:row.name,description:row.description,retrieval_mode:row.retrieval_mode,chunk_size:row.chunk_size,chunk_overlap:row.chunk_overlap,department_ids:[...row.department_ids],enabled:row.enabled});visible.value=true}async function save(){editing.value?await updateKnowledgeBase(editing.value,form):await createKnowledgeBase(form);visible.value=false;await load()}async function remove(row:KnowledgeBase){await ElMessageBox.confirm(`删除“${row.name}”及全部文档？`,'确认',{type:'warning'});await deleteKnowledgeBase(row.id);await load()}async function openDocuments(row:KnowledgeBase){selected.value=row;documents.value=await fetchKnowledgeDocuments(row.id);Object.assign(documentForm,{title:'',source_type:'text',source_uri:'',content:''});documentsVisible.value=true}async function addDocument(){if(!selected.value)return;await addKnowledgeDocument(selected.value.id,documentForm);ElMessage.success('文档已入库');await openDocuments(selected.value);await load()}async function removeDocument(row:KnowledgeDocument){if(!selected.value)return;await deleteKnowledgeDocument(selected.value.id,row.id);await openDocuments(selected.value);await load()}function openSearch(row:KnowledgeBase){selected.value=row;hits.value=[];query.value='';searchVisible.value=true}async function runSearch(){if(!selected.value||!query.value.trim())return;hits.value=await searchKnowledge(selected.value.id,{query:query.value,top_k:topK.value})}onMounted(load)</script>
