Files
workphone-sdk/sdk/app/services/device_manager.py

252 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
工作手机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硬件字段缺失时绝不能让所有设备
都得到空串 MD5d41d8cd98f00b204e9800998ecf8427e—— 那会令全部设备
互相"指纹碰撞",使碰撞检测失效。无硬件数据时回退到 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()