feat: 新增归档中心页面并完善知识库与报销查询能力

新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 16:00:19 +08:00
parent 1f15699013
commit 88ff04bef8
120 changed files with 6236 additions and 643 deletions

View File

@@ -61,6 +61,7 @@ export function useTravelReimbursementSubmitComposer(ctx) {
refreshFlowRunDetail,
rememberFilePreviews,
replaceMessage,
resolveComposerDisplaySubmitText,
resetFlowRun,
resolveComposerSubmitText,
reviewInlineForm,
@@ -76,7 +77,6 @@ export function useTravelReimbursementSubmitComposer(ctx) {
startSemanticFlowPreview,
submitting,
syncComposerFilesToDraft,
uploadDecisionDialogOpen,
toast
} = ctx
@@ -109,6 +109,33 @@ export function useTravelReimbursementSubmitComposer(ctx) {
)
}
function resolveReviewPanelScope({
reviewPayload = null,
reviewAction = '',
fileCount = 0,
rawText = ''
} = {}) {
if (!reviewPayload || typeof reviewPayload !== 'object') {
return ''
}
const normalizedAction = String(reviewAction || '').trim()
const documentCount = Array.isArray(reviewPayload.document_cards) ? reviewPayload.document_cards.length : 0
const riskCount = Array.isArray(reviewPayload.risk_briefs) ? reviewPayload.risk_briefs.length : 0
const asksRisk = /风险|隐患|超标|异常|重复|待整改|风险项|高风险|中风险|低风险/.test(String(rawText || ''))
if (fileCount > 0 && documentCount > 0) {
return 'documents'
}
if (riskCount > 0 && (asksRisk || ['next_step', 'submit', 'submit_claim'].includes(normalizedAction))) {
return 'risk'
}
if (!normalizedAction && fileCount === 0) {
return 'overview'
}
return ''
}
async function confirmPendingAttachmentAssociation(message) {
if (submitting.value || sessionSwitchBusy.value) return null
@@ -137,7 +164,6 @@ export function useTravelReimbursementSubmitComposer(ctx) {
userText: `确认归集到草稿 ${runtime.claimNo || '当前草稿'}`,
files: runtime.files,
uploadDisposition: 'continue_existing',
skipUploadDecisionPrompt: true,
skipDraftAssociationPrompt: true,
pendingText: runtime.claimNo
? `正在将票据归集到草稿 ${runtime.claimNo}...`
@@ -189,26 +215,57 @@ export function useTravelReimbursementSubmitComposer(ctx) {
return parts.join('\n')
}
function resolveDetailScopedClaimId() {
if (props.entrySource !== 'detail' || isKnowledgeSession.value) {
return ''
}
return String(
linkedRequest.value?.claimId ||
linkedRequest.value?.claim_id ||
''
).trim()
}
async function submitComposer(options = {}) {
if (submitting.value || sessionSwitchBusy.value) return null
const rawText = resolveComposerSubmitText(options.rawText).trim()
const systemGenerated = Boolean(options.systemGenerated)
const resolvedUploadDisposition =
String(options.uploadDisposition || '').trim() ||
(composerUploadIntent.value === 'continue_existing' ? 'continue_existing' : '')
const normalizedFiles = isKnowledgeSession.value ? [] : Array.from(options.files ?? attachedFiles.value)
const fileMergeResult = mergeFilesWithLimit([], normalizedFiles, MAX_ATTACHMENTS)
const files = fileMergeResult.files
const detailScopedClaimId = resolveDetailScopedClaimId()
const detailScopedUpload = Boolean(detailScopedClaimId && files.length)
if (detailScopedClaimId) {
draftClaimId.value = detailScopedClaimId
}
const resolvedUploadDisposition =
String(options.uploadDisposition || '').trim() ||
(composerUploadIntent.value === 'continue_existing' ? 'continue_existing' : '') ||
(detailScopedUpload ? 'continue_existing' : '')
if (fileMergeResult.overflowCount > 0) {
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
}
if (!rawText && !files.length) return
const fileNames = files.map((file) => file.name)
const initialExtraContext = options.extraContext && typeof options.extraContext === 'object'
const optionExtraContext = options.extraContext && typeof options.extraContext === 'object'
? { ...options.extraContext }
: {}
const detailScopedClaimNo = String(
linkedRequest.value?.documentNo ||
linkedRequest.value?.id ||
''
).trim()
const initialExtraContext = detailScopedClaimId
? {
...optionExtraContext,
draft_claim_id: detailScopedClaimId,
selected_claim_id: detailScopedClaimId,
selected_claim_no: detailScopedClaimNo,
detail_scope_claim_id: detailScopedClaimId
}
: optionExtraContext
const selectedBusinessTimeContext = isKnowledgeSession.value ? null : buildComposerBusinessTimeContext()
const extraContext = isKnowledgeSession.value
? initialExtraContext
@@ -217,7 +274,8 @@ export function useTravelReimbursementSubmitComposer(ctx) {
const attachmentAssociationConfirmed = Boolean(
options.associationConfirmed ||
extraContext.attachment_association_confirmed ||
reviewAction === 'link_to_existing_draft'
reviewAction === 'link_to_existing_draft' ||
detailScopedUpload
)
const hasSelectedExpenseType = Boolean(
extraContext.expense_scene_selection ||
@@ -238,10 +296,9 @@ export function useTravelReimbursementSubmitComposer(ctx) {
hasSelectedExpenseType
})
const reviewAttachmentNames = extractReviewAttachmentNames(activeReviewPayload.value)
const hasExistingDocumentEvent =
Boolean(String(draftClaimId.value || '').trim()) || reviewAttachmentNames.length > 0
const userText =
String(options.userText || '').trim() ||
resolveComposerDisplaySubmitText(rawText) ||
rawText ||
(isKnowledgeSession.value
? `我上传了 ${fileNames.length} 份附件,请帮我回答相关财务问题。`
@@ -254,19 +311,6 @@ export function useTravelReimbursementSubmitComposer(ctx) {
if (
!isKnowledgeSession.value &&
files.length &&
hasExistingDocumentEvent &&
!resolvedUploadDisposition &&
!options.skipUploadDecisionPrompt &&
!reviewAction
) {
uploadDecisionDialogOpen.value = true
return null
}
if (
!isKnowledgeSession.value &&
files.length &&
!hasExistingDocumentEvent &&
!resolvedUploadDisposition &&
!options.skipDraftAssociationPrompt &&
!reviewAction
@@ -300,7 +344,22 @@ export function useTravelReimbursementSubmitComposer(ctx) {
}
} catch (error) {
console.warn('Failed to load draft claims before attachment recognition:', error)
toast(error?.message || '查询可关联草稿失败,已继续按新单据识别。')
resetFlowRun()
if (!options.skipUserMessage) {
messages.value.push(createMessage('user', userText, fileNames))
}
messages.value.push(createMessage(
'assistant',
'我暂时没能查询到可关联的草稿/待补单据,所以先不识别这批附件。请稍后重试,或从对应草稿进入后继续上传票据。',
[],
{
meta: ['单据查询失败']
}
))
nextTick(scrollToBottom)
persistSessionState()
toast(error?.message || '查询可关联草稿失败,请稍后重试。')
return null
}
}
@@ -602,14 +661,24 @@ export function useTravelReimbursementSubmitComposer(ctx) {
queryPayload: normalizeExpenseQueryPayload(payload?.result?.query_payload),
draftPayload: payload?.result?.draft_payload || null,
reviewPayload: payload?.result?.review_payload || null,
reviewPanelScope: resolveReviewPanelScope({
reviewPayload: payload?.result?.review_payload || null,
reviewAction: reviewActionResult,
fileCount: files.length,
rawText
}),
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : []
})
replaceMessage(pendingMessage.id, assistantMessage)
currentInsight.value = buildAgentInsight(
const nextInsight = buildAgentInsight(
payload,
effectiveFileNames,
mergeFilePreviews(filePreviews, ocrFilePreviews)
)
if (nextInsight.agent) {
nextInsight.agent.reviewPanelScope = assistantMessage.reviewPanelScope
}
currentInsight.value = nextInsight
completeFlowResult(payload, flowRunDetail)
persistSessionState()
nextTick(scrollToBottom)