12306-specific knowledge, workflows, and pitfalls. The app uses Alipay Nebula UC WebView for the train list and booking pages, which has unique automation challenges.
Ensure the app is running and on the correct page:
# Start Appium bridge (if using bridge_daemon.py approach):
bash ~/.openclaw/workspace/skills/appium-android-adb/start_bridge.sh
# Or use uiautomator2 directly (more reliable for WebView):
python3 -c "import uiautomator2 as u2; d = u2.connect(); d.app_start('com.MobileTicket')"
The train list uses a virtual list inside the UC WebView. This has extreme implications:
| Method | Target | Reliability |
|---|---|---|
| -------- | -------- | ------------- |
uiautomator2 text('预订').click() | "预订" book buttons | ✅ Always works (proper persistent bounds) |
uiautomator2 text('查询车票').click() | Native query button | ✅ Always works |
uiautomator2 text('选择乘车人').click() | Native passenger selector | ✅ Always works |
uiautomator2.click(x, y) on native views | Date tabs, bottom buttons | ✅ Always works |
| Screenshot + OCR + ADB tap | Any visible element | ✅ Works with good OCR |
Appium element.click() on "预订" buttons | "预订" with proper bounds | ✅ Works |
| Method | Target | Why |
|---|---|---|
| -------- | -------- | ----- |
| Clicking collapsed train name text | Train entry at y=407 or y=2255 (h=6) | Virtual list collapses ALL non-current items |
ADB input tap | Any UC WebView content | UC WebView filters synthetic touch events |
| W3C Actions swipe | UC WebView scroll | Touch events filtered |
mobile: scrollGesture | UC WebView element | Returns False consistently |
d.textContains('G 7 0 0 4').click() | Train name in collapsed zone | Multiple trains share collapsed bounds |
tap_bounds on collapsed element | Train name at y=2255 with h=6 | Event goes to WebView's virtual position, not visual |
The ONLY reliable click targets in the UC WebView train list are the "预订" action buttons. Unlike the text content (train name, times, seat info) which gets collapsed by the virtual list, the "预订" button is:
dump the page → check for "查询车票" button
If on MainActivity:
→ tap "查询车票" (native button, always works)
→ wait for train list to load (H5Activity appears)
If on H5Activity (already on train list):
→ proceed to Step 2
DO NOT try to click the train name or text content. Instead:
```python
d.swipe_ext('up', scale=0.3) # scroll forward
d.swipe_ext('down', scale=0.3) # scroll backward
```
```python
xml = d.dump_hierarchy()
if 'G 7 0 0 4' in xml:
# G7004 is in the list — look for its "预订" button
pass
```
```python
# Get all "预订" buttons with positions
buttons = d(text='预订')
for btn in buttons:
pos = btn.info['bounds']
y_center = (pos['top'] + pos['bottom']) // 2
print(f"预订 at y={y_center}")
# G7004 at y=1793 → the "预订" at y=1988 is its book button
```
```python
d(text='预订').click() # clicks first one
# OR click specific one by position:
for btn in d(text='预订'):
if 1900 < btn.info['bounds']['top'] < 2100:
btn.click()
break
```
If the WebView's DOM is too collapsed to find the right "预订":
import subprocess, pytesseract
from PIL import Image
# Take screenshot
subprocess.run(['adb', 'shell', 'screencap', '-p', '/sdcard/screen.png'], timeout=10)
subprocess.run(['adb', 'pull', '/sdcard/screen.png', '/tmp/screen.png'], timeout=10)
# OCR to find departure time
data = pytesseract.image_to_data(Image.open('/tmp/screen.png'),
lang='chi_sim+eng', output_type=pytesseract.Output.DICT, config='--psm 6')
# Find "08:00" departure text on LEFT side (x < 300)
for i in range(len(data['text'])):
if '08:00' in data['text'][i] and int(data['left'][i]) < 300:
y = data['top'][i]
# Tap "预订" position (~195px below departure time)
import subprocess
subprocess.run(['adb', 'shell', 'input', 'tap', '939', str(y + 195)])
break
After clicking "预订", a dialog may appear:
Use OCR to find the confirm button position, then ADB tap it.
On order page, verify:
→ G7004 train info displayed
→ 二等座 selected (default)
→ Price ¥41 shown
If no passenger selected:
→ tap "选择乘车人" (native, works)
→ tap passenger name (e.g. "熊子慧")
→ tap "确认" (bottom button)
tap "提交订单" button
→ verify: order submission confirmation
→ if "温馨提示" with "车票信息已过期" → go back, re-search
→ if "立即支付" → proceed to payment
"立即支付" or "去支付" appears → user handles Alipay/WeChat
The UC WebView's virtual list has three zones:
Each scroll (scale=0.3) advances the list by approximately 2 train cards (~30-60 min of departures). To reach a specific train:
(target_hour - current_hour) * 60 / 15 (each scroll ≈ 15 min of departures)dump / check current page:
├─ package != "com.MobileTicket"
│ → app not running, start it
├─ ".ui.activity.MainActivity" in activity
│ → HOME PAGE
│ → verify stations are set → tap "查询车票"
├─ "H5Activity" in activity
│ ├─ "提交订单" in text → ORDER CONFIRMATION
│ │ → select passenger if needed → submit
│ ├─ "预订" in text → TRAIN LIST (with "预订" buttons)
│ │ → find target train → click its "预订" button
│ ├─ "次列车" in text → TRAIN LIST (loading)
│ │ → scroll until target appears → find "预订"
│ └─ "立即支付" in text → PAYMENT
│ → user handles payment
└─ anything else → not 12306, investigate
adb shell svc power stayon trueG 7 0 0 4 not G7004.共 2 个版本