mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(codex): 增强 OAuth 导入解析与账号信息提取,优化维护调度器线程模型
Codex OAuth: - 导入解析支持附加账号字段(account_id/plan_type/user_id/email) - enrich_codex 扩展从 access_token 和直接字段提取账号信息 - parse_codex_id_token 支持 JWT/JSON 字符串/dict 三种输入格式 - request patching 新增 openai:compact 格式支持 - codex_usage_parser 新增 credits_unlimited 字段解析 维护调度器: - 同步 DB 操作迁移到线程池执行,避免阻塞事件循环 - 新增 request_candidates 定期清理任务 - 新增每周 VACUUM ANALYZE 数据库表维护任务 - 新增 enable_db_maintenance 配置项 前端: - ElapsedTimeText 从 setInterval 改为 requestAnimationFrame - 时间线过滤 available/unused 占位记录 - Usage 页面默认关闭全局自动刷新 - 移除号池管理中的配额更新时间显示
This commit is contained in:
@@ -10,20 +10,18 @@ const props = withDefaults(defineProps<{
|
||||
status?: string | null
|
||||
responseTimeMs?: number | null
|
||||
precision?: number
|
||||
intervalMs?: number
|
||||
}>(), {
|
||||
createdAt: null,
|
||||
status: null,
|
||||
responseTimeMs: null,
|
||||
precision: 2,
|
||||
intervalMs: 200
|
||||
})
|
||||
|
||||
const now = ref(Date.now())
|
||||
const precision = computed(() => Math.max(0, props.precision))
|
||||
const isActive = computed(() => props.status === 'pending' || props.status === 'streaming')
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null
|
||||
let rafId: number | null = null
|
||||
|
||||
function parseCreatedAtMs(value: string | null | undefined): number {
|
||||
if (!value) return Number.NaN
|
||||
@@ -32,31 +30,33 @@ function parseCreatedAtMs(value: string | null | undefined): number {
|
||||
return new Date(normalized).getTime()
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
if (!timer) return
|
||||
clearInterval(timer)
|
||||
timer = null
|
||||
function stopRaf() {
|
||||
if (rafId == null) return
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
stopTimer()
|
||||
const intervalMs = Math.max(100, props.intervalMs)
|
||||
timer = setInterval(() => {
|
||||
now.value = Date.now()
|
||||
}, intervalMs)
|
||||
function tick() {
|
||||
now.value = Date.now()
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
watch([isActive, () => props.intervalMs], ([active]) => {
|
||||
function startRaf() {
|
||||
stopRaf()
|
||||
now.value = Date.now()
|
||||
rafId = requestAnimationFrame(tick)
|
||||
}
|
||||
|
||||
watch(isActive, (active) => {
|
||||
if (active) {
|
||||
now.value = Date.now()
|
||||
startTimer()
|
||||
startRaf()
|
||||
} else {
|
||||
stopTimer()
|
||||
stopRaf()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer()
|
||||
stopRaf()
|
||||
})
|
||||
|
||||
const displayText = computed(() => {
|
||||
|
||||
@@ -642,6 +642,10 @@ const normalizeTimelineStatus = (value: unknown): CandidateRecord['status'] => {
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
const isPlannedOnlyStatus = (status: CandidateRecord['status']): boolean => {
|
||||
return status === 'available' || status === 'unused'
|
||||
}
|
||||
|
||||
// 候选时间线(按实际执行顺序排序)
|
||||
const rawTimeline = computed<CandidateRecord[]>(() => {
|
||||
if (!trace.value) return []
|
||||
@@ -658,6 +662,11 @@ const rawTimeline = computed<CandidateRecord[]>(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 仅保留“已进入执行链路”的节点,过滤预创建但未执行的 available/unused 占位记录。
|
||||
const executableTimeline = computed<CandidateRecord[]>(() => {
|
||||
return rawTimeline.value.filter(candidate => !isPlannedOnlyStatus(candidate.status))
|
||||
})
|
||||
|
||||
const schedulingAudit = computed<Record<string, unknown> | null>(() => {
|
||||
const metadata = props.requestMetadata
|
||||
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) return null
|
||||
@@ -677,7 +686,7 @@ const extractPoolGroupId = (candidate: CandidateRecord): string | null => {
|
||||
|
||||
const poolAttemptCandidates = computed<CandidateRecord[]>(() => {
|
||||
// 新链路:优先使用后端写入的 extra_data.pool_group_id。
|
||||
const fromTrace = rawTimeline.value.filter((candidate) => extractPoolGroupId(candidate) !== null)
|
||||
const fromTrace = executableTimeline.value.filter((candidate) => extractPoolGroupId(candidate) !== null)
|
||||
if (fromTrace.length > 0) {
|
||||
return fromTrace
|
||||
}
|
||||
@@ -765,8 +774,8 @@ const poolAttemptKeySet = computed<Set<string>>(() => {
|
||||
})
|
||||
|
||||
const timeline = computed<CandidateRecord[]>(() => {
|
||||
if (poolAttemptCandidates.value.length === 0) return rawTimeline.value
|
||||
return rawTimeline.value.filter(
|
||||
if (poolAttemptCandidates.value.length === 0) return executableTimeline.value
|
||||
return executableTimeline.value.filter(
|
||||
(candidate) => !poolAttemptKeySet.value.has(makeAttemptKey(candidate.candidate_index, candidate.retry_index)),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -373,12 +373,6 @@
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<span
|
||||
v-if="formatQuotaUpdatedAt(key)"
|
||||
class="text-[10px] text-muted-foreground/70 whitespace-nowrap"
|
||||
>
|
||||
{{ formatQuotaUpdatedAt(key) }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="key.oauth_plan_type"
|
||||
variant="outline"
|
||||
@@ -767,12 +761,6 @@
|
||||
/>
|
||||
</Button>
|
||||
</template>
|
||||
<span
|
||||
v-if="formatQuotaUpdatedAt(key)"
|
||||
class="text-[10px] text-muted-foreground/70 truncate"
|
||||
>
|
||||
{{ formatQuotaUpdatedAt(key) }}
|
||||
</span>
|
||||
<Badge
|
||||
v-if="key.oauth_plan_type"
|
||||
variant="outline"
|
||||
@@ -2078,27 +2066,6 @@ function getOAuthStatusTitle(key: PoolKeyDetail): string {
|
||||
return `Token 剩余有效期: ${status.text}`
|
||||
}
|
||||
|
||||
function formatUpdatedAt(updatedAtSeconds: number | null | undefined): string {
|
||||
if (!updatedAtSeconds) return ''
|
||||
|
||||
// 触发响应式更新,保持“xx分钟前更新”实时变化
|
||||
void countdownTick.value
|
||||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diff = now - updatedAtSeconds
|
||||
if (diff <= 60) return '刚刚更新'
|
||||
const minutes = Math.floor(diff / 60)
|
||||
if (minutes < 60) return `${minutes}分钟前更新`
|
||||
const hours = Math.floor(minutes / 60)
|
||||
if (hours < 24) return `${hours}小时前更新`
|
||||
const days = Math.floor(hours / 24)
|
||||
return `${days}天前更新`
|
||||
}
|
||||
|
||||
function formatQuotaUpdatedAt(key: PoolKeyDetail): string {
|
||||
return formatUpdatedAt(key.quota_updated_at)
|
||||
}
|
||||
|
||||
function normalizeQuotaLabel(label: string): string {
|
||||
const normalized = label.trim()
|
||||
if (!normalized) return '额度'
|
||||
|
||||
@@ -164,7 +164,8 @@
|
||||
<p>2. <strong>压缩日志阶段</strong>: body 字段被压缩存储,节省空间</p>
|
||||
<p>3. <strong>统计阶段</strong>: 仅保留 tokens、成本等统计信息</p>
|
||||
<p>4. <strong>归档删除</strong>: 超过保留期限后完全删除记录</p>
|
||||
<p>5. <strong>审计日志</strong>: 独立清理,记录用户登录、操作等安全事件</p>
|
||||
<p>5. <strong>候选记录</strong>: 与详细记录同步清理请求候选记录(request_candidates)</p>
|
||||
<p>6. <strong>审计日志</strong>: 独立清理,记录用户登录、操作等安全事件</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardSection>
|
||||
|
||||
@@ -250,7 +250,7 @@ let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
let refreshInFlight: Promise<void> | null = null
|
||||
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次(用于活跃请求)
|
||||
const GLOBAL_AUTO_REFRESH_INTERVAL = 3000 // 3秒刷新一次(全局自动刷新)
|
||||
const globalAutoRefresh = ref(true) // 全局自动刷新开关(默认开启)
|
||||
const globalAutoRefresh = ref(false) // 全局自动刷新开关(默认关闭)
|
||||
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
|
||||
|
||||
// 轮询活跃请求状态(轻量级,只更新状态变化的记录)
|
||||
|
||||
Reference in New Issue
Block a user