import { buildApplicationPreviewFooterMessage, buildApplicationPreviewRows, buildLocalApplicationPreviewMessage, normalizeApplicationPreview } from '../../utils/expenseApplicationPreview.js' import { buildAiApplicationPrecheck, buildAiApplicationSubmitConflictMessage, isAiApplicationPrecheckBlocking } from '../../utils/aiApplicationPrecheckModel.js' import { fetchExpenseClaims } from '../../services/reimbursements.js' import { AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT, runAiApplicationPreviewAction } from '../../services/aiApplicationPreviewActions.js' import { buildFailedInlineApplicationSubmitThinkingEvents, buildInitialInlineApplicationSubmitThinkingEvents, buildInlineApplicationDetailAction, buildInlineApplicationPreview, buildInlineApplicationPreviewActionResultText, buildInlineApplicationSubmitPrecheckPayload, buildInlineApplicationSubmitThinkingEvents, completeInlineThinkingEvents, extractInlineApplicationDraftPayload, resolveInlineApplicationPreviewActionFromText } from './workbenchAiApplicationPreviewModel.js' import { AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION } from './workbenchAiMessageModel.js' function isApplicationPreviewEstimatePendingPreview(applicationPreview = {}) { const fields = normalizeApplicationPreview(applicationPreview).fields || {} return [ fields.transportPolicy, fields.policyEstimate, fields.transportEstimatedAmount, fields.amount ].some((value) => /正在|查询中/.test(String(value || ''))) } function shouldRefreshInlineApplicationPreviewEstimate(fieldKey = '') { return ['transportMode', 'time', 'location', 'days'].includes(String(fieldKey || '').trim()) } export function useWorkbenchAiApplicationPreviewFlow({ activateInlineConversation, applicationPreviewEditor, applicationSubmitConfirmContext, applicationSubmitConfirmOpen, assistantDraft, cancelApplicationPreviewEditor, clearAiModeFiles, closeWorkbenchDatePicker, commitApplicationPreviewEditor: commitBaseApplicationPreviewEditor, conversationId, conversationMessages, conversationStarted, createInlineMessage, currentUser, handleApplicationPreviewEditorKeydown, inlineConversationAutoScrollPinned, isApplicationPreviewEditing, openApplicationPreviewEditor, persistCurrentConversation, pushInlineApplicationActionUserMessage, pushInlineUserMessage, refreshApplicationPreviewEstimate, removeWorkbenchDateTag, replaceInlineMessage, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, resolveInlineThinkingEvents, resolveLatestInlineUserPrompt, scrollInlineConversationToBottom, sending, toast }) { function isApplicationPreviewEstimatePending(message = {}) { return Boolean(message?.applicationPreview && isApplicationPreviewEstimatePendingPreview(message.applicationPreview)) } function canShowInlineSuggestedActions(message = {}) { return Boolean(message?.suggestedActions?.length) && !isApplicationPreviewEstimatePending(message) } function isInlineSuggestedActionDisabled(action = {}, message = {}) { const actionType = String(action?.action_type || '').trim() return ( Boolean(action?.disabled) || (actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION && sending.value) || ( [AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT].includes(actionType) && isApplicationPreviewEstimatePending(message) ) ) } function resolveInlineApplicationPreviewRows(message) { return buildApplicationPreviewRows(message?.applicationPreview || {}) } function resolveInlineApplicationPreviewMissingFields(message) { return normalizeApplicationPreview(message?.applicationPreview || {}).missingFields || [] } function resolveInlineApplicationPreviewEditorControl(fieldKey) { const control = resolveApplicationPreviewEditorControl(fieldKey) return control === 'date' ? 'text' : control } function buildInlineApplicationPreviewSuggestedActions(applicationPreview = {}, draftPayload = null) { if (isApplicationPreviewEstimatePendingPreview(applicationPreview)) { return [] } const normalized = normalizeApplicationPreview(applicationPreview) const actions = [{ label: '保存草稿', description: '先保存当前申请表,后续可以继续补充或提交。', icon: 'mdi mdi-content-save-outline', action_type: AI_APPLICATION_ACTION_SAVE_DRAFT, payload: { draftPayload } }] if (normalized.readyToSubmit) { actions.push({ label: '直接提交', description: '提交前先核查相同日期申请单,确认通过后进入审批流程。', icon: 'mdi mdi-send-check-outline', action_type: AI_APPLICATION_ACTION_SUBMIT, payload: { draftPayload } }) } return actions } function syncInlineApplicationPreviewMessageContent(message) { if (!message?.applicationPreview) { return } const nextContent = buildLocalApplicationPreviewMessage(message.applicationPreview) message.content = nextContent message.text = nextContent message.suggestedActions = buildInlineApplicationPreviewSuggestedActions(message.applicationPreview, message.draftPayload) } async function commitInlineApplicationPreviewEditor(message) { const shouldLockForEstimate = shouldRefreshInlineApplicationPreviewEstimate(applicationPreviewEditor.value.fieldKey) if (shouldLockForEstimate) { message.suggestedActions = [] persistCurrentConversation() } const committed = await commitBaseApplicationPreviewEditor(message) syncInlineApplicationPreviewMessageContent(message) persistCurrentConversation() return committed } function handleInlineApplicationPreviewEditorKeydown(event, message) { if (event.key === 'Enter') { event.preventDefault() void commitInlineApplicationPreviewEditor(message) return } if (event.key === 'Escape') { event.preventDefault() cancelApplicationPreviewEditor() return } handleApplicationPreviewEditorKeydown(event, message) } function buildInlineApplicationPreviewFooterText(message) { const normalized = normalizeApplicationPreview(message?.applicationPreview || {}) if (isApplicationPreviewEstimatePending(message)) { return '费用测算正在同步,请稍等,完成后才能保存草稿或直接提交。' } if (normalized.validationIssues?.length || normalized.missingFields?.length) { return buildApplicationPreviewFooterMessage(normalized) } return '申请核对表已补齐,费用测算已同步。你仍可点击表格继续修改;确认无误后,可以点击下方按钮保存草稿或直接提交,也可以直接回复“保存草稿”或“提交”。' } function resolveLatestApplicationPreviewMessage() { return [...conversationMessages.value] .reverse() .find((message) => message.role === 'assistant' && message.applicationPreview) } function requestInlineApplicationSubmitConfirmation(targetMessage, options = {}) { applicationSubmitConfirmContext.value = { messageId: String(targetMessage?.id || '').trim(), draftPayload: targetMessage?.draftPayload || options.draftPayload || null, userText: String(options.userText || '直接提交').trim() || '直接提交' } applicationSubmitConfirmOpen.value = true persistCurrentConversation() } function cancelInlineApplicationSubmitConfirm() { applicationSubmitConfirmOpen.value = false applicationSubmitConfirmContext.value = null } function confirmInlineApplicationSubmit() { const context = applicationSubmitConfirmContext.value || {} applicationSubmitConfirmOpen.value = false applicationSubmitConfirmContext.value = null const sourceMessage = conversationMessages.value.find((message) => message.id === context.messageId) if (!sourceMessage?.applicationPreview) { toast('当前申请表已变化,请重新点击直接提交。') return } void executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, sourceMessage, { confirmed: true, skipUserMessage: false, draftPayload: context.draftPayload || null, userText: context.userText || '直接提交' }) } async function runInlineApplicationSubmitPrecheck(targetMessage, pendingMessage, normalizedPreview, options = {}) { try { const claimsPayload = await fetchExpenseClaims({ page: 1, pageSize: 100 }) const precheck = buildAiApplicationPrecheck(normalizedPreview, { claimsPayload: buildInlineApplicationSubmitPrecheckPayload( claimsPayload, targetMessage.draftPayload || options.draftPayload || null ), currentUser: currentUser.value || {}, expenseType: 'travel' }) const thinkingEvents = buildInlineApplicationSubmitThinkingEvents(precheck) const blocked = isAiApplicationPrecheckBlocking(precheck) if (blocked) { replaceInlineMessage( pendingMessage.id, createInlineMessage('assistant', buildAiApplicationSubmitConflictMessage(normalizedPreview, precheck), { id: pendingMessage.id, stewardPlan: { streamStatus: 'completed', thinkingEvents } }) ) persistCurrentConversation() scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) return false } const message = conversationMessages.value.find((item) => item.id === pendingMessage.id) || pendingMessage message.content = '提交前核查通过,正在提交申请并进入审批流程...' message.paragraphs = ['提交前核查通过,正在提交申请并进入审批流程...'] message.stewardPlan = { ...(message.stewardPlan || {}), streamStatus: 'streaming', thinkingEvents } scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) return true } catch (error) { replaceInlineMessage( pendingMessage.id, createInlineMessage('assistant', [ '### 提交前核查失败', '系统未能完成相同日期申请单查询,所以本次申请没有提交。', '请稍后重试;如果仍然失败,请先到单据中心核对是否已有同日期申请单。' ].join('\n\n'), { id: pendingMessage.id, stewardPlan: { streamStatus: 'failed', thinkingEvents: buildFailedInlineApplicationSubmitThinkingEvents(error) } }) ) toast('提交前核查失败,已暂停提交。') persistCurrentConversation() return false } } async function executeInlineApplicationPreviewAction(actionType, sourceMessage = null, options = {}) { const targetMessage = sourceMessage?.applicationPreview ? sourceMessage : resolveLatestApplicationPreviewMessage() if (!targetMessage?.applicationPreview) { toast('当前没有可提交的申请表。') return false } const normalizedPreview = normalizeApplicationPreview(targetMessage.applicationPreview) const isSubmit = actionType === AI_APPLICATION_ACTION_SUBMIT const userText = String(options.userText || (isSubmit ? '直接提交' : '保存草稿')).trim() if (isSubmit && !normalizedPreview.readyToSubmit) { if (!options.skipUserMessage) { pushInlineApplicationActionUserMessage(userText) } const missingText = normalizedPreview.missingFields?.length ? `当前还缺少:${normalizedPreview.missingFields.join('、')}。` : '' const validationText = normalizedPreview.validationIssues?.length ? normalizedPreview.validationIssues.map((item) => item.message).join(';') : '' conversationMessages.value.push(createInlineMessage('assistant', [ '### 暂不能提交申请', missingText || validationText || '当前申请表还未通过提交校验。', '请先点击表格中的字段补充或修正,补齐后我会开放“直接提交”入口。' ].filter(Boolean).join('\n\n'))) persistCurrentConversation() scrollInlineConversationToBottom() return true } if (isSubmit && !options.confirmed) { requestInlineApplicationSubmitConfirmation(targetMessage, { ...options, userText }) return true } if (!options.skipUserMessage) { pushInlineApplicationActionUserMessage(userText) } sending.value = true const pendingMessage = createInlineMessage( 'assistant', isSubmit ? '正在提交前核查相同日期申请单...' : '正在保存申请草稿...', { pending: true, stewardPlan: { streamStatus: 'streaming', thinkingEvents: isSubmit ? buildInitialInlineApplicationSubmitThinkingEvents() : [ { eventId: 'application-save-draft', title: '保存申请草稿', content: '正在按当前申请表内容保存草稿。', status: 'running' } ] } } ) conversationMessages.value.push(pendingMessage) scrollInlineConversationToBottom() try { if (isSubmit) { const precheckPassed = await runInlineApplicationSubmitPrecheck( targetMessage, pendingMessage, normalizedPreview, options ) if (!precheckPassed) { return true } } const payload = await runAiApplicationPreviewAction({ actionType, applicationPreview: normalizedPreview, currentUser: currentUser.value || {}, conversationId: conversationId.value, draftPayload: targetMessage.draftPayload || options.draftPayload || null }) const draftPayload = extractInlineApplicationDraftPayload(payload) if (draftPayload) { targetMessage.draftPayload = draftPayload } targetMessage.suggestedActions = [] replaceInlineMessage( pendingMessage.id, createInlineMessage('assistant', buildInlineApplicationPreviewActionResultText(actionType, payload), { id: pendingMessage.id, stewardPlan: { streamStatus: 'completed', thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage)) }, suggestedActions: isSubmit ? buildInlineApplicationDetailAction(draftPayload) : [] }) ) persistCurrentConversation() scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) return true } catch (error) { replaceInlineMessage( pendingMessage.id, createInlineMessage('assistant', error?.message || (isSubmit ? '申请提交失败,请稍后重试。' : '申请草稿保存失败,请稍后重试。'), { id: pendingMessage.id, stewardPlan: { streamStatus: 'failed', thinkingEvents: resolveInlineThinkingEvents(pendingMessage).map((item) => ({ ...item, status: 'failed' })) } }) ) toast(error?.message || (isSubmit ? '申请提交失败。' : '申请草稿保存失败。')) persistCurrentConversation() return true } finally { sending.value = false scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) } } function handleInlineApplicationPreviewTextAction(prompt, applicationPreviewEstimatePending) { if (applicationPreviewEstimatePending.value) { toast('请等待费用测算完成后再继续操作。') return true } const actionType = resolveInlineApplicationPreviewActionFromText(prompt) if (!actionType || !resolveLatestApplicationPreviewMessage()) { return false } void executeInlineApplicationPreviewAction(actionType, null, { userText: prompt }) return true } async function startAiApplicationPreview(expenseType, expenseTypeLabel, sourceText = '', options = {}) { if (!conversationStarted.value) { activateInlineConversation({ title: String(expenseTypeLabel || expenseType || '申请').trim().slice(0, 18) || '申请' }) } const previewSourceText = String(sourceText || resolveLatestInlineUserPrompt()).trim() assistantDraft.value = '' removeWorkbenchDateTag() closeWorkbenchDatePicker() clearAiModeFiles() if (options.pushUserMessage !== false) { pushInlineUserMessage(options.userMessage || '确认发起出差申请') } const pendingMessage = createInlineMessage('assistant', '正在生成申请核对表,请稍等...', { pending: true, stewardPlan: { streamStatus: 'streaming', thinkingEvents: [ { eventId: 'application-preview-build', title: '整理申请表字段', content: '正在把已识别的时间、地点、事由和差旅类型整理成可编辑表格。', status: 'running' }, { eventId: 'application-preview-estimate', title: '同步费用测算', content: '正在刷新差旅规则和费用测算,完成后会直接展示核对表。', status: 'pending' } ] } }) conversationMessages.value.push(pendingMessage) persistCurrentConversation() scrollInlineConversationToBottom() try { const preview = await refreshApplicationPreviewEstimate( buildInlineApplicationPreview(expenseTypeLabel || expenseType, previewSourceText, currentUser.value || {}) ) const content = buildLocalApplicationPreviewMessage(preview) replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', content, { id: pendingMessage.id, applicationPreview: preview, suggestedActions: buildInlineApplicationPreviewSuggestedActions(preview), stewardPlan: { streamStatus: 'completed', thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage)) }, text: content })) } catch (error) { replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', { id: pendingMessage.id, stewardPlan: { streamStatus: 'failed', thinkingEvents: resolveInlineThinkingEvents(pendingMessage).map((item) => ({ ...item, status: 'failed' })) } })) toast(error?.message || '申请核对表生成失败。') } finally { persistCurrentConversation() scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value }) } } return { buildInlineApplicationPreviewFooterText, buildInlineApplicationPreviewSuggestedActions, canShowInlineSuggestedActions, cancelInlineApplicationSubmitConfirm, commitInlineApplicationPreviewEditor, confirmInlineApplicationSubmit, executeInlineApplicationPreviewAction, handleInlineApplicationPreviewEditorKeydown, handleInlineApplicationPreviewTextAction, isApplicationPreviewEditing, isApplicationPreviewEstimatePending, isInlineSuggestedActionDisabled, openApplicationPreviewEditor, resolveApplicationPreviewEditorOptions, resolveInlineApplicationPreviewEditorControl, resolveInlineApplicationPreviewMissingFields, resolveInlineApplicationPreviewRows, startAiApplicationPreview } }