2026-05-22 08:58:59 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
buildAttachmentAssociationConfirmationMessage,
|
|
|
|
|
|
buildUnsavedDraftAttachmentConfirmationMessage
|
2026-05-22 08:58:59 +08:00
|
|
|
|
} from './travelReimbursementAttachmentModel.js'
|
2026-05-25 13:35:39 +08:00
|
|
|
|
import { resolveAssistantScopeGuard } from '../../utils/assistantSessionScope.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
import {
|
2026-06-04 14:25:14 +08:00
|
|
|
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
applyApplicationBusinessTimeContext,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
applyApplicationPolicyEstimateError,
|
|
|
|
|
|
applyApplicationPolicyEstimateResult,
|
|
|
|
|
|
buildApplicationPolicyEstimateRequest,
|
|
|
|
|
|
buildLocalApplicationPreview,
|
|
|
|
|
|
buildLocalApplicationPreviewMessage,
|
|
|
|
|
|
buildModelRefinedApplicationPreview,
|
2026-06-04 14:25:14 +08:00
|
|
|
|
normalizeApplicationPreview,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
shouldUseLocalApplicationPreview
|
|
|
|
|
|
} from '../../utils/expenseApplicationPreview.js'
|
2026-06-01 17:07:14 +08:00
|
|
|
|
import { waitForMockApplicationTransportQuote } from '../../utils/expenseApplicationEstimate.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
import { fetchOntologyParse } from '../../services/ontology.js'
|
|
|
|
|
|
import { buildExpenseApplicationOntologyContext } from '../../utils/expenseApplicationOntology.js'
|
|
|
|
|
|
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
|
2026-06-03 15:46:56 +08:00
|
|
|
|
import { fetchReceiptFolderItems } from '../../services/receiptFolder.js'
|
2026-05-27 12:27:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
handleBudgetCompileReportSubmit,
|
|
|
|
|
|
shouldUseBudgetCompileReport
|
|
|
|
|
|
} from './budgetAssistantReportModel.js'
|
2026-05-22 08:58:59 +08:00
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const STEWARD_ASSISTANT_NAME = '小财管家'
|
|
|
|
|
|
const STEWARD_DELEGATED_TYPEWRITER_INTERVAL_MS = 18
|
|
|
|
|
|
const STEWARD_DELEGATED_THINKING_INTERVAL_MS = 14
|
|
|
|
|
|
const APPLICATION_PREVIEW_FIELD_ACTION_SET = 'set_application_preview_field'
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
export function useTravelReimbursementSubmitComposer(ctx) {
|
|
|
|
|
|
const {
|
|
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
buildAgentInsight,
|
|
|
|
|
|
buildClientTimeContext,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
buildComposerFilePreviews,
|
|
|
|
|
|
buildDraftAssociationQueryPayload,
|
|
|
|
|
|
buildErrorInsight,
|
|
|
|
|
|
buildExpenseIntentConfirmationActions,
|
|
|
|
|
|
buildExpenseIntentConfirmationMessage,
|
|
|
|
|
|
buildExpenseSceneSelectionActions,
|
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
|
buildMessageMeta,
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload,
|
|
|
|
|
|
buildOcrFilePreviews,
|
|
|
|
|
|
buildOcrSummary,
|
|
|
|
|
|
buildOcrSummaryFromDocuments,
|
|
|
|
|
|
buildReviewFormContextFromPayload,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
clearFlowSimulationTimers,
|
|
|
|
|
|
completeFlowResult,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
composerUploadIntent,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
draftClaimId,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
emitDraftSaved,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
emitOperationCompleted,
|
|
|
|
|
|
emitRequestUpdated,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
extractReviewAttachmentNames,
|
|
|
|
|
|
failCurrentFlowStep,
|
|
|
|
|
|
fetchExpenseClaims,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
flowRunId,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
insightPanelCollapsed,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
mergeBusinessTimeIntoExtraContext,
|
|
|
|
|
|
mergeFilePreviews,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeUploadAttachmentNames,
|
|
|
|
|
|
mergeUploadOcrDocuments,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
normalizeExpenseQueryPayload,
|
|
|
|
|
|
normalizeOcrDocuments,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
props,
|
|
|
|
|
|
recognizeOcrFiles,
|
2026-06-01 17:07:14 +08:00
|
|
|
|
refreshCurrentUserFromBackend,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
refreshFlowRunDetail,
|
|
|
|
|
|
rememberFilePreviews,
|
|
|
|
|
|
replaceMessage,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resetFlowRun,
|
|
|
|
|
|
resolveComposerSubmitText,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
runOrchestrator,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
shouldRequestExpenseIntentConfirmation,
|
|
|
|
|
|
shouldRequestExpenseSceneSelection,
|
|
|
|
|
|
startExpenseClaimDraftFlowStep,
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
startSemanticFlowPreview,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
syncComposerFilesToDraft,
|
|
|
|
|
|
toast
|
|
|
|
|
|
} = ctx
|
2026-05-22 08:58:59 +08:00
|
|
|
|
|
|
|
|
|
|
const pendingAttachmentAssociations = new Map()
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
function isStewardDelegatedRun(options = {}) {
|
|
|
|
|
|
return Boolean(options?.stewardContinuation && typeof options.stewardContinuation === 'object')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStewardDelegatedActionLabel(sessionType = '') {
|
|
|
|
|
|
return String(sessionType || '').trim() === 'application'
|
|
|
|
|
|
? '申请单核对'
|
|
|
|
|
|
: '报销单核对'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardDelegatedPlan(continuation = null, thinkingEvents = [], streamStatus = 'streaming') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
planId: String(continuation?.planId || continuation?.plan_id || 'steward_delegation').trim(),
|
|
|
|
|
|
planStatus: 'delegating',
|
|
|
|
|
|
summary: '',
|
|
|
|
|
|
visibleThinkingEventCount: Number.MAX_SAFE_INTEGER,
|
|
|
|
|
|
initialSummaryOnly: true,
|
|
|
|
|
|
thinkingEvents,
|
|
|
|
|
|
tasks: [],
|
|
|
|
|
|
attachmentGroups: [],
|
|
|
|
|
|
confirmationGroups: [],
|
|
|
|
|
|
streamStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveApplicationPreviewMissingFieldsForSteward(preview = {}) {
|
|
|
|
|
|
const normalized = normalizeApplicationPreview(preview)
|
|
|
|
|
|
return Array.isArray(normalized.missingFields) ? normalized.missingFields : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardApplicationPreviewSuggestedActions(preview = {}) {
|
|
|
|
|
|
const missingFields = resolveApplicationPreviewMissingFieldsForSteward(preview)
|
|
|
|
|
|
if (!missingFields.includes('出行方式')) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
const iconMap = {
|
|
|
|
|
|
火车: 'mdi mdi-train',
|
|
|
|
|
|
飞机: 'mdi mdi-airplane',
|
|
|
|
|
|
轮船: 'mdi mdi-ferry'
|
|
|
|
|
|
}
|
|
|
|
|
|
return APPLICATION_TRANSPORT_MODE_OPTIONS.map((mode) => ({
|
|
|
|
|
|
action_type: APPLICATION_PREVIEW_FIELD_ACTION_SET,
|
|
|
|
|
|
label: mode,
|
|
|
|
|
|
description: `选择${mode}作为本次出行方式,并同步费用测算。`,
|
|
|
|
|
|
icon: iconMap[mode] || 'mdi mdi-map-marker-path',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
field_key: 'transportMode',
|
|
|
|
|
|
field_label: '出行方式',
|
|
|
|
|
|
value: mode
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardApplicationPreviewMessage(preview = {}, fallbackText = '') {
|
|
|
|
|
|
const normalized = normalizeApplicationPreview(preview)
|
|
|
|
|
|
const fields = normalized.fields || {}
|
|
|
|
|
|
const missingFields = resolveApplicationPreviewMissingFieldsForSteward(normalized)
|
|
|
|
|
|
if (!missingFields.length) {
|
|
|
|
|
|
return fallbackText
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (missingFields.includes('出行方式')) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'我已经生成这一步的申请单核对结果,但现在还不能继续提交。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'**原因是:还缺少“出行方式”。**',
|
|
|
|
|
|
'',
|
|
|
|
|
|
`本次申请是前往${fields.location || '目的地'}的差旅事项,出行方式会影响交通费用口径和系统预估金额。`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'请先告诉我你打算怎么出行:**火车、飞机或轮船**。我会根据你的选择更新下方核对表和费用测算,再继续判断是否可以提交申请。'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'我已经生成这一步的申请单核对结果,但现在还不能继续提交。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
`**还需要你补充:${missingFields.join('、')}。**`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
`请先补充 **${missingFields[0]}**。你也可以直接点击下方核对表里的对应行编辑;补齐后我再继续推进下一步。`
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardDelegatedThinkingEvents(sessionType = '', continuation = null, context = {}) {
|
|
|
|
|
|
const actionLabel = resolveStewardDelegatedActionLabel(sessionType)
|
|
|
|
|
|
const eventPrefix = `steward-delegated-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
|
|
|
|
const events = [
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-confirm`,
|
|
|
|
|
|
title: '接收确认',
|
|
|
|
|
|
content: '已收到你的确认,小财管家继续推进当前任务。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-coordinate`,
|
|
|
|
|
|
title: '协调能力',
|
|
|
|
|
|
content: `正在协调${actionLabel}能力,先生成可核对的结果;这个过程仍由小财管家统一呈现。`
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
const applicationMissingFields = context.applicationPreview
|
|
|
|
|
|
? resolveApplicationPreviewMissingFieldsForSteward(context.applicationPreview)
|
|
|
|
|
|
: []
|
|
|
|
|
|
if (applicationMissingFields.length) {
|
|
|
|
|
|
events.push({
|
|
|
|
|
|
eventId: `${eventPrefix}-gap`,
|
|
|
|
|
|
title: '识别缺口',
|
|
|
|
|
|
content: `核对结果还缺少${applicationMissingFields.join('、')},我会先向你追问,不直接推进提交。`
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
events.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-output`,
|
|
|
|
|
|
title: '准备输出',
|
|
|
|
|
|
content: '结果准备好后,我会先逐字输出正文,再展示核对表、卡片或确认按钮。'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return events
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStewardDelegatedFinalMeta(finalExtras = {}) {
|
|
|
|
|
|
const sourceMeta = Array.isArray(finalExtras.meta) ? finalExtras.meta : []
|
|
|
|
|
|
const sourceLabel = sourceMeta.find((item) =>
|
|
|
|
|
|
String(item || '').trim() && String(item || '').trim() !== STEWARD_ASSISTANT_NAME
|
|
|
|
|
|
)
|
|
|
|
|
|
const requiresConfirmation = Boolean(
|
|
|
|
|
|
finalExtras.applicationPreview ||
|
|
|
|
|
|
finalExtras.reviewPayload ||
|
|
|
|
|
|
(Array.isArray(finalExtras.suggestedActions) && finalExtras.suggestedActions.length)
|
|
|
|
|
|
)
|
|
|
|
|
|
return [
|
|
|
|
|
|
STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
requiresConfirmation ? '等待用户确认' : '已完成',
|
|
|
|
|
|
sourceLabel || ''
|
|
|
|
|
|
].filter(Boolean).slice(0, 3)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function waitStewardDelegatedTick(intervalMs) {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
globalThis.setTimeout(resolve, intervalMs)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function typeStewardDelegatedMessage(messageId, finalText, finalExtras = {}, context = {}) {
|
|
|
|
|
|
const continuation = finalExtras.stewardContinuation || context.stewardContinuation || null
|
|
|
|
|
|
const message = messages.value.find((item) => item.id === messageId)
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
message.text = ''
|
|
|
|
|
|
message.assistantName = STEWARD_ASSISTANT_NAME
|
|
|
|
|
|
message.meta = [STEWARD_ASSISTANT_NAME, '思考中']
|
|
|
|
|
|
message.suggestedActions = []
|
|
|
|
|
|
message.stewardContinuation = continuation
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [], 'streaming')
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
const typedEvents = []
|
|
|
|
|
|
const thinkingEvents = buildStewardDelegatedThinkingEvents(context.sessionType, continuation, context)
|
|
|
|
|
|
for (const eventData of thinkingEvents) {
|
|
|
|
|
|
const event = {
|
|
|
|
|
|
eventId: eventData.eventId,
|
|
|
|
|
|
stage: 'delegated_action',
|
|
|
|
|
|
title: eventData.title,
|
|
|
|
|
|
content: '',
|
|
|
|
|
|
status: 'running'
|
|
|
|
|
|
}
|
|
|
|
|
|
typedEvents.push(event)
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'streaming')
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
const chars = Array.from(String(eventData.content || ''))
|
|
|
|
|
|
for (let index = 0; index < chars.length; index += 1) {
|
|
|
|
|
|
await waitStewardDelegatedTick(STEWARD_DELEGATED_THINKING_INTERVAL_MS)
|
|
|
|
|
|
event.content = chars.slice(0, index + 1).join('')
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'streaming')
|
|
|
|
|
|
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
event.content = String(eventData.content || '')
|
|
|
|
|
|
event.status = 'completed'
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'streaming')
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const text = String(finalText || '')
|
|
|
|
|
|
message.text = ''
|
|
|
|
|
|
message.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'typing')
|
|
|
|
|
|
const chars = Array.from(text)
|
|
|
|
|
|
for (let index = 0; index < chars.length; index += 1) {
|
|
|
|
|
|
await waitStewardDelegatedTick(STEWARD_DELEGATED_TYPEWRITER_INTERVAL_MS)
|
|
|
|
|
|
message.text = chars.slice(0, index + 1).join('')
|
|
|
|
|
|
message.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
|
|
|
|
|
message.stewardPlan = buildStewardDelegatedPlan(continuation, [...typedEvents], 'typing')
|
|
|
|
|
|
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Object.assign(message, finalExtras, {
|
|
|
|
|
|
id: messageId,
|
|
|
|
|
|
text,
|
|
|
|
|
|
assistantName: STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
meta: resolveStewardDelegatedFinalMeta(finalExtras),
|
|
|
|
|
|
stewardContinuation: continuation,
|
|
|
|
|
|
stewardPlan: buildStewardDelegatedPlan(continuation, [...typedEvents], 'completed')
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetStewardDelegatedInsightState() {
|
|
|
|
|
|
resetFlowRun({ startedAt: 0, openDrawer: false })
|
|
|
|
|
|
insightPanelCollapsed.value = true
|
|
|
|
|
|
currentInsight.value = {
|
|
|
|
|
|
intent: 'welcome',
|
|
|
|
|
|
agent: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 15:46:51 +08:00
|
|
|
|
function isSubmittedApplicationDraftPayload(draftPayload) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
String(draftPayload?.draft_type || '').trim() === 'expense_application'
|
|
|
|
|
|
&& String(draftPayload?.status || '').trim() === 'submitted'
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildOperationFeedbackState(context) {
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
context,
|
|
|
|
|
|
submitting: false,
|
|
|
|
|
|
submitted: false,
|
|
|
|
|
|
dismissed: false,
|
|
|
|
|
|
rating: 0,
|
|
|
|
|
|
reason: '',
|
|
|
|
|
|
error: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAssistantResultText(payload, fallbackAnswer) {
|
|
|
|
|
|
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
|
|
|
|
|
|
if (isSubmittedApplicationDraftPayload(result.draft_payload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return result.answer || result.message || fallbackAnswer
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
function createPendingAttachmentAssociationId() {
|
|
|
|
|
|
return `attachment-association-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
function emitSavedDraftRefresh(draftPayload) {
|
|
|
|
|
|
if (!emitDraftSaved || isKnowledgeSession.value || !draftPayload?.claim_no) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const draftType = String(draftPayload.draft_type || '').trim()
|
|
|
|
|
|
emitDraftSaved({
|
|
|
|
|
|
claimId: String(draftPayload.claim_id || draftPayload.claimId || '').trim(),
|
|
|
|
|
|
claimNo: String(draftPayload.claim_no || draftPayload.claimNo || '').trim(),
|
|
|
|
|
|
status: String(draftPayload.status || '').trim(),
|
|
|
|
|
|
approvalStage: String(draftPayload.approval_stage || draftPayload.approvalStage || '').trim(),
|
|
|
|
|
|
documentType: draftType === 'expense_application' ? 'application' : 'reimbursement'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
function normalizeRecognizedAttachmentData(data) {
|
|
|
|
|
|
if (!data || typeof data !== 'object') {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const documents = Array.isArray(data.ocrDocuments) ? data.ocrDocuments : []
|
|
|
|
|
|
if (!documents.length) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
ocrPayload: data.ocrPayload || null,
|
|
|
|
|
|
ocrSummary: String(data.ocrSummary || '').trim(),
|
|
|
|
|
|
ocrDocuments: documents,
|
|
|
|
|
|
ocrFilePreviews: Array.isArray(data.ocrFilePreviews) ? data.ocrFilePreviews : []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
|
function hasReceiptFolderSourceFile(files) {
|
|
|
|
|
|
return files.some((file) => String(file?.receiptId || '').trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function promptUnlinkedReceiptFolderIfNeeded({
|
|
|
|
|
|
detailScopedClaimId,
|
|
|
|
|
|
files,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
resolvedUploadDisposition,
|
|
|
|
|
|
reviewAction,
|
|
|
|
|
|
systemGenerated,
|
|
|
|
|
|
userText
|
|
|
|
|
|
}) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
isKnowledgeSession.value ||
|
|
|
|
|
|
systemGenerated ||
|
|
|
|
|
|
!files.length ||
|
|
|
|
|
|
detailScopedClaimId ||
|
|
|
|
|
|
resolvedUploadDisposition ||
|
|
|
|
|
|
options.skipReceiptFolderUnlinkedPrompt ||
|
|
|
|
|
|
options.skipDraftAssociationPrompt ||
|
|
|
|
|
|
reviewAction ||
|
|
|
|
|
|
hasReceiptFolderSourceFile(files)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let unlinkedReceipts = []
|
|
|
|
|
|
try {
|
|
|
|
|
|
unlinkedReceipts = await fetchReceiptFolderItems('unlinked')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Failed to load unlinked receipt folder items before attachment upload:', error)
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const count = Array.isArray(unlinkedReceipts) ? unlinkedReceipts.length : 0
|
|
|
|
|
|
if (!count) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resetFlowRun()
|
|
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
`票据夹中还有 ${count} 份未关联票据。建议先处理这些票据再上传新附件,避免重复保存或遗漏关联。`,
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: ['票据夹待关联'],
|
|
|
|
|
|
suggestedActions: [
|
|
|
|
|
|
{
|
|
|
|
|
|
action_type: 'open_receipt_folder',
|
|
|
|
|
|
label: '去票据夹关联',
|
|
|
|
|
|
icon: 'mdi mdi-folder-open-outline',
|
|
|
|
|
|
payload: { target_view: 'receiptFolder' }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
action_type: 'continue_upload_with_unlinked_receipts',
|
|
|
|
|
|
label: '继续上传新附件',
|
|
|
|
|
|
icon: 'mdi mdi-upload-outline',
|
|
|
|
|
|
payload: { raw_text: rawText }
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
function buildConfirmedAssociationText(message) {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
return String(message?.text || '')
|
|
|
|
|
|
.replace(`[确认](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确认')
|
|
|
|
|
|
.replace(`[确定](${ATTACHMENT_ASSOCIATION_CONFIRM_HREF})`, '已确定')
|
2026-05-22 08:58:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
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 ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
async function confirmPendingAttachmentAssociation(message) {
|
|
|
|
|
|
if (submitting.value || sessionSwitchBusy.value) return null
|
|
|
|
|
|
|
|
|
|
|
|
const pending = message?.pendingAttachmentAssociation && typeof message.pendingAttachmentAssociation === 'object'
|
|
|
|
|
|
? message.pendingAttachmentAssociation
|
|
|
|
|
|
: null
|
|
|
|
|
|
const associationId = String(pending?.id || '').trim()
|
|
|
|
|
|
if (!associationId || pending?.status === 'confirmed') {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runtime = pendingAttachmentAssociations.get(associationId)
|
|
|
|
|
|
if (!runtime || !Array.isArray(runtime.files) || !runtime.files.length) {
|
|
|
|
|
|
toast('当前会话里没有可归集的附件原件,请重新上传票据后再确认。')
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pending.status = 'confirmed'
|
|
|
|
|
|
message.pendingAttachmentAssociation = pending
|
|
|
|
|
|
message.text = buildConfirmedAssociationText(message)
|
|
|
|
|
|
message.meta = ['已确认归集']
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
if (pending.mode === 'save_then_associate') {
|
|
|
|
|
|
const inheritedReviewContext = buildReviewFormContextFromPayload(
|
|
|
|
|
|
activeReviewPayload.value,
|
|
|
|
|
|
reviewInlineForm.value
|
|
|
|
|
|
)
|
|
|
|
|
|
const savePayload = await submitComposer({
|
|
|
|
|
|
rawText: '请先把当前已识别的报销信息保存为草稿,随后继续归集本次上传的附件。',
|
|
|
|
|
|
userText: '',
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
skipUserMessage: true,
|
|
|
|
|
|
pendingText: '正在先保存未保存单据...',
|
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
...runtime.extraContext,
|
|
|
|
|
|
...inheritedReviewContext,
|
|
|
|
|
|
review_action: 'save_draft'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const savedClaimId = String(savePayload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
|
|
|
|
|
const savedClaimNo = String(savePayload?.result?.draft_payload?.claim_no || '').trim()
|
|
|
|
|
|
if (!savedClaimId) {
|
|
|
|
|
|
toast('当前单据还没有保存成功,请稍后重试。')
|
|
|
|
|
|
return savePayload
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return submitComposer({
|
|
|
|
|
|
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${savedClaimNo || '当前草稿'}`,
|
|
|
|
|
|
userText: `保存草稿并归集 ${runtime.fileNames.length} 份票据`,
|
|
|
|
|
|
files: runtime.files,
|
|
|
|
|
|
uploadDisposition: 'continue_existing',
|
|
|
|
|
|
skipDraftAssociationPrompt: true,
|
|
|
|
|
|
skipUserMessage: true,
|
|
|
|
|
|
appendToCurrentFlow: true,
|
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
|
pendingText: savedClaimNo
|
|
|
|
|
|
? `草稿 ${savedClaimNo} 已保存,正在识别并归集附件...`
|
|
|
|
|
|
: '草稿已保存,正在识别并归集附件...',
|
|
|
|
|
|
associationConfirmed: true,
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
...runtime.extraContext,
|
|
|
|
|
|
review_action: 'link_to_existing_draft',
|
|
|
|
|
|
draft_claim_id: savedClaimId,
|
|
|
|
|
|
selected_claim_id: savedClaimId,
|
|
|
|
|
|
selected_claim_no: savedClaimNo,
|
|
|
|
|
|
attachment_association_confirmed: true
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
return submitComposer({
|
|
|
|
|
|
rawText: `确认将本次上传的 ${runtime.fileNames.length} 份票据归集到草稿 ${runtime.claimNo || '当前草稿'}`,
|
|
|
|
|
|
userText: `确认归集到草稿 ${runtime.claimNo || '当前草稿'}`,
|
|
|
|
|
|
files: runtime.files,
|
|
|
|
|
|
uploadDisposition: 'continue_existing',
|
|
|
|
|
|
skipDraftAssociationPrompt: true,
|
|
|
|
|
|
pendingText: runtime.claimNo
|
|
|
|
|
|
? `正在将票据归集到草稿 ${runtime.claimNo}...`
|
|
|
|
|
|
: '正在将票据归集到当前草稿...',
|
|
|
|
|
|
associationConfirmed: true,
|
|
|
|
|
|
recognizedAttachmentData: {
|
|
|
|
|
|
ocrPayload: runtime.ocrPayload,
|
|
|
|
|
|
ocrSummary: runtime.ocrSummary,
|
|
|
|
|
|
ocrDocuments: runtime.ocrDocuments,
|
|
|
|
|
|
ocrFilePreviews: runtime.ocrFilePreviews
|
|
|
|
|
|
},
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
...runtime.extraContext,
|
|
|
|
|
|
review_action: 'link_to_existing_draft',
|
|
|
|
|
|
draft_claim_id: runtime.claimId,
|
|
|
|
|
|
selected_claim_id: runtime.claimId,
|
|
|
|
|
|
selected_claim_no: runtime.claimNo,
|
|
|
|
|
|
attachment_association_confirmed: true
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
function buildBackendMessage(rawText, fileNames, ocrSummary = '', sessionTypeOverride = '') {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const parts = []
|
|
|
|
|
|
const normalizedText = String(rawText || '').trim()
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const sessionType = String(sessionTypeOverride || activeSessionType.value || '').trim()
|
|
|
|
|
|
const isKnowledgeMessage = sessionType === 'knowledge'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
if (normalizedText) {
|
|
|
|
|
|
parts.push(normalizedText)
|
|
|
|
|
|
} else if (fileNames.length) {
|
|
|
|
|
|
parts.push(
|
2026-06-04 14:25:14 +08:00
|
|
|
|
isKnowledgeMessage
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? `我上传了 ${fileNames.length} 份附件,请结合附件名称回答财务相关问题。`
|
2026-05-25 13:35:39 +08:00
|
|
|
|
: sessionType === 'application'
|
|
|
|
|
|
? `我上传了 ${fileNames.length} 份附件,请结合附件名称整理费用申请建议和待核对信息。`
|
|
|
|
|
|
: sessionType === 'approval'
|
|
|
|
|
|
? `我上传了 ${fileNames.length} 份附件,请结合附件名称整理审核风险和处理建议。`
|
|
|
|
|
|
: `我上传了 ${fileNames.length} 份票据,请结合附件名称给出报销建议并整理待核对信息。`
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (fileNames.length) {
|
|
|
|
|
|
parts.push(`附件名称:${fileNames.join('、')}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ocrSummary) {
|
|
|
|
|
|
parts.push(`OCR摘要:${ocrSummary}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
|
|
|
|
|
parts.push(`关联单号:${linkedRequest.value.id}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return parts.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function resolveDetailScopedClaimId() {
|
|
|
|
|
|
if (props.entrySource !== 'detail' || isKnowledgeSession.value) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(
|
|
|
|
|
|
linkedRequest.value?.claimId ||
|
|
|
|
|
|
linkedRequest.value?.claim_id ||
|
|
|
|
|
|
''
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
function buildApplicationPreviewReviewMeta(ontology) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'申请核对预览',
|
|
|
|
|
|
String(ontology?.parse_strategy || '').trim() === 'llm_primary'
|
|
|
|
|
|
? '模型复核完成'
|
|
|
|
|
|
: '规则兜底复核'
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-01 17:07:14 +08:00
|
|
|
|
async function resolveApplicationPreviewUser() {
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const user = currentUser.value || {}
|
2026-06-01 17:07:14 +08:00
|
|
|
|
if (String(user.position || '').trim() || typeof refreshCurrentUserFromBackend !== 'function') {
|
|
|
|
|
|
return user
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await refreshCurrentUserFromBackend({ silent: true })
|
|
|
|
|
|
return currentUser.value || user
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
async function buildApplicationPreviewWithModelReview(rawText, businessTimeContext = null, sessionTypeOverride = '') {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const user = await resolveApplicationPreviewUser()
|
2026-06-02 14:01:51 +08:00
|
|
|
|
const localPreview = applyApplicationBusinessTimeContext(
|
|
|
|
|
|
buildLocalApplicationPreview(rawText, user),
|
|
|
|
|
|
businessTimeContext
|
|
|
|
|
|
)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
|
|
|
|
|
|
const enrichWithPolicyEstimate = async (preview) => {
|
|
|
|
|
|
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, user)
|
|
|
|
|
|
if (!estimateRequest.canCalculate) {
|
|
|
|
|
|
return preview
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const fields = preview?.fields || {}
|
|
|
|
|
|
await waitForMockApplicationTransportQuote({
|
|
|
|
|
|
transportMode: fields.transportMode,
|
|
|
|
|
|
location: fields.location,
|
|
|
|
|
|
time: fields.time
|
|
|
|
|
|
})
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const result = await calculateTravelReimbursement(estimateRequest.payload)
|
|
|
|
|
|
return applyApplicationPolicyEstimateResult(preview, result, user)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Application policy estimate failed:', error)
|
|
|
|
|
|
return applyApplicationPolicyEstimateError(preview, error, user)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const ontology = await fetchOntologyParse(
|
|
|
|
|
|
{
|
|
|
|
|
|
query: rawText,
|
|
|
|
|
|
user_id: user.username || user.name || 'anonymous',
|
|
|
|
|
|
context_json: {
|
|
|
|
|
|
...buildExpenseApplicationOntologyContext(user),
|
2026-06-04 14:25:14 +08:00
|
|
|
|
session_type: String(sessionTypeOverride || activeSessionType.value || '').trim(),
|
2026-05-26 09:15:14 +08:00
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
user_input_text: rawText
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
timeoutMs: 45000,
|
|
|
|
|
|
timeoutMessage: '模型抽取申请字段超时,已保留当前本地预览。'
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
const refinedPreview = applyApplicationBusinessTimeContext(
|
|
|
|
|
|
buildModelRefinedApplicationPreview(
|
|
|
|
|
|
localPreview,
|
|
|
|
|
|
ontology,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
user
|
|
|
|
|
|
),
|
|
|
|
|
|
businessTimeContext
|
2026-05-26 09:15:14 +08:00
|
|
|
|
)
|
|
|
|
|
|
return {
|
|
|
|
|
|
applicationPreview: await enrichWithPolicyEstimate(refinedPreview),
|
|
|
|
|
|
meta: buildApplicationPreviewReviewMeta(ontology)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Application preview model refinement failed:', error)
|
|
|
|
|
|
return {
|
|
|
|
|
|
applicationPreview: await enrichWithPolicyEstimate({
|
|
|
|
|
|
...localPreview,
|
|
|
|
|
|
modelReviewStatus: 'failed'
|
|
|
|
|
|
}),
|
|
|
|
|
|
meta: ['申请核对预览', '模型复核失败']
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function submitComposer(options = {}) {
|
|
|
|
|
|
if (submitting.value || sessionSwitchBusy.value) return null
|
|
|
|
|
|
|
|
|
|
|
|
const rawText = resolveComposerSubmitText(options.rawText).trim()
|
|
|
|
|
|
const systemGenerated = Boolean(options.systemGenerated)
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const appendToCurrentFlow = Boolean(options.appendToCurrentFlow)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const effectiveSessionType = String(options.sessionTypeOverride || activeSessionType.value || '').trim()
|
|
|
|
|
|
const stewardDelegated = isStewardDelegatedRun(options)
|
|
|
|
|
|
const effectiveIsKnowledgeSession = effectiveSessionType === 'knowledge'
|
|
|
|
|
|
const normalizedFiles = effectiveIsKnowledgeSession ? [] : Array.from(options.files ?? attachedFiles.value)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const fileMergeResult = mergeFilesWithLimit([], normalizedFiles, MAX_ATTACHMENTS)
|
|
|
|
|
|
const files = fileMergeResult.files
|
2026-05-22 16:00:19 +08:00
|
|
|
|
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' : '')
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (fileMergeResult.overflowCount > 0) {
|
|
|
|
|
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rawText && !files.length) return
|
|
|
|
|
|
const fileNames = files.map((file) => file.name)
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const optionExtraContext = options.extraContext && typeof options.extraContext === 'object'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? { ...options.extraContext }
|
|
|
|
|
|
: {}
|
2026-05-22 16:00:19 +08:00
|
|
|
|
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
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const selectedBusinessTimeContext = effectiveIsKnowledgeSession ? null : buildComposerBusinessTimeContext()
|
|
|
|
|
|
const extraContext = effectiveIsKnowledgeSession
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? initialExtraContext
|
|
|
|
|
|
: mergeBusinessTimeIntoExtraContext(initialExtraContext, selectedBusinessTimeContext)
|
|
|
|
|
|
const reviewAction = String(extraContext.review_action || '').trim()
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const feedbackOperationType = String(options.feedbackOperationType || '').trim()
|
2026-06-02 14:01:51 +08:00
|
|
|
|
const isApplicationSubmitOperation = feedbackOperationType === 'submit_application'
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const attachmentAssociationConfirmed = Boolean(
|
|
|
|
|
|
options.associationConfirmed ||
|
|
|
|
|
|
extraContext.attachment_association_confirmed ||
|
2026-05-22 16:00:19 +08:00
|
|
|
|
reviewAction === 'link_to_existing_draft' ||
|
|
|
|
|
|
detailScopedUpload
|
2026-05-22 08:58:59 +08:00
|
|
|
|
)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const hasSelectedExpenseType = Boolean(
|
|
|
|
|
|
extraContext.expense_scene_selection ||
|
|
|
|
|
|
String(extraContext.review_form_values?.expense_type || extraContext.review_form_values?.reimbursement_type || '').trim()
|
|
|
|
|
|
)
|
|
|
|
|
|
const hasConfirmedExpenseIntent = Boolean(extraContext.expense_intent_confirmed)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const waitForExpenseIntentConfirmation = !stewardDelegated && shouldRequestExpenseIntentConfirmation(rawText, {
|
|
|
|
|
|
sessionType: effectiveSessionType,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
attachmentCount: files.length,
|
|
|
|
|
|
reviewAction,
|
|
|
|
|
|
hasSelectedExpenseType,
|
|
|
|
|
|
hasConfirmedExpenseIntent
|
|
|
|
|
|
})
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const waitForExpenseSceneSelection = !stewardDelegated && !waitForExpenseIntentConfirmation && shouldRequestExpenseSceneSelection(rawText, {
|
|
|
|
|
|
sessionType: effectiveSessionType,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
attachmentCount: files.length,
|
|
|
|
|
|
reviewAction,
|
|
|
|
|
|
hasSelectedExpenseType
|
|
|
|
|
|
})
|
|
|
|
|
|
const reviewAttachmentNames = extractReviewAttachmentNames(activeReviewPayload.value)
|
|
|
|
|
|
const userText =
|
|
|
|
|
|
String(options.userText || '').trim() ||
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText(rawText) ||
|
2026-05-21 23:53:03 +08:00
|
|
|
|
rawText ||
|
2026-06-04 14:25:14 +08:00
|
|
|
|
(effectiveIsKnowledgeSession
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? `我上传了 ${fileNames.length} 份附件,请帮我回答相关财务问题。`
|
|
|
|
|
|
: resolvedUploadDisposition === 'continue_existing'
|
|
|
|
|
|
? `继续上传 ${fileNames.length} 份票据,并归集到当前单据。`
|
|
|
|
|
|
: resolvedUploadDisposition === 'new_document'
|
|
|
|
|
|
? `新上传 ${fileNames.length} 份票据,请单独建立报销单。`
|
|
|
|
|
|
: `我上传了 ${fileNames.length} 份票据,请帮我识别并整理报销建议。`)
|
|
|
|
|
|
|
2026-06-02 14:01:51 +08:00
|
|
|
|
if (shouldUseBudgetCompileReport(rawText, {
|
2026-06-04 14:25:14 +08:00
|
|
|
|
sessionType: effectiveSessionType,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
entrySource: props.entrySource,
|
|
|
|
|
|
budgetContext: props.initialBudgetContext
|
|
|
|
|
|
}) && !reviewAction) {
|
2026-05-27 12:27:17 +08:00
|
|
|
|
return handleBudgetCompileReportSubmit({
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
options,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
replaceMessage,
|
|
|
|
|
|
resetFlowRun,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
refreshCurrentUserFromBackend,
|
|
|
|
|
|
budgetContext: props.initialBudgetContext,
|
2026-05-27 12:27:17 +08:00
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
userText
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const scopeGuard = resolveAssistantScopeGuard(rawText, effectiveSessionType, {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
attachmentCount: files.length,
|
|
|
|
|
|
hasActiveReviewPayload: Boolean(activeReviewPayload.value),
|
|
|
|
|
|
reviewAction
|
|
|
|
|
|
})
|
|
|
|
|
|
if (scopeGuard && !systemGenerated && !reviewAction && !options.skipScopeGuard) {
|
|
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage('assistant', scopeGuard.text, [], {
|
|
|
|
|
|
meta: scopeGuard.meta,
|
|
|
|
|
|
suggestedActions: scopeGuard.suggestedActions
|
|
|
|
|
|
}))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if (shouldUseLocalApplicationPreview(rawText, {
|
2026-06-04 14:25:14 +08:00
|
|
|
|
sessionType: effectiveSessionType,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
attachmentCount: files.length,
|
|
|
|
|
|
reviewAction,
|
|
|
|
|
|
systemGenerated
|
|
|
|
|
|
})) {
|
|
|
|
|
|
const intentStartedAt = Date.now()
|
|
|
|
|
|
const reviewStartedAt = intentStartedAt
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (stewardDelegated) {
|
|
|
|
|
|
resetStewardDelegatedInsightState()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resetFlowRun()
|
|
|
|
|
|
startFlowStep('intent', {
|
|
|
|
|
|
title: '业务意图识别',
|
|
|
|
|
|
tool: 'ontology.intent_detection',
|
|
|
|
|
|
detail: '正在识别是否为费用申请事项...'
|
|
|
|
|
|
})
|
|
|
|
|
|
startFlowStep('application-review-preview', {
|
|
|
|
|
|
title: '申请信息核对',
|
|
|
|
|
|
tool: 'ontology.application_review',
|
|
|
|
|
|
detail: '正在复核申请信息,并查询交通票价...'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
const pendingMessage = createMessage(
|
|
|
|
|
|
'assistant',
|
2026-06-04 14:25:14 +08:00
|
|
|
|
stewardDelegated ? '' : '正在复核申请信息,并查询交通票价,请稍候。',
|
2026-05-26 09:15:14 +08:00
|
|
|
|
[],
|
2026-06-04 14:25:14 +08:00
|
|
|
|
stewardDelegated
|
|
|
|
|
|
? {
|
|
|
|
|
|
assistantName: STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
meta: [STEWARD_ASSISTANT_NAME, '思考中'],
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null,
|
|
|
|
|
|
stewardPlan: buildStewardDelegatedPlan(options.stewardContinuation || null, [], 'streaming')
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
meta: ['模型复核中']
|
|
|
|
|
|
}
|
2026-05-26 09:15:14 +08:00
|
|
|
|
)
|
|
|
|
|
|
messages.value.push(pendingMessage)
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
clearAttachedFiles()
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
|
|
|
|
|
|
submitting.value = true
|
|
|
|
|
|
try {
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const { applicationPreview, meta } = await buildApplicationPreviewWithModelReview(
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
selectedBusinessTimeContext,
|
|
|
|
|
|
effectiveSessionType
|
2026-05-26 09:15:14 +08:00
|
|
|
|
)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const reviewStatus = String(meta?.[1] || '').trim()
|
|
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
completeFlowStep('intent', '已识别为费用申请事项', Date.now() - intentStartedAt)
|
|
|
|
|
|
completeFlowStep(
|
|
|
|
|
|
'application-review-preview',
|
|
|
|
|
|
reviewStatus === '模型复核完成'
|
|
|
|
|
|
? '模型复核完成,已生成申请核对表'
|
|
|
|
|
|
: reviewStatus === '模型复核失败'
|
|
|
|
|
|
? '模型复核失败,已生成临时核对表'
|
|
|
|
|
|
: '模型未返回稳定结果,已完成规则兜底核对',
|
|
|
|
|
|
Date.now() - reviewStartedAt
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (stewardDelegated) {
|
|
|
|
|
|
await typeStewardDelegatedMessage(
|
|
|
|
|
|
pendingMessage.id,
|
|
|
|
|
|
buildLocalApplicationPreviewMessage(applicationPreview),
|
|
|
|
|
|
{
|
|
|
|
|
|
meta,
|
|
|
|
|
|
applicationPreview,
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
sessionType: effectiveSessionType,
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
replaceMessage(pendingMessage.id, createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
buildLocalApplicationPreviewMessage(applicationPreview),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta,
|
|
|
|
|
|
applicationPreview,
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
2026-05-26 09:15:14 +08:00
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated && await promptUnlinkedReceiptFolderIfNeeded({
|
2026-06-03 15:46:56 +08:00
|
|
|
|
detailScopedClaimId,
|
|
|
|
|
|
files,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
resolvedUploadDisposition,
|
|
|
|
|
|
reviewAction,
|
|
|
|
|
|
systemGenerated,
|
|
|
|
|
|
userText
|
|
|
|
|
|
})) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const hasUnsavedReviewDraft = Boolean(
|
2026-06-04 14:25:14 +08:00
|
|
|
|
!stewardDelegated &&
|
|
|
|
|
|
!effectiveIsKnowledgeSession &&
|
2026-05-22 23:47:28 +08:00
|
|
|
|
files.length &&
|
|
|
|
|
|
activeReviewPayload.value &&
|
|
|
|
|
|
!String(draftClaimId.value || '').trim() &&
|
|
|
|
|
|
!detailScopedClaimId &&
|
|
|
|
|
|
!resolvedUploadDisposition &&
|
|
|
|
|
|
!options.skipDraftAssociationPrompt &&
|
|
|
|
|
|
!reviewAction
|
|
|
|
|
|
)
|
|
|
|
|
|
if (hasUnsavedReviewDraft) {
|
|
|
|
|
|
const associationId = createPendingAttachmentAssociationId()
|
|
|
|
|
|
pendingAttachmentAssociations.set(associationId, {
|
|
|
|
|
|
files,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
filePreviews: buildComposerFilePreviews(files),
|
|
|
|
|
|
extraContext
|
|
|
|
|
|
})
|
|
|
|
|
|
resetFlowRun()
|
|
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
buildUnsavedDraftAttachmentConfirmationMessage({ fileNames }),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: ['等待确认保存并归集'],
|
|
|
|
|
|
pendingAttachmentAssociation: {
|
|
|
|
|
|
id: associationId,
|
|
|
|
|
|
mode: 'save_then_associate',
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
fileNames
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (
|
2026-06-04 14:25:14 +08:00
|
|
|
|
!stewardDelegated &&
|
|
|
|
|
|
!effectiveIsKnowledgeSession &&
|
2026-05-21 23:53:03 +08:00
|
|
|
|
files.length &&
|
|
|
|
|
|
!resolvedUploadDisposition &&
|
|
|
|
|
|
!options.skipDraftAssociationPrompt &&
|
|
|
|
|
|
!reviewAction
|
|
|
|
|
|
) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const claims = await fetchExpenseClaims()
|
|
|
|
|
|
const queryPayload = buildDraftAssociationQueryPayload(claims)
|
|
|
|
|
|
if (queryPayload?.records?.length) {
|
|
|
|
|
|
resetFlowRun()
|
|
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
`我找到 ${queryPayload.records.length} 张可关联的草稿/待补单据。请先选择这批附件要归集到哪张单据,我再开始识别附件。`,
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: ['等待选择关联单据'],
|
|
|
|
|
|
queryPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Failed to load draft claims before attachment recognition:', error)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
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
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (stewardDelegated) {
|
|
|
|
|
|
resetStewardDelegatedInsightState()
|
|
|
|
|
|
} else if (!appendToCurrentFlow) {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
resetFlowRun()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
clearFlowSimulationTimers()
|
|
|
|
|
|
}
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated && isApplicationSubmitOperation) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
startFlowStep('application-submit-success', {
|
|
|
|
|
|
title: '申请单提交成功',
|
|
|
|
|
|
tool: 'ApplicationSubmit',
|
|
|
|
|
|
detail: '正在提交费用申请...'
|
|
|
|
|
|
})
|
2026-06-04 14:25:14 +08:00
|
|
|
|
} else if (!stewardDelegated && rawText && !reviewAction) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
startFlowStep('intent', '正在识别业务意图...')
|
|
|
|
|
|
if (waitForExpenseIntentConfirmation) {
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview(rawText)
|
|
|
|
|
|
} else if (waitForExpenseSceneSelection) {
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview(rawText)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
startSemanticFlowPreview(rawText, { attachmentCount: files.length })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const filePreviews = buildComposerFilePreviews(files)
|
|
|
|
|
|
rememberFilePreviews(filePreviews)
|
|
|
|
|
|
|
|
|
|
|
|
// 只有在非静默模式下才添加用户消息
|
|
|
|
|
|
if (!options.skipUserMessage) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText, fileNames))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (waitForExpenseIntentConfirmation) {
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildExpenseIntentConfirmationMessage(rawText), [], {
|
|
|
|
|
|
meta: ['等待确认意图'],
|
|
|
|
|
|
suggestedActions: buildExpenseIntentConfirmationActions(rawText)
|
|
|
|
|
|
}))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
clearAttachedFiles()
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (waitForExpenseSceneSelection) {
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(rawText), [], {
|
|
|
|
|
|
meta: ['等待选择场景'],
|
|
|
|
|
|
suggestedActions: buildExpenseSceneSelectionActions(rawText)
|
|
|
|
|
|
}))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
clearAttachedFiles()
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const pendingMessage = createMessage(
|
|
|
|
|
|
'assistant',
|
2026-06-04 14:25:14 +08:00
|
|
|
|
stewardDelegated ? '' : options.pendingText || (
|
|
|
|
|
|
effectiveIsKnowledgeSession
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? '正在整理财务知识答案...'
|
2026-06-04 14:25:14 +08:00
|
|
|
|
: effectiveSessionType === 'application'
|
2026-06-01 17:07:14 +08:00
|
|
|
|
? '正在识别申请信息并查询交通票价...'
|
2026-06-04 14:25:14 +08:00
|
|
|
|
: effectiveSessionType === 'approval'
|
2026-05-25 13:35:39 +08:00
|
|
|
|
? '正在查询审核上下文并整理风险提示...'
|
|
|
|
|
|
: '正在识别并整理右侧核对信息...'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
),
|
|
|
|
|
|
[],
|
2026-06-04 14:25:14 +08:00
|
|
|
|
stewardDelegated
|
|
|
|
|
|
? {
|
|
|
|
|
|
assistantName: STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
meta: [STEWARD_ASSISTANT_NAME, '思考中'],
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null,
|
|
|
|
|
|
stewardPlan: buildStewardDelegatedPlan(options.stewardContinuation || null, [], 'streaming')
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
meta: ['处理中']
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
messages.value.push(pendingMessage)
|
|
|
|
|
|
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
composerBusinessTimeTags.value = []
|
|
|
|
|
|
composerBusinessTimeDraftTouched.value = false
|
|
|
|
|
|
clearAttachedFiles()
|
|
|
|
|
|
if (fileInputRef.value) {
|
|
|
|
|
|
fileInputRef.value.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(adjustComposerTextareaHeight)
|
|
|
|
|
|
|
|
|
|
|
|
submitting.value = true
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
let responsePayload = null
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
let ocrPayload = null
|
|
|
|
|
|
let ocrSummary = ''
|
|
|
|
|
|
let ocrDocuments = []
|
|
|
|
|
|
let ocrFilePreviews = []
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const recognizedAttachmentData = normalizeRecognizedAttachmentData(options.recognizedAttachmentData)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
if (files.length) {
|
|
|
|
|
|
const ocrStartedAt = Date.now()
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
startFlowStep('ocr', { detail: `正在识别 ${files.length} 份附件...`, startedAt: ocrStartedAt })
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
if (recognizedAttachmentData) {
|
|
|
|
|
|
ocrPayload = recognizedAttachmentData.ocrPayload
|
|
|
|
|
|
ocrSummary = recognizedAttachmentData.ocrSummary || buildOcrSummaryFromDocuments(recognizedAttachmentData.ocrDocuments)
|
|
|
|
|
|
ocrDocuments = [...recognizedAttachmentData.ocrDocuments]
|
|
|
|
|
|
ocrFilePreviews = [...recognizedAttachmentData.ocrFilePreviews]
|
2026-05-21 23:53:03 +08:00
|
|
|
|
rememberFilePreviews(ocrFilePreviews)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
completeFlowStep('ocr', `复用已确认的 ${ocrDocuments.length || files.length} 张票据识别结果`, Date.now() - ocrStartedAt)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
try {
|
|
|
|
|
|
ocrPayload = await recognizeOcrFiles(files, {
|
|
|
|
|
|
timeoutMs: 90000,
|
|
|
|
|
|
timeoutMessage: '票据 OCR 识别超时,已继续使用附件名称处理。'
|
|
|
|
|
|
})
|
|
|
|
|
|
ocrSummary = buildOcrSummary(ocrPayload)
|
|
|
|
|
|
ocrDocuments = normalizeOcrDocuments(ocrPayload)
|
|
|
|
|
|
ocrFilePreviews = buildOcrFilePreviews(ocrPayload)
|
|
|
|
|
|
rememberFilePreviews(ocrFilePreviews)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
completeFlowStep('ocr', `识别到 ${ocrDocuments.length || files.length} 张票据`, Date.now() - ocrStartedAt)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('OCR request failed:', error)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
completeFlowStep('ocr', 'OCR识别失败,已继续使用附件名称', Date.now() - ocrStartedAt)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated && resolvedUploadDisposition === 'continue_existing') {
|
2026-05-22 08:58:59 +08:00
|
|
|
|
replaceMessage(pendingMessage.id, {
|
|
|
|
|
|
...pendingMessage,
|
|
|
|
|
|
text: attachmentAssociationConfirmed
|
|
|
|
|
|
? '票据识别已完成,正在把本次附件归集到已选择的草稿...'
|
|
|
|
|
|
: '票据识别已完成,正在整理归集前确认信息...',
|
|
|
|
|
|
meta: attachmentAssociationConfirmed ? ['正在归集'] : ['等待确认归集']
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const associationTargetClaimId = String(extraContext.draft_claim_id || draftClaimId.value || '').trim()
|
|
|
|
|
|
const associationTargetClaimNo = String(
|
|
|
|
|
|
extraContext.selected_claim_no ||
|
|
|
|
|
|
extraContext.draft_claim_no ||
|
|
|
|
|
|
''
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
if (
|
|
|
|
|
|
files.length &&
|
|
|
|
|
|
resolvedUploadDisposition === 'continue_existing' &&
|
|
|
|
|
|
associationTargetClaimId &&
|
|
|
|
|
|
!attachmentAssociationConfirmed
|
|
|
|
|
|
) {
|
|
|
|
|
|
const associationId = createPendingAttachmentAssociationId()
|
|
|
|
|
|
const pendingAssociation = {
|
|
|
|
|
|
id: associationId,
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
claimId: associationTargetClaimId,
|
|
|
|
|
|
claimNo: associationTargetClaimNo,
|
|
|
|
|
|
fileNames
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
pendingAttachmentAssociations.set(associationId, {
|
|
|
|
|
|
files,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
ocrPayload,
|
|
|
|
|
|
ocrSummary,
|
|
|
|
|
|
ocrDocuments,
|
|
|
|
|
|
ocrFilePreviews,
|
|
|
|
|
|
filePreviews,
|
|
|
|
|
|
claimId: associationTargetClaimId,
|
|
|
|
|
|
claimNo: associationTargetClaimNo,
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
...extraContext,
|
|
|
|
|
|
draft_claim_id: associationTargetClaimId,
|
|
|
|
|
|
selected_claim_id: associationTargetClaimId,
|
|
|
|
|
|
selected_claim_no: associationTargetClaimNo
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
replaceMessage(pendingMessage.id, createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
buildAttachmentAssociationConfirmationMessage({
|
|
|
|
|
|
claimNo: associationTargetClaimNo,
|
|
|
|
|
|
fileNames,
|
|
|
|
|
|
ocrDocuments
|
|
|
|
|
|
}),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: ['等待确认归集'],
|
|
|
|
|
|
pendingAttachmentAssociation: pendingAssociation
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
return null
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let effectiveFileNames = [...fileNames]
|
|
|
|
|
|
let effectiveOcrDocuments = [...ocrDocuments]
|
|
|
|
|
|
let effectiveOcrSummary = ocrSummary
|
|
|
|
|
|
|
|
|
|
|
|
if (resolvedUploadDisposition === 'continue_existing') {
|
|
|
|
|
|
extraContext.review_action = 'link_to_existing_draft'
|
|
|
|
|
|
const inheritedReviewContext = buildReviewFormContextFromPayload(
|
|
|
|
|
|
activeReviewPayload.value,
|
|
|
|
|
|
reviewInlineForm.value
|
|
|
|
|
|
)
|
|
|
|
|
|
if (inheritedReviewContext.review_form_values) {
|
|
|
|
|
|
extraContext.review_form_values = {
|
|
|
|
|
|
...inheritedReviewContext.review_form_values,
|
|
|
|
|
|
...(extraContext.review_form_values && typeof extraContext.review_form_values === 'object'
|
|
|
|
|
|
? extraContext.review_form_values
|
|
|
|
|
|
: {})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (inheritedReviewContext.business_time_context && !extraContext.business_time_context) {
|
|
|
|
|
|
extraContext.business_time_context = inheritedReviewContext.business_time_context
|
|
|
|
|
|
}
|
|
|
|
|
|
effectiveFileNames = mergeUploadAttachmentNames(reviewAttachmentNames, fileNames)
|
|
|
|
|
|
effectiveOcrDocuments = mergeUploadOcrDocuments(
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload(activeReviewPayload.value),
|
|
|
|
|
|
ocrDocuments
|
|
|
|
|
|
)
|
|
|
|
|
|
effectiveOcrSummary = buildOcrSummaryFromDocuments(effectiveOcrDocuments)
|
|
|
|
|
|
} else if (resolvedUploadDisposition === 'new_document') {
|
|
|
|
|
|
extraContext.review_action = 'create_new_claim_from_documents'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!isApplicationSubmitOperation && !stewardDelegated) {
|
2026-06-02 14:01:51 +08:00
|
|
|
|
startExpenseClaimDraftFlowStep(String(extraContext.review_action || '').trim(), {
|
|
|
|
|
|
attachmentCount: effectiveFileNames.length,
|
|
|
|
|
|
waitForSceneSelection: waitForExpenseSceneSelection
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
2026-06-04 14:25:14 +08:00
|
|
|
|
const backendMessage = buildBackendMessage(
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
effectiveFileNames,
|
|
|
|
|
|
effectiveOcrSummary,
|
|
|
|
|
|
effectiveSessionType
|
|
|
|
|
|
)
|
|
|
|
|
|
const orchestratorOptions = effectiveIsKnowledgeSession
|
2026-05-22 08:58:59 +08:00
|
|
|
|
? {
|
2026-05-23 19:54:42 +08:00
|
|
|
|
timeoutMs: 75000,
|
|
|
|
|
|
timeoutMessage: '知识问答仍在检索整理,已停止等待。请稍后重试,或补充制度名称、费用类型等限定条件。'
|
2026-05-22 08:58:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
timeoutMs: 120000,
|
|
|
|
|
|
timeoutMessage: '票据归集处理超时,当前仍停留在原草稿,请稍后重试或重新选择附件。'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const payload = await runOrchestrator(
|
|
|
|
|
|
{
|
|
|
|
|
|
source: 'user_message',
|
|
|
|
|
|
user_id: user.username || user.name || 'anonymous',
|
|
|
|
|
|
conversation_id: conversationId.value || null,
|
|
|
|
|
|
message: backendMessage,
|
|
|
|
|
|
context_json: {
|
|
|
|
|
|
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
|
|
|
|
|
is_admin: Boolean(user.isAdmin),
|
|
|
|
|
|
name: user.name || '',
|
|
|
|
|
|
role: user.role || '',
|
|
|
|
|
|
department: user.department || user.departmentName || '',
|
|
|
|
|
|
department_name: user.department || user.departmentName || '',
|
|
|
|
|
|
position: user.position || '',
|
2026-06-02 14:01:51 +08:00
|
|
|
|
employee_position: user.position || user.employeePosition || user.employee_position || '',
|
|
|
|
|
|
employeePosition: user.position || user.employeePosition || user.employee_position || '',
|
|
|
|
|
|
grade: user.grade || user.employeeGrade || user.employee_grade || '',
|
|
|
|
|
|
employee_grade: user.grade || user.employeeGrade || user.employee_grade || '',
|
|
|
|
|
|
employeeGrade: user.grade || user.employeeGrade || user.employee_grade || '',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
employee_no: user.employeeNo || user.employee_no || '',
|
2026-06-02 14:01:51 +08:00
|
|
|
|
employeeNo: user.employeeNo || user.employee_no || '',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
manager_name: user.managerName || user.manager_name || '',
|
2026-06-02 14:01:51 +08:00
|
|
|
|
managerName: user.managerName || user.manager_name || '',
|
|
|
|
|
|
direct_manager_name: user.managerName || user.manager_name || user.directManagerName || user.direct_manager_name || '',
|
|
|
|
|
|
directManagerName: user.managerName || user.manager_name || user.directManagerName || user.direct_manager_name || '',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
employee_location: user.location || '',
|
|
|
|
|
|
cost_center: user.costCenter || user.cost_center || '',
|
|
|
|
|
|
finance_owner_name: user.financeOwnerName || user.finance_owner_name || '',
|
|
|
|
|
|
employee_risk_profile: user.riskProfile && typeof user.riskProfile === 'object' ? user.riskProfile : {},
|
|
|
|
|
|
...buildClientTimeContext(),
|
2026-06-04 14:25:14 +08:00
|
|
|
|
session_type: effectiveSessionType,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
user_input_text: systemGenerated ? '' : rawText,
|
|
|
|
|
|
attachment_names: effectiveFileNames,
|
|
|
|
|
|
attachment_count: effectiveFileNames.length,
|
2026-06-04 14:25:14 +08:00
|
|
|
|
draft_claim_id: effectiveIsKnowledgeSession ? undefined : draftClaimId.value || undefined,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
ocr_summary: effectiveOcrSummary,
|
|
|
|
|
|
ocr_documents: effectiveOcrDocuments,
|
2026-06-04 14:25:14 +08:00
|
|
|
|
...(linkedRequest.value && !effectiveIsKnowledgeSession ? { request_context: linkedRequest.value } : {}),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
...extraContext
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-05-22 08:58:59 +08:00
|
|
|
|
orchestratorOptions
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
responsePayload = payload
|
2026-06-04 14:25:14 +08:00
|
|
|
|
flowRunId.value = stewardDelegated ? '' : String(payload?.run_id || '').trim()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
let flowRunDetail = null
|
|
|
|
|
|
if (flowRunId.value) {
|
|
|
|
|
|
flowRunDetail = await refreshFlowRunDetail()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
|
|
|
|
|
|
draftClaimId.value =
|
2026-06-04 14:25:14 +08:00
|
|
|
|
effectiveIsKnowledgeSession
|
2026-05-21 23:53:03 +08:00
|
|
|
|
? ''
|
|
|
|
|
|
: String(payload?.result?.draft_payload?.claim_id || '').trim() || draftClaimId.value
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const reviewActionResult = String(extraContext.review_action || '').trim()
|
|
|
|
|
|
const resultClaimNo = String(payload?.result?.draft_payload?.claim_no || '').trim()
|
|
|
|
|
|
const fallbackAnswer = reviewActionResult === 'link_to_existing_draft'
|
|
|
|
|
|
? (resultClaimNo ? `已将本次上传的票据关联到草稿 ${resultClaimNo}。` : '已将本次上传的票据关联到现有草稿。')
|
|
|
|
|
|
: '智能体已完成处理。'
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const operationFeedbackContext = String(payload?.status || '').trim() === 'succeeded'
|
|
|
|
|
|
? emitOperationCompleted?.(payload, {
|
|
|
|
|
|
operationType: feedbackOperationType || reviewActionResult || (files.length ? 'attachment_review' : 'assistant_round')
|
|
|
|
|
|
})
|
|
|
|
|
|
: null
|
|
|
|
|
|
const assistantMessage = createMessage('assistant', resolveAssistantResultText(payload, fallbackAnswer), [], {
|
2026-05-22 08:58:59 +08:00
|
|
|
|
meta: buildMessageMeta(payload, effectiveFileNames),
|
|
|
|
|
|
citations: Array.isArray(payload?.result?.citations) ? payload.result.citations : [],
|
|
|
|
|
|
suggestedActions: Array.isArray(payload?.result?.suggested_actions)
|
|
|
|
|
|
? payload.result.suggested_actions
|
|
|
|
|
|
: [],
|
|
|
|
|
|
queryPayload: normalizeExpenseQueryPayload(payload?.result?.query_payload),
|
|
|
|
|
|
draftPayload: payload?.result?.draft_payload || null,
|
|
|
|
|
|
reviewPayload: payload?.result?.review_payload || null,
|
2026-06-04 14:25:14 +08:00
|
|
|
|
reviewPanelScope: stewardDelegated
|
|
|
|
|
|
? ''
|
|
|
|
|
|
: resolveReviewPanelScope({
|
|
|
|
|
|
reviewPayload: payload?.result?.review_payload || null,
|
|
|
|
|
|
reviewAction: reviewActionResult,
|
|
|
|
|
|
fileCount: files.length,
|
|
|
|
|
|
rawText
|
|
|
|
|
|
}),
|
2026-05-30 15:46:51 +08:00
|
|
|
|
riskFlags: Array.isArray(payload?.result?.risk_flags) ? payload.result.risk_flags : [],
|
2026-06-04 14:25:14 +08:00
|
|
|
|
operationFeedback: buildOperationFeedbackState(operationFeedbackContext),
|
|
|
|
|
|
assistantName: stewardDelegated ? STEWARD_ASSISTANT_NAME : undefined,
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
2026-05-22 08:58:59 +08:00
|
|
|
|
})
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (stewardDelegated) {
|
|
|
|
|
|
await typeStewardDelegatedMessage(
|
|
|
|
|
|
pendingMessage.id,
|
|
|
|
|
|
assistantMessage.text,
|
|
|
|
|
|
{
|
|
|
|
|
|
...assistantMessage,
|
|
|
|
|
|
id: pendingMessage.id,
|
|
|
|
|
|
reviewPanelScope: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
sessionType: effectiveSessionType,
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
resetStewardDelegatedInsightState()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
replaceMessage(pendingMessage.id, assistantMessage)
|
|
|
|
|
|
const nextInsight = buildAgentInsight(
|
|
|
|
|
|
payload,
|
|
|
|
|
|
effectiveFileNames,
|
|
|
|
|
|
mergeFilePreviews(filePreviews, ocrFilePreviews)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (nextInsight.agent) {
|
|
|
|
|
|
nextInsight.agent.reviewPanelScope = assistantMessage.reviewPanelScope
|
|
|
|
|
|
}
|
|
|
|
|
|
currentInsight.value = nextInsight
|
|
|
|
|
|
completeFlowResult(payload, flowRunDetail)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
}
|
2026-06-02 14:01:51 +08:00
|
|
|
|
if (['save_draft', 'link_to_existing_draft', 'create_new_claim_from_documents'].includes(reviewActionResult)) {
|
|
|
|
|
|
emitSavedDraftRefresh(payload?.result?.draft_payload || null)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const resolvedDraftClaimId = String(payload?.result?.draft_payload?.claim_id || draftClaimId.value || '').trim()
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!effectiveIsKnowledgeSession && resolvedDraftClaimId && files.length) {
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const persistComposerFilesToDraft = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const syncResult = await syncComposerFilesToDraft(resolvedDraftClaimId, files)
|
2026-05-22 08:58:59 +08:00
|
|
|
|
persistSessionState()
|
2026-06-01 17:07:14 +08:00
|
|
|
|
if (detailScopedUpload) {
|
2026-05-30 15:46:51 +08:00
|
|
|
|
emitRequestUpdated?.({
|
|
|
|
|
|
claimId: resolvedDraftClaimId,
|
2026-06-01 17:07:14 +08:00
|
|
|
|
source: 'detail-smart-entry-attachment-sync',
|
|
|
|
|
|
uploadedCount: Number(syncResult?.uploadedCount || 0),
|
|
|
|
|
|
skippedCount: Number(syncResult?.skippedCount || 0)
|
2026-05-30 15:46:51 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-06-01 17:07:14 +08:00
|
|
|
|
} catch (error) {
|
2026-05-22 08:58:59 +08:00
|
|
|
|
console.warn('Failed to persist composer attachments to draft claim:', error)
|
|
|
|
|
|
toast(error?.message || '票据已归集到草稿,但附件原件保存失败,请在单据详情中重新上传。')
|
2026-06-01 17:07:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const persistTask = persistComposerFilesToDraft()
|
|
|
|
|
|
if (detailScopedUpload) {
|
|
|
|
|
|
await persistTask
|
|
|
|
|
|
} else {
|
|
|
|
|
|
void persistTask
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
clearFlowSimulationTimers()
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (!stewardDelegated) {
|
|
|
|
|
|
failCurrentFlowStep(error)
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
replaceMessage(
|
|
|
|
|
|
pendingMessage.id,
|
|
|
|
|
|
createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
error?.message || '无法连接后端 Orchestrator,请稍后重试。',
|
|
|
|
|
|
[],
|
2026-06-04 14:25:14 +08:00
|
|
|
|
stewardDelegated
|
|
|
|
|
|
? {
|
|
|
|
|
|
assistantName: STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
meta: [STEWARD_ASSISTANT_NAME, '调用失败'],
|
|
|
|
|
|
stewardContinuation: options.stewardContinuation || null
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
meta: ['调用失败']
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (stewardDelegated) {
|
|
|
|
|
|
resetStewardDelegatedInsightState()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
currentInsight.value = buildErrorInsight(error, fileNames)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
persistSessionState()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
submitting.value = false
|
|
|
|
|
|
composerUploadIntent.value = ''
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return responsePayload
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-22 08:58:59 +08:00
|
|
|
|
confirmPendingAttachmentAssociationInternal: confirmPendingAttachmentAssociation,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposerInternal: submitComposer
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|