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 } }