mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(pool,ui,trace): 号池按页配额刷新、配额倒计时、Trace 全量候选与 UI 用语统一
- 号池管理支持按当前页 Key 刷新配额,后端 refresh-quota 接口支持 key_ids 参数筛选 - 配额进度条 tooltip 展示重置倒计时,前端解析后端重置时间并实时倒计时 - Provider 选择器在无号池提供商时禁用,切换/刷新后保持选中状态对齐 - 启停账号后立即更新调度标签并刷新列表 - Trace 监控展示全量候选记录,不再过滤 available/unused 状态 - 请求时间线号池节点与 Provider 节点去重 - 全局 UI 用语统一:已停用/已禁用 -> 停用/禁用 - 导航菜单调整模型管理与号池管理顺序
This commit is contained in:
@@ -8,6 +8,7 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.api.base.admin_adapter import AdminApiAdapter
|
||||
@@ -349,10 +350,15 @@ CODEX_WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"
|
||||
# ========== Kiro Quota Refresh API ==========
|
||||
|
||||
|
||||
class RefreshProviderQuotaRequest(BaseModel):
|
||||
key_ids: list[str] | None = Field(default=None, description="仅刷新指定 Key 列表(可选)")
|
||||
|
||||
|
||||
@router.post("/providers/{provider_id}/refresh-quota")
|
||||
async def refresh_provider_quota(
|
||||
provider_id: str,
|
||||
request: Request,
|
||||
payload: RefreshProviderQuotaRequest | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict:
|
||||
"""
|
||||
@@ -365,13 +371,18 @@ async def refresh_provider_quota(
|
||||
|
||||
**路径参数**:
|
||||
- `provider_id`: Provider ID
|
||||
**请求体**(可选):
|
||||
- `key_ids`: 仅刷新指定 Key 列表,不传时刷新所有活跃 Key
|
||||
|
||||
**返回字段**:
|
||||
- `success`: 成功刷新的 Key 数量
|
||||
- `failed`: 失败的 Key 数量
|
||||
- `results`: 每个 Key 的刷新结果
|
||||
"""
|
||||
adapter = AdminRefreshProviderQuotaAdapter(provider_id=provider_id)
|
||||
adapter = AdminRefreshProviderQuotaAdapter(
|
||||
provider_id=provider_id,
|
||||
key_ids=payload.key_ids if payload else None,
|
||||
)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@@ -380,10 +391,12 @@ class AdminRefreshProviderQuotaAdapter(AdminApiAdapter):
|
||||
"""刷新 Provider 所有 Keys 的限额信息"""
|
||||
|
||||
provider_id: str
|
||||
key_ids: list[str] | None = None
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
return await refresh_provider_quota_for_provider(
|
||||
db=context.db,
|
||||
provider_id=self.provider_id,
|
||||
codex_wham_usage_url=CODEX_WHAM_USAGE_URL,
|
||||
key_ids=self.key_ids,
|
||||
)
|
||||
|
||||
@@ -163,18 +163,17 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
|
||||
# 查询所有候选后,默认只展示已发生调度结果的子集:
|
||||
# - 过滤 available/unused(预创建但未实际参与本次调度)
|
||||
# - 若过滤后为空(例如请求尚未开始),回退到全量,避免前端空白
|
||||
# 查询并展示该请求的全量候选:
|
||||
# - 包含 available/unused(未执行)
|
||||
# - 包含 skipped(跳过)
|
||||
# - 包含 pending/streaming/success/failed/cancelled(执行中/结果)
|
||||
all_candidates = RequestCandidateService.get_candidates_by_request_id(db, self.request_id)
|
||||
|
||||
# 如果没有数据,返回 404
|
||||
if not all_candidates:
|
||||
raise HTTPException(status_code=404, detail="Request not found")
|
||||
|
||||
candidates = [
|
||||
c for c in all_candidates if c.status not in ("available", "unused")
|
||||
] or all_candidates
|
||||
candidates = all_candidates
|
||||
|
||||
# 计算总延迟(只统计已完成的候选:success, failed, cancelled)
|
||||
# 使用显式的 is not None 检查,避免过滤掉 0ms 的快速响应
|
||||
|
||||
@@ -234,6 +234,28 @@ def _format_quota_value(value: float) -> str:
|
||||
return f"{value:.1f}"
|
||||
|
||||
|
||||
def _format_reset_after(seconds_raw: Any) -> str | None:
|
||||
seconds = _to_float(seconds_raw)
|
||||
if seconds is None:
|
||||
return None
|
||||
|
||||
total_seconds = int(seconds)
|
||||
if total_seconds <= 0:
|
||||
return "已重置"
|
||||
|
||||
days = total_seconds // 86400
|
||||
hours = (total_seconds % 86400) // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
|
||||
if days > 0:
|
||||
return f"{days}天{hours}小时后重置"
|
||||
if hours > 0:
|
||||
return f"{hours}小时{minutes}分钟后重置"
|
||||
if minutes > 0:
|
||||
return f"{minutes}分钟后重置"
|
||||
return "即将重置"
|
||||
|
||||
|
||||
def _build_codex_account_quota(upstream_metadata: dict[str, Any]) -> str | None:
|
||||
codex = upstream_metadata.get("codex")
|
||||
if not isinstance(codex, dict):
|
||||
@@ -243,11 +265,19 @@ def _build_codex_account_quota(upstream_metadata: dict[str, Any]) -> str | None:
|
||||
|
||||
primary_used = _to_float(codex.get("primary_used_percent"))
|
||||
if primary_used is not None:
|
||||
parts.append(f"周剩余 {_format_percent(100.0 - primary_used)}")
|
||||
part = f"周剩余 {_format_percent(100.0 - primary_used)}"
|
||||
reset_text = _format_reset_after(codex.get("primary_reset_seconds"))
|
||||
if reset_text:
|
||||
part = f"{part} ({reset_text})"
|
||||
parts.append(part)
|
||||
|
||||
secondary_used = _to_float(codex.get("secondary_used_percent"))
|
||||
if secondary_used is not None:
|
||||
parts.append(f"5H剩余 {_format_percent(100.0 - secondary_used)}")
|
||||
part = f"5H剩余 {_format_percent(100.0 - secondary_used)}"
|
||||
reset_text = _format_reset_after(codex.get("secondary_reset_seconds"))
|
||||
if reset_text:
|
||||
part = f"{part} ({reset_text})"
|
||||
parts.append(part)
|
||||
|
||||
if parts:
|
||||
return " | ".join(parts)
|
||||
|
||||
@@ -70,7 +70,7 @@ class PoolSchedulingDimension(Protocol):
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ManualEnableDimension:
|
||||
code: str = "manual_disabled"
|
||||
label: str = "已禁用"
|
||||
label: str = "禁用"
|
||||
source: str = "manual"
|
||||
weight: int = 8
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ async def refresh_provider_quota_for_provider(
|
||||
db: Session,
|
||||
provider_id: str,
|
||||
codex_wham_usage_url: str,
|
||||
key_ids: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""刷新 Provider 限额信息(惰性导入实现)。"""
|
||||
from src.services.provider_keys.key_quota_service import (
|
||||
@@ -111,4 +112,5 @@ async def refresh_provider_quota_for_provider(
|
||||
db=db,
|
||||
provider_id=provider_id,
|
||||
codex_wham_usage_url=codex_wham_usage_url,
|
||||
key_ids=key_ids,
|
||||
)
|
||||
|
||||
@@ -64,8 +64,9 @@ async def refresh_provider_quota_for_provider(
|
||||
db: Session,
|
||||
provider_id: str,
|
||||
codex_wham_usage_url: str,
|
||||
key_ids: list[str] | None = None,
|
||||
) -> dict:
|
||||
"""刷新指定 Provider 下所有活跃 Key 的限额信息。"""
|
||||
"""刷新指定 Provider 的限额信息(默认所有活跃 Key,可按 key_ids 限定)。"""
|
||||
provider = db.query(Provider).filter(Provider.id == provider_id).first()
|
||||
if not provider:
|
||||
raise NotFoundException(f"Provider {provider_id} 不存在")
|
||||
@@ -74,21 +75,42 @@ async def refresh_provider_quota_for_provider(
|
||||
if provider_type not in {ProviderType.CODEX, ProviderType.ANTIGRAVITY, ProviderType.KIRO}:
|
||||
raise InvalidRequestException("仅支持 Codex / Antigravity / Kiro 类型的 Provider 刷新限额")
|
||||
|
||||
keys = (
|
||||
db.query(ProviderAPIKey)
|
||||
.filter(
|
||||
ProviderAPIKey.provider_id == provider_id,
|
||||
ProviderAPIKey.is_active.is_(True),
|
||||
)
|
||||
.all()
|
||||
selected_key_ids: list[str] | None = None
|
||||
if key_ids is not None:
|
||||
deduped: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for raw in key_ids:
|
||||
value = str(raw).strip()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
deduped.append(value)
|
||||
selected_key_ids = deduped
|
||||
|
||||
keys_query = db.query(ProviderAPIKey).filter(
|
||||
ProviderAPIKey.provider_id == provider_id,
|
||||
)
|
||||
if selected_key_ids is None:
|
||||
keys_query = keys_query.filter(ProviderAPIKey.is_active.is_(True))
|
||||
else:
|
||||
if not selected_key_ids:
|
||||
return {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": 0,
|
||||
"results": [],
|
||||
"message": "未提供可刷新的 Key",
|
||||
}
|
||||
keys_query = keys_query.filter(ProviderAPIKey.id.in_(selected_key_ids))
|
||||
|
||||
keys = keys_query.all()
|
||||
if not keys:
|
||||
return {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": 0,
|
||||
"results": [],
|
||||
"message": "没有活跃的 Key",
|
||||
"message": "没有可刷新的 Key",
|
||||
}
|
||||
|
||||
endpoint = _select_refresh_endpoint(provider, provider_type)
|
||||
|
||||
Reference in New Issue
Block a user