backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Cursor 官方用量 API 服务
|
||||
Cursor 官方用量 API 服务 v2.1
|
||||
用于验证账号有效性和查询用量信息
|
||||
"""
|
||||
import httpx
|
||||
@@ -7,6 +7,7 @@ import asyncio
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -289,27 +290,30 @@ async def get_account_usage(token: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def batch_check_accounts(tokens: List[str]) -> List[Dict[str, Any]]:
|
||||
async def batch_check_accounts(tokens: List[str], max_concurrency: int = 5) -> 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
|
||||
semaphore = asyncio.Semaphore(max_concurrency)
|
||||
|
||||
async def _check_one(token: str) -> Dict[str, Any]:
|
||||
async with semaphore:
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
return {
|
||||
"token": token[:20] + "...",
|
||||
"is_valid": info.is_valid,
|
||||
"is_usable": info.is_usable,
|
||||
"pool_type": info.pool_type,
|
||||
"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 await asyncio.gather(*[_check_one(t) for t in tokens])
|
||||
|
||||
|
||||
async def check_and_classify_account(token: str) -> Dict[str, Any]:
|
||||
@@ -335,3 +339,95 @@ async def check_and_classify_account(token: str) -> Dict[str, Any]:
|
||||
"total_requests": info.total_requests,
|
||||
"recommendation": f"建议放入 {'Pro' if info.pool_type == 'pro' else 'Auto'} 号池"
|
||||
}
|
||||
|
||||
|
||||
# ============ 数据库集成函数 ============
|
||||
|
||||
def map_membership_to_account_type(membership_type: str) -> str:
|
||||
"""
|
||||
将 Cursor API 的 membershipType 映射到 AccountType
|
||||
"""
|
||||
mapping = {
|
||||
"free_trial": "free_trial",
|
||||
"pro": "pro",
|
||||
"free": "free",
|
||||
"business": "business",
|
||||
"enterprise": "business",
|
||||
}
|
||||
return mapping.get(membership_type, "unknown")
|
||||
|
||||
|
||||
def calculate_usage_percent(used: int, limit: int) -> Decimal:
|
||||
"""计算用量百分比"""
|
||||
if limit <= 0:
|
||||
return Decimal("0")
|
||||
return Decimal(str(round(used / limit * 100, 2)))
|
||||
|
||||
|
||||
async def analyze_account_from_token(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
分析账号Token,返回所有需要更新到数据库的字段
|
||||
用于后台分析任务
|
||||
"""
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
|
||||
if not info.is_valid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": info.error_message,
|
||||
"status": "invalid"
|
||||
}
|
||||
|
||||
# 计算用量百分比
|
||||
usage_percent = calculate_usage_percent(info.plan_used, info.plan_limit)
|
||||
|
||||
# 确定账号状态
|
||||
if usage_percent >= Decimal("95"):
|
||||
status = "exhausted"
|
||||
else:
|
||||
status = "available"
|
||||
|
||||
# 解析计费周期时间
|
||||
billing_start = None
|
||||
billing_end = None
|
||||
try:
|
||||
if info.billing_cycle_start:
|
||||
billing_start = datetime.fromisoformat(info.billing_cycle_start.replace('Z', '+00:00'))
|
||||
if info.billing_cycle_end:
|
||||
billing_end = datetime.fromisoformat(info.billing_cycle_end.replace('Z', '+00:00'))
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"account_type": map_membership_to_account_type(info.membership_type),
|
||||
"membership_type": info.membership_type,
|
||||
"billing_cycle_start": billing_start,
|
||||
"billing_cycle_end": billing_end,
|
||||
"trial_days_remaining": info.days_remaining_on_trial or 0,
|
||||
"usage_limit": info.plan_limit,
|
||||
"usage_used": info.plan_used,
|
||||
"usage_remaining": info.plan_remaining,
|
||||
"usage_percent": usage_percent,
|
||||
"total_requests": info.total_requests,
|
||||
"total_input_tokens": info.total_input_tokens,
|
||||
"total_output_tokens": info.total_output_tokens,
|
||||
"total_cost_cents": Decimal(str(info.total_cost_cents)),
|
||||
"last_analyzed_at": datetime.now(),
|
||||
"analyze_error": None
|
||||
}
|
||||
|
||||
|
||||
async def quick_validate_token(token: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
快速验证Token有效性(仅调用usage-summary)
|
||||
用于激活时的快速检查
|
||||
"""
|
||||
try:
|
||||
resp = await cursor_usage_service.get_usage_summary(token)
|
||||
if resp["success"]:
|
||||
return True, None
|
||||
return False, resp.get("error", "验证失败")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
Reference in New Issue
Block a user