完整修改内容: - 拆分 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 行内,阻止主类/主模块继续膨胀。
776 lines
28 KiB
JavaScript
776 lines
28 KiB
JavaScript
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
|
|
}
|
|
}
|