Files
cursornew2026/backend/app/api/client.py
ccdojox-crypto 73a71f198f 蜂鸟Pro v2.0.1 - 基础框架版本 (待完善)
## 当前状态
- 插件界面已完成重命名 (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>
2025-12-18 11:21:52 +08:00

886 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
客户端 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
}