1524 lines
56 KiB
Python
1524 lines
56 KiB
Python
# 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
|
||
) |