feat: 新增预算中心本体与风险规则评分回填
后端新增预算本体解析模块和风险规则评分回填服务,优化规则 生成本体对齐和提示词构建,增强费用类型关键词和本体验证, 完善报销查询和审计接口,前端预算中心页面增加对话框和本 体工具函数,重构审计页面元数据和视图模型,补充单元测试。
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user