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

@@ -165,6 +165,22 @@ export function generateRiskRuleAsset(payload, options = {}) {
})
}
export function updateRiskRuleDraft(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/draft`, {
method: 'PATCH',
body: JSON.stringify(payload || {}),
headers: buildWriteHeaders(options)
})
}
export function createRiskRuleRevision(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/revisions`, {
method: 'POST',
body: JSON.stringify(payload || {}),
headers: buildWriteHeaders(options)
})
}
export function fetchRiskRuleLatestTest(assetId) {
return apiRequest(`/agent-assets/${assetId}/risk-rule-tests/latest`)
}

View File

@@ -0,0 +1,93 @@
import { apiRequest } from './api.js'
const SYSTEM_DASHBOARD_FALLBACK = {
totals: null,
agentDailyRatio: null,
loginWave: null,
tokenDailyWave: null,
userTokenUsage: null,
accuracyComparison: null,
usageDurationSummary: null,
feedbackSummary: null,
toolDetailRows: null,
hasRealData: false
}
const FINANCE_DASHBOARD_FALLBACK = {
totals: null,
metricMeta: null,
trend: null,
spendByCategory: null,
exceptionMix: null,
departmentRanking: null,
bottlenecks: null,
budgetSummary: null,
hasRealData: false
}
function normalizeSystemDashboardPayload(payload = {}) {
return {
...SYSTEM_DASHBOARD_FALLBACK,
windowDays: Number(payload.window_days || payload.windowDays || 7),
generatedAt: payload.generated_at || payload.generatedAt || '',
hasRealData: Boolean(payload.has_real_data ?? payload.hasRealData),
totals: payload.totals || null,
agentDailyRatio: payload.agent_daily_ratio || payload.agentDailyRatio || null,
loginWave: payload.login_wave || payload.loginWave || null,
tokenDailyWave: payload.token_daily_wave || payload.tokenDailyWave || null,
userTokenUsage: payload.user_token_usage || payload.userTokenUsage || null,
accuracyComparison: payload.accuracy_comparison || payload.accuracyComparison || null,
usageDurationSummary: payload.usage_duration_summary || payload.usageDurationSummary || null,
feedbackSummary: payload.feedback_summary || payload.feedbackSummary || null,
toolDetailRows: payload.tool_detail_rows || payload.toolDetailRows || null
}
}
function normalizeFinanceDashboardPayload(payload = {}) {
return {
...FINANCE_DASHBOARD_FALLBACK,
rangeKey: payload.range_key || payload.rangeKey || '近10日',
startDate: payload.start_date || payload.startDate || '',
endDate: payload.end_date || payload.endDate || '',
generatedAt: payload.generated_at || payload.generatedAt || '',
hasRealData: Boolean(payload.has_real_data ?? payload.hasRealData),
totals: payload.totals || null,
metricMeta: payload.metric_meta || payload.metricMeta || null,
trend: payload.trend || null,
spendByCategory: payload.spend_by_category || payload.spendByCategory || null,
exceptionMix: payload.exception_mix || payload.exceptionMix || null,
departmentRanking: payload.department_ranking || payload.departmentRanking || null,
bottlenecks: payload.bottlenecks || null,
budgetSummary: payload.budget_summary || payload.budgetSummary || null
}
}
export async function fetchSystemDashboard(options = {}) {
const days = Number(options.days || 7)
const search = new URLSearchParams()
search.set('days', String(Math.max(1, Math.min(days, 30))))
const payload = await apiRequest(`/analytics/system-dashboard?${search.toString()}`, {
timeoutMs: Number(options.timeoutMs || 3500),
timeoutMessage: '系统看板真实数据加载超时,已保留本地展示数据。'
})
return normalizeSystemDashboardPayload(payload)
}
export async function fetchFinanceDashboard(options = {}) {
const search = new URLSearchParams()
search.set('range_key', String(options.rangeKey || options.range || '近10日'))
search.set('trend_range', String(options.trendRange || '近12天'))
search.set('department_range', String(options.departmentRange || '本月'))
if (options.startDate) search.set('start_date', String(options.startDate))
if (options.endDate) search.set('end_date', String(options.endDate))
const payload = await apiRequest(`/analytics/finance-dashboard?${search.toString()}`, {
timeoutMs: Number(options.timeoutMs || 3500),
timeoutMessage: '财务看板真实数据加载超时,已保留本地展示数据。'
})
return normalizeFinanceDashboardPayload(payload)
}

