diff --git a/config.py b/config.py
index 11114c4d..e4a4733c 100644
--- a/config.py
+++ b/config.py
@@ -39,6 +39,9 @@
"COMPATIBILITY_MODE": "compatibility_mode_enabled",
"RETURN_THOUGHTS_TO_FRONTEND": "return_thoughts_to_frontend",
"ANTIGRAVITY_STREAM2NOSTREAM": "antigravity_stream2nostream",
+ "AUTO_VERIFY_ENABLED": "auto_verify_enabled",
+ "AUTO_VERIFY_INTERVAL": "auto_verify_interval",
+ "AUTO_VERIFY_ERROR_CODES": "auto_verify_error_codes",
"HOST": "host",
"PORT": "port",
"API_PASSWORD": "api_password",
@@ -439,3 +442,65 @@ async def get_antigravity_api_url() -> str:
"ANTIGRAVITY_API_URL",
)
)
+
+
+async def get_auto_verify_enabled() -> bool:
+ """
+ Get auto verify enabled setting.
+
+ 自动检验功能:启用后会定时检查凭证状态,发现错误码自动执行检验恢复。
+
+ Environment variable: AUTO_VERIFY_ENABLED
+ Database config key: auto_verify_enabled
+ Default: False
+ """
+ env_value = os.getenv("AUTO_VERIFY_ENABLED")
+ if env_value:
+ return env_value.lower() in ("true", "1", "yes", "on")
+
+ return bool(await get_config_value("auto_verify_enabled", False))
+
+
+async def get_auto_verify_interval() -> int:
+ """
+ Get auto verify interval in seconds.
+
+ 自动检验的检查间隔(秒)。
+
+ Environment variable: AUTO_VERIFY_INTERVAL
+ Database config key: auto_verify_interval
+ Default: 300 (5 minutes)
+ Minimum: 60 (1 minute)
+ """
+ env_value = os.getenv("AUTO_VERIFY_INTERVAL")
+ if env_value:
+ try:
+ return max(60, int(env_value))
+ except ValueError:
+ pass
+
+ interval = await get_config_value("auto_verify_interval", 300)
+ return max(60, int(interval))
+
+
+async def get_auto_verify_error_codes() -> list:
+ """
+ Get auto verify error codes.
+
+ 需要自动检验的错误码列表。
+
+ Environment variable: AUTO_VERIFY_ERROR_CODES (comma-separated, e.g., "400,403")
+ Database config key: auto_verify_error_codes
+ Default: [400, 403]
+ """
+ env_value = os.getenv("AUTO_VERIFY_ERROR_CODES")
+ if env_value:
+ try:
+ return [int(code.strip()) for code in env_value.split(",") if code.strip()]
+ except ValueError:
+ pass
+
+ codes = await get_config_value("auto_verify_error_codes")
+ if codes and isinstance(codes, list):
+ return codes
+ return [400, 403]
diff --git a/front/common.js b/front/common.js
index 0988f7d7..291d6a10 100644
--- a/front/common.js
+++ b/front/common.js
@@ -2205,6 +2205,11 @@ function populateConfigForm() {
setConfigField('autoBanErrorCodes', (c.auto_ban_error_codes || []).join(','));
setConfigField('callsPerRotation', c.calls_per_rotation || 10);
+ // 自动检验恢复配置
+ document.getElementById('autoVerifyEnabled').checked = Boolean(c.auto_verify_enabled);
+ setConfigField('autoVerifyInterval', c.auto_verify_interval || 300);
+ setConfigField('autoVerifyErrorCodes', (c.auto_verify_error_codes || []).join(','));
+
document.getElementById('retry429Enabled').checked = Boolean(c.retry_429_enabled);
setConfigField('retry429MaxRetries', c.retry_429_max_retries || 20);
setConfigField('retry429Interval', c.retry_429_interval || 0.1);
@@ -2256,6 +2261,11 @@ async function saveConfig() {
auto_ban_error_codes: getValue('autoBanErrorCodes').split(',')
.map(c => parseInt(c.trim())).filter(c => !isNaN(c)),
calls_per_rotation: getInt('callsPerRotation', 10),
+ // 自动检验恢复配置
+ auto_verify_enabled: getChecked('autoVerifyEnabled'),
+ auto_verify_interval: getInt('autoVerifyInterval', 300),
+ auto_verify_error_codes: getValue('autoVerifyErrorCodes').split(',')
+ .map(c => parseInt(c.trim())).filter(c => !isNaN(c)),
retry_429_enabled: getChecked('retry429Enabled'),
retry_429_max_retries: getInt('retry429MaxRetries', 20),
retry_429_interval: getFloat('retry429Interval', 0.1),
diff --git a/front/control_panel.html b/front/control_panel.html
index 8b078193..1997ce33 100644
--- a/front/control_panel.html
+++ b/front/control_panel.html
@@ -1836,6 +1836,38 @@
自动封禁配置
+
+
自动检验恢复配置
+
+
+
+ 定时检测被封禁的凭证并尝试自动恢复 ✓ 支持热更新
+
+
+
+
+
+ 每隔多少秒检查一次被封禁的凭证,最小60秒
+
+
+
+
+
+ 用逗号分隔的错误码列表,凭证因这些错误码被封禁时会尝试自动恢复
+
+
+
+ 💡 说明:启用后,系统会定期检查因指定错误码被封禁的凭证,并尝试重新验证。
+ 如果验证通过,凭证会自动恢复可用状态,无需手动点击"检验"按钮。
+
+
+
429重试配置
diff --git a/src/auto_verify.py b/src/auto_verify.py
new file mode 100644
index 00000000..674c6333
--- /dev/null
+++ b/src/auto_verify.py
@@ -0,0 +1,250 @@
+"""
+Auto Verify Module - 自动检验凭证错误码并恢复
+定时检查凭证状态,发现错误码自动执行检验恢复
+"""
+
+import asyncio
+from typing import Optional
+
+from log import log
+
+
+class AutoVerifyService:
+ """自动检验服务 - 后台定时检查并恢复错误凭证"""
+
+ _instance: Optional["AutoVerifyService"] = None
+
+ def __new__(cls):
+ if cls._instance is None:
+ cls._instance = super().__new__(cls)
+ cls._instance._initialized = False
+ return cls._instance
+
+ def __init__(self):
+ if self._initialized:
+ return
+
+ self._task: Optional[asyncio.Task] = None
+ self._running = False
+ self._initialized = True
+
+ async def start(self):
+ """启动自动检验服务"""
+ if self._running:
+ log.debug("AutoVerifyService already running")
+ return
+
+ from config import get_config_value
+
+ enabled = await get_config_value("auto_verify_enabled", False)
+ if not enabled:
+ log.info("自动检验服务未启用 (auto_verify_enabled=False)")
+ return
+
+ self._running = True
+ self._task = asyncio.create_task(self._run_loop(), name="auto_verify_loop")
+ log.info("自动检验服务已启动")
+
+ async def stop(self):
+ """停止自动检验服务"""
+ if not self._running:
+ return
+
+ self._running = False
+ if self._task and not self._task.done():
+ self._task.cancel()
+ try:
+ await self._task
+ except asyncio.CancelledError:
+ pass
+
+ log.info("自动检验服务已停止")
+
+ async def reload(self):
+ """重新加载配置并根据需要启动/停止服务"""
+ from config import get_config_value
+
+ enabled = await get_config_value("auto_verify_enabled", False)
+
+ if enabled and not self._running:
+ await self.start()
+ elif not enabled and self._running:
+ await self.stop()
+
+ @property
+ def is_running(self) -> bool:
+ """返回服务是否正在运行"""
+ return self._running
+
+ async def _run_loop(self):
+ """主循环 - 定时检查凭证状态"""
+ from config import get_auto_verify_interval
+
+ while self._running:
+ try:
+ interval = await get_auto_verify_interval()
+
+ await self._check_and_verify_credentials()
+
+ await asyncio.sleep(interval)
+
+ except asyncio.CancelledError:
+ break
+ except Exception as e:
+ log.error(f"自动检验循环出错: {e}")
+ await asyncio.sleep(60)
+
+ async def _check_and_verify_credentials(self):
+ """检查所有凭证状态,对有错误码的凭证执行检验"""
+ from config import get_auto_verify_error_codes
+ from .storage_adapter import get_storage_adapter
+
+ try:
+ storage_adapter = await get_storage_adapter()
+
+ auto_verify_error_codes = await get_auto_verify_error_codes()
+
+ for mode in ["geminicli", "antigravity"]:
+ await self._check_mode_credentials(
+ storage_adapter, mode, auto_verify_error_codes
+ )
+
+ except Exception as e:
+ log.error(f"检查凭证状态失败: {e}")
+
+ async def _check_mode_credentials(
+ self, storage_adapter, mode: str, error_codes_to_verify: list
+ ):
+ """检查指定模式的凭证"""
+ try:
+ credentials = await storage_adapter.list_credentials(mode=mode)
+ if not credentials:
+ return
+
+ verified_count = 0
+ failed_count = 0
+
+ for filename in credentials:
+ try:
+ state = await storage_adapter.get_credential_state(filename, mode=mode)
+ if not state:
+ continue
+
+ current_error_codes = state.get("error_codes", [])
+ if not current_error_codes:
+ continue
+
+ needs_verify = any(
+ code in error_codes_to_verify for code in current_error_codes
+ )
+
+ if needs_verify:
+ log.info(
+ f"[自动检验] 检测到错误码 {current_error_codes},"
+ f"开始检验凭证: {filename} (mode={mode})"
+ )
+
+ success = await self._verify_credential(filename, mode)
+
+ if success:
+ verified_count += 1
+ log.info(f"[自动检验] 凭证检验成功: {filename} (mode={mode})")
+ else:
+ failed_count += 1
+ log.warning(f"[自动检验] 凭证检验失败: {filename} (mode={mode})")
+
+ await asyncio.sleep(1)
+
+ except Exception as e:
+ log.error(f"[自动检验] 处理凭证 {filename} 时出错: {e}")
+
+ if verified_count > 0 or failed_count > 0:
+ log.info(
+ f"[自动检验] {mode} 模式完成: "
+ f"成功 {verified_count} 个, 失败 {failed_count} 个"
+ )
+
+ except Exception as e:
+ log.error(f"[自动检验] 检查 {mode} 凭证失败: {e}")
+
+ async def _verify_credential(self, filename: str, mode: str) -> bool:
+ """执行单个凭证的检验"""
+ try:
+ from .storage_adapter import get_storage_adapter
+ from .google_oauth_api import Credentials
+ from .web_routes import fetch_project_id
+ from config import get_antigravity_api_url, get_code_assist_endpoint
+
+ storage_adapter = await get_storage_adapter()
+
+ credential_data = await storage_adapter.get_credential(filename, mode=mode)
+ if not credential_data:
+ return False
+
+ credentials = Credentials.from_dict(credential_data)
+
+ token_refreshed = await credentials.refresh_if_needed()
+ if token_refreshed:
+ credential_data = credentials.to_dict()
+ await storage_adapter.store_credential(filename, credential_data, mode=mode)
+
+ if mode == "antigravity":
+ api_base_url = await get_antigravity_api_url()
+ user_agent = "anthropic-vertex/0.1.0"
+ else:
+ api_base_url = await get_code_assist_endpoint()
+ user_agent = "anthropic-vertex/0.1.0"
+
+ project_id = await fetch_project_id(
+ access_token=credentials.access_token,
+ user_agent=user_agent,
+ api_base_url=api_base_url
+ )
+
+ if project_id:
+ credential_data["project_id"] = project_id
+ await storage_adapter.store_credential(filename, credential_data, mode=mode)
+
+ await storage_adapter.update_credential_state(filename, {
+ "disabled": False,
+ "error_codes": []
+ }, mode=mode)
+
+ return True
+ else:
+ return False
+
+ except Exception as e:
+ log.error(f"[自动检验] 检验凭证 {filename} 失败: {e}")
+ return False
+
+ async def trigger_verify_now(self) -> dict:
+ """立即触发一次检验(供API调用)"""
+ try:
+ await self._check_and_verify_credentials()
+ return {"success": True, "message": "检验完成"}
+ except Exception as e:
+ return {"success": False, "message": str(e)}
+
+
+_auto_verify_service: Optional[AutoVerifyService] = None
+
+
+async def get_auto_verify_service() -> AutoVerifyService:
+ """获取自动检验服务实例"""
+ global _auto_verify_service
+ if _auto_verify_service is None:
+ _auto_verify_service = AutoVerifyService()
+ return _auto_verify_service
+
+
+async def start_auto_verify_service():
+ """启动自动检验服务"""
+ service = await get_auto_verify_service()
+ await service.start()
+
+
+async def stop_auto_verify_service():
+ """停止自动检验服务"""
+ service = await get_auto_verify_service()
+ await service.stop()
diff --git a/src/web_routes.py b/src/web_routes.py
index ab272c8d..4657b3e6 100644
--- a/src/web_routes.py
+++ b/src/web_routes.py
@@ -1348,6 +1348,11 @@ async def get_config(token: str = Depends(verify_panel_token)):
current_config["auto_ban_enabled"] = await config.get_auto_ban_enabled()
current_config["auto_ban_error_codes"] = await config.get_auto_ban_error_codes()
+ # 自动检验恢复配置
+ current_config["auto_verify_enabled"] = await config.get_auto_verify_enabled()
+ current_config["auto_verify_interval"] = await config.get_auto_verify_interval()
+ current_config["auto_verify_error_codes"] = await config.get_auto_verify_error_codes()
+
# 429重试配置
current_config["retry_429_max_retries"] = await config.get_retry_429_max_retries()
current_config["retry_429_enabled"] = await config.get_retry_429_enabled()
@@ -1444,6 +1449,23 @@ async def save_config(request: ConfigSaveRequest, token: str = Depends(verify_pa
if not isinstance(new_config["antigravity_stream2nostream"], bool):
raise HTTPException(status_code=400, detail="Antigravity流式转非流式开关必须是布尔值")
+ # 验证自动检验恢复配置
+ if "auto_verify_enabled" in new_config:
+ if not isinstance(new_config["auto_verify_enabled"], bool):
+ raise HTTPException(status_code=400, detail="自动检验恢复开关必须是布尔值")
+
+ if "auto_verify_interval" in new_config:
+ if (
+ not isinstance(new_config["auto_verify_interval"], int)
+ or new_config["auto_verify_interval"] < 60
+ or new_config["auto_verify_interval"] > 3600
+ ):
+ raise HTTPException(status_code=400, detail="自动检验间隔必须是60-3600之间的整数")
+
+ if "auto_verify_error_codes" in new_config:
+ if not isinstance(new_config["auto_verify_error_codes"], list):
+ raise HTTPException(status_code=400, detail="自动检验错误码必须是列表")
+
# 验证服务器配置
if "host" in new_config:
if not isinstance(new_config["host"], str) or not new_config["host"].strip():
@@ -1483,6 +1505,15 @@ async def save_config(request: ConfigSaveRequest, token: str = Depends(verify_pa
# 重新加载配置缓存(关键!)
await config.reload_config()
+ # 如果自动检验配置有变更,重新加载自动检验服务
+ if "auto_verify_enabled" in new_config:
+ try:
+ from .auto_verify import get_auto_verify_service
+ service = await get_auto_verify_service()
+ await service.reload()
+ except Exception as e:
+ log.warning(f"重新加载自动检验服务失败: {e}")
+
# 验证保存后的结果
test_api_password = await config.get_api_password()
test_panel_password = await config.get_panel_password()
@@ -1954,5 +1985,58 @@ async def get_version_info(check_update: bool = False):
})
+# ==================== 自动检验相关接口 ====================
+
+@router.post("/auto-verify/trigger")
+async def trigger_auto_verify(
+ token: str = Depends(verify_panel_token)
+):
+ """
+ 手动触发一次自动检验
+ 立即检查所有凭证状态,对有错误码的凭证执行检验恢复
+ """
+ try:
+ from .auto_verify import get_auto_verify_service
+
+ service = await get_auto_verify_service()
+ result = await service.trigger_verify_now()
+
+ return JSONResponse(content=result)
+
+ except Exception as e:
+ log.error(f"触发自动检验失败: {e}")
+ raise HTTPException(status_code=500, detail=f"触发检验失败: {str(e)}")
+
+
+@router.get("/auto-verify/status")
+async def get_auto_verify_status(
+ token: str = Depends(verify_panel_token)
+):
+ """
+ 获取自动检验服务状态
+ """
+ try:
+ from .auto_verify import get_auto_verify_service
+ from config import (
+ get_auto_verify_enabled,
+ get_auto_verify_interval,
+ get_auto_verify_error_codes
+ )
+
+ service = await get_auto_verify_service()
+
+ return JSONResponse(content={
+ "success": True,
+ "enabled": await get_auto_verify_enabled(),
+ "running": service.is_running,
+ "interval": await get_auto_verify_interval(),
+ "error_codes": await get_auto_verify_error_codes()
+ })
+
+ except Exception as e:
+ log.error(f"获取自动检验状态失败: {e}")
+ raise HTTPException(status_code=500, detail=f"获取状态失败: {str(e)}")
+
+
diff --git a/web.py b/web.py
index ff85c7db..5d05ba23 100644
--- a/web.py
+++ b/web.py
@@ -15,6 +15,7 @@
# Import managers and utilities
from src.credential_manager import CredentialManager
+from src.auto_verify import start_auto_verify_service, stop_auto_verify_service
# Import all routers
from src.router.antigravity.openai import router as antigravity_openai_router
@@ -54,6 +55,12 @@ async def lifespan(app: FastAPI):
log.error(f"凭证管理器初始化失败: {e}")
global_credential_manager = None
+ # 启动自动检验服务
+ try:
+ await start_auto_verify_service()
+ except Exception as e:
+ log.error(f"自动检验服务启动失败: {e}")
+
# OAuth回调服务器将在需要时按需启动
yield
@@ -61,7 +68,14 @@ async def lifespan(app: FastAPI):
# 清理资源
log.info("开始关闭 GCLI2API 主服务")
- # 首先关闭所有异步任务
+ # 首先停止自动检验服务
+ try:
+ await stop_auto_verify_service()
+ log.info("自动检验服务已停止")
+ except Exception as e:
+ log.error(f"停止自动检验服务时出错: {e}")
+
+ # 关闭所有异步任务
try:
await shutdown_all_tasks(timeout=10.0)
log.info("所有异步任务已关闭")