Files
X-Financial/web/src/views/scripts/stewardPlanModel.js
caoxiaozhu 606a88c805 chore: stewardPlanModel 适配注册表动作结构并更新规则表与日志
- stewardPlanModel 适配新的意图注册表动作步骤结构
- 更新交通/通信/差旅等财务规则表,补 2026-06-25 work-log
2026-06-25 11:50:11 +08:00

843 lines
33 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: '费用报销',
query_travel_standard: '差旅标准查询'
}
const AGENT_LABELS = {
application_assistant: '申请助手',
application: '申请助手',
expense_application: '申请助手',
reimbursement_assistant: '报销助手',
reimbursement: '报销助手',
expense: '报销助手',
policy_query_assistant: '政策查询助手',
query_travel_standard: '政策查询助手'
}
const EXECUTABLE_STEWARD_ACTION_TYPES = new Set([
'save_application_draft',
'submit_application',
'create_reimbursement_draft',
'associate_attachments',
'execute_travel_standard_query'
])
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 || []
}
}