2025-05-11 23:06:57 +08:00

1524 lines
56 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.

# FastAPI 相关导入
from fastapi import FastAPI, HTTPException, status, UploadFile, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse, Response
from fastapi.staticfiles import StaticFiles
# 数据模型和验证相关
from pydantic import BaseModel
from typing import Optional, List
# 数据库相关
from sqlalchemy import select, func, delete, desc
from database import get_session, AccountModel, AccountUsageRecordModel, init_db
# 文件和路径处理
from pathlib import Path
# 时间处理
from datetime import datetime
# 异步和上下文管理
import asyncio
from contextlib import asynccontextmanager
# 系统和环境相关
import os
import traceback
import uvicorn
from dotenv import load_dotenv
# 日志和错误处理
from logger import info, error
# 自定义模块
from cursor_pro_keep_alive import main as register_account
from tokenManager.cursor import Cursor
# 并发和缓存
import concurrent.futures
from functools import lru_cache
# 配置参数
from config import (
MAX_ACCOUNTS,
REGISTRATION_INTERVAL,
API_HOST,
API_PORT,
API_DEBUG,
API_WORKERS,
)
# 其他工具
import json
import time
# 全局状态追踪
registration_status = {
"is_running": False,
"last_run": None,
"last_status": None,
"next_run": None,
"total_runs": 0,
"successful_runs": 0,
"failed_runs": 0,
}
# 定义静态文件目录
static_path = Path(__file__).parent / "static"
static_path.mkdir(exist_ok=True) # 确保目录存在
# 全局任务存储
background_tasks = {"registration_task": None}
# 添加lifespan管理器在应用启动时初始化数据库
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时初始化数据库
await init_db()
info(f'服务已启动! \n 首页地址http://127.0.0.1:{API_PORT}/')
yield
# 关闭时的清理操作
info("应用程序已关闭!")
app = FastAPI(
title="Cursor Account API",
description="API for managing Cursor accounts",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
debug=API_DEBUG,
)
# 挂载静态文件目录
app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Account(BaseModel):
email: str
password: Optional[str] = None
token: str
user: str
usage_limit: Optional[str] = None
created_at: Optional[str] = None
status: str = "active" # 默认为"active"
id: Optional[int] = None # 添加id字段可选
class Config:
from_attributes = True
class AccountResponse(BaseModel):
success: bool
data: Optional[Account] = None
message: str = ""
async def get_active_account_count() -> int:
"""获取当前账号总数"""
async with get_session() as session:
result = await session.execute(
select(func.count())
.select_from(AccountModel)
.where(AccountModel.status == "active")
)
return result.scalar()
async def get_account_count() -> int:
"""获取当前账号总数"""
async with get_session() as session:
result = await session.execute(select(func.count()).select_from(AccountModel))
return result.scalar()
async def run_registration():
"""运行注册脚本"""
global registration_status
browser_manager = None
try:
info("注册任务开始运行")
while registration_status["is_running"]:
try:
count = await get_active_account_count()
info(f"当前数据库已激活账号数: {count}")
if count >= MAX_ACCOUNTS:
# 修改:不再结束任务,而是进入监控模式
info(f"已达到最大账号数量 ({count}/{MAX_ACCOUNTS}),进入监控模式")
registration_status["last_status"] = "monitoring"
# 等待检测间隔时间
next_check = datetime.now().timestamp() + REGISTRATION_INTERVAL
registration_status["next_run"] = next_check
info(f"将在 {REGISTRATION_INTERVAL} 秒后重新检查账号数量")
await asyncio.sleep(REGISTRATION_INTERVAL)
# 跳过当前循环的剩余部分,继续下一次循环检查
continue
info(f"开始注册尝试 (当前账号数: {count}/{MAX_ACCOUNTS})")
registration_status["last_run"] = datetime.now().isoformat()
registration_status["total_runs"] += 1
# 调用注册函数
try:
success = await asyncio.get_event_loop().run_in_executor(
None, register_account
)
if success:
registration_status["successful_runs"] += 1
registration_status["last_status"] = "success"
info("注册成功")
else:
registration_status["failed_runs"] += 1
registration_status["last_status"] = "failed"
info("注册失败")
except SystemExit:
# 捕获 SystemExit 异常,这是注册脚本正常退出的方式
info("注册脚本正常退出")
if registration_status["last_status"] != "error":
registration_status["last_status"] = "completed"
except Exception as e:
error(f"注册过程执行出错: {str(e)}")
error(traceback.format_exc())
registration_status["failed_runs"] += 1
registration_status["last_status"] = "error"
# 更新下次运行时间
next_run = datetime.now().timestamp() + REGISTRATION_INTERVAL
registration_status["next_run"] = next_run
info(f"等待 {REGISTRATION_INTERVAL} 秒后进行下一次尝试")
await asyncio.sleep(REGISTRATION_INTERVAL)
except asyncio.CancelledError:
info("注册迭代被取消")
raise
except Exception as e:
registration_status["failed_runs"] += 1
registration_status["last_status"] = "error"
error(f"注册过程出错: {str(e)}")
error(traceback.format_exc())
if not registration_status["is_running"]:
break
await asyncio.sleep(REGISTRATION_INTERVAL)
except asyncio.CancelledError:
info("注册任务被取消")
raise
except Exception as e:
error(f"注册任务致命错误: {str(e)}")
error(traceback.format_exc())
raise
finally:
registration_status["is_running"] = False
if browser_manager:
try:
browser_manager.quit()
except Exception as e:
error(f"清理浏览器资源时出错: {str(e)}")
error(traceback.format_exc())
@app.get("/", tags=["UI"])
async def serve_index():
"""提供Web UI界面"""
index_path = Path(__file__).parent / "index.html"
return FileResponse(index_path)
@app.get("/general", tags=["General"])
async def root():
"""API根路径返回API信息"""
try:
# 获取当前账号数量和使用情况
async with get_session() as session:
result = await session.execute(select(AccountModel))
accounts = result.scalars().all()
usage_info = []
total_balance = 0
active_accounts = 0
for acc in accounts:
remaining_balance = Cursor.get_remaining_balance(acc.user, acc.token)
remaining_days = Cursor.get_trial_remaining_days(acc.user, acc.token)
if remaining_balance is not None and remaining_balance > 0:
active_accounts += 1
total_balance += remaining_balance
usage_info.append(
{
"email": acc.email,
"balance": remaining_balance,
"days": remaining_days,
"status": (
"active"
if remaining_balance is not None and remaining_balance > 0
else "inactive"
),
}
)
return {
"service": {
"name": "Cursor Account API",
"version": "1.0.0",
"status": "running",
"description": "API for managing Cursor Pro accounts and automatic registration",
},
"statistics": {
"total_accounts": len(accounts),
"active_accounts": active_accounts,
"total_remaining_balance": total_balance,
"max_accounts": MAX_ACCOUNTS,
"remaining_slots": MAX_ACCOUNTS - len(accounts),
"registration_interval": f"{REGISTRATION_INTERVAL} seconds",
},
"accounts_info": usage_info, # 添加账号详细信息
"registration_status": {
"is_running": registration_status["is_running"],
"last_run": registration_status["last_run"],
"last_status": registration_status["last_status"],
"next_run": registration_status["next_run"],
"statistics": {
"total_runs": registration_status["total_runs"],
"successful_runs": registration_status["successful_runs"],
"failed_runs": registration_status["failed_runs"],
"success_rate": (
f"{(registration_status['successful_runs'] / registration_status['total_runs'] * 100):.1f}%"
if registration_status["total_runs"] > 0
else "N/A"
),
},
},
"endpoints": {
"documentation": {"swagger": "/docs", "redoc": "/redoc"},
"health": {
"check": "/health",
"registration_status": "/registration/status",
},
"accounts": {
"list_all": "/accounts",
"random": "/account/random",
"create": {"path": "/account", "method": "POST"},
"delete": {"path": "/account/{email}", "method": "DELETE"},
"usage": {
"path": "/account/{email}/usage",
"method": "GET",
"description": "Get account usage by email",
},
},
"registration": {
"start": {"path": "/registration/start", "method": "GET"},
"stop": {"path": "/registration/stop", "method": "POST"},
"status": {"path": "/registration/status", "method": "GET"},
},
"usage": {"check": {"path": "/usage", "method": "GET"}},
"clean": {
"run": {
"path": "/clean",
"method": "POST",
"params": {"clean_type": ["check", "disable", "delete"]},
}
},
},
"support": {
"github": "https://github.com/Elawen-Carl/cursor-account-api",
"author": "Elawen Carl",
"contact": "elawencarl@gmail.com",
},
"timestamp": datetime.now().isoformat(),
}
except Exception as e:
error(f"根端点错误: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error fetching API information",
)
@app.get("/health", tags=["General"])
async def health_check():
"""健康检查端点"""
return {"status": "healthy"}
@app.get("/accounts", tags=["Accounts"])
async def get_accounts(
page: int = 1,
per_page: int = 10,
search: Optional[str] = None,
sort_by: str = "created_at", # 新增排序字段参数
order: str = "desc" # 新增排序方向参数
):
"""获取所有账号列表,支持分页、搜索和排序"""
try:
async with get_session() as session:
# 验证排序参数
valid_sort_fields = {"id", "email", "created_at", "usage_limit"}
valid_orders = {"asc", "desc"}
if sort_by not in valid_sort_fields:
sort_by = "created_at" # 默认排序字段
if order not in valid_orders:
order = "desc" # 默认排序方向
# 构建基本查询
query = select(AccountModel)
# 应用排序
sort_field = getattr(AccountModel, sort_by)
if order == "desc":
query = query.order_by(desc(sort_field))
else:
query = query.order_by(sort_field)
# 如果有搜索参数
if search:
search = f"%{search}%"
query = query.where(AccountModel.email.like(search))
# 计算总记录数
count_query = select(func.count()).select_from(AccountModel)
if search:
count_query = count_query.where(AccountModel.email.like(search))
total_count = await session.scalar(count_query)
# 计算分页
total_pages = (total_count + per_page - 1) // per_page # 向上取整
# 应用分页
query = query.offset((page - 1) * per_page).limit(per_page)
# 执行查询
result = await session.execute(query)
accounts = result.scalars().all()
# 构建分页响应
return {
"success": True,
"data": accounts,
"pagination": {
"page": page,
"per_page": per_page,
"total_count": total_count,
"total_pages": total_pages
},
"sort": {
"field": sort_by,
"order": order
}
}
except Exception as e:
error(f"获取账号列表失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"获取账号列表失败: {str(e)}",
)
@app.get("/account/random", response_model=AccountResponse, tags=["Accounts"])
async def get_random_account():
"""随机获取一个可用的账号和token"""
try:
async with get_session() as session:
result = await session.execute(
select(AccountModel).order_by(func.random()).limit(1)
)
account = result.scalar_one_or_none()
if not account:
return AccountResponse(success=False, message="No accounts available")
return AccountResponse(success=True, data=Account.from_orm(account))
except Exception as e:
error(f"获取随机账号失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(status_code=500, detail="Internal server error")
@app.post("/account", response_model=AccountResponse, tags=["Accounts"])
async def create_account(account: Account):
"""创建新账号"""
try:
async with get_session() as session:
db_account = AccountModel(
email=account.email,
password=account.password,
token=account.token,
usage_limit=account.usage_limit,
created_at=account.created_at,
)
session.add(db_account)
await session.commit()
return AccountResponse(
success=True, data=account, message="Account created successfully"
)
except Exception as e:
error(f"创建账号失败: {str(e)}")
error(traceback.format_exc())
return AccountResponse(
success=False, message=f"Failed to create account: {str(e)}"
)
@app.delete("/account/{email}", response_model=AccountResponse, tags=["Accounts"])
async def delete_account(email: str, hard_delete: bool = False):
"""删除或停用指定邮箱的账号
如果 hard_delete=True则物理删除账号
否则仅将状态设置为'deleted'
"""
try:
async with get_session() as session:
# 先检查账号是否存在
result = await session.execute(
select(AccountModel).where(AccountModel.email == email)
)
account = result.scalar_one_or_none()
if not account:
return AccountResponse(success=False, message=f"账号 {email} 不存在")
if hard_delete:
# 物理删除账号
await session.execute(
delete(AccountModel).where(AccountModel.email == email)
)
delete_message = f"账号 {email} 已永久删除"
else:
# 逻辑删除:将状态更新为'deleted'
account.status = "deleted"
delete_message = f"账号 {email} 已标记为删除状态"
await session.commit()
return AccountResponse(success=True, message=delete_message)
except Exception as e:
error(f"删除账号失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"删除账号失败: {str(e)}",
)
# 添加状态更新的请求体模型
class StatusUpdate(BaseModel):
status: str
@app.put("/account/id/{id}/status", response_model=AccountResponse, tags=["Accounts"])
async def update_account_status(id: str, update: StatusUpdate):
"""更新账号状态
可选状态: active (正常), disabled (停用), deleted (已删除)
"""
# 使用update.status替代原先的status参数
try:
# 验证状态值
valid_statuses = ["active", "disabled", "deleted"]
if update.status not in valid_statuses:
return AccountResponse(
success=False,
message=f"无效的状态值。允许的值: {', '.join(valid_statuses)}",
)
async with get_session() as session:
# 通过邮箱查询账号
result = await session.execute(
select(AccountModel).where(AccountModel.id == id)
)
account = result.scalar_one_or_none()
if not account:
return AccountResponse(
success=False, message=f"邮箱为 {email} 的账号不存在"
)
# 更新状态
account.status = update.status
await session.commit()
return AccountResponse(
success=True,
message=f"账号 {account.email} 状态已更新为 '{update.status}'",
)
except Exception as e:
error(f"通过邮箱更新账号状态失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"更新账号状态失败: {str(e)}",
)
@app.get("/registration/start", tags=["Registration"])
async def start_registration():
"""手动启动注册任务"""
info("手动启动注册任务")
global background_tasks, registration_status
try:
# 检查是否已达到最大账号数
count = await get_active_account_count()
# 检查任务是否已在运行
if (
background_tasks["registration_task"]
and not background_tasks["registration_task"].done()
):
# 确定当前状态
current_status = (
"monitoring"
if registration_status["last_status"] == "monitoring"
else "running"
)
status_message = (
f"注册任务已在运行中 (状态: {current_status})"
if current_status == "running"
else f"已达到最大账号数量({count}/{MAX_ACCOUNTS}),正在监控账号状态,当账号数量减少时将自动继续注册"
)
info(f"注册请求被忽略 - 任务已在{current_status}状态")
return {
"success": True,
"message": status_message,
"status": {
"is_running": registration_status["is_running"],
"last_run": registration_status["last_run"],
"next_run": (
datetime.fromtimestamp(
registration_status["next_run"]
).isoformat()
if registration_status["next_run"]
else None
),
"last_status": registration_status["last_status"],
"current_count": count,
"max_accounts": MAX_ACCOUNTS,
},
}
# 重置注册状态
registration_status.update(
{
"is_running": True,
"last_status": "starting",
"last_run": datetime.now().isoformat(),
"next_run": datetime.now().timestamp() + REGISTRATION_INTERVAL,
"total_runs": 0,
"successful_runs": 0,
"failed_runs": 0,
}
)
# 创建并启动新任务
loop = asyncio.get_running_loop()
task = loop.create_task(run_registration())
background_tasks["registration_task"] = task
# 添加任务完成回调
def task_done_callback(task):
try:
task.result() # 这将重新引发任何未处理的异常
except asyncio.CancelledError:
info("注册任务被取消")
registration_status["last_status"] = "cancelled"
except Exception as e:
error(f"注册任务失败: {str(e)}")
error(traceback.format_exc())
registration_status["last_status"] = "error"
finally:
if registration_status["is_running"]: # 只有在任务仍在运行时才更新状态
registration_status["is_running"] = False
background_tasks["registration_task"] = None
task.add_done_callback(task_done_callback)
info("手动启动注册任务")
# 等待任务实际开始运行
await asyncio.sleep(1)
# 检查任务是否成功启动
if task.done():
try:
task.result() # 如果任务已完成,检查是否有异常
except Exception as e:
error(f"注册任务启动失败: {str(e)}")
error(traceback.format_exc())
registration_status["is_running"] = False
registration_status["last_status"] = "error"
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start registration task: {str(e)}",
)
# 检查是否已达到最大账号数,预先设置为监控模式
if count >= MAX_ACCOUNTS:
registration_status["last_status"] = "monitoring"
status_message = f"已达到最大账号数量({count}/{MAX_ACCOUNTS}),进入监控模式,当账号数量减少时将自动继续注册"
else:
status_message = "注册任务启动成功"
return {
"success": True,
"message": status_message,
"status": {
"is_running": registration_status["is_running"],
"last_run": registration_status["last_run"],
"next_run": datetime.fromtimestamp(
registration_status["next_run"]
).isoformat(),
"last_status": registration_status["last_status"],
"current_count": count,
"max_accounts": MAX_ACCOUNTS,
},
}
except Exception as e:
error(f"启动注册任务失败: {str(e)}")
error(traceback.format_exc())
registration_status["is_running"] = False
registration_status["last_status"] = "error"
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to start registration task: {str(e)}",
)
@app.get("/registration/stop", tags=["Registration"])
async def stop_registration():
"""手动停止注册任务"""
global background_tasks
try:
if (
not background_tasks["registration_task"]
or background_tasks["registration_task"].done()
):
return {"success": False, "message": "No running registration task found"}
background_tasks["registration_task"].cancel()
try:
await background_tasks["registration_task"]
except asyncio.CancelledError:
info("注册任务被取消")
background_tasks["registration_task"] = None
registration_status["is_running"] = False
registration_status["last_status"] = "manually stopped"
return {"success": True, "message": "Registration task stopped successfully"}
except Exception as e:
error(f"停止注册任务失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to stop registration task: {str(e)}",
)
@app.get("/registration/status", tags=["Registration"])
async def get_registration_status():
"""获取注册状态"""
try:
count = await get_account_count()
active_count = await get_active_account_count() # 添加获取活跃账号数
# 更新任务状态逻辑
if (
background_tasks["registration_task"]
and not background_tasks["registration_task"].done()
):
if registration_status["last_status"] == "monitoring":
task_status = "monitoring" # 新增监控状态
else:
task_status = "running"
else:
task_status = "stopped"
status_info = {
"current_count": count,
"active_count": active_count, # 添加活跃账号数
"max_accounts": MAX_ACCOUNTS,
"is_registration_active": active_count < MAX_ACCOUNTS,
"remaining_slots": MAX_ACCOUNTS - active_count,
"task_status": task_status,
"registration_details": {
"is_running": registration_status["is_running"],
"last_run": registration_status["last_run"],
"last_status": registration_status["last_status"],
"next_run": registration_status["next_run"],
"statistics": {
"total_runs": registration_status["total_runs"],
"successful_runs": registration_status["successful_runs"],
"failed_runs": registration_status["failed_runs"],
"success_rate": (
f"{(registration_status['successful_runs'] / registration_status['total_runs'] * 100):.1f}%"
if registration_status["total_runs"] > 0
else "N/A"
),
},
},
}
# 添加状态解释信息
if task_status == "monitoring":
status_info["status_message"] = (
f"已达到最大账号数量({active_count}/{MAX_ACCOUNTS}),正在监控账号状态,当账号数量减少时将自动继续注册"
)
elif task_status == "running":
status_info["status_message"] = (
f"正在执行注册流程,当前账号数:{active_count}/{MAX_ACCOUNTS}"
)
else:
status_info["status_message"] = "注册任务未运行"
# info(f"请求注册状态 (当前账号数: {count}, 活跃账号数: {active_count}, 状态: {task_status})")
return status_info
except Exception as e:
error(f"获取注册状态失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get registration status: {str(e)}",
)
# 自定义异常处理
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
error(f"HTTP错误发生: {exc.detail}")
return JSONResponse(
status_code=exc.status_code, content={"success": False, "message": exc.detail}
)
@app.exception_handler(Exception)
async def general_exception_handler(request, exc):
error(f"意外错误发生: {str(exc)}")
error(f"错误详情: {traceback.format_exc()}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"success": False,
"message": "Internal server error occurred",
"detail": str(exc) if app.debug else None,
},
)
# 添加缓存装饰器
@lru_cache(maxsize=100)
def get_account_status(user: str, token: str, timestamp: int):
"""缓存10分钟内的账号状态"""
balance = Cursor.get_remaining_balance(user, token)
days = Cursor.get_trial_remaining_days(user, token)
return {
"balance": balance,
"days": days,
"status": "active" if balance is not None and balance > 0 else "inactive",
}
# 修改 check_usage 接口
@app.get("/usage")
async def check_usage():
"""
获取所有账号的使用量信息
该接口会并发查询所有账号的余额和剩余天数,并返回汇总信息
使用了缓存机制避免频繁请求API缓存时间为10分钟
返回:
- total_accounts: 总账号数量
- usage_info: 每个账号的详细使用量信息列表
- summary: 账号状态汇总信息,包括活跃账号数、非活跃账号数和总剩余额度
"""
try:
async with get_session() as session:
# 从数据库获取所有账号
result = await session.execute(select(AccountModel))
accounts = result.scalars().all()
# 使用当前时间的10分钟间隔作为缓存key
# 将时间戳除以600(10分钟)取整,作为缓存标识
cache_timestamp = int(datetime.now().timestamp() / 600)
# 使用线程池并发获取账号状态最多10个并发线程
# 避免串行请求API导致接口响应过慢
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
# 为每个账号创建一个异步任务
futures = [
executor.submit(
get_account_status, acc.user, acc.token, cache_timestamp
)
for acc in accounts
]
# 收集每个账号的使用量信息
usage_info = []
for acc, future in zip(accounts, futures):
status = future.result()
usage_info.append(
{
"email": acc.email,
"usage_limit": status["balance"], # 剩余额度
"remaining_days": status["days"], # 剩余天数
"status": status["status"], # 账号状态
}
)
# 返回汇总信息
return {
"total_accounts": len(accounts), # 总账号数
"usage_info": usage_info, # 详细使用量信息
"summary": {
# 统计活跃账号数量
"active_accounts": sum(
1 for info in usage_info if info["status"] == "active"
),
# 统计非活跃账号数量
"inactive_accounts": sum(
1 for info in usage_info if info["status"] == "inactive"
),
# 计算所有账号的总剩余额度
"total_remaining_balance": sum(
info["usage_limit"] or 0 for info in usage_info
),
},
}
except Exception as e:
# 记录错误日志
error(f"检查使用量失败: {str(e)}")
error(traceback.format_exc())
# 抛出HTTP异常
raise HTTPException(status_code=500, detail=str(e))
@app.get("/account/{email}/usage", tags=["Accounts"])
async def get_account_usage(email: str):
"""根据邮箱查询账户使用量并更新数据库"""
try:
async with get_session() as session:
# 查询指定邮箱的账号
result = await session.execute(
select(AccountModel).where(AccountModel.email == email)
)
account = result.scalar_one_or_none()
if not account:
raise HTTPException(
status_code=404, detail=f"Account with email {email} not found"
)
# 获取账号使用量
remaining_balance = Cursor.get_remaining_balance(
account.user, account.token
)
remaining_days = Cursor.get_trial_remaining_days(
account.user, account.token
)
# 计算总额度和已使用额度
total_limit = 150 # 默认总额度
used_limit = 0
if remaining_balance is not None:
used_limit = total_limit - remaining_balance
if remaining_days is not None and remaining_days == 0:
account.status = "disabled"
# 更新数据库中的usage_limit字段
account.usage_limit = str(remaining_balance)
await session.commit()
db_updated = True
else:
db_updated = False
return {
"success": True,
"email": account.email,
"usage": {
"remaining_balance": remaining_balance,
"total_limit": total_limit,
"used_limit": used_limit,
"remaining_days": remaining_days,
"status": (
"active"
if remaining_balance is not None and remaining_balance > 0
else "inactive"
),
},
"db_updated": db_updated,
"timestamp": datetime.now().isoformat(),
}
except HTTPException:
raise
except Exception as e:
error(f"查询账号使用量失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"Failed to get account usage: {str(e)}"}
# 添加通过ID删除账号的API
@app.delete("/account/id/{id}", response_model=AccountResponse, tags=["Accounts"])
async def delete_account_by_id(id: int, hard_delete: bool = False):
"""通过ID删除或停用账号
如果 hard_delete=True则物理删除账号
否则仅将状态设置为'deleted'
"""
try:
async with get_session() as session:
# 通过ID查询账号
result = await session.execute(
select(AccountModel).where(AccountModel.id == id)
)
account = result.scalar_one_or_none()
if not account:
return AccountResponse(success=False, message=f"ID为 {id} 的账号不存在")
email = account.email # 保存邮箱以在响应中显示
if hard_delete:
# 物理删除账号
await session.execute(delete(AccountModel).where(AccountModel.id == id))
delete_message = f"账号 {email} (ID: {id}) 已永久删除"
else:
# 逻辑删除:将状态更新为'deleted'
account.status = "deleted"
delete_message = f"账号 {email} (ID: {id}) 已标记为删除状态"
await session.commit()
return AccountResponse(success=True, message=delete_message)
except Exception as e:
error(f"通过ID删除账号失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"删除账号失败: {str(e)}",
)
# 添加导出账号列表功能
@app.get("/accounts/export", tags=["Accounts"])
async def export_accounts():
"""导出所有账号为JSON文件"""
try:
async with get_session() as session:
query = select(AccountModel).order_by(desc(AccountModel.created_at))
result = await session.execute(query)
accounts = result.scalars().all()
# 转换为可序列化的数据
accounts_data = []
for account in accounts:
account_dict = {
"id": account.id,
"email": account.email,
"password": account.password,
"token": account.token,
"user": account.user,
"usage_limit": account.usage_limit,
"created_at": account.created_at,
"status": account.status
}
accounts_data.append(account_dict)
# 创建响应
content = json.dumps(accounts_data, ensure_ascii=False, indent=2)
response = Response(content=content)
response.headers["Content-Disposition"] = "attachment; filename=cursor_accounts.json"
response.headers["Content-Type"] = "application/json"
return response
except Exception as e:
error(f"导出账号失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"导出账号失败: {str(e)}",
)
# 添加导入账号列表功能
@app.post("/accounts/import", tags=["Accounts"])
async def import_accounts(file: UploadFile):
"""从JSON文件导入账号"""
try:
# 读取上传的文件内容
content = await file.read()
# 解析JSON数据
try:
accounts_data = json.loads(content.decode("utf-8"))
except json.JSONDecodeError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="无效的JSON文件格式",
)
# 验证数据结构
if not isinstance(accounts_data, list):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="JSON数据必须是账号对象的数组",
)
# 导入计数器
imported = 0
updated = 0
skipped = 0
async with get_session() as session:
for account_data in accounts_data:
# 提取必要字段,提供默认值
email = account_data.get("email")
if not email:
skipped += 1
continue
# 检查账号是否已存在
query = select(AccountModel).where(AccountModel.email == email)
result = await session.execute(query)
existing_account = result.scalar_one_or_none()
if existing_account:
# 更新现有账号
existing_account.password = account_data.get("password", existing_account.password)
existing_account.token = account_data.get("token", existing_account.token)
existing_account.user = account_data.get("user", existing_account.user)
existing_account.usage_limit = account_data.get("usage_limit", existing_account.usage_limit)
existing_account.status = account_data.get("status", existing_account.status)
updated += 1
else:
# 创建新账号
new_account = AccountModel(
id=account_data.get("id", int(time.time() * 1000)),
email=email,
password=account_data.get("password", ""),
token=account_data.get("token", ""),
user=account_data.get("user", ""),
usage_limit=account_data.get("usage_limit", ""),
created_at=account_data.get("created_at", datetime.now().strftime("%Y-%m-%d %H:%M")),
status=account_data.get("status", "active")
)
session.add(new_account)
imported += 1
# 提交所有更改
await session.commit()
return {
"success": True,
"message": f"导入完成: 新增 {imported} 个账号, 更新 {updated} 个账号, 跳过 {skipped} 个无效记录",
}
except HTTPException:
raise
except Exception as e:
error(f"导入账号失败: {str(e)}")
error(traceback.format_exc())
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"导入账号失败: {str(e)}",
)
# 添加"使用Token"功能
@app.post("/account/use-token/{id}", tags=["Accounts"])
async def use_account_token(id: int, request: Request, reset_machine_id: bool = False):
"""使用指定账号的Token更新Cursor认证可选是否重置机器ID"""
try:
async with get_session() as session:
# 通过ID查询账号
result = await session.execute(
select(AccountModel).where(AccountModel.id == id)
)
account = result.scalar_one_or_none()
if not account:
return {"success": False, "message": f"ID为 {id} 的账号不存在"}
# 调用CursorAuthManager更新认证
from cursor_auth_manager import CursorAuthManager
auth_manager = CursorAuthManager()
success = auth_manager.update_auth(
email=account.email,
access_token=account.token,
refresh_token=account.token
)
# 仅在用户选择时才重置机器ID
patch_success = False
if reset_machine_id:
from cursor_shadow_patcher import CursorShadowPatcher
resetter = CursorShadowPatcher()
patch_success = resetter.reset_machine_ids()
# 更新返回消息
if success and reset_machine_id and patch_success:
return {
"success": True,
"message": f"成功使用账号 {account.email} 的Token并重置了机器ID",
}
elif success and reset_machine_id and not patch_success:
return {
"success": True,
"message": f"成功使用账号 {account.email} 的Token但机器ID重置失败",
}
elif success:
return {
"success": True,
"message": f"成功使用账号 {account.email} 的Token未重置机器ID",
}
else:
return {"success": False, "message": "Token更新失败"}
except Exception as e:
error(f"使用账号Token失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"使用Token失败: {str(e)}"}
# 添加获取账号使用记录接口
@app.get("/account/{id}/usage-records", tags=["Accounts"])
async def get_account_usage_records(id: int):
"""获取指定账号的使用记录"""
try:
async with get_session() as session:
# 通过ID查询账号
result = await session.execute(
select(AccountModel).where(AccountModel.id == id)
)
account = result.scalar_one_or_none()
if not account:
return {"success": False, "message": f"ID为 {id} 的账号不存在"}
# 查询使用记录
result = await session.execute(
select(AccountUsageRecordModel)
.where(AccountUsageRecordModel.account_id == id)
.order_by(desc(AccountUsageRecordModel.created_at))
)
records = result.scalars().all()
# 转换记录为字典列表
records_list = []
for record in records:
records_list.append({
"id": record.id,
"account_id": record.account_id,
"email": record.email,
"ip": record.ip,
"user_agent": record.user_agent,
"created_at": record.created_at
})
return {
"success": True,
"records": records_list
}
except Exception as e:
error(f"获取账号使用记录失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"获取账号使用记录失败: {str(e)}"}
# 添加"重置设备id"功能
@app.get("/reset-machine", tags=["System"])
async def reset_machine():
"""重置设备id"""
try:
# 重置Cursor的机器ID
from cursor_shadow_patcher import CursorShadowPatcher
resetter = CursorShadowPatcher()
patch_success = resetter.reset_machine_ids()
if patch_success:
return {
"success": True,
"message": f"成功重置了机器ID",
}
else:
return {"success": False, "message": "重置机器ID失败"}
except Exception as e:
error(f"重置机器ID失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"重置机器ID失败: {str(e)}"}
# 添加配置相关模型
class ConfigModel(BaseModel):
BROWSER_HEADLESS: bool
DYNAMIC_USERAGENT: Optional[bool] = False
BROWSER_USER_AGENT: str
MAX_ACCOUNTS: int
EMAIL_DOMAINS: str
EMAIL_USERNAME: str
EMAIL_PIN: str
BROWSER_PATH: Optional[str] = None
CURSOR_PATH: Optional[str] = None
USE_PROXY: Optional[bool] = False
PROXY_TYPE: Optional[str] = None
PROXY_HOST: Optional[str] = None
PROXY_PORT: Optional[str] = None
PROXY_TIMEOUT: Optional[int] = None
PROXY_USERNAME: Optional[str] = None
PROXY_PASSWORD: Optional[str] = None
EMAIL_CODE_TYPE: str = "AUTO" # 默认值为AUTO
# 获取配置端点
@app.get("/config", tags=["Config"])
async def get_config():
"""获取当前系统配置"""
try:
# 重新加载配置以确保获取最新值
load_dotenv()
config = {
"BROWSER_HEADLESS": os.getenv("BROWSER_HEADLESS", "True").lower() == "true",
"BROWSER_USER_AGENT": os.getenv("BROWSER_USER_AGENT", ""),
"MAX_ACCOUNTS": int(os.getenv("MAX_ACCOUNTS", "10")),
"EMAIL_TYPE": os.getenv("EMAIL_TYPE", "tempemail"),
"EMAIL_PROXY_ENABLED": os.getenv("EMAIL_PROXY_ENABLED", "False").lower() == "true",
"EMAIL_PROXY_ADDRESS": os.getenv("EMAIL_PROXY_ADDRESS", ""),
"EMAIL_API": os.getenv("EMAIL_API", ""),
"EMAIL_DOMAINS": os.getenv("EMAIL_DOMAINS", ""),
"EMAIL_USERNAME": os.getenv("EMAIL_USERNAME", ""),
"EMAIL_DOMAIN": os.getenv("EMAIL_DOMAIN", ""),
"EMAIL_PIN": os.getenv("EMAIL_PIN", ""),
"EMAIL_CODE_TYPE": os.getenv("EMAIL_CODE_TYPE", "AUTO"),
"BROWSER_PATH": os.getenv("BROWSER_PATH", ""),
"CURSOR_PATH": os.getenv("CURSOR_PATH", ""),
"DYNAMIC_USERAGENT": os.getenv("DYNAMIC_USERAGENT", "False").lower() == "true",
# 添加代理配置
"USE_PROXY": os.getenv("USE_PROXY", "False"),
"PROXY_TYPE": os.getenv("PROXY_TYPE", "http"),
"PROXY_HOST": os.getenv("PROXY_HOST", ""),
"PROXY_PORT": os.getenv("PROXY_PORT", ""),
"PROXY_TIMEOUT": os.getenv("PROXY_TIMEOUT", "10"),
"PROXY_USERNAME": os.getenv("PROXY_USERNAME", ""),
"PROXY_PASSWORD": os.getenv("PROXY_PASSWORD", ""),
}
return {"success": True, "data": config}
except Exception as e:
error(f"获取配置失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"获取配置失败: {str(e)}"}
# 更新配置端点
@app.post("/config", tags=["Config"])
async def update_config(config: ConfigModel):
"""更新配置"""
try:
# 获取.env文件路径
env_path = Path(__file__).parent / ".env"
# 读取当前.env文件内容
current_lines = []
if env_path.exists():
with open(env_path, "r", encoding="utf-8") as f:
current_lines = f.readlines()
# 构建配置字典 - 修正直接使用模型属性而非get方法
config_dict = {
"BROWSER_HEADLESS": str(config.BROWSER_HEADLESS),
"DYNAMIC_USERAGENT": str(config.DYNAMIC_USERAGENT),
"BROWSER_USER_AGENT": config.BROWSER_USER_AGENT,
"MAX_ACCOUNTS": str(config.MAX_ACCOUNTS),
"EMAIL_DOMAINS": config.EMAIL_DOMAINS,
"EMAIL_USERNAME": config.EMAIL_USERNAME,
"EMAIL_PIN": config.EMAIL_PIN,
"EMAIL_CODE_TYPE": config.EMAIL_CODE_TYPE,
# 添加代理配置
"USE_PROXY": str(config.USE_PROXY),
"PROXY_TYPE": config.PROXY_TYPE,
"PROXY_HOST": config.PROXY_HOST,
"PROXY_PORT": config.PROXY_PORT,
"PROXY_TIMEOUT": str(config.PROXY_TIMEOUT),
"PROXY_USERNAME": config.PROXY_USERNAME,
"PROXY_PASSWORD": config.PROXY_PASSWORD,
}
# 添加可选配置(如果提供)
if config.BROWSER_PATH:
config_dict["BROWSER_PATH"] = config.BROWSER_PATH
if config.CURSOR_PATH:
config_dict["CURSOR_PATH"] = config.CURSOR_PATH
# 处理现有行或创建新行
updated_lines = []
updated_keys = set()
for line in current_lines:
line = line.strip()
if not line or line.startswith("#"):
updated_lines.append(line)
continue
key, value = line.split("=", 1) if "=" in line else (line, "")
key = key.strip()
if key in config_dict:
updated_lines.append(f"{key}={config_dict[key]}")
updated_keys.add(key)
else:
updated_lines.append(line)
# 添加未更新的配置项
for key, value in config_dict.items():
if key not in updated_keys and value:
updated_lines.append(f"{key}={value}")
# 写入更新后的配置
with open(env_path, "w", encoding="utf-8") as f:
f.write("\n".join(updated_lines))
# 重新加载环境变量
load_dotenv(override=True)
return {"success": True, "message": "配置已更新"}
except Exception as e:
error(f"更新配置失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"更新配置失败: {str(e)}"}
# 优化重启API功能解决卡住问题
@app.post("/restart", tags=["System"])
async def restart_service():
"""重启应用服务"""
try:
info("收到重启服务请求")
# 使用更简单的重启方法 - 通过reload环境变量触发uvicorn重载
# 1. 修改.env文件添加/更新一个时间戳,触发热重载
env_path = os.path.join(os.getcwd(), ".env")
timestamp = str(int(time.time()))
# 读取现有.env文件
if os.path.exists(env_path):
with open(env_path, "r", encoding="utf-8") as f:
env_lines = f.read().splitlines()
else:
env_lines = []
# 更新或添加RESTART_TIMESTAMP变量
timestamp_found = False
for i, line in enumerate(env_lines):
if line.startswith("RESTART_TIMESTAMP="):
env_lines[i] = f"RESTART_TIMESTAMP={timestamp}"
timestamp_found = True
break
if not timestamp_found:
env_lines.append(f"RESTART_TIMESTAMP={timestamp}")
# 写回.env文件
with open(env_path, "w", encoding="utf-8") as f:
f.write("\n".join(env_lines))
info(f"已更新重启时间戳: {timestamp}")
# 仅提示用户手动刷新页面
return {
"success": True,
"message": "配置已更新,请手动刷新页面以应用更改。"
}
except Exception as e:
error(f"重启服务失败: {str(e)}")
error(traceback.format_exc())
return {"success": False, "message": f"重启服务失败: {str(e)}"}
@app.post("/update_all_usage", tags=["Accounts"])
async def update_all_accounts_usage():
"""更新所有账户的使用量信息"""
try:
async with get_session() as session:
# 查询所有账号
result = await session.execute(select(AccountModel))
accounts = result.scalars().all()
updated_count = 0
error_count = 0
for account in accounts:
try:
# 获取账号使用量
remaining_balance = Cursor.get_remaining_balance(
account.user, account.token
)
remaining_days = Cursor.get_trial_remaining_days(
account.user, account.token
)
# 更新数据库
if remaining_balance is not None:
total_limit = 150 # 默认总额度
used_limit = total_limit - remaining_balance
if remaining_days is not None and remaining_days == 0:
account.status = "disabled"
# 更新数据库中的usage_limit字段
account.usage_limit = str(remaining_balance)
updated_count += 1
else:
error_count += 1
except Exception as e:
error_count += 1
logger.error(f"更新账户 {account.email} 失败: {str(e)}")
# 提交所有更改
await session.commit()
return {
"success": True,
"message": f"成功更新 {updated_count} 个账户,失败 {error_count}"
}
except Exception as e:
logger.error(f"批量更新账户余量失败: {str(e)}")
return {"success": False, "message": str(e)}
if __name__ == "__main__":
uvicorn.run(
"api:app",
host=API_HOST,
port=API_PORT,
reload=API_DEBUG,
access_log=True,
log_level="error",
workers=API_WORKERS,
loop="asyncio", # Windows下使用默认的asyncio
)