diff --git a/src/services/devopsApi.ts b/src/services/devopsApi.ts index b42ce63..e29d019 100644 --- a/src/services/devopsApi.ts +++ b/src/services/devopsApi.ts @@ -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 { + const { data } = await http.get('/notification-outbox') + lastDataSource = 'backend' + return Array.isArray(data) ? data.map(sanitizeNotificationOutboxMessage) : [] + }, + async getAgentCards(): Promise { 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' diff --git a/src/stores/platform.ts b/src/stores/platform.ts index 179ac22..814a6d7 100644 --- a/src/stores/platform.ts +++ b/src/stores/platform.ts @@ -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([]) const settings = ref([]) const auditLogs = ref([]) + const notificationOutbox = ref([]) const integrationConfig = ref({ checkedAt: '', integrations: [], @@ -59,9 +61,17 @@ export const usePlatformStore = defineStore('platform', () => { const auditLogsPromise = includeAdminData ? devopsApi.getAuditLogs() : Promise.resolve([]) - const [processXml, recentAuditLogs] = await Promise.all([processXmlPromise, auditLogsPromise]) + const notificationOutboxPromise = includeAdminData + ? devopsApi.getNotificationOutbox() + : Promise.resolve([]) + 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, } }) diff --git a/src/types/devops.ts b/src/types/devops.ts index 357ec61..805cc19 100644 --- a/src/types/devops.ts +++ b/src/types/devops.ts @@ -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 + status: NotificationOutboxStatus + attemptCount: number + nextAttemptAt?: string + lastError?: string + idempotencyKey: string + createdAt: string + updatedAt: string +} + export interface AgentCard { mode: AgentMode title: string diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index a19a230..920920e 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -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(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) { +
+
+

通知 outbox

+ + 刷新 + +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+

最近运维审计

@@ -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));