View File

@@ -1,4 +1,4 @@
import { apiRequest } from './api.js'
import { apiRequest, getRuntimeApiBaseUrl } from './api.js'
export function login(payload) {
return apiRequest('/auth/login', {
@@ -6,3 +6,34 @@ export function login(payload) {
body: JSON.stringify(payload)
})
}
export function finishSession(sessionId, payload) {
return apiRequest(`/auth/sessions/${encodeURIComponent(sessionId)}/finish`, {
method: 'POST',
body: JSON.stringify(payload),
keepalive: true,
timeoutMs: 3000
})
}
export function finishSessionOnUnload(sessionId, payload) {
const normalizedSessionId = String(sessionId || '').trim()
if (!normalizedSessionId || typeof window === 'undefined') {
return false
}
const url = `${getRuntimeApiBaseUrl()}/auth/sessions/${encodeURIComponent(normalizedSessionId)}/finish`
const body = JSON.stringify(payload || {})
if (typeof window.navigator?.sendBeacon === 'function') {
return window.navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }))
}
window.fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
keepalive: true
}).catch(() => {})
return true
}

View File

@@ -0,0 +1,27 @@
import { apiRequest } from './api.js'
function buildQuery(params = {}) {
const search = new URLSearchParams()
if (params.agent) {
search.set('agent', String(params.agent).trim())
}
if (params.sessionType || params.session_type) {
search.set('session_type', String(params.sessionType || params.session_type).trim())
}
if (params.limit) {
search.set('limit', String(params.limit))
}
const query = search.toString()
return query ? `?${query}` : ''
}
export function createOperationFeedback(payload) {
return apiRequest('/agent-feedback', {
method: 'POST',
body: JSON.stringify(payload || {})
})
}
export function fetchOperationFeedbackSummary(params = {}) {
return apiRequest(`/agent-feedback/summary${buildQuery(params)}`)
}

View File

