Expand generation harness observability
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
GenerationJobEvent,
|
||||
GenerationJobSummary,
|
||||
GenerationProviderStats,
|
||||
GenerationTraceSummary,
|
||||
} from '../types/generation'
|
||||
import LoadingSpinner from './ui/LoadingSpinner.vue'
|
||||
|
||||
@@ -27,6 +28,7 @@ const props = withDefaults(
|
||||
const jobHistory = ref<GenerationJobSummary[]>([])
|
||||
const activeJob = ref<GenerationJobDetail | null>(null)
|
||||
const providerStats = ref<GenerationProviderStats | null>(null)
|
||||
const traceSummary = ref<GenerationTraceSummary | null>(null)
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
const error = ref('')
|
||||
@@ -42,6 +44,8 @@ const providerSuccessRate = computed(() => {
|
||||
if (!providerStats.value?.total_calls) return null
|
||||
return Math.round((providerStats.value.successful_calls / providerStats.value.total_calls) * 100)
|
||||
})
|
||||
const topTraceStep = computed(() => traceSummary.value?.by_step[0] ?? null)
|
||||
const topFailureCategory = computed(() => traceSummary.value?.failure_categories[0] ?? null)
|
||||
|
||||
const containerClass = computed(() => (
|
||||
isDark.value
|
||||
@@ -100,6 +104,7 @@ function getJobStatusLabel(status?: string) {
|
||||
function getEventLabel(eventType: string) {
|
||||
const labels: Record<string, string> = {
|
||||
request_accepted: '请求接收',
|
||||
workflow_planned: '工作流规划',
|
||||
worker_started: '后台任务开始',
|
||||
retry_queued: '重新排队',
|
||||
cancel_requested: '已请求取消',
|
||||
@@ -122,6 +127,7 @@ function getEventLabel(eventType: string) {
|
||||
provider_call_started: '供应商调用',
|
||||
provider_call_succeeded: '供应商成功',
|
||||
provider_call_failed: '供应商失败',
|
||||
quality_gate_failed: '质量门失败',
|
||||
asset_retry_started: '资源重试开始',
|
||||
asset_retry_completed: '资源重试完成',
|
||||
asset_retry_failed: '资源重试失败',
|
||||
@@ -134,6 +140,72 @@ function getEventLabel(eventType: string) {
|
||||
return labels[eventType] ?? eventType
|
||||
}
|
||||
|
||||
function getStepLabel(step?: unknown) {
|
||||
const labels: Record<string, string> = {
|
||||
request_acceptance: '请求接收',
|
||||
worker_start: '后台启动',
|
||||
context_preparation: '上下文准备',
|
||||
narrative_generation: '主内容生成',
|
||||
story_persistence: '故事保存',
|
||||
provider_invocation: '供应商调用',
|
||||
image_generation: '图片生成',
|
||||
audio_generation: '音频生成',
|
||||
asset_retry: '资源重试',
|
||||
asset_generation: '资源生成',
|
||||
postprocessing: '后处理',
|
||||
completion: '任务完成',
|
||||
cancellation: '取消',
|
||||
stale_recovery: '超时收敛',
|
||||
unknown: '未知步骤',
|
||||
}
|
||||
const key = typeof step === 'string' ? step : ''
|
||||
return labels[key] ?? key
|
||||
}
|
||||
|
||||
function getArtifactLabel(artifact?: unknown) {
|
||||
const labels: Record<string, string> = {
|
||||
story_text: '故事正文',
|
||||
storybook_pages: '绘本分页',
|
||||
cover_image: '封面图',
|
||||
page_image: '分页插图',
|
||||
image: '图片资源',
|
||||
audio: '音频',
|
||||
achievement_memory: '成长记忆',
|
||||
none: '无资源',
|
||||
unknown: '未知资源',
|
||||
}
|
||||
const key = typeof artifact === 'string' ? artifact : ''
|
||||
return labels[key] ?? key
|
||||
}
|
||||
|
||||
function getFailureCategoryLabel(category?: unknown) {
|
||||
const labels: Record<string, string> = {
|
||||
provider_error: '供应商失败',
|
||||
schema_error: '结构不完整',
|
||||
safety_error: '儿童安全风险',
|
||||
timeout: '超时',
|
||||
canceled: '用户取消',
|
||||
stale_job: '任务卡住',
|
||||
storage_error: '存储失败',
|
||||
validation_error: '输入校验失败',
|
||||
unknown_error: '未知失败',
|
||||
}
|
||||
const key = typeof category === 'string' ? category : ''
|
||||
return labels[key] ?? key
|
||||
}
|
||||
|
||||
function getTraceMetaText(event: GenerationJobEvent) {
|
||||
const meta = event.event_metadata
|
||||
const step = getStepLabel(meta.step)
|
||||
const artifact = getArtifactLabel(meta.artifact)
|
||||
const failureCategory = meta.failure_category
|
||||
? getFailureCategoryLabel(meta.failure_category)
|
||||
: ''
|
||||
return [step, artifact && artifact !== '无资源' ? artifact : '', failureCategory]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
}
|
||||
|
||||
function formatDateTime(value: string) {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
hour: '2-digit',
|
||||
@@ -175,22 +247,25 @@ async function selectGenerationJob(jobId: string) {
|
||||
|
||||
async function refresh() {
|
||||
if (props.storyId === null) {
|
||||
jobHistory.value = []
|
||||
activeJob.value = null
|
||||
providerStats.value = null
|
||||
return
|
||||
jobHistory.value = []
|
||||
activeJob.value = null
|
||||
providerStats.value = null
|
||||
traceSummary.value = null
|
||||
return
|
||||
}
|
||||
|
||||
error.value = ''
|
||||
const selectedJobId = activeJob.value?.id ?? null
|
||||
|
||||
try {
|
||||
const [jobs, stats] = await Promise.all([
|
||||
const [jobs, stats, trace] = await Promise.all([
|
||||
api.get<GenerationJobSummary[]>(`/api/generations/${props.storyId}/jobs`),
|
||||
api.get<GenerationProviderStats>(`/api/generations/${props.storyId}/provider-stats`),
|
||||
api.get<GenerationTraceSummary>(`/api/generations/${props.storyId}/trace-summary`),
|
||||
])
|
||||
jobHistory.value = jobs
|
||||
providerStats.value = stats
|
||||
traceSummary.value = trace
|
||||
const nextJobId = (
|
||||
selectedJobId
|
||||
? jobHistory.value.find((job) => job.id === selectedJobId)?.id
|
||||
@@ -205,6 +280,7 @@ async function refresh() {
|
||||
jobHistory.value = []
|
||||
activeJob.value = null
|
||||
providerStats.value = null
|
||||
traceSummary.value = null
|
||||
error.value = e instanceof Error ? e.message : '生成轨迹加载失败'
|
||||
}
|
||||
}
|
||||
@@ -318,6 +394,32 @@ defineExpose({ refresh })
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="traceSummary?.total_events"
|
||||
class="grid gap-3 md:grid-cols-4"
|
||||
>
|
||||
<div class="rounded-lg border p-3" :class="panelClass">
|
||||
<div class="text-xs" :class="mutedTextClass">流程事件</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ traceSummary.total_events }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border p-3" :class="panelClass">
|
||||
<div class="text-xs" :class="mutedTextClass">失败事件</div>
|
||||
<div class="mt-1 text-xl font-semibold">{{ traceSummary.failed_events }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border p-3" :class="panelClass">
|
||||
<div class="text-xs" :class="mutedTextClass">主要步骤</div>
|
||||
<div class="mt-1 text-base font-semibold">
|
||||
{{ topTraceStep ? `${getStepLabel(topTraceStep.name)} · ${topTraceStep.count}` : '暂无' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border p-3" :class="panelClass">
|
||||
<div class="text-xs" :class="mutedTextClass">主要失败</div>
|
||||
<div class="mt-1 text-base font-semibold">
|
||||
{{ topFailureCategory ? `${getFailureCategoryLabel(topFailureCategory.name)} · ${topFailureCategory.count}` : '暂无' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!jobHistory.length" class="rounded-lg border border-dashed border-gray-200 p-4 text-sm" :class="mutedTextClass">
|
||||
暂无生成轨迹。旧数据会在下一次资源补全后开始记录。
|
||||
</div>
|
||||
@@ -432,6 +534,9 @@ defineExpose({ refresh })
|
||||
<p v-else-if="event.message" class="mt-1 text-xs" :class="mutedTextClass">
|
||||
{{ event.message }}
|
||||
</p>
|
||||
<p v-if="getTraceMetaText(event)" class="mt-1 text-xs" :class="mutedTextClass">
|
||||
{{ getTraceMetaText(event) }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -58,6 +58,21 @@ export interface GenerationProviderStats {
|
||||
}>
|
||||
}
|
||||
|
||||
export interface GenerationTraceBucket {
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface GenerationTraceSummary {
|
||||
story_id: number
|
||||
window_days: number | null
|
||||
total_events: number
|
||||
failed_events: number
|
||||
by_step: GenerationTraceBucket[]
|
||||
by_artifact: GenerationTraceBucket[]
|
||||
failure_categories: GenerationTraceBucket[]
|
||||
}
|
||||
|
||||
export interface GenerationProviderAnalytics {
|
||||
window_days: number | null
|
||||
capability: string | null
|
||||
|
||||
Reference in New Issue
Block a user