import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue' import ConfirmDialog from '../../components/shared/ConfirmDialog.vue' import { fetchEmployees } from '../../services/employees.js' import TableEmptyState from '../../components/shared/TableEmptyState.vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { activateAgentAsset, createAgentAssetReview, createAgentAssetVersion, fetchAgentAssetDetail, fetchAgentAssets, fetchAgentAssetSpreadsheetBlob, fetchAgentAssetSpreadsheetChangeRecords, fetchAgentAssetSpreadsheetOnlyOfficeConfig, fetchAgentAssetRuleJson, fetchAgentAssetVersionTimeline, fetchAgentRuns, saveAgentAssetRuleJson, importAgentAssetSpreadsheetContent, restoreAgentAssetVersion, updateAgentAsset } from '../../services/agentAssets.js' import { loadOnlyOfficeApi } from '../../services/onlyoffice.js' import { isFinanceUser, isManagerUser } from '../../utils/accessControl.js' import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js' const RULE_TABLE_COLUMNS = { name: '规则名称', category: '业务域', owner: '负责人', scope: '适用场景', version: '修改次数', metric: '修改人' } const TYPE_META = { rules: { assetType: 'rule', label: '规则', typeLabel: '规则', tableColumns: RULE_TABLE_COLUMNS }, skills: { assetType: 'skill', label: '技能', typeLabel: '技能', createButtonLabel: '技能已接入', hintText: '技能页签已接到真实资产 API,可查看输入、输出、依赖和场景信息。', searchPlaceholder: '搜索技能名称、编码或负责人', showMetricColumn: false, tableColumns: { name: '技能名称', category: '业务域', owner: '负责人', scope: '适用场景', runtime: '输入摘要', version: '当前版本', metric: '' } }, mcp: { assetType: 'mcp', label: 'MCP', typeLabel: 'MCP', createButtonLabel: 'MCP 已接入', hintText: 'MCP 页签已接到真实资产 API,可查看服务地址、鉴权方式、超时和降级策略。', searchPlaceholder: '搜索 MCP 名称、编码或负责人', tableColumns: { name: 'MCP 服务', category: '业务域', owner: '维护人', scope: '适用场景', runtime: '调用地址', version: '当前版本', metric: '超时配置' } }, tasks: { assetType: 'task', label: '任务', typeLabel: '任务', createButtonLabel: '任务已接入', hintText: '任务页签已接到真实资产 API,可查看调度周期、执行 Agent 和最近执行结果。', searchPlaceholder: '搜索任务名称、编码或负责人', tableColumns: { name: '任务名称', category: '业务域', owner: '负责人', scope: '适用场景', runtime: '调度周期', version: '当前版本', metric: '执行 Agent' } } } const TAB_META = { financialRules: { assetType: 'rule', typeKey: 'rules', label: '财务规则', typeLabel: '财务规则', createButtonLabel: '财务规则已接入', hintText: '仅展示 tag 为“财务规则”的规则资产;未打新 tag 的旧规则已从规则中心隐藏。', searchPlaceholder: '搜索财务规则名称、编码或负责人', tableColumns: RULE_TABLE_COLUMNS, showRuntimeColumn: false, showStatusColumn: false, badgeTone: 'emerald' }, riskRules: { assetType: 'rule', typeKey: 'rules', label: '风险规则', typeLabel: '风险规则', createButtonLabel: '风险规则已接入', hintText: '仅展示平台风险规则;适用场景按差旅、发票、餐饮招待等分类,可用「使用场景」筛选。', searchPlaceholder: '搜索风险规则名称、编码或负责人', tableColumns: RULE_TABLE_COLUMNS, showRuntimeColumn: false, showVersionColumn: false, showStatusColumn: false, badgeTone: 'rose' }, skills: { ...TYPE_META.skills, typeKey: 'skills', badgeTone: 'blue' }, mcp: { ...TYPE_META.mcp, typeKey: 'mcp', badgeTone: 'amber' }, tasks: { ...TYPE_META.tasks, typeKey: 'tasks', badgeTone: 'violet' } } const STATUS_META = { draft: { label: '草稿中', tone: 'draft' }, review: { label: '待审核', tone: 'warning' }, active: { label: '已上线', tone: 'success' }, disabled: { label: '已停用', tone: 'disabled' } } const REVIEW_META = { approved: { label: '已通过', tone: 'success' }, pending: { label: '待审核', tone: 'warning' }, rejected: { label: '已驳回', tone: 'danger' } } const VERSION_STATE_META = { published: { label: '已上线', tone: 'success' }, draft: { label: '草稿', tone: 'draft' }, pending_review: { label: '待审核', tone: 'warning' }, approved: { label: '已通过待上线', tone: 'success' }, rejected: { label: '已驳回', tone: 'danger' }, history: { label: '历史版本', tone: 'disabled' } } const DOMAIN_LABELS = { expense: '报销', ar: '应收', ap: '应付', knowledge: '知识', system: '系统' } const SCENARIO_LABELS = { expense: '报销', risk_check: '风险检查', duplicate_expense: '重复报销', explain: '规则解释', invoice_anomaly: '票据异常', travel_policy: '差旅制度', travel_standard: '差旅标准', communication_expense: '通信费报销', expense_standard: '费用标准', accounts_payable: '应付', accounts_receivable: '应收', approval_required: '需审批', query: '查询', summary: '汇总', system: '系统', schedule: '调度', rule_center: '规则中心', review_digest: '待审摘要', aging_summary: '账龄汇总', invoice_validation: '发票验真' } const DETAIL_TITLES = { rules: { configTitle: '规则元信息', configDesc: '展示规则编码、版本、业务域和当前审核 / 上线状态。', detailTitle: '规则版本说明', detailDesc: '规则正文由 Markdown 驱动,保存后会生成新的版本快照。', outputTitle: '审核与上线', outputDesc: '规则上线受审核状态控制,未审核通过的版本会被后端拦截。', ruleListTitle: '上线要求', checkListTitle: '当前状态', triggerTitle: '适用场景', triggerDesc: '当前规则注册到的业务场景', toolTitle: '关联信息', toolDesc: '规则当前审核、保存和版本快照信息', historyTitle: '版本历史', historyDesc: '最近 5 个规则版本', publishTitle: '上线控制', publishDesc: '正式上线会调用后端激活接口,审核未通过时会被拦截。' }, skills: { configTitle: '技能配置', configDesc: '展示技能编码、输入摘要、版本和业务域。', detailTitle: '技能结构', detailDesc: '按输入、输出和依赖组织技能定义。', outputTitle: '输出契约', outputDesc: '技能详情重点展示输入参数、输出参数和依赖能力。', ruleListTitle: '输出要求', checkListTitle: '当前快照', triggerTitle: '适用场景', triggerDesc: '当前技能注册到的场景标签', toolTitle: '依赖能力', toolDesc: '技能当前依赖的数据库或其他能力', historyTitle: '版本历史', historyDesc: '最近版本记录', publishTitle: '发布状态', publishDesc: '技能当前状态由资产中心统一管理。' }, mcp: { configTitle: 'MCP 连接配置', configDesc: '展示服务地址、超时和调用方式。', detailTitle: '服务协议', detailDesc: '按服务类型、鉴权方式和降级策略组织外部服务信息。', outputTitle: '调用约束', outputDesc: 'MCP 详情重点展示鉴权方式、返回策略和最近调用状态。', ruleListTitle: '调用约束', checkListTitle: '最近状态', triggerTitle: '适用场景', triggerDesc: '当前 MCP 覆盖的业务场景', toolTitle: '运行信息', toolDesc: '结合 AgentRun 中的 ToolCall 还原最近一次调用状态', historyTitle: '版本历史', historyDesc: '最近版本记录', publishTitle: '服务状态', publishDesc: 'MCP 资产已接入规则中心,但真实外部调用仍以后续链路集成为准。' }, tasks: { configTitle: '任务配置', configDesc: '展示调度周期、执行 Agent 和任务编码。', detailTitle: '任务结构', detailDesc: '按调度计划、目标场景和运行结果组织任务信息。', outputTitle: '运行要求', outputDesc: '任务详情重点展示调度 Agent、最近运行结果和运行日志入口。', ruleListTitle: '运行要求', checkListTitle: '最近执行', triggerTitle: '适用场景', triggerDesc: '当前任务覆盖的业务场景', toolTitle: '最近调用', toolDesc: '根据 AgentRun 中的最近执行记录回显任务运行情况', historyTitle: '版本历史', historyDesc: '最近版本记录', publishTitle: '调度状态', publishDesc: '任务资产已接入规则中心,后续 Day 4 运行时会继续消费这些配置。' } } const STATUS_OPTIONS = [ { value: '', label: '全部状态' }, { value: 'draft', label: '草稿中' }, { value: 'review', label: '待审核' }, { value: 'active', label: '已上线' }, { value: 'disabled', label: '已停用' } ] const EXPENSE_RULE_BLOCK_PATTERN = /```expense-rule\s*([\s\S]*?)\s*```/i const RULE_SPREADSHEET_BLOCK_PATTERN = /```rule-spreadsheet\s*([\s\S]*?)\s*```/i const RULE_TEMPLATE_LABELS = { travel_standard_v1: '差旅标准模板', expense_amount_limit_v1: '金额上限模板', attachment_requirement_v1: '附件要求模板', general_policy_v1: '通用制度模板' } const RULE_TAB_TAG_ALIASES = { financialRules: new Set(['财务规则', '财务', 'financialrule', 'financialrules', 'financerule', 'financerules', 'financial', 'finance']), riskRules: new Set(['风险规则', '风险', '风控', 'riskrule', 'riskrules', 'risk']) } const RISK_SCENARIO_OPTIONS = [ { value: '', label: '全部场景' }, { value: '差旅', label: '差旅' }, { value: '发票', label: '发票' }, { value: '餐饮招待', label: '餐饮招待' }, { value: '交通出行', label: '交通出行' }, { value: '办公物料', label: '办公物料' }, { value: '费用科目', label: '费用科目' }, { value: '通用', label: '通用' } ] const LEGACY_RISK_SCENARIO_KEYS = new Set([ 'expense', 'risk_check', 'travel', 'meal', 'invoice', 'travel_policy', 'travel_standard', 'attachment_policy', 'scene_policy', 'invoice_anomaly' ]) const SPREADSHEET_DETAIL_MODE = 'spreadsheet' const JSON_RISK_DETAIL_MODE = 'json_risk' const PREVIEW_RULE_ID = 'preview-rule-expense-company-travel-expense' const PREVIEW_RULE_CODE = 'rule.expense.company_travel_expense_reimbursement' const PREVIEW_RULE_VERSION_SPECS = [ { version: 'v1.2.0', fileName: '公司差旅费报销规则.xlsx', updatedAt: '2026-05-17T09:30:00Z', updatedBy: '王楠', note: '补充城市分级与住宿限额示例。', source: 'preview', isCurrent: true }, { version: 'v1.1.0', fileName: '公司差旅费报销规则-v1.1.0.xlsx', updatedAt: '2026-05-14T15:20:00Z', updatedBy: '顾承宇', note: '新增票据要求与超标审批列。', source: 'preview', isCurrent: false }, { version: 'v1.0.0', fileName: '公司差旅费报销规则-v1.0.0.xlsx', updatedAt: '2026-05-10T11:10:00Z', updatedBy: '系统初始化', note: '首版差旅费报销规则表预览。', source: 'preview', isCurrent: false } ] function buildPreviewSpreadsheetMeta(spec) { return { file_name: spec.fileName, storage_key: `preview/agent-assets/${PREVIEW_RULE_ID}/${spec.version}/${encodeURIComponent(spec.fileName)}`, mime_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', size_bytes: 82416, checksum: `preview-${spec.version.replace(/\./g, '-')}`, updated_at: spec.updatedAt, updated_by: spec.updatedBy, source: spec.source } } function buildPreviewSpreadsheetVersionMarkdown(spec) { const metadata = buildPreviewSpreadsheetMeta(spec) return [ '# 公司差旅费报销规则', '', '## 规则载体', '', '- 页面状态:前端预览', `- 当前规则版本:\`${spec.version}\``, `- 表格文件:\`${spec.fileName}\``, `- 最近更新人:${spec.updatedBy}`, `- 最近更新时间:${spec.updatedAt}`, '', '## 说明', '', '- 当前环境暂无真实规则文件,先用于展示 Excel 规则详情页布局。', '- 真实上传、编辑与版本回写逻辑会在接好正式数据后启用。', '', '```rule-spreadsheet', JSON.stringify(metadata, null, 2), '```' ].join('\n') } function createPreviewRuleDetailPayload() { const recentVersions = PREVIEW_RULE_VERSION_SPECS.map((spec, index) => ({ id: `${PREVIEW_RULE_ID}-version-${index + 1}`, asset_id: PREVIEW_RULE_ID, version: spec.version, content: buildPreviewSpreadsheetVersionMarkdown(spec), content_type: 'markdown', change_note: spec.note, created_by: spec.updatedBy, created_at: spec.updatedAt, is_current: spec.isCurrent })) const currentSpec = PREVIEW_RULE_VERSION_SPECS[0] const currentMeta = buildPreviewSpreadsheetMeta(currentSpec) return { id: PREVIEW_RULE_ID, asset_type: 'rule', code: PREVIEW_RULE_CODE, name: '公司差旅费报销规则', description: '前端预览态:先展示 Excel 规则详情页布局、版本卡片和编辑入口位置。', domain: 'expense', scenario_json: ['expense', 'travel_policy', 'travel_standard'], owner: '财务制度管理组', reviewer: '顾承宇', status: 'active', current_version: currentSpec.version, published_version: currentSpec.version, working_version: currentSpec.version, config_json: { severity: 'medium', enabled: true, tag: '财务规则', detail_mode: 'spreadsheet', runtime_kind: 'travel_policy', rule_template_label: '差旅报销 Excel 模板', rule_document: { ...currentMeta, asset_version: currentSpec.version } }, created_at: '2026-05-10T11:10:00Z', updated_at: currentSpec.updatedAt, current_version_content: recentVersions[0].content, current_version_content_type: 'markdown', current_version_change_note: currentSpec.note, recent_versions: recentVersions, latest_review: { id: `${PREVIEW_RULE_ID}-review-1`, asset_id: PREVIEW_RULE_ID, version: currentSpec.version, reviewer: '顾承宇', review_status: 'approved', review_note: '当前为页面预览态,先确认布局与交互位置。', reviewed_at: '2026-05-17T10:00:00Z', created_at: '2026-05-17T10:00:00Z' } } } function buildPreviewRuleListItem() { const payload = createPreviewRuleDetailPayload() return { ...buildListItem(payload), isPreviewMock: true } } function buildPreviewRuleDetail() { const detail = buildDetailViewModel(createPreviewRuleDetailPayload(), []) return { ...detail, isPreviewMock: true } } function normalizeText(value) { return String(value || '').trim() } function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value) } function readConfigJson(value) { if (isPlainObject(value?.configJson)) { return value.configJson } if (isPlainObject(value?.config_json)) { return value.config_json } return {} } function readRuleDocumentMeta(value) { const configJson = readConfigJson(value) return isPlainObject(configJson.rule_document) ? configJson.rule_document : null } function isSpreadsheetRuleSource(value) { const configJson = readConfigJson(value) return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE } function isJsonRiskRuleSource(value) { const configJson = readConfigJson(value) return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE } function normalizeRuleTagValue(value) { return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '') } function collectRuleTagValues(source) { const configJson = readConfigJson(source) const rawValues = [ configJson.tag, configJson.rule_tag, ...(Array.isArray(configJson.tags) ? configJson.tags : []), ...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : []) ] return rawValues.map((item) => normalizeText(item)).filter(Boolean) } function resolveRuleTabId(source) { const code = normalizeText(source?.code || '').toLowerCase() if (code.startsWith('risk.')) { return 'riskRules' } if (isJsonRiskRuleSource(source)) { return 'riskRules' } const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item)) if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) { return 'riskRules' } if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) { return 'financialRules' } return '' } function resolveTabId(source, typeKey) { if (typeKey === 'rules') { return resolveRuleTabId(source) } return typeKey } function resolveTabMeta(tabId, typeKey) { if (TAB_META[tabId]) { return TAB_META[tabId] } if (typeKey === 'rules') { return { ...TYPE_META.rules, typeKey: 'rules', badgeTone: 'emerald' } } return TAB_META[typeKey] } function resolveRiskRuleDescription(payload) { if (!isPlainObject(payload)) { return '' } return normalizeText(payload.description) } function resolveRiskRuleSourceRef(payload) { if (!isPlainObject(payload)) { return '' } const metadata = isPlainObject(payload.metadata) ? payload.metadata : {} return normalizeText(metadata.source_ref) } function inferRiskCategoryFromCode(code) { const normalized = normalizeText(code).toLowerCase() if (normalized.startsWith('risk.travel.')) { return '差旅' } if (normalized.startsWith('risk.invoice.')) { return '发票' } if (normalized.includes('entertainment') || normalized.includes('meal_localized')) { return '餐饮招待' } if (normalized.includes('consecutive_transport')) { return '交通出行' } if (normalized.startsWith('risk.expense.')) { return '费用科目' } return '通用' } function resolveRiskRuleCategory(source) { const configJson = readConfigJson(source) const explicit = normalizeText(configJson.risk_category) if (explicit) { return explicit } const payloadCategory = normalizeText(source?.risk_category) if (payloadCategory) { return payloadCategory } const scenarioItems = Array.isArray(source?.scenario_json) ? source.scenario_json : Array.isArray(source?.scenarioList) ? source.scenarioList : [] const businessScenario = scenarioItems .map((item) => normalizeText(item)) .find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item)) if (businessScenario) { return businessScenario } return inferRiskCategoryFromCode(source?.code) } function buildRiskListSubtitle(text, maxLength = 42) { const normalized = normalizeText(text) if (!normalized) { return '平台内置风险规则' } const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized if (firstSentence.length <= maxLength) { return firstSentence } return `${firstSentence.slice(0, maxLength)}…` } function applyRiskRuleJsonState(target, payload, apiPayload) { const rulePayload = isPlainObject(payload) ? payload : {} const fullDescription = resolveRiskRuleDescription(rulePayload) || normalizeText(apiPayload?.description) || normalizeText(target.riskRuleDescription) const riskCategory = normalizeText(rulePayload.risk_category) || resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload }) return { ...target, riskRuleDescription: fullDescription, riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48), riskCategory, scope: riskCategory, riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload), riskRuleSummary: { name: apiPayload?.name || target.name, evaluator: apiPayload?.evaluator || rulePayload.evaluator || '', ontologySignal: apiPayload?.ontology_signal || rulePayload.ontology_signal || '', inputs: apiPayload?.inputs || rulePayload.inputs || {}, outcomes: apiPayload?.outcomes || rulePayload.outcomes || {} }, riskRuleJsonText: JSON.stringify(rulePayload, null, 2) } } function cloneJsonObject(value) { if (!isPlainObject(value)) { return null } try { return JSON.parse(JSON.stringify(value)) } catch { return { ...value } } } function resolveRuleTemplateLabel(value) { const templateKey = normalizeText(value) return RULE_TEMPLATE_LABELS[templateKey] || templateKey || '未指定模板' } function extractRuntimeRuleFromMarkdown(markdown) { const match = String(markdown || '').match(EXPENSE_RULE_BLOCK_PATTERN) if (!match) { return null } try { const payload = JSON.parse(match[1]) return isPlainObject(payload) ? payload : null } catch { return null } } function extractSpreadsheetMetaFromMarkdown(markdown) { const match = String(markdown || '').match(RULE_SPREADSHEET_BLOCK_PATTERN) if (!match) { return null } try { const payload = JSON.parse(match[1]) return isPlainObject(payload) ? payload : null } catch { return null } } function stripRuntimeRuleBlock(markdown) { const text = String(markdown || '') const stripped = text.replace(EXPENSE_RULE_BLOCK_PATTERN, '').replace(/\n{3,}/g, '\n\n').trim() return stripped } function stringifyRuntimeRule(runtimeRule) { return JSON.stringify(isPlainObject(runtimeRule) ? runtimeRule : {}, null, 2) } function parseRuntimeRuleText(runtimeRuleText) { const text = normalizeText(runtimeRuleText) if (!text) { return null } try { const payload = JSON.parse(text) return isPlainObject(payload) ? payload : null } catch { return null } } function buildDefaultRuntimeRule(source) { const configJson = readConfigJson(source) const scenarioItems = Array.isArray(source?.scenario_json) ? source.scenario_json : Array.isArray(source?.scenarioList) ? source.scenarioList : [] const configRuntimeRule = cloneJsonObject(configJson.runtime_rule) return { kind: normalizeText(configRuntimeRule?.kind || configJson.runtime_kind) || 'policy_rule_draft', version: typeof configRuntimeRule?.version === 'number' && Number.isFinite(configRuntimeRule.version) ? configRuntimeRule.version : 1, template_key: normalizeText(configRuntimeRule?.template_key || configJson.rule_template_key) || 'general_policy_v1', rule_name: normalizeText(configRuntimeRule?.rule_name || source?.name) || '未命名规则', scenario: normalizeText(configRuntimeRule?.scenario || scenarioItems[0]) || 'expense', review_required: typeof configRuntimeRule?.review_required === 'boolean' ? configRuntimeRule.review_required : true } } function resolveRuntimeRuleForVersion(source, rawMarkdown, runtimeRuleFallback = null) { return ( cloneJsonObject(extractRuntimeRuleFromMarkdown(rawMarkdown)) || cloneJsonObject(runtimeRuleFallback) || buildDefaultRuntimeRule(source) ) } function buildMarkdownVersionContent(markdownContent, runtimeRule) { const body = stripRuntimeRuleBlock(markdownContent) const runtimeBlock = ['```expense-rule', stringifyRuntimeRule(runtimeRule), '```'].join('\n') return body ? `${body}\n\n${runtimeBlock}` : runtimeBlock } function makeShort(value) { const text = normalizeText(value).replace(/\s+/g, '') if (!text) { return 'AG' } return text.slice(0, 2).toUpperCase() } function formatDateTime(value) { if (!value) { return '未记录' } const date = new Date(value) if (Number.isNaN(date.getTime())) { return String(value) } return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) .format(date) .replace(/\//g, '-') } function resolveDomainLabel(value) { return DOMAIN_LABELS[value] || normalizeText(value) || '未分类' } function resolveStatusMeta(value) { return STATUS_META[value] || { label: normalizeText(value) || '未知状态', tone: 'draft' } } function resolveReviewMeta(value) { return REVIEW_META[value] || { label: '暂无审核', tone: 'draft' } } function resolveTimelineEventMeta(value) { return { created: { label: '创建工作稿', icon: 'mdi mdi-file-document-edit-outline', tone: 'draft' }, submitted: { label: '提交审核', icon: 'mdi mdi-send-outline', tone: 'warning' }, approved: { label: '审核通过', icon: 'mdi mdi-check-decagram-outline', tone: 'success' }, rejected: { label: '审核驳回', icon: 'mdi mdi-close-octagon-outline', tone: 'danger' }, published: { label: '正式上线', icon: 'mdi mdi-rocket-launch-outline', tone: 'success' }, restored: { label: '恢复生成工作稿', icon: 'mdi mdi-history', tone: 'info' } }[normalizeText(value)] || { label: normalizeText(value) || '版本事件', icon: 'mdi mdi-circle-medium', tone: 'draft' } } function resolveDiffChangeMeta(value) { return { added: { label: '新增', tone: 'success' }, removed: { label: '删除', tone: 'danger' }, modified: { label: '修改', tone: 'warning' } }[normalizeText(value)] || { label: normalizeText(value) || '变化', tone: 'draft' } } function formatScenarioList(items) { if (!Array.isArray(items) || !items.length) { return '未配置场景' } return items .map((item) => SCENARIO_LABELS[item] || item) .filter(Boolean) .join(' / ') } function buildHistory(recentVersions = [], source) { const currentRuntimeRule = cloneJsonObject(readConfigJson(source).runtime_rule) return recentVersions.map((item) => { const rawContent = typeof item.content === 'string' ? item.content : '' return { version: item.version, note: item.change_note || '无版本说明', time: formatDateTime(item.created_at), content: rawContent, markdownContent: stripRuntimeRuleBlock(rawContent), runtimeRule: resolveRuntimeRuleForVersion( source, rawContent, item.is_current ? currentRuntimeRule : null ), spreadsheetMeta: extractSpreadsheetMetaFromMarkdown(rawContent), contentType: item.content_type, createdBy: item.created_by, isCurrent: Boolean(item.is_current), isPublished: Boolean(item.is_published), isWorking: Boolean(item.is_working), lifecycleState: item.lifecycle_state || 'history', lifecycleMeta: VERSION_STATE_META[item.lifecycle_state] || VERSION_STATE_META.history } }) } function resolveTypeKey(assetType) { if (assetType === 'rule') { return 'rules' } if (assetType === 'skill') { return 'skills' } if (assetType === 'mcp') { return 'mcp' } return 'tasks' } function formatSeverity(value) { const severity = normalizeText(value).toLowerCase() if (severity === 'high') { return '高风险' } if (severity === 'medium') { return '中风险' } if (severity === 'low') { return '低风险' } return '未配置' } function formatInputSummary(items) { if (!Array.isArray(items) || !items.length) { return '无输入' } return `${items.length} 项输入` } function formatOutputSummary(items) { if (!Array.isArray(items) || !items.length) { return '无输出' } return `${items.length} 项输出` } function formatTaskRisk(scenarios) { if (Array.isArray(scenarios) && scenarios.includes('risk_check')) { return '高风险' } if ( Array.isArray(scenarios) && (scenarios.includes('accounts_receivable') || scenarios.includes('accounts_payable')) ) { return '中风险' } return '常规' } function findLatestTaskRun(runs, assetId) { return runs.find((item) => item.task_id === assetId) || null } function findLatestMcpCall(runs, assetCode) { const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '') for (const run of runs) { for (const toolCall of run.tool_calls || []) { const toolName = normalizeText(toolCall.tool_name) if ( toolName === expectedToolName || toolName.endsWith(expectedToolName) || expectedToolName.endsWith(toolName) ) { return { run, toolCall } } } } return null } function buildRowRuntime(asset, typeKey) { if (typeKey === 'rules') { return formatSeverity(asset.config_json?.severity) } if (typeKey === 'skills') { return formatInputSummary(asset.config_json?.input_schema) } if (typeKey === 'mcp') { return normalizeText(asset.config_json?.endpoint) || '未配置地址' } return normalizeText(asset.config_json?.cron) || '未配置调度' } function buildRowMetric(asset, typeKey) { if (typeKey === 'rules') { return normalizeText(asset.modified_by) || '未记录' } if (typeKey === 'skills') { return '进入详情查看输出' } if (typeKey === 'mcp') { return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时' } return normalizeText(asset.config_json?.agent) || '未配置 Agent' } function formatSpreadsheetChangeSummary(summary) { const normalized = normalizeText(summary) return ( normalized .replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '') .replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '') .replace(/^保存表格[::]\s*/i, '') .trim() || '表格内容已保存。' ) } function buildListItem(asset) { const typeKey = resolveTypeKey(asset.asset_type) const tabId = resolveTabId(asset, typeKey) if (!tabId) { return null } const tabMeta = resolveTabMeta(tabId, typeKey) const statusMeta = resolveStatusMeta(asset.status) const workingVersion = asset.working_version || asset.current_version || '-' const changeCount = typeof asset.change_count === 'number' ? asset.change_count : Array.isArray(asset.recent_versions) ? Math.max(asset.recent_versions.length - 1, 0) : 0 const modifiedBy = normalizeText(asset.modified_by) || normalizeText( Array.isArray(asset.recent_versions) ? asset.recent_versions.find((item) => item.version === workingVersion)?.created_by : '' ) const isRiskRule = tabId === 'riskRules' const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(asset) const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(asset) const ruleDocument = readRuleDocumentMeta(asset) const riskCategory = isRiskRule ? resolveRiskRuleCategory(asset) : '' const listSubtitle = isRiskRule ? buildRiskListSubtitle(asset.description) : normalizeText(asset.description) return { id: asset.id, tabId, type: typeKey, isPreviewMock: Boolean(asset.isPreviewMock), usesSpreadsheetRule, usesJsonRiskRule, ruleDocument, typeLabel: tabMeta.typeLabel, short: makeShort(asset.name), name: asset.name, code: asset.code, summary: listSubtitle, listSubtitle, category: resolveDomainLabel(asset.domain), owner: asset.owner, reviewer: asset.reviewer || '待分配', scope: isRiskRule ? riskCategory || '通用' : formatScenarioList(asset.scenario_json), riskCategory, model: buildRowRuntime(asset, typeKey), version: workingVersion, versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion, publishedVersion: asset.published_version || '-', workingVersion, status: statusMeta.label, statusValue: asset.status, statusTone: statusMeta.tone, hitRate: buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey), modifiedBy, changeCount, updatedAt: formatDateTime(asset.updated_at), badgeTone: tabMeta.badgeTone, spotlight: asset.status === 'active', domainValue: asset.domain } } function buildRuleFields(detail) { const ruleDocument = readRuleDocumentMeta(detail) return [ { label: '规则编码', value: detail.code }, { label: '明细载体', value: isSpreadsheetRuleSource(detail) ? 'Excel 表格' : 'Markdown / JSON' }, ...(ruleDocument ? [ { label: '关联文件', value: normalizeText(ruleDocument.file_name) || '未上传' } ] : []), { label: '模板键', value: normalizeText(detail.config_json?.rule_template_key) || '未指定' }, { label: '业务域', value: resolveDomainLabel(detail.domain) }, { label: '运行时类型', value: normalizeText(detail.config_json?.runtime_kind) || 'policy_rule_draft' }, { label: '适用场景', value: formatScenarioList(detail.scenario_json) }, { label: '线上版本', value: detail.published_version || '-' }, { label: '工作版本', value: detail.working_version || detail.current_version || '-' } ] } function buildSkillFields(detail) { const content = detail.current_version_content || {} return [ { label: '技能编码', value: detail.code }, { label: '业务域', value: resolveDomainLabel(detail.domain) }, { label: '输入参数', value: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置' }, { label: '输出参数', value: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置' } ] } function buildMcpFields(detail, latestCall) { const content = detail.current_version_content || {} return [ { label: '服务编码', value: detail.code }, { label: '调用地址', value: normalizeText(detail.config_json?.endpoint) || '未配置' }, { label: '鉴权方式', value: normalizeText(content.auth_mode) || '未配置' }, { label: '最近调用', value: latestCall ? `${latestCall.toolCall.status} / ${formatDateTime(latestCall.run.started_at)}` : '暂无调用记录' } ] } function buildTaskFields(detail, latestRun) { const content = detail.current_version_content || {} return [ { label: '任务编码', value: detail.code }, { label: 'Cron', value: normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置' }, { label: '执行 Agent', value: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置' }, { label: '风险等级', value: formatTaskRisk(detail.scenario_json) }, { label: '最近执行', value: latestRun ? formatDateTime(latestRun.started_at) : '暂无执行记录' } ] } function buildFields(detail, typeKey, latestRun, latestCall) { if (typeKey === 'rules') { return buildRuleFields(detail) } if (typeKey === 'skills') { return buildSkillFields(detail) } if (typeKey === 'mcp') { return buildMcpFields(detail, latestCall) } return buildTaskFields(detail, latestRun) } function buildPromptSections(detail, typeKey, latestRun, latestCall) { const content = detail.current_version_content || {} if (typeKey === 'skills') { return [ { title: '输入参数', intent: '技能入口', content: Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('\n') : '未配置输入参数。' }, { title: '输出参数', intent: '技能产出', content: Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('\n') : '未配置输出参数。' }, { title: '依赖能力', intent: '外部依赖', content: Array.isArray(content.dependencies) && content.dependencies.length ? content.dependencies.join('\n') : '当前技能未声明外部依赖。' } ] } if (typeKey === 'mcp') { return [ { title: '服务类型', intent: '协议说明', content: normalizeText(content.service_type) || '未配置服务类型。' }, { title: '鉴权方式', intent: '安全要求', content: normalizeText(content.auth_mode) || '未配置鉴权方式。' }, { title: '降级策略', intent: '失败处理', content: normalizeText(content.degrade_strategy) || '未配置降级策略。' } ] } return [ { title: '任务场景', intent: '调度目标', content: formatScenarioList(detail.scenario_json) }, { title: '执行 Agent', intent: '运行主体', content: normalizeText(content.target_agent) || normalizeText(detail.config_json?.agent) || '未配置执行 Agent。' }, { title: '最近执行结果', intent: '运行反馈', content: latestRun?.result_summary || latestRun?.error_message || '暂无执行记录。' } ] } function buildOutputRules(detail, typeKey, latestRun, latestCall) { const content = detail.current_version_content || {} if (typeKey === 'rules') { if (isSpreadsheetRuleSource(detail)) { return [ '规则详情页以内联 Excel 表格作为主载体,管理员可直接编辑当前版本。', '上传新的 Excel 文件后,会自动生成新的规则版本快照。', '切换到历史版本时仅支持预览,不允许直接覆盖历史版本。', '规则表发生变更后,仍需完成审核才能再次正式上线。' ] } return [ '规则使用固定模板落 Markdown,并配套维护 runtime_rule JSON。', '保存 Markdown 或 JSON 都会生成新版本快照。', '未审核通过的规则版本不能正式上线。', '版本切换当前只影响前端展示内容,不会直接回滚后端版本。' ] } if (typeKey === 'skills') { return [ `输入参数:${Array.isArray(content.inputs) && content.inputs.length ? content.inputs.join('、') : '未配置'}`, `输出参数:${Array.isArray(content.outputs) && content.outputs.length ? content.outputs.join('、') : '未配置'}`, `依赖能力:${Array.isArray(content.dependencies) && content.dependencies.length ? content.dependencies.join('、') : '未声明'}` ] } if (typeKey === 'mcp') { return [ `服务地址:${normalizeText(detail.config_json?.endpoint) || '未配置'}`, `鉴权方式:${normalizeText(content.auth_mode) || '未配置'}`, `降级策略:${normalizeText(content.degrade_strategy) || '未配置'}` ] } return [ `调度周期:${normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置'}`, `执行 Agent:${normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置'}`, `风险等级:${formatTaskRisk(detail.scenario_json)}`, `最近执行结果:${latestRun?.status || '暂无执行记录'}` ] } function buildTests(detail, typeKey, latestRun, latestCall) { if (typeKey === 'rules') { const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status) return [ { name: '审核状态', input: detail.latest_review?.version || detail.current_version || '暂无版本', result: reviewMeta.label, tone: reviewMeta.tone }, { name: '上线状态', input: detail.current_version || '暂无版本', result: resolveStatusMeta(detail.status).label, tone: resolveStatusMeta(detail.status).tone } ] } if (typeKey === 'skills') { const content = detail.current_version_content || {} return [ { name: '输入数量', input: detail.current_version || '暂无版本', result: `${content.inputs?.length || 0} 项`, tone: 'success' }, { name: '输出数量', input: detail.current_version || '暂无版本', result: `${content.outputs?.length || 0} 项`, tone: 'success' } ] } if (typeKey === 'mcp') { return [ { name: '最近调用状态', input: latestCall?.run?.run_id || '暂无调用', result: latestCall?.toolCall?.status || '未记录', tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'success' }, { name: '最近调用耗时', input: latestCall?.toolCall?.tool_name || '暂无调用', result: typeof latestCall?.toolCall?.duration_ms === 'number' ? `${latestCall.toolCall.duration_ms} ms` : '未记录', tone: 'success' } ] } return [ { name: '最近运行状态', input: latestRun?.run_id || '暂无运行', result: latestRun?.status || '未记录', tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'success' }, { name: '结果摘要', input: latestRun?.agent || normalizeText(detail.config_json?.agent) || '未配置', result: latestRun?.result_summary || '暂无摘要', tone: 'success' } ] } function buildTools(detail, typeKey, latestRun, latestCall) { const content = detail.current_version_content || {} if (typeKey === 'skills') { return (content.dependencies || []).map((item) => ({ name: item, scope: '技能依赖', mode: '读取', tone: 'safe' })) } if (typeKey === 'mcp') { return [ { name: normalizeText(content.service_type) || '未配置服务类型', scope: '服务类型', mode: 'MCP', tone: 'active' }, { name: normalizeText(content.auth_mode) || '未配置鉴权方式', scope: '鉴权', mode: '安全', tone: 'safe' }, { name: latestCall?.run?.run_id || '暂无调用记录', scope: '最近 Run', mode: latestCall?.toolCall?.status || '未执行', tone: latestCall?.toolCall?.status === 'failed' ? 'danger' : 'active' } ] } return [ { name: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置 Agent', scope: '执行 Agent', mode: '调度', tone: 'active' }, { name: latestRun?.run_id || '暂无执行记录', scope: '最近 Run', mode: latestRun?.status || '未执行', tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'active' }, { name: latestRun?.permission_level || '未记录', scope: '权限级别', mode: 'Trace', tone: 'safe' } ] } function buildPublishDescription(detail, typeKey) { if (typeKey === 'rules') { if (detail.published_version && detail.working_version && detail.published_version !== detail.working_version) { return '当前存在尚未上线的工作版本,系统运行仍以线上版本为准。' } if (detail.status === 'active') { return '当前规则线上版本已生效,仍可继续保存新的工作版本并重新走审核。' } return '当前规则需要先完成审核,再调用上线接口正式激活。' } return DETAIL_TITLES[typeKey].publishDesc } function buildDetailViewModel(detail, runs) { const typeKey = resolveTypeKey(detail.asset_type) const tabId = resolveTabId(detail, typeKey) || typeKey const tabMeta = resolveTabMeta(tabId, typeKey) const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null const configJson = readConfigJson(detail) const statusMeta = resolveStatusMeta(detail.status) const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status) const history = buildHistory(detail.recent_versions || [], detail) const previewVersion = history.find((item) => item.isWorking) || history[0] || null const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(detail) const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(detail) const ruleDocument = readRuleDocumentMeta(detail) const previewRawMarkdown = detail.current_version_content_type === 'markdown' ? String(previewVersion?.content ?? detail.current_version_content ?? '') : '' const previewRuntimeRule = resolveRuntimeRuleForVersion( detail, previewRawMarkdown, previewVersion?.runtimeRule || configJson.runtime_rule ) const previewMarkdown = stripRuntimeRuleBlock(previewRawMarkdown) const titles = DETAIL_TITLES[typeKey] const previewChangeNote = previewVersion?.note || detail.current_version_change_note || '无版本说明' const ruleTemplateKey = normalizeText(configJson.rule_template_key || previewRuntimeRule.template_key) const ruleTemplateLabel = normalizeText(configJson.rule_template_label) || resolveRuleTemplateLabel(ruleTemplateKey) const runtimeKind = normalizeText(configJson.runtime_kind || previewRuntimeRule.kind) || 'policy_rule_draft' return { id: detail.id, tabId, type: typeKey, typeLabel: tabMeta.typeLabel, short: makeShort(detail.name), name: detail.name, code: detail.code, summary: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : detail.description, listSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : normalizeText(detail.description), owner: detail.owner, reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配', category: resolveDomainLabel(detail.domain), scope: usesJsonRiskRule ? resolveRiskRuleCategory(detail) || '通用' : formatScenarioList(detail.scenario_json), version: detail.working_version || detail.current_version || '-', currentVersion: detail.current_version || '-', publishedVersion: detail.published_version || '-', workingVersion: detail.working_version || detail.current_version || '-', displayVersion: previewVersion?.version || detail.working_version || detail.current_version || '-', status: statusMeta.label, statusValue: detail.status, statusTone: statusMeta.tone, hitRate: buildRowMetric(detail, typeKey), updatedAt: formatDateTime(detail.updated_at), badgeTone: tabMeta.badgeTone, configJson, usesSpreadsheetRule, usesJsonRiskRule, riskRuleJsonText: '{}', riskRuleSummary: null, riskRuleDescription: '', riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '', riskRuleSourceRef: '', riskCategory: usesJsonRiskRule ? resolveRiskRuleCategory(detail) : '', ruleDocument, scenarioList: Array.isArray(detail.scenario_json) ? [...detail.scenario_json] : [], markdownContent: previewMarkdown, runtimeRuleText: stringifyRuntimeRule(previewRuntimeRule), ruleTemplateKey, ruleTemplateLabel, runtimeKind, currentVersionContentType: detail.current_version_content_type, currentVersionChangeNote: detail.current_version_change_note || '无版本说明', displayVersionChangeNote: previewChangeNote, reviewStatusLabel: reviewMeta.label, reviewStatusTone: reviewMeta.tone, reviewStatusValue: detail.latest_review?.review_status || '', reviewTimeLabel: formatDateTime(detail.latest_review?.reviewed_at), reviewNote: detail.latest_review?.review_note || '', latestRun, latestCall, fields: buildFields(detail, typeKey, latestRun, latestCall), promptSections: typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey, latestRun, latestCall), outputRules: buildOutputRules(detail, typeKey, latestRun, latestCall), tests: buildTests(detail, typeKey, latestRun, latestCall), triggers: detail.scenario_json?.length ? detail.scenario_json.map((item) => SCENARIO_LABELS[item] || item) : ['未配置场景'], tools: typeKey === 'rules' ? [ { name: detail.latest_review?.reviewer || '待分配审核人', scope: '审核负责人', mode: reviewMeta.label, tone: reviewMeta.tone }, { name: detail.published_version || '暂无版本', scope: '线上版本', mode: '正式生效', tone: 'safe' }, { name: detail.working_version || detail.current_version || '暂无版本', scope: '工作版本', mode: detail.current_version_change_note || '无版本说明', tone: 'safe' } ] : buildTools(detail, typeKey, latestRun, latestCall), history, configTitle: titles.configTitle, configDesc: titles.configDesc, detailTitle: titles.detailTitle, detailDesc: titles.detailDesc, outputTitle: titles.outputTitle, outputDesc: titles.outputDesc, ruleListTitle: titles.ruleListTitle, checkListTitle: titles.checkListTitle, triggerTitle: titles.triggerTitle, triggerDesc: titles.triggerDesc, toolTitle: titles.toolTitle, toolDesc: titles.toolDesc, historyTitle: titles.historyTitle, historyDesc: titles.historyDesc, publishTitle: titles.publishTitle, publishDesc: buildPublishDescription(detail, typeKey), publishMeta: typeKey === 'rules' ? `最近保存:${formatDateTime(detail.updated_at)}` : latestRun ? `最近运行:${formatDateTime(latestRun.started_at)}` : `最近更新:${formatDateTime(detail.updated_at)}`, publishState: statusMeta.label, latestReviewVersion: detail.latest_review?.version || detail.current_version || '-', loading: false } } function incrementVersion(version) { const normalized = normalizeText(version).replace(/^v/i, '') const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)$/) if (!match) { return 'v1.0.0' } const major = Number(match[1]) const minor = Number(match[2]) const patch = Number(match[3]) + 1 return `v${major}.${minor}.${patch}` } function buildReviewNote(status) { if (status === 'approved') { return '通过任务规则中心审核。' } if (status === 'rejected') { return '在任务规则中心驳回当前版本。' } return '提交任务规则中心待审核。' } export default { name: 'AuditView', components: { ConfirmDialog, 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 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 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 assetBuckets = ref({ financialRules: [], riskRules: [], skills: [], mcp: [], tasks: [] }) const isAdmin = 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 selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules') const selectedSkillUsesSpreadsheet = computed( () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule) ) const selectedSkillUsesJsonRisk = computed( () => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule) ) const canManageSelected = computed( () => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock ) const canEditSelected = computed( () => Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock && (isAdmin.value || isFinance.value) ) const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value) const isDisplayingWorkingVersion = computed( () => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion ) const canSubmitReview = computed( () => 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: '全部负责人' }, ...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 || '负责人' ) const selectedStatusLabel = computed( () => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态' ) const showRiskScenarioFilter = computed(() => activeType.value === 'riskRules') const showStatusFilter = computed(() => activeType.value !== 'riskRules') const selectedRiskScenarioLabel = computed( () => RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label || '使用场景' ) const activeFilterTokens = computed(() => { const tokens = [] if (selectedDomain.value) { tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`) } if (showRiskScenarioFilter.value && selectedRiskScenario.value) { tokens.push(`使用场景:${selectedRiskScenario.value}`) } if (showStatusFilter.value && selectedStatus.value) { tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`) } if (selectedOwner.value) { tokens.push(`负责人:${selectedOwner.value}`) } if (keyword.value.trim()) { tokens.push(`搜索:${keyword.value.trim()}`) } return tokens }) const auditEmptyState = computed(() => { const hasFilters = activeFilterTokens.value.length > 0 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: activeType.value === 'riskRules' ? ['切换页签可查看其他资产类型', '支持按业务域、负责人和使用场景做过滤'] : ['切换页签可查看其他资产类型', '支持按业务域、负责人和状态做过滤'] } } return { eyebrow: '筛选结果为空', title: `没有找到匹配的${activeTabLabel.value}`, desc: hasFilters ? showRiskScenarioFilter.value ? '试试清空业务域、负责人、使用场景或关键词筛选,再重新查看。' : '试试清空业务域、负责人、状态或关键词筛选,再重新查看。' : `当前列表中还没有满足展示条件的${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 ? 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(() => { const normalizedKeyword = keyword.value.trim().toLowerCase() return currentAssets.value.filter((item) => { const matchesKeyword = normalizedKeyword ? [item.name, item.code, item.summary, item.owner, item.scope] .filter(Boolean) .some((value) => String(value).toLowerCase().includes(normalizedKeyword)) : true const matchesDomain = selectedDomain.value ? item.domainValue === selectedDomain.value : true const matchesOwner = selectedOwner.value ? item.owner === selectedOwner.value : true const matchesStatus = showStatusFilter.value ? selectedStatus.value ? item.statusValue === selectedStatus.value : true : true const matchesRiskScenario = showRiskScenarioFilter.value ? selectedRiskScenario.value ? item.riskCategory === selectedRiskScenario.value : true : true return matchesKeyword && matchesDomain && matchesOwner && matchesStatus && matchesRiskScenario }) }) 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 = '' 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 } 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 buildRuleConfigPayload(asset, runtimeRule) { const configJson = { ...readConfigJson(asset), runtime_kind: normalizeText(runtimeRule?.kind) || asset.runtimeKind || 'policy_rule_draft', runtime_rule: runtimeRule } const templateKey = normalizeText(runtimeRule?.template_key) || asset.ruleTemplateKey if (templateKey) { configJson.rule_template_key = templateKey configJson.rule_template_label = resolveRuleTemplateLabel(templateKey) } return configJson } 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) { const records = spreadsheetChangeRecordsByAsset.value[assetId] || [] const latest = records.find((item) => item?.changed_at) if (!latest) { return '' } const previewSignature = Array.isArray(latest.cell_changes) ? latest.cell_changes .slice(0, 8) .map((item) => [ item?.sheet_name, item?.cell, item?.change_type, item?.before_value, item?.after_value ] .map((value) => normalizeText(value)) .join(':') ) .join('|') : '' const sheetSignature = Array.isArray(latest.sheet_changes) ? latest.sheet_changes .map((item) => [item?.sheet_name, item?.change_type] .map((value) => normalizeText(value)) .join(':') ) .join('|') : '' return [ latest.id, latest.changed_at, latest.actor, latest.summary, latest.changed_sheet_count, latest.changed_cell_count, sheetSignature, previewSignature ] .map((value) => normalizeText(value)) .join('-') } 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 = {}) { loading.value = true 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 (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 { loading.value = false } } async function refreshCurrentAssets() { await loadAssets({ force: true, silent: 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) { 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 } } 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) { 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 = [] } 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 (!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 } 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 = '' } } 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() document.removeEventListener('click', handleDocumentClick) }) return { tabs, activeType, activeTabLabel, selectedSkill, versionSwitchTarget, keyword, createButtonLabel, hintText, searchPlaceholder, tableColumns, showRuntimeColumn, showVersionColumn, showMetricColumn, showStatusColumn, visibleSkills, auditEmptyState, loading, errorMessage, detailLoading, detailError, selectedDomain, selectedOwner, selectedStatus, selectedRiskScenario, selectedDomainLabel, selectedOwnerLabel, selectedStatusLabel, selectedRiskScenarioLabel, showRiskScenarioFilter, showStatusFilter, domainOptions, ownerOptions, statusOptions: STATUS_OPTIONS, riskScenarioOptions: RISK_SCENARIO_OPTIONS, activeFilterPopover, activeFilterTokens, canManageSelected, canEditSelected, canSubmitReview, hasReviewSubmitReviewers, canReviewSelected, canEditMarkdown, canUploadSpreadsheet, canDownloadSpreadsheet, canEditSpreadsheetInline, canActivateSelected, activateBlockedReason, selectedSkillIsRule, selectedSkillUsesSpreadsheet, selectedSkillUsesJsonRisk, selectedSpreadsheetFileName, selectedSpreadsheetModeLabel, selectedVersionTimelineItems, selectedSpreadsheetChangeRecords, detailBusy, actionState, reviewSubmitOpen, reviewSubmitVersion, reviewSubmitReviewer, reviewSubmitReviewerLoading, reviewSubmitReviewerOptions, showReviewNote, spreadsheetUploadInput, spreadsheetOnlyOfficeLoading, spreadsheetOnlyOfficeError, spreadsheetOnlyOfficeReady, spreadsheetOnlyOfficeHostId, versionTimelineOpen, versionTimelineLoading, versionTimelineError, spreadsheetChangeDetailOpen, selectedSpreadsheetChangeRecord, selectedSpreadsheetChangeSheetRows, selectedSpreadsheetChangeCellRows, openAssetDetail, closeDetail, resetFilters, handleAuditEmptyAction, toggleFilterPopover, selectFilter, closeFilterPopover, openVersionSwitch, cancelVersionSwitch, confirmVersionSwitch, saveRuleMarkdown, saveRuleRuntimeJson, saveRiskRuleJson, formatRiskRuleJson, downloadRiskRuleJson, triggerSpreadsheetUpload, downloadSpreadsheetFile, handleSpreadsheetFileInput, reviewSelectedRule, openSubmitReviewDialog, closeSubmitReviewDialog, submitSelectedRuleForReview, activateSelectedRule, restoreSelectedVersion, openVersionTimeline, closeVersionTimeline, openSpreadsheetChangeDetail, closeSpreadsheetChangeDetail, loadAssets } } }