## 当前状态 - 插件界面已完成重命名 (cursorpro → hummingbird) - 双账号池 UI 已实现 (Auto/Pro 卡片) - 后端已切换到 MySQL 数据库 - 添加了 Cursor 官方用量 API 文档 ## 已知问题 (待修复) 1. 激活时检查账号导致无账号时激活失败 2. 未启用无感换号时不应获取账号 3. 账号用量模块不显示 (seamless 未启用时应隐藏) 4. 积分显示为 0 (后端未正确返回) 5. Auto/Pro 双密钥逻辑混乱,状态不同步 6. 账号添加后无自动分析功能 ## 下一版本计划 - 重构数据模型,优化账号状态管理 - 实现 Cursor API 自动分析账号 - 修复激活流程,不依赖账号 - 启用无感时才分配账号 - 完善账号用量实时显示 ## 文件说明 - docs/系统设计文档.md - 完整架构设计 - cursor 官方用量接口.md - Cursor API 文档 - 参考计费/ - Vibeviewer 开源项目参考 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
886 lines
31 KiB
Python
886 lines
31 KiB
Python
"""
|
||
客户端 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": "激活码不存在"}
|
||
|
||
# 激活密钥(支持合并)
|
||
activate_ok, activate_msg, master_key = KeyService.activate(db, key, request.device_id)
|
||
if not activate_ok:
|
||
LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message=activate_msg)
|
||
return {"success": False, "valid": False, "error": activate_msg}
|
||
|
||
# 使用主密钥进行后续操作
|
||
active_key = master_key if master_key else key
|
||
|
||
# 检查主密钥是否有效
|
||
is_valid, message = KeyService.is_valid(active_key, db)
|
||
if not is_valid:
|
||
LogService.log(db, active_key.id, "verify", ip_address=req.client.host, success=False, message=message)
|
||
return {"success": False, "valid": False, "error": message}
|
||
|
||
# 获取当前绑定的账号,或分配新账号
|
||
account = None
|
||
if active_key.current_account_id:
|
||
account = AccountService.get_by_id(db, active_key.current_account_id)
|
||
|
||
# 只有账号不存在或被禁用/过期才分配新的
|
||
if not account or account.status in (AccountStatus.DISABLED, AccountStatus.EXPIRED):
|
||
account = AccountService.get_available(db, active_key.membership_type)
|
||
if not account:
|
||
LogService.log(db, active_key.id, "verify", ip_address=req.client.host, success=False, message="无可用账号")
|
||
return {"success": False, "valid": False, "error": "暂无可用账号,请稍后重试"}
|
||
|
||
KeyService.bind_account(db, active_key, account)
|
||
AccountService.mark_used(db, account, active_key.id)
|
||
|
||
# 只记录首次激活,不记录每次验证(减少日志量)
|
||
if "激活成功" in activate_msg or "合并" in activate_msg:
|
||
LogService.log(db, active_key.id, "activate", account.id, ip_address=req.client.host, success=True, message=activate_msg)
|
||
|
||
# 返回格式
|
||
expire_date = active_key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if active_key.expire_at else None
|
||
is_pro = active_key.membership_type == MembershipType.PRO
|
||
|
||
return {
|
||
"success": True,
|
||
"valid": True,
|
||
"message": activate_msg,
|
||
"membership_type": active_key.membership_type.value,
|
||
"expire_date": expire_date,
|
||
"switch_remaining": active_key.quota - active_key.quota_used if is_pro else 999,
|
||
"switch_limit": active_key.quota if is_pro else 999,
|
||
"quota": active_key.quota if is_pro else None,
|
||
"quota_used": active_key.quota_used if is_pro else None,
|
||
"merged_count": active_key.merged_count,
|
||
"master_key": active_key.key[:8] + "****", # 隐藏部分密钥
|
||
"data": build_account_data(account, active_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("/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"}
|
||
|
||
keys_info = KeyService.get_device_keys(db, device_id)
|
||
|
||
result = {
|
||
"success": True,
|
||
"device_id": device_id,
|
||
"auto": None,
|
||
"pro": None
|
||
}
|
||
|
||
# Auto 密钥信息
|
||
if keys_info["auto"]:
|
||
auto_data = keys_info["auto"]
|
||
master = auto_data["master"]
|
||
result["auto"] = {
|
||
"has_key": True,
|
||
"master_key": master.key[:8] + "****",
|
||
"expire_at": master.expire_at.strftime("%Y/%m/%d %H:%M:%S") if master.expire_at else None,
|
||
"merged_count": auto_data["total_keys"],
|
||
"current_account": master.current_account.email if master.current_account else None,
|
||
"status": master.status.value
|
||
}
|
||
else:
|
||
result["auto"] = {"has_key": False}
|
||
|
||
# Pro 密钥信息
|
||
if keys_info["pro"]:
|
||
pro_data = keys_info["pro"]
|
||
master = pro_data["master"]
|
||
result["pro"] = {
|
||
"has_key": True,
|
||
"master_key": master.key[:8] + "****",
|
||
"quota": pro_data["quota"],
|
||
"quota_used": pro_data["quota_used"],
|
||
"quota_remaining": pro_data["quota_remaining"],
|
||
"merged_count": pro_data["total_keys"],
|
||
"expire_at": master.expire_at.strftime("%Y/%m/%d %H:%M:%S") if master.expire_at else None,
|
||
"current_account": master.current_account.email if master.current_account else None,
|
||
"status": master.status.value
|
||
}
|
||
else:
|
||
result["pro"] = {"has_key": False}
|
||
|
||
return result
|
||
|
||
|
||
@router.get("/device-keys/detail")
|
||
async def get_device_keys_detail(device_id: str = None, membership_type: str = None, db: Session = Depends(get_db)):
|
||
"""获取设备某类型密钥的详细信息(包括所有合并的密钥)"""
|
||
if not device_id:
|
||
return {"success": False, "error": "缺少设备ID"}
|
||
|
||
if membership_type not in ["auto", "pro", "free"]:
|
||
return {"success": False, "error": "无效的密钥类型"}
|
||
|
||
# 映射类型
|
||
mem_type = MembershipType.FREE if membership_type in ["auto", "free"] else MembershipType.PRO
|
||
|
||
# 获取主密钥
|
||
master = KeyService.get_master_key(db, device_id, mem_type)
|
||
if not master:
|
||
return {"success": True, "has_key": False, "keys": []}
|
||
|
||
# 获取所有合并的密钥
|
||
from app.models import ActivationKey
|
||
merged_keys = db.query(ActivationKey).filter(
|
||
ActivationKey.master_key_id == master.id
|
||
).order_by(ActivationKey.merged_at.desc()).all()
|
||
|
||
keys_list = []
|
||
# 主密钥
|
||
keys_list.append({
|
||
"id": master.id,
|
||
"key": master.key[:8] + "****",
|
||
"is_master": True,
|
||
"status": master.status.value,
|
||
"contribution": master.quota_contribution if mem_type == MembershipType.PRO else master.duration_days,
|
||
"contribution_type": "积分" if mem_type == MembershipType.PRO else "天",
|
||
"activated_at": master.first_activated_at.strftime("%Y/%m/%d %H:%M") if master.first_activated_at else None
|
||
})
|
||
|
||
# 合并的密钥
|
||
for k in merged_keys:
|
||
keys_list.append({
|
||
"id": k.id,
|
||
"key": k.key[:8] + "****",
|
||
"is_master": False,
|
||
"status": k.status.value,
|
||
"contribution": k.quota_contribution if mem_type == MembershipType.PRO else k.duration_days,
|
||
"contribution_type": "积分" if mem_type == MembershipType.PRO else "天",
|
||
"merged_at": k.merged_at.strftime("%Y/%m/%d %H:%M") if k.merged_at else None
|
||
})
|
||
|
||
return {
|
||
"success": True,
|
||
"has_key": True,
|
||
"membership_type": membership_type,
|
||
"total_keys": len(keys_list),
|
||
"keys": keys_list
|
||
}
|
||
|
||
|
||
# ========== 版本 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
|
||
|
||
# 只记录获取新账号的情况,不记录每次token验证
|
||
if req and is_new:
|
||
LogService.log(db, activation_key.id, "seamless_get_token", account.id, ip_address=req.client.host, success=True, message="分配新账号")
|
||
|
||
# 返回格式需要直接包含字段,供注入代码使用
|
||
# 注入代码检查: 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
|
||
}
|