12306-specific knowledge, workflows, and pitfalls. The app (com.MobileTicket) uses Alipay Nebula UC WebView for the train list and booking pages, which has unique automation challenges. This doc reflects two successful end-to-end bookings on the current app version (2026-08-30, Realme RMX3350 1080x2400).
Requires the appium-android-adb base skill. It ships ocr_bridge.py — the OCR loop every
command below runs. ClawHub has no dependency resolver, so it will not be installed for you;
install it alongside:
bash
clawhub install appium-android-adb
bash
# venv (one-time, shared with the other *-android-adb skills). The pins ship
# with the base skill — that is the other reason to install it first.
BASE=~/.openclaw/workspace/skills/appium-android-adb # the base skill (ships ocr_bridge.py)
PY=~/.openclaw/workspace/.venv-adb/bin/python
test -f $BASE/ocr_bridge.py || echo "base skill missing — clawhub install appium-android-adb"
python3 -m venv ~/.openclaw/workspace/.venv-adb # no-op if it already exists
$PY -m pip install -r $BASE/requirements-adb.txt
# App must be installed + logged in. If missing, see "Install & Login".
$PY -c "import uiautomator2 as u2; d=u2.connect(); d.app_start('com.MobileTicket')"
Do NOT use the Appium bridge for 12306 on consumer phones — Realme etc. block WRITE_SECURE_SETTINGS, so Appium settings/IME fail and the session never starts. Direct uiautomator2 + OCR works (atx-agent needs no special permission). All interaction goes through the OCR loop: ocr_bridge.py in the appium-android-adb base skill — screenshot → RapidOCR → click/swipe → re-OCR verify.
The train list uses a virtual list inside the UC WebView:
adb shell input tap is filtered by the WebView. d.click(x, y) (uiautomator2) reliably delivers touches.Collapsed train cards have NO 预订 button. The flow is:
二等 ¥41 9折).bash
BASE=~/.openclaw/workspace/skills/appium-android-adb # the base skill (ships ocr_bridge.py)
PY=~/.openclaw/workspace/.venv-adb/bin/python
# after OCR shows target train card on screen:
$PY $BASE/ocr_bridge.py tap-text 二等 # expand the card
$PY $BASE/ocr_bridge.py tap-text 预订 --y0 <row_top> --y1 <row_bottom> # row's 预订
(Example coordinates from the real run: card row at (141, 2026) expanded; 二等 row 预订 at (939, 1987); order page followed.)
Without the filter, the list mixes departures from multiple stations and time order breaks. A train that stops at several departure stations (一车多站, e.g. G7357) appears once per stop-time, all grouped adjacently — so you see 08:10 / 08:35 / 08:52 for the same train number and can't scan by time. Hit this in production (thought "no 08:16 train exists" — there was one).
Fix: tap the departure-station filter chip at the top of the list first (chips row, y≈669-708 on 1080x2400; e.g. 上海松江 / 杭州南 / 上海虹桥 / 上海南):
python
chip = [x for x in t if '松江' in x[0] and x[2] < 600] # OCR: station name near top
d.click((chip[0][1]+chip[0][3])//2, (chip[0][2]+chip[0][4])//2)
After filtering: one row per train, sorted by departure time, target times are findable by simple scrolling.
⚠️ The filter resets after every re-query (查询车票) — re-apply the chip each time.
curl the direct link → adb install -r app.apkStations: tap the station field → search box opens. Chinese input via clipboard (IME switching is blocked):
bash
BASE=~/.openclaw/workspace/skills/appium-android-adb # the base skill (ships ocr_bridge.py)
PY=~/.openclaw/workspace/.venv-adb/bin/python
$PY $BASE/ocr_bridge.py type 苏州 --x 400 --y 330 # set_clipboard + long-press field
$PY $BASE/ocr_bridge.py dump # find 粘贴 in OCR
$PY $BASE/ocr_bridge.py tap-text 粘贴 # paste → results appear
$PY $BASE/ocr_bridge.py tap-text 苏州站 # pick the main station
Date: the date picker opens with day cells — OCR the month grid and tap the day cell (e.g. tomorrow's cell right of 今天).
Tap 查询车票. The list loads with earliest departures first.
bash
BASE=~/.openclaw/workspace/skills/appium-android-adb # the base skill (ships ocr_bridge.py)
PY=~/.openclaw/workspace/.venv-adb/bin/python
$PY $BASE/ocr_bridge.py deps # HH:MM markers at x<250 = which time window is visible
$PY $BASE/ocr_bridge.py swipe up --scale 0.35 # normal forward scroll (≈2 cards)
deps — you always know where you are, DOM or not.$PY $BASE/ocr_bridge.py jump --n 3 (3 rapid scale-0.5 swipes breaks the stuck point), then micro-swipes back: swipe up --scale 0.25, micro down, or d.swipe(540, 1900, 540, 800, duration=0.4).^[GDCK]\d+ plus the destination name in the row text (ignore trains to other destinations, e.g. 太仓 when heading to 苏州).tap-text <name> (e.g. 张三).bash
BASE=~/.openclaw/workspace/skills/appium-android-adb # the base skill (ships ocr_bridge.py)
PY=~/.openclaw/workspace/.venv-adb/bin/python
$PY $BASE/ocr_bridge.py tap-text 提交订单
$PY $BASE/ocr_bridge.py wait-text 订单处理中 # order processing
$PY $BASE/ocr_bridge.py markers # look for 快速支付 / 立即支付
订单处理中 → wait and re-OCR (can take ~20-60s).快速支付 or 立即支付 + countdown at top = order locked, success. Report: train, date, route, seat, price, passenger, seat number, countdown.If the list data sat too long before submitting, the order page throws "车票信息已过期(M0013)". Detect it by polling markers after 提交订单:
python
for i in range(8):
time.sleep(4)
markers = [x[0] for x in shot() if any(k in x[0] for k in
('立即支付','快速支付','订单处理','温馨提示','取消订单','已过期','确定'))]
if any('已过期' in m or '确定' in m for m in markers): break # ERROR
if any('立即支付' in m for m in markers): break # SUCCESS
Recovery (proven):
d.press('back') ×2 → home page. (Pull-to-refresh on the list did NOT refresh the data — don't bother.)SituationCommandNormal advanceswipe up --scale 0.35Fine-tune positionmicro up / micro down (scale 0.15)Precise pixel scrolld.swipe(540, 1900, 540, 800, duration=0.4)List stuck / skips target windowjump --n 3, then micro backBack up a littleswipe down --scale 0.3
text
OCR markers:
├─ 查询车票 → HOME — set stations/date → tap 查询车票
├─ 提交订单 / 选择乘车人 → ORDER PAGE — passenger → 提交订单
├─ 订单处理中 → wait, re-OCR
├─ 车票信息已过期 / 确定对话框 → EXPIRED — 确定 → back → 查询车票 → re-filter → re-book
├─ 快速支付 / 立即支付 → SUCCESS — tell user to pay
├─ deps() times + train numbers → TRAIN LIST — scroll/find → expand → 预订
├─ 粘贴 menu visible → after clipboard type, tap 粘贴
└─ unknown → dump full OCR and re-orient
~/.cache/ocr-bridge) — see appium-android-adb for cleanup.d.click.set_clipboard + long-press 粘贴.jump past the stuck point, then micro-scroll.adb shell svc power stayon true.G3314 (no spaces); DOM gives G 3 3 1 4. Regex on OCR text.