231 lines
11 KiB
JavaScript
231 lines
11 KiB
JavaScript
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|