蜂鸟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) { ... }
|
||||
|
||||
@@ -4,11 +4,11 @@ from typing import Optional
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# 数据库配置
|
||||
USE_SQLITE: bool = True # 设为 False 使用 MySQL
|
||||
DB_HOST: str = "localhost"
|
||||
USE_SQLITE: bool = False # 设为 False 使用 MySQL
|
||||
DB_HOST: str = "127.0.0.1"
|
||||
DB_PORT: int = 3306
|
||||
DB_USER: str = "root"
|
||||
DB_PASSWORD: str = ""
|
||||
DB_USER: str = "cursorpro"
|
||||
DB_PASSWORD: str = "jf6BntYBPz6KH6Pw"
|
||||
DB_NAME: str = "cursorpro"
|
||||
|
||||
# JWT配置
|
||||
|
||||
@@ -15,9 +15,12 @@ class AccountStatus(str, enum.Enum):
|
||||
EXPIRED = "expired" # 过期
|
||||
|
||||
class KeyStatus(str, enum.Enum):
|
||||
ACTIVE = "active"
|
||||
DISABLED = "disabled"
|
||||
EXPIRED = "expired"
|
||||
UNUSED = "unused" # 未使用
|
||||
ACTIVE = "active" # 已激活(主密钥)
|
||||
MERGED = "merged" # 已合并到主密钥
|
||||
REVOKED = "revoked" # 已撤销
|
||||
DISABLED = "disabled" # 禁用
|
||||
EXPIRED = "expired" # 过期
|
||||
|
||||
|
||||
class CursorAccount(Base):
|
||||
@@ -50,34 +53,41 @@ class ActivationKey(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
key = Column(String(64), unique=True, nullable=False, index=True, comment="激活码")
|
||||
status = Column(Enum(KeyStatus), default=KeyStatus.ACTIVE, comment="状态")
|
||||
status = Column(Enum(KeyStatus), default=KeyStatus.UNUSED, comment="状态")
|
||||
|
||||
# 套餐类型
|
||||
membership_type = Column(Enum(MembershipType), default=MembershipType.PRO, comment="套餐类型: free=无限auto, pro=高级模型")
|
||||
membership_type = Column(Enum(MembershipType), default=MembershipType.PRO, comment="套餐类型: free=Auto池, pro=Pro池")
|
||||
|
||||
# 额度系统
|
||||
quota = Column(Integer, default=500, comment="总额度")
|
||||
quota_used = Column(Integer, default=0, comment="已用额度")
|
||||
# 密钥合并关系
|
||||
master_key_id = Column(Integer, ForeignKey("activation_keys.id"), nullable=True, comment="主密钥ID(如果已合并)")
|
||||
device_id = Column(String(255), nullable=True, index=True, comment="绑定的设备ID")
|
||||
|
||||
# 有效期设置
|
||||
valid_days = Column(Integer, default=30, comment="有效天数(0表示永久)")
|
||||
# 该密钥贡献的资源 (创建时设置,不变)
|
||||
duration_days = Column(Integer, default=30, comment="Auto: 该密钥贡献的天数")
|
||||
quota_contribution = Column(Integer, default=500, comment="Pro: 该密钥贡献的积分")
|
||||
|
||||
# 额度系统 (仅主密钥使用,累计值)
|
||||
quota = Column(Integer, default=500, comment="Pro主密钥: 总额度(累加)")
|
||||
quota_used = Column(Integer, default=0, comment="Pro主密钥: 已用额度")
|
||||
|
||||
# 有效期 (仅主密钥使用)
|
||||
expire_at = Column(DateTime, nullable=True, comment="Auto主密钥: 到期时间(累加)")
|
||||
|
||||
# 激活信息
|
||||
first_activated_at = Column(DateTime, nullable=True, comment="首次激活时间")
|
||||
expire_at = Column(DateTime, nullable=True, comment="过期时间(首次激活时计算)")
|
||||
merged_at = Column(DateTime, nullable=True, comment="合并时间")
|
||||
|
||||
# 设备限制
|
||||
max_devices = Column(Integer, default=2, comment="最大设备数")
|
||||
# 设备限制 (可换设备,此字段保留但不强制)
|
||||
max_devices = Column(Integer, default=3, comment="最大设备数(可换设备)")
|
||||
|
||||
# 换号频率限制(已废弃,现由全局设置控制)
|
||||
switch_interval_minutes = Column(Integer, default=30, comment="[已废弃]换号间隔(分钟)")
|
||||
switch_limit_per_interval = Column(Integer, default=2, comment="[已废弃]间隔内最大换号次数")
|
||||
|
||||
# 当前绑定的账号
|
||||
# 当前绑定的账号 (仅主密钥使用)
|
||||
current_account_id = Column(Integer, ForeignKey("cursor_accounts.id"), nullable=True)
|
||||
current_account = relationship("CursorAccount", foreign_keys=[current_account_id])
|
||||
|
||||
# 统计
|
||||
# 统计 (仅主密钥使用)
|
||||
switch_count = Column(Integer, default=0, comment="总换号次数")
|
||||
last_switch_at = Column(DateTime, nullable=True, comment="最后换号时间")
|
||||
merged_count = Column(Integer, default=0, comment="已合并的密钥数量")
|
||||
|
||||
# 备注
|
||||
remark = Column(String(500), nullable=True, comment="备注")
|
||||
@@ -85,6 +95,14 @@ class ActivationKey(Base):
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# 关系
|
||||
master_key = relationship("ActivationKey", remote_side=[id], foreign_keys=[master_key_id])
|
||||
|
||||
@property
|
||||
def valid_days(self):
|
||||
"""兼容旧API: duration_days的别名"""
|
||||
return self.duration_days or 0
|
||||
|
||||
|
||||
class KeyDevice(Base):
|
||||
"""激活码绑定的设备"""
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
from app.services.account_service import AccountService, KeyService, LogService, GlobalSettingsService, BatchService
|
||||
from app.services.auth_service import authenticate_admin, create_access_token, get_current_user
|
||||
from app.services.cursor_usage_service import (
|
||||
CursorUsageService,
|
||||
CursorUsageInfo,
|
||||
cursor_usage_service,
|
||||
check_account_valid,
|
||||
get_account_usage,
|
||||
batch_check_accounts,
|
||||
check_and_classify_account
|
||||
)
|
||||
|
||||
@@ -121,11 +121,17 @@ class KeyService:
|
||||
if retry == max_retries - 1:
|
||||
raise ValueError(f"无法生成唯一激活码,请重试")
|
||||
|
||||
# 根据类型设置默认值
|
||||
is_pro = key_data.membership_type == MembershipType.PRO
|
||||
db_key = ActivationKey(
|
||||
key=key_str,
|
||||
status=KeyStatus.UNUSED, # 新密钥默认未使用
|
||||
membership_type=key_data.membership_type,
|
||||
quota=key_data.quota if key_data.membership_type == MembershipType.PRO else 0, # Free不需要额度
|
||||
valid_days=key_data.valid_days,
|
||||
# 该密钥贡献的资源
|
||||
duration_days=key_data.valid_days if not is_pro else 0, # Auto贡献天数
|
||||
quota_contribution=key_data.quota if is_pro else 0, # Pro贡献积分
|
||||
# 主密钥初始值(激活时使用)
|
||||
quota=key_data.quota if is_pro else 0,
|
||||
max_devices=key_data.max_devices,
|
||||
remark=key_data.remark
|
||||
)
|
||||
@@ -171,22 +177,139 @@ class KeyService:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def activate(db: Session, key: ActivationKey):
|
||||
"""首次激活:设置激活时间和过期时间"""
|
||||
if key.first_activated_at is None:
|
||||
key.first_activated_at = datetime.now()
|
||||
if key.valid_days > 0:
|
||||
key.expire_at = key.first_activated_at + timedelta(days=key.valid_days)
|
||||
def activate(db: Session, key: ActivationKey, device_id: str = None) -> Tuple[bool, str, Optional[ActivationKey]]:
|
||||
"""
|
||||
激活密钥
|
||||
- 如果设备已有同类型主密钥,则合并(叠加时长/积分)
|
||||
- 否则,该密钥成为主密钥
|
||||
返回: (成功, 消息, 主密钥)
|
||||
"""
|
||||
now = datetime.now()
|
||||
|
||||
# 检查密钥状态
|
||||
if key.status == KeyStatus.MERGED:
|
||||
return False, "该密钥已被合并使用", None
|
||||
if key.status == KeyStatus.REVOKED:
|
||||
return False, "该密钥已被撤销", None
|
||||
if key.status == KeyStatus.DISABLED:
|
||||
return False, "该密钥已被禁用", None
|
||||
if key.status == KeyStatus.ACTIVE:
|
||||
# 已激活的密钥,检查是否是同设备
|
||||
if device_id and key.device_id and key.device_id != device_id:
|
||||
# 换设备激活,更新设备ID
|
||||
key.device_id = device_id
|
||||
db.commit()
|
||||
return True, "密钥已激活", key
|
||||
|
||||
# 查找该设备同类型的主密钥
|
||||
master_key = None
|
||||
if device_id:
|
||||
master_key = db.query(ActivationKey).filter(
|
||||
ActivationKey.device_id == device_id,
|
||||
ActivationKey.membership_type == key.membership_type,
|
||||
ActivationKey.status == KeyStatus.ACTIVE,
|
||||
ActivationKey.master_key_id == None # 是主密钥
|
||||
).first()
|
||||
|
||||
if master_key:
|
||||
# 合并到现有主密钥
|
||||
key.status = KeyStatus.MERGED
|
||||
key.master_key_id = master_key.id
|
||||
key.merged_at = now
|
||||
key.device_id = device_id
|
||||
|
||||
# 叠加资源到主密钥
|
||||
if key.membership_type == MembershipType.PRO:
|
||||
# Pro: 叠加积分
|
||||
master_key.quota += key.quota_contribution
|
||||
else:
|
||||
# Auto: 叠加时长
|
||||
if master_key.expire_at:
|
||||
master_key.expire_at += timedelta(days=key.duration_days)
|
||||
else:
|
||||
master_key.expire_at = now + timedelta(days=key.duration_days)
|
||||
|
||||
master_key.merged_count += 1
|
||||
db.commit()
|
||||
return True, f"密钥已合并,{'积分' if key.membership_type == MembershipType.PRO else '时长'}已叠加", master_key
|
||||
else:
|
||||
# 该密钥成为主密钥
|
||||
key.status = KeyStatus.ACTIVE
|
||||
key.device_id = device_id
|
||||
key.first_activated_at = now
|
||||
|
||||
# 设置初始到期时间(Auto)
|
||||
if key.membership_type == MembershipType.FREE and key.duration_days > 0:
|
||||
key.expire_at = now + timedelta(days=key.duration_days)
|
||||
|
||||
db.commit()
|
||||
return True, "激活成功", key
|
||||
|
||||
@staticmethod
|
||||
def get_master_key(db: Session, device_id: str, membership_type: MembershipType) -> Optional[ActivationKey]:
|
||||
"""获取设备的主密钥"""
|
||||
return db.query(ActivationKey).filter(
|
||||
ActivationKey.device_id == device_id,
|
||||
ActivationKey.membership_type == membership_type,
|
||||
ActivationKey.status == KeyStatus.ACTIVE,
|
||||
ActivationKey.master_key_id == None
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def get_device_keys(db: Session, device_id: str) -> dict:
|
||||
"""获取设备的所有密钥信息"""
|
||||
result = {"auto": None, "pro": None}
|
||||
|
||||
# 获取Auto主密钥
|
||||
auto_master = KeyService.get_master_key(db, device_id, MembershipType.FREE)
|
||||
if auto_master:
|
||||
# 获取合并的密钥
|
||||
merged_keys = db.query(ActivationKey).filter(
|
||||
ActivationKey.master_key_id == auto_master.id
|
||||
).all()
|
||||
result["auto"] = {
|
||||
"master": auto_master,
|
||||
"merged_keys": merged_keys,
|
||||
"total_keys": 1 + len(merged_keys),
|
||||
"expire_at": auto_master.expire_at
|
||||
}
|
||||
|
||||
# 获取Pro主密钥
|
||||
pro_master = KeyService.get_master_key(db, device_id, MembershipType.PRO)
|
||||
if pro_master:
|
||||
merged_keys = db.query(ActivationKey).filter(
|
||||
ActivationKey.master_key_id == pro_master.id
|
||||
).all()
|
||||
result["pro"] = {
|
||||
"master": pro_master,
|
||||
"merged_keys": merged_keys,
|
||||
"total_keys": 1 + len(merged_keys),
|
||||
"quota": pro_master.quota,
|
||||
"quota_used": pro_master.quota_used,
|
||||
"quota_remaining": pro_master.quota - pro_master.quota_used
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def is_valid(key: ActivationKey, db: Session) -> Tuple[bool, str]:
|
||||
"""检查激活码是否有效"""
|
||||
if key.status != KeyStatus.ACTIVE:
|
||||
"""检查激活码是否有效(仅检查主密钥)"""
|
||||
# 状态检查
|
||||
if key.status == KeyStatus.UNUSED:
|
||||
return False, "激活码未激活"
|
||||
if key.status == KeyStatus.MERGED:
|
||||
return False, "该密钥已合并,请使用主密钥"
|
||||
if key.status == KeyStatus.REVOKED:
|
||||
return False, "激活码已被撤销"
|
||||
if key.status == KeyStatus.DISABLED:
|
||||
return False, "激活码已禁用"
|
||||
if key.status == KeyStatus.EXPIRED:
|
||||
return False, "激活码已过期"
|
||||
if key.status != KeyStatus.ACTIVE:
|
||||
return False, "激活码状态异常"
|
||||
|
||||
# 检查是否已过期(只有激活后才检查)
|
||||
if key.first_activated_at and key.expire_at and key.expire_at < datetime.now():
|
||||
if key.expire_at and key.expire_at < datetime.now():
|
||||
return False, "激活码已过期"
|
||||
|
||||
# Pro套餐检查额度
|
||||
@@ -292,6 +415,66 @@ class KeyService:
|
||||
key.current_account_id = account.id
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def revoke_key(db: Session, key_id: int) -> Tuple[bool, str]:
|
||||
"""
|
||||
撤销密钥
|
||||
- 如果是主密钥:不允许直接撤销(需要先撤销所有合并的密钥)
|
||||
- 如果是合并的密钥:从主密钥扣除贡献的资源
|
||||
"""
|
||||
key = db.query(ActivationKey).filter(ActivationKey.id == key_id).first()
|
||||
if not key:
|
||||
return False, "密钥不存在"
|
||||
|
||||
if key.status == KeyStatus.REVOKED:
|
||||
return False, "密钥已被撤销"
|
||||
|
||||
if key.status == KeyStatus.ACTIVE and key.master_key_id is None:
|
||||
# 是主密钥,检查是否有合并的密钥
|
||||
merged_count = db.query(ActivationKey).filter(
|
||||
ActivationKey.master_key_id == key.id,
|
||||
ActivationKey.status == KeyStatus.MERGED
|
||||
).count()
|
||||
if merged_count > 0:
|
||||
return False, f"该密钥有{merged_count}个合并密钥,请先撤销合并的密钥"
|
||||
|
||||
# 主密钥没有合并密钥,可以直接撤销
|
||||
key.status = KeyStatus.REVOKED
|
||||
db.commit()
|
||||
return True, "主密钥已撤销"
|
||||
|
||||
elif key.status == KeyStatus.MERGED:
|
||||
# 是合并的密钥,从主密钥扣除资源
|
||||
master = db.query(ActivationKey).filter(ActivationKey.id == key.master_key_id).first()
|
||||
if not master:
|
||||
return False, "找不到主密钥"
|
||||
|
||||
if key.membership_type == MembershipType.PRO:
|
||||
# Pro: 检查扣除后是否会导致已用超额
|
||||
new_quota = master.quota - key.quota_contribution
|
||||
if master.quota_used > new_quota:
|
||||
return False, f"无法撤销:撤销后剩余额度({new_quota})小于已用额度({master.quota_used})"
|
||||
master.quota = new_quota
|
||||
else:
|
||||
# Auto: 扣除时长
|
||||
if master.expire_at:
|
||||
master.expire_at -= timedelta(days=key.duration_days)
|
||||
# 检查扣除后是否已过期
|
||||
if master.expire_at < datetime.now():
|
||||
return False, "无法撤销:撤销后密钥将立即过期"
|
||||
|
||||
master.merged_count -= 1
|
||||
key.status = KeyStatus.REVOKED
|
||||
key.master_key_id = None # 解除关联
|
||||
db.commit()
|
||||
return True, "合并密钥已撤销,资源已扣除"
|
||||
|
||||
else:
|
||||
# 其他状态(UNUSED, DISABLED 等)
|
||||
key.status = KeyStatus.REVOKED
|
||||
db.commit()
|
||||
return True, "密钥已撤销"
|
||||
|
||||
@staticmethod
|
||||
def count(db: Session) -> dict:
|
||||
"""统计激活码数量"""
|
||||
|
||||
337
backend/app/services/cursor_usage_service.py
Normal file
337
backend/app/services/cursor_usage_service.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Cursor 官方用量 API 服务
|
||||
用于验证账号有效性和查询用量信息
|
||||
"""
|
||||
import httpx
|
||||
import asyncio
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@dataclass
|
||||
class CursorUsageInfo:
|
||||
"""Cursor 用量信息"""
|
||||
is_valid: bool = False # 账号是否有效
|
||||
error_message: Optional[str] = None # 错误信息
|
||||
|
||||
# 用户信息
|
||||
user_id: Optional[int] = None
|
||||
email: Optional[str] = None
|
||||
team_id: Optional[int] = None
|
||||
is_enterprise: bool = False
|
||||
|
||||
# 会员信息
|
||||
membership_type: str = "free" # free, free_trial, pro, business
|
||||
billing_cycle_start: Optional[str] = None
|
||||
billing_cycle_end: Optional[str] = None
|
||||
days_remaining_on_trial: Optional[int] = None # 试用剩余天数 (free_trial)
|
||||
|
||||
# 套餐用量
|
||||
plan_used: int = 0
|
||||
plan_limit: int = 0
|
||||
plan_remaining: int = 0
|
||||
|
||||
# Token 用量
|
||||
total_input_tokens: int = 0
|
||||
total_output_tokens: int = 0
|
||||
total_cache_read_tokens: int = 0
|
||||
total_cost_cents: float = 0.0
|
||||
|
||||
# 请求次数
|
||||
total_requests: int = 0 # totalUsageEventsCount
|
||||
|
||||
@property
|
||||
def pool_type(self) -> str:
|
||||
"""
|
||||
判断账号应归入哪个号池
|
||||
- 'pro': Pro池 (free_trial, pro, business)
|
||||
- 'auto': Auto池 (free)
|
||||
"""
|
||||
if self.membership_type in ('free_trial', 'pro', 'business'):
|
||||
return 'pro'
|
||||
return 'auto'
|
||||
|
||||
@property
|
||||
def is_pro_trial(self) -> bool:
|
||||
"""是否为 Pro 试用账号"""
|
||||
return self.membership_type == 'free_trial'
|
||||
|
||||
@property
|
||||
def is_usable(self) -> bool:
|
||||
"""账号是否可用 (有效且有剩余额度)"""
|
||||
if not self.is_valid:
|
||||
return False
|
||||
# Pro试用和Pro需要检查剩余额度
|
||||
if self.pool_type == 'pro':
|
||||
return self.plan_remaining > 0
|
||||
# Auto池 free账号始终可用
|
||||
return True
|
||||
|
||||
|
||||
class CursorUsageService:
|
||||
"""Cursor 用量查询服务"""
|
||||
|
||||
BASE_URL = "https://cursor.com"
|
||||
TIMEOUT = 15.0
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
"accept": "*/*",
|
||||
"content-type": "application/json",
|
||||
"origin": "https://cursor.com",
|
||||
"referer": "https://cursor.com/dashboard",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
}
|
||||
|
||||
def _get_cookie_header(self, token: str) -> str:
|
||||
"""构造 Cookie Header"""
|
||||
# 支持直接传 token 或完整 cookie
|
||||
if token.startswith("WorkosCursorSessionToken="):
|
||||
return token
|
||||
return f"WorkosCursorSessionToken={token}"
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
token: str,
|
||||
json_data: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""发送请求"""
|
||||
headers = {**self.headers, "Cookie": self._get_cookie_header(token)}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.TIMEOUT) as client:
|
||||
if method == "GET":
|
||||
resp = await client.get(f"{self.BASE_URL}{path}", headers=headers)
|
||||
else:
|
||||
resp = await client.post(
|
||||
f"{self.BASE_URL}{path}",
|
||||
headers=headers,
|
||||
json=json_data or {}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
return {"success": True, "data": resp.json()}
|
||||
elif resp.status_code in [401, 403]:
|
||||
return {"success": False, "error": "认证失败,Token 无效或已过期"}
|
||||
else:
|
||||
return {"success": False, "error": f"请求失败: {resp.status_code}"}
|
||||
|
||||
async def get_usage_summary(self, token: str) -> Dict[str, Any]:
|
||||
"""获取用量摘要"""
|
||||
return await self._request("GET", "/api/usage-summary", token)
|
||||
|
||||
async def get_billing_cycle(self, token: str) -> Dict[str, Any]:
|
||||
"""获取当前计费周期"""
|
||||
return await self._request("POST", "/api/dashboard/get-current-billing-cycle", token, {})
|
||||
|
||||
async def get_filtered_usage(
|
||||
self,
|
||||
token: str,
|
||||
start_date_ms: str,
|
||||
end_date_ms: str,
|
||||
page: int = 1,
|
||||
page_size: int = 100
|
||||
) -> Dict[str, Any]:
|
||||
"""获取过滤后的使用事件"""
|
||||
return await self._request(
|
||||
"POST",
|
||||
"/api/dashboard/get-filtered-usage-events",
|
||||
token,
|
||||
{
|
||||
"startDate": start_date_ms,
|
||||
"endDate": end_date_ms,
|
||||
"page": page,
|
||||
"pageSize": page_size
|
||||
}
|
||||
)
|
||||
|
||||
async def get_aggregated_usage(
|
||||
self,
|
||||
token: str,
|
||||
start_date_ms: int,
|
||||
team_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取聚合使用事件"""
|
||||
data = {"startDate": start_date_ms}
|
||||
if team_id:
|
||||
data["teamId"] = team_id
|
||||
return await self._request(
|
||||
"POST",
|
||||
"/api/dashboard/get-aggregated-usage-events",
|
||||
token,
|
||||
data
|
||||
)
|
||||
|
||||
async def validate_and_get_usage(self, token: str) -> CursorUsageInfo:
|
||||
"""
|
||||
验证账号并获取完整用量信息
|
||||
这是主要的对外接口
|
||||
"""
|
||||
result = CursorUsageInfo()
|
||||
|
||||
try:
|
||||
# 1. 获取用量摘要 (验证 token 有效性)
|
||||
summary_resp = await self.get_usage_summary(token)
|
||||
if not summary_resp["success"]:
|
||||
result.error_message = summary_resp["error"]
|
||||
return result
|
||||
|
||||
summary = summary_resp["data"]
|
||||
result.is_valid = True
|
||||
result.membership_type = summary.get("membershipType", "free")
|
||||
result.billing_cycle_start = summary.get("billingCycleStart")
|
||||
result.billing_cycle_end = summary.get("billingCycleEnd")
|
||||
result.days_remaining_on_trial = summary.get("daysRemainingOnTrial") # 试用剩余天数
|
||||
|
||||
# 套餐用量
|
||||
individual = summary.get("individualUsage", {})
|
||||
plan = individual.get("plan", {})
|
||||
result.plan_used = plan.get("used", 0)
|
||||
result.plan_limit = plan.get("limit", 0)
|
||||
result.plan_remaining = plan.get("remaining", 0)
|
||||
|
||||
# 2. 获取计费周期
|
||||
billing_resp = await self.get_billing_cycle(token)
|
||||
if billing_resp["success"]:
|
||||
billing = billing_resp["data"]
|
||||
start_ms = billing.get("startDateEpochMillis", "0")
|
||||
end_ms = billing.get("endDateEpochMillis", "0")
|
||||
|
||||
# 3. 获取请求次数 (totalUsageEventsCount)
|
||||
filtered_resp = await self.get_filtered_usage(token, start_ms, end_ms, 1, 1)
|
||||
if filtered_resp["success"]:
|
||||
filtered = filtered_resp["data"]
|
||||
result.total_requests = filtered.get("totalUsageEventsCount", 0)
|
||||
|
||||
# 4. 获取 Token 用量
|
||||
aggregated_resp = await self.get_aggregated_usage(token, int(start_ms))
|
||||
if aggregated_resp["success"]:
|
||||
agg = aggregated_resp["data"]
|
||||
result.total_input_tokens = int(agg.get("totalInputTokens", "0"))
|
||||
result.total_output_tokens = int(agg.get("totalOutputTokens", "0"))
|
||||
result.total_cache_read_tokens = int(agg.get("totalCacheReadTokens", "0"))
|
||||
result.total_cost_cents = agg.get("totalCostCents", 0.0)
|
||||
|
||||
return result
|
||||
|
||||
except httpx.TimeoutException:
|
||||
result.error_message = "请求超时"
|
||||
return result
|
||||
except Exception as e:
|
||||
result.error_message = f"请求异常: {str(e)}"
|
||||
return result
|
||||
|
||||
def validate_and_get_usage_sync(self, token: str) -> CursorUsageInfo:
|
||||
"""同步版本的验证和获取用量"""
|
||||
return asyncio.run(self.validate_and_get_usage(token))
|
||||
|
||||
|
||||
# 单例
|
||||
cursor_usage_service = CursorUsageService()
|
||||
|
||||
|
||||
# ============ 便捷函数 ============
|
||||
|
||||
async def check_account_valid(token: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
检查账号是否有效
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
try:
|
||||
resp = await cursor_usage_service.get_usage_summary(token)
|
||||
if resp["success"]:
|
||||
return True, None
|
||||
return False, resp["error"]
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
async def get_account_usage(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取账号用量信息
|
||||
返回格式化的用量数据
|
||||
"""
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
|
||||
if not info.is_valid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": info.error_message
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"membership_type": info.membership_type,
|
||||
"pool_type": info.pool_type, # 号池类型: pro/auto
|
||||
"is_pro_trial": info.is_pro_trial, # 是否Pro试用
|
||||
"is_usable": info.is_usable, # 是否可用
|
||||
"days_remaining_on_trial": info.days_remaining_on_trial,
|
||||
"billing_cycle": {
|
||||
"start": info.billing_cycle_start,
|
||||
"end": info.billing_cycle_end
|
||||
},
|
||||
"plan_usage": {
|
||||
"used": info.plan_used,
|
||||
"limit": info.plan_limit,
|
||||
"remaining": info.plan_remaining
|
||||
},
|
||||
"token_usage": {
|
||||
"input_tokens": info.total_input_tokens,
|
||||
"output_tokens": info.total_output_tokens,
|
||||
"cache_read_tokens": info.total_cache_read_tokens,
|
||||
"total_cost_usd": round(info.total_cost_cents / 100, 4)
|
||||
},
|
||||
"total_requests": info.total_requests
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def batch_check_accounts(tokens: List[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
批量检查多个账号
|
||||
"""
|
||||
results = []
|
||||
for token in tokens:
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
results.append({
|
||||
"token": token[:20] + "...", # 脱敏
|
||||
"is_valid": info.is_valid,
|
||||
"is_usable": info.is_usable,
|
||||
"pool_type": info.pool_type, # pro/auto
|
||||
"membership_type": info.membership_type if info.is_valid else None,
|
||||
"days_remaining_on_trial": info.days_remaining_on_trial,
|
||||
"plan_used": info.plan_used if info.is_valid else 0,
|
||||
"plan_limit": info.plan_limit if info.is_valid else 0,
|
||||
"plan_remaining": info.plan_remaining if info.is_valid else 0,
|
||||
"total_requests": info.total_requests if info.is_valid else 0,
|
||||
"error": info.error_message
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
async def check_and_classify_account(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
检查账号并分类到对应号池
|
||||
返回账号信息和推荐的号池
|
||||
"""
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
|
||||
if not info.is_valid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": info.error_message
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"pool_type": info.pool_type, # 'pro' 或 'auto'
|
||||
"is_usable": info.is_usable, # 是否可用
|
||||
"membership_type": info.membership_type,
|
||||
"is_pro_trial": info.is_pro_trial,
|
||||
"plan_remaining": info.plan_remaining,
|
||||
"total_requests": info.total_requests,
|
||||
"recommendation": f"建议放入 {'Pro' if info.pool_type == 'pro' else 'Auto'} 号池"
|
||||
}
|
||||
104
backend/test_cursor_service.py
Normal file
104
backend/test_cursor_service.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
测试 CursorUsageService
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
|
||||
from app.services.cursor_usage_service import (
|
||||
cursor_usage_service,
|
||||
get_account_usage,
|
||||
check_account_valid,
|
||||
check_and_classify_account
|
||||
)
|
||||
|
||||
# 测试 Token (free_trial)
|
||||
TEST_TOKEN = "user_01KCG2G9K4Q37C1PKTNR7EVNGW::eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhdXRoMHx1c2VyXzAxS0NHMkc5SzRRMzdDMVBLVE5SN0VWTkdXIiwidGltZSI6IjE3NjU3ODc5NjYiLCJyYW5kb21uZXNzIjoiOTA1NTU4NjktYTlmMC00M2NhIiwiZXhwIjoxNzcwOTcxOTY2LCJpc3MiOiJodHRwczovL2F1dGhlbnRpY2F0aW9uLmN1cnNvci5zaCIsInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgZW1haWwgb2ZmbGluZV9hY2Nlc3MiLCJhdWQiOiJodHRwczovL2N1cnNvci5jb20iLCJ0eXBlIjoic2Vzc2lvbiJ9.vreEnprZ7q9pU7b6TTVGQ0HUIQTJrxLXcnkz4Ne4Dng"
|
||||
|
||||
|
||||
async def test_check_valid():
|
||||
"""测试账号有效性检查"""
|
||||
print("\n========== 1. 检查账号有效性 ==========")
|
||||
is_valid, error = await check_account_valid(TEST_TOKEN)
|
||||
print(f"账号有效: {is_valid}")
|
||||
if error:
|
||||
print(f"错误: {error}")
|
||||
|
||||
|
||||
async def test_get_usage():
|
||||
"""测试获取用量信息"""
|
||||
print("\n========== 2. 获取用量信息 ==========")
|
||||
result = await get_account_usage(TEST_TOKEN)
|
||||
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
print(f"会员类型: {data['membership_type']}")
|
||||
if data.get('days_remaining_on_trial'):
|
||||
print(f"试用剩余天数: {data['days_remaining_on_trial']}")
|
||||
print(f"计费周期: {data['billing_cycle']['start']} ~ {data['billing_cycle']['end']}")
|
||||
print(f"套餐用量: {data['plan_usage']['used']}/{data['plan_usage']['limit']} (剩余 {data['plan_usage']['remaining']})")
|
||||
print(f"总请求次数: {data['total_requests']}")
|
||||
print(f"Token 用量:")
|
||||
print(f" - 输入: {data['token_usage']['input_tokens']}")
|
||||
print(f" - 输出: {data['token_usage']['output_tokens']}")
|
||||
print(f" - 缓存读取: {data['token_usage']['cache_read_tokens']}")
|
||||
print(f" - 总费用: ${data['token_usage']['total_cost_usd']}")
|
||||
else:
|
||||
print(f"获取失败: {result['error']}")
|
||||
|
||||
|
||||
async def test_full_info():
|
||||
"""测试完整信息"""
|
||||
print("\n========== 3. 完整验证结果 ==========")
|
||||
info = await cursor_usage_service.validate_and_get_usage(TEST_TOKEN)
|
||||
|
||||
print(f"账号有效: {info.is_valid}")
|
||||
print(f"会员类型: {info.membership_type}")
|
||||
print(f"号池类型: {info.pool_type}")
|
||||
print(f"是否Pro试用: {info.is_pro_trial}")
|
||||
print(f"是否可用: {info.is_usable}")
|
||||
if info.days_remaining_on_trial:
|
||||
print(f"试用剩余天数: {info.days_remaining_on_trial}")
|
||||
print(f"套餐用量: {info.plan_used}/{info.plan_limit} (剩余 {info.plan_remaining})")
|
||||
print(f"总请求次数: {info.total_requests}")
|
||||
print(f"总输入Token: {info.total_input_tokens}")
|
||||
print(f"总输出Token: {info.total_output_tokens}")
|
||||
print(f"总费用: ${info.total_cost_cents / 100:.4f}")
|
||||
|
||||
if info.error_message:
|
||||
print(f"错误: {info.error_message}")
|
||||
|
||||
|
||||
async def test_classify():
|
||||
"""测试号池分类"""
|
||||
print("\n========== 4. 号池分类 ==========")
|
||||
result = await check_and_classify_account(TEST_TOKEN)
|
||||
|
||||
if result["success"]:
|
||||
print(f"号池类型: {result['pool_type']}")
|
||||
print(f"是否可用: {result['is_usable']}")
|
||||
print(f"会员类型: {result['membership_type']}")
|
||||
print(f"是否Pro试用: {result['is_pro_trial']}")
|
||||
print(f"剩余额度: {result['plan_remaining']}")
|
||||
print(f">>> {result['recommendation']}")
|
||||
else:
|
||||
print(f"分类失败: {result['error']}")
|
||||
|
||||
|
||||
async def main():
|
||||
print("=" * 50)
|
||||
print(" CursorUsageService 测试")
|
||||
print("=" * 50)
|
||||
|
||||
await test_check_valid()
|
||||
await test_get_usage()
|
||||
await test_full_info()
|
||||
await test_classify()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(" 测试完成")
|
||||
print("=" * 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user