""" 蜂鸟Pro 后台定时任务 v2.1 - 账号分析任务:定期从 Cursor API 获取账号用量数据 - 自动换号任务:检查用量超阈值的账号并自动换号 """ import asyncio import logging from datetime import datetime, timedelta from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger from app.database import SessionLocal from app.services import ( AccountService, KeyService, GlobalSettingsService, analyze_account_from_token ) from app.models.models import AccountStatus, KeyStatus, KeyMembershipType # 配置日志 logger = logging.getLogger("tasks") logger.setLevel(logging.INFO) # 调度器 scheduler = AsyncIOScheduler() async def analyze_accounts_task(): """ 账号分析任务 定期扫描 pending/available 状态的账号,从 Cursor API 获取最新用量数据 执行频率:每 5 分钟 """ db = SessionLocal() try: # 检查开关 enabled = GlobalSettingsService.get(db, "auto_analyze_enabled") if not enabled or str(enabled).strip().lower() not in ("true", "1", "yes", "y", "on"): logger.debug("[账号分析] 自动分析已关闭,跳过") return logger.info("[账号分析] 开始执行...") # 获取需要分析的账号 accounts = AccountService.get_pending_accounts(db, limit=10) if not accounts: logger.info("[账号分析] 无需分析的账号") return logger.info(f"[账号分析] 发现 {len(accounts)} 个待分析账号") success_count = 0 fail_count = 0 for account in accounts: try: # 调用 Cursor API 分析账号 analysis_data = await analyze_account_from_token(account.token) # 更新账号信息 AccountService.update_from_analysis(db, account.id, analysis_data) if analysis_data.get("success"): success_count += 1 logger.info( f"[账号分析] {account.email} 分析成功: " f"类型={analysis_data.get('account_type')}, " f"用量={analysis_data.get('usage_percent')}%" ) else: fail_count += 1 logger.warning( f"[账号分析] {account.email} 分析失败: {analysis_data.get('error')}" ) # 避免请求过于频繁 await asyncio.sleep(1) except Exception as e: fail_count += 1 logger.error(f"[账号分析] {account.email} 异常: {str(e)}") logger.info(f"[账号分析] 完成: 成功 {success_count}, 失败 {fail_count}") except Exception as e: logger.error(f"[账号分析] 任务异常: {str(e)}") finally: db.close() async def auto_switch_task(): """ 自动换号任务 检查已启用无感换号的密钥,如果当前账号用量超阈值则自动换号 执行频率:每 10 分钟 """ db = SessionLocal() try: # 检查开关 enabled = GlobalSettingsService.get(db, "auto_switch_enabled") if not enabled or str(enabled).strip().lower() not in ("true", "1", "yes", "y", "on"): logger.debug("[自动换号] 自动换号已关闭,跳过") return logger.info("[自动换号] 开始执行...") # 获取阈值设置 auto_threshold = GlobalSettingsService.get_int(db, "auto_switch_threshold") or 98 pro_threshold = GlobalSettingsService.get_int(db, "pro_switch_threshold") or 98 # 查找已启用无感的活跃密钥 from app.models.models import ActivationKey, CursorAccount active_keys = db.query(ActivationKey).filter( ActivationKey.status == KeyStatus.ACTIVE, ActivationKey.seamless_enabled == True, ActivationKey.current_account_id != None, ActivationKey.master_key_id == None # 只处理主密钥 ).all() if not active_keys: logger.info("[自动换号] 无需处理的密钥") return logger.info(f"[自动换号] 检查 {len(active_keys)} 个密钥") switch_count = 0 for key in active_keys: try: # 获取当前账号 account = AccountService.get_by_id(db, key.current_account_id) if not account: continue # 确定阈值 threshold = auto_threshold if key.membership_type == KeyMembershipType.AUTO else pro_threshold # 检查是否需要换号 usage_percent = float(account.usage_percent) if account.usage_percent else 0 if usage_percent < threshold: continue logger.info( f"[自动换号] 密钥 {key.key[:8]}**** 账号 {account.email} " f"用量 {usage_percent}% >= {threshold}%, 触发换号" ) # 执行换号 success, message, new_account = KeyService.switch_account(db, key) if success: switch_count += 1 logger.info( f"[自动换号] 换号成功: {account.email} -> {new_account.email}" ) # 记录日志 from app.services import LogService LogService.log( db, key.id, "auto_switch", account_id=new_account.id, success=True, message=f"自动换号: {account.email} -> {new_account.email}", usage_snapshot={ "old_account": account.to_dict(), "new_account": new_account.to_dict(), "trigger_usage_percent": usage_percent, "threshold": threshold } ) else: logger.warning(f"[自动换号] 换号失败: {message}") except Exception as e: logger.error(f"[自动换号] 密钥 {key.key[:8]}**** 处理异常: {str(e)}") logger.info(f"[自动换号] 完成: 换号 {switch_count} 次") except Exception as e: logger.error(f"[自动换号] 任务异常: {str(e)}") finally: db.close() async def cleanup_expired_keys_task(): """ 清理过期密钥任务 将过期的密钥状态更新为 expired,释放关联的账号 执行频率:每小时 """ db = SessionLocal() try: logger.info("[清理过期] 开始执行...") from app.models.models import ActivationKey # 查找需要检查的活跃密钥 active_keys = db.query(ActivationKey).filter( ActivationKey.status == KeyStatus.ACTIVE ).all() expired_count = 0 for key in active_keys: if key.is_expired: # 释放账号 if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) if account: AccountService.release_account(db, account) # 更新状态 key.status = KeyStatus.EXPIRED key.seamless_enabled = False key.current_account_id = None db.commit() expired_count += 1 logger.info(f"[清理过期] 密钥 {key.key[:8]}**** 已过期") logger.info(f"[清理过期] 完成: 处理 {expired_count} 个过期密钥") except Exception as e: logger.error(f"[清理过期] 任务异常: {str(e)}") finally: db.close() async def init_global_settings_task(): """ 初始化全局设置任务 确保所有默认设置都存在 执行频率:启动时执行一次 """ db = SessionLocal() try: logger.info("[初始化设置] 开始执行...") GlobalSettingsService.init_settings(db) logger.info("[初始化设置] 完成") except Exception as e: logger.error(f"[初始化设置] 任务异常: {str(e)}") finally: db.close() def start_scheduler(): """启动调度器""" # 添加定时任务 scheduler.add_job( analyze_accounts_task, trigger=IntervalTrigger(minutes=5), id="analyze_accounts", name="账号分析任务", replace_existing=True ) scheduler.add_job( auto_switch_task, trigger=IntervalTrigger(minutes=10), id="auto_switch", name="自动换号任务", replace_existing=True ) scheduler.add_job( cleanup_expired_keys_task, trigger=IntervalTrigger(hours=1), id="cleanup_expired", name="清理过期密钥任务", replace_existing=True ) # 启动调度器 scheduler.start() logger.info("[调度器] 后台任务调度器已启动") def stop_scheduler(): """停止调度器""" if scheduler.running: scheduler.shutdown() logger.info("[调度器] 后台任务调度器已停止") async def run_startup_tasks(): """运行启动任务""" await init_global_settings_task() # 启动后立即执行一次账号分析 await analyze_accounts_task()