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("所有异步任务已关闭")