""" 工作手机SDK v3.0 - 设备管理服务(含设备指纹校验) """ import hashlib from typing import Optional, List, TYPE_CHECKING try: from motor.motor_asyncio import AsyncIOMotorClient except ImportError: # NAS 无 Mongo 依赖时降级 AsyncIOMotorClient = None # type: ignore[misc, assignment] import logging from datetime import datetime from config import settings logger = logging.getLogger(__name__) class DeviceManager: """设备管理器""" def __init__(self): self.client: Optional[AsyncIOMotorClient] = None self.db = None async def init(self): """初始化数据库连接(MongoDB 不可用时降级为无 DB 模式)""" if AsyncIOMotorClient is None: logger.warning("motor 未安装,MongoDB 功能降级为无 DB 模式") return try: self.client = AsyncIOMotorClient( settings.MONGO_URI, serverSelectionTimeoutMS=3000, # 3秒超时,不阻塞启动 ) await self.client.admin.command("ping") self.db = self.client[settings.MONGO_DB] # 尝试创建索引 try: await self.db.devices.create_index("device_id", unique=True) await self.db.commands.create_index("device_id") await self.db.commands.create_index("created_at") logger.info(f"MongoDB连接成功: {settings.MONGO_DB}") except Exception as e: logger.warning(f"MongoDB索引创建跳过(可能需要认证): {e}") logger.info(f"MongoDB连接成功(无索引): {settings.MONGO_DB}") except Exception as e: logger.warning(f"MongoDB 连接失败(降级为无DB模式,ADB 直连仍可用): {e}") if self.client: self.client.close() self.client = None self.db = None async def close(self): """关闭连接""" if self.client: self.client.close() # ========== 设备管理 ========== async def register_device(self, device_data: dict) -> dict: """注册或更新设备(自动计算并校验指纹)""" device_id = device_data.get("device_id") fp_hash = self.compute_fingerprint(device_data) device_data["fingerprint_hash"] = fp_hash if self.db is None: return {"device_id": device_id, "updated": False, "fingerprint": fp_hash} device_data["updated_at"] = datetime.now() result = await self.db.devices.update_one( {"device_id": device_id}, { "$set": device_data, "$setOnInsert": {"created_at": datetime.now()} }, upsert=True ) collision = await self.check_fingerprint_collision(device_id, device_data) if collision["collision"]: logger.warning( f"[防封] 注册设备 {device_id} 指纹碰撞: {collision['collided_with']}" ) return { "device_id": device_id, "updated": result.modified_count > 0, "fingerprint": fp_hash, "fingerprint_collision": collision["collision"], "collided_with": collision.get("collided_with", []), } async def get_device(self, device_id: str) -> Optional[dict]: """获取设备信息""" if self.db is None: return None return await self.db.devices.find_one({"device_id": device_id}, {"_id": 0}) async def get_all_devices(self, skip: int = 0, limit: int = 100) -> List[dict]: """获取所有设备""" if self.db is None: return [] cursor = self.db.devices.find({}, {"_id": 0}).skip(skip).limit(limit) return await cursor.to_list(length=limit) async def update_device_status(self, device_id: str, status: str): """更新设备状态""" if self.db is None: return await self.db.devices.update_one( {"device_id": device_id}, {"$set": {"status": status, "updated_at": datetime.now()}} ) async def update_heartbeat(self, device_id: str): """更新设备心跳时间(设备状态实时反馈到服务器/数据库)""" if self.db is None: return now = datetime.now() await self.db.devices.update_one( {"device_id": device_id}, {"$set": {"last_heartbeat": now, "status": "online", "updated_at": now}}, upsert=True ) # ========== 设备指纹校验(防封 AF11) ========== @staticmethod def compute_fingerprint(info: dict) -> str: """从设备上报信息计算指纹哈希(MD5)。 防封要点(G2 修复 2026-05-31):硬件字段缺失时,绝不能让所有设备 都得到空串 MD5(d41d8cd98f00b204e9800998ecf8427e)—— 那会令全部设备 互相"指纹碰撞",使碰撞检测失效。无硬件数据时回退到 device_id 作为 稳定种子,保证一机一指纹;同时兼容嵌套 display.{width,height}。 """ keys = sorted([ "brand", "model", "manufacturer", "android_version", "sdk_version", "serial", "imei", "mac", "bluetooth_mac", "screen_width", "screen_height", "density", "cpu_abi", "fingerprint", "android_id", ]) merged = dict(info or {}) # 兼容 agent 上报的嵌套 display.{width,height} disp = merged.get("display") if isinstance(disp, dict): merged.setdefault("screen_width", disp.get("width") or disp.get("displayWidth")) merged.setdefault("screen_height", disp.get("height") or disp.get("displayHeight")) parts = [] for k in keys: v = merged.get(k, "") if v: parts.append(f"{k}={v}") if not parts: # 无任何硬件字段:用 device_id 兜底,避免全设备空哈希误撞 did = merged.get("device_id", "") if did: parts.append(f"device_id={did}") raw = "|".join(parts) return hashlib.md5(raw.encode("utf-8")).hexdigest() async def check_fingerprint_collision(self, device_id: str, info: dict) -> dict: """ 检查设备指纹是否与其他设备碰撞。 返回 {"collision": bool, "collided_with": [device_ids...]} """ fp = self.compute_fingerprint(info) if self.db is None: return {"collision": False, "collided_with": [], "fingerprint": fp} try: await self.db.devices.update_one( {"device_id": device_id}, {"$set": {"fingerprint_hash": fp, "fingerprint_info": info}}, upsert=True, ) except Exception as e: logger.warning(f"指纹写入失败: {e}") try: cursor = self.db.devices.find( {"fingerprint_hash": fp, "device_id": {"$ne": device_id}}, {"device_id": 1, "_id": 0}, ) collisions = [doc["device_id"] async for doc in cursor] except Exception as e: logger.warning(f"指纹碰撞查询失败: {e}") collisions = [] if collisions: logger.warning( f"[防封] 设备指纹碰撞! {device_id} 与 {collisions} 指纹相同 (hash={fp})" ) return { "collision": len(collisions) > 0, "collided_with": collisions, "fingerprint": fp, } # ========== 命令日志 ========== async def log_command( self, device_id: str, command_type: str, params: dict, result: dict ): """记录命令执行日志""" if self.db is None: return try: await self.db.commands.insert_one({ "device_id": device_id, "command_type": command_type, "params": params, "result": result, "created_at": datetime.now() }) except Exception as e: # Mongo 无权限/不可用时不阻断主流程 logger.warning(f"命令日志写入失败(已跳过): {e}") async def get_command_logs( self, device_id: str, limit: int = 50 ) -> List[dict]: """获取命令日志""" if self.db is None: return [] try: cursor = self.db.commands.find( {"device_id": device_id}, {"_id": 0} ).sort("created_at", -1).limit(limit) return await cursor.to_list(length=limit) except Exception as e: logger.warning(f"命令日志查询失败(已跳过): {e}") return [] # 全局实例 device_manager = DeviceManager()