feat: 按测测功能重构 H5 首页与各子页,并修正顶栏「+」添加档案菜单

对齐测测交互:首页「+」支持邀请填档案/添加档案/邀请合盘;各 Tab 与工具页按同一视觉与功能标准落地;本地对照截图目录显式忽略。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-05 14:32:35 +08:00
co-authored by Cursor
parent bd22d9dddd
commit 53eb4577b3
35 changed files with 8900 additions and 1356 deletions
+1958
View File
File diff suppressed because it is too large Load Diff
+247
View File
@@ -0,0 +1,247 @@
#!/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()
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
# Watchdog: keep deep crawl alive; restart on death.
set -u
ROOT="/Users/jack/Project/digital-psychology"
LOG="/tmp/cece-capture-deep-full.log"
PIDFILE="/tmp/cece-capture-deep.pid"
MAX_RESTARTS=30
cd "$ROOT"
restart=0
while [ "$restart" -lt "$MAX_RESTARTS" ]; do
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
sleep 20
continue
fi
restart=$((restart + 1))
echo "[$(date '+%F %T')] watchdog start #$restart" >> "$LOG"
adb shell am start -n com.xxwolo.cc5/com.cece.app.MainActivity >/dev/null 2>&1 || true
sleep 2
nohup python3 -u tools/cece_capture_deep.py >> "$LOG" 2>&1 &
echo $! > "$PIDFILE"
echo "[$(date '+%F %T')] pid=$(cat "$PIDFILE")" >> "$LOG"
# wait until death
while kill -0 "$(cat "$PIDFILE")" 2>/dev/null; do
sleep 15
# stall detection: no log growth 3 min
if [ -f "$LOG" ]; then
sz1=$(wc -c < "$LOG")
sleep 90
sz2=$(wc -c < "$LOG")
if [ "$sz1" -eq "$sz2" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "[$(date '+%F %T')] stall → kill $(cat "$PIDFILE")" >> "$LOG"
kill "$(cat "$PIDFILE")" 2>/dev/null || true
sleep 2
kill -9 "$(cat "$PIDFILE")" 2>/dev/null || true
break
fi
fi
done
echo "[$(date '+%F %T')] crawl exited, restart soon" >> "$LOG"
sleep 5
done
echo "[$(date '+%F %T')] watchdog gave up after $MAX_RESTARTS" >> "$LOG"
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Daemonize + run cece_capture_deep outside the parent process group.
Cursor/agent shells often SIGKILL the whole process group when a command ends;
nohup alone is not enough. Double-fork so the crawl survives.
"""
from __future__ import annotations
import atexit
import faulthandler
import os
import runpy
import sys
import time
from pathlib import Path
ROOT = Path("/Users/jack/Project/digital-psychology")
LOG = Path("/tmp/cece-capture-deep-full.log")
FAULT = Path("/tmp/cece-fault.log")
PIDFILE = Path("/tmp/cece-capture-deep.pid")
def log(msg: str) -> None:
line = f"[{time.strftime('%F %T')}] {msg}\n"
with LOG.open("a", encoding="utf-8") as f:
f.write(line)
def daemonize() -> None:
# first fork
if os.fork() > 0:
sys.exit(0)
os.setsid()
# second fork
if os.fork() > 0:
sys.exit(0)
# detach stdio
sys.stdin.close()
sys.stdout.flush()
sys.stderr.flush()
out = open(LOG, "a", encoding="utf-8", buffering=1)
sys.stdout = out # type: ignore
sys.stderr = out # type: ignore
def main() -> None:
os.chdir(ROOT)
daemonize()
PIDFILE.write_text(str(os.getpid()), encoding="utf-8")
ff = FAULT.open("a", encoding="utf-8")
faulthandler.enable(file=ff, all_threads=True)
log(f"daemon start pid={os.getpid()} argv={sys.argv[1:]}")
def _atexit() -> None:
log(f"daemon atexit pid={os.getpid()}")
atexit.register(_atexit)
# remaining args after this script name
args = sys.argv[1:]
sys.argv = ["cece_capture_deep.py", *args]
try:
runpy.run_path(str(ROOT / "tools" / "cece_capture_deep.py"), run_name="__main__")
log("daemon finished ok")
except BaseException as ex:
log(f"daemon error: {type(ex).__name__}: {ex}")
import traceback
traceback.print_exc()
raise SystemExit(1)
if __name__ == "__main__":
main()
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Stable launcher for cece_capture_deep — logs exit code, avoids silent death."""
from __future__ import annotations
import atexit
import faulthandler
import os
import runpy
import sys
import time
from pathlib import Path
LOG = Path("/tmp/cece-capture-deep-full.log")
FAULT = Path("/tmp/cece-fault.log")
ROOT = Path("/Users/jack/Project/digital-psychology")
def _log(msg: str) -> None:
line = f"[{time.strftime('%F %T')}] {msg}\n"
with LOG.open("a", encoding="utf-8") as f:
f.write(line)
sys.stdout.write(line)
sys.stdout.flush()
def main() -> int:
os.chdir(ROOT)
FAULT.parent.mkdir(parents=True, exist_ok=True)
ff = FAULT.open("a", encoding="utf-8")
faulthandler.enable(file=ff, all_threads=True)
_log(f"launcher start pid={os.getpid()} argv={sys.argv[1:]}")
def _atexit() -> None:
_log(f"atexit pid={os.getpid()}")
atexit.register(_atexit)
sys.argv = ["cece_capture_deep.py", *sys.argv[1:]]
try:
runpy.run_path(str(ROOT / "tools" / "cece_capture_deep.py"), run_name="__main__")
_log("launcher normal return")
return 0
except BaseException as ex:
_log(f"launcher exception: {type(ex).__name__}: {ex}")
import traceback
with LOG.open("a", encoding="utf-8") as f:
traceback.print_exc(file=f)
traceback.print_exc()
return 1
if __name__ == "__main__":
raise SystemExit(main())