phone-harnessHarness
§ · Nested detail page

滚动与列表收集 深度解析

滚动与列表收集 这页聚焦 用 OCR 文本重叠判断列表是否真的移动。关键证据包括 src/phone_harness/helpers.py:45、src/phone_harness/mirror.py:91、src/phone_harness/ocr.py:19、src/phone_harness/admin.py:12,所有判断都回到真实源码行号。

§ 00 · IMPORT CHAIN

导入链与契约

滚动子系统先限制 OCR 区域,避开状态栏和底部导航。src/phone_harness/helpers.py:153、src/phone_harness/helpers.py:156、src/phone_harness/helpers.py:159、src/phone_harness/helpers.py:167 把屏幕文本变成集合,后续不依赖业务解析器是否抽到新 item。

src/phone_harness/helpers.py · L153-171Python
153def _content_texts(min_conf=0.4, top_frac=0.06, bottom_frac=0.92):
154    """OCR of the scrollable content area, excluding the volatile status bar
155    (clock/battery) at top and the nav/home strip at bottom."""
156    path, win = mirror.capture()
157    top = win["y"] + win["h"] * top_frac
158    bot = win["y"] + win["h"] * bottom_frac
159    return [o for o in _ocr.recognize(path, win)
160            if top < o["y"] < bot and o["confidence"] >= min_conf]
161
162
163def _text_set(boxes):
164    return frozenset(o["text"].strip() for o in boxes if o["text"].strip())
165
166
167def _overlap(a, b):
168    """Jaccard overlap of two text sets: ~1.0 = same screen, low = it moved."""
169    if not a or not b:
170        return 0.0
171    return len(a & b) / len(a | b)
§ 01 · CORE IMPL

核心实现,逐段拆

▸单步滚动

scroll_screen 的核心是先取 before,再发 wheel scroll,然后等待 OCR 稳定。src/phone_harness/helpers.py:174、src/phone_harness/helpers.py:189、src/phone_harness/helpers.py:193、src/phone_harness/helpers.py:198、src/phone_harness/helpers.py:206 让“是否移动”变成可度量的 overlap,而不是凭感觉。

src/phone_harness/helpers.py · L174-208Python
174def scroll_screen(direction="up", amount=0.6, settle=2.5, moved_thresh=0.6):
175    """One scroll gesture, then wait for the screen to settle (so a lazy-load
176    spinner resolves before we judge movement).
177
178    Returns {moved, overlap, before, after, boxes} — `boxes` is the settled
179    content OCR, ready to parse. `moved` is False when overlap >= moved_thresh:
180    the list didn't advance. The default 0.6 sits in the empirical gap between
181    real forward progress (overlap < ~0.45) and overscroll bounce at a boundary
182    (overlap > ~0.7), which springs the content and would otherwise read as
183    movement and defeat end-detection.
184    """
185    w = mirror.ensure_window()
186    sign = {"up": -1, "down": 1}.get(direction)  # 'up' reveals content below
187    if sign is None:
188        raise ValueError(f"direction must be 'up' or 'down', got {direction!r}")
189    before = _text_set(_content_texts())
190    # Wheel scroll, not drag: a slow touch-drag barely moves an iOS list and
191    # bounces back, while wheel events advance it deterministically (proven on
192    # long lists). amount is a fraction of window height.
193    mirror.scroll_wheel(sign * int(w["h"] * amount),
194                        w["x"] + w["w"] / 2, w["y"] + w["h"] / 2, steps=10)
195    time.sleep(0.4)
196    prev_boxes, prev = None, None
197    deadline = time.time() + settle
198    while time.time() < deadline:
199        boxes = _content_texts()
200        cur = _text_set(boxes)
201        if cur == prev:                 # two identical captures = settled
202            break
203        prev, prev_boxes = cur, boxes
204        time.sleep(0.35)
205    after = prev or frozenset()
206    return {"moved": _overlap(before, after) < moved_thresh,
207            "overlap": round(_overlap(before, after), 3),
208            "before": before, "after": after, "boxes": prev_boxes or []}

▸收集到终点

scroll_collect 对每屏提取结果去重,并用 end_after 连续不移动确认终点。src/phone_harness/helpers.py:239、src/phone_harness/helpers.py:258、src/phone_harness/helpers.py:271、src/phone_harness/helpers.py:276、src/phone_harness/helpers.py:280 这正是移动端长列表自动化最容易写错的部分。

src/phone_harness/helpers.py · L239-284Python
239def scroll_collect(extract=None, key=None, direction="up", amount=0.6,
240                   max_scrolls=400, end_after=3, settle=2.5, on_progress=None):
241    """Scroll a list top-to-bottom, extracting and de-duping items each screen,
242    until the list reaches its true end.
243
244    - extract(boxes) -> list of items for the current screen. Default returns
245      each content text line, so a bare scroll_collect() gathers all text.
246    - key(item) -> hashable de-dup key (default: the item itself).
247    - Stops after `end_after` consecutive non-moving scrolls (the settle window
248      already gave lazy-load a chance), or `max_scrolls`.
249
250    Returns {items, stop, scrolls}. `stop` is 'reached-end' or 'max-scrolls'.
251    Use amount < 1.0 so screens overlap and no row falls between captures.
252    """
253    extract = extract or (lambda boxes: [o["text"].strip() for o in boxes
254                                         if o["text"].strip()])
255    key = key or (lambda x: x)
256    seen, order = set(), []
257
258    def ingest(boxes):
259        new = 0
260        for item in extract(boxes):
261            k = key(item)
262            if k in seen:
263                continue
264            seen.add(k)
265            order.append(item)
266            new += 1
267        return new
268
269    ingest(_content_texts())
270    stale = 0
271    for i in range(1, max_scrolls + 1):
272        res = scroll_screen(direction, amount, settle)
273        new = ingest(res["boxes"])
274        if on_progress:
275            on_progress(i, len(order), new, res["moved"], res["overlap"])
276        if res["moved"]:
277            stale = 0
278        else:
279            stale += 1
280            if stale >= end_after:
281                return {"items": order, "stop": "reached-end", "scrolls": i}
282            time.sleep(0.8)            # extra grace for a slow lazy-load
283            mirror.activate()
284    return {"items": order, "stop": "max-scrolls", "scrolls": max_scrolls}
§ 02 · KEY FLOW

关键流程

capture / OCR / helper → compute screen-point evidence → post Quartz event or raise user-action error → capture again and compare visible state

这个流程的关键是每一步都有可观察证据:src/phone_harness/mirror.py:84、src/phone_harness/ocr.py:48、src/phone_harness/helpers.py:73、src/phone_harness/helpers.py:316、src/phone_harness/admin.py:65 分别覆盖截图、OCR 坐标、screen_info、稳定等待和 doctor 结果。它不像浏览器自动化那样有 DOM,所以截图就是最终断言。

§ 03 · EDGE CASES

边界 / 失败模式

  • src/phone_harness/helpers.py:67 遇到 blocked interstitial 时要求用户处理,防止 agent 越权。
  • src/phone_harness/mirror.py:251 对无法输入字符抛错,避免用户以为文本已发送。
  • src/phone_harness/helpers.py:280 需要连续不移动才结束列表,避免 lazy-load 误判。
  • src/phone_harness/admin.py:55 用截图字节数识别空 capture,提示重启终端让录屏权限生效。
Sub-page · 2026-08-10 Source · ShawnPana/phone-harness ↑ phone-harness 主页面