Files
digital-psychology/tools/cece_capture_home_plus.py
jackyu66gitandCursor 53eb4577b3 feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单
对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:32:35 +08:00

248 lines
7.3 KiB
Python
Raw Permalink 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.
#!/usr/bin/env python3
"""Capture CeCe homepage top-right 「+」→ add family/friend archive flow."""
from __future__ import annotations
import json
import os
import re
import subprocess
import time
from pathlib import Path
ROOT = Path("/Users/jack/Project/digital-psychology")
TOOLS = ROOT / "tools" / "android"
ANDROID_HOME = Path("/opt/homebrew/share/android-commandlinetools")
PKG = "com.xxwolo.cc5"
ACT = f"{PKG}/com.cece.app.MainActivity"
STAMP = time.strftime("%Y%m%d-%H%M%S")
OUT = ROOT / ".tmp" / "cece-validation" / time.strftime("%Y%m%d") / f"home-plus-{STAMP}"
NOTES = OUT / "NOTES.md"
os.environ["ANDROID_HOME"] = str(ANDROID_HOME)
os.environ["ANDROID_SDK_ROOT"] = str(ANDROID_HOME)
os.environ["PATH"] = (
f"{ANDROID_HOME}/emulator:{ANDROID_HOME}/platform-tools:"
f"/opt/homebrew/bin:/usr/bin:/bin:" + os.environ.get("PATH", "")
)
def sh(args, timeout=120):
return subprocess.run(args, capture_output=True, text=True, timeout=timeout)
def adb(*args, timeout=120):
return sh(["adb", *args], timeout=timeout)
def tool(*args, timeout=120):
r = sh([str(TOOLS), *args, "--json"], timeout=timeout)
try:
return json.loads(r.stdout or "{}")
except Exception:
return {"error": r.stderr, "raw": (r.stdout or "")[:500]}
def label(e):
return (e.get("text") or e.get("contentDesc") or e.get("contentDescription") or "").strip()
def elements(data):
return data.get("elements") or []
def dump():
return tool("ui", "dump")
def center(e):
c = e.get("center")
if isinstance(c, dict):
return int(c["x"]), int(c["y"])
b = e.get("bounds") or {}
if isinstance(b, dict) and "width" in b:
return int(b["x"] + b["width"] / 2), int(b["y"] + b["height"] / 2)
return None
def area(e):
b = e.get("bounds") or {}
return int(b.get("width") or 0) * int(b.get("height") or 0)
def tap_xy(x, y, wait=1.6):
adb("shell", "input", "tap", str(x), str(y))
time.sleep(wait)
def back(n=1):
for _ in range(n):
adb("shell", "input", "keyevent", "4")
time.sleep(1.0)
def shot(rel: str, note: str = ""):
path = OUT / rel
path.parent.mkdir(parents=True, exist_ok=True)
tool("screenshot", "--out", str(path))
labs = sorted({label(e).split("\n", 1)[0] for e in elements(dump()) if label(e)})
(OUT / rel.replace(".png", ".labels.txt")).write_text("\n".join(labs), encoding="utf-8")
with NOTES.open("a", encoding="utf-8") as f:
f.write(f"- `{rel}` {note}\n")
f.write(f" labels: {', '.join(labs[:40])}\n")
print("SHOT", rel, note, flush=True)
return path
def save_dump(rel):
path = OUT / rel
path.parent.mkdir(parents=True, exist_ok=True)
data = dump()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return data
def first_lines(data=None):
data = data or dump()
return [label(e).split("\n", 1)[0] for e in elements(data) if label(e)]
def go_home():
adb("shell", "am", "start", "-n", ACT)
time.sleep(2.0)
# tap 首页 tab
tap_xy(108, 2255, wait=1.2)
# scroll top
for _ in range(2):
adb("shell", "input", "swipe", "540", "500", "540", "1600", "280")
time.sleep(0.5)
def find_plus_candidates(data):
"""Top-right + / 添加 / contentDesc."""
hits = []
for e in elements(data):
lab = label(e)
first = lab.split("\n", 1)[0]
b = e.get("bounds") or {}
y = int(b.get("y") or 0)
x = int(b.get("x") or 0)
w = int(b.get("width") or 0)
h = int(b.get("height") or 0)
if y > 280:
continue
if x < 700:
continue
# plus-like
if first in ("+", "", "添加", "新建", "添加档案") or "添加" in lab or lab in ("+", ""):
hits.append(e)
continue
cd = (e.get("contentDesc") or e.get("contentDescription") or "")
if any(k in cd for k in ("添加", "新建", "plus", "Plus", "+")):
hits.append(e)
continue
# small square top-right icon without text
if e.get("clickable") and not lab and w < 120 and h < 120 and x > 850:
hits.append(e)
hits.sort(key=lambda e: (area(e), -(center(e) or (0, 0))[0]))
return hits
def tap_contains(needle, ymax=9999):
data = dump()
cands = []
for e in elements(data):
lab = label(e)
if needle not in lab:
continue
b = e.get("bounds") or {}
if int(b.get("y") or 0) > ymax:
continue
if not e.get("clickable") and area(e) > 200 * 200:
continue
cands.append(e)
cands.sort(key=area)
if not cands:
return False
xy = center(cands[0])
if not xy:
return False
print(f" TAP contains {needle!r} @ {xy}", flush=True)
tap_xy(*xy)
return True
def explore_add_flow():
OUT.mkdir(parents=True, exist_ok=True)
NOTES.write_text(f"# home + archive capture {STAMP}\n\n", encoding="utf-8")
if not re.search(r"emulator-\d+\s+device", adb("devices").stdout or ""):
raise SystemExit("no emulator")
go_home()
shot("00-home.png", "home before +")
data = save_dump("00-home.json")
plus = find_plus_candidates(data)
print("plus candidates:", len(plus), flush=True)
for e in plus[:8]:
print(" ", label(e) or "(empty)", center(e), e.get("bounds"), flush=True)
opened = False
if plus:
xy = center(plus[0])
if xy:
print(f"tap plus candidate {xy}", flush=True)
tap_xy(*xy, wait=2.0)
opened = True
if not opened:
# fallback top-right +
for xy in ((1008, 168), (990, 155), (1020, 190), (960, 170)):
print(f"fallback tap {xy}", flush=True)
tap_xy(*xy, wait=1.8)
fls = first_lines()
if any(
k in " ".join(fls)
for k in ("添加档案", "新建档案", "家人", "朋友", "伴侣", "关系", "昵称", "生日", "档案")
):
opened = True
break
back(1)
time.sleep(0.6)
shot("01-after-plus.png", "after tapping +")
data = save_dump("01-after-plus.json")
fls = first_lines(data)
print("after-plus labels:", fls[:30], flush=True)
# Dive relation type chips if present
for lab in ("家人", "朋友", "伴侣", "同事", "其他", "添加档案", "新建档案", "创建档案", "立即添加"):
if lab in fls or any(lab in x for x in fls):
if tap_contains(lab, ymax=1800):
time.sleep(1.2)
shot(f"02-tap-{lab}.png", f"tap {lab}")
save_dump(f"02-tap-{lab}.json")
# Try fill-ish fields visibility
shot("03-form.png", "add archive form state")
save_dump("03-form.json")
# Look for 关系 / 生日 / 保存
for lab in ("保存", "完成", "确定", "下一步", "生成"):
if tap_contains(lab, ymax=2200):
time.sleep(1.2)
shot(f"04-{lab}.png", f"tap {lab}")
break
# Summary of labels for NOTES
final = first_lines()
with NOTES.open("a", encoding="utf-8") as f:
f.write("\n## Summary labels after +\n")
f.write("\n".join(f"- {x}" for x in final[:80]))
f.write("\n")
print("DONE", OUT, flush=True)
if __name__ == "__main__":
explore_add_flow()