feat(pool,trace): 账号封禁原因细分与请求追踪 attempted_only 过滤

- 将账号封禁原因从笼统的"账号异常"细分为封禁/停用/需要验证三类,
  前后端关键词组同步拆分,号池管理页面展示对应分类标签
- trace API 新增 attempted_only 参数,支持仅返回实际尝试过的候选,
  前端时间线组件默认启用过滤,排除 available/unused/skipped 记录
This commit is contained in:
fawney19
2026-03-05 17:32:21 +08:00
parent a7697032a4
commit 694167f78f
8 changed files with 226 additions and 52 deletions

View File

@@ -8,9 +8,16 @@ from unittest.mock import MagicMock
from src.api.admin.monitoring.trace import AdminGetRequestTraceAdapter
from src.services.request.candidate import RequestCandidateService
_STARTED_STATUSES = {"pending", "streaming", "success", "failed", "cancelled"}
def _candidate(*, status: str, latency_ms: int | None = None) -> SimpleNamespace:
def _candidate(
*, status: str, latency_ms: int | None = None, started_at: datetime | None = ... # type: ignore[assignment]
) -> SimpleNamespace:
now = datetime.now(timezone.utc)
resolved_started_at = (
(now if status in _STARTED_STATUSES else None) if started_at is ... else started_at
)
return SimpleNamespace(
id=f"cand-{status}",
request_id="req-1",
@@ -30,7 +37,7 @@ def _candidate(*, status: str, latency_ms: int | None = None) -> SimpleNamespace
concurrent_requests=None,
extra_data=None,
created_at=now,
started_at=None,
started_at=resolved_started_at,
finished_at=None,
)
@@ -80,3 +87,46 @@ def test_trace_returns_unattempted_candidates(monkeypatch: object) -> None:
assert response.total_candidates == 2
assert len(response.candidates) == 2
assert {c.status for c in response.candidates} == {"available", "unused"}
def test_trace_attempted_only_filters_unattempted_candidates(monkeypatch: object) -> None:
candidates = [
_candidate(status="available"),
_candidate(status="unused"),
_candidate(status="skipped"),
_candidate(status="failed", latency_ms=123),
_candidate(status="success", latency_ms=456),
]
monkeypatch.setattr(
RequestCandidateService,
"get_candidates_by_request_id",
lambda _db, _request_id: candidates,
)
adapter = AdminGetRequestTraceAdapter(request_id="req-1", attempted_only=True)
response = asyncio.run(adapter.handle(_context()))
assert response.total_candidates == 2
assert len(response.candidates) == 2
assert {c.status for c in response.candidates} == {"failed", "success"}
assert response.total_latency_ms == 579
def test_trace_attempted_only_excludes_pending_without_started_at(monkeypatch: object) -> None:
candidates = [
_candidate(status="available"),
_candidate(status="pending", started_at=None),
_candidate(status="pending"),
]
monkeypatch.setattr(
RequestCandidateService,
"get_candidates_by_request_id",
lambda _db, _request_id: candidates,
)
adapter = AdminGetRequestTraceAdapter(request_id="req-1", attempted_only=True)
response = asyncio.run(adapter.handle(_context()))
assert response.total_candidates == 1
assert len(response.candidates) == 1
assert response.candidates[0].status == "pending"