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 } } }