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 行内,阻止主类/主模块继续膨胀。
This commit is contained in:
775
web/src/views/scripts/useTravelReimbursementStewardRuntime.js
Normal file
775
web/src/views/scripts/useTravelReimbursementStewardRuntime.js
Normal file
@@ -0,0 +1,775 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user