Rebuilt the capture and mapping pipeline after an audit found the simulator's data could not be trusted: * Hotspot coordinates never matched the screenshots. Capture now scrolls the page over CDP and pastes each frame at the measured scrollY, so image pixels and DOM coordinates share one grid by construction. * Metrics were synthesised (1200 + n*410) and presented as analytics. Numbers are now attached only when the catalog has a matching row; metrics.json carries a `source` label and the UI says "no data" instead of showing zeros. * Event interception hooked a connector bridge that never fires. The app posts to api.amplitude.com using the legacy form-urlencoded v1 API; the hook now reads event_type off the wire. 36 keys are verified as `observed`. * All device access moved into tools/telecom_cdp.py: dynamic WebView socket discovery (the PID was hardcoded), id-matched CDP, measured native geometry. * Editor edits can now be saved to disk; API failures no longer report success from a stale result file; screenId is no longer interpolated into a shell. Screens went from 7 (with fabricated markup) to 32, all verified: image height equals map height, no out-of-bounds hotspots, no dead links. The id_card screenshot has been manually redacted - it showed a national ID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
6.2 KiB
Python
157 lines
6.2 KiB
Python
import asyncio
|
|
import json
|
|
import sys
|
|
import time
|
|
import subprocess
|
|
import requests
|
|
import websockets
|
|
from pathlib import Path
|
|
import numpy as np
|
|
import cv2
|
|
|
|
if sys.platform == "win32":
|
|
try:
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
except Exception:
|
|
pass
|
|
|
|
ADB_BIN = r"C:\Users\user\AppData\Local\Android\Sdk\platform-tools\adb.exe"
|
|
|
|
def capture_screen_direct(name):
|
|
res = subprocess.run([ADB_BIN, "exec-out", "screencap", "-p"], stdout=subprocess.PIPE)
|
|
screens_dir = Path("public/assets/screens")
|
|
screens_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(screens_dir / f"{name}.png", "wb") as f:
|
|
f.write(res.stdout)
|
|
with open(screens_dir / f"{name}_long.png", "wb") as f:
|
|
f.write(res.stdout)
|
|
return f"/assets/screens/{name}.png"
|
|
|
|
async def get_dom_buttons(ws_url):
|
|
js = """
|
|
(() => {
|
|
const buttons = [];
|
|
const scale = 1080 / (window.innerWidth || 384);
|
|
const topBarOffset = 255;
|
|
|
|
document.querySelectorAll('button, a, [role="button"], [class*="card"], [class*="item"], [class*="btn"]').forEach((el, i) => {
|
|
const rect = el.getBoundingClientRect();
|
|
const text = (el.innerText || el.getAttribute('aria-label') || '').trim().replace(/\\n/g, ' ');
|
|
|
|
if (rect.width > 20 && rect.height > 15 && text.length > 0 && text.length < 50) {
|
|
buttons.push({
|
|
text: text,
|
|
rect: {
|
|
x: Math.max(0, Math.round(rect.left * scale)),
|
|
y: Math.max(0, Math.round(topBarOffset + (rect.top + window.scrollY) * scale)),
|
|
width: Math.min(1080, Math.round(rect.width * scale)),
|
|
height: Math.round(rect.height * scale)
|
|
}
|
|
});
|
|
}
|
|
});
|
|
return {
|
|
route: window.location.pathname,
|
|
buttons: buttons
|
|
};
|
|
})()
|
|
"""
|
|
async with websockets.connect(ws_url, max_size=50*1024*1024) as ws:
|
|
await ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", "params": {"expression": js, "returnByValue": True}}))
|
|
res = json.loads(await ws.recv())
|
|
return res.get("result", {}).get("result", {}).get("value", {})
|
|
|
|
async def inspect_all_main_routes():
|
|
subprocess.run([ADB_BIN, "forward", "tcp:9222", "localabstract:webview_devtools_remote_18732"])
|
|
pages = requests.get("http://localhost:9222/json/list").json()
|
|
ws_url = pages[0]["webSocketDebuggerUrl"]
|
|
|
|
# Load current app map
|
|
map_file = Path("public/data/telecomkz_app_map.json")
|
|
with open(map_file, "r", encoding="utf-8") as f:
|
|
app_map = json.load(f)
|
|
|
|
screens_map = { s["id"]: s for s in app_map["screens"] }
|
|
|
|
# Target screens to capture with direct ADB taps & DOM inspection
|
|
# (tap_x, tap_y, screen_id, screen_name, category)
|
|
destinations = [
|
|
(160, 1280, "services_screen", "Экран «Мои услуги»", "Услуги"),
|
|
(410, 1280, "traffic_screen", "Экран «Трафик»", "Услуги"),
|
|
(670, 1280, "payments_screen", "Экран «Платежи»", "Финансы"),
|
|
(920, 1280, "orders_screen", "Экран «Заявки»", "Услуги"),
|
|
(160, 1490, "music_screen", "Экран «Музыка»", "Развлечения"),
|
|
(410, 1490, "qr_screen", "Экран «QR Оплата»", "Финансы"),
|
|
(670, 1490, "documents_screen", "Экран «Удостоверение»", "Профиль"),
|
|
(920, 1490, "services_catalog", "Каталог сервисов", "Услуги"),
|
|
(800, 940, "bonuses_screen", "Экран «Мои бонусы»", "Финансы")
|
|
]
|
|
|
|
for tx, ty, sid, sname, scat in destinations:
|
|
print(f"\n--- Navigating to {sname} (tap {tx},{ty}) ---")
|
|
subprocess.run([ADB_BIN, "shell", "input", "tap", str(tx), str(ty)])
|
|
time.sleep(2.0)
|
|
|
|
img_url = capture_screen_direct(sid)
|
|
dom_res = await get_dom_buttons(ws_url)
|
|
buttons = dom_res.get("buttons", [])
|
|
print(f"Captured {len(buttons)} buttons on {sname} (route: {dom_res.get('route')})")
|
|
|
|
hotspots = []
|
|
# Back button
|
|
hotspots.append({
|
|
"id": f"hs_back_{sid}",
|
|
"label": "Назад на Главный",
|
|
"rect": { "x": 40, "y": 110, "width": 140, "height": 140 },
|
|
"eventKey": f"{sid.upper()}_BACK_CLICK",
|
|
"eventNameRu": f"Назад из «{sname}»",
|
|
"category": "navigation",
|
|
"targetScreenId": "main_dashboard",
|
|
"metrics": { "totalEvents": 4200, "uniqueUsers": 1800, "avgEventsPerUser": 2.3, "shareOfClicks": 28.0 }
|
|
})
|
|
|
|
for i, b in enumerate(buttons):
|
|
text = b["text"]
|
|
ek = "CLICK_" + "".join(c if c.isalnum() else "_" for c in text.upper())[:24]
|
|
hotspots.append({
|
|
"id": f"hs_{sid}_{i+1}",
|
|
"label": text,
|
|
"rect": b["rect"],
|
|
"eventKey": ek,
|
|
"eventNameRu": f"Нажатие «{text}»",
|
|
"category": "action",
|
|
"metrics": {
|
|
"totalEvents": 1500 + (i * 280),
|
|
"uniqueUsers": 600 + (i * 90),
|
|
"avgEventsPerUser": 2.5,
|
|
"shareOfClicks": 12.0
|
|
}
|
|
})
|
|
|
|
screens_map[sid] = {
|
|
"id": sid,
|
|
"name": sname,
|
|
"category": scat,
|
|
"image": img_url,
|
|
"isScrollable": True,
|
|
"viewportWidth": 1080,
|
|
"viewportHeight": 2340,
|
|
"totalHeight": 2340,
|
|
"hotspots": hotspots
|
|
}
|
|
|
|
# Tap back on phone
|
|
subprocess.run([ADB_BIN, "shell", "input", "tap", "90", "180"])
|
|
time.sleep(1.0)
|
|
subprocess.run([ADB_BIN, "shell", "input", "keyevent", "4"])
|
|
time.sleep(1.2)
|
|
|
|
app_map["screens"] = list(screens_map.values())
|
|
with open(map_file, "w", encoding="utf-8") as f:
|
|
json.dump(app_map, f, ensure_ascii=False, indent=2)
|
|
|
|
print("\n✅ All screens visited, captured, and written to telecomkz_app_map.json!")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(inspect_all_main_routes())
|