2026-06-15 22:55:18 +08:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { useRouter } from 'vue-router'
|
2026-05-29 14:11:06 +08:00
|
|
|
|
import { ElDialog } from 'element-plus/es/components/dialog/index.mjs'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
2026-05-27 09:17:57 +08:00
|
|
|
|
import TravelReimbursementInsightPanel from '../../components/travel/TravelReimbursementInsightPanel.vue'
|
|
|
|
|
|
import TravelReimbursementMessageItem from '../../components/travel/TravelReimbursementMessageItem.vue'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { useTravelReimbursementFlow } from './useTravelReimbursementFlow.js'
|
|
|
|
|
|
import { useTravelReimbursementComposerTools } from './useTravelReimbursementComposerTools.js'
|
|
|
|
|
|
import { useTravelReimbursementAttachments } from './useTravelReimbursementAttachments.js'
|
|
|
|
|
|
import { useTravelReimbursementSessionState } from './useTravelReimbursementSessionState.js'
|
|
|
|
|
|
import { useTravelReimbursementReviewDrawer } from './useTravelReimbursementReviewDrawer.js'
|
|
|
|
|
|
import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSubmitComposer.js'
|
|
|
|
|
|
import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js'
|
2026-05-23 19:54:42 +08:00
|
|
|
|
import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js'
|
2026-06-04 11:03:29 +08:00
|
|
|
|
import { useStewardPlanFlow } from './useStewardPlanFlow.js'
|
2026-06-04 14:25:14 +08:00
|
|
|
|
import {
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildStewardFieldItems,
|
|
|
|
|
|
formatStewardMissingFieldList,
|
|
|
|
|
|
formatStewardOntologyFields
|
|
|
|
|
|
} from './stewardPlanModel.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
import { useApplicationPreviewEditor } from './useApplicationPreviewEditor.js'
|
2026-06-06 17:19:07 +08:00
|
|
|
|
import {
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildStewardFieldCompletionContinuation,
|
2026-06-18 22:12:24 +08:00
|
|
|
|
buildStewardFieldCompletionRawText,
|
|
|
|
|
|
resolveStewardRuntimeFieldCompletion
|
2026-06-15 22:55:18 +08:00
|
|
|
|
} from './stewardFieldCompletionModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildOperationFeedbackPayload,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
normalizeOperationFeedbackContext
|
|
|
|
|
|
} from '../../composables/useOperationFeedback.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
|
|
|
|
|
import { fetchAgentRunDetail } from '../../services/agentAssets.js'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
import { createOperationFeedback } from '../../services/operationFeedback.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { deleteConversation, runOrchestrator } from '../../services/orchestrator.js'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
import { fetchStewardPlan, fetchStewardPlanStream, fetchStewardRuntimeDecision } from '../../services/steward.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { renderMarkdown } from '../../utils/markdown.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { clearAssistantSessionSnapshot } from '../../utils/assistantSessionSnapshot.js'
|
2026-05-20 21:00:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildLocalExtractionProgressMessages,
|
|
|
|
|
|
buildLocalIntentPreview,
|
2026-05-21 16:09:47 +08:00
|
|
|
|
shouldRequestExpenseIntentConfirmation,
|
|
|
|
|
|
shouldRequestExpenseSceneSelection,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
summarizeSemanticIntentDetail
|
2026-05-20 21:00:47 +08:00
|
|
|
|
} from '../../utils/reimbursementTextInference.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildExpenseIntentConfirmationActions,
|
|
|
|
|
|
buildExpenseSceneSelectionActions
|
|
|
|
|
|
} from '../../utils/expenseAssistantActions.js'
|
|
|
|
|
|
import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
import { ASSISTANT_SCOPE_ACTION_SWITCH } from '../../utils/assistantSessionScope.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
mergeComposerPrefill,
|
|
|
|
|
|
resolveSuggestedActionPrefill
|
|
|
|
|
|
} from '../../utils/assistantSuggestedActionPrefill.js'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
import {
|
2026-06-15 22:55:18 +08:00
|
|
|
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
|
|
|
|
|
buildApplicationPreviewFooterMessage,
|
|
|
|
|
|
buildApplicationPreviewSubmitText,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
buildLocalApplicationPreviewMessage,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
normalizeApplicationPreview,
|
|
|
|
|
|
normalizeTransportModeOption
|
2026-05-26 09:15:14 +08:00
|
|
|
|
} from '../../utils/expenseApplicationPreview.js'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
import {
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_GENERATE,
|
|
|
|
|
|
TRAVEL_PLANNING_ACTION_SKIP,
|
|
|
|
|
|
buildTravelPlanningNudgeMessage,
|
|
|
|
|
|
buildTravelPlanningRecommendation,
|
|
|
|
|
|
buildTravelPlanningSuggestedActions
|
|
|
|
|
|
} from '../../utils/travelApplicationPlanning.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import {
|
2026-05-21 09:28:33 +08:00
|
|
|
|
calculateTravelReimbursement,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
createExpenseClaimItem,
|
2026-05-21 16:09:47 +08:00
|
|
|
|
fetchExpenseClaims,
|
2026-05-19 17:24:13 +00:00
|
|
|
|
fetchExpenseClaimAttachmentAsset,
|
|
|
|
|
|
fetchExpenseClaimDetail,
|
|
|
|
|
|
fetchExpenseClaimItemAttachmentMeta,
|
|
|
|
|
|
uploadExpenseClaimItemAttachment
|
|
|
|
|
|
} from '../../services/reimbursements.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
EXPENSE_TYPE_LABELS,
|
|
|
|
|
|
REVIEW_SLOT_CONFIG,
|
|
|
|
|
|
REVIEW_CATEGORY_PRESET_OPTIONS,
|
|
|
|
|
|
REVIEW_OTHER_CATEGORY_OPTIONS,
|
|
|
|
|
|
REVIEW_SCENE_OTHER_OPTION,
|
|
|
|
|
|
REVIEW_SCENE_OPTIONS,
|
|
|
|
|
|
DATE_INPUT_FORMAT,
|
|
|
|
|
|
cloneReviewDocumentDrafts,
|
|
|
|
|
|
buildReviewDocumentDrafts,
|
|
|
|
|
|
normalizeReviewDocumentComparableValue,
|
|
|
|
|
|
buildReviewDocumentCorrectionMessage,
|
|
|
|
|
|
buildReviewDocumentCorrectionContext,
|
|
|
|
|
|
cloneReviewEditFields,
|
|
|
|
|
|
buildReviewFormValues,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
createEmptyInlineReviewState,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveReviewRecognizedSlotCards,
|
|
|
|
|
|
resolveReviewMissingSlotCards,
|
|
|
|
|
|
resolveReviewExtraMissingLabels,
|
|
|
|
|
|
formatConfidenceLabel,
|
|
|
|
|
|
resolveDocumentTypeLabel,
|
|
|
|
|
|
resolveExpenseTypeLabel,
|
|
|
|
|
|
buildReviewRecognizedLines,
|
|
|
|
|
|
buildReviewSlotMap,
|
|
|
|
|
|
resolveExpenseTypeCode,
|
|
|
|
|
|
isValidIsoDateString,
|
|
|
|
|
|
parseAmountNumber,
|
|
|
|
|
|
normalizeAmountValue,
|
|
|
|
|
|
extractAmountInputValue,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
formatAmountDisplay,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
inferPresetSceneFromReview,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
formatReviewSceneDisplayValue,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
summarizeReviewScene,
|
|
|
|
|
|
buildInlineReviewState,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildReviewAttachmentStatus,
|
|
|
|
|
|
shouldShowReviewFactCard,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveReviewCategoryConfidenceScore,
|
|
|
|
|
|
buildReviewCategoryOptions,
|
|
|
|
|
|
buildReviewPanelConfidence,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
|
|
|
|
|
buildInlineReviewChangedLines,
|
|
|
|
|
|
buildLocalReviewSavedMessage,
|
|
|
|
|
|
buildReviewSubmitUserText,
|
|
|
|
|
|
mergeInlineReviewFields,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildBusinessTimeContextFromReviewValues as buildBusinessTimeContextFromReviewValuesModel,
|
|
|
|
|
|
buildReviewFormContextFromPayload as buildReviewFormContextFromPayloadModel,
|
|
|
|
|
|
isTravelReviewPayload as isTravelReviewPayloadModel,
|
|
|
|
|
|
resolveReviewTravelTransportType as resolveReviewTravelTransportTypeModel,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildClientTimeContext,
|
|
|
|
|
|
formatDraftApplyTime,
|
|
|
|
|
|
formatDateInputValue,
|
|
|
|
|
|
buildDraftSavedPayload,
|
|
|
|
|
|
buildReviewHeadline,
|
|
|
|
|
|
buildReviewSubline,
|
|
|
|
|
|
buildReviewStateLabel,
|
|
|
|
|
|
buildReviewStateTone,
|
|
|
|
|
|
buildReviewPlainFollowupCopy,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildReviewNextStepRichCopy,
|
|
|
|
|
|
buildReviewRiskLevelCounts,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveReviewFooterActions,
|
|
|
|
|
|
resolveReviewSaveDraftAction,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
resolveReviewNextStepAction,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildReviewPrimaryButtonLabel,
|
|
|
|
|
|
buildReviewIntentText,
|
|
|
|
|
|
buildReviewSceneValue,
|
|
|
|
|
|
buildMissingRiskLine,
|
|
|
|
|
|
buildReviewRiskSummary as buildReviewRiskSummaryModel,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
normalizeReviewRiskLevel,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildLocalReviewCompletionMessage,
|
|
|
|
|
|
buildReviewRecognitionNotes,
|
|
|
|
|
|
buildReviewDocumentSummaries
|
|
|
|
|
|
} from './travelReimbursementReviewModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildDraftAssociationQueryPayload,
|
|
|
|
|
|
buildExpenseQueryHint,
|
|
|
|
|
|
buildExpenseQueryWindowLabel,
|
|
|
|
|
|
getExpenseQueryActivePage,
|
|
|
|
|
|
getExpenseQueryTotalPages,
|
|
|
|
|
|
getExpenseQueryVisibleRecords,
|
|
|
|
|
|
normalizeExpenseQueryPayload
|
|
|
|
|
|
} from './travelReimbursementExpenseQueryModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
MAX_OCR_DOCUMENTS,
|
|
|
|
|
|
VISIBLE_ATTACHMENT_CHIPS,
|
|
|
|
|
|
buildAgentInsight,
|
|
|
|
|
|
buildErrorInsight,
|
2026-05-22 08:58:59 +08:00
|
|
|
|
ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildFileIdentity,
|
|
|
|
|
|
buildFilePreviews,
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload,
|
|
|
|
|
|
buildOcrSummaryFromDocuments,
|
|
|
|
|
|
buildReviewFilePreviewsFromReviewPayload,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
isTemporaryPreviewUrl,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
mergeFilePreviews,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeUploadAttachmentNames,
|
|
|
|
|
|
mergeUploadOcrDocuments,
|
|
|
|
|
|
resolveAttachmentPreviewKind,
|
|
|
|
|
|
resolveDocumentPreview
|
|
|
|
|
|
} from './travelReimbursementAttachmentModel.js'
|
|
|
|
|
|
import {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
ASSISTANT_SESSION_MODE_OPTIONS,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
ASSISTANT_DISPLAY_NAME,
|
|
|
|
|
|
FLOW_STEP_FALLBACKS,
|
|
|
|
|
|
HOT_KNOWLEDGE_QUESTIONS,
|
|
|
|
|
|
INTENT_LABELS,
|
|
|
|
|
|
SCENARIO_LABELS,
|
2026-05-27 10:32:08 +08:00
|
|
|
|
SESSION_TYPE_BUDGET,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
SESSION_TYPE_APPROVAL,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
SESSION_TYPE_EXPENSE,
|
|
|
|
|
|
SESSION_TYPE_KNOWLEDGE,
|
2026-06-04 11:03:29 +08:00
|
|
|
|
SESSION_TYPE_STEWARD,
|
2026-05-27 10:32:08 +08:00
|
|
|
|
canUseBudgetAssistantSession,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
aiAvatar,
|
|
|
|
|
|
buildExpenseIntentConfirmationMessage,
|
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
|
buildMessageMeta,
|
|
|
|
|
|
buildWelcomeInsight,
|
|
|
|
|
|
createMessage,
|
2026-05-27 10:32:08 +08:00
|
|
|
|
filterAssistantSessionModes,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
hasMeaningfulSessionMessages,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
resolveAssistantSessionMode,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveKnowledgeRankLabel,
|
|
|
|
|
|
resolveKnowledgeRankTone,
|
|
|
|
|
|
sanitizeRequest,
|
|
|
|
|
|
summarizeSemanticParseDetail,
|
|
|
|
|
|
userAvatar
|
|
|
|
|
|
} from './travelReimbursementConversationModel.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const STEWARD_ASSISTANT_NAME = '小财管家'
|
|
|
|
|
|
const STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS = 10
|
|
|
|
|
|
const STEWARD_FOLLOWUP_THINKING_INTERVAL_MS = 8
|
|
|
|
|
|
const STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE = 4
|
|
|
|
|
|
const STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE = 5
|
|
|
|
|
|
const APPLICATION_PREVIEW_FIELD_ACTION_SET = 'set_application_preview_field'
|
|
|
|
|
|
const APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN = /^(确认|确定|确认提交|确定提交|提交|提交审批|确认审批|确认无误|核对无误|信息无误|无误|没问题|可以提交|确认进入审批|提交至审批流程|确认提交审批|同意提交)$/
|
|
|
|
|
|
const APPLICATION_SUBMIT_CANCEL_TEXT_PATTERN = /^(不|否|否定|取消|暂不|先不|不确认|不提交|再检查|再看看|等等|等一下)/
|
|
|
|
|
|
const STEWARD_RUNTIME_CONTINUE_TEXT_PATTERN = /^(继续|继续执行|下一步|继续下一步|开始下一步|处理下一项|继续处理|确认开始|确定开始|可以|好的|好|行)$/
|
|
|
|
|
|
const STEWARD_RUNTIME_CANCEL_TEXT_PATTERN = /^(取消|暂不|先不|不用|不要|不继续|不处理|先等等|等一下|停止|终止|算了)$/
|
|
|
|
|
|
const STEWARD_RUNTIME_NEW_TASK_MIN_LENGTH = 12
|
|
|
|
|
|
const STEWARD_RUNTIME_BUSINESS_HINT_PATTERN = /(申请|报销|出差|差旅|招待|交通费|住宿费|餐费|发票|票据|费用|预算|借款|付款|审批|审核)/
|
|
|
|
|
|
const STEWARD_RUNTIME_TIME_OR_ACTION_HINT_PATTERN = /(今天|明天|后天|昨天|前天|\d{1,2}月\d{1,2}日|\d{4}-\d{1,2}-\d{1,2}|我要|帮我|需要|创建|填写|处理|去|前往)/
|
|
|
|
|
|
const STEWARD_RUNTIME_CURRENT_CONTEXT_HINT_PATTERN = /(当前|这个|这一步|上面|上述|申请单|核对表|出行方式|交通方式|火车|高铁|动车|飞机|轮船|提交|审批|确认)/
|
|
|
|
|
|
|
|
|
|
|
|
const REVIEW_RISK_LEVEL_META = {
|
|
|
|
|
|
high: {
|
|
|
|
|
|
label: '高风险',
|
|
|
|
|
|
icon: 'mdi mdi-alert-octagon-outline',
|
|
|
|
|
|
suggestion: '建议先处理该项,再提交审批;如果属于合理业务场景,请补充说明或附件。'
|
|
|
|
|
|
},
|
|
|
|
|
|
medium: {
|
|
|
|
|
|
label: '中风险',
|
|
|
|
|
|
icon: 'mdi mdi-alert-circle-outline',
|
|
|
|
|
|
suggestion: '提交前建议核对业务真实性、附件完整性和规则口径。'
|
|
|
|
|
|
},
|
|
|
|
|
|
info: {
|
|
|
|
|
|
label: '提示',
|
|
|
|
|
|
icon: 'mdi mdi-information-outline',
|
|
|
|
|
|
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
|
|
|
|
|
},
|
|
|
|
|
|
low: {
|
|
|
|
|
|
label: '低风险',
|
|
|
|
|
|
icon: 'mdi mdi-information-outline',
|
|
|
|
|
|
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
const COMPOSER_TEXTAREA_HEIGHT = 36
|
|
|
|
|
|
const COMPOSER_MAX_ROWS = 5
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_REVIEW = 'review'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_RISK = 'risk'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_FLOW = 'flow'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const REVIEW_PANEL_SCOPE_OVERVIEW = 'overview'
|
|
|
|
|
|
const REVIEW_PANEL_SCOPE_DOCUMENTS = 'documents'
|
|
|
|
|
|
const REVIEW_PANEL_SCOPE_RISK = 'risk'
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const REVIEW_NEXT_STEP_HREF = '#review-next-step'
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const APPLICATION_SUBMIT_HREF = '#application-submit'
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const REVIEW_RISK_PANEL_HREF_PREFIX = '#review-risk'
|
|
|
|
|
|
const REVIEW_QUICK_EDIT_HREF = '#review-quick-edit'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
const FLOW_STEP_STATUS_PENDING = 'pending'
|
|
|
|
|
|
const FLOW_STEP_STATUS_RUNNING = 'running'
|
|
|
|
|
|
const FLOW_STEP_STATUS_COMPLETED = 'completed'
|
|
|
|
|
|
const FLOW_STEP_STATUS_FAILED = 'failed'
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = ['历史报销画像', '用户画像', '制度注意事项', '制度注意']
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeReviewPanelScope(scope) {
|
|
|
|
|
|
const normalized = String(scope || '').trim()
|
|
|
|
|
|
return [REVIEW_PANEL_SCOPE_OVERVIEW, REVIEW_PANEL_SCOPE_DOCUMENTS, REVIEW_PANEL_SCOPE_RISK].includes(normalized)
|
|
|
|
|
|
? normalized
|
|
|
|
|
|
: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canExposeReviewPanelScope(scope) {
|
|
|
|
|
|
return Boolean(normalizeReviewPanelScope(scope))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildBusinessTimeContextFromReviewValues(values = {}) {
|
|
|
|
|
|
return buildBusinessTimeContextFromReviewValuesModel(values)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewFormContextFromPayload(reviewPayload, inlineState = null) {
|
|
|
|
|
|
return buildReviewFormContextFromPayloadModel(reviewPayload, inlineState)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewCorrectionMessage(fields) {
|
|
|
|
|
|
const lines = ['请按以下核对后的报销信息更新当前识别结果:']
|
|
|
|
|
|
for (const item of cloneReviewEditFields(fields)) {
|
|
|
|
|
|
if (!item.label || (!item.value && !item.required)) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push(`${item.label}:${String(item.value || '').trim() || '待补充'}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isTravelReviewPayload(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
|
|
|
|
|
return isTravelReviewPayloadModel(reviewPayload, inlineState)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReviewTravelTransportType(reviewPayload, fallbackText = '') {
|
|
|
|
|
|
return resolveReviewTravelTransportTypeModel(reviewPayload, fallbackText)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReviewRiskBriefs(reviewPayload) {
|
|
|
|
|
|
if (!Array.isArray(reviewPayload?.risk_briefs)) return []
|
|
|
|
|
|
return reviewPayload.risk_briefs.filter((item) => {
|
|
|
|
|
|
const title = String(item?.title || '').trim()
|
|
|
|
|
|
return !DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS.some((keyword) => title.includes(keyword))
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
|
|
|
|
|
const pendingAttachmentCount = Math.max(0, Number(inlineState.pending_attachment_count || 0))
|
|
|
|
|
|
const totalAttachmentCount = Math.max(0, Number(inlineState.attachment_count || 0))
|
|
|
|
|
|
const existingAttachmentCount = Math.max(0, totalAttachmentCount - pendingAttachmentCount)
|
|
|
|
|
|
const attachmentStatus =
|
|
|
|
|
|
pendingAttachmentCount > 0
|
|
|
|
|
|
? existingAttachmentCount > 0
|
|
|
|
|
|
? `已上传 ${existingAttachmentCount} 份,待新增 ${pendingAttachmentCount} 份`
|
|
|
|
|
|
: `待保存 ${pendingAttachmentCount} 份`
|
|
|
|
|
|
: totalAttachmentCount > 0
|
|
|
|
|
|
? `已上传 ${totalAttachmentCount} 份`
|
|
|
|
|
|
: buildReviewAttachmentStatus(reviewPayload)
|
|
|
|
|
|
if (isTravelReviewPayload(reviewPayload, inlineState)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'occurred_date',
|
|
|
|
|
|
label: '发生时间',
|
|
|
|
|
|
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-calendar-month-outline',
|
|
|
|
|
|
editor: 'date',
|
|
|
|
|
|
modelKey: 'occurred_date',
|
|
|
|
|
|
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
label: '金额',
|
|
|
|
|
|
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-cash',
|
|
|
|
|
|
editor: 'amount',
|
|
|
|
|
|
modelKey: 'amount',
|
|
|
|
|
|
placeholder: '例如 200.00'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'transport_type',
|
|
|
|
|
|
label: '交通类型',
|
|
|
|
|
|
value: String(inlineState.transport_type || '').trim() || '待确认',
|
|
|
|
|
|
icon: 'mdi mdi-train-car',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'transport_type',
|
|
|
|
|
|
placeholder: '例如 火车/高铁、飞机'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'hotel_name',
|
|
|
|
|
|
label: '酒店名称',
|
|
|
|
|
|
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-bed-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'merchant_name',
|
|
|
|
|
|
placeholder: '请输入酒店名称'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'travel_purpose',
|
|
|
|
|
|
label: '出差事宜',
|
|
|
|
|
|
value: String(inlineState.reason_value || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-briefcase-edit-outline',
|
|
|
|
|
|
editor: 'textarea',
|
|
|
|
|
|
modelKey: 'reason_value',
|
|
|
|
|
|
placeholder: '请填写本次出差的具体工作内容或业务意图',
|
|
|
|
|
|
wide: true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'occurred_date',
|
|
|
|
|
|
label: '发生时间',
|
|
|
|
|
|
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-calendar-month-outline',
|
|
|
|
|
|
editor: 'date',
|
|
|
|
|
|
modelKey: 'occurred_date',
|
|
|
|
|
|
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
label: '金额',
|
|
|
|
|
|
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-cash',
|
|
|
|
|
|
editor: 'amount',
|
|
|
|
|
|
modelKey: 'amount',
|
|
|
|
|
|
placeholder: '例如 200.00'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'scene',
|
|
|
|
|
|
label: '场景 / 事由',
|
|
|
|
|
|
value: formatReviewSceneDisplayValue(inlineState),
|
|
|
|
|
|
icon: 'mdi mdi-silverware-fork-knife',
|
|
|
|
|
|
editor: 'select',
|
|
|
|
|
|
modelKey: 'scene_label',
|
|
|
|
|
|
placeholder: '请选择场景'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'attachments',
|
|
|
|
|
|
label: '票据状态',
|
|
|
|
|
|
value: attachmentStatus,
|
|
|
|
|
|
icon: 'mdi mdi-file-document-outline',
|
|
|
|
|
|
editor: 'upload',
|
|
|
|
|
|
modelKey: 'attachment_names',
|
|
|
|
|
|
placeholder: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'customer_name', inlineState.customer_name)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'customer_name',
|
|
|
|
|
|
label: '关联客户',
|
|
|
|
|
|
value: String(inlineState.customer_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-domain',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'customer_name',
|
|
|
|
|
|
placeholder: '请输入客户名称'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'location', inlineState.location)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'location',
|
|
|
|
|
|
label: '业务地点',
|
|
|
|
|
|
value: String(inlineState.location || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-map-marker-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'location',
|
|
|
|
|
|
placeholder: '请输入业务地点'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'merchant_name', inlineState.merchant_name)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'merchant_name',
|
|
|
|
|
|
label: '酒店/商户',
|
|
|
|
|
|
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-storefront-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'merchant_name',
|
|
|
|
|
|
placeholder: '请输入酒店或商户名称'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'participants', inlineState.participants)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'participants',
|
|
|
|
|
|
label: '同行人员',
|
|
|
|
|
|
value: String(inlineState.participants || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-account-group-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'participants',
|
|
|
|
|
|
placeholder: '例如 客户 2 人,我方 1 人'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cards
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeReviewRiskTitle(title, fallbackTitle) {
|
|
|
|
|
|
const normalized = String(title || '').trim()
|
|
|
|
|
|
const fallback = String(fallbackTitle || '风险提示').trim() || '风险提示'
|
|
|
|
|
|
if (!normalized) return fallback
|
|
|
|
|
|
const cleaned = normalized
|
|
|
|
|
|
.replace(/AI\s*预审\s*(暂未通过|未通过|不通过)?/g, '风险提示')
|
|
|
|
|
|
.replace(/(高风险|中风险|低风险)/g, '')
|
|
|
|
|
|
.replace(/^[::\-—\s]+|[::\-—\s]+$/g, '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
return cleaned || fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewRiskItems(reviewPayload) {
|
|
|
|
|
|
return resolveReviewRiskBriefs(reviewPayload)
|
|
|
|
|
|
.map((brief, index) => {
|
|
|
|
|
|
const title = String(brief?.title || '').trim()
|
|
|
|
|
|
const content = String(brief?.content || '').trim()
|
|
|
|
|
|
const detail = String(brief?.detail || '').trim()
|
|
|
|
|
|
const suggestion = String(brief?.suggestion || '').trim()
|
|
|
|
|
|
const level = normalizeReviewRiskLevel(brief?.level)
|
|
|
|
|
|
const meta = REVIEW_RISK_LEVEL_META[level] || REVIEW_RISK_LEVEL_META.low
|
|
|
|
|
|
const fallbackTitle = content ? `风险提示 ${index + 1}` : '风险提示'
|
|
|
|
|
|
const normalizedTitle = normalizeReviewRiskTitle(title, fallbackTitle)
|
|
|
|
|
|
const summary = content || normalizedTitle
|
|
|
|
|
|
|
|
|
|
|
|
if (!normalizedTitle && !summary) return null
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
key: `${level}-${normalizedTitle}-${index}`,
|
|
|
|
|
|
title: normalizedTitle,
|
|
|
|
|
|
summary,
|
|
|
|
|
|
detail: detail || content || '当前风险项没有返回更长解释,建议结合票据、报销事由和规则要求进行复核。',
|
|
|
|
|
|
level,
|
|
|
|
|
|
levelLabel: meta.label,
|
|
|
|
|
|
icon: meta.icon,
|
|
|
|
|
|
sourceLabel: meta.label,
|
|
|
|
|
|
suggestion: suggestion || meta.suggestion
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewRiskConversationText(item, detailTarget = {}) {
|
|
|
|
|
|
const title = String(item?.title || '风险提示').trim()
|
|
|
|
|
|
const summary = String(item?.summary || '').trim()
|
|
|
|
|
|
const detail = String(item?.detail || '').trim()
|
|
|
|
|
|
const suggestion = String(item?.suggestion || '').trim()
|
|
|
|
|
|
const isInfo = String(item?.level || '').trim() === 'info'
|
|
|
|
|
|
const detailHref = String(detailTarget?.href || '').trim()
|
|
|
|
|
|
const detailLabel = String(detailTarget?.label || '').trim() || '进入该单据详情重新填写'
|
|
|
|
|
|
const lines = [`${title}`]
|
|
|
|
|
|
|
|
|
|
|
|
if (summary) {
|
|
|
|
|
|
lines.push('', `${isInfo ? '提示内容' : '风险点'}:${summary}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (detail && detail !== summary) {
|
|
|
|
|
|
lines.push('', `规则依据:${detail}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (suggestion) {
|
|
|
|
|
|
lines.push('', `${isInfo ? '处理建议' : '修改建议'}:${suggestion}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (detailHref) {
|
|
|
|
|
|
lines.push('', `[${detailLabel}](${detailHref})`)
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const REVIEW_PENDING_SUMMARY_PATTERN = /(^|\n)\s*(?:当前还有|我这边看到还有|下方还有|这笔报销还有|目前还有|还有|这次识别结果里还有|我还需要你确认|当前信息还差|本次报销还有)\s+[^\n]*(?:信息待补充|风险提醒|细节还需要进一步确认)[^\n]*(?:草稿)[^\n]*。\s*/g
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewMainMessageText(message) {
|
|
|
|
|
|
const text = String(message?.text || '')
|
|
|
|
|
|
if (!message?.reviewPayload) {
|
|
|
|
|
|
return text
|
|
|
|
|
|
}
|
|
|
|
|
|
return text
|
|
|
|
|
|
.replace(REVIEW_PENDING_SUMMARY_PATTERN, '\n')
|
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TravelReimbursementCreateView',
|
|
|
|
|
|
components: {
|
2026-05-29 14:11:06 +08:00
|
|
|
|
ElDialog,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
ConfirmDialog,
|
|
|
|
|
|
TravelReimbursementInsightPanel,
|
|
|
|
|
|
TravelReimbursementMessageItem
|
2026-05-21 23:53:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
props: {
|
|
|
|
|
|
initialPrompt: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
2026-06-03 09:25:23 +08:00
|
|
|
|
initialPromptAutoSubmit: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
initialApplicationPreview: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
2026-05-21 23:53:03 +08:00
|
|
|
|
initialFiles: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
initialConversation: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
2026-06-02 14:01:51 +08:00
|
|
|
|
initialBudgetContext: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
2026-05-30 15:46:51 +08:00
|
|
|
|
initialSessionType: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
2026-05-21 23:53:03 +08:00
|
|
|
|
entrySource: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'requests'
|
|
|
|
|
|
},
|
|
|
|
|
|
requestContext: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
2026-05-22 16:00:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
invalidatedDraftClaimId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
2026-05-26 09:15:14 +08:00
|
|
|
|
},
|
|
|
|
|
|
reopenToken: {
|
|
|
|
|
|
type: Number,
|
|
|
|
|
|
default: 0
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
},
|
2026-05-30 15:46:51 +08:00
|
|
|
|
emits: ['close', 'draft-saved', 'request-updated'],
|
2026-05-21 23:53:03 +08:00
|
|
|
|
setup(props, { emit }) {
|
|
|
|
|
|
const router = useRouter()
|
2026-06-01 17:07:14 +08:00
|
|
|
|
const { currentUser, refreshCurrentUserFromBackend } = useSystemState()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const { toast } = useToast()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const fileInputRef = ref(null)
|
|
|
|
|
|
const composerTextareaRef = ref(null)
|
|
|
|
|
|
const messageListRef = ref(null)
|
|
|
|
|
|
const composerDraft = ref('')
|
|
|
|
|
|
const submitting = ref(false)
|
|
|
|
|
|
const workbenchVisible = ref(false)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const closeAfterBusy = ref(false)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
|
|
|
|
|
|
const hotKnowledgeQuestions = HOT_KNOWLEDGE_QUESTIONS
|
|
|
|
|
|
let sessionRuntimeRefs = {}
|
|
|
|
|
|
const {
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
conversationId,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
stewardState,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
draftClaimId,
|
|
|
|
|
|
sessionSnapshots,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
reviewFilePreviews,
|
|
|
|
|
|
composerUploadIntent,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
guidedFlowState,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
insightPanelCollapsed,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
buildEmptySessionState,
|
|
|
|
|
|
resolveCurrentUserId,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
applySessionState,
|
|
|
|
|
|
switchSessionType
|
|
|
|
|
|
} = useTravelReimbursementSessionState({
|
|
|
|
|
|
props,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
getSessionRuntimeRefs: () => sessionRuntimeRefs
|
|
|
|
|
|
})
|
|
|
|
|
|
const deleteSessionDialogOpen = ref(false)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const applicationSubmitConfirmDialog = ref({
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null
|
|
|
|
|
|
})
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const nextStepConfirmDialog = ref({
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const reviewActionBusy = ref(false)
|
|
|
|
|
|
const deleteSessionBusy = ref(false)
|
|
|
|
|
|
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const {
|
|
|
|
|
|
applicationPreviewEditor,
|
|
|
|
|
|
resolveApplicationPreviewRows,
|
|
|
|
|
|
resolveApplicationPreviewEditorControl,
|
|
|
|
|
|
resolveApplicationPreviewEditorOptions,
|
|
|
|
|
|
isApplicationPreviewEditing,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
isApplicationPreviewDateEditorOpen,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
openApplicationPreviewEditor,
|
|
|
|
|
|
commitApplicationPreviewEditor,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
commitApplicationPreviewDateEditor,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
cancelApplicationPreviewEditor,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
setApplicationPreviewDateMode,
|
|
|
|
|
|
canApplyApplicationPreviewDateSelection,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
handleApplicationPreviewEditorKeydown
|
|
|
|
|
|
} = useApplicationPreviewEditor({
|
|
|
|
|
|
persistSessionState,
|
2026-06-03 09:25:23 +08:00
|
|
|
|
toast,
|
|
|
|
|
|
calculateTravelReimbursement,
|
|
|
|
|
|
currentUser
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
function applyLinkedApplicationPreviewDateSelection(selection) {
|
|
|
|
|
|
const editor = applicationPreviewEditor.value
|
|
|
|
|
|
if (editor.fieldKey !== 'time' || !editor.messageId) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const targetMessage = messages.value.find((item) =>
|
|
|
|
|
|
String(item.id || '') === String(editor.messageId || '')
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!targetMessage?.applicationPreview) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
applicationPreviewEditor.value = {
|
|
|
|
|
|
...editor,
|
|
|
|
|
|
dateMode: selection.mode === 'range' ? 'range' : 'single',
|
|
|
|
|
|
singleDate: selection.startDate,
|
|
|
|
|
|
rangeStartDate: selection.startDate,
|
|
|
|
|
|
rangeEndDate: selection.endDate || selection.startDate
|
|
|
|
|
|
}
|
|
|
|
|
|
return commitApplicationPreviewDateEditor(targetMessage)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const isApplicationSession = computed(() => activeSessionType.value === SESSION_TYPE_APPLICATION)
|
2026-06-04 11:03:29 +08:00
|
|
|
|
const isStewardSession = computed(() => activeSessionType.value === SESSION_TYPE_STEWARD)
|
2026-05-25 13:35:39 +08:00
|
|
|
|
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
|
2026-06-04 11:03:29 +08:00
|
|
|
|
const assistantHeaderTitle = computed(() => (isStewardSession.value ? '小财管家' : '个人工作台'))
|
|
|
|
|
|
const assistantHeaderDescription = computed(() =>
|
|
|
|
|
|
isStewardSession.value ? '统一财务任务编排入口' : '个人工作窗,一站式费控解决枢纽'
|
|
|
|
|
|
)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const hasStewardInitialAutoSubmitPayload = computed(() => (
|
|
|
|
|
|
isStewardSession.value &&
|
|
|
|
|
|
props.initialPromptAutoSubmit !== false &&
|
|
|
|
|
|
(
|
|
|
|
|
|
Boolean(String(props.initialPrompt || '').trim()) ||
|
|
|
|
|
|
(Array.isArray(props.initialFiles) && props.initialFiles.length > 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
))
|
|
|
|
|
|
const showStewardInitialRecognition = computed(() => (
|
|
|
|
|
|
hasStewardInitialAutoSubmitPayload.value &&
|
|
|
|
|
|
!messages.value.length &&
|
|
|
|
|
|
(workbenchVisible.value || submitting.value)
|
|
|
|
|
|
))
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
flowRunId,
|
|
|
|
|
|
flowSteps,
|
2026-05-27 12:27:17 +08:00
|
|
|
|
activeFlowSteps,
|
2026-05-27 10:32:08 +08:00
|
|
|
|
visibleFlowSteps,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
flowRefreshBusy,
|
|
|
|
|
|
completedFlowStepCount,
|
|
|
|
|
|
flowOverallStatusTone,
|
|
|
|
|
|
flowOverallStatusText,
|
|
|
|
|
|
flowTotalDurationText,
|
|
|
|
|
|
clearFlowSimulationTimers,
|
|
|
|
|
|
resetFlowRun,
|
|
|
|
|
|
startFlowTick,
|
|
|
|
|
|
stopFlowRuntime,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
failCurrentFlowStep,
|
|
|
|
|
|
startSemanticFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview,
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionAfterIntentConfirmation,
|
|
|
|
|
|
startReviewActionFlowStep,
|
|
|
|
|
|
startExpenseClaimDraftFlowStep,
|
|
|
|
|
|
completeFlowResult,
|
|
|
|
|
|
refreshFlowRunDetail,
|
|
|
|
|
|
formatFlowStepDuration,
|
|
|
|
|
|
resolveFlowStepStatusLabel,
|
|
|
|
|
|
resolveFlowStepDetail
|
|
|
|
|
|
} = useTravelReimbursementFlow({
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
reviewDrawerMode,
|
|
|
|
|
|
insightPanelCollapsed,
|
|
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
fetchAgentRunDetail,
|
|
|
|
|
|
buildLocalIntentPreview,
|
|
|
|
|
|
buildLocalExtractionProgressMessages,
|
|
|
|
|
|
summarizeSemanticIntentDetail,
|
|
|
|
|
|
summarizeSemanticParseDetail,
|
|
|
|
|
|
SCENARIO_LABELS,
|
|
|
|
|
|
INTENT_LABELS,
|
|
|
|
|
|
EXPENSE_TYPE_LABELS,
|
|
|
|
|
|
FLOW_STEP_FALLBACKS,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_FLOW,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_REVIEW,
|
|
|
|
|
|
FLOW_STEP_STATUS_PENDING,
|
|
|
|
|
|
FLOW_STEP_STATUS_RUNNING,
|
|
|
|
|
|
FLOW_STEP_STATUS_COMPLETED,
|
|
|
|
|
|
FLOW_STEP_STATUS_FAILED
|
|
|
|
|
|
})
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const hasScopedReviewPayload = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
if (agent?.reviewPayload && canExposeReviewPanelScope(agent.reviewPanelScope)) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return messages.value.some((item) =>
|
|
|
|
|
|
item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope)
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
const hasQueryInsight = computed(() => Boolean(currentInsight.value.agent?.queryPayload))
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const hasInsightPanelContent = computed(() => {
|
2026-05-27 12:27:17 +08:00
|
|
|
|
return isKnowledgeSession.value || hasScopedReviewPayload.value || hasQueryInsight.value || activeFlowSteps.value.length > 0
|
2026-05-26 09:15:14 +08:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value)
|
|
|
|
|
|
const insightPanelToggleLabel = computed(() =>
|
|
|
|
|
|
showInsightPanel.value ? '隐藏详细信息' : '展开详细信息'
|
|
|
|
|
|
)
|
|
|
|
|
|
const composerPlaceholder = computed(() => {
|
2026-06-04 11:03:29 +08:00
|
|
|
|
if (isStewardSession.value) {
|
|
|
|
|
|
return '例如:申请7月2日去北京出差,同时报销昨天交通费和6月3日上海出差费用。'
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (isKnowledgeSession.value) {
|
|
|
|
|
|
return '例如:差旅住宿标准是什么?发票抬头不一致还能报销吗?'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
|
|
|
|
|
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-25 13:35:39 +08:00
|
|
|
|
if (activeSessionType.value === SESSION_TYPE_APPLICATION) {
|
|
|
|
|
|
return '例如:我想先申请一笔差旅费用,去上海支持项目部署。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (activeSessionType.value === SESSION_TYPE_APPROVAL) {
|
|
|
|
|
|
return '例如:查一下待我审核的单据,或帮我生成这张单据的审核意见。'
|
|
|
|
|
|
}
|
2026-05-27 12:27:17 +08:00
|
|
|
|
if (activeSessionType.value === SESSION_TYPE_BUDGET) {
|
|
|
|
|
|
return '例如:查询市场部 Q1 预算编制情况,重点看差旅、通信、招待费和办公用品。'
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件整理报销核对信息。'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const currentIntentLabel = computed(() => {
|
|
|
|
|
|
if (isKnowledgeSession.value && currentInsight.value.intent === 'welcome') {
|
|
|
|
|
|
return '热门问题'
|
|
|
|
|
|
}
|
|
|
|
|
|
const labels = isKnowledgeSession.value
|
|
|
|
|
|
? {
|
|
|
|
|
|
welcome: '热门问题',
|
|
|
|
|
|
agent: '知识回答'
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
welcome: activeAssistantMode.value?.label || '财务助手',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
agent: '处理中'
|
|
|
|
|
|
}
|
|
|
|
|
|
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
|
|
|
|
|
})
|
|
|
|
|
|
const canDeleteCurrentSession = computed(
|
2026-05-27 14:35:17 +08:00
|
|
|
|
() => Boolean(conversationId.value) || hasMeaningfulSessionMessages(messages.value)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const latestReviewMessage = computed(() =>
|
2026-05-22 16:00:19 +08:00
|
|
|
|
[...messages.value].reverse().find((item) =>
|
|
|
|
|
|
item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope)
|
|
|
|
|
|
) ?? null
|
2026-05-21 09:28:33 +08:00
|
|
|
|
)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const activeReviewPanelScope = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
const agentScope = normalizeReviewPanelScope(agent?.reviewPanelScope)
|
|
|
|
|
|
if (agent?.reviewPayload && agentScope) {
|
|
|
|
|
|
return agentScope
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeReviewPanelScope(latestReviewMessage.value?.reviewPanelScope)
|
|
|
|
|
|
})
|
|
|
|
|
|
const activeReviewPayload = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
if (agent?.reviewPayload && normalizeReviewPanelScope(agent.reviewPanelScope)) {
|
|
|
|
|
|
return agent.reviewPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return latestReviewMessage.value?.reviewPayload || null
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const reviewRiskBriefResolver = (payload) => resolveReviewRiskBriefs(payload)
|
|
|
|
|
|
const buildReviewRiskSummary = (payload) => buildReviewRiskSummaryModel(payload, reviewRiskBriefResolver)
|
|
|
|
|
|
const {
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlineBaseForm,
|
|
|
|
|
|
reviewInlineBaseFields,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
reviewInlineErrors,
|
|
|
|
|
|
reviewOtherCategoryOpen,
|
|
|
|
|
|
reviewDocumentDrafts,
|
|
|
|
|
|
reviewDocumentBaseDrafts,
|
|
|
|
|
|
activeReviewDocumentIndex,
|
|
|
|
|
|
documentPreviewDialog,
|
|
|
|
|
|
activeReviewFilePreviews,
|
|
|
|
|
|
reviewIntentText,
|
|
|
|
|
|
reviewFactCards,
|
|
|
|
|
|
reviewCategoryOptions,
|
|
|
|
|
|
reviewOtherCategoryOptions,
|
|
|
|
|
|
reviewSelectedOtherCategory,
|
|
|
|
|
|
reviewInlineDirty,
|
|
|
|
|
|
reviewPanelConfidence,
|
|
|
|
|
|
reviewRiskSummary,
|
|
|
|
|
|
reviewRiskItems,
|
|
|
|
|
|
reviewRiskEmpty,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
reviewOverviewDrawerAvailable,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewDocumentDrawerAvailable,
|
|
|
|
|
|
reviewRiskDrawerAvailable,
|
|
|
|
|
|
reviewFlowDrawerAvailable,
|
|
|
|
|
|
recognizedNarratives,
|
|
|
|
|
|
reviewRecognitionNotes,
|
|
|
|
|
|
reviewDocumentSummaries,
|
|
|
|
|
|
reviewDocumentCount,
|
|
|
|
|
|
isReviewDocumentDrawer,
|
|
|
|
|
|
isReviewRiskDrawer,
|
|
|
|
|
|
isReviewFlowDrawer,
|
|
|
|
|
|
reviewDrawerTitle,
|
|
|
|
|
|
reviewDocumentDrawerLabel,
|
|
|
|
|
|
reviewDocumentDrawerIcon,
|
|
|
|
|
|
reviewRiskDrawerLabel,
|
|
|
|
|
|
reviewRiskDrawerIcon,
|
|
|
|
|
|
reviewFlowDrawerLabel,
|
|
|
|
|
|
reviewFlowDrawerIcon,
|
|
|
|
|
|
activeReviewDocument,
|
|
|
|
|
|
activeReviewDocumentPreview,
|
|
|
|
|
|
canPreviewActiveReviewDocument,
|
|
|
|
|
|
reviewDocumentDirty,
|
|
|
|
|
|
reviewHasUnsavedChanges,
|
|
|
|
|
|
setInlineReviewFieldError,
|
|
|
|
|
|
clearInlineReviewFieldError,
|
|
|
|
|
|
resetReviewDrawerFromPayload,
|
|
|
|
|
|
enforceReviewDrawerAvailability,
|
|
|
|
|
|
openInlineReviewEditor,
|
|
|
|
|
|
closeInlineReviewEditor,
|
|
|
|
|
|
commitInlineReviewEditor,
|
|
|
|
|
|
selectInlineScene,
|
|
|
|
|
|
selectReviewCategory,
|
|
|
|
|
|
selectReviewOtherCategory,
|
|
|
|
|
|
goReviewDocument,
|
|
|
|
|
|
openActiveReviewDocumentPreview,
|
|
|
|
|
|
closeDocumentPreview
|
|
|
|
|
|
} = useTravelReimbursementReviewDrawer({
|
|
|
|
|
|
activeReviewPayload,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
activeReviewPanelScope,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewFilePreviews,
|
2026-05-27 12:27:17 +08:00
|
|
|
|
flowSteps: activeFlowSteps,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
triggerFileUpload: (...args) => triggerFileUpload(...args),
|
|
|
|
|
|
resolveDocumentPreview,
|
|
|
|
|
|
buildReviewFactCards,
|
|
|
|
|
|
buildReviewRiskItems,
|
|
|
|
|
|
buildReviewRiskSummary,
|
|
|
|
|
|
buildReviewIntentText,
|
|
|
|
|
|
resolveReviewRiskBriefs,
|
|
|
|
|
|
reviewDrawerMode,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_REVIEW,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_DOCUMENTS,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_RISK,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_FLOW
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
composerDatePickerOpen,
|
|
|
|
|
|
composerDateMode,
|
|
|
|
|
|
composerSingleDate,
|
|
|
|
|
|
composerRangeStartDate,
|
|
|
|
|
|
composerRangeEndDate,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerCanApplyDateSelection,
|
|
|
|
|
|
travelCalculatorOpen,
|
|
|
|
|
|
travelCalculatorBusy,
|
|
|
|
|
|
travelCalculatorError,
|
|
|
|
|
|
travelCalculatorResult,
|
|
|
|
|
|
travelCalculatorForm,
|
|
|
|
|
|
travelCalculatorCanSubmit,
|
|
|
|
|
|
buildComposerBusinessTimeLabel,
|
|
|
|
|
|
hasComposerBusinessTimeSelection,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
mergeBusinessTimeIntoExtraContext,
|
|
|
|
|
|
syncComposerBusinessTimeToReviewCard,
|
|
|
|
|
|
resolveComposerSubmitText,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
toggleComposerDatePicker,
|
|
|
|
|
|
closeComposerDatePicker,
|
|
|
|
|
|
setComposerDateMode,
|
|
|
|
|
|
handleComposerDateInputChange,
|
|
|
|
|
|
removeComposerBusinessTimeTag,
|
|
|
|
|
|
handleComposerDatePickerOutside,
|
|
|
|
|
|
applyComposerDateSelection,
|
|
|
|
|
|
resolveTravelCalculatorInitialDays,
|
|
|
|
|
|
resolveTravelCalculatorInitialLocation,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
openTravelCalculator: openTravelCalculatorInternal,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
toggleTravelCalculator: toggleTravelCalculatorInternal,
|
|
|
|
|
|
closeTravelCalculator,
|
|
|
|
|
|
formatTravelCalculatorMoney,
|
|
|
|
|
|
buildTravelCalculatorResultText,
|
|
|
|
|
|
submitTravelCalculator: submitTravelCalculatorInternal
|
|
|
|
|
|
} = useTravelReimbursementComposerTools({
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
latestReviewMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
composerTextareaRef,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
calculateTravelReimbursement,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
buildReviewSlotMap,
|
|
|
|
|
|
isValidIsoDateString,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
formatDateInputValue,
|
|
|
|
|
|
onComposerDateSelection: applyLinkedApplicationPreviewDateSelection
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-30 15:46:51 +08:00
|
|
|
|
|
|
|
|
|
|
function syncComposerDateFromApplicationEditor() {
|
|
|
|
|
|
const editor = applicationPreviewEditor.value
|
|
|
|
|
|
const today = formatDateInputValue()
|
|
|
|
|
|
composerDateMode.value = editor.dateMode === 'range' ? 'range' : 'single'
|
|
|
|
|
|
composerSingleDate.value = editor.singleDate || today
|
|
|
|
|
|
composerRangeStartDate.value = editor.rangeStartDate || composerSingleDate.value || today
|
|
|
|
|
|
composerRangeEndDate.value = editor.rangeEndDate || composerRangeStartDate.value || today
|
|
|
|
|
|
composerDatePickerOpen.value = true
|
|
|
|
|
|
travelCalculatorOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openApplicationPreviewEditorFromUi(message, fieldKey, value) {
|
|
|
|
|
|
openApplicationPreviewEditor(message, fieldKey, value)
|
|
|
|
|
|
if (fieldKey === 'time' && isApplicationPreviewEditing(message, 'time')) {
|
|
|
|
|
|
syncComposerDateFromApplicationEditor()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(composerDatePickerOpen, (open, previousOpen) => {
|
|
|
|
|
|
if (!open && previousOpen && applicationPreviewEditor.value.fieldKey === 'time') {
|
|
|
|
|
|
cancelApplicationPreviewEditor()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const canShowTravelCalculator = computed(() => activeSessionType.value === SESSION_TYPE_EXPENSE)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
fileInputMode,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
composerFilesExpanded,
|
|
|
|
|
|
visibleAttachedFiles,
|
|
|
|
|
|
hiddenAttachedFileCount,
|
|
|
|
|
|
rememberFilePreviews,
|
|
|
|
|
|
buildComposerFilePreviews,
|
|
|
|
|
|
resolveActiveClaimId,
|
|
|
|
|
|
restorePersistedDraftAttachmentPreviews,
|
|
|
|
|
|
syncComposerFilesToDraft,
|
|
|
|
|
|
triggerFileUpload,
|
|
|
|
|
|
handleFilesChange,
|
|
|
|
|
|
toggleAttachedFilesExpanded,
|
|
|
|
|
|
removeAttachedFile,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
stopAttachmentRuntime
|
|
|
|
|
|
} = useTravelReimbursementAttachments({
|
|
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
reviewFilePreviews,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
draftClaimId,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
composerUploadIntent,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
fileInputRef,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
createExpenseClaimItem,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
fetchExpenseClaimDetail,
|
|
|
|
|
|
fetchExpenseClaimItemAttachmentMeta,
|
|
|
|
|
|
fetchExpenseClaimAttachmentAsset,
|
|
|
|
|
|
uploadExpenseClaimItemAttachment,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeFilePreviews,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
isTemporaryPreviewUrl,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveAttachmentPreviewKind,
|
|
|
|
|
|
resolveDocumentPreview,
|
|
|
|
|
|
buildFilePreviews,
|
|
|
|
|
|
buildFileIdentity,
|
|
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
VISIBLE_ATTACHMENT_CHIPS,
|
|
|
|
|
|
clearInlineReviewFieldError
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
sessionRuntimeRefs = {
|
|
|
|
|
|
attachedFiles,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
composerFilesExpanded,
|
|
|
|
|
|
guidedFlowState
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-30 15:46:51 +08:00
|
|
|
|
const promptedOperationFeedbackRunIds = new Set()
|
|
|
|
|
|
|
|
|
|
|
|
function emitOperationCompleted(payload = {}, extras = {}) {
|
|
|
|
|
|
const runId = String(payload?.run_id || payload?.runId || '').trim()
|
|
|
|
|
|
const operationStatus = String(payload?.status || '').trim()
|
|
|
|
|
|
if (!runId || promptedOperationFeedbackRunIds.has(runId) || operationStatus !== 'succeeded') {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
|
|
|
|
|
|
promptedOperationFeedbackRunIds.add(runId)
|
|
|
|
|
|
return normalizeOperationFeedbackContext({
|
|
|
|
|
|
run_id: runId,
|
|
|
|
|
|
conversation_id: String(payload?.conversation_id || payload?.conversationId || conversationId.value || '').trim(),
|
|
|
|
|
|
user_id: resolveCurrentUserId(),
|
|
|
|
|
|
selected_agent: String(payload?.selected_agent || payload?.selectedAgent || '').trim(),
|
|
|
|
|
|
source: 'user_message',
|
|
|
|
|
|
session_type: activeSessionType.value,
|
|
|
|
|
|
operation_type: String(extras.operationType || 'assistant_round').trim(),
|
|
|
|
|
|
operation_status: operationStatus,
|
|
|
|
|
|
status: operationStatus,
|
|
|
|
|
|
route_reason: String(payload?.route_reason || payload?.routeReason || '').trim(),
|
|
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
trace_summary: payload?.trace_summary || payload?.traceSummary || null,
|
|
|
|
|
|
result_summary: String(result.answer || result.message || '').trim()
|
|
|
|
|
|
}, currentUser.value || {})
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const {
|
|
|
|
|
|
confirmPendingAttachmentAssociationInternal,
|
|
|
|
|
|
submitComposerInternal
|
|
|
|
|
|
} = useTravelReimbursementSubmitComposer({
|
2026-05-21 23:53:03 +08:00
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
buildAgentInsight,
|
|
|
|
|
|
buildClientTimeContext,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
buildComposerFilePreviews,
|
|
|
|
|
|
buildDraftAssociationQueryPayload,
|
|
|
|
|
|
buildErrorInsight,
|
|
|
|
|
|
buildExpenseIntentConfirmationActions,
|
|
|
|
|
|
buildExpenseIntentConfirmationMessage,
|
|
|
|
|
|
buildExpenseSceneSelectionActions,
|
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
|
buildMessageMeta,
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload,
|
|
|
|
|
|
buildOcrSummaryFromDocuments,
|
|
|
|
|
|
buildReviewFormContextFromPayload,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
clearFlowSimulationTimers,
|
|
|
|
|
|
completeFlowResult,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
composerUploadIntent,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
currentUser,
|
2026-06-01 17:07:14 +08:00
|
|
|
|
refreshCurrentUserFromBackend,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
draftClaimId,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
|
|
|
|
|
failCurrentFlowStep,
|
|
|
|
|
|
fetchExpenseClaims,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
flowRunId,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
insightPanelCollapsed,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
mergeBusinessTimeIntoExtraContext,
|
|
|
|
|
|
mergeFilePreviews,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeUploadAttachmentNames,
|
|
|
|
|
|
mergeUploadOcrDocuments,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
normalizeExpenseQueryPayload,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
props,
|
|
|
|
|
|
recognizeOcrFiles,
|
|
|
|
|
|
refreshFlowRunDetail,
|
|
|
|
|
|
rememberFilePreviews,
|
|
|
|
|
|
replaceMessage,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resetFlowRun,
|
|
|
|
|
|
resolveComposerSubmitText,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
runOrchestrator,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
shouldRequestExpenseIntentConfirmation,
|
|
|
|
|
|
shouldRequestExpenseSceneSelection,
|
|
|
|
|
|
startExpenseClaimDraftFlowStep,
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
startSemanticFlowPreview,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
syncComposerFilesToDraft,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
emitOperationCompleted,
|
2026-06-02 14:01:51 +08:00
|
|
|
|
emitDraftSaved: (payload) => emit('draft-saved', payload),
|
2026-05-30 15:46:51 +08:00
|
|
|
|
emitRequestUpdated: (payload) => emit('request-updated', payload),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
toast
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
|
|
|
|
|
const canSubmit = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
!submitting.value
|
2026-05-21 23:53:03 +08:00
|
|
|
|
&& !sessionSwitchBusy.value
|
|
|
|
|
|
&& Boolean(
|
|
|
|
|
|
composerDraft.value.trim()
|
|
|
|
|
|
|| attachedFiles.value.length
|
|
|
|
|
|
|| composerBusinessTimeTags.value.length
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-05-23 19:54:42 +08:00
|
|
|
|
const {
|
|
|
|
|
|
handleGuidedShortcut,
|
|
|
|
|
|
handleGuidedComposerSubmit,
|
|
|
|
|
|
handleGuidedSuggestedAction,
|
2026-05-27 14:35:17 +08:00
|
|
|
|
handleSceneSelectionApplicationGate,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
resetGuidedFlowState
|
|
|
|
|
|
} = useTravelReimbursementGuidedFlow({
|
|
|
|
|
|
guidedFlowState,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
openTravelCalculator,
|
|
|
|
|
|
lockSuggestedActionMessage,
|
|
|
|
|
|
submitExistingComposer: submitComposerInternal,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
currentUser,
|
2026-06-01 17:07:14 +08:00
|
|
|
|
refreshCurrentUserFromBackend,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
toast
|
|
|
|
|
|
})
|
2026-05-26 09:15:14 +08:00
|
|
|
|
function openTravelCalculator() {
|
|
|
|
|
|
if (!canShowTravelCalculator.value) {
|
|
|
|
|
|
closeTravelCalculator()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return openTravelCalculatorInternal()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function toggleTravelCalculator() {
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if (!canShowTravelCalculator.value) {
|
|
|
|
|
|
closeTravelCalculator()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return toggleTravelCalculatorInternal()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function submitTravelCalculator() {
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if (!canShowTravelCalculator.value) {
|
|
|
|
|
|
closeTravelCalculator()
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
// 兼容旧测试的源码锚点;真实 calculateTravelReimbursement 调用在 composable 内。
|
|
|
|
|
|
// calculateTravelReimbursement({ grade: String(user.grade || '').trim() })
|
|
|
|
|
|
// 根据您输入的地点和天数,匹配到您要出差的地区为,参考可报销合计
|
|
|
|
|
|
// 住宿费:${hotelRate} × ${days} = ${hotelAmount} 元
|
|
|
|
|
|
// 鏍规嵁鎮ㄨ緭鍏ョ殑鍦扮偣鍜屽ぉ鏁帮紝鍖归厤鍒版偍瑕佸嚭宸殑鍦板尯涓猴紝鍙傝€冨彲鎶ラ攢鍚堣
|
|
|
|
|
|
// 浣忓璐癸細${hotelRate} 脳 ${days} = ${hotelAmount} 鍏
|
|
|
|
|
|
// messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload)
|
|
|
|
|
|
return submitTravelCalculatorInternal()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-26 09:15:14 +08:00
|
|
|
|
watch(canShowTravelCalculator, (visible) => {
|
|
|
|
|
|
if (!visible && travelCalculatorOpen.value) {
|
|
|
|
|
|
closeTravelCalculator()
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-27 12:27:17 +08:00
|
|
|
|
const shortcuts = computed(() => {
|
2026-06-04 11:03:29 +08:00
|
|
|
|
if (isStewardSession.value) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
2026-05-27 12:27:17 +08:00
|
|
|
|
const accessibleModes = filterAssistantSessionModes(ASSISTANT_SESSION_MODE_OPTIONS, currentUser.value)
|
2026-06-06 17:19:07 +08:00
|
|
|
|
.filter((mode) => mode.key !== SESSION_TYPE_STEWARD)
|
2026-05-27 12:27:17 +08:00
|
|
|
|
const visibleModes = props.entrySource === 'budget'
|
|
|
|
|
|
? accessibleModes.filter((mode) => mode.key === SESSION_TYPE_BUDGET)
|
|
|
|
|
|
: accessibleModes
|
|
|
|
|
|
return visibleModes.map((mode) => ({
|
2026-05-25 13:35:39 +08:00
|
|
|
|
label: mode.label,
|
|
|
|
|
|
icon: mode.icon,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
action: 'switch_view',
|
2026-05-25 13:35:39 +08:00
|
|
|
|
targetSessionType: mode.key,
|
|
|
|
|
|
active: mode.key === activeSessionType.value
|
|
|
|
|
|
}))
|
2026-05-27 12:27:17 +08:00
|
|
|
|
})
|
2026-06-15 22:55:18 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [activeReviewPayload.value, activeReviewPanelScope.value],
|
|
|
|
|
|
([payload]) => {
|
|
|
|
|
|
rememberFilePreviews(buildReviewFilePreviewsFromReviewPayload(payload))
|
|
|
|
|
|
// reviewDrawerMode.value = resolveReviewRiskBriefs(payload).length
|
|
|
|
|
|
// ? REVIEW_DRAWER_MODE_RISK
|
|
|
|
|
|
// : REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
const shouldKeepFlowDrawer = reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && activeFlowSteps.value.length > 0
|
|
|
|
|
|
resetReviewDrawerFromPayload(payload)
|
|
|
|
|
|
if (shouldKeepFlowDrawer) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => hasInsightPanelContent.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available) {
|
|
|
|
|
|
insightPanelCollapsed.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewDocumentDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewRiskDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewFlowDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => [activeSessionType.value, activeFlowSteps.value.length],
|
|
|
|
|
|
([, activeCount], [, previousActiveCount] = []) => {
|
|
|
|
|
|
if (activeCount <= 0 || previousActiveCount > 0) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
|
|
|
|
|
insightPanelCollapsed.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => composerDraft.value,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
nextTick(adjustComposerTextareaHeight)
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
sessionType: activeSessionType.value,
|
|
|
|
|
|
conversationId: conversationId.value,
|
|
|
|
|
|
stewardState: stewardState.value,
|
|
|
|
|
|
draftClaimId: draftClaimId.value,
|
|
|
|
|
|
messages: messages.value,
|
|
|
|
|
|
currentInsight: currentInsight.value,
|
|
|
|
|
|
reviewFilePreviews: reviewFilePreviews.value,
|
|
|
|
|
|
composerDraft: composerDraft.value,
|
|
|
|
|
|
composerUploadIntent: composerUploadIntent.value,
|
|
|
|
|
|
guidedFlowState: guidedFlowState.value,
|
|
|
|
|
|
insightPanelCollapsed: insightPanelCollapsed.value
|
|
|
|
|
|
}),
|
|
|
|
|
|
() => {
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => [activeSessionType.value, resolveActiveClaimId()],
|
|
|
|
|
|
([sessionType, claimId]) => {
|
|
|
|
|
|
if (sessionType !== SESSION_TYPE_EXPENSE || !claimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
void restorePersistedDraftAttachmentPreviews(claimId)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.invalidatedDraftClaimId,
|
|
|
|
|
|
(claimId) => {
|
|
|
|
|
|
clearExpenseSessionForDeletedClaim(claimId)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => workbenchVisible.value,
|
|
|
|
|
|
(visible) => {
|
|
|
|
|
|
if (visible) {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maybeFinalizeDeferredClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => props.reopenToken,
|
|
|
|
|
|
(token, previousToken) => {
|
|
|
|
|
|
if (token === previousToken) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
closeAfterBusy.value = false
|
|
|
|
|
|
workbenchVisible.value = true
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => [submitting.value, reviewActionBusy.value, sessionSwitchBusy.value, workbenchVisible.value],
|
|
|
|
|
|
() => {
|
|
|
|
|
|
maybeFinalizeDeferredClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => messages.value.length,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
if (!workbenchVisible.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
document.addEventListener('click', handleComposerDatePickerOutside)
|
|
|
|
|
|
startFlowTick()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
workbenchVisible.value = true
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
currentInsight.value =
|
|
|
|
|
|
currentInsight.value
|
|
|
|
|
|
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
|
|
|
|
|
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
|
|
|
|
|
|
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
|
|
|
|
|
|
meta: ['修改申请'],
|
|
|
|
|
|
applicationPreview
|
|
|
|
|
|
}))
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
|
|
|
|
|
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
|
|
|
|
|
composerDraft.value = props.initialPrompt.trim()
|
|
|
|
|
|
attachedFiles.value = initialMerge.files
|
|
|
|
|
|
composerFilesExpanded.value = initialMerge.files.length > VISIBLE_ATTACHMENT_CHIPS
|
|
|
|
|
|
if (initialMerge.overflowCount > 0) {
|
|
|
|
|
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
})
|
|
|
|
|
|
if (props.initialPromptAutoSubmit !== false) {
|
|
|
|
|
|
submitComposer()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleComposerDatePickerOutside)
|
|
|
|
|
|
clearStewardThinkingTimers()
|
|
|
|
|
|
stopFlowRuntime()
|
|
|
|
|
|
stopAttachmentRuntime()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function scrollToBottom() {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const scrollOnce = () => {
|
2026-06-06 17:19:07 +08:00
|
|
|
|
const list = messageListRef.value?.$el || messageListRef.value
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (!list) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
list.scrollTop = list.scrollHeight
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (scrollOnce()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
scrollOnce()
|
|
|
|
|
|
requestAnimationFrame(scrollOnce)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAssistantModalAfterEnter() {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function resetCurrentSessionState() {
|
|
|
|
|
|
const emptyState = buildEmptySessionState(activeSessionType.value)
|
|
|
|
|
|
sessionSnapshots.value[activeSessionType.value] = emptyState
|
2026-05-23 19:54:42 +08:00
|
|
|
|
resetGuidedFlowState()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
applySessionState(emptyState)
|
|
|
|
|
|
resetFlowRun({ startedAt: 0, openDrawer: false })
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function clearExpenseSessionForDeletedClaim(claimId) {
|
|
|
|
|
|
const normalizedClaimId = String(claimId || '').trim()
|
|
|
|
|
|
if (!normalizedClaimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expenseSnapshot = sessionSnapshots.value[SESSION_TYPE_EXPENSE]
|
|
|
|
|
|
const snapshotMatchesDeletedClaim = String(expenseSnapshot?.draftClaimId || '').trim() === normalizedClaimId
|
|
|
|
|
|
const currentMatchesDeletedClaim =
|
|
|
|
|
|
activeSessionType.value === SESSION_TYPE_EXPENSE
|
|
|
|
|
|
&& String(resolveActiveClaimId() || '').trim() === normalizedClaimId
|
|
|
|
|
|
if (!snapshotMatchesDeletedClaim && !currentMatchesDeletedClaim) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearAssistantSessionSnapshot(resolveCurrentUserId(), SESSION_TYPE_EXPENSE)
|
|
|
|
|
|
if (currentMatchesDeletedClaim) {
|
|
|
|
|
|
resetCurrentSessionState()
|
|
|
|
|
|
toast('该草稿单据已删除,相关财务助手会话已清空。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sessionSnapshots.value[SESSION_TYPE_EXPENSE] = buildEmptySessionState(SESSION_TYPE_EXPENSE)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function adjustComposerTextareaHeight() {
|
|
|
|
|
|
if (!composerTextareaRef.value) return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const textarea = composerTextareaRef.value
|
|
|
|
|
|
textarea.style.height = 'auto'
|
|
|
|
|
|
const styles = window.getComputedStyle(textarea)
|
|
|
|
|
|
const lineHeight = Number.parseFloat(styles.lineHeight) || 20
|
|
|
|
|
|
const verticalPadding =
|
|
|
|
|
|
Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0')
|
|
|
|
|
|
const minHeight = COMPOSER_TEXTAREA_HEIGHT
|
|
|
|
|
|
const maxHeight = lineHeight * COMPOSER_MAX_ROWS + verticalPadding
|
|
|
|
|
|
const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
textarea.style.height = `${nextHeight}px`
|
|
|
|
|
|
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function handleComposerInput() {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function handleComposerEnter(event) {
|
|
|
|
|
|
if (event?.isComposing || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposer()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function replaceMessage(messageId, nextMessage) {
|
|
|
|
|
|
const index = messages.value.findIndex((item) => item.id === messageId)
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
messages.value.push(nextMessage)
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
messages.value.splice(index, 1, nextMessage)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 11:03:29 +08:00
|
|
|
|
const { submitStewardPlan, clearStewardThinkingTimers } = useStewardPlanFlow({
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
composerDraft,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
conversationId,
|
2026-06-04 11:03:29 +08:00
|
|
|
|
currentUser,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
fetchStewardPlan,
|
|
|
|
|
|
fetchStewardPlanStream,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
replaceMessage,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
2026-06-04 14:25:14 +08:00
|
|
|
|
executeStewardSuggestedAction: (message, action) => handleSuggestedAction(message, action),
|
2026-06-04 11:03:29 +08:00
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
sessionSwitchBusy,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
stewardState,
|
2026-06-04 11:03:29 +08:00
|
|
|
|
toast
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
async function runShortcut(shortcut) {
|
|
|
|
|
|
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
|
|
|
|
|
if (shortcut.targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
|
|
|
|
|
|
toast('目前暂无权限访问预算编制助手')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (shortcut.active) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await switchSessionType(shortcut.targetSessionType)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (await handleGuidedShortcut(shortcut)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const prompt = String(shortcut?.prompt || '').trim()
|
|
|
|
|
|
if (!prompt) return
|
|
|
|
|
|
composerDraft.value = prompt
|
|
|
|
|
|
submitComposer()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isSuggestedActionSelected(message, action) {
|
|
|
|
|
|
const selectedKey = String(message?.selectedSuggestedActionKey || '').trim()
|
|
|
|
|
|
return Boolean(selectedKey) && selectedKey === buildSuggestedActionKey(action)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
function lockSuggestedActionMessage(message, action) {
|
|
|
|
|
|
const messageId = String(message?.id || '').trim()
|
|
|
|
|
|
const targetMessage = messages.value.find((item) => String(item.id || '') === messageId) || message
|
|
|
|
|
|
if (!targetMessage || targetMessage.suggestedActionsLocked) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const selectedLabel = String(action?.label || actionPayload.expense_type_label || '').trim()
|
|
|
|
|
|
const nextMeta = Array.isArray(targetMessage.meta)
|
|
|
|
|
|
? targetMessage.meta.filter((item) => item !== '等待选择场景')
|
|
|
|
|
|
: []
|
|
|
|
|
|
const selectedMeta = selectedLabel ? `已选择${selectedLabel}` : '已选择场景'
|
|
|
|
|
|
|
|
|
|
|
|
targetMessage.suggestedActionsLocked = true
|
|
|
|
|
|
targetMessage.selectedSuggestedActionKey = buildSuggestedActionKey(action)
|
|
|
|
|
|
targetMessage.selectedSuggestedActionLabel = selectedLabel
|
|
|
|
|
|
targetMessage.meta = Array.from(new Set([...nextMeta, selectedMeta]))
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function buildApplicationPreviewFieldAppliedText(message, fieldLabel = '', value = '') {
|
|
|
|
|
|
const missingFields = resolveApplicationPreviewMissingFields(message)
|
|
|
|
|
|
const resolvedFieldLabel = String(fieldLabel || '补充项').trim()
|
|
|
|
|
|
const resolvedValue = String(value || '').trim()
|
|
|
|
|
|
if (missingFields.length) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
`已更新:**${resolvedFieldLabel}:${resolvedValue}**。`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
`我重新检查了一遍,当前还需要补充:**${missingFields.join('、')}**。`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'请继续补齐下方核对表里的待补充项;补齐后我再继续推进申请提交。'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
|
|
|
`已更新:**${resolvedFieldLabel}:${resolvedValue}**。`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'我已经重新同步下方申请核对表和费用测算。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'请继续核查表格内容;如果信息无误,点击确认进入审批环节。'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
2026-06-06 17:19:07 +08:00
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function isStewardApplicationPreviewFieldCompletion(targetMessage, payload = {}) {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
payload.steward_delegated_field_completion ||
|
|
|
|
|
|
String(targetMessage?.assistantName || '').trim() === STEWARD_ASSISTANT_NAME ||
|
|
|
|
|
|
targetMessage?.stewardPlan
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-06-06 17:19:07 +08:00
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
async function continueStewardApplicationFieldCompletion({
|
|
|
|
|
|
targetMessage,
|
|
|
|
|
|
action,
|
|
|
|
|
|
sourcePreview,
|
|
|
|
|
|
fieldKey,
|
|
|
|
|
|
fieldLabel,
|
|
|
|
|
|
value
|
|
|
|
|
|
}) {
|
|
|
|
|
|
if (!lockSuggestedActionMessage(targetMessage, action)) {
|
|
|
|
|
|
return true
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
|
|
|
|
|
|
const continuation = buildStewardFieldCompletionContinuation(
|
|
|
|
|
|
targetMessage?.stewardContinuation || null,
|
|
|
|
|
|
fieldKey,
|
|
|
|
|
|
value
|
|
|
|
|
|
)
|
|
|
|
|
|
const userText = `选择${fieldLabel || '补充项'}:${value}`
|
|
|
|
|
|
const carryText = buildStewardFieldCompletionRawText({
|
|
|
|
|
|
preview: sourcePreview,
|
|
|
|
|
|
fieldKey,
|
|
|
|
|
|
fieldLabel,
|
|
|
|
|
|
value,
|
|
|
|
|
|
continuation
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!action?.suppressUserEcho) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText))
|
|
|
|
|
|
}
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
await submitComposerInternal({
|
|
|
|
|
|
rawText: carryText,
|
|
|
|
|
|
userText,
|
|
|
|
|
|
pendingText: '小财管家正在根据补齐信息查询票据并测算费用...',
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
skipScopeGuard: true,
|
|
|
|
|
|
skipApplicationModelReview: true,
|
|
|
|
|
|
skipStewardPlan: true,
|
|
|
|
|
|
skipUserMessage: true,
|
|
|
|
|
|
sessionTypeOverride: SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
stewardContinuation: continuation
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
async function applyApplicationPreviewFieldAction(message, action) {
|
|
|
|
|
|
const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const fieldKey = String(payload.field_key || payload.fieldKey || '').trim()
|
|
|
|
|
|
const fieldLabel = String(payload.field_label || payload.fieldLabel || action?.label || '').trim()
|
|
|
|
|
|
let value = String(payload.value || action?.label || '').trim()
|
|
|
|
|
|
const targetMessage = messages.value.find((item) => String(item.id || '') === String(message?.id || '')) || message
|
|
|
|
|
|
const sourcePreview = targetMessage?.applicationPreview ||
|
|
|
|
|
|
payload.applicationPreview ||
|
|
|
|
|
|
payload.application_preview ||
|
|
|
|
|
|
payload.preview ||
|
|
|
|
|
|
null
|
|
|
|
|
|
if (!sourcePreview || !fieldKey || !value) {
|
|
|
|
|
|
return false
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
if (fieldKey === 'transportMode') {
|
|
|
|
|
|
value = normalizeTransportModeOption(value, '')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (fieldKey === 'transportMode' && !APPLICATION_TRANSPORT_MODE_OPTIONS.includes(value)) {
|
|
|
|
|
|
toast('请选择有效的出行方式。')
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isStewardApplicationPreviewFieldCompletion(targetMessage, payload)) {
|
|
|
|
|
|
return continueStewardApplicationFieldCompletion({
|
|
|
|
|
|
targetMessage,
|
|
|
|
|
|
action,
|
|
|
|
|
|
sourcePreview,
|
|
|
|
|
|
fieldKey,
|
|
|
|
|
|
fieldLabel,
|
|
|
|
|
|
value
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!lockSuggestedActionMessage(targetMessage, action)) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
targetMessage.applicationPreview = normalizeApplicationPreview(sourcePreview)
|
|
|
|
|
|
messages.value.push(createMessage('user', `选择${fieldLabel || '补充项'}:${value}`))
|
|
|
|
|
|
openApplicationPreviewEditor(targetMessage, fieldKey, targetMessage.applicationPreview?.fields?.[fieldKey] || '')
|
|
|
|
|
|
applicationPreviewEditor.value = {
|
|
|
|
|
|
...applicationPreviewEditor.value,
|
|
|
|
|
|
draftValue: value
|
|
|
|
|
|
}
|
|
|
|
|
|
await commitApplicationPreviewEditor(targetMessage)
|
|
|
|
|
|
if (String(targetMessage.assistantName || '').trim() === STEWARD_ASSISTANT_NAME || targetMessage.stewardPlan) {
|
|
|
|
|
|
targetMessage.assistantName = STEWARD_ASSISTANT_NAME
|
|
|
|
|
|
targetMessage.text = buildApplicationPreviewFieldAppliedText(targetMessage, fieldLabel, value)
|
|
|
|
|
|
const nextMeta = Array.isArray(targetMessage.meta) ? targetMessage.meta : []
|
|
|
|
|
|
targetMessage.meta = Array.from(new Set([
|
|
|
|
|
|
STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
resolveApplicationPreviewMissingFields(targetMessage).length ? '等待补充' : '等待用户确认',
|
|
|
|
|
|
...nextMeta.filter((item) => String(item || '').trim() && item !== STEWARD_ASSISTANT_NAME)
|
|
|
|
|
|
])).slice(0, 4)
|
|
|
|
|
|
}
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
return true
|
2026-06-06 17:19:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function pushExpenseSceneSelectionPrompt(originalMessage) {
|
|
|
|
|
|
const sourceText = String(originalMessage || '').trim()
|
|
|
|
|
|
if (!sourceText) {
|
2026-05-21 16:09:47 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
|
|
|
|
|
|
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
|
|
|
|
|
|
messages.value.push(createMessage('user', '我要报销'))
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), [], {
|
|
|
|
|
|
meta: ['等待选择场景'],
|
|
|
|
|
|
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applySuggestedActionPrefill(action) {
|
|
|
|
|
|
const prefillText = resolveSuggestedActionPrefill(action)
|
|
|
|
|
|
if (!prefillText) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
composerDraft.value = mergeComposerPrefill(composerDraft.value, prefillText)
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
composerTextareaRef.value?.focus()
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSuggestedAction(message, action) {
|
|
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
|
|
|
|
if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return
|
|
|
|
|
|
if (message?.suggestedActionsLocked) return
|
|
|
|
|
|
if (applySuggestedActionPrefill(action)) return
|
|
|
|
|
|
if (await handleGuidedSuggestedAction(message, action)) return
|
|
|
|
|
|
if (await handleSceneSelectionApplicationGate(message, action)) return
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === APPLICATION_PREVIEW_FIELD_ACTION_SET) {
|
|
|
|
|
|
await applyApplicationPreviewFieldAction(message, action)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'open_application_detail') {
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const claimId = String(actionPayload.claim_id || actionPayload.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
toast('当前没有可查看的申请单据。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await router.push({
|
|
|
|
|
|
name: 'app-document-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'open_receipt_folder') {
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await router.push({ name: 'app-receiptFolder' })
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'continue_upload_with_unlinked_receipts') {
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: String(actionPayload.raw_text || composerDraft.value || '').trim(),
|
|
|
|
|
|
files: Array.from(attachedFiles.value || []),
|
|
|
|
|
|
skipReceiptFolderUnlinkedPrompt: true
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === TRAVEL_PLANNING_ACTION_GENERATE) {
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
const sourcePreview = action?.payload?.applicationPreview || action?.payload?.preview || null
|
|
|
|
|
|
const sourceDraftPayload = action?.payload?.draftPayload || action?.payload?.draft_payload || null
|
|
|
|
|
|
const recommendation = buildTravelPlanningRecommendation(sourcePreview, sourceDraftPayload)
|
|
|
|
|
|
if (recommendation) {
|
|
|
|
|
|
messages.value.push(createMessage('user', '生成行程规划'))
|
|
|
|
|
|
messages.value.push(createMessage('assistant', recommendation, [], {
|
|
|
|
|
|
meta: ['行程规划建议']
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === TRAVEL_PLANNING_ACTION_SKIP) {
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
messages.value.push(createMessage('assistant', '好的,本次先保留申请结果。后续需要规划交通或酒店时,可以继续在这里告诉我。', [], {
|
|
|
|
|
|
meta: ['暂不规划']
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) {
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const targetSessionType = String(actionPayload.session_type || '').trim()
|
|
|
|
|
|
if (!targetSessionType) return
|
|
|
|
|
|
if (targetSessionType === SESSION_TYPE_BUDGET && !canUseBudgetAssistantSession(currentUser.value)) {
|
|
|
|
|
|
toast('目前暂无权限访问预算编制助手')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const carryText = String(actionPayload.carry_text || '').trim()
|
|
|
|
|
|
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
if (actionPayload.steward_confirm_flow) {
|
|
|
|
|
|
await handleStewardRuntimeDecision({
|
|
|
|
|
|
rawText: String(actionPayload.selected_flow_label || action.label || carryText || '').trim(),
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
skipUserMessage: false
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-18 22:12:24 +08:00
|
|
|
|
if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') {
|
|
|
|
|
|
pushExpenseSceneSelectionPrompt(carryText)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
if (String(actionPayload.steward_plan_id || '').trim()) {
|
|
|
|
|
|
const confirmedByText = Boolean(action.confirmedByText)
|
|
|
|
|
|
delete action.confirmedByText
|
|
|
|
|
|
await submitComposerInternal({
|
|
|
|
|
|
rawText: carryText,
|
|
|
|
|
|
userText: action.label || '确定',
|
|
|
|
|
|
pendingText: targetSessionType === SESSION_TYPE_APPLICATION
|
|
|
|
|
|
? '小财管家正在调用申请助手生成申请单核对结果...'
|
|
|
|
|
|
: '小财管家正在调用报销助手整理报销核对结果...',
|
|
|
|
|
|
files: carryFiles,
|
|
|
|
|
|
skipScopeGuard: true,
|
|
|
|
|
|
skipApplicationModelReview: targetSessionType === SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
skipStewardSlotDecision: targetSessionType === SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
skipStewardPlan: true,
|
|
|
|
|
|
skipUserMessage: confirmedByText,
|
|
|
|
|
|
sessionTypeOverride: targetSessionType,
|
|
|
|
|
|
stewardContinuation: {
|
|
|
|
|
|
planId: String(actionPayload.steward_plan_id || '').trim(),
|
|
|
|
|
|
currentTaskId: String(actionPayload.steward_next_task_id || '').trim(),
|
|
|
|
|
|
currentTask: actionPayload.steward_current_task || null,
|
|
|
|
|
|
remainingTasks: Array.isArray(actionPayload.steward_remaining_tasks)
|
|
|
|
|
|
? actionPayload.steward_remaining_tasks
|
|
|
|
|
|
: []
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await switchSessionType(targetSessionType)
|
|
|
|
|
|
if (carryText) {
|
|
|
|
|
|
composerDraft.value = carryText
|
|
|
|
|
|
}
|
|
|
|
|
|
if (carryFiles.length) {
|
|
|
|
|
|
const fileMergeResult = mergeFilesWithLimit([], carryFiles, MAX_ATTACHMENTS)
|
|
|
|
|
|
attachedFiles.value = fileMergeResult.files
|
|
|
|
|
|
composerFilesExpanded.value = fileMergeResult.files.length > VISIBLE_ATTACHMENT_CHIPS
|
|
|
|
|
|
}
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
if (actionPayload.auto_submit && carryText) {
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: carryText,
|
|
|
|
|
|
userText: action.label || '确认继续处理',
|
|
|
|
|
|
pendingText: '正在按确认内容继续处理...',
|
|
|
|
|
|
files: carryFiles,
|
|
|
|
|
|
skipScopeGuard: true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType === 'confirm_expense_intent') {
|
|
|
|
|
|
const originalMessage = String(action?.payload?.original_message || message?.text || '').trim()
|
|
|
|
|
|
if (!originalMessage) return
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
pushExpenseSceneSelectionPrompt(originalMessage)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType !== 'select_expense_type') {
|
|
|
|
|
|
const fallbackText = String(action?.description || action?.label || '').trim()
|
|
|
|
|
|
if (!fallbackText) return
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: fallbackText,
|
|
|
|
|
|
userText: fallbackText,
|
|
|
|
|
|
pendingText: '正在继续处理...'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const expenseType = String(actionPayload.expense_type || '').trim()
|
|
|
|
|
|
const expenseTypeLabel = String(actionPayload.expense_type_label || action?.label || '').trim()
|
|
|
|
|
|
const originalMessage = String(actionPayload.original_message || message?.text || '').trim()
|
|
|
|
|
|
if (!expenseTypeLabel || !originalMessage) return
|
|
|
|
|
|
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: `${originalMessage}\n用户选择报销场景:${expenseTypeLabel}`,
|
|
|
|
|
|
userText: `选择${expenseTypeLabel}`,
|
|
|
|
|
|
pendingText: `已选择${expenseTypeLabel},正在按该场景识别...`,
|
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
draft_claim_id: '',
|
|
|
|
|
|
user_input_text: originalMessage,
|
|
|
|
|
|
expense_scene_selection: {
|
|
|
|
|
|
expense_type: expenseType,
|
|
|
|
|
|
expense_type_label: expenseTypeLabel,
|
|
|
|
|
|
original_message: originalMessage
|
|
|
|
|
|
},
|
|
|
|
|
|
review_form_values: {
|
|
|
|
|
|
expense_type: expenseTypeLabel
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleInsightPanel() {
|
|
|
|
|
|
if (!hasInsightPanelContent.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
insightPanelCollapsed.value = !insightPanelCollapsed.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchReviewDrawerMode(mode) {
|
|
|
|
|
|
if (reviewDrawerMode.value === mode) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
reviewDrawerMode.value = mode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function switchToReviewOverviewDrawer() {
|
|
|
|
|
|
if (!reviewOverviewDrawerAvailable.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW)
|
|
|
|
|
|
}
|
2026-06-06 17:19:07 +08:00
|
|
|
|
|
2026-06-13 14:52:26 +00:00
|
|
|
|
function toggleReviewDocumentDrawer() {
|
|
|
|
|
|
if (!reviewDocumentDrawerAvailable.value) {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_DOCUMENTS)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleReviewRiskDrawer() {
|
|
|
|
|
|
if (!reviewRiskDrawerAvailable.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toggleReviewFlowDrawer() {
|
|
|
|
|
|
if (!reviewFlowDrawerAvailable.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_FLOW)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function queryDraftByClaimNo(claimNo) {
|
|
|
|
|
|
const normalized = String(claimNo || '').trim()
|
|
|
|
|
|
if (!normalized || submitting.value || reviewActionBusy.value) return
|
|
|
|
|
|
submitComposer({
|
|
|
|
|
|
rawText: `查看报销草稿 ${normalized} 的当前信息`,
|
|
|
|
|
|
userText: `查看草稿 ${normalized}`,
|
|
|
|
|
|
systemGenerated: true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function appendReviewRiskBriefToConversation(item) {
|
|
|
|
|
|
if (!item) return
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildReviewRiskConversationText(item, resolveReviewRiskDetailTarget()), [], {
|
|
|
|
|
|
meta: [item.sourceLabel || item.levelLabel || '风险提示'],
|
|
|
|
|
|
metaTone: item.level || 'low'
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function appendExpenseQueryRiskToConversation(record, risk) {
|
|
|
|
|
|
if (!record || !risk) return
|
|
|
|
|
|
const claimId = String(record.claimId || '').trim()
|
|
|
|
|
|
const claimNo = String(record.claimNo || '该单据').trim()
|
|
|
|
|
|
const route = claimId
|
|
|
|
|
|
? router.resolve({
|
|
|
|
|
|
name: 'app-document-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
: null
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
buildReviewRiskConversationText(
|
|
|
|
|
|
{
|
|
|
|
|
|
title: `${claimNo} ${risk.levelLabel || '风险提示'}:${risk.title || '风险提示'}`,
|
|
|
|
|
|
summary: risk.summary,
|
|
|
|
|
|
detail: risk.detail,
|
|
|
|
|
|
suggestion: '请进入单据详情核对费用明细、票据附件和附加说明;如属于合理例外,请补充业务说明后再继续流程。',
|
|
|
|
|
|
sourceLabel: risk.levelLabel,
|
|
|
|
|
|
level: risk.level
|
|
|
|
|
|
},
|
|
|
|
|
|
route?.href
|
|
|
|
|
|
? {
|
|
|
|
|
|
href: route.href,
|
|
|
|
|
|
label: `进入 ${claimNo} 详情重新填写`
|
|
|
|
|
|
}
|
|
|
|
|
|
: {}
|
|
|
|
|
|
),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: [`${claimNo} 风险详情`],
|
|
|
|
|
|
metaTone: risk.level || 'medium'
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReviewDetailTarget(message = null) {
|
|
|
|
|
|
const latestDraftMessage = [...messages.value].reverse().find((item) => item?.draftPayload)
|
|
|
|
|
|
const candidates = [
|
|
|
|
|
|
message?.draftPayload,
|
|
|
|
|
|
currentInsight.value.agent?.draftPayload,
|
|
|
|
|
|
latestReviewMessage.value?.draftPayload,
|
|
|
|
|
|
latestDraftMessage?.draftPayload,
|
|
|
|
|
|
linkedRequest.value
|
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
|
const claimTarget = candidates.find((item) => String(item?.claim_id || item?.claimId || item?.id || '').trim())
|
|
|
|
|
|
const claimId = String(claimTarget?.claim_id || claimTarget?.claimId || claimTarget?.id || draftClaimId.value || resolveActiveClaimId() || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
const claimNoTarget = candidates.find((item) => String(item?.claim_no || item?.claimNo || item?.documentNo || '').trim())
|
|
|
|
|
|
const claimNo = String(claimNoTarget?.claim_no || claimNoTarget?.claimNo || claimNoTarget?.documentNo || '').trim()
|
|
|
|
|
|
const route = router.resolve({
|
|
|
|
|
|
name: 'app-document-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
|
|
|
href: route.href,
|
|
|
|
|
|
label: claimNo ? `进入 ${claimNo} 详情重新填写` : '进入该单据详情重新填写'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReviewRiskDetailTarget() {
|
|
|
|
|
|
return resolveReviewDetailTarget()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewNextStepRichCopyForMessage(message) {
|
|
|
|
|
|
const target = resolveReviewDetailTarget(message)
|
|
|
|
|
|
return buildReviewNextStepRichCopy(message?.reviewPayload, {
|
|
|
|
|
|
detailHref: target.href || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageBubbleClass(message) {
|
2026-06-18 22:12:24 +08:00
|
|
|
|
if (message?.role === 'assistant' && message?.assistantVariant === 'compact_guidance') {
|
|
|
|
|
|
return 'message-bubble-compact-guidance'
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
if (message?.role === 'assistant' && message?.budgetReport) {
|
|
|
|
|
|
return 'message-bubble-budget-report'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message?.role === 'assistant' && message?.applicationPreview) {
|
|
|
|
|
|
return 'message-bubble-application-preview'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message?.role !== 'assistant' || !resolveReviewNextStepAction(message?.reviewPayload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const counts = buildReviewRiskLevelCounts(message.reviewPayload)
|
|
|
|
|
|
if (counts.high > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-high'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (counts.medium > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-medium'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (counts.low > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-low'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openReviewNextStepConfirm(message) {
|
|
|
|
|
|
const action = resolveReviewNextStepAction(message?.reviewPayload)
|
|
|
|
|
|
if (!action) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
message,
|
|
|
|
|
|
action
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeReviewNextStepConfirm() {
|
|
|
|
|
|
if (reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmReviewNextStepSubmit() {
|
|
|
|
|
|
const message = nextStepConfirmDialog.value.message
|
|
|
|
|
|
const action = nextStepConfirmDialog.value.action
|
|
|
|
|
|
if (!message || !action || reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await handleReviewActionInternal(message, action)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildApplicationPreviewFooterText(message) {
|
|
|
|
|
|
if (!message?.applicationPreview) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return buildApplicationPreviewFooterMessage(message.applicationPreview)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isApplicationDraftPayload(draftPayload) {
|
|
|
|
|
|
return String(draftPayload?.draft_type || '').trim() === 'expense_application'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveDraftPayloadBodyField(draftPayload, label) {
|
|
|
|
|
|
const body = String(draftPayload?.body || '')
|
|
|
|
|
|
const pattern = new RegExp(`^${label}:(.+)$`, 'm')
|
|
|
|
|
|
return String(body.match(pattern)?.[1] || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveApplicationDraftStatusLabel(draftPayload) {
|
|
|
|
|
|
const status = String(draftPayload?.status || '').trim()
|
|
|
|
|
|
if (status === 'submitted') return '审批中'
|
|
|
|
|
|
return status || '已生成'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildApplicationDraftSummaryItems(draftPayload) {
|
|
|
|
|
|
if (!isApplicationDraftPayload(draftPayload)) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '单号', value: String(draftPayload?.claim_no || '').trim() || '待生成' },
|
|
|
|
|
|
{ label: '类型', value: String(draftPayload?.title || '').trim() || '费用申请' },
|
|
|
|
|
|
{ label: '节点', value: String(draftPayload?.approval_stage || '').trim() || '直属领导审批' },
|
|
|
|
|
|
{ label: '时间', value: resolveDraftPayloadBodyField(draftPayload, '发生时间') },
|
|
|
|
|
|
{ label: '费用', value: resolveDraftPayloadBodyField(draftPayload, '用户预估费用') }
|
|
|
|
|
|
].filter((item) => String(item.value || '').trim() && item.value !== '待补充')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldShowDraftSavedCard(message) {
|
|
|
|
|
|
const draftPayload = message?.draftPayload || null
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
draftPayload
|
|
|
|
|
|
&& (
|
|
|
|
|
|
String(draftPayload.claim_id || draftPayload.claimId || '').trim()
|
|
|
|
|
|
|| String(draftPayload.claim_no || draftPayload.claimNo || '').trim()
|
|
|
|
|
|
|| String(draftPayload.title || '').trim()
|
|
|
|
|
|
|| String(draftPayload.body || '').trim()
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canOpenDraftDetail(message) {
|
|
|
|
|
|
const draftPayload = message?.draftPayload || {}
|
|
|
|
|
|
return Boolean(String(draftPayload.claim_id || draftPayload.claimId || '').trim())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveReimbursementDraftClaimNo(draftPayload) {
|
|
|
|
|
|
return String(
|
|
|
|
|
|
draftPayload?.claim_no
|
|
|
|
|
|
|| draftPayload?.claimNo
|
|
|
|
|
|
|| draftPayload?.claim_id
|
|
|
|
|
|
|| draftPayload?.claimId
|
|
|
|
|
|
|| ''
|
|
|
|
|
|
).trim() || '保存后生成'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateMessageOperationFeedback(message, patch = {}) {
|
|
|
|
|
|
if (!message?.id) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value = messages.value.map((item) => (
|
|
|
|
|
|
item.id === message.id
|
|
|
|
|
|
? {
|
|
|
|
|
|
...item,
|
|
|
|
|
|
operationFeedback: {
|
|
|
|
|
|
...(item.operationFeedback || {}),
|
|
|
|
|
|
...patch
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
: item
|
|
|
|
|
|
))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldShowAssistantMessageActions(message) {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
message?.role === 'assistant'
|
|
|
|
|
|
&& (
|
|
|
|
|
|
String(message.text || '').trim()
|
|
|
|
|
|
|| message.applicationPreview
|
|
|
|
|
|
|| message.reviewPayload
|
|
|
|
|
|
|| message.queryPayload
|
|
|
|
|
|
|| message.draftPayload
|
|
|
|
|
|
|| message.budgetReport
|
|
|
|
|
|
)
|
|
|
|
|
|
&& !(message.stewardPlan?.streamStatus === 'streaming' && !String(message.text || '').trim())
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageActionText(message) {
|
|
|
|
|
|
const parts = []
|
|
|
|
|
|
const text = String(message?.text || '').trim()
|
|
|
|
|
|
if (text) {
|
|
|
|
|
|
parts.push(text)
|
|
|
|
|
|
}
|
|
|
|
|
|
const applicationPreview = message?.applicationPreview
|
|
|
|
|
|
? normalizeApplicationPreview(message.applicationPreview)
|
|
|
|
|
|
: null
|
|
|
|
|
|
if (applicationPreview?.fields) {
|
|
|
|
|
|
const previewLines = resolveApplicationPreviewRows({ applicationPreview }).map((row) =>
|
|
|
|
|
|
`${row.label}:${row.value || '待补充'}`
|
|
|
|
|
|
)
|
|
|
|
|
|
if (previewLines.length) {
|
|
|
|
|
|
parts.push(previewLines.join('\n'))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message?.draftPayload) {
|
|
|
|
|
|
const claimNo = resolveReimbursementDraftClaimNo(message.draftPayload)
|
|
|
|
|
|
if (claimNo) {
|
|
|
|
|
|
parts.push(`单据:${claimNo}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return parts.join('\n\n').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function copyAssistantMessage(message) {
|
|
|
|
|
|
const text = buildMessageActionText(message)
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
toast('当前消息没有可复制的内容。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (globalThis.navigator?.clipboard?.writeText) {
|
|
|
|
|
|
await globalThis.navigator.clipboard.writeText(text)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const textarea = globalThis.document.createElement('textarea')
|
|
|
|
|
|
textarea.value = text
|
|
|
|
|
|
textarea.setAttribute('readonly', 'readonly')
|
|
|
|
|
|
textarea.style.position = 'fixed'
|
|
|
|
|
|
textarea.style.opacity = '0'
|
|
|
|
|
|
globalThis.document.body.appendChild(textarea)
|
|
|
|
|
|
textarea.select()
|
|
|
|
|
|
globalThis.document.execCommand('copy')
|
|
|
|
|
|
globalThis.document.body.removeChild(textarea)
|
|
|
|
|
|
}
|
|
|
|
|
|
toast('已复制。')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Failed to copy assistant message:', error)
|
|
|
|
|
|
toast('复制失败,请稍后重试。')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function speakAssistantMessage(message) {
|
|
|
|
|
|
const text = buildMessageActionText(message)
|
|
|
|
|
|
if (!text) {
|
|
|
|
|
|
toast('当前消息没有可播报的内容。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const speech = globalThis.speechSynthesis
|
|
|
|
|
|
if (!speech || typeof globalThis.SpeechSynthesisUtterance === 'undefined') {
|
|
|
|
|
|
toast('当前浏览器不支持语音播报。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
speech.cancel()
|
|
|
|
|
|
const utterance = new globalThis.SpeechSynthesisUtterance(text.slice(0, 1200))
|
|
|
|
|
|
utterance.lang = 'zh-CN'
|
|
|
|
|
|
utterance.rate = 1
|
|
|
|
|
|
speech.speak(utterance)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageOperationFeedbackContext(message) {
|
|
|
|
|
|
const existingContext = message?.operationFeedback?.context || null
|
|
|
|
|
|
if (existingContext) {
|
|
|
|
|
|
return existingContext
|
|
|
|
|
|
}
|
|
|
|
|
|
const messageId = String(message?.id || '').trim()
|
|
|
|
|
|
const assistantName = String(message?.assistantName || '').trim()
|
|
|
|
|
|
return normalizeOperationFeedbackContext({
|
|
|
|
|
|
run_id: messageId.slice(0, 50) || null,
|
|
|
|
|
|
conversation_id: String(conversationId.value || '').trim(),
|
|
|
|
|
|
user_id: resolveCurrentUserId(),
|
|
|
|
|
|
selected_agent: assistantName || (message?.stewardPlan ? STEWARD_ASSISTANT_NAME : 'user_agent'),
|
|
|
|
|
|
source: 'assistant_message_action',
|
|
|
|
|
|
session_type: activeSessionType.value,
|
|
|
|
|
|
operation_type: message?.stewardPlan ? 'steward_message' : 'assistant_message',
|
|
|
|
|
|
operation_status: 'succeeded',
|
|
|
|
|
|
status: 'succeeded',
|
|
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
result_summary: buildMessageActionText(message).slice(0, 500)
|
|
|
|
|
|
}, currentUser.value || {})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitOperationFeedbackForMessage(message, feedback = {}) {
|
|
|
|
|
|
const rating = Number(feedback.rating || 0)
|
|
|
|
|
|
if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
|
|
|
|
|
|
updateMessageOperationFeedback(message, { error: '请选择 1 到 5 星评分。' })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const context = buildMessageOperationFeedbackContext(message)
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateMessageOperationFeedback(message, {
|
|
|
|
|
|
submitting: true,
|
|
|
|
|
|
rating,
|
|
|
|
|
|
reason: String(feedback.reason || '').trim(),
|
|
|
|
|
|
context,
|
|
|
|
|
|
error: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
try {
|
|
|
|
|
|
await createOperationFeedback(
|
|
|
|
|
|
buildOperationFeedbackPayload(context, feedback, currentUser.value || {})
|
|
|
|
|
|
)
|
|
|
|
|
|
updateMessageOperationFeedback(message, {
|
|
|
|
|
|
submitting: false,
|
|
|
|
|
|
submitted: true,
|
|
|
|
|
|
dismissed: false,
|
|
|
|
|
|
rating,
|
|
|
|
|
|
reason: String(feedback.reason || '').trim(),
|
|
|
|
|
|
error: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
updateMessageOperationFeedback(message, {
|
|
|
|
|
|
submitting: false,
|
|
|
|
|
|
error: error?.message || '评价提交失败,请稍后重试。'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isMessageFeedbackSelected(message, rating) {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
message?.operationFeedback?.submitted
|
|
|
|
|
|
&& Number(message.operationFeedback.rating || 0) === Number(rating || 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function openApplicationDraftDetail(message) {
|
|
|
|
|
|
const draftPayload = message?.draftPayload || {}
|
|
|
|
|
|
const claimId = String(draftPayload.claim_id || draftPayload.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
toast('暂未获取到单据 ID,稍后可在单据中心查看。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await router.push({
|
|
|
|
|
|
name: 'app-document-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveApplicationPreviewMissingFields(message) {
|
|
|
|
|
|
if (!message?.applicationPreview) {
|
|
|
|
|
|
return []
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
|
|
|
|
|
|
return Array.isArray(normalizedPreview.missingFields) ? normalizedPreview.missingFields : []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isApplicationSubmitConfirmationText(value = '') {
|
|
|
|
|
|
const normalized = String(value || '')
|
|
|
|
|
|
.replace(/\s+/g, '')
|
|
|
|
|
|
.replace(/[,,。.!!??;;::]/g, '')
|
|
|
|
|
|
if (!normalized || APPLICATION_SUBMIT_CANCEL_TEXT_PATTERN.test(normalized)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return APPLICATION_SUBMIT_CONFIRM_TEXT_PATTERN.test(normalized)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeStewardRuntimeInputText(value = '') {
|
|
|
|
|
|
return String(value || '')
|
|
|
|
|
|
.replace(/\s+/g, '')
|
|
|
|
|
|
.replace(/[,,。.!!??;;::]/g, '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isStewardRuntimeContinueText(value = '') {
|
|
|
|
|
|
const normalized = normalizeStewardRuntimeInputText(value)
|
|
|
|
|
|
return Boolean(normalized && STEWARD_RUNTIME_CONTINUE_TEXT_PATTERN.test(normalized))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isStewardRuntimeCancelText(value = '') {
|
|
|
|
|
|
const normalized = normalizeStewardRuntimeInputText(value)
|
|
|
|
|
|
return Boolean(normalized && STEWARD_RUNTIME_CANCEL_TEXT_PATTERN.test(normalized))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStewardRuntimeTransportAlias(value = '') {
|
|
|
|
|
|
const normalized = normalizeStewardRuntimeInputText(value)
|
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const matchedModes = []
|
|
|
|
|
|
if (/火车|高铁|动车|列车|铁路/.test(normalized)) {
|
|
|
|
|
|
matchedModes.push('火车')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/飞机|机票|航班|航空/.test(normalized)) {
|
|
|
|
|
|
matchedModes.push('飞机')
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/轮船|船票|客轮|渡轮|坐船/.test(normalized)) {
|
|
|
|
|
|
matchedModes.push('轮船')
|
|
|
|
|
|
}
|
|
|
|
|
|
return matchedModes.length === 1 ? matchedModes[0] : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldPlanNewStewardTasksLocally(rawText = '', runtimeState = {}) {
|
|
|
|
|
|
const text = String(rawText || '').trim()
|
|
|
|
|
|
const normalized = normalizeStewardRuntimeInputText(text)
|
|
|
|
|
|
if (
|
|
|
|
|
|
normalized.length < STEWARD_RUNTIME_NEW_TASK_MIN_LENGTH ||
|
|
|
|
|
|
isApplicationSubmitConfirmationText(normalized) ||
|
|
|
|
|
|
isStewardRuntimeContinueText(normalized) ||
|
|
|
|
|
|
isStewardRuntimeCancelText(normalized)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!STEWARD_RUNTIME_BUSINESS_HINT_PATTERN.test(text) || !STEWARD_RUNTIME_TIME_OR_ACTION_HINT_PATTERN.test(text)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const waitingFor = String(runtimeState?.waiting_for || '').trim()
|
|
|
|
|
|
if (waitingFor && STEWARD_RUNTIME_CURRENT_CONTEXT_HINT_PATTERN.test(text)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findLatestApplicationPreviewMessage() {
|
|
|
|
|
|
for (const message of [...messages.value].reverse()) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
message?.role !== 'assistant' ||
|
|
|
|
|
|
!message.applicationPreview ||
|
|
|
|
|
|
message.applicationSubmitConfirmed
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
return message
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findPendingApplicationSubmitMessage() {
|
|
|
|
|
|
const message = findLatestApplicationPreviewMessage()
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
|
|
|
|
|
|
if (normalizedPreview.readyToSubmit) {
|
|
|
|
|
|
message.applicationPreview = normalizedPreview
|
|
|
|
|
|
return message
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pushApplicationSubmitBlockedMessage(userText = '', message = null, options = {}) {
|
|
|
|
|
|
const normalizedPreview = normalizeApplicationPreview(message?.applicationPreview || {})
|
|
|
|
|
|
const missingFields = Array.isArray(normalizedPreview.missingFields)
|
|
|
|
|
|
? normalizedPreview.missingFields
|
|
|
|
|
|
: []
|
|
|
|
|
|
if (userText && !options.userMessageAlreadyAdded) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText))
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
[
|
|
|
|
|
|
'我理解你是在确认当前申请单,但这张申请单还不能提交。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
missingFields.length
|
|
|
|
|
|
? `还需要先补充:**${missingFields.join('、')}**。`
|
|
|
|
|
|
: '请先把申请核对表中的待补充信息补齐。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'补齐后再输入“确认”,我会继续提交至审批流程。'
|
|
|
|
|
|
].join('\n'),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
assistantName: String(message?.assistantName || '').trim() || undefined,
|
|
|
|
|
|
meta: ['等待补充']
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleApplicationSubmitConfirmationText(options = {}) {
|
|
|
|
|
|
const rawText = String(options.rawText ?? composerDraft.value ?? '').trim()
|
|
|
|
|
|
const files = Array.from(options.files ?? attachedFiles.value ?? [])
|
|
|
|
|
|
if (!isApplicationSubmitConfirmationText(rawText) || files.length) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const latestApplicationMessage = findLatestApplicationPreviewMessage()
|
|
|
|
|
|
if (!latestApplicationMessage) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const targetMessage = findPendingApplicationSubmitMessage()
|
|
|
|
|
|
if (!targetMessage) {
|
|
|
|
|
|
pushApplicationSubmitBlockedMessage(rawText, latestApplicationMessage)
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
applicationSubmitConfirmDialog.value = {
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
message: targetMessage
|
|
|
|
|
|
}
|
|
|
|
|
|
await confirmApplicationSubmit({ userText: rawText })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findPendingStewardSuggestedActionContext(decision = null) {
|
|
|
|
|
|
const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim()
|
|
|
|
|
|
const targetTaskId = String(decision?.target_task_id || decision?.targetTaskId || '').trim()
|
|
|
|
|
|
for (const message of [...messages.value].reverse()) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
message?.role !== 'assistant' ||
|
|
|
|
|
|
message.suggestedActionsLocked ||
|
|
|
|
|
|
!Array.isArray(message.suggestedActions) ||
|
|
|
|
|
|
!message.suggestedActions.length
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if (targetMessageId && String(message.id || '') !== targetMessageId) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const action = message.suggestedActions.find((item) => {
|
|
|
|
|
|
if (String(item?.action_type || '').trim() === APPLICATION_PREVIEW_FIELD_ACTION_SET) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {}
|
|
|
|
|
|
return !targetTaskId ||
|
|
|
|
|
|
String(payload.steward_next_task_id || payload.target_task_id || '').trim() === targetTaskId
|
|
|
|
|
|
}) || message.suggestedActions[0]
|
|
|
|
|
|
if (action) {
|
|
|
|
|
|
return { message, action }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findPendingSlotSuggestedActionContext(decision = null) {
|
|
|
|
|
|
const fieldKey = String(decision?.field_key || decision?.fieldKey || '').trim()
|
|
|
|
|
|
const fieldValue = String(decision?.field_value || decision?.fieldValue || '').trim()
|
|
|
|
|
|
for (const message of [...messages.value].reverse()) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
message?.role !== 'assistant' ||
|
|
|
|
|
|
message.suggestedActionsLocked ||
|
|
|
|
|
|
!Array.isArray(message.suggestedActions) ||
|
|
|
|
|
|
!message.suggestedActions.length
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const action = message.suggestedActions.find((item) => {
|
|
|
|
|
|
if (String(item?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = item?.payload && typeof item.payload === 'object' ? item.payload : {}
|
|
|
|
|
|
const payloadField = String(payload.field_key || payload.fieldKey || '').trim()
|
|
|
|
|
|
const payloadValue = String(payload.value || item?.label || '').trim()
|
|
|
|
|
|
return payloadField && (!fieldKey || payloadField === fieldKey) && (!fieldValue || payloadValue === fieldValue)
|
|
|
|
|
|
})
|
|
|
|
|
|
if (action) {
|
|
|
|
|
|
return { message, action }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function findPendingSlotSuggestedActionContextByInput(rawText = '') {
|
|
|
|
|
|
const normalizedInput = normalizeStewardRuntimeInputText(rawText)
|
|
|
|
|
|
if (!normalizedInput) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const transportAlias = resolveStewardRuntimeTransportAlias(normalizedInput)
|
|
|
|
|
|
for (const message of [...messages.value].reverse()) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
message?.role !== 'assistant' ||
|
|
|
|
|
|
message.suggestedActionsLocked ||
|
|
|
|
|
|
!Array.isArray(message.suggestedActions) ||
|
|
|
|
|
|
!message.suggestedActions.length
|
|
|
|
|
|
) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const exactMatches = []
|
|
|
|
|
|
const fuzzyMatches = []
|
|
|
|
|
|
message.suggestedActions.forEach((action) => {
|
|
|
|
|
|
if (String(action?.action_type || '').trim() !== APPLICATION_PREVIEW_FIELD_ACTION_SET) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const fieldKey = String(payload.field_key || payload.fieldKey || '').trim()
|
|
|
|
|
|
const value = String(payload.value || action?.label || '').trim()
|
|
|
|
|
|
const label = String(action?.label || value).trim()
|
|
|
|
|
|
const tokens = [value, label]
|
|
|
|
|
|
.map((item) => normalizeStewardRuntimeInputText(item))
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
if (!fieldKey || !value || !tokens.length) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokens.includes(normalizedInput)) {
|
|
|
|
|
|
exactMatches.push({ message, action })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const actionTransportAlias = resolveStewardRuntimeTransportAlias(`${value}${label}`)
|
|
|
|
|
|
if (
|
|
|
|
|
|
transportAlias &&
|
|
|
|
|
|
(
|
|
|
|
|
|
tokens.includes(normalizeStewardRuntimeInputText(transportAlias)) ||
|
|
|
|
|
|
actionTransportAlias === transportAlias
|
|
|
|
|
|
)
|
|
|
|
|
|
) {
|
|
|
|
|
|
fuzzyMatches.push({ message, action })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (tokens.some((token) => token.length >= 2 && normalizedInput.includes(token))) {
|
|
|
|
|
|
fuzzyMatches.push({ message, action })
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (exactMatches.length === 1) {
|
|
|
|
|
|
return exactMatches[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
if (exactMatches.length > 1) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const uniqueFuzzyMatches = fuzzyMatches.filter((item, index, list) =>
|
|
|
|
|
|
list.findIndex((candidate) => candidate.action === item.action) === index
|
|
|
|
|
|
)
|
|
|
|
|
|
if (uniqueFuzzyMatches.length === 1) {
|
|
|
|
|
|
return uniqueFuzzyMatches[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
if (uniqueFuzzyMatches.length > 1) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardRuntimeState() {
|
|
|
|
|
|
const latestApplicationMessage = findLatestApplicationPreviewMessage()
|
|
|
|
|
|
const applicationPreview = latestApplicationMessage?.applicationPreview
|
|
|
|
|
|
? normalizeApplicationPreview(latestApplicationMessage.applicationPreview)
|
|
|
|
|
|
: null
|
|
|
|
|
|
const applicationContinuation = latestApplicationMessage?.stewardContinuation || null
|
|
|
|
|
|
const pendingSlotContext = findPendingSlotSuggestedActionContext()
|
|
|
|
|
|
const pendingStewardContext = pendingSlotContext ? null : findPendingStewardSuggestedActionContext()
|
|
|
|
|
|
const pendingActionPayload = pendingStewardContext?.action?.payload && typeof pendingStewardContext.action.payload === 'object'
|
|
|
|
|
|
? pendingStewardContext.action.payload
|
|
|
|
|
|
: {}
|
|
|
|
|
|
const pendingSlotPayload = pendingSlotContext?.action?.payload && typeof pendingSlotContext.action.payload === 'object'
|
|
|
|
|
|
? pendingSlotContext.action.payload
|
|
|
|
|
|
: {}
|
|
|
|
|
|
const continuation = applicationContinuation || pendingStewardContext?.message?.stewardContinuation || null
|
|
|
|
|
|
const remainingTasks = Array.isArray(continuation?.remainingTasks)
|
|
|
|
|
|
? continuation.remainingTasks
|
|
|
|
|
|
: []
|
|
|
|
|
|
const pendingApplication = latestApplicationMessage && applicationPreview
|
|
|
|
|
|
? {
|
|
|
|
|
|
message_id: String(latestApplicationMessage.id || '').trim(),
|
|
|
|
|
|
task_id: String(
|
|
|
|
|
|
applicationContinuation?.currentTaskId ||
|
|
|
|
|
|
applicationContinuation?.current_task_id ||
|
|
|
|
|
|
applicationContinuation?.currentTask?.task_id ||
|
|
|
|
|
|
applicationContinuation?.currentTask?.taskId ||
|
|
|
|
|
|
''
|
|
|
|
|
|
).trim(),
|
|
|
|
|
|
ready_to_submit: Boolean(applicationPreview.readyToSubmit),
|
|
|
|
|
|
missing_fields: Array.isArray(applicationPreview.missingFields) ? applicationPreview.missingFields : [],
|
|
|
|
|
|
fields: applicationPreview.fields || {}
|
|
|
|
|
|
}
|
|
|
|
|
|
: null
|
|
|
|
|
|
return {
|
|
|
|
|
|
waiting_for: pendingApplication
|
|
|
|
|
|
? (pendingApplication.ready_to_submit ? 'application_submit_confirmation' : 'application_field_completion')
|
|
|
|
|
|
: pendingSlotContext
|
|
|
|
|
|
? 'application_field_completion'
|
|
|
|
|
|
: pendingStewardContext
|
|
|
|
|
|
? 'steward_next_task_confirmation'
|
|
|
|
|
|
: '',
|
|
|
|
|
|
current_task: continuation?.currentTask || continuation?.current_task || null,
|
|
|
|
|
|
remaining_tasks: remainingTasks,
|
|
|
|
|
|
completed_tasks: messages.value
|
|
|
|
|
|
.filter((message) => message?.applicationSubmitConfirmed)
|
|
|
|
|
|
.map((message) => ({
|
|
|
|
|
|
message_id: String(message.id || '').trim(),
|
|
|
|
|
|
task_type: 'expense_application'
|
|
|
|
|
|
})),
|
|
|
|
|
|
pending_application: pendingApplication,
|
|
|
|
|
|
pending_steward_action: pendingStewardContext
|
|
|
|
|
|
? {
|
|
|
|
|
|
message_id: String(pendingStewardContext.message?.id || '').trim(),
|
|
|
|
|
|
action_type: String(pendingStewardContext.action?.action_type || '').trim(),
|
|
|
|
|
|
label: String(pendingStewardContext.action?.label || '').trim(),
|
|
|
|
|
|
target_task_id: String(pendingActionPayload.steward_next_task_id || pendingActionPayload.target_task_id || '').trim(),
|
|
|
|
|
|
payload: pendingActionPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
: null,
|
|
|
|
|
|
pending_slot_action: pendingSlotContext
|
|
|
|
|
|
? {
|
|
|
|
|
|
message_id: String(pendingSlotContext.message?.id || '').trim(),
|
|
|
|
|
|
field_key: String(pendingSlotPayload.field_key || pendingSlotPayload.fieldKey || '').trim(),
|
|
|
|
|
|
label: String(pendingSlotContext.action?.label || '').trim(),
|
|
|
|
|
|
payload: pendingSlotPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
: null,
|
|
|
|
|
|
steward_state: stewardState.value || null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function hasActiveStewardRuntimeDecisionContext(runtimeState = {}) {
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
String(runtimeState?.waiting_for || '').trim() ||
|
|
|
|
|
|
runtimeState?.pending_application ||
|
|
|
|
|
|
runtimeState?.pending_steward_action ||
|
|
|
|
|
|
runtimeState?.pending_slot_action ||
|
|
|
|
|
|
runtimeState?.current_task ||
|
|
|
|
|
|
runtimeState?.steward_state ||
|
|
|
|
|
|
(Array.isArray(runtimeState?.remaining_tasks) && runtimeState.remaining_tasks.length > 0) ||
|
|
|
|
|
|
(Array.isArray(runtimeState?.completed_tasks) && runtimeState.completed_tasks.length > 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function resolveStewardStateFlow(state = {}, flowId = '') {
|
|
|
|
|
|
const flows = state?.flows && typeof state.flows === 'object' ? state.flows : {}
|
|
|
|
|
|
const flow = flows[String(flowId || '').trim()]
|
|
|
|
|
|
return flow && typeof flow === 'object' ? flow : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildSelectedStewardFlowTaskPayload(flowId = '', flow = {}) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
task_id: String(flowId || '').trim(),
|
|
|
|
|
|
task_type: flowId === 'travel_application' ? 'expense_application' : 'reimbursement',
|
|
|
|
|
|
title: flowId === 'travel_application' ? '补办出差申请' : '发起费用报销',
|
|
|
|
|
|
summary: buildSelectedStewardFlowSummary(flow),
|
|
|
|
|
|
assigned_agent: flowId === 'travel_application' ? 'application_assistant' : 'reimbursement_assistant',
|
|
|
|
|
|
ontology_fields: flow?.fields || {},
|
|
|
|
|
|
missing_fields: Array.isArray(flow?.missing_fields || flow?.missingFields)
|
|
|
|
|
|
? flow.missing_fields || flow.missingFields
|
|
|
|
|
|
: []
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildSelectedStewardFlowCarryText(flowId = '', flow = {}, state = {}) {
|
|
|
|
|
|
const label = flowId === 'travel_application' ? '补办出差申请' : '发起费用报销'
|
|
|
|
|
|
const fields = flow?.fields && typeof flow.fields === 'object' ? flow.fields : {}
|
|
|
|
|
|
const fieldLines = [
|
|
|
|
|
|
fields.time_range ? `时间:${fields.time_range}` : '',
|
|
|
|
|
|
fields.location ? `地点:${fields.location}` : '',
|
|
|
|
|
|
fields.expense_type ? `费用类型:${fields.expense_type}` : '',
|
|
|
|
|
|
fields.reason ? `事由:${fields.reason}` : ''
|
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
|
const missingFields = Array.isArray(flow?.missing_fields || flow?.missingFields)
|
|
|
|
|
|
? flow.missing_fields || flow.missingFields
|
|
|
|
|
|
: []
|
|
|
|
|
|
const sourceMessage = String(state?.pending_flow_confirmation?.source_message || '').trim()
|
|
|
|
|
|
return [
|
|
|
|
|
|
`小财管家已确认本次按“${label}”继续处理。`,
|
|
|
|
|
|
sourceMessage ? `用户原始描述:${sourceMessage}` : '',
|
|
|
|
|
|
fieldLines.length ? `已识别信息:${fieldLines.join(';')}` : '',
|
|
|
|
|
|
missingFields.length ? `还需要补充:${missingFields.join('、')}` : '',
|
|
|
|
|
|
flowId === 'travel_application'
|
|
|
|
|
|
? '请进入申请流程继续核对出差申请材料;如果仍缺关键字段,请先追问用户。'
|
|
|
|
|
|
: '请进入报销流程继续核对报销材料、票据和金额;创建草稿或提交前仍需用户确认。'
|
|
|
|
|
|
].filter(Boolean).join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildSelectedStewardFlowSummary(flow = {}) {
|
|
|
|
|
|
const fields = flow?.fields && typeof flow.fields === 'object' ? flow.fields : {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
fields.time_range ? `时间:${fields.time_range}` : '',
|
|
|
|
|
|
fields.location ? `地点:${fields.location}` : '',
|
|
|
|
|
|
fields.reason ? `事由:${fields.reason}` : ''
|
|
|
|
|
|
].filter(Boolean).join(';')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pushStewardRuntimeUserMessage(userText = '') {
|
|
|
|
|
|
const normalizedText = String(userText || '').trim()
|
|
|
|
|
|
if (!normalizedText) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
messages.value.push(createMessage('user', normalizedText))
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pushStewardRuntimeResponse(userText = '', decision = null, options = {}) {
|
|
|
|
|
|
if (userText && !options.userMessageAlreadyAdded) {
|
|
|
|
|
|
messages.value.push(createMessage('user', userText))
|
|
|
|
|
|
}
|
|
|
|
|
|
const text = String(decision?.question || decision?.response_text || decision?.responseText || decision?.rationale || '').trim()
|
|
|
|
|
|
if (text) {
|
|
|
|
|
|
messages.value.push(createMessage('assistant', text, [], {
|
|
|
|
|
|
assistantName: STEWARD_ASSISTANT_NAME,
|
|
|
|
|
|
meta: [STEWARD_ASSISTANT_NAME]
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardRuntimeFastPathDecision(rawText = '', runtimeState = {}) {
|
|
|
|
|
|
const normalizedText = String(rawText || '').trim()
|
|
|
|
|
|
if (!normalizedText) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (shouldPlanNewStewardTasksLocally(normalizedText, runtimeState)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'plan_new_tasks'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isStewardRuntimeCancelText(normalizedText)) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'cancel_current_action',
|
|
|
|
|
|
response_text: '已暂停当前等待动作。我不会继续提交或进入下一步;如果你要重新规划,请直接告诉我新的财务事项。'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const slotContext = findPendingSlotSuggestedActionContextByInput(normalizedText)
|
|
|
|
|
|
const payload = slotContext?.action?.payload && typeof slotContext.action.payload === 'object'
|
|
|
|
|
|
? slotContext.action.payload
|
|
|
|
|
|
: {}
|
|
|
|
|
|
if (slotContext) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'fill_current_slot',
|
|
|
|
|
|
target_message_id: String(slotContext.message?.id || '').trim(),
|
|
|
|
|
|
field_key: String(payload.field_key || payload.fieldKey || '').trim(),
|
|
|
|
|
|
field_value: String(payload.value || slotContext.action?.label || normalizedText).trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) {
|
|
|
|
|
|
if (runtimeState?.pending_application?.ready_to_submit) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'submit_current_application',
|
|
|
|
|
|
target_message_id: runtimeState.pending_application.message_id || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (runtimeState?.pending_steward_action) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'continue_next_task',
|
|
|
|
|
|
target_message_id: runtimeState.pending_steward_action.message_id || '',
|
|
|
|
|
|
target_task_id: runtimeState.pending_steward_action.target_task_id || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (String(runtimeState?.waiting_for || '').trim() === 'application_field_completion') {
|
|
|
|
|
|
if (isApplicationSubmitConfirmationText(normalizedText) || isStewardRuntimeContinueText(normalizedText)) {
|
|
|
|
|
|
const missingFields = Array.isArray(runtimeState?.pending_application?.missing_fields)
|
|
|
|
|
|
? runtimeState.pending_application.missing_fields
|
|
|
|
|
|
: []
|
|
|
|
|
|
return {
|
|
|
|
|
|
next_action: 'ask_user',
|
|
|
|
|
|
response_text: missingFields.length
|
|
|
|
|
|
? `当前申请还不能继续提交,请先补充:${missingFields.join('、')}。你可以直接回复对应选项或填写具体内容。`
|
|
|
|
|
|
: '当前申请还有信息需要先补充。请先回复系统刚刚追问的内容,我再继续生成核对结果。'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-18 22:12:24 +08:00
|
|
|
|
const fieldCompletionDecision = resolveStewardRuntimeFieldCompletion(normalizedText, runtimeState)
|
|
|
|
|
|
if (fieldCompletionDecision) {
|
|
|
|
|
|
return fieldCompletionDecision
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shouldUseStewardRuntimeLlmDecision(rawText = '', runtimeState = {}) {
|
|
|
|
|
|
if (shouldPlanNewStewardTasksLocally(rawText, runtimeState)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalizedText = normalizeStewardRuntimeInputText(rawText)
|
|
|
|
|
|
if (!normalizedText) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
isApplicationSubmitConfirmationText(normalizedText) ||
|
|
|
|
|
|
isStewardRuntimeContinueText(normalizedText) ||
|
|
|
|
|
|
isStewardRuntimeCancelText(normalizedText)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
findPendingSlotSuggestedActionContextByInput(normalizedText)
|
|
|
|
|
|
) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function executeStewardRuntimeDecision(decision = null, rawText = '', options = {}) {
|
|
|
|
|
|
const nextAction = String(decision?.next_action || decision?.nextAction || '').trim()
|
|
|
|
|
|
const userMessageAlreadyAdded = Boolean(options.userMessageAlreadyAdded)
|
|
|
|
|
|
if (nextAction === 'submit_current_application') {
|
|
|
|
|
|
const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim()
|
|
|
|
|
|
const targetMessage = targetMessageId
|
|
|
|
|
|
? messages.value.find((message) => String(message.id || '') === targetMessageId)
|
|
|
|
|
|
: findPendingApplicationSubmitMessage()
|
|
|
|
|
|
if (!targetMessage?.applicationPreview) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const normalizedPreview = normalizeApplicationPreview(targetMessage.applicationPreview)
|
|
|
|
|
|
if (!normalizedPreview.readyToSubmit) {
|
|
|
|
|
|
pushApplicationSubmitBlockedMessage(rawText, targetMessage, { userMessageAlreadyAdded })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
targetMessage.applicationPreview = normalizedPreview
|
|
|
|
|
|
applicationSubmitConfirmDialog.value = { open: true, message: targetMessage }
|
|
|
|
|
|
await confirmApplicationSubmit({ userText: rawText, skipUserMessage: userMessageAlreadyAdded })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextAction === 'continue_next_task') {
|
|
|
|
|
|
const context = findPendingStewardSuggestedActionContext(decision)
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
if (rawText && !userMessageAlreadyAdded) {
|
|
|
|
|
|
messages.value.push(createMessage('user', rawText))
|
|
|
|
|
|
}
|
|
|
|
|
|
context.action.confirmedByText = true
|
|
|
|
|
|
composerDraft.value = ''
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
|
|
|
|
|
await handleSuggestedAction(context.message, context.action)
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextAction === 'continue_selected_flow') {
|
|
|
|
|
|
const selectedState = decision?.steward_state || decision?.stewardState || stewardState.value || {}
|
|
|
|
|
|
const flowId = String(
|
|
|
|
|
|
decision?.target_task_id ||
|
|
|
|
|
|
decision?.targetTaskId ||
|
|
|
|
|
|
selectedState?.active_flow ||
|
|
|
|
|
|
selectedState?.activeFlow ||
|
|
|
|
|
|
''
|
|
|
|
|
|
).trim()
|
|
|
|
|
|
const flow = resolveStewardStateFlow(selectedState, flowId)
|
|
|
|
|
|
if (!flowId || !flow) {
|
|
|
|
|
|
pushStewardRuntimeResponse(rawText, decision, { userMessageAlreadyAdded })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
const targetSessionType = flowId === 'travel_application'
|
|
|
|
|
|
? SESSION_TYPE_APPLICATION
|
|
|
|
|
|
: SESSION_TYPE_EXPENSE
|
|
|
|
|
|
await submitComposerInternal({
|
|
|
|
|
|
rawText: buildSelectedStewardFlowCarryText(flowId, flow, selectedState),
|
|
|
|
|
|
userText: rawText || (flowId === 'travel_application' ? '补办出差申请' : '发起费用报销'),
|
|
|
|
|
|
pendingText: targetSessionType === SESSION_TYPE_APPLICATION
|
|
|
|
|
|
? '小财管家正在按申请流程整理出差材料...'
|
|
|
|
|
|
: '小财管家正在按报销流程整理票据和费用材料...',
|
|
|
|
|
|
files: [],
|
|
|
|
|
|
skipScopeGuard: true,
|
|
|
|
|
|
skipApplicationModelReview: targetSessionType === SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
skipStewardSlotDecision: targetSessionType === SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
skipStewardPlan: true,
|
|
|
|
|
|
skipUserMessage: userMessageAlreadyAdded,
|
|
|
|
|
|
sessionTypeOverride: targetSessionType,
|
|
|
|
|
|
stewardContinuation: {
|
|
|
|
|
|
planId: 'steward_selected_flow',
|
|
|
|
|
|
currentTaskId: flowId,
|
|
|
|
|
|
currentTask: buildSelectedStewardFlowTaskPayload(flowId, flow),
|
|
|
|
|
|
remainingTasks: []
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (nextAction === 'fill_current_slot') {
|
|
|
|
|
|
const context = findPendingSlotSuggestedActionContext(decision)
|
|
|
|
|
|
if (!context) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
await handleSuggestedAction(context.message, {
|
|
|
|
|
|
...context.action,
|
|
|
|
|
|
label: String(decision?.field_value || decision?.fieldValue || context.action.label || '').trim(),
|
|
|
|
|
|
suppressUserEcho: userMessageAlreadyAdded
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-06-18 22:12:24 +08:00
|
|
|
|
if (nextAction === 'fill_current_application_field') {
|
|
|
|
|
|
const targetMessageId = String(decision?.target_message_id || decision?.targetMessageId || '').trim()
|
|
|
|
|
|
const targetMessage = targetMessageId
|
|
|
|
|
|
? messages.value.find((message) => String(message.id || '') === targetMessageId)
|
|
|
|
|
|
: findLatestApplicationPreviewMessage()
|
|
|
|
|
|
if (!targetMessage?.applicationPreview) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const fieldKey = String(decision?.field_key || decision?.fieldKey || '').trim()
|
|
|
|
|
|
const fieldLabel = String(decision?.field_label || decision?.fieldLabel || '').trim()
|
|
|
|
|
|
const fieldValue = String(decision?.field_value || decision?.fieldValue || rawText).trim()
|
|
|
|
|
|
if (!fieldKey || !fieldValue) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
await continueStewardApplicationFieldCompletion({
|
|
|
|
|
|
targetMessage,
|
|
|
|
|
|
action: {
|
|
|
|
|
|
label: fieldValue,
|
|
|
|
|
|
suppressUserEcho: userMessageAlreadyAdded,
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
steward_delegated_field_completion: true,
|
|
|
|
|
|
field_key: fieldKey,
|
|
|
|
|
|
field_label: fieldLabel,
|
|
|
|
|
|
value: fieldValue
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
sourcePreview: targetMessage.applicationPreview,
|
|
|
|
|
|
fieldKey,
|
|
|
|
|
|
fieldLabel,
|
|
|
|
|
|
value: fieldValue
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
if (nextAction === 'ask_user' || nextAction === 'cancel_current_action' || nextAction === 'no_op') {
|
|
|
|
|
|
pushStewardRuntimeResponse(rawText, decision, { userMessageAlreadyAdded })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleStewardRuntimeDecision(options = {}) {
|
|
|
|
|
|
if (!isStewardSession.value || options.skipStewardPlan) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const rawText = String(options.rawText ?? composerDraft.value ?? '').trim()
|
|
|
|
|
|
const files = Array.from(options.files ?? attachedFiles.value ?? [])
|
|
|
|
|
|
if (!rawText || files.length) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const runtimeState = buildStewardRuntimeState()
|
|
|
|
|
|
if (!hasActiveStewardRuntimeDecisionContext(runtimeState)) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const userMessageAlreadyAdded = options.skipUserMessage
|
|
|
|
|
|
? false
|
|
|
|
|
|
: pushStewardRuntimeUserMessage(rawText)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const fastDecision = buildStewardRuntimeFastPathDecision(rawText, runtimeState)
|
|
|
|
|
|
if (fastDecision) {
|
|
|
|
|
|
if (String(fastDecision.next_action || fastDecision.nextAction || '').trim() === 'plan_new_tasks') {
|
|
|
|
|
|
await submitStewardPlan({
|
|
|
|
|
|
...options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
userText: rawText,
|
|
|
|
|
|
skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
const fastExecuted = await executeStewardRuntimeDecision(fastDecision, rawText, { userMessageAlreadyAdded })
|
|
|
|
|
|
if (fastExecuted) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!shouldUseStewardRuntimeLlmDecision(rawText, runtimeState)) {
|
|
|
|
|
|
if (userMessageAlreadyAdded) {
|
|
|
|
|
|
pushStewardRuntimeResponse('', {
|
|
|
|
|
|
response_text: '我还需要先确认当前等待项。请回复系统刚刚追问的选项或具体补充内容。'
|
|
|
|
|
|
}, { userMessageAlreadyAdded: true })
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
const decision = await fetchStewardRuntimeDecision({
|
|
|
|
|
|
user_message: rawText,
|
|
|
|
|
|
session_type: SESSION_TYPE_STEWARD,
|
|
|
|
|
|
runtime_state: runtimeState,
|
|
|
|
|
|
context_json: {
|
|
|
|
|
|
entry_source: props.entrySource,
|
|
|
|
|
|
user_id: resolveCurrentUserId(),
|
|
|
|
|
|
conversation_id: conversationId.value || '',
|
|
|
|
|
|
steward_state: stewardState.value || null
|
|
|
|
|
|
}
|
|
|
|
|
|
}, {
|
|
|
|
|
|
timeoutMs: 45000,
|
|
|
|
|
|
timeoutMessage: '小财管家运行时决策超时,已回到当前上下文兜底处理。'
|
|
|
|
|
|
})
|
|
|
|
|
|
const nextStewardState = decision?.steward_state || decision?.stewardState || null
|
|
|
|
|
|
if (nextStewardState && typeof nextStewardState === 'object') {
|
|
|
|
|
|
stewardState.value = nextStewardState
|
|
|
|
|
|
}
|
|
|
|
|
|
if (String(decision?.next_action || decision?.nextAction || '').trim() === 'plan_new_tasks') {
|
|
|
|
|
|
await submitStewardPlan({
|
|
|
|
|
|
...options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
userText: rawText,
|
|
|
|
|
|
skipUserMessage: userMessageAlreadyAdded || options.skipUserMessage
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
const executed = await executeStewardRuntimeDecision(decision, rawText, { userMessageAlreadyAdded })
|
|
|
|
|
|
if (executed) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (userMessageAlreadyAdded) {
|
|
|
|
|
|
await submitStewardPlan({
|
|
|
|
|
|
...options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
userText: rawText,
|
|
|
|
|
|
skipUserMessage: true
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Steward runtime decision failed:', error)
|
|
|
|
|
|
if (userMessageAlreadyAdded) {
|
|
|
|
|
|
await submitStewardPlan({
|
|
|
|
|
|
...options,
|
|
|
|
|
|
rawText,
|
|
|
|
|
|
userText: rawText,
|
|
|
|
|
|
skipUserMessage: true
|
|
|
|
|
|
})
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openApplicationSubmitConfirm(message) {
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message.applicationPreview) {
|
|
|
|
|
|
const normalizedPreview = normalizeApplicationPreview(message.applicationPreview)
|
|
|
|
|
|
message.applicationPreview = normalizedPreview
|
|
|
|
|
|
message.text = buildLocalApplicationPreviewMessage(normalizedPreview)
|
|
|
|
|
|
if (!normalizedPreview.readyToSubmit) {
|
|
|
|
|
|
toast(`请先补充:${normalizedPreview.missingFields.join('、')}。`)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
applicationSubmitConfirmDialog.value = {
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
message
|
|
|
|
|
|
}
|
2026-05-25 13:35:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function closeApplicationSubmitConfirm() {
|
|
|
|
|
|
if (reviewActionBusy.value) {
|
2026-05-25 13:35:39 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
applicationSubmitConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null
|
|
|
|
|
|
}
|
2026-06-04 14:25:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function resolveApplicationEditClaimId() {
|
|
|
|
|
|
if (activeSessionType.value !== SESSION_TYPE_APPLICATION) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const request = linkedRequest.value || {}
|
|
|
|
|
|
if (!request.applicationEditMode) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(request.claimId || request.claim_id || '').trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardContinuationAfterAction(message, completedLabel = '当前动作已完成') {
|
|
|
|
|
|
const continuation = message?.stewardContinuation || null
|
|
|
|
|
|
const remainingTasks = Array.isArray(continuation?.remainingTasks)
|
|
|
|
|
|
? continuation.remainingTasks
|
|
|
|
|
|
: []
|
|
|
|
|
|
if (!remainingTasks.length) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextTask = remainingTasks[0]
|
|
|
|
|
|
const nextTaskType = String(nextTask.task_type || nextTask.taskType || '').trim()
|
|
|
|
|
|
const targetSessionType = nextTaskType === 'expense_application'
|
|
|
|
|
|
? SESSION_TYPE_APPLICATION
|
|
|
|
|
|
: SESSION_TYPE_EXPENSE
|
|
|
|
|
|
const nextLabel = targetSessionType === SESSION_TYPE_APPLICATION
|
|
|
|
|
|
? '继续创建申请单'
|
|
|
|
|
|
: '继续填写报销单'
|
|
|
|
|
|
const restTasks = remainingTasks.slice(1)
|
|
|
|
|
|
return createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
[
|
|
|
|
|
|
`**${completedLabel}。**`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'我会重新检查剩余任务队列。',
|
|
|
|
|
|
`下一步:${nextTask.title || (targetSessionType === SESSION_TYPE_APPLICATION ? '费用申请' : '费用报销')}。`,
|
|
|
|
|
|
'请回复“确定”,我再继续执行。'
|
|
|
|
|
|
].join('\n'),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
assistantName: '小财管家',
|
|
|
|
|
|
meta: ['小财管家', '等待用户确认'],
|
|
|
|
|
|
suggestedActions: [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: nextLabel,
|
|
|
|
|
|
description: '确认后小财管家继续调用对应助手完成下一步。',
|
|
|
|
|
|
icon: targetSessionType === SESSION_TYPE_APPLICATION
|
|
|
|
|
|
? 'mdi mdi-file-plus-outline'
|
|
|
|
|
|
: 'mdi mdi-receipt-text-plus-outline',
|
|
|
|
|
|
action_type: ASSISTANT_SCOPE_ACTION_SWITCH,
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
session_type: targetSessionType,
|
|
|
|
|
|
carry_text: buildStewardContinuationCarryText(nextTask, restTasks),
|
|
|
|
|
|
carry_files: targetSessionType !== SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
auto_submit: true,
|
|
|
|
|
|
steward_plan_id: String(continuation.planId || '').trim() || 'steward_continuation',
|
|
|
|
|
|
steward_next_task_id: String(nextTask.task_id || nextTask.taskId || '').trim(),
|
|
|
|
|
|
steward_current_task: nextTask,
|
|
|
|
|
|
steward_remaining_tasks: restTasks
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardFollowupPlan(thinkingEvents = [], streamStatus = 'streaming', planId = '') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
planId: planId || `steward-followup-${Date.now()}`,
|
|
|
|
|
|
planStatus: 'delegating',
|
|
|
|
|
|
summary: '',
|
|
|
|
|
|
visibleThinkingEventCount: Number.MAX_SAFE_INTEGER,
|
|
|
|
|
|
initialSummaryOnly: true,
|
|
|
|
|
|
thinkingEvents,
|
|
|
|
|
|
tasks: [],
|
|
|
|
|
|
attachmentGroups: [],
|
|
|
|
|
|
confirmationGroups: [],
|
|
|
|
|
|
streamStatus
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractStewardCarryLine(text = '', label = '') {
|
|
|
|
|
|
const escapedLabel = String(label || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
|
const match = String(text || '').match(new RegExp(`(?:^|\\n)${escapedLabel}[::]([^\\n]+)`, 'u'))
|
|
|
|
|
|
return match ? match[1].trim() : ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractStewardFollowupNextTitle(text = '') {
|
|
|
|
|
|
const taskMatch = String(text || '').match(/请(?:先)?(?:创建申请单|填写报销单|继续填写报销单)[::]([^。\n]+)/u)
|
|
|
|
|
|
if (taskMatch?.[1]) {
|
|
|
|
|
|
return taskMatch[1].trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
const nextMatch = String(text || '').match(/下一步[::]([^。\n]+)/u)
|
|
|
|
|
|
return nextMatch?.[1]?.trim() || '下一项财务任务'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildStewardFollowupThinkingEvents(finalMessage = null, actions = []) {
|
|
|
|
|
|
const eventPrefix = `steward-followup-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
|
|
|
|
const firstAction = Array.isArray(actions) ? actions[0] : null
|
|
|
|
|
|
const actionPayload = firstAction?.payload && typeof firstAction.payload === 'object' ? firstAction.payload : {}
|
|
|
|
|
|
const carryText = String(actionPayload.carry_text || '').trim()
|
|
|
|
|
|
const finalText = String(finalMessage?.text || '').trim()
|
|
|
|
|
|
const nextTitle = extractStewardFollowupNextTitle(carryText || finalText)
|
|
|
|
|
|
const nextSummary = extractStewardCarryLine(carryText, '任务摘要')
|
|
|
|
|
|
const nextMissing = extractStewardCarryLine(carryText, '还需要补充')
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-review`,
|
|
|
|
|
|
title: '复盘结果',
|
|
|
|
|
|
content: finalText.includes('申请单已完成')
|
|
|
|
|
|
? '申请单已经完成,我把当前出差申请标记为已处理,不会重复创建。'
|
|
|
|
|
|
: '当前动作已经完成,我会把已完成事项从任务队列中移除。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-next`,
|
|
|
|
|
|
title: '读取剩余任务',
|
|
|
|
|
|
content: nextSummary
|
|
|
|
|
|
? `剩余队列里的下一项是“${nextTitle}”:${nextSummary}。`
|
|
|
|
|
|
: `剩余队列里的下一项是“${nextTitle}”。`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
eventId: `${eventPrefix}-gate`,
|
|
|
|
|
|
title: '判断下一步条件',
|
|
|
|
|
|
content: nextMissing
|
|
|
|
|
|
? `这一步还需要补充${nextMissing},进入对应核对环节后我会继续追问,不会直接提交。`
|
|
|
|
|
|
: '我会先等你确认,再进入下一项核对;创建草稿、绑定附件或提交前仍会再次确认。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function waitStewardFollowupTick(intervalMs) {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
window.setTimeout(resolve, intervalMs)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pushStewardContinuationMessage(finalMessage) {
|
|
|
|
|
|
if (!finalMessage) {
|
2026-06-04 14:25:14 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const finalText = String(finalMessage.text || '')
|
|
|
|
|
|
const followupPlanId = `steward-followup-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
|
|
|
|
const finalActions = Array.isArray(finalMessage.suggestedActions)
|
|
|
|
|
|
? finalMessage.suggestedActions
|
|
|
|
|
|
: []
|
|
|
|
|
|
finalMessage.text = ''
|
|
|
|
|
|
finalMessage.assistantName = STEWARD_ASSISTANT_NAME
|
|
|
|
|
|
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '思考中']
|
|
|
|
|
|
finalMessage.suggestedActions = []
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([], 'streaming', followupPlanId)
|
|
|
|
|
|
messages.value.push(finalMessage)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
const typedEvents = []
|
|
|
|
|
|
for (const eventData of buildStewardFollowupThinkingEvents(finalMessage, finalActions)) {
|
|
|
|
|
|
const event = {
|
|
|
|
|
|
eventId: eventData.eventId,
|
|
|
|
|
|
stage: 'steward_followup',
|
|
|
|
|
|
title: eventData.title,
|
|
|
|
|
|
content: '',
|
|
|
|
|
|
status: 'running'
|
|
|
|
|
|
}
|
|
|
|
|
|
typedEvents.push(event)
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
|
|
|
|
|
|
const chars = Array.from(eventData.content)
|
|
|
|
|
|
for (let index = 0; index < chars.length;) {
|
|
|
|
|
|
await waitStewardFollowupTick(STEWARD_FOLLOWUP_THINKING_INTERVAL_MS)
|
|
|
|
|
|
index = Math.min(chars.length, index + STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE)
|
|
|
|
|
|
event.content = chars.slice(0, index).join('')
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
|
|
|
|
|
|
if (index % STEWARD_FOLLOWUP_THINKING_CHUNK_SIZE === 0 || index === chars.length) {
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
event.content = eventData.content
|
|
|
|
|
|
event.status = 'completed'
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'streaming', followupPlanId)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'typing', followupPlanId)
|
|
|
|
|
|
const chars = Array.from(finalText)
|
|
|
|
|
|
for (let index = 0; index < chars.length;) {
|
|
|
|
|
|
await waitStewardFollowupTick(STEWARD_FOLLOWUP_TYPEWRITER_INTERVAL_MS)
|
|
|
|
|
|
index = Math.min(chars.length, index + STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE)
|
|
|
|
|
|
finalMessage.text = chars.slice(0, index).join('')
|
|
|
|
|
|
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '输出中']
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'typing', followupPlanId)
|
|
|
|
|
|
if (index % STEWARD_FOLLOWUP_TYPEWRITER_CHUNK_SIZE === 0 || index === chars.length) {
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
finalMessage.text = finalText
|
|
|
|
|
|
finalMessage.meta = [STEWARD_ASSISTANT_NAME, '等待用户确认']
|
|
|
|
|
|
finalMessage.suggestedActions = finalActions
|
|
|
|
|
|
finalMessage.stewardPlan = buildStewardFollowupPlan([...typedEvents], 'completed', followupPlanId)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function buildStewardContinuationCarryText(task, restTasks = []) {
|
|
|
|
|
|
const taskType = String(task?.task_type || task?.taskType || '').trim()
|
|
|
|
|
|
const fields = formatStewardOntologyFields(task?.ontology_fields || task?.ontologyFields || {}, taskType)
|
|
|
|
|
|
const missingFields = formatStewardMissingFieldList(
|
|
|
|
|
|
task?.missing_fields || task?.missingFields || [],
|
|
|
|
|
|
taskType,
|
|
|
|
|
|
{ includeHints: false }
|
|
|
|
|
|
)
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
taskType === 'expense_application'
|
|
|
|
|
|
? `小财管家继续执行剩余任务,请创建申请单:${task.title || '费用申请'}。`
|
|
|
|
|
|
: `小财管家继续执行剩余任务,请填写报销单:${task.title || '费用报销'}。`,
|
|
|
|
|
|
task.summary ? `任务摘要:${task.summary}` : '',
|
|
|
|
|
|
fields ? `已识别信息:${fields}` : '',
|
|
|
|
|
|
missingFields ? `还需要补充:${missingFields}` : '',
|
|
|
|
|
|
missingFields
|
|
|
|
|
|
? '请先追问上述缺失信息,不要直接生成核对结果,也不要替用户默认填写。'
|
|
|
|
|
|
: '请生成核对结果;创建草稿、绑定附件或提交审批前仍需让我确认。'
|
|
|
|
|
|
]
|
|
|
|
|
|
if (restTasks.length) {
|
|
|
|
|
|
lines.push('当前步骤完成后,请继续引导我处理剩余任务:')
|
|
|
|
|
|
restTasks.forEach((item, index) => {
|
|
|
|
|
|
lines.push(`${index + 1}. ${item.title || item.task_type || item.taskType}`)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.filter(Boolean).join('\n')
|
|
|
|
|
|
}
|
2026-06-04 14:25:14 +08:00
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
function resolveStewardMissingFieldItems(task) {
|
|
|
|
|
|
if (Array.isArray(task?.missingFieldItems) && task.missingFieldItems.length) {
|
|
|
|
|
|
return task.missingFieldItems
|
|
|
|
|
|
}
|
|
|
|
|
|
const fields = task?.missingFields || task?.missing_fields || []
|
|
|
|
|
|
const taskType = String(task?.taskType || task?.task_type || '').trim()
|
|
|
|
|
|
return buildStewardFieldItems(fields, taskType)
|
|
|
|
|
|
}
|
2026-06-04 14:25:14 +08:00
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
async function confirmApplicationSubmit(options = {}) {
|
|
|
|
|
|
const message = applicationSubmitConfirmDialog.value.message
|
|
|
|
|
|
if (!message || submitting.value || reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const applicationPreview = message?.applicationPreview && typeof message.applicationPreview === 'object'
|
|
|
|
|
|
? normalizeApplicationPreview(message.applicationPreview)
|
|
|
|
|
|
: null
|
|
|
|
|
|
const applicationSubmitText = applicationPreview
|
|
|
|
|
|
? buildApplicationPreviewSubmitText(applicationPreview)
|
|
|
|
|
|
: '确认提交'
|
|
|
|
|
|
const applicationEditClaimId = resolveApplicationEditClaimId()
|
|
|
|
|
|
applicationSubmitConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null
|
|
|
|
|
|
}
|
|
|
|
|
|
const stewardSubmitContinuation = message?.stewardContinuation || null
|
|
|
|
|
|
reviewActionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const payload = await submitComposer({
|
|
|
|
|
|
rawText: applicationSubmitText,
|
|
|
|
|
|
userText: String(options.userText || '').trim() || '确认提交',
|
|
|
|
|
|
skipUserMessage: Boolean(options.skipUserMessage),
|
|
|
|
|
|
pendingText: '正在提交费用申请...',
|
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
|
skipScopeGuard: true,
|
|
|
|
|
|
skipStewardPlan: true,
|
|
|
|
|
|
stewardContinuation: stewardSubmitContinuation,
|
|
|
|
|
|
sessionTypeOverride: SESSION_TYPE_APPLICATION,
|
|
|
|
|
|
feedbackOperationType: 'submit_application',
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
application_preview: applicationPreview,
|
|
|
|
|
|
user_input_text: applicationSubmitText,
|
|
|
|
|
|
...(applicationEditClaimId
|
|
|
|
|
|
? {
|
|
|
|
|
|
application_edit_claim_id: applicationEditClaimId,
|
|
|
|
|
|
application_edit_claim_no: String(linkedRequest.value?.claimNo || linkedRequest.value?.id || '').trim(),
|
|
|
|
|
|
application_edit_mode: true,
|
|
|
|
|
|
draft_claim_id: applicationEditClaimId,
|
|
|
|
|
|
selected_claim_id: applicationEditClaimId
|
|
|
|
|
|
}
|
|
|
|
|
|
: {})
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
const draftPayload = payload?.result?.draft_payload || {}
|
|
|
|
|
|
const claimNo = String(draftPayload.claim_no || '').trim()
|
|
|
|
|
|
const claimId = String(draftPayload.claim_id || '').trim()
|
|
|
|
|
|
if (String(payload?.status || '').trim() === 'succeeded' && (claimNo || claimId)) {
|
|
|
|
|
|
message.applicationSubmitConfirmed = true
|
|
|
|
|
|
emit('draft-saved', {
|
|
|
|
|
|
claimId,
|
|
|
|
|
|
claimNo,
|
|
|
|
|
|
status: 'submitted',
|
|
|
|
|
|
approvalStage: String(draftPayload.approval_stage || '直属领导审批').trim(),
|
|
|
|
|
|
documentType: 'application'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
const planningText = buildTravelPlanningNudgeMessage(applicationPreview, draftPayload)
|
|
|
|
|
|
const planningActions = buildTravelPlanningSuggestedActions(applicationPreview, draftPayload).map((action) => ({
|
|
|
|
|
|
...action,
|
|
|
|
|
|
payload: {
|
|
|
|
|
|
...(action.payload || {}),
|
|
|
|
|
|
applicationPreview,
|
|
|
|
|
|
draftPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
if (planningText && planningActions.length) {
|
|
|
|
|
|
messages.value.push(createMessage('assistant', planningText, [], {
|
|
|
|
|
|
meta: ['行程规划推荐'],
|
|
|
|
|
|
suggestedActions: planningActions
|
|
|
|
|
|
}))
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
const stewardFollowup = buildStewardContinuationAfterAction(message, '申请单已完成')
|
|
|
|
|
|
if (stewardFollowup) {
|
|
|
|
|
|
await pushStewardContinuationMessage(stewardFollowup)
|
|
|
|
|
|
}
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewActionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-25 13:35:39 +08:00
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function isWorkbenchBusy() {
|
|
|
|
|
|
return submitting.value || reviewActionBusy.value || sessionSwitchBusy.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function maybeFinalizeDeferredClose() {
|
|
|
|
|
|
if (!closeAfterBusy.value || workbenchVisible.value || isWorkbenchBusy()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
closeAfterBusy.value = false
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
function requestCloseWorkbench() {
|
2026-05-21 16:09:47 +08:00
|
|
|
|
persistSessionState()
|
2026-05-22 16:00:19 +08:00
|
|
|
|
closeAfterBusy.value = isWorkbenchBusy()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
workbenchVisible.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function emitCloseAfterLeave() {
|
2026-05-26 09:15:14 +08:00
|
|
|
|
if (workbenchVisible.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (closeAfterBusy.value && isWorkbenchBusy()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
closeAfterBusy.value = false
|
2026-05-19 17:24:13 +00:00
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openExpenseQueryRecord(record) {
|
|
|
|
|
|
const claimId = String(record?.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.push({
|
2026-05-26 09:15:14 +08:00
|
|
|
|
name: 'app-document-detail',
|
2026-05-19 17:24:13 +00:00
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
async function handleExpenseQueryRecordClick(message, record) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (message?.queryPayload?.selectionMode !== 'draft_association') {
|
|
|
|
|
|
openExpenseQueryRecord(record)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message.querySelectionLocked || message.queryPayload.selectionLocked || submitting.value || reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const claimId = String(record?.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const files = Array.from(attachedFiles.value || [])
|
|
|
|
|
|
if (!files.length) {
|
|
|
|
|
|
toast('本次上传的附件已不在当前会话中,请重新选择附件后再关联草稿。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
message.querySelectionLocked = true
|
|
|
|
|
|
message.selectedQueryRecordId = claimId
|
|
|
|
|
|
message.queryPayload.selectionLocked = true
|
|
|
|
|
|
message.queryPayload.selectedClaimId = claimId
|
|
|
|
|
|
draftClaimId.value = claimId
|
|
|
|
|
|
persistSessionState()
|
2026-05-20 09:36:01 +08:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: `将本次上传的 ${files.length} 份票据关联到报销草稿 ${record.claimNo}`,
|
|
|
|
|
|
userText: `关联到草稿 ${record.claimNo}`,
|
|
|
|
|
|
pendingText: `已选择草稿 ${record.claimNo},正在识别并归集附件...`,
|
|
|
|
|
|
files,
|
|
|
|
|
|
uploadDisposition: 'continue_existing',
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
draft_claim_id: claimId,
|
2026-05-22 08:58:59 +08:00
|
|
|
|
selected_claim_id: claimId,
|
|
|
|
|
|
selected_claim_no: String(record?.claimNo || '').trim()
|
2026-05-20 09:36:01 +08:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function setExpenseQueryPage(message, page) {
|
|
|
|
|
|
if (!message?.queryPayload) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const totalPages = getExpenseQueryTotalPages(message.queryPayload)
|
|
|
|
|
|
const nextPage = Math.min(Math.max(1, Number(page || 1)), totalPages)
|
|
|
|
|
|
message.queryPayload.currentPage = nextPage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shiftExpenseQueryPage(message, delta) {
|
|
|
|
|
|
if (!message?.queryPayload) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
setExpenseQueryPage(message, getExpenseQueryActivePage(message.queryPayload) + Number(delta || 0))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openDeleteSessionDialog() {
|
|
|
|
|
|
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || sessionSwitchBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionDialogOpen.value = true
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function closeDeleteSessionDialog() {
|
|
|
|
|
|
if (deleteSessionBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionDialogOpen.value = false
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function confirmDeleteCurrentSession() {
|
|
|
|
|
|
if (deleteSessionBusy.value || sessionSwitchBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionBusy.value = true
|
2026-05-19 17:24:13 +00:00
|
|
|
|
try {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (conversationId.value) {
|
|
|
|
|
|
await deleteConversation(conversationId.value, resolveCurrentUserId())
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
clearAssistantSessionSnapshot(resolveCurrentUserId(), activeSessionType.value)
|
|
|
|
|
|
resetCurrentSessionState()
|
|
|
|
|
|
deleteSessionDialogOpen.value = false
|
|
|
|
|
|
toast('当前会话已删除。')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '删除当前会话失败,请稍后重试。')
|
2026-05-19 17:24:13 +00:00
|
|
|
|
} finally {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionBusy.value = false
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const {
|
|
|
|
|
|
handleReviewActionInternal,
|
|
|
|
|
|
handleSaveDraftDirectlyInternal,
|
|
|
|
|
|
saveInlineReviewChangesInternal
|
|
|
|
|
|
} = useTravelReimbursementReviewActions({
|
2026-05-21 23:53:03 +08:00
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
buildDraftSavedPayload,
|
|
|
|
|
|
buildLocalReviewCompletionMessage,
|
|
|
|
|
|
buildLocalReviewSavedMessage,
|
|
|
|
|
|
buildReviewCorrectionMessage,
|
|
|
|
|
|
buildReviewDocumentCorrectionContext,
|
|
|
|
|
|
buildReviewDocumentCorrectionMessage,
|
|
|
|
|
|
buildReviewFormValues,
|
|
|
|
|
|
buildReviewRiskItems,
|
|
|
|
|
|
buildReviewSubmitUserText,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
|
|
|
|
|
cloneReviewDocumentDrafts,
|
|
|
|
|
|
cloneReviewEditFields,
|
|
|
|
|
|
commitInlineReviewEditor,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
emit,
|
|
|
|
|
|
latestReviewMessage,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
mergeInlineReviewFields,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
reviewDocumentBaseDrafts,
|
|
|
|
|
|
reviewDocumentDrafts,
|
|
|
|
|
|
reviewHasUnsavedChanges,
|
|
|
|
|
|
reviewInlineBaseFields,
|
|
|
|
|
|
reviewInlineBaseForm,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
submitComposer,
|
|
|
|
|
|
submitting
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function saveInlineReviewChanges() {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!activeReviewPayload.value
|
|
|
|
|
|
|| !reviewHasUnsavedChanges.value
|
|
|
|
|
|
|| submitting.value
|
|
|
|
|
|
|| reviewActionBusy.value
|
|
|
|
|
|
|| sessionSwitchBusy.value
|
|
|
|
|
|
) return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return saveInlineReviewChangesInternal()
|
|
|
|
|
|
}
|
|
|
|
|
|
function askHotKnowledgeQuestion(question) {
|
|
|
|
|
|
const normalizedQuestion = String(question || '').trim()
|
|
|
|
|
|
if (!normalizedQuestion || !isKnowledgeSession.value || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposer({
|
|
|
|
|
|
rawText: normalizedQuestion,
|
|
|
|
|
|
userText: normalizedQuestion,
|
|
|
|
|
|
pendingText: '正在整理财务知识答案...'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function submitComposer(options = {}) {
|
|
|
|
|
|
// resolvedUploadDisposition === 'continue_existing'
|
|
|
|
|
|
// buildReviewFormContextFromPayload(
|
|
|
|
|
|
// activeReviewPayload.value,
|
|
|
|
|
|
// reviewInlineForm.value
|
|
|
|
|
|
// )
|
|
|
|
|
|
// extraContext.review_form_values
|
|
|
|
|
|
// inheritedReviewContext.business_time_context
|
|
|
|
|
|
// extraContext.business_time_context = inheritedReviewContext.business_time_context
|
|
|
|
|
|
// submitting.value = true
|
|
|
|
|
|
// recognizeOcrFiles(files)
|
|
|
|
|
|
// submitting.value = false
|
2026-06-06 17:19:07 +08:00
|
|
|
|
if (await handleStewardRuntimeDecision(options)) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
if (await handleApplicationSubmitConfirmationText(options)) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-06-04 14:25:14 +08:00
|
|
|
|
if (isStewardSession.value && !options.skipStewardPlan && await submitStewardPlan(options)) {
|
2026-06-04 11:03:29 +08:00
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-05-23 19:54:42 +08:00
|
|
|
|
if (await handleGuidedComposerSubmit(options)) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return submitComposerInternal(options)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
|
|
|
|
|
|
async function handleAssistantMarkdownClick(event, message) {
|
|
|
|
|
|
const anchor = event?.target?.closest?.('a')
|
|
|
|
|
|
if (!anchor || !message || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const href = String(anchor.getAttribute('href') || '').trim()
|
2026-05-25 13:35:39 +08:00
|
|
|
|
if (href === APPLICATION_SUBMIT_HREF) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
openApplicationSubmitConfirm(message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
if (href === REVIEW_NEXT_STEP_HREF) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
openReviewNextStepConfirm(message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (href.startsWith(REVIEW_RISK_PANEL_HREF_PREFIX)) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
if (reviewRiskDrawerAvailable.value) {
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast('当前没有需要额外处理的风险信息。')
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (href === REVIEW_QUICK_EDIT_HREF) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
if (reviewOverviewDrawerAvailable.value) {
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW)
|
|
|
|
|
|
toast('已打开右侧核对信息,可以直接修改当前单据。')
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (href.startsWith('/app/')) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
router.push(href)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
if (href !== ATTACHMENT_ASSOCIATION_CONFIRM_HREF) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
reviewActionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await confirmPendingAttachmentAssociationInternal(message)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewActionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleReviewAction(message, action) {
|
|
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
|
|
|
|
if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return
|
|
|
|
|
|
return handleReviewActionInternal(message, action)
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleSaveDraftDirectly(message, actionType = 'save_draft') {
|
|
|
|
|
|
return handleSaveDraftDirectlyInternal(message, actionType)
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function isDraftSavedReviewMessage(message) {
|
|
|
|
|
|
if (!message?.reviewPayload) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
String(message?.draftPayload?.claim_no || message?.draftPayload?.claim_id || '').trim()
|
|
|
|
|
|
|| String(draftClaimId.value || '').trim()
|
|
|
|
|
|
|| String(resolveActiveClaimId() || '').trim()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewPlainFollowupForMessage(message) {
|
|
|
|
|
|
return buildReviewPlainFollowupCopy(message?.reviewPayload, {
|
|
|
|
|
|
savedDraft: isDraftSavedReviewMessage(message)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function canUseInlineSaveDraft(message) {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (!message?.reviewPayload || isDraftSavedReviewMessage(message)) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return Boolean(resolveReviewSaveDraftAction(message.reviewPayload))
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleInlineSaveDraft(message) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!canUseInlineSaveDraft(message)
|
|
|
|
|
|
|| submitting.value
|
|
|
|
|
|
|| reviewActionBusy.value
|
|
|
|
|
|
|| sessionSwitchBusy.value
|
|
|
|
|
|
) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
await handleSaveDraftDirectly(message, 'save_draft')
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 22:55:18 +08:00
|
|
|
|
const messageItemUi = computed(() => ({
|
2026-05-27 09:17:57 +08:00
|
|
|
|
ASSISTANT_DISPLAY_NAME,
|
|
|
|
|
|
aiAvatar,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
userAvatar,
|
|
|
|
|
|
submitting: submitting.value,
|
|
|
|
|
|
reviewActionBusy: reviewActionBusy.value,
|
|
|
|
|
|
sessionSwitchBusy: sessionSwitchBusy.value,
|
|
|
|
|
|
applicationPreviewEditor: applicationPreviewEditor.value,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
buildMessageBubbleClass,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
resolveStewardMissingFieldItems,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
buildReviewMainMessageText,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
renderMarkdown,
|
|
|
|
|
|
handleAssistantMarkdownClick,
|
|
|
|
|
|
resolveApplicationPreviewRows,
|
|
|
|
|
|
resolveApplicationPreviewEditorControl,
|
|
|
|
|
|
resolveApplicationPreviewEditorOptions,
|
|
|
|
|
|
resolveApplicationPreviewMissingFields,
|
|
|
|
|
|
isApplicationPreviewEditing,
|
|
|
|
|
|
isApplicationPreviewDateEditorOpen,
|
|
|
|
|
|
openApplicationPreviewEditor: openApplicationPreviewEditorFromUi,
|
|
|
|
|
|
commitApplicationPreviewEditor,
|
|
|
|
|
|
commitApplicationPreviewDateEditor,
|
|
|
|
|
|
setApplicationPreviewDateMode,
|
2026-05-30 15:46:51 +08:00
|
|
|
|
canApplyApplicationPreviewDateSelection,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
handleApplicationPreviewEditorKeydown,
|
|
|
|
|
|
buildApplicationPreviewFooterText,
|
|
|
|
|
|
isApplicationDraftPayload,
|
|
|
|
|
|
resolveApplicationDraftStatusLabel,
|
|
|
|
|
|
buildApplicationDraftSummaryItems,
|
|
|
|
|
|
shouldShowDraftSavedCard,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
canOpenDraftDetail,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
resolveReimbursementDraftClaimNo,
|
|
|
|
|
|
openApplicationDraftDetail,
|
|
|
|
|
|
shouldShowAssistantMessageActions,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
copyAssistantMessage,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
speakAssistantMessage,
|
|
|
|
|
|
isMessageFeedbackSelected,
|
|
|
|
|
|
submitOperationFeedbackForMessage,
|
|
|
|
|
|
runWelcomeQuickAction: runShortcut,
|
|
|
|
|
|
handleSuggestedAction,
|
|
|
|
|
|
isSuggestedActionSelected,
|
|
|
|
|
|
buildExpenseQueryWindowLabel,
|
|
|
|
|
|
buildExpenseQueryHint,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
getExpenseQueryActivePage,
|
|
|
|
|
|
getExpenseQueryTotalPages,
|
|
|
|
|
|
getExpenseQueryVisibleRecords,
|
|
|
|
|
|
handleExpenseQueryRecordClick,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
appendExpenseQueryRiskToConversation,
|
|
|
|
|
|
shiftExpenseQueryPage,
|
|
|
|
|
|
setExpenseQueryPage,
|
|
|
|
|
|
buildReviewPlainFollowupForMessage,
|
|
|
|
|
|
canUseInlineSaveDraft,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
handleInlineSaveDraft,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildReviewNextStepRichCopyForMessage,
|
|
|
|
|
|
resolveReviewFooterActions,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
handleReviewAction,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
buildReviewPrimaryButtonLabel
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
|
|
const insightPanelUi = computed(() => ({
|
|
|
|
|
|
showInsightPanel: showInsightPanel.value,
|
|
|
|
|
|
isKnowledgeSession: isKnowledgeSession.value,
|
|
|
|
|
|
activeReviewPayload: activeReviewPayload.value,
|
|
|
|
|
|
isReviewFlowDrawer: isReviewFlowDrawer.value,
|
|
|
|
|
|
currentInsight: currentInsight.value,
|
|
|
|
|
|
currentIntentLabel: currentIntentLabel.value,
|
|
|
|
|
|
reviewDrawerTitle: reviewDrawerTitle.value,
|
|
|
|
|
|
reviewOverviewDrawerAvailable: reviewOverviewDrawerAvailable.value,
|
|
|
|
|
|
isReviewOverviewDrawer: isReviewOverviewDrawer.value,
|
|
|
|
|
|
submitting: submitting.value,
|
|
|
|
|
|
reviewActionBusy: reviewActionBusy.value,
|
|
|
|
|
|
switchToReviewOverviewDrawer,
|
|
|
|
|
|
reviewDocumentDrawerAvailable: reviewDocumentDrawerAvailable.value,
|
|
|
|
|
|
isReviewDocumentDrawer: isReviewDocumentDrawer.value,
|
|
|
|
|
|
toggleReviewDocumentDrawer,
|
|
|
|
|
|
reviewDocumentDrawerIcon: reviewDocumentDrawerIcon.value,
|
|
|
|
|
|
reviewRiskDrawerAvailable: reviewRiskDrawerAvailable.value,
|
|
|
|
|
|
isReviewRiskDrawer: isReviewRiskDrawer.value,
|
|
|
|
|
|
toggleReviewRiskDrawer,
|
|
|
|
|
|
reviewRiskDrawerIcon: reviewRiskDrawerIcon.value,
|
|
|
|
|
|
reviewFlowDrawerAvailable: reviewFlowDrawerAvailable.value,
|
|
|
|
|
|
flowOverallStatusTone: flowOverallStatusTone.value,
|
|
|
|
|
|
toggleReviewFlowDrawer,
|
|
|
|
|
|
reviewFlowDrawerIcon: reviewFlowDrawerIcon.value,
|
|
|
|
|
|
activeSessionType: activeSessionType.value,
|
|
|
|
|
|
reviewDrawerMode: reviewDrawerMode.value,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
hotKnowledgeQuestions,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
deleteSessionBusy: deleteSessionBusy.value,
|
|
|
|
|
|
sessionSwitchBusy: sessionSwitchBusy.value,
|
|
|
|
|
|
askHotKnowledgeQuestion,
|
|
|
|
|
|
resolveKnowledgeRankTone,
|
|
|
|
|
|
resolveKnowledgeRankLabel,
|
|
|
|
|
|
flowOverallStatusText: flowOverallStatusText.value,
|
|
|
|
|
|
flowTotalDurationText: flowTotalDurationText.value,
|
|
|
|
|
|
flowRunId: flowRunId.value,
|
|
|
|
|
|
flowRefreshBusy: flowRefreshBusy.value,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
refreshFlowRunDetail,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
flowSteps: activeFlowSteps.value,
|
|
|
|
|
|
visibleFlowSteps: visibleFlowSteps.value,
|
2026-06-13 14:52:26 +00:00
|
|
|
|
resolveFlowStepStatusLabel,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
formatFlowStepDuration,
|
|
|
|
|
|
resolveFlowStepDetail,
|
|
|
|
|
|
reviewIntentText: reviewIntentText.value,
|
|
|
|
|
|
reviewFactCards: reviewFactCards.value,
|
|
|
|
|
|
reviewInlineEditorKey: reviewInlineEditorKey.value,
|
|
|
|
|
|
reviewInlineErrors: reviewInlineErrors.value,
|
|
|
|
|
|
reviewInlineForm: reviewInlineForm.value,
|
|
|
|
|
|
DATE_INPUT_FORMAT,
|
|
|
|
|
|
REVIEW_SCENE_OPTIONS,
|
|
|
|
|
|
REVIEW_SCENE_OTHER_OPTION,
|
|
|
|
|
|
clearInlineReviewFieldError,
|
|
|
|
|
|
commitInlineReviewEditor,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
selectInlineScene,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
reviewInlinePendingFiles: reviewInlinePendingFiles.value,
|
|
|
|
|
|
openInlineReviewEditor,
|
|
|
|
|
|
reviewPanelConfidence: reviewPanelConfidence.value,
|
|
|
|
|
|
reviewCategoryOptions: reviewCategoryOptions.value,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
selectReviewCategory,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
reviewSelectedOtherCategory: reviewSelectedOtherCategory.value,
|
|
|
|
|
|
reviewOtherCategoryOpen: reviewOtherCategoryOpen.value,
|
|
|
|
|
|
reviewOtherCategoryOptions: reviewOtherCategoryOptions.value,
|
2026-05-27 09:17:57 +08:00
|
|
|
|
selectReviewOtherCategory,
|
2026-06-15 22:55:18 +08:00
|
|
|
|
activeReviewDocumentIndex: activeReviewDocumentIndex.value,
|
|
|
|
|
|
reviewDocumentCount: reviewDocumentCount.value,
|
|
|
|
|
|
goReviewDocument,
|
|
|
|
|
|
activeReviewDocument: activeReviewDocument.value,
|
|
|
|
|
|
activeReviewDocumentPreview: activeReviewDocumentPreview.value,
|
|
|
|
|
|
canPreviewActiveReviewDocument: canPreviewActiveReviewDocument.value,
|
|
|
|
|
|
openActiveReviewDocumentPreview,
|
|
|
|
|
|
reviewRiskSummary: reviewRiskSummary.value,
|
|
|
|
|
|
reviewRiskItems: reviewRiskItems.value,
|
|
|
|
|
|
appendReviewRiskBriefToConversation,
|
|
|
|
|
|
reviewRiskEmpty: reviewRiskEmpty.value,
|
|
|
|
|
|
reviewHasUnsavedChanges: reviewHasUnsavedChanges.value,
|
|
|
|
|
|
saveInlineReviewChanges
|
|
|
|
|
|
}))
|
2026-05-27 09:17:57 +08:00
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return {
|
2026-05-27 09:17:57 +08:00
|
|
|
|
emit, messageItemUi, insightPanelUi, ASSISTANT_DISPLAY_NAME, aiAvatar, userAvatar, fileInputRef, composerTextareaRef, messageListRef, composerDraft, composerDatePickerOpen, composerDateMode, composerSingleDate, composerRangeStartDate, composerRangeEndDate, composerBusinessTimeTags, composerCanApplyDateSelection,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
toggleComposerDatePicker, closeComposerDatePicker, setComposerDateMode, handleComposerDateInputChange, removeComposerBusinessTimeTag, flowSteps, flowRunId, flowRefreshBusy, completedFlowStepCount, flowOverallStatusTone, flowOverallStatusText, flowTotalDurationText,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
attachedFiles, composerFilesExpanded, visibleAttachedFiles, hiddenAttachedFileCount, submitting, sessionSwitchBusy, messages, currentInsight, linkedRequest, canSubmit, activeSessionType, isKnowledgeSession, isStewardSession, showStewardInitialRecognition, hotKnowledgeQuestions,
|
2026-05-25 13:35:39 +08:00
|
|
|
|
hasInsightPanelContent, showInsightPanel, insightPanelToggleLabel, assistantHeaderTitle, assistantHeaderDescription, composerPlaceholder, currentIntentLabel, canDeleteCurrentSession, latestReviewMessage, activeReviewPayload, activeReviewPanelScope, activeReviewFilePreviews, reviewDrawerMode, isReviewOverviewDrawer, isReviewDocumentDrawer, isReviewRiskDrawer, isReviewFlowDrawer,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
reviewDrawerTitle, reviewOverviewDrawerAvailable, reviewDocumentDrawerAvailable, reviewRiskDrawerAvailable, reviewFlowDrawerAvailable, reviewDocumentDrawerLabel, reviewDocumentDrawerIcon, reviewRiskDrawerLabel, reviewRiskDrawerIcon, reviewFlowDrawerLabel, reviewFlowDrawerIcon, activeReviewDocument, activeReviewDocumentIndex, activeReviewDocumentPreview, canPreviewActiveReviewDocument,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewOtherCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewInlineForm, reviewInlineEditorKey, reviewInlineErrors, reviewOtherCategoryOpen, reviewInlinePendingFiles, DATE_INPUT_FORMAT, REVIEW_SCENE_OTHER_OPTION, REVIEW_SCENE_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
workbenchVisible, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, reviewDocumentDirty, reviewHasUnsavedChanges,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, canShowTravelCalculator, deleteSessionDialogOpen, applicationSubmitConfirmDialog, applicationPreviewEditor, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, buildReviewPlainFollowupForMessage, buildReviewNextStepRichCopyForMessage, buildMessageBubbleClass, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewMainMessageText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
renderMarkdown, buildExpenseQueryWindowLabel, buildExpenseQueryHint, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, resolveDocumentPreview, triggerFileUpload, applyComposerDateSelection, handleFilesChange, handleComposerInput, handleComposerEnter, runShortcut, runWelcomeQuickAction: runShortcut, handleSuggestedAction, isSuggestedActionSelected, askHotKnowledgeQuestion, resolveKnowledgeRankLabel, resolveKnowledgeRankTone,
|
|
|
|
|
|
refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail, toggleInsightPanel, openTravelCalculator, toggleTravelCalculator, closeTravelCalculator, submitTravelCalculator, switchToReviewOverviewDrawer, toggleReviewDocumentDrawer, toggleReviewRiskDrawer, toggleReviewFlowDrawer, toggleAttachedFilesExpanded, removeAttachedFile, clearAttachedFiles,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
requestCloseWorkbench, emitCloseAfterLeave, handleAssistantModalAfterEnter, openExpenseQueryRecord, handleExpenseQueryRecordClick, setExpenseQueryPage, shiftExpenseQueryPage, openDeleteSessionDialog, closeDeleteSessionDialog, confirmDeleteCurrentSession, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, clearInlineReviewFieldError, selectInlineScene, selectReviewCategory, selectReviewOtherCategory,
|
2026-06-06 17:19:07 +08:00
|
|
|
|
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, resolveApplicationPreviewRows, resolveApplicationPreviewEditorControl, resolveApplicationPreviewEditorOptions, isApplicationPreviewEditing, isApplicationPreviewDateEditorOpen, openApplicationPreviewEditor: openApplicationPreviewEditorFromUi, commitApplicationPreviewEditor, commitApplicationPreviewDateEditor, cancelApplicationPreviewEditor, setApplicationPreviewDateMode, canApplyApplicationPreviewDateSelection, handleApplicationPreviewEditorKeydown, buildApplicationPreviewFooterText, isApplicationDraftPayload, resolveApplicationDraftStatusLabel, buildApplicationDraftSummaryItems, shouldShowDraftSavedCard, resolveReimbursementDraftClaimNo, openApplicationDraftDetail, shouldShowAssistantMessageActions, copyAssistantMessage, speakAssistantMessage, isMessageFeedbackSelected, submitOperationFeedbackForMessage, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|