Files
X-Financial/web/src/views/scripts/AuditView.js

1349 lines
42 KiB
JavaScript
Raw Normal View History

import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
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,
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,
TableEmptyState
},
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 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: 'mdi mdi-refresh',
tone: 'amber',
artLabel: 'ASSET',
tips: ['切换页签可查看其他资产类型', '支持按业务域、负责人和状态做过滤']
}
}
return {
eyebrow: '筛选结果为空',
title: `没有找到匹配的${activeTabLabel.value}`,
desc: hasFilters
? '试试清空业务域、负责人、状态或关键词筛选,再重新查看。'
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
actionLabel: hasFilters ? '清空筛选' : '重新加载',
actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : 'mdi mdi-refresh',
tone: hasFilters ? 'emerald' : 'slate',
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
tips: hasFilters
? ['业务域、负责人、状态与关键词会叠加过滤', '可以换个编码、名称或负责人关键词继续搜索']
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
}
})
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 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
}
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,
auditEmptyState,
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,
handleAuditEmptyAction,
toggleFilterPopover,
selectFilter,
closeFilterPopover,
openVersionSwitch,
cancelVersionSwitch,
confirmVersionSwitch,
saveRuleMarkdown,
reviewSelectedRule,
activateSelectedRule,
loadAssets
}
}
}