Browse Source

guiji真人轨迹模板入库 + 采集链路稳定性修复

- yzm/tmp_captcha_test6: 模板路径指向项目内guiji/, 加载门槛由老录制器原始单位改为像素(适配新录制器, 分桶450/600px与运行时一致)
- 白屏: 详情页等待循环提前检测(连续3轮仅状态栏文字→确认白屏立即跳过, 免盲等90s); 恢复策略改为关App休息60~120s再重启(替代无效的冷却等待)
- 店铺反馈弹窗: 滑动误触长按弹出的"店铺问题"弹窗自动检测并关闭(点暗区/右上×, 不用back), 关闭后补滑防批次空转
- 手势层: TouchPipe异常必补UP防指针卡屏; down延迟>0.35s中止防长按; Windows定时器精度调至1ms(修复sleep最小15.6ms粒度导致的整体变慢); 左滑提速至~0.25s
- 决策留档: 重启恢复4类判定点自动存时间戳截图(restart_home1/2/kw/ai), 便于核对AI误判
- requirements: paddleocr 3.7.0 / paddlepaddle 3.3.1(适配predict新API), 补chardet与uiautodev
- snapshot: 落盘前先建scrape_data目录, 修复首次截图FileNotFoundError

Co-Authored-By: Claude Code <noreply@anthropic.com>
zengyongsheng 1 week ago
parent
commit
b628b1c13b
35 changed files with 151 additions and 37 deletions
  1. 0 0
      tbsg/guiji/track_20260807_175356_717200.json
  2. BIN
      tbsg/guiji/track_20260807_175356_717200.png
  3. 0 0
      tbsg/guiji/track_20260807_175408_670300.json
  4. BIN
      tbsg/guiji/track_20260807_175408_670300.png
  5. 0 0
      tbsg/guiji/track_20260807_175421_249400.json
  6. BIN
      tbsg/guiji/track_20260807_175421_249400.png
  7. 0 0
      tbsg/guiji/track_20260807_175431_035900.json
  8. BIN
      tbsg/guiji/track_20260807_175431_035900.png
  9. 0 0
      tbsg/guiji/track_20260807_175449_432800.json
  10. BIN
      tbsg/guiji/track_20260807_175449_432800.png
  11. 0 0
      tbsg/guiji/track_20260807_175501_274200.json
  12. BIN
      tbsg/guiji/track_20260807_175501_274200.png
  13. 0 0
      tbsg/guiji/track_20260807_175512_099500.json
  14. BIN
      tbsg/guiji/track_20260807_175512_099500.png
  15. 0 0
      tbsg/guiji/track_20260807_175525_817000.json
  16. BIN
      tbsg/guiji/track_20260807_175525_817000.png
  17. 0 0
      tbsg/guiji/track_20260807_175537_562400.json
  18. BIN
      tbsg/guiji/track_20260807_175537_562400.png
  19. 0 0
      tbsg/guiji/track_20260807_175548_208600.json
  20. BIN
      tbsg/guiji/track_20260807_175548_208600.png
  21. 0 0
      tbsg/guiji/track_20260807_175558_198400.json
  22. BIN
      tbsg/guiji/track_20260807_175558_198400.png
  23. 0 0
      tbsg/guiji/track_20260807_175659_466000.json
  24. BIN
      tbsg/guiji/track_20260807_175659_466000.png
  25. 0 0
      tbsg/guiji/track_20260807_175710_765300.json
  26. BIN
      tbsg/guiji/track_20260807_175710_765300.png
  27. 0 0
      tbsg/guiji/track_20260807_175720_455600.json
  28. BIN
      tbsg/guiji/track_20260807_175720_455600.png
  29. 0 0
      tbsg/guiji/track_20260807_175732_986000.json
  30. BIN
      tbsg/guiji/track_20260807_175732_986000.png
  31. 41 6
      tbsg/human_touch.py
  32. 103 27
      tbsg/main1.py
  33. 5 3
      tbsg/requirements.txt
  34. 1 0
      tbsg/snapshot.py
  35. 1 1
      tbsg/yzm/tmp_captcha_test6.py

File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175356_717200.json


BIN
tbsg/guiji/track_20260807_175356_717200.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175408_670300.json


BIN
tbsg/guiji/track_20260807_175408_670300.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175421_249400.json


BIN
tbsg/guiji/track_20260807_175421_249400.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175431_035900.json


