import { AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT } from '../../services/aiApplicationPreviewActions.js' import { executeStewardAction } from '../../services/steward.js' import { buildAiDocumentDetailRequest } from '../../utils/aiDocumentDetailReference.js' import { mergeComposerPrefill, resolveSuggestedActionPrefill } from '../../utils/assistantSuggestedActionPrefill.js' import { AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION, AI_ATTACHMENT_OCR_DETAIL_ACTION } from './workbenchAiMessageModel.js' import { SESSION_TYPE_EXPENSE } from './useWorkbenchAiExpenseFlow.js' import { CANCEL_STANDALONE_REIMBURSEMENT_DRAFT_ACTION, CONTINUE_REIMBURSEMENT_DRAFT_ACTION, CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION, SKIP_REQUIRED_APPLICATION_LINK_ACTION, SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION } from '../../views/scripts/travelReimbursementAssociationGateModel.js' export function useWorkbenchAiActionRouter({ aiExpenseDraft, applicationFlow, assistantDraft, attachmentFlow, conversationMessages, createInlineMessage, emit, expenseFlow, focusAiModeInput, hasInlineAttachmentOcrDetails, persistCurrentConversation, replaceInlineMessage, resolveLatestInlineUserPrompt, scrollInlineConversationToBottom, selectedFiles, startInlineConversation, toast, toggleInlineAttachmentOcrDetails }) { function handleInlineSuggestedAction(action = {}, sourceMessage = null) { const prefillText = resolveSuggestedActionPrefill(action) if (prefillText) { assistantDraft.value = mergeComposerPrefill(assistantDraft.value, prefillText) focusAiModeInput() return } const actionType = String(action?.action_type || '').trim() const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} if (actionPayload.steward_execute_action) { return executeInlineStewardAction(action, sourceMessage) } if (actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION) { if (!hasInlineAttachmentOcrDetails(sourceMessage)) { toast('当前消息没有可查看的附件识别明细。') return } toggleInlineAttachmentOcrDetails(sourceMessage, true) return } if (actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION) { if (applicationFlow.isInlineSuggestedActionDisabled(action, sourceMessage)) { return } void attachmentFlow.confirmAiAttachmentAssociation(actionPayload, sourceMessage) return } if ([AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT].includes(actionType)) { if (applicationFlow.isInlineSuggestedActionDisabled(action, sourceMessage)) { toast('请等待费用测算完成后再继续操作。') return } void applicationFlow.executeInlineApplicationPreviewAction(actionType, sourceMessage, { userText: action.label, draftPayload: actionPayload.draftPayload || actionPayload.draft_payload || null }) return } if (actionType === 'ai_application_confirm_intent') { aiExpenseDraft.value = null void applicationFlow.startAiApplicationPreview('travel', '差旅费', String(actionPayload.sourceText || '').trim(), { userMessage: String(actionPayload.sourceText || '').trim() || '确认发起出差申请', pushUserMessage: true, ontologyFields: actionPayload.ontologyFields || {}, autoSubmit: Boolean(actionPayload.autoSubmit), autoSaveDraft: Boolean(actionPayload.autoSaveDraft), requestedSubmit: Boolean(actionPayload.requestedSubmit), submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation) }) return } if (actionType === 'open_application_detail') { const claimNo = String(actionPayload.claim_no || actionPayload.claimNo || '').trim() const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim() emit('open-document', buildAiDocumentDetailRequest({ reference: claimNo || claimId, claimId, claimNo })) return } if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_reimbursement') { const expenseType = String(actionPayload.expense_type || 'travel').trim() || 'travel' const expenseTypeLabel = String(actionPayload.expense_type_label || '差旅费').trim() || '差旅费' expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, true) return } if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_application') { aiExpenseDraft.value = null void expenseFlow.startAiApplicationPreviewFromAction(actionPayload) return } // steward plan 的"确认创建申请单"按钮:payload 有 steward_current_task + session_type=application, // 直接拉起申请预览(带 remaining tasks),不走 startInlineConversation(会丢失 steward 上下文) if ( actionPayload.steward_current_task && String(actionPayload.session_type || '').trim() === 'application' && String(actionPayload.steward_current_task.task_type || '').trim() === 'expense_application' ) { aiExpenseDraft.value = null void expenseFlow.startAiApplicationPreviewFromAction(actionPayload) return } if (actionType === 'select_expense_type') { const expenseType = String(action?.payload?.expense_type || '').trim() const expenseTypeLabel = String(action?.payload?.expense_type_label || action?.label || '').trim() const requiresApplicationBeforeReimbursement = Boolean(action?.payload?.requires_application_before_reimbursement) expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement) return } if (actionType === 'select_required_application') { expenseFlow.linkAiExpenseApplication(action?.payload || {}) return } if (actionType === CONTINUE_REIMBURSEMENT_DRAFT_ACTION) { expenseFlow.promptAiReimbursementDraftContinuation(actionPayload) focusAiModeInput() return } if (actionType === CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION) { expenseFlow.promptStandaloneReimbursementDraftCreation( actionPayload.original_message || '我要报销', action.label || '独立新建报销单' ) return } if (actionType === CANCEL_STANDALONE_REIMBURSEMENT_DRAFT_ACTION) { expenseFlow.cancelStandaloneReimbursementDraftCreation() return } if (actionType === SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION) { void expenseFlow.startAiReimbursementAssociationGate( actionPayload.original_message || '我要报销', action.label || '不用草稿,关联申请单新建报销单', { skipDraftCheck: true } ) return } if (actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION) { expenseFlow.pushInlineExpenseSceneSelectionPrompt( actionPayload.original_message || '我要报销', action.label || '单独新建报销单' ) return } if (actionType === 'ai_application_start_inline') { aiExpenseDraft.value = null void expenseFlow.startAiApplicationPreviewFromAction(action?.payload || {}, action?.label) return } const carryText = String(action?.payload?.carry_text || action?.label || '').trim() if (!carryText) { return } if (String(action?.payload?.session_type || '').trim() === SESSION_TYPE_EXPENSE && carryText === '我要报销') { void expenseFlow.startAiReimbursementAssociationGate(carryText, action.label) return } startInlineConversation(carryText, { label: action.label, source: 'steward-action', sessionType: action?.payload?.session_type || 'steward' }, action?.payload?.carry_files ? Array.from(selectedFiles.value) : []) } async function executeInlineStewardAction(action = {}, sourceMessage = null) { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const actionType = String(actionPayload.steward_action_type || '').trim() if (!actionType || !actionPayload.steward_current_task) { toast('当前动作缺少可执行任务快照,请重新发起规划。') return false } if (sourceMessage?.suggestedActionsLocked) { return true } if (sourceMessage) { sourceMessage.suggestedActionsLocked = true } const pendingMessage = appendStewardActionMessage( `正在执行:${String(action.label || '小财管家动作').trim() || '小财管家动作'}...`, { pending: true } ) try { let precheckResult = null if (actionType === 'submit_application') { precheckResult = await executeStewardAction( buildStewardActionExecutePayload(action, 'run_duplicate_precheck') ) if (!isSuccessfulStewardPrecheck(precheckResult)) { finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(precheckResult), { suggestedActions: [] }) return true } } const contextJson = precheckResult ? { precheck_result: precheckResult.result_payload || precheckResult.resultPayload || {} } : {} const result = await executeStewardAction( buildStewardActionExecutePayload(action, actionType, contextJson) ) const resultActions = buildStewardActionResultActions(result) const nextTaskAction = buildNextTaskSuggestedAction(actionPayload) finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(result), { suggestedActions: nextTaskAction ? [...resultActions, nextTaskAction] : resultActions }) return true } catch (error) { if (sourceMessage) { sourceMessage.suggestedActionsLocked = false } finalizeStewardActionMessage( pendingMessage, `动作执行失败:${String(error?.message || '请稍后重试。').trim()}` ) return false } } function buildStewardActionExecutePayload(action = {}, actionType = '', contextJson = {}) { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const currentTask = actionPayload.steward_current_task || null return { action_type: actionType, message: resolveStewardActionMessage(action), plan_id: String(actionPayload.steward_plan_id || '').trim(), conversation_id: String(actionPayload.conversation_id || actionPayload.conversationId || '').trim() || null, task: currentTask, action_step: resolveStewardActionStep(actionPayload, actionType), confirmed: true, context_json: contextJson, client_trace_id: buildStewardActionClientTraceId(actionPayload, actionType) } } function resolveStewardActionMessage(action = {}) { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} return String( actionPayload.carry_text || actionPayload.original_message || resolveLatestInlineUserPrompt?.() || action.label || '' ).trim() } function resolveStewardActionStep(actionPayload = {}, actionType = '') { if (String(actionPayload.steward_action_step?.action_type || '').trim() === actionType) { return actionPayload.steward_action_step } const steps = Array.isArray(actionPayload.steward_current_task?.action_steps) ? actionPayload.steward_current_task.action_steps : [] return steps.find((step) => String(step?.action_type || '').trim() === actionType) || null } function buildStewardActionClientTraceId(actionPayload = {}, actionType = '') { const planId = String(actionPayload.steward_plan_id || 'steward').trim() || 'steward' const taskId = String(actionPayload.steward_action_task_id || actionPayload.steward_current_task?.task_id || 'task').trim() || 'task' return `${planId}:${taskId}:${actionType}:${Date.now()}` } function isSuccessfulStewardPrecheck(result = {}) { const status = String(result?.status || '').trim() const payload = result?.result_payload || result?.resultPayload || {} return status === 'succeeded' && payload?.blocking !== true } function appendStewardActionMessage(content, options = {}) { if (!conversationMessages?.value || typeof createInlineMessage !== 'function') { toast(String(content || '').trim()) return null } const message = createInlineMessage('assistant', content, options) conversationMessages.value.push(message) persistStewardActionConversation() return message } function finalizeStewardActionMessage(pendingMessage, content, options = {}) { if (!conversationMessages?.value || typeof createInlineMessage !== 'function') { toast(String(content || '').trim()) return } const finalMessage = createInlineMessage('assistant', content, { ...options, id: pendingMessage?.id }) if (pendingMessage?.id && typeof replaceInlineMessage === 'function') { replaceInlineMessage(pendingMessage.id, finalMessage) } else if (pendingMessage?.id) { const index = conversationMessages.value.findIndex((item) => item.id === pendingMessage.id) if (index >= 0) { conversationMessages.value.splice(index, 1, finalMessage) } else { conversationMessages.value.push(finalMessage) } } else { conversationMessages.value.push(finalMessage) } persistStewardActionConversation() } function persistStewardActionConversation() { persistCurrentConversation?.() scrollInlineConversationToBottom?.({ force: true }) } function buildStewardActionResultText(result = {}) { const status = String(result?.status || '').trim() const message = String(result?.message || '').trim() if (status === 'succeeded') { return message || '动作已完成。' } if (status === 'needs_confirmation') { return message || '这一步还需要您确认后才能继续。' } if (status === 'blocked') { return ['这一步暂时不能继续。', message].filter(Boolean).join('\n\n') } if (status === 'failed') { return `动作执行失败:${message || '请稍后重试。'}` } return message || '动作已返回结果。' } function buildStewardActionResultActions(result = {}) { if (String(result?.status || '').trim() !== 'succeeded') { return [] } const resultPayload = result?.result_payload || result?.resultPayload || {} const draftPayload = resultPayload?.draft_payload || resultPayload?.draftPayload || resultPayload const claimNo = String(draftPayload?.claim_no || draftPayload?.claimNo || '').trim() const claimId = String(draftPayload?.claim_id || draftPayload?.claimId || '').trim() if (!claimNo && !claimId) { return [] } return [{ label: claimNo ? `查看单据 ${claimNo}` : '查看单据', description: '打开刚生成的单据详情。', icon: 'mdi mdi-open-in-new', action_type: 'open_application_detail', payload: { claim_id: claimId, claim_no: claimNo } }] } function buildNextTaskSuggestedAction(actionPayload = {}) { // 多 task 串行推进:task1 完成后,从剩余 task 列表取下一个,生成推进按钮。 // 用户点击推进按钮后,handleInlineSuggestedAction 的 steward_confirm_flow 分支 // 会自动拉起下一个 task 的申请预览/报销流程,实现"先做完 A 再做 B"。 const remainingTasks = Array.isArray(actionPayload.steward_remaining_tasks) ? actionPayload.steward_remaining_tasks : [] const nextTask = remainingTasks[0] if (!nextTask || !nextTask.task_type) { return null } const taskType = String(nextTask.task_type || '').trim() const isApplication = taskType === 'expense_application' const flowId = isApplication ? 'travel_application' : 'travel_reimbursement' const taskLabel = isApplication ? '出差申请' : '费用报销' const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {} return { label: `继续处理${taskLabel}`, description: `接下来处理${taskLabel}: ${String(nextTask.summary || nextTask.title || '').slice(0, 40)}`, icon: isApplication ? 'mdi mdi-file-plus-outline' : 'mdi mdi-receipt-text-plus-outline', action_type: 'steward_continue_next_task', payload: { steward_confirm_flow: true, flow_id: flowId, steward_current_task: nextTask, expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel', expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费', ontology_fields: ontologyFields, original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim() } } } return { handleInlineSuggestedAction } }