import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import ConfirmDialog from '../../components/shared/ConfirmDialog.vue' import { useSystemState } from '../../composables/useSystemState.js' import { useToast } from '../../composables/useToast.js' import { activateAgentAsset, createAgentAssetReview, createAgentAssetVersion, fetchAgentAssetDetail, fetchAgentAssets, fetchAgentRuns } from '../../services/agentAssets.js' import { isManagerUser } from '../../utils/accessControl.js' const TYPE_META = { rules: { assetType: 'rule', label: '规则', typeLabel: '规则', createButtonLabel: '规则已接入', hintText: '规则列表已接到真实资产 API,可查看 Markdown、版本、审核状态和上线约束。', searchPlaceholder: '搜索规则名称、编码或负责人', tableColumns: { name: '规则名称', category: '业务域', owner: '负责人', scope: '适用场景', runtime: '风险等级', version: '当前版本', metric: '审核状态' } }, 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 BADGE_TONES = { rules: 'emerald', skills: 'blue', mcp: 'amber', tasks: '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 DOMAIN_LABELS = { expense: '报销', ar: '应收', ap: '应付', knowledge: '知识', system: '系统' } const SCENARIO_LABELS = { expense: '报销', risk_check: '风险检查', duplicate_expense: '重复报销', explain: '规则解释', invoice_anomaly: '票据异常', 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: '已停用' } ] function normalizeText(value) { return String(value || '').trim() } 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 formatScenarioList(items) { if (!Array.isArray(items) || !items.length) { return '未配置场景' } return items .map((item) => SCENARIO_LABELS[item] || item) .filter(Boolean) .join(' / ') } function buildHistory(recentVersions = []) { return recentVersions.map((item) => ({ version: item.version, note: item.change_note || '无版本说明', time: formatDateTime(item.created_at), content: item.content, contentType: item.content_type, createdBy: item.created_by, isCurrent: Boolean(item.is_current) })) } 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 asset.reviewer ? `审核人:${asset.reviewer}` : '待分配审核人' } 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 buildListItem(asset) { const typeKey = resolveTypeKey(asset.asset_type) const statusMeta = resolveStatusMeta(asset.status) return { id: asset.id, type: typeKey, typeLabel: TYPE_META[typeKey].typeLabel, short: makeShort(asset.name), name: asset.name, code: asset.code, summary: asset.description, category: resolveDomainLabel(asset.domain), owner: asset.owner, reviewer: asset.reviewer || '待分配', scope: formatScenarioList(asset.scenario_json), model: buildRowRuntime(asset, typeKey), version: asset.current_version || '-', status: statusMeta.label, statusValue: asset.status, statusTone: statusMeta.tone, hitRate: buildRowMetric(asset, typeKey), updatedAt: formatDateTime(asset.updated_at), badgeTone: BADGE_TONES[typeKey], spotlight: asset.status === 'active', domainValue: asset.domain } } function buildRuleFields(detail) { return [ { label: '规则编码', value: detail.code }, { label: '业务域', value: resolveDomainLabel(detail.domain) }, { label: '适用场景', value: formatScenarioList(detail.scenario_json) }, { label: '当前版本', value: 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') { return [ '规则 Markdown 保存后会生成新版本。', '未审核通过的规则版本不能正式上线。', '版本切换当前只影响前端展示内容,不会直接回滚后端版本。' ] } 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.status === 'active') { return '当前规则版本已经上线,仍可继续保存新版本并重新走审核。' } return '当前规则需要先完成审核,再调用上线接口正式激活。' } return DETAIL_TITLES[typeKey].publishDesc } function buildDetailViewModel(detail, runs) { const typeKey = resolveTypeKey(detail.asset_type) const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null const statusMeta = resolveStatusMeta(detail.status) const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status) const history = buildHistory(detail.recent_versions || []) const previewVersion = history.find((item) => item.isCurrent) || history[0] || null const previewMarkdown = detail.current_version_content_type === 'markdown' ? String(previewVersion?.content ?? detail.current_version_content ?? '') : '' const titles = DETAIL_TITLES[typeKey] return { id: detail.id, type: typeKey, typeLabel: TYPE_META[typeKey].typeLabel, short: makeShort(detail.name), name: detail.name, code: detail.code, summary: detail.description, owner: detail.owner, reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配', category: resolveDomainLabel(detail.domain), scope: formatScenarioList(detail.scenario_json), version: detail.current_version || '-', currentVersion: detail.current_version || '-', displayVersion: previewVersion?.version || detail.current_version || '-', status: statusMeta.label, statusValue: detail.status, statusTone: statusMeta.tone, hitRate: buildRowMetric(detail, typeKey), updatedAt: formatDateTime(detail.updated_at), badgeTone: BADGE_TONES[typeKey], markdownContent: previewMarkdown, currentVersionContentType: detail.current_version_content_type, currentVersionChangeNote: detail.current_version_change_note || '无版本说明', 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.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 }, emits: ['detail-open-change'], setup(_, { emit }) { const { toast } = useToast() const { currentUser } = useSystemState() const tabs = Object.entries(TYPE_META).map(([id, meta]) => ({ id, label: meta.label })) const activeType = ref('rules') const selectedSkill = ref(null) const versionSwitchTarget = ref(null) const keyword = ref('') const activeFilterPopover = ref('') const selectedDomain = ref('') const selectedOwner = ref('') const selectedStatus = ref('') const loading = ref(false) const errorMessage = ref('') const detailLoading = ref(false) const detailError = ref('') const actionState = ref('') const runLoading = ref(false) const runs = ref([]) const assetBuckets = ref({ rules: [], skills: [], mcp: [], tasks: [] }) const isAdmin = computed(() => isManagerUser(currentUser.value)) const activeMeta = computed(() => TYPE_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 showMetricColumn = computed(() => activeMeta.value.showMetricColumn !== false) const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules') const canManageSelected = computed(() => isAdmin.value && Boolean(selectedSkill.value)) const canEditMarkdown = computed(() => canManageSelected.value && selectedSkillIsRule.value) 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 activeFilterTokens = computed(() => { const tokens = [] if (selectedDomain.value) { tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`) } if (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 canActivateSelected = computed(() => { if (!selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) { return false } return selectedSkill.value?.reviewStatusValue === 'approved' && selectedSkill.value?.statusValue !== 'active' }) const activateBlockedReason = computed(() => { if (!selectedSkillIsRule.value) { return '' } if (!canManageSelected.value) { return '仅管理员可执行审核和上线。' } if (selectedSkill.value?.statusValue === 'active') { 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 = selectedStatus.value ? item.statusValue === selectedStatus.value : true return matchesKeyword && matchesDomain && matchesOwner && matchesStatus }) }) watch( selectedSkill, (value) => { emit('detail-open-change', Boolean(value)) }, { immediate: true } ) watch(activeType, () => { 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 = '' activeFilterPopover.value = '' } 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 } 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' } 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 }) assetBuckets.value = { ...assetBuckets.value, [activeType.value]: Array.isArray(payload) ? payload.map(buildListItem) : [] } } catch (error) { 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) } catch (error) { detailError.value = error?.message || '资产详情加载失败,请稍后重试。' toast(detailError.value) } finally { detailLoading.value = false } } function openAssetDetail(asset) { selectedSkill.value = { ...asset, fields: [], promptSections: [], outputRules: [], tests: [], triggers: [], tools: [], history: [], markdownContent: '', displayVersion: asset.version, loading: true, reviewStatusLabel: '加载中', reviewStatusTone: 'draft' } versionSwitchTarget.value = null loadSelectedAssetDetail(asset.id).catch(() => {}) } function closeDetail() { selectedSkill.value = null detailError.value = '' detailLoading.value = false versionSwitchTarget.value = null } 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 if (typeof versionSwitchTarget.value.content === 'string') { selectedSkill.value.markdownContent = versionSwitchTarget.value.content } versionSwitchTarget.value = null } async function saveRuleMarkdown() { if (!selectedSkill.value || !selectedSkillIsRule.value || !canEditMarkdown.value || detailBusy.value) { return } if (!normalizeText(selectedSkill.value.markdownContent)) { toast('规则 Markdown 内容不能为空。') return } const nextVersion = incrementVersion(selectedSkill.value.currentVersion) actionState.value = 'save-markdown' try { await createAgentAssetVersion( selectedSkill.value.id, { version: nextVersion, content: selectedSkill.value.markdownContent, content_type: 'markdown', change_note: '通过任务规则中心保存 Markdown 规则内容。', created_by: resolveActor() }, { actor: resolveActor() } ) await refreshCurrentAssets() await loadSelectedAssetDetail(selectedSkill.value.id) toast(`规则 Markdown 已保存为 ${nextVersion}。`) } catch (error) { toast(error?.message || '规则 Markdown 保存失败,请稍后重试。') } finally { actionState.value = '' } } async function reviewSelectedRule(reviewStatus) { if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) { return } actionState.value = `review-${reviewStatus}` try { await createAgentAssetReview( selectedSkill.value.id, { version: selectedSkill.value.displayVersion || selectedSkill.value.currentVersion, 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 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 = '' } } onMounted(() => { document.addEventListener('click', handleDocumentClick) loadAssets({ force: true }).catch(() => {}) loadRuns().catch(() => {}) }) onBeforeUnmount(() => { document.removeEventListener('click', handleDocumentClick) }) return { tabs, activeType, activeTabLabel, selectedSkill, versionSwitchTarget, keyword, createButtonLabel, hintText, searchPlaceholder, tableColumns, showMetricColumn, visibleSkills, loading, errorMessage, detailLoading, detailError, selectedDomain, selectedOwner, selectedStatus, selectedDomainLabel, selectedOwnerLabel, selectedStatusLabel, domainOptions, ownerOptions, statusOptions: STATUS_OPTIONS, activeFilterPopover, activeFilterTokens, canManageSelected, canEditMarkdown, canActivateSelected, activateBlockedReason, selectedSkillIsRule, detailBusy, actionState, showReviewNote, openAssetDetail, closeDetail, resetFilters, toggleFilterPopover, selectFilter, closeFilterPopover, openVersionSwitch, cancelVersionSwitch, confirmVersionSwitch, saveRuleMarkdown, reviewSelectedRule, activateSelectedRule, loadAssets } } }