feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造

- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制
- 引入费用审批动态路由、平台风险分级、预审与风险阶段管理
- 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板
- 新增 Hermes 风险线索收集器、Agent 链路追踪中心
- 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估
- 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-01 17:07:14 +08:00
parent 7989f3a159
commit 92444e7eae
285 changed files with 25075 additions and 2986 deletions

View File

@@ -165,6 +165,10 @@ export function generateRiskRuleAsset(payload, options = {}) {
})
}
export function fetchRiskRuleTemplates() {
return apiRequest('/agent-assets/risk-rules/templates')
}
export function updateRiskRuleDraft(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/draft`, {
method: 'PATCH',
@@ -181,6 +185,28 @@ export function createRiskRuleRevision(assetId, payload, options = {}) {
})
}
export function regenerateRiskRuleAsset(assetId, payload = {}, options = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/regenerate`, {
method: 'POST',
body: JSON.stringify(payload || {}),
headers: buildWriteHeaders(options),
timeoutMs: options.timeoutMs || 60000,
timeoutMessage: '风险规则重新生成时间较长,请稍后查看最新结果。'
})
}
export function submitRiskRuleFeedback(assetId, payload, options = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/feedback`, {
method: 'POST',
body: JSON.stringify(payload || {}),
headers: buildWriteHeaders(options)
})
}
export function fetchRiskRuleFeedback(assetId, params = {}) {
return apiRequest(`/agent-assets/${assetId}/risk-rules/feedback${buildQuery(params)}`)
}
export function fetchRiskRuleLatestTest(assetId) {
return apiRequest(`/agent-assets/${assetId}/risk-rule-tests/latest`)
}

View File

@@ -0,0 +1,39 @@
import { apiRequest } from './api.js'
function buildQuery(params = {}) {
const search = new URLSearchParams()
if (params.agent) {
search.set('agent', params.agent)
}
if (params.status) {
search.set('status', params.status)
}
if (params.source) {
search.set('source', params.source)
}
if (params.conversationId || params.conversation_id) {
search.set('conversation_id', params.conversationId || params.conversation_id)
}
if (params.keyword) {
search.set('keyword', params.keyword)
}
if (params.limit) {
search.set('limit', String(params.limit))
}
const query = search.toString()
return query ? `?${query}` : ''
}
export function fetchAgentTraces(params = {}) {
return apiRequest(`/agent-traces${buildQuery(params)}`)
}
export function fetchAgentTraceDetail(runId) {
return apiRequest(`/agent-traces/${encodeURIComponent(String(runId || '').trim())}`)
}
export function fetchConversationTrace(conversationId) {
return apiRequest(`/agent-traces/conversations/${encodeURIComponent(String(conversationId || '').trim())}`)
}

View File

@@ -25,6 +25,15 @@ const FINANCE_DASHBOARD_FALLBACK = {
hasRealData: false
}
const DIGITAL_EMPLOYEE_DASHBOARD_FALLBACK = {
totals: null,
dailyWork: [],
taskDistribution: [],
categoryDistribution: [],
recentRuns: [],
hasRealData: false
}
function normalizeSystemDashboardPayload(payload = {}) {
return {
...SYSTEM_DASHBOARD_FALLBACK,
@@ -62,6 +71,20 @@ function normalizeFinanceDashboardPayload(payload = {}) {
}
}
export function normalizeDigitalEmployeeDashboardPayload(payload = {}) {
return {
...DIGITAL_EMPLOYEE_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,
dailyWork: payload.daily_work || payload.dailyWork || [],
taskDistribution: payload.task_distribution || payload.taskDistribution || [],
categoryDistribution: payload.category_distribution || payload.categoryDistribution || [],
recentRuns: payload.recent_runs || payload.recentRuns || []
}
}
export async function fetchSystemDashboard(options = {}) {
const days = Number(options.days || 7)
const search = new URLSearchParams()
@@ -75,6 +98,21 @@ export async function fetchSystemDashboard(options = {}) {
return normalizeSystemDashboardPayload(payload)
}
export async function fetchDigitalEmployeeDashboard(options = {}) {
const days = Number(options.days || 7)
const limit = Number(options.limit || 300)
const search = new URLSearchParams()
search.set('days', String(Math.max(1, Math.min(days, 30))))
search.set('limit', String(Math.max(1, Math.min(limit, 1000))))
const payload = await apiRequest(`/analytics/digital-employee-dashboard?${search.toString()}`, {
timeoutMs: Number(options.timeoutMs || 3500),
timeoutMessage: '数字员工看板真实数据加载超时,请稍后重试。'
})
return normalizeDigitalEmployeeDashboardPayload(payload)
}
export async function fetchFinanceDashboard(options = {}) {
const search = new URLSearchParams()
search.set('range_key', String(options.rangeKey || options.range || '近10日'))

View File

@@ -1,3 +1,5 @@
import { normalizeAuthUserSnapshot } from '../utils/authUser.js'
const API_BASE_STORAGE_KEY = 'x-financial-api-base-url'
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
@@ -43,16 +45,25 @@ function readCurrentUserHeaders() {
try {
const payload = JSON.parse(raw)
const username = String(payload?.username || '').trim()
const name = String(payload?.name || username).trim()
const roleCodes = Array.isArray(payload?.roleCodes) ? payload.roleCodes.filter(Boolean) : []
const user = normalizeAuthUserSnapshot(payload)
const username = user.username
const name = user.name || username
const roleCodes = user.roleCodes
const isAdmin = resolveStoredUserAdminFlag(payload, roleCodes)
const department = String(payload?.department || payload?.departmentName || '').trim()
const costCenter = String(payload?.costCenter || payload?.cost_center || '').trim()
const department = user.department || user.departmentName
const costCenter = user.costCenter
const position = user.position
const grade = user.grade
const employeeNo = user.employeeNo
const managerName = user.managerName
const safeUsername = pickSafeHeaderValue(username)
const safeName = pickSafeHeaderValue(name)
const safeDepartment = pickSafeHeaderValue(department)
const safeCostCenter = pickSafeHeaderValue(costCenter)
const safePosition = pickSafeHeaderValue(position)
const safeGrade = pickSafeHeaderValue(grade)
const safeEmployeeNo = pickSafeHeaderValue(employeeNo)
const safeManagerName = pickSafeHeaderValue(managerName)
if (!safeUsername && !safeName) {
return {}
@@ -79,6 +90,22 @@ function readCurrentUserHeaders() {
headers['x-auth-cost-center'] = safeCostCenter
}
if (safePosition) {
headers['x-auth-position'] = safePosition
}
if (safeGrade) {
headers['x-auth-grade'] = safeGrade
}
if (safeEmployeeNo) {
headers['x-auth-employee-no'] = safeEmployeeNo
}
if (safeManagerName) {
headers['x-auth-manager-name'] = safeManagerName
}
return headers
} catch {
return {}

View File

@@ -7,6 +7,10 @@ export function login(payload) {
})
}
export function fetchCurrentAuthUser() {
return apiRequest('/auth/me')
}
export function finishSession(sessionId, payload) {
return apiRequest(`/auth/sessions/${encodeURIComponent(sessionId)}/finish`, {
method: 'POST',

View File

@@ -157,6 +157,13 @@ export function submitExpenseClaim(claimId) {
})
}
export function preReviewExpenseClaim(claimId) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/pre-review`, {
method: 'POST',
body: JSON.stringify({})
})
}
export function returnExpenseClaim(claimId, payload = {}) {
return apiRequest(`/reimbursements/claims/${encodeURIComponent(String(claimId || '').trim())}/return`, {
method: 'POST',

View File

@@ -53,9 +53,13 @@ export function normalizeRiskObservationDashboard(payload = {}) {
windowDays: toNumber(payload.window_days ?? payload.windowDays, 30),
totalObservations: toNumber(payload.total_observations ?? payload.totalObservations),
pendingCount: toNumber(payload.pending_count ?? payload.pendingCount),
riskClueCount: toNumber(payload.risk_clue_count ?? payload.riskClueCount),
highOrAboveCount: toNumber(payload.high_or_above_count ?? payload.highOrAboveCount),
confirmedCount: toNumber(payload.confirmed_count ?? payload.confirmedCount),
falsePositiveCount: toNumber(payload.false_positive_count ?? payload.falsePositiveCount),
feedbackSampleCount: toNumber(
payload.feedback_sample_count ?? payload.feedbackSampleCount
),
totalAmount: toNumber(payload.total_amount ?? payload.totalAmount),
averageScore: toNumber(payload.average_score ?? payload.averageScore),
confirmationRate: toNumber(payload.confirmation_rate ?? payload.confirmationRate),