feat(web): 报销单新增关联申请单门控与草稿检测流程

- 新增 travelReimbursementAssociationGateModel,查询可关联申请单/草稿报销单并生成跳过/选择/单独新建动作,区分差旅费与业务招待费类型
- travelReimbursementApplicationLinkModel 补充 buildLinkedApplicationReferenceIndex/buildRequiredApplicationActions 等关联构建逻辑
- useTravelReimbursementSuggestedActions 接入 select_required_application/skip 系列动作,'我要报销'入口改为先走关联门控
- useWorkbenchAiActionRouter 新增 SKIP_REQUIRED_APPLICATION_LINK/SKIP_REIMBURSEMENT_DRAFT_CHECK 动作分发
- useWorkbenchAiExpenseFlow 暴露 startAiReimbursementAssociationGate,stewardPlanModel 待处理流程适配
- 新增 workbench-ai-action-router、workbench-ai-reimbursement-association-gate 测试并更新 guided-flow、steward-plan 测试
This commit is contained in:
caoxiaozhu
2026-06-22 15:55:59 +08:00
parent aa965da69d
commit ba444a514f
11 changed files with 1756 additions and 25 deletions

View File

@@ -56,6 +56,15 @@ import {
filterRequiredApplicationCandidates,
requiresApplicationBeforeReimbursement
} from '../src/views/scripts/travelReimbursementApplicationLinkModel.js'
import {
SKIP_REQUIRED_APPLICATION_LINK_ACTION,
buildReimbursementAssociationActions,
buildReimbursementAssociationMissingText,
buildReimbursementAssociationQueryPayload,
buildReimbursementAssociationSelectionText,
buildReimbursementAssociationSubmitOptions,
buildReimbursementAssociationThinkingEvents
} from '../src/views/scripts/travelReimbursementAssociationGateModel.js'
import {
ASSISTANT_SCOPE_ACTION_SWITCH,
resolveAssistantScopeGuard
@@ -335,6 +344,28 @@ test('guided reimbursement requires application selection for travel and enterta
assert.equal(actions[0].action_type, GUIDED_ACTION_SELECT_REQUIRED_APPLICATION)
assert.equal(actions[0].payload.application_claim_no, 'AP-202605-001')
const associationActions = buildReimbursementAssociationActions(travelApplications, '我要报销')
assert.equal(associationActions[0].action_type, 'select_required_application')
assert.equal(associationActions[0].payload.application_claim_no, 'AP-202605-001')
assert.equal(associationActions.at(-1).action_type, SKIP_REQUIRED_APPLICATION_LINK_ACTION)
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /单独新建报销单/)
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /ai-document-card-list/)
assert.match(buildReimbursementAssociationSelectionText(travelApplications), /ai-document-card--application/)
assert.match(buildReimbursementAssociationMissingText(), /单独新建报销单/)
const associationQueryPayload = buildReimbursementAssociationQueryPayload(travelApplications)
assert.equal(associationQueryPayload.selectionMode, 'reimbursement_application_association')
assert.equal(associationQueryPayload.records[0].claimNo, 'AP-202605-001')
const completedThinking = buildReimbursementAssociationThinkingEvents('completed', { candidateCount: 1 })
assert.equal(completedThinking[0].title, '判断用户意图')
assert.equal(completedThinking.at(-1).status, 'completed')
const associationSubmitOptions = buildReimbursementAssociationSubmitOptions(
associationActions[0].payload,
'我要报销'
)
assert.equal(associationSubmitOptions.skipDraftAssociationPrompt, true)
assert.equal(associationSubmitOptions.extraContext.expense_scene_selection.application_claim_no, 'AP-202605-001')
assert.equal(associationSubmitOptions.extraContext.review_form_values.application_business_time, '2026-05-20 至 2026-05-23')
let state = waitForGuidedApplicationSelection(createGuidedReimbursementState(), 'travel', travelApplications)
assert.equal(state.stepKey, 'application_selection')
assert.equal(state.applicationCandidates[0].claim_no, 'AP-202605-001')
@@ -459,7 +490,13 @@ test('guided flow is local until final confirmation or collected query handoff',
assert.match(messageHandlersScript, /if \(await handleGuidedComposerSubmit\(options\)\) return null[\s\S]*return submitComposerInternal\(options\)/)
assert.match(suggestedActionsScript, /ASSISTANT_SCOPE_ACTION_SWITCH/)
assert.match(suggestedActionsScript, /actionPayload\.carry_text/)
assert.match(suggestedActionsScript, /targetSessionType === SESSION_TYPE_EXPENSE[\s\S]*carryText === '我要报销'[\s\S]*pushExpenseSceneSelectionPrompt\(carryText\)/)
assert.match(suggestedActionsScript, /targetSessionType === SESSION_TYPE_EXPENSE[\s\S]*carryText === '我要报销'[\s\S]*pushExpenseAssociationGatePrompt\(carryText\)/)
assert.match(suggestedActionsScript, /actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION[\s\S]*pushExpenseSceneSelectionPrompt/)
assert.match(suggestedActionsScript, /actionType === 'confirm_expense_intent'[\s\S]*pushExpenseAssociationGatePrompt\(originalMessage\)/)
assert.match(suggestedActionsScript, /pushReimbursementAssociationPromptMessage\(\{[\s\S]*skipDraftCheck: Boolean\(options\.skipDraftCheck\)/)
assert.match(submitComposerScript, /waitForExpenseSceneSelection[\s\S]*pushReimbursementAssociationPromptMessage\(\{[\s\S]*rawText/)
assert.match(submitComposerScript, /fetchExpenseClaims[\s\S]*currentUser/)
assert.doesNotMatch(submitComposerScript, /if \(waitForExpenseSceneSelection\) \{[\s\S]{0,260}buildExpenseSceneSelectionMessage/)
assert.match(submitComposerScript, /resolveAssistantScopeGuard/)
assert.match(submitComposerScript, /skipScopeGuard/)
assert.match(guidedFlowScript, /submitExistingComposer\(submitOptions\)/)