fix(codex): 配额刷新 401/403 不再自动禁用 key,区分软性请求失败与账户封禁

- 新增 OAUTH_REQUEST_FAILED_PREFIX 标记非 token 失效的 403 请求失败
- 引入 _merge_invalid_reason 合并逻辑,避免低优先级原因覆盖高优先级状态
- account_state 中 REFRESH_FAILED/REQUEST_FAILED 前缀不再触发封禁判定
- 401/403 返回 auto_disabled=False,不再直接设置 is_active=False
This commit is contained in:
fawney19
2026-03-16 01:12:35 +08:00
parent 131471a13f
commit 025e979935
4 changed files with 141 additions and 22 deletions

View File

@@ -14,6 +14,7 @@ from src.services.provider_keys.quota_reader import get_quota_reader
OAUTH_ACCOUNT_BLOCK_PREFIX = "[ACCOUNT_BLOCK] "
OAUTH_REFRESH_FAILED_PREFIX = "[REFRESH_FAILED] "
OAUTH_EXPIRED_PREFIX = "[OAUTH_EXPIRED] "
OAUTH_REQUEST_FAILED_PREFIX = "[REQUEST_FAILED] "
# -- 按原因细分的关键词组 --
# 封禁类 (suspended / banned)
@@ -192,6 +193,12 @@ def _resolve_from_oauth_invalid_reason(reason: str | None) -> PoolAccountState |
reason=cleaned or "OAuth Token 已过期且无法续期",
)
if text.startswith(OAUTH_REFRESH_FAILED_PREFIX) or text.startswith(OAUTH_REQUEST_FAILED_PREFIX):
return None
if text.startswith("["):
return None
lowered = text.lower()
if any(keyword in lowered for keyword in ACCOUNT_BLOCK_REASON_KEYWORDS):
code, label = _classify_block_reason(text)
@@ -229,6 +236,7 @@ __all__ = [
"OAUTH_ACCOUNT_BLOCK_PREFIX",
"OAUTH_EXPIRED_PREFIX",
"OAUTH_REFRESH_FAILED_PREFIX",
"OAUTH_REQUEST_FAILED_PREFIX",
"PoolAccountState",
"resolve_pool_account_state",
]

View File

@@ -19,6 +19,7 @@ from src.services.provider.oauth_token import looks_like_token_invalidated
from src.services.provider.pool.account_state import (
OAUTH_ACCOUNT_BLOCK_PREFIX,
OAUTH_EXPIRED_PREFIX,
OAUTH_REQUEST_FAILED_PREFIX,
)
from src.services.provider_keys.auth_type import normalize_auth_type
from src.services.provider_keys.codex_usage_parser import (
@@ -107,6 +108,45 @@ def _build_structured_invalid_reason(*, status_code: int, upstream_message: str
return message
def _build_soft_request_failure_reason(*, status_code: int, upstream_message: str | None) -> str:
detail = str(upstream_message or "").strip() or f"Codex 请求失败 ({status_code})"
return f"{OAUTH_REQUEST_FAILED_PREFIX}{detail}"
def _get_current_invalid_reason(key: ProviderAPIKey) -> str:
return str(getattr(key, "oauth_invalid_reason", None) or "").strip()
def _merge_invalid_reason(current: str, candidate_reason: str) -> str:
if not current:
return candidate_reason
if current.startswith(OAUTH_ACCOUNT_BLOCK_PREFIX):
return current
if current.startswith(OAUTH_EXPIRED_PREFIX) and candidate_reason.startswith(
OAUTH_REQUEST_FAILED_PREFIX
):
return current
return candidate_reason
def _build_invalid_state_update(
key: ProviderAPIKey,
*,
candidate_reason: str,
) -> dict[str, Any]:
current_reason = _get_current_invalid_reason(key)
merged_reason = _merge_invalid_reason(current_reason, candidate_reason)
if merged_reason == current_reason:
return {
"oauth_invalid_at": getattr(key, "oauth_invalid_at", None),
"oauth_invalid_reason": merged_reason,
}
return {
"oauth_invalid_at": datetime.now(timezone.utc),
"oauth_invalid_reason": merged_reason,
}
async def refresh_codex_key_quota(
*,
db: Session,
@@ -187,21 +227,20 @@ async def refresh_codex_key_quota(
metadata_updates[key.id] = {"codex": header_quota}
if status_code == 401:
state_updates[key.id] = {
"is_active": False,
"oauth_invalid_at": datetime.now(timezone.utc),
"oauth_invalid_reason": _build_structured_invalid_reason(
state_updates[key.id] = _build_invalid_state_update(
key,
candidate_reason=_build_structured_invalid_reason(
status_code=401,
upstream_message=err_msg,
),
}
)
return {
"key_id": key.id,
"key_name": key.name,
"status": "auth_invalid",
"message": f"wham/usage API 返回状态码 401{f': {err_msg}' if err_msg else ''}",
"status_code": 401,
"auto_disabled": True,
"auto_disabled": False,
}
if status_code == 402:
@@ -219,13 +258,13 @@ async def refresh_codex_key_quota(
if oauth_plan_type and not codex_meta.get("plan_type"):
codex_meta["plan_type"] = oauth_plan_type
metadata_updates[key.id] = {"codex": codex_meta}
state_updates[key.id] = {
"oauth_invalid_at": datetime.now(timezone.utc),
"oauth_invalid_reason": _build_structured_invalid_reason(
state_updates[key.id] = _build_invalid_state_update(
key,
candidate_reason=_build_structured_invalid_reason(
status_code=402,
upstream_message=err_msg,
),
}
)
return {
"key_id": key.id,
"key_name": key.name,
@@ -251,21 +290,26 @@ async def refresh_codex_key_quota(
}
if status_code == 403:
state_updates[key.id] = {
"is_active": False,
"oauth_invalid_at": datetime.now(timezone.utc),
"oauth_invalid_reason": _build_structured_invalid_reason(
candidate_reason = _build_structured_invalid_reason(
status_code=403,
upstream_message=err_msg,
)
if not looks_like_token_invalidated(err_msg):
candidate_reason = _build_soft_request_failure_reason(
status_code=403,
upstream_message=err_msg,
),
}
)
state_updates[key.id] = _build_invalid_state_update(
key,
candidate_reason=candidate_reason,
)
return {
"key_id": key.id,
"key_name": key.name,
"status": "forbidden",
"message": f"wham/usage API 返回状态码 403{f': {err_msg}' if err_msg else ''}",
"status_code": 403,
"auto_disabled": True,
"auto_disabled": False,
}
return {