feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -0,0 +1,21 @@
const INTERNAL_MESSAGE_META_PATTERNS = [
/^Agent\s*[:]/i,
/^权限\s*[:]/,
/^Run\s*[:]/i,
/^工具\s*[:]/,
/^能力\s*[:]/,
/^Capability\s*[:]/i,
/(?:^|[_\s-])user[_\s-]?agent(?:$|[_\s-])/i,
/\bdraft[_\s-]?write\b/i
]
export function isInternalMessageMeta(item) {
const normalized = String(item || '').trim()
return !normalized || INTERNAL_MESSAGE_META_PATTERNS.some((pattern) => pattern.test(normalized))
}
export function filterVisibleMessageMeta(meta = []) {
return (Array.isArray(meta) ? meta : [])
.map((item) => String(item || '').trim())
.filter((item) => item && !isInternalMessageMeta(item))
}

View File

@@ -31,7 +31,17 @@ const SESSION_SCOPE_CONFIG = {
const SESSION_SCOPE_TYPES = Object.keys(SESSION_SCOPE_CONFIG)
const APPLICATION_PATTERN =
/费用申请|发起申请|申请单|事前申请|事前审批|前置审批|出差申请|采购申请|用款申请|预算申请|申请材料|材料清单|先申请|立项申请/
/费用申请|发起申请|申请单|事前申请|事前审批|前置审批|出差申请|申请出差|差旅申请|申请差旅|采购申请|用款申请|预算申请|申请材料|材料清单|先申请|立项申请/
const APPLICATION_PLANNING_PATTERN =
/计划|安排|准备|需要|打算|预计|申请|发起|提交|提出|先走|先办|要去|将要|下周|下月|明天|后天|近期|月底|去|到|赴|前往|参加/
const APPLICATION_BUSINESS_PATTERN =
/出差|差旅|客户现场|现场|客户|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|会务|驻场|上线|验收|采购|购置|用款|立项/
const APPLICATION_FUTURE_OR_DURATION_PATTERN =
/明天|后天|下周|下月|近期|月底|预计|计划|安排|准备|将要|[0-9]+天|[一二两三四五六七八九十]+天/
const APPLICATION_ROUTE_PATTERN =
/(?:去|到|赴|前往)[^,。;;?!\n]{0,24}(?:出差|差旅|客户|现场|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|驻场|上线|验收)|(?:出差|差旅)[^,。;;?!\n]{0,24}(?:[0-9]+天|[一二两三四五六七八九十]+天|客户|现场|项目|部署|实施|支撑|支持|协助|拜访|调研|培训|会议|驻场|上线|验收)/
const COMPLETED_EXPENSE_PATTERN =
/已经|已|昨天|前天|上周|上月|去年|花了|花销|消费|垫付|支付|付了|买了|采购了|招待了|发生了/
const EXPENSE_PATTERN =
/报销|报销单|票据|发票|火车票|高铁票|机票|飞机票|的士票|出租车|网约车|酒店票|住宿票|住宿单据|保存草稿|草稿|费用明细|归集|上传.*票|关联单据|继续下一步/
const APPROVAL_PATTERN =
@@ -52,6 +62,34 @@ function normalizeText(rawText) {
.toLowerCase()
}
export function hasReimbursementIntentSignal(rawText) {
return EXPENSE_PATTERN.test(normalizeText(rawText))
}
export function hasExpenseApplicationIntentSignal(rawText) {
const text = normalizeText(rawText)
if (!text) {
return false
}
if (APPLICATION_PATTERN.test(text)) {
return true
}
if (hasReimbursementIntentSignal(text) || COMPLETED_EXPENSE_PATTERN.test(text)) {
return false
}
if (KNOWLEDGE_PATTERN.test(text) && !EXPENSE_OPERATION_PATTERN.test(text)) {
return false
}
const hasBusinessSignal = APPLICATION_BUSINESS_PATTERN.test(text)
const planningScore = APPLICATION_PLANNING_PATTERN.test(text) ? 1 : 0
const timingScore = APPLICATION_FUTURE_OR_DURATION_PATTERN.test(text) ? 1 : 0
const routeScore = APPLICATION_ROUTE_PATTERN.test(text) ? 2 : 0
return hasBusinessSignal && planningScore + timingScore + routeScore >= 2
}
function resolveScopeConfig(sessionType) {
return SESSION_SCOPE_CONFIG[normalizeSessionType(sessionType)] || SESSION_SCOPE_CONFIG[ASSISTANT_SCOPE_SESSION_EXPENSE]
}
@@ -62,7 +100,7 @@ export function inferAssistantScopeTarget(rawText, options = {}) {
return ''
}
const applicationMatched = APPLICATION_PATTERN.test(text)
const applicationMatched = hasExpenseApplicationIntentSignal(text)
const expenseMatched = EXPENSE_PATTERN.test(text)
const approvalMatched = APPROVAL_PATTERN.test(text)
const knowledgeMatched = KNOWLEDGE_PATTERN.test(text)

View File

@@ -0,0 +1,90 @@
import {
finishSession,
finishSessionOnUnload
} from '../services/auth.js'
const AUTH_SESSION_ID_KEY = 'x-financial-auth-session-id'
const AUTH_LAST_ACTIVITY_KEY = 'x-financial-auth-last-activity'
const AUTH_ACTIVITY_COUNT_KEY = 'x-financial-auth-activity-count'
function readStoredSessionId() {
if (typeof window === 'undefined') {
return ''
}
return String(window.sessionStorage.getItem(AUTH_SESSION_ID_KEY) || '').trim()
}
function readLastActivityAt() {
if (typeof window === 'undefined') {
return 0
}
return Number(window.sessionStorage.getItem(AUTH_LAST_ACTIVITY_KEY) || 0)
}
function readActivityEventCount() {
if (typeof window === 'undefined') {
return 0
}
const value = Number(window.sessionStorage.getItem(AUTH_ACTIVITY_COUNT_KEY) || 0)
return Number.isFinite(value) && value > 0 ? Math.round(value) : 0
}
function buildSessionFinishPayload(reason) {
const lastActivityAt = readLastActivityAt()
const pagePath = typeof window === 'undefined'
? ''
: `${window.location.pathname}${window.location.search}`
return {
reason,
lastActivityAt: lastActivityAt ? new Date(lastActivityAt).toISOString() : null,
activityEventCount: readActivityEventCount(),
pagePath
}
}
export function persistAuthSessionMetrics(sessionId) {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.setItem(AUTH_SESSION_ID_KEY, String(sessionId || '').trim())
window.sessionStorage.setItem(AUTH_ACTIVITY_COUNT_KEY, '0')
}
export function clearAuthSessionMetrics() {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.removeItem(AUTH_SESSION_ID_KEY)
window.sessionStorage.removeItem(AUTH_ACTIVITY_COUNT_KEY)
}
export function incrementAuthActivityCount() {
if (typeof window === 'undefined') {
return
}
window.sessionStorage.setItem(AUTH_ACTIVITY_COUNT_KEY, String(readActivityEventCount() + 1))
}
export function finalizeAuthSession(reason, options = {}) {
const sessionId = readStoredSessionId()
if (!sessionId) {
return
}
const payload = buildSessionFinishPayload(reason)
if (options.unload) {
finishSessionOnUnload(sessionId, payload)
return
}
finishSession(sessionId, payload).catch((error) => {
console.warn('Failed to finish auth session:', error)
})
}

View File

@@ -46,6 +46,41 @@ const RADAR_COLORS = [
'#db2777'
]
const FINANCIAL_RADAR_CODES = [
'expense_intensity',
'application_rhythm',
'travel_entertainment',
'material_completeness',
'process_pressure'
]
const GOVERNANCE_RADAR_CODES = [
'ai_collaboration',
'approval_efficiency',
'approval_control'
]
export const USER_PROFILE_RADAR_VIEW_OPTIONS = [
{
value: 'financial_risk',
label: '财务风险视角',
shortLabel: '财务风险',
description: '费用、材料和流程相关维度'
},
{
value: 'collaboration_governance',
label: '协作治理视角',
shortLabel: '协作治理',
description: 'AI 协作和审批治理维度'
},
{
value: 'all_behavior',
label: '全部行为视角',
shortLabel: '全部行为',
description: '展示全部可用画像维度'
}
]
const TAG_ACCENT_COUNT = 8
const SOURCE_LABELS = {
@@ -89,7 +124,7 @@ const JOB_TYPE_LABELS = {
llm_wiki_sync: '知识库归纳同步',
employee_behavior_profile_scan: '用户画像测算',
workbench_on_demand: '工作台画像测算',
global_risk_scan: '全局风险巡检',
global_risk_scan: '财务风险图谱巡检',
weekly_expense_report: '周费用报告'
}
@@ -98,9 +133,7 @@ export function buildUserProfileMetricCards(profile, runs = [], currentUser = {}
const aiMetrics = metricsOf(index.ai_usage)
const userRuns = filterRunsByCurrentUser(runs, currentUser)
const windowedUserRuns = filterRunsByProfileWindow(userRuns, profile)
const durationMs = hasProfileDurationMetric(aiMetrics)
? resolveNumber(aiMetrics.ai_run_duration_ms)
: sumRunDurationMs(windowedUserRuns)
const durationMs = resolveUsageDurationMs(aiMetrics, windowedUserRuns)
const durationDisplay = formatDurationMetric(durationMs)
const commonAgent = resolveCommonAgent(windowedUserRuns)
const tokenCount = resolveNumber(aiMetrics.exact_token_count) || resolveNumber(aiMetrics.estimated_token_count)
@@ -113,7 +146,7 @@ export function buildUserProfileMetricCards(profile, runs = [], currentUser = {}
label: '使用时长',
value: durationDisplay.value,
unit: durationDisplay.unit,
hint: `${resolveWindowDays(profile)}天智能体运行累计`,
hint: resolveUsageDurationHint(aiMetrics, profile),
icon: 'mdi mdi-timer-sand',
tone: 'primary'
},
@@ -157,10 +190,12 @@ export function normalizeUserProfileTags(profile, limit = 8) {
code: normalizeText(tag.code || tag.label),
label: normalizeText(tag.label),
displayLabel: normalizeText(tag.display_label || tag.displayLabel || tag.label),
category: normalizeCode(tag.category),
tone: resolveTagTone(tag),
score: clampScore(tag.score),
reason: normalizeText(tag.reason) || '画像算法已识别该行为特征。',
confidence: resolveNumber(tag.confidence)
confidence: resolveNumber(tag.confidence),
radarDimensions: normalizeRadarDimensions(tag)
}))
.filter((tag) => tag.code && tag.displayLabel)
.sort((left, right) => right.score - left.score)
@@ -194,6 +229,55 @@ export function normalizeUserProfileRadarDimensions(profile) {
)
}
export function filterUserProfileRadarDimensions(dimensions, viewKey) {
const items = Array.isArray(dimensions) ? dimensions : []
const codes = resolveRadarViewCodes(viewKey)
if (!codes.length) {
return items
}
const filtered = items.filter((item) => codes.includes(normalizeCode(item?.code)))
return filtered.length ? filtered : items
}
export function filterUserProfileTagsByRadarView(tags, viewKey) {
const items = Array.isArray(tags) ? tags : []
const codes = resolveRadarViewCodes(viewKey)
if (!codes.length) {
return items
}
return items.filter((tag) => {
const dimensions = Array.isArray(tag?.radarDimensions) ? tag.radarDimensions : []
if (dimensions.some((code) => codes.includes(normalizeCode(code)))) {
return true
}
return resolveFallbackTagRadarCodes(tag).some((code) => codes.includes(code))
})
}
export function resolveUserProfileDefaultRadarView(profile) {
const profileTypes = new Set(
(Array.isArray(profile?.profiles) ? profile.profiles : [])
.map((item) => normalizeCode(item?.profile_type))
.filter(Boolean)
)
if (profileTypes.has('expense') || profileTypes.has('process_quality')) {
return 'financial_risk'
}
if (profileTypes.has('ai_usage') || profileTypes.has('approval')) {
return 'collaboration_governance'
}
const dimensions = normalizeUserProfileRadarDimensions(profile)
const financialScore = sumRadarScores(dimensions, FINANCIAL_RADAR_CODES)
const governanceScore = sumRadarScores(dimensions, GOVERNANCE_RADAR_CODES)
if (financialScore > 0 || governanceScore > 0) {
return financialScore >= governanceScore ? 'financial_risk' : 'collaboration_governance'
}
return 'all_behavior'
}
export function buildProfileOperationsFromAgentRuns(runs, currentUser, limit = 5) {
const identities = resolveCurrentUserIdentities(currentUser)
return (Array.isArray(runs) ? runs : [])
@@ -227,8 +311,69 @@ function metricsOf(profile) {
return profile?.metrics && typeof profile.metrics === 'object' ? profile.metrics : {}
}
function resolveRadarViewCodes(viewKey) {
if (viewKey === 'financial_risk') {
return FINANCIAL_RADAR_CODES
}
if (viewKey === 'collaboration_governance') {
return GOVERNANCE_RADAR_CODES
}
return []
}
function resolveFallbackTagRadarCodes(tag) {
const category = normalizeCode(tag?.category)
if (['expense', 'travel', 'entertainment', 'process'].includes(category)) {
return FINANCIAL_RADAR_CODES
}
if (['ai', 'approval'].includes(category)) {
return GOVERNANCE_RADAR_CODES
}
return []
}
function normalizeRadarDimensions(tag) {
const dimensions = Array.isArray(tag?.radar_dimensions)
? tag.radar_dimensions
: Array.isArray(tag?.radarDimensions)
? tag.radarDimensions
: []
return dimensions.map((item) => normalizeCode(item)).filter(Boolean)
}
function sumRadarScores(dimensions, codes) {
const codeSet = new Set(codes)
return (Array.isArray(dimensions) ? dimensions : [])
.filter((item) => codeSet.has(normalizeCode(item?.code)))
.reduce((total, item) => total + clampScore(item?.score), 0)
}
function hasProfileDurationMetric(metrics) {
return Object.prototype.hasOwnProperty.call(metrics || {}, 'ai_run_duration_ms')
return (
Object.prototype.hasOwnProperty.call(metrics || {}, 'usage_duration_ms')
|| Object.prototype.hasOwnProperty.call(metrics || {}, 'online_duration_ms')
|| Object.prototype.hasOwnProperty.call(metrics || {}, 'ai_run_duration_ms')
)
}
function resolveUsageDurationMs(metrics, fallbackRuns) {
if (!hasProfileDurationMetric(metrics)) {
return sumRunDurationMs(fallbackRuns)
}
return resolveNumber(metrics.usage_duration_ms)
|| resolveNumber(metrics.online_duration_ms)
|| resolveNumber(metrics.ai_run_duration_ms)
}
function resolveUsageDurationHint(metrics, profile) {
const days = resolveWindowDays(profile)
if (normalizeCode(metrics?.usage_duration_mode) === 'online_session') {
return `${days}天在线会话累计`
}
if (normalizeCode(metrics?.usage_duration_mode) === 'agent_run_fallback') {
return `${days}天智能体运行累计`
}
return `${days}天使用行为累计`
}
function filterRunsByCurrentUser(runs, currentUser) {

View File

@@ -104,3 +104,54 @@ export function buildApplicationDetailFactItems(request = {}) {
return rows.filter((row) => isProvided(row.value))
}
export function buildRelatedApplicationFactItems(request = {}) {
const related = request.relatedApplication || {}
const rows = [
{
key: 'claim_no',
label: '关联单据单号',
value: related.claimNo,
highlight: true
},
{
key: 'content',
label: '申请内容',
value: related.content
},
{
key: 'days',
label: '申请天数',
value: related.days
},
{
key: 'reason',
label: '申请事由',
value: related.reason
},
{
key: 'location',
label: '申请地点',
value: related.location
},
{
key: 'time',
label: '申请时间',
value: related.time
},
{
key: 'amount',
label: '预计金额',
value: related.amountLabel,
highlight: true,
emphasis: true
},
{
key: 'transport_mode',
label: '出行方式',
value: related.transportMode
}
]
return rows.filter((row) => isProvided(row.value))
}

View File

@@ -186,8 +186,10 @@ export function expandApplicationTimeWithDays(timeText, days = 0) {
if (!startDate) return normalizedTime
const endDate = new Date(startDate.getTime())
endDate.setUTCDate(endDate.getUTCDate() + dayCount)
return `${formatApplicationDate(startDate)}${formatApplicationDate(endDate)}`
endDate.setUTCDate(endDate.getUTCDate() + Math.max(dayCount - 1, 0))
const startText = formatApplicationDate(startDate)
const endText = formatApplicationDate(endDate)
return startText === endText ? startText : `${startText}${endText}`
}
function normalizeApplicationTimeCandidate(value) {

View File

@@ -1,4 +1,5 @@
import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
import { getTodayDateValue } from './workbenchComposerDate.js'
const APPLICATION_SESSION_TYPE = 'application'
const APPLICATION_CREATE_PATTERN = /申请|事前|前置|预算|出差|差旅|采购|会务|会议|培训|办公用品|交通|住宿/
@@ -54,11 +55,11 @@ function formatIsoDate(date) {
}
function buildEndDateFromDays(startText, daysText = '') {
const days = Number(String(daysText || '').replace(/[^\d]/g, ''))
const days = parseApplicationDaysValue(daysText)
const start = parseIsoDate(startText)
if (!days || !start) return ''
const end = new Date(start.getTime())
end.setUTCDate(end.getUTCDate() + days)
end.setUTCDate(end.getUTCDate() + Math.max(days - 1, 0))
return formatIsoDate(end)
}
@@ -69,7 +70,16 @@ function resolveDaysFromDateRange(rangeText) {
const end = parseIsoDate(match[2])
if (!start || !end) return ''
const diffDays = Math.round((end.getTime() - start.getTime()) / 86400000)
return diffDays > 0 ? `${diffDays}` : '1天'
return diffDays >= 0 ? `${diffDays + 1}` : ''
}
function resolvePreviewToday(options = {}) {
const explicitToday = String(options.today || options.currentDate || '').trim()
if (parseIsoDate(explicitToday)) return normalizeDateText(explicitToday)
if (options.now instanceof Date && !Number.isNaN(options.now.getTime())) {
return getTodayDateValue(options.now)
}
return getTodayDateValue()
}
function resolveApplicationType(text) {
@@ -106,10 +116,35 @@ function resolveCurrentUserGrade(currentUser = {}) {
function parseApplicationDaysValue(value) {
const match = String(value || '').match(/\d+/)
const days = match ? Number(match[0]) : 0
const days = match ? Number(match[0]) : parseChineseNumber(value)
return Number.isFinite(days) && days > 0 ? Math.max(1, Math.floor(days)) : 0
}
function parseChineseNumber(value) {
const digits = {
: 1,
: 2,
: 2,
: 3,
: 4,
: 5,
: 6,
: 7,
: 8,
: 9
}
const text = String(value || '').match(/[一二两三四五六七八九十]{1,3}/)?.[0] || ''
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
}
function parseMoneyNumber(value) {
const normalized = String(value ?? '').replace(/[^\d.-]/g, '')
const amount = Number(normalized)
@@ -161,7 +196,7 @@ function resolveApplicationDays(text) {
return value ? `${value}` : ''
}
function resolveApplicationTime(text, daysText = '') {
function resolveApplicationTime(text, daysText = '', options = {}) {
const range = text.match(
/(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})\s*(?:至|到|~|—||--)\s*(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u
)
@@ -176,7 +211,18 @@ function resolveApplicationTime(text, daysText = '') {
if (!single) return ''
const normalized = normalizeDateText(single)
const endDate = buildEndDateFromDays(normalized, daysText)
return endDate ? `${normalized}${endDate}` : normalized
return endDate && endDate !== normalized ? `${normalized}${endDate}` : normalized
}
function resolveApplicationTimeWithDefault(text, daysText = '', options = {}) {
const resolvedTime = resolveApplicationTime(text, daysText)
if (resolvedTime || !parseApplicationDaysValue(daysText)) {
return resolvedTime
}
const startDate = resolvePreviewToday(options)
const endDate = buildEndDateFromDays(startDate, daysText)
return endDate && endDate !== startDate ? `${startDate}${endDate}` : startDate
}
function resolveApplicationLocation(text) {
@@ -449,10 +495,10 @@ export function buildApplicationPreviewSubmitText(preview = {}) {
].join('\n')
}
export function buildLocalApplicationPreview(rawText, currentUser = {}) {
export function buildLocalApplicationPreview(rawText, currentUser = {}, options = {}) {
const sourceText = String(rawText || '').trim()
const explicitDays = resolveApplicationDays(sourceText)
const time = resolveApplicationTime(sourceText, explicitDays)
const time = resolveApplicationTimeWithDefault(sourceText, explicitDays, options)
const days = explicitDays || resolveDaysFromDateRange(time)
const location = resolveApplicationLocation(sourceText)
const fields = {

View File

@@ -2,8 +2,8 @@
export const HERMES_SIMPLE_TASKS = [
{
id: 'global_risk_scan',
label: '风险每日巡检',
hint: '扫描报销、付款等风险信号',
label: '财务风险图谱巡检',
hint: '扫描单据、票据、审批链和画像异常',
frequency: 'daily',
frequencyLabel: '每天'
},

View File

@@ -0,0 +1,53 @@
const SESSION_TYPE_APPLICATION = 'application'
const SESSION_TYPE_APPROVAL = 'approval'
const SESSION_TYPE_BUDGET = 'budget'
const SESSION_TYPE_EXPENSE = 'expense'
const SESSION_TYPE_KNOWLEDGE = 'knowledge'
const DEFAULT_WORKBENCH_ENTRY = {
source: 'workbench',
sessionType: SESSION_TYPE_EXPENSE
}
const CAPABILITY_ASSISTANT_ENTRIES = {
'expense-application': {
source: 'application',
sessionType: SESSION_TYPE_APPLICATION
},
'quick-reimbursement': DEFAULT_WORKBENCH_ENTRY,
'budget-planning': {
source: 'budget',
sessionType: SESSION_TYPE_BUDGET
},
'quick-approval': {
source: 'workbench',
sessionType: SESSION_TYPE_APPROVAL
},
'finance-analysis': {
source: 'budget',
sessionType: SESSION_TYPE_BUDGET
},
'company-policy': {
source: 'workbench',
sessionType: SESSION_TYPE_KNOWLEDGE
}
}
export function resolveWorkbenchCapabilityAssistantEntry(item = {}) {
const key = String(item?.key || '').trim()
const entry = CAPABILITY_ASSISTANT_ENTRIES[key] || DEFAULT_WORKBENCH_ENTRY
return { ...entry }
}
export function buildWorkbenchCapabilityAssistantPayload(item = {}, basePayload = {}) {
const entry = resolveWorkbenchCapabilityAssistantEntry(item)
return {
...basePayload,
...entry,
prompt: String(basePayload.prompt || '').trim(),
files: Array.isArray(basePayload.files) ? basePayload.files : [],
conversation: null
}
}

View File

@@ -0,0 +1,105 @@
import {
ASSISTANT_SCOPE_SESSION_APPLICATION,
ASSISTANT_SCOPE_SESSION_EXPENSE,
ASSISTANT_SCOPE_SESSION_KNOWLEDGE,
hasExpenseApplicationIntentSignal,
hasReimbursementIntentSignal,
inferAssistantScopeTarget
} from './assistantSessionScope.js'
const ASSISTANT_SCOPE_SESSION_BUDGET = 'budget'
function normalizeText(rawText) {
return String(rawText || '')
.replace(/\s+/g, '')
.toLowerCase()
}
function hasEntity(ontology, type, normalizedValue = '') {
const expectedType = String(type || '').trim()
const expectedValue = String(normalizedValue || '').trim()
return Array.isArray(ontology?.entities) && ontology.entities.some((item) => {
if (String(item?.type || '').trim() !== expectedType) {
return false
}
if (!expectedValue) {
return true
}
return String(item?.normalized_value || item?.value || '').trim() === expectedValue
})
}
function hasTravelExpenseType(ontology) {
return hasEntity(ontology, 'expense_type', 'travel')
}
function hasApplicationDocumentEntity(ontology) {
return hasEntity(ontology, 'document_type', 'expense_application')
|| hasEntity(ontology, 'workflow_stage', 'pre_approval')
}
export function buildWorkbenchIntentOntologyContext({ currentUser = {}, files = [] } = {}) {
return {
entry_source: 'workbench',
session_type: '',
attachment_count: Array.isArray(files) ? files.length : 0,
role_codes: Array.isArray(currentUser.roleCodes) ? currentUser.roleCodes : [],
name: currentUser.name || currentUser.username || '',
username: currentUser.username || '',
department: currentUser.department || currentUser.departmentName || '',
grade: currentUser.grade || ''
}
}
export function resolveWorkbenchSessionTypeFromOntology(ontology, rawText, fallbackSessionType = '') {
const text = normalizeText(rawText)
const fallback = String(fallbackSessionType || '').trim()
const scenario = String(ontology?.scenario || '').trim()
const intent = String(ontology?.intent || '').trim()
const reimbursementSignal = hasReimbursementIntentSignal(text)
const applicationSignal = hasExpenseApplicationIntentSignal(text)
if (!text) {
return fallback
}
if (hasApplicationDocumentEntity(ontology)) {
return ASSISTANT_SCOPE_SESSION_APPLICATION
}
if (
!reimbursementSignal
&& (
applicationSignal
|| (
scenario === 'expense'
&& intent === 'draft'
&& hasTravelExpenseType(ontology)
&& applicationSignal
)
|| (scenario === 'budget' && hasTravelExpenseType(ontology) && applicationSignal)
)
) {
return ASSISTANT_SCOPE_SESSION_APPLICATION
}
if (reimbursementSignal && scenario === 'expense' && intent === 'draft') {
return ASSISTANT_SCOPE_SESSION_EXPENSE
}
if (scenario === 'knowledge') {
return ASSISTANT_SCOPE_SESSION_KNOWLEDGE
}
if (scenario === 'budget') {
return ASSISTANT_SCOPE_SESSION_BUDGET
}
if (scenario === 'expense' && intent === 'draft') {
return ASSISTANT_SCOPE_SESSION_EXPENSE
}
return fallback || inferAssistantScopeTarget(rawText)
}
export function resolveWorkbenchSessionTypeFallback(rawText, options = {}) {
return inferAssistantScopeTarget(rawText, options)
}

View File

@@ -0,0 +1,74 @@
function normalizeDateValue(value) {
return String(value || '').trim()
}
const ISO_DATE_PATTERN = '20\\d{2}-\\d{1,2}-\\d{1,2}'
const DATE_RANGE_PATTERN = `${ISO_DATE_PATTERN}(?:\\s*\\u81f3\\s*${ISO_DATE_PATTERN})?`
const LABELED_DATE_PREFIX_RE = new RegExp(
`^(?:(?:\\u4e1a\\u52a1)?\\u53d1\\u751f\\u65f6\\u95f4|\\u65e5\\u671f|\\u65f6\\u95f4)\\s*[:\\uFF1A]\\s*${DATE_RANGE_PATTERN}[\\uFF0C,\\u3002\\s]*`,
'u'
)
const RAW_DATE_PREFIX_RE = new RegExp(`^${DATE_RANGE_PATTERN}[\\uFF0C,\\u3002\\s]*`, 'u')
export function getTodayDateValue(date = new Date()) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return ''
}
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
export function canApplyWorkbenchDateSelection({
mode = 'single',
singleDate = '',
rangeStartDate = '',
rangeEndDate = ''
} = {}) {
if (mode === 'range') {
const startDate = normalizeDateValue(rangeStartDate)
const endDate = normalizeDateValue(rangeEndDate)
return Boolean(startDate && endDate && startDate <= endDate)
}
return Boolean(normalizeDateValue(singleDate))
}
export function buildWorkbenchDateLabel({
mode = 'single',
singleDate = '',
rangeStartDate = '',
rangeEndDate = ''
} = {}) {
if (!canApplyWorkbenchDateSelection({ mode, singleDate, rangeStartDate, rangeEndDate })) {
return ''
}
if (mode !== 'range') {
return normalizeDateValue(singleDate)
}
const startDate = normalizeDateValue(rangeStartDate)
const endDate = normalizeDateValue(rangeEndDate)
return startDate === endDate ? startDate : `${startDate} \u81f3 ${endDate}`
}
export function stripWorkbenchDateLabelFromDraft(rawDraft) {
return String(rawDraft || '')
.replace(LABELED_DATE_PREFIX_RE, '')
.replace(RAW_DATE_PREFIX_RE, '')
.trim()
}
export function mergeWorkbenchDateLabelIntoDraft(rawDraft, dateLabel) {
const label = String(dateLabel || '').trim()
if (!label) {
return String(rawDraft || '').trim()
}
const draft = stripWorkbenchDateLabelFromDraft(rawDraft)
return draft ? `${label}\uFF0C${draft}` : label
}