Files
X-Financial/web/src/views/scripts/travelReimbursementStewardFollowupFlow.js
2026-06-22 11:58:53 +08:00

231 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ASSISTANT_SCOPE_ACTION_SWITCH } from '../../utils/assistantSessionScope.js'
import { STEWARD_ASSISTANT_NAME } from './useTravelReimbursementStewardRuntime.js'
import { SESSION_TYPE_APPLICATION, SESSION_TYPE_EXPENSE } from './travelReimbursementConversationModel.js'
import { resolveStewardTypewriterNextIndex } from './stewardTypewriter.js'
const STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS = 10
const STEWARD_FOLLOWUP_THINKING_INTERVAL_MS = 8
const STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE = 4
const STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE = 5
export function useTravelReimbursementStewardFollowupFlow({
buildStewardFieldItems,
createMessage,
formatStewardMissingFieldList,
formatStewardOntologyFields,
messages,
nextTick,
persistSessionState,
scrollToBottom
}) {
function buildStewardContinuationAfterAction(message, completedLabel = '当前动作已完成') {
const continuation = message?.stewardContinuation || null
const remainingTasks = Array.isArray(continuation?.remainingTasks) ? continuation.remainingTasks : []
if (!remainingTasks.length) return null
const nextTask = remainingTasks[0]
const nextTaskType = String(nextTask.task_type || nextTask.taskType || '').trim()
const targetSessionType = nextTaskType === 'expense_application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE
const nextLabel = targetSessionType === SESSION_TYPE_APPLICATION ? '继续创建申请单' : '继续填写报销单'
const restTasks = remainingTasks.slice(1)
return createMessage(
'assistant',
[
`**${completedLabel}。**`,
'',
'我会重新检查剩余任务队列。',
`下一步:${nextTask.title || (targetSessionType === SESSION_TYPE_APPLICATION ? '费用申请' : '费用报销')}`,
'请回复“确定”,我再继续执行。'
].join('\n'),
[],
{
assistantName: STEWARD_ASSISTANT_NAME,
meta: [STEWARD_ASSISTANT_NAME, '等待用户确认'],
suggestedActions: [
{
label: nextLabel,
description: '确认后小财管家继续调用对应助手完成下一步。',
icon: targetSessionType === SESSION_TYPE_APPLICATION ? 'mdi mdi-file-plus-outline' : 'mdi mdi-receipt-text-plus-outline',
action_type: ASSISTANT_SCOPE_ACTION_SWITCH,
payload: {
session_type: targetSessionType,
carry_text: buildStewardContinuationCarryText(nextTask, restTasks),
carry_files: targetSessionType !== SESSION_TYPE_APPLICATION,
auto_submit: true,
steward_plan_id: String(continuation.planId || '').trim() || 'steward_continuation',
steward_next_task_id: String(nextTask.task_id || nextTask.taskId || '').trim(),
steward_current_task: nextTask,
steward_remaining_tasks: restTasks
}
}
]
}
)
}
function buildStewardFollowupPlan(thinkingEvents = [], streamStatus = 'streaming', planId = '') {
return {
planId: planId || `steward-followup-${Date.now()}`,
planStatus: 'delegating',
summary: '',
visibleThinkingEventCount: Number.MAX_SAFE_INTEGER,
initialSummaryOnly: true,
thinkingEvents,
tasks: [],
attachmentGroups: [],
confirmationGroups: [],
streamStatus
}
}
function extractStewardCarryLine(text = '', label = '') {
const escapedLabel = String(label || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const match = String(text || '').match(new RegExp(`(?:^|\\n)${escapedLabel}[:]([^\\n]+)`, 'u'))
return match ? match[1].trim() : ''
}
function extractStewardFollowupNextTitle(text = '') {
const taskMatch = String(text || '').match(/请(?:先)?(?:创建申请单|填写报销单|继续填写报销单)[:]([^。\n]+)/u)
if (taskMatch?.[1]) return taskMatch[1].trim()
const nextMatch = String(text || '').match(/下一步[:]([^。\n]+)/u)
return nextMatch?.[1]?.trim() || '下一项财务任务'
}
function buildStewardFollowupThinkingEvents(finalMessage = null, actions = []) {
const eventPrefix = `steward-followup-${Date.now()}-${Math.random().toString(16).slice(2)}`
const firstAction = Array.isArray(actions) ? actions[0] : null
const actionPayload = firstAction?.payload && typeof firstAction.payload === 'object' ? firstAction.payload : {}
const carryText = String(actionPayload.carry_text || '').trim()
const finalText = String(finalMessage?.text || '').trim()
const nextTitle = extractStewardFollowupNextTitle(carryText || finalText)
const nextSummary = extractStewardCarryLine(carryText, '任务摘要')
const nextMissing = extractStewardCarryLine(carryText, '还需要补充')
return [
{
eventId: `${eventPrefix}-review`,
title: '复盘结果',
content: finalText.includes('申请单已完成')
? '申请单已经完成,我把当前出差申请标记为已处理,不会重复创建。'
: '当前动作已经完成,我会把已完成事项从任务队列中移除。'
},
{
eventId: `${eventPrefix}-next`,
title: '读取剩余任务',
content: nextSummary ? `剩余队列里的下一项是“${nextTitle}”:${nextSummary}` : `剩余队列里的下一项是“${nextTitle}”。`
},
{
eventId: `${eventPrefix}-gate`,
title: '判断下一步条件',
content: nextMissing
? `这一步还需要补充${nextMissing},进入对应核对环节后我会继续追问,不会直接提交。`
: '我会先等你确认,再进入下一项核对;创建草稿、绑定附件或提交前仍会再次确认。'
}
]
}
function waitStewardFollowupTick(intervalMs) {
return new Promise((resolve) => {
window.setTimeout(resolve, intervalMs)
})
}
async function pushStewardContinuationMessage(finalMessage) {
if (!finalMessage) return
const finalText = String(finalMessage.text || '')
const followupPlanId = `steward-followup-${Date.now()}-${Math.random().toString(16).slice(2)}`
const finalActions = Array.isArray(finalMessage.suggestedActions) ? finalMessage.suggestedActions : []
finalMessage.text = ''
finalMessage.assistantName = STEWARD_ASSISTANT_NAME
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '思考中']
finalMessage.suggestedActions = []
finalMessage.stewardPlan = buildStewardFollowupPlan([], 'streaming', followupPlanId)
messages.value.push(finalMessage)
persistSessionState()
nextTick(scrollToBottom)
const typedEvents = []
for (const eventData of buildStewardFollowupThinkingEvents(finalMessage, finalActions)) {
const event = {
eventId: eventData.eventId,
stage: 'steward_followup',
title: eventData.title,
content: '',
status: 'running'
}
typedEvents.push(event)
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
persistSessionState()
nextTick(scrollToBottom)
const chars = Array.from(eventData.content)
for (let index = 0; index < chars.length;) {
await waitStewardFollowupTick(STEWARD_FOLLOWUP_THINKING_INTERVAL_MS)
index = resolveStewardTypewriterNextIndex(chars, index, STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE)
event.content = chars.slice(0, index).join('')
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
if (index % STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE === 0 || index === chars.length) nextTick(scrollToBottom)
}
event.content = eventData.content
event.status = 'completed'
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
persistSessionState()
}
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '输出中']
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'typing', followupPlanId)
const chars = Array.from(finalText)
for (let index = 0; index < chars.length;) {
await waitStewardFollowupTick(STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS)
index = resolveStewardTypewriterNextIndex(chars, index, STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE)
finalMessage.text = chars.slice(0, index).join('')
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '输出中']
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'typing', followupPlanId)
if (index % STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE === 0 || index === chars.length) nextTick(scrollToBottom)
}
finalMessage.text = finalText
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '等待用户确认']
finalMessage.suggestedActions = finalActions
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'completed', followupPlanId)
persistSessionState()
nextTick(scrollToBottom)
}
function buildStewardContinuationCarryText(task, restTasks = []) {
const taskType = String(task?.task_type || task?.taskType || '').trim()
const fields = formatStewardOntologyFields(task?.ontology_fields || task?.ontologyFields || {}, taskType)
const missingFields = formatStewardMissingFieldList(task?.missing_fields || task?.missingFields || [], taskType, { includeHints: false })
const lines = [
taskType === 'expense_application'
? `小财管家继续执行剩余任务,请创建申请单:${task.title || '费用申请'}`
: `小财管家继续执行剩余任务,请填写报销单:${task.title || '费用报销'}`,
task.summary ? `任务摘要:${task.summary}` : '',
fields ? `已识别信息:${fields}` : '',
missingFields ? `还需要补充:${missingFields}` : '',
missingFields ? '请先追问上述缺失信息,不要直接生成核对结果,也不要替用户默认填写。' : '请生成核对结果;创建草稿、绑定附件或提交审批前仍需让我确认。'
]
if (restTasks.length) {
lines.push('当前步骤完成后,请继续引导我处理剩余任务:')
restTasks.forEach((item, index) => {
lines.push(`${index + 1}. ${item.title || item.task_type || item.taskType}`)
})
}
return lines.filter(Boolean).join('\n')
}
function resolveStewardMissingFieldItems(task) {
if (Array.isArray(task?.missingFieldItems) && task.missingFieldItems.length) return task.missingFieldItems
const fields = task?.missingFields || task?.missing_fields || []
const taskType = String(task?.taskType || task?.task_type || '').trim()
return buildStewardFieldItems(fields, taskType)
}
return {
buildStewardContinuationAfterAction,
buildStewardContinuationCarryText,
pushStewardContinuationMessage,
resolveStewardMissingFieldItems
}
}