feat: 小财管家意图规划与报销提交编排增强
- 完善管家意图识别、模型计划构建与规划器调度 - 重构差旅报销提交编排器与管家计划流程前端交互 - 优化报销消息项样式与文档中心视图 - 新增小财管家与附件上传风险前置复核设计文档 - 补充管家规划器与文档中心测试覆盖
This commit is contained in:
@@ -16,6 +16,11 @@ import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSu
|
||||
import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js'
|
||||
import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js'
|
||||
import { useStewardPlanFlow } from './useStewardPlanFlow.js'
|
||||
import {
|
||||
buildStewardFieldItems,
|
||||
formatStewardMissingFieldList,
|
||||
formatStewardOntologyFields
|
||||
} from './stewardPlanModel.js'
|
||||
import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js'
|
||||
import {
|
||||
buildOperationFeedbackPayload,
|
||||
@@ -202,6 +207,10 @@ import {
|
||||
userAvatar
|
||||
} from './travelReimbursementConversationModel.js'
|
||||
|
||||
const STEWARD_ASSISTANT_NAME = '小财管家'
|
||||
const STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS = 18
|
||||
const STEWARD_FOLLOWUP_THINKING_INTERVAL_MS = 14
|
||||
|
||||
const REVIEW_RISK_LEVEL_META = {
|
||||
high: {
|
||||
label: '高风险',
|
||||
@@ -1546,6 +1555,7 @@ export default {
|
||||
replaceMessage,
|
||||
scrollToBottom,
|
||||
adjustComposerTextareaHeight,
|
||||
executeStewardSuggestedAction: (message, action) => handleSuggestedAction(message, action),
|
||||
submitting,
|
||||
reviewActionBusy,
|
||||
sessionSwitchBusy,
|
||||
@@ -1695,6 +1705,30 @@ export default {
|
||||
const carryText = String(actionPayload.carry_text || '').trim()
|
||||
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
if (String(actionPayload.steward_plan_id || '').trim()) {
|
||||
const confirmedByText = Boolean(action.confirmedByText)
|
||||
delete action.confirmedByText
|
||||
await submitComposerInternal({
|
||||
rawText: carryText,
|
||||
userText: action.label || '确定',
|
||||
pendingText: targetSessionType === SESSION_TYPE_APPLICATION
|
||||
? '小财管家正在调用申请助手生成申请单核对结果...'
|
||||
: '小财管家正在调用报销助手整理报销核对结果...',
|
||||
files: carryFiles,
|
||||
skipScopeGuard: true,
|
||||
skipStewardPlan: true,
|
||||
skipUserMessage: confirmedByText,
|
||||
sessionTypeOverride: targetSessionType,
|
||||
stewardContinuation: {
|
||||
planId: String(actionPayload.steward_plan_id || '').trim(),
|
||||
currentTaskId: String(actionPayload.steward_next_task_id || '').trim(),
|
||||
remainingTasks: Array.isArray(actionPayload.steward_remaining_tasks)
|
||||
? actionPayload.steward_remaining_tasks
|
||||
: []
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
await switchSessionType(targetSessionType)
|
||||
if (carryText) {
|
||||
composerDraft.value = carryText
|
||||
@@ -2161,6 +2195,195 @@ export default {
|
||||
return String(request.claimId || request.claim_id || '').trim()
|
||||
}
|
||||
|
||||
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: '小财管家',
|
||||
meta: ['小财管家', '等待用户确认'],
|
||||
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_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 buildStewardFollowupThinkingEvents() {
|
||||
const eventPrefix = `steward-followup-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
return [
|
||||
{
|
||||
eventId: `${eventPrefix}-review`,
|
||||
title: '复盘结果',
|
||||
content: '当前动作已完成,小财管家正在检查剩余任务队列。'
|
||||
},
|
||||
{
|
||||
eventId: `${eventPrefix}-next`,
|
||||
title: '选择下一步',
|
||||
content: '我会继续保持一步一步推进,先说明下一步,再等你确认后执行。'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
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()) {
|
||||
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; index += 1) {
|
||||
await waitStewardFollowupTick(STEWARD_FOLLOWUP_THINKING_INTERVAL_MS)
|
||||
event.content = chars.slice(0, index + 1).join('')
|
||||
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
|
||||
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
||||
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; index += 1) {
|
||||
await waitStewardFollowupTick(STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS)
|
||||
finalMessage.text = chars.slice(0, index + 1).join('')
|
||||
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
||||
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'typing', followupPlanId)
|
||||
if ((index + 1) % 4 === 0 || index === chars.length - 1) {
|
||||
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)
|
||||
const lines = [
|
||||
taskType === 'expense_application'
|
||||
? `小财管家继续执行剩余任务,请创建申请单:${task.title || '费用申请'}。`
|
||||
: `小财管家继续执行剩余任务,请填写报销单:${task.title || '费用报销'}。`,
|
||||
task.summary ? `任务摘要:${task.summary}` : '',
|
||||
fields ? `已识别信息:${fields}` : '',
|
||||
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)
|
||||
}
|
||||
|
||||
async function confirmApplicationSubmit() {
|
||||
const message = applicationSubmitConfirmDialog.value.message
|
||||
if (!message || submitting.value || reviewActionBusy.value) {
|
||||
@@ -2185,6 +2408,8 @@ export default {
|
||||
pendingText: '正在提交费用申请...',
|
||||
systemGenerated: true,
|
||||
skipScopeGuard: true,
|
||||
skipStewardPlan: true,
|
||||
sessionTypeOverride: SESSION_TYPE_APPLICATION,
|
||||
feedbackOperationType: 'submit_application',
|
||||
extraContext: {
|
||||
application_preview: applicationPreview,
|
||||
@@ -2229,6 +2454,10 @@ export default {
|
||||
persistSessionState()
|
||||
nextTick(scrollToBottom)
|
||||
}
|
||||
const stewardFollowup = buildStewardContinuationAfterAction(message, '申请单已完成')
|
||||
if (stewardFollowup) {
|
||||
await pushStewardContinuationMessage(stewardFollowup)
|
||||
}
|
||||
} finally {
|
||||
reviewActionBusy.value = false
|
||||
}
|
||||
@@ -2449,7 +2678,7 @@ export default {
|
||||
// submitting.value = true
|
||||
// recognizeOcrFiles(files)
|
||||
// submitting.value = false
|
||||
if (isStewardSession.value && await submitStewardPlan(options)) {
|
||||
if (isStewardSession.value && !options.skipStewardPlan && await submitStewardPlan(options)) {
|
||||
return null
|
||||
}
|
||||
if (await handleGuidedComposerSubmit(options)) {
|
||||
@@ -2570,6 +2799,7 @@ export default {
|
||||
sessionSwitchBusy: sessionSwitchBusy.value,
|
||||
applicationPreviewEditor: applicationPreviewEditor.value,
|
||||
buildMessageBubbleClass,
|
||||
resolveStewardMissingFieldItems,
|
||||
buildReviewMainMessageText,
|
||||
renderMarkdown,
|
||||
handleAssistantMarkdownClick,
|
||||
|
||||
Reference in New Issue
Block a user