mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: Antigravity 端点签名迁移至 gemini:chat,流式 usage 提取重构与请求详情自动刷新
- Antigravity 端点签名从 gemini:cli 统一为 gemini:chat,保留向后兼容,含 DB 迁移 - 流式处理中 usage/completion/text 提取抽离为 _update_ctx_from_provider_event, 支持 envelope 解包后提取,避免格式转换流程中重复计数 - 新增 has_format_conversion 属性,区分真正的格式转换与 envelope rewrite, 修正 usage 展示层的格式转换标记 - 请求详情抽屉对未完成请求支持自动轮询刷新,关闭时自动停止 - 按次计费样式调整;实际成本仅在倍率非 1.0 时显示;缓存日志降级为 trace
This commit is contained in:
@@ -161,6 +161,7 @@ export interface RequestDetail {
|
||||
request_type: string
|
||||
is_stream: boolean
|
||||
status_code: number
|
||||
status?: string // pending, streaming, completed, failed, cancelled
|
||||
error_message?: string
|
||||
response_time_ms: number
|
||||
created_at: string
|
||||
|
||||
@@ -184,7 +184,7 @@ async function handleTestModel(modelName: string) {
|
||||
provider_id: props.providerId,
|
||||
model_name: modelName,
|
||||
api_key_id: props.keyId,
|
||||
api_format: 'gemini:cli',
|
||||
api_format: 'gemini:chat',
|
||||
message: 'hello',
|
||||
})
|
||||
|
||||
|
||||
@@ -642,19 +642,28 @@ const navigateGroup = (direction: number) => {
|
||||
}
|
||||
|
||||
// 加载请求追踪数据
|
||||
const loadTrace = async () => {
|
||||
const isSilentRefresh = ref(false)
|
||||
const loadTrace = async (silent = false) => {
|
||||
if (!props.requestId) return
|
||||
|
||||
loading.value = true
|
||||
isSilentRefresh.value = silent
|
||||
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
}
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
trace.value = await requestTraceApi.getRequestTrace(props.requestId)
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
if (!silent) {
|
||||
error.value = err.response?.data?.detail || err.message || '加载失败'
|
||||
}
|
||||
log.error('加载请求追踪失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (!silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,6 +671,12 @@ const loadTrace = async () => {
|
||||
watch(groupedTimeline, (newGroups) => {
|
||||
if (!newGroups || newGroups.length === 0) return
|
||||
|
||||
// 静默刷新时不重置选中状态
|
||||
if (isSilentRefresh.value) {
|
||||
isSilentRefresh.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 查找成功的组
|
||||
const successIdx = newGroups.findIndex(g => g.primaryStatus === 'success')
|
||||
if (successIdx >= 0) {
|
||||
@@ -712,6 +727,8 @@ watch(() => props.requestId, () => {
|
||||
loadTrace()
|
||||
}, { immediate: true })
|
||||
|
||||
defineExpose({ refresh: () => loadTrace(true) })
|
||||
|
||||
// 格式化时间(详细)
|
||||
const formatTime = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
|
||||
@@ -66,13 +66,13 @@
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
:disabled="loading"
|
||||
title="刷新"
|
||||
:disabled="loading && !autoRefreshing"
|
||||
:title="autoRefreshing ? '停止自动刷新' : '刷新'"
|
||||
@click="refreshDetail"
|
||||
>
|
||||
<RefreshCw
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
:class="{ 'animate-spin': loading || autoRefreshing, 'text-primary': autoRefreshing }"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -275,25 +275,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== 3. 按次计费(独立隔离) ========== -->
|
||||
<!-- ========== 3. 按次计费 ========== -->
|
||||
<div
|
||||
v-if="perRequestCost > 0 && !detail.video_billing"
|
||||
class="rounded-lg p-3 bg-amber-500/5 border border-amber-500/30 mb-3"
|
||||
class="space-y-2 mb-3"
|
||||
>
|
||||
<div class="flex items-center justify-between text-xs mb-2">
|
||||
<span class="font-medium text-amber-600 dark:text-amber-400">按次计费</span>
|
||||
<span
|
||||
v-if="detail.price_per_request"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
单价 ${{ detail.price_per_request.toFixed(6) }}/次
|
||||
</span>
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="font-medium text-foreground">按次计费</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">请求次数</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">1</span>
|
||||
<span class="text-xs font-mono font-medium">${{ perRequestCost.toFixed(6) }}</span>
|
||||
<div class="rounded-lg p-3 bg-primary/5 border border-primary/30 space-y-2">
|
||||
<div
|
||||
v-if="detail.price_per_request"
|
||||
class="flex items-center justify-end text-xs"
|
||||
>
|
||||
<span class="text-muted-foreground">${{ detail.price_per_request.toFixed(6) }}/次</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">请求次数</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">1</span>
|
||||
<span class="text-xs font-mono font-medium">${{ perRequestCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -361,6 +363,7 @@
|
||||
<!-- 请求链路追踪卡片 -->
|
||||
<div v-if="detail.request_id || detail.id">
|
||||
<HorizontalRequestTimeline
|
||||
ref="timelineRef"
|
||||
:request-id="detail.request_id || detail.id"
|
||||
:override-status-code="detail.status_code"
|
||||
/>
|
||||
@@ -590,7 +593,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { ref, watch, computed, onBeforeUnmount } from 'vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useEscapeKey } from '@/composables/useEscapeKey'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
@@ -631,6 +634,7 @@ const emit = defineEmits<{
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const detail = ref<RequestDetail | null>(null)
|
||||
const timelineRef = ref<InstanceType<typeof HorizontalRequestTimeline> | null>(null)
|
||||
const activeTab = ref('request-body')
|
||||
const copiedStates = ref<Record<string, boolean>>({})
|
||||
const viewMode = ref<'compare' | 'formatted' | 'raw'>('formatted')
|
||||
@@ -645,6 +649,8 @@ const historicalPricing = ref<{
|
||||
cache_read_price: string
|
||||
request_price: string
|
||||
} | null>(null)
|
||||
const autoRefreshTimer = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const autoRefreshing = ref(false)
|
||||
|
||||
// 监听标签页切换
|
||||
watch(activeTab, (newTab) => {
|
||||
@@ -870,26 +876,38 @@ watch(() => props.requestId, async (newId) => {
|
||||
watch(() => props.isOpen, async (isOpen) => {
|
||||
if (isOpen && props.requestId) {
|
||||
await loadDetail(props.requestId)
|
||||
} else if (!isOpen) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadDetail(id: string) {
|
||||
loading.value = true
|
||||
async function loadDetail(id: string, silent = false) {
|
||||
if (!silent) {
|
||||
loading.value = true
|
||||
historicalPricing.value = null
|
||||
}
|
||||
error.value = null
|
||||
historicalPricing.value = null
|
||||
try {
|
||||
detail.value = await dashboardApi.getRequestDetail(id)
|
||||
|
||||
// 默认显示有内容的第一个可见 tab
|
||||
const visibleTabNames = visibleTabs.value.map(t => t.name)
|
||||
if (detail.value.request_body && visibleTabNames.includes('request-body')) {
|
||||
activeTab.value = 'request-body'
|
||||
} else if (detail.value.response_body && visibleTabNames.includes('response-body')) {
|
||||
activeTab.value = 'response-body'
|
||||
} else if (visibleTabNames.length > 0) {
|
||||
activeTab.value = visibleTabNames[0]
|
||||
// 首次加载时选择默认 tab
|
||||
if (!silent) {
|
||||
const visibleTabNames = visibleTabs.value.map(t => t.name)
|
||||
if (detail.value.request_body && visibleTabNames.includes('request-body')) {
|
||||
activeTab.value = 'request-body'
|
||||
} else if (detail.value.response_body && visibleTabNames.includes('response-body')) {
|
||||
activeTab.value = 'response-body'
|
||||
} else if (visibleTabNames.length > 0) {
|
||||
activeTab.value = visibleTabNames[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 根据数据可用性自动选择请求头数据源
|
||||
// provider_request_headers 在 streaming 完成后才写入,pending/streaming 期间为空
|
||||
const hasProviderReqHeaders = detail.value.provider_request_headers &&
|
||||
Object.keys(detail.value.provider_request_headers).length > 0
|
||||
dataSource.value = hasProviderReqHeaders ? 'provider' : 'client'
|
||||
|
||||
// 使用请求记录中保存的历史价格
|
||||
if (detail.value.input_price_per_1m || detail.value.output_price_per_1m || detail.value.price_per_request) {
|
||||
historicalPricing.value = {
|
||||
@@ -900,24 +918,81 @@ async function loadDetail(id: string) {
|
||||
request_price: detail.value.price_per_request ? detail.value.price_per_request.toFixed(4) : 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
// 静默刷新时同步刷新链路追踪
|
||||
if (silent) {
|
||||
timelineRef.value?.refresh()
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Failed to load request detail:', err)
|
||||
error.value = '加载请求详情失败'
|
||||
if (!silent) {
|
||||
error.value = '加载请求详情失败'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (!silent) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
stopAutoRefresh()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (props.requestId) {
|
||||
await loadDetail(props.requestId)
|
||||
}
|
||||
function isRequestCompleted(): boolean {
|
||||
if (!detail.value?.status) return true
|
||||
return !['pending', 'streaming'].includes(detail.value.status)
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (autoRefreshTimer.value) {
|
||||
clearInterval(autoRefreshTimer.value)
|
||||
autoRefreshTimer.value = null
|
||||
}
|
||||
autoRefreshing.value = false
|
||||
}
|
||||
|
||||
async function refreshDetail() {
|
||||
if (!props.requestId) return
|
||||
|
||||
// 已完成:单次静默刷新
|
||||
if (isRequestCompleted()) {
|
||||
await loadDetail(props.requestId, true)
|
||||
return
|
||||
}
|
||||
|
||||
// 未完成:如果已在自动刷新则停止,否则启动
|
||||
if (autoRefreshing.value) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshing.value = true
|
||||
await loadDetail(props.requestId, true)
|
||||
|
||||
// 加载后可能已经完成了
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
|
||||
autoRefreshTimer.value = setInterval(async () => {
|
||||
if (!props.requestId || !props.isOpen) {
|
||||
stopAutoRefresh()
|
||||
return
|
||||
}
|
||||
await loadDetail(props.requestId, true)
|
||||
if (isRequestCompleted()) {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
|
||||
function formatDateTime(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return 'N/A'
|
||||
const date = new Date(dateStr)
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
<div class="flex flex-col items-end flex-shrink-0">
|
||||
<span class="text-xs text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined"
|
||||
v-if="showActualCost && record.actual_cost !== undefined && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-[10px] text-muted-foreground"
|
||||
>{{ formatCurrency(record.actual_cost) }}</span>
|
||||
</div>
|
||||
@@ -562,7 +562,7 @@
|
||||
<div class="flex flex-col items-end text-xs gap-0.5">
|
||||
<span class="text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
|
||||
<span
|
||||
v-if="showActualCost && record.actual_cost !== undefined"
|
||||
v-if="showActualCost && record.actual_cost !== undefined && record.rate_multiplier && record.rate_multiplier !== 1.0"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ formatCurrency(record.actual_cost) }}
|
||||
|
||||
Reference in New Issue
Block a user