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 = { all: '全部', 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] : [] if (values.some((item) => ['all', '*', 'overall', 'general', '全部', '通用'].includes(normalizeText(item).toLowerCase()))) { return ['全部'] } 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)}…` }