2026-05-21 23:53:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
DETAIL_TITLES,
|
|
|
|
|
|
DOMAIN_LABELS,
|
|
|
|
|
|
EXPENSE_RULE_BLOCK_PATTERN,
|
|
|
|
|
|
JSON_RISK_DETAIL_MODE,
|
|
|
|
|
|
LEGACY_RISK_SCENARIO_KEYS,
|
|
|
|
|
|
PREVIEW_RULE_CODE,
|
|
|
|
|
|
PREVIEW_RULE_ID,
|
|
|
|
|
|
PREVIEW_RULE_VERSION_SPECS,
|
|
|
|
|
|
REVIEW_META,
|
|
|
|
|
|
RISK_SCENARIO_VALUES,
|
|
|
|
|
|
RULE_SPREADSHEET_BLOCK_PATTERN,
|
|
|
|
|
|
RULE_TAB_TAG_ALIASES,
|
|
|
|
|
|
RULE_TEMPLATE_LABELS,
|
|
|
|
|
|
SCENARIO_LABELS,
|
|
|
|
|
|
SPREADSHEET_DETAIL_MODE,
|
|
|
|
|
|
STATUS_META,
|
|
|
|
|
|
TAB_META,
|
|
|
|
|
|
TYPE_META,
|
|
|
|
|
|
VERSION_STATE_META
|
|
|
|
|
|
} from './auditViewMetadata.js'
|
2026-05-23 19:54:42 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildRiskRuleFieldSummary,
|
|
|
|
|
|
formatRiskRuleAge,
|
|
|
|
|
|
resolveRiskRuleBusinessDescription,
|
|
|
|
|
|
resolveRiskRuleCreatedAt,
|
|
|
|
|
|
resolveRiskRuleFields,
|
|
|
|
|
|
resolveRiskRuleFlow,
|
|
|
|
|
|
resolveRiskRuleFlowDiagramSvg,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
resolveRiskRuleScore,
|
|
|
|
|
|
resolveRiskRuleScoreDetail,
|
|
|
|
|
|
resolveRiskRuleScoreLabel,
|
|
|
|
|
|
resolveRiskRuleScoreLevel,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
resolveRiskRuleSeverity,
|
|
|
|
|
|
resolveRiskRuleSeverityLabel
|
|
|
|
|
|
} from './auditViewRiskRuleModel.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
|
|
|
|
|
travel: '差旅费',
|
|
|
|
|
|
hotel: '住宿费',
|
|
|
|
|
|
transport: '交通费',
|
|
|
|
|
|
meal: '业务招待费',
|
|
|
|
|
|
meeting: '会务费',
|
|
|
|
|
|
marketing: '市场推广费',
|
|
|
|
|
|
office: '办公用品费',
|
|
|
|
|
|
training: '培训费',
|
|
|
|
|
|
software: '软件服务费',
|
|
|
|
|
|
communication: '通信费',
|
|
|
|
|
|
welfare: '福利费'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
export {
|
|
|
|
|
|
DETAIL_TITLES,
|
|
|
|
|
|
DOMAIN_LABELS,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
ENABLED_STATE_OPTIONS,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
EXPENSE_RULE_BLOCK_PATTERN,
|
|
|
|
|
|
JSON_RISK_DETAIL_MODE,
|
|
|
|
|
|
LEGACY_RISK_SCENARIO_KEYS,
|
|
|
|
|
|
PREVIEW_RULE_CODE,
|
|
|
|
|
|
PREVIEW_RULE_ID,
|
|
|
|
|
|
PREVIEW_RULE_VERSION_SPECS,
|
|
|
|
|
|
REVIEW_META,
|
|
|
|
|
|
RISK_SCENARIO_OPTIONS,
|
|
|
|
|
|
RISK_SCENARIO_VALUES,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
RISK_RULE_TABLE_COLUMNS,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
RULE_SPREADSHEET_BLOCK_PATTERN,
|
|
|
|
|
|
RULE_TABLE_COLUMNS,
|
|
|
|
|
|
RULE_TAB_TAG_ALIASES,
|
|
|
|
|
|
RULE_TEMPLATE_LABELS,
|
|
|
|
|
|
SCENARIO_LABELS,
|
|
|
|
|
|
SPREADSHEET_DETAIL_MODE,
|
|
|
|
|
|
STATUS_META,
|
|
|
|
|
|
STATUS_OPTIONS,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
ONLINE_STATE_OPTIONS,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
TAB_META,
|
|
|
|
|
|
TYPE_META,
|
|
|
|
|
|
VERSION_STATE_META
|
|
|
|
|
|
} from './auditViewMetadata.js'
|
|
|
|
|
|
|
|
|
|
|
|
export function buildPreviewSpreadsheetMeta(spec) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
file_name: spec.fileName,
|
|
|
|
|
|
storage_key: `preview/agent-assets/${PREVIEW_RULE_ID}/${spec.version}/${encodeURIComponent(spec.fileName)}`,
|
|
|
|
|
|
mime_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
|
|
|
size_bytes: 82416,
|
|
|
|
|
|
checksum: `preview-${spec.version.replace(/\./g, '-')}`,
|
|
|
|
|
|
updated_at: spec.updatedAt,
|
|
|
|
|
|
updated_by: spec.updatedBy,
|
|
|
|
|
|
source: spec.source
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildPreviewSpreadsheetVersionMarkdown(spec) {
|
|
|
|
|
|
const metadata = buildPreviewSpreadsheetMeta(spec)
|
|
|
|
|
|
return [
|
|
|
|
|
|
'# 公司差旅费报销规则',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 规则载体',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- 页面状态:前端预览',
|
|
|
|
|
|
`- 当前规则版本:\`${spec.version}\``,
|
|
|
|
|
|
`- 表格文件:\`${spec.fileName}\``,
|
|
|
|
|
|
`- 最近更新人:${spec.updatedBy}`,
|
|
|
|
|
|
`- 最近更新时间:${spec.updatedAt}`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'## 说明',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'- 当前环境暂无真实规则文件,先用于展示 Excel 规则详情页布局。',
|
|
|
|
|
|
'- 真实上传、编辑与版本回写逻辑会在接好正式数据后启用。',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'```rule-spreadsheet',
|
|
|
|
|
|
JSON.stringify(metadata, null, 2),
|
|
|
|
|
|
'```'
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function createPreviewRuleDetailPayload() {
|
|
|
|
|
|
const recentVersions = PREVIEW_RULE_VERSION_SPECS.map((spec, index) => ({
|
|
|
|
|
|
id: `${PREVIEW_RULE_ID}-version-${index + 1}`,
|
|
|
|
|
|
asset_id: PREVIEW_RULE_ID,
|
|
|
|
|
|
version: spec.version,
|
|
|
|
|
|
content: buildPreviewSpreadsheetVersionMarkdown(spec),
|
|
|
|
|
|
content_type: 'markdown',
|
|
|
|
|
|
change_note: spec.note,
|
|
|
|
|
|
created_by: spec.updatedBy,
|
|
|
|
|
|
created_at: spec.updatedAt,
|
|
|
|
|
|
is_current: spec.isCurrent
|
|
|
|
|
|
}))
|
|
|
|
|
|
const currentSpec = PREVIEW_RULE_VERSION_SPECS[0]
|
|
|
|
|
|
const currentMeta = buildPreviewSpreadsheetMeta(currentSpec)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: PREVIEW_RULE_ID,
|
|
|
|
|
|
asset_type: 'rule',
|
|
|
|
|
|
code: PREVIEW_RULE_CODE,
|
|
|
|
|
|
name: '公司差旅费报销规则',
|
|
|
|
|
|
description: '前端预览态:先展示 Excel 规则详情页布局、版本卡片和编辑入口位置。',
|
|
|
|
|
|
domain: 'expense',
|
|
|
|
|
|
scenario_json: ['差旅'],
|
|
|
|
|
|
owner: '财务制度管理组',
|
|
|
|
|
|
reviewer: '顾承宇',
|
|
|
|
|
|
status: 'active',
|
|
|
|
|
|
current_version: currentSpec.version,
|
|
|
|
|
|
published_version: currentSpec.version,
|
|
|
|
|
|
working_version: currentSpec.version,
|
|
|
|
|
|
config_json: {
|
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
tag: '财务规则',
|
|
|
|
|
|
detail_mode: 'spreadsheet',
|
|
|
|
|
|
runtime_kind: 'travel_policy',
|
|
|
|
|
|
scenario_category: '差旅',
|
|
|
|
|
|
ai_review_category: '差旅',
|
|
|
|
|
|
rule_template_label: '差旅报销 Excel 模板',
|
|
|
|
|
|
rule_document: {
|
|
|
|
|
|
...currentMeta,
|
|
|
|
|
|
asset_version: currentSpec.version
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
created_at: '2026-05-10T11:10:00Z',
|
|
|
|
|
|
updated_at: currentSpec.updatedAt,
|
|
|
|
|
|
current_version_content: recentVersions[0].content,
|
|
|
|
|
|
current_version_content_type: 'markdown',
|
|
|
|
|
|
current_version_change_note: currentSpec.note,
|
|
|
|
|
|
recent_versions: recentVersions,
|
|
|
|
|
|
latest_review: {
|
|
|
|
|
|
id: `${PREVIEW_RULE_ID}-review-1`,
|
|
|
|
|
|
asset_id: PREVIEW_RULE_ID,
|
|
|
|
|
|
version: currentSpec.version,
|
|
|
|
|
|
reviewer: '顾承宇',
|
|
|
|
|
|
review_status: 'approved',
|
|
|
|
|
|
review_note: '当前为页面预览态,先确认布局与交互位置。',
|
|
|
|
|
|
reviewed_at: '2026-05-17T10:00:00Z',
|
|
|
|
|
|
created_at: '2026-05-17T10:00:00Z'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildPreviewRuleListItem() {
|
|
|
|
|
|
const payload = createPreviewRuleDetailPayload()
|
|
|
|
|
|
return {
|
|
|
|
|
|
...buildListItem(payload),
|
|
|
|
|
|
isPreviewMock: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildPreviewRuleDetail() {
|
|
|
|
|
|
const detail = buildDetailViewModel(createPreviewRuleDetailPayload(), [])
|
|
|
|
|
|
return {
|
|
|
|
|
|
...detail,
|
|
|
|
|
|
isPreviewMock: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
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}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
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: 'emerald'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2026-05-26 17:29:35 +08:00
|
|
|
|
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
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
|
|
|
|
|
if (expenseScenarioLabels.length) {
|
|
|
|
|
|
return formatScenarioList(expenseScenarioLabels)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const expenseCategoryLabel =
|
|
|
|
|
|
normalizeText(configJson.expense_category_label) ||
|
|
|
|
|
|
normalizeText(configJson.metadata?.expense_category_label) ||
|
|
|
|
|
|
normalizeText(source?.expense_category_label)
|
|
|
|
|
|
if (expenseCategoryLabel) {
|
|
|
|
|
|
return expenseCategoryLabel
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
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 '办公物料'
|
|
|
|
|
|
}
|
2026-05-26 17:29:35 +08:00
|
|
|
|
if (/(communication|telecom|phone|通信|通讯|手机)/i.test(haystack)) {
|
|
|
|
|
|
return '通信费'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(welfare|福利)/i.test(haystack)) {
|
|
|
|
|
|
return '福利费'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (/(expense_standard|费用科目|费用标准|补贴|科目)/i.test(haystack)) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return '费用科目'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '通用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRuleScenarioCategory(source, tabId = '') {
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const scenarioList = resolveRuleScenarioList(source, tabId)
|
|
|
|
|
|
if (scenarioList.length) {
|
|
|
|
|
|
return formatScenarioList(scenarioList)
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveRuleScenarioList(source, tabId = '') {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const resolvedTabId = tabId || resolveRuleTabId(source)
|
|
|
|
|
|
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
|
|
|
|
|
if (expenseScenarioLabels.length) {
|
|
|
|
|
|
return expenseScenarioLabels
|
|
|
|
|
|
}
|
|
|
|
|
|
const riskCategory = resolveRiskRuleCategory(source)
|
|
|
|
|
|
return riskCategory ? [riskCategory] : []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (resolvedTabId === 'financialRules') {
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const financialCategory = inferFinancialRuleCategory(source)
|
|
|
|
|
|
return financialCategory ? [financialCategory] : []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
2026-05-26 17:29:35 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)}…`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function applyRiskRuleJsonState(target, payload, apiPayload) {
|
|
|
|
|
|
const rulePayload = isPlainObject(payload) ? payload : {}
|
2026-05-24 21:44:17 +08:00
|
|
|
|
const metadata = rulePayload.metadata && typeof rulePayload.metadata === 'object'
|
|
|
|
|
|
? rulePayload.metadata
|
|
|
|
|
|
: {}
|
|
|
|
|
|
const apiConfig = apiPayload?.config_json && typeof apiPayload.config_json === 'object'
|
|
|
|
|
|
? apiPayload.config_json
|
|
|
|
|
|
: {}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const fullDescription =
|
|
|
|
|
|
resolveRiskRuleDescription(rulePayload) ||
|
|
|
|
|
|
normalizeText(apiPayload?.description) ||
|
|
|
|
|
|
normalizeText(target.riskRuleDescription)
|
|
|
|
|
|
const riskCategory =
|
2026-05-26 09:15:14 +08:00
|
|
|
|
normalizeText(metadata.expense_category_label) ||
|
|
|
|
|
|
normalizeText(apiConfig.expense_category_label) ||
|
2026-05-21 23:53:03 +08:00
|
|
|
|
normalizeText(rulePayload.risk_category) ||
|
|
|
|
|
|
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
|
2026-05-26 12:16:20 +08:00
|
|
|
|
const businessStage = resolveRiskRuleBusinessStage(target, rulePayload)
|
2026-05-23 19:54:42 +08:00
|
|
|
|
const riskRuleFields = resolveRiskRuleFields(rulePayload)
|
|
|
|
|
|
const riskRuleCreatedAt = resolveRiskRuleCreatedAt(rulePayload, target.createdAt || target.updatedAt)
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const riskRuleScoreLevel = resolveRiskRuleScoreLevel(rulePayload, apiConfig)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
|
const statusValue = apiPayload?.status || target.statusValue || 'draft'
|
2026-05-26 12:16:20 +08:00
|
|
|
|
const onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
|
2026-05-24 21:44:17 +08:00
|
|
|
|
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
|
|
|
|
|
|
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const publisher =
|
|
|
|
|
|
target.creator ||
|
|
|
|
|
|
normalizeText(apiPayload?.owner) ||
|
|
|
|
|
|
normalizeText(metadata.created_by) ||
|
|
|
|
|
|
normalizeText(apiPayload?.recent_versions?.[0]?.created_by) ||
|
|
|
|
|
|
'未知'
|
2026-05-24 21:44:17 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return {
|
|
|
|
|
|
...target,
|
|
|
|
|
|
riskRuleDescription: fullDescription,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
|
|
|
|
|
riskCategory,
|
2026-05-26 12:16:20 +08:00
|
|
|
|
businessStageValue: businessStage.value,
|
|
|
|
|
|
businessStageLabel: businessStage.label,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
scope: riskCategory,
|
|
|
|
|
|
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
2026-05-26 09:15:14 +08:00
|
|
|
|
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),
|
2026-05-23 19:54:42 +08:00
|
|
|
|
riskRuleCreatedAt: formatDateTime(riskRuleCreatedAt),
|
|
|
|
|
|
riskRuleAgeLabel: formatRiskRuleAge(riskRuleCreatedAt),
|
|
|
|
|
|
riskRuleFields,
|
|
|
|
|
|
riskRuleFieldSummary: buildRiskRuleFieldSummary(riskRuleFields),
|
|
|
|
|
|
riskRuleFlow: resolveRiskRuleFlow(rulePayload, riskRuleFields),
|
2026-05-26 09:15:14 +08:00
|
|
|
|
riskRuleFlowDiagramSvg: resolveRiskRuleFlowDiagramSvg({
|
|
|
|
|
|
...rulePayload,
|
|
|
|
|
|
flow_diagram_svg: normalizeText(apiPayload?.flow_diagram_svg) || rulePayload?.flow_diagram_svg
|
|
|
|
|
|
}),
|
2026-05-24 21:44:17 +08:00
|
|
|
|
riskRuleRequiresAttachment: Boolean(
|
|
|
|
|
|
rulePayload.requires_attachment ||
|
|
|
|
|
|
metadata.requires_attachment ||
|
|
|
|
|
|
apiConfig.requires_attachment ||
|
|
|
|
|
|
target.configJson?.requires_attachment
|
|
|
|
|
|
),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
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 || {}
|
|
|
|
|
|
},
|
2026-05-24 21:44:17 +08:00
|
|
|
|
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
|
2026-05-26 12:16:20 +08:00
|
|
|
|
isOnlineValue: onlineMeta.online,
|
|
|
|
|
|
isOnlineLabel: onlineMeta.label,
|
|
|
|
|
|
isOnlineTone: onlineMeta.tone,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
isEnabledValue,
|
|
|
|
|
|
isEnabledLabel: isEnabledValue ? '是' : '否',
|
|
|
|
|
|
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
2026-05-26 12:16:20 +08:00
|
|
|
|
lastOperationLabel: resolveLastOperationLabel(target, {
|
|
|
|
|
|
actor: publisher,
|
|
|
|
|
|
at: riskRuleCreatedAt
|
|
|
|
|
|
}),
|
2026-05-24 21:44:17 +08:00
|
|
|
|
publisher,
|
|
|
|
|
|
publishedAt
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
const text = String(markdown || '')
|
|
|
|
|
|
const stripped = text.replace(EXPENSE_RULE_BLOCK_PATTERN, '').replace(/\n{3,}/g, '\n\n').trim()
|
|
|
|
|
|
return stripped
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 buildHistory(recentVersions = [], source) {
|
|
|
|
|
|
const currentRuntimeRule = cloneJsonObject(readConfigJson(source).runtime_rule)
|
|
|
|
|
|
|
|
|
|
|
|
return recentVersions.map((item) => {
|
|
|
|
|
|
const rawContent = typeof item.content === 'string' ? item.content : ''
|
|
|
|
|
|
return {
|
|
|
|
|
|
version: item.version,
|
|
|
|
|
|
note: item.change_note || '无版本说明',
|
|
|
|
|
|
time: formatDateTime(item.created_at),
|
|
|
|
|
|
content: rawContent,
|
|
|
|
|
|
markdownContent: stripRuntimeRuleBlock(rawContent),
|
|
|
|
|
|
runtimeRule: resolveRuntimeRuleForVersion(
|
|
|
|
|
|
source,
|
|
|
|
|
|
rawContent,
|
|
|
|
|
|
item.is_current ? currentRuntimeRule : null
|
|
|
|
|
|
),
|
|
|
|
|
|
spreadsheetMeta: extractSpreadsheetMetaFromMarkdown(rawContent),
|
|
|
|
|
|
contentType: item.content_type,
|
|
|
|
|
|
createdBy: item.created_by,
|
|
|
|
|
|
isCurrent: Boolean(item.is_current),
|
|
|
|
|
|
isPublished: Boolean(item.is_published),
|
|
|
|
|
|
isWorking: Boolean(item.is_working),
|
|
|
|
|
|
lifecycleState: item.lifecycle_state || 'history',
|
|
|
|
|
|
lifecycleMeta: VERSION_STATE_META[item.lifecycle_state] || VERSION_STATE_META.history
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolveTypeKey(assetType) {
|
|
|
|
|
|
if (assetType === 'rule') {
|
|
|
|
|
|
return 'rules'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assetType === 'skill') {
|
|
|
|
|
|
return 'skills'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (assetType === 'mcp') {
|
|
|
|
|
|
return 'mcp'
|
|
|
|
|
|
}
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return ''
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 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 === 'skills') {
|
|
|
|
|
|
return formatInputSummary(asset.config_json?.input_schema)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
|
|
|
|
|
}
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return ''
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildRowMetric(asset, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return normalizeText(asset.modified_by) || '未记录'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return '进入详情查看输出'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
|
|
|
|
|
}
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return ''
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatSpreadsheetChangeSummary(summary) {
|
|
|
|
|
|
const normalized = normalizeText(summary)
|
|
|
|
|
|
return (
|
|
|
|
|
|
normalized
|
|
|
|
|
|
.replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '')
|
|
|
|
|
|
.replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '')
|
|
|
|
|
|
.replace(/^保存表格[::]\s*/i, '')
|
|
|
|
|
|
.trim() || '表格内容已保存。'
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-26 12:16:20 +08:00
|
|
|
|
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
|
|
|
|
|
|
const isOnlineValue = onlineMeta.online
|
2026-05-24 21:44:17 +08:00
|
|
|
|
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
|
|
|
|
|
|
const reviewer = normalizeText(asset.reviewer) || '待分配'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
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)
|
2026-05-26 12:16:20 +08:00
|
|
|
|
const businessStage = usesJsonRiskRule
|
|
|
|
|
|
? resolveRiskRuleBusinessStage(asset)
|
|
|
|
|
|
: { value: '', label: '' }
|
2026-05-26 17:29:35 +08:00
|
|
|
|
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)
|
|
|
|
|
|
: ''
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: asset.id,
|
|
|
|
|
|
tabId,
|
|
|
|
|
|
type: typeKey,
|
|
|
|
|
|
isPreviewMock: Boolean(asset.isPreviewMock),
|
|
|
|
|
|
usesSpreadsheetRule,
|
|
|
|
|
|
usesJsonRiskRule,
|
|
|
|
|
|
ruleDocument,
|
|
|
|
|
|
typeLabel: tabMeta.typeLabel,
|
|
|
|
|
|
short: makeShort(asset.name),
|
|
|
|
|
|
name: asset.name,
|
|
|
|
|
|
code: asset.code,
|
|
|
|
|
|
summary: listSubtitle,
|
|
|
|
|
|
listSubtitle,
|
|
|
|
|
|
category: resolveDomainLabel(asset.domain),
|
2026-05-26 17:29:35 +08:00
|
|
|
|
owner: isRiskRule ? creator : asset.owner,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
reviewer,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
|
|
|
|
|
|
riskCategory: ruleScenarioCategory,
|
2026-05-26 17:29:35 +08:00
|
|
|
|
scenarioList: ruleScenarioList,
|
2026-05-26 12:16:20 +08:00
|
|
|
|
businessStageValue: businessStage.value,
|
|
|
|
|
|
businessStageLabel: businessStage.label,
|
2026-05-26 17:29:35 +08:00
|
|
|
|
riskLevelValue,
|
|
|
|
|
|
riskLevelLabel,
|
|
|
|
|
|
riskLevelTone: riskLevelValue,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
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,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
hitRate: isRiskRule ? publisher : buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
|
2026-05-26 09:15:14 +08:00
|
|
|
|
creator,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
publisher,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
|
2026-05-24 21:44:17 +08:00
|
|
|
|
isOnlineValue,
|
2026-05-26 12:16:20 +08:00
|
|
|
|
isOnlineLabel: onlineMeta.label,
|
|
|
|
|
|
isOnlineTone: onlineMeta.tone,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
isEnabledValue,
|
|
|
|
|
|
isEnabledLabel: isEnabledValue ? '是' : '否',
|
|
|
|
|
|
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
modifiedBy,
|
|
|
|
|
|
changeCount,
|
2026-05-26 09:15:14 +08:00
|
|
|
|
updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
badgeTone: tabMeta.badgeTone,
|
|
|
|
|
|
domainValue: asset.domain
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildRuleFields(detail) {
|
|
|
|
|
|
const ruleDocument = readRuleDocumentMeta(detail)
|
|
|
|
|
|
const ruleScenarioCategory = resolveRuleScenarioCategory(detail)
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '规则编码', value: detail.code },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '明细载体',
|
|
|
|
|
|
value: isSpreadsheetRuleSource(detail) ? 'Excel 表格' : 'Markdown / JSON'
|
|
|
|
|
|
},
|
|
|
|
|
|
...(ruleDocument
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '关联文件',
|
|
|
|
|
|
value: normalizeText(ruleDocument.file_name) || '未上传'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
: []),
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '模板键',
|
|
|
|
|
|
value: normalizeText(detail.config_json?.rule_template_key) || '未指定'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '运行时类型',
|
|
|
|
|
|
value: normalizeText(detail.config_json?.runtime_kind) || 'policy_rule_draft'
|
|
|
|
|
|
},
|
|
|
|
|
|
{ label: '适用场景', value: ruleScenarioCategory || '通用' },
|
|
|
|
|
|
{ label: '线上版本', value: detail.published_version || '-' },
|
|
|
|
|
|
{ label: '工作版本', value: detail.working_version || detail.current_version || '-' }
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildSkillFields(detail) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '技能编码', value: detail.code },
|
|
|
|
|
|
{ label: '业务域', value: resolveDomainLabel(detail.domain) },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '输入参数',
|
|
|
|
|
|
value: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '输出参数',
|
|
|
|
|
|
value: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildMcpFields(detail, latestCall) {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{ label: '服务编码', value: detail.code },
|
|
|
|
|
|
{ label: '调用地址', value: normalizeText(detail.config_json?.endpoint) || '未配置' },
|
|
|
|
|
|
{ label: '鉴权方式', value: normalizeText(content.auth_mode) || '未配置' },
|
|
|
|
|
|
{
|
|
|
|
|
|
label: '最近调用',
|
|
|
|
|
|
value: latestCall ? `${latestCall.toolCall.status} / ${formatDateTime(latestCall.run.started_at)}` : '暂无调用记录'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
export function buildFields(detail, typeKey, latestCall) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
return buildRuleFields(detail)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return buildSkillFields(detail)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return buildMcpFields(detail, latestCall)
|
|
|
|
|
|
}
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
export function buildPromptSections(detail, typeKey) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '输入参数',
|
|
|
|
|
|
intent: '技能入口',
|
|
|
|
|
|
content: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('\n') : '未配置输入参数。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '输出参数',
|
|
|
|
|
|
intent: '技能产出',
|
|
|
|
|
|
content: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('\n') : '未配置输出参数。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '依赖能力',
|
|
|
|
|
|
intent: '外部依赖',
|
|
|
|
|
|
content:
|
|
|
|
|
|
Array.isArray(content.dependencies) && content.dependencies.length
|
|
|
|
|
|
? content.dependencies.join('\n')
|
|
|
|
|
|
: '当前技能未声明外部依赖。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '服务类型',
|
|
|
|
|
|
intent: '协议说明',
|
|
|
|
|
|
content: normalizeText(content.service_type) || '未配置服务类型。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '鉴权方式',
|
|
|
|
|
|
intent: '安全要求',
|
|
|
|
|
|
content: normalizeText(content.auth_mode) || '未配置鉴权方式。'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '降级策略',
|
|
|
|
|
|
intent: '失败处理',
|
|
|
|
|
|
content: normalizeText(content.degrade_strategy) || '未配置降级策略。'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
export function buildOutputRules(detail, typeKey) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
if (isSpreadsheetRuleSource(detail)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
'规则详情页以内联 Excel 表格作为主载体,管理员可直接编辑当前版本。',
|
|
|
|
|
|
'上传新的 Excel 文件后,会自动生成新的规则版本快照。',
|
|
|
|
|
|
'切换到历史版本时仅支持预览,不允许直接覆盖历史版本。',
|
|
|
|
|
|
'规则表发生变更后,仍需完成审核才能再次正式上线。'
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
|
|
|
'规则使用固定模板落 Markdown,并配套维护 runtime_rule JSON。',
|
|
|
|
|
|
'保存 Markdown 或 JSON 都会生成新版本快照。',
|
|
|
|
|
|
'未审核通过的规则版本不能正式上线。',
|
|
|
|
|
|
'版本切换当前只影响前端展示内容,不会直接回滚后端版本。'
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
`输入参数:${Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'}`,
|
|
|
|
|
|
`输出参数:${Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'}`,
|
|
|
|
|
|
`依赖能力:${Array.isArray(content.dependencies) && content.dependencies.length ? content.dependencies.join('、') : '未声明'}`
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
`服务地址:${normalizeText(detail.config_json?.endpoint) || '未配置'}`,
|
|
|
|
|
|
`鉴权方式:${normalizeText(content.auth_mode) || '未配置'}`,
|
|
|
|
|
|
`降级策略:${normalizeText(content.degrade_strategy) || '未配置'}`
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
export function buildTests(detail, typeKey, latestCall) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '审核状态',
|
|
|
|
|
|
input: detail.latest_review?.version || detail.current_version || '暂无版本',
|
|
|
|
|
|
result: reviewMeta.label,
|
|
|
|
|
|
tone: reviewMeta.tone
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '上线状态',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: resolveStatusMeta(detail.status).label,
|
|
|
|
|
|
tone: resolveStatusMeta(detail.status).tone
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '输入数量',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: `${content.inputs?.length || 0} 项`,
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '输出数量',
|
|
|
|
|
|
input: detail.current_version || '暂无版本',
|
|
|
|
|
|
result: `${content.outputs?.length || 0} 项`,
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '最近调用状态',
|
|
|
|
|
|
input: latestCall?.run?.run_id || '暂无调用',
|
|
|
|
|
|
result: latestCall?.toolCall?.status || '未记录',
|
|
|
|
|
|
tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'success'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '最近调用耗时',
|
|
|
|
|
|
input: latestCall?.toolCall?.tool_name || '暂无调用',
|
|
|
|
|
|
result:
|
|
|
|
|
|
typeof latestCall?.toolCall?.duration_ms === 'number'
|
|
|
|
|
|
? `${latestCall.toolCall.duration_ms} ms`
|
|
|
|
|
|
: '未记录',
|
|
|
|
|
|
tone: 'success'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
export function buildTools(detail, typeKey, latestCall) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const content = detail.current_version_content || {}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'skills') {
|
|
|
|
|
|
return (content.dependencies || []).map((item) => ({
|
|
|
|
|
|
name: item,
|
|
|
|
|
|
scope: '技能依赖',
|
|
|
|
|
|
mode: '读取',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (typeKey === 'mcp') {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: normalizeText(content.service_type) || '未配置服务类型',
|
|
|
|
|
|
scope: '服务类型',
|
|
|
|
|
|
mode: 'MCP',
|
|
|
|
|
|
tone: 'active'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: normalizeText(content.auth_mode) || '未配置鉴权方式',
|
|
|
|
|
|
scope: '鉴权',
|
|
|
|
|
|
mode: '安全',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: latestCall?.run?.run_id || '暂无调用记录',
|
|
|
|
|
|
scope: '最近 Run',
|
|
|
|
|
|
mode: latestCall?.toolCall?.status || '未执行',
|
|
|
|
|
|
tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'active'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return []
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildPublishDescription(detail, typeKey) {
|
|
|
|
|
|
if (typeKey === 'rules') {
|
|
|
|
|
|
if (detail.published_version && detail.working_version && detail.published_version !== detail.working_version) {
|
|
|
|
|
|
return '当前存在尚未上线的工作版本,系统运行仍以线上版本为准。'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (detail.status === 'active') {
|
|
|
|
|
|
return '当前规则线上版本已生效,仍可继续保存新的工作版本并重新走审核。'
|
|
|
|
|
|
}
|
|
|
|
|
|
return '当前规则需要先完成审核,再调用上线接口正式激活。'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 12:16:20 +08:00
|
|
|
|
return DETAIL_TITLES[typeKey]?.publishDesc || ''
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function buildDetailViewModel(detail, runs) {
|
|
|
|
|
|
const typeKey = resolveTypeKey(detail.asset_type)
|
|
|
|
|
|
const tabId = resolveTabId(detail, typeKey) || typeKey
|
2026-05-26 12:16:20 +08:00
|
|
|
|
if (!typeKey || !tabId) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const tabMeta = resolveTabMeta(tabId, typeKey)
|
|
|
|
|
|
const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null
|
|
|
|
|
|
const configJson = readConfigJson(detail)
|
|
|
|
|
|
const statusMeta = resolveStatusMeta(detail.status)
|
|
|
|
|
|
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
|
|
|
|
|
|
const history = buildHistory(detail.recent_versions || [], detail)
|
|
|
|
|
|
const previewVersion = history.find((item) => item.isWorking) || history[0] || null
|
|
|
|
|
|
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(detail)
|
|
|
|
|
|
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(detail)
|
|
|
|
|
|
const ruleDocument = readRuleDocumentMeta(detail)
|
|
|
|
|
|
const previewRawMarkdown =
|
|
|
|
|
|
detail.current_version_content_type === 'markdown'
|
|
|
|
|
|
? String(previewVersion?.content ?? detail.current_version_content ?? '')
|
|
|
|
|
|
: ''
|
|
|
|
|
|
const previewRuntimeRule = resolveRuntimeRuleForVersion(
|
|
|
|
|
|
detail,
|
|
|
|
|
|
previewRawMarkdown,
|
|
|
|
|
|
previewVersion?.runtimeRule || configJson.runtime_rule
|
|
|
|
|
|
)
|
|
|
|
|
|
const previewMarkdown = stripRuntimeRuleBlock(previewRawMarkdown)
|
|
|
|
|
|
const titles = DETAIL_TITLES[typeKey]
|
|
|
|
|
|
const previewChangeNote = previewVersion?.note || detail.current_version_change_note || '无版本说明'
|
|
|
|
|
|
const ruleTemplateKey = normalizeText(configJson.rule_template_key || previewRuntimeRule.template_key)
|
|
|
|
|
|
const ruleTemplateLabel = normalizeText(configJson.rule_template_label) || resolveRuleTemplateLabel(ruleTemplateKey)
|
|
|
|
|
|
const runtimeKind = normalizeText(configJson.runtime_kind || previewRuntimeRule.kind) || 'policy_rule_draft'
|
|
|
|
|
|
const ruleScenarioCategory = typeKey === 'rules' ? resolveRuleScenarioCategory(detail, tabId) : ''
|
2026-05-26 17:29:35 +08:00
|
|
|
|
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(detail, tabId) : []
|
2026-05-24 21:44:17 +08:00
|
|
|
|
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(detail) : true
|
2026-05-26 09:15:14 +08:00
|
|
|
|
const generationStatus = normalizeText(configJson.generation_status || detail.status)
|
|
|
|
|
|
const riskRuleGenerationFailed = usesJsonRiskRule && (detail.status === 'failed' || generationStatus === 'failed')
|
|
|
|
|
|
const riskRuleGenerationBusy = usesJsonRiskRule && (detail.status === 'generating' || generationStatus === 'generating')
|
|
|
|
|
|
const riskRuleCreator =
|
|
|
|
|
|
normalizeText(detail.owner) ||
|
|
|
|
|
|
normalizeText(detail.recent_versions?.[0]?.created_by) ||
|
|
|
|
|
|
'未知'
|
2026-05-26 12:16:20 +08:00
|
|
|
|
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)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: detail.id,
|
|
|
|
|
|
tabId,
|
|
|
|
|
|
type: typeKey,
|
|
|
|
|
|
typeLabel: tabMeta.typeLabel,
|
|
|
|
|
|
short: makeShort(detail.name),
|
|
|
|
|
|
name: detail.name,
|
|
|
|
|
|
code: detail.code,
|
|
|
|
|
|
summary: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description,
|
|
|
|
|
|
listSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : normalizeText(detail.description),
|
|
|
|
|
|
owner: detail.owner,
|
|
|
|
|
|
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
|
|
|
|
|
|
category: resolveDomainLabel(detail.domain),
|
|
|
|
|
|
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json),
|
2026-05-26 12:16:20 +08:00
|
|
|
|
businessStageValue: businessStage.value,
|
|
|
|
|
|
businessStageLabel: businessStage.label,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
version: detail.working_version || detail.current_version || '-',
|
|
|
|
|
|
currentVersion: detail.current_version || '-',
|
|
|
|
|
|
publishedVersion: detail.published_version || '-',
|
|
|
|
|
|
workingVersion: detail.working_version || detail.current_version || '-',
|
|
|
|
|
|
displayVersion: previewVersion?.version || detail.working_version || detail.current_version || '-',
|
|
|
|
|
|
status: statusMeta.label,
|
|
|
|
|
|
statusValue: detail.status,
|
|
|
|
|
|
statusTone: statusMeta.tone,
|
|
|
|
|
|
hitRate: buildRowMetric(detail, typeKey),
|
2026-05-23 19:54:42 +08:00
|
|
|
|
createdAt: detail.created_at,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
updatedAt: formatDateTime(detail.updated_at),
|
|
|
|
|
|
badgeTone: tabMeta.badgeTone,
|
|
|
|
|
|
configJson,
|
|
|
|
|
|
usesSpreadsheetRule,
|
|
|
|
|
|
usesJsonRiskRule,
|
|
|
|
|
|
riskRuleJsonText: '{}',
|
|
|
|
|
|
riskRuleSummary: null,
|
|
|
|
|
|
riskRuleDescription: '',
|
2026-05-23 19:54:42 +08:00
|
|
|
|
riskRuleBusinessDescription: '',
|
2026-05-21 23:53:03 +08:00
|
|
|
|
riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '',
|
|
|
|
|
|
riskRuleSourceRef: '',
|
2026-05-26 12:16:20 +08:00
|
|
|
|
riskRuleSeverity: initialRiskRuleSeverity,
|
|
|
|
|
|
riskRuleScore: initialRiskRuleScore,
|
|
|
|
|
|
riskRuleScoreLevel: initialRiskRuleScoreLevel || initialRiskRuleSeverity,
|
|
|
|
|
|
riskRuleScoreDetail: resolveRiskRuleScoreDetail(configJson, configJson),
|
|
|
|
|
|
riskRuleSeverityLabel: initialRiskRuleScoreLevel
|
|
|
|
|
|
? resolveRiskRuleScoreLabel(configJson, configJson)
|
|
|
|
|
|
: resolveRiskRuleSeverityLabel(configJson),
|
|
|
|
|
|
riskRuleScoreLabel: resolveRiskRuleScoreLabel(configJson, configJson),
|
2026-05-23 19:54:42 +08:00
|
|
|
|
riskRuleCreatedAt: formatDateTime(detail.created_at),
|
|
|
|
|
|
riskRuleAgeLabel: formatRiskRuleAge(detail.created_at),
|
2026-05-26 12:16:20 +08:00
|
|
|
|
isOnlineValue: onlineMeta.online,
|
|
|
|
|
|
isOnlineLabel: onlineMeta.label,
|
|
|
|
|
|
isOnlineTone: onlineMeta.tone,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
isEnabledValue,
|
|
|
|
|
|
isEnabledLabel: isEnabledValue ? '是' : '否',
|
|
|
|
|
|
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
|
|
|
|
|
publisher:
|
|
|
|
|
|
detail.status === 'active'
|
|
|
|
|
|
? normalizeText(detail.published_by) ||
|
|
|
|
|
|
detail.latest_review?.reviewer ||
|
|
|
|
|
|
detail.reviewer ||
|
|
|
|
|
|
(detail.recent_versions && detail.recent_versions[0]?.created_by) ||
|
|
|
|
|
|
'系统管理员'
|
2026-05-26 09:15:14 +08:00
|
|
|
|
: riskRuleCreator,
|
|
|
|
|
|
creator: riskRuleCreator,
|
2026-05-24 21:44:17 +08:00
|
|
|
|
publishedAt:
|
|
|
|
|
|
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) : '-'),
|
2026-05-26 12:16:20 +08:00
|
|
|
|
lastOperationLabel: resolveLastOperationLabel(detail, {
|
|
|
|
|
|
actor: riskRuleCreator,
|
|
|
|
|
|
at: detail.created_at
|
|
|
|
|
|
}),
|
|
|
|
|
|
lastOperationTone: onlineMeta.tone,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
riskRuleFields: [],
|
|
|
|
|
|
riskRuleFieldSummary: '未识别字段',
|
|
|
|
|
|
riskRuleFlow: resolveRiskRuleFlow({}, []),
|
|
|
|
|
|
riskRuleFlowDiagramSvg: normalizeText(configJson.flow_diagram_svg),
|
2026-05-24 21:44:17 +08:00
|
|
|
|
riskRuleRequiresAttachment: Boolean(configJson.requires_attachment),
|
2026-05-26 09:15:14 +08:00
|
|
|
|
riskRuleGenerationStatus: generationStatus,
|
|
|
|
|
|
riskRuleGenerationFailed,
|
|
|
|
|
|
riskRuleGenerationBusy,
|
|
|
|
|
|
riskRuleGenerationError: normalizeText(configJson.generation_error),
|
2026-05-24 21:44:17 +08:00
|
|
|
|
latestTestSummary: detail.latest_test_summary || detail.latestTestSummary || null,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
riskCategory: typeKey === 'rules' ? ruleScenarioCategory : '',
|
|
|
|
|
|
ruleDocument,
|
2026-05-26 17:29:35 +08:00
|
|
|
|
scenarioList: typeKey === 'rules' && ruleScenarioList.length
|
|
|
|
|
|
? ruleScenarioList
|
2026-05-21 23:53:03 +08:00
|
|
|
|
: Array.isArray(detail.scenario_json)
|
|
|
|
|
|
? [...detail.scenario_json]
|
|
|
|
|
|
: [],
|
|
|
|
|
|
markdownContent: previewMarkdown,
|
|
|
|
|
|
runtimeRuleText: stringifyRuntimeRule(previewRuntimeRule),
|
|
|
|
|
|
ruleTemplateKey,
|
|
|
|
|
|
ruleTemplateLabel,
|
|
|
|
|
|
runtimeKind,
|
|
|
|
|
|
currentVersionContentType: detail.current_version_content_type,
|
|
|
|
|
|
currentVersionChangeNote: detail.current_version_change_note || '无版本说明',
|
|
|
|
|
|
displayVersionChangeNote: previewChangeNote,
|
|
|
|
|
|
reviewStatusLabel: reviewMeta.label,
|
|
|
|
|
|
reviewStatusTone: reviewMeta.tone,
|
|
|
|
|
|
reviewStatusValue: detail.latest_review?.review_status || '',
|
|
|
|
|
|
reviewTimeLabel: formatDateTime(detail.latest_review?.reviewed_at),
|
|
|
|
|
|
reviewNote: detail.latest_review?.review_note || '',
|
|
|
|
|
|
latestCall,
|
2026-05-26 12:16:20 +08:00
|
|
|
|
fields: buildFields(detail, typeKey, latestCall),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
promptSections:
|
2026-05-26 12:16:20 +08:00
|
|
|
|
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey),
|
|
|
|
|
|
outputRules: buildOutputRules(detail, typeKey),
|
|
|
|
|
|
tests: buildTests(detail, typeKey, latestCall),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
triggers:
|
|
|
|
|
|
typeKey === 'rules'
|
|
|
|
|
|
? [ruleScenarioCategory || '通用']
|
|
|
|
|
|
: detail.scenario_json?.length
|
|
|
|
|
|
? detail.scenario_json.map((item) => SCENARIO_LABELS[item] || item)
|
|
|
|
|
|
: ['未配置场景'],
|
|
|
|
|
|
tools:
|
|
|
|
|
|
typeKey === 'rules'
|
|
|
|
|
|
? [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: detail.latest_review?.reviewer || '待分配审核人',
|
|
|
|
|
|
scope: '审核负责人',
|
|
|
|
|
|
mode: reviewMeta.label,
|
|
|
|
|
|
tone: reviewMeta.tone
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: detail.published_version || '暂无版本',
|
|
|
|
|
|
scope: '线上版本',
|
|
|
|
|
|
mode: '正式生效',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: detail.working_version || detail.current_version || '暂无版本',
|
|
|
|
|
|
scope: '工作版本',
|
|
|
|
|
|
mode: detail.current_version_change_note || '无版本说明',
|
|
|
|
|
|
tone: 'safe'
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-05-26 12:16:20 +08:00
|
|
|
|
: buildTools(detail, typeKey, latestCall),
|
2026-05-21 23:53:03 +08:00
|
|
|
|
history,
|
|
|
|
|
|
configTitle: titles.configTitle,
|
|
|
|
|
|
configDesc: titles.configDesc,
|
|
|
|
|
|
detailTitle: titles.detailTitle,
|
|
|
|
|
|
detailDesc: titles.detailDesc,
|
|
|
|
|
|
outputTitle: titles.outputTitle,
|
|
|
|
|
|
outputDesc: titles.outputDesc,
|
|
|
|
|
|
ruleListTitle: titles.ruleListTitle,
|
|
|
|
|
|
checkListTitle: titles.checkListTitle,
|
|
|
|
|
|
triggerTitle: titles.triggerTitle,
|
|
|
|
|
|
triggerDesc: titles.triggerDesc,
|
|
|
|
|
|
toolTitle: titles.toolTitle,
|
|
|
|
|
|
toolDesc: titles.toolDesc,
|
|
|
|
|
|
historyTitle: titles.historyTitle,
|
|
|
|
|
|
historyDesc: titles.historyDesc,
|
|
|
|
|
|
publishTitle: titles.publishTitle,
|
|
|
|
|
|
publishDesc: buildPublishDescription(detail, typeKey),
|
|
|
|
|
|
publishMeta:
|
|
|
|
|
|
typeKey === 'rules'
|
|
|
|
|
|
? `最近保存:${formatDateTime(detail.updated_at)}`
|
2026-05-26 12:16:20 +08:00
|
|
|
|
: `最近更新:${formatDateTime(detail.updated_at)}`,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
publishState: statusMeta.label,
|
|
|
|
|
|
latestReviewVersion: detail.latest_review?.version || detail.current_version || '-',
|
|
|
|
|
|
loading: false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|