- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
295 lines
9.4 KiB
Python
295 lines
9.4 KiB
Python
"""
|
||
蜂鸟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()
|