feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和 提取器支持多种格式,增强编排器报销查询的多维度检索,优 化本体规则和用户代理审核消息,前端完善报销创建和审批详 情交互细节,补充单元测试覆盖。
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
isActionableRiskFlag,
|
||||
isRiskSummaryWithRisk,
|
||||
normalizeRiskFlagTone
|
||||
} from '../../utils/riskFlags.js'
|
||||
|
||||
const DOCUMENT_TYPE_LABELS = {
|
||||
flight_itinerary: '机票/航班行程单',
|
||||
train_ticket: '火车/高铁票',
|
||||
@@ -34,6 +40,62 @@ export function normalizeRiskTone(value) {
|
||||
return normalizeTone(value)
|
||||
}
|
||||
|
||||
function resolveFlagTone(flag) {
|
||||
return normalizeRiskFlagTone(flag)
|
||||
}
|
||||
|
||||
function isRiskTone(tone) {
|
||||
return ['medium', 'high'].includes(normalizeText(tone).toLowerCase())
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
return normalizeText(value)
|
||||
}
|
||||
|
||||
function resolveItemRiskFlag(item, claimRiskFlags) {
|
||||
const itemId = normalizeId(item?.id)
|
||||
if (!itemId || !Array.isArray(claimRiskFlags)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return claimRiskFlags.find((flag) => {
|
||||
if (!flag || typeof flag !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isActionableRiskFlag(flag)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const flagItemId = normalizeId(flag.item_id || flag.itemId)
|
||||
const tone = resolveFlagTone(flag)
|
||||
return flagItemId === itemId && isRiskTone(tone)
|
||||
}) || null
|
||||
}
|
||||
|
||||
export function buildItemClaimRiskState(item, claimRiskFlags = []) {
|
||||
const flag = resolveItemRiskFlag(item, claimRiskFlags)
|
||||
if (!flag) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tone = resolveFlagTone(flag)
|
||||
const label = normalizeText(flag.label) || (tone === 'high' ? '高风险' : '中风险')
|
||||
const points = Array.isArray(flag.points)
|
||||
? flag.points.map((point) => normalizeText(point)).filter(Boolean)
|
||||
: []
|
||||
const summary = normalizeText(flag.summary || flag.message || flag.reason)
|
||||
|
||||
return {
|
||||
label,
|
||||
tone,
|
||||
headline: normalizeText(flag.headline || flag.title) || label,
|
||||
summary,
|
||||
points,
|
||||
suggestion: normalizeText(flag.suggestion) || '如业务确需提交,请在附加说明中补充特殊情况原因后继续提交。'
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRiskTagTone(tag) {
|
||||
const normalized = normalizeText(tag).toLowerCase()
|
||||
if (normalized === '#high_risk') return 'high'
|
||||
@@ -99,6 +161,68 @@ function normalizeRuleBasis(value) {
|
||||
return text ? [text] : []
|
||||
}
|
||||
|
||||
function resolveClaimRiskRuleBasis(flag = {}, { risk = '', summary = '', tone = 'medium' } = {}) {
|
||||
const explicitBasis = normalizeRuleBasis(flag.rule_basis || flag.ruleBasis)
|
||||
if (explicitBasis.length) {
|
||||
return explicitBasis
|
||||
}
|
||||
|
||||
const source = normalizeText(flag.source)
|
||||
const label = normalizeText(flag.label || flag.title || flag.name)
|
||||
const corpus = [risk, summary, label].map((item) => normalizeText(item)).join(' ')
|
||||
const basis = []
|
||||
|
||||
if (/高风险|中风险/.test(corpus)) {
|
||||
basis.push(`风险文本已明确标记为${tone === 'high' ? '高风险' : '中风险'}。`)
|
||||
}
|
||||
if (source === 'attachment_analysis' || /附件|票据|OCR|识别|发票/.test(corpus)) {
|
||||
basis.push('附件识别或票据核验未完全通过,系统将该项同步为单据风险。')
|
||||
}
|
||||
if (/直属领导|审批链|审批人缺失|审批人信息|补充分配|分配/.test(corpus)) {
|
||||
basis.push('审批链校验未匹配到完整审批人信息,因此按中风险提醒。')
|
||||
}
|
||||
if (/金额|超标|阈值|住宿标准|报销标准|标准/.test(corpus)) {
|
||||
basis.push('金额或标准核算命中制度阈值,需要补充说明或人工复核。')
|
||||
}
|
||||
if (/历史|近\s*\d+\s*天|重复|多次/.test(corpus)) {
|
||||
basis.push('历史报销风险次数达到预警条件,系统提示审批人重点关注。')
|
||||
}
|
||||
if (/缺失|缺少|未识别|待补充|不一致|不匹配/.test(corpus)) {
|
||||
basis.push('单据、附件或审批信息存在缺失、不一致或待补充项。')
|
||||
}
|
||||
if (summary) {
|
||||
basis.push(`风险汇总:${summary}`)
|
||||
}
|
||||
|
||||
return uniqueTexts(basis.length ? basis : [`系统预审根据“${label || '单据风险'}”将该项列为${tone === 'high' ? '高风险' : '中风险'}。`])
|
||||
}
|
||||
|
||||
function resolveClaimRiskSuggestion(flag = {}, { risk = '', summary = '' } = {}) {
|
||||
const explicitSuggestion = normalizeText(flag.suggestion)
|
||||
if (explicitSuggestion) {
|
||||
return explicitSuggestion
|
||||
}
|
||||
|
||||
const corpus = [risk, summary, flag.label, flag.title].map((item) => normalizeText(item)).join(' ')
|
||||
if (/直属领导|审批链|审批人缺失|审批人信息|补充分配|分配/.test(corpus)) {
|
||||
return '请先核对员工档案中的直属领导和审批链配置;如果信息无误,可由审批环节人工补充分配说明。'
|
||||
}
|
||||
if (/金额|超标|阈值|住宿标准|报销标准|标准/.test(corpus)) {
|
||||
return '请核对金额、天数、地点和职级标准;如确需超标,请在附加说明中写清楚业务原因和佐证材料。'
|
||||
}
|
||||
if (/附件|票据|OCR|识别|发票/.test(corpus)) {
|
||||
return '请打开对应费用明细的附件预览,核对票据类型、金额、日期和说明;识别有误时先修正明细或重新上传附件。'
|
||||
}
|
||||
if (/历史|近\s*\d+\s*天|重复|多次/.test(corpus)) {
|
||||
return '请核对近期同类报销记录,必要时补充本次费用与历史单据不同的业务背景。'
|
||||
}
|
||||
if (/缺失|缺少|未识别|待补充|不一致|不匹配/.test(corpus)) {
|
||||
return '请按风险点补齐缺失信息,并核对费用明细与附件内容是否一致。'
|
||||
}
|
||||
|
||||
return '请先核对上方触发原因;如属于真实业务例外,在附加说明中写清楚原因和佐证后再继续流转。'
|
||||
}
|
||||
|
||||
export function buildAttachmentInsightViewModel(metadata, item = {}) {
|
||||
if (!metadata) {
|
||||
return null
|
||||
@@ -245,6 +369,7 @@ export function buildAttachmentRiskCards({
|
||||
attachmentMetaByItemId = {},
|
||||
claimRiskFlags = []
|
||||
} = {}) {
|
||||
const attachmentRiskItemIds = new Set()
|
||||
const attachmentCards = expenseItems.flatMap((item, index) => {
|
||||
if (!item?.invoiceId) {
|
||||
return []
|
||||
@@ -257,6 +382,10 @@ export function buildAttachmentRiskCards({
|
||||
if (!analysis || !['medium', 'high'].includes(tone)) {
|
||||
return []
|
||||
}
|
||||
const itemId = normalizeId(item.id)
|
||||
if (itemId) {
|
||||
attachmentRiskItemIds.add(itemId)
|
||||
}
|
||||
|
||||
const points = Array.isArray(analysis.points) && analysis.points.length
|
||||
? analysis.points
|
||||
@@ -276,6 +405,10 @@ export function buildAttachmentRiskCards({
|
||||
}
|
||||
|
||||
if (!flag || typeof flag !== 'object') {
|
||||
if (!isActionableRiskFlag(flag)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const risk = normalizeText(flag)
|
||||
return risk
|
||||
? [withRiskTags({
|
||||
@@ -285,29 +418,41 @@ export function buildAttachmentRiskCards({
|
||||
title: '单据风险提示',
|
||||
risk,
|
||||
summary: '',
|
||||
ruleBasis: ['系统预审规则命中该风险提示。'],
|
||||
suggestion: '请结合业务背景补充说明或调整单据后再提交。'
|
||||
ruleBasis: resolveClaimRiskRuleBasis({}, { risk, tone: 'medium' }),
|
||||
suggestion: resolveClaimRiskSuggestion({}, { risk })
|
||||
})]
|
||||
: []
|
||||
}
|
||||
|
||||
const tone = normalizeTone(flag.severity)
|
||||
if (!['medium', 'high'].includes(tone)) {
|
||||
if (!isActionableRiskFlag(flag)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const source = normalizeText(flag.source)
|
||||
const flagItemId = normalizeId(flag.item_id || flag.itemId)
|
||||
if (source === 'attachment_analysis' && flagItemId && attachmentRiskItemIds.has(flagItemId)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tone = resolveFlagTone(flag)
|
||||
if (!isRiskTone(tone)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const flagPoints = Array.isArray(flag.points)
|
||||
? flag.points.map((point) => normalizeText(point)).filter(Boolean)
|
||||
: []
|
||||
const primaryRisk = normalizeText(flag.message || flag.reason || flag.summary)
|
||||
const fallbackRisk = normalizeText(flag.description || flag.detail || flag.title || flag.label || flag.name)
|
||||
const risks = flagPoints.length
|
||||
? flagPoints
|
||||
: [normalizeText(flag.message || flag.reason || flag.summary)].filter(Boolean)
|
||||
const summary = normalizeText(flag.summary)
|
||||
const ruleBasis = uniqueTexts([
|
||||
...normalizeRuleBasis(flag.rule_basis || flag.ruleBasis),
|
||||
summary ? `风险汇总:${summary}` : '',
|
||||
'系统预审规则命中该风险提示。'
|
||||
])
|
||||
: [primaryRisk || fallbackRisk].filter(Boolean)
|
||||
const summary = normalizeText(flag.summary || flag.message || flag.reason)
|
||||
const ruleBasis = resolveClaimRiskRuleBasis(flag, {
|
||||
risk: risks[0] || primaryRisk || fallbackRisk,
|
||||
summary,
|
||||
tone
|
||||
})
|
||||
|
||||
return risks.map((risk, pointIndex) => withRiskTags({
|
||||
id: `claim-risk-${index}-${pointIndex}`,
|
||||
@@ -317,7 +462,7 @@ export function buildAttachmentRiskCards({
|
||||
risk,
|
||||
summary,
|
||||
ruleBasis,
|
||||
suggestion: normalizeText(flag.suggestion) || '请结合业务背景补充说明或调整单据后再提交。'
|
||||
suggestion: resolveClaimRiskSuggestion(flag, { risk, summary })
|
||||
}))
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -328,6 +473,52 @@ export function buildAttachmentRiskCards({
|
||||
return [...attachmentCards, ...claimCards]
|
||||
}
|
||||
|
||||
function isNoRiskSummary(value) {
|
||||
return !isRiskSummaryWithRisk(value)
|
||||
}
|
||||
|
||||
function resolveClaimSummaryTone(request) {
|
||||
const explicitTone = normalizeText(request?.riskTone || request?.risk_tone || request?.severity)
|
||||
if (explicitTone) {
|
||||
return normalizeTone(explicitTone)
|
||||
}
|
||||
|
||||
const summary = normalizeText(request?.riskSummary || request?.risk || request?.riskText)
|
||||
if (/高风险|重大风险|严重|超标|违规/.test(summary)) {
|
||||
return 'high'
|
||||
}
|
||||
if (/中风险|待关注|待复核|提醒|异常|风险/.test(summary)) {
|
||||
return 'medium'
|
||||
}
|
||||
return 'medium'
|
||||
}
|
||||
|
||||
export function buildClaimSummaryRiskCards(request = {}) {
|
||||
const summary = normalizeText(request?.riskSummary || request?.risk || request?.riskText)
|
||||
if (isNoRiskSummary(summary)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tone = resolveClaimSummaryTone(request)
|
||||
if (!isRiskTone(tone)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [withRiskTags({
|
||||
id: 'claim-risk-summary',
|
||||
tone,
|
||||
label: tone === 'high' ? '高风险' : '中风险',
|
||||
title: '单据风险提示',
|
||||
risk: summary,
|
||||
summary,
|
||||
ruleBasis: resolveClaimRiskRuleBasis(
|
||||
{ source: 'risk_summary', message: summary, severity: tone },
|
||||
{ risk: summary, summary, tone }
|
||||
),
|
||||
suggestion: resolveClaimRiskSuggestion({ source: 'risk_summary' }, { risk: summary, summary })
|
||||
})]
|
||||
}
|
||||
|
||||
export function buildAiAdviceViewModel({ completionItems = [], riskCards = [] } = {}) {
|
||||
const normalizedCompletionItems = completionItems.map((item) => normalizeText(item)).filter(Boolean)
|
||||
const normalizedRiskCards = riskCards.filter(Boolean)
|
||||
|
||||
Reference in New Issue
Block a user