refactor: enforce 800 line source limits
This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
import {
|
||||
buildApplicationPreviewFooterMessage,
|
||||
buildApplicationPreviewRows,
|
||||
buildLocalApplicationPreviewMessage,
|
||||
normalizeApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import {
|
||||
buildAiApplicationPrecheck,
|
||||
buildAiApplicationSubmitConflictMessage,
|
||||
isAiApplicationPrecheckBlocking
|
||||
} from '../../utils/aiApplicationPrecheckModel.js'
|
||||
import { fetchExpenseClaims } from '../../services/reimbursements.js'
|
||||
import {
|
||||
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
AI_APPLICATION_ACTION_SUBMIT,
|
||||
runAiApplicationPreviewAction
|
||||
} from '../../services/aiApplicationPreviewActions.js'
|
||||
import {
|
||||
buildFailedInlineApplicationSubmitThinkingEvents,
|
||||
buildInitialInlineApplicationSubmitThinkingEvents,
|
||||
buildInlineApplicationDetailAction,
|
||||
buildInlineApplicationPreview,
|
||||
buildInlineApplicationPreviewActionResultText,
|
||||
buildInlineApplicationSubmitPrecheckPayload,
|
||||
buildInlineApplicationSubmitThinkingEvents,
|
||||
completeInlineThinkingEvents,
|
||||
extractInlineApplicationDraftPayload,
|
||||
resolveInlineApplicationPreviewActionFromText
|
||||
} from './workbenchAiApplicationPreviewModel.js'
|
||||
import { AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION } from './workbenchAiMessageModel.js'
|
||||
|
||||
function isApplicationPreviewEstimatePendingPreview(applicationPreview = {}) {
|
||||
const fields = normalizeApplicationPreview(applicationPreview).fields || {}
|
||||
return [
|
||||
fields.transportPolicy,
|
||||
fields.policyEstimate,
|
||||
fields.transportEstimatedAmount,
|
||||
fields.amount
|
||||
].some((value) => /正在|查询中/.test(String(value || '')))
|
||||
}
|
||||
|
||||
function shouldRefreshInlineApplicationPreviewEstimate(fieldKey = '') {
|
||||
return ['transportMode', 'time', 'location', 'days'].includes(String(fieldKey || '').trim())
|
||||
}
|
||||
|
||||
export function useWorkbenchAiApplicationPreviewFlow({
|
||||
activateInlineConversation,
|
||||
applicationPreviewEditor,
|
||||
applicationSubmitConfirmContext,
|
||||
applicationSubmitConfirmOpen,
|
||||
assistantDraft,
|
||||
cancelApplicationPreviewEditor,
|
||||
clearAiModeFiles,
|
||||
closeWorkbenchDatePicker,
|
||||
commitApplicationPreviewEditor: commitBaseApplicationPreviewEditor,
|
||||
conversationId,
|
||||
conversationMessages,
|
||||
conversationStarted,
|
||||
createInlineMessage,
|
||||
currentUser,
|
||||
handleApplicationPreviewEditorKeydown,
|
||||
inlineConversationAutoScrollPinned,
|
||||
isApplicationPreviewEditing,
|
||||
openApplicationPreviewEditor,
|
||||
persistCurrentConversation,
|
||||
pushInlineApplicationActionUserMessage,
|
||||
pushInlineUserMessage,
|
||||
refreshApplicationPreviewEstimate,
|
||||
removeWorkbenchDateTag,
|
||||
replaceInlineMessage,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineThinkingEvents,
|
||||
resolveLatestInlineUserPrompt,
|
||||
scrollInlineConversationToBottom,
|
||||
sending,
|
||||
toast
|
||||
}) {
|
||||
function isApplicationPreviewEstimatePending(message = {}) {
|
||||
return Boolean(message?.applicationPreview && isApplicationPreviewEstimatePendingPreview(message.applicationPreview))
|
||||
}
|
||||
|
||||
function canShowInlineSuggestedActions(message = {}) {
|
||||
return Boolean(message?.suggestedActions?.length) && !isApplicationPreviewEstimatePending(message)
|
||||
}
|
||||
|
||||
function isInlineSuggestedActionDisabled(action = {}, message = {}) {
|
||||
const actionType = String(action?.action_type || '').trim()
|
||||
return (
|
||||
Boolean(action?.disabled) ||
|
||||
(actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION && sending.value) ||
|
||||
(
|
||||
[AI_APPLICATION_ACTION_SAVE_DRAFT, AI_APPLICATION_ACTION_SUBMIT].includes(actionType) &&
|
||||
isApplicationPreviewEstimatePending(message)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewRows(message) {
|
||||
return buildApplicationPreviewRows(message?.applicationPreview || {})
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewMissingFields(message) {
|
||||
return normalizeApplicationPreview(message?.applicationPreview || {}).missingFields || []
|
||||
}
|
||||
|
||||
function resolveInlineApplicationPreviewEditorControl(fieldKey) {
|
||||
const control = resolveApplicationPreviewEditorControl(fieldKey)
|
||||
return control === 'date' ? 'text' : control
|
||||
}
|
||||
|
||||
function buildInlineApplicationPreviewSuggestedActions(applicationPreview = {}, draftPayload = null) {
|
||||
if (isApplicationPreviewEstimatePendingPreview(applicationPreview)) {
|
||||
return []
|
||||
}
|
||||
const normalized = normalizeApplicationPreview(applicationPreview)
|
||||
const actions = [{
|
||||
label: '保存草稿',
|
||||
description: '先保存当前申请表,后续可以继续补充或提交。',
|
||||
icon: 'mdi mdi-content-save-outline',
|
||||
action_type: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||||
payload: { draftPayload }
|
||||
}]
|
||||
if (normalized.readyToSubmit) {
|
||||
actions.push({
|
||||
label: '直接提交',
|
||||
description: '提交前先核查相同日期申请单,确认通过后进入审批流程。',
|
||||
icon: 'mdi mdi-send-check-outline',
|
||||
action_type: AI_APPLICATION_ACTION_SUBMIT,
|
||||
payload: { draftPayload }
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
function syncInlineApplicationPreviewMessageContent(message) {
|
||||
if (!message?.applicationPreview) {
|
||||
return
|
||||
}
|
||||
const nextContent = buildLocalApplicationPreviewMessage(message.applicationPreview)
|
||||
message.content = nextContent
|
||||
message.text = nextContent
|
||||
message.suggestedActions = buildInlineApplicationPreviewSuggestedActions(message.applicationPreview, message.draftPayload)
|
||||
}
|
||||
|
||||
async function commitInlineApplicationPreviewEditor(message) {
|
||||
const shouldLockForEstimate = shouldRefreshInlineApplicationPreviewEstimate(applicationPreviewEditor.value.fieldKey)
|
||||
if (shouldLockForEstimate) {
|
||||
message.suggestedActions = []
|
||||
persistCurrentConversation()
|
||||
}
|
||||
const committed = await commitBaseApplicationPreviewEditor(message)
|
||||
syncInlineApplicationPreviewMessageContent(message)
|
||||
persistCurrentConversation()
|
||||
return committed
|
||||
}
|
||||
|
||||
function handleInlineApplicationPreviewEditorKeydown(event, message) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
void commitInlineApplicationPreviewEditor(message)
|
||||
return
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
cancelApplicationPreviewEditor()
|
||||
return
|
||||
}
|
||||
handleApplicationPreviewEditorKeydown(event, message)
|
||||
}
|
||||
|
||||
function buildInlineApplicationPreviewFooterText(message) {
|
||||
const normalized = normalizeApplicationPreview(message?.applicationPreview || {})
|
||||
if (isApplicationPreviewEstimatePending(message)) {
|
||||
return '费用测算正在同步,请稍等,完成后才能保存草稿或直接提交。'
|
||||
}
|
||||
if (normalized.validationIssues?.length || normalized.missingFields?.length) {
|
||||
return buildApplicationPreviewFooterMessage(normalized)
|
||||
}
|
||||
return '申请核对表已补齐,费用测算已同步。你仍可点击表格继续修改;确认无误后,可以点击下方按钮保存草稿或直接提交,也可以直接回复“保存草稿”或“提交”。'
|
||||
}
|
||||
|
||||
function resolveLatestApplicationPreviewMessage() {
|
||||
return [...conversationMessages.value]
|
||||
.reverse()
|
||||
.find((message) => message.role === 'assistant' && message.applicationPreview)
|
||||
}
|
||||
|
||||
function requestInlineApplicationSubmitConfirmation(targetMessage, options = {}) {
|
||||
applicationSubmitConfirmContext.value = {
|
||||
messageId: String(targetMessage?.id || '').trim(),
|
||||
draftPayload: targetMessage?.draftPayload || options.draftPayload || null,
|
||||
userText: String(options.userText || '直接提交').trim() || '直接提交'
|
||||
}
|
||||
applicationSubmitConfirmOpen.value = true
|
||||
persistCurrentConversation()
|
||||
}
|
||||
|
||||
function cancelInlineApplicationSubmitConfirm() {
|
||||
applicationSubmitConfirmOpen.value = false
|
||||
applicationSubmitConfirmContext.value = null
|
||||
}
|
||||
|
||||
function confirmInlineApplicationSubmit() {
|
||||
const context = applicationSubmitConfirmContext.value || {}
|
||||
applicationSubmitConfirmOpen.value = false
|
||||
applicationSubmitConfirmContext.value = null
|
||||
const sourceMessage = conversationMessages.value.find((message) => message.id === context.messageId)
|
||||
if (!sourceMessage?.applicationPreview) {
|
||||
toast('当前申请表已变化,请重新点击直接提交。')
|
||||
return
|
||||
}
|
||||
void executeInlineApplicationPreviewAction(AI_APPLICATION_ACTION_SUBMIT, sourceMessage, {
|
||||
confirmed: true,
|
||||
skipUserMessage: false,
|
||||
draftPayload: context.draftPayload || null,
|
||||
userText: context.userText || '直接提交'
|
||||
})
|
||||
}
|
||||
|
||||
async function runInlineApplicationSubmitPrecheck(targetMessage, pendingMessage, normalizedPreview, options = {}) {
|
||||
try {
|
||||
const claimsPayload = await fetchExpenseClaims({ page: 1, pageSize: 100 })
|
||||
const precheck = buildAiApplicationPrecheck(normalizedPreview, {
|
||||
claimsPayload: buildInlineApplicationSubmitPrecheckPayload(
|
||||
claimsPayload,
|
||||
targetMessage.draftPayload || options.draftPayload || null
|
||||
),
|
||||
currentUser: currentUser.value || {},
|
||||
expenseType: 'travel'
|
||||
})
|
||||
const thinkingEvents = buildInlineApplicationSubmitThinkingEvents(precheck)
|
||||
const blocked = isAiApplicationPrecheckBlocking(precheck)
|
||||
|
||||
if (blocked) {
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', buildAiApplicationSubmitConflictMessage(normalizedPreview, precheck), {
|
||||
id: pendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents
|
||||
}
|
||||
})
|
||||
)
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
return false
|
||||
}
|
||||
|
||||
const message = conversationMessages.value.find((item) => item.id === pendingMessage.id) || pendingMessage
|
||||
message.content = '提交前核查通过,正在提交申请并进入审批流程...'
|
||||
message.paragraphs = ['提交前核查通过,正在提交申请并进入审批流程...']
|
||||
message.stewardPlan = {
|
||||
...(message.stewardPlan || {}),
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents
|
||||
}
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
return true
|
||||
} catch (error) {
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', [
|
||||
'### 提交前核查失败',
|
||||
'系统未能完成相同日期申请单查询,所以本次申请没有提交。',
|
||||
'请稍后重试;如果仍然失败,请先到单据中心核对是否已有同日期申请单。'
|
||||
].join('\n\n'), {
|
||||
id: pendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'failed',
|
||||
thinkingEvents: buildFailedInlineApplicationSubmitThinkingEvents(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
toast('提交前核查失败,已暂停提交。')
|
||||
persistCurrentConversation()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function executeInlineApplicationPreviewAction(actionType, sourceMessage = null, options = {}) {
|
||||
const targetMessage = sourceMessage?.applicationPreview ? sourceMessage : resolveLatestApplicationPreviewMessage()
|
||||
if (!targetMessage?.applicationPreview) {
|
||||
toast('当前没有可提交的申请表。')
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizedPreview = normalizeApplicationPreview(targetMessage.applicationPreview)
|
||||
const isSubmit = actionType === AI_APPLICATION_ACTION_SUBMIT
|
||||
const userText = String(options.userText || (isSubmit ? '直接提交' : '保存草稿')).trim()
|
||||
|
||||
if (isSubmit && !normalizedPreview.readyToSubmit) {
|
||||
if (!options.skipUserMessage) {
|
||||
pushInlineApplicationActionUserMessage(userText)
|
||||
}
|
||||
const missingText = normalizedPreview.missingFields?.length
|
||||
? `当前还缺少:${normalizedPreview.missingFields.join('、')}。`
|
||||
: ''
|
||||
const validationText = normalizedPreview.validationIssues?.length
|
||||
? normalizedPreview.validationIssues.map((item) => item.message).join(';')
|
||||
: ''
|
||||
conversationMessages.value.push(createInlineMessage('assistant', [
|
||||
'### 暂不能提交申请',
|
||||
missingText || validationText || '当前申请表还未通过提交校验。',
|
||||
'请先点击表格中的字段补充或修正,补齐后我会开放“直接提交”入口。'
|
||||
].filter(Boolean).join('\n\n')))
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom()
|
||||
return true
|
||||
}
|
||||
|
||||
if (isSubmit && !options.confirmed) {
|
||||
requestInlineApplicationSubmitConfirmation(targetMessage, { ...options, userText })
|
||||
return true
|
||||
}
|
||||
|
||||
if (!options.skipUserMessage) {
|
||||
pushInlineApplicationActionUserMessage(userText)
|
||||
}
|
||||
|
||||
sending.value = true
|
||||
const pendingMessage = createInlineMessage(
|
||||
'assistant',
|
||||
isSubmit ? '正在提交前核查相同日期申请单...' : '正在保存申请草稿...',
|
||||
{
|
||||
pending: true,
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: isSubmit
|
||||
? buildInitialInlineApplicationSubmitThinkingEvents()
|
||||
: [
|
||||
{
|
||||
eventId: 'application-save-draft',
|
||||
title: '保存申请草稿',
|
||||
content: '正在按当前申请表内容保存草稿。',
|
||||
status: 'running'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
)
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
scrollInlineConversationToBottom()
|
||||
|
||||
try {
|
||||
if (isSubmit) {
|
||||
const precheckPassed = await runInlineApplicationSubmitPrecheck(
|
||||
targetMessage,
|
||||
pendingMessage,
|
||||
normalizedPreview,
|
||||
options
|
||||
)
|
||||
if (!precheckPassed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const payload = await runAiApplicationPreviewAction({
|
||||
actionType,
|
||||
applicationPreview: normalizedPreview,
|
||||
currentUser: currentUser.value || {},
|
||||
conversationId: conversationId.value,
|
||||
draftPayload: targetMessage.draftPayload || options.draftPayload || null
|
||||
})
|
||||
const draftPayload = extractInlineApplicationDraftPayload(payload)
|
||||
if (draftPayload) {
|
||||
targetMessage.draftPayload = draftPayload
|
||||
}
|
||||
targetMessage.suggestedActions = []
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', buildInlineApplicationPreviewActionResultText(actionType, payload), {
|
||||
id: pendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
||||
},
|
||||
suggestedActions: isSubmit ? buildInlineApplicationDetailAction(draftPayload) : []
|
||||
})
|
||||
)
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
return true
|
||||
} catch (error) {
|
||||
replaceInlineMessage(
|
||||
pendingMessage.id,
|
||||
createInlineMessage('assistant', error?.message || (isSubmit ? '申请提交失败,请稍后重试。' : '申请草稿保存失败,请稍后重试。'), {
|
||||
id: pendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'failed',
|
||||
thinkingEvents: resolveInlineThinkingEvents(pendingMessage).map((item) => ({
|
||||
...item,
|
||||
status: 'failed'
|
||||
}))
|
||||
}
|
||||
})
|
||||
)
|
||||
toast(error?.message || (isSubmit ? '申请提交失败。' : '申请草稿保存失败。'))
|
||||
persistCurrentConversation()
|
||||
return true
|
||||
} finally {
|
||||
sending.value = false
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
}
|
||||
}
|
||||
|
||||
function handleInlineApplicationPreviewTextAction(prompt, applicationPreviewEstimatePending) {
|
||||
if (applicationPreviewEstimatePending.value) {
|
||||
toast('请等待费用测算完成后再继续操作。')
|
||||
return true
|
||||
}
|
||||
const actionType = resolveInlineApplicationPreviewActionFromText(prompt)
|
||||
if (!actionType || !resolveLatestApplicationPreviewMessage()) {
|
||||
return false
|
||||
}
|
||||
void executeInlineApplicationPreviewAction(actionType, null, { userText: prompt })
|
||||
return true
|
||||
}
|
||||
|
||||
async function startAiApplicationPreview(expenseType, expenseTypeLabel, sourceText = '', options = {}) {
|
||||
if (!conversationStarted.value) {
|
||||
activateInlineConversation({ title: String(expenseTypeLabel || expenseType || '申请').trim().slice(0, 18) || '申请' })
|
||||
}
|
||||
const previewSourceText = String(sourceText || resolveLatestInlineUserPrompt()).trim()
|
||||
assistantDraft.value = ''
|
||||
removeWorkbenchDateTag()
|
||||
closeWorkbenchDatePicker()
|
||||
clearAiModeFiles()
|
||||
if (options.pushUserMessage !== false) {
|
||||
pushInlineUserMessage(options.userMessage || '确认发起出差申请')
|
||||
}
|
||||
|
||||
const pendingMessage = createInlineMessage('assistant', '正在生成申请核对表,请稍等...', {
|
||||
pending: true,
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: [
|
||||
{
|
||||
eventId: 'application-preview-build',
|
||||
title: '整理申请表字段',
|
||||
content: '正在把已识别的时间、地点、事由和差旅类型整理成可编辑表格。',
|
||||
status: 'running'
|
||||
},
|
||||
{
|
||||
eventId: 'application-preview-estimate',
|
||||
title: '同步费用测算',
|
||||
content: '正在刷新差旅规则和费用测算,完成后会直接展示核对表。',
|
||||
status: 'pending'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
conversationMessages.value.push(pendingMessage)
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom()
|
||||
|
||||
try {
|
||||
const preview = await refreshApplicationPreviewEstimate(
|
||||
buildInlineApplicationPreview(expenseTypeLabel || expenseType, previewSourceText, currentUser.value || {})
|
||||
)
|
||||
const content = buildLocalApplicationPreviewMessage(preview)
|
||||
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', content, {
|
||||
id: pendingMessage.id,
|
||||
applicationPreview: preview,
|
||||
suggestedActions: buildInlineApplicationPreviewSuggestedActions(preview),
|
||||
stewardPlan: {
|
||||
streamStatus: 'completed',
|
||||
thinkingEvents: completeInlineThinkingEvents(resolveInlineThinkingEvents(pendingMessage))
|
||||
},
|
||||
text: content
|
||||
}))
|
||||
} catch (error) {
|
||||
replaceInlineMessage(pendingMessage.id, createInlineMessage('assistant', error?.message || '申请核对表生成失败,请稍后重试。', {
|
||||
id: pendingMessage.id,
|
||||
stewardPlan: {
|
||||
streamStatus: 'failed',
|
||||
thinkingEvents: resolveInlineThinkingEvents(pendingMessage).map((item) => ({
|
||||
...item,
|
||||
status: 'failed'
|
||||
}))
|
||||
}
|
||||
}))
|
||||
toast(error?.message || '申请核对表生成失败。')
|
||||
} finally {
|
||||
persistCurrentConversation()
|
||||
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buildInlineApplicationPreviewFooterText,
|
||||
buildInlineApplicationPreviewSuggestedActions,
|
||||
canShowInlineSuggestedActions,
|
||||
cancelInlineApplicationSubmitConfirm,
|
||||
commitInlineApplicationPreviewEditor,
|
||||
confirmInlineApplicationSubmit,
|
||||
executeInlineApplicationPreviewAction,
|
||||
handleInlineApplicationPreviewEditorKeydown,
|
||||
handleInlineApplicationPreviewTextAction,
|
||||
isApplicationPreviewEditing,
|
||||
isApplicationPreviewEstimatePending,
|
||||
isInlineSuggestedActionDisabled,
|
||||
openApplicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorOptions,
|
||||
resolveInlineApplicationPreviewEditorControl,
|
||||
resolveInlineApplicationPreviewMissingFields,
|
||||
resolveInlineApplicationPreviewRows,
|
||||
startAiApplicationPreview
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user