feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
21
web/src/utils/assistantMessageMeta.js
Normal file
21
web/src/utils/assistantMessageMeta.js
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
90
web/src/utils/authSessionMetrics.js
Normal file
90
web/src/utils/authSessionMetrics.js
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
export const HERMES_SIMPLE_TASKS = [
|
||||
{
|
||||
id: 'global_risk_scan',
|
||||
label: '风险每日巡检',
|
||||
hint: '扫描报销、付款等风险信号',
|
||||
label: '财务风险图谱巡检',
|
||||
hint: '扫描单据、票据、审批链和画像异常',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
|
||||
53
web/src/utils/personalWorkbenchAssistantEntry.js
Normal file
53
web/src/utils/personalWorkbenchAssistantEntry.js
Normal 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
|
||||
}
|
||||
}
|
||||
105
web/src/utils/workbenchAssistantIntent.js
Normal file
105
web/src/utils/workbenchAssistantIntent.js
Normal 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)
|
||||
}
|
||||
74
web/src/utils/workbenchComposerDate.js
Normal file
74
web/src/utils/workbenchComposerDate.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user