蜂鸟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>
This commit is contained in:
@@ -587,6 +587,101 @@ async def delete_key(
|
||||
return {"message": "删除成功"}
|
||||
|
||||
|
||||
@router.post("/keys/{key_id}/revoke")
|
||||
async def revoke_key(
|
||||
key_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""撤销激活码(从主密钥扣除资源)"""
|
||||
success, message = KeyService.revoke_key(db, key_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
return {"success": True, "message": message}
|
||||
|
||||
|
||||
@router.get("/keys/by-device/{device_id}")
|
||||
async def get_keys_by_device(
|
||||
device_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""获取设备的所有密钥(管理后台用)"""
|
||||
keys_info = KeyService.get_device_keys(db, device_id)
|
||||
|
||||
result = {
|
||||
"device_id": device_id,
|
||||
"auto": None,
|
||||
"pro": None
|
||||
}
|
||||
|
||||
# Auto 密钥组
|
||||
if keys_info["auto"]:
|
||||
auto_data = keys_info["auto"]
|
||||
master = auto_data["master"]
|
||||
merged_keys = auto_data["merged_keys"]
|
||||
|
||||
all_keys = [{
|
||||
"id": master.id,
|
||||
"key": master.key,
|
||||
"is_master": True,
|
||||
"status": master.status.value,
|
||||
"duration_days": master.duration_days,
|
||||
"activated_at": master.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if master.first_activated_at else None
|
||||
}]
|
||||
for k in merged_keys:
|
||||
all_keys.append({
|
||||
"id": k.id,
|
||||
"key": k.key,
|
||||
"is_master": False,
|
||||
"status": k.status.value,
|
||||
"duration_days": k.duration_days,
|
||||
"merged_at": k.merged_at.strftime("%Y-%m-%d %H:%M:%S") if k.merged_at else None
|
||||
})
|
||||
|
||||
result["auto"] = {
|
||||
"total_keys": len(all_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,
|
||||
"keys": all_keys
|
||||
}
|
||||
|
||||
# Pro 密钥组
|
||||
if keys_info["pro"]:
|
||||
pro_data = keys_info["pro"]
|
||||
master = pro_data["master"]
|
||||
merged_keys = pro_data["merged_keys"]
|
||||
|
||||
all_keys = [{
|
||||
"id": master.id,
|
||||
"key": master.key,
|
||||
"is_master": True,
|
||||
"status": master.status.value,
|
||||
"quota_contribution": master.quota_contribution,
|
||||
"activated_at": master.first_activated_at.strftime("%Y-%m-%d %H:%M:%S") if master.first_activated_at else None
|
||||
}]
|
||||
for k in merged_keys:
|
||||
all_keys.append({
|
||||
"id": k.id,
|
||||
"key": k.key,
|
||||
"is_master": False,
|
||||
"status": k.status.value,
|
||||
"quota_contribution": k.quota_contribution,
|
||||
"merged_at": k.merged_at.strftime("%Y-%m-%d %H:%M:%S") if k.merged_at else None
|
||||
})
|
||||
|
||||
result["pro"] = {
|
||||
"total_keys": len(all_keys),
|
||||
"quota": pro_data["quota"],
|
||||
"quota_used": pro_data["quota_used"],
|
||||
"quota_remaining": pro_data["quota_remaining"],
|
||||
"current_account": master.current_account.email if master.current_account else None,
|
||||
"keys": all_keys
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/keys/{key_id}/usage-info")
|
||||
async def get_key_usage_info(
|
||||
key_id: int,
|
||||
|
||||
@@ -79,55 +79,63 @@ async def verify_key(request: VerifyKeyRequest, req: Request, db: Session = Depe
|
||||
|
||||
|
||||
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)
|
||||
# 激活密钥(支持合并)
|
||||
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}
|
||||
|
||||
# 检查设备限制
|
||||
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}
|
||||
# 使用主密钥进行后续操作
|
||||
active_key = master_key if master_key else key
|
||||
|
||||
# 检查激活码是否有效
|
||||
is_valid, message = KeyService.is_valid(key, db)
|
||||
# 检查主密钥是否有效
|
||||
is_valid, message = KeyService.is_valid(active_key, db)
|
||||
if not is_valid:
|
||||
LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message=message)
|
||||
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 key.current_account_id:
|
||||
account = AccountService.get_by_id(db, key.current_account_id)
|
||||
if active_key.current_account_id:
|
||||
account = AccountService.get_by_id(db, active_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)
|
||||
account = AccountService.get_available(db, active_key.membership_type)
|
||||
if not account:
|
||||
LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message="无可用账号")
|
||||
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, key, account)
|
||||
AccountService.mark_used(db, account, key.id)
|
||||
KeyService.bind_account(db, active_key, account)
|
||||
AccountService.mark_used(db, account, active_key.id)
|
||||
|
||||
LogService.log(db, key.id, "verify", account.id, ip_address=req.client.host, success=True)
|
||||
# 只记录首次激活,不记录每次验证(减少日志量)
|
||||
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
|
||||
|
||||
# 返回格式匹配原版插件期望
|
||||
expire_date = key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None
|
||||
return {
|
||||
"success": True,
|
||||
"valid": True,
|
||||
"message": activate_msg,
|
||||
"membership_type": active_key.membership_type.value,
|
||||
"expire_date": expire_date,
|
||||
"switch_remaining": key.quota - key.quota_used,
|
||||
"switch_limit": key.quota,
|
||||
"data": build_account_data(account, key)
|
||||
"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)
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +203,115 @@ async def switch_account_impl(request: SwitchAccountRequest, req: Request, db: S
|
||||
)
|
||||
|
||||
|
||||
# ========== 设备密钥信息 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")
|
||||
@@ -479,9 +596,9 @@ async def get_seamless_token_v2(userKey: str = None, key: str = None, req: Reque
|
||||
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)
|
||||
# 只记录获取新账号的情况,不记录每次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) { ... }
|
||||
|
||||
Reference in New Issue
Block a user