feat: 展示真实运维审计日志

This commit is contained in:
湛兮
2026-06-11 23:06:49 +08:00
parent c89118ee00
commit 88836590cd
5 changed files with 155 additions and 1 deletions
+41
View File
@@ -4,6 +4,7 @@
import type {
AgentCard,
AgentInvocation,
AuditLogEntry,
CreateAgentInvocationPayload,
CreateDeployRunPayload,
DashboardSummary,
@@ -267,6 +268,12 @@ export const devopsApi = {
return toJenkinsSyncSummary(data, projectList)
},
async getAuditLogs(): Promise<AuditLogEntry[]> {
const { data } = await http.get<AuditLogEntry[]>('/audit-logs')
lastDataSource = 'backend'
return Array.isArray(data) ? data.map(sanitizeAuditLogEntry) : []
},
async getAgentCards(): Promise<AgentCard[]> {
return agentCards
},
@@ -416,6 +423,22 @@ function sanitizeIntegrationNote(note?: string): string | undefined {
.replace(/key=[^&\s]+/g, 'key=[已隐藏]')
}
function safeText(value: unknown, fallback: string): string {
return typeof value === 'string' && value.trim() ? value : fallback
}
function optionalSafeText(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value : undefined
}
function safeRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined
}
return value as Record<string, unknown>
}
function toProjectConfigs(backendProjects: BackendProject[]): ProjectConfig[] {
return backendProjects.map((project) => {
const repo = parseRepository(project.repositoryUrl, project.key)
@@ -498,6 +521,24 @@ function toJenkinsSyncSummary(
}
}
function sanitizeAuditLogEntry(record: AuditLogEntry): AuditLogEntry {
return {
id: safeText(record.id, 'audit-unknown'),
action: safeText(record.action, 'UNKNOWN_ACTION'),
resourceType: safeText(record.resourceType, 'unknown'),
resourceId: optionalSafeText(record.resourceId),
actorId: optionalSafeText(record.actorId),
actorName: optionalSafeText(record.actorName),
requestId: optionalSafeText(record.requestId),
sourceIp: optionalSafeText(record.sourceIp),
userAgent: optionalSafeText(record.userAgent),
before: safeRecord(record.before),
after: safeRecord(record.after),
parameterDigest: safeRecord(record.parameterDigest),
createdAt: safeText(record.createdAt, ''),
}
}
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
const name = step.name || bpmnNodeId
+16 -1
View File
@@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import { devopsApi } from '../services/devopsApi'
import type {
AgentCard,
AuditLogEntry,
CreateDeployRunPayload,
IntegrationStatus,
IntegrationConfigStatus,
@@ -24,6 +25,7 @@ export const usePlatformStore = defineStore('platform', () => {
const integrations = ref<IntegrationStatus[]>([])
const agentCards = ref<AgentCard[]>([])
const settings = ref<SystemSetting[]>([])
const auditLogs = ref<AuditLogEntry[]>([])
const integrationConfig = ref<IntegrationConfigStatus>({
checkedAt: '',
integrations: [],
@@ -46,7 +48,12 @@ export const usePlatformStore = defineStore('platform', () => {
agentCards.value = summary.agentCards
settings.value = summary.settings
integrationConfig.value = summary.integrationConfig
releaseProcessXml.value = await devopsApi.getReleaseProcessXml()
const [processXml, recentAuditLogs] = await Promise.all([
devopsApi.getReleaseProcessXml(),
devopsApi.getAuditLogs(),
])
releaseProcessXml.value = processXml
auditLogs.value = recentAuditLogs
dataSource.value = devopsApi.getLastDataSource()
} finally {
loading.value = false
@@ -98,6 +105,12 @@ export const usePlatformStore = defineStore('platform', () => {
return refs
}
async function loadAuditLogs() {
auditLogs.value = await devopsApi.getAuditLogs()
dataSource.value = devopsApi.getLastDataSource()
return auditLogs.value
}
function upsertRun(run: ReleaseRun) {
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
}
@@ -110,6 +123,7 @@ export const usePlatformStore = defineStore('platform', () => {
integrations,
agentCards,
settings,
auditLogs,
integrationConfig,
releaseProcessXml,
projectRefs,
@@ -122,5 +136,6 @@ export const usePlatformStore = defineStore('platform', () => {
syncJenkinsRun,
syncJenkinsRuns,
loadProjectRefs,
loadAuditLogs,
}
})
+16
View File
@@ -165,6 +165,22 @@ export interface JenkinsSyncSummary {
failedRuns: JenkinsSyncError[]
}
export interface AuditLogEntry {
id: string
action: string
resourceType: string
resourceId?: string
actorId?: string
actorName?: string
requestId?: string
sourceIp?: string
userAgent?: string
before?: Record<string, unknown>
after?: Record<string, unknown>
parameterDigest?: Record<string, unknown>
createdAt: string
}
export interface AgentCard {
mode: AgentMode
title: string
+72
View File
@@ -1,6 +1,8 @@
<script setup lang="ts">
import { Refresh } from '@element-plus/icons-vue'
import StatusPill from '../components/StatusPill.vue'
import { usePlatformStore } from '../stores/platform'
import type { AuditLogEntry } from '../types/devops'
import type { IntegrationConfigState } from '../types/devops'
const platform = usePlatformStore()
@@ -43,6 +45,26 @@ function formatCheckedAt(value: string): string {
minute: '2-digit',
}).format(date)
}
function auditActor(record: AuditLogEntry): string {
return record.actorName || record.actorId || '-'
}
function auditResource(record: AuditLogEntry): string {
return record.resourceId
? `${record.resourceType} / ${record.resourceId}`
: record.resourceType
}
function auditDigest(record: AuditLogEntry): string {
const payload = record.after ?? record.parameterDigest ?? record.before
if (!payload) {
return '-'
}
return JSON.stringify(payload)
}
</script>
<template>
@@ -158,6 +180,45 @@ function formatCheckedAt(value: string): string {
</section>
</div>
<section class="panel">
<div class="panel-header">
<h2>最近运维审计</h2>
<ElButton :icon="Refresh" size="small" @click="platform.loadAuditLogs()">
刷新
</ElButton>
</div>
<div class="panel-body">
<ElTable :data="platform.auditLogs" border>
<ElTableColumn label="时间" min-width="150">
<template #default="{ row }">
{{ formatCheckedAt(row.createdAt) }}
</template>
</ElTableColumn>
<ElTableColumn prop="action" label="动作" min-width="220" />
<ElTableColumn label="资源" min-width="220">
<template #default="{ row }">
<span class="mono">{{ auditResource(row) }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="操作者" min-width="130">
<template #default="{ row }">
{{ auditActor(row) }}
</template>
</ElTableColumn>
<ElTableColumn label="脱敏摘要" min-width="280">
<template #default="{ row }">
<span class="audit-digest">{{ auditDigest(row) }}</span>
</template>
</ElTableColumn>
</ElTable>
<ElEmpty
v-if="platform.auditLogs.length === 0"
description="暂无审计记录"
:image-size="72"
/>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h2>前端环境变量</h2>
@@ -279,6 +340,17 @@ function formatCheckedAt(value: string): string {
line-height: 24px;
}
.audit-digest {
display: block;
max-width: 100%;
overflow: hidden;
color: #4b5565;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1080px) {
.variable-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));