feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -15,7 +15,10 @@ import {
|
||||
buildLocalApplicationPreview,
|
||||
buildLocalApplicationPreviewMessage,
|
||||
buildModelRefinedApplicationPreview,
|
||||
applicationDateRangesOverlap,
|
||||
normalizeApplicationPreview,
|
||||
normalizeTransportModeOption,
|
||||
resolveApplicationDateRange,
|
||||
resolveApplicationTimeLabel,
|
||||
shouldUseLocalApplicationPreview
|
||||
} from '../src/utils/expenseApplicationPreview.js'
|
||||
@@ -36,6 +39,17 @@ import {
|
||||
createMessage as createConversationMessage,
|
||||
hasMeaningfulSessionMessages
|
||||
} from '../src/views/scripts/travelReimbursementConversationModel.js'
|
||||
import {
|
||||
buildStewardSuggestedActions,
|
||||
filterStewardBlockingMissingFields
|
||||
} from '../src/views/scripts/stewardPlanModel.js'
|
||||
import {
|
||||
buildStewardFieldCompletionContinuation,
|
||||
buildStewardFieldCompletionRawText
|
||||
} from '../src/views/scripts/stewardFieldCompletionModel.js'
|
||||
import {
|
||||
shouldUseBudgetCompileReport
|
||||
} from '../src/views/scripts/budgetAssistantReportModel.js'
|
||||
import { useTravelReimbursementFlow } from '../src/views/scripts/useTravelReimbursementFlow.js'
|
||||
import { useApplicationPreviewEditor } from '../src/views/scripts/useApplicationPreviewEditor.js'
|
||||
|
||||
@@ -43,10 +57,22 @@ const submitComposerScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementSubmitComposer.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stewardServiceScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/services/steward.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stewardPlanFlowScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/useStewardPlanFlow.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const stewardFieldCompletionScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/stewardFieldCompletionModel.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const createViewTemplate = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/TravelReimbursementCreateView.vue', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -506,7 +532,7 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(submitComposerScript, /startFlowStep\('intent'/)
|
||||
assert.match(submitComposerScript, /startFlowStep\('application-review-preview'/)
|
||||
assert.match(submitComposerScript, /completeFlowStep\('intent'/)
|
||||
assert.doesNotMatch(submitComposerScript, /insightPanelCollapsed\.value = true/)
|
||||
assert.match(submitComposerScript, /function resetStewardDelegatedInsightState\(\) \{[\s\S]*insightPanelCollapsed\.value = true/)
|
||||
assert.doesNotMatch(submitComposerScript, /void refineApplicationPreviewWithModel/)
|
||||
assert.match(submitComposerScript, /return null[\s\S]*const hasUnsavedReviewDraft/)
|
||||
assert.ok(
|
||||
@@ -542,9 +568,16 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(messageItemTemplate, /申请单据已生成/)
|
||||
assert.match(messageItemTemplate, /ui\.shouldShowDraftSavedCard\(message\)/)
|
||||
assert.match(messageItemTemplate, /报销草稿已生成/)
|
||||
assert.match(messageItemTemplate, /报销草稿待保存/)
|
||||
assert.match(messageItemTemplate, /ui\.resolveReimbursementDraftClaimNo\(message\.draftPayload\)/)
|
||||
assert.match(messageItemTemplate, /v-if="ui\.canOpenDraftDetail\(message\)"/)
|
||||
assert.match(messageItemTemplate, /class="reimbursement-draft-link"/)
|
||||
assert.match(messageItemTemplate, /查看详情/)
|
||||
assert.match(messageItemTemplate, /class="reimbursement-draft-pending-detail"/)
|
||||
assert.match(messageItemTemplate, /保存后可查看详情/)
|
||||
assert.match(createViewScript, /function canOpenDraftDetail\(message\)/)
|
||||
assert.match(createViewScript, /canOpenDraftDetail,/)
|
||||
assert.match(createViewScript, /保存后生成/)
|
||||
assert.doesNotMatch(messageItemTemplate, /ui\.buildReimbursementDraftSummaryItems\(message\.draftPayload\)/)
|
||||
assert.doesNotMatch(messageItemTemplate, /可以继续上传票据,我会归集到这张草稿。/)
|
||||
assert.ok(
|
||||
@@ -558,9 +591,15 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(messageItemTemplate, /完整审批链、附件和明细可在单据详情中[\s\S]*application-draft-detail-link[\s\S]*>查看<\/button>/)
|
||||
assert.doesNotMatch(messageItemTemplate, /application-draft-detail-btn/)
|
||||
assert.match(messageItemTemplate, /ui\.openApplicationDraftDetail\(message\)/)
|
||||
assert.match(messageItemTemplate, /<OperationFeedbackInlineCard/)
|
||||
assert.match(messageItemTemplate, /ui\.isOperationFeedbackVisible\(message\)/)
|
||||
assert.match(messageItemTemplate, /ui\.submitOperationFeedbackForMessage\(message, \$event\)/)
|
||||
assert.doesNotMatch(messageItemTemplate, /<OperationFeedbackInlineCard/)
|
||||
assert.match(messageItemTemplate, /class="message-action-toolbar"/)
|
||||
assert.match(messageItemTemplate, /ui\.shouldShowAssistantMessageActions\(message\)/)
|
||||
assert.match(messageItemTemplate, /ui\.copyAssistantMessage\(message\)/)
|
||||
assert.match(messageItemTemplate, /ui\.speakAssistantMessage\(message\)/)
|
||||
assert.match(messageItemTemplate, /ui\.submitOperationFeedbackForMessage\(message, \{ rating: 5, reason: 'thumbs_up' \}\)/)
|
||||
assert.match(messageItemTemplate, /ui\.submitOperationFeedbackForMessage\(message, \{ rating: 1, reason: 'thumbs_down' \}\)/)
|
||||
assert.match(messageItemTemplate, /mdi mdi-content-copy/)
|
||||
assert.match(messageItemTemplate, /mdi mdi-volume-high/)
|
||||
assert.match(submitComposerScript, /employee_grade:\s*user\.grade \|\| user\.employeeGrade \|\| user\.employee_grade/)
|
||||
assert.match(submitComposerScript, /employeeGrade:\s*user\.grade \|\| user\.employeeGrade \|\| user\.employee_grade/)
|
||||
assert.match(createViewTemplate, /'has-insight': hasInsightPanelContent && showInsightPanel/)
|
||||
@@ -580,7 +619,12 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(createViewScript, /onComposerDateSelection: applyLinkedApplicationPreviewDateSelection/)
|
||||
assert.match(createViewScript, /function openApplicationPreviewEditorFromUi/)
|
||||
assert.match(createViewScript, /syncComposerDateFromApplicationEditor/)
|
||||
assert.match(createViewScript, /function shouldShowAssistantMessageActions/)
|
||||
assert.match(createViewScript, /function buildMessageOperationFeedbackContext/)
|
||||
assert.match(createViewScript, /function isMessageFeedbackSelected/)
|
||||
assert.match(createViewScript, /function submitOperationFeedbackForMessage/)
|
||||
assert.match(createViewScript, /const stewardSubmitContinuation = message\?\.stewardContinuation \|\| null/)
|
||||
assert.match(createViewScript, /stewardContinuation:\s*stewardSubmitContinuation/)
|
||||
assert.match(createViewTemplate, /handleComposerDateInputChange\('single'\)/)
|
||||
assert.match(createViewTemplate, /handleComposerDateInputChange\('range-start'\)/)
|
||||
assert.match(createViewTemplate, /handleComposerDateInputChange\('range-end'\)/)
|
||||
@@ -621,9 +665,9 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(flowScript, /return null/)
|
||||
assert.match(flowScript, /申请单提交成功/)
|
||||
assert.match(submitComposerScript, /const isApplicationSubmitOperation = feedbackOperationType === 'submit_application'/)
|
||||
assert.match(submitComposerScript, /if \(isApplicationSubmitOperation\) \{[\s\S]*startFlowStep\('application-submit-success'/)
|
||||
assert.match(submitComposerScript, /else if \(rawText && !reviewAction\) \{[\s\S]*startFlowStep\('intent'/)
|
||||
assert.match(submitComposerScript, /if \(!isApplicationSubmitOperation\) \{[\s\S]*startExpenseClaimDraftFlowStep/)
|
||||
assert.match(submitComposerScript, /if \(!stewardDelegated && isApplicationSubmitOperation\) \{[\s\S]*startFlowStep\('application-submit-success'/)
|
||||
assert.match(submitComposerScript, /else if \(!stewardDelegated && rawText && !reviewAction\) \{[\s\S]*startFlowStep\('intent'/)
|
||||
assert.match(submitComposerScript, /if \(!isApplicationSubmitOperation && !stewardDelegated\) \{[\s\S]*startExpenseClaimDraftFlowStep/)
|
||||
assert.match(flowScript, /function resolveDurationFromFields/)
|
||||
assert.match(flowScript, /function resolveStartedTimestamp/)
|
||||
assert.match(flowScript, /function resolveFinishedTimestamp/)
|
||||
@@ -631,6 +675,261 @@ test('application session shows intent flow, persists preview, and supports inli
|
||||
assert.match(flowScript, /refreshCompleted/)
|
||||
})
|
||||
|
||||
test('steward application missing transport asks before rendering preview table', () => {
|
||||
assert.match(submitComposerScript, /function shouldPauseStewardApplicationPreview/)
|
||||
assert.match(submitComposerScript, /function sanitizeStewardDelegatedTaskSummary/)
|
||||
assert.match(submitComposerScript, /交通方式和\(\?:预算\|预计\)\?金额待补充/)
|
||||
assert.match(submitComposerScript, /出差费用预算/)
|
||||
assert.match(submitComposerScript, /预估\|预计\|预算\)\?费用/)
|
||||
assert.match(submitComposerScript, /applicationPreview:\s*pauseForMissingFields \? null : applicationPreview/)
|
||||
assert.match(submitComposerScript, /applicationPreview:\s*normalized/)
|
||||
assert.match(submitComposerScript, /请先告诉我你打算怎么出行:\*\*火车、飞机或轮船\*\*/)
|
||||
assert.doesNotMatch(submitComposerScript, /缺少“出行方式”[\s\S]{0,500}更新下方核对表/)
|
||||
|
||||
assert.match(createViewScript, /payload\.applicationPreview/)
|
||||
assert.match(createViewScript, /function continueStewardApplicationFieldCompletion/)
|
||||
assert.match(createViewScript, /submitComposerInternal\(\{[\s\S]*stewardContinuation: continuation/)
|
||||
assert.match(createViewScript, /skipUserMessage:\s*true/)
|
||||
assert.match(createViewScript, /targetMessage\.applicationPreview = normalizeApplicationPreview\(sourcePreview\)/)
|
||||
assert.match(createViewScript, /openApplicationPreviewEditor\(targetMessage, fieldKey/)
|
||||
assert.match(createViewScript, /commitApplicationPreviewEditor\(targetMessage\)/)
|
||||
assert.match(stewardFieldCompletionScript, /transportMode:\s*'transport_mode'/)
|
||||
assert.match(stewardFieldCompletionScript, /模拟查询交通票据/)
|
||||
})
|
||||
|
||||
test('steward field completion reruns application preview instead of directly rendering table', () => {
|
||||
const continuation = {
|
||||
planId: 'steward-plan-transport-gap',
|
||||
currentTaskId: 'task-application-beijing',
|
||||
currentTask: {
|
||||
task_id: 'task-application-beijing',
|
||||
task_type: 'expense_application',
|
||||
summary: '明天前往北京出差3天,支撑国网仿生产部署',
|
||||
ontology_fields: {
|
||||
time_range: '2026-06-05 至 2026-06-07',
|
||||
location: '北京',
|
||||
reason: '支撑国网仿生产部署'
|
||||
},
|
||||
missing_fields: ['transport_mode']
|
||||
},
|
||||
remainingTasks: []
|
||||
}
|
||||
const preview = normalizeApplicationPreview({
|
||||
fields: {
|
||||
applicationType: '差旅费用申请',
|
||||
time: '2026-06-05 至 2026-06-07',
|
||||
location: '北京',
|
||||
reason: '支撑国网仿生产部署',
|
||||
days: '3天',
|
||||
transportMode: ''
|
||||
}
|
||||
})
|
||||
|
||||
const nextContinuation = buildStewardFieldCompletionContinuation(continuation, 'transportMode', '火车')
|
||||
assert.equal(nextContinuation.currentTask.ontology_fields.transport_mode, '火车')
|
||||
assert.deepEqual(nextContinuation.currentTask.missing_fields, [])
|
||||
|
||||
const carryText = buildStewardFieldCompletionRawText({
|
||||
preview,
|
||||
fieldKey: 'transportMode',
|
||||
fieldLabel: '出行方式',
|
||||
value: '火车',
|
||||
continuation: nextContinuation
|
||||
})
|
||||
assert.match(carryText, /用户已补充:出行方式:火车/)
|
||||
assert.match(carryText, /地点:北京/)
|
||||
assert.match(carryText, /天数:3天/)
|
||||
assert.match(carryText, /请先根据已补齐字段模拟查询交通票据/)
|
||||
|
||||
const rebuiltPreview = buildLocalApplicationPreview(carryText, { name: '曹笑竹', grade: 'P5' })
|
||||
assert.equal(rebuiltPreview.fields.location, '北京')
|
||||
assert.equal(rebuiltPreview.fields.transportMode, '火车')
|
||||
assert.equal(rebuiltPreview.fields.days, '3天')
|
||||
})
|
||||
|
||||
test('budget compile report does not steal steward delegated application rerun', () => {
|
||||
const staleBudgetContext = {
|
||||
budgetNo: 'BUD-2026-TECH',
|
||||
mode: 'edit',
|
||||
categoryRows: []
|
||||
}
|
||||
const stewardApplicationText = [
|
||||
'小财管家继续执行申请单字段补齐。',
|
||||
'用户已补充:出行方式:火车。',
|
||||
'地点:北京',
|
||||
'天数:3天',
|
||||
'处理要求:请先根据已补齐字段模拟查询交通票据和费用口径,完成系统预估金额测算,再生成申请单核对表。'
|
||||
].join('\n')
|
||||
|
||||
assert.equal(shouldUseBudgetCompileReport(stewardApplicationText, {
|
||||
sessionType: 'application',
|
||||
entrySource: 'workbench',
|
||||
budgetContext: staleBudgetContext
|
||||
}), false)
|
||||
assert.equal(shouldUseBudgetCompileReport('帮我生成 2026 年 Q3 预算编制建议', {
|
||||
sessionType: 'budget',
|
||||
entrySource: 'budget',
|
||||
budgetContext: staleBudgetContext
|
||||
}), true)
|
||||
assert.match(submitComposerScript, /if \(!stewardDelegated && shouldUseBudgetCompileReport/)
|
||||
})
|
||||
|
||||
test('text confirmation submits pending application preview before replanning steward task', () => {
|
||||
assert.match(stewardServiceScript, /fetchStewardRuntimeDecision/)
|
||||
assert.match(stewardServiceScript, /\/steward\/runtime-decisions/)
|
||||
assert.match(createViewScript, /function buildStewardRuntimeState/)
|
||||
assert.match(createViewScript, /function buildStewardRuntimeFastPathDecision/)
|
||||
assert.match(createViewScript, /function shouldUseStewardRuntimeLlmDecision/)
|
||||
assert.match(createViewScript, /function findPendingSlotSuggestedActionContextByInput/)
|
||||
assert.match(createViewScript, /function shouldPlanNewStewardTasksLocally/)
|
||||
assert.match(createViewScript, /function resolveStewardRuntimeTransportAlias/)
|
||||
assert.match(createViewScript, /const actionTransportAlias = resolveStewardRuntimeTransportAlias/)
|
||||
assert.match(createViewScript, /actionTransportAlias === transportAlias/)
|
||||
assert.match(createViewScript, /next_action:\s*'continue_next_task'/)
|
||||
assert.match(createViewScript, /next_action:\s*'submit_current_application'/)
|
||||
assert.match(createViewScript, /next_action:\s*'fill_current_slot'/)
|
||||
assert.match(createViewScript, /next_action:\s*'plan_new_tasks'/)
|
||||
assert.match(createViewScript, /suppressUserEcho:\s*userMessageAlreadyAdded/)
|
||||
assert.match(createViewScript, /if \(!action\?\.suppressUserEcho\) \{[\s\S]*messages\.value\.push\(createMessage\('user', userText\)\)/)
|
||||
assert.match(createViewScript, /skipApplicationModelReview:\s*true/)
|
||||
assert.match(createViewScript, /skipApplicationModelReview:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
||||
assert.match(createViewScript, /skipStewardSlotDecision:\s*targetSessionType === SESSION_TYPE_APPLICATION/)
|
||||
assert.match(submitComposerScript, /skipModelReview:\s*Boolean\(stewardDelegated && options\.skipApplicationModelReview\)/)
|
||||
assert.match(submitComposerScript, /if \(options\.skipModelReview\) \{[\s\S]*结构化快路径/)
|
||||
assert.match(submitComposerScript, /const localPauseForMissingFields = shouldPauseStewardApplicationPreview\(applicationPreview\)/)
|
||||
assert.match(submitComposerScript, /const shouldFetchSlotDecision = localPauseForMissingFields && !options\.skipStewardSlotDecision/)
|
||||
assert.match(submitComposerScript, /const slotDecision = shouldFetchSlotDecision[\s\S]*fetchStewardApplicationSlotDecision/)
|
||||
assert.match(submitComposerScript, /const pendingSuggestedActions = Array\.isArray\(finalExtras\.suggestedActions\)/)
|
||||
assert.match(submitComposerScript, /message\.suggestedActions = pendingSuggestedActions[\s\S]*message\.stewardPlan = buildStewardDelegatedPlan\(continuation, \[\.\.\.typedEvents\], 'typing'\)/)
|
||||
assert.match(createViewScript, /async function handleStewardRuntimeDecision/)
|
||||
assert.match(createViewScript, /const runtimeState = buildStewardRuntimeState\(\)/)
|
||||
assert.match(createViewScript, /if \(!hasActiveStewardRuntimeDecisionContext\(runtimeState\)\) \{[\s\S]*return false/)
|
||||
assert.match(createViewScript, /function pushStewardRuntimeUserMessage\(userText = ''\)/)
|
||||
assert.match(createViewScript, /const userMessageAlreadyAdded = options\.skipUserMessage[\s\S]*pushStewardRuntimeUserMessage\(rawText\)/)
|
||||
assert.match(createViewScript, /const fastDecision = buildStewardRuntimeFastPathDecision\(rawText, runtimeState\)[\s\S]*submitStewardPlan\(\{[\s\S]*skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/)
|
||||
assert.match(createViewScript, /executeStewardRuntimeDecision\(fastDecision, rawText, \{ userMessageAlreadyAdded \}\)[\s\S]*if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)/)
|
||||
assert.match(createViewScript, /if \(!shouldUseStewardRuntimeLlmDecision\(rawText, runtimeState\)\)[\s\S]*fetchStewardRuntimeDecision/)
|
||||
assert.match(createViewScript, /executeStewardRuntimeDecision\(decision, rawText, \{ userMessageAlreadyAdded \}\)/)
|
||||
assert.match(createViewScript, /skipUserMessage: userMessageAlreadyAdded \|\| options\.skipUserMessage/)
|
||||
assert.match(createViewScript, /fetchStewardRuntimeDecision\(\{[\s\S]*runtime_state: runtimeState/)
|
||||
assert.match(createViewScript, /if \(await handleStewardRuntimeDecision\(options\)\) \{[\s\S]*return null/)
|
||||
assert.match(createViewScript, /function isApplicationSubmitConfirmationText/)
|
||||
assert.match(createViewScript, /APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN[\s\S]*确认提交[\s\S]*提交审批/)
|
||||
assert.match(createViewScript, /function findPendingApplicationSubmitMessage/)
|
||||
assert.match(createViewScript, /normalizedPreview\.readyToSubmit/)
|
||||
assert.match(createViewScript, /async function handleApplicationSubmitConfirmationText/)
|
||||
assert.match(createViewScript, /await confirmApplicationSubmit\(\{ userText: rawText \}\)/)
|
||||
assert.match(createViewScript, /if \(await handleApplicationSubmitConfirmationText\(options\)\) \{[\s\S]*return null[\s\S]*\}[\s\S]*if \(isStewardSession\.value && !options\.skipStewardPlan/)
|
||||
assert.match(createViewScript, /message\.applicationSubmitConfirmed = true/)
|
||||
assert.match(createViewScript, /message\.applicationSubmitConfirmed[\s\S]*continue/)
|
||||
})
|
||||
|
||||
test('steward streaming uses chunked typewriter to reduce perceived latency', () => {
|
||||
assert.match(stewardPlanFlowScript, /STEWARD_TYPEWRITER_CHUNK_SIZE = 4/)
|
||||
assert.match(stewardPlanFlowScript, /STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE = 5/)
|
||||
assert.match(stewardPlanFlowScript, /index = Math\.min\(total, index \+ STEWARD_TYPEWRITER_CHUNK_SIZE\)/)
|
||||
assert.match(stewardPlanFlowScript, /index = Math\.min\(chars\.length, index \+ STEWARD_THINKING_TYPEWRITER_CHUNK_SIZE\)/)
|
||||
assert.match(submitComposerScript, /STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE = 4/)
|
||||
assert.match(submitComposerScript, /STEWARD_DELEGATED_THINKING_CHUNK_SIZE = 5/)
|
||||
assert.match(submitComposerScript, /index = Math\.min\(chars\.length, index \+ STEWARD_DELEGATED_TYPEWRITER_CHUNK_SIZE\)/)
|
||||
assert.match(submitComposerScript, /index = Math\.min\(chars\.length, index \+ STEWARD_DELEGATED_THINKING_CHUNK_SIZE\)/)
|
||||
assert.match(createViewScript, /STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE = 4/)
|
||||
assert.match(createViewScript, /STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE = 5/)
|
||||
assert.match(createViewScript, /index = Math\.min\(chars\.length, index \+ STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE\)/)
|
||||
assert.match(createViewScript, /index = Math\.min\(chars\.length, index \+ STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE\)/)
|
||||
})
|
||||
|
||||
test('steward initial workbench entry shows recognition state before messages arrive', () => {
|
||||
assert.match(createViewScript, /const hasStewardInitialAutoSubmitPayload = computed/)
|
||||
assert.match(createViewScript, /const showStewardInitialRecognition = computed/)
|
||||
assert.match(createViewScript, /!messages\.value\.length/)
|
||||
assert.match(createViewScript, /workbenchVisible\.value \|\| submitting\.value/)
|
||||
assert.match(createViewScript, /showStewardInitialRecognition/)
|
||||
assert.match(createViewTemplate, /v-if="showStewardInitialRecognition"/)
|
||||
assert.match(createViewTemplate, /class="steward-initial-recognition"/)
|
||||
assert.match(createViewTemplate, /小财管家正在识别意图/)
|
||||
})
|
||||
|
||||
test('steward application carry text does not leak transport examples into extraction', () => {
|
||||
const actions = buildStewardSuggestedActions({
|
||||
plan_id: 'steward-plan-transport-gap',
|
||||
plan_status: 'ready',
|
||||
tasks: [
|
||||
{
|
||||
task_id: 'task-application-beijing',
|
||||
task_type: 'expense_application',
|
||||
title: '北京出差申请',
|
||||
summary: '明天前往北京出差3天,支撑国网仿生产部署',
|
||||
assigned_agent: 'application_assistant',
|
||||
ontology_fields: {
|
||||
expense_type: 'travel',
|
||||
time_range: '2026-06-05 至 2026-06-07',
|
||||
location: '北京',
|
||||
reason: '支撑国网仿生产部署'
|
||||
},
|
||||
missing_fields: ['transport_mode', 'amount', 'attachments', 'employee_no']
|
||||
}
|
||||
],
|
||||
confirmation_groups: [
|
||||
{
|
||||
action_type: 'confirm_create_application',
|
||||
target_task_id: 'task-application-beijing'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const carryText = actions[0]?.payload?.carry_text || ''
|
||||
const currentTask = actions[0]?.payload?.steward_current_task || null
|
||||
assert.match(carryText, /费用类型:差旅/)
|
||||
assert.doesNotMatch(carryText, /费用类型:travel/)
|
||||
assert.match(carryText, /还需要补充:出行方式/)
|
||||
assert.match(carryText, /请先追问上述缺失信息/)
|
||||
assert.doesNotMatch(carryText, /高铁|火车|飞机|轮船|自驾|出租车/)
|
||||
assert.doesNotMatch(carryText, /预计金额|附件\/凭证|员工编号|金额/)
|
||||
assert.equal(currentTask?.task_type, 'expense_application')
|
||||
assert.deepEqual(currentTask?.missing_fields, ['transport_mode'])
|
||||
assert.deepEqual(
|
||||
filterStewardBlockingMissingFields(
|
||||
['transport_type', 'amount', 'attachments', 'employee_no', 'department_name'],
|
||||
'expense_application'
|
||||
),
|
||||
['transport_mode']
|
||||
)
|
||||
assert.deepEqual(
|
||||
filterStewardBlockingMissingFields(['amount', 'attachments'], 'reimbursement'),
|
||||
['amount', 'attachments']
|
||||
)
|
||||
|
||||
const preview = buildLocalApplicationPreview(carryText, { name: '曹笑竹', grade: 'P5' })
|
||||
assert.equal(preview.fields.transportMode, '')
|
||||
assert.equal(preview.missingFields.includes('出行方式'), true)
|
||||
|
||||
assert.match(stewardServiceScript, /fetchStewardSlotDecision/)
|
||||
assert.match(stewardServiceScript, /\/steward\/slot-decisions/)
|
||||
assert.match(submitComposerScript, /fetchStewardApplicationSlotDecision/)
|
||||
assert.match(submitComposerScript, /task_type:\s*'expense_application'/)
|
||||
assert.match(submitComposerScript, /steward_continuation:\s*continuation/)
|
||||
assert.match(createViewScript, /currentTask:\s*actionPayload\.steward_current_task/)
|
||||
})
|
||||
|
||||
test('steward application slot fallback ignores non-blocking application fields', () => {
|
||||
assert.match(submitComposerScript, /APPLICATION_NON_BLOCKING_ONTOLOGY_FIELDS/)
|
||||
assert.match(submitComposerScript, /'attachments'/)
|
||||
assert.match(submitComposerScript, /'employee_no'/)
|
||||
assert.match(submitComposerScript, /'amount'/)
|
||||
assert.match(submitComposerScript, /function formatStewardDecisionUserText/)
|
||||
assert.match(submitComposerScript, /formatStewardDecisionUserText\(decision\.question/)
|
||||
assert.match(submitComposerScript, /formatStewardDecisionUserText\(decision\.rationale/)
|
||||
assert.match(submitComposerScript, /normalizeTransportModeOption\(value \|\| label, ''\)/)
|
||||
assert.match(createViewScript, /normalizeTransportModeOption\(value, ''\)/)
|
||||
assert.equal(normalizeTransportModeOption('高铁', ''), '火车')
|
||||
assert.equal(normalizeTransportModeOption('自驾', ''), '')
|
||||
assert.match(submitComposerScript, /function resolveBlockingApplicationMissingFieldsForSteward/)
|
||||
assert.match(submitComposerScript, /isBlockingApplicationOntologyField\(key\)/)
|
||||
assert.match(submitComposerScript, /canonicalField && !isBlockingApplicationOntologyField\(canonicalField\)/)
|
||||
assert.doesNotMatch(submitComposerScript, /附件\/凭证和员工编号为合规必需字段/)
|
||||
})
|
||||
|
||||
test('flow panel durations use backend timing instead of local preview delay', () => {
|
||||
const flow = createFlowHarness()
|
||||
flow.resetFlowRun({ startedAt: Date.parse('2026-05-29T00:00:00.000Z') })
|
||||
@@ -750,6 +1049,42 @@ test('assistant markdown tables render with component-scoped table styling', ()
|
||||
assert.match(messageItemStyles, /\.message-answer-markdown :deep\(th\),[\s\S]*\.message-answer-markdown :deep\(td\) \{[\s\S]*padding: 8px 10px;/)
|
||||
})
|
||||
|
||||
test('assistant reimbursement recognition copy renders structured markdown sections', () => {
|
||||
const rendered = renderMarkdown([
|
||||
'识别到您希望报销一笔“业务招待费”费用:',
|
||||
'',
|
||||
'基础信息识别结果:',
|
||||
'时间:2026-06-04',
|
||||
'事由:小财管家继续执行剩余任务,请填写报销单:客户接待费用报销。',
|
||||
'',
|
||||
'报销测算参考:',
|
||||
'先以用户填写金额或票据识别金额为基础,再结合费用类型、发生地点、业务事由和规则中心限额进行复核。'
|
||||
].join('\n'))
|
||||
|
||||
assert.match(rendered, /<h3>基础信息识别结果<\/h3>/)
|
||||
assert.match(rendered, /<li><strong>时间<\/strong>:2026-06-04<\/li>/)
|
||||
assert.match(rendered, /<li><strong>事由<\/strong>:小财管家继续执行剩余任务/)
|
||||
assert.match(rendered, /<h3>报销测算参考<\/h3>/)
|
||||
assert.doesNotMatch(rendered, /基础信息识别结果:<\/h3>/)
|
||||
})
|
||||
|
||||
test('application date overlap blocks steward preview before duplicate application table', () => {
|
||||
const existingRange = resolveApplicationDateRange('2026-06-05 至 2026-06-07')
|
||||
const currentRange = resolveApplicationDateRange('2026-06-06 至 2026-06-08')
|
||||
const disjointRange = resolveApplicationDateRange('2026-06-08 至 2026-06-10')
|
||||
|
||||
assert.equal(applicationDateRangesOverlap(currentRange, existingRange), true)
|
||||
assert.equal(applicationDateRangesOverlap(disjointRange, existingRange), false)
|
||||
assert.match(submitComposerScript, /function findOverlappingApplicationClaim\(applicationPreview, claimsPayload\)/)
|
||||
assert.match(submitComposerScript, /function normalizeApplicationExpenseType\(value\)/)
|
||||
assert.match(submitComposerScript, /currentExpenseType !== existingExpenseType/)
|
||||
assert.match(submitComposerScript, /fetchExpenseClaims\(\{ page: 1, pageSize: 100 \}\)/)
|
||||
assert.match(submitComposerScript, /buildApplicationDateConflictMessage\(applicationDateConflict\)/)
|
||||
assert.match(submitComposerScript, /meta: \[STEWARD_ASSISTANT_NAME, '申请日期冲突'\]/)
|
||||
assert.match(submitComposerScript, /applicationPreview: pauseForMissingFields \? null : applicationPreview/)
|
||||
assert.match(createViewScript, /actionType === 'open_application_detail'/)
|
||||
})
|
||||
|
||||
test('application preview merges rule center travel estimate into highlighted rows', () => {
|
||||
const preview = buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-28 去上海出差3天,服务项目部署,火车,预计费用1800元', {
|
||||
name: '李文静',
|
||||
|
||||
Reference in New Issue
Block a user