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

736 lines
29 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,
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: '报销助手'
}
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),
ontologyFields: item.ontology_fields || item.ontologyFields || {},
missingFields,
missingFieldItems: buildStewardFieldItems(missingFields, taskType),
confirmationRequired: item.confirmation_required ?? item.confirmationRequired ?? true
}
})
: [],
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)
: []
}
}
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
}
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
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,
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 || '',
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 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 || '',
ontology_fields: task.ontologyFields || task.ontology_fields || {},
missing_fields: task.missingFields || task.missing_fields || []
}
}