Add async request body cleanup records

This commit is contained in:
Entropy.Xu
2026-05-08 13:50:18 +08:00
parent fa22384f24
commit 3f29335fd6
15 changed files with 1151 additions and 15 deletions

View File

@@ -236,6 +236,28 @@ export interface EmailTemplateResetResponse {
}
}
export interface CleanupRunRecord {
id: string
kind: string
trigger: string
status: 'processing' | 'completed' | 'failed'
message: string
started_at_unix_secs: number
completed_at_unix_secs: number | null
duration_ms: number | null
summary: Record<string, unknown>
error: string | null
}
export interface CleanupRunListResponse {
items: CleanupRunRecord[]
}
export interface RequestBodyCleanupTaskResponse {
message: string
task: CleanupRunRecord
}
// 检查更新响应
export interface CheckUpdateResponse {
current_version: string
@@ -1056,7 +1078,15 @@ export const adminApi = {
purgeUsage: () => purge<{ message: string; deleted: Record<string, number> }>('usage'),
purgeAuditLogs: () => purge<{ message: string; deleted: Record<string, number> }>('audit-logs'),
purgeRequestBodies: () => purge<{ message: string; cleaned: Record<string, number> }>('request-bodies'),
async purgeRequestBodiesAsync(): Promise<RequestBodyCleanupTaskResponse> {
const response = await apiClient.post<RequestBodyCleanupTaskResponse>('/api/admin/system/purge/request-bodies/task')
return response.data
},
purgeStats: () => purge<{ message: string }>('stats'),
async getCleanupRuns(): Promise<CleanupRunListResponse> {
const response = await apiClient.get<CleanupRunListResponse>('/api/admin/system/cleanup/runs')
return response.data
},
async getTimeSeries(params?: {
start_date?: string

View File

@@ -208,10 +208,104 @@
<p>6. <strong>审计日志</strong>: 独立清理记录用户登录操作等安全事件</p>
</div>
</div>
<div class="mt-4 border border-border rounded-lg overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b border-border">
<div>
<h4 class="text-sm font-medium">
最近清理记录
</h4>
<p class="text-xs text-muted-foreground">
自动清理手动系统清理和请求体后台任务的执行结果
</p>
</div>
<Button
variant="outline"
size="sm"
:disabled="cleanupRunsLoading"
@click="loadCleanupRuns"
>
<RefreshCw
class="w-3.5 h-3.5 mr-1.5"
:class="{ 'animate-spin': cleanupRunsLoading }"
/>
刷新
</Button>
</div>
<div
v-if="cleanupRuns.length === 0 && !cleanupRunsLoading"
class="px-4 py-6 text-sm text-muted-foreground"
>
暂无清理记录
</div>
<div
v-else
class="overflow-x-auto"
>
<table class="w-full text-sm">
<thead class="bg-muted/30 text-xs text-muted-foreground">
<tr>
<th class="px-4 py-2 text-left font-medium">
时间
</th>
<th class="px-4 py-2 text-left font-medium">
类型
</th>
<th class="px-4 py-2 text-left font-medium">
来源
</th>
<th class="px-4 py-2 text-left font-medium">
状态
</th>
<th class="px-4 py-2 text-left font-medium">
结果
</th>
<th class="px-4 py-2 text-right font-medium">
耗时
</th>
</tr>
</thead>
<tbody>
<tr
v-for="run in cleanupRuns"
:key="run.id"
class="border-t border-border"
>
<td class="px-4 py-2 whitespace-nowrap">
{{ formatRunTime(run.started_at_unix_secs) }}
</td>
<td class="px-4 py-2 whitespace-nowrap">
{{ cleanupKindLabel(run.kind) }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-muted-foreground">
{{ run.trigger === 'manual' ? '手动' : '自动' }}
</td>
<td class="px-4 py-2 whitespace-nowrap">
<span :class="cleanupStatusClass(run.status)">
{{ cleanupStatusLabel(run.status) }}
</span>
</td>
<td class="px-4 py-2 min-w-[18rem]">
<div>{{ run.error || run.message }}</div>
<div class="text-xs text-muted-foreground">
{{ cleanupSummaryText(run.summary) }}
</div>
</td>
<td class="px-4 py-2 text-right whitespace-nowrap text-muted-foreground">
{{ formatDuration(run.duration_ms) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</CardSection>
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { RefreshCw } from 'lucide-vue-next'
import { adminApi, type CleanupRunRecord } from '@/api/admin'
import Button from '@/components/ui/button.vue'
import Input from '@/components/ui/input.vue'
import Label from '@/components/ui/label.vue'
@@ -244,4 +338,90 @@ defineEmits<{
'update:requestCandidatesRetentionDays': [value: number]
'update:requestCandidatesCleanupBatchSize': [value: number]
}>()
const cleanupRuns = ref<CleanupRunRecord[]>([])
const cleanupRunsLoading = ref(false)
let cleanupRunsTimer: ReturnType<typeof window.setInterval> | null = null
async function loadCleanupRuns() {
cleanupRunsLoading.value = true
try {
const response = await adminApi.getCleanupRuns()
cleanupRuns.value = response.items.slice(0, 10)
} finally {
cleanupRunsLoading.value = false
}
}
function cleanupKindLabel(kind: string): string {
const labels: Record<string, string> = {
usage_cleanup: '请求记录',
audit_cleanup: '审计日志',
request_candidate_cleanup: '候选记录',
request_bodies: '请求体',
system_cleanup: '系统清理',
}
return labels[kind] || kind
}
function cleanupStatusLabel(status: string): string {
if (status === 'processing') return '执行中'
if (status === 'failed') return '失败'
return '完成'
}
function cleanupStatusClass(status: string): string {
if (status === 'processing') return 'text-amber-500'
if (status === 'failed') return 'text-destructive'
return 'text-emerald-500'
}
function formatRunTime(value: number): string {
if (!value) return '-'
return new Date(value * 1000).toLocaleString()
}
function formatDuration(value: number | null): string {
if (value === null || value === undefined) return '-'
if (value < 1000) return `${value}ms`
return `${(value / 1000).toFixed(1)}s`
}
function cleanupSummaryText(summary: Record<string, unknown>): string {
const total = typeof summary.total === 'number' ? summary.total : null
if (total !== null) return `影响 ${total}`
const entries = Object.entries(summary)
.filter(([, value]) => typeof value === 'number' && value > 0)
.map(([key, value]) => `${summaryLabel(key)} ${value}`)
return entries.length > 0 ? entries.join(' / ') : '无数据变更'
}
function summaryLabel(key: string): string {
const labels: Record<string, string> = {
body_externalized: '压缩',
legacy_body_refs_migrated: '迁移',
body_cleaned: '清体',
header_cleaned: '清头',
keys_cleaned: 'Key',
records_deleted: '删记录',
audit_logs_deleted: '删日志',
request_candidates_deleted: '删候选',
}
return labels[key] || key
}
onMounted(() => {
void loadCleanupRuns()
cleanupRunsTimer = window.setInterval(() => {
void loadCleanupRuns()
}, 15_000)
})
onBeforeUnmount(() => {
if (cleanupRunsTimer) {
window.clearInterval(cleanupRunsTimer)
cleanupRunsTimer = null
}
})
</script>

View File

@@ -98,11 +98,11 @@ const purgeItems: PurgeItem[] = [
{
key: 'request-bodies',
title: '清空请求体',
description: '清空所有请求/响应体数据,保留统计信息',
description: '后台分批清空所有请求/响应体数据,保留统计信息',
buttonText: '清空请求体',
icon: markRaw(FileText),
confirmMessage: '确定要清空全部请求体吗?请求/响应内容将被清除,但 token 和成本等统计信息会保留,操作不可逆。',
action: () => adminApi.purgeRequestBodies(),
confirmMessage: '确定要后台清空全部请求体吗?请求/响应内容将被分批清除,但 token 和成本等统计信息会保留,操作不可逆。',
action: () => adminApi.purgeRequestBodiesAsync(),
},
{
key: 'stats',