备份: 完整开发状态(含反混淆脚本和临时文件)

This commit is contained in:
ccdojox-crypto
2025-12-17 17:18:02 +08:00
parent 9e2333c90c
commit 7e9ea173a7
2872 changed files with 326818 additions and 249 deletions

View File

@@ -3,16 +3,18 @@
"""
from datetime import datetime
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from fastapi import APIRouter, Depends, HTTPException, status, Query, Header
from sqlalchemy.orm import Session
from app.database import get_db
from app.config import settings
from app.services import AccountService, KeyService, LogService, GlobalSettingsService, BatchService, authenticate_admin, create_access_token, get_current_user
from app.schemas import (
AccountCreate, AccountUpdate, AccountResponse, AccountImport,
KeyCreate, KeyUpdate, KeyResponse,
DashboardStats, Token, LoginRequest,
GlobalSettingsResponse, GlobalSettingsUpdate,
BatchExtendRequest, BatchExtendResponse
BatchExtendRequest, BatchExtendResponse,
ExternalBatchUpload, ExternalBatchResponse
)
from app.models import MembershipType, KeyDevice, UsageLog, ActivationKey
@@ -33,6 +35,138 @@ async def login(request: LoginRequest):
return Token(access_token=access_token)
# ========== 外部系统API (Token认证) ==========
def verify_api_token(x_api_token: str = Header(..., alias="X-API-Token")):
"""验证外部系统API Token"""
if x_api_token != settings.API_TOKEN:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API Token"
)
return True
@router.post("/external/accounts/batch", response_model=ExternalBatchResponse)
async def external_batch_upload(
data: ExternalBatchUpload,
db: Session = Depends(get_db),
_: bool = Depends(verify_api_token)
):
"""外部系统批量上传账号
使用方法:
POST /admin/external/accounts/batch
Headers: X-API-Token: your-api-token
Body: {
"accounts": [
{
"email": "user@example.com",
"access_token": "xxx",
"refresh_token": "xxx",
"workos_session_token": "xxx",
"membership_type": "free", // free=auto账号, pro=高级账号
"remark": "备注"
}
],
"update_existing": true // 是否更新已存在的账号
}
"""
created = 0
updated = 0
failed = 0
errors = []
for item in data.accounts:
try:
# 转换membership_type
mt = MembershipType.FREE if item.membership_type == "free" else MembershipType.PRO
existing = AccountService.get_by_email(db, item.email)
if existing:
if data.update_existing:
# 更新已存在的账号
AccountService.update(
db, existing.id,
access_token=item.access_token,
refresh_token=item.refresh_token,
workos_session_token=item.workos_session_token,
membership_type=mt,
remark=item.remark or existing.remark
)
updated += 1
else:
failed += 1
errors.append(f"{item.email}: 账号已存在")
else:
# 创建新账号
account_data = AccountCreate(
email=item.email,
access_token=item.access_token,
refresh_token=item.refresh_token,
workos_session_token=item.workos_session_token,
membership_type=mt,
remark=item.remark
)
AccountService.create(db, account_data)
created += 1
except Exception as e:
failed += 1
errors.append(f"{item.email}: {str(e)}")
return ExternalBatchResponse(
success=failed == 0,
total=len(data.accounts),
created=created,
updated=updated,
failed=failed,
errors=errors[:20] # 只返回前20个错误
)
@router.get("/external/accounts/stats")
async def external_account_stats(
db: Session = Depends(get_db),
_: bool = Depends(verify_api_token)
):
"""外部系统获取账号统计"""
stats = AccountService.count(db)
return {
"total": stats["total"],
"active": stats["active"],
"pro": stats["pro"],
"free": stats["total"] - stats["pro"]
}
@router.delete("/external/accounts/batch")
async def external_batch_delete(
emails: List[str],
db: Session = Depends(get_db),
_: bool = Depends(verify_api_token)
):
"""外部系统批量删除账号"""
deleted = 0
failed = 0
for email in emails:
try:
account = AccountService.get_by_email(db, email)
if account:
AccountService.delete(db, account.id)
deleted += 1
else:
failed += 1
except Exception:
failed += 1
return {
"success": failed == 0,
"deleted": deleted,
"failed": failed
}
# ========== 仪表盘 ==========
@router.get("/dashboard", response_model=DashboardStats)
@@ -118,6 +252,16 @@ async def import_accounts(
}
@router.get("/accounts/count")
async def get_accounts_count(
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取账号总数"""
stats = AccountService.count(db)
return {"total": stats["total"]}
@router.get("/accounts/{account_id}", response_model=AccountResponse)
async def get_account(
account_id: int,
@@ -157,6 +301,166 @@ async def delete_account(
return {"message": "删除成功"}
@router.post("/accounts/{account_id}/toggle-status")
async def toggle_account_status(
account_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""快捷切换账号状态
切换逻辑:
- 使用中(in_use) -> 可用(active) 释放账号
- 可用(active) -> 禁用(disabled)
- 禁用(disabled) -> 可用(active)
- 过期(expired) -> 可用(active)
"""
from app.models import AccountStatus, Account
account = db.query(Account).filter(Account.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
old_status = account.status
# 根据当前状态切换
if account.status == AccountStatus.IN_USE:
account.status = AccountStatus.ACTIVE
account.current_key_id = None # 释放绑定
elif account.status == AccountStatus.ACTIVE:
account.status = AccountStatus.DISABLED
elif account.status == AccountStatus.DISABLED:
account.status = AccountStatus.ACTIVE
elif account.status == AccountStatus.EXPIRED:
account.status = AccountStatus.ACTIVE
db.commit()
return {
"success": True,
"old_status": old_status.value,
"new_status": account.status.value,
"message": f"状态已从 {old_status.value} 切换为 {account.status.value}"
}
@router.post("/accounts/{account_id}/release")
async def release_account(
account_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""释放账号(从使用中变为可用)"""
from app.models import AccountStatus, Account
account = db.query(Account).filter(Account.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
if account.status != AccountStatus.IN_USE:
return {"success": False, "message": "账号不在使用中状态"}
account.status = AccountStatus.ACTIVE
account.current_key_id = None
db.commit()
return {"success": True, "message": "账号已释放"}
@router.post("/accounts/batch-enable")
async def batch_enable_accounts(
account_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量启用账号"""
from app.models import AccountStatus, Account
success = 0
failed = 0
for account_id in account_ids:
try:
account = db.query(Account).filter(Account.id == account_id).first()
if account:
account.status = AccountStatus.ACTIVE
success += 1
else:
failed += 1
except Exception:
failed += 1
db.commit()
return {
"success": success,
"failed": failed,
"message": f"成功启用 {success} 个账号"
}
@router.post("/accounts/batch-disable")
async def batch_disable_accounts(
account_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量禁用账号"""
from app.models import AccountStatus, Account
success = 0
failed = 0
for account_id in account_ids:
try:
account = db.query(Account).filter(Account.id == account_id).first()
if account:
account.status = AccountStatus.DISABLED
success += 1
else:
failed += 1
except Exception:
failed += 1
db.commit()
return {
"success": success,
"failed": failed,
"message": f"成功禁用 {success} 个账号"
}
@router.post("/accounts/batch-delete")
async def batch_delete_accounts(
account_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量删除账号"""
success = 0
failed = 0
for account_id in account_ids:
try:
if AccountService.delete(db, account_id):
success += 1
else:
failed += 1
except Exception:
failed += 1
return {
"success": success,
"failed": failed,
"message": f"成功删除 {success} 个账号"
}
@router.get("/accounts/count")
async def get_accounts_count(
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取账号总数"""
stats = AccountService.count(db)
return {"total": stats["total"]}
# ========== 激活码管理 ==========
@router.get("/keys", response_model=List[KeyResponse])
@@ -165,7 +469,7 @@ async def list_keys(
limit: int = 100,
search: Optional[str] = Query(None, description="搜索激活码"),
status: Optional[str] = Query(None, description="状态筛选: active/disabled"),
activated: Optional[bool] = Query(None, description="是否已激活"),
activated: Optional[str] = Query(None, description="是否已激活: true/false"),
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
@@ -180,12 +484,13 @@ async def list_keys(
# 状态筛选
if status:
query = query.filter(ActivationKey.status == status)
status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED
query = query.filter(ActivationKey.status == status_enum)
# 是否已激活
if activated is True:
if activated and activated == "true":
query = query.filter(ActivationKey.first_activated_at != None)
elif activated is False:
elif activated and activated == "false":
query = query.filter(ActivationKey.first_activated_at == None)
# 套餐类型筛选
@@ -196,6 +501,43 @@ async def list_keys(
return query.offset(skip).limit(limit).all()
@router.get("/keys/count")
async def get_keys_count(
search: Optional[str] = Query(None, description="搜索激活码"),
status: Optional[str] = Query(None, description="状态筛选: active/disabled"),
activated: Optional[str] = Query(None, description="是否已激活: true/false"),
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码总数(支持筛选)"""
from app.models import KeyStatus
query = db.query(ActivationKey)
# 搜索激活码
if search:
query = query.filter(ActivationKey.key.contains(search))
# 状态筛选
if status:
status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED
query = query.filter(ActivationKey.status == status_enum)
# 是否已激活
if activated and activated == "true":
query = query.filter(ActivationKey.first_activated_at != None)
elif activated and activated == "false":
query = query.filter(ActivationKey.first_activated_at == None)
# 套餐类型筛选
if membership_type:
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
query = query.filter(ActivationKey.membership_type == mt)
total = query.count()
return {"total": total}
@router.post("/keys", response_model=List[KeyResponse])
async def create_keys(
key_data: KeyCreate,
@@ -344,6 +686,129 @@ async def enable_key(
return {"message": "激活码已启用"}
@router.post("/keys/batch-enable")
async def batch_enable_keys(
key_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量启用激活码"""
from app.models import KeyStatus
success = 0
failed = 0
for key_id in key_ids:
try:
key = KeyService.get_by_id(db, key_id)
if key:
key.status = KeyStatus.ACTIVE
success += 1
else:
failed += 1
except Exception:
failed += 1
db.commit()
return {
"success": success,
"failed": failed,
"message": f"成功启用 {success} 个激活码"
}
@router.post("/keys/batch-disable")
async def batch_disable_keys(
key_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量禁用激活码"""
from app.models import KeyStatus
success = 0
failed = 0
for key_id in key_ids:
try:
key = KeyService.get_by_id(db, key_id)
if key:
key.status = KeyStatus.DISABLED
success += 1
else:
failed += 1
except Exception:
failed += 1
db.commit()
return {
"success": success,
"failed": failed,
"message": f"成功禁用 {success} 个激活码"
}
@router.post("/keys/batch-delete")
async def batch_delete_keys(
key_ids: List[int],
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""批量删除激活码"""
success = 0
failed = 0
for key_id in key_ids:
try:
if KeyService.delete(db, key_id):
success += 1
else:
failed += 1
except Exception:
failed += 1
return {
"success": success,
"failed": failed,
"message": f"成功删除 {success} 个激活码"
}
@router.get("/keys/count")
async def get_keys_count(
search: Optional[str] = Query(None, description="搜索激活码"),
status: Optional[str] = Query(None, description="状态筛选: active/disabled"),
activated: Optional[str] = Query(None, description="是否已激活: true/false"),
membership_type: Optional[str] = Query(None, description="套餐类型: pro/free"),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user)
):
"""获取激活码总数(支持筛选)"""
from app.models import KeyStatus
query = db.query(ActivationKey)
# 搜索激活码
if search:
query = query.filter(ActivationKey.key.contains(search))
# 状态筛选
if status:
status_enum = KeyStatus.ACTIVE if status == "active" else KeyStatus.DISABLED
query = query.filter(ActivationKey.status == status_enum)
# 是否已激活
if activated and activated == "true":
query = query.filter(ActivationKey.first_activated_at != None)
elif activated and activated == "false":
query = query.filter(ActivationKey.first_activated_at == None)
# 套餐类型筛选
if membership_type:
mt = MembershipType.PRO if membership_type.lower() == "pro" else MembershipType.FREE
query = query.filter(ActivationKey.membership_type == mt)
total = query.count()
return {"total": total}
@router.post("/keys/{key_id}/add-quota", response_model=KeyResponse)
async def add_key_quota(
key_id: int,

View File

@@ -3,20 +3,87 @@
"""
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"])
@router.post("/verify-key", response_model=VerifyKeyResponse)
# ========== 账号数据响应模型 ==========
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 VerifyKeyResponse(success=False, error="激活码不存在")
return {"success": False, "valid": False, "error": "激活码不存在"}
# 首次激活:设置激活时间和过期时间
KeyService.activate(db, key)
@@ -26,75 +93,81 @@ async def verify_key(request: VerifyKeyRequest, req: Request, db: Session = Depe
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 VerifyKeyResponse(success=False, error=device_msg)
return {"success": False, "valid": False, "error": device_msg}
# 检查激活码是否有效
is_valid, message = KeyService.is_valid(key, db)
if not is_valid:
LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message=message)
return VerifyKeyResponse(success=False, error=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 not account or account.status != "active":
# 只有账号不存在或被禁用/过期才分配新的IN_USE 状态的账号继续使用)
if not account or account.status in (AccountStatus.DISABLED, AccountStatus.EXPIRED):
# 分配新账号
account = AccountService.get_available(db, key.membership_type)
if not account:
LogService.log(db, key.id, "verify", ip_address=req.client.host, success=False, message="无可用账号")
return VerifyKeyResponse(success=False, error="暂无可用账号,请稍后重试")
return {"success": False, "valid": False, "error": "暂无可用账号,请稍后重试"}
KeyService.bind_account(db, key, account)
AccountService.mark_used(db, account, key.id)
LogService.log(db, key.id, "verify", account.id, ip_address=req.client.host, success=True)
# 返回格式匹配原版插件期望
expire_date = key.expire_at.strftime("%Y-%m-%d %H:%M:%S") if key.expire_at else None
quota_cost = KeyService.get_quota_cost(db, key.membership_type)
return VerifyKeyResponse(
success=True,
email=account.email,
accessToken=account.access_token,
refreshToken=account.refresh_token,
WorkosCursorSessionToken=account.workos_session_token,
membership_type=account.membership_type.value,
quota=key.quota,
quotaUsed=key.quota_used,
quotaRemaining=key.quota - key.quota_used,
quotaCost=quota_cost,
expireDate=expire_date
)
return {
"success": True,
"valid": True,
"expire_date": expire_date,
"switch_remaining": key.quota - key.quota_used,
"switch_limit": key.quota,
"data": build_account_data(account, key)
}
@router.post("/switch-account", response_model=SwitchAccountResponse)
@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 SwitchAccountResponse(success=False, error="激活码不存在")
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 SwitchAccountResponse(success=False, error=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 SwitchAccountResponse(success=False, error=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 SwitchAccountResponse(success=False, error=switch_msg)
return ApiResponse(success=False, error=switch_msg)
# 释放当前账号
if key.current_account_id:
@@ -106,7 +179,7 @@ async def switch_account(request: SwitchAccountRequest, req: Request, db: Sessio
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 SwitchAccountResponse(success=False, error="暂无可用账号,请稍后重试")
return ApiResponse(success=False, error="暂无可用账号,请稍后重试")
# 绑定新账号并扣除额度
KeyService.bind_account(db, key, account)
@@ -115,34 +188,581 @@ async def switch_account(request: SwitchAccountRequest, req: Request, db: Sessio
LogService.log(db, key.id, "switch", account.id, ip_address=req.client.host, success=True)
return SwitchAccountResponse(
return ApiResponse(
success=True,
message="切换成功",
email=account.email,
accessToken=account.access_token,
refreshToken=account.refresh_token,
WorkosCursorSessionToken=account.workos_session_token,
membership_type=account.membership_type.value,
quotaUsed=key.quota_used,
quotaRemaining=key.quota - key.quota_used
data=build_account_data(account, key)
)
# ========== 版本 API ==========
@router.get("/version")
async def get_version():
"""获取版本信息"""
return {
"success": True,
"version": "1.0.0",
"update_url": None,
"message": None
"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():
"""无感换号状态(简化实现)"""
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
# 记录日志
if req:
LogService.log(db, activation_key.id, "seamless_get_token", account.id, ip_address=req.client.host, success=True)
# 返回格式需要直接包含字段,供注入代码使用
# 注入代码检查: 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,
"message": "无感换号功能暂未开放"
"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
}