feat: 完善文档中心与报销申请交互及侧边栏重构
后端优化编排器报销查询和本体检测精度,增强报销单草稿保 存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导 航,完善文档中心状态筛选和详情提示,报销创建和审批详情 页优化会话管理和费用明细交互,新增助手应用服务和预设动 作工具函数,补充单元测试覆盖。
This commit is contained in:
173
web/src/utils/assistantSessionScope.js
Normal file
173
web/src/utils/assistantSessionScope.js
Normal file
@@ -0,0 +1,173 @@
|
||||
export const ASSISTANT_SCOPE_ACTION_SWITCH = 'switch_assistant_session'
|
||||
|
||||
export const ASSISTANT_SCOPE_SESSION_APPLICATION = 'application'
|
||||
export const ASSISTANT_SCOPE_SESSION_EXPENSE = 'expense'
|
||||
export const ASSISTANT_SCOPE_SESSION_APPROVAL = 'approval'
|
||||
export const ASSISTANT_SCOPE_SESSION_KNOWLEDGE = 'knowledge'
|
||||
|
||||
const SESSION_SCOPE_CONFIG = {
|
||||
[ASSISTANT_SCOPE_SESSION_APPLICATION]: {
|
||||
label: '申请助手',
|
||||
icon: 'mdi mdi-file-plus-outline',
|
||||
scope: '费用申请、事前审批、申请材料清单、申请单状态查询'
|
||||
},
|
||||
[ASSISTANT_SCOPE_SESSION_EXPENSE]: {
|
||||
label: '报销助手',
|
||||
icon: 'mdi mdi-receipt-text-plus-outline',
|
||||
scope: '发起报销、票据识别、草稿归集、报销单状态查询和报销信息核对'
|
||||
},
|
||||
[ASSISTANT_SCOPE_SESSION_APPROVAL]: {
|
||||
label: '审核助手',
|
||||
icon: 'mdi mdi-clipboard-check-outline',
|
||||
scope: '待审单据查询、审批动作、风险解释和审核意见草稿'
|
||||
},
|
||||
[ASSISTANT_SCOPE_SESSION_KNOWLEDGE]: {
|
||||
label: '财务知识助手',
|
||||
icon: 'mdi mdi-book-open-page-variant-outline',
|
||||
scope: '财务制度、报销标准、票据要求、流程规则和政策口径解释'
|
||||
}
|
||||
}
|
||||
|
||||
const SESSION_SCOPE_TYPES = Object.keys(SESSION_SCOPE_CONFIG)
|
||||
|
||||
const APPLICATION_PATTERN =
|
||||
/费用申请|发起申请|申请单|事前申请|事前审批|前置审批|出差申请|采购申请|用款申请|预算申请|申请材料|材料清单|先申请|立项申请/
|
||||
const EXPENSE_PATTERN =
|
||||
/报销|报销单|票据|发票|火车票|高铁票|机票|飞机票|的士票|出租车|网约车|酒店票|住宿票|住宿单据|保存草稿|草稿|费用明细|归集|上传.*票|关联单据|继续下一步/
|
||||
const APPROVAL_PATTERN =
|
||||
/待我审核|待审|审核|审批|审核意见|审批意见|审批通过|审批驳回|驳回|退回|审核中心|审批中心|领导审批|财务审核|处理意见/
|
||||
const KNOWLEDGE_PATTERN =
|
||||
/制度|政策|标准|规则|规定|流程|口径|依据|上限|额度|补贴|住宿标准|差旅标准|报销标准|票据要求|可不可以|能不能|怎么规定|如何计算|怎么算/
|
||||
const EXPENSE_OPERATION_PATTERN = /发起报销|报销单|票据|发票|火车票|高铁票|机票|的士票|草稿|归集|上传|关联单据|继续下一步/
|
||||
const CURRENT_CLAIM_RISK_PATTERN = /这张|当前|本单|该单|单据|风险|超标|异常|重复|待补/
|
||||
|
||||
function normalizeSessionType(sessionType) {
|
||||
const normalized = String(sessionType || '').trim()
|
||||
return SESSION_SCOPE_TYPES.includes(normalized) ? normalized : ASSISTANT_SCOPE_SESSION_EXPENSE
|
||||
}
|
||||
|
||||
function normalizeText(rawText) {
|
||||
return String(rawText || '')
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
function resolveScopeConfig(sessionType) {
|
||||
return SESSION_SCOPE_CONFIG[normalizeSessionType(sessionType)] || SESSION_SCOPE_CONFIG[ASSISTANT_SCOPE_SESSION_EXPENSE]
|
||||
}
|
||||
|
||||
export function inferAssistantScopeTarget(rawText, options = {}) {
|
||||
const text = normalizeText(rawText)
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const applicationMatched = APPLICATION_PATTERN.test(text)
|
||||
const expenseMatched = EXPENSE_PATTERN.test(text)
|
||||
const approvalMatched = APPROVAL_PATTERN.test(text)
|
||||
const knowledgeMatched = KNOWLEDGE_PATTERN.test(text)
|
||||
|
||||
if (approvalMatched && /(待我审核|待审|审核意见|审批意见|审批通过|审批驳回|驳回|退回|审核中心|审批中心|处理意见)/.test(text)) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPROVAL
|
||||
}
|
||||
|
||||
if (knowledgeMatched && !options.hasActiveReviewPayload && !EXPENSE_OPERATION_PATTERN.test(text)) {
|
||||
return ASSISTANT_SCOPE_SESSION_KNOWLEDGE
|
||||
}
|
||||
|
||||
if (expenseMatched && !applicationMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_EXPENSE
|
||||
}
|
||||
|
||||
if (applicationMatched && !expenseMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPLICATION
|
||||
}
|
||||
|
||||
if (knowledgeMatched && !expenseMatched && !approvalMatched && !applicationMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_KNOWLEDGE
|
||||
}
|
||||
|
||||
if (knowledgeMatched && !options.hasActiveReviewPayload) {
|
||||
return ASSISTANT_SCOPE_SESSION_KNOWLEDGE
|
||||
}
|
||||
|
||||
if (approvalMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPROVAL
|
||||
}
|
||||
|
||||
if (expenseMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_EXPENSE
|
||||
}
|
||||
|
||||
if (applicationMatched) {
|
||||
return ASSISTANT_SCOPE_SESSION_APPLICATION
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function shouldAllowCurrentExpensePolicyQuestion(rawText, currentSessionType, targetSessionType, options = {}) {
|
||||
if (
|
||||
normalizeSessionType(currentSessionType) !== ASSISTANT_SCOPE_SESSION_EXPENSE ||
|
||||
targetSessionType !== ASSISTANT_SCOPE_SESSION_KNOWLEDGE ||
|
||||
!options.hasActiveReviewPayload
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return CURRENT_CLAIM_RISK_PATTERN.test(normalizeText(rawText))
|
||||
}
|
||||
|
||||
function buildScopeSwitchAction(targetSessionType, rawText, options = {}) {
|
||||
const target = resolveScopeConfig(targetSessionType)
|
||||
const carryText = String(rawText || '').trim()
|
||||
|
||||
return {
|
||||
label: `切换到${target.label}`,
|
||||
description: `带着这条内容进入${target.label}继续处理`,
|
||||
icon: target.icon,
|
||||
action_type: ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||
payload: {
|
||||
session_type: normalizeSessionType(targetSessionType),
|
||||
carry_text: carryText,
|
||||
carry_files: Boolean(options.attachmentCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildScopeBoundaryText(currentSessionType, targetSessionType) {
|
||||
const current = resolveScopeConfig(currentSessionType)
|
||||
const target = resolveScopeConfig(targetSessionType)
|
||||
|
||||
return [
|
||||
`我先暂停在「${current.label}」里继续处理这条消息。`,
|
||||
'',
|
||||
`当前助手的业务范围是:${current.scope}。`,
|
||||
'',
|
||||
`您这条内容更适合交给「${target.label}」处理;它的业务范围是:${target.scope}。`,
|
||||
'',
|
||||
`建议切换到「${target.label}」后继续,我会尽量把这条内容带过去,避免在错误的会话里把流程跑偏。`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function resolveAssistantScopeGuard(rawText, currentSessionType, options = {}) {
|
||||
const normalizedCurrent = normalizeSessionType(currentSessionType)
|
||||
const targetSessionType = inferAssistantScopeTarget(rawText, options)
|
||||
if (!targetSessionType || targetSessionType === normalizedCurrent) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (shouldAllowCurrentExpensePolicyQuestion(rawText, normalizedCurrent, targetSessionType, options)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const target = resolveScopeConfig(targetSessionType)
|
||||
|
||||
return {
|
||||
targetSessionType,
|
||||
targetLabel: target.label,
|
||||
text: buildScopeBoundaryText(normalizedCurrent, targetSessionType),
|
||||
meta: [`建议切换至${target.label}`],
|
||||
suggestedActions: [buildScopeSwitchAction(targetSessionType, rawText, options)]
|
||||
}
|
||||
}
|
||||
46
web/src/utils/assistantSuggestedActionPrefill.js
Normal file
46
web/src/utils/assistantSuggestedActionPrefill.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const APPLICATION_FIELD_PREFILLS = {
|
||||
time: '申请时间段:',
|
||||
time_range: '申请时间段:',
|
||||
location: '地点:',
|
||||
reason: '事由:',
|
||||
days: '天数:',
|
||||
transport_mode: '出行方式:',
|
||||
amount: '预计总费用:'
|
||||
}
|
||||
|
||||
export function resolveSuggestedActionPrefill(action = {}) {
|
||||
const payload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const explicitPrefill = String(
|
||||
payload.prompt_prefill
|
||||
|| payload.input_prefill
|
||||
|| payload.prefill_text
|
||||
|| ''
|
||||
).trim()
|
||||
if (explicitPrefill) {
|
||||
return explicitPrefill
|
||||
}
|
||||
|
||||
const actionType = String(action?.action_type || '').trim()
|
||||
if (actionType !== 'prefill_composer') {
|
||||
return ''
|
||||
}
|
||||
|
||||
const applicationField = String(payload.application_field || '').trim()
|
||||
return APPLICATION_FIELD_PREFILLS[applicationField] || ''
|
||||
}
|
||||
|
||||
export function mergeComposerPrefill(currentDraft = '', prefill = '') {
|
||||
const normalizedPrefill = String(prefill || '').trim()
|
||||
if (!normalizedPrefill) {
|
||||
return String(currentDraft || '')
|
||||
}
|
||||
|
||||
const current = String(currentDraft || '')
|
||||
if (!current.trim()) {
|
||||
return normalizedPrefill
|
||||
}
|
||||
if (current.includes(normalizedPrefill)) {
|
||||
return current
|
||||
}
|
||||
return `${current.trimEnd()}\n${normalizedPrefill}`
|
||||
}
|
||||
@@ -14,6 +14,32 @@ function normalizeExpenseType(value) {
|
||||
return String(value || '').trim() || 'other'
|
||||
}
|
||||
|
||||
function isApplicationDocumentRequest(request) {
|
||||
const documentType = String(
|
||||
request?.documentTypeCode
|
||||
|| request?.document_type_code
|
||||
|| request?.documentType
|
||||
|| request?.document_type
|
||||
|| ''
|
||||
).trim()
|
||||
const claimNo = String(
|
||||
request?.claimNo
|
||||
|| request?.claim_no
|
||||
|| request?.documentNo
|
||||
|| request?.id
|
||||
|| ''
|
||||
).trim().toUpperCase()
|
||||
const typeCode = normalizeExpenseType(request?.typeCode || request?.expense_type)
|
||||
|
||||
return (
|
||||
documentType === 'application'
|
||||
|| documentType === 'expense_application'
|
||||
|| claimNo.startsWith('APP-')
|
||||
|| typeCode === 'application'
|
||||
|| typeCode.endsWith('_application')
|
||||
)
|
||||
}
|
||||
|
||||
function isSystemGeneratedExpenseItem(item) {
|
||||
const itemType = normalizeExpenseType(item?.itemType || item?.item_type)
|
||||
return Boolean(item?.isSystemGenerated || item?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType))
|
||||
@@ -29,6 +55,10 @@ function getExpenseItems(request) {
|
||||
}
|
||||
|
||||
export function hasMissingAttachment(request) {
|
||||
if (isApplicationDocumentRequest(request)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const expenseItems = getExpenseItems(request)
|
||||
|
||||
if (expenseItems.length) {
|
||||
|
||||
71
web/src/utils/documentCenterNewState.js
Normal file
71
web/src/utils/documentCenterNewState.js
Normal file
@@ -0,0 +1,71 @@
|
||||
const STORAGE_KEY = 'x-financial.documents.viewed'
|
||||
const SCOPE_STORAGE_KEY = 'x-financial.documents.scope'
|
||||
|
||||
function getStorage() {
|
||||
return typeof window === 'undefined' ? null : window.localStorage
|
||||
}
|
||||
|
||||
export function resolveDocumentNewKey(row) {
|
||||
const source = String(row?.source || 'document').trim()
|
||||
const id = String(row?.claimId || row?.documentNo || row?.documentKey || row?.id || '').trim()
|
||||
return id ? `${source}:${id}` : ''
|
||||
}
|
||||
|
||||
export function readViewedDocumentKeys(storage = getStorage()) {
|
||||
if (!storage) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(storage.getItem(STORAGE_KEY) || '[]')
|
||||
return new Set(Array.isArray(parsed) ? parsed.map((item) => String(item || '').trim()).filter(Boolean) : [])
|
||||
} catch {
|
||||
return new Set()
|
||||
}
|
||||
}
|
||||
|
||||
export function writeViewedDocumentKeys(keys, storage = getStorage()) {
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
|
||||
storage.setItem(STORAGE_KEY, JSON.stringify(Array.from(keys).filter(Boolean)))
|
||||
}
|
||||
|
||||
export function readDocumentScope(fallback, allowedScopes = [], storage = getStorage()) {
|
||||
if (!storage) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const storedScope = String(storage.getItem(SCOPE_STORAGE_KEY) || '').trim()
|
||||
return allowedScopes.includes(storedScope) ? storedScope : fallback
|
||||
}
|
||||
|
||||
export function writeDocumentScope(scope, allowedScopes = [], storage = getStorage()) {
|
||||
if (!storage || !allowedScopes.includes(scope)) {
|
||||
return
|
||||
}
|
||||
|
||||
storage.setItem(SCOPE_STORAGE_KEY, scope)
|
||||
}
|
||||
|
||||
export function isNewDocument(row, viewedKeys) {
|
||||
const key = resolveDocumentNewKey(row)
|
||||
return Boolean(key) && !viewedKeys.has(key)
|
||||
}
|
||||
|
||||
export function countNewDocuments(rows, viewedKeys) {
|
||||
return rows.filter((row) => isNewDocument(row, viewedKeys)).length
|
||||
}
|
||||
|
||||
export function markDocumentViewed(row, viewedKeys, storage = getStorage()) {
|
||||
const key = resolveDocumentNewKey(row)
|
||||
if (!key) {
|
||||
return viewedKeys
|
||||
}
|
||||
|
||||
const nextKeys = new Set(viewedKeys)
|
||||
nextKeys.add(key)
|
||||
writeViewedDocumentKeys(nextKeys, storage)
|
||||
return nextKeys
|
||||
}
|
||||
@@ -16,7 +16,10 @@ const SLOT_LABELS = {
|
||||
expense_type: '费用场景',
|
||||
amount: '申请金额',
|
||||
time_range: '业务时间',
|
||||
location: '业务地点',
|
||||
reason: '申请事由',
|
||||
days: '天数',
|
||||
transport_mode: '出行方式',
|
||||
attachments: '附件说明',
|
||||
customer_name: '客户名称',
|
||||
participants: '参与人员'
|
||||
@@ -24,6 +27,33 @@ const SLOT_LABELS = {
|
||||
|
||||
const PRE_APPROVAL_TYPES = new Set(['travel', 'meeting', 'office', 'training'])
|
||||
const ATTACHMENT_REQUIRED_TYPES = new Set(['meeting', 'training'])
|
||||
const PLACEHOLDER_VALUES = new Set(['', '待补充', '暂无', '无', '未知'])
|
||||
const PROMPT_FIELD_LABELS = [
|
||||
'发生时间',
|
||||
'业务发生时间',
|
||||
'申请时间',
|
||||
'时间',
|
||||
'地点',
|
||||
'业务地点',
|
||||
'发生地点',
|
||||
'事由',
|
||||
'申请事由',
|
||||
'出差事由',
|
||||
'原因',
|
||||
'用途',
|
||||
'天数',
|
||||
'出差天数',
|
||||
'申请天数',
|
||||
'出行方式',
|
||||
'交通方式',
|
||||
'交通工具',
|
||||
'预计总费用',
|
||||
'预计费用',
|
||||
'预计金额',
|
||||
'申请金额',
|
||||
'预算',
|
||||
'金额'
|
||||
]
|
||||
|
||||
export const APPLICATION_EXAMPLES = [
|
||||
'申请下周去北京做客户现场验收,差旅预算18000元',
|
||||
@@ -89,6 +119,112 @@ export function resolveTimeRangeText(ontology) {
|
||||
return String(range.raw || '').trim()
|
||||
}
|
||||
|
||||
function parseApplicationDate(value) {
|
||||
const normalized = String(value || '')
|
||||
.trim()
|
||||
.replace(/日$/, '')
|
||||
.replace(/年|月|\//g, '-')
|
||||
.replace(/\./g, '-')
|
||||
const match = normalized.match(/^(20\d{2})-(\d{1,2})-(\d{1,2})$/)
|
||||
if (!match) return null
|
||||
const [, year, month, day] = match
|
||||
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)))
|
||||
if (Number.isNaN(date.getTime())) return null
|
||||
return date
|
||||
}
|
||||
|
||||
function formatApplicationDate(date) {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function parseChineseNumber(value) {
|
||||
const digits = {
|
||||
一: 1,
|
||||
二: 2,
|
||||
两: 2,
|
||||
三: 3,
|
||||
四: 4,
|
||||
五: 5,
|
||||
六: 6,
|
||||
七: 7,
|
||||
八: 8,
|
||||
九: 9
|
||||
}
|
||||
const text = String(value || '').trim()
|
||||
if (!text) return 0
|
||||
if (text === '十') return 10
|
||||
if (text.includes('十')) {
|
||||
const [left, right] = text.split('十')
|
||||
const tens = left ? digits[left] || 0 : 1
|
||||
const ones = right ? digits[right] || 0 : 0
|
||||
return tens * 10 + ones
|
||||
}
|
||||
return digits[text] || 0
|
||||
}
|
||||
|
||||
export function resolvePromptDays(prompt) {
|
||||
const labeled = resolvePromptField(prompt, ['天数', '出差天数', '申请天数'])
|
||||
const source = labeled || String(prompt || '')
|
||||
const match = source.match(/(?<days>\d+|[一二两三四五六七八九十]{1,3})\s*天/)
|
||||
if (!match?.groups?.days) return 0
|
||||
if (/^\d+$/.test(match.groups.days)) return Number(match.groups.days)
|
||||
return parseChineseNumber(match.groups.days)
|
||||
}
|
||||
|
||||
export function expandApplicationTimeWithDays(timeText, days = 0) {
|
||||
const normalizedTime = String(timeText || '').trim()
|
||||
const dayCount = Number(days || 0)
|
||||
if (!normalizedTime || !dayCount) return normalizedTime
|
||||
if (/\s*(至|到|~|--|—)\s*/.test(normalizedTime)) return normalizedTime
|
||||
|
||||
const match = normalizedTime.match(/20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?/)
|
||||
const startDate = parseApplicationDate(match?.[0] || '')
|
||||
if (!startDate) return normalizedTime
|
||||
|
||||
const endDate = new Date(startDate.getTime())
|
||||
endDate.setUTCDate(endDate.getUTCDate() + dayCount)
|
||||
return `${formatApplicationDate(startDate)} 至 ${formatApplicationDate(endDate)}`
|
||||
}
|
||||
|
||||
export function resolveApplicationTimeRange(ontology, prompt) {
|
||||
const range = ontology?.time_range || {}
|
||||
const baseTime = resolveTimeRangeText(ontology)
|
||||
|| resolvePromptField(prompt, ['发生时间', '业务发生时间', '申请时间', '时间'])
|
||||
if (range.start_date && range.end_date && range.start_date !== range.end_date) {
|
||||
return `${range.start_date} 至 ${range.end_date}`
|
||||
}
|
||||
return expandApplicationTimeWithDays(baseTime, resolvePromptDays(prompt)) || baseTime
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export function resolvePromptField(prompt, labels = []) {
|
||||
const text = String(prompt || '').trim()
|
||||
if (!text) return ''
|
||||
const labelSet = new Set(labels.map((item) => String(item || '').trim()).filter(Boolean))
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const match = line.match(/^\s*([^::\s]+)\s*[::]\s*(.+?)\s*$/)
|
||||
if (match && labelSet.has(match[1].trim())) {
|
||||
return match[2].trim()
|
||||
}
|
||||
}
|
||||
|
||||
const labelPattern = labels.map(escapeRegExp).join('|')
|
||||
const nextLabelPattern = PROMPT_FIELD_LABELS.map(escapeRegExp).join('|')
|
||||
if (!labelPattern) return ''
|
||||
const match = text.match(
|
||||
new RegExp(`(?:${labelPattern})\\s*[::]\\s*([\\s\\S]*?)(?=\\s*(?:${nextLabelPattern})\\s*[::]|$)`)
|
||||
)
|
||||
return match ? match[1].trim().replace(/[,。;;]$/, '') : ''
|
||||
}
|
||||
|
||||
export function resolveApplicationReason(prompt) {
|
||||
const labeled = resolvePromptField(prompt, ['事由', '申请事由', '出差事由', '原因', '用途'])
|
||||
return labeled || String(prompt || '').trim()
|
||||
}
|
||||
|
||||
export function resolveAttachmentPolicy(expenseTypeCode, amount = 0) {
|
||||
const code = String(expenseTypeCode || '').trim()
|
||||
if (ATTACHMENT_REQUIRED_TYPES.has(code)) {
|
||||
@@ -128,8 +264,15 @@ export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser
|
||||
const documentTypeEntity = resolveEntity(ontology, 'document_type')
|
||||
const workflowStageEntity = resolveEntity(ontology, 'workflow_stage')
|
||||
const attachmentPolicy = resolveAttachmentPolicy(expenseTypeCode, amount.value)
|
||||
const timeRange = resolveApplicationTimeRange(ontology, prompt)
|
||||
|| '待补充'
|
||||
const location = locationEntity?.normalized_value
|
||||
|| locationEntity?.value
|
||||
|| resolvePromptField(prompt, ['地点', '业务地点', '发生地点'])
|
||||
|| '待补充'
|
||||
const reason = resolveApplicationReason(prompt) || '待补充'
|
||||
|
||||
return {
|
||||
const fields = {
|
||||
documentType: documentTypeEntity?.normalized_value || 'expense_application',
|
||||
documentTypeLabel: documentTypeEntity?.value || '费用申请',
|
||||
workflowStage: workflowStageEntity?.normalized_value || 'pre_approval',
|
||||
@@ -138,21 +281,40 @@ export function buildApplicationFieldsFromOntology(ontology, prompt, currentUser
|
||||
expenseTypeLabel: resolveExpenseTypeLabel(expenseTypeCode),
|
||||
amount: amount.value,
|
||||
amountDisplay: amount.value ? `¥${amount.value.toLocaleString('zh-CN')}` : '待补充',
|
||||
timeRange: resolveTimeRangeText(ontology) || '待补充',
|
||||
location: locationEntity?.normalized_value || locationEntity?.value || '待补充',
|
||||
reason: String(prompt || '').trim() || '待补充',
|
||||
timeRange,
|
||||
location,
|
||||
reason,
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充',
|
||||
preApprovalRequired: PRE_APPROVAL_TYPES.has(expenseTypeCode),
|
||||
attachmentPolicy,
|
||||
missingSlots: normalizeMissingSlots(ontology?.missing_slots || [])
|
||||
attachmentPolicy
|
||||
}
|
||||
|
||||
return {
|
||||
...fields,
|
||||
missingSlots: normalizeMissingSlots(ontology?.missing_slots || [], fields)
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeMissingSlots(slots = []) {
|
||||
function hasProvidedValue(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
return !PLACEHOLDER_VALUES.has(normalized)
|
||||
}
|
||||
|
||||
function isSlotAlreadyResolved(slot, fields = {}) {
|
||||
const key = String(slot || '').trim()
|
||||
if (key === 'reason') return hasProvidedValue(fields.reason)
|
||||
if (key === 'time_range' || key === 'time') return hasProvidedValue(fields.timeRange)
|
||||
if (key === 'location') return hasProvidedValue(fields.location)
|
||||
if (key === 'amount') return Number(fields.amount || 0) > 0
|
||||
if (key === 'transport_mode') return hasProvidedValue(fields.transportMode)
|
||||
return false
|
||||
}
|
||||
|
||||
export function normalizeMissingSlots(slots = [], fields = {}) {
|
||||
const normalized = Array.isArray(slots) ? slots : []
|
||||
return normalized.map((item) => ({
|
||||
key: String(item || '').trim(),
|
||||
label: SLOT_LABELS[String(item || '').trim()] || String(item || '').trim()
|
||||
})).filter((item) => item.key)
|
||||
})).filter((item) => item.key && !isSlotAlreadyResolved(item.key, fields))
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const RISK_TEXT_CLASS_BY_LABEL = {
|
||||
|
||||
const ACTION_LINK_CLASS_BY_HREF = {
|
||||
'#confirm-attachment-association': 'markdown-action-link-confirm',
|
||||
'#application-submit': 'markdown-action-link-confirm',
|
||||
'#review-next-step': 'markdown-action-link-next',
|
||||
'#review-quick-edit': 'markdown-action-link-edit',
|
||||
'#review-risk-panel': 'markdown-action-link-risk'
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const DEFAULT_SESSION_TYPE_EXPENSE = 'expense'
|
||||
const DEFAULT_SESSION_TYPE_APPLICATION = 'application'
|
||||
const DEFAULT_SESSION_TYPE_APPROVAL = 'approval'
|
||||
const DEFAULT_SESSION_TYPE_KNOWLEDGE = 'knowledge'
|
||||
|
||||
const DEFAULT_INTENT_LABELS = {
|
||||
@@ -145,7 +147,8 @@ export function inferLocalFlowCandidates(rawText) {
|
||||
}
|
||||
|
||||
export function shouldRequestExpenseSceneSelection(rawText, options = {}) {
|
||||
if (options.sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
const sessionType = options.sessionType || DEFAULT_SESSION_TYPE_EXPENSE
|
||||
if (sessionType !== DEFAULT_SESSION_TYPE_EXPENSE) {
|
||||
return false
|
||||
}
|
||||
if (Number(options.attachmentCount || 0) > 0) {
|
||||
@@ -172,7 +175,8 @@ export function shouldRequestExpenseSceneSelection(rawText, options = {}) {
|
||||
}
|
||||
|
||||
export function shouldRequestExpenseIntentConfirmation(rawText, options = {}) {
|
||||
if (options.sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
const sessionType = options.sessionType || DEFAULT_SESSION_TYPE_EXPENSE
|
||||
if (sessionType !== DEFAULT_SESSION_TYPE_EXPENSE) {
|
||||
return false
|
||||
}
|
||||
if (Number(options.attachmentCount || 0) > 0) {
|
||||
@@ -203,6 +207,12 @@ export function buildLocalIntentPreview(rawText, sessionType = DEFAULT_SESSION_T
|
||||
if (sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
return '初步识别为财务知识问答,正在准备检索范围'
|
||||
}
|
||||
if (sessionType === DEFAULT_SESSION_TYPE_APPLICATION) {
|
||||
return '初步识别为费用申请事项,准备进入申请信息识别'
|
||||
}
|
||||
if (sessionType === DEFAULT_SESSION_TYPE_APPROVAL) {
|
||||
return '初步识别为审核处理事项,准备进入单据查询或风险核对'
|
||||
}
|
||||
|
||||
if (shouldRequestExpenseIntentConfirmation(rawText, { ...options, sessionType })) {
|
||||
return '识别到业务事项描述,但是否发起报销尚不明确,需要先由用户确认'
|
||||
|
||||
@@ -5,6 +5,30 @@ const REQUEST_TYPE_META = {
|
||||
tone: 'travel',
|
||||
secondaryStatusLabel: '行程状态'
|
||||
},
|
||||
travel_application: {
|
||||
label: '差旅费用申请',
|
||||
detailVariant: 'travel',
|
||||
tone: 'travel',
|
||||
secondaryStatusLabel: '申请材料'
|
||||
},
|
||||
expense_application: {
|
||||
label: '费用申请',
|
||||
detailVariant: 'general',
|
||||
tone: 'other',
|
||||
secondaryStatusLabel: '申请材料'
|
||||
},
|
||||
purchase_application: {
|
||||
label: '采购费用申请',
|
||||
detailVariant: 'general',
|
||||
tone: 'office',
|
||||
secondaryStatusLabel: '申请材料'
|
||||
},
|
||||
meeting_application: {
|
||||
label: '会务费用申请',
|
||||
detailVariant: 'general',
|
||||
tone: 'meeting',
|
||||
secondaryStatusLabel: '申请材料'
|
||||
},
|
||||
train_ticket: {
|
||||
label: '火车票',
|
||||
detailVariant: 'travel',
|
||||
|
||||
Reference in New Issue
Block a user