feat: 增强规则资产管理与审计页面运行时调试

后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险
规则生成模板执行和员工数据模型字段,知识库 RAG 增强本
地回退和文档提取能力,清理旧风险规则文件统一由生成引擎
管理,前端审计页面增加运行时调试面板和规则资产编辑交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-24 21:44:17 +08:00
parent 575f093c74
commit 50b1c3f9a9
113 changed files with 13896 additions and 5044 deletions

View File

@@ -3,6 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
import { fetchEmployees } from '../../services/employees.js'
import RiskRuleFlowDiagram from '../../components/shared/RiskRuleFlowDiagram.vue'
import RiskRuleTestDialog from '../../components/shared/RiskRuleTestDialog.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
import { useSystemState } from '../../composables/useSystemState.js'
@@ -11,6 +12,7 @@ import {
activateAgentAsset,
createAgentAssetReview,
createAgentAssetVersion,
deleteAgentAsset,
fetchAgentAssetDetail,
fetchAgentAssets,
fetchAgentAssetSpreadsheetBlob,
@@ -20,9 +22,12 @@ import {
fetchAgentAssetVersionTimeline,
fetchAgentRuns,
generateRiskRuleAsset,
publishRiskRuleAsset,
returnRiskRuleAsset,
saveAgentAssetRuleJson,
importAgentAssetSpreadsheetContent,
restoreAgentAssetVersion,
setRiskRuleAssetEnabled,
updateAgentAsset
} from '../../services/agentAssets.js'
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
@@ -39,6 +44,8 @@ import {
import {
TAB_META,
STATUS_OPTIONS,
ENABLED_STATE_OPTIONS,
ONLINE_STATE_OPTIONS,
RISK_SCENARIO_OPTIONS,
normalizeText,
readConfigJson,
@@ -63,6 +70,7 @@ import {
import {
createDefaultRiskRuleForm,
RISK_RULE_CREATE_DOMAIN_OPTIONS,
RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
RISK_RULE_LEVEL_OPTIONS
} from './auditViewRiskRuleModel.js'
@@ -71,6 +79,7 @@ export default {
components: {
ConfirmDialog,
RiskRuleFlowDiagram,
RiskRuleTestDialog,
TableLoadingState,
TableEmptyState
},
@@ -93,6 +102,8 @@ export default {
const selectedOwner = ref('')
const selectedStatus = ref('')
const selectedRiskScenario = ref('')
const selectedOnlineState = ref('')
const selectedEnabledState = ref('')
const loading = ref(false)
const errorMessage = ref('')
const detailLoading = ref(false)
@@ -105,6 +116,11 @@ export default {
const reviewSubmitReviewerOptions = ref([])
const riskRuleCreateOpen = ref(false)
const riskRuleCreateForm = ref(createDefaultRiskRuleForm())
const riskRuleTestOpen = ref(false)
const riskRuleDeleteOpen = ref(false)
const riskRuleReturnOpen = ref(false)
const riskRulePublishOpen = ref(false)
const riskRuleReturnNote = ref('')
const runLoading = ref(false)
const runs = ref([])
const spreadsheetUploadInput = ref(null)
@@ -146,6 +162,8 @@ export default {
const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false)
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
const showOnlineColumn = computed(() => activeType.value === 'riskRules')
const showEnabledColumn = computed(() => activeType.value === 'riskRules')
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
const selectedSkillUsesSpreadsheet = computed(
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
@@ -165,6 +183,46 @@ export default {
const canCreateRiskRule = computed(
() => activeType.value === 'riskRules' && (isAdmin.value || isFinance.value) && !detailBusy.value
)
const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null)
const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed))
const riskRuleInReview = computed(
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'review'
)
const canOpenRiskRuleTest = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canEditSelected.value &&
Boolean(selectedSkill.value?.id) &&
!detailBusy.value
)
const canDeleteRiskRule = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canEditSelected.value &&
Boolean(selectedSkill.value?.id) &&
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') &&
!detailBusy.value
)
const canSubmitRiskRuleReview = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canSubmitReview.value &&
!riskRuleInReview.value &&
riskRuleTestPassed.value
)
const canReturnRiskRule = computed(
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && riskRuleInReview.value
)
const canPublishRiskRule = computed(
() =>
selectedSkillUsesJsonRisk.value &&
canManageSelected.value &&
riskRuleInReview.value &&
riskRuleTestPassed.value
)
const canToggleRiskRuleEnabled = computed(
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && !detailBusy.value
)
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
const isDisplayingWorkingVersion = computed(
@@ -276,7 +334,7 @@ export default {
const ownerOptions = computed(() => {
const uniqueOwners = [...new Set(currentAssets.value.map((item) => item.owner).filter(Boolean))]
return [
{ value: '', label: '全部负责人' },
{ value: '', label: activeType.value === 'riskRules' ? '全部审核人' : '全部负责人' },
...uniqueOwners.map((value) => ({
value,
label: value
@@ -287,7 +345,9 @@ export default {
() => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
)
const selectedOwnerLabel = computed(
() => ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label || '负责人'
() =>
ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label ||
(activeType.value === 'riskRules' ? '审核人' : '负责人')
)
const selectedStatusLabel = computed(
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
@@ -296,11 +356,23 @@ export default {
['financialRules', 'riskRules'].includes(activeType.value)
)
const showStatusFilter = computed(() => activeType.value !== 'riskRules')
const showOnlineFilter = computed(() => activeType.value === 'riskRules')
const showEnabledFilter = computed(() => activeType.value === 'riskRules')
const selectedRiskScenarioLabel = computed(
() =>
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
'使用场景'
)
const selectedOnlineStateLabel = computed(
() =>
ONLINE_STATE_OPTIONS.find((item) => item.value === selectedOnlineState.value)?.label ||
'是否上线'
)
const selectedEnabledStateLabel = computed(
() =>
ENABLED_STATE_OPTIONS.find((item) => item.value === selectedEnabledState.value)?.label ||
'是否启用'
)
const activeFilterTokens = computed(() => {
const tokens = []
@@ -313,8 +385,14 @@ export default {
if (showStatusFilter.value && selectedStatus.value) {
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
}
if (showOnlineFilter.value && selectedOnlineState.value) {
tokens.push(`是否上线:${selectedOnlineStateLabel.value}`)
}
if (showEnabledFilter.value && selectedEnabledState.value) {
tokens.push(`是否启用:${selectedEnabledStateLabel.value}`)
}
if (selectedOwner.value) {
tokens.push(`负责人${selectedOwner.value}`)
tokens.push(`${activeType.value === 'riskRules' ? '审核人' : '负责人'}${selectedOwner.value}`)
}
if (keyword.value.trim()) {
tokens.push(`搜索:${keyword.value.trim()}`)
@@ -326,9 +404,11 @@ export default {
const hasFilters = activeFilterTokens.value.length > 0
const supportedFilters = [
'业务域',
'负责人',
activeType.value === 'riskRules' ? '审核人' : '负责人',
...(showRiskScenarioFilter.value ? ['使用场景'] : []),
...(showStatusFilter.value ? ['状态'] : []),
...(showOnlineFilter.value ? ['是否上线'] : []),
...(showEnabledFilter.value ? ['是否启用'] : []),
'关键词'
]
@@ -409,8 +489,12 @@ export default {
selectedOwner: selectedOwner.value,
selectedStatus: selectedStatus.value,
selectedRiskScenario: selectedRiskScenario.value,
selectedOnlineState: selectedOnlineState.value,
selectedEnabledState: selectedEnabledState.value,
showStatusFilter: showStatusFilter.value,
showRiskScenarioFilter: showRiskScenarioFilter.value
showRiskScenarioFilter: showRiskScenarioFilter.value,
showOnlineFilter: showOnlineFilter.value,
showEnabledFilter: showEnabledFilter.value
})
)
@@ -455,6 +539,8 @@ export default {
selectedOwner.value = ''
selectedStatus.value = ''
selectedRiskScenario.value = ''
selectedOnlineState.value = ''
selectedEnabledState.value = ''
activeFilterPopover.value = ''
}
@@ -488,6 +574,12 @@ export default {
if (name === 'riskScenario') {
selectedRiskScenario.value = value
}
if (name === 'online') {
selectedOnlineState.value = value
}
if (name === 'enabled') {
selectedEnabledState.value = value
}
closeFilterPopover()
}
@@ -536,7 +628,11 @@ export default {
const detail = await generateRiskRuleAsset(
{
business_domain: riskRuleCreateForm.value.business_domain,
expense_category: riskRuleCreateForm.value.business_domain === 'expense'
? riskRuleCreateForm.value.expense_category
: null,
risk_level: riskRuleCreateForm.value.risk_level,
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
natural_language: naturalLanguage
},
{ actor: resolveActor() }
@@ -1105,6 +1201,11 @@ export default {
versionSwitchTarget.value = null
versionTimelineOpen.value = false
versionTimelineItems.value = []
riskRuleTestOpen.value = false
riskRuleDeleteOpen.value = false
riskRuleReturnOpen.value = false
riskRulePublishOpen.value = false
riskRuleReturnNote.value = ''
}
function openVersionSwitch(version) {
@@ -1299,6 +1400,10 @@ export default {
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
return
}
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
toast('请先在“测试规则”中保存测试通过报告,再提交审核。')
return
}
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
reviewSubmitOpen.value = true
@@ -1323,6 +1428,10 @@ export default {
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
return
}
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
toast('当前规则版本尚未确认测试通过,不能提交审核。')
return
}
const version = normalizeText(reviewSubmitVersion.value)
const reviewer = normalizeText(reviewSubmitReviewer.value)
if (!version) {
@@ -1357,6 +1466,155 @@ export default {
}
}
function openRiskRuleTestDialog() {
if (!canOpenRiskRuleTest.value) {
if (!selectedSkill.value?.id) {
toast('规则详情还没有加载完成,请稍后再测试。')
}
return
}
riskRuleTestOpen.value = true
}
function closeRiskRuleTestDialog() {
riskRuleTestOpen.value = false
}
async function handleRiskRuleReportSaved(summary) {
if (selectedSkill.value) {
selectedSkill.value.latestTestSummary = summary
}
await refreshCurrentAssets()
if (selectedSkill.value?.id) {
await loadSelectedAssetDetail(selectedSkill.value.id)
}
}
function openDeleteRiskRuleDialog() {
if (!canDeleteRiskRule.value) {
return
}
riskRuleDeleteOpen.value = true
}
function closeDeleteRiskRuleDialog() {
if (detailBusy.value) {
return
}
riskRuleDeleteOpen.value = false
}
async function deleteSelectedRiskRule() {
if (!selectedSkill.value || !canDeleteRiskRule.value || detailBusy.value) {
return
}
actionState.value = 'delete-risk-rule'
try {
await deleteAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
riskRuleDeleteOpen.value = false
const deletedName = selectedSkill.value.name
closeDetail()
await refreshCurrentAssets()
toast(`风险规则“${deletedName}”已删除。`)
} catch (error) {
toast(error?.message || '风险规则删除失败,请稍后重试。')
} finally {
actionState.value = ''
}
}
function openReturnRiskRuleDialog() {
if (!canReturnRiskRule.value) {
return
}
riskRuleReturnNote.value = ''
riskRuleReturnOpen.value = true
}
function closeReturnRiskRuleDialog() {
if (detailBusy.value) {
return
}
riskRuleReturnOpen.value = false
}
async function returnSelectedRiskRule() {
if (!selectedSkill.value || !canReturnRiskRule.value || detailBusy.value) {
return
}
const note = normalizeText(riskRuleReturnNote.value)
if (!note) {
toast('请填写回退原因。')
return
}
actionState.value = 'return-risk-rule'
try {
await returnRiskRuleAsset(selectedSkill.value.id, { note }, { actor: resolveActor() })
riskRuleReturnOpen.value = false
await refreshCurrentAssets()
await loadSelectedAssetDetail(selectedSkill.value.id)
toast('风险规则已回退到草稿。')
} catch (error) {
toast(error?.message || '风险规则回退失败,请稍后重试。')
} finally {
actionState.value = ''
}
}
function openPublishRiskRuleDialog() {
if (!canPublishRiskRule.value) {
if (!riskRuleTestPassed.value) {
toast('请先确认测试报告通过,再发布上线。')
}
return
}
riskRulePublishOpen.value = true
}
function closePublishRiskRuleDialog() {
if (detailBusy.value) {
return
}
riskRulePublishOpen.value = false
}
async function publishSelectedRiskRule() {
if (!selectedSkill.value || !canPublishRiskRule.value || detailBusy.value) {
return
}
actionState.value = 'publish-risk-rule'
try {
await publishRiskRuleAsset(selectedSkill.value.id, { actor: resolveActor() })
riskRulePublishOpen.value = false
await refreshCurrentAssets()
await loadSelectedAssetDetail(selectedSkill.value.id)
toast('风险规则已发布上线。')
} catch (error) {
toast(error?.message || '风险规则发布失败,请稍后重试。')
} finally {
actionState.value = ''
}
}
async function toggleSelectedRiskRuleEnabled() {
if (!selectedSkill.value || !canToggleRiskRuleEnabled.value || detailBusy.value) {
return
}
const assetId = selectedSkill.value.id
const nextEnabled = !selectedSkill.value.isEnabledValue
actionState.value = 'toggle-risk-rule-enabled'
try {
await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
await refreshCurrentAssets()
await loadSelectedAssetDetail(assetId)
toast(nextEnabled ? '风险规则已启用。' : '风险规则已停用,不会进入业务扫描。')
} catch (error) {
toast(error?.message || '风险规则启用状态更新失败,请稍后重试。')
} finally {
actionState.value = ''
}
}
async function activateSelectedRule() {
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
return
@@ -1458,6 +1716,8 @@ export default {
showVersionColumn,
showMetricColumn,
showStatusColumn,
showOnlineColumn,
showEnabledColumn,
visibleSkills,
auditEmptyState,
loading,
@@ -1468,21 +1728,37 @@ export default {
selectedOwner,
selectedStatus,
selectedRiskScenario,
selectedOnlineState,
selectedEnabledState,
selectedDomainLabel,
selectedOwnerLabel,
selectedStatusLabel,
selectedRiskScenarioLabel,
selectedOnlineStateLabel,
selectedEnabledStateLabel,
showRiskScenarioFilter,
showStatusFilter,
showOnlineFilter,
showEnabledFilter,
domainOptions,
ownerOptions,
statusOptions: STATUS_OPTIONS,
riskScenarioOptions: RISK_SCENARIO_OPTIONS,
onlineStateOptions: ONLINE_STATE_OPTIONS,
enabledStateOptions: ENABLED_STATE_OPTIONS,
activeFilterPopover,
activeFilterTokens,
canManageSelected,
canEditSelected,
canCreateRiskRule,
canOpenRiskRuleTest,
canDeleteRiskRule,
canSubmitRiskRuleReview,
canReturnRiskRule,
canPublishRiskRule,
canToggleRiskRuleEnabled,
riskRuleTestPassed,
riskRuleInReview,
canSubmitReview,
hasReviewSubmitReviewers,
canReviewSelected,
@@ -1509,7 +1785,13 @@ export default {
riskRuleCreateOpen,
riskRuleCreateForm,
riskRuleCreateBusy,
riskRuleTestOpen,
riskRuleDeleteOpen,
riskRuleReturnOpen,
riskRulePublishOpen,
riskRuleReturnNote,
riskRuleCreateDomainOptions: RISK_RULE_CREATE_DOMAIN_OPTIONS,
riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
riskRuleLevelOptions: RISK_RULE_LEVEL_OPTIONS,
showReviewNote,
spreadsheetUploadInput,
@@ -1549,6 +1831,19 @@ export default {
openSubmitReviewDialog,
closeSubmitReviewDialog,
submitSelectedRuleForReview,
openRiskRuleTestDialog,
closeRiskRuleTestDialog,
handleRiskRuleReportSaved,
openDeleteRiskRuleDialog,
closeDeleteRiskRuleDialog,
deleteSelectedRiskRule,
openReturnRiskRuleDialog,
closeReturnRiskRuleDialog,
returnSelectedRiskRule,
openPublishRiskRuleDialog,
closePublishRiskRuleDialog,
publishSelectedRiskRule,
toggleSelectedRiskRuleEnabled,
activateSelectedRule,
restoreSelectedVersion,
openVersionTimeline,