BIN
tbsg/guiji/track_20260807_175431_035900.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175449_432800.json


BIN
tbsg/guiji/track_20260807_175449_432800.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175501_274200.json


BIN
tbsg/guiji/track_20260807_175501_274200.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175512_099500.json


BIN
tbsg/guiji/track_20260807_175512_099500.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175525_817000.json


BIN
tbsg/guiji/track_20260807_175525_817000.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175537_562400.json


BIN
tbsg/guiji/track_20260807_175537_562400.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175548_208600.json


BIN
tbsg/guiji/track_20260807_175548_208600.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175558_198400.json


BIN
tbsg/guiji/track_20260807_175558_198400.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175659_466000.json


BIN
tbsg/guiji/track_20260807_175659_466000.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175710_765300.json


BIN
tbsg/guiji/track_20260807_175710_765300.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175720_455600.json


BIN
tbsg/guiji/track_20260807_175720_455600.png


File diff suppressed because it is too large
+ 0 - 0
tbsg/guiji/track_20260807_175732_986000.json


BIN
tbsg/guiji/track_20260807_175732_986000.png


+ 41 - 6
tbsg/human_touch.py

@@ -10,10 +10,21 @@
 """
 import json
 import random
+import socket
 import subprocess
+import sys
 import threading
 import time
 
+# Windows 默认定时器精度 ~15.6ms, time.sleep(0.004) 实际睡 ~15.6ms,
+# 导致滑动触点节奏全部变慢(设计4ms实跑15ms)。进程内调到 1ms 精度,一次性全局生效。
+if sys.platform == "win32":
+    try:
+        import ctypes
+        ctypes.windll.winmm.timeBeginPeriod(1)
+    except Exception:
+        pass
+
 from uiautomator2.core import AdbHTTPConnection
 
 
@@ -71,36 +82,60 @@ class TouchPipe:
 
     def close(self):
         self._stop.set()
-        if self._drainer:
-            self._drainer.join(timeout=1)
+        try:
+            if self._sock:
+                self._sock.shutdown(socket.SHUT_RDWR)   # 立即打断drainer的recv,不等0.5s超时
+        except Exception:
+            pass
         try:
             if self._sock:
                 self._sock.close()
         except Exception:
             pass
+        if self._drainer:
+            self._drainer.join(timeout=0.2)
 
 
-def touchpipe_drag(d, pts: list, step: float = None) -> bool:
+def touchpipe_drag(d, pts: list, step: float = None, hold: float = None,
+                   tail: float = None) -> bool:
     """TouchPipe 长连发送一笔弧线(60点/100Hz级, 手指全程不抬, 丝滑+弧线)。
-    d = uiautomator2 设备对象。返回 True=成功, False=通道不可用(调用方降级)。"""
+    d = uiautomator2 设备对象。返回 True=成功, False=通道不可用(调用方降级)。
+    hold: 按下后停顿(默认0.08~0.14s拟人); tail: 抬起前停顿(默认0.03~0.06s);
+    轻扫场景(如图片左滑)传更小的 step/hold/tail。
+    注: 按住 ~500ms 不动 App 会判长按(列表页误触"店铺问题"反馈弹窗),
+    故 down→首move 间隔超 0.35s 即中止; 异常时 finally 必补 UP, 防手指钉在屏幕上。"""
     if step is None:
         step = random.uniform(0.008, 0.014)
+    if hold is None:
+        hold = random.uniform(0.08, 0.14)
+    if tail is None:
+        tail = random.uniform(0.03, 0.06)
     pipe = None
+    up_sent = False
     try:
         pipe = TouchPipe(d).open()
         if not pipe._open_ok:
             return False
+        t0 = time.time()
         d.touch.down(int(pts[0][0]), int(pts[0][1]))
-        time.sleep(random.uniform(0.08, 0.14))          # 按住停顿(验证码同款手感)
+        time.sleep(hold)                                # 按住停顿(验证码同款手感)
+        if time.time() - t0 > 0.35:                     # down延迟过久→一动就成长按
+            return False
         for (x, y) in pts[1:-1]:
             time.sleep(step)
             pipe.move(int(x), int(y), 0)
-        time.sleep(random.uniform(0.03, 0.06))
+        time.sleep(tail)
         d.touch.up(int(pts[-1][0]), int(pts[-1][1]))
+        up_sent = True
         return True
     except Exception:
         return False
     finally:
