import { ref } from 'vue' import { GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW, GUIDED_ACTION_CONTINUE_FILLING, GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR, GUIDED_ACTION_PROCESS_INTERRUPTION, GUIDED_ACTION_SELECT_EXPENSE_TYPE, GUIDED_ACTION_SELECT_QUERY_MODE, GUIDED_ACTION_SELECT_QUERY_STATUS, GUIDED_ACTION_START_REIMBURSEMENT, GUIDED_ACTION_START_STATUS_QUERY, GUIDED_FLOW_MODE_REIMBURSEMENT, GUIDED_FLOW_MODE_STATUS_QUERY, applyGuidedReimbursementAnswer, buildGuidedExpenseTypeActions, buildGuidedInterruptionActions, buildGuidedInterruptionText, buildGuidedQueryModeActions, buildGuidedQueryPromptText, buildGuidedQueryStatusActions, buildGuidedReimbursementStartText, buildGuidedReimbursementSummaryText, buildGuidedReviewConfirmationActions, buildGuidedReviewSubmitOptions, buildGuidedStatusQueryStartText, buildGuidedStatusQueryText, buildGuidedStepPromptText, createEmptyGuidedFlowState, createGuidedReimbursementState, createGuidedStatusQueryState, getCurrentGuidedStep, isGuidedFlowActive, isGuidedReimbursementReadyForReview, normalizeGuidedFlowState, resolveGuidedExpenseTypeFromText, resolveGuidedQueryModeFromText, selectGuidedExpenseType, selectGuidedQueryMode, shouldConfirmGuidedInterruption } from './travelReimbursementGuidedFlowModel.js' function normalizeText(value) { return String(value || '').trim() } function buildFileNames(files) { return Array.from(files || []) .map((file) => normalizeText(file?.name)) .filter(Boolean) } function mergePendingFiles(currentFiles, nextFiles) { const merged = [...Array.from(currentFiles || [])] Array.from(nextFiles || []).forEach((file) => { const name = normalizeText(file?.name) if (!name) return const duplicated = merged.some((item) => normalizeText(item?.name) === name && Number(item?.size || 0) === Number(file?.size || 0)) if (!duplicated) { merged.push(file) } }) return merged } export function useTravelReimbursementGuidedFlow({ guidedFlowState, messages, composerDraft, attachedFiles, composerBusinessTimeTags, composerBusinessTimeDraftTouched, fileInputRef, submitting, reviewActionBusy, sessionSwitchBusy, createMessage, nextTick, scrollToBottom, persistSessionState, clearAttachedFiles, adjustComposerTextareaHeight, buildComposerBusinessTimeContext, openTravelCalculator, lockSuggestedActionMessage, submitExistingComposer, toast }) { const guidedPendingFiles = ref([]) function persistAndScroll() { persistSessionState() nextTick(() => { adjustComposerTextareaHeight?.() scrollToBottom?.() }) } function clearComposerRuntime() { composerDraft.value = '' clearAttachedFiles?.() if (fileInputRef?.value) { fileInputRef.value.value = '' } if (composerBusinessTimeTags) { composerBusinessTimeTags.value = [] } if (composerBusinessTimeDraftTouched) { composerBusinessTimeDraftTouched.value = false } } function pushAssistant(text, extras = {}) { messages.value.push(createMessage('assistant', text, [], extras)) } function pushUser(text, attachmentNames = []) { const normalizedText = normalizeText(text) messages.value.push(createMessage('user', normalizedText || `上传 ${attachmentNames.length} 份附件`, attachmentNames)) } function resetGuidedFlowState() { guidedFlowState.value = createEmptyGuidedFlowState() guidedPendingFiles.value = [] } function startGuidedReimbursement() { guidedFlowState.value = createGuidedReimbursementState() guidedPendingFiles.value = [] pushAssistant(buildGuidedReimbursementStartText(), { meta: ['引导式报销'], suggestedActions: buildGuidedExpenseTypeActions() }) persistAndScroll() } function startGuidedStatusQuery() { guidedFlowState.value = createGuidedStatusQueryState() guidedPendingFiles.value = [] pushAssistant(buildGuidedStatusQueryStartText(), { meta: ['引导式查询'], suggestedActions: buildGuidedQueryModeActions() }) persistAndScroll() } function handleGuidedShortcut(shortcut) { const actionType = normalizeText(shortcut?.action) if (actionType === GUIDED_ACTION_START_REIMBURSEMENT) { startGuidedReimbursement() return true } if (actionType === GUIDED_ACTION_START_STATUS_QUERY) { startGuidedStatusQuery() return true } if (actionType === GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR) { openTravelCalculator?.() pushAssistant('差旅计算器已打开。你可以直接填写目的地、天数和金额,我会按规则中心标准帮你测算。', { meta: ['差旅计算器'] }) persistAndScroll() return true } return false } function buildAnswerText(rawText, state) { const text = normalizeText(rawText) if (text) { return text } const currentStep = getCurrentGuidedStep(state) if (currentStep?.key === 'time_range') { const businessTimeContext = buildComposerBusinessTimeContext?.() return normalizeText(businessTimeContext?.time_range || businessTimeContext?.business_time) } return '' } function pushNextReimbursementPrompt() { pushAssistant(buildGuidedStepPromptText(guidedFlowState.value), { meta: ['引导式报销'] }) } function pushReimbursementSummary() { pushAssistant(buildGuidedReimbursementSummaryText(guidedFlowState.value), { meta: ['待生成核对信息'], suggestedActions: buildGuidedReviewConfirmationActions() }) } function handleReimbursementAnswer(answerText, files) { const currentState = normalizeGuidedFlowState(guidedFlowState.value) const currentStep = getCurrentGuidedStep(currentState) const fileNames = buildFileNames(files) if (currentState.stepKey === 'expense_type') { const expenseType = resolveGuidedExpenseTypeFromText(answerText) if (!expenseType) { pushAssistant('我还需要先确认报销类型。请点击下面最贴近的费用场景后,我再继续问下一项。', { meta: ['等待选择报销类型'], suggestedActions: buildGuidedExpenseTypeActions() }) return } guidedFlowState.value = selectGuidedExpenseType(currentState, expenseType) pushNextReimbursementPrompt() return } if (!currentStep) { pushAssistant(buildGuidedReimbursementStartText(), { meta: ['引导式报销'], suggestedActions: buildGuidedExpenseTypeActions() }) return } if (!answerText && fileNames.length && currentStep.key !== 'attachments') { guidedPendingFiles.value = mergePendingFiles(guidedPendingFiles.value, files) pushAssistant([ `我已先记录 ${fileNames.length} 份附件。`, '', `当前还需要补充:${currentStep.summaryLabel}。`, currentStep.prompt ].join('\n'), { meta: ['已记录附件'] }) return } if (fileNames.length) { guidedPendingFiles.value = mergePendingFiles(guidedPendingFiles.value, files) } guidedFlowState.value = applyGuidedReimbursementAnswer(currentState, answerText, fileNames) if (isGuidedReimbursementReadyForReview(guidedFlowState.value)) { pushReimbursementSummary() return } pushNextReimbursementPrompt() } async function runStatusQuery(queryText, skipUserMessage = true) { const normalizedQuery = normalizeText(queryText) resetGuidedFlowState() clearComposerRuntime() persistAndScroll() if (!normalizedQuery) { return true } await submitExistingComposer({ rawText: normalizedQuery, userText: normalizedQuery, pendingText: '正在查询单据状态...', skipUserMessage }) return true } async function handleStatusQueryAnswer(answerText) { const currentState = normalizeGuidedFlowState(guidedFlowState.value) if (currentState.stepKey === 'query_mode') { const queryMode = resolveGuidedQueryModeFromText(answerText) if (!queryMode) { pushAssistant(buildGuidedStatusQueryStartText(), { meta: ['引导式查询'], suggestedActions: buildGuidedQueryModeActions() }) return true } guidedFlowState.value = selectGuidedQueryMode(currentState, queryMode) const actions = guidedFlowState.value.stepKey === 'status_value' ? buildGuidedQueryStatusActions() : [] pushAssistant(buildGuidedQueryPromptText(guidedFlowState.value), { meta: ['引导式查询'], suggestedActions: actions }) return true } const queryText = buildGuidedStatusQueryText(currentState, answerText) return runStatusQuery(queryText, true) } async function handleGuidedComposerSubmit(options = {}) { const currentState = normalizeGuidedFlowState(guidedFlowState.value) if (!isGuidedFlowActive(currentState)) { return false } if (options.systemGenerated || normalizeText(options.extraContext?.review_action)) { return false } if (submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) { return true } const files = Array.from(options.files ?? attachedFiles.value ?? []) const fileNames = buildFileNames(files) const answerText = buildAnswerText(options.rawText ?? composerDraft.value, currentState) if (!answerText && !fileNames.length) { return true } pushUser(answerText, fileNames) if (shouldConfirmGuidedInterruption(answerText, currentState) && !fileNames.length) { guidedFlowState.value = { ...currentState, pendingInterruptionText: answerText } pushAssistant(buildGuidedInterruptionText(answerText), { meta: ['等待确认是否打断'], suggestedActions: buildGuidedInterruptionActions() }) clearComposerRuntime() persistAndScroll() return true } if (currentState.mode === GUIDED_FLOW_MODE_REIMBURSEMENT) { handleReimbursementAnswer(answerText, files) clearComposerRuntime() persistAndScroll() return true } if (currentState.mode === GUIDED_FLOW_MODE_STATUS_QUERY) { clearComposerRuntime() persistAndScroll() await handleStatusQueryAnswer(answerText) return true } return false } async function handleGuidedSuggestedAction(message, action) { const actionType = normalizeText(action?.action_type) if (!actionType) { return false } const guidedActionTypes = new Set([ GUIDED_ACTION_SELECT_EXPENSE_TYPE, GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW, GUIDED_ACTION_CONTINUE_FILLING, GUIDED_ACTION_PROCESS_INTERRUPTION, GUIDED_ACTION_SELECT_QUERY_MODE, GUIDED_ACTION_SELECT_QUERY_STATUS ]) if (!guidedActionTypes.has(actionType)) { return false } if (submitting.value || reviewActionBusy.value || sessionSwitchBusy.value || message?.suggestedActionsLocked) { return true } if (!lockSuggestedActionMessage(message, action)) { return true } if (actionType === GUIDED_ACTION_SELECT_EXPENSE_TYPE) { const expenseType = normalizeText(action?.payload?.expense_type) const expenseTypeLabel = normalizeText(action?.payload?.expense_type_label || action?.label) guidedFlowState.value = selectGuidedExpenseType(guidedFlowState.value, expenseType) pushUser(`选择${expenseTypeLabel || '报销类型'}`) pushNextReimbursementPrompt() persistAndScroll() return true } if (actionType === GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW) { const submitOptions = buildGuidedReviewSubmitOptions(guidedFlowState.value, guidedPendingFiles.value) resetGuidedFlowState() persistAndScroll() await submitExistingComposer(submitOptions) return true } if (actionType === GUIDED_ACTION_CONTINUE_FILLING) { const pendingState = { ...normalizeGuidedFlowState(guidedFlowState.value), pendingInterruptionText: '' } guidedFlowState.value = pendingState if (pendingState.mode === GUIDED_FLOW_MODE_STATUS_QUERY) { pushAssistant(buildGuidedQueryPromptText(pendingState), { meta: ['引导式查询'], suggestedActions: pendingState.stepKey === 'status_value' ? buildGuidedQueryStatusActions() : [] }) } else { pushNextReimbursementPrompt() } persistAndScroll() return true } if (actionType === GUIDED_ACTION_PROCESS_INTERRUPTION) { const pendingText = normalizeText(guidedFlowState.value?.pendingInterruptionText) resetGuidedFlowState() persistAndScroll() await submitExistingComposer({ rawText: pendingText, userText: pendingText, pendingText: '正在处理你的问题...', skipUserMessage: true }) return true } if (actionType === GUIDED_ACTION_SELECT_QUERY_MODE) { const queryMode = normalizeText(action?.payload?.query_mode) const queryModeLabel = normalizeText(action?.payload?.query_mode_label || action?.label) guidedFlowState.value = selectGuidedQueryMode(guidedFlowState.value, queryMode) pushUser(`选择${queryModeLabel || '查询方式'}`) pushAssistant(buildGuidedQueryPromptText(guidedFlowState.value), { meta: ['引导式查询'], suggestedActions: guidedFlowState.value.stepKey === 'status_value' ? buildGuidedQueryStatusActions() : [] }) persistAndScroll() return true } if (actionType === GUIDED_ACTION_SELECT_QUERY_STATUS) { const statusLabel = normalizeText(action?.payload?.query_status_label || action?.label) pushUser(`选择${statusLabel || '单据状态'}`) const queryText = buildGuidedStatusQueryText(guidedFlowState.value, statusLabel) await runStatusQuery(queryText, true) return true } return false } return { handleGuidedShortcut, handleGuidedComposerSubmit, handleGuidedSuggestedAction, resetGuidedFlowState } }