feat: add provider analytics summary

This commit is contained in:
2026-04-18 22:01:34 +08:00
parent e99a7fbe14
commit 4d54c144a8
15 changed files with 437 additions and 36 deletions

View File

@@ -34,8 +34,30 @@ interface StoryItem {
last_error: string | null
}
interface GenerationProviderStat {
capability: string
adapter: string
call_count: number
success_count: number
failure_count: number
avg_latency_ms: number | null
estimated_cost_usd: number
}
interface GenerationProviderAnalytics {
total_calls: number
successful_calls: number
failed_calls: number
avg_latency_ms: number | null
estimated_cost_usd: number
job_count: number
story_count: number
by_provider: GenerationProviderStat[]
}
const router = useRouter()
const stories = ref<StoryItem[]>([])
const providerAnalytics = ref<GenerationProviderAnalytics | null>(null)
const loading = ref(true)
const error = ref('')
const showCreateModal = ref(false)
@@ -45,10 +67,22 @@ const readableCount = computed(() =>
const attentionCount = computed(() =>
stories.value.filter((story) => needsGenerationAttention(story.generation_status)).length,
)
const providerSuccessRate = computed(() => {
if (!providerAnalytics.value?.total_calls) return null
return Math.round(
(providerAnalytics.value.successful_calls / providerAnalytics.value.total_calls) * 100,
)
})
const topProvider = computed(() => providerAnalytics.value?.by_provider[0] ?? null)
async function fetchStories() {
try {
stories.value = await api.get<StoryItem[]>('/api/stories')
const [storyList, analytics] = await Promise.all([
api.get<StoryItem[]>('/api/stories'),
api.get<GenerationProviderAnalytics>('/api/generations/provider-analytics'),
])
stories.value = storyList
providerAnalytics.value = analytics
} catch (e) {
error.value = e instanceof Error ? e.message : '加载失败'
} finally {
@@ -81,6 +115,14 @@ function getStoryPath(story: StoryItem) {
return story.mode === 'storybook' ? `/storybook/view/${story.id}` : `/story/${story.id}`
}
function formatLatency(value?: number | null) {
return typeof value === 'number' ? `${Math.round(value)}ms` : '暂无'
}
function formatCost(value?: number | null) {
return typeof value === 'number' ? `$${value.toFixed(4)}` : '$0.0000'
}
onMounted(() => {
fetchStories()
if (router.currentRoute.value.query.openCreate) {
@@ -160,6 +202,42 @@ onMounted(() => {
</div>
</BaseCard>
<BaseCard
v-if="providerAnalytics?.total_calls"
class="mb-8"
padding="lg"
>
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-xl font-bold text-gray-800">Provider 运营摘要</h2>
<p class="mt-2 text-sm leading-6 text-gray-500">
生成资源补全和失败恢复留下的供应商调用轨迹
</p>
</div>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4 lg:min-w-[520px]">
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-3">
<div class="text-xs text-gray-500">成功率</div>
<div class="mt-1 text-lg font-semibold text-gray-800">{{ providerSuccessRate }}%</div>
</div>
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-3">
<div class="text-xs text-gray-500">平均耗时</div>
<div class="mt-1 text-lg font-semibold text-gray-800">{{ formatLatency(providerAnalytics.avg_latency_ms) }}</div>
</div>
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-3">
<div class="text-xs text-gray-500">预估成本</div>
<div class="mt-1 text-lg font-semibold text-gray-800">{{ formatCost(providerAnalytics.estimated_cost_usd) }}</div>
</div>
<div class="rounded-lg border border-gray-100 bg-gray-50 px-3 py-3">
<div class="text-xs text-gray-500">调用次数</div>
<div class="mt-1 text-lg font-semibold text-gray-800">{{ providerAnalytics.total_calls }}</div>
</div>
</div>
</div>
<p v-if="topProvider" class="mt-4 text-sm text-gray-500">
当前样本中最前面的能力组合是 {{ topProvider.capability }} / {{ topProvider.adapter }}成功 {{ topProvider.success_count }} 失败 {{ topProvider.failure_count }}
</p>
</BaseCard>
<!-- 故事网格 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<router-link