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

@@ -31,7 +31,7 @@ import {
updateAgentAsset
} from '../../services/agentAssets.js'
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
import { isFinanceUser, isManagerUser } from '../../utils/accessControl.js'
import { isFinanceUser, isManagerUser, isPlatformAdminUser } from '../../utils/accessControl.js'
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
import {
buildReviewNote,
@@ -69,6 +69,7 @@ import {
} from './auditViewModel.js'
import {
createDefaultRiskRuleForm,
RISK_RULE_BUSINESS_STAGE_OPTIONS,
RISK_RULE_EXPENSE_CATEGORY_OPTIONS
} from './auditViewRiskRuleModel.js'
@@ -144,11 +145,11 @@ export default {
financialRules: [],
riskRules: [],
skills: [],
mcp: [],
tasks: []
mcp: []
})
const isAdmin = computed(() => isManagerUser(currentUser.value))
const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
const isRuleManager = computed(() => isManagerUser(currentUser.value))
const isFinance = computed(() => isFinanceUser(currentUser.value))
const activeMeta = computed(() => TAB_META[activeType.value])
const activeTabLabel = computed(() => activeMeta.value.label)
@@ -162,7 +163,7 @@ export default {
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
const showOnlineColumn = computed(() => false)
const showEnabledColumn = computed(() => activeType.value === 'riskRules')
const showEnabledColumn = computed(() => false)
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
const selectedSkillUsesSpreadsheet = computed(
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
@@ -171,6 +172,9 @@ export default {
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule)
)
const canManageSelected = computed(
() => isRuleManager.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
)
const canAdminOperateSelected = computed(
() => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
)
const canEditSelected = computed(
@@ -180,7 +184,7 @@ export default {
(isAdmin.value || isFinance.value)
)
const canCreateRiskRule = computed(
() => activeType.value === 'riskRules' && (isAdmin.value || isFinance.value) && !detailBusy.value
() => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
)
const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null)
const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed))
@@ -196,27 +200,20 @@ export default {
const canOpenRiskRuleTest = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canEditSelected.value &&
canAdminOperateSelected.value &&
Boolean(selectedSkill.value?.id) &&
!riskRuleGenerationBusy.value &&
!riskRuleGenerationFailed.value &&
!detailBusy.value
!riskRuleGenerationFailed.value
)
const canDeleteRiskRule = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canEditSelected.value &&
canAdminOperateSelected.value &&
Boolean(selectedSkill.value?.id) &&
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') &&
!detailBusy.value
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '')
)
const canOpenRiskRuleReviewSubmit = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canSubmitReview.value &&
!riskRuleInReview.value &&
!riskRuleGenerationBusy.value &&
!riskRuleGenerationFailed.value
() => false
)
const canSubmitRiskRuleReview = computed(
() =>
@@ -224,17 +221,14 @@ export default {
riskRuleTestPassed.value
)
const canReturnRiskRule = computed(
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && riskRuleInReview.value
() => false
)
const canPublishRiskRule = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canManageSelected.value &&
riskRuleInReview.value &&
riskRuleTestPassed.value
false
)
const canToggleRiskRuleEnabled = computed(
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && !detailBusy.value
() => selectedSkillUsesJsonRisk.value && canManageSelected.value
)
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
@@ -242,7 +236,11 @@ export default {
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
)
const canSubmitReview = computed(
() => canEditSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
() =>
!selectedSkillUsesJsonRisk.value &&
canEditSelected.value &&
selectedSkillIsRule.value &&
isDisplayingWorkingVersion.value
)
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
const canReviewSelected = computed(
@@ -370,7 +368,7 @@ export default {
)
const showStatusFilter = computed(() => true)
const showOnlineFilter = computed(() => false)
const showEnabledFilter = computed(() => activeType.value === 'riskRules')
const showEnabledFilter = computed(() => false)
const selectedRiskScenarioLabel = computed(
() =>
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
@@ -646,6 +644,7 @@ export default {
const detail = await generateRiskRuleAsset(
{
business_domain: 'expense',
business_stage: riskRuleCreateForm.value.business_stage,
expense_category: riskRuleCreateForm.value.expense_category,
rule_title: ruleTitle,
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
@@ -1007,8 +1006,13 @@ export default {
}
async function loadAssets(options = {}) {
loading.value = true
errorMessage.value = ''
const shouldShowLoading = !options.silent && !options.background
if (shouldShowLoading) {
loading.value = true
}
if (!options.silent) {
errorMessage.value = ''
}
try {
const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
@@ -1037,6 +1041,9 @@ export default {
}
}
} catch (error) {
if (options.silent || options.background) {
return
}
if (activeMeta.value.assetType === 'rule') {
assetBuckets.value = {
...assetBuckets.value,
@@ -1056,12 +1063,14 @@ export default {
toast(errorMessage.value)
}
} finally {
loading.value = false
if (shouldShowLoading) {
loading.value = false
}
}
}
async function refreshCurrentAssets() {
await loadAssets({ force: true, silent: true })
await loadAssets({ force: true, silent: true, background: true })
}
async function loadSelectedAssetDetail(assetId) {
@@ -1110,6 +1119,39 @@ export default {
}
}
function mergeSelectedRuleLifecycle(detail) {
if (!selectedSkill.value || !detail) {
return
}
const next = buildDetailViewModel(detail, runs.value)
selectedSkill.value = {
...selectedSkill.value,
status: next.status,
statusValue: next.statusValue,
statusTone: next.statusTone,
publishedVersion: next.publishedVersion,
workingVersion: next.workingVersion,
currentVersion: next.currentVersion,
displayVersion: next.displayVersion,
reviewer: next.reviewer,
publisher: next.publisher,
publishedAt: next.publishedAt,
isOnlineValue: next.isOnlineValue,
isOnlineLabel: next.isOnlineLabel,
isOnlineTone: next.isOnlineTone,
isEnabledValue: next.isEnabledValue,
isEnabledLabel: next.isEnabledLabel,
isEnabledTone: next.isEnabledTone,
latestTestSummary: next.latestTestSummary,
lastOperationLabel: next.lastOperationLabel,
lastOperationTone: next.lastOperationTone,
publishMeta: next.publishMeta,
publishState: next.publishState,
updatedAt: next.updatedAt,
configJson: next.configJson
}
}
async function loadRiskRuleJson(assetId) {
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
return
@@ -1525,6 +1567,9 @@ export default {
}
function openRiskRuleTestDialog() {
if (detailBusy.value) {
return
}
if (!canOpenRiskRuleTest.value) {
if (!selectedSkill.value?.id) {
toast('规则详情还没有加载完成,请稍后再测试。')
@@ -1544,7 +1589,8 @@ export default {
}
await refreshCurrentAssets()
if (selectedSkill.value?.id) {
await loadSelectedAssetDetail(selectedSkill.value.id)
const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
mergeSelectedRuleLifecycle(detail)
}
}
@@ -1659,15 +1705,15 @@ export default {
return
}
const assetId = selectedSkill.value.id
const nextEnabled = !selectedSkill.value.isEnabledValue
const nextEnabled = !selectedSkill.value.isOnlineValue
actionState.value = 'toggle-risk-rule-enabled'
try {
await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
mergeSelectedRuleLifecycle(detail)
await refreshCurrentAssets()
await loadSelectedAssetDetail(assetId)
toast(nextEnabled ? '风险规则已启用。' : '风险规则已停用,不会进入业务扫描。')
toast(nextEnabled ? '风险规则已上线。' : '风险规则已下线,不会进入业务扫描。')
} catch (error) {
toast(error?.message || '风险规则启用状态更新失败,请稍后重试。')
toast(error?.message || '风险规则上线状态更新失败,请稍后重试。')
} finally {
actionState.value = ''
}
@@ -1851,6 +1897,7 @@ export default {
riskRuleReturnOpen,
riskRulePublishOpen,
riskRuleReturnNote,
riskRuleBusinessStageOptions: RISK_RULE_BUSINESS_STAGE_OPTIONS,
riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
showReviewNote,
spreadsheetUploadInput,