2026-05-23 19:54:42 +08:00
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
|
|
|
|
import {
|
2026-05-26 09:15:14 +08:00
|
|
|
buildApplicationTemplatePreview,
|
|
|
|
|
buildLocalApplicationPreviewMessage
|
|
|
|
|
} from '../../utils/expenseApplicationPreview.js'
|
2026-05-27 14:35:17 +08:00
|
|
|
import { fetchExpenseClaims } from '../../services/reimbursements.js'
|
|
|
|
|
import {
|
|
|
|
|
buildRequiredApplicationActions,
|
|
|
|
|
buildRequiredApplicationMissingText,
|
|
|
|
|
buildRequiredApplicationSelectionText,
|
|
|
|
|
filterRequiredApplicationCandidates,
|
|
|
|
|
requiresApplicationBeforeReimbursement
|
|
|
|
|
} from './travelReimbursementApplicationLinkModel.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
import {
|
|
|
|
|
GUIDED_ACTION_START_APPLICATION,
|
2026-05-23 19:54:42 +08:00
|
|
|
GUIDED_ACTION_CONFIRM_REIMBURSEMENT_REVIEW,
|
|
|
|
|
GUIDED_ACTION_CONTINUE_FILLING,
|
|
|
|
|
GUIDED_ACTION_OPEN_TRAVEL_CALCULATOR,
|
|
|
|
|
GUIDED_ACTION_PROCESS_INTERRUPTION,
|
|
|
|
|
GUIDED_ACTION_SELECT_EXPENSE_TYPE,
|
2026-05-27 14:35:17 +08:00
|
|
|
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
2026-05-23 19:54:42 +08:00
|
|
|
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,
|
2026-05-27 14:35:17 +08:00
|
|
|
selectGuidedRequiredApplication,
|
2026-05-23 19:54:42 +08:00
|
|
|
selectGuidedQueryMode,
|
2026-05-27 14:35:17 +08:00
|
|
|
shouldConfirmGuidedInterruption,
|
|
|
|
|
waitForGuidedApplicationSelection
|
2026-05-23 19:54:42 +08:00
|
|
|
} 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,
|
2026-05-26 09:15:14 +08:00
|
|
|
currentUser,
|
2026-05-23 19:54:42 +08:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
function startGuidedApplicationTemplate() {
|
|
|
|
|
resetGuidedFlowState()
|
|
|
|
|
const applicationPreview = buildApplicationTemplatePreview(currentUser?.value || {})
|
|
|
|
|
pushAssistant(buildLocalApplicationPreviewMessage(applicationPreview), {
|
|
|
|
|
meta: ['申请模板'],
|
|
|
|
|
applicationPreview
|
|
|
|
|
})
|
|
|
|
|
persistAndScroll()
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 19:54:42 +08:00
|
|
|
function startGuidedStatusQuery() {
|
|
|
|
|
guidedFlowState.value = createGuidedStatusQueryState()
|
|
|
|
|
guidedPendingFiles.value = []
|
|
|
|
|
pushAssistant(buildGuidedStatusQueryStartText(), {
|
|
|
|
|
meta: ['引导式查询'],
|
|
|
|
|
suggestedActions: buildGuidedQueryModeActions()
|
|
|
|
|
})
|
|
|
|
|
persistAndScroll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleGuidedShortcut(shortcut) {
|
|
|
|
|
const actionType = normalizeText(shortcut?.action)
|
2026-05-26 09:15:14 +08:00
|
|
|
if (actionType === GUIDED_ACTION_START_APPLICATION) {
|
|
|
|
|
startGuidedApplicationTemplate()
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-05-23 19:54:42 +08:00
|
|
|
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()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
async function selectExpenseTypeForGuidedReimbursement(currentState, expenseType, options = {}) {
|
|
|
|
|
const nextState = options.pendingSceneSelection
|
|
|
|
|
? {
|
|
|
|
|
...currentState,
|
|
|
|
|
values: {
|
|
|
|
|
...currentState.values,
|
|
|
|
|
pending_scene_original_message: normalizeText(options.pendingSceneSelection.originalMessage),
|
|
|
|
|
pending_scene_expense_type_label: normalizeText(options.pendingSceneSelection.expenseTypeLabel)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
: currentState
|
|
|
|
|
|
|
|
|
|
if (!requiresApplicationBeforeReimbursement(expenseType)) {
|
|
|
|
|
guidedFlowState.value = selectGuidedExpenseType(nextState, expenseType)
|
|
|
|
|
pushNextReimbursementPrompt()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let claimsPayload = null
|
|
|
|
|
try {
|
|
|
|
|
claimsPayload = await fetchExpenseClaims()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Fetch reimbursement applications failed:', error)
|
|
|
|
|
pushAssistant('查询可关联申请单时出现异常,请稍后再试。为避免直接报销,我先暂停当前流程。', {
|
|
|
|
|
meta: ['申请单查询失败']
|
|
|
|
|
})
|
|
|
|
|
toast?.('申请单查询失败,请稍后再试')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const applications = filterRequiredApplicationCandidates(claimsPayload, expenseType, currentUser?.value || {})
|
|
|
|
|
if (!applications.length) {
|
|
|
|
|
guidedFlowState.value = createGuidedReimbursementState()
|
|
|
|
|
pushAssistant(buildRequiredApplicationMissingText(expenseType), {
|
|
|
|
|
meta: ['缺少可关联申请单'],
|
|
|
|
|
suggestedActions: buildGuidedExpenseTypeActions()
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guidedFlowState.value = waitForGuidedApplicationSelection(nextState, expenseType, applications)
|
|
|
|
|
pushAssistant(buildRequiredApplicationSelectionText(expenseType, applications), {
|
|
|
|
|
meta: ['等待关联申请单'],
|
|
|
|
|
suggestedActions: buildRequiredApplicationActions(applications, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildPendingSceneSubmitOptions(state) {
|
|
|
|
|
const current = normalizeGuidedFlowState(state)
|
|
|
|
|
const originalMessage = normalizeText(current.values.pending_scene_original_message)
|
|
|
|
|
const expenseTypeLabel = normalizeText(current.values.pending_scene_expense_type_label)
|
|
|
|
|
const applicationNo = normalizeText(current.values.application_claim_no)
|
|
|
|
|
const applicationId = normalizeText(current.values.application_claim_id)
|
|
|
|
|
if (!originalMessage || !expenseTypeLabel || !applicationNo) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawText = [
|
|
|
|
|
originalMessage,
|
|
|
|
|
`用户选择报销场景:${expenseTypeLabel}`,
|
|
|
|
|
`关联申请单:${applicationNo}`
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
rawText,
|
|
|
|
|
userText: `关联申请单 ${applicationNo}`,
|
|
|
|
|
pendingText: `已关联申请单,正在按${expenseTypeLabel}识别...`,
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
skipUserMessage: true,
|
|
|
|
|
extraContext: {
|
|
|
|
|
draft_claim_id: '',
|
|
|
|
|
user_input_text: originalMessage,
|
|
|
|
|
expense_scene_selection: {
|
|
|
|
|
expense_type: current.expenseType || 'other',
|
|
|
|
|
expense_type_label: expenseTypeLabel,
|
|
|
|
|
original_message: originalMessage,
|
|
|
|
|
application_claim_id: applicationId,
|
|
|
|
|
application_claim_no: applicationNo
|
|
|
|
|
},
|
|
|
|
|
review_form_values: {
|
|
|
|
|
expense_type: expenseTypeLabel,
|
|
|
|
|
application_claim_id: applicationId,
|
|
|
|
|
application_claim_no: applicationNo,
|
|
|
|
|
application_reason: current.values.application_reason || '',
|
|
|
|
|
application_location: current.values.application_location || '',
|
|
|
|
|
application_amount: current.values.application_amount || ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleReimbursementAnswer(answerText, files) {
|
2026-05-23 19:54:42 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-05-27 14:35:17 +08:00
|
|
|
await selectExpenseTypeForGuidedReimbursement(currentState, expenseType)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (currentState.stepKey === 'application_selection') {
|
|
|
|
|
pushAssistant('请先点击上方列出的申请单完成关联。关联后,我再继续询问报销依据。', {
|
|
|
|
|
meta: ['等待关联申请单'],
|
|
|
|
|
suggestedActions: buildRequiredApplicationActions(
|
|
|
|
|
currentState.applicationCandidates,
|
|
|
|
|
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-05-23 19:54:42 +08:00
|
|
|
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) {
|
2026-05-27 14:35:17 +08:00
|
|
|
await handleReimbursementAnswer(answerText, files)
|
2026-05-23 19:54:42 +08:00
|
|
|
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,
|
2026-05-27 14:35:17 +08:00
|
|
|
GUIDED_ACTION_SELECT_REQUIRED_APPLICATION,
|
2026-05-23 19:54:42 +08:00
|
|
|
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)
|
|
|
|
|
pushUser(`选择${expenseTypeLabel || '报销类型'}`)
|
2026-05-27 14:35:17 +08:00
|
|
|
await selectExpenseTypeForGuidedReimbursement(guidedFlowState.value, expenseType)
|
|
|
|
|
persistAndScroll()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (actionType === GUIDED_ACTION_SELECT_REQUIRED_APPLICATION) {
|
|
|
|
|
const applicationNo = normalizeText(action?.payload?.application_claim_no || action?.label)
|
|
|
|
|
pushUser(`关联申请单 ${applicationNo || ''}`.trim())
|
|
|
|
|
guidedFlowState.value = selectGuidedRequiredApplication(guidedFlowState.value, action?.payload || {})
|
|
|
|
|
const pendingSceneSubmitOptions = buildPendingSceneSubmitOptions(guidedFlowState.value)
|
|
|
|
|
if (pendingSceneSubmitOptions) {
|
|
|
|
|
resetGuidedFlowState()
|
|
|
|
|
persistAndScroll()
|
|
|
|
|
await submitExistingComposer(pendingSceneSubmitOptions)
|
|
|
|
|
return true
|
|
|
|
|
}
|
2026-05-23 19:54:42 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-27 14:35:17 +08:00
|
|
|
async function handleSceneSelectionApplicationGate(message, action) {
|
|
|
|
|
const actionType = normalizeText(action?.action_type)
|
|
|
|
|
if (actionType !== 'select_expense_type') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
const expenseType = normalizeText(actionPayload.expense_type)
|
|
|
|
|
if (!requiresApplicationBeforeReimbursement(expenseType)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const expenseTypeLabel = normalizeText(actionPayload.expense_type_label || action?.label)
|
|
|
|
|
const originalMessage = normalizeText(actionPayload.original_message || message?.text)
|
|
|
|
|
if (!expenseTypeLabel || !originalMessage) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guidedPendingFiles.value = []
|
|
|
|
|
pushUser(`选择${expenseTypeLabel}`)
|
|
|
|
|
await selectExpenseTypeForGuidedReimbursement(createGuidedReimbursementState(), expenseType, {
|
|
|
|
|
pendingSceneSelection: {
|
|
|
|
|
originalMessage,
|
|
|
|
|
expenseTypeLabel
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
persistAndScroll()
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 19:54:42 +08:00
|
|
|
return {
|
|
|
|
|
handleGuidedShortcut,
|
|
|
|
|
handleGuidedComposerSubmit,
|
|
|
|
|
handleGuidedSuggestedAction,
|
2026-05-27 14:35:17 +08:00
|
|
|
handleSceneSelectionApplicationGate,
|
2026-05-23 19:54:42 +08:00
|
|
|
resetGuidedFlowState
|
|
|
|
|
}
|
|
|
|
|
}
|