Files
X-Financial/web/src/composables/workbenchAiMode/useWorkbenchAiActionRouter.js

376 lines
14 KiB
JavaScript
Raw Normal View History

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
}
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)
)
finalizeStewardActionMessage(pendingMessage, buildStewardActionResultText(result), {
suggestedActions: buildStewardActionResultActions(result)
})
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
}
}]
}
return {
handleInlineSuggestedAction
}
}