feat: 增强差旅报销审核流程与票据智能推理
优化本体解析和编排器的差旅场景处理能力,完善报销单草稿 保存和费用明细同步逻辑,前端报销创建页面增加行程推理和 票据审核交互,新增助手会话快照工具函数,补充单元测试。
This commit is contained in:
99
web/src/utils/assistantSessionSnapshot.js
Normal file
99
web/src/utils/assistantSessionSnapshot.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const SNAPSHOT_VERSION = 1
|
||||
const STORAGE_PREFIX = 'x-financial:assistant-session'
|
||||
|
||||
export const ASSISTANT_SESSION_SNAPSHOT_EVENT = 'x-financial-assistant-session-snapshot'
|
||||
|
||||
function normalizeSessionType(sessionType) {
|
||||
return String(sessionType || 'expense').trim() || 'expense'
|
||||
}
|
||||
|
||||
function normalizeUserId(userId) {
|
||||
return String(userId || 'anonymous').trim() || 'anonymous'
|
||||
}
|
||||
|
||||
export function resolveAssistantSessionSnapshotKey(userId, sessionType = 'expense') {
|
||||
return `${STORAGE_PREFIX}:${normalizeUserId(userId)}:${normalizeSessionType(sessionType)}`
|
||||
}
|
||||
|
||||
function getStorage() {
|
||||
if (typeof window === 'undefined' || !window.localStorage) {
|
||||
return null
|
||||
}
|
||||
return window.localStorage
|
||||
}
|
||||
|
||||
function emitSnapshotChange(sessionType) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(ASSISTANT_SESSION_SNAPSHOT_EVENT, {
|
||||
detail: { sessionType: normalizeSessionType(sessionType) }
|
||||
}))
|
||||
}
|
||||
|
||||
export function readAssistantSessionSnapshot(userId, sessionType = 'expense') {
|
||||
const storage = getStorage()
|
||||
if (!storage) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const rawValue = storage.getItem(resolveAssistantSessionSnapshotKey(userId, sessionType))
|
||||
if (!rawValue) {
|
||||
return null
|
||||
}
|
||||
const parsed = JSON.parse(rawValue)
|
||||
if (!parsed || parsed.version !== SNAPSHOT_VERSION || !parsed.state) {
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
} catch (error) {
|
||||
console.warn('Failed to read assistant session snapshot:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writeAssistantSessionSnapshot(userId, sessionType = 'expense', state = {}) {
|
||||
const storage = getStorage()
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedSessionType = normalizeSessionType(sessionType)
|
||||
const snapshot = {
|
||||
version: SNAPSHOT_VERSION,
|
||||
updatedAt: Date.now(),
|
||||
userId: normalizeUserId(userId),
|
||||
sessionType: normalizedSessionType,
|
||||
state
|
||||
}
|
||||
|
||||
try {
|
||||
storage.setItem(
|
||||
resolveAssistantSessionSnapshotKey(userId, normalizedSessionType),
|
||||
JSON.stringify(snapshot)
|
||||
)
|
||||
emitSnapshotChange(normalizedSessionType)
|
||||
} catch (error) {
|
||||
console.warn('Failed to write assistant session snapshot:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAssistantSessionSnapshot(userId, sessionType = 'expense') {
|
||||
const storage = getStorage()
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedSessionType = normalizeSessionType(sessionType)
|
||||
try {
|
||||
storage.removeItem(resolveAssistantSessionSnapshotKey(userId, normalizedSessionType))
|
||||
emitSnapshotChange(normalizedSessionType)
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear assistant session snapshot:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAssistantSessionSnapshot(userId, sessionType = 'expense') {
|
||||
return Boolean(readAssistantSessionSnapshot(userId, sessionType)?.state)
|
||||
}
|
||||
@@ -41,6 +41,10 @@ const FLOW_INTENT_KEYWORDS = {
|
||||
explain: ['为什么', '依据', '规则', '怎么']
|
||||
}
|
||||
|
||||
const EXPLICIT_EXPENSE_INTENT_PATTERN = /报销|报账|费用|发票|票据|单据|垫付|报销单|冲销|借款/
|
||||
const NON_EXPENSE_INTENT_PATTERN = /怎么部署|如何部署|部署步骤|技术方案|排期|任务|工单|需求|代码|脚本|服务器配置|运维|实施计划|项目计划|会议纪要|周报|日报|总结/
|
||||
const BUSINESS_ACTIVITY_PATTERN = /去|到|赴|前往|支撑|支持|部署|实施|驻场|出差|拜访|客户|项目|现场|电力|银行|医院|学校|园区|公司|集团|服务器/
|
||||
|
||||
function normalizeCompactText(value) {
|
||||
return String(value || '').trim().replace(/\s+/g, '')
|
||||
}
|
||||
@@ -125,11 +129,74 @@ export function inferLocalFlowCandidates(rawText) {
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRequestExpenseSceneSelection(rawText, options = {}) {
|
||||
if (options.sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
return false
|
||||
}
|
||||
if (Number(options.attachmentCount || 0) > 0) {
|
||||
return false
|
||||
}
|
||||
if (String(options.reviewAction || '').trim()) {
|
||||
return false
|
||||
}
|
||||
if (options.hasSelectedExpenseType) {
|
||||
return false
|
||||
}
|
||||
|
||||
const compact = normalizeCompactText(rawText)
|
||||
if (!compact) {
|
||||
return false
|
||||
}
|
||||
const hasExpenseIntent = /报销|报账|费用|申请/.test(compact)
|
||||
if (!hasExpenseIntent) {
|
||||
return false
|
||||
}
|
||||
|
||||
const candidates = inferLocalFlowCandidates(rawText)
|
||||
return !candidates.expenseType
|
||||
}
|
||||
|
||||
export function shouldRequestExpenseIntentConfirmation(rawText, options = {}) {
|
||||
if (options.sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
return false
|
||||
}
|
||||
if (Number(options.attachmentCount || 0) > 0) {
|
||||
return false
|
||||
}
|
||||
if (String(options.reviewAction || '').trim()) {
|
||||
return false
|
||||
}
|
||||
if (options.hasConfirmedExpenseIntent || options.hasSelectedExpenseType) {
|
||||
return false
|
||||
}
|
||||
|
||||
const compact = normalizeCompactText(rawText)
|
||||
if (!compact || compact.length < 6) {
|
||||
return false
|
||||
}
|
||||
if (EXPLICIT_EXPENSE_INTENT_PATTERN.test(compact)) {
|
||||
return false
|
||||
}
|
||||
if (NON_EXPENSE_INTENT_PATTERN.test(compact)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return BUSINESS_ACTIVITY_PATTERN.test(compact)
|
||||
}
|
||||
|
||||
export function buildLocalIntentPreview(rawText, sessionType = DEFAULT_SESSION_TYPE_EXPENSE, options = {}) {
|
||||
if (sessionType === DEFAULT_SESSION_TYPE_KNOWLEDGE) {
|
||||
return '初步识别为财务知识问答,正在准备检索范围'
|
||||
}
|
||||
|
||||
if (shouldRequestExpenseIntentConfirmation(rawText, { ...options, sessionType })) {
|
||||
return '识别到业务事项描述,但是否发起报销尚不明确,需要先由用户确认'
|
||||
}
|
||||
|
||||
if (shouldRequestExpenseSceneSelection(rawText, { ...options, sessionType })) {
|
||||
return '初步识别为报销申请,但报销场景尚未明确,需要先由用户选择场景'
|
||||
}
|
||||
|
||||
const compact = normalizeCompactText(rawText)
|
||||
const intentLabels = options.intentLabels || DEFAULT_INTENT_LABELS
|
||||
const intentKey = Object.entries(FLOW_INTENT_KEYWORDS).find(([, keywords]) =>
|
||||
|
||||
Reference in New Issue
Block a user