+        if not up_sent:
+            try:                                        # 补发UP,防指针残留触发长按
+                d.touch.up(int(pts[-1][0]), int(pts[-1][1]))
+            except Exception:
+                pass
         if pipe is not None:
             pipe.close()
 

+ 103 - 27
tbsg/main1.py

@@ -45,7 +45,7 @@ ROW_REST_MAX = 17             # 50条整点休息最长分钟数
 
 # 进入商品页unknown(页面没加载出来)时的处理:
 #   每确认一次unknown → back一层+重新点击进入;累计UNKNOWN_REENTER_LIMIT次加载全失败 → 跳过本商品
-#   连续2个商品都这样 → step3白屏分级恢复:冷却×2轮 → 重启App(最多MAX_WHITE_RESTART次)→ 超限回告调度重派
+#   连续2个商品都这样 → step3白屏恢复:关App休息60~120s → 重启App(最多MAX_WHITE_RESTART次)→ 超限回告调度重派
 UNKNOWN_REENTER_LIMIT = 5
 MAX_WHITE_RESTART = 3       # 白屏重启恢复上限(超过→置重派标志,回告调度换设备,不终止任务数据)
 WHITE_SUCCESS_RESET_N = 5   # 连续成功采N个商品后清零白屏重启计数(额度恢复)
@@ -154,6 +154,18 @@ def _shot_path(ex: SafeExecutor, name: str) -> str:
     return str(d / name)
 
 
+def _save_evidence(shot: str, tag: str):
+    """决策留档: 触发重启/放弃时刻的截图另存时间戳副本。
+    page_check/detail_check 同一家店内会被下一轮覆盖,
+    留档(时间戳+原因标签)便于事后人工核对AI是否误判。"""
+    try:
+        if shot and os.path.exists(shot):
+            ts = time.strftime("%Y%m%d_%H%M%S")
+            shutil.copy2(shot, str(Path(shot).parent / f"{ts}_{tag}.png"))
+    except Exception:
+        pass
+
+
 def _screenshot(ex: SafeExecutor, name: str) -> str:
     import os
     # 按 设备ID/步骤 隔离截图(目录已含设备ID,文件名不再加设备前缀)
@@ -990,13 +1002,16 @@ def _adb_swipe_left(ex: SafeExecutor, y_ratio: float = 0.3):
     w, h = ex.driver.window_size()
     distance = int(w * _random.uniform(0.55, 0.68))
     try:
-        pts = wrist_arc_pts_h(w, h, distance, y_ratio=y_ratio, n=60, drift_range=(30, 70))
-        if touchpipe_drag(ex.driver, pts):
+        pts = wrist_arc_pts_h(w, h, distance, y_ratio=y_ratio, n=30, drift_range=(30, 70))
+        # 真人轻扫~0.2s: 点数减半 + 步进/停顿收紧, 整笔含down/up往返~0.25s完成
+        if touchpipe_drag(ex.driver, pts, step=_random.uniform(0.003, 0.005),
+                          hold=_random.uniform(0.02, 0.05),
+                          tail=_random.uniform(0.015, 0.04)):
             human_sleep(1.2)
             return
         print("    [图片左滑] TouchPipe未生效,降级motionevent")
         pts10 = wrist_arc_pts_h(w, h, distance, y_ratio=y_ratio, n=10, drift_range=(30, 70))
-        motionevent_drag(ex.device_id, pts10)
+        motionevent_drag(ex.device_id, pts10, step=_random.uniform(0.015, 0.022))
         human_sleep(1.2)
         return
     except Exception as e:
@@ -1032,6 +1047,57 @@ def _adb_swipe_up_short(ex: SafeExecutor):
     )
     human_sleep(1)
 
