fix: Codex 刷新限额功能优化

- 修复并发协程中直接操作 SQLAlchemy Session 的问题,改为收集后统一更新
- 抽取硬编码模型名为 CODEX_QUOTA_REFRESH_MODEL 常量
- 前端刷新限额前添加确认对话框,提示会产生 API 调用费用
- 修正 loguru 日志格式为 {} 占位符风格
This commit is contained in:
fawney19
2026-02-05 18:05:46 +08:00
parent 93090e227f
commit a9b24c9161
2 changed files with 46 additions and 12 deletions

View File

@@ -663,6 +663,7 @@ import Button from '@/components/ui/button.vue'
import Badge from '@/components/ui/badge.vue' import Badge from '@/components/ui/badge.vue'
import Card from '@/components/ui/card.vue' import Card from '@/components/ui/card.vue'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { useClipboard } from '@/composables/useClipboard' import { useClipboard } from '@/composables/useClipboard'
import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer' import { useCountdownTimer, formatCountdown, getOAuthExpiresCountdown } from '@/composables/useCountdownTimer'
import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints' import { getProvider, getProviderEndpoints, updateProvider } from '@/api/endpoints'
@@ -715,6 +716,7 @@ const emit = defineEmits<{
}>() }>()
const { error: showError, success: showSuccess } = useToast() const { error: showError, success: showSuccess } = useToast()
const { confirm } = useConfirm()
const { copyToClipboard } = useClipboard() const { copyToClipboard } = useClipboard()
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer() const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
@@ -1066,6 +1068,16 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
// 刷新 Codex 所有账号限额 // 刷新 Codex 所有账号限额
async function handleRefreshQuota() { async function handleRefreshQuota() {
if (refreshingQuota.value || !props.providerId) return if (refreshingQuota.value || !props.providerId) return
// 确认对话框
const confirmed = await confirm({
title: '获取限额',
message: '这将使用每个账号发送测试请求以获取最新限额信息,会产生少量 API 调用费用。是否继续?',
confirmText: '继续',
variant: 'info'
})
if (!confirmed) return
refreshingQuota.value = true refreshingQuota.value = true
try { try {
const result = await refreshProviderQuota(props.providerId) const result = await refreshProviderQuota(props.providerId)

View File

@@ -869,6 +869,9 @@ class AdminCreateProviderKeyAdapter(AdminApiAdapter):
# ========== Codex Quota Refresh API ========== # ========== Codex Quota Refresh API ==========
# Codex 限额刷新测试请求使用的模型(选择最小/最便宜的模型)
CODEX_QUOTA_REFRESH_MODEL = "gpt-5.1-codex-mini"
@router.post("/providers/{provider_id}/refresh-quota") @router.post("/providers/{provider_id}/refresh-quota")
async def refresh_provider_quota( async def refresh_provider_quota(
@@ -906,7 +909,10 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
import httpx import httpx
from src.api.handlers.base.request_builder import get_provider_auth from src.api.handlers.base.request_builder import get_provider_auth
from src.services.provider.metadata_collectors import MetadataCollectorRegistry, ensure_collectors_registered from src.services.provider.metadata_collectors import (
MetadataCollectorRegistry,
ensure_collectors_registered,
)
from src.services.provider.transport import build_provider_url from src.services.provider.transport import build_provider_url
from src.utils.ssl_utils import get_ssl_context from src.utils.ssl_utils import get_ssl_context
@@ -933,7 +939,13 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
) )
if not keys: if not keys:
return {"success": 0, "failed": 0, "total": 0, "results": [], "message": "没有活跃的 Key"} return {
"success": 0,
"failed": 0,
"total": 0,
"results": [],
"message": "没有活跃的 Key",
}
# 获取 openai:cli 端点 # 获取 openai:cli 端点
endpoint = None endpoint = None
@@ -949,6 +961,9 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
success_count = 0 success_count = 0
failed_count = 0 failed_count = 0
# 用于收集需要更新的 key 元数据(避免在并发任务中直接操作 db session
metadata_updates: dict[str, dict] = {} # key_id -> metadata
# 单个 Key 刷新函数 # 单个 Key 刷新函数
async def refresh_single_key(key: ProviderAPIKey) -> dict: async def refresh_single_key(key: ProviderAPIKey) -> dict:
try: try:
@@ -972,7 +987,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
# 发送最小的测试请求,使用 Codex Responses API 格式 # 发送最小的测试请求,使用 Codex Responses API 格式
test_body = { test_body = {
"model": "gpt-5.1-codex-mini", "model": CODEX_QUOTA_REFRESH_MODEL,
"input": [ "input": [
{ {
"type": "message", "type": "message",
@@ -985,9 +1000,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
"store": False, "store": False,
} }
async with httpx.AsyncClient( async with httpx.AsyncClient(timeout=30.0, verify=get_ssl_context()) as client:
timeout=30.0, verify=get_ssl_context()
) as client:
response = await client.post(url, json=test_body, headers=headers) response = await client.post(url, json=test_body, headers=headers)
# 解析响应头中的限额信息 # 解析响应头中的限额信息
@@ -995,9 +1008,8 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
metadata = MetadataCollectorRegistry.collect("codex", response_headers) metadata = MetadataCollectorRegistry.collect("codex", response_headers)
if metadata: if metadata:
# 更新数据库中的元数据 # 收集元数据,稍后统一更新数据库
key.upstream_metadata = metadata metadata_updates[key.id] = metadata
db.add(key)
return { return {
"key_id": key.id, "key_id": key.id,
"key_name": key.name, "key_name": key.name,
@@ -1015,7 +1027,7 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
} }
except Exception as e: except Exception as e:
logger.error(f"刷新 Key {key.id} 限额失败: {e}") logger.error("刷新 Key {} 限额失败: {}", key.id, e)
return { return {
"key_id": key.id, "key_id": key.id,
"key_name": key.name, "key_name": key.name,
@@ -1038,12 +1050,22 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
else: else:
failed_count += 1 failed_count += 1
# 统一更新数据库(避免在并发任务中操作 session
if metadata_updates:
for key in keys:
if key.id in metadata_updates:
key.upstream_metadata = metadata_updates[key.id]
db.add(key)
# 提交数据库更改 # 提交数据库更改
db.commit() db.commit()
logger.info( logger.info(
f"[QUOTA_REFRESH] Provider {self.provider_id}: " "[QUOTA_REFRESH] Provider {}: 成功 {}/{}, 失败 {}",
f"成功 {success_count}/{len(keys)}, 失败 {failed_count}" self.provider_id,
success_count,
len(keys),
failed_count,
) )
return { return {