蜂鸟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:
ccdojox-crypto
2025-12-18 11:21:52 +08:00
parent f310ca7b97
commit 73a71f198f
202 changed files with 19142 additions and 252 deletions

View File

@@ -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) { ... }