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' import { buildRiskRuleFieldSummary, formatRiskRuleAge, resolveRiskRuleBusinessDescription, resolveRiskRuleCreatedAt, resolveRiskRuleFields, resolveRiskRuleFlow, resolveRiskRuleFlowDiagramSvg, resolveRiskRuleScore, resolveRiskRuleScoreDetail, resolveRiskRuleScoreLabel, resolveRiskRuleScoreLevel, resolveRiskRuleSeverity, resolveRiskRuleSeverityLabel } from './auditViewRiskRuleModel.js' const EXPENSE_TYPE_SCENARIO_LABELS = { travel: '差旅费', hotel: '住宿费', transport: '交通费', meal: '业务招待费', meeting: '会务费', marketing: '市场推广费', office: '办公用品费', training: '培训费', software: '软件服务费', communication: '通信费', welfare: '福利费' } export { DETAIL_TITLES, DOMAIN_LABELS, ENABLED_STATE_OPTIONS, 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, RISK_RULE_TABLE_COLUMNS, RULE_SPREADSHEET_BLOCK_PATTERN, RULE_TABLE_COLUMNS, RULE_TAB_TAG_ALIASES, RULE_TEMPLATE_LABELS, SCENARIO_LABELS, SPREADSHEET_DETAIL_MODE, STATUS_META, STATUS_OPTIONS, ONLINE_STATE_OPTIONS, 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 {} } 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: '费用报销' } 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}` } 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) 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)}…` } 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 } } 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' } 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 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) || '未配置地址' } return '' } 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` : '未配置超时' } return '' } 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) 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) : '' 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), owner: isRiskRule ? creator : asset.owner, reviewer, scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json), 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, isEnabledLabel: isEnabledValue ? '是' : '否', isEnabledTone: isEnabledValue ? 'success' : 'disabled', modifiedBy, changeCount, updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at), 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)}` : '暂无调用记录' } ] } export function buildFields(detail, typeKey, latestCall) { if (typeKey === 'rules') { return buildRuleFields(detail) } if (typeKey === 'skills') { return buildSkillFields(detail) } if (typeKey === 'mcp') { return buildMcpFields(detail, latestCall) } return [] } export function buildPromptSections(detail, typeKey) { 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) || '未配置降级策略。' } ] } return [] } export function buildOutputRules(detail, typeKey) { 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) || '未配置'}` ] } return [] } export function buildTests(detail, typeKey, latestCall) { 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' } ] } return [] } export function buildTools(detail, typeKey, latestCall) { 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' } ] } return [] } 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 '当前规则需要先完成审核,再调用上线接口正式激活。' } return DETAIL_TITLES[typeKey]?.publishDesc || '' } export function buildDetailViewModel(detail, runs) { const typeKey = resolveTypeKey(detail.asset_type) const tabId = resolveTabId(detail, typeKey) || typeKey if (!typeKey || !tabId) { return null } 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) : '' const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(detail, tabId) : [] const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(detail) : true 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) || '未知' 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) 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), businessStageValue: businessStage.value, businessStageLabel: businessStage.label, 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), createdAt: detail.created_at, updatedAt: formatDateTime(detail.updated_at), badgeTone: tabMeta.badgeTone, configJson, usesSpreadsheetRule, usesJsonRiskRule, riskRuleJsonText: '{}', riskRuleSummary: null, riskRuleDescription: '', riskRuleBusinessDescription: '', riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '', riskRuleSourceRef: '', riskRuleSeverity: initialRiskRuleSeverity, riskRuleScore: initialRiskRuleScore, riskRuleScoreLevel: initialRiskRuleScoreLevel || initialRiskRuleSeverity, riskRuleScoreDetail: resolveRiskRuleScoreDetail(configJson, configJson), riskRuleSeverityLabel: initialRiskRuleScoreLevel ? resolveRiskRuleScoreLabel(configJson, configJson) : resolveRiskRuleSeverityLabel(configJson), riskRuleScoreLabel: resolveRiskRuleScoreLabel(configJson, configJson), riskRuleCreatedAt: formatDateTime(detail.created_at), riskRuleAgeLabel: formatRiskRuleAge(detail.created_at), isOnlineValue: onlineMeta.online, isOnlineLabel: onlineMeta.label, isOnlineTone: onlineMeta.tone, 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) || '系统管理员' : riskRuleCreator, creator: riskRuleCreator, 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) : '-'), lastOperationLabel: resolveLastOperationLabel(detail, { actor: riskRuleCreator, at: detail.created_at }), lastOperationTone: onlineMeta.tone, riskRuleFields: [], riskRuleFieldSummary: '未识别字段', riskRuleFlow: resolveRiskRuleFlow({}, []), riskRuleFlowDiagramSvg: normalizeText(configJson.flow_diagram_svg), riskRuleRequiresAttachment: Boolean(configJson.requires_attachment), riskRuleGenerationStatus: generationStatus, riskRuleGenerationFailed, riskRuleGenerationBusy, riskRuleGenerationError: normalizeText(configJson.generation_error), latestTestSummary: detail.latest_test_summary || detail.latestTestSummary || null, riskCategory: typeKey === 'rules' ? ruleScenarioCategory : '', ruleDocument, scenarioList: typeKey === 'rules' && ruleScenarioList.length ? ruleScenarioList : 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, fields: buildFields(detail, typeKey, latestCall), promptSections: typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey), outputRules: buildOutputRules(detail, typeKey), tests: buildTests(detail, typeKey, latestCall), 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' } ] : buildTools(detail, typeKey, latestCall), 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)}` : `最近更新:${formatDateTime(detail.updated_at)}`, publishState: statusMeta.label, latestReviewVersion: detail.latest_review?.version || detail.current_version || '-', loading: false } }