mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(oauth): ACCOUNT_BLOCK 标记不再禁用 key,token 刷新成功自动解除所有 invalid 标记
- error_handler 标记 ACCOUNT_BLOCK 时保持 is_active=True,oauth_invalid 标记已 足够阻止调度,配额刷新仍可覆盖该 key - 移除 is_account_level_block 守卫:token 刷新成功即证明账号可用,清除所有 oauth_invalid 标记(含 ACCOUNT_BLOCK) - health_policy 401 瞬时失败不再设置 60s cooldown,仅清除 token 缓存后立即重试 - key_quota_service 全量刷新时纳入 ACCOUNT_BLOCK 的 key,账号恢复后可自动解除 - 各 refresher 刷新成功时显式恢复 is_active=True
This commit is contained in:
@@ -79,8 +79,6 @@ def _store_refreshed_oauth_sync(
|
||||
access_token: str,
|
||||
parsed_auth_config: dict[str, Any],
|
||||
) -> None:
|
||||
from src.services.provider.oauth_token import is_account_level_block
|
||||
|
||||
with get_db_context() as db:
|
||||
key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
|
||||
if not key:
|
||||
@@ -88,10 +86,12 @@ def _store_refreshed_oauth_sync(
|
||||
|
||||
key.api_key = crypto_service.encrypt(access_token)
|
||||
key.auth_config = crypto_service.encrypt(json.dumps(parsed_auth_config))
|
||||
if not is_account_level_block(getattr(key, "oauth_invalid_reason", None)):
|
||||
# 刷新成功 => 清除所有 oauth_invalid 标记(包括 [ACCOUNT_BLOCK])。
|
||||
# Token 能成功刷新说明账号可用,之前的 block 标记应视为过时。
|
||||
if getattr(key, "oauth_invalid_at", None) is not None:
|
||||
key.oauth_invalid_at = None
|
||||
key.oauth_invalid_reason = None
|
||||
key.is_active = True
|
||||
key.is_active = True
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
@@ -407,7 +407,8 @@ class ErrorHandlerService:
|
||||
|
||||
key.oauth_invalid_at = datetime.now(timezone.utc)
|
||||
key.oauth_invalid_reason = f"{OAUTH_ACCOUNT_BLOCK_PREFIX}{reason}"
|
||||
key.is_active = False
|
||||
# 不设 is_active=False:oauth_invalid 标记已足够阻止调度,
|
||||
# 保持 is_active=True 使配额刷新仍能覆盖该 key,账号恢复后可自动解除。
|
||||
|
||||
pool_cfg = parse_pool_config(getattr(provider, "config", None))
|
||||
auto_remove_enabled = bool(pool_cfg and pool_cfg.auto_remove_banned_keys)
|
||||
|
||||
@@ -101,13 +101,11 @@ def _persist_refreshed_token(
|
||||
key.api_key = crypto_service.encrypt(access_token)
|
||||
key.auth_config = crypto_service.encrypt(json.dumps(token_meta))
|
||||
|
||||
# 刷新成功 => 清除非账号级别的 oauth_invalid 标记(如 [REFRESH_FAILED])
|
||||
from src.services.provider.oauth_token import is_account_level_block
|
||||
|
||||
if not is_account_level_block(getattr(key, "oauth_invalid_reason", None)):
|
||||
if getattr(key, "oauth_invalid_at", None) is not None:
|
||||
key.oauth_invalid_at = None
|
||||
key.oauth_invalid_reason = None
|
||||
# 刷新成功 => 清除所有 oauth_invalid 标记(包括 [ACCOUNT_BLOCK])。
|
||||
# Token 能成功刷新说明账号可用,之前的 block 标记应视为过时。
|
||||
if getattr(key, "oauth_invalid_at", None) is not None:
|
||||
key.oauth_invalid_at = None
|
||||
key.oauth_invalid_reason = None
|
||||
|
||||
sess = _safe_object_session(key)
|
||||
if sess is not None:
|
||||
|
||||
@@ -135,12 +135,13 @@ async def resolve_oauth_access_token(
|
||||
if row is not None:
|
||||
row.api_key = key_obj.api_key
|
||||
row.auth_config = key_obj.auth_config
|
||||
# Refresh succeeded => clear token-level invalid markers.
|
||||
# Preserve account-level blocks (刷新 token 无法修复).
|
||||
if not is_account_level_block(getattr(row, "oauth_invalid_reason", None)):
|
||||
# Refresh succeeded => clear all invalid markers (including
|
||||
# account-level blocks). A successful token refresh proves the
|
||||
# account is usable; stale block marks should not persist.
|
||||
if row.oauth_invalid_at is not None:
|
||||
row.oauth_invalid_at = None
|
||||
row.oauth_invalid_reason = None
|
||||
row.is_active = True
|
||||
row.is_active = True
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
# Don't fail caller path; token is still usable for this request.
|
||||
|
||||
@@ -4,7 +4,7 @@ Maps upstream HTTP status codes to pool-level actions:
|
||||
|
||||
| Code | Action |
|
||||
|--------------|------------------------------------------------------------|
|
||||
| 401 | Invalidate OAuth token cache; permanent (deactivated) 1h, else 60s |
|
||||
| 401 | Invalidate OAuth token cache; permanent (deactivated) 1h, else no cooldown |
|
||||
| 402 | Long cooldown (payment issue) |
|
||||
| 403 | Graded cooldown: severe (suspended/banned) 1h, else 300s+ |
|
||||
| 400 | Check body for "organization has been disabled" -> cooldown |
|
||||
@@ -183,10 +183,11 @@ async def _apply(
|
||||
key_id[:8],
|
||||
)
|
||||
else:
|
||||
# Transient auth failure (e.g. expired token) — short cooldown.
|
||||
await redis_ops.set_cooldown(provider_id, key_id, "auth_failed_401", ttl=60)
|
||||
# Transient auth failure (e.g. expired token) — token cache already
|
||||
# invalidated above; the next request will trigger a token refresh.
|
||||
# No cooldown needed: the key should be retried immediately after refresh.
|
||||
logger.info(
|
||||
"Pool[{}]: key {} got 401, token cache invalidated + 60s cooldown",
|
||||
"Pool[{}]: key {} got 401, token cache invalidated (no cooldown)",
|
||||
provider_id[:8],
|
||||
key_id[:8],
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import or_ as db_or
|
||||
from sqlalchemy.orm import Session, defer
|
||||
|
||||
from src.core.exceptions import InvalidRequestException, NotFoundException
|
||||
@@ -114,7 +115,16 @@ async def refresh_provider_quota_for_provider(
|
||||
)
|
||||
)
|
||||
if selected_key_ids is None:
|
||||
keys_query = keys_query.filter(ProviderAPIKey.is_active.is_(True))
|
||||
# 全量刷新:活跃 key + 被系统自动标记 ACCOUNT_BLOCK 的 key。
|
||||
# 后者使 ACCOUNT_BLOCK 标记的 key 也参与刷新,账号恢复后可自动解除。
|
||||
# 注意:不能用宽泛的 oauth_invalid_reason IS NOT NULL,否则会纳入
|
||||
# 用户手动停用 (is_active=False) 但恰好也有 reason 的历史 key。
|
||||
keys_query = keys_query.filter(
|
||||
db_or(
|
||||
ProviderAPIKey.is_active.is_(True),
|
||||
ProviderAPIKey.oauth_invalid_reason.startswith("[ACCOUNT_BLOCK]"),
|
||||
)
|
||||
)
|
||||
else:
|
||||
if not selected_key_ids:
|
||||
return {
|
||||
|
||||
@@ -114,6 +114,7 @@ async def refresh_antigravity_key_quota(
|
||||
state_updates[key.id] = {
|
||||
"oauth_invalid_at": None,
|
||||
"oauth_invalid_reason": None,
|
||||
"is_active": True,
|
||||
}
|
||||
return {
|
||||
"key_id": key.id,
|
||||
|
||||
@@ -280,6 +280,7 @@ async def refresh_codex_key_quota(
|
||||
state_updates[key.id] = {
|
||||
"oauth_invalid_at": None,
|
||||
"oauth_invalid_reason": None,
|
||||
"is_active": True,
|
||||
}
|
||||
return {
|
||||
"key_id": key.id,
|
||||
@@ -351,6 +352,7 @@ async def refresh_codex_key_quota(
|
||||
state_updates[key.id] = {
|
||||
"oauth_invalid_at": None,
|
||||
"oauth_invalid_reason": None,
|
||||
"is_active": True,
|
||||
}
|
||||
return {
|
||||
"key_id": key.id,
|
||||
|
||||
@@ -137,6 +137,7 @@ async def refresh_kiro_key_quota(
|
||||
state_updates[key.id] = {
|
||||
"oauth_invalid_at": None,
|
||||
"oauth_invalid_reason": None,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
# 如果 auth_config 有更新(例如 token 刷新),也需要更新
|
||||
|
||||
Reference in New Issue
Block a user