Files
X-Financial/web/src/views/scripts/AuditView.js
caoxiaozhu 151787ada2 refactor(web): update view scripts
- AuditView.js: update audit view logic
- EmployeeManagementView.js: update employee management logic
- RequestsView.js: update requests view logic
- TravelRequestDetailView.js: update travel detail view logic
2026-05-13 06:52:30 +00:00

1349 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}
}