2026-06-24 10:42:50 +08:00
|
|
|
import {
|
|
|
|
|
REIMBURSEMENT_LIST_PREVIEW_PARAMS,
|
|
|
|
|
fetchExpenseClaims
|
|
|
|
|
} from '../../services/reimbursements.js'
|
2026-06-22 15:55:59 +08:00
|
|
|
import { runOrchestrator } from '../../services/orchestrator.js'
|
2026-06-24 10:42:50 +08:00
|
|
|
import {
|
|
|
|
|
createLinkedReimbursementDraftJob,
|
|
|
|
|
fetchLinkedReimbursementDraftJob
|
|
|
|
|
} from '../../services/linkedReimbursementDraftJobs.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
import {
|
|
|
|
|
applyAiExpenseAnswer,
|
|
|
|
|
buildAiExpenseStepPrompt,
|
|
|
|
|
buildAiExpenseSummary,
|
|
|
|
|
createAiExpenseDraft,
|
|
|
|
|
isAiExpenseDraftComplete
|
|
|
|
|
} from '../../utils/aiExpenseDraftModel.js'
|
|
|
|
|
import {
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
SESSION_TYPE_EXPENSE
|
|
|
|
|
} from '../../views/scripts/travelReimbursementConversationModel.js'
|
|
|
|
|
import { buildExpenseSceneSelectionActions } from '../../utils/expenseAssistantActions.js'
|
|
|
|
|
import {
|
|
|
|
|
buildRequiredApplicationActions,
|
|
|
|
|
buildRequiredApplicationMissingText,
|
|
|
|
|
buildRequiredApplicationSelectionText,
|
2026-06-22 15:55:59 +08:00
|
|
|
filterRequiredApplicationCandidates,
|
|
|
|
|
resolveRequiredApplicationReimbursementType
|
2026-06-22 11:58:53 +08:00
|
|
|
} from '../../views/scripts/travelReimbursementApplicationLinkModel.js'
|
2026-06-22 15:55:59 +08:00
|
|
|
import {
|
|
|
|
|
buildReimbursementAssociationActions,
|
|
|
|
|
buildReimbursementAssociationMissingText,
|
|
|
|
|
buildReimbursementAssociationSubmitOptions,
|
|
|
|
|
buildReimbursementAssociationThinkingEvents,
|
|
|
|
|
buildReimbursementAssociationSelectionText,
|
|
|
|
|
buildReimbursementAssociationQueryFailedText,
|
|
|
|
|
buildReimbursementDraftActions,
|
2026-06-24 10:42:50 +08:00
|
|
|
buildReimbursementDraftContinuationText,
|
2026-06-22 15:55:59 +08:00
|
|
|
buildReimbursementDraftSelectionText,
|
2026-06-24 10:42:50 +08:00
|
|
|
buildStandaloneReimbursementDraftConfirmationActions,
|
|
|
|
|
buildStandaloneReimbursementDraftConfirmationText,
|
|
|
|
|
buildViewReimbursementDraftAction,
|
2026-06-22 15:55:59 +08:00
|
|
|
fetchReimbursementAssociationClaims,
|
|
|
|
|
filterReimbursementAssociationCandidates,
|
|
|
|
|
filterReimbursementDraftCandidates,
|
|
|
|
|
REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS
|
|
|
|
|
} from '../../views/scripts/travelReimbursementAssociationGateModel.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
|
|
|
export { SESSION_TYPE_EXPENSE }
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
const AI_REIMBURSEMENT_ASSOCIATION_STEP_DELAY_MS = 320
|
2026-06-24 10:42:50 +08:00
|
|
|
const LINKED_DRAFT_JOB_POLL_INTERVAL_MS = 1200
|
|
|
|
|
const LINKED_DRAFT_JOB_MAX_POLLS = 100
|
|
|
|
|
const LINKED_DRAFT_JOB_PENDING_STATUSES = new Set(['queued', 'running'])
|
|
|
|
|
const LINKED_DRAFT_RUNNING_PHRASE = '正在后台生成报销草稿'
|
2026-06-22 15:55:59 +08:00
|
|
|
|
|
|
|
|
function waitForReimbursementAssociationStep() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
globalThis.setTimeout(resolve, AI_REIMBURSEMENT_ASSOCIATION_STEP_DELAY_MS)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
export function buildLinkedDraftRunningText(job = {}, claimNo = '') {
|
|
|
|
|
const statusText = String(job?.message || '').trim()
|
|
|
|
|
const shouldShowStatusText = Boolean(
|
|
|
|
|
statusText && !statusText.includes(LINKED_DRAFT_RUNNING_PHRASE)
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
`已关联申请单${claimNo ? ` ${claimNo}` : ''},正在后台生成报销草稿...`,
|
|
|
|
|
shouldShowStatusText ? '' : null,
|
|
|
|
|
shouldShowStatusText ? `处理状态:${statusText}` : null,
|
|
|
|
|
'',
|
|
|
|
|
'您可以先离开当前会话,回来后我会继续查询任务结果。'
|
|
|
|
|
].filter((line) => line !== null).join('\n')
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
export function useWorkbenchAiExpenseFlow({
|
|
|
|
|
activateInlineConversation,
|
|
|
|
|
aiExpenseDraft,
|
|
|
|
|
assistantDraft,
|
|
|
|
|
clearAiModeFiles,
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
conversationMessages,
|
|
|
|
|
conversationStarted,
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
currentUser,
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
pushInlineUserMessage,
|
2026-06-22 15:55:59 +08:00
|
|
|
replaceInlineMessage = (id, nextMessage) => {
|
|
|
|
|
const index = conversationMessages.value.findIndex((item) => item.id === id)
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
conversationMessages.value.push(nextMessage)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conversationMessages.value.splice(index, 1, nextMessage)
|
|
|
|
|
},
|
2026-06-22 11:58:53 +08:00
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
resolveLatestInlineUserPrompt,
|
|
|
|
|
scrollInlineConversationToBottom,
|
2026-06-22 15:55:59 +08:00
|
|
|
startAiApplicationPreview,
|
|
|
|
|
fetchExpenseClaimsForAi = fetchExpenseClaims,
|
|
|
|
|
runOrchestratorForAi = runOrchestrator,
|
2026-06-24 10:42:50 +08:00
|
|
|
createLinkedReimbursementDraftJobForAi = createLinkedReimbursementDraftJob,
|
|
|
|
|
fetchLinkedReimbursementDraftJobForAi = fetchLinkedReimbursementDraftJob,
|
|
|
|
|
linkedDraftJobPollIntervalMs = LINKED_DRAFT_JOB_POLL_INTERVAL_MS,
|
|
|
|
|
linkedDraftJobMaxPolls = LINKED_DRAFT_JOB_MAX_POLLS,
|
2026-06-22 15:55:59 +08:00
|
|
|
associationQueryTimeoutMs = REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS
|
2026-06-22 11:58:53 +08:00
|
|
|
}) {
|
2026-06-22 15:55:59 +08:00
|
|
|
function replaceInlineAssistantMessage(messageId, content = '', options = {}) {
|
|
|
|
|
const nextMessage = createInlineMessage('assistant', content, {
|
|
|
|
|
id: messageId,
|
|
|
|
|
pending: Boolean(options.pending),
|
|
|
|
|
stewardPlan: options.stewardPlan || null,
|
|
|
|
|
suggestedActions: Array.isArray(options.suggestedActions) ? options.suggestedActions : [],
|
|
|
|
|
draftPayload: options.draftPayload || null,
|
2026-06-24 10:42:50 +08:00
|
|
|
linkedReimbursementDraftJob: options.linkedReimbursementDraftJob || null,
|
2026-06-22 15:55:59 +08:00
|
|
|
text: options.text || content
|
|
|
|
|
})
|
|
|
|
|
replaceInlineMessage(messageId, nextMessage)
|
|
|
|
|
return nextMessage
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
function pushInlineExpenseSceneSelectionPrompt(originalMessage, selectedLabel = '') {
|
|
|
|
|
const sourceText = String(originalMessage || '我要报销').trim()
|
|
|
|
|
if (!conversationStarted.value) {
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
title: String(selectedLabel || sourceText || '报销').trim().slice(0, 18) || '报销'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', String(selectedLabel || sourceText).trim()))
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), {
|
|
|
|
|
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startAiApplicationPreviewFromAction(payload = {}, fallbackLabel = '') {
|
|
|
|
|
const expenseType = String(payload.expense_type || '').trim()
|
|
|
|
|
const expenseTypeLabel = String(payload.expense_type_label || fallbackLabel || '').trim()
|
|
|
|
|
return startAiApplicationPreview(
|
|
|
|
|
expenseType,
|
|
|
|
|
expenseTypeLabel,
|
2026-06-26 11:21:16 +08:00
|
|
|
payload.carry_text || resolveLatestInlineUserPrompt(),
|
|
|
|
|
{
|
|
|
|
|
stewardRemainingTasks: Array.isArray(payload.steward_remaining_tasks)
|
|
|
|
|
? payload.steward_remaining_tasks
|
|
|
|
|
: []
|
|
|
|
|
}
|
2026-06-22 11:58:53 +08:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
function normalizeDraftActionPayload(payload = {}) {
|
|
|
|
|
return {
|
|
|
|
|
id: String(payload.id || payload.claim_id || payload.claimId || '').trim(),
|
|
|
|
|
claim_no: String(payload.claim_no || payload.claimNo || '').trim(),
|
|
|
|
|
original_message: String(payload.original_message || payload.originalMessage || '我要报销').trim() || '我要报销'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pushPromptConversationUserMessage(text = '') {
|
|
|
|
|
const normalizedText = String(text || '').trim()
|
|
|
|
|
if (normalizedText) {
|
|
|
|
|
pushInlineUserMessage(normalizedText)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function promptAiReimbursementDraftContinuation(payload = {}) {
|
|
|
|
|
const draft = normalizeDraftActionPayload(payload)
|
|
|
|
|
const claimNo = draft.claim_no || '当前草稿'
|
|
|
|
|
if (!conversationStarted.value) {
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
title: `继续草稿 ${claimNo}`.trim().slice(0, 18) || '继续草稿'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
pushPromptConversationUserMessage(`继续关联草稿 ${claimNo}`)
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildReimbursementDraftContinuationText(draft), {
|
|
|
|
|
meta: ['等待上传附件或说明'],
|
|
|
|
|
suggestedActions: [buildViewReimbursementDraftAction(draft, draft.original_message)]
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function promptStandaloneReimbursementDraftCreation(originalMessage = '我要报销', selectedLabel = '独立新建报销单') {
|
|
|
|
|
const sourceText = String(originalMessage || '我要报销').trim() || '我要报销'
|
|
|
|
|
const userText = String(selectedLabel || '独立新建报销单').trim() || '独立新建报销单'
|
|
|
|
|
if (!conversationStarted.value) {
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
title: userText.slice(0, 18) || '新建报销'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
pushPromptConversationUserMessage(userText)
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildStandaloneReimbursementDraftConfirmationText(), {
|
|
|
|
|
meta: ['等待确认新建草稿'],
|
|
|
|
|
suggestedActions: buildStandaloneReimbursementDraftConfirmationActions(sourceText)
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelStandaloneReimbursementDraftCreation() {
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', '好的,本次先不新建报销草稿。您可以继续查看已有草稿,或补充新的报销说明。', {
|
|
|
|
|
meta: ['已取消新建']
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
async function startAiReimbursementAssociationGate(originalMessage = '我要报销', selectedLabel = '', options = {}) {
|
|
|
|
|
const sourceText = String(originalMessage || '我要报销').trim() || '我要报销'
|
|
|
|
|
if (!conversationStarted.value) {
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
title: String(selectedLabel || sourceText || '报销').trim().slice(0, 18) || '报销'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
clearAiModeFiles()
|
|
|
|
|
aiExpenseDraft.value = null
|
|
|
|
|
pushInlineUserMessage(String(selectedLabel || sourceText).trim())
|
|
|
|
|
const pendingMessage = createInlineMessage('assistant', '', {
|
|
|
|
|
pending: true,
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('intent')
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
conversationMessages.value.push(pendingMessage)
|
|
|
|
|
const pendingMessageId = pendingMessage.id
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
await waitForReimbursementAssociationStep()
|
|
|
|
|
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, '', {
|
|
|
|
|
pending: true,
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('query')
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
|
|
|
|
|
let claims = null
|
|
|
|
|
try {
|
|
|
|
|
claims = await fetchReimbursementAssociationClaims({
|
|
|
|
|
fetchExpenseClaims: fetchExpenseClaimsForAi,
|
|
|
|
|
timeoutMs: associationQueryTimeoutMs
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, buildReimbursementAssociationQueryFailedText(error), {
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'failed',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('failed')
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: buildReimbursementAssociationActions([], sourceText)
|
|
|
|
|
})
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const draftCandidates = options.skipDraftCheck
|
|
|
|
|
? []
|
|
|
|
|
: filterReimbursementDraftCandidates(claims, currentUser.value || {})
|
|
|
|
|
if (draftCandidates.length) {
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, '', {
|
|
|
|
|
pending: true,
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('filter', { candidateCount: draftCandidates.length })
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
await waitForReimbursementAssociationStep()
|
|
|
|
|
|
|
|
|
|
const content = buildReimbursementDraftSelectionText(draftCandidates)
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, content, {
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'completed',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('completed', { candidateCount: draftCandidates.length })
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: buildReimbursementDraftActions(draftCandidates, sourceText)
|
|
|
|
|
})
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidates = filterReimbursementAssociationCandidates(claims, currentUser.value || {})
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, '', {
|
|
|
|
|
pending: true,
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('filter', { candidateCount: candidates.length })
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
await waitForReimbursementAssociationStep()
|
|
|
|
|
|
|
|
|
|
const content = candidates.length
|
|
|
|
|
? buildReimbursementAssociationSelectionText(candidates)
|
|
|
|
|
: buildReimbursementAssociationMissingText()
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, content, {
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
streamStatus: 'completed',
|
|
|
|
|
thinkingEvents: buildReimbursementAssociationThinkingEvents('completed', { candidateCount: candidates.length })
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: buildReimbursementAssociationActions(candidates, sourceText)
|
|
|
|
|
})
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
function startAiExpenseDraft(expenseType, expenseTypeLabel, requiresApplicationBeforeReimbursement) {
|
|
|
|
|
if (!conversationStarted.value) {
|
|
|
|
|
activateInlineConversation({ title: String(expenseTypeLabel || '报销').trim().slice(0, 18) || '报销' })
|
|
|
|
|
}
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
clearAiModeFiles()
|
|
|
|
|
pushInlineUserMessage(`选择${expenseTypeLabel || expenseType || '报销'}`)
|
|
|
|
|
|
|
|
|
|
if (requiresApplicationBeforeReimbursement) {
|
|
|
|
|
void resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const draft = createAiExpenseDraft(expenseType, expenseTypeLabel)
|
|
|
|
|
aiExpenseDraft.value = draft
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildAiExpenseStepPrompt(draft)))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function advanceAiExpenseDraft(answer, files = []) {
|
|
|
|
|
const fileNames = Array.from(files || [])
|
|
|
|
|
pushInlineUserMessage(answer || (fileNames.length ? `上传 ${fileNames.length} 份附件` : ''))
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
clearAiModeFiles()
|
|
|
|
|
|
|
|
|
|
const next = applyAiExpenseAnswer(aiExpenseDraft.value, answer, fileNames)
|
|
|
|
|
aiExpenseDraft.value = next
|
|
|
|
|
|
|
|
|
|
if (isAiExpenseDraftComplete(next)) {
|
2026-06-24 10:42:50 +08:00
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', `${buildAiExpenseSummary(next)}\n\n如果哪一项需要修改,请直接告诉我;确认无误后我再帮您生成报销草稿。`))
|
2026-06-22 11:58:53 +08:00
|
|
|
aiExpenseDraft.value = null
|
|
|
|
|
} else {
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildAiExpenseStepPrompt(next)))
|
|
|
|
|
}
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveAiExpenseApplicationLink(expenseType, expenseTypeLabel) {
|
|
|
|
|
let claims = null
|
|
|
|
|
try {
|
2026-06-24 10:42:50 +08:00
|
|
|
claims = await fetchExpenseClaimsForAi(REIMBURSEMENT_LIST_PREVIEW_PARAMS)
|
2026-06-22 11:58:53 +08:00
|
|
|
} catch {
|
|
|
|
|
aiExpenseDraft.value = null
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', '查询可关联申请单时出现异常,请稍后再试,我先暂停这次报销流程。'))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidates = filterRequiredApplicationCandidates(claims, expenseType, currentUser.value || {})
|
|
|
|
|
aiExpenseDraft.value = createAiExpenseDraft(expenseType, expenseTypeLabel)
|
|
|
|
|
|
|
|
|
|
if (!candidates.length) {
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildRequiredApplicationMissingText(expenseType), {
|
|
|
|
|
suggestedActions: [{
|
|
|
|
|
label: '确认发起出差申请',
|
|
|
|
|
description: '生成完整申请表,并预填已识别的时间、地点和事由',
|
|
|
|
|
icon: 'mdi mdi-file-plus-outline',
|
|
|
|
|
action_type: 'ai_application_start_inline',
|
|
|
|
|
payload: {
|
|
|
|
|
expense_type: expenseType,
|
|
|
|
|
expense_type_label: expenseTypeLabel
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('assistant', buildRequiredApplicationSelectionText(expenseType, candidates), {
|
|
|
|
|
suggestedActions: buildRequiredApplicationActions(candidates, 'select_required_application')
|
|
|
|
|
}))
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
function buildWorkbenchUserContext() {
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
return {
|
|
|
|
|
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
|
|
|
|
is_admin: Boolean(user.isAdmin),
|
|
|
|
|
name: user.name || '',
|
|
|
|
|
role: user.role || '',
|
|
|
|
|
department: user.department || user.departmentName || '',
|
|
|
|
|
department_name: user.department || user.departmentName || '',
|
|
|
|
|
position: user.position || '',
|
|
|
|
|
employee_position: user.position || user.employeePosition || user.employee_position || '',
|
|
|
|
|
employee_no: user.employeeNo || user.employee_no || '',
|
|
|
|
|
employeeNo: user.employeeNo || user.employee_no || '',
|
|
|
|
|
session_type: SESSION_TYPE_EXPENSE,
|
|
|
|
|
entry_source: 'workbench-ai'
|
2026-06-22 11:58:53 +08:00
|
|
|
}
|
2026-06-22 15:55:59 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
function waitForLinkedDraftJobPoll() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
globalThis.setTimeout(resolve, linkedDraftJobPollIntervalMs)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeLinkedDraftJob(job = {}) {
|
|
|
|
|
const jobId = String(job?.job_id || job?.jobId || '').trim()
|
|
|
|
|
if (!jobId) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
jobId,
|
|
|
|
|
status: String(job?.status || 'queued').trim() || 'queued',
|
|
|
|
|
message: String(job?.message || '').trim(),
|
|
|
|
|
error: String(job?.error || '').trim(),
|
|
|
|
|
runId: String(job?.run_id || job?.runId || '').trim(),
|
|
|
|
|
applicationClaimNo: String(job?.application_claim_no || job?.applicationClaimNo || '').trim(),
|
|
|
|
|
draftPayload: job?.draft_payload && typeof job.draft_payload === 'object'
|
|
|
|
|
? job.draft_payload
|
|
|
|
|
: job?.draftPayload && typeof job.draftPayload === 'object'
|
|
|
|
|
? job.draftPayload
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildLinkedDraftFailedText(job = {}) {
|
|
|
|
|
return String(job?.message || job?.error || '').trim()
|
|
|
|
|
|| '生成报销草稿时出现异常。申请单关联信息我先保留在当前会话里,您可以稍后重试,或单独新建报销单。'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const activeLinkedDraftJobPolls = new Set()
|
|
|
|
|
|
|
|
|
|
async function pollLinkedDraftJob({
|
|
|
|
|
jobId,
|
|
|
|
|
pendingMessageId,
|
|
|
|
|
claimNo = '',
|
|
|
|
|
initialJob = null
|
|
|
|
|
}) {
|
|
|
|
|
const normalizedJobId = String(jobId || '').trim()
|
|
|
|
|
if (!normalizedJobId || activeLinkedDraftJobPolls.has(normalizedJobId)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
activeLinkedDraftJobPolls.add(normalizedJobId)
|
|
|
|
|
let currentJob = initialJob ? normalizeLinkedDraftJob(initialJob) : null
|
|
|
|
|
try {
|
|
|
|
|
for (let index = 0; index <= linkedDraftJobMaxPolls; index += 1) {
|
|
|
|
|
if (!currentJob && index > 0) {
|
|
|
|
|
currentJob = normalizeLinkedDraftJob(await fetchLinkedReimbursementDraftJobForAi(normalizedJobId))
|
|
|
|
|
}
|
|
|
|
|
if (currentJob && !LINKED_DRAFT_JOB_PENDING_STATUSES.has(currentJob.status)) {
|
|
|
|
|
if (currentJob.status === 'succeeded') {
|
|
|
|
|
const draftPayload = currentJob.draftPayload || null
|
|
|
|
|
const draftClaimNo = String(draftPayload?.claim_no || draftPayload?.claimNo || '').trim()
|
|
|
|
|
const content = draftClaimNo
|
|
|
|
|
? `报销草稿 ${draftClaimNo} 已生成,并已关联申请单 ${claimNo || '所选申请单'}。后续可以在草稿详情中上传票据或补充信息。`
|
|
|
|
|
: `报销草稿已生成,并已关联申请单 ${claimNo || '所选申请单'}。后续可以在草稿详情中上传票据或补充信息。`
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, content, {
|
|
|
|
|
draftPayload,
|
|
|
|
|
linkedReimbursementDraftJob: {
|
|
|
|
|
...currentJob,
|
|
|
|
|
applicationClaimNo: claimNo
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: buildLinkedDraftAction(draftPayload)
|
|
|
|
|
})
|
|
|
|
|
aiExpenseDraft.value = null
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
throw new Error(buildLinkedDraftFailedText(currentJob))
|
|
|
|
|
}
|
|
|
|
|
if (currentJob) {
|
|
|
|
|
replaceInlineAssistantMessage(pendingMessageId, buildLinkedDraftRunningText(currentJob, claimNo), {
|
|
|
|
|
pending: true,
|
|
|
|
|
linkedReimbursementDraftJob: {
|
|
|
|
|
...currentJob,
|
|
|
|
|
applicationClaimNo: claimNo
|
|
|
|
|
},
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
}
|
|
|
|
|
await waitForLinkedDraftJobPoll()
|
|
|
|
|
currentJob = normalizeLinkedDraftJob(await fetchLinkedReimbursementDraftJobForAi(normalizedJobId))
|
|
|
|
|
}
|
|
|
|
|
throw new Error('报销草稿仍在后台生成中,稍后回到会话会继续刷新结果。')
|
|
|
|
|
} finally {
|
|
|
|
|
activeLinkedDraftJobPolls.delete(normalizedJobId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resumePendingLinkedReimbursementDraftJobs() {
|
|
|
|
|
conversationMessages.value.forEach((message) => {
|
|
|
|
|
const job = normalizeLinkedDraftJob(message.linkedReimbursementDraftJob || null)
|
|
|
|
|
if (!job || !LINKED_DRAFT_JOB_PENDING_STATUSES.has(job.status)) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
void pollLinkedDraftJob({
|
|
|
|
|
jobId: job.jobId,
|
|
|
|
|
pendingMessageId: message.id,
|
|
|
|
|
claimNo: job.applicationClaimNo,
|
|
|
|
|
initialJob: job
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
replaceInlineAssistantMessage(message.id, buildLinkedDraftFailedText(error), {
|
|
|
|
|
linkedReimbursementDraftJob: {
|
|
|
|
|
...job,
|
|
|
|
|
status: 'failed',
|
|
|
|
|
message: error?.message || '报销草稿生成状态查询失败。'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:55:59 +08:00
|
|
|
function buildLinkedDraftAction(draftPayload = {}) {
|
|
|
|
|
const claimNo = String(draftPayload.claim_no || draftPayload.claimNo || '').trim()
|
|
|
|
|
const claimId = String(draftPayload.claim_id || draftPayload.claimId || '').trim()
|
|
|
|
|
if (!claimNo && !claimId) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
return [{
|
|
|
|
|
label: '查看报销草稿',
|
|
|
|
|
description: '打开草稿详情继续上传票据或补充信息。',
|
|
|
|
|
icon: 'mdi mdi-file-document-outline',
|
|
|
|
|
action_type: 'open_application_detail',
|
|
|
|
|
payload: {
|
|
|
|
|
claim_id: claimId,
|
|
|
|
|
claim_no: claimNo
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function linkAiExpenseApplication(application = {}) {
|
|
|
|
|
const draft = aiExpenseDraft.value || (() => {
|
|
|
|
|
const resolved = resolveRequiredApplicationReimbursementType(application)
|
|
|
|
|
return createAiExpenseDraft(resolved.expenseType, resolved.expenseTypeLabel)
|
|
|
|
|
})()
|
2026-06-22 11:58:53 +08:00
|
|
|
const claimNo = String(application.application_claim_no || '').trim()
|
|
|
|
|
pushInlineUserMessage(`关联申请单 ${claimNo}`.trim())
|
|
|
|
|
|
|
|
|
|
const linked = {
|
|
|
|
|
...draft,
|
|
|
|
|
applicationClaim: application,
|
|
|
|
|
values: {
|
|
|
|
|
...draft.values,
|
|
|
|
|
reason: String(application.application_reason || '').trim(),
|
|
|
|
|
location: String(application.application_location || '').trim(),
|
|
|
|
|
time_range: String(application.application_business_time || '').trim(),
|
|
|
|
|
amount: String(application.application_amount_label || application.application_amount || '').trim()
|
|
|
|
|
},
|
|
|
|
|
stepKey: 'attachments'
|
|
|
|
|
}
|
|
|
|
|
aiExpenseDraft.value = linked
|
2026-06-22 15:55:59 +08:00
|
|
|
const pendingMessage = createInlineMessage('assistant', `已关联申请单${claimNo ? ` ${claimNo}` : ''},正在生成报销草稿...`, {
|
|
|
|
|
pending: true,
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
})
|
|
|
|
|
conversationMessages.value.push(pendingMessage)
|
|
|
|
|
const pendingMessageId = pendingMessage.id
|
2026-06-22 11:58:53 +08:00
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
2026-06-22 15:55:59 +08:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const submitOptions = buildReimbursementAssociationSubmitOptions(
|
|
|
|
|
application,
|
|
|
|
|
application.original_message || resolveLatestInlineUserPrompt() || '我要报销'
|
|
|
|
|
)
|
2026-06-24 10:42:50 +08:00
|
|
|
const job = await createLinkedReimbursementDraftJobForAi({
|
|
|
|
|
message: submitOptions.rawText,
|
|
|
|
|
conversation_id: '',
|
|
|
|
|
context_json: {
|
|
|
|
|
...buildWorkbenchUserContext(),
|
|
|
|
|
...submitOptions.extraContext
|
2026-06-22 15:55:59 +08:00
|
|
|
}
|
|
|
|
|
})
|
2026-06-24 10:42:50 +08:00
|
|
|
const normalizedJob = normalizeLinkedDraftJob(job)
|
|
|
|
|
if (!normalizedJob) {
|
|
|
|
|
throw new Error('报销草稿生成任务创建失败,请稍后重试。')
|
|
|
|
|
}
|
|
|
|
|
await pollLinkedDraftJob({
|
|
|
|
|
jobId: normalizedJob.jobId,
|
|
|
|
|
pendingMessageId,
|
|
|
|
|
claimNo,
|
|
|
|
|
initialJob: normalizedJob
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
2026-06-22 15:55:59 +08:00
|
|
|
replaceInlineAssistantMessage(
|
|
|
|
|
pendingMessageId,
|
2026-06-24 10:42:50 +08:00
|
|
|
buildLinkedDraftFailedText(error),
|
2026-06-22 15:55:59 +08:00
|
|
|
{
|
|
|
|
|
suggestedActions: []
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
}
|
2026-06-22 11:58:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
advanceAiExpenseDraft,
|
2026-06-24 10:42:50 +08:00
|
|
|
cancelStandaloneReimbursementDraftCreation,
|
2026-06-22 11:58:53 +08:00
|
|
|
linkAiExpenseApplication,
|
2026-06-24 10:42:50 +08:00
|
|
|
promptAiReimbursementDraftContinuation,
|
|
|
|
|
promptStandaloneReimbursementDraftCreation,
|
2026-06-22 11:58:53 +08:00
|
|
|
pushInlineExpenseSceneSelectionPrompt,
|
2026-06-24 10:42:50 +08:00
|
|
|
resumePendingLinkedReimbursementDraftJobs,
|
2026-06-22 11:58:53 +08:00
|
|
|
startAiApplicationPreviewFromAction,
|
2026-06-22 15:55:59 +08:00
|
|
|
startAiReimbursementAssociationGate,
|
2026-06-22 11:58:53 +08:00
|
|
|
startAiExpenseDraft
|
|
|
|
|
}
|
|
|
|
|
}
|