Files
cursornew2026/backend/app/tasks.py
huangzhenpc ac19d029da backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查
- 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用)
- 客户端 /api/announcement 改为从数据库读取
- 账号服务重构,新增无感换号、自动分析等功能
- 新增后台任务调度器、数据库迁移脚本
- Schema/Service/Config 全面升级至 v2.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 19:58:05 +08:00

295 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
蜂鸟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()