@@ -0,0 +1,143 @@
import { apiRequest } from './api.js'
function toNumber(value, fallback = 0) {
const number = Number(value)
return Number.isFinite(number) ? number : fallback
}
function toArray(value) {
return Array.isArray(value) ? value : []
}
function toObject(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
}
export function normalizeRiskObservation(item = {}) {
return {
id: String(item.id || '').trim(),
observationKey: String(item.observation_key || item.observationKey || '').trim(),
claimId: String(item.claim_id || item.claimId || '').trim(),
claimNo: String(item.claim_no || item.claimNo || '').trim(),
riskType: String(item.risk_type || item.riskType || '').trim(),
riskSignal: String(item.risk_signal || item.riskSignal || '').trim(),
title: String(item.title || '').trim(),
description: String(item.description || '').trim(),
riskScore: toNumber(item.risk_score ?? item.riskScore),
riskLevel: String(item.risk_level || item.riskLevel || '').trim(),
confidenceScore: toNumber(item.confidence_score ?? item.confidenceScore),
controlStage: String(item.control_stage || item.controlStage || '').trim(),
controlMode: String(item.control_mode || item.controlMode || '').trim(),
automationMode: String(item.automation_mode || item.automationMode || '').trim(),
source: String(item.source || '').trim(),
algorithmVersion: String(item.algorithm_version || item.algorithmVersion || '').trim(),
status: String(item.status || '').trim(),
feedbackStatus: String(item.feedback_status || item.feedbackStatus || '').trim(),
contributionScores: toObject(item.contribution_scores || item.contributionScores),
baseline: toObject(item.baseline),
evidence: toArray(item.evidence),
graphNodeKeys: toArray(item.graph_node_keys || item.graphNodeKeys),
graphEdgeKeys: toArray(item.graph_edge_keys || item.graphEdgeKeys),
policyRefs: toArray(item.policy_refs || item.policyRefs),
similarCaseClaimIds: toArray(item.similar_case_claim_ids || item.similarCaseClaimIds),
ontology: toObject(item.ontology_json || item.ontologyJson),
decisionTrace: toObject(item.decision_trace || item.decisionTrace),
feedbackItems: toArray(item.feedback_items || item.feedbackItems),
createdAt: String(item.created_at || item.createdAt || '').trim(),
updatedAt: String(item.updated_at || item.updatedAt || '').trim()
}
}
export function normalizeRiskObservationDashboard(payload = {}) {
return {
windowDays: toNumber(payload.window_days ?? payload.windowDays, 30),
totalObservations: toNumber(payload.total_observations ?? payload.totalObservations),
pendingCount: toNumber(payload.pending_count ?? payload.pendingCount),
highOrAboveCount: toNumber(payload.high_or_above_count ?? payload.highOrAboveCount),
confirmedCount: toNumber(payload.confirmed_count ?? payload.confirmedCount),
falsePositiveCount: toNumber(payload.false_positive_count ?? payload.falsePositiveCount),
totalAmount: toNumber(payload.total_amount ?? payload.totalAmount),
averageScore: toNumber(payload.average_score ?? payload.averageScore),
confirmationRate: toNumber(payload.confirmation_rate ?? payload.confirmationRate),
falsePositiveRate: toNumber(payload.false_positive_rate ?? payload.falsePositiveRate),
candidateRuleCount: toNumber(payload.candidate_rule_count ?? payload.candidateRuleCount),
levelDistribution: toObject(payload.level_distribution || payload.levelDistribution),
statusDistribution: toObject(payload.status_distribution || payload.statusDistribution),
signalDistribution: toObject(payload.signal_distribution || payload.signalDistribution),
sourceDistribution: toObject(payload.source_distribution || payload.sourceDistribution),
automationDistribution: toObject(
payload.automation_distribution || payload.automationDistribution
),
departmentDistribution: toObject(
payload.department_distribution || payload.departmentDistribution
),
expenseTypeDistribution: toObject(
payload.expense_type_distribution || payload.expenseTypeDistribution
),
riskTypeDistribution: toObject(payload.risk_type_distribution || payload.riskTypeDistribution),
supplierDistribution: toObject(payload.supplier_distribution || payload.supplierDistribution),
employeeGradeDistribution: toObject(
payload.employee_grade_distribution || payload.employeeGradeDistribution
),
dailyTrend: toArray(payload.daily_trend || payload.dailyTrend),
topRiskSignals: toArray(payload.top_risk_signals || payload.topRiskSignals),
topDepartments: toArray(payload.top_departments || payload.topDepartments),
topEmployees: toArray(payload.top_employees || payload.topEmployees),
topSuppliers: toArray(payload.top_suppliers || payload.topSuppliers),
topExpenseTypes: toArray(payload.top_expense_types || payload.topExpenseTypes),
topRules: toArray(payload.top_rules || payload.topRules),
recentHighObservations: toArray(
payload.recent_high_observations || payload.recentHighObservations
).map(normalizeRiskObservation)
}
}
export async function fetchRiskObservationDashboard(options = {}) {
const windowDays = Math.max(1, Math.min(toNumber(options.windowDays || 30), 365))
const limit = Math.max(1, Math.min(toNumber(options.limit || 500), 2000))
const search = new URLSearchParams()
search.set('window_days', String(windowDays))
search.set('limit', String(limit))
const payload = await apiRequest(`/risk-observations/dashboard?${search.toString()}`, {
timeoutMs: toNumber(options.timeoutMs || 3500),
timeoutMessage: '风险看板数据加载超时,请稍后重试。'
})
return normalizeRiskObservationDashboard(payload)
}
export async function fetchClaimRiskObservations(claimId, options = {}) {
const normalizedClaimId = String(claimId || '').trim()
if (!normalizedClaimId) {
return []
}
const payload = await apiRequest(
`/risk-observations/claim/${encodeURIComponent(normalizedClaimId)}`,
{
timeoutMs: toNumber(options.timeoutMs || 3500),
timeoutMessage: '风险证据链加载超时,请稍后重试。'
}
)
return toArray(payload).map(normalizeRiskObservation)
}
export async function fetchRunRiskObservations(runId, options = {}) {
const normalizedRunId = String(runId || '').trim()
if (!normalizedRunId) {
return []
}
const search = new URLSearchParams()
search.set('run_id', normalizedRunId)
search.set('limit', String(Math.max(1, Math.min(toNumber(options.limit || 100), 200))))
const payload = await apiRequest(`/risk-observations?${search.toString()}`, {
timeoutMs: toNumber(options.timeoutMs || 3500),
timeoutMessage: '本次运行风险观察加载超时,请稍后重试。'
})
return toArray(payload.items || payload).map(normalizeRiskObservation)
}