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

@@ -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) {