feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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`)
|
||||
}
|
||||
|
||||
93
web/src/services/analytics.js
Normal file
93
web/src/services/analytics.js
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
27
web/src/services/operationFeedback.js
Normal file
27
web/src/services/operationFeedback.js
Normal 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)}`)
|
||||
}
|
||||
143
web/src/services/riskObservations.js
Normal file
143
web/src/services/riskObservations.js
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user