247 lines
7.8 KiB
JavaScript
247 lines
7.8 KiB
JavaScript
|
|
import { filterVisibleMessageMeta } from '../../utils/assistantMessageMeta.js'
|
||
|
|
import { resolveExpenseTypeLabel } from './travelReimbursementReviewModel.js'
|
||
|
|
import {
|
||
|
|
FLOW_MISSING_SLOT_LABELS,
|
||
|
|
FLOW_STEP_FALLBACKS
|
||
|
|
} from './travelReimbursementConversationSessionModel.js'
|
||
|
|
|
||
|
|
let messageSeed = 0
|
||
|
|
|
||
|
|
export function nowTime() {
|
||
|
|
return new Date().toLocaleTimeString('zh-CN', {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
hour12: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createMessage(role, text, attachments = [], extras = {}) {
|
||
|
|
messageSeed += 1
|
||
|
|
const message = {
|
||
|
|
id: `msg-${messageSeed}`,
|
||
|
|
role,
|
||
|
|
text,
|
||
|
|
attachments,
|
||
|
|
time: nowTime(),
|
||
|
|
meta: [],
|
||
|
|
citations: [],
|
||
|
|
suggestedActions: [],
|
||
|
|
suggestedActionsLocked: false,
|
||
|
|
selectedSuggestedActionKey: '',
|
||
|
|
selectedSuggestedActionLabel: '',
|
||
|
|
querySelectionLocked: false,
|
||
|
|
selectedQueryRecordId: '',
|
||
|
|
queryPayload: null,
|
||
|
|
draftPayload: null,
|
||
|
|
reviewPayload: null,
|
||
|
|
reviewPanelScope: '',
|
||
|
|
riskFlags: [],
|
||
|
|
pendingAttachmentAssociation: null,
|
||
|
|
applicationPreview: null,
|
||
|
|
budgetReport: null,
|
||
|
|
stewardPlan: null,
|
||
|
|
operationFeedback: null,
|
||
|
|
...extras
|
||
|
|
}
|
||
|
|
message.meta = filterVisibleMessageMeta(message.meta)
|
||
|
|
return message
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildExpenseIntentConfirmationMessage(rawText) {
|
||
|
|
const text = String(rawText || '').trim()
|
||
|
|
return [
|
||
|
|
text
|
||
|
|
? `我看到了「${text}」这类业务事项描述。`
|
||
|
|
: '我看到了这类业务事项描述。',
|
||
|
|
'但现在还不能确定你是要发起报销,还是要处理其他事项,所以我先暂停后续识别。',
|
||
|
|
'如果你是想报销,请点击下面的“我要报销”,我再继续引导你选择具体报销场景。'
|
||
|
|
].join('\n')
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildExpenseSceneSelectionMessage(rawText) {
|
||
|
|
const text = String(rawText || '').trim()
|
||
|
|
const hasBusinessTime = /业务发生时间|发生时间|20\d{2}[-年\/.]\d{1,2}/.test(text)
|
||
|
|
const prefix = hasBusinessTime
|
||
|
|
? '我已看到你提供了业务发生时间和报销意图。'
|
||
|
|
: '我已识别到这是报销申请。'
|
||
|
|
|
||
|
|
return [
|
||
|
|
`${prefix}先选一下这笔费用属于哪一类,我再按对应流程继续。`,
|
||
|
|
'差旅和业务招待通常需要先关联申请单;交通、住宿、办公用品这类一般可以直接继续填写。',
|
||
|
|
'选完后我会把下一步需要准备的内容整理给你。'
|
||
|
|
].join('\n')
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatMessageTime(value) {
|
||
|
|
if (!value) {
|
||
|
|
return nowTime()
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = new Date(value)
|
||
|
|
if (Number.isNaN(parsed.getTime())) {
|
||
|
|
return nowTime()
|
||
|
|
}
|
||
|
|
|
||
|
|
return parsed.toLocaleTimeString('zh-CN', {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
hour12: false
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
export function formatSemanticEntityValue(entity) {
|
||
|
|
const normalizedValue = String(entity?.normalized_value || '').trim()
|
||
|
|
const rawValue = String(entity?.value || '').trim()
|
||
|
|
const entityType = String(entity?.type || '').trim()
|
||
|
|
|
||
|
|
if (entityType === 'amount') {
|
||
|
|
const numericValue = Number(normalizedValue || rawValue)
|
||
|
|
if (Number.isFinite(numericValue) && numericValue > 0) {
|
||
|
|
return Number.isInteger(numericValue) ? `${numericValue}元` : `${numericValue.toFixed(2)}元`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return rawValue || normalizedValue
|
||
|
|
}
|
||
|
|
|
||
|
|
export function summarizeSemanticParseDetail(semanticParse, ontologyJson = {}) {
|
||
|
|
if (!semanticParse || typeof semanticParse !== 'object') {
|
||
|
|
return FLOW_STEP_FALLBACKS.extraction.completedText
|
||
|
|
}
|
||
|
|
|
||
|
|
const entities = Array.isArray(semanticParse.entities_json) ? semanticParse.entities_json : []
|
||
|
|
const entityMap = new Map()
|
||
|
|
for (const item of entities) {
|
||
|
|
const entityType = String(item?.type || '').trim()
|
||
|
|
if (!entityType || entityMap.has(entityType)) continue
|
||
|
|
entityMap.set(entityType, item)
|
||
|
|
}
|
||
|
|
|
||
|
|
const extractedParts = []
|
||
|
|
const timeRange = semanticParse.time_range_json && typeof semanticParse.time_range_json === 'object'
|
||
|
|
? semanticParse.time_range_json
|
||
|
|
: {}
|
||
|
|
const startDate = String(timeRange.start_date || '').trim()
|
||
|
|
const endDate = String(timeRange.end_date || '').trim()
|
||
|
|
if (startDate) {
|
||
|
|
extractedParts.push(`时间 ${startDate}${endDate && endDate !== startDate ? ` 至 ${endDate}` : ''}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const amountEntity = entityMap.get('amount')
|
||
|
|
if (amountEntity) {
|
||
|
|
const amountValue = formatSemanticEntityValue(amountEntity)
|
||
|
|
if (amountValue) {
|
||
|
|
extractedParts.push(`金额 ${amountValue}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const expenseTypeEntity = entityMap.get('expense_type')
|
||
|
|
if (expenseTypeEntity) {
|
||
|
|
const expenseTypeLabel = resolveExpenseTypeLabel(
|
||
|
|
String(expenseTypeEntity?.normalized_value || '').trim(),
|
||
|
|
String(expenseTypeEntity?.value || '').trim()
|
||
|
|
)
|
||
|
|
if (expenseTypeLabel) {
|
||
|
|
extractedParts.push(`费用类型 ${expenseTypeLabel}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const customerEntity = entityMap.get('customer')
|
||
|
|
if (customerEntity) {
|
||
|
|
const customerValue = formatSemanticEntityValue(customerEntity)
|
||
|
|
if (customerValue) {
|
||
|
|
extractedParts.push(`客户 ${customerValue}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const missingSlots = Array.isArray(ontologyJson?.missing_slots) ? ontologyJson.missing_slots : []
|
||
|
|
const missingLabels = missingSlots
|
||
|
|
.map((item) => FLOW_MISSING_SLOT_LABELS[String(item || '').trim()] || String(item || '').trim())
|
||
|
|
.filter(Boolean)
|
||
|
|
|
||
|
|
if (extractedParts.length && missingLabels.length) {
|
||
|
|
return `已提取${extractedParts.join('、')};待补充 ${missingLabels.join('、')}`
|
||
|
|
}
|
||
|
|
if (extractedParts.length) {
|
||
|
|
return `已提取${extractedParts.join('、')}`
|
||
|
|
}
|
||
|
|
if (missingLabels.length) {
|
||
|
|
return `已完成信息提取;待补充 ${missingLabels.join('、')}`
|
||
|
|
}
|
||
|
|
return FLOW_STEP_FALLBACKS.extraction.completedText
|
||
|
|
}
|
||
|
|
|
||
|
|
export function sanitizeRequest(request) {
|
||
|
|
if (!request || typeof request !== 'object') return null
|
||
|
|
|
||
|
|
const normalized = {
|
||
|
|
claimId: String(request.claimId || request.claim_id || '').trim(),
|
||
|
|
claimNo: String(request.claimNo || request.claim_no || request.documentNo || '').trim(),
|
||
|
|
id: String(request.id || '').trim(),
|
||
|
|
typeLabel: String(request.typeLabel || request.category || '').trim(),
|
||
|
|
reason: String(request.reason || request.title || '').trim(),
|
||
|
|
entity: String(request.entity || '').trim(),
|
||
|
|
city: String(request.city || request.location || '').trim(),
|
||
|
|
period: String(request.period || '').trim(),
|
||
|
|
applyTime: String(request.applyTime || request.occurredAt || '').trim(),
|
||
|
|
amount: String(request.amount || '').trim(),
|
||
|
|
node: String(request.node || '').trim(),
|
||
|
|
approval: String(request.approval || '').trim(),
|
||
|
|
travel: String(request.travel || '').trim(),
|
||
|
|
applicationEditMode: Boolean(request.applicationEditMode || request.application_edit_mode)
|
||
|
|
}
|
||
|
|
|
||
|
|
return Object.values(normalized).some(Boolean) ? normalized : null
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveStatusLabel(status) {
|
||
|
|
if (status === 'succeeded') return '已完成'
|
||
|
|
if (status === 'blocked') return '已阻断'
|
||
|
|
return '失败'
|
||
|
|
}
|
||
|
|
|
||
|
|
export function resolveStatusTone(status) {
|
||
|
|
if (status === 'succeeded') return 'success'
|
||
|
|
if (status === 'blocked') return 'warning'
|
||
|
|
return 'note'
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildMessageMeta(payload, fileNames = []) {
|
||
|
|
const items = []
|
||
|
|
|
||
|
|
if (payload?.trace_summary?.degraded) {
|
||
|
|
items.push('已降级')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (payload?.requires_confirmation) {
|
||
|
|
items.push('待确认')
|
||
|
|
}
|
||
|
|
|
||
|
|
if (fileNames.length) {
|
||
|
|
items.push(`附件: ${fileNames.length}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
return filterVisibleMessageMeta(items)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildStoredMessageMeta(messageJson, attachmentNames = []) {
|
||
|
|
const payload = messageJson?.orchestrator_payload
|
||
|
|
if (payload) {
|
||
|
|
return buildMessageMeta(payload, attachmentNames)
|
||
|
|
}
|
||
|
|
|
||
|
|
const items = []
|
||
|
|
if (messageJson?.status) {
|
||
|
|
items.push(`状态: ${messageJson.status}`)
|
||
|
|
}
|
||
|
|
if (attachmentNames.length) {
|
||
|
|
items.push(`附件: ${attachmentNames.length}`)
|
||
|
|
}
|
||
|
|
return filterVisibleMessageMeta(items)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildRestoredMessageId(sourceId = '') {
|
||
|
|
const normalizedId = String(sourceId || '').trim()
|
||
|
|
return `restored-${normalizedId || ++messageSeed}`
|
||
|
|
}
|