feat: 新增 Antigravity/Kiro 账户禁用封禁状态监控与展示

- 前端:为 Antigravity 和 Kiro 上游元数据添加禁用/封禁状态字段
- 前端:在密钥详情抽屉中展示账户禁用/封禁警告信息,合并重复的时间格式化函数
- 后端:优化 Antigravity 和 Kiro 适配器的状态检测与上报逻辑

Co-authored-by: AAEE86 <ppk0227@hotmail.com>

Closes #158
This commit is contained in:
fawney19
2026-02-09 17:24:48 +08:00
parent 65658e58d5
commit b14fb31933
6 changed files with 383 additions and 113 deletions

View File

@@ -123,6 +123,56 @@ def _should_fallback_status(status_code: int) -> bool:
return False
# ---------------------------------------------------------------------------
# 账户封禁异常(对齐 AM: is_forbidden 标志)
# ---------------------------------------------------------------------------
class AntigravityAccountForbiddenException(Exception):
"""Antigravity 账户被封禁/禁止访问异常。
当 API 返回 403 Forbidden 时抛出,表示账户权限被撤销。
"""
def __init__(
self,
message: str = "账户访问被禁止",
status_code: int = 403,
reason: str | None = None,
):
super().__init__(message)
self.message = message
self.status_code = status_code
self.reason = reason
def _extract_forbidden_reason(response_text: str) -> str | None:
"""从 403 响应体中提取封禁原因。
尝试解析 JSON 响应中的 error.message 字段。
"""
if not response_text:
return None
try:
data = json.loads(response_text)
if isinstance(data, dict):
error = data.get("error")
if isinstance(error, dict):
message = error.get("message")
if isinstance(message, str) and message.strip():
return message.strip()
# 直接在顶层查找 message
message = data.get("message")
if isinstance(message, str) and message.strip():
return message.strip()
except Exception:
pass
# 如果无法解析,返回原始文本的前 100 个字符
if len(response_text) > 100:
return response_text[:100] + "..."
return response_text if response_text.strip() else None
# ---------------------------------------------------------------------------
# 从 loadCodeAssist 响应中提取信息
# ---------------------------------------------------------------------------
@@ -441,12 +491,25 @@ async def fetch_available_models(
)
continue
# 403 Forbidden账户权限被禁止对齐 AM is_forbidden 标志)
if resp.status_code == 403:
reason = _extract_forbidden_reason(resp.text)
logger.warning(
"[antigravity] fetchAvailableModels 403 Forbidden: {}",
reason or "unknown",
)
raise AntigravityAccountForbiddenException(
message="账户访问被禁止",
status_code=403,
reason=reason,
)
# 不可 fallback 的 4xx直接抛出
raise RuntimeError(
f"fetchAvailableModels failed: status={resp.status_code} base_url={base_url} "
f"body={resp.text[:200] if resp.text else ''}"
)
except RuntimeError:
except (RuntimeError, AntigravityAccountForbiddenException):
raise
except Exception as e:
url_availability.mark_unavailable(base_url)
@@ -513,6 +576,7 @@ def generate_fallback_project_id() -> str:
__all__ = [
"AntigravityAccountForbiddenException",
"extract_project_id",
"extract_tier_id",
"fetch_available_models",

View File

@@ -208,7 +208,10 @@ async def fetch_models_antigravity(
调用 v1internal:fetchAvailableModels 获取可用模型列表,
解析配额信息,过滤黑名单模型。
"""
from src.services.provider.adapters.antigravity.client import fetch_available_models
from src.services.provider.adapters.antigravity.client import (
AntigravityAccountForbiddenException,
fetch_available_models,
)
auth_config = ctx.auth_config or {}
project_id = auth_config.get("project_id")
@@ -222,6 +225,9 @@ async def fetch_models_antigravity(
proxy_config=ctx.proxy_config,
timeout_seconds=timeout_seconds,
)
except AntigravityAccountForbiddenException:
# 重新抛出由上层keys.py处理自动停用逻辑
raise
except Exception as e:
return [], [f"antigravity: fetchAvailableModels error: {e}"], False, None

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import re
import time
import uuid
from typing import Any
@@ -27,6 +28,21 @@ from src.services.provider.adapters.kiro.token_manager import (
)
class KiroAccountBannedException(Exception):
"""Kiro 账户被封禁异常"""
def __init__(
self,
message: str = "账户已封禁",
status_code: int = 403,
reason: str | None = None,
):
super().__init__(message)
self.message = message
self.status_code = status_code
self.reason = reason
async def fetch_kiro_usage_limits(
auth_config: dict[str, Any],
proxy_config: dict[str, Any] | None = None,
@@ -97,9 +113,36 @@ async def fetch_kiro_usage_limits(
response = await client.get(url, headers=headers, timeout=httpx.Timeout(30.0))
if response.status_code != 200:
response_text = (response.text or "").strip()
# 检测账户封禁403/423 + 特定错误信息)
is_banned = False
ban_reason = None
if response.status_code in (403, 423):
# 检测封禁相关错误(正则模式,通过 re.search 匹配)
banned_keywords = [
"AccountSuspendedException",
"account.*suspend",
"account.*banned",
"account.*disabled",
"account.*access.*denied",
]
for keyword in banned_keywords:
if re.search(keyword, response_text, re.IGNORECASE):
is_banned = True
ban_reason = (
response_text[:200] if response_text else f"HTTP {response.status_code}"
)
break
if not is_banned and response.status_code == 423:
# 423 Locked 通常表示账户被锁定/封禁
is_banned = True
ban_reason = response_text[:200] if response_text else "HTTP 423 Locked"
error_msg = {
401: "认证失败Token 无效或已过期",
403: "权限不足,无法获取使用额度",
403: "账户已封禁" if is_banned else "权限不足,无法获取使用额度",
423: "账户已封禁",
429: "请求过于频繁,已被限流",
}.get(response.status_code, "获取使用额度失败")
if 500 <= response.status_code < 600:
@@ -107,8 +150,17 @@ async def fetch_kiro_usage_limits(
logger.debug(
"kiro usage API error: HTTP {} | {}",
response.status_code,
(response.text or "").strip()[:200],
response_text[:200],
)
# 如果检测到封禁,抛出带有封禁标记的异常
if is_banned:
raise KiroAccountBannedException(
message=error_msg,
status_code=response.status_code,
reason=ban_reason,
)
raise RuntimeError(f"{error_msg}: HTTP {response.status_code}")
try:
@@ -178,6 +230,7 @@ def parse_kiro_usage_response(data: dict) -> dict | None:
__all__ = [
"KiroAccountBannedException",
"fetch_kiro_usage_limits",
"parse_kiro_usage_response",
]