import { fetchStewardRuntimeDecision } from '../../services/steward.js' import { buildApplicationPreviewSubmitText, buildLocalApplicationPreviewMessage, normalizeApplicationPreview } from '../../utils/expenseApplicationPreview.js' import { buildTravelPlanningNudgeMessage, buildTravelPlanningSuggestedActions } from '../../utils/travelApplicationPlanning.js' import { SESSION_TYPE_APPLICATION, SESSION_TYPE_STEWARD } from './travelReimbursementConversationModel.js' import { APPLICATION_PREVIEW_FIELD_ACTION_SET, STEWARD_ASSISTANT_NAME, isApplicationSubmitConfirmationText, isStewardRuntimeCancelText, isStewardRuntimeContinueText, normalizeStewardRuntimeInputText, resolveStewardRuntimeTransportAlias, shouldPlanNewStewardTasksLocally } from './travelReimbursementStewardRuntimeTextModel.js' import { buildStewardContinuationAfterAction, pushStewardContinuationMessage, resolveStewardMissingFieldItems } from './travelReimbursementStewardFollowupFlow.js' export { STEWARD_ASSISTANT_NAME } from './travelReimbursementStewardRuntimeTextModel.js' export function useTravelReimbursementStewardRuntime(ctx) { const { activeSessionType, applicationSubmitConfirmDialog, attachedFiles, composerDraft, createMessage, currentUser, emit, handleSuggestedAction, isStewardSession, linkedRequest, messages, nextTick, persistSessionState, props, reviewActionBusy, scrollToBottom, sessionSwitchBusy, submitComposer, submitStewardPlan, submitting, toast, adjustComposerTextareaHeight, resolveCurrentUserId } = ctx function findLatestApplicationPreviewMessage() { for (const message of [...messages.value].reverse()) { if ( message?.role !== 'assistant' || !message.applicationPreview || message.applicationSubmitConfirmed ) { continue } return message } return null } function findPendingApplicationSubmitMessage() { const message = findLatestApplicationPreviewMessage() if (!message) { return null } const normalizedPreview = normalizeApplicationPreview(message.applicationPreview) if (normalizedPreview.readyToSubmit) { message.applicationPreview = normalizedPreview return message } return null } function pushApplicationSubmitBlockedMessage(userText = '', message = null, options = {}) { const normalizedPreview = normalizeApplicationPreview(message?.applicationPreview || {}) const missingFields = Array.isArray(normalizedPreview.missingFields) ? normalizedPreview.missingFields : [] const validationIssues = Array.isArray(normalizedPreview.validationIssues) ? normalizedPreview.validationIssues : [] if (userText && !options.userMessageAlreadyAdded) { messages.value.push(createMessage('user', userText)) } messages.value.push(createMessage( 'assistant', [ '我理解你是在确认当前申请单,但这张申请单还不能提交。', '', missingFields.length ? `还需要先补充:**${missingFields.join('、')}**。` : validationIssues.length ? `需要先修正:**${validationIssues[0].message}**` : '请先把申请核对表中的待补充信息补齐。', '', '补齐后再输入“确认”,我会继续提交至审批流程。' ].join('\n'), [], { assistantName: String(message?.assistantName || '').trim() || undefined, meta: ['等待补充'] } )) composerDraft.value = '' persistSessionState() nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) } async function handleApplicationSubmitConfirmationText(options = {}) { const rawText = String(options.rawText ?? composerDraft.value ?? '').trim() const files = Array.from(options.files ?? attachedFiles.value ?? []) if (!isApplicationSubmitConfirmationText(rawText) || files.length) { return false } const latestApplicationMessage = findLatestApplicationPreviewMessage() if (!latestApplicationMessage) { return false } const targetMessage = findPendingApplicationSubmitMessage() if (!targetMessage) { pushApplicationSubmitBlockedMessage(rawText, latestApplicationMessage) return true } applicationSubmitConfirmDialog.value = { open: true, message: targetMessage } await confirmApplicationSubmit({ userText: rawText }) return true } function findPendingStewardSuggestedActionContext(decision = null) { const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim() const targetTaskId = String(decision?.target_task_id || decision?.targetTaskId || '').trim() for (const message of [...messages.value].reverse()) { if ( message?.role !== 'assistant' || message.suggestedActionsLocked || !Array.isArray(message.suggestedActions) || !message.suggestedActions.length ) { continue } if (targetMessageId && String(message.id || '') !== targetMessageId) { continue } const action = message.suggestedActions.find((item) => { if (String(item?.action_type || '').trim() === APPLICATION_PREVIEW_FIELD_ACTION_SET) { return false } const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {} return !targetTaskId || String(payload.steward_next_task_id || payload.target_task_id || '').trim() === targetTaskId }) || message.suggestedActions[0] if (action) { return { message, action } } } return null } function findPendingSlotSuggestedActionContext(decision = null) { const fieldKey = String(decision?.field_key || decision?.fieldKey || '').trim() const fieldValue = String(decision?.field_value || decision?.fieldValue || '').trim() for (const message of [...messages.value].reverse()) { if ( message?.role !== 'assistant' || message.suggestedActionsLocked || !Array.isArray(message.suggestedActions) || !message.suggestedActions.length ) { continue } const action = message.suggestedActions.find((item) => { if (String(item?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) { return false } const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {} const payloadField = String(payload.field_key || payload.fieldKey || '').trim() const payloadValue = String(payload.value || item?.label || '').trim() return payloadField && (!fieldKey || payloadField === fieldKey) && (!fieldValue || payloadValue === fieldValue) }) if (action) { return { message, action } } } return null } function findPendingSlotSuggestedActionContextByInput(rawText = '') { const normalizedInput = normalizeStewardRuntimeInputText(rawText) if (!normalizedInput) { return null } const transportAlias = resolveStewardRuntimeTransportAlias(normalizedInput) for (const message of [...messages.value].reverse()) { if ( message?.role !== 'assistant' || message.suggestedActionsLocked || !Array.isArray(message.suggestedActions) || !message.suggestedActions.length ) { continue } const exactMatches = [] const fuzzyMatches = [] message.suggestedActions.forEach((action) => { if (String(action?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) { return } const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const fieldKey = String(payload.field_key || payload.fieldKey || '').trim() const value = String(payload.value || action?.label || '').trim() const label = String(action?.label || value).trim() const tokens = [value, label] .map((item) => normalizeStewardRuntimeInputText(item)) .filter(Boolean) if (!fieldKey || !value || !tokens.length) { return } if (tokens.includes(normalizedInput)) { exactMatches.push({ message, action }) return } const actionTransportAlias = resolveStewardRuntimeTransportAlias(`${value}${label}`) if ( transportAlias && ( tokens.includes(normalizeStewardRuntimeInputText(transportAlias)) || actionTransportAlias === transportAlias ) ) { fuzzyMatches.push({ message, action }) return } if (tokens.some((token) => token.length >= 2 && normalizedInput.includes(token))) { fuzzyMatches.push({ message, action }) } }) if (exactMatches.length === 1) { return exactMatches[0] } if (exactMatches.length > 1) { return null } const uniqueFuzzyMatches = fuzzyMatches.filter((item, index, list) => list.findIndex((candidate) => candidate.action === item.action) === index ) if (uniqueFuzzyMatches.length === 1) { return uniqueFuzzyMatches[0] } if (uniqueFuzzyMatches.length > 1) { return null } } return null } function buildStewardRuntimeState() { const latestApplicationMessage = findLatestApplicationPreviewMessage() const applicationPreview = latestApplicationMessage?.applicationPreview ? normalizeApplicationPreview(latestApplicationMessage.applicationPreview) : null const applicationContinuation = latestApplicationMessage?.stewardContinuation || null const pendingSlotContext = findPendingSlotSuggestedActionContext() const pendingStewardContext = pendingSlotContext ? null : findPendingStewardSuggestedActionContext() const pendingActionPayload = pendingStewardContext?.action?.payload && typeof pendingStewardContext.action.payload === 'object' ? pendingStewardContext.action.payload : {} const pendingSlotPayload = pendingSlotContext?.action?.payload && typeof pendingSlotContext.action.payload === 'object' ? pendingSlotContext.action.payload : {} const continuation = applicationContinuation || pendingStewardContext?.message?.stewardContinuation || null const remainingTasks = Array.isArray(continuation?.remainingTasks) ? continuation.remainingTasks : [] const pendingApplication = latestApplicationMessage && applicationPreview ? { message_id: String(latestApplicationMessage.id || '').trim(), task_id: String( applicationContinuation?.currentTaskId || applicationContinuation?.current_task_id || applicationContinuation?.currentTask?.task_id || applicationContinuation?.currentTask?.taskId || '' ).trim(), ready_to_submit: Boolean(applicationPreview.readyToSubmit), missing_fields: Array.isArray(applicationPreview.missingFields) ? applicationPreview.missingFields : [], fields: applicationPreview.fields || {} } : null return { waiting_for: pendingApplication ? (pendingApplication.ready_to_submit ? 'application_submit_confirmation' : 'application_field_completion') : pendingSlotContext ? 'application_field_completion' : pendingStewardContext ? 'steward_next_task_confirmation' : '', current_task: continuation?.currentTask || continuation?.current_task || null, remaining_tasks: remainingTasks, completed_tasks: messages.value .filter((message) => message?.applicationSubmitConfirmed) .map((message) => ({ message_id: String(message.id || '').trim(), task_type: 'expense_application' })), pending_application: pendingApplication, pending_steward_action: pendingStewardContext ? { message_id: String(pendingStewardContext.message?.id || '').trim(), action_type: String(pendingStewardContext.action?.action_type || '').trim(), label: String(pendingStewardContext.action?.label || '').trim(), target_task_id: String(pendingActionPayload.steward_next_task_id || pendingActionPayload.target_task_id || '').trim(), payload: pendingActionPayload } : null, pending_slot_action: pendingSlotContext ? { message_id: String(pendingSlotContext.message?.id || '').trim(), field_key: String(pendingSlotPayload.field_key || pendingSlotPayload.fieldKey || '').trim(), label: String(pendingSlotContext.action?.label || '').trim(), payload: pendingSlotPayload } : null } } function hasActiveStewardRuntimeDecisionContext(runtimeState = {}) { return Boolean( String(runtimeState?.waiting_for || '').trim() || runtimeState?.pending_application || runtimeState?.pending_steward_action || runtimeState?.pending_slot_action || runtimeState?.current_task || (Array.isArray(runtimeState?.remaining_tasks) && runtimeState.remaining_tasks.length > 0) || (Array.isArray(runtimeState?.completed_tasks) && runtimeState.completed_tasks.length > 0) ) } function pushStewardRuntimeUserMessage(userText = '') { const normalizedText = String(userText || '').trim() if (!normalizedText) { return false } messages.value.push(createMessage('user', normalizedText)) composerDraft.value = '' persistSessionState() nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) return true } function pushStewardRuntimeResponse(userText = '', decision = null, options = {}) { if (userText && !options.userMessageAlreadyAdded) { messages.value.push(createMessage('user', userText)) } const text = String(decision?.question || decision?.response_text || decision?.responseText || decision?.rationale || '').trim() if (text) { messages.value.push(createMessage('assistant', text, [], { assistantName: STEWARD_ASSISTANT_NAME, meta: [STEWARD_ASSISTANT_NAME] })) } composerDraft.value = '' persistSessionState() nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) } function buildStewardRuntimeFastPathDecision(rawText = '', runtimeState = {}) { const normalizedText = String(rawText || '').trim() if (!normalizedText) { return null } if (shouldPlanNewStewardTasksLocally(normalizedText, runtimeState)) { return { next_action: 'plan_new_tasks' } } if (isStewardRuntimeCancelText(normalizedText)) { return { next_action: 'cancel_current_action', response_text: '已暂停当前等待动作。我不会继续提交或进入下一步;如果你要重新规划,请直接告诉我新的财务事项。' } } const slotContext = findPendingSlotSuggestedActionContextByInput(normalizedText) const payload = slotContext?.action?.payload && typeof slotContext.action.payload === 'object' ? slotContext.action.payload : {} if (slotContext) { return { next_action: 'fill_current_slot', target_message_id: String(slotContext.message?.id || '').trim(), field_key: String(payload.field_key || payload.fieldKey || '').trim(), field_value: String(payload.value || slotContext.action?.label || normalizedText).trim() } } if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) { if (runtimeState?.pending_application?.ready_to_submit) { return { next_action: 'submit_current_application', target_message_id: runtimeState.pending_application.message_id || '' } } if (runtimeState?.pending_steward_action) { return { next_action: 'continue_next_task', target_message_id: runtimeState.pending_steward_action.message_id || '', target_task_id: runtimeState.pending_steward_action.target_task_id || '' } } } if (String(runtimeState?.waiting_for || '').trim() === 'application_field_completion') { if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) { const missingFields = Array.isArray(runtimeState?.pending_application?.missing_fields) ? runtimeState.pending_application.missing_fields : [] return { next_action: 'ask_user', response_text: missingFields.length ? `当前申请还不能继续提交,请先补充:${missingFields.join('、')}。你可以直接回复对应选项或填写具体内容。` : '当前申请还有信息需要先补充。请先回复系统刚刚追问的内容,我再继续生成核对结果。' } } } return null } function shouldUseStewardRuntimeLlmDecision(rawText = '', runtimeState = {}) { if (shouldPlanNewStewardTasksLocally(rawText, runtimeState)) { return false } const normalizedText = normalizeStewardRuntimeInputText(rawText) if (!normalizedText) { return false } if ( isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText) || isStewardRuntimeCancelText(normalizedText) ) { return false } if ( findPendingSlotSuggestedActionContextByInput(normalizedText) ) { return false } return true } async function executeStewardRuntimeDecision(decision = null, rawText = '', options = {}) { const nextAction = String(decision?.next_action || decision?.nextAction || '').trim() const userMessageAlreadyAdded = Boolean(options.userMessageAlreadyAdded) if (nextAction === 'submit_current_application') { const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim() const targetMessage = targetMessageId ? messages.value.find((message) => String(message.id || '') === targetMessageId) : findPendingApplicationSubmitMessage() if (!targetMessage?.applicationPreview) { return false } const normalizedPreview = normalizeApplicationPreview(targetMessage.applicationPreview) if (!normalizedPreview.readyToSubmit) { pushApplicationSubmitBlockedMessage(rawText, targetMessage, { userMessageAlreadyAdded }) return true } targetMessage.applicationPreview = normalizedPreview applicationSubmitConfirmDialog.value = { open: true, message: targetMessage } await confirmApplicationSubmit({ userText: rawText, skipUserMessage: userMessageAlreadyAdded }) return true } if (nextAction === 'continue_next_task') { const context = findPendingStewardSuggestedActionContext(decision) if (!context) { return false } if (rawText && !userMessageAlreadyAdded) { messages.value.push(createMessage('user', rawText)) } context.action.confirmedByText = true composerDraft.value = '' persistSessionState() nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) await handleSuggestedAction(context.message, context.action) return true } if (nextAction === 'fill_current_slot') { const context = findPendingSlotSuggestedActionContext(decision) if (!context) { return false } await handleSuggestedAction(context.message, { ...context.action, label: String(decision?.field_value || decision?.fieldValue || context.action.label || '').trim(), suppressUserEcho: userMessageAlreadyAdded }) return true } if (nextAction === 'ask_user' || nextAction === 'cancel_current_action' || nextAction === 'no_op') { pushStewardRuntimeResponse(rawText, decision, { userMessageAlreadyAdded }) return true } return false } async function handleStewardRuntimeDecision(options = {}) { if (!isStewardSession.value || options.skipStewardPlan) { return false } const rawText = String(options.rawText ?? composerDraft.value ?? '').trim() const files = Array.from(options.files ?? attachedFiles.value ?? []) if (!rawText || files.length) { return false } const runtimeState = buildStewardRuntimeState() if (!hasActiveStewardRuntimeDecisionContext(runtimeState)) { return false } const userMessageAlreadyAdded = options.skipUserMessage ? false : pushStewardRuntimeUserMessage(rawText) try { const fastDecision = buildStewardRuntimeFastPathDecision(rawText, runtimeState) if (fastDecision) { if (String(fastDecision.next_action || fastDecision.nextAction || '').trim() === 'plan_new_tasks') { await submitStewardPlan({ ...options, rawText, userText: rawText, skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage }) return true } const fastExecuted = await executeStewardRuntimeDecision(fastDecision, rawText, { userMessageAlreadyAdded }) if (fastExecuted) { return true } } if (!shouldUseStewardRuntimeLlmDecision(rawText, runtimeState)) { if (userMessageAlreadyAdded) { pushStewardRuntimeResponse('', { response_text: '我还需要先确认当前等待项。请回复系统刚刚追问的选项或具体补充内容。' }, { userMessageAlreadyAdded: true }) return true } return false } const decision = await fetchStewardRuntimeDecision({ user_message: rawText, session_type: SESSION_TYPE_STEWARD, runtime_state: runtimeState, context_json: { entry_source: props.entrySource, user_id: resolveCurrentUserId() } }, { timeoutMs: 45000, timeoutMessage: '小财管家运行时决策超时,已回到当前上下文兜底处理。' }) if (String(decision?.next_action || decision?.nextAction || '').trim() === 'plan_new_tasks') { await submitStewardPlan({ ...options, rawText, userText: rawText, skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage }) return true } const executed = await executeStewardRuntimeDecision(decision, rawText, { userMessageAlreadyAdded }) if (executed) { return true } if (userMessageAlreadyAdded) { await submitStewardPlan({ ...options, rawText, userText: rawText, skipUserMessage: true }) return true } return false } catch (error) { console.warn('Steward runtime decision failed:', error) if (userMessageAlreadyAdded) { await submitStewardPlan({ ...options, rawText, userText: rawText, skipUserMessage: true }) return true } return false } } function openApplicationSubmitConfirm(message) { if (!message) { return } if (message.applicationPreview) { const normalizedPreview = normalizeApplicationPreview(message.applicationPreview) message.applicationPreview = normalizedPreview message.text = buildLocalApplicationPreviewMessage(normalizedPreview) if (!normalizedPreview.readyToSubmit) { const validationIssues = Array.isArray(normalizedPreview.validationIssues) ? normalizedPreview.validationIssues : [] toast( validationIssues.length ? validationIssues[0].message : `请先补充:${normalizedPreview.missingFields.join('、')}。` ) persistSessionState() return } } applicationSubmitConfirmDialog.value = { open: true, message } } function closeApplicationSubmitConfirm() { if (reviewActionBusy.value) { return } applicationSubmitConfirmDialog.value = { open: false, message: null } } function resolveApplicationEditClaimId() { if (activeSessionType.value !== SESSION_TYPE_APPLICATION) { return '' } const request = linkedRequest.value || {} if (!request.applicationEditMode) { return '' } return String(request.claimId || request.claim_id || '').trim() } async function confirmApplicationSubmit(options = {}) { const message = applicationSubmitConfirmDialog.value.message if (!message || submitting.value || reviewActionBusy.value) { return } const applicationPreview = message?.applicationPreview && typeof message.applicationPreview === 'object' ? normalizeApplicationPreview(message.applicationPreview) : null const applicationSubmitText = applicationPreview ? buildApplicationPreviewSubmitText(applicationPreview) : '确认提交' const applicationEditClaimId = resolveApplicationEditClaimId() applicationSubmitConfirmDialog.value = { open: false, message: null } const stewardSubmitContinuation = message?.stewardContinuation || null reviewActionBusy.value = true try { const payload = await submitComposer({ rawText: applicationSubmitText, userText: String(options.userText || '').trim() || '确认提交', skipUserMessage: Boolean(options.skipUserMessage), pendingText: '正在提交费用申请...', systemGenerated: true, skipScopeGuard: true, skipStewardPlan: true, stewardContinuation: stewardSubmitContinuation, sessionTypeOverride: SESSION_TYPE_APPLICATION, feedbackOperationType: 'submit_application', extraContext: { application_preview: applicationPreview, user_input_text: applicationSubmitText, ...(applicationEditClaimId ? { application_edit_claim_id: applicationEditClaimId, application_edit_claim_no: String(linkedRequest.value?.claimNo || linkedRequest.value?.id || '').trim(), application_edit_mode: true, draft_claim_id: applicationEditClaimId, selected_claim_id: applicationEditClaimId } : {}) } }) const draftPayload = payload?.result?.draft_payload || {} const claimNo = String(draftPayload.claim_no || '').trim() const claimId = String(draftPayload.claim_id || '').trim() if (String(payload?.status || '').trim() === 'succeeded' && (claimNo || claimId)) { message.applicationSubmitConfirmed = true emit('draft-saved', { claimId, claimNo, status: 'submitted', approvalStage: String(draftPayload.approval_stage || '直属领导审批').trim(), documentType: 'application' }) } const planningText = buildTravelPlanningNudgeMessage(applicationPreview, draftPayload) const planningActions = buildTravelPlanningSuggestedActions(applicationPreview, draftPayload).map((action) => ({ ...action, payload: { ...(action.payload || {}), applicationPreview, draftPayload } })) if (planningText && planningActions.length) { messages.value.push(createMessage('assistant', planningText, [], { meta: ['行程规划推荐'], suggestedActions: planningActions })) persistSessionState() nextTick(scrollToBottom) } const stewardFollowup = buildStewardContinuationAfterAction({ createMessage, message, completedLabel: '申请单已完成' }) if (stewardFollowup) { await pushStewardContinuationMessage({ finalMessage: stewardFollowup, messages, nextTick, persistSessionState, scrollToBottom }) } } finally { reviewActionBusy.value = false } } return { closeApplicationSubmitConfirm, confirmApplicationSubmit, handleApplicationSubmitConfirmationText, handleStewardRuntimeDecision, isApplicationSubmitConfirmationText, openApplicationSubmitConfirm, resolveStewardMissingFieldItems } }