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:
@@ -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 协议登录将自动生成全新设备环境并重新注册"}
|
||||
|
||||
Reference in New Issue
Block a user