feat: 展示通知 outbox 状态

- services: 增加 /notification-outbox 真实接口读取与摘要清洗

- store: 超级管理员加载最近通知 outbox

- settings: 增加通知 outbox 运维表格
This commit is contained in:
湛兮
2026-06-12 03:22:38 +08:00
parent 4988c5be38
commit cc72a24c7d
4 changed files with 170 additions and 1 deletions
+41
View File
@@ -25,6 +25,7 @@ import type {
LoginPayload,
LoginResult,
MemberSummary,
NotificationOutboxMessage,
PipelineStep,
PlatformMessageSummary,
ProjectConfig,
@@ -417,6 +418,12 @@ export const devopsApi = {
return Array.isArray(data) ? data.map(sanitizeAuditLogEntry) : []
},
async getNotificationOutbox(): Promise<NotificationOutboxMessage[]> {
const { data } = await http.get<NotificationOutboxMessage[]>('/notification-outbox')
lastDataSource = 'backend'
return Array.isArray(data) ? data.map(sanitizeNotificationOutboxMessage) : []
},
async getAgentCards(): Promise<AgentCard[]> {
return agentCards
},
@@ -848,6 +855,25 @@ function sanitizeAuditLogEntry(record: AuditLogEntry): AuditLogEntry {
}
}
function sanitizeNotificationOutboxMessage(
record: NotificationOutboxMessage,
): NotificationOutboxMessage {
return {
id: safeText(record.id, 'outbox-unknown'),
deployRunId: optionalSafeText(record.deployRunId),
channel: safeText(record.channel, 'none'),
template: safeText(record.template, 'unknown-template'),
payloadSummary: safeRecord(record.payloadSummary),
status: toNotificationOutboxStatus(record.status),
attemptCount: Number.isFinite(record.attemptCount) ? record.attemptCount : 0,
nextAttemptAt: optionalSafeText(record.nextAttemptAt),
lastError: optionalSafeText(record.lastError),
idempotencyKey: safeText(record.idempotencyKey, 'unknown-idempotency'),
createdAt: safeText(record.createdAt, ''),
updatedAt: safeText(record.updatedAt, ''),
}
}
function toPipelineStep(step: BackendDeployRunStep): PipelineStep {
const bpmnNodeId = step.bpmnNodeId || step.id || 'unknown-step'
const name = step.name || bpmnNodeId
@@ -916,6 +942,21 @@ function toStepStatus(status?: BackendRunStatus): StepStatus {
return 'pending'
}
function toNotificationOutboxStatus(
status?: NotificationOutboxMessage['status'],
): NotificationOutboxMessage['status'] {
if (
status === 'pending' ||
status === 'sent' ||
status === 'failed' ||
status === 'dead'
) {
return status
}
return 'failed'
}
function stepOwner(nodeId: string): PipelineStep['owner'] {
if (nodeId.includes('gitea')) {
return 'Gitea'
+19 -1
View File
@@ -14,6 +14,7 @@ import type {
ProjectConfig,
ProjectRefs,
JenkinsSyncSummary,
NotificationOutboxMessage,
ReleaseRun,
SystemSetting,
} from '../types/devops'
@@ -30,6 +31,7 @@ export const usePlatformStore = defineStore('platform', () => {
const agentCards = ref<AgentCard[]>([])
const settings = ref<SystemSetting[]>([])
const auditLogs = ref<AuditLogEntry[]>([])
const notificationOutbox = ref<NotificationOutboxMessage[]>([])
const integrationConfig = ref<IntegrationConfigStatus>({
checkedAt: '',
integrations: [],
@@ -59,9 +61,17 @@ export const usePlatformStore = defineStore('platform', () => {
const auditLogsPromise = includeAdminData
? devopsApi.getAuditLogs()
: Promise.resolve<AuditLogEntry[]>([])
const [processXml, recentAuditLogs] = await Promise.all([processXmlPromise, auditLogsPromise])
const notificationOutboxPromise = includeAdminData
? devopsApi.getNotificationOutbox()
: Promise.resolve<NotificationOutboxMessage[]>([])
const [processXml, recentAuditLogs, recentNotificationOutbox] = await Promise.all([
processXmlPromise,
auditLogsPromise,
notificationOutboxPromise,
])
releaseProcessXml.value = processXml
auditLogs.value = recentAuditLogs
notificationOutbox.value = recentNotificationOutbox
dataSource.value = devopsApi.getLastDataSource()
} finally {
loading.value = false
@@ -119,6 +129,12 @@ export const usePlatformStore = defineStore('platform', () => {
return auditLogs.value
}
async function loadNotificationOutbox() {
notificationOutbox.value = await devopsApi.getNotificationOutbox()
dataSource.value = devopsApi.getLastDataSource()
return notificationOutbox.value
}
function upsertRun(run: ReleaseRun) {
runs.value = [run, ...runs.value.filter((item) => item.id !== run.id)]
}
@@ -132,6 +148,7 @@ export const usePlatformStore = defineStore('platform', () => {
agentCards,
settings,
auditLogs,
notificationOutbox,
integrationConfig,
releaseProcessXml,
projectRefs,
@@ -145,5 +162,6 @@ export const usePlatformStore = defineStore('platform', () => {
syncJenkinsRuns,
loadProjectRefs,
loadAuditLogs,
loadNotificationOutbox,
}
})
+17
View File
@@ -317,6 +317,23 @@ export interface AuditLogEntry {
createdAt: string
}
export type NotificationOutboxStatus = 'pending' | 'sent' | 'failed' | 'dead'
export interface NotificationOutboxMessage {
id: string
deployRunId?: string
channel: string
template: string
payloadSummary?: Record<string, unknown>
status: NotificationOutboxStatus
attemptCount: number
nextAttemptAt?: string
lastError?: string
idempotencyKey: string
createdAt: string
updatedAt: string
}
export interface AgentCard {
mode: AgentMode
title: string
+93
View File
@@ -9,14 +9,17 @@ import AuditJsonCell from '../components/AuditJsonCell.vue'
import StatusPill from '../components/StatusPill.vue'
import { devopsApi } from '../services/devopsApi'
import { usePlatformStore } from '../stores/platform'
import { useSessionStore } from '../stores/session'
import type {
AgentConfigSummary,
AuditLogEntry,
IntegrationConfigItem,
IntegrationConfigState,
NotificationOutboxMessage,
} from '../types/devops'
const platform = usePlatformStore()
const session = useSessionStore()
const agentConfigLoading = ref(false)
const agentConfigTesting = ref(false)
const agentConfig = ref<AgentConfigSummary | null>(null)
@@ -42,6 +45,15 @@ const configStatusMeta: Record<
partial: { label: '部分配置', type: 'warning' },
missing: { label: '未配置', type: 'danger' },
}
const outboxStatusMeta: Record<
NotificationOutboxMessage['status'],
{ label: string; type: 'success' | 'warning' | 'danger' | 'info' }
> = {
pending: { label: '待投递', type: 'warning' },
sent: { label: '已投递', type: 'success' },
failed: { label: '投递失败', type: 'danger' },
dead: { label: '已终止', type: 'info' },
}
onMounted(() => {
void loadAgentConfig()
@@ -109,6 +121,20 @@ function auditPayload(record: AuditLogEntry): unknown {
return Object.keys(payload).length > 0 ? payload : null
}
function outboxPayload(record: NotificationOutboxMessage): unknown {
return record.payloadSummary ?? null
}
function outboxStatusLabel(status: NotificationOutboxMessage['status']): string {
return outboxStatusMeta[status]?.label ?? '未知'
}
function outboxStatusType(
status: NotificationOutboxMessage['status'],
): 'success' | 'warning' | 'danger' | 'info' {
return outboxStatusMeta[status]?.type ?? 'info'
}
function variableGroups(item: IntegrationConfigItem): VariableGroup[] {
return [
{
@@ -211,6 +237,15 @@ async function testAgentConfig() {
}
}
async function refreshNotificationOutbox() {
try {
await platform.loadNotificationOutbox()
ElMessage.success('通知 outbox 已刷新')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '通知 outbox 刷新失败')
}
}
function applyAgentConfig(config: AgentConfigSummary) {
agentConfig.value = config
agentConfigForm.baseURL = config.baseURL
@@ -386,6 +421,58 @@ function applyAgentConfig(config: AgentConfigSummary) {
</div>
</section>
<section v-if="session.role === 'super_admin'" class="panel">
<div class="panel-header">
<h2>通知 outbox</h2>
<ElButton :icon="Refresh" size="small" @click="refreshNotificationOutbox">
刷新
</ElButton>
</div>
<div class="panel-body">
<ElTable :data="platform.notificationOutbox" border>
<ElTableColumn label="创建时间" min-width="150">
<template #default="{ row }">
{{ formatCheckedAt(row.createdAt) }}
</template>
</ElTableColumn>
<ElTableColumn label="状态" min-width="110">
<template #default="{ row }">
<ElTag :type="outboxStatusType(row.status)" effect="light" round>
{{ outboxStatusLabel(row.status) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="channel" label="渠道" min-width="100" />
<ElTableColumn prop="template" label="模板" min-width="130" />
<ElTableColumn label="发布单" min-width="220">
<template #default="{ row }">
<span class="mono">{{ row.deployRunId || '-' }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="尝试" min-width="80">
<template #default="{ row }">
{{ row.attemptCount }}
</template>
</ElTableColumn>
<ElTableColumn label="错误摘要" min-width="240">
<template #default="{ row }">
<span class="outbox-error">{{ row.lastError || '-' }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="脱敏 payload" min-width="360">
<template #default="{ row }">
<AuditJsonCell :value="outboxPayload(row)" />
</template>
</ElTableColumn>
</ElTable>
<ElEmpty
v-if="platform.notificationOutbox.length === 0"
description="暂无通知 outbox 记录"
:image-size="72"
/>
</div>
</section>
<section class="panel">
<div class="panel-header">
<h2>最近运维审计</h2>
@@ -609,6 +696,12 @@ function applyAgentConfig(config: AgentConfigSummary) {
line-height: 24px;
}
.outbox-error {
color: #b42318;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 1080px) {
.variable-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));