mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Merge pull request #410 from Entropy-Xu/codex/codex-image-progress-heartbeat
Add Codex image progress heartbeat
This commit is contained in:
@@ -63,6 +63,7 @@
|
||||
:password-policy-level="systemConfig.password_policy_level"
|
||||
:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys"
|
||||
:enable-format-conversion="systemConfig.enable_format_conversion"
|
||||
:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat"
|
||||
:loading="basicConfigLoading"
|
||||
:has-changes="hasBasicConfigChanges"
|
||||
@save="saveBasicConfig"
|
||||
@@ -72,6 +73,7 @@
|
||||
@update:password-policy-level="systemConfig.password_policy_level = $event"
|
||||
@update:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys = $event"
|
||||
@update:enable-format-conversion="systemConfig.enable_format_conversion = $event"
|
||||
@update:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat = $event"
|
||||
/>
|
||||
|
||||
<!-- 请求记录配置 -->
|
||||
|
||||
@@ -150,6 +150,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center h-full">
|
||||
<div class="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="enable-openai-image-sync-heartbeat"
|
||||
:checked="enableOpenaiImageSyncHeartbeat"
|
||||
@update:checked="$emit('update:enableOpenaiImageSyncHeartbeat', $event)"
|
||||
/>
|
||||
<div>
|
||||
<Label
|
||||
for="enable-openai-image-sync-heartbeat"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
同步生图心跳
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
开启后同步生图外层 HTTP 状态固定为 200,上游失败需读取响应体 error.upstream_status
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
</template>
|
||||
@@ -173,6 +194,7 @@ defineProps<{
|
||||
passwordPolicyLevel: string
|
||||
autoDeleteExpiredKeys: boolean
|
||||
enableFormatConversion: boolean
|
||||
enableOpenaiImageSyncHeartbeat: boolean
|
||||
loading: boolean
|
||||
hasChanges: boolean
|
||||
}>()
|
||||
@@ -185,5 +207,6 @@ defineEmits<{
|
||||
'update:passwordPolicyLevel': [value: string]
|
||||
'update:autoDeleteExpiredKeys': [value: boolean]
|
||||
'update:enableFormatConversion': [value: boolean]
|
||||
'update:enableOpenaiImageSyncHeartbeat': [value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface SystemConfig {
|
||||
auto_delete_expired_keys: boolean
|
||||
// 格式转换
|
||||
enable_format_conversion: boolean
|
||||
// 同步生图心跳
|
||||
enable_openai_image_sync_heartbeat: boolean
|
||||
// 请求记录
|
||||
request_record_level: string
|
||||
max_request_body_size: number
|
||||
@@ -58,6 +60,8 @@ const CONFIG_KEYS = [
|
||||
'auto_delete_expired_keys',
|
||||
// 格式转换
|
||||
'enable_format_conversion',
|
||||
// 同步生图心跳
|
||||
'enable_openai_image_sync_heartbeat',
|
||||
// 请求记录
|
||||
'request_record_level',
|
||||
'max_request_body_size',
|
||||
@@ -98,6 +102,8 @@ function createDefaultConfig(): SystemConfig {
|
||||
auto_delete_expired_keys: false,
|
||||
// 格式转换
|
||||
enable_format_conversion: false,
|
||||
// 同步生图心跳
|
||||
enable_openai_image_sync_heartbeat: false,
|
||||
// 请求记录
|
||||
request_record_level: 'basic',
|
||||
max_request_body_size: 1048576,
|
||||
@@ -160,7 +166,8 @@ export function useSystemConfig() {
|
||||
systemConfig.value.enable_registration !== originalConfig.value.enable_registration ||
|
||||
systemConfig.value.password_policy_level !== originalConfig.value.password_policy_level ||
|
||||
systemConfig.value.auto_delete_expired_keys !== originalConfig.value.auto_delete_expired_keys ||
|
||||
systemConfig.value.enable_format_conversion !== originalConfig.value.enable_format_conversion
|
||||
systemConfig.value.enable_format_conversion !== originalConfig.value.enable_format_conversion ||
|
||||
systemConfig.value.enable_openai_image_sync_heartbeat !== originalConfig.value.enable_openai_image_sync_heartbeat
|
||||
)
|
||||
})
|
||||
|
||||
@@ -340,6 +347,11 @@ export function useSystemConfig() {
|
||||
value: systemConfig.value.enable_format_conversion,
|
||||
description: '全局格式转换开关:开启时强制允许所有提供商的格式转换',
|
||||
},
|
||||
{
|
||||
key: 'enable_openai_image_sync_heartbeat',
|
||||
value: systemConfig.value.enable_openai_image_sync_heartbeat,
|
||||
description: '同步生图心跳开关:开启后外层 HTTP 状态固定为 200,上游失败写入响应体',
|
||||
},
|
||||
]
|
||||
|
||||
await Promise.all(
|
||||
@@ -356,6 +368,8 @@ export function useSystemConfig() {
|
||||
systemConfig.value.auto_delete_expired_keys
|
||||
originalConfig.value.enable_format_conversion =
|
||||
systemConfig.value.enable_format_conversion
|
||||
originalConfig.value.enable_openai_image_sync_heartbeat =
|
||||
systemConfig.value.enable_openai_image_sync_heartbeat
|
||||
}
|
||||
success('基础配置已保存')
|
||||
} catch (err) {
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
:is-open="detailModalOpen"
|
||||
:request-id="selectedRequestId"
|
||||
@close="detailModalOpen = false"
|
||||
@request-state="handleDetailRequestState"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -129,6 +130,7 @@ import { useRoute } from 'vue-router'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usageApi } from '@/api/usage'
|
||||
import type { ImageProgress } from '@/api/requestTrace'
|
||||
import { usersApi } from '@/api/users'
|
||||
import { meApi } from '@/api/me'
|
||||
import { dashboardApi } from '@/api/dashboard'
|
||||
@@ -153,7 +155,7 @@ import {
|
||||
isUsageUpstreamStream,
|
||||
resolveDisplayRequestStatus,
|
||||
} from '@/features/usage/utils/status'
|
||||
import type { DateRangeParams, FilterStatusValue } from '@/features/usage/types'
|
||||
import type { DateRangeParams, FilterStatusValue, RequestStatus } from '@/features/usage/types'
|
||||
import type { UserOption } from '@/features/usage/components/UsageRecordsTable.vue'
|
||||
import { log } from '@/utils/logger'
|
||||
import type { ActivityHeatmap } from '@/types/activity'
|
||||
@@ -445,12 +447,16 @@ async function pollActiveRequests() {
|
||||
const shouldApply = newRank >= currentRank
|
||||
const updateHasFailureSignal =
|
||||
(typeof update.status_code === 'number' && update.status_code >= 400) ||
|
||||
(typeof update.error_message === 'string' && update.error_message.trim().length > 0)
|
||||
(typeof update.error_message === 'string' && update.error_message.trim().length > 0) ||
|
||||
update.image_progress?.phase === 'failed'
|
||||
const shouldApplyData = shouldApply || updateHasFailureSignal
|
||||
|
||||
if (shouldApply && record.status !== update.status) {
|
||||
record.status = update.status
|
||||
}
|
||||
if ('image_progress' in update) {
|
||||
record.image_progress = update.image_progress ?? null
|
||||
}
|
||||
if (shouldApplyData) {
|
||||
// 进行中状态也需要持续更新(provider/key/TTFB 可能在 streaming 后才落库)
|
||||
record.input_tokens = update.input_tokens
|
||||
@@ -877,6 +883,63 @@ function showRequestDetail(id: string) {
|
||||
detailModalOpen.value = true
|
||||
}
|
||||
|
||||
function sameImageProgress(left?: ImageProgress | null, right?: ImageProgress | null): boolean {
|
||||
if (!left && !right) return true
|
||||
if (!left || !right) return false
|
||||
return left.phase === right.phase &&
|
||||
left.upstream_ttfb_ms === right.upstream_ttfb_ms &&
|
||||
left.upstream_sse_frame_count === right.upstream_sse_frame_count &&
|
||||
left.last_upstream_event === right.last_upstream_event &&
|
||||
left.last_upstream_frame_at_unix_ms === right.last_upstream_frame_at_unix_ms &&
|
||||
left.partial_image_count === right.partial_image_count &&
|
||||
left.last_client_visible_event === right.last_client_visible_event &&
|
||||
left.downstream_heartbeat_count === right.downstream_heartbeat_count &&
|
||||
left.last_downstream_heartbeat_at_unix_ms === right.last_downstream_heartbeat_at_unix_ms &&
|
||||
left.downstream_heartbeat_interval_ms === right.downstream_heartbeat_interval_ms
|
||||
}
|
||||
|
||||
function handleDetailRequestState(update: {
|
||||
id: string
|
||||
status?: RequestStatus
|
||||
statusCode?: number | null
|
||||
responseTimeMs?: number | null
|
||||
imageProgress?: ImageProgress | null
|
||||
errorMessage?: string | null
|
||||
}) {
|
||||
const record = currentRecords.value.find(record => record.id === update.id)
|
||||
if (!record) return
|
||||
|
||||
const statusPriority: Record<RequestStatus, number> = {
|
||||
pending: 0,
|
||||
streaming: 1,
|
||||
completed: 2,
|
||||
failed: 2,
|
||||
cancelled: 2,
|
||||
}
|
||||
if (update.status) {
|
||||
const currentRank = record.status ? statusPriority[record.status] : 0
|
||||
const nextRank = statusPriority[update.status]
|
||||
if (nextRank >= currentRank) {
|
||||
record.status = update.status
|
||||
}
|
||||
}
|
||||
if ('statusCode' in update) {
|
||||
record.status_code = update.statusCode ?? undefined
|
||||
}
|
||||
if ('responseTimeMs' in update && update.responseTimeMs != null) {
|
||||
record.response_time_ms = update.responseTimeMs
|
||||
}
|
||||
if ('imageProgress' in update) {
|
||||
const nextProgress = update.imageProgress ?? null
|
||||
if (!sameImageProgress(record.image_progress, nextProgress)) {
|
||||
record.image_progress = nextProgress
|
||||
}
|
||||
}
|
||||
if ('errorMessage' in update) {
|
||||
record.error_message = update.errorMessage ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
function prefetchRequestDetail(id: string) {
|
||||
if (!isAdminPage.value) return
|
||||
void dashboardApi.prefetchRequestDetail(id).catch(error => {
|
||||
|
||||
Reference in New Issue
Block a user