Răsfoiți Sursa

问题解决

linyang 3 săptămâni în urmă
părinte
comite
46e032ae54

+ 2 - 0
src/api/search-engine.ts

@@ -60,6 +60,8 @@ export interface KBSearchResultItem {
   doc_name: string
   content: string
   meta_info: string
+  document_id?: string
+  metadata?: Record<string, any>
   score: number
 }
 

+ 8 - 0
src/api/snippet.ts

@@ -47,6 +47,14 @@ export const getSnippets = (params: SnippetParams) => {
   })
 }
 
+export const getSnippetDetail = (kb: string, id: string) => {
+  return request({
+    url: '/api/v1/document/snippet/detail',
+    method: 'get',
+    params: { kb, id }
+  })
+}
+
 export const createSnippet = (data: CreateSnippetData) => {
   return request({
     url: '/api/v1/document/snippet',

+ 164 - 69
src/views/documents/KnowledgeBase.vue

@@ -44,7 +44,11 @@
           </template>
         </el-table-column>
 
-        <el-table-column prop="collection_name" label="知识库表名" min-width="150" />
+        <el-table-column label="知识库表名" min-width="150">
+          <template #default="{ row }">
+            <div style="white-space: pre-wrap; line-height: 1.5;">{{ row.collection_name }}</div>
+          </template>
+        </el-table-column>
         
         <el-table-column prop="document_count" label="文档数量" width="100" />
 
@@ -56,17 +60,17 @@
 
         <el-table-column prop="created_by" label="创建人" width="100" />
         
-        <el-table-column prop="created_at" label="创建时间" width="160">
+        <el-table-column prop="created_time" label="创建时间" width="160">
             <template #default="{ row }">
-                {{ formatTime(row.created_at) }}
+                {{ formatTime(row.created_time) }}
             </template>
         </el-table-column>
 
         <el-table-column prop="updated_by" label="修改人" width="100" />
 
-        <el-table-column prop="updated_at" label="修改时间" width="160">
+        <el-table-column prop="updated_time" label="修改时间" width="160">
             <template #default="{ row }">
-                {{ formatTime(row.updated_at) }}
+                {{ formatTime(row.updated_time) }}
             </template>
         </el-table-column>
 
@@ -144,12 +148,19 @@
                     </el-form-item>
                 </el-col>
                 <el-col :span="12">
-                    <el-form-item label="集合名称" prop="collection_name" v-if="dialogType === 'create'">
-                        <el-input v-model="formData.collection_name" placeholder="Milvus集合名(英文)" />
-                    </el-form-item>
-                    <el-form-item label="集合名称" v-else>
-                        <el-input v-model="formData.collection_name" disabled />
-                    </el-form-item>
+                    <template v-if="dialogType === 'create'">
+                        <el-form-item label="父集合名称" prop="parent_collection_name">
+                            <el-input v-model="formData.parent_collection_name" placeholder="父集合名(英文)" />
+                        </el-form-item>
+                        <el-form-item label="子集合名称" prop="child_collection_name">
+                            <el-input v-model="formData.child_collection_name" placeholder="子集合名(英文)" />
+                        </el-form-item>
+                    </template>
+                    <template v-else>
+                        <el-form-item label="集合名称">
+                            <el-input v-model="formData.collection_name" disabled />
+                        </el-form-item>
+                    </template>
                 </el-col>
             </el-row>
             
@@ -253,12 +264,61 @@
         </span>
       </template>
     </el-dialog>
+    <!-- View Dialog -->
+    <el-dialog
+      v-model="viewDialogVisible"
+      title="知识库详情"
+      width="800px"
+      class="view-dialog"
+    >
+      <el-descriptions :column="2" border class="detail-descriptions">
+        <el-descriptions-item label="知识库名称" label-class-name="desc-label">{{ viewData.name }}</el-descriptions-item>
+        <el-descriptions-item label="集合名称" label-class-name="desc-label">{{ viewData.collection_name }}</el-descriptions-item>
+        <el-descriptions-item label="状态" label-class-name="desc-label">
+            <el-tag :type="getStatusType(viewData.status)" effect="plain">
+                {{ getStatusText(viewData.status) }}
+            </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="文档数量" label-class-name="desc-label">{{ viewData.document_count }}</el-descriptions-item>
+        <el-descriptions-item label="创建时间" label-class-name="desc-label">{{ formatTime(viewData.created_time) }}</el-descriptions-item>
+        <el-descriptions-item label="修改时间" label-class-name="desc-label">{{ formatTime(viewData.updated_time) }}</el-descriptions-item>
+        <el-descriptions-item label="描述" :span="2" label-class-name="desc-label">{{ viewData.description || '暂无描述' }}</el-descriptions-item>
+      </el-descriptions>
+
+      <div class="view-content-section">
+        <div class="section-title">
+            <el-icon><Menu /></el-icon> Milvus Schema 定义
+        </div>
+        <el-table :data="viewSchema" border stripe size="small" style="width: 100%">
+            <el-table-column prop="name" label="字段名称" width="150" />
+            <el-table-column prop="type" label="数据类型" width="150" />
+            <el-table-column prop="desc" label="说明" />
+        </el-table>
+      </div>
+
+      <div class="view-content-section">
+        <div class="section-title">
+            <el-icon><List /></el-icon> 元数据字段 (Metadata)
+        </div>
+        <el-table v-if="viewMetadata.length > 0" :data="viewMetadata" border stripe size="small" style="width: 100%">
+            <el-table-column prop="field_zh_name" label="中文名称" width="150" />
+            <el-table-column prop="field_en_name" label="英文标识" width="150" />
+            <el-table-column prop="field_type" label="类型" width="100">
+                <template #default="{ row }">
+                    <el-tag size="small" type="info">{{ row.field_type === 'text' ? '文本' : '数字' }}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="remark" label="备注" />
+        </el-table>
+        <div v-else class="empty-text">暂无元数据字段定义</div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed } from 'vue'
-import { Search, Plus, Collection, View, Edit, Delete, MoreFilled, Refresh } from '@element-plus/icons-vue'
+import { Search, Plus, Collection, View, Edit, Delete, MoreFilled, Refresh, Menu, List } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
 import { 
@@ -299,12 +359,20 @@ const formData = reactive({
   id: '',
   name: '',
   collection_name: '',
+  parent_collection_name: '',
+  child_collection_name: '',
   description: '',
   dimension: 768,
   status: 'normal',
   metadata_fields: [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
 })
 
+// View Dialog
+const viewDialogVisible = ref(false)
+const viewData = ref<KnowledgeBase>({} as KnowledgeBase)
+const viewMetadata = ref<any[]>([])
+const viewSchema = ref<any[]>([])
+
 // 可选字段列表
 const availableFields = [
     { label: "文件名称", value: "文件名称" },
@@ -331,7 +399,14 @@ const getDisabledOptions = (currentIndex: number) => {
 const rules: FormRules = {
   name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }],
   collection_name: [
-    { required: true, message: '请输入集合名称', trigger: 'blur' },
+    { required: false, message: '请输入集合名称', trigger: 'blur' }
+  ],
+  parent_collection_name: [
+    { required: true, message: '请输入父集合名称', trigger: 'blur' },
+    { pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '必须以字母或下划线开头,只能包含字母、数字和下划线', trigger: 'blur' }
+  ],
+  child_collection_name: [
+    { required: true, message: '请输入子集合名称', trigger: 'blur' },
     { pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '必须以字母或下划线开头,只能包含字母、数字和下划线', trigger: 'blur' }
   ],
   dimension: [{ required: true, message: '请输入维度', trigger: 'blur' }]
@@ -400,8 +475,10 @@ const handleAdd = () => {
   formData.id = ''
   formData.name = ''
   formData.collection_name = ''
+  formData.parent_collection_name = ''
+  formData.child_collection_name = ''
   formData.description = ''
- formData.dimension = 768
+  formData.dimension = 768
   formData.status = 'normal'
   formData.metadata_fields = [{ field_zh_name: '', field_en_name: '', field_type: 'text', remark: '' }]
   dialogVisible.value = true
@@ -411,6 +488,7 @@ const handleEdit = async (row: KnowledgeBase) => {
   formData.id = row.id
   formData.name = row.name
   formData.collection_name = row.collection_name
+  // 暂时不需要回显 parent/child,因为编辑是对单个进行的
   formData.description = row.description || ''
   formData.status = row.status
   
@@ -444,68 +522,39 @@ const handleSync = async (row: KnowledgeBase) => {
 }
 
 const handleView = async (row: KnowledgeBase) => {
-    // 复用编辑时的逻辑加载元数据,但不打开编辑弹窗
+    viewData.value = row
+    viewMetadata.value = []
+    viewSchema.value = [
+        { name: 'pk', type: 'INT64', desc: '主键' },
+        { name: 'text', type: 'VARCHAR', desc: '内容 (Max: 65535)' },
+        { name: 'vector', type: 'FLOAT_VECTOR', desc: '向量列 (Dim: 768)' },
+        { name: 'sparse', type: 'BM25', desc: '关键字检索' },
+        { name: 'document_id', type: 'VARCHAR', desc: '文档ID' },
+        { name: 'parent_id', type: 'VARCHAR', desc: '父段ID' },
+        { name: 'index', type: 'INT64', desc: '索引序号' },
+        { name: 'tag_list', type: 'VARCHAR', desc: '标签' },
+        { name: 'permission', type: 'JSON', desc: '权限' },
+        { name: 'metadata', type: 'JSON', desc: '元数据' },
+        { name: 'is_deleted', type: 'BOOL/INT64', desc: '删除标志' },
+        { name: 'created_by', type: 'VARCHAR', desc: '创建人' },
+        { name: 'created_time', type: 'INT64', desc: '创建时间' },
+        { name: 'updated_by', type: 'VARCHAR', desc: '修改人' },
+        { name: 'updated_time', type: 'INT64', desc: '修改时间' }
+    ]
+
     try {
         const res = await getKnowledgeBaseMetadata(row.id)
-        let metadataInfo = ''
-        let schemaInfo = ''
-        
         if (res.code === 0 && res.data) {
             if (res.data.metadata_fields && res.data.metadata_fields.length > 0) {
-                metadataInfo = res.data.metadata_fields.map((f: any) => 
-                    `<li>${f.field_zh_name} (${f.field_en_name}): ${f.field_type === 'text' ? '文本' : '数字'} - ${f.remark || '无备注'}</li>`
-                ).join('')
-            }
-            
-            if (res.data.custom_schemas && res.data.custom_schemas.length > 0) {
-                schemaInfo = res.data.custom_schemas.map((s: any) => 
-                    `<li>${s.field_name}: ${s.field_type} ${s.max_length ? `(Max: ${s.max_length})` : ''} - ${s.description || '无描述'}</li>`
-                ).join('')
+                viewMetadata.value = res.data.metadata_fields
             }
+            // 如果有动态 Schema 也可以合并到 viewSchema 中,但目前是固定的
         }
-        
-        const content = `
-            <div style="text-align: left;">
-                <p><strong>名称:</strong> ${row.name}</p>
-                <p><strong>集合:</strong> ${row.collection_name}</p>
-                <p><strong>描述:</strong> ${row.description || '暂无'}</p>
-                <p><strong>状态:</strong> ${getStatusText(row.status)}</p>
-                <p><strong>文档数:</strong> ${row.document_count}</p>
-                <p><strong>创建时间:</strong> ${formatTime(row.created_at)}</p>
-                <br>
-                <p><strong>自定义 Schema (Milvus):</strong></p>
-                <ul>
-                    <li>pk: INT64 (主键)</li>
-                    <li>text: VARCHAR (65535) - 内容</li>
-                    <li>vector: FLOAT_VECTOR (768) - 向量列</li>
-                    <li>sparse: BM25 - 关键字检索</li>
-                    <li>document_id: VARCHAR (128) - 文档ID</li>
-                    <li>parent_id: VARCHAR (128) - 父段ID</li>
-                    <li>index: INT64 - 索引序号</li>
-                    <li>tag_list: VARCHAR (2048) - 标签</li>
-                    <li>permission: JSON - 权限</li>
-                    <li>metadata: JSON - 元数据</li>
-                    <li>is_deleted: BOOL - 删除标志</li>
-                    <li>created_by: VARCHAR (128) - 创建人</li>
-                    <li>created_time: INT64 - 创建时间</li>
-                    <li>updated_by: VARCHAR (128) - 修改人</li>
-                    <li>updated_time: INT64 - 修改时间</li>
-                </ul>
-                <br>
-                <p><strong>元数据字段 (Metadata):</strong></p>
-                <ul>${metadataInfo || '<li>无元数据字段</li>'}</ul>
-            </div>
-        `
-        
-        ElMessageBox.alert(content, '知识库详情', {
-            dangerouslyUseHTMLString: true,
-            confirmButtonText: '关闭',
-            customStyle: { maxWidth: '600px' }
-        })
     } catch (error) {
         console.error("加载详情失败", error)
-        ElMessage.error("获取详情失败")
     }
+    
+    viewDialogVisible.value = true
 }
 
 const handleDelete = (row: KnowledgeBase) => {
@@ -564,13 +613,24 @@ const handleSubmit = async () => {
       submitLoading.value = true
       try {
         if (dialogType.value === 'create') {
+          // 1. 创建父集合 KB
           await createKnowledgeBase({
-            name: formData.name,
-            collection_name: formData.collection_name,
+            name: formData.name + ' (父)',
+            collection_name: formData.parent_collection_name,
             description: formData.description,
             dimension: formData.dimension,
             metadata_fields: formData.metadata_fields.filter(f => f.field_zh_name && f.field_en_name)
           })
+
+          // 2. 创建子集合 KB
+          await createKnowledgeBase({
+            name: formData.name + ' (子)',
+            collection_name: formData.child_collection_name,
+            description: formData.description,
+            dimension: formData.dimension,
+            metadata_fields: formData.metadata_fields.filter(f => f.field_zh_name && f.field_en_name)
+          })
+
           ElMessage.success('创建成功')
         } else {
           await updateKnowledgeBase(formData.id, {
@@ -585,6 +645,8 @@ const handleSubmit = async () => {
         loadData()
       } catch (error) {
         console.error(error)
+        // 注意:如果是第二个失败,第一个可能已经创建成功。
+        // 这里简单提示错误,用户可以刷新列表看到已创建的,然后手动处理。
       } finally {
         submitLoading.value = false
       }
@@ -779,4 +841,37 @@ onMounted(() => {
 .delete-btn {
     margin-left: 10px;
 }
+
+/* Detail View Styles */
+:deep(.detail-descriptions .desc-label) {
+    width: 120px;
+    background-color: #f9fafc !important;
+    font-weight: 600;
+    color: #606266;
+}
+
+.view-content-section {
+    margin-top: 24px;
+}
+
+.view-content-section .section-title {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-weight: 600;
+    margin-bottom: 12px;
+    font-size: 15px;
+    color: #303133;
+    border-left: 4px solid #409EFF;
+    padding-left: 10px;
+}
+
+.empty-text {
+    color: #909399;
+    font-size: 13px;
+    padding: 10px 0;
+    text-align: center;
+    background-color: #f8f9fa;
+    border-radius: 4px;
+}
 </style>

+ 147 - 16
src/views/documents/KnowledgeSnippet.vue

@@ -56,9 +56,11 @@
           </template>
         </el-table-column>
 
-        <el-table-column prop="code" label="片段编号" width="160">
+        <el-table-column prop="id" label="系统ID" width="140">
             <template #default="{ row }">
-                <span>{{ row.document_id || row.code || '-' }}</span>
+                <el-tooltip :content="row.code" placement="top">
+                    <span style="font-family: monospace; color: #606266;">{{ row.id }}</span>
+                </el-tooltip>
             </template>
         </el-table-column>
 
@@ -81,7 +83,7 @@
                 </el-tooltip>
             </template>
         </el-table-column>
-
+        
         <el-table-column prop="char_count" label="字符数量" width="100" />
 
         <el-table-column label="元数据信息" min-width="200">
@@ -295,20 +297,30 @@
     <el-dialog
       v-model="viewDialogVisible"
       title="知识片段详情"
-      width="700px"
+      width="800px"
       class="view-dialog"
     >
-      <el-descriptions :column="2" border>
-        <el-descriptions-item label="片段编号">{{ viewData.code }}</el-descriptions-item>
-        <el-descriptions-item label="文档名称">{{ viewData.doc_name }}</el-descriptions-item>
-        <el-descriptions-item label="所属知识库">{{ viewData.collection_name }}</el-descriptions-item>
-        <el-descriptions-item label="字符数量">{{ viewData.char_count }}</el-descriptions-item>
-        <el-descriptions-item label="状态">
+      <el-descriptions :column="2" border class="detail-descriptions">
+        <el-descriptions-item label="系统ID (PK)" label-class-name="desc-label">
+            <span class="copyable-text">{{ viewData.id }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="片段编号" label-class-name="desc-label">{{ viewData.code }}</el-descriptions-item>
+        <el-descriptions-item label="文档名称" label-class-name="desc-label">{{ viewData.doc_name }}</el-descriptions-item>
+        <el-descriptions-item label="所属知识库" label-class-name="desc-label">{{ viewData.collection_name }}</el-descriptions-item>
+        <el-descriptions-item label="字符数量" label-class-name="desc-label">{{ viewData.char_count }}</el-descriptions-item>
+        <el-descriptions-item label="Document ID" label-class-name="desc-label">
+            <span class="copyable-text">{{ viewData.document_id || '-' }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="Parent ID" label-class-name="desc-label">
+            <span class="copyable-text">{{ viewData.parent_id || '-' }}</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="状态" label-class-name="desc-label">
             <el-tag :type="viewData.status === 'normal' ? 'success' : 'info'" effect="plain">
                 {{ viewData.status === 'normal' ? '启用' : '禁用' }}
             </el-tag>
         </el-descriptions-item>
-        <el-descriptions-item label="标签">
+        <el-descriptions-item label="创建时间" label-class-name="desc-label">{{ viewData.created_at || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="标签" :span="2" label-class-name="desc-label">
             <div class="tags-container">
                 <el-tag 
                     v-for="(tag, index) in parseTags(viewData.tag_list)" 
@@ -318,15 +330,28 @@
                 >
                     {{ tag }}
                 </el-tag>
+                <span v-if="!viewData.tag_list" class="no-data">无标签</span>
             </div>
         </el-descriptions-item>
-        <el-descriptions-item label="元数据信息">{{ formatMetaInfo(viewData) }}</el-descriptions-item>
-        <el-descriptions-item label="创建时间">{{ viewData.created_at || '-' }}</el-descriptions-item>
-        <el-descriptions-item label="修改时间">{{ viewData.updated_at || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="元数据信息" :span="2" label-class-name="desc-label">
+            <div class="meta-info-content">{{ formatMetaInfo(viewData) }}</div>
+        </el-descriptions-item>
       </el-descriptions>
       
+      <div class="view-content-section" v-if="viewData.parent_id && viewData.parent_id !== '0'">
+        <div class="section-title">
+            <el-icon><Connection /></el-icon> 父段内容 
+            <span class="sub-title">(ID: {{ viewData.parent_id }})</span>
+        </div>
+        <div class="content-box parent-content" v-loading="parentLoading">
+            {{ parentContent || '暂无内容' }}
+        </div>
+      </div>
+
       <div class="view-content-section">
-        <div class="section-title">片段内容</div>
+        <div class="section-title">
+            <el-icon><DocumentText /></el-icon> 片段内容
+        </div>
         <div class="content-box">
             {{ viewData.content }}
         </div>
@@ -344,6 +369,7 @@ import {
     createSnippet, 
     updateSnippet, 
     deleteSnippet,
+    getSnippetDetail,
     type Snippet 
 } from '@/api/snippet'
 import { getKnowledgeBases, getKnowledgeBaseMetadata, type KnowledgeBase } from '@/api/knowledge-base'
@@ -463,6 +489,8 @@ const loadDocOptions = async (query: string) => {
 // View Dialog
 const viewDialogVisible = ref(false)
 const viewData = ref<Snippet>({} as Snippet)
+const parentContent = ref('')
+const parentLoading = ref(false)
 
 const currentKbSchema = ref<any[]>([]) // 当前选中知识库的自定义Schema
 
@@ -817,8 +845,28 @@ const handleDelete = (row: Snippet) => {
     })
 }
 
-const handleView = (row: Snippet) => {
+const handleView = async (row: Snippet) => {
     viewData.value = { ...row }
+    
+    // 加载父段内容
+    parentContent.value = ''
+    if (row.parent_id && row.parent_id !== '0' && row.parent_id !== '') {
+        parentLoading.value = true
+        try {
+            const res = await getSnippetDetail(row.collection_name, row.parent_id)
+            if (res.code === 0 && res.data) {
+                parentContent.value = res.data.content || '无内容'
+            } else {
+                parentContent.value = '未找到父段内容'
+            }
+        } catch (e) {
+            console.error(e)
+            parentContent.value = '加载失败'
+        } finally {
+            parentLoading.value = false
+        }
+    }
+
     viewDialogVisible.value = true
 }
 
@@ -1018,4 +1066,87 @@ const handleCurrentChange = (val: number) => {
     color: #909399;
     font-size: 12px;
 }
+.id-info-cell {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    font-size: 12px;
+}
+
+.id-row {
+    display: flex;
+    align-items: center;
+    max-width: 100%;
+}
+
+.id-row .label {
+    color: #909399;
+    margin-right: 6px;
+    flex-shrink: 0;
+    width: 45px;
+}
+
+.id-row .value {
+    color: #303133;
+    font-family: monospace;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    background: #f4f4f5;
+    padding: 1px 6px;
+    border-radius: 3px;
+    flex: 1;
+}
+
+.no-data {
+    color: #c0c4cc;
+}
+
+/* Detail View Styles */
+:deep(.detail-descriptions .desc-label) {
+    width: 120px;
+    background-color: #f9fafc !important;
+    font-weight: 600;
+    color: #606266;
+}
+
+.copyable-text {
+    font-family: monospace;
+    color: #409EFF;
+    cursor: text;
+    background: #ecf5ff;
+    padding: 2px 6px;
+    border-radius: 4px;
+}
+
+.meta-info-content {
+    white-space: pre-wrap;
+    word-break: break-all;
+    font-size: 13px;
+    color: #606266;
+}
+
+.view-content-section .section-title {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    font-weight: 600;
+    margin-bottom: 12px;
+    font-size: 16px;
+    color: #303133;
+    border-left: 4px solid #409EFF;
+    padding-left: 10px;
+}
+
+.sub-title {
+    font-size: 13px;
+    color: #909399;
+    font-weight: normal;
+}
+
+.parent-content {
+    background-color: #fcf6ec;
+    border-color: #f3d19e;
+    color: #e6a23c;
+}
 </style>

+ 107 - 7
src/views/documents/SearchEngine.vue

@@ -148,13 +148,31 @@
     <el-card class="result-card" shadow="never" v-if="hasSearched">
       <el-table :data="tableData" v-loading="loading" style="width: 100%">
         <el-table-column prop="kb_name" label="知识库" width="150" show-overflow-tooltip />
-        <el-table-column prop="doc_name" label="文档名称" width="200" show-overflow-tooltip />
+        <el-table-column label="文档名称" width="200" show-overflow-tooltip>
+             <template #default="{ row }">
+                 <span v-if="docNamesMap[row.document_id]">{{ docNamesMap[row.document_id] }}</span>
+                 <span v-else>{{ row.doc_name }}</span>
+             </template>
+        </el-table-column>
         <el-table-column prop="content" label="检索片段内容" min-width="400">
             <template #default="{ row }">
-                <div class="snippet-content" v-html="highlightKeyword(row.content)"></div>
+                <el-tooltip
+                    effect="light"
+                    placement="top-start"
+                    :disabled="!row.content || row.content.length <= 200"
+                >
+                    <template #content>
+                        <div class="tooltip-content-scroll" v-html="highlightKeyword(row.content)"></div>
+                    </template>
+                    <div class="snippet-content" v-html="highlightKeyword(truncateText(row.content, 200))"></div>
+                </el-tooltip>
             </template>
         </el-table-column>
-        <el-table-column prop="meta_info" label="元数据信息" width="250" show-overflow-tooltip />
+        <el-table-column label="元数据信息" width="250" show-overflow-tooltip>
+             <template #default="{ row }">
+                 <span>{{ formatMetaInfo(row) }}</span>
+             </template>
+        </el-table-column>
         <el-table-column prop="score" label="相似度" width="100">
             <template #default="{ row }">
                 <el-tag :type="getScoreType(row.score)">{{ row.score }}%</el-tag>
@@ -337,6 +355,71 @@ const handleKbChange = async () => {
     }
 }
 
+const docNamesMap = ref<Record<string, string>>({})
+
+const formatMetaInfo = (row: any) => {
+    // 优先展示 metadata 字段中的信息
+    if (row.metadata) {
+        // 如果是 JSON 对象,尝试格式化展示
+        if (typeof row.metadata === 'object') {
+             const displayParts = []
+             for (const [key, value] of Object.entries(row.metadata)) {
+                 if (!['doc_name', 'file_name', 'title'].includes(key)) {
+                     const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                     displayParts.push(`${key}: ${valStr}`)
+                 }
+             }
+             if (displayParts.length > 0) return displayParts.join(' | ')
+        } else if (typeof row.metadata === 'string') {
+            try {
+                const metaObj = JSON.parse(row.metadata)
+                const displayParts = []
+                for (const [key, value] of Object.entries(metaObj)) {
+                    if (!['doc_name', 'file_name', 'title'].includes(key)) {
+                        const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                        displayParts.push(`${key}: ${valStr}`)
+                    }
+                }
+                if (displayParts.length > 0) return displayParts.join(' | ')
+            } catch (e) {
+                return String(row.metadata)
+            }
+        }
+    }
+    return row.meta_info || '-'
+}
+
+const loadDocNames = async (docIds: string[]) => {
+    if (docIds.length === 0) return
+    
+    // 过滤掉已经有的
+    const missingIds = docIds.filter(id => !docNamesMap.value[id] && id)
+    if (missingIds.length === 0) return
+
+    // 批量查询文档详情比较低效,documentApi 如果有批量接口最好
+    // 暂时循环查询,或者如果 documentApi.getList 支持 ids 参数
+    // 假设我们只能单个查,为了性能,我们这里只查前 20 个 unique 的
+    
+    // 更好的方式:如果后端 search 接口能直接返回 doc_name 最好。
+    // 但用户要求 "文档来源就根据这个片段的document_id去数据库里面查找"
+    // 意味着我们需要前端二次查询或者后端聚合。
+    // 为了前端响应速度,我们异步查询。
+    
+    for (const id of missingIds) {
+        try {
+            // 这里用 getDetail 查
+            const res = await documentApi.getDetail(id)
+            if (res.code === 0 && res.data) {
+                docNamesMap.value[id] = res.data.title
+            } else {
+                 docNamesMap.value[id] = '未知文档'
+            }
+        } catch (e) {
+            console.error(`加载文档 ${id} 失败`, e)
+        }
+    }
+}
+
 const handleSearch = async () => {
   if (!searchForm.kb_id) {
     ElMessage.warning('请选择知识库')
@@ -388,6 +471,12 @@ const handleSearch = async () => {
     
     tableData.value = res.data.results
     total.value = res.data.total
+    
+    // 异步加载文档名称
+    const docIds = res.data.results.map((r: any) => r.document_id).filter((id: string) => id)
+    // 不阻塞 UI
+    loadDocNames(docIds)
+    
   } catch (error: any) {
     console.error(error)
     ElMessage.error(error.message || '检索失败,请检查配置或稍后重试')
@@ -414,6 +503,12 @@ const highlightKeyword = (text: string) => {
     return text.replace(regex, `<span style="color: #409eff; font-weight: bold;">$&</span>`)
 }
 
+const truncateText = (text: string, length: number) => {
+    if (!text) return ''
+    if (text.length <= length) return text
+    return text.slice(0, length) + '...'
+}
+
 onMounted(() => {
   loadKBs()
 })
@@ -533,12 +628,17 @@ onMounted(() => {
 }
 
 .snippet-content {
-    display: -webkit-box;
-    -webkit-box-orient: vertical;
-    -webkit-line-clamp: 2;
-    overflow: hidden;
     line-height: 1.5;
     color: #606266;
+    word-break: break-all;
+}
+
+.tooltip-content-scroll {
+    max-width: 500px;
+    max-height: 400px;
+    overflow-y: auto;
+    line-height: 1.6;
+    word-break: break-all;
 }
 
 .pagination-container {