根因:steward plan 的'确认创建申请单'按钮 action_type 是 ASSISTANT_SCOPE_ACTION_SWITCH,handleInlineSuggestedAction 没有匹配分支, 落到 startInlineConversation 重新发起对话,steward_remaining_tasks 完全丢失。 修复:当 payload 有 steward_current_task + session_type=application + task_type=expense_application 时,直接调 startAiApplicationPreviewFromAction (会透传 steward_remaining_tasks 到申请预览 message),不走 startInlineConversation。 这样保存草稿成功后,targetMessage 上有 stewardRemainingTasks, buildApplicationPreviewNextTaskAction 能生成'继续处理费用报销'按钮。
422 lines
17 KiB
JavaScript
422 lines
17 KiB
JavaScript
import {
|
|
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
|
AI_APPLICATION_ACTION_SUBMIT
|
|
} from '../../services/aiApplicationPreviewActions.js'
|
|
import { executeStewardAction } from '../../services/steward.js'
|
|
import { buildAiDocumentDetailRequest } from '../../utils/aiDocumentDetailReference.js'
|
|
import {
|
|
mergeComposerPrefill,
|
|
resolveSuggestedActionPrefill
|
|
} from '../../utils/assistantSuggestedActionPrefill.js'
|
|
import {
|
|
AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION,
|
|
AI_ATTACHMENT_OCR_DETAIL_ACTION
|
|
} from './workbenchAiMessageModel.js'
|
|
import { SESSION_TYPE_EXPENSE } from './useWorkbenchAiExpenseFlow.js'
|
|
import {
|
|
CANCEL_STANDALONE_REIMBURSEMENT_DRAFT_ACTION,
|
|
CONTINUE_REIMBURSEMENT_DRAFT_ACTION,
|
|
CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION,
|
|
SKIP_REQUIRED_APPLICATION_LINK_ACTION,
|
|
SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION
|
|
} from '../../views/scripts/travelReimbursementAssociationGateModel.js'
|
|
|
|
export function useWorkbenchAiActionRouter({
|
|
aiExpenseDraft,
|
|
applicationFlow,
|
|
assistantDraft,
|
|
attachmentFlow,
|
|
conversationMessages,
|
|
createInlineMessage,
|
|
emit,
|
|
expenseFlow,
|
|
focusAiModeInput,
|
|
hasInlineAttachmentOcrDetails,
|
|
persistCurrentConversation,
|
|
replaceInlineMessage,
|
|
resolveLatestInlineUserPrompt,
|
|
scrollInlineConversationToBottom,
|
|
selectedFiles,
|
|
startInlineConversation,
|
|
toast,
|
|
toggleInlineAttachmentOcrDetails
|
|
}) {
|
|
function handleInlineSuggestedAction(action = {}, sourceMessage = null) {
|
|
const prefillText = resolveSuggestedActionPrefill(action)
|
|
if (prefillText) {
|
|
assistantDraft.value = mergeComposerPrefill(assistantDraft.value, prefillText)
|
|
focusAiModeInput()
|
|
return
|
|
}
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
if (actionPayload.steward_execute_action) {
|
|
return executeInlineStewardAction(action, sourceMessage)
|
|
}
|
|
if (actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION) {
|
|
if (!hasInlineAttachmentOcrDetails(sourceMessage)) {
|
|
toast('当前消息没有可查看的附件识别明细。')
|
|
return
|
|
}
|
|
toggleInlineAttachmentOcrDetails(sourceMessage, true)
|
|
return
|
|
}
|
|
if (actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION) {
|
|
if (applicationFlow.isInlineSuggestedActionDisabled(action, sourceMessage)) {
|
|
return
|
|
}
|
|
void attachmentFlow.confirmAiAttachmentAssociation(actionPayload, sourceMessage)
|
|
return
|
|
}
|
|
if ([AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT].includes(actionType)) {
|
|
if (applicationFlow.isInlineSuggestedActionDisabled(action, sourceMessage)) {
|
|
toast('请等待费用测算完成后再继续操作。')
|
|
return
|
|
}
|
|
void applicationFlow.executeInlineApplicationPreviewAction(actionType, sourceMessage, {
|
|
userText: action.label,
|
|
draftPayload: actionPayload.draftPayload || actionPayload.draft_payload || null
|
|
})
|
|
return
|
|
}
|
|
if (actionType === 'ai_application_confirm_intent') {
|
|
aiExpenseDraft.value = null
|
|
void applicationFlow.startAiApplicationPreview('travel', '差旅费', String(actionPayload.sourceText || '').trim(), {
|
|
userMessage: String(actionPayload.sourceText || '').trim() || '确认发起出差申请',
|
|
pushUserMessage: true,
|
|
ontologyFields: actionPayload.ontologyFields || {},
|
|
autoSubmit: Boolean(actionPayload.autoSubmit),
|
|
autoSaveDraft: Boolean(actionPayload.autoSaveDraft),
|
|
requestedSubmit: Boolean(actionPayload.requestedSubmit),
|
|
submitRequiresConfirmation: Boolean(actionPayload.submitRequiresConfirmation)
|
|
})
|
|
return
|
|
}
|
|
if (actionType === 'open_application_detail') {
|
|
const claimNo = String(actionPayload.claim_no || actionPayload.claimNo || '').trim()
|
|
const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim()
|
|
emit('open-document', buildAiDocumentDetailRequest({
|
|
reference: claimNo || claimId,
|
|
claimId,
|
|
claimNo
|
|
}))
|
|
return
|
|
}
|
|
if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_reimbursement') {
|
|
const expenseType = String(actionPayload.expense_type || 'travel').trim() || 'travel'
|
|
const expenseTypeLabel = String(actionPayload.expense_type_label || '差旅费').trim() || '差旅费'
|
|
expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, true)
|
|
return
|
|
}
|
|
if (actionPayload.steward_confirm_flow && actionPayload.flow_id === 'travel_application') {
|
|
aiExpenseDraft.value = null
|
|
void expenseFlow.startAiApplicationPreviewFromAction(actionPayload)
|
|
return
|
|
}
|
|
// steward plan 的"确认创建申请单"按钮:payload 有 steward_current_task + session_type=application,
|
|
// 直接拉起申请预览(带 remaining tasks),不走 startInlineConversation(会丢失 steward 上下文)
|
|
if (
|
|
actionPayload.steward_current_task
|
|
&& String(actionPayload.session_type || '').trim() === 'application'
|
|
&& String(actionPayload.steward_current_task.task_type || '').trim() === 'expense_application'
|
|
) {
|
|
aiExpenseDraft.value = null
|
|
void expenseFlow.startAiApplicationPreviewFromAction(actionPayload)
|
|
return
|
|
}
|
|
if (actionType === 'select_expense_type') {
|
|
const expenseType = String(action?.payload?.expense_type || '').trim()
|
|
const expenseTypeLabel = String(action?.payload?.expense_type_label || action?.label || '').trim()
|
|
const requiresApplicationBeforeReimbursement = Boolean(action?.payload?.requires_application_before_reimbursement)
|
|
expenseFlow.startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement)
|
|
return
|
|
}
|
|
|
|
if (actionType === 'select_required_application') {
|
|
expenseFlow.linkAiExpenseApplication(action?.payload || {})
|
|
return
|
|
}
|
|
|
|
if (actionType === CONTINUE_REIMBURSEMENT_DRAFT_ACTION) {
|
|
expenseFlow.promptAiReimbursementDraftContinuation(actionPayload)
|
|
focusAiModeInput()
|
|
return
|
|
}
|
|
|
|
if (actionType === CREATE_STANDALONE_REIMBURSEMENT_DRAFT_ACTION) {
|
|
expenseFlow.promptStandaloneReimbursementDraftCreation(
|
|
actionPayload.original_message || '我要报销',
|
|
action.label || '独立新建报销单'
|
|
)
|
|
return
|
|
}
|
|
|
|
if (actionType === CANCEL_STANDALONE_REIMBURSEMENT_DRAFT_ACTION) {
|
|
expenseFlow.cancelStandaloneReimbursementDraftCreation()
|
|
return
|
|
}
|
|
|
|
if (actionType === SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION) {
|
|
void expenseFlow.startAiReimbursementAssociationGate(
|
|
actionPayload.original_message || '我要报销',
|
|
action.label || '不用草稿,关联申请单新建报销单',
|
|
{ skipDraftCheck: true }
|
|
)
|
|
return
|
|
}
|
|
|
|
if (actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION) {
|
|
expenseFlow.pushInlineExpenseSceneSelectionPrompt(
|
|
actionPayload.original_message || '我要报销',
|
|
action.label || '单独新建报销单'
|
|
)
|
|
return
|
|
}
|
|
|
|
if (actionType === 'ai_application_start_inline') {
|
|
aiExpenseDraft.value = null
|
|
void expenseFlow.startAiApplicationPreviewFromAction(action?.payload || {}, action?.label)
|
|
return
|
|
}
|
|
|
|
const carryText = String(action?.payload?.carry_text || action?.label || '').trim()
|
|
if (!carryText) {
|
|
return
|
|
}
|
|
if (String(action?.payload?.session_type || '').trim() === SESSION_TYPE_EXPENSE && carryText === '我要报销') {
|
|
void expenseFlow.startAiReimbursementAssociationGate(carryText, action.label)
|
|
return
|
|
}
|
|
startInlineConversation(carryText, {
|
|
label: action.label,
|
|
source: 'steward-action',
|
|
sessionType: action?.payload?.session_type || 'steward'
|
|
}, action?.payload?.carry_files ? Array.from(selectedFiles.value) : [])
|
|
}
|
|
|
|
async function executeInlineStewardAction(action = {}, sourceMessage = null) {
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
const actionType = String(actionPayload.steward_action_type || '').trim()
|
|
if (!actionType || !actionPayload.steward_current_task) {
|
|
toast('当前动作缺少可执行任务快照,请重新发起规划。')
|
|
return false
|
|
}
|
|
if (sourceMessage?.suggestedActionsLocked) {
|
|
return true
|
|
}
|
|
if (sourceMessage) {
|
|
sourceMessage.suggestedActionsLocked = true
|
|
}
|
|
|
|
const pendingMessage = appendStewardActionMessage(
|
|
`正在执行:${String(action.label || '小财管家动作').trim() || '小财管家动作'}...`,
|
|
{ pending: true }
|
|
)
|
|
|
|
try {
|
|
let precheckResult = null
|
|
if (actionType === 'submit_application') {
|
|
precheckResult = await executeStewardAction(
|
|
buildStewardActionExecutePayload(action, 'run_duplicate_precheck')
|
|
)
|
|
if (!isSuccessfulStewardPrecheck(precheckResult)) {
|
|
finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(precheckResult), {
|
|
suggestedActions: []
|
|
})
|
|
return true
|
|
}
|
|
}
|
|
|
|
const contextJson = precheckResult
|
|
? { precheck_result: precheckResult.result_payload || precheckResult.resultPayload || {} }
|
|
: {}
|
|
const result = await executeStewardAction(
|
|
buildStewardActionExecutePayload(action, actionType, contextJson)
|
|
)
|
|
const resultActions = buildStewardActionResultActions(result)
|
|
const nextTaskAction = buildNextTaskSuggestedAction(actionPayload)
|
|
finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(result), {
|
|
suggestedActions: nextTaskAction ? [...resultActions, nextTaskAction] : resultActions
|
|
})
|
|
return true
|
|
} catch (error) {
|
|
if (sourceMessage) {
|
|
sourceMessage.suggestedActionsLocked = false
|
|
}
|
|
finalizeStewardActionMessage(
|
|
pendingMessage,
|
|
`动作执行失败:${String(error?.message || '请稍后重试。').trim()}`
|
|
)
|
|
return false
|
|
}
|
|
}
|
|
|
|
function buildStewardActionExecutePayload(action = {}, actionType = '', contextJson = {}) {
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
const currentTask = actionPayload.steward_current_task || null
|
|
return {
|
|
action_type: actionType,
|
|
message: resolveStewardActionMessage(action),
|
|
plan_id: String(actionPayload.steward_plan_id || '').trim(),
|
|
conversation_id: String(actionPayload.conversation_id || actionPayload.conversationId || '').trim() || null,
|
|
task: currentTask,
|
|
action_step: resolveStewardActionStep(actionPayload, actionType),
|
|
confirmed: true,
|
|
context_json: contextJson,
|
|
client_trace_id: buildStewardActionClientTraceId(actionPayload, actionType)
|
|
}
|
|
}
|
|
|
|
function resolveStewardActionMessage(action = {}) {
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
return String(
|
|
actionPayload.carry_text ||
|
|
actionPayload.original_message ||
|
|
resolveLatestInlineUserPrompt?.() ||
|
|
action.label ||
|
|
''
|
|
).trim()
|
|
}
|
|
|
|
function resolveStewardActionStep(actionPayload = {}, actionType = '') {
|
|
if (String(actionPayload.steward_action_step?.action_type || '').trim() === actionType) {
|
|
return actionPayload.steward_action_step
|
|
}
|
|
const steps = Array.isArray(actionPayload.steward_current_task?.action_steps)
|
|
? actionPayload.steward_current_task.action_steps
|
|
: []
|
|
return steps.find((step) => String(step?.action_type || '').trim() === actionType) || null
|
|
}
|
|
|
|
function buildStewardActionClientTraceId(actionPayload = {}, actionType = '') {
|
|
const planId = String(actionPayload.steward_plan_id || 'steward').trim() || 'steward'
|
|
const taskId = String(actionPayload.steward_action_task_id || actionPayload.steward_current_task?.task_id || 'task').trim() || 'task'
|
|
return `${planId}:${taskId}:${actionType}:${Date.now()}`
|
|
}
|
|
|
|
function isSuccessfulStewardPrecheck(result = {}) {
|
|
const status = String(result?.status || '').trim()
|
|
const payload = result?.result_payload || result?.resultPayload || {}
|
|
return status === 'succeeded' && payload?.blocking !== true
|
|
}
|
|
|
|
function appendStewardActionMessage(content, options = {}) {
|
|
if (!conversationMessages?.value || typeof createInlineMessage !== 'function') {
|
|
toast(String(content || '').trim())
|
|
return null
|
|
}
|
|
const message = createInlineMessage('assistant', content, options)
|
|
conversationMessages.value.push(message)
|
|
persistStewardActionConversation()
|
|
return message
|
|
}
|
|
|
|
function finalizeStewardActionMessage(pendingMessage, content, options = {}) {
|
|
if (!conversationMessages?.value || typeof createInlineMessage !== 'function') {
|
|
toast(String(content || '').trim())
|
|
return
|
|
}
|
|
const finalMessage = createInlineMessage('assistant', content, {
|
|
...options,
|
|
id: pendingMessage?.id
|
|
})
|
|
if (pendingMessage?.id && typeof replaceInlineMessage === 'function') {
|
|
replaceInlineMessage(pendingMessage.id, finalMessage)
|
|
} else if (pendingMessage?.id) {
|
|
const index = conversationMessages.value.findIndex((item) => item.id === pendingMessage.id)
|
|
if (index >= 0) {
|
|
conversationMessages.value.splice(index, 1, finalMessage)
|
|
} else {
|
|
conversationMessages.value.push(finalMessage)
|
|
}
|
|
} else {
|
|
conversationMessages.value.push(finalMessage)
|
|
}
|
|
persistStewardActionConversation()
|
|
}
|
|
|
|
function persistStewardActionConversation() {
|
|
persistCurrentConversation?.()
|
|
scrollInlineConversationToBottom?.({ force: true })
|
|
}
|
|
|
|
function buildStewardActionResultText(result = {}) {
|
|
const status = String(result?.status || '').trim()
|
|
const message = String(result?.message || '').trim()
|
|
if (status === 'succeeded') {
|
|
return message || '动作已完成。'
|
|
}
|
|
if (status === 'needs_confirmation') {
|
|
return message || '这一步还需要您确认后才能继续。'
|
|
}
|
|
if (status === 'blocked') {
|
|
return ['这一步暂时不能继续。', message].filter(Boolean).join('\n\n')
|
|
}
|
|
if (status === 'failed') {
|
|
return `动作执行失败:${message || '请稍后重试。'}`
|
|
}
|
|
return message || '动作已返回结果。'
|
|
}
|
|
|
|
function buildStewardActionResultActions(result = {}) {
|
|
if (String(result?.status || '').trim() !== 'succeeded') {
|
|
return []
|
|
}
|
|
const resultPayload = result?.result_payload || result?.resultPayload || {}
|
|
const draftPayload = resultPayload?.draft_payload || resultPayload?.draftPayload || resultPayload
|
|
const claimNo = String(draftPayload?.claim_no || draftPayload?.claimNo || '').trim()
|
|
const claimId = String(draftPayload?.claim_id || draftPayload?.claimId || '').trim()
|
|
if (!claimNo && !claimId) {
|
|
return []
|
|
}
|
|
return [{
|
|
label: claimNo ? `查看单据 ${claimNo}` : '查看单据',
|
|
description: '打开刚生成的单据详情。',
|
|
icon: 'mdi mdi-open-in-new',
|
|
action_type: 'open_application_detail',
|
|
payload: {
|
|
claim_id: claimId,
|
|
claim_no: claimNo
|
|
}
|
|
}]
|
|
}
|
|
|
|
function buildNextTaskSuggestedAction(actionPayload = {}) {
|
|
// 多 task 串行推进:task1 完成后,从剩余 task 列表取下一个,生成推进按钮。
|
|
// 用户点击推进按钮后,handleInlineSuggestedAction 的 steward_confirm_flow 分支
|
|
// 会自动拉起下一个 task 的申请预览/报销流程,实现"先做完 A 再做 B"。
|
|
const remainingTasks = Array.isArray(actionPayload.steward_remaining_tasks)
|
|
? actionPayload.steward_remaining_tasks
|
|
: []
|
|
const nextTask = remainingTasks[0]
|
|
if (!nextTask || !nextTask.task_type) {
|
|
return null
|
|
}
|
|
const taskType = String(nextTask.task_type || '').trim()
|
|
const isApplication = taskType === 'expense_application'
|
|
const flowId = isApplication ? 'travel_application' : 'travel_reimbursement'
|
|
const taskLabel = isApplication ? '出差申请' : '费用报销'
|
|
const ontologyFields = nextTask.ontology_fields || nextTask.ontologyFields || {}
|
|
return {
|
|
label: `继续处理${taskLabel}`,
|
|
description: `接下来处理${taskLabel}: ${String(nextTask.summary || nextTask.title || '').slice(0, 40)}`,
|
|
icon: isApplication ? 'mdi mdi-file-plus-outline' : 'mdi mdi-receipt-text-plus-outline',
|
|
action_type: 'steward_continue_next_task',
|
|
payload: {
|
|
steward_confirm_flow: true,
|
|
flow_id: flowId,
|
|
steward_current_task: nextTask,
|
|
expense_type: String(ontologyFields.expense_type || 'travel').trim() || 'travel',
|
|
expense_type_label: String(ontologyFields.expense_type_label || '差旅费').trim() || '差旅费',
|
|
ontology_fields: ontologyFields,
|
|
original_message: String(nextTask.summary || nextTask.title || `继续处理${taskLabel}`).trim()
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
handleInlineSuggestedAction
|
|
}
|
|
}
|