refactor: 重构 AuditView 和 TravelReimbursementCreateView 相关代码

- 优化 agent_assets、agent_foundation、user_agent 服务层结构
- 更新 AuditView 视图和脚本
- 更新 TravelReimbursementCreateView 脚本

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caoxiaozhu
2026-05-19 20:23:58 +08:00
parent dc007f948a
commit 9472813739
7 changed files with 4287 additions and 2204 deletions

View File

@@ -15,8 +15,10 @@ import {
fetchAgentAssetSpreadsheetBlob,
fetchAgentAssetSpreadsheetChangeRecords,
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
fetchAgentAssetRuleJson,
fetchAgentAssetVersionTimeline,
fetchAgentRuns,
saveAgentAssetRuleJson,
importAgentAssetSpreadsheetContent,
restoreAgentAssetVersion,
updateAgentAsset
@@ -30,9 +32,8 @@ const RULE_TABLE_COLUMNS = {
category: '业务域',
owner: '负责人',
scope: '适用场景',
runtime: '风险等级',
version: '当前版本',
metric: '审核状态'
version: '修改次数',
metric: '修改人'
}
const TYPE_META = {
@@ -106,6 +107,8 @@ const TAB_META = {
hintText: '仅展示 tag 为“财务规则”的规则资产;未打新 tag 的旧规则已从规则中心隐藏。',
searchPlaceholder: '搜索财务规则名称、编码或负责人',
tableColumns: RULE_TABLE_COLUMNS,
showRuntimeColumn: false,
showStatusColumn: false,
badgeTone: 'emerald'
},
riskRules: {
@@ -114,9 +117,12 @@ const TAB_META = {
label: '风险规则',
typeLabel: '风险规则',
createButtonLabel: '风险规则已接入',
hintText: '仅展示 tag 为“风险规则”的规则资产;当前未打标签的规则不会显示在这里。',
hintText: '仅展示平台风险规则;适用场景按差旅、发票、餐饮招待等分类,可用「使用场景」筛选。',
searchPlaceholder: '搜索风险规则名称、编码或负责人',
tableColumns: RULE_TABLE_COLUMNS,
showRuntimeColumn: false,
showVersionColumn: false,
showStatusColumn: false,
badgeTone: 'rose'
},
skills: {
@@ -287,7 +293,32 @@ const RULE_TAB_TAG_ALIASES = {
riskRules: new Set(['风险规则', '风险', '风控', 'riskrule', 'riskrules', 'risk'])
}
const RISK_SCENARIO_OPTIONS = [
{ value: '', label: '全部场景' },
{ value: '差旅', label: '差旅' },
{ value: '发票', label: '发票' },
{ value: '餐饮招待', label: '餐饮招待' },
{ value: '交通出行', label: '交通出行' },
{ value: '办公物料', label: '办公物料' },
{ value: '费用科目', label: '费用科目' },
{ value: '通用', label: '通用' }
]
const LEGACY_RISK_SCENARIO_KEYS = new Set([
'expense',
'risk_check',
'travel',
'meal',
'invoice',
'travel_policy',
'travel_standard',
'attachment_policy',
'scene_policy',
'invoice_anomaly'
])
const SPREADSHEET_DETAIL_MODE = 'spreadsheet'
const JSON_RISK_DETAIL_MODE = 'json_risk'
const PREVIEW_RULE_ID = 'preview-rule-expense-company-travel-expense'
const PREVIEW_RULE_CODE = 'rule.expense.company_travel_expense_reimbursement'
const PREVIEW_RULE_VERSION_SPECS = [
@@ -461,6 +492,11 @@ function isSpreadsheetRuleSource(value) {
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
}
function isJsonRiskRuleSource(value) {
const configJson = readConfigJson(value)
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
}
function normalizeRuleTagValue(value) {
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
}
@@ -478,6 +514,14 @@ function collectRuleTagValues(source) {
}
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))) {
@@ -510,6 +554,108 @@ function resolveTabMeta(tabId, typeKey) {
return TAB_META[typeKey]
}
function resolveRiskRuleDescription(payload) {
if (!isPlainObject(payload)) {
return ''
}
return normalizeText(payload.description)
}
function resolveRiskRuleSourceRef(payload) {
if (!isPlainObject(payload)) {
return ''
}
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
return normalizeText(metadata.source_ref)
}
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 '通用'
}
function resolveRiskRuleCategory(source) {
const configJson = readConfigJson(source)
const explicit = normalizeText(configJson.risk_category)
if (explicit) {
return explicit
}
const payloadCategory = normalizeText(source?.risk_category)
if (payloadCategory) {
return payloadCategory
}
const scenarioItems = Array.isArray(source?.scenario_json)
? source.scenario_json
: Array.isArray(source?.scenarioList)
? source.scenarioList
: []
const businessScenario = scenarioItems
.map((item) => normalizeText(item))
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item))
if (businessScenario) {
return businessScenario
}
return inferRiskCategoryFromCode(source?.code)
}
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)}`
}
function applyRiskRuleJsonState(target, payload, apiPayload) {
const rulePayload = isPlainObject(payload) ? payload : {}
const fullDescription =
resolveRiskRuleDescription(rulePayload) ||
normalizeText(apiPayload?.description) ||
normalizeText(target.riskRuleDescription)
const riskCategory =
normalizeText(rulePayload.risk_category) ||
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
return {
...target,
riskRuleDescription: fullDescription,
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
riskCategory,
scope: riskCategory,
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
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)
}
}
function cloneJsonObject(value) {
if (!isPlainObject(value)) {
return null
@@ -812,7 +958,7 @@ function buildRowRuntime(asset, typeKey) {
function buildRowMetric(asset, typeKey) {
if (typeKey === 'rules') {
return asset.reviewer ? `审核人:${asset.reviewer}` : '待分配审核人'
return normalizeText(asset.modified_by) || '未记录'
}
if (typeKey === 'skills') {
return '进入详情查看输出'
@@ -832,6 +978,25 @@ function buildListItem(asset) {
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 riskCategory = isRiskRule ? resolveRiskRuleCategory(asset) : ''
const listSubtitle = isRiskRule
? buildRiskListSubtitle(asset.description)
: normalizeText(asset.description)
return {
id: asset.id,
@@ -842,19 +1007,24 @@ function buildListItem(asset) {
short: makeShort(asset.name),
name: asset.name,
code: asset.code,
summary: asset.description,
summary: listSubtitle,
listSubtitle,
category: resolveDomainLabel(asset.domain),
owner: asset.owner,
reviewer: asset.reviewer || '待分配',
scope: formatScenarioList(asset.scenario_json),
scope: isRiskRule ? riskCategory || '通用' : formatScenarioList(asset.scenario_json),
riskCategory,
model: buildRowRuntime(asset, typeKey),
version: asset.working_version || asset.current_version || '-',
version: workingVersion,
versionDisplay: typeKey === 'rules' ? `${changeCount}` : workingVersion,
publishedVersion: asset.published_version || '-',
workingVersion: asset.working_version || asset.current_version || '-',
workingVersion,
status: statusMeta.label,
statusValue: asset.status,
statusTone: statusMeta.tone,
hitRate: buildRowMetric(asset, typeKey),
hitRate: buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
modifiedBy,
changeCount,
updatedAt: formatDateTime(asset.updated_at),
badgeTone: tabMeta.badgeTone,
spotlight: asset.status === 'active',
@@ -1214,6 +1384,7 @@ function buildDetailViewModel(detail, runs) {
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'
@@ -1239,11 +1410,12 @@ function buildDetailViewModel(detail, runs) {
short: makeShort(detail.name),
name: detail.name,
code: detail.code,
summary: detail.description,
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: formatScenarioList(detail.scenario_json),
scope: usesJsonRiskRule ? resolveRiskRuleCategory(detail) || '通用' : formatScenarioList(detail.scenario_json),
version: detail.working_version || detail.current_version || '-',
currentVersion: detail.current_version || '-',
publishedVersion: detail.published_version || '-',
@@ -1257,6 +1429,13 @@ function buildDetailViewModel(detail, runs) {
badgeTone: tabMeta.badgeTone,
configJson,
usesSpreadsheetRule,
usesJsonRiskRule,
riskRuleJsonText: '{}',
riskRuleSummary: null,
riskRuleDescription: '',
riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '',
riskRuleSourceRef: '',
riskCategory: usesJsonRiskRule ? resolveRiskRuleCategory(detail) : '',
ruleDocument,
scenarioList: Array.isArray(detail.scenario_json) ? [...detail.scenario_json] : [],
markdownContent: previewMarkdown,
@@ -1380,6 +1559,7 @@ export default {
const selectedDomain = ref('')
const selectedOwner = ref('')
const selectedStatus = ref('')
const selectedRiskScenario = ref('')
const loading = ref(false)
const errorMessage = ref('')
const detailLoading = ref(false)
@@ -1434,11 +1614,17 @@ export default {
const createButtonLabel = computed(() => activeMeta.value.createButtonLabel)
const hintText = computed(() => activeMeta.value.hintText)
const tableColumns = computed(() => activeMeta.value.tableColumns)
const showRuntimeColumn = computed(() => activeMeta.value.showRuntimeColumn !== false)
const showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false)
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
const selectedSkillUsesSpreadsheet = computed(
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
)
const selectedSkillUsesJsonRisk = computed(
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule)
)
const canManageSelected = computed(
() => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
)
@@ -1581,13 +1767,23 @@ export default {
const selectedStatusLabel = computed(
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
)
const showRiskScenarioFilter = computed(() => activeType.value === 'riskRules')
const showStatusFilter = computed(() => activeType.value !== 'riskRules')
const selectedRiskScenarioLabel = computed(
() =>
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
'使用场景'
)
const activeFilterTokens = computed(() => {
const tokens = []
if (selectedDomain.value) {
tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`)
}
if (selectedStatus.value) {
if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
tokens.push(`使用场景:${selectedRiskScenario.value}`)
}
if (showStatusFilter.value && selectedStatus.value) {
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
}
if (selectedOwner.value) {
@@ -1612,7 +1808,10 @@ export default {
actionIcon: '',
tone: 'amber',
artLabel: 'ASSET',
tips: ['切换页签可查看其他资产类型', '支持按业务域、负责人和状态做过滤']
tips:
activeType.value === 'riskRules'
? ['切换页签可查看其他资产类型', '支持按业务域、负责人和使用场景做过滤']
: ['切换页签可查看其他资产类型', '支持按业务域、负责人和状态做过滤']
}
}
@@ -1620,7 +1819,9 @@ export default {
eyebrow: '筛选结果为空',
title: `没有找到匹配的${activeTabLabel.value}`,
desc: hasFilters
? '试试清空业务域、负责人、状态或关键词筛选,再重新查看。'
? showRiskScenarioFilter.value
? '试试清空业务域、负责人、使用场景或关键词筛选,再重新查看。'
: '试试清空业务域、负责人、状态或关键词筛选,再重新查看。'
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
actionLabel: hasFilters ? '清空筛选' : '',
@@ -1628,7 +1829,9 @@ export default {
tone: hasFilters ? 'emerald' : 'slate',
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
tips: hasFilters
? ['业务域、负责人、状态与关键词会叠加过滤', '可以换个编码、名称或负责人关键词继续搜索']
? showRiskScenarioFilter.value
? ['业务域、负责人、使用场景与关键词会叠加过滤', '可以换个规则名称或场景分类继续搜索']
: ['业务域、负责人、状态与关键词会叠加过滤', '可以换个编码、名称或负责人关键词继续搜索']
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
}
})
@@ -1675,9 +1878,18 @@ export default {
: true
const matchesDomain = selectedDomain.value ? item.domainValue === selectedDomain.value : true
const matchesOwner = selectedOwner.value ? item.owner === selectedOwner.value : true
const matchesStatus = selectedStatus.value ? item.statusValue === selectedStatus.value : true
const matchesStatus = showStatusFilter.value
? selectedStatus.value
? item.statusValue === selectedStatus.value
: true
: true
const matchesRiskScenario = showRiskScenarioFilter.value
? selectedRiskScenario.value
? item.riskCategory === selectedRiskScenario.value
: true
: true
return matchesKeyword && matchesDomain && matchesOwner && matchesStatus
return matchesKeyword && matchesDomain && matchesOwner && matchesStatus && matchesRiskScenario
})
})
@@ -1723,6 +1935,7 @@ export default {
selectedDomain.value = ''
selectedOwner.value = ''
selectedStatus.value = ''
selectedRiskScenario.value = ''
activeFilterPopover.value = ''
}
@@ -1753,6 +1966,9 @@ export default {
if (name === 'status') {
selectedStatus.value = value
}
if (name === 'riskScenario') {
selectedRiskScenario.value = value
}
closeFilterPopover()
}
@@ -1840,7 +2056,7 @@ export default {
)
spreadsheetChangeRecordsByAsset.value = {
...spreadsheetChangeRecordsByAsset.value,
[assetId]: [nextRecord, ...deduped].slice(0, 5)
[assetId]: [nextRecord, ...deduped].slice(0, 30)
}
}
@@ -1898,22 +2114,51 @@ export default {
function getLatestSpreadsheetChangeKey(assetId) {
const records = spreadsheetChangeRecordsByAsset.value[assetId] || []
const latest = records.find((item) => item?.changed_at)
return latest ? `${latest.changed_at}-${latest.actor}-${latest.summary}` : ''
if (!latest) {
return ''
}
const previewSignature = Array.isArray(latest.cell_changes)
? latest.cell_changes
.slice(0, 8)
.map((item) =>
[
item?.sheet_name,
item?.cell,
item?.change_type,
item?.before_value,
item?.after_value
]
.map((value) => normalizeText(value))
.join(':')
)
.join('|')
: ''
return [
latest.id,
latest.changed_at,
latest.actor,
latest.summary,
latest.changed_sheet_count,
latest.changed_cell_count,
previewSignature
]
.map((value) => normalizeText(value))
.join('-')
}
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
const normalizedAssetId = normalizeText(assetId)
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
return
return false
}
await loadSpreadsheetChangeRecords(normalizedAssetId)
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
return
return true
}
if (attempt >= 9) {
return
return false
}
await new Promise((resolve) => window.setTimeout(resolve, 800))
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
@@ -1973,6 +2218,17 @@ export default {
stopSpreadsheetOnlyOfficeVersionSync()
return
}
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
normalizedAssetId,
previousLatestChangeKey
)
if (changeRecordRefreshed) {
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
await refreshCurrentAssets()
stopSpreadsheetOnlyOfficeVersionSync()
return
}
} catch {
// Ignore transient polling failures and continue retrying within the window.
}
@@ -2275,10 +2531,15 @@ export default {
const detail = await fetchAgentAssetDetail(assetId)
selectedSkill.value = buildDetailViewModel(detail, runs.value)
if (selectedSkill.value?.type === 'rules') {
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
if (!selectedSkill.value.usesJsonRiskRule) {
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
}
if (selectedSkill.value.usesSpreadsheetRule) {
loadSpreadsheetChangeRecords(assetId).catch(() => {})
}
if (selectedSkill.value.usesJsonRiskRule) {
await loadRiskRuleJson(assetId)
}
}
} catch (error) {
detailError.value = error?.message || '资产详情加载失败,请稍后重试。'
@@ -2288,6 +2549,67 @@ export default {
}
}
async function loadRiskRuleJson(assetId) {
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
return
}
const payload = await fetchAgentAssetRuleJson(assetId)
const rulePayload = payload?.payload && typeof payload.payload === 'object' ? payload.payload : payload
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, payload)
}
async function saveRiskRuleJson() {
if (!selectedSkill.value?.id || !canEditMarkdown.value) {
return
}
actionState.value = 'save-risk-json'
detailBusy.value = true
try {
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
const saved = await saveAgentAssetRuleJson(selectedSkill.value.id, { payload: parsed })
const rulePayload = saved?.payload && typeof saved.payload === 'object' ? saved.payload : saved
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, rulePayload, saved)
toast('风险规则 JSON 已保存。')
} catch (error) {
toast(error?.message || '风险规则 JSON 保存失败。')
} finally {
detailBusy.value = false
actionState.value = ''
}
}
function formatRiskRuleJson() {
if (!selectedSkill.value?.usesJsonRiskRule) {
return
}
try {
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, parsed, {
name: selectedSkill.value.name,
description: resolveRiskRuleDescription(parsed)
})
} catch (error) {
toast(error?.message || 'JSON 格式无效,无法格式化。')
}
}
function downloadRiskRuleJson() {
if (!selectedSkill.value?.usesJsonRiskRule) {
return
}
const blob = new Blob([String(selectedSkill.value.riskRuleJsonText || '{}')], {
type: 'application/json;charset=utf-8'
})
const fileName =
selectedSkill.value.ruleDocument?.file_name ||
`${selectedSkill.value.code || 'risk-rule'}.json`
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = fileName
link.click()
URL.revokeObjectURL(link.href)
}
async function loadSpreadsheetChangeRecords(assetId) {
if (!assetId) {
return
@@ -2328,6 +2650,11 @@ export default {
configJson: {},
isPreviewMock: false,
usesSpreadsheetRule: false,
usesJsonRiskRule: false,
riskRuleJsonText: '{}',
riskRuleSummary: null,
riskRuleDescription: '',
riskRuleSourceRef: '',
ruleDocument: null,
scenarioList: [],
fields: [],
@@ -2775,7 +3102,10 @@ export default {
hintText,
searchPlaceholder,
tableColumns,
showRuntimeColumn,
showVersionColumn,
showMetricColumn,
showStatusColumn,
visibleSkills,
auditEmptyState,
loading,
@@ -2785,12 +3115,17 @@ export default {
selectedDomain,
selectedOwner,
selectedStatus,
selectedRiskScenario,
selectedDomainLabel,
selectedOwnerLabel,
selectedStatusLabel,
selectedRiskScenarioLabel,
showRiskScenarioFilter,
showStatusFilter,
domainOptions,
ownerOptions,
statusOptions: STATUS_OPTIONS,
riskScenarioOptions: RISK_SCENARIO_OPTIONS,
activeFilterPopover,
activeFilterTokens,
canManageSelected,
@@ -2806,6 +3141,7 @@ export default {
activateBlockedReason,
selectedSkillIsRule,
selectedSkillUsesSpreadsheet,
selectedSkillUsesJsonRisk,
selectedSpreadsheetFileName,
selectedSpreadsheetVersionModeLabel,
selectedVersionTimelineItems,
@@ -2850,6 +3186,9 @@ export default {
confirmVersionSwitch,
saveRuleMarkdown,
saveRuleRuntimeJson,
saveRiskRuleJson,
formatRiskRuleJson,
downloadRiskRuleJson,
triggerSpreadsheetUpload,
downloadSpreadsheetFile,
handleSpreadsheetFileInput,