后端新增预算本体解析模块和风险规则评分回填服务,优化规则 生成本体对齐和提示词构建,增强费用类型关键词和本体验证, 完善报销查询和审计接口,前端预算中心页面增加对话框和本 体工具函数,重构审计页面元数据和视图模型,补充单元测试。
1963 lines
67 KiB
JavaScript
1963 lines
67 KiB
JavaScript
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||
|
||
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
||
import { fetchEmployees } from '../../services/employees.js'
|
||
import RiskRuleFlowDiagram from '../../components/shared/RiskRuleFlowDiagram.vue'
|
||
import RiskRuleTestDialog from '../../components/shared/RiskRuleTestDialog.vue'
|
||
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
||
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
||
import { useSystemState } from '../../composables/useSystemState.js'
|
||
import { useToast } from '../../composables/useToast.js'
|
||
import {
|
||
activateAgentAsset,
|
||
createAgentAssetReview,
|
||
createAgentAssetVersion,
|
||
deleteAgentAsset,
|
||
fetchAgentAssetDetail,
|
||
fetchAgentAssets,
|
||
fetchAgentAssetSpreadsheetBlob,
|
||
fetchAgentAssetSpreadsheetChangeRecords,
|
||
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
|
||
fetchAgentAssetRuleJson,
|
||
fetchAgentAssetVersionTimeline,
|
||
fetchAgentRuns,
|
||
generateRiskRuleAsset,
|
||
publishRiskRuleAsset,
|
||
returnRiskRuleAsset,
|
||
saveAgentAssetRuleJson,
|
||
importAgentAssetSpreadsheetContent,
|
||
restoreAgentAssetVersion,
|
||
setRiskRuleAssetEnabled,
|
||
updateAgentAsset
|
||
} from '../../services/agentAssets.js'
|
||
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
||
import { isFinanceUser, isManagerUser, isPlatformAdminUser } from '../../utils/accessControl.js'
|
||
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
||
import {
|
||
buildReviewNote,
|
||
buildRuleConfigPayload,
|
||
buildSpreadsheetChangeRecordKey,
|
||
filterAuditAssets,
|
||
incrementVersion
|
||
} from './auditViewRuntimeModel.js'
|
||
|
||
import {
|
||
TAB_META,
|
||
STATUS_OPTIONS,
|
||
ENABLED_STATE_OPTIONS,
|
||
ONLINE_STATE_OPTIONS,
|
||
RISK_SCENARIO_OPTIONS,
|
||
normalizeText,
|
||
readConfigJson,
|
||
resolveRuleTemplateLabel,
|
||
resolveTimelineEventMeta,
|
||
formatDateTime,
|
||
formatSpreadsheetChangeSummary,
|
||
resolveDiffChangeMeta,
|
||
resolveDomainLabel,
|
||
resolveStatusMeta,
|
||
resolveReviewMeta,
|
||
buildListItem,
|
||
buildDetailViewModel,
|
||
applyRiskRuleJsonState,
|
||
resolveRiskRuleDescription,
|
||
buildPreviewRuleDetail,
|
||
buildDefaultRuntimeRule,
|
||
stringifyRuntimeRule,
|
||
parseRuntimeRuleText,
|
||
buildMarkdownVersionContent
|
||
} from './auditViewModel.js'
|
||
import {
|
||
createDefaultRiskRuleForm,
|
||
RISK_RULE_BUSINESS_STAGE_OPTIONS,
|
||
RISK_RULE_EXPENSE_CATEGORY_OPTIONS
|
||
} from './auditViewRiskRuleModel.js'
|
||
|
||
export default {
|
||
name: 'AuditView',
|
||
components: {
|
||
ConfirmDialog,
|
||
RiskRuleFlowDiagram,
|
||
RiskRuleTestDialog,
|
||
TableLoadingState,
|
||
TableEmptyState
|
||
},
|
||
emits: ['detail-open-change'],
|
||
setup(_, { emit }) {
|
||
const { toast } = useToast()
|
||
const { currentUser } = useSystemState()
|
||
|
||
const tabs = Object.entries(TAB_META).map(([id, meta]) => ({
|
||
id,
|
||
label: meta.label
|
||
}))
|
||
|
||
const activeType = ref('financialRules')
|
||
const selectedSkill = ref(null)
|
||
const versionSwitchTarget = ref(null)
|
||
const keyword = ref('')
|
||
const activeFilterPopover = ref('')
|
||
const selectedDomain = ref('')
|
||
const selectedOwner = ref('')
|
||
const selectedStatus = ref('')
|
||
const selectedRiskScenario = ref('')
|
||
const selectedOnlineState = ref('')
|
||
const selectedEnabledState = ref('')
|
||
const loading = ref(false)
|
||
const errorMessage = ref('')
|
||
const detailLoading = ref(false)
|
||
const detailError = ref('')
|
||
const actionState = ref('')
|
||
const reviewSubmitOpen = ref(false)
|
||
const reviewSubmitVersion = ref('')
|
||
const reviewSubmitReviewer = ref('')
|
||
const reviewSubmitReviewerLoading = ref(false)
|
||
const reviewSubmitReviewerOptions = ref([])
|
||
const riskRuleCreateOpen = ref(false)
|
||
const riskRuleCreateForm = ref(createDefaultRiskRuleForm())
|
||
const riskRuleTestOpen = ref(false)
|
||
const riskRuleDeleteOpen = ref(false)
|
||
const riskRuleReturnOpen = ref(false)
|
||
const riskRulePublishOpen = ref(false)
|
||
const riskRuleReturnNote = ref('')
|
||
const runLoading = ref(false)
|
||
const runs = ref([])
|
||
const spreadsheetUploadInput = ref(null)
|
||
const spreadsheetOnlyOfficeLoading = ref(false)
|
||
const spreadsheetOnlyOfficeError = ref('')
|
||
const spreadsheetOnlyOfficeEditor = ref(null)
|
||
const spreadsheetOnlyOfficeReady = ref(false)
|
||
const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
|
||
const versionTimelineOpen = ref(false)
|
||
const versionTimelineLoading = ref(false)
|
||
const versionTimelineError = ref('')
|
||
const versionTimelineItems = ref([])
|
||
const spreadsheetChangeRecordsByAsset = ref({})
|
||
const spreadsheetChangeDetailOpen = ref(false)
|
||
const selectedSpreadsheetChangeRecord = ref(null)
|
||
let spreadsheetOnlyOfficeMountSeq = 0
|
||
let spreadsheetOnlyOfficeLoadTimer = null
|
||
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||
let spreadsheetOnlyOfficeSyncSeq = 0
|
||
let spreadsheetOnlyOfficeChangePollTimer = null
|
||
const riskRuleGenerationPollTimers = new Map()
|
||
const assetBuckets = ref({
|
||
financialRules: [],
|
||
riskRules: [],
|
||
skills: [],
|
||
mcp: []
|
||
})
|
||
|
||
const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
|
||
const isRuleManager = computed(() => isManagerUser(currentUser.value))
|
||
const isFinance = computed(() => isFinanceUser(currentUser.value))
|
||
const activeMeta = computed(() => TAB_META[activeType.value])
|
||
const activeTabLabel = computed(() => activeMeta.value.label)
|
||
const currentAssets = computed(() => assetBuckets.value[activeType.value] || [])
|
||
const searchPlaceholder = computed(() => activeMeta.value.searchPlaceholder)
|
||
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 showOnlineColumn = computed(() => false)
|
||
const showEnabledColumn = computed(() => 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(
|
||
() => isRuleManager.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
|
||
)
|
||
const canAdminOperateSelected = computed(
|
||
() => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
|
||
)
|
||
const canEditSelected = computed(
|
||
() =>
|
||
Boolean(selectedSkill.value) &&
|
||
!selectedSkill.value?.isPreviewMock &&
|
||
(isAdmin.value || isFinance.value)
|
||
)
|
||
const canCreateRiskRule = computed(
|
||
() => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
|
||
)
|
||
const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null)
|
||
const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed))
|
||
const riskRuleInReview = computed(
|
||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'review'
|
||
)
|
||
const riskRuleGenerationBusy = computed(
|
||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'generating'
|
||
)
|
||
const riskRuleGenerationFailed = computed(
|
||
() => selectedSkillUsesJsonRisk.value && selectedSkill.value?.statusValue === 'failed'
|
||
)
|
||
const canOpenRiskRuleTest = computed(
|
||
() =>
|
||
selectedSkillUsesJsonRisk.value &&
|
||
canAdminOperateSelected.value &&
|
||
Boolean(selectedSkill.value?.id) &&
|
||
!riskRuleGenerationBusy.value &&
|
||
!riskRuleGenerationFailed.value
|
||
)
|
||
const canDeleteRiskRule = computed(
|
||
() =>
|
||
selectedSkillUsesJsonRisk.value &&
|
||
canAdminOperateSelected.value &&
|
||
Boolean(selectedSkill.value?.id) &&
|
||
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '')
|
||
)
|
||
const canOpenRiskRuleReviewSubmit = computed(
|
||
() => false
|
||
)
|
||
const canSubmitRiskRuleReview = computed(
|
||
() =>
|
||
canOpenRiskRuleReviewSubmit.value &&
|
||
riskRuleTestPassed.value
|
||
)
|
||
const canReturnRiskRule = computed(
|
||
() => false
|
||
)
|
||
const canPublishRiskRule = computed(
|
||
() =>
|
||
false
|
||
)
|
||
const canToggleRiskRuleEnabled = computed(
|
||
() => selectedSkillUsesJsonRisk.value && canManageSelected.value
|
||
)
|
||
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
|
||
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
|
||
const isDisplayingWorkingVersion = computed(
|
||
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
||
)
|
||
const canSubmitReview = computed(
|
||
() =>
|
||
!selectedSkillUsesJsonRisk.value &&
|
||
canEditSelected.value &&
|
||
selectedSkillIsRule.value &&
|
||
isDisplayingWorkingVersion.value
|
||
)
|
||
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
|
||
const canReviewSelected = computed(
|
||
() => canManageSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
||
)
|
||
const canUploadSpreadsheet = computed(
|
||
() =>
|
||
canEditSelected.value &&
|
||
selectedSkillUsesSpreadsheet.value &&
|
||
!detailBusy.value
|
||
)
|
||
const canDownloadSpreadsheet = computed(
|
||
() =>
|
||
selectedSkillUsesSpreadsheet.value &&
|
||
Boolean(selectedSkill.value?.id) &&
|
||
!detailBusy.value
|
||
)
|
||
const canEditSpreadsheetInline = computed(
|
||
() =>
|
||
selectedSkillUsesSpreadsheet.value &&
|
||
(selectedSkill.value?.isPreviewMock || canEditSelected.value)
|
||
)
|
||
const selectedSpreadsheetFileName = computed(
|
||
() =>
|
||
normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表'
|
||
)
|
||
const selectedSpreadsheetModeLabel = computed(() => {
|
||
if (selectedSkill.value?.isPreviewMock) {
|
||
return canEditSpreadsheetInline.value ? '可编辑' : '只读'
|
||
}
|
||
return canEditSpreadsheetInline.value ? '在线可编辑' : '只读'
|
||
})
|
||
const selectedVersionTimelineItems = computed(() =>
|
||
versionTimelineItems.value.map((item) => ({
|
||
...item,
|
||
meta: resolveTimelineEventMeta(item.event_type),
|
||
timeLabel: formatDateTime(item.event_time)
|
||
}))
|
||
)
|
||
const selectedSpreadsheetChangeRecords = computed(() => {
|
||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
|
||
return []
|
||
}
|
||
return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
|
||
.filter((item) => item?.changed_at)
|
||
.map((item) => {
|
||
const sheetNames = [
|
||
...(Array.isArray(item.sheet_changes)
|
||
? item.sheet_changes.map((change) => normalizeText(change.sheet_name))
|
||
: []),
|
||
...(Array.isArray(item.cell_changes)
|
||
? item.cell_changes.map((change) => normalizeText(change.sheet_name))
|
||
: [])
|
||
].filter(Boolean)
|
||
const changedSheetNames = [...new Set(sheetNames)]
|
||
const previewChanges = Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : []
|
||
return {
|
||
...item,
|
||
time: formatDateTime(item.changed_at),
|
||
summary: formatSpreadsheetChangeSummary(item.summary),
|
||
changeCountLabel: item.changed_cell_count
|
||
? `${item.changed_cell_count} 处改动`
|
||
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
||
changedSheetNames,
|
||
sheetPreview: changedSheetNames.slice(0, 4),
|
||
remainingSheetCount: Math.max(changedSheetNames.length - 4, 0),
|
||
previewChanges,
|
||
remainingChangeCount: Math.max((item.changed_cell_count || 0) - previewChanges.length, 0)
|
||
}
|
||
})
|
||
})
|
||
const selectedSpreadsheetChangeSheetRows = computed(() =>
|
||
Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
|
||
? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
|
||
...item,
|
||
meta: resolveDiffChangeMeta(item.change_type)
|
||
}))
|
||
: []
|
||
)
|
||
const selectedSpreadsheetChangeCellRows = computed(() =>
|
||
Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
|
||
? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
|
||
...item,
|
||
meta: resolveDiffChangeMeta(item.change_type)
|
||
}))
|
||
: []
|
||
)
|
||
const detailBusy = computed(() => Boolean(actionState.value))
|
||
const showReviewNote = computed(
|
||
() => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel)
|
||
)
|
||
const domainOptions = computed(() => {
|
||
const uniqueValues = [...new Set(currentAssets.value.map((item) => item.domainValue).filter(Boolean))]
|
||
return [
|
||
{ value: '', label: '全部业务域' },
|
||
...uniqueValues.map((value) => ({
|
||
value,
|
||
label: resolveDomainLabel(value)
|
||
}))
|
||
]
|
||
})
|
||
const ownerOptions = computed(() => {
|
||
const uniqueOwners = [...new Set(currentAssets.value.map((item) => item.owner).filter(Boolean))]
|
||
return [
|
||
{ value: '', label: activeType.value === 'riskRules' ? '全部审核人' : '全部负责人' },
|
||
...uniqueOwners.map((value) => ({
|
||
value,
|
||
label: value
|
||
}))
|
||
]
|
||
})
|
||
const selectedDomainLabel = computed(
|
||
() => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
|
||
)
|
||
const selectedOwnerLabel = computed(
|
||
() =>
|
||
ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label ||
|
||
(activeType.value === 'riskRules' ? '审核人' : '负责人')
|
||
)
|
||
const selectedStatusLabel = computed(
|
||
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
|
||
)
|
||
const showRiskScenarioFilter = computed(() =>
|
||
['financialRules', 'riskRules'].includes(activeType.value)
|
||
)
|
||
const showStatusFilter = computed(() => true)
|
||
const showOnlineFilter = computed(() => false)
|
||
const showEnabledFilter = computed(() => false)
|
||
const selectedRiskScenarioLabel = computed(
|
||
() =>
|
||
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
|
||
'使用场景'
|
||
)
|
||
const selectedOnlineStateLabel = computed(
|
||
() =>
|
||
ONLINE_STATE_OPTIONS.find((item) => item.value === selectedOnlineState.value)?.label ||
|
||
'是否上线'
|
||
)
|
||
const selectedEnabledStateLabel = computed(
|
||
() =>
|
||
ENABLED_STATE_OPTIONS.find((item) => item.value === selectedEnabledState.value)?.label ||
|
||
'是否启用'
|
||
)
|
||
const activeFilterTokens = computed(() => {
|
||
const tokens = []
|
||
|
||
if (selectedDomain.value) {
|
||
tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`)
|
||
}
|
||
if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
|
||
tokens.push(`使用场景:${selectedRiskScenario.value}`)
|
||
}
|
||
if (showStatusFilter.value && selectedStatus.value) {
|
||
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
|
||
}
|
||
if (showOnlineFilter.value && selectedOnlineState.value) {
|
||
tokens.push(`是否上线:${selectedOnlineStateLabel.value}`)
|
||
}
|
||
if (showEnabledFilter.value && selectedEnabledState.value) {
|
||
tokens.push(`是否启用:${selectedEnabledStateLabel.value}`)
|
||
}
|
||
if (selectedOwner.value) {
|
||
tokens.push(`${activeType.value === 'riskRules' ? '审核人' : '负责人'}:${selectedOwner.value}`)
|
||
}
|
||
if (keyword.value.trim()) {
|
||
tokens.push(`搜索:${keyword.value.trim()}`)
|
||
}
|
||
|
||
return tokens
|
||
})
|
||
const auditEmptyState = computed(() => {
|
||
const hasFilters = activeFilterTokens.value.length > 0
|
||
const supportedFilters = [
|
||
'业务域',
|
||
activeType.value === 'riskRules' ? '审核人' : '负责人',
|
||
...(showRiskScenarioFilter.value ? ['使用场景'] : []),
|
||
...(showStatusFilter.value ? ['状态'] : []),
|
||
...(showOnlineFilter.value ? ['是否上线'] : []),
|
||
...(showEnabledFilter.value ? ['是否启用'] : []),
|
||
'关键词'
|
||
]
|
||
|
||
if (!currentAssets.value.length) {
|
||
return {
|
||
eyebrow: `${activeTabLabel.value}资产`,
|
||
title: `${activeTabLabel.value}列表暂时还是空的`,
|
||
desc: `当前环境里还没有可展示的${activeTabLabel.value}资产。完成接入或同步后,会统一展示在这里。`,
|
||
icon: 'mdi mdi-database-search-outline',
|
||
actionLabel: '',
|
||
actionIcon: '',
|
||
tone: 'amber',
|
||
artLabel: 'ASSET',
|
||
tips: [
|
||
'切换页签可查看其他资产类型',
|
||
`支持按${supportedFilters.slice(0, -1).join('、')}和关键词做过滤`
|
||
]
|
||
}
|
||
}
|
||
|
||
return {
|
||
eyebrow: '筛选结果为空',
|
||
title: `没有找到匹配的${activeTabLabel.value}`,
|
||
desc: hasFilters
|
||
? `试试清空${supportedFilters.join('、')}筛选,再重新查看。`
|
||
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
|
||
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
|
||
actionLabel: hasFilters ? '清空筛选' : '',
|
||
actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : '',
|
||
tone: hasFilters ? 'emerald' : 'slate',
|
||
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
|
||
tips: hasFilters
|
||
? [
|
||
`${supportedFilters.join('、')}会叠加过滤`,
|
||
showRiskScenarioFilter.value
|
||
? '可以换个规则名称或场景分类继续搜索'
|
||
: '可以换个编码、名称或负责人关键词继续搜索'
|
||
]
|
||
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
|
||
}
|
||
})
|
||
const canActivateSelected = computed(() => {
|
||
if (!selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
||
return false
|
||
}
|
||
|
||
return (
|
||
isDisplayingWorkingVersion.value &&
|
||
selectedSkill.value?.reviewStatusValue === 'approved' &&
|
||
selectedSkill.value?.workingVersion !== selectedSkill.value?.publishedVersion
|
||
)
|
||
})
|
||
const activateBlockedReason = computed(() => {
|
||
if (!selectedSkillIsRule.value) {
|
||
return ''
|
||
}
|
||
if (selectedSkill.value?.isPreviewMock) {
|
||
return '当前为页面预览态,暂不执行真实审核和上线。'
|
||
}
|
||
if (!canManageSelected.value) {
|
||
return '仅高级管理人员可执行审核和上线。'
|
||
}
|
||
if (!isDisplayingWorkingVersion.value) {
|
||
return '请先切回当前工作版本,再执行审核或上线。'
|
||
}
|
||
if (selectedSkill.value?.workingVersion === selectedSkill.value?.publishedVersion) {
|
||
return '当前工作版本已经是线上版本。'
|
||
}
|
||
if (selectedSkill.value?.reviewStatusValue !== 'approved') {
|
||
return '当前规则版本未审核通过,不能上线。'
|
||
}
|
||
return ''
|
||
})
|
||
const visibleSkills = computed(() =>
|
||
filterAuditAssets(currentAssets.value, {
|
||
keyword: keyword.value,
|
||
selectedDomain: selectedDomain.value,
|
||
selectedOwner: selectedOwner.value,
|
||
selectedStatus: selectedStatus.value,
|
||
selectedRiskScenario: selectedRiskScenario.value,
|
||
selectedOnlineState: selectedOnlineState.value,
|
||
selectedEnabledState: selectedEnabledState.value,
|
||
showStatusFilter: showStatusFilter.value,
|
||
showRiskScenarioFilter: showRiskScenarioFilter.value,
|
||
showOnlineFilter: showOnlineFilter.value,
|
||
showEnabledFilter: showEnabledFilter.value
|
||
})
|
||
)
|
||
|
||
watch(
|
||
selectedSkill,
|
||
(value) => {
|
||
emit('detail-open-change', Boolean(value))
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
watch(
|
||
() => [
|
||
selectedSkill.value?.id || '',
|
||
selectedSkill.value?.loading ? '1' : '0',
|
||
selectedSkill.value?.usesSpreadsheetRule ? '1' : '0'
|
||
],
|
||
async () => {
|
||
if (!selectedSkillUsesSpreadsheet.value || selectedSkill.value?.loading) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
spreadsheetOnlyOfficeError.value = ''
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
return
|
||
}
|
||
await mountSpreadsheetOnlyOfficeEditor()
|
||
}
|
||
)
|
||
|
||
watch(activeType, () => {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
selectedSkill.value = null
|
||
versionSwitchTarget.value = null
|
||
resetFilters()
|
||
loadAssets({ force: true }).catch((error) => {
|
||
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
||
})
|
||
})
|
||
|
||
function resetFilters() {
|
||
keyword.value = ''
|
||
selectedDomain.value = ''
|
||
selectedOwner.value = ''
|
||
selectedStatus.value = ''
|
||
selectedRiskScenario.value = ''
|
||
selectedOnlineState.value = ''
|
||
selectedEnabledState.value = ''
|
||
activeFilterPopover.value = ''
|
||
}
|
||
|
||
function handleAuditEmptyAction() {
|
||
if (!currentAssets.value.length || !activeFilterTokens.value.length) {
|
||
loadAssets({ force: true }).catch(() => {})
|
||
return
|
||
}
|
||
|
||
resetFilters()
|
||
}
|
||
|
||
function toggleFilterPopover(name) {
|
||
activeFilterPopover.value = activeFilterPopover.value === name ? '' : name
|
||
}
|
||
|
||
function closeFilterPopover() {
|
||
activeFilterPopover.value = ''
|
||
}
|
||
|
||
function selectFilter(name, value) {
|
||
if (name === 'domain') {
|
||
selectedDomain.value = value
|
||
}
|
||
if (name === 'owner') {
|
||
selectedOwner.value = value
|
||
}
|
||
if (name === 'status') {
|
||
selectedStatus.value = value
|
||
}
|
||
if (name === 'riskScenario') {
|
||
selectedRiskScenario.value = value
|
||
}
|
||
if (name === 'online') {
|
||
selectedOnlineState.value = value
|
||
}
|
||
if (name === 'enabled') {
|
||
selectedEnabledState.value = value
|
||
}
|
||
closeFilterPopover()
|
||
}
|
||
|
||
function handleDocumentClick(event) {
|
||
const target = event.target
|
||
if (!(target instanceof Element)) {
|
||
closeFilterPopover()
|
||
return
|
||
}
|
||
if (!target.closest('.picker-filter')) {
|
||
closeFilterPopover()
|
||
}
|
||
}
|
||
|
||
function resolveActor() {
|
||
return currentUser.value?.name || currentUser.value?.username || 'system'
|
||
}
|
||
|
||
function openRiskRuleCreateDialog() {
|
||
if (activeType.value !== 'riskRules') {
|
||
return
|
||
}
|
||
riskRuleCreateForm.value = createDefaultRiskRuleForm()
|
||
riskRuleCreateOpen.value = true
|
||
}
|
||
|
||
function closeRiskRuleCreateDialog() {
|
||
if (riskRuleCreateBusy.value) {
|
||
return
|
||
}
|
||
riskRuleCreateOpen.value = false
|
||
}
|
||
|
||
async function submitRiskRuleCreate() {
|
||
if (!canCreateRiskRule.value || riskRuleCreateBusy.value) {
|
||
return
|
||
}
|
||
const naturalLanguage = String(riskRuleCreateForm.value.natural_language || '').trim()
|
||
const ruleTitle = String(riskRuleCreateForm.value.rule_title || '').trim()
|
||
if (ruleTitle.length < 2) {
|
||
toast('请输入至少 2 个字的规则标题。')
|
||
return
|
||
}
|
||
if (naturalLanguage.length < 8) {
|
||
toast('请至少输入 8 个字的风险规则描述。')
|
||
return
|
||
}
|
||
|
||
actionState.value = 'generate-risk-rule'
|
||
try {
|
||
const detail = await generateRiskRuleAsset(
|
||
{
|
||
business_domain: 'expense',
|
||
business_stage: riskRuleCreateForm.value.business_stage,
|
||
expense_category: riskRuleCreateForm.value.expense_category,
|
||
rule_title: ruleTitle,
|
||
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
|
||
natural_language: naturalLanguage
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
riskRuleCreateOpen.value = false
|
||
await refreshCurrentAssets()
|
||
scheduleRiskRuleGenerationPoll(detail.id)
|
||
toast('风险规则已进入后台生成,列表会先显示生成中。')
|
||
} catch (error) {
|
||
toast(error?.message || '风险规则生成失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
function stopRiskRuleGenerationPoll(assetId) {
|
||
const timer = riskRuleGenerationPollTimers.get(assetId)
|
||
if (timer) {
|
||
window.clearTimeout(timer)
|
||
riskRuleGenerationPollTimers.delete(assetId)
|
||
}
|
||
}
|
||
|
||
function scheduleRiskRuleGenerationPoll(assetId, attempt = 0) {
|
||
const normalizedAssetId = normalizeText(assetId)
|
||
if (!normalizedAssetId) {
|
||
return
|
||
}
|
||
stopRiskRuleGenerationPoll(normalizedAssetId)
|
||
const timer = window.setTimeout(async () => {
|
||
try {
|
||
await refreshCurrentAssets()
|
||
const latest = (assetBuckets.value.riskRules || []).find((item) => item.id === normalizedAssetId)
|
||
if (!latest || latest.statusValue !== 'generating' || attempt >= 59) {
|
||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||
return
|
||
}
|
||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||
} catch {
|
||
if (attempt < 59) {
|
||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||
} else {
|
||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||
}
|
||
}
|
||
}, attempt === 0 ? 1200 : 3000)
|
||
riskRuleGenerationPollTimers.set(normalizedAssetId, timer)
|
||
}
|
||
|
||
async function persistRuleRuntimeConfig(asset, runtimeRule) {
|
||
await updateAgentAsset(
|
||
asset.id,
|
||
{
|
||
config_json: buildRuleConfigPayload(asset, runtimeRule)
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
}
|
||
|
||
function destroySpreadsheetOnlyOfficeEditor() {
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||
spreadsheetOnlyOfficeSyncSeq += 1
|
||
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
||
spreadsheetOnlyOfficeEditor.value.destroyEditor()
|
||
}
|
||
spreadsheetOnlyOfficeEditor.value = null
|
||
spreadsheetOnlyOfficeReady.value = false
|
||
}
|
||
|
||
function stopSpreadsheetOnlyOfficeChangeSync() {
|
||
if (spreadsheetOnlyOfficeChangePollTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
|
||
spreadsheetOnlyOfficeChangePollTimer = null
|
||
}
|
||
}
|
||
|
||
function getLatestSpreadsheetChangeKey(assetId) {
|
||
return buildSpreadsheetChangeRecordKey(spreadsheetChangeRecordsByAsset.value[assetId] || [])
|
||
}
|
||
|
||
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
|
||
const normalizedAssetId = normalizeText(assetId)
|
||
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return false
|
||
}
|
||
|
||
await loadSpreadsheetChangeRecords(normalizedAssetId)
|
||
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
|
||
return true
|
||
}
|
||
if (attempt >= 9) {
|
||
return false
|
||
}
|
||
await new Promise((resolve) => window.setTimeout(resolve, 800))
|
||
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
||
}
|
||
|
||
function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
|
||
const normalizedAssetId = normalizeText(assetId)
|
||
if (!normalizedAssetId) {
|
||
return
|
||
}
|
||
|
||
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||
|
||
const runSync = async () => {
|
||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
||
normalizedAssetId,
|
||
previousLatestChangeKey
|
||
)
|
||
if (changeRecordRefreshed) {
|
||
await refreshCurrentAssets()
|
||
stopSpreadsheetOnlyOfficeChangeSync()
|
||
return
|
||
}
|
||
} catch {
|
||
// Ignore transient polling failures and continue retrying within the window.
|
||
}
|
||
|
||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||
return
|
||
}
|
||
if (attempt >= 29) {
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
|
||
}, 2000)
|
||
}
|
||
|
||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||
runSync().catch(() => {})
|
||
}, attempt === 0 ? 800 : 2000)
|
||
}
|
||
|
||
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
|
||
return (
|
||
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
||
!selectedSkillUsesSpreadsheet.value ||
|
||
selectedSkill.value?.id !== assetId ||
|
||
selectedSkill.value?.loading
|
||
)
|
||
}
|
||
|
||
async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
|
||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
return
|
||
}
|
||
|
||
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
||
const assetId = selectedSkill.value.id
|
||
const editable = canEditSpreadsheetInline.value
|
||
|
||
spreadsheetOnlyOfficeLoading.value = true
|
||
spreadsheetOnlyOfficeError.value = ''
|
||
spreadsheetOnlyOfficeReady.value = false
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
|
||
try {
|
||
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
|
||
await loadOnlyOfficeApi(payload.documentServerUrl)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (!window.DocsAPI?.DocEditor) {
|
||
throw new Error('表格编辑器未正确加载。')
|
||
}
|
||
|
||
// Host id must be unique for every mount. ONLYOFFICE mutates its host DOM
|
||
// during lifecycle teardown; reusing the same element can leave the next
|
||
// DocEditor instance with a dead container even though config loading succeeds.
|
||
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
|
||
await nextTick()
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
|
||
const config = buildOnlyOfficeEditorConfig(payload.config, {
|
||
viewportHeight: window.innerHeight,
|
||
editable,
|
||
fillContainer: true
|
||
})
|
||
const upstreamEvents = config.events || {}
|
||
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (retryAttempt < 1) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
spreadsheetOnlyOfficeLoading.value = true
|
||
window.setTimeout(() => {
|
||
mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
|
||
}, 600)
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeError.value = '表格加载超时,请退出详情后重试。'
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
}, 15000)
|
||
config.events = {
|
||
...upstreamEvents,
|
||
onAppReady(event) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
spreadsheetOnlyOfficeReady.value = true
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
upstreamEvents.onAppReady?.(event)
|
||
},
|
||
onError(event) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||
spreadsheetOnlyOfficeLoadTimer = null
|
||
}
|
||
const errorCode = event?.data?.errorCode
|
||
const errorDescription = event?.data?.errorDescription
|
||
spreadsheetOnlyOfficeError.value = errorDescription
|
||
? `表格加载失败:${errorDescription}`
|
||
: `表格加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
upstreamEvents.onError?.(event)
|
||
},
|
||
onDocumentStateChange(event) {
|
||
const hasChanges = Boolean(event?.data)
|
||
if (hasChanges) {
|
||
spreadsheetOnlyOfficeHadLocalEdits = true
|
||
if (!spreadsheetOnlyOfficeChangePollTimer) {
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||
}
|
||
} else if (
|
||
spreadsheetOnlyOfficeHadLocalEdits &&
|
||
editable &&
|
||
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
|
||
) {
|
||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||
}
|
||
upstreamEvents.onDocumentStateChange?.(event)
|
||
}
|
||
}
|
||
spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
|
||
spreadsheetOnlyOfficeHostId.value,
|
||
config
|
||
)
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
}
|
||
} catch (error) {
|
||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||
return
|
||
}
|
||
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
toast(spreadsheetOnlyOfficeError.value)
|
||
}
|
||
}
|
||
|
||
function triggerSpreadsheetUpload() {
|
||
if (!canUploadSpreadsheet.value) {
|
||
return
|
||
}
|
||
spreadsheetUploadInput.value?.click()
|
||
}
|
||
|
||
async function downloadSpreadsheetFile() {
|
||
if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
|
||
return
|
||
}
|
||
|
||
actionState.value = 'download-spreadsheet'
|
||
try {
|
||
const blob = await fetchAgentAssetSpreadsheetBlob(
|
||
selectedSkill.value.id,
|
||
'attachment'
|
||
)
|
||
const objectUrl = URL.createObjectURL(blob)
|
||
const anchor = document.createElement('a')
|
||
anchor.href = objectUrl
|
||
anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
|
||
document.body.appendChild(anchor)
|
||
anchor.click()
|
||
anchor.remove()
|
||
URL.revokeObjectURL(objectUrl)
|
||
} catch (error) {
|
||
toast(error?.message || '规则表下载失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function uploadSpreadsheetFile(file) {
|
||
if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
|
||
return
|
||
}
|
||
|
||
actionState.value = 'upload-spreadsheet'
|
||
try {
|
||
await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
|
||
actor: resolveActor()
|
||
})
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
||
toast(`已导入 ${file.name} 的表格内容,右侧会记录本次修改。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
if (spreadsheetUploadInput.value) {
|
||
spreadsheetUploadInput.value.value = ''
|
||
}
|
||
}
|
||
}
|
||
|
||
async function handleSpreadsheetFileInput(event) {
|
||
await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
|
||
}
|
||
|
||
async function loadRuns(options = {}) {
|
||
if (runLoading.value && !options.force) {
|
||
return
|
||
}
|
||
|
||
runLoading.value = true
|
||
try {
|
||
const payload = await fetchAgentRuns({ limit: 50 })
|
||
runs.value = Array.isArray(payload) ? payload : []
|
||
} finally {
|
||
runLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadAssets(options = {}) {
|
||
const shouldShowLoading = !options.silent && !options.background
|
||
if (shouldShowLoading) {
|
||
loading.value = true
|
||
}
|
||
if (!options.silent) {
|
||
errorMessage.value = ''
|
||
}
|
||
|
||
try {
|
||
const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
|
||
const items = Array.isArray(payload) ? payload.map(buildListItem).filter(Boolean) : []
|
||
|
||
if (activeMeta.value.assetType === 'rule') {
|
||
const nextBuckets = {
|
||
financialRules: [],
|
||
riskRules: []
|
||
}
|
||
|
||
items.forEach((item) => {
|
||
if (item?.tabId === 'financialRules' || item?.tabId === 'riskRules') {
|
||
nextBuckets[item.tabId].push(item)
|
||
}
|
||
})
|
||
|
||
assetBuckets.value = {
|
||
...assetBuckets.value,
|
||
...nextBuckets
|
||
}
|
||
} else {
|
||
assetBuckets.value = {
|
||
...assetBuckets.value,
|
||
[activeType.value]: items
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (options.silent || options.background) {
|
||
return
|
||
}
|
||
if (activeMeta.value.assetType === 'rule') {
|
||
assetBuckets.value = {
|
||
...assetBuckets.value,
|
||
financialRules:
|
||
activeType.value === 'financialRules' ? [] : assetBuckets.value.financialRules,
|
||
riskRules: []
|
||
}
|
||
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
||
} else {
|
||
assetBuckets.value = {
|
||
...assetBuckets.value,
|
||
[activeType.value]: []
|
||
}
|
||
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
||
}
|
||
if (!options.silent) {
|
||
toast(errorMessage.value)
|
||
}
|
||
} finally {
|
||
if (shouldShowLoading) {
|
||
loading.value = false
|
||
}
|
||
}
|
||
}
|
||
|
||
async function refreshCurrentAssets() {
|
||
await loadAssets({ force: true, silent: true, background: true })
|
||
}
|
||
|
||
async function loadSelectedAssetDetail(assetId) {
|
||
detailLoading.value = true
|
||
detailError.value = ''
|
||
|
||
try {
|
||
if (!runs.value.length) {
|
||
await loadRuns()
|
||
}
|
||
const detail = await fetchAgentAssetDetail(assetId)
|
||
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
||
if (selectedSkill.value?.type === 'rules') {
|
||
if (!selectedSkill.value.usesSpreadsheetRule && !selectedSkill.value.usesJsonRiskRule) {
|
||
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
||
}
|
||
if (selectedSkill.value.usesSpreadsheetRule) {
|
||
loadSpreadsheetChangeRecords(assetId).catch(() => {})
|
||
}
|
||
if (selectedSkill.value.usesJsonRiskRule) {
|
||
if (selectedSkill.value.riskRuleGenerationFailed || selectedSkill.value.riskRuleGenerationBusy) {
|
||
return
|
||
}
|
||
try {
|
||
await loadRiskRuleJson(assetId)
|
||
} catch (jsonError) {
|
||
console.warn('Failed to load risk rule JSON:', jsonError)
|
||
const jsonMessage =
|
||
jsonError?.message || '风险规则 JSON 文件缺失或无法读取,请同步规则库后重试。'
|
||
toast(jsonMessage)
|
||
selectedSkill.value = {
|
||
...selectedSkill.value,
|
||
riskRuleJsonText: '{}',
|
||
riskRuleDescription:
|
||
selectedSkill.value.riskRuleDescription ||
|
||
'规则 JSON 尚未就绪,请联系管理员执行平台风险规则同步。'
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
detailError.value = error?.message || '资产详情加载失败,请稍后重试。'
|
||
toast(detailError.value)
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
}
|
||
|
||
function mergeSelectedRuleLifecycle(detail) {
|
||
if (!selectedSkill.value || !detail) {
|
||
return
|
||
}
|
||
const next = buildDetailViewModel(detail, runs.value)
|
||
selectedSkill.value = {
|
||
...selectedSkill.value,
|
||
status: next.status,
|
||
statusValue: next.statusValue,
|
||
statusTone: next.statusTone,
|
||
publishedVersion: next.publishedVersion,
|
||
workingVersion: next.workingVersion,
|
||
currentVersion: next.currentVersion,
|
||
displayVersion: next.displayVersion,
|
||
reviewer: next.reviewer,
|
||
publisher: next.publisher,
|
||
publishedAt: next.publishedAt,
|
||
isOnlineValue: next.isOnlineValue,
|
||
isOnlineLabel: next.isOnlineLabel,
|
||
isOnlineTone: next.isOnlineTone,
|
||
isEnabledValue: next.isEnabledValue,
|
||
isEnabledLabel: next.isEnabledLabel,
|
||
isEnabledTone: next.isEnabledTone,
|
||
latestTestSummary: next.latestTestSummary,
|
||
lastOperationLabel: next.lastOperationLabel,
|
||
lastOperationTone: next.lastOperationTone,
|
||
publishMeta: next.publishMeta,
|
||
publishState: next.publishState,
|
||
updatedAt: next.updatedAt,
|
||
configJson: next.configJson
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
|
||
spreadsheetChangeRecordsByAsset.value = {
|
||
...spreadsheetChangeRecordsByAsset.value,
|
||
[assetId]: Array.isArray(payload) ? payload : []
|
||
}
|
||
}
|
||
|
||
function openSpreadsheetChangeDetail(item) {
|
||
if (!item?.changed_at) {
|
||
return
|
||
}
|
||
selectedSpreadsheetChangeRecord.value = item
|
||
spreadsheetChangeDetailOpen.value = true
|
||
}
|
||
|
||
function closeSpreadsheetChangeDetail() {
|
||
spreadsheetChangeDetailOpen.value = false
|
||
}
|
||
|
||
function openAssetDetail(asset) {
|
||
if (asset?.usesJsonRiskRule && asset.statusValue === 'generating') {
|
||
toast('规则仍在后台生成中,生成完成后才能进入详情。')
|
||
return
|
||
}
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
spreadsheetOnlyOfficeError.value = ''
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
if (asset?.isPreviewMock) {
|
||
selectedSkill.value = buildPreviewRuleDetail()
|
||
detailError.value = ''
|
||
detailLoading.value = false
|
||
versionSwitchTarget.value = null
|
||
return
|
||
}
|
||
const opensSpreadsheetRule = Boolean(asset?.usesSpreadsheetRule)
|
||
selectedSkill.value = {
|
||
...asset,
|
||
configJson: {},
|
||
isPreviewMock: false,
|
||
usesSpreadsheetRule: opensSpreadsheetRule,
|
||
usesJsonRiskRule: Boolean(asset?.usesJsonRiskRule),
|
||
riskRuleJsonText: '{}',
|
||
riskRuleSummary: null,
|
||
riskRuleDescription: '',
|
||
riskRuleSourceRef: '',
|
||
ruleDocument: asset?.ruleDocument || null,
|
||
scenarioList: [],
|
||
fields: [],
|
||
promptSections: [],
|
||
outputRules: [],
|
||
tests: [],
|
||
triggers: [],
|
||
tools: [],
|
||
history: [],
|
||
markdownContent: '',
|
||
runtimeRuleText: '',
|
||
ruleTemplateKey: '',
|
||
ruleTemplateLabel: '',
|
||
runtimeKind: 'policy_rule_draft',
|
||
displayVersion: asset.version,
|
||
displayVersionChangeNote: '无版本说明',
|
||
loading: !opensSpreadsheetRule,
|
||
reviewStatusLabel: opensSpreadsheetRule ? '' : '加载中',
|
||
reviewStatusTone: 'draft'
|
||
}
|
||
versionSwitchTarget.value = null
|
||
if (opensSpreadsheetRule) {
|
||
loadSpreadsheetChangeRecords(asset.id).catch(() => {})
|
||
}
|
||
loadSelectedAssetDetail(asset.id).catch(() => {})
|
||
}
|
||
|
||
function closeDetail() {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
spreadsheetOnlyOfficeError.value = ''
|
||
spreadsheetOnlyOfficeLoading.value = false
|
||
selectedSkill.value = null
|
||
detailError.value = ''
|
||
detailLoading.value = false
|
||
versionSwitchTarget.value = null
|
||
versionTimelineOpen.value = false
|
||
versionTimelineItems.value = []
|
||
riskRuleTestOpen.value = false
|
||
riskRuleDeleteOpen.value = false
|
||
riskRuleReturnOpen.value = false
|
||
riskRulePublishOpen.value = false
|
||
riskRuleReturnNote.value = ''
|
||
}
|
||
|
||
function openVersionSwitch(version) {
|
||
if (!selectedSkill.value || version.version === selectedSkill.value.displayVersion) {
|
||
return
|
||
}
|
||
versionSwitchTarget.value = version
|
||
}
|
||
|
||
function cancelVersionSwitch() {
|
||
versionSwitchTarget.value = null
|
||
}
|
||
|
||
function confirmVersionSwitch() {
|
||
if (!selectedSkill.value || !versionSwitchTarget.value) {
|
||
return
|
||
}
|
||
|
||
selectedSkill.value.displayVersion = versionSwitchTarget.value.version
|
||
selectedSkill.value.displayVersionChangeNote = versionSwitchTarget.value.note || '无版本说明'
|
||
if (selectedSkill.value.usesSpreadsheetRule) {
|
||
versionSwitchTarget.value = null
|
||
return
|
||
}
|
||
if (typeof versionSwitchTarget.value.markdownContent === 'string') {
|
||
selectedSkill.value.markdownContent = versionSwitchTarget.value.markdownContent
|
||
}
|
||
const runtimeRule = versionSwitchTarget.value.runtimeRule || buildDefaultRuntimeRule(selectedSkill.value)
|
||
selectedSkill.value.runtimeRuleText = stringifyRuntimeRule(runtimeRule)
|
||
selectedSkill.value.runtimeKind =
|
||
normalizeText(runtimeRule.kind) || selectedSkill.value.runtimeKind || 'policy_rule_draft'
|
||
selectedSkill.value.ruleTemplateKey =
|
||
normalizeText(runtimeRule.template_key) || selectedSkill.value.ruleTemplateKey
|
||
selectedSkill.value.ruleTemplateLabel = resolveRuleTemplateLabel(selectedSkill.value.ruleTemplateKey)
|
||
versionSwitchTarget.value = null
|
||
}
|
||
|
||
async function saveRuleMarkdown() {
|
||
if (
|
||
!selectedSkill.value ||
|
||
!selectedSkillIsRule.value ||
|
||
selectedSkill.value.usesSpreadsheetRule ||
|
||
!canEditMarkdown.value ||
|
||
detailBusy.value
|
||
) {
|
||
return
|
||
}
|
||
|
||
if (!normalizeText(selectedSkill.value.markdownContent)) {
|
||
toast('规则 Markdown 内容不能为空。')
|
||
return
|
||
}
|
||
|
||
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
|
||
if (!runtimeRule) {
|
||
toast('运行时 JSON 必须是合法的对象。')
|
||
return
|
||
}
|
||
|
||
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
|
||
actionState.value = 'save-markdown'
|
||
|
||
try {
|
||
await createAgentAssetVersion(
|
||
selectedSkill.value.id,
|
||
{
|
||
version: nextVersion,
|
||
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||
content_type: 'markdown',
|
||
change_note: '通过任务规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
||
created_by: resolveActor()
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast(`规则 Markdown 已保存为 ${nextVersion}。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则 Markdown 保存失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function saveRuleRuntimeJson() {
|
||
if (
|
||
!selectedSkill.value ||
|
||
!selectedSkillIsRule.value ||
|
||
selectedSkill.value.usesSpreadsheetRule ||
|
||
!canEditMarkdown.value ||
|
||
detailBusy.value
|
||
) {
|
||
return
|
||
}
|
||
|
||
if (!normalizeText(selectedSkill.value.markdownContent)) {
|
||
toast('规则 Markdown 模板不能为空。')
|
||
return
|
||
}
|
||
|
||
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
|
||
if (!runtimeRule) {
|
||
toast('运行时 JSON 必须是合法的对象。')
|
||
return
|
||
}
|
||
|
||
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
|
||
actionState.value = 'save-runtime-json'
|
||
|
||
try {
|
||
await createAgentAssetVersion(
|
||
selectedSkill.value.id,
|
||
{
|
||
version: nextVersion,
|
||
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||
content_type: 'markdown',
|
||
change_note: '通过任务规则中心保存运行时 JSON 配置。',
|
||
created_by: resolveActor()
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast(`规则 JSON 已保存为 ${nextVersion}。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则 JSON 保存失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function reviewSelectedRule(reviewStatus) {
|
||
if (!selectedSkill.value || !selectedSkillIsRule.value || detailBusy.value) {
|
||
return
|
||
}
|
||
if (reviewStatus === 'pending' && !canSubmitReview.value) {
|
||
return
|
||
}
|
||
if (reviewStatus !== 'pending' && !canReviewSelected.value) {
|
||
return
|
||
}
|
||
|
||
actionState.value = `review-${reviewStatus}`
|
||
|
||
try {
|
||
await createAgentAssetReview(
|
||
selectedSkill.value.id,
|
||
{
|
||
version: selectedSkill.value.workingVersion,
|
||
reviewer: resolveActor(),
|
||
review_status: reviewStatus,
|
||
review_note: buildReviewNote(reviewStatus)
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast(`当前规则版本已标记为${resolveReviewMeta(reviewStatus).label}。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function loadReviewSubmitReviewers() {
|
||
reviewSubmitReviewerLoading.value = true
|
||
try {
|
||
const employees = await fetchEmployees()
|
||
reviewSubmitReviewerOptions.value = (Array.isArray(employees) ? employees : [])
|
||
.filter(
|
||
(item) =>
|
||
item.status === '在职' &&
|
||
Array.isArray(item.roleCodes) &&
|
||
item.roleCodes.includes('manager')
|
||
)
|
||
.map((item) => ({
|
||
value: item.name,
|
||
label: `${item.name} · ${item.position || '高级管理员'}`
|
||
}))
|
||
} catch (error) {
|
||
reviewSubmitReviewerOptions.value = []
|
||
toast(error?.message || '审核人列表加载失败,请稍后重试。')
|
||
} finally {
|
||
reviewSubmitReviewerLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openSubmitReviewDialog() {
|
||
if (
|
||
selectedSkillUsesJsonRisk.value &&
|
||
!canOpenRiskRuleReviewSubmit.value
|
||
) {
|
||
return
|
||
}
|
||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||
return
|
||
}
|
||
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
|
||
reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
|
||
reviewSubmitOpen.value = true
|
||
await loadReviewSubmitReviewers()
|
||
if (
|
||
!reviewSubmitReviewerOptions.value.some(
|
||
(item) => item.value === reviewSubmitReviewer.value
|
||
)
|
||
) {
|
||
reviewSubmitReviewer.value = reviewSubmitReviewerOptions.value[0]?.value || ''
|
||
}
|
||
}
|
||
|
||
function closeSubmitReviewDialog() {
|
||
if (detailBusy.value) {
|
||
return
|
||
}
|
||
reviewSubmitOpen.value = false
|
||
}
|
||
|
||
async function submitSelectedRuleForReview() {
|
||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||
return
|
||
}
|
||
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
|
||
toast('当前规则版本尚未确认测试通过,不能提交审核。')
|
||
return
|
||
}
|
||
const version = normalizeText(reviewSubmitVersion.value)
|
||
const reviewer = normalizeText(reviewSubmitReviewer.value)
|
||
if (!version) {
|
||
toast('请输入送审版本号。')
|
||
return
|
||
}
|
||
if (!reviewer) {
|
||
toast('请选择审核人。')
|
||
return
|
||
}
|
||
|
||
actionState.value = 'review-pending'
|
||
try {
|
||
await createAgentAssetReview(
|
||
selectedSkill.value.id,
|
||
{
|
||
version,
|
||
reviewer,
|
||
review_status: 'pending',
|
||
review_note: buildReviewNote('pending')
|
||
},
|
||
{ actor: resolveActor() }
|
||
)
|
||
reviewSubmitOpen.value = false
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast(`规则版本 ${version} 已提交给 ${reviewer} 审核。`)
|
||
} catch (error) {
|
||
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
function openRiskRuleTestDialog() {
|
||
if (detailBusy.value) {
|
||
return
|
||
}
|
||
if (!canOpenRiskRuleTest.value) {
|
||
if (!selectedSkill.value?.id) {
|
||
toast('规则详情还没有加载完成,请稍后再测试。')
|
||
}
|
||
return
|
||
}
|
||
riskRuleTestOpen.value = true
|
||
}
|
||
|
||
function closeRiskRuleTestDialog() {
|
||
riskRuleTestOpen.value = false
|
||
}
|
||
|
||
async function handleRiskRuleReportSaved(summary) {
|
||
if (selectedSkill.value) {
|
||
selectedSkill.value.latestTestSummary = summary
|
||
}
|
||
await refreshCurrentAssets()
|
||
if (selectedSkill.value?.id) {
|
||
const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
|
||
mergeSelectedRuleLifecycle(detail)
|
||
}
|
||
}
|
||
|
||
function openDeleteRiskRuleDialog() {
|
||
if (!canDeleteRiskRule.value) {
|
||
return
|
||
}
|
||
riskRuleDeleteOpen.value = true
|
||
}
|
||
|
||
function closeDeleteRiskRuleDialog() {
|
||
if (detailBusy.value) {
|
||
return
|
||
}
|
||
riskRuleDeleteOpen.value = false
|
||
}
|
||
|
||
async function deleteSelectedRiskRule() {
|
||
if (!selectedSkill.value || !canDeleteRiskRule.value || detailBusy.value) {
|
||
return
|
||
}
|
||
actionState.value = 'delete-risk-rule'
|
||
try {
|
||
await deleteAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||
riskRuleDeleteOpen.value = false
|
||
const deletedName = selectedSkill.value.name
|
||
closeDetail()
|
||
await refreshCurrentAssets()
|
||
toast(`风险规则“${deletedName}”已删除。`)
|
||
} catch (error) {
|
||
toast(error?.message || '风险规则删除失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
function openReturnRiskRuleDialog() {
|
||
if (!canReturnRiskRule.value) {
|
||
return
|
||
}
|
||
riskRuleReturnNote.value = ''
|
||
riskRuleReturnOpen.value = true
|
||
}
|
||
|
||
function closeReturnRiskRuleDialog() {
|
||
if (detailBusy.value) {
|
||
return
|
||
}
|
||
riskRuleReturnOpen.value = false
|
||
}
|
||
|
||
async function returnSelectedRiskRule() {
|
||
if (!selectedSkill.value || !canReturnRiskRule.value || detailBusy.value) {
|
||
return
|
||
}
|
||
const note = normalizeText(riskRuleReturnNote.value)
|
||
if (!note) {
|
||
toast('请填写回退原因。')
|
||
return
|
||
}
|
||
actionState.value = 'return-risk-rule'
|
||
try {
|
||
await returnRiskRuleAsset(selectedSkill.value.id, { note }, { actor: resolveActor() })
|
||
riskRuleReturnOpen.value = false
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast('风险规则已回退到草稿。')
|
||
} catch (error) {
|
||
toast(error?.message || '风险规则回退失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
function openPublishRiskRuleDialog() {
|
||
if (!canPublishRiskRule.value) {
|
||
if (!riskRuleTestPassed.value) {
|
||
toast('请先确认测试报告通过,再发布上线。')
|
||
}
|
||
return
|
||
}
|
||
riskRulePublishOpen.value = true
|
||
}
|
||
|
||
function closePublishRiskRuleDialog() {
|
||
if (detailBusy.value) {
|
||
return
|
||
}
|
||
riskRulePublishOpen.value = false
|
||
}
|
||
|
||
async function publishSelectedRiskRule() {
|
||
if (!selectedSkill.value || !canPublishRiskRule.value || detailBusy.value) {
|
||
return
|
||
}
|
||
actionState.value = 'publish-risk-rule'
|
||
try {
|
||
await publishRiskRuleAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||
riskRulePublishOpen.value = false
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast('风险规则已发布上线。')
|
||
} catch (error) {
|
||
toast(error?.message || '风险规则发布失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function toggleSelectedRiskRuleEnabled() {
|
||
if (!selectedSkill.value || !canToggleRiskRuleEnabled.value || detailBusy.value) {
|
||
return
|
||
}
|
||
const assetId = selectedSkill.value.id
|
||
const nextEnabled = !selectedSkill.value.isOnlineValue
|
||
actionState.value = 'toggle-risk-rule-enabled'
|
||
try {
|
||
const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
|
||
mergeSelectedRuleLifecycle(detail)
|
||
await refreshCurrentAssets()
|
||
toast(nextEnabled ? '风险规则已上线。' : '风险规则已下线,不会进入业务扫描。')
|
||
} catch (error) {
|
||
toast(error?.message || '风险规则上线状态更新失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function activateSelectedRule() {
|
||
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
||
return
|
||
}
|
||
|
||
actionState.value = 'activate'
|
||
|
||
try {
|
||
await activateAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast('规则已正式上线。')
|
||
} catch (error) {
|
||
toast(error?.message || '规则上线失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function restoreSelectedVersion(version) {
|
||
if (
|
||
!selectedSkill.value ||
|
||
!selectedSkillIsRule.value ||
|
||
!canManageSelected.value ||
|
||
detailBusy.value ||
|
||
!version
|
||
) {
|
||
return
|
||
}
|
||
|
||
actionState.value = `restore-${version}`
|
||
try {
|
||
await restoreAgentAssetVersion(selectedSkill.value.id, version, { actor: resolveActor() })
|
||
await refreshCurrentAssets()
|
||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||
toast(`已基于 ${version} 生成新的工作版本。`)
|
||
} catch (error) {
|
||
toast(error?.message || '历史版本恢复失败,请稍后重试。')
|
||
} finally {
|
||
actionState.value = ''
|
||
}
|
||
}
|
||
|
||
async function loadVersionTimeline(assetId = selectedSkill.value?.id, options = {}) {
|
||
if (!assetId) {
|
||
return
|
||
}
|
||
|
||
versionTimelineLoading.value = true
|
||
versionTimelineError.value = ''
|
||
try {
|
||
const payload = await fetchAgentAssetVersionTimeline(assetId)
|
||
versionTimelineItems.value = Array.isArray(payload) ? payload : []
|
||
} catch (error) {
|
||
versionTimelineError.value = error?.message || '操作记录加载失败,请稍后重试。'
|
||
if (!options.silent) {
|
||
toast(versionTimelineError.value)
|
||
}
|
||
} finally {
|
||
versionTimelineLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function openVersionTimeline() {
|
||
if (!selectedSkill.value?.id) {
|
||
return
|
||
}
|
||
versionTimelineOpen.value = true
|
||
await loadVersionTimeline(selectedSkill.value.id)
|
||
}
|
||
|
||
function closeVersionTimeline() {
|
||
versionTimelineOpen.value = false
|
||
}
|
||
|
||
onMounted(() => {
|
||
document.addEventListener('click', handleDocumentClick)
|
||
loadAssets({ force: true }).catch(() => {})
|
||
loadRuns().catch(() => {})
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
destroySpreadsheetOnlyOfficeEditor()
|
||
riskRuleGenerationPollTimers.forEach((timer) => window.clearTimeout(timer))
|
||
riskRuleGenerationPollTimers.clear()
|
||
document.removeEventListener('click', handleDocumentClick)
|
||
})
|
||
|
||
return {
|
||
tabs,
|
||
activeType,
|
||
activeTabLabel,
|
||
selectedSkill,
|
||
versionSwitchTarget,
|
||
keyword,
|
||
createButtonLabel,
|
||
hintText,
|
||
searchPlaceholder,
|
||
tableColumns,
|
||
showRuntimeColumn,
|
||
showVersionColumn,
|
||
showMetricColumn,
|
||
showStatusColumn,
|
||
showOnlineColumn,
|
||
showEnabledColumn,
|
||
visibleSkills,
|
||
auditEmptyState,
|
||
loading,
|
||
errorMessage,
|
||
detailLoading,
|
||
detailError,
|
||
selectedDomain,
|
||
selectedOwner,
|
||
selectedStatus,
|
||
selectedRiskScenario,
|
||
selectedOnlineState,
|
||
selectedEnabledState,
|
||
selectedDomainLabel,
|
||
selectedOwnerLabel,
|
||
selectedStatusLabel,
|
||
selectedRiskScenarioLabel,
|
||
selectedOnlineStateLabel,
|
||
selectedEnabledStateLabel,
|
||
showRiskScenarioFilter,
|
||
showStatusFilter,
|
||
showOnlineFilter,
|
||
showEnabledFilter,
|
||
domainOptions,
|
||
ownerOptions,
|
||
statusOptions: STATUS_OPTIONS,
|
||
riskScenarioOptions: RISK_SCENARIO_OPTIONS,
|
||
onlineStateOptions: ONLINE_STATE_OPTIONS,
|
||
enabledStateOptions: ENABLED_STATE_OPTIONS,
|
||
activeFilterPopover,
|
||
activeFilterTokens,
|
||
canManageSelected,
|
||
canEditSelected,
|
||
canCreateRiskRule,
|
||
canOpenRiskRuleTest,
|
||
canDeleteRiskRule,
|
||
canOpenRiskRuleReviewSubmit,
|
||
canSubmitRiskRuleReview,
|
||
canReturnRiskRule,
|
||
canPublishRiskRule,
|
||
canToggleRiskRuleEnabled,
|
||
riskRuleTestPassed,
|
||
riskRuleInReview,
|
||
canSubmitReview,
|
||
hasReviewSubmitReviewers,
|
||
canReviewSelected,
|
||
canEditMarkdown,
|
||
canUploadSpreadsheet,
|
||
canDownloadSpreadsheet,
|
||
canEditSpreadsheetInline,
|
||
canActivateSelected,
|
||
activateBlockedReason,
|
||
selectedSkillIsRule,
|
||
selectedSkillUsesSpreadsheet,
|
||
selectedSkillUsesJsonRisk,
|
||
selectedSpreadsheetFileName,
|
||
selectedSpreadsheetModeLabel,
|
||
selectedVersionTimelineItems,
|
||
selectedSpreadsheetChangeRecords,
|
||
detailBusy,
|
||
actionState,
|
||
reviewSubmitOpen,
|
||
reviewSubmitVersion,
|
||
reviewSubmitReviewer,
|
||
reviewSubmitReviewerLoading,
|
||
reviewSubmitReviewerOptions,
|
||
riskRuleCreateOpen,
|
||
riskRuleCreateForm,
|
||
riskRuleCreateBusy,
|
||
riskRuleTestOpen,
|
||
riskRuleDeleteOpen,
|
||
riskRuleReturnOpen,
|
||
riskRulePublishOpen,
|
||
riskRuleReturnNote,
|
||
riskRuleBusinessStageOptions: RISK_RULE_BUSINESS_STAGE_OPTIONS,
|
||
riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
|
||
showReviewNote,
|
||
spreadsheetUploadInput,
|
||
spreadsheetOnlyOfficeLoading,
|
||
spreadsheetOnlyOfficeError,
|
||
spreadsheetOnlyOfficeReady,
|
||
spreadsheetOnlyOfficeHostId,
|
||
versionTimelineOpen,
|
||
versionTimelineLoading,
|
||
versionTimelineError,
|
||
spreadsheetChangeDetailOpen,
|
||
selectedSpreadsheetChangeRecord,
|
||
selectedSpreadsheetChangeSheetRows,
|
||
selectedSpreadsheetChangeCellRows,
|
||
openAssetDetail,
|
||
closeDetail,
|
||
resetFilters,
|
||
handleAuditEmptyAction,
|
||
toggleFilterPopover,
|
||
selectFilter,
|
||
closeFilterPopover,
|
||
openRiskRuleCreateDialog,
|
||
closeRiskRuleCreateDialog,
|
||
submitRiskRuleCreate,
|
||
openVersionSwitch,
|
||
cancelVersionSwitch,
|
||
confirmVersionSwitch,
|
||
saveRuleMarkdown,
|
||
saveRuleRuntimeJson,
|
||
saveRiskRuleJson,
|
||
formatRiskRuleJson,
|
||
downloadRiskRuleJson,
|
||
triggerSpreadsheetUpload,
|
||
downloadSpreadsheetFile,
|
||
handleSpreadsheetFileInput,
|
||
reviewSelectedRule,
|
||
openSubmitReviewDialog,
|
||
closeSubmitReviewDialog,
|
||
submitSelectedRuleForReview,
|
||
openRiskRuleTestDialog,
|
||
closeRiskRuleTestDialog,
|
||
handleRiskRuleReportSaved,
|
||
openDeleteRiskRuleDialog,
|
||
closeDeleteRiskRuleDialog,
|
||
deleteSelectedRiskRule,
|
||
openReturnRiskRuleDialog,
|
||
closeReturnRiskRuleDialog,
|
||
returnSelectedRiskRule,
|
||
openPublishRiskRuleDialog,
|
||
closePublishRiskRuleDialog,
|
||
publishSelectedRiskRule,
|
||
toggleSelectedRiskRuleEnabled,
|
||
activateSelectedRule,
|
||
restoreSelectedVersion,
|
||
openVersionTimeline,
|
||
closeVersionTimeline,
|
||
openSpreadsheetChangeDetail,
|
||
closeSpreadsheetChangeDetail,
|
||
loadAssets
|
||
}
|
||
}
|
||
}
|