Files
X-Financial/web/src/views/scripts/useTravelReimbursementStewardRuntime.js

776 lines
28 KiB
JavaScript
Raw Normal View History

refactor(travel): split reimbursement create workflow 完整修改内容: - 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。 - 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。 - 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。 - 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。 - 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。 验证: - node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块 - npm --prefix web run build - node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs 说明: - 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。 - 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。 - TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
2026-06-13 14:52:26 +00:00
import { fetchStewardRuntimeDecision } from '../../services/steward.js'
import {
buildApplicationPreviewSubmitText,
buildLocalApplicationPreviewMessage,
normalizeApplicationPreview
} from '../../utils/expenseApplicationPreview.js'
import {
buildTravelPlanningNudgeMessage,
buildTravelPlanningSuggestedActions
} from '../../utils/travelApplicationPlanning.js'
import {
SESSION_TYPE_APPLICATION,
SESSION_TYPE_STEWARD
} from './travelReimbursementConversationModel.js'
import {
APPLICATION_PREVIEW_FIELD_ACTION_SET,
STEWARD_ASSISTANT_NAME,
isApplicationSubmitConfirmationText,
isStewardRuntimeCancelText,
isStewardRuntimeContinueText,
normalizeStewardRuntimeInputText,
resolveStewardRuntimeTransportAlias,
shouldPlanNewStewardTasksLocally
} from './travelReimbursementStewardRuntimeTextModel.js'
import {
buildStewardContinuationAfterAction,
pushStewardContinuationMessage,
resolveStewardMissingFieldItems
} from './travelReimbursementStewardFollowupFlow.js'
export { STEWARD_ASSISTANT_NAME } from './travelReimbursementStewardRuntimeTextModel.js'
export function useTravelReimbursementStewardRuntime(ctx) {
const {
activeSessionType,
applicationSubmitConfirmDialog,
attachedFiles,
composerDraft,
createMessage,
currentUser,
emit,
handleSuggestedAction,
isStewardSession,
linkedRequest,
messages,
nextTick,
persistSessionState,
props,
reviewActionBusy,
scrollToBottom,
sessionSwitchBusy,
submitComposer,
submitStewardPlan,
submitting,
toast,
adjustComposerTextareaHeight,
resolveCurrentUserId
} = ctx
function findLatestApplicationPreviewMessage() {
for (const message of [...messages.value].reverse()) {
if (
message?.role !== 'assistant' ||
!message.applicationPreview ||
message.applicationSubmitConfirmed
) {
continue
}
return message
}
return null
}
function findPendingApplicationSubmitMessage() {
const message = findLatestApplicationPreviewMessage()
if (!message) {
return null
}
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
if (normalizedPreview.readyToSubmit) {
message.applicationPreview = normalizedPreview
return message
}
return null
}
function pushApplicationSubmitBlockedMessage(userText = '', message = null, options = {}) {
const normalizedPreview = normalizeApplicationPreview(message?.applicationPreview || {})
const missingFields = Array.isArray(normalizedPreview.missingFields)
? normalizedPreview.missingFields
: []
const validationIssues = Array.isArray(normalizedPreview.validationIssues)
? normalizedPreview.validationIssues
: []
if (userText && !options.userMessageAlreadyAdded) {
messages.value.push(createMessage('user', userText))
}
messages.value.push(createMessage(
'assistant',
[
'我理解你是在确认当前申请单,但这张申请单还不能提交。',
'',
missingFields.length
? `还需要先补充:**${missingFields.join('、')}**。`
: validationIssues.length
? `需要先修正:**${validationIssues[0].message}**`
: '请先把申请核对表中的待补充信息补齐。',
'',
'补齐后再输入“确认”,我会继续提交至审批流程。'
].join('\n'),
[],
{
assistantName: String(message?.assistantName || '').trim() || undefined,
meta: ['等待补充']
}
))
composerDraft.value = ''
persistSessionState()
nextTick(() => {
adjustComposerTextareaHeight()
scrollToBottom()
})
}
async function handleApplicationSubmitConfirmationText(options = {}) {
const rawText = String(options.rawText ?? composerDraft.value ?? '').trim()
const files = Array.from(options.files ?? attachedFiles.value ?? [])
if (!isApplicationSubmitConfirmationText(rawText) || files.length) {
return false
}
const latestApplicationMessage = findLatestApplicationPreviewMessage()
if (!latestApplicationMessage) {
return false
}
const targetMessage = findPendingApplicationSubmitMessage()
if (!targetMessage) {
pushApplicationSubmitBlockedMessage(rawText, latestApplicationMessage)
return true
}
applicationSubmitConfirmDialog.value = {
open: true,
message: targetMessage
}
await confirmApplicationSubmit({ userText: rawText })
return true
}
function findPendingStewardSuggestedActionContext(decision = null) {
const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim()
const targetTaskId = String(decision?.target_task_id || decision?.targetTaskId || '').trim()
for (const message of [...messages.value].reverse()) {
if (
message?.role !== 'assistant' ||
message.suggestedActionsLocked ||
!Array.isArray(message.suggestedActions) ||
!message.suggestedActions.length
) {
continue
}
if (targetMessageId && String(message.id || '') !== targetMessageId) {
continue
}
const action = message.suggestedActions.find((item) => {
if (String(item?.action_type || '').trim() === APPLICATION_PREVIEW_FIELD_ACTION_SET) {
return false
}
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {}
return !targetTaskId ||
String(payload.steward_next_task_id || payload.target_task_id || '').trim() === targetTaskId
}) || message.suggestedActions[0]
if (action) {
return { message, action }
}
}
return null
}
function findPendingSlotSuggestedActionContext(decision = null) {
const fieldKey = String(decision?.field_key || decision?.fieldKey || '').trim()
const fieldValue = String(decision?.field_value || decision?.fieldValue || '').trim()
for (const message of [...messages.value].reverse()) {
if (
message?.role !== 'assistant' ||
message.suggestedActionsLocked ||
!Array.isArray(message.suggestedActions) ||
!message.suggestedActions.length
) {
continue
}
const action = message.suggestedActions.find((item) => {
if (String(item?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) {
return false
}
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {}
const payloadField = String(payload.field_key || payload.fieldKey || '').trim()
const payloadValue = String(payload.value || item?.label || '').trim()
return payloadField && (!fieldKey || payloadField === fieldKey) && (!fieldValue || payloadValue === fieldValue)
})
if (action) {
return { message, action }
}
}
return null
}
function findPendingSlotSuggestedActionContextByInput(rawText = '') {
const normalizedInput = normalizeStewardRuntimeInputText(rawText)
if (!normalizedInput) {
return null
}
const transportAlias = resolveStewardRuntimeTransportAlias(normalizedInput)
for (const message of [...messages.value].reverse()) {
if (
message?.role !== 'assistant' ||
message.suggestedActionsLocked ||
!Array.isArray(message.suggestedActions) ||
!message.suggestedActions.length
) {
continue
}
const exactMatches = []
const fuzzyMatches = []
message.suggestedActions.forEach((action) => {
if (String(action?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) {
return
}
const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const fieldKey = String(payload.field_key || payload.fieldKey || '').trim()
const value = String(payload.value || action?.label || '').trim()
const label = String(action?.label || value).trim()
const tokens = [value, label]
.map((item) => normalizeStewardRuntimeInputText(item))
.filter(Boolean)
if (!fieldKey || !value || !tokens.length) {
return
}
if (tokens.includes(normalizedInput)) {
exactMatches.push({ message, action })
return
}
const actionTransportAlias = resolveStewardRuntimeTransportAlias(`${value}${label}`)
if (
transportAlias &&
(
tokens.includes(normalizeStewardRuntimeInputText(transportAlias)) ||
actionTransportAlias === transportAlias
)
) {
fuzzyMatches.push({ message, action })
return
}
if (tokens.some((token) => token.length >= 2 && normalizedInput.includes(token))) {
fuzzyMatches.push({ message, action })
}
})
if (exactMatches.length === 1) {
return exactMatches[0]
}
if (exactMatches.length > 1) {
return null
}
const uniqueFuzzyMatches = fuzzyMatches.filter((item, index, list) =>
list.findIndex((candidate) => candidate.action === item.action) === index
)
if (uniqueFuzzyMatches.length === 1) {
return uniqueFuzzyMatches[0]
}
if (uniqueFuzzyMatches.length > 1) {
return null
}
}
return null
}
function buildStewardRuntimeState() {
const latestApplicationMessage = findLatestApplicationPreviewMessage()
const applicationPreview = latestApplicationMessage?.applicationPreview
? normalizeApplicationPreview(latestApplicationMessage.applicationPreview)
: null
const applicationContinuation = latestApplicationMessage?.stewardContinuation || null
const pendingSlotContext = findPendingSlotSuggestedActionContext()
const pendingStewardContext = pendingSlotContext ? null : findPendingStewardSuggestedActionContext()
const pendingActionPayload = pendingStewardContext?.action?.payload && typeof pendingStewardContext.action.payload === 'object'
? pendingStewardContext.action.payload
: {}
const pendingSlotPayload = pendingSlotContext?.action?.payload && typeof pendingSlotContext.action.payload === 'object'
? pendingSlotContext.action.payload
: {}
const continuation = applicationContinuation || pendingStewardContext?.message?.stewardContinuation || null
const remainingTasks = Array.isArray(continuation?.remainingTasks)
? continuation.remainingTasks
: []
const pendingApplication = latestApplicationMessage && applicationPreview
? {
message_id: String(latestApplicationMessage.id || '').trim(),
task_id: String(
applicationContinuation?.currentTaskId ||
applicationContinuation?.current_task_id ||
applicationContinuation?.currentTask?.task_id ||
applicationContinuation?.currentTask?.taskId ||
''
).trim(),
ready_to_submit: Boolean(applicationPreview.readyToSubmit),
missing_fields: Array.isArray(applicationPreview.missingFields) ? applicationPreview.missingFields : [],
fields: applicationPreview.fields || {}
}
: null
return {
waiting_for: pendingApplication
? (pendingApplication.ready_to_submit ? 'application_submit_confirmation' : 'application_field_completion')
: pendingSlotContext
? 'application_field_completion'
: pendingStewardContext
? 'steward_next_task_confirmation'
: '',
current_task: continuation?.currentTask || continuation?.current_task || null,
remaining_tasks: remainingTasks,
completed_tasks: messages.value
.filter((message) => message?.applicationSubmitConfirmed)
.map((message) => ({
message_id: String(message.id || '').trim(),
task_type: 'expense_application'
})),
pending_application: pendingApplication,
pending_steward_action: pendingStewardContext
? {
message_id: String(pendingStewardContext.message?.id || '').trim(),
action_type: String(pendingStewardContext.action?.action_type || '').trim(),
label: String(pendingStewardContext.action?.label || '').trim(),
target_task_id: String(pendingActionPayload.steward_next_task_id || pendingActionPayload.target_task_id || '').trim(),
payload: pendingActionPayload
}
: null,
pending_slot_action: pendingSlotContext
? {
message_id: String(pendingSlotContext.message?.id || '').trim(),
field_key: String(pendingSlotPayload.field_key || pendingSlotPayload.fieldKey || '').trim(),
label: String(pendingSlotContext.action?.label || '').trim(),
payload: pendingSlotPayload
}
: null
}
}
function hasActiveStewardRuntimeDecisionContext(runtimeState = {}) {
return Boolean(
String(runtimeState?.waiting_for || '').trim() ||
runtimeState?.pending_application ||
runtimeState?.pending_steward_action ||
runtimeState?.pending_slot_action ||
runtimeState?.current_task ||
(Array.isArray(runtimeState?.remaining_tasks) && runtimeState.remaining_tasks.length > 0) ||
(Array.isArray(runtimeState?.completed_tasks) && runtimeState.completed_tasks.length > 0)
)
}
function pushStewardRuntimeUserMessage(userText = '') {
const normalizedText = String(userText || '').trim()
if (!normalizedText) {
return false
}
messages.value.push(createMessage('user', normalizedText))
composerDraft.value = ''
persistSessionState()
nextTick(() => {
adjustComposerTextareaHeight()
scrollToBottom()
})
return true
}
function pushStewardRuntimeResponse(userText = '', decision = null, options = {}) {
if (userText && !options.userMessageAlreadyAdded) {
messages.value.push(createMessage('user', userText))
}
const text = String(decision?.question || decision?.response_text || decision?.responseText || decision?.rationale || '').trim()
if (text) {
messages.value.push(createMessage('assistant', text, [], {
assistantName: STEWARD_ASSISTANT_NAME,
meta: [STEWARD_ASSISTANT_NAME]
}))
}
composerDraft.value = ''
persistSessionState()
nextTick(() => {
adjustComposerTextareaHeight()
scrollToBottom()
})
}
function buildStewardRuntimeFastPathDecision(rawText = '', runtimeState = {}) {
const normalizedText = String(rawText || '').trim()
if (!normalizedText) {
return null
}
if (shouldPlanNewStewardTasksLocally(normalizedText, runtimeState)) {
return {
next_action: 'plan_new_tasks'
}
}
if (isStewardRuntimeCancelText(normalizedText)) {
return {
next_action: 'cancel_current_action',
response_text: '已暂停当前等待动作。我不会继续提交或进入下一步;如果你要重新规划,请直接告诉我新的财务事项。'
}
}
const slotContext = findPendingSlotSuggestedActionContextByInput(normalizedText)
const payload = slotContext?.action?.payload && typeof slotContext.action.payload === 'object'
? slotContext.action.payload
: {}
if (slotContext) {
return {
next_action: 'fill_current_slot',
target_message_id: String(slotContext.message?.id || '').trim(),
field_key: String(payload.field_key || payload.fieldKey || '').trim(),
field_value: String(payload.value || slotContext.action?.label || normalizedText).trim()
}
}
if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) {
if (runtimeState?.pending_application?.ready_to_submit) {
return {
next_action: 'submit_current_application',
target_message_id: runtimeState.pending_application.message_id || ''
}
}
if (runtimeState?.pending_steward_action) {
return {
next_action: 'continue_next_task',
target_message_id: runtimeState.pending_steward_action.message_id || '',
target_task_id: runtimeState.pending_steward_action.target_task_id || ''
}
}
}
if (String(runtimeState?.waiting_for || '').trim() === 'application_field_completion') {
if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) {
const missingFields = Array.isArray(runtimeState?.pending_application?.missing_fields)
? runtimeState.pending_application.missing_fields
: []
return {
next_action: 'ask_user',
response_text: missingFields.length
? `当前申请还不能继续提交,请先补充:${missingFields.join('、')}。你可以直接回复对应选项或填写具体内容。`
: '当前申请还有信息需要先补充。请先回复系统刚刚追问的内容,我再继续生成核对结果。'
}
}
}
return null
}
function shouldUseStewardRuntimeLlmDecision(rawText = '', runtimeState = {}) {
if (shouldPlanNewStewardTasksLocally(rawText, runtimeState)) {
return false
}
const normalizedText = normalizeStewardRuntimeInputText(rawText)
if (!normalizedText) {
return false
}
if (
isApplicationSubmitConfirmationText(normalizedText) ||
isStewardRuntimeContinueText(normalizedText) ||
isStewardRuntimeCancelText(normalizedText)
) {
return false
}
if (
findPendingSlotSuggestedActionContextByInput(normalizedText)
) {
return false
}
return true
}
async function executeStewardRuntimeDecision(decision = null, rawText = '', options = {}) {
const nextAction = String(decision?.next_action || decision?.nextAction || '').trim()
const userMessageAlreadyAdded = Boolean(options.userMessageAlreadyAdded)
if (nextAction === 'submit_current_application') {
const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim()
const targetMessage = targetMessageId
? messages.value.find((message) => String(message.id || '') === targetMessageId)
: findPendingApplicationSubmitMessage()
if (!targetMessage?.applicationPreview) {
return false
}
const normalizedPreview = normalizeApplicationPreview(targetMessage.applicationPreview)
if (!normalizedPreview.readyToSubmit) {
pushApplicationSubmitBlockedMessage(rawText, targetMessage, { userMessageAlreadyAdded })
return true
}
targetMessage.applicationPreview = normalizedPreview
applicationSubmitConfirmDialog.value = { open: true, message: targetMessage }
await confirmApplicationSubmit({ userText: rawText, skipUserMessage: userMessageAlreadyAdded })
return true
}
if (nextAction === 'continue_next_task') {
const context = findPendingStewardSuggestedActionContext(decision)
if (!context) {
return false
}
if (rawText && !userMessageAlreadyAdded) {
messages.value.push(createMessage('user', rawText))
}
context.action.confirmedByText = true
composerDraft.value = ''
persistSessionState()
nextTick(() => {
adjustComposerTextareaHeight()
scrollToBottom()
})
await handleSuggestedAction(context.message, context.action)
return true
}
if (nextAction === 'fill_current_slot') {
const context = findPendingSlotSuggestedActionContext(decision)
if (!context) {
return false
}
await handleSuggestedAction(context.message, {
...context.action,
label: String(decision?.field_value || decision?.fieldValue || context.action.label || '').trim(),
suppressUserEcho: userMessageAlreadyAdded
})
return true
}
if (nextAction === 'ask_user' || nextAction === 'cancel_current_action' || nextAction === 'no_op') {
pushStewardRuntimeResponse(rawText, decision, { userMessageAlreadyAdded })
return true
}
return false
}
async function handleStewardRuntimeDecision(options = {}) {
if (!isStewardSession.value || options.skipStewardPlan) {
return false
}
const rawText = String(options.rawText ?? composerDraft.value ?? '').trim()
const files = Array.from(options.files ?? attachedFiles.value ?? [])
if (!rawText || files.length) {
return false
}
const runtimeState = buildStewardRuntimeState()
if (!hasActiveStewardRuntimeDecisionContext(runtimeState)) {
return false
}
const userMessageAlreadyAdded = options.skipUserMessage
? false
: pushStewardRuntimeUserMessage(rawText)
try {
const fastDecision = buildStewardRuntimeFastPathDecision(rawText, runtimeState)
if (fastDecision) {
if (String(fastDecision.next_action || fastDecision.nextAction || '').trim() === 'plan_new_tasks') {
await submitStewardPlan({
...options,
rawText,
userText: rawText,
skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage
})
return true
}
const fastExecuted = await executeStewardRuntimeDecision(fastDecision, rawText, { userMessageAlreadyAdded })
if (fastExecuted) {
return true
}
}
if (!shouldUseStewardRuntimeLlmDecision(rawText, runtimeState)) {
if (userMessageAlreadyAdded) {
pushStewardRuntimeResponse('', {
response_text: '我还需要先确认当前等待项。请回复系统刚刚追问的选项或具体补充内容。'
}, { userMessageAlreadyAdded: true })
return true
}
return false
}
const decision = await fetchStewardRuntimeDecision({
user_message: rawText,
session_type: SESSION_TYPE_STEWARD,
runtime_state: runtimeState,
context_json: {
entry_source: props.entrySource,
user_id: resolveCurrentUserId()
}
}, {
timeoutMs: 45000,
timeoutMessage: '小财管家运行时决策超时,已回到当前上下文兜底处理。'
})
if (String(decision?.next_action || decision?.nextAction || '').trim() === 'plan_new_tasks') {
await submitStewardPlan({
...options,
rawText,
userText: rawText,
skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage
})
return true
}
const executed = await executeStewardRuntimeDecision(decision, rawText, { userMessageAlreadyAdded })
if (executed) {
return true
}
if (userMessageAlreadyAdded) {
await submitStewardPlan({
...options,
rawText,
userText: rawText,
skipUserMessage: true
})
return true
}
return false
} catch (error) {
console.warn('Steward runtime decision failed:', error)
if (userMessageAlreadyAdded) {
await submitStewardPlan({
...options,
rawText,
userText: rawText,
skipUserMessage: true
})
return true
}
return false
}
}
function openApplicationSubmitConfirm(message) {
if (!message) {
return
}
if (message.applicationPreview) {
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
message.applicationPreview = normalizedPreview
message.text = buildLocalApplicationPreviewMessage(normalizedPreview)
if (!normalizedPreview.readyToSubmit) {
const validationIssues = Array.isArray(normalizedPreview.validationIssues)
? normalizedPreview.validationIssues
: []
toast(
validationIssues.length
? validationIssues[0].message
: `请先补充:${normalizedPreview.missingFields.join('、')}`
)
persistSessionState()
return
}
}
applicationSubmitConfirmDialog.value = {
open: true,
message
}
}
function closeApplicationSubmitConfirm() {
if (reviewActionBusy.value) {
return
}
applicationSubmitConfirmDialog.value = {
open: false,
message: null
}
}
function resolveApplicationEditClaimId() {
if (activeSessionType.value !== SESSION_TYPE_APPLICATION) {
return ''
}
const request = linkedRequest.value || {}
if (!request.applicationEditMode) {
return ''
}
return String(request.claimId || request.claim_id || '').trim()
}
async function confirmApplicationSubmit(options = {}) {
const message = applicationSubmitConfirmDialog.value.message
if (!message || submitting.value || reviewActionBusy.value) {
return
}
const applicationPreview = message?.applicationPreview && typeof message.applicationPreview === 'object'
? normalizeApplicationPreview(message.applicationPreview)
: null
const applicationSubmitText = applicationPreview
? buildApplicationPreviewSubmitText(applicationPreview)
: '确认提交'
const applicationEditClaimId = resolveApplicationEditClaimId()
applicationSubmitConfirmDialog.value = {
open: false,
message: null
}
const stewardSubmitContinuation = message?.stewardContinuation || null
reviewActionBusy.value = true
try {
const payload = await submitComposer({
rawText: applicationSubmitText,
userText: String(options.userText || '').trim() || '确认提交',
skipUserMessage: Boolean(options.skipUserMessage),
pendingText: '正在提交费用申请...',
systemGenerated: true,
skipScopeGuard: true,
skipStewardPlan: true,
stewardContinuation: stewardSubmitContinuation,
sessionTypeOverride: SESSION_TYPE_APPLICATION,
feedbackOperationType: 'submit_application',
extraContext: {
application_preview: applicationPreview,
user_input_text: applicationSubmitText,
...(applicationEditClaimId
? {
application_edit_claim_id: applicationEditClaimId,
application_edit_claim_no: String(linkedRequest.value?.claimNo || linkedRequest.value?.id || '').trim(),
application_edit_mode: true,
draft_claim_id: applicationEditClaimId,
selected_claim_id: applicationEditClaimId
}
: {})
}
})
const draftPayload = payload?.result?.draft_payload || {}
const claimNo = String(draftPayload.claim_no || '').trim()
const claimId = String(draftPayload.claim_id || '').trim()
if (String(payload?.status || '').trim() === 'succeeded' && (claimNo || claimId)) {
message.applicationSubmitConfirmed = true
emit('draft-saved', {
claimId,
claimNo,
status: 'submitted',
approvalStage: String(draftPayload.approval_stage || '直属领导审批').trim(),
documentType: 'application'
})
}
const planningText = buildTravelPlanningNudgeMessage(applicationPreview, draftPayload)
const planningActions = buildTravelPlanningSuggestedActions(applicationPreview, draftPayload).map((action) => ({
...action,
payload: {
...(action.payload || {}),
applicationPreview,
draftPayload
}
}))
if (planningText && planningActions.length) {
messages.value.push(createMessage('assistant', planningText, [], {
meta: ['行程规划推荐'],
suggestedActions: planningActions
}))
persistSessionState()
nextTick(scrollToBottom)
}
const stewardFollowup = buildStewardContinuationAfterAction({
createMessage,
message,
completedLabel: '申请单已完成'
})
if (stewardFollowup) {
await pushStewardContinuationMessage({
finalMessage: stewardFollowup,
messages,
nextTick,
persistSessionState,
scrollToBottom
})
}
} finally {
reviewActionBusy.value = false
}
}
return {
closeApplicationSubmitConfirm,
confirmApplicationSubmit,
handleApplicationSubmitConfirmationText,
handleStewardRuntimeDecision,
isApplicationSubmitConfirmationText,
openApplicationSubmitConfirm,
resolveStewardMissingFieldItems
}
}