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

@@ -17,8 +17,15 @@ import {
import {
SESSION_TYPE_APPLICATION,
SESSION_TYPE_BUDGET,
SESSION_TYPE_EXPENSE,
canUseBudgetAssistantSession
} from './travelReimbursementConversationModel.js'
import {
SKIP_REQUIRED_APPLICATION_LINK_ACTION,
SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION,
buildReimbursementAssociationSubmitOptions,
pushReimbursementAssociationPromptMessage
} from './travelReimbursementAssociationGateModel.js'
import { STEWARD_ASSISTANT_NAME } from './useTravelReimbursementStewardRuntime.js'
import {
buildStewardFieldCompletionContinuation,
@@ -40,6 +47,7 @@ export function useTravelReimbursementSuggestedActions({
createMessage,
currentUser,
emit,
fetchExpenseClaims = async () => ({ items: [] }),
handleGuidedShortcut,
handleGuidedSuggestedAction,
handleSceneSelectionApplicationGate,
@@ -224,14 +232,14 @@ export function useTravelReimbursementSuggestedActions({
return true
}
function pushExpenseSceneSelectionPrompt(originalMessage) {
function pushExpenseSceneSelectionPrompt(originalMessage, userEcho = '我要报销') {
const sourceText = String(originalMessage || '').trim()
if (!sourceText) {
return
}
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
messages.value.push(createMessage('user', '我要报销'))
messages.value.push(createMessage('user', String(userEcho || '我要报销').trim() || '我要报销'))
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), [], {
meta: ['等待选择场景'],
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
@@ -240,6 +248,23 @@ export function useTravelReimbursementSuggestedActions({
persistSessionState()
}
async function pushExpenseAssociationGatePrompt(originalMessage, options = {}) {
const sourceText = String(originalMessage || '我要报销').trim() || '我要报销'
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
messages.value.push(createMessage('user', String(options.userText || '我要报销').trim() || '我要报销'))
await pushReimbursementAssociationPromptMessage({
rawText: sourceText,
createMessage,
messages,
nextTick,
scrollToBottom,
persistSessionState,
fetchExpenseClaims,
currentUser,
skipDraftCheck: Boolean(options.skipDraftCheck)
})
}
function applySuggestedActionPrefill(action) {
const prefillText = resolveSuggestedActionPrefill(action)
if (!prefillText) {
@@ -263,6 +288,35 @@ export function useTravelReimbursementSuggestedActions({
if (await handleGuidedSuggestedAction(message, action)) return
if (await handleSceneSelectionApplicationGate(message, action)) return
if (actionType === SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION) {
const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销'
if (!lockSuggestedActionMessage(message, action)) return
await pushExpenseAssociationGatePrompt(originalMessage, {
skipDraftCheck: true,
userText: action?.label || '不用草稿,关联申请单新建报销单'
})
return
}
if (actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION) {
const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销'
if (!lockSuggestedActionMessage(message, action)) return
pushExpenseSceneSelectionPrompt(originalMessage, action?.label || '不关联,单独新建报销单')
return
}
if (actionType === 'select_required_application') {
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
const applicationNo = String(actionPayload.application_claim_no || actionPayload.claim_no || '').trim()
const originalMessage = String(actionPayload.original_message || message?.text || '我要报销').trim() || '我要报销'
if (!lockSuggestedActionMessage(message, action)) return
messages.value.push(createMessage('user', `关联申请单 ${applicationNo || ''}`.trim() || '关联申请单'))
nextTick(scrollToBottom)
persistSessionState()
await submitComposer(buildReimbursementAssociationSubmitOptions(actionPayload, originalMessage))
return
}
if (actionType === APPLICATION_PREVIEW_FIELD_ACTION_SET) {
await applyApplicationPreviewFieldAction(message, action)
return
@@ -340,7 +394,7 @@ export function useTravelReimbursementSuggestedActions({
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
if (!lockSuggestedActionMessage(message, action)) return
if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') {
pushExpenseSceneSelectionPrompt(carryText)
await pushExpenseAssociationGatePrompt(carryText)
return
}
if (String(actionPayload.steward_plan_id || '').trim()) {
@@ -400,7 +454,7 @@ export function useTravelReimbursementSuggestedActions({
const originalMessage = String(action?.payload?.original_message || message?.text || '').trim()
if (!originalMessage) return
if (!lockSuggestedActionMessage(message, action)) return
pushExpenseSceneSelectionPrompt(originalMessage)
await pushExpenseAssociationGatePrompt(originalMessage)
return
}