+# ── 店铺反馈弹窗(滑动被误判长按店铺名)检测与关闭 ──
+FEEDBACK_POPUP_KW = ("店铺问题", "不喜欢该店铺", "与搜索词无关",
+                     "图片不够真实", "商品品质不好", "价格太贵", "疑似情色")
+
+
+def _close_feedback_popup(ex: SafeExecutor) -> bool:
+    """检测并关闭"店铺问题/商品问题"反馈弹窗(滑动被误判长按所致)。
+    关闭方式: 点弹窗上方暗区,或右上角×(back 会退回搜索页,不能用)。
+    返回 True=检测到弹窗(无论是否成功关闭), False=没弹窗。"""
+    try:
+        shot = _shot_path(ex, "ad_feedback_popup.png")
+        ex.driver.screenshot(shot)
+        blocks = OCR.recognize(shot, detail="all")
+        texts = [b.get("text", "") for b in blocks]
+        if not any(kw in t for t in texts for kw in FEEDBACK_POPUP_KW):
+            return False
+        print("[step3] 检测到店铺反馈弹窗(滑动误触长按),关闭中")
+        w, h = ex.driver.window_size()
+        anchor_y = 0
+        for b in blocks:
+            if "店铺问题" in b.get("text", ""):
+                anchor_y = (b["box"][1] + b["box"][3]) // 2
+                break
+        # 1) 点弹窗上方暗区(大目标); 2) 点×(与"店铺问题"同行右侧); 3) ×实测兜底位
+        taps = [(int(w * 0.5), int(h * 0.25))]
+        if anchor_y:
+            taps.append((int(w * 0.84), anchor_y))
+        taps.append((int(w * 0.84), int(h * 0.52)))
+        for tx, ty in taps:
+            ex.tap(tx, ty)
+            time.sleep(1.2)
+            ex.driver.screenshot(shot)
+            blocks = OCR.recognize(shot, detail="all")
+            texts = [b.get("text", "") for b in blocks]
+            if not any(kw in t for t in texts for kw in FEEDBACK_POPUP_KW):
+                print("[step3] 反馈弹窗已关闭")
+                return True
+        print("[step3] 反馈弹窗关闭未生效,后续批次可能空转")
+        return True
+    except Exception as e:
+        print(f"[step3] 反馈弹窗检测异常: {e}")
+        return False
+
+
+def _swipe_next_batch(ex: SafeExecutor, distance: int):
+    """列表上滑统一入口: 滑动后检查长按误触的反馈弹窗。
+    弹窗会骗过滑动的像素diff验证且挡住列表(后续批次全是旧卡片) → 关掉后补滑一次。"""
+    _adb_swipe_up(ex, distance)
+    if _close_feedback_popup(ex):
+        _adb_swipe_up(ex, distance)
+
 
 def _find_close_btn(shot_path: str) -> Optional[dict]:
     """
@@ -1877,6 +1943,7 @@ def step4_parse_qr(ex: SafeExecutor, product_title: str, shop_name: str = "",
             confirm_raw = OCR.recognize(shot_check, detail="all")
             ptype2 = AIParser().check_page(confirm_raw).get("type", "unknown")
             if ptype2 == "home":
+                _save_evidence(shot_check, "restart_home1")
                 print("    ⚠ 二次确认仍为首页(被踢回/退过头),触发重启恢复")
                 return "__RESTART__"
             if ptype2 == "normal":
@@ -1931,6 +1998,7 @@ def step4_parse_qr(ex: SafeExecutor, product_title: str, shop_name: str = "",
                 continue
             return "__TERMINATE__"
         if ptype2 == "home":
+            _save_evidence(shot_check, "restart_home2")
             print("    ⚠ 二次确认检测到首页,触发重启恢复")
             return "__RESTART__"
         if ptype2 == "list":
@@ -2062,6 +2130,7 @@ def step4_parse_qr(ex: SafeExecutor, product_title: str, shop_name: str = "",
         human_sleep(3)  # 点击后先等页面响应(验证码处理后/网络慢时切换慢)
 
         # 检测是否进入商品详情页(验证码检测优先——验证码页文字块少,不能被"加载中"分支挡掉)
+        notready_streak = 0   # 连续"未就绪"轮次(白屏时OCR只剩状态栏1~3块文字)
         for _ in range(8):
             human_sleep(2)
             detail_check = _pfx("detail_check.png")
@@ -2092,12 +2161,21 @@ def step4_parse_qr(ex: SafeExecutor, product_title: str, shop_name: str = "",
 
             # 2. 页面加载中/切换中(文字少且无验证码)→ 继续等待,不误判
             if len(detail_texts) <= 8:
+                notready_streak += 1
+                # 连续3轮仅状态栏级文字 → 大概率白屏(有图无字),立即跳过别盲等
+                # (原逻辑只会"继续等待",3次尝试×8轮能空转1分半)
+                if notready_streak >= 3 and _is_white_screen(detail_check):
+                    _save_evidence(detail_check, "white_skip_detail")
+                    _LAST_SKIP_WHITE = True
+                    print("    ⚠ 详情页白屏(连续多轮仅状态栏文字),跳过本商品")
+                    return "__SKIP__"
                 print(f"    页面未就绪(仅{len(detail_texts)}块文字),继续等待...")
                 continue
 
             # 2.5 检测是否退回首页/列表页(点标题失败/back过头时常见)→ 立即处理,不盲等
             joined_texts = "".join(detail_texts)
             if ("看病买药" in joined_texts) and ("我的" in joined_texts):
+                _save_evidence(detail_check, "restart_home_kw")
                 print("    ⚠ 检测到已退回首页,触发重启恢复")
                 return "__RESTART__"
             if "筛选" in joined_texts:
@@ -2136,6 +2214,7 @@ def step4_parse_qr(ex: SafeExecutor, product_title: str, shop_name: str = "",
                             continue
                         return "__TERMINATE__"
                 if ai_page.get("type") == "home":
+                    _save_evidence(last_shot, "restart_home_ai")
                     print("    ⚠ AI检测到首页,触发重启恢复")
                     return "__RESTART__"
                 if ai_page.get("type") == "list":
@@ -2539,7 +2618,7 @@ def _recover_to_list(ex: SafeExecutor, brand_keyword: str,
         remain = total_scroll_px
         while remain > 0:
             step = min(remain, int(h * 0.5))
-            _adb_swipe_up(ex, step)
+            _swipe_next_batch(ex, step)
             remain -= step
         human_sleep(2)
 
@@ -2640,7 +2719,7 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
     if start_page > 0:
         print(f"[step3] 跨设备接力恢复: 调度页码={start_page},滑动{start_page}页...")
         for i in range(start_page):
-            _adb_swipe_up(ex, int(h * 0.7))
+            _swipe_next_batch(ex, int(h * 0.7))
         human_sleep(2)
     elif progress:
         visited = set(progress.get("visited") or [])
@@ -2649,13 +2728,12 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
         remain = total_scroll_px
         while remain > 0:
             step = min(remain, int(h * 0.5))
-            _adb_swipe_up(ex, step)
+            _swipe_next_batch(ex, step)
             remain -= step
         human_sleep(2)
     else:
         visited = set()
 
-    white_fail_round = 0     # 白屏分级恢复轮次(冷却→冷却→重启;采到正常商品清零)
     white_restart_count = 0  # 白屏重启恢复累计(>=MAX_WHITE_RESTART → 置重派标志)
     ws_success_count = 0     # 连续成功采集计数(>=WHITE_SUCCESS_RESET_N → 白屏重启额度清零)
     unknown_skip_streak = 0  # 连续"进页失败跳过"计数(跨批次,采到正常商品清零;连续2→分级恢复)
@@ -2792,7 +2870,7 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
                 swipe_dist = max(target_y - int(h * 0.15), int(h * 0.15))
             else:
                 swipe_dist = int(h * 0.3)
-            _adb_swipe_up(ex, swipe_dist)
+            _swipe_next_batch(ex, swipe_dist)
             total_scroll_px += swipe_dist
             human_sleep(1.1)          # 滑动后停顿 0.8~1.4s(±30%抖动)
             batch_no += 1
@@ -2824,7 +2902,6 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
                 continue
             if result:
                 unknown_skip_streak = 0  # 采到一个正常商品就清零
-                white_fail_round = 0     # 白屏分级轮次也清零
                 ws_success_count += 1
                 if ws_success_count >= WHITE_SUCCESS_RESET_N:
                     if white_restart_count > 0:
@@ -2847,34 +2924,33 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
                         break
 
         if ws_recover_requested:
-            # 白屏分级恢复:连续2商品进页失败 → 冷却×2轮(不重启) → 重启App → 超限置重派标志
-            white_fail_round += 1
-            import random as _random
-            if white_fail_round <= 2:
-                _cd = _random.uniform(180, 300) if white_fail_round == 1 else _random.uniform(300, 480)
-                print(f"[step3] 连续{unknown_skip_streak}个商品进页失败,冷却{_cd:.0f}秒"
-                      f"(分级恢复第{white_fail_round}/2轮,不重启)...")
-                if not _chunked_sleep_with_report(_cd, "白屏冷却"):
-                    stopped = True
-                    break
-                unknown_skip_streak = 0
-                continue
+            # 白屏恢复:冷却等待无效 → 直接关App → 休息60~120s → 重启重搜滑回(超限回告调度重派)
             white_restart_count += 1
             if white_restart_count >= MAX_WHITE_RESTART:
                 WHITE_SCREEN_REASSIGN = True
-                WHITE_SCREEN_REASSIGN_REASON = f"商品页反复白屏,重启恢复{white_restart_count}次无效"
+                WHITE_SCREEN_REASSIGN_REASON = f"商品页反复白屏,关App重启恢复{white_restart_count}次无效"
                 print(f"[step3] 白屏重启恢复超限({MAX_WHITE_RESTART}次),置重派标志,停止采集(进度保留)")
                 stopped = True
                 break
-            print(f"[step3] 白屏反复出现,重启App恢复(第{white_restart_count}/{MAX_WHITE_RESTART}次)")
+            print(f"[step3] 检测到白屏,关App休息后重启(第{white_restart_count}/{MAX_WHITE_RESTART}次)")
             _save_progress(ex.device_id, keyword, visited, total_scroll_px, batch_no)
+            try:
+                ex.driver.app_stop(APP_PACKAGE)    # 先把App彻底关掉再休息
+            except Exception as _e:
+                print(f"    关App失败(不影响流程): {_e}")
+            import random as _random
+            _rest = _random.uniform(60, 120)
+            print(f"    休息{_rest:.0f}秒...")
+            if not _chunked_sleep_with_report(_rest, "白屏重启"):
+                stopped = True
+                break
             step1_open_app(ex)
             step2_search(ex, (brand + keyword).strip() or keyword)
             # 从列表顶部滑动恢复到上次位置
             remain = total_scroll_px
             while remain > 0:
                 step = min(remain, int(h * 0.5))
-                _adb_swipe_up(ex, step)
+                _swipe_next_batch(ex, step)
                 remain -= step
             human_sleep(2)
             unknown_skip_streak = 0
@@ -2890,7 +2966,7 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
             remain = total_scroll_px
             while remain > 0:
                 step = min(remain, int(h * 0.5))
-                _adb_swipe_up(ex, step)
+                _swipe_next_batch(ex, step)
                 remain -= step
             human_sleep(2)
             continue
@@ -2905,7 +2981,7 @@ def step3_swipe_and_enter(ex: SafeExecutor, keyword: str, brand: str = "", task:
             swipe_dist = max(target_y - int(h * 0.15), int(h * 0.15))
         else:
             swipe_dist = int(h * 0.3)
-        _adb_swipe_up(ex, swipe_dist)
+        _swipe_next_batch(ex, swipe_dist)
         total_scroll_px += swipe_dist
         human_sleep(2)
         batch_no += 1

+ 5 - 3
tbsg/requirements.txt

@@ -1,14 +1,16 @@
 
 rapidocr-onnxruntime==1.4.4
-paddleocr==2.7.3
-paddlepaddle==2.6.2
+paddleocr>=3.7.0
+paddlepaddle>=3.3.1
 
-opencv-contrib-python==4.6.0.66
+opencv-contrib-python==4.10.0.84
 numpy==1.26.4
 Pillow>=10.0.0
 
 baidu-aip==4.16.13
+chardet>=5.2.0
 uiautomator2>=3.2.9
+uiautodev>=0.14.0
 
 PyMySQL>=1.1.1
 oss2==2.19.1

+ 1 - 0
tbsg/snapshot.py

@@ -101,6 +101,7 @@ def collect_snapshot(driver, title: str = "", device_id: str = "", scroll_times:
     """
     try:
         import cv2
+        SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)   # 中间帧截图落盘前先建目录
         w, h = driver.window_size()
         # 1. 滚动截图:首屏 + 1次上滑(共2屏,慢速、间隔充足)
         #    每屏落盘并用cv2验证PNG完整性(adb流式传输可能损坏,损坏则重试)

+ 1 - 1
tbsg/yzm/tmp_captcha_test6.py

@@ -61,7 +61,7 @@ SLIDER_POSITION_TOLERANCE = 12
 
 # 真人轨迹模板目录。模板只使用归一化后的形状,不直接复用原始触摸坐标,
 # 因此不会把旧设备的绝对坐标带到当前屏幕。
-GUIJI_ROOT = r"D:\drug\yzm\sg\yzm1\image\guiji"
+GUIJI_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "guiji")
 _GUIJI_TEMPLATES = None
 
 

Some files were not shown because too many files changed in this diff