import { ASSISTANT_SCOPE_ACTION_SWITCH, ASSISTANT_SCOPE_ACTION_FILL_COMPOSER } from '../../utils/assistantSessionScope.js' import { SESSION_TYPE_APPLICATION, SESSION_TYPE_EXPENSE } from './travelReimbursementConversationModel.js' import { APPLICATION_NON_BLOCKING_MISSING_FIELDS, FLOW_EXPENSE_TYPE_LABELS, formatStewardFieldDisplayValue, normalizeFieldKey, resolveFieldDisplay } from './stewardPlanFields.js' const TASK_TYPE_LABELS = { expense_application: '费用申请', reimbursement: '费用报销' } const AGENT_LABELS = { application_assistant: '申请助手', application: '申请助手', expense_application: '申请助手', reimbursement_assistant: '报销助手', reimbursement: '报销助手', expense: '报销助手' } const EXECUTABLE_STEWARD_ACTION_TYPES = new Set([ 'save_application_draft', 'submit_application', 'create_reimbursement_draft', 'associate_attachments' ]) export function buildStewardPlanRequest({ rawText = '', files = [], currentUser = {}, conversationId = '', stewardState = null } = {}) { const safeFiles = Array.isArray(files) ? files : [] const normalizedConversationId = String(conversationId || '').trim() return { message: String(rawText || '').trim(), user_id: String(currentUser.username || currentUser.name || 'anonymous').trim() || 'anonymous', client_now_iso: new Date().toISOString(), attachments: safeFiles.map((file) => ({ name: String(file?.name || '').trim(), media_type: String(file?.type || '').trim() })).filter((item) => item.name), context_json: { entry_source: 'workbench', session_type: 'steward', conversation_id: normalizedConversationId, steward_state: stewardState && typeof stewardState === 'object' ? stewardState : null, role_codes: Array.isArray(currentUser.roleCodes) ? currentUser.roleCodes : [], username: currentUser.username || '', name: currentUser.name || currentUser.username || '', department_name: currentUser.departmentName || currentUser.department || '', employee_grade: currentUser.grade || '' } } } export function normalizeStewardPlan(rawPlan = {}, options = {}) { const visibleThinkingEventCount = Number.isFinite(options.visibleThinkingEventCount) ? Number(options.visibleThinkingEventCount) : Number(rawPlan.visibleThinkingEventCount || rawPlan.visible_thinking_event_count || 0) const pendingFlowConfirmation = normalizePendingFlowConfirmation(rawPlan) return { planId: String(rawPlan.plan_id || rawPlan.planId || ''), planStatus: String(rawPlan.plan_status || rawPlan.planStatus || ''), nextAction: String(rawPlan.next_action || rawPlan.nextAction || ''), conversationId: String(rawPlan.conversation_id || rawPlan.conversationId || ''), stewardState: rawPlan.steward_state || rawPlan.stewardState || null, summary: String(rawPlan.summary || ''), visibleThinkingEventCount, initialSummaryOnly: Boolean(rawPlan.initial_summary_only || rawPlan.initialSummaryOnly || options.initialSummaryOnly), thinkingEvents: Array.isArray(rawPlan.thinking_events) ? rawPlan.thinking_events.map((item) => ({ eventId: String(item.event_id || item.eventId || ''), stage: String(item.stage || ''), title: String(item.title || ''), content: String(item.content || ''), status: String(item.status || 'completed') })) : [], tasks: Array.isArray(rawPlan.tasks) ? rawPlan.tasks.map((item) => { const taskType = String(item.task_type || item.taskType || '') const rawMissingFields = Array.isArray(item.missing_fields || item.missingFields) ? item.missing_fields || item.missingFields : [] const missingFields = filterStewardBlockingMissingFields(rawMissingFields, taskType) return { taskId: String(item.task_id || item.taskId || ''), taskType, taskTypeLabel: TASK_TYPE_LABELS[taskType] || '财务任务', assignedAgent: String(item.assigned_agent || item.assignedAgent || ''), assignedAgentLabel: AGENT_LABELS[String(item.assigned_agent || item.assignedAgent || '')] || AGENT_LABELS[taskType] || '小财管家', title: String(item.title || ''), summary: String(item.summary || ''), status: String(item.status || ''), confidence: Number(item.confidence || 0), requestedAction: String(item.requested_action || item.requestedAction || ''), ontologyFields: item.ontology_fields || item.ontologyFields || {}, missingFields, missingFieldItems: buildStewardFieldItems(missingFields, taskType), confirmationRequired: item.confirmation_required ?? item.confirmationRequired ?? true, actionSteps: normalizeStewardActionSteps(item.action_steps || item.actionSteps) } }) : [], attachmentGroups: Array.isArray(rawPlan.attachment_groups) ? rawPlan.attachment_groups.map((item) => ({ groupId: String(item.group_id || item.groupId || ''), targetTaskId: String(item.target_task_id || item.targetTaskId || ''), scene: String(item.scene || ''), sceneLabel: String(item.scene_label || item.sceneLabel || ''), attachmentNames: Array.isArray(item.attachment_names || item.attachmentNames) ? item.attachment_names || item.attachmentNames : [], excludedAttachmentNames: Array.isArray(item.excluded_attachment_names || item.excludedAttachmentNames) ? item.excluded_attachment_names || item.excludedAttachmentNames : [], confidence: Number(item.confidence || 0), rationale: String(item.rationale || ''), confirmationRequired: item.confirmation_required ?? item.confirmationRequired ?? true })) : [], confirmationGroups: Array.isArray(rawPlan.confirmation_groups) ? rawPlan.confirmation_groups : [], pendingFlowConfirmation, candidateFlows: pendingFlowConfirmation.candidateFlows, suggestedPrompts: Array.isArray(rawPlan.suggested_prompts) ? rawPlan.suggested_prompts.map((item) => String(item || '').trim()).filter(Boolean) : [] } } function normalizeStewardActionSteps(rawSteps = []) { if (!Array.isArray(rawSteps)) { return [] } return rawSteps .map((step) => ({ step_id: String(step?.step_id || step?.stepId || ''), action_type: String(step?.action_type || step?.actionType || ''), label: String(step?.label || ''), target_task_id: String(step?.target_task_id || step?.targetTaskId || ''), status: String(step?.status || 'planned'), requires_confirmation: Boolean(step?.requires_confirmation ?? step?.requiresConfirmation), depends_on: Array.isArray(step?.depends_on || step?.dependsOn) ? step.depends_on || step.dependsOn : [], payload: step?.payload && typeof step.payload === 'object' ? step.payload : {} })) .filter((step) => step.step_id && step.action_type) } export function buildStewardPlanMessageText(plan) { const normalized = normalizeStewardPlan(plan) if (isOffTopicPlan(normalized)) { return buildOffTopicMessageText(normalized) } if (isPendingFlowConfirmationPlan(normalized)) { return buildPendingFlowConfirmationMessageText(normalized) } const genericReimbursementTask = normalized.tasks.find((task) => isGenericReimbursementTask(task)) if (genericReimbursementTask && normalized.tasks.length === 1) { return buildGenericReimbursementIntentMessageText(genericReimbursementTask) } const nextContext = resolveNextActionContext(normalized) const orderedTasks = buildOrderedStewardTasks(normalized, nextContext?.task) const taskLines = orderedTasks.map((task, index) => `${index + 1}. **${buildTaskOrderVerb(index)}${buildTaskOrderTarget(task)}**\n - ${buildTaskOrderActionDescription(task)}` ) return [ '### 我先帮您把步骤理清楚', '', buildStewardPlanFriendlyIntro(normalized), '', ...taskLines, '', '您看这个顺序是否合适?如果没问题,回复 **确定** 即可。我会先带您进入第一步,需要补充的信息会在具体步骤里再温和提醒。' ].filter((line, index, lines) => line || lines[index - 1]).join('\n') } export function buildStewardFieldItems(fields = [], taskType = '') { const safeFields = filterStewardBlockingMissingFields(fields, taskType) const seen = new Set() return safeFields .map((field) => normalizeFieldKey(field)) .filter((field) => { if (!field || seen.has(field)) { return false } seen.add(field) return true }) .map((field) => resolveFieldDisplay(field, taskType)) } export function formatStewardMissingFieldList(fields = [], taskType = '', options = {}) { const includeHints = options.includeHints !== false return buildStewardFieldItems(fields, taskType) .map((item) => includeHints && item.hint ? `${item.label}(${item.hint})` : item.label) .join('、') } export function filterStewardBlockingMissingFields(fields = [], taskType = '') { const safeFields = Array.isArray(fields) ? fields : [] const seen = new Set() if (taskType !== 'expense_application') { return safeFields .map((field) => normalizeFieldKey(field)) .filter((field) => { if (!field || seen.has(field)) { return false } seen.add(field) return true }) } return safeFields .map((field) => normalizeFieldKey(field)) .filter((field) => { if (!field || seen.has(field) || APPLICATION_NON_BLOCKING_MISSING_FIELDS.has(field)) { return false } seen.add(field) return true }) } export function formatStewardOntologyFields(fields = {}, taskType = '') { return Object.entries(fields || {}) .filter(([, value]) => String(value || '').trim()) .map(([key, value]) => { const field = resolveFieldDisplay(key, taskType) return `${field.label}:${formatStewardFieldDisplayValue(field.key, value)}` }) .join(';') } function buildStewardOntologyFieldRows(fields = {}, taskType = '') { return Object.entries(fields || {}) .filter(([, value]) => String(value || '').trim()) .map(([key, value]) => { const field = resolveFieldDisplay(key, taskType) return { label: field.label, value: formatStewardFieldDisplayValue(field.key, value) } }) } function escapeMarkdownTableCell(value) { return String(value || '').replace(/\|/g, '\\|').replace(/\n+/g, ' ').trim() } function formatStewardOntologyFieldsTable(fields = {}, taskType = '') { const rows = buildStewardOntologyFieldRows(fields, taskType) if (!rows.length) { return '' } return [ '| 字段 | 内容 |', '| --- | --- |', ...rows.map((row) => `| ${escapeMarkdownTableCell(row.label)} | ${escapeMarkdownTableCell(row.value)} |`) ].join('\n') } function resolveCandidateFlowExpenseType(flow = {}) { const rawType = String(flow?.ontologyFields?.expense_type || flow?.ontologyFields?.expenseType || '').trim() if (rawType === '差旅' || rawType === 'travel') { return 'travel' } return rawType } function normalizeStewardExpenseTypeCode(value = '') { const text = String(value || '').trim() if (text === '差旅' || text === '差旅费' || text === 'travel') { return 'travel' } return text } function resolveTaskExpenseType(task = null) { const fields = task?.ontologyFields || task?.ontology_fields || {} const explicitType = normalizeStewardExpenseTypeCode( fields.expense_type || fields.expenseType || fields.application_type || fields.applicationType || '' ) if (explicitType) { return explicitType } const taskText = [ task?.title, task?.summary, fields.reason, fields.location ].map((item) => String(item || '').trim()).join(' ') return /差旅|出差/.test(taskText) ? 'travel' : '' } function buildStewardApplicationPreviewRoutePayload(actionType, task = null) { if (actionType !== 'confirm_create_application') { return {} } const expenseType = resolveTaskExpenseType(task) if (expenseType !== 'travel') { return {} } return { steward_confirm_flow: true, flow_id: 'travel_application', expense_type: expenseType, expense_type_label: FLOW_EXPENSE_TYPE_LABELS[expenseType] || '' } } export function buildStewardSuggestedActions(plan) { const normalized = normalizeStewardPlan(plan) if (isOffTopicPlan(normalized)) { return normalized.suggestedPrompts.map((prompt) => ({ label: prompt.length > 24 ? `${prompt.slice(0, 24)}...` : prompt, description: '点击填入输入框,可编辑后发送', icon: 'mdi mdi-comment-text-outline', action_type: ASSISTANT_SCOPE_ACTION_FILL_COMPOSER, payload: { steward_plan_id: normalized.planId, fill_text: prompt } })) } if (isPendingFlowConfirmationPlan(normalized)) { return normalized.candidateFlows.map((flow) => { const expenseType = resolveCandidateFlowExpenseType(flow) return { label: flow.label, description: flow.reason || '选择后小财管家会继续整理对应流程材料。', icon: flow.flowId === 'travel_application' ? 'mdi mdi-file-plus-outline' : 'mdi mdi-receipt-text-plus-outline', action_type: ASSISTANT_SCOPE_ACTION_SWITCH, payload: { steward_confirm_flow: true, steward_plan_id: normalized.planId, flow_id: flow.flowId, session_type: flow.flowId === 'travel_application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE, selected_flow_label: flow.label, expense_type: expenseType, expense_type_label: FLOW_EXPENSE_TYPE_LABELS[expenseType] || '', requires_application_before_reimbursement: flow.flowId === 'travel_reimbursement' && expenseType === 'travel', carry_text: flow.flowId === 'travel_reimbursement' && expenseType === 'travel' ? '我要报销' : flow.label, auto_submit: true, steward_state: normalized.stewardState || null } } }) } const nextContext = resolveNextActionContext(normalized) if (!nextContext) { return [] } const { action, actionType, task, group } = nextContext const targetSessionType = actionType === 'confirm_create_application' ? SESSION_TYPE_APPLICATION : SESSION_TYPE_EXPENSE const executableStep = resolveExecutableStewardActionStep(task) return [ { label: buildNextActionLabel(actionType, task), description: buildNextActionDescription(actionType, normalized, task, group), icon: actionType === 'confirm_create_application' ? 'mdi mdi-file-plus-outline' : actionType === 'confirm_attachment_group' ? 'mdi mdi-folder-check-outline' : 'mdi mdi-receipt-text-plus-outline', action_type: ASSISTANT_SCOPE_ACTION_SWITCH, payload: { session_type: targetSessionType, ...buildStewardApplicationPreviewRoutePayload(actionType, task), carry_text: buildStewardCarryText(actionType, task, group, normalized), carry_files: actionType !== 'confirm_create_application', auto_submit: true, steward_confirmation_id: String(action.confirmation_id || action.confirmationId || ''), steward_plan_id: normalized.planId, steward_next_task_id: task?.taskId || '', ...buildStewardExecuteActionPayload(executableStep, task), steward_current_task: buildStewardTaskPayload(task), steward_remaining_task_count: normalized.tasks.filter((item) => item.taskId !== task?.taskId).length, steward_remaining_tasks: buildRemainingTaskPayload(normalized, task?.taskId) } } ] } function resolveExecutableStewardActionStep(task = null) { const steps = Array.isArray(task?.actionSteps || task?.action_steps) ? task.actionSteps || task.action_steps : [] return [...steps].reverse().find((step) => EXECUTABLE_STEWARD_ACTION_TYPES.has(String(step.action_type || step.actionType || ''))) || null } function buildStewardExecuteActionPayload(step, task) { if (!step) { return {} } return { steward_execute_action: true, steward_action_type: String(step.action_type || step.actionType || ''), steward_action_step: step, steward_action_requires_confirmation: Boolean(step.requires_confirmation ?? step.requiresConfirmation), steward_action_task_id: task?.taskId || task?.task_id || '' } } function normalizePendingFlowConfirmation(rawPlan = {}) { const rawPending = rawPlan.pending_flow_confirmation || rawPlan.pendingFlowConfirmation || {} const rawCandidates = Array.isArray(rawPlan.candidate_flows || rawPlan.candidateFlows) ? rawPlan.candidate_flows || rawPlan.candidateFlows : rawPending?.candidate_flows || rawPending?.candidateFlows || [] const candidateFlows = Array.isArray(rawCandidates) ? rawCandidates .map((item) => normalizeCandidateFlow(item)) .filter((item) => item.flowId) : [] return { status: String(rawPending?.status || '').trim(), sourceMessage: String(rawPending?.source_message || rawPending?.sourceMessage || '').trim(), reason: String(rawPending?.reason || '').trim(), candidateFlows } } function normalizeCandidateFlow(item = {}) { const flowId = String(item.flow_id || item.flowId || '').trim() if (!['travel_application', 'travel_reimbursement'].includes(flowId)) { return { flowId: '' } } return { flowId, label: String(item.label || (flowId === 'travel_application' ? '补办出差申请' : '发起费用报销')).trim(), confidence: Number(item.confidence || 0), reason: String(item.reason || '').trim(), ontologyFields: item.ontology_fields || item.ontologyFields || {}, missingFields: Array.isArray(item.missing_fields || item.missingFields) ? item.missing_fields || item.missingFields : [] } } function isPendingFlowConfirmationPlan(normalized) { return ( String(normalized?.nextAction || '').trim() === 'confirm_flow' || String(normalized?.planStatus || '').trim() === 'needs_flow_confirmation' || String(normalized?.pendingFlowConfirmation?.status || '').trim() === 'pending' ) && Array.isArray(normalized?.candidateFlows) && normalized.candidateFlows.length > 0 } function isOffTopicPlan(normalized) { return String(normalized?.planStatus || '').trim() === 'off_topic' } export function isOffTopicStewardPlan(rawPlan) { return isOffTopicPlan(normalizeStewardPlan(rawPlan)) } function buildOffTopicMessageText(normalized) { // off_topic 计划的引导文案完全由后端生成(含 ### 标题 + 正文 + 引导句), // 前端透传 summary 即可,避免重复拼接导致与后端表达不一致。 const summary = String(normalized?.summary || '').trim() if (summary) { return summary } return ( '### 这句话我暂时没识别到财务事项\n\n' + '很抱歉主人,目前小财管家只能帮您整理**费用申请**和**费用报销**这两类事项。\n\n' + '要不您换种说法告诉我:' ) } function buildPendingFlowConfirmationMessageText(normalized) { const fields = normalized.candidateFlows[0]?.ontologyFields || {} const knownTable = formatStewardOntologyFieldsTable(fields, 'expense_application') const candidateLines = normalized.candidateFlows.map((flow, index) => `${index + 1}. **${flow.label}**${flow.reason ? `\n - ${flow.reason}` : ''}` ) const singleCandidate = normalized.candidateFlows.length === 1 return [ '### 需要先确认流程方向', '', knownTable ? ['我识别到这是一项财务事项,已提取到:', '', knownTable].join('\n') : '我识别到这是一项财务事项,但还需要确认您要进入哪个流程。', '', normalized.pendingFlowConfirmation.reason || normalized.summary || '当前还不能确定您是要补办申请,还是发起报销。', '', ...candidateLines, '', singleCandidate ? `请先点击下方 **${normalized.candidateFlows[0].label}**,我会继续整理对应材料。` : '请先选择一个方向,我会继续整理对应材料。' ].filter((line, index, lines) => line || lines[index - 1]).join('\n') } function buildGenericReimbursementIntentMessageText() { return [ '### 我来带您发起报销', '', '您现在只说了要报销,还没告诉我具体是哪类费用。先不用一次性补全所有信息,我会按报销流程一步步带您填。', '', '1. **先选报销场景**', ' - 例如差旅费、交通费、住宿费、业务招待费或办公用品费,不同场景需要的材料不一样。', '2. **再补关键材料**', ' - 我会继续追问事由、发生时间、金额和票据附件;如果是差旅或招待,还会先帮您核对是否需要关联事前申请。', '', '点击下面的 **确定,选择报销场景**,我会进入报销助手继续引导。' ].join('\n') } function resolveNextActionContext(normalized) { const applicationTask = normalized.tasks.find((task) => task.taskType === 'expense_application') const applicationAction = applicationTask ? findConfirmationAction(normalized, 'confirm_create_application', applicationTask.taskId) : null if (applicationAction) { return { action: applicationAction, actionType: 'confirm_create_application', task: applicationTask, group: null } } const reimbursementTask = normalized.tasks.find((task) => task.taskType === 'reimbursement') const reimbursementAction = reimbursementTask ? findConfirmationAction(normalized, 'confirm_create_reimbursement_draft', reimbursementTask.taskId) : null if (reimbursementAction) { return { action: reimbursementAction, actionType: 'confirm_create_reimbursement_draft', task: reimbursementTask, group: findAttachmentGroupForTask(normalized, reimbursementTask.taskId) } } const attachmentAction = normalized.confirmationGroups.find((action) => normalizeActionType(action) === 'confirm_attachment_group' ) if (attachmentAction) { const groupId = String(attachmentAction.attachment_group_id || attachmentAction.attachmentGroupId || '').trim() const group = normalized.attachmentGroups.find((item) => item.groupId === groupId) const task = normalized.tasks.find((item) => item.taskId === group?.targetTaskId) return { action: attachmentAction, actionType: 'confirm_attachment_group', task, group } } const fallbackAction = normalized.confirmationGroups[0] if (!fallbackAction) { return null } const actionType = normalizeActionType(fallbackAction) const taskId = String(fallbackAction.target_task_id || fallbackAction.targetTaskId || '').trim() return { action: fallbackAction, actionType, task: normalized.tasks.find((task) => task.taskId === taskId), group: null } } function findConfirmationAction(normalized, actionType, taskId) { return normalized.confirmationGroups.find((action) => normalizeActionType(action) === actionType && String(action.target_task_id || action.targetTaskId || '').trim() === taskId ) || normalized.confirmationGroups.find((action) => normalizeActionType(action) === actionType) } function findAttachmentGroupForTask(normalized, taskId) { return normalized.attachmentGroups.find((group) => group.targetTaskId === taskId) || normalized.attachmentGroups[0] || null } function normalizeActionType(action) { return String(action?.action_type || action?.actionType || '').trim() } function buildStewardExecutionSummary(normalized) { const attachmentCount = normalized.attachmentGroups .reduce((total, group) => total + group.attachmentNames.length, 0) const summary = [`我识别到 **${normalized.tasks.length} 个待处理任务**`] if (attachmentCount) { summary.push(`并形成 ${attachmentCount} 份附件的归集建议`) } summary.push(`。${buildTaskOrderDescription(normalized)}`) return summary.join('') } function buildOrderedStewardTasks(normalized, nextTask = null) { if (!nextTask?.taskId) { return normalized.tasks } return [ nextTask, ...normalized.tasks.filter((task) => task.taskId !== nextTask.taskId) ] } function buildTaskOrderVerb(index) { if (index === 0) { return '先' } if (index === 1) { return '再' } return '然后' } function buildTaskOrderTarget(task) { const title = task.title || task.taskTypeLabel if (task.taskType === 'expense_application') { return `整理“${title}”` } if (task.taskType === 'reimbursement') { return `核对“${title}”` } return `处理“${title}”` } function buildTaskOrderActionDescription(task) { const agent = task.assignedAgentLabel || '对应助手' if (task.taskType === 'expense_application') { // 申请类:先给行动,再说目的,主语后置 return `这步交给${agent}——先把申请单草稿拉出来给您过目,没问题了再往下走。` } if (task.taskType === 'reimbursement') { if (isGenericReimbursementTask(task)) { // 通用报销:换个句式,省掉主语,突出"先定方向" return `报销还差一个关键信息:具体是哪类费用。${agent}会先带您把报销场景定下来,再逐项补事由、时间、金额和票据。` } // 有明确场景的报销:直接说动作,不绕弯 return `票据、金额和制度口径,${agent}会一并核清楚;前一步确认后才会继续,不会越级往下推。` } // 兜底:用"等您点头"的语气,区别于上面三条 return `${agent}先把能核对的结果摆出来,真正动手前仍会等您点头。` } function buildStewardPlanFriendlyIntro(normalized) { const taskCountText = normalized.tasks.length > 1 ? `${normalized.tasks.length} 个相关事项` : '1 个事项' return `我先看了一下,您这次主要是 **${taskCountText}**。为了不让步骤混在一起,我会先把要做的事拆开,让您每一步都能看清楚、确认后再继续。` } function buildTaskOrderDescription(normalized) { const hasApplication = normalized.tasks.some((task) => task.taskType === 'expense_application') const hasReimbursement = normalized.tasks.some((task) => task.taskType === 'reimbursement') if (hasApplication && hasReimbursement) { return '处理顺序是:先创建申请单,再引导填写报销单。' } if (hasApplication) { return '我会先引导创建申请单,并等待您确认。' } if (hasReimbursement) { return '我会引导填写报销单,并等待您确认。' } return '我会按识别顺序逐项推进,并在执行前等待您确认。' } function buildNextTaskLead(task) { if (task.taskType === 'expense_application') { return `先创建“${task.title || task.taskTypeLabel}”` } if (task.taskType === 'reimbursement') { return `继续填写“${task.title || task.taskTypeLabel}”` } return `处理“${task.title || task.taskTypeLabel}”` } function buildNextActionLabel(actionType, task = null) { if (actionType === 'confirm_create_application') { return '确定,先创建申请单' } if (actionType === 'confirm_attachment_group') { return '确定,确认附件归集' } if (isGenericReimbursementTask(task)) { return '确定,选择报销场景' } return '确定,继续填写报销单' } function buildNextActionDescription(actionType, normalized, task, group) { const remainingCount = normalized.tasks.filter((item) => item.taskId !== task?.taskId).length if (actionType === 'confirm_create_application') { return remainingCount > 0 ? '申请助手会先生成申请单核对结果,完成后再继续引导后续报销。' : '申请助手会生成申请单核对结果,入库前仍需确认。' } if (actionType === 'confirm_attachment_group') { return group?.attachmentNames?.length ? `先归集 ${group.attachmentNames.length} 份附件,再进入报销核对。` : '先确认附件归集,再进入报销核对。' } return group?.attachmentNames?.length ? `报销助手会带入 ${group.attachmentNames.length} 份相关附件生成核对结果。` : isGenericReimbursementTask(task) ? '先进入报销助手选择具体费用类型,再按场景补齐事由、时间、金额和票据。' : '报销助手会根据当前任务生成报销核对结果。' } function isGenericReimbursementTask(task) { if (!task || task.taskType !== 'reimbursement') { return false } const fields = task.ontologyFields || {} const expenseType = String(fields.expense_type || '').trim() const hasSpecificField = ['time_range', 'location', 'amount', 'attachments', 'transport_mode'] .some((key) => String(fields[key] || '').trim()) || isSpecificReimbursementReason(fields.reason) return !hasSpecificField && (!expenseType || expenseType === 'other') } function isSpecificReimbursementReason(value) { const text = String(value || '').trim().replace(/\s+/g, '') if (!text) { return false } return !/^(?:我想要|我想|我要|还需要|需要|请帮我|帮我)?报销(?:费用|报销单|报销流程)?$/.test(text) } function buildStewardCarryText(actionType, task, group, normalized = null) { if (actionType === 'confirm_attachment_group' && group) { return [ `我确认将以下附件归集为${group.sceneLabel || '当前报销任务'},请继续整理报销核对信息。`, `附件:${group.attachmentNames.join('、') || '待确认'}`, group.excludedAttachmentNames.length ? `暂不归集:${group.excludedAttachmentNames.join('、')}` : '' ].filter(Boolean).join('\n') } if (!task) { return '我确认继续处理这项财务任务,请按现有流程核对信息。' } if (actionType === 'confirm_create_reimbursement_draft' && isGenericReimbursementTask(task)) { return '我要报销' } const fields = formatStewardOntologyFields(task.ontologyFields || {}, task.taskType) const missingFields = formatStewardMissingFieldList( task.missingFields || [], task.taskType, { includeHints: false } ) const lines = [ actionType === 'confirm_create_application' ? `小财管家已完成意图识别,请先创建申请单:${task.title || task.taskTypeLabel}。` : `小财管家已完成意图识别,请继续填写报销单:${task.title || task.taskTypeLabel}。`, task.summary ? `任务摘要:${task.summary}` : '', fields ? `已识别信息:${fields}` : '', group?.attachmentNames?.length ? `相关附件:${group.attachmentNames.join('、')}` : '', group?.excludedAttachmentNames?.length ? `暂不归集附件:${group.excludedAttachmentNames.join('、')}` : '', missingFields ? `还需要补充:${missingFields}` : '', actionType === 'confirm_create_application' ? missingFields ? '请先追问上述缺失信息,不要直接生成申请单核对表,也不要替用户默认填写。' : '请直接生成申请单核对结果;信息足够时生成申请单,但在入库或提交审批前仍需让我确认。' : missingFields ? '请先追问上述缺失信息,不要直接生成报销核对结果,也不要替用户默认填写。' : '请直接生成报销核对结果;需要创建草稿、绑定附件或提交审批前仍需让我确认。' ] const remainingTaskText = normalized ? buildRemainingTaskText(normalized, task.taskId) : '' if (remainingTaskText) { lines.push(remainingTaskText) } return lines.filter(Boolean).join('\n') } function buildRemainingTaskText(normalized, currentTaskId) { const remainingTasks = normalized.tasks.filter((task) => task.taskId !== currentTaskId) if (!remainingTasks.length) { return '' } const taskLines = remainingTasks.map((task, index) => `${index + 1}. ${task.title || task.taskTypeLabel}(${task.assignedAgentLabel}):${task.summary || '待继续核对'}` ) return [ '当前步骤完成后,请继续引导我处理后续任务:', ...taskLines ].join('\n') } function buildRemainingTaskPayload(normalized, currentTaskId) { return normalized.tasks .filter((task) => task.taskId !== currentTaskId) .map((task) => buildStewardTaskPayload(task)) } function buildStewardTaskPayload(task) { if (!task) { return null } return { task_id: task.taskId || task.task_id || '', task_type: task.taskType || task.task_type || '', title: task.title || '', summary: task.summary || '', assigned_agent: task.assignedAgent || task.assigned_agent || '', requested_action: task.requestedAction || task.requested_action || '', ontology_fields: task.ontologyFields || task.ontology_fields || {}, missing_fields: task.missingFields || task.missing_fields || [], action_steps: task.actionSteps || task.action_steps || [] } }