feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和 提取器支持多种格式,增强编排器报销查询的多维度检索,优 化本体规则和用户代理审核消息,前端完善报销创建和审批详 情交互细节,补充单元测试覆盖。
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user