feat(usage,pool,codex): 请求详情 body 按需加载、配额选择器重构与 Codex body rules 修正

- usage 详情 API 新增 include_bodies 参数,支持跳过 body 内容返回 has_*_body 标记
- 前端请求详情抽屉首次加载不含 body,切换到 body tab 时延迟加载并展示 skeleton
- Timeline 组件延迟 120ms 挂载,避免阻塞抽屉渲染
- 提取号池配额判断逻辑到 quota-selectors 工具模块并添加单测
- Codex 移除 openai:compact 的默认 body rules 注册
- ModelTestDialog/TestResultDialog 模板格式化
This commit is contained in:
fawney19
2026-03-06 15:40:11 +08:00
parent d97ec3fde2
commit bdccfa6e78
10 changed files with 395 additions and 100 deletions

View File

@@ -398,8 +398,9 @@
</Card>
<!-- 请求链路追踪卡片 -->
<div v-if="detail.request_id || detail.id">
<div>
<HorizontalRequestTimeline
v-if="showTimeline && (detail.request_id || detail.id)"
ref="timelineRef"
:request-id="detail.request_id || detail.id"
:override-status-code="detail.status_code"
@@ -584,13 +585,17 @@
</TabsContent>
<TabsContent value="request-body">
<!-- 对话视图 -->
<div
v-if="isRequestBodyLoading"
class="p-4"
>
<Skeleton class="h-32 w-full" />
</div>
<ConversationView
v-if="contentViewMode === 'conversation'"
v-else-if="contentViewMode === 'conversation'"
:render-result="requestRenderResult"
empty-message="无请求体信息"
/>
<!-- JSON 视图 -->
<JsonContent
v-else
:data="currentRequestBody"
@@ -629,13 +634,17 @@
</TabsContent>
<TabsContent value="response-body">
<!-- 对话视图 -->
<div
v-if="isResponseBodyLoading"
class="p-4"
>
<Skeleton class="h-32 w-full" />
</div>
<ConversationView
v-if="contentViewMode === 'conversation'"
v-else-if="contentViewMode === 'conversation'"
:render-result="responseRenderResult"
empty-message="无响应体信息"
/>
<!-- JSON 视图 -->
<JsonContent
v-else
:data="currentResponseBody"
@@ -751,9 +760,15 @@ const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hid
const curlCopying = ref(false)
const curlCopied = ref(false)
const replayDialogOpen = ref(false)
const bodyLoading = ref(false)
const bodiesLoadedForRequestId = ref<string | null>(null)
const showTimeline = ref(false)
const AUTO_REFRESH_INTERVAL_MS = 1000
const TIMELINE_MOUNT_DELAY_MS = 120
let loadDetailRequestId = 0
let bodyLoadRequestId = 0
let loadDetailInFlight = false
let timelineMountTimer: ReturnType<typeof setTimeout> | null = null
// 监听标签页切换
watch(activeTab, (newTab) => {
@@ -765,6 +780,10 @@ watch(activeTab, (newTab) => {
contentViewMode.value = 'json'
}
dataSource.value = getDefaultDataSourceForTab(newTab)
if (['request-body', 'response-body'].includes(newTab)) {
void ensureBodyContentLoaded()
}
})
// 检测暗色模式
@@ -778,6 +797,46 @@ const traceRequestMetadata = computed<Record<string, unknown> | null>(() => {
return meta as Record<string, unknown>
})
function hasBodyContent(flag: boolean | undefined, data: unknown): boolean {
return Boolean(flag) || hasContent(data)
}
const hasRequestBodyAvailable = computed(() => {
return hasBodyContent(detail.value?.has_request_body, detail.value?.request_body)
|| hasBodyContent(detail.value?.has_provider_request_body, detail.value?.provider_request_body)
})
const hasResponseBodyAvailable = computed(() => {
return hasBodyContent(detail.value?.has_response_body, detail.value?.response_body)
|| hasBodyContent(detail.value?.has_client_response_body, detail.value?.client_response_body)
})
const isRequestBodyLoading = computed(() => {
return bodyLoading.value && activeTab.value === 'request-body' && !currentRequestBody.value
})
const isResponseBodyLoading = computed(() => {
return bodyLoading.value && activeTab.value === 'response-body' && !currentResponseBody.value
})
function clearTimelineMountTimer() {
if (timelineMountTimer) {
clearTimeout(timelineMountTimer)
timelineMountTimer = null
}
}
function scheduleTimelineMount() {
clearTimelineMountTimer()
if (typeof window === 'undefined') {
showTimeline.value = true
return
}
timelineMountTimer = window.setTimeout(() => {
showTimeline.value = true
}, TIMELINE_MOUNT_DELAY_MS)
}
// 检测是否有提供商请求头
const hasProviderHeaders = computed(() => {
return !!(detail.value?.provider_request_headers &&
@@ -786,12 +845,13 @@ const hasProviderHeaders = computed(() => {
// 请求体:仅当 provider_request_body 存在时才展示来源切换
const hasProviderRequestBody = computed(() => {
return hasContent(detail.value?.provider_request_body)
return hasBodyContent(detail.value?.has_provider_request_body, detail.value?.provider_request_body)
})
// 响应体:只有客户端侧和 provider 侧都存在时才展示来源切换
const hasProviderResponseBody = computed(() => {
return hasContent(detail.value?.response_body) && hasContent(detail.value?.client_response_body)
return hasBodyContent(detail.value?.has_response_body, detail.value?.response_body)
&& hasBodyContent(detail.value?.has_client_response_body, detail.value?.client_response_body)
})
// 检测是否有两套响应头(客户端侧 + 提供商侧)
@@ -895,6 +955,9 @@ const requestRenderResult = computed<RenderResult>(() => {
if (!body) {
return { blocks: [], isStream: false }
}
if (activeTab.value !== 'request-body' || contentViewMode.value !== 'conversation') {
return { blocks: [], isStream: false }
}
return renderRequest(body, currentResponseBody.value, detail.value?.api_format)
})
@@ -904,6 +967,9 @@ const responseRenderResult = computed<RenderResult>(() => {
if (!body) {
return { blocks: [], isStream: false }
}
if (activeTab.value !== 'response-body' || contentViewMode.value !== 'conversation') {
return { blocks: [], isStream: false }
}
return renderResponse(body, currentRequestBody.value, detail.value?.api_format)
})
@@ -913,14 +979,13 @@ const supportsConversationView = computed(() => {
})
// 当前对话数据是否有效(用于禁用按钮)
// 不依赖带 tab/mode 守卫的 renderResult直接检查 body 数据是否存在
const hasValidConversation = computed(() => {
if (activeTab.value === 'request-body') {
return !requestRenderResult.value.error &&
requestRenderResult.value.blocks.length > 0
return !!currentRequestBody.value
}
if (activeTab.value === 'response-body') {
return !responseRenderResult.value.error &&
responseRenderResult.value.blocks.length > 0
return !!currentResponseBody.value
}
return false
})
@@ -1168,8 +1233,8 @@ function getDefaultDataSourceForTab(tab: string): 'client' | 'provider' {
}
if (tab === 'request-body') {
if (hasContent(detail.value.provider_request_body)) return 'provider'
if (hasContent(detail.value.request_body)) return 'client'
if (hasBodyContent(detail.value.has_provider_request_body, detail.value.provider_request_body)) return 'provider'
if (hasBodyContent(detail.value.has_request_body, detail.value.request_body)) return 'client'
return 'provider'
}
@@ -1180,8 +1245,8 @@ function getDefaultDataSourceForTab(tab: string): 'client' | 'provider' {
}
if (tab === 'response-body') {
if (hasContent(detail.value.client_response_body)) return 'client'
if (hasContent(detail.value.response_body)) return 'provider'
if (hasBodyContent(detail.value.has_client_response_body, detail.value.client_response_body)) return 'client'
if (hasBodyContent(detail.value.has_response_body, detail.value.response_body)) return 'provider'
return 'client'
}
@@ -1213,11 +1278,11 @@ const visibleTabs = computed(() => {
case 'request-headers':
return hasContent(detail.value?.request_headers) || hasContent(detail.value?.provider_request_headers)
case 'request-body':
return hasContent(detail.value?.request_body) || hasContent(detail.value?.provider_request_body)
return hasRequestBodyAvailable.value
case 'response-headers':
return hasContent(detail.value?.response_headers) || hasContent(detail.value?.client_response_headers)
case 'response-body':
return hasContent(detail.value?.response_body) || hasContent(detail.value?.client_response_body)
return hasResponseBodyAvailable.value
case 'metadata':
return hasContent(detail.value?.metadata)
default:
@@ -1237,9 +1302,47 @@ watch(() => props.isOpen, async (isOpen) => {
await loadDetail(props.requestId)
} else if (!isOpen) {
stopAutoRefresh()
showTimeline.value = false
clearTimelineMountTimer()
bodyLoading.value = false
bodiesLoadedForRequestId.value = null
}
})
async function ensureBodyContentLoaded() {
if (!props.requestId || !detail.value) return
const cacheKey = detail.value.request_id || detail.value.id
if (bodiesLoadedForRequestId.value === cacheKey || bodyLoading.value) return
if (!hasRequestBodyAvailable.value && !hasResponseBodyAvailable.value) return
const requestId = ++bodyLoadRequestId
bodyLoading.value = true
try {
const response = await dashboardApi.getRequestDetail(props.requestId, { includeBodies: true })
if (requestId !== bodyLoadRequestId || !detail.value) return
detail.value = {
...detail.value,
request_body: response.request_body,
provider_request_body: response.provider_request_body,
response_body: response.response_body,
client_response_body: response.client_response_body,
has_request_body: response.has_request_body,
has_provider_request_body: response.has_provider_request_body,
has_response_body: response.has_response_body,
has_client_response_body: response.has_client_response_body,
}
bodiesLoadedForRequestId.value = cacheKey
} catch (err) {
if (requestId !== bodyLoadRequestId) return
log.error('Failed to load request bodies:', err)
} finally {
if (requestId === bodyLoadRequestId) {
bodyLoading.value = false
}
}
}
async function loadDetail(id: string, silent = false) {
if (silent && loadDetailInFlight) {
return
@@ -1249,23 +1352,40 @@ async function loadDetail(id: string, silent = false) {
if (!silent) {
loading.value = true
historicalPricing.value = null
showTimeline.value = false
clearTimelineMountTimer()
++bodyLoadRequestId
bodyLoading.value = false
}
error.value = null
try {
const response = await dashboardApi.getRequestDetail(id)
const response = await dashboardApi.getRequestDetail(id, { includeBodies: false })
if (requestId !== loadDetailRequestId) return
detail.value = response
const previousDetail = detail.value
const prevKey = previousDetail?.request_id || previousDetail?.id
const currKey = response.request_id || response.id
const sameRequest = !!prevKey && prevKey === currKey
detail.value = {
...response,
request_body: sameRequest ? previousDetail?.request_body : undefined,
provider_request_body: sameRequest ? previousDetail?.provider_request_body : undefined,
response_body: sameRequest ? previousDetail?.response_body : undefined,
client_response_body: sameRequest ? previousDetail?.client_response_body : undefined,
}
bodiesLoadedForRequestId.value = sameRequest ? bodiesLoadedForRequestId.value : null
// 首次加载时选择默认 tab
if (!silent) {
const visibleTabNames = visibleTabs.value.map(t => t.name)
if ((detail.value.request_body || detail.value.provider_request_body) && visibleTabNames.includes('request-body')) {
if (hasRequestBodyAvailable.value && visibleTabNames.includes('request-body')) {
activeTab.value = 'request-body'
} else if ((detail.value.response_body || detail.value.client_response_body) && visibleTabNames.includes('response-body')) {
} else if (hasResponseBodyAvailable.value && visibleTabNames.includes('response-body')) {
activeTab.value = 'response-body'
} else if (visibleTabNames.length > 0) {
activeTab.value = visibleTabNames[0]
}
scheduleTimelineMount()
}
// 根据当前 Tab 的数据可用性自动选择默认数据源
@@ -1396,6 +1516,7 @@ onMounted(() => {
onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
stopAutoRefresh()
clearTimelineMountTimer()
loadDetailRequestId += 1
loadDetailInFlight = false
})