feat: 新增预算中心本体与风险规则评分回填

后端新增预算本体解析模块和风险规则评分回填服务,优化规则
生成本体对齐和提示词构建,增强费用类型关键词和本体验证,
完善报销查询和审计接口,前端预算中心页面增加对话框和本
体工具函数,重构审计页面元数据和视图模型,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-26 12:16:20 +08:00
parent 0e861d8fa6
commit e1e515ecae
53 changed files with 4350 additions and 921 deletions

View File

@@ -207,6 +207,65 @@ export function resolveRiskRuleEnabled(source, rulePayload = null) {
return true
}
const LAST_OPERATION_LABELS = {
generate: '开始生成',
create: '创建',
test: '测试',
online: '上线',
offline: '下线',
delete: '删除',
update: '更新'
}
const RISK_RULE_BUSINESS_STAGE_LABELS = {
expense_application: '费用申请',
reimbursement: '费用报销'
}
function resolveRiskRuleBusinessStage(source, rulePayload = null) {
const configJson = readConfigJson(source)
const metadata = rulePayload && typeof rulePayload === 'object' ? rulePayload.metadata || {} : {}
const stage =
normalizeText(configJson.business_stage) ||
normalizeText(metadata.business_stage) ||
normalizeText(rulePayload?.business_stage)
const label =
normalizeText(configJson.business_stage_label) ||
normalizeText(metadata.business_stage_label) ||
RISK_RULE_BUSINESS_STAGE_LABELS[stage]
return {
value: stage || 'reimbursement',
label: label || '费用报销'
}
}
function resolveRiskRuleOnlineMeta(statusValue) {
if (statusValue === 'active') {
return { label: '已上线', tone: 'success', online: true }
}
if (statusValue === 'disabled') {
return { label: '已下线', tone: 'disabled', online: false }
}
if (statusValue === 'generating') {
return { label: '生成中', tone: 'info', online: false }
}
if (statusValue === 'failed') {
return { label: '生成失败', tone: 'danger', online: false }
}
return { label: '待上线', tone: 'draft', online: false }
}
function resolveLastOperationLabel(source, fallback = {}) {
const configJson = readConfigJson(source)
const operation = isPlainObject(configJson.last_operation) ? configJson.last_operation : {}
const action = normalizeText(operation.action) || normalizeText(fallback.action) || 'create'
const actor = normalizeText(operation.actor) || normalizeText(fallback.actor) || '系统'
const at = normalizeText(operation.at) || normalizeText(fallback.at)
const actionLabel = LAST_OPERATION_LABELS[action] || action
const timeLabel = formatDateTime(at)
return timeLabel && timeLabel !== '-' ? `${actionLabel}${actor} · ${timeLabel}` : `${actionLabel}${actor}`
}
export function readRuleDocumentMeta(value) {
const configJson = readConfigJson(value)
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
@@ -458,12 +517,13 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
normalizeText(apiConfig.expense_category_label) ||
normalizeText(rulePayload.risk_category) ||
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
const businessStage = resolveRiskRuleBusinessStage(target, rulePayload)
const riskRuleFields = resolveRiskRuleFields(rulePayload)
const riskRuleCreatedAt = resolveRiskRuleCreatedAt(rulePayload, target.createdAt || target.updatedAt)
const riskRuleScoreLevel = resolveRiskRuleScoreLevel(rulePayload, apiConfig)
const statusValue = apiPayload?.status || target.statusValue || 'draft'
const isOnlineLabel = statusValue === 'active' ? '是' : '否'
const onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
const publisher =
@@ -488,6 +548,8 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
riskCategory,
businessStageValue: businessStage.value,
businessStageLabel: businessStage.label,
scope: riskCategory,
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
riskRuleSeverity: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
@@ -521,10 +583,16 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
},
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
isOnlineLabel,
isOnlineValue: onlineMeta.online,
isOnlineLabel: onlineMeta.label,
isOnlineTone: onlineMeta.tone,
isEnabledValue,
isEnabledLabel: isEnabledValue ? '是' : '否',
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
lastOperationLabel: resolveLastOperationLabel(target, {
actor: publisher,
at: riskRuleCreatedAt
}),
publisher,
publishedAt
}
@@ -747,7 +815,7 @@ export function resolveTypeKey(assetType) {
if (assetType === 'mcp') {
return 'mcp'
}
return 'tasks'
return ''
}
export function formatSeverity(value) {
@@ -778,23 +846,6 @@ export function formatOutputSummary(items) {
return `${items.length} 项输出`
}
export function formatTaskRisk(scenarios) {
if (Array.isArray(scenarios) && scenarios.includes('risk_check')) {
return '高风险'
}
if (
Array.isArray(scenarios) &&
(scenarios.includes('accounts_receivable') || scenarios.includes('accounts_payable'))
) {
return '中风险'
}
return '常规'
}
export function findLatestTaskRun(runs, assetId) {
return runs.find((item) => item.task_id === assetId) || null
}
export function findLatestMcpCall(runs, assetCode) {
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
@@ -827,7 +878,7 @@ export function buildRowRuntime(asset, typeKey) {
if (typeKey === 'mcp') {
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
}
return normalizeText(asset.config_json?.cron) || '未配置调度'
return ''
}
export function buildRowMetric(asset, typeKey) {
@@ -840,7 +891,7 @@ export function buildRowMetric(asset, typeKey) {
if (typeKey === 'mcp') {
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
}
return normalizeText(asset.config_json?.agent) || '未配置 Agent'
return ''
}
export function formatSpreadsheetChangeSummary(summary) {
@@ -885,7 +936,8 @@ export function buildListItem(asset) {
const listSubtitle = isRiskRule
? buildRiskListSubtitle(asset.description)
: normalizeText(asset.description)
const isOnlineValue = asset.status === 'active'
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
const isOnlineValue = onlineMeta.online
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
const reviewer = normalizeText(asset.reviewer) || '待分配'
const creator =
@@ -895,6 +947,9 @@ export function buildListItem(asset) {
'未知'
const publisher = isRiskRule ? creator : ''
const riskRuleCreatedAt = formatDateTime(asset.created_at || asset.updated_at)
const businessStage = usesJsonRiskRule
? resolveRiskRuleBusinessStage(asset)
: { value: '', label: '' }
return {
id: asset.id,
@@ -915,6 +970,8 @@ export function buildListItem(asset) {
reviewer,
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
riskCategory: ruleScenarioCategory,
businessStageValue: businessStage.value,
businessStageLabel: businessStage.label,
model: buildRowRuntime(asset, typeKey),
version: workingVersion,
versionDisplay: typeKey === 'rules' ? `${changeCount}` : workingVersion,
@@ -928,8 +985,8 @@ export function buildListItem(asset) {
publisher,
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
isOnlineValue,
isOnlineLabel: isOnlineValue ? '是' : '否',
isOnlineTone: isOnlineValue ? 'success' : 'disabled',
isOnlineLabel: onlineMeta.label,
isOnlineTone: onlineMeta.tone,
isEnabledValue,
isEnabledLabel: isEnabledValue ? '是' : '否',
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
@@ -1002,21 +1059,7 @@ export function buildMcpFields(detail, latestCall) {
]
}
export function buildTaskFields(detail, latestRun) {
const content = detail.current_version_content || {}
return [
{ label: '任务编码', value: detail.code },
{ label: 'Cron', value: normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置' },
{ label: '执行 Agent', value: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置' },
{ label: '风险等级', value: formatTaskRisk(detail.scenario_json) },
{
label: '最近执行',
value: latestRun ? formatDateTime(latestRun.started_at) : '暂无执行记录'
}
]
}
export function buildFields(detail, typeKey, latestRun, latestCall) {
export function buildFields(detail, typeKey, latestCall) {
if (typeKey === 'rules') {
return buildRuleFields(detail)
}
@@ -1026,10 +1069,10 @@ export function buildFields(detail, typeKey, latestRun, latestCall) {
if (typeKey === 'mcp') {
return buildMcpFields(detail, latestCall)
}
return buildTaskFields(detail, latestRun)
return []
}
export function buildPromptSections(detail, typeKey, latestRun, latestCall) {
export function buildPromptSections(detail, typeKey) {
const content = detail.current_version_content || {}
if (typeKey === 'skills') {
@@ -1075,26 +1118,10 @@ export function buildPromptSections(detail, typeKey, latestRun, latestCall) {
]
}
return [
{
title: '任务场景',
intent: '调度目标',
content: formatScenarioList(detail.scenario_json)
},
{
title: '执行 Agent',
intent: '运行主体',
content: normalizeText(content.target_agent) || normalizeText(detail.config_json?.agent) || '未配置执行 Agent。'
},
{
title: '最近执行结果',
intent: '运行反馈',
content: latestRun?.result_summary || latestRun?.error_message || '暂无执行记录。'
}
]
return []
}
export function buildOutputRules(detail, typeKey, latestRun, latestCall) {
export function buildOutputRules(detail, typeKey) {
const content = detail.current_version_content || {}
if (typeKey === 'rules') {
@@ -1130,15 +1157,10 @@ export function buildOutputRules(detail, typeKey, latestRun, latestCall) {
]
}
return [
`调度周期:${normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置'}`,
`执行 Agent${normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置'}`,
`风险等级:${formatTaskRisk(detail.scenario_json)}`,
`最近执行结果:${latestRun?.status || '暂无执行记录'}`
]
return []
}
export function buildTests(detail, typeKey, latestRun, latestCall) {
export function buildTests(detail, typeKey, latestCall) {
if (typeKey === 'rules') {
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
return [
@@ -1195,23 +1217,10 @@ export function buildTests(detail, typeKey, latestRun, latestCall) {
]
}
return [
{
name: '最近运行状态',
input: latestRun?.run_id || '暂无运行',
result: latestRun?.status || '未记录',
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'success'
},
{
name: '结果摘要',
input: latestRun?.agent || normalizeText(detail.config_json?.agent) || '未配置',
result: latestRun?.result_summary || '暂无摘要',
tone: 'success'
}
]
return []
}
export function buildTools(detail, typeKey, latestRun, latestCall) {
export function buildTools(detail, typeKey, latestCall) {
const content = detail.current_version_content || {}
if (typeKey === 'skills') {
@@ -1246,26 +1255,7 @@ export function buildTools(detail, typeKey, latestRun, latestCall) {
]
}
return [
{
name: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置 Agent',
scope: '执行 Agent',
mode: '调度',
tone: 'active'
},
{
name: latestRun?.run_id || '暂无执行记录',
scope: '最近 Run',
mode: latestRun?.status || '未执行',
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'active'
},
{
name: latestRun?.permission_level || '未记录',
scope: '权限级别',
mode: 'Trace',
tone: 'safe'
}
]
return []
}
export function buildPublishDescription(detail, typeKey) {
@@ -1279,14 +1269,16 @@ export function buildPublishDescription(detail, typeKey) {
return '当前规则需要先完成审核,再调用上线接口正式激活。'
}
return DETAIL_TITLES[typeKey].publishDesc
return DETAIL_TITLES[typeKey]?.publishDesc || ''
}
export function buildDetailViewModel(detail, runs) {
const typeKey = resolveTypeKey(detail.asset_type)
const tabId = resolveTabId(detail, typeKey) || typeKey
if (!typeKey || !tabId) {
return null
}
const tabMeta = resolveTabMeta(tabId, typeKey)
const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null
const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null
const configJson = readConfigJson(detail)
const statusMeta = resolveStatusMeta(detail.status)
@@ -1320,6 +1312,14 @@ export function buildDetailViewModel(detail, runs) {
normalizeText(detail.owner) ||
normalizeText(detail.recent_versions?.[0]?.created_by) ||
'未知'
const onlineMeta = resolveRiskRuleOnlineMeta(detail.status)
const businessStage = usesJsonRiskRule
? resolveRiskRuleBusinessStage(detail)
: { value: '', label: '' }
const initialRiskRuleScore = resolveRiskRuleScore(configJson, configJson)
const initialRiskRuleScoreLevel = resolveRiskRuleScoreLevel(configJson, configJson)
const initialRiskRuleSeverity = initialRiskRuleScoreLevel || resolveRiskRuleSeverity(configJson)
return {
id: detail.id,
@@ -1335,6 +1335,8 @@ export function buildDetailViewModel(detail, runs) {
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
category: resolveDomainLabel(detail.domain),
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json),
businessStageValue: businessStage.value,
businessStageLabel: businessStage.label,
version: detail.working_version || detail.current_version || '-',
currentVersion: detail.current_version || '-',
publishedVersion: detail.published_version || '-',
@@ -1356,15 +1358,19 @@ export function buildDetailViewModel(detail, runs) {
riskRuleBusinessDescription: '',
riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '',
riskRuleSourceRef: '',
riskRuleSeverity: 'medium',
riskRuleSeverityLabel: '中风险',
riskRuleScore: null,
riskRuleScoreLabel: '待计算',
riskRuleScoreLevel: 'medium',
riskRuleScoreDetail: null,
riskRuleSeverity: initialRiskRuleSeverity,
riskRuleScore: initialRiskRuleScore,
riskRuleScoreLevel: initialRiskRuleScoreLevel || initialRiskRuleSeverity,
riskRuleScoreDetail: resolveRiskRuleScoreDetail(configJson, configJson),
riskRuleSeverityLabel: initialRiskRuleScoreLevel
? resolveRiskRuleScoreLabel(configJson, configJson)
: resolveRiskRuleSeverityLabel(configJson),
riskRuleScoreLabel: resolveRiskRuleScoreLabel(configJson, configJson),
riskRuleCreatedAt: formatDateTime(detail.created_at),
riskRuleAgeLabel: formatRiskRuleAge(detail.created_at),
isOnlineLabel: detail.status === 'active' ? '是' : '否',
isOnlineValue: onlineMeta.online,
isOnlineLabel: onlineMeta.label,
isOnlineTone: onlineMeta.tone,
isEnabledValue,
isEnabledLabel: isEnabledValue ? '是' : '否',
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
@@ -1381,6 +1387,11 @@ export function buildDetailViewModel(detail, runs) {
history.find((item) => item.isPublished || item.lifecycleState === 'published')?.time ||
(detail.published_at ? formatDateTime(detail.published_at) : '') ||
(detail.latest_review?.reviewed_at ? formatDateTime(detail.latest_review.reviewed_at) : '-'),
lastOperationLabel: resolveLastOperationLabel(detail, {
actor: riskRuleCreator,
at: detail.created_at
}),
lastOperationTone: onlineMeta.tone,
riskRuleFields: [],
riskRuleFieldSummary: '未识别字段',
riskRuleFlow: resolveRiskRuleFlow({}, []),
@@ -1411,13 +1422,12 @@ export function buildDetailViewModel(detail, runs) {
reviewStatusValue: detail.latest_review?.review_status || '',
reviewTimeLabel: formatDateTime(detail.latest_review?.reviewed_at),
reviewNote: detail.latest_review?.review_note || '',
latestRun,
latestCall,
fields: buildFields(detail, typeKey, latestRun, latestCall),
fields: buildFields(detail, typeKey, latestCall),
promptSections:
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey, latestRun, latestCall),
outputRules: buildOutputRules(detail, typeKey, latestRun, latestCall),
tests: buildTests(detail, typeKey, latestRun, latestCall),
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey),
outputRules: buildOutputRules(detail, typeKey),
tests: buildTests(detail, typeKey, latestCall),
triggers:
typeKey === 'rules'
? [ruleScenarioCategory || '通用']
@@ -1446,7 +1456,7 @@ export function buildDetailViewModel(detail, runs) {
tone: 'safe'
}
]
: buildTools(detail, typeKey, latestRun, latestCall),
: buildTools(detail, typeKey, latestCall),
history,
configTitle: titles.configTitle,
configDesc: titles.configDesc,
@@ -1467,9 +1477,7 @@ export function buildDetailViewModel(detail, runs) {
publishMeta:
typeKey === 'rules'
? `最近保存:${formatDateTime(detail.updated_at)}`
: latestRun
? `最近运行:${formatDateTime(latestRun.started_at)}`
: `最近更新:${formatDateTime(detail.updated_at)}`,
: `最近更新:${formatDateTime(detail.updated_at)}`,
publishState: statusMeta.label,
latestReviewVersion: detail.latest_review?.version || detail.current_version || '-',
loading: false