- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
785 lines
27 KiB
Python
785 lines
27 KiB
Python
"""
|
||
蜂鸟Pro 客户端 API v2.1
|
||
基于系统设计文档重构
|
||
"""
|
||
import logging
|
||
from fastapi import APIRouter, Depends, Request
|
||
from sqlalchemy.orm import Session
|
||
from typing import Optional, Dict, Any
|
||
from pydantic import BaseModel
|
||
from datetime import datetime, timedelta
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
from app.database import get_db
|
||
from app.services import (
|
||
AccountService, KeyService, LogService, GlobalSettingsService,
|
||
cursor_usage_service, analyze_account_from_token
|
||
)
|
||
from app.models.models import (
|
||
AccountStatus, AccountType, KeyMembershipType, KeyStatus,
|
||
CursorAccount, ActivationKey, Announcement
|
||
)
|
||
|
||
router = APIRouter(prefix="/api", tags=["Client API"])
|
||
|
||
|
||
# ==================== 请求/响应模型 ====================
|
||
|
||
class ActivateRequest(BaseModel):
|
||
"""激活请求"""
|
||
key: str
|
||
device_id: Optional[str] = None
|
||
|
||
|
||
class EnableSeamlessRequest(BaseModel):
|
||
"""启用无感换号请求"""
|
||
key: str
|
||
device_id: str
|
||
|
||
|
||
class DisableSeamlessRequest(BaseModel):
|
||
"""禁用无感换号请求"""
|
||
key: str
|
||
|
||
|
||
class SwitchRequest(BaseModel):
|
||
"""换号请求"""
|
||
key: str
|
||
|
||
|
||
# ==================== 辅助函数 ====================
|
||
|
||
def build_key_response(key: ActivationKey, include_account: bool = False) -> Dict[str, Any]:
|
||
"""构建密钥响应数据"""
|
||
data = {
|
||
"key": key.key[:8] + "****" + key.key[-4:],
|
||
"status": key.status.value if key.status else None,
|
||
"membership_type": key.membership_type.value if key.membership_type else None,
|
||
"seamless_enabled": key.seamless_enabled,
|
||
"switch_count": key.switch_count,
|
||
"first_activated_at": key.first_activated_at.isoformat() if key.first_activated_at else None,
|
||
"last_active_at": key.last_active_at.isoformat() if key.last_active_at else None,
|
||
}
|
||
|
||
# Auto密钥信息
|
||
if key.membership_type == KeyMembershipType.AUTO:
|
||
data["expire_at"] = key.expire_at.isoformat() if key.expire_at else None
|
||
if key.expire_at:
|
||
delta = key.expire_at - datetime.now()
|
||
data["days_remaining"] = max(0, delta.days)
|
||
else:
|
||
data["days_remaining"] = key.duration_days
|
||
|
||
# Pro密钥信息
|
||
if key.membership_type == KeyMembershipType.PRO:
|
||
data["quota"] = key.quota
|
||
data["quota_used"] = key.quota_used
|
||
data["quota_remaining"] = key.quota_remaining
|
||
|
||
return data
|
||
|
||
|
||
def build_account_response(account: CursorAccount) -> Dict[str, Any]:
|
||
"""构建账号响应数据"""
|
||
return {
|
||
"email": account.email,
|
||
"token": account.token,
|
||
"access_token": account.access_token,
|
||
"refresh_token": account.refresh_token,
|
||
"workos_session_token": account.workos_session_token,
|
||
"account_type": account.account_type.value if account.account_type else None,
|
||
"membership_type": account.membership_type,
|
||
"trial_days_remaining": account.trial_days_remaining,
|
||
"usage_percent": float(account.usage_percent) if account.usage_percent else 0,
|
||
"usage_limit": account.usage_limit,
|
||
"usage_used": account.usage_used,
|
||
"usage_remaining": account.usage_remaining,
|
||
"total_requests": account.total_requests,
|
||
"total_cost_usd": account.total_cost_usd,
|
||
"last_analyzed_at": account.last_analyzed_at.isoformat() if account.last_analyzed_at else None
|
||
}
|
||
|
||
|
||
# ==================== 核心 API ====================
|
||
|
||
@router.post("/activate")
|
||
async def activate(request: ActivateRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
激活密钥 (不分配账号)
|
||
|
||
流程:
|
||
1. 验证密钥
|
||
2. 处理设备绑定
|
||
3. 处理密钥合并 (如果该设备已有同类型主密钥)
|
||
4. 返回密钥信息 (不包含账号)
|
||
"""
|
||
requested_key = request.key
|
||
key = KeyService.get_by_key(db, request.key)
|
||
if not key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 激活密钥
|
||
success, message, active_key = KeyService.activate(db, key, request.device_id)
|
||
|
||
if not success:
|
||
LogService.log(
|
||
db, key.id, "activate",
|
||
ip_address=req.client.host if req.client else None,
|
||
device_id=request.device_id,
|
||
success=False,
|
||
message=message
|
||
)
|
||
code = "KEY_MERGED" if "已合并" in message else "ACTIVATION_FAILED"
|
||
return {"success": False, "error": message, "code": code}
|
||
|
||
# 记录日志
|
||
LogService.log(
|
||
db, active_key.id, "activate",
|
||
ip_address=req.client.host if req.client else None,
|
||
device_id=request.device_id,
|
||
success=True,
|
||
message=message
|
||
)
|
||
|
||
# 构建兼容前端的扁平响应
|
||
merged = requested_key != active_key.key
|
||
response = {
|
||
"success": True,
|
||
"valid": True,
|
||
"message": message,
|
||
"key": active_key.key,
|
||
"merged": merged,
|
||
"original_key": requested_key if merged else None,
|
||
"master_key": active_key.key if merged else None,
|
||
"membership_type": active_key.membership_type.value if active_key.membership_type else "pro",
|
||
"status": active_key.status.value if active_key.status else None,
|
||
"seamless_enabled": active_key.seamless_enabled,
|
||
"switch_count": active_key.switch_count,
|
||
}
|
||
|
||
# Auto密钥信息
|
||
if active_key.membership_type == KeyMembershipType.AUTO:
|
||
response["expire_date"] = active_key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if active_key.expire_at else None
|
||
if active_key.expire_at:
|
||
delta = active_key.expire_at - datetime.now()
|
||
response["days_remaining"] = max(0, delta.days)
|
||
else:
|
||
response["days_remaining"] = active_key.duration_days
|
||
response["switch_remaining"] = 999 # Auto无限换号
|
||
|
||
# Pro密钥信息
|
||
if active_key.membership_type == KeyMembershipType.PRO:
|
||
response["quota"] = active_key.quota
|
||
response["quota_used"] = active_key.quota_used
|
||
response["switch_remaining"] = active_key.quota_remaining
|
||
|
||
# 合并信息
|
||
response["merged_count"] = active_key.merged_count
|
||
if active_key.master_key_id:
|
||
master = KeyService.get_by_id(db, active_key.master_key_id)
|
||
response["master_key"] = master.key[:8] + "****" if master else None
|
||
|
||
return response
|
||
|
||
|
||
@router.post("/enable-seamless")
|
||
async def enable_seamless(request: EnableSeamlessRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
启用无感换号并分配账号
|
||
|
||
前置条件: 密钥已激活 (status=active)
|
||
流程:
|
||
1. 验证密钥状态
|
||
2. 根据密钥类型选择账号池
|
||
3. 分配并锁定账号
|
||
4. 返回账号信息
|
||
"""
|
||
key = KeyService.get_by_key(db, request.key)
|
||
if not key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if key.master_key_id:
|
||
key = KeyService.get_by_id(db, key.master_key_id)
|
||
if not key:
|
||
return {"success": False, "error": "主密钥不存在", "code": "INVALID_KEY"}
|
||
|
||
# 启用无感换号
|
||
success, message, account = KeyService.enable_seamless(db, key, request.device_id)
|
||
|
||
if not success:
|
||
LogService.log(
|
||
db, key.id, "enable_seamless",
|
||
ip_address=req.client.host if req.client else None,
|
||
device_id=request.device_id,
|
||
success=False,
|
||
message=message
|
||
)
|
||
return {"success": False, "error": message, "code": "ENABLE_FAILED"}
|
||
|
||
# 记录日志
|
||
LogService.log(
|
||
db, key.id, "enable_seamless",
|
||
account_id=account.id,
|
||
ip_address=req.client.host if req.client else None,
|
||
device_id=request.device_id,
|
||
success=True,
|
||
message="无感换号已启用"
|
||
)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "无感换号已启用",
|
||
"data": {
|
||
"key_info": build_key_response(key),
|
||
"account": build_account_response(account)
|
||
}
|
||
}
|
||
|
||
|
||
@router.post("/disable-seamless")
|
||
async def disable_seamless(request: DisableSeamlessRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
禁用无感换号
|
||
|
||
流程:
|
||
1. 释放当前账号
|
||
2. 清除无感状态
|
||
"""
|
||
key = KeyService.get_by_key(db, request.key)
|
||
if not key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if key.master_key_id:
|
||
key = KeyService.get_by_id(db, key.master_key_id)
|
||
|
||
success, message = KeyService.disable_seamless(db, key)
|
||
|
||
LogService.log(
|
||
db, key.id, "disable_seamless",
|
||
ip_address=req.client.host if req.client else None,
|
||
success=success,
|
||
message=message
|
||
)
|
||
|
||
return {
|
||
"success": success,
|
||
"message": message
|
||
}
|
||
|
||
|
||
@router.post("/switch")
|
||
async def switch_account(request: SwitchRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
手动换号
|
||
|
||
前置条件: 已启用无感换号 (seamless_enabled=true)
|
||
流程:
|
||
1. 检查换号条件
|
||
2. 释放当前账号
|
||
3. 分配新账号
|
||
4. Pro密钥扣除积分
|
||
"""
|
||
key = KeyService.get_by_key(db, request.key)
|
||
if not key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if key.master_key_id:
|
||
key = KeyService.get_by_id(db, key.master_key_id)
|
||
|
||
success, message, new_account = KeyService.switch_account(db, key)
|
||
|
||
if not success:
|
||
return {"success": False, "error": message, "code": "SWITCH_FAILED"}
|
||
|
||
# 计算下次可换号时间
|
||
next_switch_at = None
|
||
if key.membership_type == KeyMembershipType.AUTO:
|
||
interval_minutes = GlobalSettingsService.get_int(db, "auto_switch_interval") or 0
|
||
if interval_minutes > 0 and key.last_switch_at:
|
||
next_switch_at = (key.last_switch_at + timedelta(minutes=interval_minutes)).isoformat()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "换号成功",
|
||
"data": {
|
||
"key_info": build_key_response(key),
|
||
"account": build_account_response(new_account),
|
||
"switch_count": key.switch_count,
|
||
"quota_remaining": key.quota_remaining if key.membership_type == KeyMembershipType.PRO else None,
|
||
"next_switch_at": next_switch_at
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/status")
|
||
async def get_status(key: str, db: Session = Depends(get_db)):
|
||
"""
|
||
获取密钥完整状态
|
||
|
||
返回:
|
||
- 密钥信息
|
||
- 账号信息 (如果已启用无感)
|
||
"""
|
||
activation_key = KeyService.get_by_key(db, key)
|
||
if not activation_key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if activation_key.master_key_id:
|
||
activation_key = KeyService.get_by_id(db, activation_key.master_key_id)
|
||
|
||
data = KeyService.get_status(db, activation_key)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": data
|
||
}
|
||
|
||
|
||
@router.get("/account-usage")
|
||
async def get_account_usage(key: str, refresh: bool = False, db: Session = Depends(get_db)):
|
||
"""
|
||
获取当前账号用量 (可选刷新)
|
||
|
||
参数:
|
||
- refresh: 是否从 Cursor API 刷新数据
|
||
"""
|
||
activation_key = KeyService.get_by_key(db, key)
|
||
if not activation_key:
|
||
return {"success": False, "error": "激活码不存在", "code": "INVALID_KEY"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if activation_key.master_key_id:
|
||
activation_key = KeyService.get_by_id(db, activation_key.master_key_id)
|
||
|
||
if not activation_key.seamless_enabled or not activation_key.current_account_id:
|
||
return {"success": False, "error": "未启用无感换号", "code": "SEAMLESS_NOT_ENABLED"}
|
||
|
||
account = AccountService.get_by_id(db, activation_key.current_account_id)
|
||
if not account:
|
||
return {"success": False, "error": "账号不存在", "code": "ACCOUNT_NOT_FOUND"}
|
||
|
||
# 如果需要刷新,从 Cursor API 获取最新数据
|
||
if refresh:
|
||
try:
|
||
analysis_data = await analyze_account_from_token(account.token)
|
||
if analysis_data.get("success"):
|
||
AccountService.update_from_analysis(db, account.id, analysis_data)
|
||
db.refresh(account)
|
||
except Exception as e:
|
||
logger.warning("刷新账号用量失败 (account_id=%s): %s", activation_key.current_account_id, e)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"email": account.email,
|
||
"membership_type": account.membership_type,
|
||
"trial_days_remaining": account.trial_days_remaining,
|
||
"billing_cycle": {
|
||
"start": account.billing_cycle_start.isoformat() if account.billing_cycle_start else None,
|
||
"end": account.billing_cycle_end.isoformat() if account.billing_cycle_end else None
|
||
},
|
||
"usage": {
|
||
"limit": account.usage_limit,
|
||
"used": account.usage_used,
|
||
"remaining": account.usage_remaining,
|
||
"percent": float(account.usage_percent) if account.usage_percent else 0
|
||
},
|
||
"cost": {
|
||
"total_requests": account.total_requests,
|
||
"total_input_tokens": account.total_input_tokens,
|
||
"total_output_tokens": account.total_output_tokens,
|
||
"total_cost_usd": account.total_cost_usd
|
||
},
|
||
"updated_at": account.last_analyzed_at.isoformat() if account.last_analyzed_at else None
|
||
}
|
||
}
|
||
|
||
|
||
# ==================== 兼容旧 API ====================
|
||
|
||
@router.post("/verify")
|
||
async def verify(request: ActivateRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""兼容旧版验证接口 - 重定向到 activate"""
|
||
return await activate(request, req, db)
|
||
|
||
|
||
@router.post("/verify-key")
|
||
async def verify_key(request: ActivateRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""兼容旧版验证接口 - 重定向到 activate"""
|
||
return await activate(request, req, db)
|
||
|
||
|
||
@router.post("/switch-account")
|
||
async def switch_account_compat(request: SwitchRequest, req: Request, db: Session = Depends(get_db)):
|
||
"""兼容旧版换号接口 - 重定向到 switch"""
|
||
return await switch_account(request, req, db)
|
||
|
||
|
||
# ==================== 无感换号注入代码 API ====================
|
||
|
||
@router.get("/seamless/status")
|
||
async def seamless_status():
|
||
"""
|
||
获取无感换号状态 (前端用)
|
||
注意:实际注入状态由前端本地检测,此接口仅用于兼容
|
||
"""
|
||
return {
|
||
"success": True,
|
||
"is_injected": False, # 实际状态由前端本地检测
|
||
"message": "请使用前端本地检测"
|
||
}
|
||
|
||
|
||
@router.get("/seamless/config")
|
||
async def get_seamless_config(db: Session = Depends(get_db)):
|
||
"""获取无感配置"""
|
||
return {
|
||
"success": True,
|
||
"enabled": True,
|
||
"auto_switch_threshold": GlobalSettingsService.get_int(db, "auto_switch_threshold") or 98,
|
||
"auto_switch_interval": GlobalSettingsService.get_int(db, "auto_switch_interval") or 0
|
||
}
|
||
|
||
|
||
@router.post("/seamless/config")
|
||
async def update_seamless_config(config: dict):
|
||
"""更新无感配置 (客户端本地管理)"""
|
||
return {"success": True, "message": "配置已保存"}
|
||
|
||
|
||
@router.get("/seamless/get-token")
|
||
async def seamless_get_token(
|
||
userKey: str = None,
|
||
key: str = None,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
获取无感Token (注入代码调用)
|
||
|
||
返回格式直接包含 token、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": "激活码不存在"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if activation_key.master_key_id:
|
||
activation_key = KeyService.get_by_id(db, activation_key.master_key_id)
|
||
|
||
# 检查状态
|
||
if activation_key.status != KeyStatus.ACTIVE:
|
||
return {"success": False, "error": "密钥未激活"}
|
||
|
||
if activation_key.is_expired:
|
||
return {"success": False, "error": "密钥已过期"}
|
||
|
||
if not activation_key.seamless_enabled or not activation_key.current_account_id:
|
||
return {"success": False, "error": "未启用无感换号"}
|
||
|
||
account = AccountService.get_by_id(db, activation_key.current_account_id)
|
||
if not account:
|
||
return {"success": False, "error": "账号不存在"}
|
||
|
||
if account.status not in (AccountStatus.AVAILABLE, AccountStatus.IN_USE):
|
||
return {"success": False, "error": "账号不可用"}
|
||
|
||
# 返回注入代码需要的格式
|
||
access_token = account.access_token or account.token
|
||
workos_token = account.workos_session_token or account.token
|
||
refresh_token = account.refresh_token
|
||
return {
|
||
"success": True,
|
||
"accessToken": access_token,
|
||
"refreshToken": refresh_token,
|
||
"workosToken": workos_token,
|
||
"email": account.email,
|
||
"membership_type": account.membership_type,
|
||
"usage_percent": float(account.usage_percent) if account.usage_percent else 0,
|
||
"switchVersion": activation_key.switch_count,
|
||
"switchRemaining": activation_key.quota_remaining if activation_key.membership_type == KeyMembershipType.PRO else 999
|
||
}
|
||
|
||
|
||
@router.post("/seamless/switch-token")
|
||
async def seamless_switch_token(data: dict, req: Request, db: Session = Depends(get_db)):
|
||
"""
|
||
注入代码触发换号
|
||
"""
|
||
user_key = data.get("userKey")
|
||
if not user_key:
|
||
return {"switched": False, "message": "缺少userKey参数"}
|
||
|
||
request = SwitchRequest(key=user_key)
|
||
result = await switch_account(request, req, db)
|
||
|
||
if not result.get("success"):
|
||
return {"switched": False, "message": result.get("error")}
|
||
|
||
account_data = result.get("data", {}).get("account", {})
|
||
access_token = account_data.get("access_token") or account_data.get("token")
|
||
workos_token = account_data.get("workos_session_token") or account_data.get("token")
|
||
refresh_token = account_data.get("refresh_token")
|
||
|
||
# 构建前端 _writeAccountToLocal 需要的数据
|
||
write_data = {
|
||
"accessToken": access_token,
|
||
"refreshToken": refresh_token,
|
||
"email": account_data.get("email"),
|
||
"membership_type": account_data.get("membership_type"),
|
||
"workosToken": workos_token
|
||
}
|
||
|
||
return {
|
||
"switched": True,
|
||
"switchRemaining": result.get("data", {}).get("quota_remaining", 999),
|
||
"email": account_data.get("email"),
|
||
"accessToken": access_token,
|
||
"workosToken": workos_token,
|
||
"switchVersion": result.get("data", {}).get("switch_count", 0),
|
||
"data": write_data # 供 _writeAccountToLocal 使用
|
||
}
|
||
|
||
|
||
@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": "激活码不存在"}
|
||
|
||
# 如果是子密钥,找到主密钥
|
||
if activation_key.master_key_id:
|
||
activation_key = KeyService.get_by_id(db, activation_key.master_key_id)
|
||
|
||
# 检查是否有效
|
||
if activation_key.status != KeyStatus.ACTIVE:
|
||
return {"success": False, "valid": False, "error": "密钥未激活"}
|
||
|
||
if activation_key.is_expired:
|
||
return {"success": False, "valid": False, "error": "密钥已过期"}
|
||
|
||
# 获取当前账号信息
|
||
locked_account = None
|
||
if activation_key.seamless_enabled and activation_key.current_account_id:
|
||
account = AccountService.get_by_id(db, activation_key.current_account_id)
|
||
if account and account.status in (AccountStatus.AVAILABLE, AccountStatus.IN_USE):
|
||
locked_account = {
|
||
"email": account.email,
|
||
"membership_type": account.membership_type
|
||
}
|
||
|
||
# 计算剩余次数
|
||
is_pro = activation_key.membership_type == KeyMembershipType.PRO
|
||
switch_remaining = activation_key.quota_remaining if is_pro else 999
|
||
|
||
# 计算下次可换号时间 (Auto密钥)
|
||
next_switch_at = None
|
||
if not is_pro:
|
||
interval_minutes = GlobalSettingsService.get_int(db, "auto_switch_interval") or 0
|
||
if interval_minutes > 0 and activation_key.last_switch_at:
|
||
next_time = activation_key.last_switch_at + timedelta(minutes=interval_minutes)
|
||
if next_time > datetime.now():
|
||
next_switch_at = next_time.isoformat()
|
||
|
||
return {
|
||
"success": True,
|
||
"valid": True,
|
||
"switchRemaining": switch_remaining,
|
||
"canSwitch": switch_remaining > 0,
|
||
"lockedAccount": locked_account,
|
||
"membershipType": activation_key.membership_type.value if activation_key.membership_type else None,
|
||
"seamlessEnabled": activation_key.seamless_enabled,
|
||
"nextSwitchAt": next_switch_at,
|
||
"data": {
|
||
"canSwitch": switch_remaining > 0,
|
||
"quotaRemaining": activation_key.quota_remaining if is_pro else None,
|
||
"switchCount": activation_key.switch_count
|
||
}
|
||
}
|
||
|
||
|
||
# ==================== 设备密钥信息 API ====================
|
||
|
||
@router.get("/device-keys")
|
||
async def get_device_keys(device_id: str = None, db: Session = Depends(get_db)):
|
||
"""获取设备的所有密钥信息(Auto和Pro)"""
|
||
if not device_id:
|
||
return {"success": False, "error": "缺少设备ID"}
|
||
|
||
result = {
|
||
"success": True,
|
||
"device_id": device_id,
|
||
"auto": {"has_key": False},
|
||
"pro": {"has_key": False}
|
||
}
|
||
|
||
# 查找 Auto 主密钥
|
||
auto_key = db.query(ActivationKey).filter(
|
||
ActivationKey.device_id == device_id,
|
||
ActivationKey.membership_type == KeyMembershipType.AUTO,
|
||
ActivationKey.status == KeyStatus.ACTIVE,
|
||
ActivationKey.master_key_id == None
|
||
).first()
|
||
|
||
if auto_key:
|
||
result["auto"] = {
|
||
"has_key": True,
|
||
"master_key": auto_key.key[:8] + "****",
|
||
"expire_at": auto_key.expire_at.strftime("%Y/%m/%d %H:%M:%S") if auto_key.expire_at else None,
|
||
"days_remaining": max(0, (auto_key.expire_at - datetime.now()).days) if auto_key.expire_at else auto_key.duration_days,
|
||
"merged_count": auto_key.merged_count,
|
||
"seamless_enabled": auto_key.seamless_enabled,
|
||
"current_account": auto_key.current_account.email if auto_key.current_account else None,
|
||
"status": auto_key.status.value
|
||
}
|
||
|
||
# 查找 Pro 主密钥
|
||
pro_key = db.query(ActivationKey).filter(
|
||
ActivationKey.device_id == device_id,
|
||
ActivationKey.membership_type == KeyMembershipType.PRO,
|
||
ActivationKey.status == KeyStatus.ACTIVE,
|
||
ActivationKey.master_key_id == None
|
||
).first()
|
||
|
||
if pro_key:
|
||
result["pro"] = {
|
||
"has_key": True,
|
||
"master_key": pro_key.key[:8] + "****",
|
||
"quota": pro_key.quota,
|
||
"quota_used": pro_key.quota_used,
|
||
"quota_remaining": pro_key.quota_remaining,
|
||
"merged_count": pro_key.merged_count,
|
||
"seamless_enabled": pro_key.seamless_enabled,
|
||
"current_account": pro_key.current_account.email if pro_key.current_account else None,
|
||
"status": pro_key.status.value
|
||
}
|
||
|
||
return result
|
||
|
||
|
||
# ==================== 账号用量查询 API ====================
|
||
|
||
@router.get("/cursor-accounts/query")
|
||
async def query_cursor_account(
|
||
email: str,
|
||
refresh: bool = False,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
查询 Cursor 账号用量信息
|
||
前端用于显示当前账号的用量
|
||
"""
|
||
if not email:
|
||
return {"success": False, "error": "缺少邮箱参数"}
|
||
|
||
account = AccountService.get_by_email(db, email)
|
||
if not account:
|
||
return {"success": False, "error": "账号不存在"}
|
||
|
||
# 如果需要刷新,从 Cursor API 获取最新数据
|
||
if refresh:
|
||
try:
|
||
analysis_data = await analyze_account_from_token(account.token)
|
||
if analysis_data.get("success"):
|
||
AccountService.update_from_analysis(db, account.id, analysis_data)
|
||
db.refresh(account)
|
||
except Exception as e:
|
||
logger.warning("刷新账号用量失败 (email=%s): %s", email, e)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"email": account.email,
|
||
"membership_type": account.membership_type,
|
||
"account_type": account.account_type.value if account.account_type else None,
|
||
"trial_days_remaining": account.trial_days_remaining,
|
||
"usage": {
|
||
"limit": account.usage_limit,
|
||
"used": account.usage_used,
|
||
"remaining": account.usage_remaining,
|
||
"percent": float(account.usage_percent) if account.usage_percent else 0,
|
||
"totalUsageCount": account.total_requests,
|
||
"totalCostUSD": account.total_cost_usd
|
||
},
|
||
"last_analyzed_at": account.last_analyzed_at.isoformat() if account.last_analyzed_at else None
|
||
}
|
||
}
|
||
|
||
|
||
# ==================== 其他 API ====================
|
||
|
||
@router.get("/version")
|
||
async def get_version():
|
||
"""获取版本信息"""
|
||
return {
|
||
"success": True,
|
||
"version": "2.1.0",
|
||
"latest_version": "2.1.0",
|
||
"has_update": False,
|
||
"download_url": "",
|
||
"changelog": ""
|
||
}
|
||
|
||
|
||
@router.get("/announcement")
|
||
async def get_announcement(db: Session = Depends(get_db)):
|
||
"""获取公告信息"""
|
||
announcement = db.query(Announcement).filter(
|
||
Announcement.is_active == True
|
||
).order_by(Announcement.id.desc()).first()
|
||
|
||
if not announcement:
|
||
return {
|
||
"success": True,
|
||
"has_announcement": False,
|
||
"data": None
|
||
}
|
||
|
||
return {
|
||
"success": True,
|
||
"has_announcement": True,
|
||
"data": {
|
||
"is_active": announcement.is_active,
|
||
"title": announcement.title,
|
||
"content": announcement.content,
|
||
"type": announcement.type,
|
||
"created_at": announcement.created_at.strftime("%Y-%m-%d %H:%M:%S") if announcement.created_at else None
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/announcements/latest")
|
||
async def get_announcements_latest(db: Session = Depends(get_db)):
|
||
"""获取最新公告"""
|
||
return await get_announcement(db)
|
||
|
||
|
||
@router.get("/proxy-config")
|
||
async def get_proxy_config():
|
||
"""获取代理配置 (客户端本地管理)"""
|
||
return {
|
||
"success": True,
|
||
"enabled": False,
|
||
"host": "",
|
||
"port": 0
|
||
}
|
||
|
||
|
||
@router.post("/proxy-config")
|
||
async def update_proxy_config(config: dict):
|
||
"""更新代理配置 (客户端本地管理)"""
|
||
return {"success": True, "message": "代理配置已保存"}
|