mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 共享请求管道、按需懒加载、流式内存护栏与连接池治理
- 抽取 ApiRequestPipeline 单例,44 个路由文件共享同一实例 - Handler/Adapter 模块级 __getattr__ 延迟导入,减少启动时间 - 新增 ensure_stream_buffer_limit() 流式内存护栏(16MB 单行 / 32MB 总量) - HTTP 空闲连接清理与 curl_cffi LRU 会话池 - ensure_providers_bootstrapped 按需引导指定 provider_types - Usage 事件序列化迁移至 msgpack,Redis codec 隔离 - 启动预热任务(/readyz 就绪门控)与优雅关闭 - 通知邮件模块独立开关与 SMTP 配置校验 - CryptoService DCL 线程安全修复 - 通知模块开关 DB 查询 30s 内存缓存 - /readyz 对 unknown 状态返回 503 - 预热关闭 5s 超时保护 - 预热适配器逐个 try-except 容错 - FormatConversionRegistry 哨兵模式防并发重复物化 - 流式缓冲检查无条件执行 Closes #230 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -68,6 +68,77 @@ export function getProbeCountdown(nextProbeAt: string | null | undefined, _tick:
|
||||
return '探测中'
|
||||
}
|
||||
|
||||
/**
|
||||
* Codex 配额重置倒计时状态
|
||||
*/
|
||||
export interface CodexResetStatus {
|
||||
text: string
|
||||
isUrgent: boolean
|
||||
isCritical: boolean
|
||||
isExpired: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Codex 配额重置倒计时
|
||||
* @param resetAt 绝对重置时间(Unix 秒)
|
||||
* @param resetSecs 相对剩余秒数(用于 fallback)
|
||||
* @param updatedAt 元数据更新时间(Unix 秒)
|
||||
* @param _tick 响应式触发器(传入 tick.value 以触发响应式更新)
|
||||
*/
|
||||
export function getCodexResetCountdown(
|
||||
resetAt: number | null | undefined,
|
||||
resetSecs: number | null | undefined,
|
||||
updatedAt: number | null | undefined,
|
||||
_tick: number
|
||||
): CodexResetStatus | null {
|
||||
void _tick
|
||||
|
||||
const nowSec = Math.floor(Date.now() / 1000)
|
||||
let remaining: number
|
||||
|
||||
if (resetAt != null && resetAt > 0) {
|
||||
// 优先绝对时间戳,避免相对秒数快照漂移。
|
||||
remaining = resetAt - nowSec
|
||||
} else if (resetSecs != null && resetSecs >= 0) {
|
||||
if (updatedAt != null && updatedAt > 0) {
|
||||
// 时钟偏移下 updatedAt 可能晚于当前时间,elapsed 需要下限钳制到 0。
|
||||
const elapsedSec = Math.max(nowSec - updatedAt, 0)
|
||||
remaining = resetSecs - elapsedSec
|
||||
} else {
|
||||
remaining = resetSecs
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
if (remaining <= 0) {
|
||||
return { text: '已重置', isUrgent: false, isCritical: false, isExpired: true }
|
||||
}
|
||||
|
||||
const total = Math.floor(remaining)
|
||||
const days = Math.floor(total / 86400)
|
||||
const hours = Math.floor((total % 86400) / 3600)
|
||||
const minutes = Math.floor((total % 3600) / 60)
|
||||
const seconds = total % 60
|
||||
const pad = (n: number) => n.toString().padStart(2, '0')
|
||||
|
||||
let text: string
|
||||
if (days > 0) {
|
||||
text = `${days}天 ${hours}:${pad(minutes)}:${pad(seconds)}`
|
||||
} else if (hours > 0) {
|
||||
text = `${hours}:${pad(minutes)}:${pad(seconds)}`
|
||||
} else {
|
||||
text = `${minutes}:${pad(seconds)}`
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
isUrgent: total < 3600,
|
||||
isCritical: total < 300,
|
||||
isExpired: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Token 状态信息
|
||||
*/
|
||||
|
||||
@@ -573,10 +573,19 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="key.upstream_metadata.codex.primary_reset_seconds"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
v-if="key.upstream_metadata.codex.primary_reset_at || key.upstream_metadata.codex.primary_reset_seconds"
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
key.upstream_metadata.codex.primary_reset_at,
|
||||
key.upstream_metadata.codex.primary_reset_seconds,
|
||||
key.upstream_metadata.codex.updated_at
|
||||
)"
|
||||
>
|
||||
{{ formatResetTime(key.upstream_metadata.codex.primary_reset_seconds) }}后重置
|
||||
{{ getResetCountdownText(
|
||||
key.upstream_metadata.codex.primary_reset_at,
|
||||
key.upstream_metadata.codex.primary_reset_seconds,
|
||||
key.upstream_metadata.codex.updated_at
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 5H限额(仅 Team/Plus/Enterprise 显示) -->
|
||||
@@ -594,9 +603,20 @@
|
||||
:style="{ width: `${Math.max(100 - key.upstream_metadata.codex.secondary_used_percent, 0)}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-[9px] text-muted-foreground/70 mt-0.5">
|
||||
<template v-if="key.upstream_metadata.codex.secondary_reset_seconds">
|
||||
{{ formatResetTime(key.upstream_metadata.codex.secondary_reset_seconds) }}后重置
|
||||
<div
|
||||
class="text-[9px] mt-0.5 tabular-nums"
|
||||
:class="getResetCountdownClass(
|
||||
key.upstream_metadata.codex.secondary_reset_at,
|
||||
key.upstream_metadata.codex.secondary_reset_seconds,
|
||||
key.upstream_metadata.codex.updated_at
|
||||
)"
|
||||
>
|
||||
<template v-if="key.upstream_metadata.codex.secondary_reset_at || key.upstream_metadata.codex.secondary_reset_seconds">
|
||||
{{ getResetCountdownText(
|
||||
key.upstream_metadata.codex.secondary_reset_at,
|
||||
key.upstream_metadata.codex.secondary_reset_seconds,
|
||||
key.upstream_metadata.codex.updated_at
|
||||
) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
已重置
|
||||
@@ -1063,7 +1083,7 @@ import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown, getCodexResetCountdown } from '@/composables/useCountdownTimer'
|
||||
import {
|
||||
getProvider,
|
||||
getProviderEndpoints,
|
||||
@@ -2526,6 +2546,28 @@ function getAntigravityQuotaSummary(metadata: UpstreamMetadata | null | undefine
|
||||
return result
|
||||
}
|
||||
|
||||
function getResetCountdownText(
|
||||
resetAt: number | null | undefined,
|
||||
resetSecs: number | null | undefined,
|
||||
updatedAt: number | null | undefined
|
||||
): string {
|
||||
const status = getCodexResetCountdown(resetAt, resetSecs, updatedAt, countdownTick.value)
|
||||
if (!status) return ''
|
||||
return status.isExpired ? status.text : `${status.text} 后重置`
|
||||
}
|
||||
|
||||
function getResetCountdownClass(
|
||||
resetAt: number | null | undefined,
|
||||
resetSecs: number | null | undefined,
|
||||
updatedAt: number | null | undefined
|
||||
): string {
|
||||
const status = getCodexResetCountdown(resetAt, resetSecs, updatedAt, countdownTick.value)
|
||||
if (!status || status.isExpired) return 'text-muted-foreground/70'
|
||||
if (status.isCritical) return 'text-destructive font-medium animate-pulse'
|
||||
if (status.isUrgent) return 'text-amber-500 dark:text-amber-400'
|
||||
return 'text-muted-foreground/70'
|
||||
}
|
||||
|
||||
// 格式化重置时间
|
||||
function formatResetTime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400)
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
:key="`${key.key_id}-quota-${idx}`"
|
||||
class="w-full"
|
||||
>
|
||||
<div class="h-4 grid grid-cols-[20px_minmax(0,1fr)_42px] items-center gap-1 text-[10px] leading-tight">
|
||||
<div class="min-h-4 grid grid-cols-[20px_minmax(0,1fr)_42px] items-center gap-1 text-[10px] leading-tight">
|
||||
<span
|
||||
class="text-muted-foreground whitespace-nowrap text-right tabular-nums"
|
||||
:title="getQuotaProgressTooltip(item)"
|
||||
@@ -1153,7 +1153,7 @@ import {
|
||||
import RefreshButton from '@/components/ui/refresh-button.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useClipboard } from '@/composables/useClipboard'
|
||||
import { useCountdownTimer, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
|
||||
import { useCountdownTimer, getOAuthExpiresCountdown, getCodexResetCountdown } from '@/composables/useCountdownTimer'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import {
|
||||
@@ -2426,10 +2426,22 @@ function getQuotaProgressLabel(label: string): string {
|
||||
return label
|
||||
}
|
||||
|
||||
function getQuotaProgressCountdown(item: QuotaProgressItem) {
|
||||
if ((item.label !== '5H' && item.label !== '周') || item.resetAtSeconds == null) return null
|
||||
return getCodexResetCountdown(item.resetAtSeconds, null, null, countdownTick.value)
|
||||
}
|
||||
|
||||
function getQuotaProgressCountdownText(item: QuotaProgressItem): string {
|
||||
const status = getQuotaProgressCountdown(item)
|
||||
if (!status) return ''
|
||||
return status.isExpired ? status.text : `${status.text} 后重置`
|
||||
}
|
||||
|
||||
function getQuotaProgressTooltip(item: QuotaProgressItem): string {
|
||||
const detail = item.detail?.trim() || ''
|
||||
if ((item.label === '5H' || item.label === '周') && item.resetAtSeconds != null) {
|
||||
return `${formatQuotaInlineCountdown(item.resetAtSeconds)} 后重置`
|
||||
const countdownText = getQuotaProgressCountdownText(item)
|
||||
if (countdownText) {
|
||||
return countdownText
|
||||
}
|
||||
return detail
|
||||
}
|
||||
@@ -2472,26 +2484,6 @@ function parseQuotaResetRemainingSeconds(detail: string | undefined): number | n
|
||||
return total
|
||||
}
|
||||
|
||||
function formatQuotaInlineCountdown(resetAtSeconds: number): string {
|
||||
// 触发响应式更新,保持倒计时每秒刷新
|
||||
void countdownTick.value
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const remain = Math.max(0, Math.floor(resetAtSeconds - now))
|
||||
const days = Math.floor(remain / 86400)
|
||||
const hours = Math.floor((remain % 86400) / 3600)
|
||||
const minutes = Math.floor((remain % 3600) / 60)
|
||||
const seconds = remain % 60
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}:${String(minutes).padStart(2, '0')}`
|
||||
}
|
||||
return `${minutes}:${String(seconds).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function parseQuotaProgressItems(quotaText: string | null | undefined): QuotaProgressItem[] {
|
||||
if (!quotaText) return []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user