feat(web): 新增『虎牙设备绑定』管理页 — 一号一环境可视化

后端 (/huya/device-bindings):
- GET: 三方合并列表 (设备画像 + hydevice 指纹状态 + 账号表登录渠道)
  标识脱敏展示 (fingerprint/guid32 只留前12位)
- DELETE: 解绑 = 删除画像 + hydevice 状态 → 下次登录自动生成全新环境重新注册
前端:
- HuyaDeviceBindingsPage: 汇总卡片(绑定数/指纹状态/入库账号) + 明细表
  (机型/屏幕/CDID40/GUID32/Hebe/hydevice状态/登录渠道/最后登录) + 解绑重绑
- 路由 /huya/device-bindings + 菜单『设备绑定』(账号管理之下)
This commit is contained in:
yml2213
2026-08-29 16:39:30 +08:00
parent bf6dee1850
commit fda3d5257b
6 changed files with 243 additions and 0 deletions
+71
View File
@@ -3,9 +3,13 @@
import asyncio
import csv
import io
import json
import shutil
import threading
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import StreamingResponse
from sqlalchemy import func, or_
@@ -22,6 +26,8 @@ from core.huya import (
send_huya_sms_code,
)
from core.huya.cookie_utils import normalize_huya_cookie
from core.huya.device_fingerprint import account_state_dir as _fp_state_dir
from core.huya.device_profile import _load_db as _load_profile_db, _save_db as _save_profile_db
from core.sms_provider import parse_sms_lines
from ..database import SessionLocal, get_db
@@ -1749,3 +1755,68 @@ async def ws_huya_logs(websocket: WebSocket, batch_id: str):
latest = huya_batch_registry.get(batch_id)
if latest and latest.get("finished"):
huya_batch_registry.pop(batch_id)
# ==================== 设备绑定管理 (一号一环境, R40) ====================
# 环境数据源:
# 1) data/huya_device_profiles.json — 账号 ↔ 设备画像 (core.huya.device_profile)
# 2) data/huya_fp_states/<账号>/ — hydevice 高信任指纹 localStorage (account_state_dir)
# 3) huya_accounts 表 — 登录渠道/账号状态 (登录成功后回填)
# 解绑 = 删除 1+2 → 下次 App 登录自动生成全新环境并重新注册签发 t2/t5。
_DEVICE_BINDINGS_PERMS_VIEW = ("huya:account", "huya:view_all", "huya:view_assigned")
def _mask_hex(value: str | None, keep: int = 12) -> str:
"""设备标识脱敏展示: 只保留前 keep 位。"""
v = value or ""
return f"{v[:keep]}…({len(v)})" if len(v) > keep else v
@router.get("/device-bindings")
def list_device_bindings(db: Session = Depends(get_db), current: User = Depends(get_current_user)):
"""列出所有账号 ↔ 设备环境绑定 (画像 + 指纹状态 + 账号表三方合并)。"""
if not any(user_has_permission(current, p) for p in _DEVICE_BINDINGS_PERMS_VIEW):
raise HTTPException(status_code=403, detail="无虎牙账号查看权限")
profiles = _load_profile_db()
accounts = {a.username: a for a in db.query(HuyaAccount).all() if a.username}
rows = []
for account, prof in sorted(profiles.items()):
acct = accounts.get(account)
state_dir = _fp_state_dir(account)
last_login = prof.get("last_login") or {}
rows.append({
"account": account,
"vendor": prof.get("vendor", ""),
"model": prof.get("model", ""),
"screen": prof.get("screen", ""),
"fingerprint_masked": _mask_hex(prof.get("fingerprint")),
"guid32_masked": _mask_hex(prof.get("guid32")),
"hebe_count": len(prof.get("hebe") or {}),
"hdid32": prof.get("hdid", ""), # t1.t0 app 级常量, 非敏感
"has_hydevice_state": state_dir.exists(),
"login_channel": (acct.login_channel if acct else "") or "",
"account_status": (acct.status if acct else "") or "未入库",
"bound_at": prof.get("bound_at"),
"last_login_at": last_login.get("at"),
"last_login_ok": last_login.get("ok"),
})
return {"items": rows, "total": len(rows)}
@router.delete("/device-bindings/{account}")
def delete_device_binding(account: str, db: Session = Depends(get_db), current: User = Depends(get_current_user)):
"""解绑: 删除该账号的设备画像与 hydevice 指纹状态, 下次登录自动生成全新环境。"""
_require_huya_perm(current, "huya:import")
profiles = _load_profile_db()
if account not in profiles:
raise HTTPException(status_code=404, detail="该账号没有设备绑定记录")
profiles.pop(account)
_save_profile_db(profiles)
state_dir = _fp_state_dir(account)
removed_state = False
if state_dir.exists():
shutil.rmtree(state_dir, ignore_errors=True)
removed_state = True
return {"ok": True, "account": account, "profile_removed": True, "fp_state_removed": removed_state,
"message": "已解绑, 下次 App 协议登录将自动生成全新设备环境并重新注册"}