feat: 增强知识库索引与设置页面模块化拆分

扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优
化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件
和 Hermes 员工同步子面板并重构样式,新增日志详情组件和
知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 23:47:28 +08:00
parent 88ff04bef8
commit 5b388d08c0
84 changed files with 10170 additions and 2599 deletions

View File

@@ -39,6 +39,26 @@ export const MAX_OCR_DOCUMENTS = 10
export const VISIBLE_ATTACHMENT_CHIPS = 2
export const ATTACHMENT_ASSOCIATION_CONFIRM_HREF = '#confirm-attachment-association'
export function buildUnsavedDraftAttachmentConfirmationMessage({ fileNames = [] } = {}) {
const names = (Array.isArray(fileNames) ? fileNames : [])
.map((item) => String(item || '').trim())
.filter(Boolean)
const attachmentLine = names.length
? `本次待归集附件:${names.length} 份(${names.join('、')}`
: '本次待归集附件:待识别'
return [
'当前这笔报销信息还没有保存为草稿。',
'',
'如果继续上传票据,我需要先把当前已识别的信息保存成一张草稿单据,再识别并归集本次附件。',
'',
attachmentLine,
'',
'',
`如果 **[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})**,我会先保存这笔未保存单据,再把此次上传的附件归集到该单据。`
].join('\n').trim()
}
export function normalizeOcrDocuments(payload) {
const documents = Array.isArray(payload?.documents) ? payload.documents : []
return documents.slice(0, MAX_OCR_DOCUMENTS).map((item) => ({
@@ -333,6 +353,10 @@ export function resolveDocumentPreview(filePreviews, filename) {
)
}
export function isTemporaryPreviewUrl(url) {
return String(url || '').trim().toLowerCase().startsWith('blob:')
}
export function buildFileIdentity(file) {
return [file?.name, file?.size, file?.lastModified, file?.type].join('__')
}
@@ -374,18 +398,39 @@ export function mergeFilesWithLimit(existingFiles, incomingFiles, limit = MAX_AT
export function mergeFilePreviews(existingPreviews, incomingPreviews) {
const result = []
const seen = new Set()
const indexByKey = new Map()
for (const preview of [...(existingPreviews || []), ...(incomingPreviews || [])]) {
const key = [preview?.filename, preview?.kind].join('__')
if (!preview?.filename || seen.has(key)) continue
seen.add(key)
result.push(preview)
if (!preview?.filename) continue
const existingIndex = indexByKey.get(key)
if (existingIndex === undefined) {
indexByKey.set(key, result.length)
result.push(preview)
continue
}
const existingPreview = result[existingIndex]
const nextUrl = String(preview?.url || '').trim()
const existingUrl = String(existingPreview?.url || '').trim()
if (nextUrl && (!existingUrl || isTemporaryPreviewUrl(existingUrl) || nextUrl !== existingUrl)) {
result[existingIndex] = preview
}
}
return result
}
export function filterPersistableFilePreviews(filePreviews) {
return (Array.isArray(filePreviews) ? filePreviews : [])
.filter((preview) => {
const filename = String(preview?.filename || '').trim()
const url = String(preview?.url || '').trim()
return filename && !isTemporaryPreviewUrl(url)
})
}
function inferPreviewKindFromUrl(url) {
const normalized = String(url || '').trim().toLowerCase()
if (!normalized) return ''