mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(pool,trace): 账号封禁原因细分与请求追踪 attempted_only 过滤
- 将账号封禁原因从笼统的"账号异常"细分为封禁/停用/需要验证三类, 前后端关键词组同步拆分,号池管理页面展示对应分类标签 - trace API 新增 attempted_only 参数,支持仅返回实际尝试过的候选, 前端时间线组件默认启用过滤,排除 available/unused/skipped 记录
This commit is contained in:
@@ -75,6 +75,7 @@ class RequestTraceResponse(BaseModel):
|
||||
async def get_request_trace(
|
||||
request_id: str,
|
||||
request: Request,
|
||||
attempted_only: bool = Query(False, description="仅返回实际尝试过的候选"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> Any:
|
||||
"""
|
||||
@@ -119,7 +120,7 @@ async def get_request_trace(
|
||||
- `finished_at`: 完成时间
|
||||
"""
|
||||
|
||||
adapter = AdminGetRequestTraceAdapter(request_id=request_id)
|
||||
adapter = AdminGetRequestTraceAdapter(request_id=request_id, attempted_only=attempted_only)
|
||||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||||
|
||||
|
||||
@@ -159,6 +160,18 @@ async def get_provider_failure_rate(
|
||||
@dataclass
|
||||
class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
||||
request_id: str
|
||||
attempted_only: bool = False
|
||||
|
||||
@staticmethod
|
||||
def _is_attempted_candidate(candidate: Any) -> bool:
|
||||
status = str(getattr(candidate, "status", "") or "").strip().lower()
|
||||
# pre-created / never executed rows should not be considered as attempted
|
||||
if status in {"", "available", "unused", "skipped"}:
|
||||
return False
|
||||
# pending must have started_at to be considered truly entered execution
|
||||
if status == "pending" and getattr(candidate, "started_at", None) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||||
db = context.db
|
||||
@@ -173,7 +186,11 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
||||
if not all_candidates:
|
||||
raise HTTPException(status_code=404, detail="Request not found")
|
||||
|
||||
candidates = all_candidates
|
||||
candidates = (
|
||||
[c for c in all_candidates if self._is_attempted_candidate(c)]
|
||||
if self.attempted_only
|
||||
else all_candidates
|
||||
)
|
||||
|
||||
# 计算总延迟(只统计已完成的候选:success, failed, cancelled)
|
||||
# 使用显式的 is not None 检查,避免过滤掉 0ms 的快速响应
|
||||
@@ -191,14 +208,15 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
||||
# 3. status="streaming" 表示流式请求正在进行中
|
||||
# 4. status="pending" 表示请求尚未开始执行
|
||||
# 5. status="cancelled" 表示客户端主动断开连接(不算失败)
|
||||
final_status_source = all_candidates if self.attempted_only else candidates
|
||||
has_success = any(
|
||||
c.status == "success" or (c.status_code is not None and 200 <= c.status_code < 300)
|
||||
for c in candidates
|
||||
for c in final_status_source
|
||||
)
|
||||
has_streaming = any(c.status == "streaming" for c in candidates)
|
||||
has_pending = any(c.status == "pending" for c in candidates)
|
||||
has_cancelled = any(c.status == "cancelled" for c in candidates)
|
||||
has_failed = any(c.status == "failed" for c in candidates)
|
||||
has_streaming = any(c.status == "streaming" for c in final_status_source)
|
||||
has_pending = any(c.status == "pending" for c in final_status_source)
|
||||
has_cancelled = any(c.status == "cancelled" for c in final_status_source)
|
||||
has_failed = any(c.status == "failed" for c in final_status_source)
|
||||
|
||||
if has_success:
|
||||
final_status = "success"
|
||||
@@ -385,6 +403,7 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
||||
total_candidates=len(candidates),
|
||||
final_status=final_status,
|
||||
total_latency_ms=total_latency,
|
||||
attempted_only=self.attempted_only,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -11,9 +11,21 @@ from typing import Any
|
||||
|
||||
OAUTH_ACCOUNT_BLOCK_PREFIX = "[ACCOUNT_BLOCK] "
|
||||
|
||||
ACCOUNT_BLOCK_REASON_KEYWORDS: tuple[str, ...] = (
|
||||
# -- 按原因细分的关键词组 --
|
||||
# 封禁类 (suspended / banned)
|
||||
_KEYWORDS_SUSPENDED: tuple[str, ...] = (
|
||||
"suspended",
|
||||
"account_block",
|
||||
"account blocked",
|
||||
"封禁",
|
||||
"封号",
|
||||
"被封",
|
||||
"账户已封禁",
|
||||
"账号异常",
|
||||
)
|
||||
|
||||
# 停用类 (disabled / deactivated)
|
||||
_KEYWORDS_DISABLED: tuple[str, ...] = (
|
||||
"account has been disabled",
|
||||
"account disabled",
|
||||
"account has been deactivated",
|
||||
@@ -21,21 +33,36 @@ ACCOUNT_BLOCK_REASON_KEYWORDS: tuple[str, ...] = (
|
||||
"account deactivated",
|
||||
"organization has been disabled",
|
||||
"organization_disabled",
|
||||
"deactivated",
|
||||
"访问被禁止",
|
||||
"账户访问被禁止",
|
||||
)
|
||||
|
||||
# 需要验证类
|
||||
_KEYWORDS_VERIFICATION: tuple[str, ...] = (
|
||||
"validation_required",
|
||||
"verify your account",
|
||||
"suspended",
|
||||
"deactivated",
|
||||
# Kiro quota refresher 写入的确切文本
|
||||
"账户已封禁",
|
||||
# Antigravity quota refresher 写入的确切文本
|
||||
"账户访问被禁止",
|
||||
"封禁",
|
||||
"封号",
|
||||
"被封",
|
||||
"访问被禁止",
|
||||
"账号异常",
|
||||
)
|
||||
|
||||
# 合并的完整列表(用于 is_account_level_block_reason 快速判断)
|
||||
ACCOUNT_BLOCK_REASON_KEYWORDS: tuple[str, ...] = (
|
||||
*_KEYWORDS_SUSPENDED,
|
||||
*_KEYWORDS_DISABLED,
|
||||
*_KEYWORDS_VERIFICATION,
|
||||
)
|
||||
|
||||
|
||||
def _classify_block_reason(text: str) -> tuple[str, str]:
|
||||
"""Return (code, label) based on the oauth_invalid_reason text."""
|
||||
lowered = text.lower()
|
||||
if any(kw in lowered for kw in _KEYWORDS_VERIFICATION):
|
||||
return "account_verification", "需要验证"
|
||||
if any(kw in lowered for kw in _KEYWORDS_DISABLED):
|
||||
return "account_disabled", "账号停用"
|
||||
if any(kw in lowered for kw in _KEYWORDS_SUSPENDED):
|
||||
return "account_suspended", "账号封禁"
|
||||
return "account_blocked", "账号异常"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PoolAccountState:
|
||||
@@ -147,19 +174,23 @@ def _resolve_from_oauth_invalid_reason(reason: str | None) -> PoolAccountState |
|
||||
|
||||
if text.startswith(OAUTH_ACCOUNT_BLOCK_PREFIX):
|
||||
cleaned = text[len(OAUTH_ACCOUNT_BLOCK_PREFIX) :].strip()
|
||||
code, label = (
|
||||
_classify_block_reason(cleaned) if cleaned else ("account_blocked", "账号异常")
|
||||
)
|
||||
return PoolAccountState(
|
||||
blocked=True,
|
||||
code="account_blocked",
|
||||
label="账号异常",
|
||||
code=code,
|
||||
label=label,
|
||||
reason=cleaned or "账号异常",
|
||||
)
|
||||
|
||||
lowered = text.lower()
|
||||
if any(keyword in lowered for keyword in ACCOUNT_BLOCK_REASON_KEYWORDS):
|
||||
code, label = _classify_block_reason(text)
|
||||
return PoolAccountState(
|
||||
blocked=True,
|
||||
code="account_blocked",
|
||||
label="账号异常",
|
||||
code=code,
|
||||
label=label,
|
||||
reason=text,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user