""" 客户端 API - 兼容原 CursorPro 插件 """ from fastapi import APIRouter, Depends, Request from sqlalchemy.orm import Session from typing import Optional, List from pydantic import BaseModel from app.database import get_db from app.services import AccountService, KeyService, LogService, GlobalSettingsService from app.schemas import VerifyKeyRequest, VerifyKeyResponse, SwitchAccountRequest, SwitchAccountResponse from app.models import AccountStatus, MembershipType router = APIRouter(prefix="/api", tags=["Client API"]) # ========== 账号数据响应模型 ========== class AccountData(BaseModel): """账号数据 - 写入本地Cursor的数据""" accessToken: str refreshToken: Optional[str] = None workosSessionToken: Optional[str] = None email: str membership_type: str usage_type: Optional[str] = "default" # 机器ID相关 (可选) serviceMachineId: Optional[str] = None machineId: Optional[str] = None macMachineId: Optional[str] = None devDeviceId: Optional[str] = None sqmId: Optional[str] = None machineIdFile: Optional[str] = None # 使用统计 requestCount: Optional[int] = 0 usageAmount: Optional[float] = 0.0 # 额度信息 quota: Optional[int] = None quotaUsed: Optional[int] = None quotaRemaining: Optional[int] = None expireDate: Optional[str] = None class ApiResponse(BaseModel): """通用API响应""" success: bool message: Optional[str] = None error: Optional[str] = None data: Optional[AccountData] = None # ========== 验证和切换 API ========== def build_account_data(account, key) -> AccountData: """构建账号数据对象""" expire_date = key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None return AccountData( accessToken=account.access_token, refreshToken=account.refresh_token, workosSessionToken=account.workos_session_token, email=account.email, membership_type=account.membership_type.value, quota=key.quota, quotaUsed=key.quota_used, quotaRemaining=key.quota - key.quota_used, expireDate=expire_date ) @router.post("/verify") async def verify(request: VerifyKeyRequest, req: Request, db: Session = Depends(get_db)): """验证激活码 (前端调用的路径)""" return await verify_key_impl(request, req, db) @router.post("/verify-key") async def verify_key(request: VerifyKeyRequest, req: Request, db: Session = Depends(get_db)): """验证激活码 (兼容路径)""" return await verify_key_impl(request, req, db) async def verify_key_impl(request: VerifyKeyRequest, req: Request, db: Session): """验证激活码实现""" key = KeyService.get_by_key(db, request.key) if not key: return {"success": False, "valid": False, "error": "激活码不存在"} # 首次激活:设置激活时间和过期时间 KeyService.activate(db, key) # 检查设备限制 if request.device_id: device_ok, device_msg = KeyService.check_device(db, key, request.device_id) if not device_ok: LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message=device_msg) return {"success": False, "valid": False, "error": device_msg} # 检查激活码是否有效 is_valid, message = KeyService.is_valid(key, db) if not is_valid: LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message=message) return {"success": False, "valid": False, "error": message} # 获取当前绑定的账号,或分配新账号 account = None if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) # 只有账号不存在或被禁用/过期才分配新的(IN_USE 状态的账号继续使用) if not account or account.status in (AccountStatus.DISABLED, AccountStatus.EXPIRED): # 分配新账号 account = AccountService.get_available(db, key.membership_type) if not account: LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message="无可用账号") return {"success": False, "valid": False, "error": "暂无可用账号,请稍后重试"} KeyService.bind_account(db, key, account) AccountService.mark_used(db, account, key.id) LogService.log(db, key.id, "verify", account.id, ip_address=req.client.host, success=True) # 返回格式匹配原版插件期望 expire_date = key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None return { "success": True, "valid": True, "expire_date": expire_date, "switch_remaining": key.quota - key.quota_used, "switch_limit": key.quota, "data": build_account_data(account, key) } @router.post("/switch") async def switch(request: SwitchAccountRequest, req: Request, db: Session = Depends(get_db)): """切换账号 (前端调用的路径)""" return await switch_account_impl(request, req, db) @router.post("/switch-account") async def switch_account(request: SwitchAccountRequest, req: Request, db: Session = Depends(get_db)): """切换账号 (兼容路径)""" return await switch_account_impl(request, req, db) async def switch_account_impl(request: SwitchAccountRequest, req: Request, db: Session): """切换账号实现""" key = KeyService.get_by_key(db, request.key) if not key: return ApiResponse(success=False, error="激活码不存在") # 检查设备限制 if request.device_id: device_ok, device_msg = KeyService.check_device(db, key, request.device_id) if not device_ok: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=device_msg) return ApiResponse(success=False, error=device_msg) # 检查激活码是否有效 is_valid, message = KeyService.is_valid(key, db) if not is_valid: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=message) return ApiResponse(success=False, error=message) # 检查换号频率限制 can_switch, switch_msg = KeyService.can_switch(db, key) if not can_switch: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=switch_msg) return ApiResponse(success=False, error=switch_msg) # 释放当前账号 if key.current_account_id: old_account = AccountService.get_by_id(db, key.current_account_id) if old_account: AccountService.release(db, old_account) # 获取新账号 account = AccountService.get_available(db, key.membership_type) if not account: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message="无可用账号") return ApiResponse(success=False, error="暂无可用账号,请稍后重试") # 绑定新账号并扣除额度 KeyService.bind_account(db, key, account) KeyService.use_switch(db, key) AccountService.mark_used(db, account, key.id) LogService.log(db, key.id, "switch", account.id, ip_address=req.client.host, success=True) return ApiResponse( success=True, message="切换成功", data=build_account_data(account, key) ) # ========== 版本 API ========== @router.get("/version") async def get_version(): """获取版本信息""" return { "success": True, "version": "1.0.0", "latestVersion": "1.0.0", "updateUrl": None, "message": None, "forceUpdate": False } # ========== 代理配置 API ========== # 内存存储代理配置 (生产环境应存数据库) proxy_config = { "is_enabled": False, "proxy_url": "" } @router.get("/proxy-config") async def get_proxy_config(): """获取代理配置""" return { "success": True, "data": proxy_config } @router.post("/proxy-config") async def update_proxy_config(config: dict): """更新代理配置""" global proxy_config if "is_enabled" in config: proxy_config["is_enabled"] = config["is_enabled"] if "proxy_url" in config: proxy_config["proxy_url"] = config["proxy_url"] return { "success": True, "message": "代理配置已更新", "data": proxy_config } # ========== 无感换号 API ========== # 内存存储无感配置 (生产环境应存数据库) seamless_config = { "enabled": False, "mode": "auto", "switchThreshold": 10, "accountPool": [], "currentIndex": 0 } @router.get("/seamless/status") async def seamless_status(db: Session = Depends(get_db)): """获取无感换号状态""" return { "success": True, "enabled": seamless_config["enabled"], "message": "无感换号功能已就绪" if seamless_config["enabled"] else "无感换号功能未启用" } @router.get("/seamless/user-status") async def seamless_user_status(key: str = None, userKey: str = None, db: Session = Depends(get_db)): """获取用户切换状态""" # 兼容两种参数名 actual_key = key or userKey if not actual_key: return {"success": False, "valid": False, "error": "缺少激活码参数"} activation_key = KeyService.get_by_key(db, actual_key) if not activation_key: return {"success": False, "valid": False, "error": "激活码不存在"} # 检查是否有效 is_valid, message = KeyService.is_valid(activation_key, db) if not is_valid: return {"success": False, "valid": False, "error": message} # 获取当前绑定的账号 locked_account = None if activation_key.current_account_id: account = AccountService.get_by_id(db, activation_key.current_account_id) if account and account.status not in (AccountStatus.DISABLED, AccountStatus.EXPIRED): locked_account = { "email": account.email, "membership_type": account.membership_type.value } # Pro密钥:使用 quota (积分制) # Auto密钥:使用换号次数限制 is_pro = activation_key.membership_type == MembershipType.PRO switch_remaining = activation_key.quota - activation_key.quota_used if is_pro else 999 # Auto不限积分 return { "success": True, "valid": True, "switchRemaining": switch_remaining, "canSwitch": switch_remaining > 0, "lockedAccount": locked_account, "membershipType": activation_key.membership_type.value, "data": { "canSwitch": True, "quotaRemaining": activation_key.quota - activation_key.quota_used, "switchCount": activation_key.switch_count } } @router.get("/seamless/config") async def get_seamless_config(): """获取无感配置""" return { "success": True, "data": seamless_config } @router.post("/seamless/config") async def update_seamless_config(config: dict): """更新无感配置""" global seamless_config for key in ["enabled", "mode", "switchThreshold", "accountPool", "currentIndex"]: if key in config: seamless_config[key] = config[key] return { "success": True, "message": "配置已更新", "data": seamless_config } @router.post("/seamless/inject") async def inject_seamless(data: dict, req: Request, db: Session = Depends(get_db)): """注入无感模式 - 返回账号数据""" user_key = data.get("user_key") if not user_key: return {"success": False, "error": "缺少user_key参数"} key = KeyService.get_by_key(db, user_key) if not key: return {"success": False, "error": "激活码不存在"} # 检查有效性 is_valid, message = KeyService.is_valid(key, db) if not is_valid: return {"success": False, "error": message} # 获取账号 account = None if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) # 只有账号不存在或被禁用/过期才分配新的 if not account or account.status in (AccountStatus.DISABLED, AccountStatus.EXPIRED): account = AccountService.get_available(db, key.membership_type) if not account: return {"success": False, "error": "暂无可用账号"} KeyService.bind_account(db, key, account) AccountService.mark_used(db, account, key.id) LogService.log(db, key.id, "seamless_inject", account.id, ip_address=req.client.host, success=True) return { "success": True, "message": "无感模式已注入", "data": build_account_data(account, key).model_dump() } @router.post("/seamless/restore") async def restore_seamless(): """恢复无感模式设置""" global seamless_config seamless_config["enabled"] = False return { "success": True, "message": "已恢复默认设置" } @router.get("/seamless/accounts") async def get_seamless_accounts(db: Session = Depends(get_db)): """获取账号池列表""" # 返回可用账号列表 (脱敏) accounts = AccountService.get_all(db, limit=100) account_list = [] for acc in accounts: if acc.status == AccountStatus.ACTIVE: account_list.append({ "id": acc.id, "email": acc.email[:3] + "***" + acc.email[acc.email.index("@"):], "membership_type": acc.membership_type.value, "status": acc.status.value }) return { "success": True, "data": { "accounts": account_list, "total": len(account_list) } } @router.post("/seamless/accounts") async def sync_seamless_accounts(data: dict): """同步账号池""" # 这个接口在我们的架构中不需要实际功能 # 账号由管理后台统一管理 return { "success": True, "message": "账号由管理后台统一管理" } @router.get("/seamless/token") async def get_seamless_token(key: str, db: Session = Depends(get_db)): """获取无感Token""" activation_key = KeyService.get_by_key(db, key) if not activation_key: return {"success": False, "error": "激活码不存在"} account = None if activation_key.current_account_id: account = AccountService.get_by_id(db, activation_key.current_account_id) if not account: return {"success": False, "error": "未绑定账号"} return { "success": True, "data": build_account_data(account, activation_key).model_dump() } @router.get("/seamless/get-token") async def get_seamless_token_v2(userKey: str = None, key: str = None, req: Request = None, db: Session = Depends(get_db)): """获取无感Token (注入代码调用的路径,兼容 userKey 和 key 两种参数名) 返回格式需要直接包含 accessToken、email 等字段,供注入代码使用 """ actual_key = userKey or key if not actual_key: return {"success": False, "error": "缺少激活码参数"} activation_key = KeyService.get_by_key(db, actual_key) if not activation_key: return {"success": False, "error": "激活码不存在"} # 检查有效性 is_valid, message = KeyService.is_valid(activation_key, db) if not is_valid: return {"success": False, "error": message} # 获取或分配账号 account = None is_new = False if activation_key.current_account_id: account = AccountService.get_by_id(db, activation_key.current_account_id) # 只有账号不存在或被禁用/过期才分配新的 if not account or account.status in (AccountStatus.DISABLED, AccountStatus.EXPIRED): # Auto密钥:检查是否允许获取新账号(频率限制) if activation_key.membership_type == MembershipType.FREE: can_switch, switch_msg = KeyService.can_switch(db, activation_key) if not can_switch: return {"success": False, "error": switch_msg} # 分配新账号 account = AccountService.get_available(db, activation_key.membership_type) if not account: return {"success": False, "error": "暂无可用账号"} KeyService.bind_account(db, activation_key, account) AccountService.mark_used(db, account, activation_key.id) # 记录换号(用于频率限制) KeyService.use_switch(db, activation_key) is_new = True # 记录日志 if req: LogService.log(db, activation_key.id, "seamless_get_token", account.id, ip_address=req.client.host, success=True) # 返回格式需要直接包含字段,供注入代码使用 # 注入代码检查: if(d && d.accessToken) { ... } return { "success": True, "accessToken": account.access_token, "refreshToken": account.refresh_token or "", "email": account.email, "membership_type": account.membership_type.value, "switchVersion": activation_key.switch_count, # 用于检测是否需要更新 "switchRemaining": activation_key.quota - activation_key.quota_used, "is_new": is_new, "machineIds": None # 机器ID由客户端本地管理 } @router.post("/seamless/switch") async def switch_seamless_token(data: dict, req: Request, db: Session = Depends(get_db)): """切换无感Token""" user_key = data.get("userKey") if not user_key: return {"switched": False, "message": "缺少userKey参数"} # 复用切换逻辑 request = SwitchAccountRequest(key=user_key) return await switch_token_impl(request, req, db) @router.post("/seamless/switch-token") async def switch_seamless_token_v2(data: dict, req: Request, db: Session = Depends(get_db)): """切换无感Token (client.js 调用的路径)""" user_key = data.get("userKey") if not user_key: return {"switched": False, "message": "缺少userKey参数"} request = SwitchAccountRequest(key=user_key) return await switch_token_impl(request, req, db) async def switch_token_impl(request: SwitchAccountRequest, req: Request, db: Session): """切换Token实现 - 返回插件期望的格式""" key = KeyService.get_by_key(db, request.key) if not key: return {"switched": False, "message": "激活码不存在"} # 检查设备限制 if request.device_id: device_ok, device_msg = KeyService.check_device(db, key, request.device_id) if not device_ok: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=device_msg) return {"switched": False, "message": device_msg} # 检查激活码是否有效 is_valid, message = KeyService.is_valid(key, db) if not is_valid: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=message) return {"switched": False, "message": message} # 检查换号频率限制 can_switch, switch_msg = KeyService.can_switch(db, key) if not can_switch: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message=switch_msg) return {"switched": False, "message": switch_msg} # 释放当前账号 if key.current_account_id: old_account = AccountService.get_by_id(db, key.current_account_id) if old_account: AccountService.release(db, old_account) # 获取新账号 account = AccountService.get_available(db, key.membership_type) if not account: LogService.log(db, key.id, "switch", ip_address=req.client.host, success=False, message="无可用账号") return {"switched": False, "message": "暂无可用账号,请稍后重试"} # 绑定新账号并扣除额度 KeyService.bind_account(db, key, account) KeyService.use_switch(db, key) AccountService.mark_used(db, account, key.id) LogService.log(db, key.id, "switch", account.id, ip_address=req.client.host, success=True) return { "switched": True, "switchRemaining": key.quota - key.quota_used, "email": account.email, "data": build_account_data(account, key) } # ========== 代理配置 API ========== @router.get("/proxy-config") async def get_proxy_config(db: Session = Depends(get_db)): """获取代理配置""" return { "success": True, "enabled": False, "host": "", "port": 0, "username": "", "password": "" } @router.put("/proxy-config") async def update_proxy_config(config: dict, db: Session = Depends(get_db)): """更新代理配置 (客户端本地使用,后端仅返回成功)""" return {"success": True, "message": "代理配置已保存"} # ========== 无感换号配置 API ========== @router.get("/seamless/status") async def get_seamless_status(db: Session = Depends(get_db)): """获取无感换号全局状态""" settings = GlobalSettingsService.get_all(db) account_stats = AccountService.count(db) return { "success": True, "enabled": True, "available_accounts": account_stats.get("active", 0), "total_accounts": account_stats.get("total", 0), "switch_interval_minutes": settings.auto_switch_interval_minutes, "max_switches_per_day": settings.auto_max_switches_per_day } @router.get("/seamless/config") async def get_seamless_config(db: Session = Depends(get_db)): """获取无感换号配置""" settings = GlobalSettingsService.get_all(db) return { "success": True, "auto_switch": True, "switch_interval_minutes": settings.auto_switch_interval_minutes, "max_switches_per_day": settings.auto_max_switches_per_day, "pro_quota_cost": settings.pro_quota_cost } @router.post("/seamless/config") async def update_seamless_config(config: dict, db: Session = Depends(get_db)): """更新无感换号配置""" # 从请求中提取配置 updates = {} if "switch_interval_minutes" in config: updates["auto_switch_interval_minutes"] = config["switch_interval_minutes"] if "max_switches_per_day" in config: updates["auto_max_switches_per_day"] = config["max_switches_per_day"] if "pro_quota_cost" in config: updates["pro_quota_cost"] = config["pro_quota_cost"] if updates: GlobalSettingsService.update_all(db, **updates) return {"success": True, "message": "配置已更新"} @router.post("/seamless/inject") async def seamless_inject(data: dict, db: Session = Depends(get_db)): """注入无感换号代码 (客户端本地操作,后端仅记录)""" user_key = data.get("userKey") if user_key: key = KeyService.get_by_key(db, user_key) if key: LogService.log(db, key.id, "seamless_inject", success=True, message="启用无感换号") return {"success": True, "message": "无感换号已启用"} @router.post("/seamless/restore") async def seamless_restore(data: dict = None, db: Session = Depends(get_db)): """恢复原始代码 (客户端本地操作,后端仅记录)""" if data and data.get("userKey"): key = KeyService.get_by_key(db, data["userKey"]) if key: LogService.log(db, key.id, "seamless_restore", success=True, message="禁用无感换号") return {"success": True, "message": "已恢复原始状态"} @router.get("/seamless/accounts") async def get_seamless_accounts(userKey: str = None, db: Session = Depends(get_db)): """获取可用账号列表 (供管理使用)""" if not userKey: return {"success": False, "error": "缺少 userKey 参数"} key = KeyService.get_by_key(db, userKey) if not key: return {"success": False, "error": "激活码不存在"} # 获取该激活码类型的可用账号数量 accounts = AccountService.get_all(db, limit=100) available_count = sum(1 for a in accounts if a.status == AccountStatus.ACTIVE and a.membership_type == key.membership_type) return { "success": True, "available_count": available_count, "membership_type": key.membership_type.value } @router.post("/seamless/sync-accounts") async def sync_seamless_accounts(data: dict, db: Session = Depends(get_db)): """同步账号 (客户端上报账号信息)""" # 客户端可能上报当前使用的账号状态 return {"success": True, "message": "同步成功"} # ========== 版本检查 API ========== @router.get("/version") async def get_version(): """获取最新版本信息""" return { "success": True, "current_version": "0.4.5", "latest_version": "0.4.5", "has_update": False, "download_url": "", "changelog": "" } # ========== 公告 API ========== @router.get("/announcement") async def get_announcement(db: Session = Depends(get_db)): """获取公告信息""" return { "success": True, "has_announcement": True, "data": { "is_active": True, "title": "欢迎使用蜂鸟Pro", "content": "感谢使用蜂鸟Pro!\n\n如有问题请联系客服。", "type": "info", "created_at": "2024-12-17 00:00:00" } } @router.get("/announcements/latest") async def get_announcements_latest(db: Session = Depends(get_db)): """获取最新公告 (前端调用的路径)""" return { "success": True, "data": { "is_active": True, "title": "欢迎使用蜂鸟Pro", "content": "感谢使用蜂鸟Pro!\n\n如有问题请联系客服。", "type": "info", "created_at": "2024-12-17 00:00:00" } } # ========== 用量查询 API ========== @router.get("/usage") async def get_usage(userKey: str = None, db: Session = Depends(get_db)): """获取用量信息""" if not userKey: return {"success": False, "error": "缺少 userKey 参数"} key = KeyService.get_by_key(db, userKey) if not key: return {"success": False, "error": "激活码不存在"} account = None if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) return { "success": True, "membership_type": key.membership_type.value, "quota": key.quota, "quota_used": key.quota_used, "quota_remaining": key.quota - key.quota_used, "switch_count": key.switch_count, "expire_at": key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None, "current_email": account.email if account else None }