2026-06-22 11:58:53 +08:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
|
import { useSystemState } from '../useSystemState.js'
|
|
|
|
|
|
import { useToast } from '../useToast.js'
|
|
|
|
|
|
import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js'
|
|
|
|
|
|
import { fetchSettings } from '../../services/settings.js'
|
2026-06-25 10:55:49 +08:00
|
|
|
|
import { calculateTravelReimbursement, fetchExpenseClaimDetail } from '../../services/reimbursements.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
|
import { useApplicationPreviewEditor } from '../../views/scripts/useApplicationPreviewEditor.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
deleteAiWorkbenchConversation,
|
|
|
|
|
|
loadAiWorkbenchConversationHistory,
|
2026-06-25 10:55:49 +08:00
|
|
|
|
markAiWorkbenchConversationDocumentDeleted,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
saveAiWorkbenchConversation
|
|
|
|
|
|
} from '../../utils/aiWorkbenchConversationStore.js'
|
|
|
|
|
|
import { renderAiConversationHtml } from '../../utils/aiConversationHtmlRenderer.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildAiDocumentDetailRequest,
|
|
|
|
|
|
parseAiApplicationDetailHref,
|
|
|
|
|
|
parseAiDocumentDetailHref
|
|
|
|
|
|
} from '../../utils/aiDocumentDetailReference.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
AI_MODE_ACTION_ITEMS,
|
|
|
|
|
|
shouldRunAiAttachmentAutoAssociation
|
|
|
|
|
|
} from './workbenchAiComposerModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
createWorkbenchAiMessageRuntime,
|
2026-06-23 11:21:18 +08:00
|
|
|
|
formatMessageTime
|
2026-06-22 11:58:53 +08:00
|
|
|
|
} from './workbenchAiMessageModel.js'
|
|
|
|
|
|
import { useWorkbenchAiActionRouter } from './useWorkbenchAiActionRouter.js'
|
|
|
|
|
|
import { useWorkbenchAiAttachmentAssociationFlow } from './useWorkbenchAiAttachmentAssociationFlow.js'
|
|
|
|
|
|
import { useWorkbenchAiApplicationPreviewFlow } from './useWorkbenchAiApplicationPreviewFlow.js'
|
|
|
|
|
|
import { useWorkbenchAiComposerFiles } from './useWorkbenchAiComposerFiles.js'
|
|
|
|
|
|
import { useWorkbenchAiDocumentQueryFlow } from './useWorkbenchAiDocumentQueryFlow.js'
|
|
|
|
|
|
import { useWorkbenchAiExpenseFlow } from './useWorkbenchAiExpenseFlow.js'
|
2026-06-24 10:42:50 +08:00
|
|
|
|
import { useWorkbenchAiFilePreview } from './useWorkbenchAiFilePreview.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
|
import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js'
|
2026-06-23 11:21:18 +08:00
|
|
|
|
import { useWorkbenchAiMessageExpansion } from './useWorkbenchAiMessageExpansion.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
|
import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js'
|
|
|
|
|
|
import { useWorkbenchAiStewardFlow } from './useWorkbenchAiStewardFlow.js'
|
2026-06-24 21:58:46 +08:00
|
|
|
|
import {
|
|
|
|
|
|
isReimbursementCreationIntent
|
|
|
|
|
|
} from './workbenchAiApplicationGateModel.js'
|
2026-06-24 22:58:59 +08:00
|
|
|
|
import { useWorkbenchAiCommandIntents } from './useWorkbenchAiCommandIntents.js'
|
2026-06-24 21:58:46 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildRuleFallbackWorkbenchAiIntentPlan,
|
2026-06-25 10:55:49 +08:00
|
|
|
|
isLowConfidenceTravelApplicationPlan,
|
2026-06-24 21:58:46 +08:00
|
|
|
|
normalizeWorkbenchAiIntentPlan,
|
|
|
|
|
|
resolveExecutableTravelApplicationPlan,
|
|
|
|
|
|
shouldRequestWorkbenchAiIntentPlan
|
|
|
|
|
|
} from './workbenchAiIntentPlannerModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildInitialModelPlanningThinkingEvents,
|
|
|
|
|
|
buildModelPlanningProgressSchedule,
|
|
|
|
|
|
mergeWorkbenchAiThinkingEvents
|
|
|
|
|
|
} from './workbenchAiPlanningThinkingModel.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
|
|
|
|
|
const AI_SEARCH_CONVERSATION_ID = 'ai-search'
|
|
|
|
|
|
const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
|
|
|
|
|
|
const INLINE_ANSWER_STREAM_DELAY_MS = 24
|
|
|
|
|
|
const INLINE_AUTO_SCROLL_THRESHOLD = 96
|
|
|
|
|
|
const INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS = 260
|
|
|
|
|
|
|
|
|
|
|
|
export function usePersonalWorkbenchAiMode(props, emit) {
|
|
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
|
const assistantDraft = ref('')
|
|
|
|
|
|
const assistantInputRef = ref(null)
|
|
|
|
|
|
const fileInputRef = ref(null)
|
|
|
|
|
|
const conversationScrollRef = ref(null)
|
|
|
|
|
|
const inlineConversationAutoScrollPinned = ref(true)
|
|
|
|
|
|
const selectedFiles = ref([])
|
|
|
|
|
|
const systemSettings = ref(null)
|
|
|
|
|
|
const conversationStarted = ref(false)
|
|
|
|
|
|
const conversationMessages = ref([])
|
|
|
|
|
|
const conversationId = ref('')
|
|
|
|
|
|
const activeConversationTitle = ref('')
|
|
|
|
|
|
const sending = ref(false)
|
|
|
|
|
|
const stewardState = ref(null)
|
|
|
|
|
|
const aiExpenseDraft = ref(null)
|
|
|
|
|
|
const thinkingExpandedMessageIds = ref(new Set())
|
|
|
|
|
|
const thinkingCollapsedMessageIds = ref(new Set())
|
|
|
|
|
|
const attachmentOcrExpandedMessageIds = ref(new Set())
|
|
|
|
|
|
const deleteDialogOpen = ref(false)
|
|
|
|
|
|
const applicationSubmitConfirmOpen = ref(false)
|
|
|
|
|
|
const applicationSubmitConfirmContext = ref(null)
|
|
|
|
|
|
const aiAttachmentAssociationRuntime = new Map()
|
|
|
|
|
|
const messageRuntime = createWorkbenchAiMessageRuntime()
|
|
|
|
|
|
const {
|
|
|
|
|
|
createAiAttachmentAssociationId,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
normalizeRuntimeMessage,
|
|
|
|
|
|
serializeRuntimeMessage
|
|
|
|
|
|
} = messageRuntime
|
|
|
|
|
|
const {
|
|
|
|
|
|
applicationPreviewEditor,
|
|
|
|
|
|
resolveApplicationPreviewEditorControl,
|
2026-06-23 09:42:13 +08:00
|
|
|
|
resolveApplicationPreviewEditorDateMax,
|
|
|
|
|
|
resolveApplicationPreviewEditorDateMin,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
resolveApplicationPreviewEditorOptions,
|
|
|
|
|
|
refreshApplicationPreviewEstimate,
|
|
|
|
|
|
isApplicationPreviewEditing,
|
|
|
|
|
|
openApplicationPreviewEditor,
|
|
|
|
|
|
commitApplicationPreviewEditor,
|
|
|
|
|
|
cancelApplicationPreviewEditor,
|
|
|
|
|
|
handleApplicationPreviewEditorKeydown
|
|
|
|
|
|
} = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState: () => persistCurrentConversation(),
|
|
|
|
|
|
toast,
|
|
|
|
|
|
calculateTravelReimbursement,
|
|
|
|
|
|
currentUser
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
workbenchDatePickerOpen,
|
|
|
|
|
|
workbenchDateMode,
|
|
|
|
|
|
workbenchSingleDate,
|
|
|
|
|
|
workbenchRangeStartDate,
|
|
|
|
|
|
workbenchRangeEndDate,
|
|
|
|
|
|
workbenchDateTagLabel,
|
|
|
|
|
|
workbenchCanApplyDateSelection,
|
|
|
|
|
|
clearWorkbenchDateSelection,
|
|
|
|
|
|
toggleWorkbenchDatePicker,
|
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
|
setWorkbenchDateMode,
|
|
|
|
|
|
handleWorkbenchDatePickerOutside,
|
|
|
|
|
|
applyWorkbenchDateSelection,
|
|
|
|
|
|
handleWorkbenchDateInputChange,
|
|
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
buildWorkbenchPromptText
|
|
|
|
|
|
} = useWorkbenchComposerDate({ draft: assistantDraft, focusInput: focusAiModeInput })
|
|
|
|
|
|
|
|
|
|
|
|
const aiModeActionItems = AI_MODE_ACTION_ITEMS
|
|
|
|
|
|
const displayUserName = computed(() => {
|
|
|
|
|
|
const user = currentUser.value || {}
|
|
|
|
|
|
return String(user.name || user.username || '同事').trim() || '同事'
|
|
|
|
|
|
})
|
|
|
|
|
|
const displayModelName = computed(() => {
|
|
|
|
|
|
const llmForm = systemSettings.value?.llmForm
|
|
|
|
|
|
if (!llmForm) return 'Axiom Ultra 3.1'
|
|
|
|
|
|
const model = llmForm.mainModel || ''
|
|
|
|
|
|
const provider = llmForm.mainProvider || ''
|
|
|
|
|
|
if (!model) return 'Axiom Ultra 3.1'
|
|
|
|
|
|
return provider ? `${provider} / ${model}` : model
|
|
|
|
|
|
})
|
|
|
|
|
|
const modelSelectorTitle = computed(() => {
|
|
|
|
|
|
const llmForm = systemSettings.value?.llmForm
|
|
|
|
|
|
if (!llmForm) return '当前模型:Axiom Ultra 3.1'
|
|
|
|
|
|
const model = llmForm.mainModel || 'Axiom Ultra 3.1'
|
|
|
|
|
|
const provider = llmForm.mainProvider || ''
|
|
|
|
|
|
return provider ? `当前模型:${provider} / ${model}` : `当前模型:${model}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const filesFlow = useWorkbenchAiComposerFiles({
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
focusAiModeInput,
|
|
|
|
|
|
isInputLocked: () => isAiModeInputLocked.value,
|
2026-06-24 10:42:50 +08:00
|
|
|
|
resolveInputLockedMessage: () => resolveAiModeInputLockMessage(),
|
2026-06-22 11:58:53 +08:00
|
|
|
|
selectedFiles,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const documentQueryFlow = useWorkbenchAiDocumentQueryFlow({
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
replaceInlineMessage,
|
|
|
|
|
|
scrollInlineConversationToBottom
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
|
const commandIntents = useWorkbenchAiCommandIntents({
|
|
|
|
|
|
activateInlineConversation,
|
|
|
|
|
|
activeConversationTitle,
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
clearAiModeFiles: filesFlow.clearAiModeFiles,
|
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
documentQueryFlow,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
searchConversationId: AI_SEARCH_CONVERSATION_ID,
|
|
|
|
|
|
sending
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
const attachmentFlow = useWorkbenchAiAttachmentAssociationFlow({
|
|
|
|
|
|
aiAttachmentAssociationRuntime,
|
2026-06-24 10:42:50 +08:00
|
|
|
|
conversationId,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
conversationMessages,
|
|
|
|
|
|
createAiAttachmentAssociationId,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
replaceInlineMessage,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
sending,
|
|
|
|
|
|
streamOrSetInlineAssistantContent,
|
2026-06-23 09:42:13 +08:00
|
|
|
|
notifyRequestUpdated: (payload) => emit('request-updated', payload),
|
2026-06-22 11:58:53 +08:00
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
const filePreview = useWorkbenchAiFilePreview({
|
|
|
|
|
|
attachmentFlow,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
selectedFiles
|
2026-06-23 09:42:13 +08:00
|
|
|
|
})
|
2026-06-23 11:21:18 +08:00
|
|
|
|
const {
|
|
|
|
|
|
hasInlineAttachmentOcrDetails,
|
|
|
|
|
|
hasInlineThinking,
|
|
|
|
|
|
isInlineAttachmentOcrExpanded,
|
|
|
|
|
|
isInlineThinkingExpanded,
|
|
|
|
|
|
resolveInlineAttachmentOcrDocuments,
|
|
|
|
|
|
resolveInlineAttachmentOcrFileCount,
|
|
|
|
|
|
resolveInlineThinkingEvents,
|
|
|
|
|
|
toggleInlineAttachmentOcrDetails,
|
|
|
|
|
|
toggleInlineThinking
|
|
|
|
|
|
} = useWorkbenchAiMessageExpansion({
|
|
|
|
|
|
attachmentOcrExpandedMessageIds,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
thinkingCollapsedMessageIds,
|
|
|
|
|
|
thinkingExpandedMessageIds
|
|
|
|
|
|
})
|
2026-06-23 09:42:13 +08:00
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
const applicationFlow = useWorkbenchAiApplicationPreviewFlow({
|
|
|
|
|
|
activateInlineConversation,
|
|
|
|
|
|
applicationPreviewEditor,
|
|
|
|
|
|
applicationSubmitConfirmContext,
|
|
|
|
|
|
applicationSubmitConfirmOpen,
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
cancelApplicationPreviewEditor,
|
|
|
|
|
|
clearAiModeFiles: filesFlow.clearAiModeFiles,
|
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
|
commitApplicationPreviewEditor,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
handleApplicationPreviewEditorKeydown,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
isApplicationPreviewEditing,
|
|
|
|
|
|
openApplicationPreviewEditor,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
pushInlineApplicationActionUserMessage,
|
|
|
|
|
|
pushInlineUserMessage,
|
|
|
|
|
|
refreshApplicationPreviewEstimate,
|
|
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
replaceInlineMessage,
|
2026-06-23 09:42:13 +08:00
|
|
|
|
resolveApplicationPreviewEditorDateMax,
|
|
|
|
|
|
resolveApplicationPreviewEditorDateMin,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
resolveApplicationPreviewEditorControl,
|
|
|
|
|
|
resolveApplicationPreviewEditorOptions,
|
|
|
|
|
|
resolveInlineThinkingEvents,
|
|
|
|
|
|
resolveLatestInlineUserPrompt,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
sending,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const expenseFlow = useWorkbenchAiExpenseFlow({
|
|
|
|
|
|
activateInlineConversation,
|
|
|
|
|
|
aiExpenseDraft,
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
clearAiModeFiles: filesFlow.clearAiModeFiles,
|
|
|
|
|
|
closeWorkbenchDatePicker,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
pushInlineUserMessage,
|
2026-06-22 15:56:06 +08:00
|
|
|
|
replaceInlineMessage,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
resolveLatestInlineUserPrompt,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
startAiApplicationPreview: applicationFlow.startAiApplicationPreview
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const actionRouter = useWorkbenchAiActionRouter({
|
|
|
|
|
|
aiExpenseDraft,
|
|
|
|
|
|
applicationFlow,
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
attachmentFlow,
|
2026-06-24 21:58:46 +08:00
|
|
|
|
conversationMessages,
|
|
|
|
|
|
createInlineMessage,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
emit,
|
|
|
|
|
|
expenseFlow,
|
|
|
|
|
|
focusAiModeInput,
|
|
|
|
|
|
hasInlineAttachmentOcrDetails,
|
2026-06-24 21:58:46 +08:00
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
replaceInlineMessage,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
resolveLatestInlineUserPrompt,
|
2026-06-24 21:58:46 +08:00
|
|
|
|
scrollInlineConversationToBottom,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
selectedFiles,
|
|
|
|
|
|
startInlineConversation,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
toggleInlineAttachmentOcrDetails
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const sessionCommands = useWorkbenchAiSessionCommands({
|
|
|
|
|
|
activeConversationTitle,
|
|
|
|
|
|
attachmentOcrExpandedMessageIds,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
deleteAiWorkbenchConversation,
|
|
|
|
|
|
emit,
|
|
|
|
|
|
focusAiModeInput,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
normalizeRuntimeMessage,
|
|
|
|
|
|
refreshConversationHistory,
|
|
|
|
|
|
resetInlineConversationState,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
stewardState,
|
|
|
|
|
|
thinkingCollapsedMessageIds,
|
|
|
|
|
|
thinkingExpandedMessageIds,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const messageActions = useWorkbenchAiMessageActions({
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
focusAiModeInput,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const stewardFlow = useWorkbenchAiStewardFlow({
|
|
|
|
|
|
activeConversationTitle,
|
|
|
|
|
|
collectAiModeReceiptContext: attachmentFlow.collectAiModeReceiptContext,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
createInlineMessage,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
deleteAiWorkbenchConversation,
|
|
|
|
|
|
emit,
|
|
|
|
|
|
handleAiDocumentQueryIntent: documentQueryFlow.handleAiDocumentQueryIntent,
|
|
|
|
|
|
inlineConversationAutoScrollPinned,
|
|
|
|
|
|
persistCurrentConversation,
|
|
|
|
|
|
replaceInlineMessage,
|
|
|
|
|
|
resolveInlineThinkingEvents,
|
|
|
|
|
|
scrollInlineConversationToBottom,
|
|
|
|
|
|
sending,
|
|
|
|
|
|
stewardState,
|
|
|
|
|
|
streamInlineAssistantContent,
|
|
|
|
|
|
updateInlineMessageContent,
|
|
|
|
|
|
appendInlineMessageContent,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const applicationPreviewEstimatePending = computed(() => (
|
|
|
|
|
|
conversationMessages.value.some((message) => applicationFlow.isApplicationPreviewEstimatePending(message))
|
|
|
|
|
|
))
|
2026-06-24 10:42:50 +08:00
|
|
|
|
const isAiModeReceiptRecognitionPending = computed(() => attachmentFlow.hasPendingAiModeReceiptRecognition(selectedFiles.value))
|
|
|
|
|
|
const hasAiModeReceiptRecognitionFailure = computed(() => attachmentFlow.hasFailedAiModeReceiptRecognition(selectedFiles.value))
|
|
|
|
|
|
const isAiModeInputLocked = computed(() => applicationPreviewEstimatePending.value || isAiModeReceiptRecognitionPending.value)
|
|
|
|
|
|
const aiModeInputLockMessage = computed(() => resolveAiModeInputLockMessage())
|
2026-06-22 11:58:53 +08:00
|
|
|
|
const canSubmitAiModePrompt = computed(() => (
|
2026-06-24 10:42:50 +08:00
|
|
|
|
!isAiModeInputLocked.value &&
|
|
|
|
|
|
!hasAiModeReceiptRecognitionFailure.value && (
|
2026-06-22 11:58:53 +08:00
|
|
|
|
Boolean(assistantDraft.value.trim()) ||
|
|
|
|
|
|
selectedFiles.value.length > 0 ||
|
|
|
|
|
|
Boolean(workbenchDateTagLabel.value)
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSystemSettings() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
systemSettings.value = await fetchSettings()
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
systemSettings.value = { llmForm: {} }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function focusAiModeInput() {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
assistantInputRef.value?.focus()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-23 11:21:18 +08:00
|
|
|
|
function setAssistantInputRef(element) {
|
|
|
|
|
|
assistantInputRef.value = element
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
function isInlineConversationNearBottom() {
|
|
|
|
|
|
const el = conversationScrollRef.value
|
|
|
|
|
|
if (!el) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return el.scrollHeight - el.clientHeight - el.scrollTop <= INLINE_AUTO_SCROLL_THRESHOLD
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleInlineConversationScroll() {
|
|
|
|
|
|
inlineConversationAutoScrollPinned.value = isInlineConversationNearBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function forceInlineConversationToBottom() {
|
|
|
|
|
|
const el = conversationScrollRef.value
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.scrollTop = el.scrollHeight
|
|
|
|
|
|
inlineConversationAutoScrollPinned.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollInlineConversationToBottom(options = {}) {
|
|
|
|
|
|
const shouldScroll = options.force !== false
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (!shouldScroll) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
forceInlineConversationToBottom()
|
|
|
|
|
|
window.requestAnimationFrame(() => {
|
|
|
|
|
|
forceInlineConversationToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
|
if (inlineConversationAutoScrollPinned.value) {
|
|
|
|
|
|
forceInlineConversationToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, INLINE_LAYOUT_SETTLE_SCROLL_DELAY_MS)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollInlineConversationToTop() {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
const el = conversationScrollRef.value
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
inlineConversationAutoScrollPinned.value = false
|
|
|
|
|
|
el.scrollTo({ top: 0, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateInlineMessageContent(message, content) {
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
message.content = String(content || '')
|
|
|
|
|
|
message.paragraphs = String(message.content || '')
|
|
|
|
|
|
.split(/\n{2,}|\n/)
|
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function appendInlineMessageContent(message, delta) {
|
|
|
|
|
|
const nextDelta = String(delta || '')
|
|
|
|
|
|
if (!nextDelta) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
updateInlineMessageContent(message, `${message.content || ''}${nextDelta}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function waitInlineAnswerStreamFrame() {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
window.setTimeout(resolve, INLINE_ANSWER_STREAM_DELAY_MS)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function streamInlineAssistantContent(messageId, content) {
|
|
|
|
|
|
const targetContent = String(content || '').trim()
|
|
|
|
|
|
let streamedContent = ''
|
|
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < targetContent.length; index += INLINE_ANSWER_STREAM_CHUNK_SIZE) {
|
|
|
|
|
|
const message = conversationMessages.value.find((item) => item.id === messageId)
|
|
|
|
|
|
if (!message || !message.pending) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const shouldAutoScroll = inlineConversationAutoScrollPinned.value
|
|
|
|
|
|
streamedContent += targetContent.slice(index, index + INLINE_ANSWER_STREAM_CHUNK_SIZE)
|
|
|
|
|
|
updateInlineMessageContent(message, streamedContent)
|
|
|
|
|
|
scrollInlineConversationToBottom({ force: shouldAutoScroll })
|
|
|
|
|
|
await waitInlineAnswerStreamFrame()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function streamOrSetInlineAssistantContent(messageId, content) {
|
|
|
|
|
|
const targetContent = String(content || '').trim()
|
|
|
|
|
|
if (/<!--\s*ai-trusted-html:start\s*-->/.test(targetContent)) {
|
|
|
|
|
|
const message = conversationMessages.value.find((item) => item.id === messageId)
|
|
|
|
|
|
if (message?.pending) {
|
|
|
|
|
|
updateInlineMessageContent(message, targetContent)
|
|
|
|
|
|
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await streamInlineAssistantContent(messageId, targetContent)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function refreshConversationHistory() {
|
|
|
|
|
|
const history = loadAiWorkbenchConversationHistory(currentUser.value || {})
|
|
|
|
|
|
emit('conversation-history-change', history)
|
|
|
|
|
|
return history
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isPersistableInlineConversation() {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
conversationId.value &&
|
|
|
|
|
|
conversationId.value !== AI_SEARCH_CONVERSATION_ID &&
|
|
|
|
|
|
conversationMessages.value.length
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function persistCurrentConversation() {
|
|
|
|
|
|
if (!isPersistableInlineConversation()) {
|
|
|
|
|
|
refreshConversationHistory()
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const history = saveAiWorkbenchConversation(currentUser.value || {}, {
|
|
|
|
|
|
id: conversationId.value,
|
|
|
|
|
|
conversationId: conversationId.value,
|
|
|
|
|
|
title: activeConversationTitle.value,
|
|
|
|
|
|
source: 'workbench',
|
|
|
|
|
|
sessionType: 'steward',
|
|
|
|
|
|
stewardState: stewardState.value,
|
|
|
|
|
|
messages: conversationMessages.value.map((message) => serializeRuntimeMessage(message))
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('conversation-history-change', history)
|
|
|
|
|
|
return history
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resetInlineConversationState() {
|
|
|
|
|
|
conversationStarted.value = false
|
|
|
|
|
|
conversationMessages.value = []
|
|
|
|
|
|
conversationId.value = ''
|
|
|
|
|
|
stewardState.value = null
|
|
|
|
|
|
activeConversationTitle.value = ''
|
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
|
thinkingExpandedMessageIds.value = new Set()
|
|
|
|
|
|
thinkingCollapsedMessageIds.value = new Set()
|
|
|
|
|
|
attachmentOcrExpandedMessageIds.value = new Set()
|
|
|
|
|
|
deleteDialogOpen.value = false
|
|
|
|
|
|
applicationSubmitConfirmOpen.value = false
|
|
|
|
|
|
applicationSubmitConfirmContext.value = null
|
|
|
|
|
|
clearWorkbenchDateSelection()
|
|
|
|
|
|
filesFlow.clearAiModeFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function activateInlineConversation(options = {}) {
|
|
|
|
|
|
conversationStarted.value = true
|
|
|
|
|
|
if (!conversationId.value) {
|
|
|
|
|
|
conversationId.value = options.id || `inline-${Date.now()}`
|
|
|
|
|
|
}
|
|
|
|
|
|
activeConversationTitle.value = options.title || activeConversationTitle.value || '新对话'
|
|
|
|
|
|
emit('conversation-change', { id: conversationId.value, title: activeConversationTitle.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
function renderInlineConversationHtml(content) { return renderAiConversationHtml(content) }
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
2026-06-25 10:55:49 +08:00
|
|
|
|
function isUnavailableDocumentDetailError(error) {
|
|
|
|
|
|
const message = String(error?.message || '').trim()
|
|
|
|
|
|
return /not\s*found|不存在|已删除|不可访问|无权|forbidden|404/i.test(message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDocumentDetailLookupClaimId(detailRequest = {}) {
|
|
|
|
|
|
return String(detailRequest.claimId || detailRequest.claim_id || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function markActiveAiDocumentDetailLinkDeleted(detailRequest = {}) {
|
|
|
|
|
|
const nextHistory = markAiWorkbenchConversationDocumentDeleted(currentUser.value || {}, {
|
|
|
|
|
|
claimId: detailRequest.claimId,
|
|
|
|
|
|
claim_id: detailRequest.claimId,
|
|
|
|
|
|
claimNo: detailRequest.claimNo,
|
|
|
|
|
|
claim_no: detailRequest.claimNo,
|
|
|
|
|
|
documentNo: detailRequest.documentNo,
|
|
|
|
|
|
document_no: detailRequest.documentNo,
|
|
|
|
|
|
id: detailRequest.id
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('conversation-history-change', nextHistory)
|
|
|
|
|
|
const activeConversation = nextHistory.find((item) => (
|
|
|
|
|
|
String(item.id || item.conversationId || '').trim() === String(conversationId.value || '').trim()
|
|
|
|
|
|
))
|
|
|
|
|
|
if (activeConversation?.messages?.length) {
|
|
|
|
|
|
conversationMessages.value = activeConversation.messages.map((message) => normalizeRuntimeMessage(message))
|
|
|
|
|
|
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function ensureAiDocumentDetailStillAvailable(detailRequest = {}) {
|
|
|
|
|
|
const claimId = resolveDocumentDetailLookupClaimId(detailRequest)
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await fetchExpenseClaimDetail(claimId)
|
|
|
|
|
|
return true
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (!isUnavailableDocumentDetailError(error)) {
|
|
|
|
|
|
console.warn('AI document detail availability check failed, continuing navigation:', error)
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
markActiveAiDocumentDetailLinkDeleted(detailRequest)
|
|
|
|
|
|
toast('该单据已经删除或不可访问,已将这条历史入口标记为不可查看。')
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
function buildInlinePromptText(rawPrompt, files = []) {
|
|
|
|
|
|
const prompt = buildWorkbenchPromptText(rawPrompt)
|
2026-06-24 10:42:50 +08:00
|
|
|
|
if (prompt) return prompt
|
2026-06-22 11:58:53 +08:00
|
|
|
|
return files.length ? '请帮我处理已上传的附件。' : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
function resolveAiModeInputLockMessage() {
|
|
|
|
|
|
if (isAiModeReceiptRecognitionPending.value) {
|
|
|
|
|
|
return '附件识别中,请稍等...'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (applicationPreviewEstimatePending.value) {
|
|
|
|
|
|
return '费用测算中,请稍等...'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveAiModeSubmitBlockedMessage() {
|
|
|
|
|
|
if (applicationPreviewEstimatePending.value) {
|
|
|
|
|
|
return '请等待费用测算完成后再继续操作。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isAiModeReceiptRecognitionPending.value) {
|
|
|
|
|
|
return '附件 OCR 识别中,请稍等,识别完成后再继续对话。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hasAiModeReceiptRecognitionFailure.value) {
|
|
|
|
|
|
return '请先移除识别失败的附件或重新上传。'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureAiModeCanStartConversation() {
|
|
|
|
|
|
const blockedMessage = resolveAiModeSubmitBlockedMessage()
|
|
|
|
|
|
if (!blockedMessage) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
toast(blockedMessage)
|
|
|
|
|
|
focusAiModeInput()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
function isModelPlannedReimbursementTask(modelPlan = {}) {
|
|
|
|
|
|
const tasks = Array.isArray(modelPlan?.tasks) ? modelPlan.tasks : []
|
|
|
|
|
|
return tasks.some((task) => {
|
|
|
|
|
|
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
|
|
|
|
|
const assignedAgent = String(task?.assigned_agent || task?.assignedAgent || '').trim()
|
|
|
|
|
|
return taskType === 'reimbursement' || assignedAgent === 'reimbursement_assistant'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateModelPlanningThinkingEvent(messageId, event) {
|
|
|
|
|
|
const message = conversationMessages.value.find((item) => item.id === messageId)
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const currentPlan = message.stewardPlan || {}
|
|
|
|
|
|
message.stewardPlan = {
|
|
|
|
|
|
...currentPlan,
|
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
|
thinkingEvents: mergeWorkbenchAiThinkingEvents(resolveInlineThinkingEvents(message), [event])
|
|
|
|
|
|
}
|
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
|
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startModelPlanningProgressUpdates(messageId) {
|
|
|
|
|
|
const timerIds = buildModelPlanningProgressSchedule().map(({ delayMs, event }) => (
|
|
|
|
|
|
globalThis.setTimeout(() => {
|
|
|
|
|
|
updateModelPlanningThinkingEvent(messageId, event)
|
|
|
|
|
|
}, delayMs)
|
|
|
|
|
|
))
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
timerIds.forEach((timerId) => globalThis.clearTimeout(timerId))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startModelPlanningConversation(cleanPrompt, entry = {}) {
|
|
|
|
|
|
if (conversationId.value === AI_SEARCH_CONVERSATION_ID) {
|
|
|
|
|
|
conversationId.value = ''
|
|
|
|
|
|
conversationMessages.value = []
|
|
|
|
|
|
activeConversationTitle.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
|
title: entry.label || cleanPrompt.slice(0, 18) || '新对话'
|
|
|
|
|
|
})
|
|
|
|
|
|
inlineConversationAutoScrollPinned.value = true
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', cleanPrompt))
|
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
|
filesFlow.clearAiModeFiles()
|
|
|
|
|
|
const pendingMessage = createInlineMessage('assistant', '正在识别意图,准备拆解申请、报销和附件任务。', {
|
|
|
|
|
|
pending: true,
|
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
|
streamStatus: 'streaming',
|
|
|
|
|
|
thinkingEvents: buildInitialModelPlanningThinkingEvents()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
conversationMessages.value.push(pendingMessage)
|
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
|
return pendingMessage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage = null) {
|
|
|
|
|
|
void applicationFlow.startAiApplicationPreview(
|
|
|
|
|
|
travelApplicationRequest.expenseType,
|
|
|
|
|
|
travelApplicationRequest.expenseTypeLabel,
|
|
|
|
|
|
travelApplicationRequest.sourceText,
|
|
|
|
|
|
{
|
|
|
|
|
|
userMessage: travelApplicationRequest.sourceText,
|
|
|
|
|
|
pushUserMessage: !plannerPendingMessage,
|
|
|
|
|
|
pendingMessageId: plannerPendingMessage?.id,
|
|
|
|
|
|
ontologyFields: travelApplicationRequest.ontologyFields,
|
|
|
|
|
|
autoSubmit: travelApplicationRequest.autoSubmit,
|
2026-06-25 10:55:49 +08:00
|
|
|
|
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
|
|
|
|
|
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
|
|
|
|
|
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
2026-06-24 21:58:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 10:55:49 +08:00
|
|
|
|
function startModelPlannedTravelApplicationConfirmation(travelApplicationRequest, plan, plannerPendingMessage) {
|
|
|
|
|
|
const confirmText = buildLowConfidenceTravelApplicationConfirmationText(travelApplicationRequest, plan)
|
|
|
|
|
|
const confirmAction = {
|
|
|
|
|
|
label: '确认发起出差申请',
|
|
|
|
|
|
description: '根据上面识别到的信息生成出差申请预览。',
|
|
|
|
|
|
icon: 'mdi mdi-check-circle-outline',
|
|
|
|
|
|
action_type: 'ai_application_confirm_intent',
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
ontologyFields: travelApplicationRequest.ontologyFields,
|
|
|
|
|
|
sourceText: travelApplicationRequest.sourceText,
|
|
|
|
|
|
autoSubmit: travelApplicationRequest.autoSubmit,
|
|
|
|
|
|
autoSaveDraft: travelApplicationRequest.autoSaveDraft,
|
|
|
|
|
|
requestedSubmit: travelApplicationRequest.requestedSubmit,
|
|
|
|
|
|
submitRequiresConfirmation: travelApplicationRequest.submitRequiresConfirmation
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
replaceInlineMessage(plannerPendingMessage.id, createInlineMessage('assistant', confirmText, {
|
|
|
|
|
|
id: plannerPendingMessage.id,
|
|
|
|
|
|
suggestedActions: [confirmAction],
|
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
|
streamStatus: 'completed',
|
|
|
|
|
|
thinkingEvents: resolveInlineThinkingEvents(plannerPendingMessage).map((item) => ({ ...item, status: 'completed' }))
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
|
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildLowConfidenceTravelApplicationConfirmationText(request, plan) {
|
|
|
|
|
|
const fields = request.ontologyFields || {}
|
|
|
|
|
|
const summaryParts = []
|
|
|
|
|
|
if (fields.time_range) {
|
|
|
|
|
|
summaryParts.push(`时间:${fields.time_range}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fields.location) {
|
|
|
|
|
|
summaryParts.push(`地点:${fields.location}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fields.reason) {
|
|
|
|
|
|
summaryParts.push(`事由:${fields.reason}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fields.transport_mode) {
|
|
|
|
|
|
summaryParts.push(`交通:${fields.transport_mode}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
const summary = summaryParts.length ? `\n\n${summaryParts.join(';')}` : ''
|
|
|
|
|
|
const confidenceNote = Number.isFinite(Number(plan?.confidence))
|
|
|
|
|
|
? `(模型识别置信度较低,约 ${Math.round(Number(plan.confidence) * 100)}%)`
|
|
|
|
|
|
: '(模型识别置信度较低)'
|
|
|
|
|
|
return [
|
|
|
|
|
|
'### 需要确认:您是要发起出差申请吗?',
|
|
|
|
|
|
'',
|
|
|
|
|
|
`小财管家把这句话理解成了“发起差旅申请”${confidenceNote},为避免误操作,先请您确认。`,
|
|
|
|
|
|
summary,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'点击下方「确认发起出差申请」即可继续;如果理解有误,请补充说明您的实际需求。'
|
|
|
|
|
|
].filter(Boolean).join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
async function executeModelPlannedWorkbenchIntent(cleanPrompt, entry = {}, files = []) {
|
|
|
|
|
|
let intentPlan = null
|
|
|
|
|
|
let modelPlan = null
|
|
|
|
|
|
const plannerPendingMessage = startModelPlanningConversation(cleanPrompt, entry)
|
|
|
|
|
|
const stopPlanningProgressUpdates = startModelPlanningProgressUpdates(plannerPendingMessage.id)
|
|
|
|
|
|
sending.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
modelPlan = await stewardFlow.resolveInlineExecutionPlan(cleanPrompt, entry, files, {
|
|
|
|
|
|
pendingMessageId: plannerPendingMessage.id
|
|
|
|
|
|
})
|
|
|
|
|
|
intentPlan = normalizeWorkbenchAiIntentPlan(modelPlan, { prompt: cleanPrompt })
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('AI mode intent planner failed, using local fallback:', error)
|
|
|
|
|
|
const rulePlan = buildRuleFallbackWorkbenchAiIntentPlan(cleanPrompt)
|
|
|
|
|
|
const ruleRequest = resolveExecutableTravelApplicationPlan(rulePlan)
|
|
|
|
|
|
if (ruleRequest) {
|
|
|
|
|
|
sending.value = false
|
|
|
|
|
|
startModelPlannedApplicationPreview(ruleRequest, plannerPendingMessage)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
stopPlanningProgressUpdates()
|
|
|
|
|
|
sending.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const travelApplicationRequest = resolveExecutableTravelApplicationPlan(intentPlan)
|
|
|
|
|
|
if (travelApplicationRequest) {
|
2026-06-25 10:55:49 +08:00
|
|
|
|
if (isLowConfidenceTravelApplicationPlan(intentPlan)) {
|
|
|
|
|
|
startModelPlannedTravelApplicationConfirmation(travelApplicationRequest, intentPlan, plannerPendingMessage)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-24 21:58:46 +08:00
|
|
|
|
startModelPlannedApplicationPreview(travelApplicationRequest, plannerPendingMessage)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isModelPlannedReimbursementTask(modelPlan) || isReimbursementCreationIntent(cleanPrompt)) {
|
|
|
|
|
|
replaceInlineMessage(plannerPendingMessage.id, createInlineMessage('assistant', '已识别为报销任务,正在进入报销流程。', {
|
|
|
|
|
|
id: plannerPendingMessage.id,
|
|
|
|
|
|
stewardPlan: {
|
|
|
|
|
|
streamStatus: 'completed',
|
|
|
|
|
|
thinkingEvents: resolveInlineThinkingEvents(plannerPendingMessage).map((item) => ({ ...item, status: 'completed' }))
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || cleanPrompt)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files, { pendingMessageId: plannerPendingMessage.id })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 10:55:49 +08:00
|
|
|
|
async function handleAiAnswerMarkdownClick(event) {
|
2026-06-22 11:58:53 +08:00
|
|
|
|
const target = event?.target
|
|
|
|
|
|
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
|
|
|
|
|
if (!link) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const href = link.getAttribute('href')
|
|
|
|
|
|
const detailReference = parseAiDocumentDetailHref(href) || parseAiApplicationDetailHref(href)
|
|
|
|
|
|
if (!detailReference) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
event.stopPropagation()
|
2026-06-25 10:55:49 +08:00
|
|
|
|
const detailRequest = buildAiDocumentDetailRequest(detailReference)
|
|
|
|
|
|
if (!(await ensureAiDocumentDetailStillAvailable(detailRequest))) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
emit('open-document', detailRequest)
|
2026-06-22 11:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startInlineConversation(prompt, entry = {}, files = []) {
|
2026-06-24 10:42:50 +08:00
|
|
|
|
if (!ensureAiModeCanStartConversation()) {
|
2026-06-22 11:58:53 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const cleanPrompt = buildInlinePromptText(prompt, files)
|
|
|
|
|
|
if (!cleanPrompt || sending.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
|
if (commandIntents.handleInlineDraftDeletionIntent(cleanPrompt, entry)) {
|
2026-06-22 11:58:53 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (applicationFlow.handleInlineApplicationPreviewTextAction(cleanPrompt, applicationPreviewEstimatePending)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 22:58:59 +08:00
|
|
|
|
if (aiExpenseDraft.value && !isAiExpenseDraftComplete(aiExpenseDraft.value)) {
|
|
|
|
|
|
expenseFlow.advanceAiExpenseDraft(cleanPrompt, files)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 21:58:46 +08:00
|
|
|
|
if (shouldRequestWorkbenchAiIntentPlan(cleanPrompt)) {
|
|
|
|
|
|
void executeModelPlannedWorkbenchIntent(cleanPrompt, entry, files)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 15:56:06 +08:00
|
|
|
|
if (isReimbursementCreationIntent(cleanPrompt)) {
|
|
|
|
|
|
void expenseFlow.startAiReimbursementAssociationGate(cleanPrompt, entry.label || '我要报销')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
|
if (conversationId.value === AI_SEARCH_CONVERSATION_ID) {
|
|
|
|
|
|
conversationId.value = ''
|
|
|
|
|
|
conversationMessages.value = []
|
|
|
|
|
|
activeConversationTitle.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sending.value = true
|
|
|
|
|
|
activateInlineConversation({
|
|
|
|
|
|
title: entry.label || cleanPrompt.slice(0, 18) || '新对话'
|
|
|
|
|
|
})
|
|
|
|
|
|
inlineConversationAutoScrollPinned.value = true
|
|
|
|
|
|
conversationMessages.value.push(createInlineMessage('user', cleanPrompt))
|
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
|
filesFlow.clearAiModeFiles()
|
|
|
|
|
|
scrollInlineConversationToBottom()
|
|
|
|
|
|
persistCurrentConversation()
|
|
|
|
|
|
if (shouldRunAiAttachmentAutoAssociation(entry, files, cleanPrompt)) {
|
|
|
|
|
|
void attachmentFlow.requestAiAttachmentAssociationReply(cleanPrompt, entry, files)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
void stewardFlow.requestInlineAssistantReply(cleanPrompt, entry, files)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function submitAiModePrompt() {
|
2026-06-24 10:42:50 +08:00
|
|
|
|
if (!ensureAiModeCanStartConversation()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-22 11:58:53 +08:00
|
|
|
|
if (!canSubmitAiModePrompt.value) {
|
|
|
|
|
|
toast('请输入需求后再发送。')
|
|
|
|
|
|
focusAiModeInput()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
startInlineConversation(assistantDraft.value, { source: 'workbench', sessionType: 'steward' }, Array.from(selectedFiles.value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function runAiModeAction(item) {
|
2026-06-24 10:42:50 +08:00
|
|
|
|
if (!ensureAiModeCanStartConversation()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-22 11:58:53 +08:00
|
|
|
|
if (String(item?.label || '').trim() === '发起报销') {
|
2026-06-22 15:56:06 +08:00
|
|
|
|
void expenseFlow.startAiReimbursementAssociationGate(item.prompt, item.label)
|
2026-06-22 11:58:53 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
startInlineConversation(item.prompt, item, Array.from(selectedFiles.value))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function regenerateLastReply() {
|
|
|
|
|
|
const lastUserMessage = [...conversationMessages.value].reverse().find((message) => message.role === 'user')
|
|
|
|
|
|
if (!lastUserMessage || sending.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const lastAssistantIndex = conversationMessages.value.map((message) => message.role).lastIndexOf('assistant')
|
|
|
|
|
|
if (lastAssistantIndex >= 0) {
|
|
|
|
|
|
conversationMessages.value.splice(lastAssistantIndex, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
sending.value = true
|
|
|
|
|
|
void stewardFlow.requestInlineAssistantReply(lastUserMessage.content, { source: 'workbench', sessionType: 'steward' }, [])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 10:42:50 +08:00
|
|
|
|
function pushInlineUserMessage(text) { conversationMessages.value.push(createInlineMessage('user', String(text || '').trim())) }
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
|
|
|
|
|
function pushInlineApplicationActionUserMessage(text) {
|
|
|
|
|
|
pushInlineUserMessage(text)
|
|
|
|
|
|
assistantDraft.value = ''
|
|
|
|
|
|
removeWorkbenchDateTag()
|
|
|
|
|
|
closeWorkbenchDatePicker()
|
|
|
|
|
|
filesFlow.clearAiModeFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveLatestInlineUserPrompt() {
|
2026-06-24 10:42:50 +08:00
|
|
|
|
return String([...conversationMessages.value].reverse().find((message) => message.role === 'user')?.content || '').trim()
|
2026-06-22 11:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleVoiceInput() {
|
2026-06-24 10:42:50 +08:00
|
|
|
|
if (!ensureAiModeCanStartConversation()) {
|
2026-06-22 11:58:53 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
toast('语音输入正在准备中,您可以先输入文字需求。')
|
|
|
|
|
|
focusAiModeInput()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.sidebarCommand?.seq,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
const command = props.sidebarCommand || {}
|
|
|
|
|
|
if (command.type === 'new-chat') {
|
|
|
|
|
|
sessionCommands.startNewInlineConversation()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (command.type === 'search-chat') {
|
|
|
|
|
|
sessionCommands.openInlineSearchConversation(activateInlineConversation)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (command.type === 'open-recent') {
|
|
|
|
|
|
sessionCommands.openInlineRecentConversation(command.payload || {})
|
2026-06-24 10:42:50 +08:00
|
|
|
|
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
|
|
|
|
|
|
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
|
2026-06-22 11:58:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadSystemSettings()
|
|
|
|
|
|
refreshConversationHistory()
|
2026-06-24 10:42:50 +08:00
|
|
|
|
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
|
|
|
|
|
|
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
|
2026-06-22 11:58:53 +08:00
|
|
|
|
document.addEventListener('click', handleWorkbenchDatePickerOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleWorkbenchDatePickerOutside)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
activeConversationTitle,
|
|
|
|
|
|
aiModeActionItems,
|
2026-06-24 10:42:50 +08:00
|
|
|
|
aiModeInputLockMessage,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
applicationPreviewEditor,
|
|
|
|
|
|
applicationSubmitConfirmOpen,
|
|
|
|
|
|
assistantInputRef,
|
|
|
|
|
|
assistantDraft,
|
|
|
|
|
|
canShowInlineSuggestedActions: applicationFlow.canShowInlineSuggestedActions,
|
|
|
|
|
|
canSubmitAiModePrompt,
|
|
|
|
|
|
cancelDeleteConversation: () => sessionCommands.cancelDeleteConversation(deleteDialogOpen),
|
|
|
|
|
|
cancelInlineApplicationSubmitConfirm: applicationFlow.cancelInlineApplicationSubmitConfirm,
|
|
|
|
|
|
clearWorkbenchDateSelection,
|
|
|
|
|
|
commitInlineApplicationPreviewEditor: applicationFlow.commitInlineApplicationPreviewEditor,
|
|
|
|
|
|
confirmDeleteConversation: () => sessionCommands.confirmDeleteConversation(deleteDialogOpen),
|
|
|
|
|
|
confirmInlineApplicationSubmit: applicationFlow.confirmInlineApplicationSubmit,
|
|
|
|
|
|
conversationMessages,
|
|
|
|
|
|
conversationScrollRef,
|
|
|
|
|
|
conversationStarted,
|
|
|
|
|
|
deleteDialogOpen,
|
|
|
|
|
|
displayModelName,
|
|
|
|
|
|
displayUserName,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
handleAiAnswerMarkdownClick,
|
|
|
|
|
|
handleAiModeFilesChange: filesFlow.handleAiModeFilesChange,
|
|
|
|
|
|
handleInlineApplicationPreviewEditorKeydown: applicationFlow.handleInlineApplicationPreviewEditorKeydown,
|
|
|
|
|
|
handleInlineConversationScroll,
|
|
|
|
|
|
handleInlineSuggestedAction: actionRouter.handleInlineSuggestedAction,
|
|
|
|
|
|
handleVoiceInput,
|
|
|
|
|
|
hasInlineAttachmentOcrDetails,
|
|
|
|
|
|
hasInlineThinking,
|
|
|
|
|
|
isAiModeInputLocked,
|
|
|
|
|
|
isApplicationPreviewEditing: applicationFlow.isApplicationPreviewEditing,
|
|
|
|
|
|
isApplicationPreviewEstimatePending: applicationFlow.isApplicationPreviewEstimatePending,
|
|
|
|
|
|
isInlineAttachmentOcrExpanded,
|
|
|
|
|
|
isInlineSuggestedActionDisabled: applicationFlow.isInlineSuggestedActionDisabled,
|
|
|
|
|
|
isInlineThinkingExpanded,
|
|
|
|
|
|
markInlineMessageFeedback: messageActions.markInlineMessageFeedback,
|
|
|
|
|
|
modelSelectorTitle,
|
|
|
|
|
|
openApplicationPreviewEditor: applicationFlow.openApplicationPreviewEditor,
|
|
|
|
|
|
quoteInlineMessage: messageActions.quoteInlineMessage,
|
|
|
|
|
|
regenerateLastReply,
|
|
|
|
|
|
removeAiModeFile: filesFlow.removeAiModeFile,
|
|
|
|
|
|
removeWorkbenchDateTag,
|
|
|
|
|
|
renderInlineConversationHtml,
|
|
|
|
|
|
requestDeleteCurrentConversation: () => sessionCommands.requestDeleteCurrentConversation(deleteDialogOpen),
|
|
|
|
|
|
resolveApplicationPreviewEditorOptions: applicationFlow.resolveApplicationPreviewEditorOptions,
|
|
|
|
|
|
resolveInlineApplicationPreviewEditorControl: applicationFlow.resolveInlineApplicationPreviewEditorControl,
|
2026-06-23 09:42:13 +08:00
|
|
|
|
resolveInlineApplicationPreviewEditorDateMax: applicationFlow.resolveInlineApplicationPreviewEditorDateMax,
|
|
|
|
|
|
resolveInlineApplicationPreviewEditorDateMin: applicationFlow.resolveInlineApplicationPreviewEditorDateMin,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
resolveInlineApplicationPreviewMissingFields: applicationFlow.resolveInlineApplicationPreviewMissingFields,
|
|
|
|
|
|
resolveInlineApplicationPreviewRows: applicationFlow.resolveInlineApplicationPreviewRows,
|
|
|
|
|
|
resolveInlineAttachmentOcrDocuments,
|
|
|
|
|
|
resolveInlineAttachmentOcrFileCount,
|
|
|
|
|
|
resolveInlineThinkingEvents,
|
|
|
|
|
|
runAiModeAction,
|
|
|
|
|
|
scrollInlineConversationToTop,
|
2026-06-24 10:42:50 +08:00
|
|
|
|
...filePreview,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
sending,
|
2026-06-23 11:21:18 +08:00
|
|
|
|
setAssistantInputRef,
|
2026-06-22 11:58:53 +08:00
|
|
|
|
setWorkbenchDateMode,
|
|
|
|
|
|
submitAiModePrompt,
|
|
|
|
|
|
toggleInlineAttachmentOcrDetails,
|
|
|
|
|
|
toggleInlineThinking,
|
|
|
|
|
|
toggleWorkbenchDatePicker,
|
|
|
|
|
|
triggerAiModeFileUpload: filesFlow.triggerAiModeFileUpload,
|
|
|
|
|
|
workbenchCanApplyDateSelection,
|
|
|
|
|
|
workbenchDateMode,
|
|
|
|
|
|
workbenchDatePickerOpen,
|
|
|
|
|
|
workbenchDateTagLabel,
|
|
|
|
|
|
workbenchRangeEndDate,
|
|
|
|
|
|
workbenchRangeStartDate,
|
|
|
|
|
|
workbenchSingleDate,
|
|
|
|
|
|
applyWorkbenchDateSelection,
|
|
|
|
|
|
buildInlineApplicationPreviewFooterText: applicationFlow.buildInlineApplicationPreviewFooterText,
|
|
|
|
|
|
copyInlineMessage: messageActions.copyInlineMessage,
|
|
|
|
|
|
formatMessageTime,
|
|
|
|
|
|
handleWorkbenchDateInputChange
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|