import { APPLICATION_TRANSPORT_MODE_OPTIONS, normalizeApplicationPreview, normalizeTransportModeOption } from '../../utils/expenseApplicationPreview.js' import { mergeComposerPrefill, resolveSuggestedActionPrefill } from '../../utils/assistantSuggestedActionPrefill.js' import { ASSISTANT_SCOPE_ACTION_SWITCH } from '../../utils/assistantSessionScope.js' import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js' import { TRAVEL_PLANNING_ACTION_GENERATE, TRAVEL_PLANNING_ACTION_SKIP, buildTravelPlanningRecommendation } from '../../utils/travelApplicationPlanning.js' import { SESSION_TYPE_APPLICATION, SESSION_TYPE_BUDGET, SESSION_TYPE_EXPENSE, canUseBudgetAssistantSession } from './travelReimbursementConversationModel.js' import { SKIP_REQUIRED_APPLICATION_LINK_ACTION, SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION, buildReimbursementAssociationSubmitOptions, pushReimbursementAssociationPromptMessage } from './travelReimbursementAssociationGateModel.js' import { STEWARD_ASSISTANT_NAME } from './useTravelReimbursementStewardRuntime.js' import { buildStewardFieldCompletionContinuation, buildStewardFieldCompletionRawText } from './stewardFieldCompletionModel.js' import { MAX_ATTACHMENTS, VISIBLE_ATTACHMENT_CHIPS } from './travelReimbursementAttachmentModel.js' export const APPLICATION_PREVIEW_FIELD_ACTION_SET = 'set_application_preview_field' export function useTravelReimbursementSuggestedActions({ applicationPreviewEditor, attachedFiles, buildExpenseSceneSelectionActions, buildExpenseSceneSelectionMessage, commitApplicationPreviewEditor, composerDraft, composerFilesExpanded, composerTextareaRef, createMessage, currentUser, emit, fetchExpenseClaims = async () => ({ items: [] }), handleGuidedShortcut, handleGuidedSuggestedAction, handleSceneSelectionApplicationGate, lockSuggestedActionMessage, mergeFilesWithLimit, messages, nextTick, openApplicationPreviewEditor, persistSessionState, resolveApplicationPreviewMissingFields, reviewActionBusy, router, scrollToBottom, sessionSwitchBusy, startExpenseSceneSelectionAfterIntentConfirmation, submitComposer, submitComposerInternal, submitting, switchSessionType, toast, adjustComposerTextareaHeight }) { async function runShortcut(shortcut) { if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) { if (shortcut.targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) { toast('目前暂无权限访问预算编制助手') return } if (shortcut.active) { return } await switchSessionType(shortcut.targetSessionType) return } if (await handleGuidedShortcut(shortcut)) { return } const prompt = String(shortcut?.prompt || '').trim() if (!prompt) return composerDraft.value = prompt submitComposer() } function isSuggestedActionSelected(message, action) { const selectedKey = String(message?.selectedSuggestedActionKey || '').trim() return Boolean(selectedKey) && selectedKey === buildSuggestedActionKey(action) } function buildApplicationPreviewFieldAppliedText(message, fieldLabel = '', value = '') { const missingFields = resolveApplicationPreviewMissingFields(message) const resolvedFieldLabel = String(fieldLabel || '补充项').trim() const resolvedValue = String(value || '').trim() if (missingFields.length) { return [ `已更新:**${resolvedFieldLabel}:${resolvedValue}**。`, '', `我重新检查了一遍,当前还需要补充:**${missingFields.join('、')}**。`, '', '请继续补齐下方核对表里的待补充项;补齐后我再继续推进申请提交。' ].join('\n') } return [ `已更新:**${resolvedFieldLabel}:${resolvedValue}**。`, '', '我已经重新同步下方申请核对表和费用测算。', '', '请继续核查表格内容;如果信息无误,点击确认进入审批环节。' ].join('\n') } function isStewardApplicationPreviewFieldCompletion(targetMessage, payload = {}) { return Boolean( payload.steward_delegated_field_completion || String(targetMessage?.assistantName || '').trim() === STEWARD_ASSISTANT_NAME || targetMessage?.stewardPlan ) } async function continueStewardApplicationFieldCompletion({ targetMessage, action, sourcePreview, fieldKey, fieldLabel, value }) { if (!lockSuggestedActionMessage(targetMessage, action)) { return true } const continuation = buildStewardFieldCompletionContinuation( targetMessage?.stewardContinuation || null, fieldKey, value ) const userText = `选择${fieldLabel || '补充项'}:${value}` const carryText = buildStewardFieldCompletionRawText({ preview: sourcePreview, fieldKey, fieldLabel, value, continuation }) if (!action?.suppressUserEcho) { messages.value.push(createMessage('user', userText)) } persistSessionState() nextTick(scrollToBottom) await submitComposerInternal({ rawText: carryText, userText, pendingText: '小财管家正在根据补齐信息查询票据并测算费用...', files: [], skipScopeGuard: true, skipApplicationModelReview: true, skipStewardPlan: true, skipUserMessage: true, sessionTypeOverride: SESSION_TYPE_APPLICATION, stewardContinuation: continuation }) return true } async function applyApplicationPreviewFieldAction(message, action) { const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const fieldKey = String(payload.field_key || payload.fieldKey || '').trim() const fieldLabel = String(payload.field_label || payload.fieldLabel || action?.label || '').trim() let value = String(payload.value || action?.label || '').trim() const targetMessage = messages.value.find((item) => String(item.id || '') === String(message?.id || '')) || message const sourcePreview = targetMessage?.applicationPreview || payload.applicationPreview || payload.application_preview || payload.preview || null if (!sourcePreview || !fieldKey || !value) { return false } if (fieldKey === 'transportMode') { value = normalizeTransportModeOption(value, '') } if (fieldKey === 'transportMode' && !APPLICATION_TRANSPORT_MODE_OPTIONS.includes(value)) { toast('请选择有效的出行方式。') return true } if (isStewardApplicationPreviewFieldCompletion(targetMessage, payload)) { return continueStewardApplicationFieldCompletion({ targetMessage, action, sourcePreview, fieldKey, fieldLabel, value }) } if (!lockSuggestedActionMessage(targetMessage, action)) { return true } targetMessage.applicationPreview = normalizeApplicationPreview(sourcePreview) messages.value.push(createMessage('user', `选择${fieldLabel || '补充项'}:${value}`)) openApplicationPreviewEditor(targetMessage, fieldKey, targetMessage.applicationPreview?.fields?.[fieldKey] || '') applicationPreviewEditor.value = { ...applicationPreviewEditor.value, draftValue: value } await commitApplicationPreviewEditor(targetMessage) if (String(targetMessage.assistantName || '').trim() === STEWARD_ASSISTANT_NAME || targetMessage.stewardPlan) { targetMessage.assistantName = STEWARD_ASSISTANT_NAME targetMessage.text = buildApplicationPreviewFieldAppliedText(targetMessage, fieldLabel, value) const nextMeta = Array.isArray(targetMessage.meta) ? targetMessage.meta : [] targetMessage.meta = Array.from(new Set([ STEWARD_ASSISTANT_NAME, resolveApplicationPreviewMissingFields(targetMessage).length ? '等待补充' : '等待用户确认', ...nextMeta.filter((item) => String(item || '').trim() && item !== STEWARD_ASSISTANT_NAME) ])).slice(0, 4) } persistSessionState() nextTick(scrollToBottom) return true } function pushExpenseSceneSelectionPrompt(originalMessage, userEcho = '我要报销') { const sourceText = String(originalMessage || '').trim() if (!sourceText) { return } startExpenseSceneSelectionAfterIntentConfirmation(sourceText) messages.value.push(createMessage('user', String(userEcho || '我要报销').trim() || '我要报销')) messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), [], { meta: ['等待选择场景'], suggestedActions: buildExpenseSceneSelectionActions(sourceText) })) nextTick(scrollToBottom) persistSessionState() } async function pushExpenseAssociationGatePrompt(originalMessage, options = {}) { const sourceText = String(originalMessage || '我要报销').trim() || '我要报销' startExpenseSceneSelectionAfterIntentConfirmation(sourceText) messages.value.push(createMessage('user', String(options.userText || '我要报销').trim() || '我要报销')) await pushReimbursementAssociationPromptMessage({ rawText: sourceText, createMessage, messages, nextTick, scrollToBottom, persistSessionState, fetchExpenseClaims, currentUser, skipDraftCheck: Boolean(options.skipDraftCheck) }) } function applySuggestedActionPrefill(action) { const prefillText = resolveSuggestedActionPrefill(action) if (!prefillText) { return false } composerDraft.value = mergeComposerPrefill(composerDraft.value, prefillText) nextTick(() => { adjustComposerTextareaHeight() composerTextareaRef.value?.focus() }) persistSessionState() return true } async function handleSuggestedAction(message, action) { const actionType = String(action?.action_type || '').trim() if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return if (message?.suggestedActionsLocked) return if (applySuggestedActionPrefill(action)) return if (await handleGuidedSuggestedAction(message, action)) return if (await handleSceneSelectionApplicationGate(message, action)) return if (actionType === SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION) { const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销' if (!lockSuggestedActionMessage(message, action)) return await pushExpenseAssociationGatePrompt(originalMessage, { skipDraftCheck: true, userText: action?.label || '不用草稿,关联申请单新建报销单' }) return } if (actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION) { const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销' if (!lockSuggestedActionMessage(message, action)) return pushExpenseSceneSelectionPrompt(originalMessage, action?.label || '不关联,单独新建报销单') return } if (actionType === 'select_required_application') { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const applicationNo = String(actionPayload.application_claim_no || actionPayload.claim_no || '').trim() const originalMessage = String(actionPayload.original_message || message?.text || '我要报销').trim() || '我要报销' if (!lockSuggestedActionMessage(message, action)) return messages.value.push(createMessage('user', `关联申请单 ${applicationNo || ''}`.trim() || '关联申请单')) nextTick(scrollToBottom) persistSessionState() await submitComposer(buildReimbursementAssociationSubmitOptions(actionPayload, originalMessage)) return } if (actionType === APPLICATION_PREVIEW_FIELD_ACTION_SET) { await applyApplicationPreviewFieldAction(message, action) return } if (actionType === 'open_application_detail') { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim() if (!claimId) { toast('当前没有可查看的申请单据。') return } if (!lockSuggestedActionMessage(message, action)) return await router.push({ name: 'app-document-detail', params: { requestId: claimId } }) emit('close') return } if (actionType === 'open_receipt_folder') { if (!lockSuggestedActionMessage(message, action)) return await router.push({ name: 'app-receiptFolder' }) emit('close') return } if (actionType === 'continue_upload_with_unlinked_receipts') { if (!lockSuggestedActionMessage(message, action)) return const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} await submitComposer({ rawText: String(actionPayload.raw_text || composerDraft.value || '').trim(), files: Array.from(attachedFiles.value || []), skipReceiptFolderUnlinkedPrompt: true }) return } if (actionType === TRAVEL_PLANNING_ACTION_GENERATE) { if (!lockSuggestedActionMessage(message, action)) return const sourcePreview = action?.payload?.applicationPreview || action?.payload?.preview || null const sourceDraftPayload = action?.payload?.draftPayload || action?.payload?.draft_payload || null const recommendation = buildTravelPlanningRecommendation(sourcePreview, sourceDraftPayload) if (recommendation) { messages.value.push(createMessage('user', '生成行程规划')) messages.value.push(createMessage('assistant', recommendation, [], { meta: ['行程规划建议'] })) nextTick(scrollToBottom) persistSessionState() } return } if (actionType === TRAVEL_PLANNING_ACTION_SKIP) { if (!lockSuggestedActionMessage(message, action)) return messages.value.push(createMessage('assistant', '好的,本次先保留申请结果。后续需要规划交通或酒店时,可以继续在这里告诉我。', [], { meta: ['暂不规划'] })) nextTick(scrollToBottom) persistSessionState() return } if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) { const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const targetSessionType = String(actionPayload.session_type || '').trim() if (!targetSessionType) return if (targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) { toast('目前暂无权限访问预算编制助手') return } const carryText = String(actionPayload.carry_text || '').trim() const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : [] if (!lockSuggestedActionMessage(message, action)) return if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') { await pushExpenseAssociationGatePrompt(carryText) return } if (String(actionPayload.steward_plan_id || '').trim()) { const confirmedByText = Boolean(action.confirmedByText) delete action.confirmedByText await submitComposerInternal({ rawText: carryText, userText: action.label || '确定', pendingText: targetSessionType === SESSION_TYPE_APPLICATION ? '小财管家正在调用申请助手生成申请单核对结果...' : '小财管家正在调用报销助手整理报销核对结果...', files: carryFiles, skipScopeGuard: true, skipApplicationModelReview: targetSessionType === SESSION_TYPE_APPLICATION, skipStewardSlotDecision: targetSessionType === SESSION_TYPE_APPLICATION, skipStewardPlan: true, skipUserMessage: confirmedByText, sessionTypeOverride: targetSessionType, stewardContinuation: { planId: String(actionPayload.steward_plan_id || '').trim(), currentTaskId: String(actionPayload.steward_next_task_id || '').trim(), currentTask: actionPayload.steward_current_task || null, remainingTasks: Array.isArray(actionPayload.steward_remaining_tasks) ? actionPayload.steward_remaining_tasks : [] } }) return } await switchSessionType(targetSessionType) if (carryText) { composerDraft.value = carryText } if (carryFiles.length) { const fileMergeResult = mergeFilesWithLimit([], carryFiles, MAX_ATTACHMENTS) attachedFiles.value = fileMergeResult.files composerFilesExpanded.value = fileMergeResult.files.length > VISIBLE_ATTACHMENT_CHIPS } nextTick(() => { adjustComposerTextareaHeight() scrollToBottom() }) persistSessionState() if (actionPayload.auto_submit && carryText) { await submitComposer({ rawText: carryText, userText: action.label || '确认继续处理', pendingText: '正在按确认内容继续处理...', files: carryFiles, skipScopeGuard: true }) } return } if (actionType === 'confirm_expense_intent') { const originalMessage = String(action?.payload?.original_message || message?.text || '').trim() if (!originalMessage) return if (!lockSuggestedActionMessage(message, action)) return await pushExpenseAssociationGatePrompt(originalMessage) return } if (actionType !== 'select_expense_type') { const fallbackText = String(action?.description || action?.label || '').trim() if (!fallbackText) return if (!lockSuggestedActionMessage(message, action)) return await submitComposer({ rawText: fallbackText, userText: fallbackText, pendingText: '正在继续处理...' }) return } const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {} const expenseType = String(actionPayload.expense_type || '').trim() const expenseTypeLabel = String(actionPayload.expense_type_label || action?.label || '').trim() const originalMessage = String(actionPayload.original_message || message?.text || '').trim() if (!expenseTypeLabel || !originalMessage) return if (!lockSuggestedActionMessage(message, action)) return await submitComposer({ rawText: `${originalMessage}\n用户选择报销场景:${expenseTypeLabel}`, userText: `选择${expenseTypeLabel}`, pendingText: `已选择${expenseTypeLabel},正在按该场景识别...`, systemGenerated: true, extraContext: { draft_claim_id: '', user_input_text: originalMessage, expense_scene_selection: { expense_type: expenseType, expense_type_label: expenseTypeLabel, original_message: originalMessage }, review_form_values: { expense_type: expenseTypeLabel } } }) } return { continueStewardApplicationFieldCompletion, handleSuggestedAction, isSuggestedActionSelected, runShortcut } }