refactor(audit): reuse list shells and split models
This commit is contained in:
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
export function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
export function readConfigJson(value) {
|
||||
if (isPlainObject(value?.configJson)) {
|
||||
return value.configJson
|
||||
}
|
||||
if (isPlainObject(value?.config_json)) {
|
||||
return value.config_json
|
||||
}
|
||||
return {}
|
||||
}
|
||||
128
web/src/views/scripts/auditViewFormatters.js
Normal file
128
web/src/views/scripts/auditViewFormatters.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
DOMAIN_LABELS,
|
||||
REVIEW_META,
|
||||
SCENARIO_LABELS,
|
||||
STATUS_META
|
||||
} from './auditViewMetadata.js'
|
||||
import { normalizeText } from './auditViewDataUtils.js'
|
||||
|
||||
export function makeShort(value) {
|
||||
const text = normalizeText(value).replace(/\s+/g, '')
|
||||
if (!text) {
|
||||
return 'AG'
|
||||
}
|
||||
return text.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
export function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '未记录'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
.format(date)
|
||||
.replace(/\//g, '-')
|
||||
}
|
||||
|
||||
export function resolveDomainLabel(value) {
|
||||
return DOMAIN_LABELS[value] || normalizeText(value) || '未分类'
|
||||
}
|
||||
|
||||
export function resolveStatusMeta(value) {
|
||||
return STATUS_META[value] || { label: normalizeText(value) || '未知状态', tone: 'draft' }
|
||||
}
|
||||
|
||||
export function resolveReviewMeta(value) {
|
||||
return REVIEW_META[value] || { label: '暂无审核', tone: 'draft' }
|
||||
}
|
||||
|
||||
export function resolveTimelineEventMeta(value) {
|
||||
return {
|
||||
created: { label: '创建工作稿', icon: 'mdi mdi-file-document-edit-outline', tone: 'draft' },
|
||||
submitted: { label: '提交审核', icon: 'mdi mdi-send-outline', tone: 'warning' },
|
||||
approved: { label: '审核通过', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
||||
rejected: { label: '审核驳回', icon: 'mdi mdi-close-octagon-outline', tone: 'danger' },
|
||||
published: { label: '正式上线', icon: 'mdi mdi-rocket-launch-outline', tone: 'success' },
|
||||
restored: { label: '恢复生成工作稿', icon: 'mdi mdi-history', tone: 'info' }
|
||||
}[normalizeText(value)] || { label: normalizeText(value) || '版本事件', icon: 'mdi mdi-circle-medium', tone: 'draft' }
|
||||
}
|
||||
|
||||
export function resolveDiffChangeMeta(value) {
|
||||
return {
|
||||
added: { label: '新增', tone: 'success' },
|
||||
removed: { label: '删除', tone: 'danger' },
|
||||
modified: { label: '修改', tone: 'warning' }
|
||||
}[normalizeText(value)] || { label: normalizeText(value) || '变化', tone: 'draft' }
|
||||
}
|
||||
|
||||
export function formatScenarioList(items) {
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return '未配置场景'
|
||||
}
|
||||
|
||||
return items
|
||||
.map((item) => SCENARIO_LABELS[item] || item)
|
||||
.filter(Boolean)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
export function resolveTypeKey(assetType) {
|
||||
if (assetType === 'rule') {
|
||||
return 'rules'
|
||||
}
|
||||
if (assetType === 'mcp') {
|
||||
return 'mcp'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function formatSeverity(value) {
|
||||
const severity = normalizeText(value).toLowerCase()
|
||||
if (severity === 'high') {
|
||||
return '高风险'
|
||||
}
|
||||
if (severity === 'medium') {
|
||||
return '中风险'
|
||||
}
|
||||
if (severity === 'low') {
|
||||
return '低风险'
|
||||
}
|
||||
return '未配置'
|
||||
}
|
||||
|
||||
export function formatInputSummary(items) {
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return '无输入'
|
||||
}
|
||||
return `${items.length} 项输入`
|
||||
}
|
||||
|
||||
export function formatOutputSummary(items) {
|
||||
if (!Array.isArray(items) || !items.length) {
|
||||
return '无输出'
|
||||
}
|
||||
return `${items.length} 项输出`
|
||||
}
|
||||
|
||||
export function formatSpreadsheetChangeSummary(summary) {
|
||||
const normalized = normalizeText(summary)
|
||||
return (
|
||||
normalized
|
||||
.replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '')
|
||||
.replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '')
|
||||
.replace(/^保存表格[::]\s*/i, '')
|
||||
.trim() || '表格内容已保存。'
|
||||
)
|
||||
}
|
||||
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
resolveRiskRuleScoreLabel,
|
||||
resolveRiskRuleScoreLevel,
|
||||
resolveRiskRuleSeverity,
|
||||
resolveRiskRuleSeverityLabel
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
import { normalizeText } from './auditViewDataUtils.js'
|
||||
import {
|
||||
formatDateTime,
|
||||
formatScenarioList,
|
||||
formatSeverity,
|
||||
makeShort,
|
||||
resolveDomainLabel,
|
||||
resolveStatusMeta,
|
||||
resolveTypeKey
|
||||
} from './auditViewFormatters.js'
|
||||
import {
|
||||
buildRiskListSubtitle,
|
||||
isJsonRiskRuleSource,
|
||||
isSpreadsheetRuleSource,
|
||||
readRuleDocumentMeta,
|
||||
resolveRuleScenarioCategory,
|
||||
resolveRuleScenarioList,
|
||||
resolveTabId,
|
||||
resolveTabMeta
|
||||
} from './auditViewRuleClassifier.js'
|
||||
import {
|
||||
resolveRiskRuleBusinessStage,
|
||||
resolveRiskRuleEnabled,
|
||||
resolveRiskRuleOnlineMeta
|
||||
} from './auditViewRiskRuleState.js'
|
||||
export function findLatestMcpCall(runs, assetCode) {
|
||||
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
|
||||
|
||||
for (const run of runs) {
|
||||
for (const toolCall of run.tool_calls || []) {
|
||||
const toolName = normalizeText(toolCall.tool_name)
|
||||
if (
|
||||
toolName === expectedToolName ||
|
||||
toolName.endsWith(expectedToolName) ||
|
||||
expectedToolName.endsWith(toolName)
|
||||
) {
|
||||
return {
|
||||
run,
|
||||
toolCall
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function buildRowRuntime(asset, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return formatSeverity(asset.config_json?.severity)
|
||||
}
|
||||
if (typeKey === 'mcp') {
|
||||
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function buildRowMetric(asset, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return normalizeText(asset.modified_by) || '未记录'
|
||||
}
|
||||
if (typeKey === 'mcp') {
|
||||
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
export function buildListItem(asset) {
|
||||
const typeKey = resolveTypeKey(asset.asset_type)
|
||||
const tabId = resolveTabId(asset, typeKey)
|
||||
if (!tabId) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tabMeta = resolveTabMeta(tabId, typeKey)
|
||||
const statusMeta = resolveStatusMeta(asset.status)
|
||||
const workingVersion = asset.working_version || asset.current_version || '-'
|
||||
const changeCount =
|
||||
typeof asset.change_count === 'number'
|
||||
? asset.change_count
|
||||
: Array.isArray(asset.recent_versions)
|
||||
? Math.max(asset.recent_versions.length - 1, 0)
|
||||
: 0
|
||||
const modifiedBy =
|
||||
normalizeText(asset.modified_by) ||
|
||||
normalizeText(
|
||||
Array.isArray(asset.recent_versions)
|
||||
? asset.recent_versions.find((item) => item.version === workingVersion)?.created_by
|
||||
: ''
|
||||
)
|
||||
const isRiskRule = tabId === 'riskRules'
|
||||
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(asset)
|
||||
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(asset)
|
||||
const ruleDocument = readRuleDocumentMeta(asset)
|
||||
const ruleScenarioCategory = typeKey === 'rules' ? resolveRuleScenarioCategory(asset, tabId) : ''
|
||||
const listSubtitle = isRiskRule
|
||||
? buildRiskListSubtitle(asset.description)
|
||||
: normalizeText(asset.description)
|
||||
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
|
||||
const isOnlineValue = onlineMeta.online
|
||||
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
|
||||
const reviewer = normalizeText(asset.reviewer) || '待分配'
|
||||
const creator =
|
||||
normalizeText(asset.owner) ||
|
||||
normalizeText(asset.config_json?.generation_request?.actor) ||
|
||||
modifiedBy ||
|
||||
'未知'
|
||||
const publisher = isRiskRule ? creator : ''
|
||||
const riskRuleCreatedAt = formatDateTime(asset.created_at || asset.updated_at)
|
||||
const businessStage = usesJsonRiskRule
|
||||
? resolveRiskRuleBusinessStage(asset)
|
||||
: { value: '', label: '' }
|
||||
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(asset, tabId) : []
|
||||
const riskScoreLevel = usesJsonRiskRule
|
||||
? resolveRiskRuleScoreLevel(asset.config_json, asset.config_json)
|
||||
: ''
|
||||
const riskLevelValue = usesJsonRiskRule
|
||||
? riskScoreLevel || resolveRiskRuleSeverity(asset.config_json)
|
||||
: ''
|
||||
const riskLevelLabel = usesJsonRiskRule
|
||||
? riskScoreLevel
|
||||
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: resolveRiskRuleSeverityLabel(asset.config_json)
|
||||
: ''
|
||||
const displayName = asset.name
|
||||
const displayCode = asset.code
|
||||
const displaySummary = listSubtitle
|
||||
const displayOwner = isRiskRule ? creator : asset.owner
|
||||
const displayReviewer = reviewer
|
||||
const displayCategory = resolveDomainLabel(asset.domain)
|
||||
const displayScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json)
|
||||
const displayEnabledValue = isEnabledValue
|
||||
const displayEnabledLabel = isEnabledValue ? '是' : '否'
|
||||
const displayEnabledTone = isEnabledValue ? 'success' : 'disabled'
|
||||
const searchText = [
|
||||
displayName,
|
||||
displayCode,
|
||||
displaySummary,
|
||||
displayOwner,
|
||||
displayScope,
|
||||
riskLevelLabel
|
||||
]
|
||||
.map((value) => normalizeText(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
tabId,
|
||||
type: typeKey,
|
||||
isPreviewMock: Boolean(asset.isPreviewMock),
|
||||
usesSpreadsheetRule,
|
||||
usesJsonRiskRule,
|
||||
ruleDocument,
|
||||
typeLabel: tabMeta.typeLabel,
|
||||
short: makeShort(displayName),
|
||||
name: displayName,
|
||||
code: displayCode,
|
||||
rawCode: asset.code,
|
||||
summary: displaySummary,
|
||||
listSubtitle: displaySummary,
|
||||
category: displayCategory,
|
||||
owner: displayOwner,
|
||||
reviewer: displayReviewer,
|
||||
scope: displayScope,
|
||||
riskCategory: ruleScenarioCategory,
|
||||
scenarioList: ruleScenarioList,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
riskLevelValue,
|
||||
riskLevelLabel,
|
||||
riskLevelTone: riskLevelValue,
|
||||
model: buildRowRuntime(asset, typeKey),
|
||||
version: workingVersion,
|
||||
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
||||
publishedVersion: asset.published_version || '-',
|
||||
workingVersion,
|
||||
status: statusMeta.label,
|
||||
statusValue: asset.status,
|
||||
statusTone: statusMeta.tone,
|
||||
hitRate: isRiskRule ? publisher : buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
|
||||
creator,
|
||||
publisher,
|
||||
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
|
||||
isOnlineValue,
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue: displayEnabledValue,
|
||||
isEnabledLabel: displayEnabledLabel,
|
||||
isEnabledTone: displayEnabledTone,
|
||||
modifiedBy,
|
||||
changeCount,
|
||||
updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at),
|
||||
badgeTone: tabMeta.badgeTone,
|
||||
domainValue: asset.domain,
|
||||
searchText
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
193
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
193
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
buildRiskRuleFieldSummary,
|
||||
formatRiskRuleAge,
|
||||
resolveRiskRuleBusinessDescription,
|
||||
resolveRiskRuleCreatedAt,
|
||||
resolveRiskRuleFields,
|
||||
resolveRiskRuleFlow,
|
||||
resolveRiskRuleFlowDiagramSvg,
|
||||
resolveRiskRuleScore,
|
||||
resolveRiskRuleScoreDetail,
|
||||
resolveRiskRuleScoreLabel,
|
||||
resolveRiskRuleScoreLevel,
|
||||
resolveRiskRuleSeverity,
|
||||
resolveRiskRuleSeverityLabel
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
import {
|
||||
isPlainObject,
|
||||
normalizeText,
|
||||
readConfigJson
|
||||
} from './auditViewDataUtils.js'
|
||||
import { formatDateTime } from './auditViewFormatters.js'
|
||||
import {
|
||||
resolveRiskRuleCategory,
|
||||
resolveRiskRuleDescription,
|
||||
resolveRiskRuleSourceRef
|
||||
} from './auditViewRuleClassifier.js'
|
||||
export function resolveRiskRuleEnabled(source, rulePayload = null) {
|
||||
const configJson = readConfigJson(source)
|
||||
if (isPlainObject(rulePayload) && rulePayload.enabled === false) {
|
||||
return false
|
||||
}
|
||||
if (source?.enabled === false || configJson.enabled === false) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const LAST_OPERATION_LABELS = {
|
||||
generate: '开始生成',
|
||||
create: '创建',
|
||||
test: '测试',
|
||||
online: '上线',
|
||||
offline: '下线',
|
||||
delete: '删除',
|
||||
update: '更新'
|
||||
}
|
||||
|
||||
const RISK_RULE_BUSINESS_STAGE_LABELS = {
|
||||
expense_application: '费用申请',
|
||||
reimbursement: '费用报销'
|
||||
}
|
||||
|
||||
export 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 || '费用报销'
|
||||
}
|
||||
}
|
||||
|
||||
export 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 applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||
const rulePayload = isPlainObject(payload) ? payload : {}
|
||||
const metadata = rulePayload.metadata && typeof rulePayload.metadata === 'object'
|
||||
? rulePayload.metadata
|
||||
: {}
|
||||
const apiConfig = apiPayload?.config_json && typeof apiPayload.config_json === 'object'
|
||||
? apiPayload.config_json
|
||||
: {}
|
||||
const fullDescription =
|
||||
resolveRiskRuleDescription(rulePayload) ||
|
||||
normalizeText(apiPayload?.description) ||
|
||||
normalizeText(target.riskRuleDescription)
|
||||
const riskCategory =
|
||||
normalizeText(metadata.expense_category_label) ||
|
||||
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 onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
|
||||
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
|
||||
|
||||
const publisher =
|
||||
target.creator ||
|
||||
normalizeText(apiPayload?.owner) ||
|
||||
normalizeText(metadata.created_by) ||
|
||||
normalizeText(apiPayload?.recent_versions?.[0]?.created_by) ||
|
||||
'未知'
|
||||
|
||||
let publishedAt = target.publishedAt || '-'
|
||||
if (apiPayload?.recent_versions) {
|
||||
const history = buildHistory(apiPayload.recent_versions, { ...target, config_json: payload })
|
||||
const publishedVersionObj = history.find((item) => item.isPublished || item.lifecycleState === 'published')
|
||||
publishedAt = publishedVersionObj ? publishedVersionObj.time : (apiPayload?.latest_review?.reviewed_at ? formatDateTime(apiPayload.latest_review.reviewed_at) : '-')
|
||||
} else if (apiPayload?.latest_review?.reviewed_at) {
|
||||
publishedAt = formatDateTime(apiPayload.latest_review.reviewed_at)
|
||||
}
|
||||
|
||||
return {
|
||||
...target,
|
||||
riskRuleDescription: fullDescription,
|
||||
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
|
||||
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
||||
riskCategory,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
scope: riskCategory,
|
||||
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
||||
riskRuleSeverity: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||
riskRuleSeverityLabel: riskRuleScoreLevel
|
||||
? resolveRiskRuleScoreLabel(rulePayload, apiConfig)
|
||||
: resolveRiskRuleSeverityLabel(rulePayload),
|
||||
riskRuleScore: resolveRiskRuleScore(rulePayload, apiConfig),
|
||||
riskRuleScoreLabel: resolveRiskRuleScoreLabel(rulePayload, apiConfig),
|
||||
riskRuleScoreLevel: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||
riskRuleScoreDetail: resolveRiskRuleScoreDetail(rulePayload, apiConfig),
|
||||
riskRuleCreatedAt: formatDateTime(riskRuleCreatedAt),
|
||||
riskRuleAgeLabel: formatRiskRuleAge(riskRuleCreatedAt),
|
||||
riskRuleFields,
|
||||
riskRuleFieldSummary: buildRiskRuleFieldSummary(riskRuleFields),
|
||||
riskRuleFlow: resolveRiskRuleFlow(rulePayload, riskRuleFields),
|
||||
riskRuleFlowDiagramSvg: resolveRiskRuleFlowDiagramSvg({
|
||||
...rulePayload,
|
||||
flow_diagram_svg: normalizeText(apiPayload?.flow_diagram_svg) || rulePayload?.flow_diagram_svg
|
||||
}),
|
||||
riskRuleRequiresAttachment: Boolean(
|
||||
rulePayload.requires_attachment ||
|
||||
metadata.requires_attachment ||
|
||||
apiConfig.requires_attachment ||
|
||||
target.configJson?.requires_attachment
|
||||
),
|
||||
riskRuleSummary: {
|
||||
name: apiPayload?.name || target.name,
|
||||
evaluator: apiPayload?.evaluator || rulePayload.evaluator || '',
|
||||
ontologySignal: apiPayload?.ontology_signal || rulePayload.ontology_signal || '',
|
||||
inputs: apiPayload?.inputs || rulePayload.inputs || {},
|
||||
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
|
||||
},
|
||||
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
|
||||
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
|
||||
}
|
||||
}
|
||||
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
@@ -0,0 +1,328 @@
|
||||
import {
|
||||
JSON_RISK_DETAIL_MODE,
|
||||
LEGACY_RISK_SCENARIO_KEYS,
|
||||
RISK_SCENARIO_VALUES,
|
||||
RULE_TAB_TAG_ALIASES,
|
||||
SPREADSHEET_DETAIL_MODE,
|
||||
TAB_META,
|
||||
TYPE_META
|
||||
} from './auditViewMetadata.js'
|
||||
import {
|
||||
isPlainObject,
|
||||
normalizeText,
|
||||
readConfigJson
|
||||
} from './auditViewDataUtils.js'
|
||||
import { formatScenarioList } from './auditViewFormatters.js'
|
||||
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||
travel: '差旅费',
|
||||
hotel: '住宿费',
|
||||
transport: '交通费',
|
||||
meal: '业务招待费',
|
||||
meeting: '会务费',
|
||||
marketing: '市场推广费',
|
||||
office: '办公用品费',
|
||||
training: '培训费',
|
||||
software: '软件服务费',
|
||||
communication: '通信费',
|
||||
welfare: '福利费'
|
||||
}
|
||||
|
||||
export function readRuleDocumentMeta(value) {
|
||||
const configJson = readConfigJson(value)
|
||||
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
||||
}
|
||||
|
||||
export function isSpreadsheetRuleSource(value) {
|
||||
const configJson = readConfigJson(value)
|
||||
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
|
||||
}
|
||||
|
||||
export function isJsonRiskRuleSource(value) {
|
||||
const configJson = readConfigJson(value)
|
||||
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
|
||||
}
|
||||
|
||||
export function normalizeRuleTagValue(value) {
|
||||
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
|
||||
}
|
||||
|
||||
export function collectRuleTagValues(source) {
|
||||
const configJson = readConfigJson(source)
|
||||
const rawValues = [
|
||||
configJson.tag,
|
||||
configJson.rule_tag,
|
||||
...(Array.isArray(configJson.tags) ? configJson.tags : []),
|
||||
...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : [])
|
||||
]
|
||||
|
||||
return rawValues.map((item) => normalizeText(item)).filter(Boolean)
|
||||
}
|
||||
|
||||
export function resolveRuleTabId(source) {
|
||||
const code = normalizeText(source?.code || '').toLowerCase()
|
||||
if (code.startsWith('risk.')) {
|
||||
return 'riskRules'
|
||||
}
|
||||
if (isJsonRiskRuleSource(source)) {
|
||||
return 'riskRules'
|
||||
}
|
||||
|
||||
const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item))
|
||||
|
||||
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) {
|
||||
return 'riskRules'
|
||||
}
|
||||
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) {
|
||||
return 'financialRules'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function resolveTabId(source, typeKey) {
|
||||
if (typeKey === 'rules') {
|
||||
return resolveRuleTabId(source)
|
||||
}
|
||||
return typeKey
|
||||
}
|
||||
|
||||
export function resolveTabMeta(tabId, typeKey) {
|
||||
if (TAB_META[tabId]) {
|
||||
return TAB_META[tabId]
|
||||
}
|
||||
if (typeKey === 'rules') {
|
||||
return {
|
||||
...TYPE_META.rules,
|
||||
typeKey: 'rules',
|
||||
badgeTone: 'primary'
|
||||
}
|
||||
}
|
||||
return TAB_META[typeKey]
|
||||
}
|
||||
|
||||
export function resolveRiskRuleDescription(payload) {
|
||||
if (!isPlainObject(payload)) {
|
||||
return ''
|
||||
}
|
||||
return normalizeText(payload.description)
|
||||
}
|
||||
|
||||
export function resolveRiskRuleSourceRef(payload) {
|
||||
if (!isPlainObject(payload)) {
|
||||
return ''
|
||||
}
|
||||
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
|
||||
return normalizeText(metadata.source_ref)
|
||||
}
|
||||
|
||||
export function inferRiskCategoryFromCode(code) {
|
||||
const normalized = normalizeText(code).toLowerCase()
|
||||
if (normalized.startsWith('risk.travel.')) {
|
||||
return '差旅'
|
||||
}
|
||||
if (normalized.startsWith('risk.invoice.')) {
|
||||
return '发票'
|
||||
}
|
||||
if (normalized.includes('entertainment') || normalized.includes('meal_localized')) {
|
||||
return '餐饮招待'
|
||||
}
|
||||
if (normalized.includes('consecutive_transport')) {
|
||||
return '交通出行'
|
||||
}
|
||||
if (normalized.startsWith('risk.expense.')) {
|
||||
return '费用科目'
|
||||
}
|
||||
return '通用'
|
||||
}
|
||||
|
||||
export function normalizeRiskScenarioCategory(value) {
|
||||
const normalized = normalizeText(value)
|
||||
const alias = normalized === '通讯费' ? '通信费' : normalized
|
||||
return RISK_SCENARIO_VALUES.has(alias) ? alias : ''
|
||||
}
|
||||
|
||||
export function normalizeExpenseTypeScenarioLabels(value) {
|
||||
const values = Array.isArray(value) ? value : normalizeText(value) ? [value] : []
|
||||
const labels = []
|
||||
const seen = new Set()
|
||||
|
||||
values.forEach((item) => {
|
||||
const key = normalizeText(item).toLowerCase()
|
||||
const label = EXPENSE_TYPE_SCENARIO_LABELS[key] || normalizeRiskScenarioCategory(item)
|
||||
if (!label || seen.has(label)) {
|
||||
return
|
||||
}
|
||||
seen.add(label)
|
||||
labels.push(label)
|
||||
})
|
||||
|
||||
return labels
|
||||
}
|
||||
|
||||
export function readRiskRuleExpenseTypes(source) {
|
||||
const configJson = readConfigJson(source)
|
||||
const metadata = isPlainObject(configJson.metadata) ? configJson.metadata : {}
|
||||
const appliesTo = isPlainObject(configJson.applies_to) ? configJson.applies_to : {}
|
||||
const values = []
|
||||
|
||||
;[
|
||||
configJson.expense_types,
|
||||
metadata.expense_types,
|
||||
appliesTo.expense_types,
|
||||
source?.expense_types
|
||||
].forEach((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
values.push(...item)
|
||||
} else if (normalizeText(item)) {
|
||||
values.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
export function readScenarioItems(source) {
|
||||
if (Array.isArray(source?.scenario_json)) {
|
||||
return source.scenario_json
|
||||
}
|
||||
if (Array.isArray(source?.scenarioList)) {
|
||||
return source.scenarioList
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function resolveRiskRuleCategory(source) {
|
||||
const configJson = readConfigJson(source)
|
||||
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||
if (expenseScenarioLabels.length) {
|
||||
return formatScenarioList(expenseScenarioLabels)
|
||||
}
|
||||
|
||||
const expenseCategoryLabel =
|
||||
normalizeText(configJson.expense_category_label) ||
|
||||
normalizeText(configJson.metadata?.expense_category_label) ||
|
||||
normalizeText(source?.expense_category_label)
|
||||
if (expenseCategoryLabel) {
|
||||
return expenseCategoryLabel
|
||||
}
|
||||
|
||||
const explicit = normalizeRiskScenarioCategory(configJson.risk_category)
|
||||
if (explicit) {
|
||||
return explicit
|
||||
}
|
||||
|
||||
const payloadCategory = normalizeRiskScenarioCategory(source?.risk_category)
|
||||
if (payloadCategory) {
|
||||
return payloadCategory
|
||||
}
|
||||
|
||||
const scenarioItems = readScenarioItems(source)
|
||||
const businessScenario = scenarioItems
|
||||
.map((item) => normalizeText(item))
|
||||
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item) && RISK_SCENARIO_VALUES.has(item))
|
||||
if (businessScenario) {
|
||||
return businessScenario
|
||||
}
|
||||
|
||||
return inferRiskCategoryFromCode(source?.code)
|
||||
}
|
||||
|
||||
export function inferFinancialRuleCategory(source) {
|
||||
const configJson = readConfigJson(source)
|
||||
const explicit =
|
||||
normalizeRiskScenarioCategory(configJson.scenario_category) ||
|
||||
normalizeRiskScenarioCategory(configJson.ai_review_category) ||
|
||||
normalizeRiskScenarioCategory(configJson.risk_category) ||
|
||||
normalizeRiskScenarioCategory(source?.scenario_category) ||
|
||||
normalizeRiskScenarioCategory(source?.risk_category)
|
||||
if (explicit) {
|
||||
return explicit
|
||||
}
|
||||
|
||||
const scenarioCategory = readScenarioItems(source)
|
||||
.map((item) => normalizeRiskScenarioCategory(item))
|
||||
.find(Boolean)
|
||||
if (scenarioCategory) {
|
||||
return scenarioCategory
|
||||
}
|
||||
|
||||
const configRuntimeRule = isPlainObject(configJson.runtime_rule) ? configJson.runtime_rule : {}
|
||||
const haystack = [
|
||||
source?.code,
|
||||
source?.name,
|
||||
source?.description,
|
||||
configJson.runtime_kind,
|
||||
configRuntimeRule.kind,
|
||||
configRuntimeRule.scenario,
|
||||
configRuntimeRule.template_key,
|
||||
...readScenarioItems(source)
|
||||
]
|
||||
.map((item) => normalizeText(item).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
if (!haystack) {
|
||||
return '通用'
|
||||
}
|
||||
if (/(travel|trip|差旅|出差|住宿|酒店)/i.test(haystack)) {
|
||||
return '差旅'
|
||||
}
|
||||
if (/(invoice|receipt|attachment|票据|发票|单据|附件)/i.test(haystack)) {
|
||||
return '发票'
|
||||
}
|
||||
if (/(meal|dining|entertainment|餐饮|招待|餐费|用餐)/i.test(haystack)) {
|
||||
return '餐饮招待'
|
||||
}
|
||||
if (/(transport|traffic|taxi|交通|出行|打车|机票|火车|高铁|地铁|公交)/i.test(haystack)) {
|
||||
return '交通出行'
|
||||
}
|
||||
if (/(office|material|suppl|办公|物料|耗材)/i.test(haystack)) {
|
||||
return '办公物料'
|
||||
}
|
||||
if (/(communication|telecom|phone|通信|通讯|手机)/i.test(haystack)) {
|
||||
return '通信费'
|
||||
}
|
||||
if (/(welfare|福利)/i.test(haystack)) {
|
||||
return '福利费'
|
||||
}
|
||||
if (/(expense_standard|费用科目|费用标准|补贴|科目)/i.test(haystack)) {
|
||||
return '费用科目'
|
||||
}
|
||||
return '通用'
|
||||
}
|
||||
|
||||
export function resolveRuleScenarioCategory(source, tabId = '') {
|
||||
const scenarioList = resolveRuleScenarioList(source, tabId)
|
||||
if (scenarioList.length) {
|
||||
return formatScenarioList(scenarioList)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function resolveRuleScenarioList(source, tabId = '') {
|
||||
const resolvedTabId = tabId || resolveRuleTabId(source)
|
||||
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
||||
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||
if (expenseScenarioLabels.length) {
|
||||
return expenseScenarioLabels
|
||||
}
|
||||
const riskCategory = resolveRiskRuleCategory(source)
|
||||
return riskCategory ? [riskCategory] : []
|
||||
}
|
||||
if (resolvedTabId === 'financialRules') {
|
||||
const financialCategory = inferFinancialRuleCategory(source)
|
||||
return financialCategory ? [financialCategory] : []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildRiskListSubtitle(text, maxLength = 42) {
|
||||
const normalized = normalizeText(text)
|
||||
if (!normalized) {
|
||||
return '平台内置风险规则'
|
||||
}
|
||||
const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized
|
||||
if (firstSentence.length <= maxLength) {
|
||||
return firstSentence
|
||||
}
|
||||
return `${firstSentence.slice(0, maxLength)}…`
|
||||
}
|
||||
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
EXPENSE_RULE_BLOCK_PATTERN,
|
||||
RULE_SPREADSHEET_BLOCK_PATTERN,
|
||||
RULE_TEMPLATE_LABELS
|
||||
} from './auditViewMetadata.js'
|
||||
import {
|
||||
isPlainObject,
|
||||
normalizeText,
|
||||
readConfigJson
|
||||
} from './auditViewDataUtils.js'
|
||||
|
||||
export function cloneJsonObject(value) {
|
||||
if (!isPlainObject(value)) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
} catch {
|
||||
return { ...value }
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRuleTemplateLabel(value) {
|
||||
const templateKey = normalizeText(value)
|
||||
return RULE_TEMPLATE_LABELS[templateKey] || templateKey || '未指定模板'
|
||||
}
|
||||
|
||||
export function extractRuntimeRuleFromMarkdown(markdown) {
|
||||
const match = String(markdown || '').match(EXPENSE_RULE_BLOCK_PATTERN)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(match[1])
|
||||
return isPlainObject(payload) ? payload : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function extractSpreadsheetMetaFromMarkdown(markdown) {
|
||||
const match = String(markdown || '').match(RULE_SPREADSHEET_BLOCK_PATTERN)
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(match[1])
|
||||
return isPlainObject(payload) ? payload : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function stripRuntimeRuleBlock(markdown) {
|
||||
return String(markdown || '')
|
||||
.replace(EXPENSE_RULE_BLOCK_PATTERN, '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export function stringifyRuntimeRule(runtimeRule) {
|
||||
return JSON.stringify(isPlainObject(runtimeRule) ? runtimeRule : {}, null, 2)
|
||||
}
|
||||
|
||||
export function parseRuntimeRuleText(runtimeRuleText) {
|
||||
const text = normalizeText(runtimeRuleText)
|
||||
if (!text) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(text)
|
||||
return isPlainObject(payload) ? payload : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDefaultRuntimeRule(source) {
|
||||
const configJson = readConfigJson(source)
|
||||
const scenarioItems = Array.isArray(source?.scenario_json)
|
||||
? source.scenario_json
|
||||
: Array.isArray(source?.scenarioList)
|
||||
? source.scenarioList
|
||||
: []
|
||||
const configRuntimeRule = cloneJsonObject(configJson.runtime_rule)
|
||||
|
||||
return {
|
||||
kind: normalizeText(configRuntimeRule?.kind || configJson.runtime_kind) || 'policy_rule_draft',
|
||||
version:
|
||||
typeof configRuntimeRule?.version === 'number' && Number.isFinite(configRuntimeRule.version)
|
||||
? configRuntimeRule.version
|
||||
: 1,
|
||||
template_key:
|
||||
normalizeText(configRuntimeRule?.template_key || configJson.rule_template_key) || 'general_policy_v1',
|
||||
rule_name: normalizeText(configRuntimeRule?.rule_name || source?.name) || '未命名规则',
|
||||
scenario: normalizeText(configRuntimeRule?.scenario || scenarioItems[0]) || 'expense',
|
||||
review_required:
|
||||
typeof configRuntimeRule?.review_required === 'boolean' ? configRuntimeRule.review_required : true
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveRuntimeRuleForVersion(source, rawMarkdown, runtimeRuleFallback = null) {
|
||||
return (
|
||||
cloneJsonObject(extractRuntimeRuleFromMarkdown(rawMarkdown)) ||
|
||||
cloneJsonObject(runtimeRuleFallback) ||
|
||||
buildDefaultRuntimeRule(source)
|
||||
)
|
||||
}
|
||||
|
||||
export function buildMarkdownVersionContent(markdownContent, runtimeRule) {
|
||||
const body = stripRuntimeRuleBlock(markdownContent)
|
||||
const runtimeBlock = ['```expense-rule', stringifyRuntimeRule(runtimeRule), '```'].join('\n')
|
||||
return body ? `${body}\n\n${runtimeBlock}` : runtimeBlock
|
||||
}
|
||||
Reference in New Issue
Block a user