1524 lines
56 KiB
Python
Raw Normal View History

2025-05-11 23:06:57 +08:00
# 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
)