验证码自动化的两条通道:ADB 直读与短信转发 App,6 个真实坑

2026-10-28 · AI智工坊 · 一手实测,数据来自真机运行记录与线上接口返回

我们每天要跑十几次「注册 / 登录 / 取验证码」的自动化:平台保活、账号开通、巡检。这条链路上我先后踩塌过两次,第一次以为是网络问题,第二次以为是 App 的 bug,最后发现根因是同一个认知错误:短信通道不是一条,是两条,各有各的死法,而我把两者的角色搞反了。

先把结论摆出来:ADB 直读适合「人在机器旁调试」,转发 App 适合「长期无人值守」。反过来用,就是你要熬一整晚的那种故障。

一、两条通道的真实分工

维度ADB 直读(读手机 inbox)转发 App(短信转发器 → 邮箱)
依赖什么手机 USB 连在这一台机器上,且调试授权有效手机联网即可,与本机物理无关
延迟秒级(轮询 inbox)通常 2~10 秒
失效形态手机一拔线就归零,静默App 被系统杀掉就不到,同样静默
能不能远程扩容不能,必须有人在机器旁插线能,加收件人即可,一台机器坏了另一台还能读
适合现场调试、批量取码、临时救急7×24 无人值守
最容易错的地方以为「一直连着」以为「一直活着」

我们真实的部署是:手机常年插在主机上充电,所以我一开始把 ADB 当主通道,还写过「关了转发器本机也不受影响」的结论。后来被现实纠正了——那台手机是日常在用的手机,不是专用测试机。只要它被拿走一次,主通道立刻变成 0,而且不报任何错,只是永远取不到码。这就是第一坑。

二、第一坑:把「临时连着」当成「永久在线」

判断通道是否可用,不能看「上一次成功过」,只能看这一次设备列表里有没有唯一的那一行:

adb devices
# 正常:  <serial>    device
# 异常:  (空)        → 线 / USB 口 / 驱动
# 异常:  <serial>    unauthorized  → 手机弹了授权框没人点

unauthorized 特别容易骗人:设备是在的,命令也不报错,但每次读 inbox 都返回空。换 USB 口、换线、手机重启之后都会重弹授权框,你以为勾过「一律允许」就永远有效,其实不是。

正确的做法是:取码脚本的第一件事不是读短信,是自检设备;设备不在就立刻降级到邮箱通道,并把「本次已降级」写进日志。沉默降级比报错危险得多——报错你会去修,沉默降级你会以为「今天没人发验证码」。

三、第二坑:转发 App 被系统杀掉,也不告诉你

转发 App(短信转发器,包名 cn.ppps.forwarder,不是网上很多教程里写的那个包名)靠后台常驻工作。在 Android 13 + 厂商 ROM 上,它会被四层机制分别杀掉,而普通 ADB 只能碰到其中两层:

层机制普通 adb 能不能改
Doze 低电耗deviceidle 白名单能:dumpsys deviceidle whitelist +<pkg>
待机分桶standby bucket 被降级到 rare能:am set-standby-bucket <pkg> active
后台运行限制appops RUN_IN_BACKGROUND不能:Android 13 普通 adb 报 MANAGE_APP_OPS_MODES 权限拒绝,要 root
厂商层自启动管理 / 耗电管理 / 最近任务清理不能,必须人在手机上点

所以「用 ADB 把转发 App 加固到杀不掉」这件事,做不到 100%。能做的只是降低被杀概率,最后三层必须人工点:最近任务里长按卡片锁定(最有效,防误划)、电池→应用耗电管理→不限制、手机管家→自启动管理→开启。

判断它是否还活着,别猜,看 pid:

adb shell pidof cn.ppps.forwarder      # 有输出 = 活着
adb shell monkey -p cn.ppps.forwarder -c android.intent.category.LAUNCHER 1   # 拉起来
adb shell dumpsys deviceidle whitelist +cn.ppps.forwarder                    # 补白名单

顺便纠正一个常见误解:App 内的「通用设置」里没有「前台服务 / 自启动」这类开关,只有蓝牙、GPS、短信指令、听筒关屏等增强功能。找不到不是版本问题,是根本没有,别再翻了。

四、第三坑:收件人只能填一个

这条纯粹是踩坑记录,但很费时间:转发 App 的「收件人邮箱」字段填逗号分隔的多个地址,保存时直接报「地址不合法」。它并非不支持多收件人,而是不支持在一行里写多个。正确做法是在字段上方点「添加收件人」,加一行,再填第二个地址。

为什么值得折腾?因为双收件人是这套方案里最便宜的一层冗余:一条短信、两处可读,主控机和备用机各读一个邮箱,任何一台机器挂了都能取到码。比加机器、加监控便宜得多。

五、第四坑:content query 的 limit 位置

要直接读 inbox,命令长这样:

adb shell "content query --uri content://sms/inbox --projection address,body,date --sort 'date desc limit 5'"

关键点:limit 不是 content query 的参数,它是 --sort 字符串的一部分,必须写在引号「里面」。写成 --sort 'date desc' limit 5 会报错,或者干脆忽略它、把整张表吐出来——几万条短信一次灌进管道,脚本就卡死了,看起来像「adb 挂了」。

另外,返回空不等于没短信:先确认收件箱真的有,再确认 shell 读短信的权限没被 ROM 收回。顺序反了,你会把时间花在错的层上排查。

六、第五坑:验证码正则,兜底规则比主规则更容易取错码

取码规则通常写成两层:优先匹配「验证码 / 校验码 / 动态码 / code」后面的 4~8 位数字;匹配不到,兜底取「独立的 6 位数字」。

第二层在真实短信里非常危险。同一条短信里往往同时出现订单号、金额、日期、卡号尾数、链接里的参数。「您尾号 6765 的信用卡欠款 221118.94,请于 11 月 01 日前还款」——独立的六位数字候选不止一个,兜底会稳稳地取到一个「长得像验证码」的东西。然后你在登录页面前反复重试,以为是平台的问题,其实你从来没取到真正的码。

我们的处理办法是把兜底收窄:只在「正文含验证码类关键词,但主规则没抓到」时才启用兜底;纯通知类短信直接判无码。取不到就明确失败,不要返回一个猜出来的数字——猜出来的码会把「通道故障」伪装成「验证码错误」,这是最难查的一类假象。

七、第六坑:UI 自动化的坐标缓存与 BACK 键

有时不得不让脚本去点 App(改配置、加收件人)。用 uiautomator dump + input tap 时,下面四种现象都真实发生过:

现象根因做法
点了没反应,字打到别处键盘弹出/收起使布局整体上移,脚本还在用上一步的坐标每一步操作后重新 dump 取坐标,绝不缓存
input text 被吞目标输入框没有获得焦点先确认 focused=true;tap 后等 1~2 秒再 dump
输入框里混进怪字符滚动 swipe 的终点落在输入框上,被识别成滑行输入滚动路径避开所有输入框区域
填到一半整页退出、内容全丢input keyevent 4(BACK) 在键盘已关闭时等于「退出页面」收键盘改成点非输入区,不要用 BACK

这四条的共同点:都是「脚本写得对,环境在动」。UI 自动化里最贵的不是写动作,是每次动作后重新确认状态。

八、一个能用的取码契约

自动化里真正贵的不是「取到码」,而是「取不到时上层知道该干什么」。所以我们把接口定成三条硬约定:

结果输出退出码
成功stdout 只打印纯数字码(可直接管道给上层),来源信息走 stderr0
无码stderr 打 NO_CODE_FOUND,stdout 什么都不给1
设备 / 通道异常stderr 说明是哪种异常(设备不在 / 已降级到邮箱 / 权限不足)非 0

配合三个开关:--code 取 15 分钟内最新一条;--code --wait 120 点完「发送验证码」后轮询最多 120 秒;--code --from <平台名> 按发件人/正文关键词过滤。

为什么一定要 --from?双卡双待的手机在同一分钟里完全可能收到另一张卡的银行验证码。不过滤就有概率取到「别人的码」,然后你会在平台侧排查「为什么验证码错误」——而真正的原因是它根本不是这个平台的码。

九、上线验收清单(照着打勾就行)

  1. adb devices 有且仅有一行 device;出现 unauthorized 视为不可用。
  2. 取码脚本先自检设备,再读短信;不可用时明确降级并写日志,不静默。
  3. 转发 App 的 pid 存活(pidof 有输出),Doze 白名单与 standby bucket 已设。
  4. 收件人是「加行」加的,不是逗号拼的;点一次「测试」按钮,两个邮箱都收到。
  5. 用一条真实的通知类短信(银行账单)验证:必须判为无码,不能返回任何数字。
  6. 用一条真实验证码短信验证:返回的码与短信里的码逐位一致,且 --from 能挡住另一张卡的码。
  7. 断网、拔线、杀 App 三种故障各演一次,确认上层收到的是「失败」而不是「空」。

十、说句实话:这套东西的边界在哪

它解决的是「机器怎么可靠地拿到一条短信」,不解决人机验证。滑块、点选、行为风控这类东西,ADB 和转发 App 都越不过去;能过的只有人工。所以整条链路的正确设计是:机器负责 95% 的常规取码,剩下 5% 明确抛给人,而不是把失败重试到天亮。

另一个容易忽略的成本:一台长期插着 USB 的日常手机,本身就是不稳定因素——被拿走、被小孩玩、厂商更新后权限策略变化。把它当「基础设施」之前,先想清楚谁负责让它一直在线。

如果你也在做「无人值守 + 自动取码」这一类自动化,或者手上有一堆机器需要被统一管起来(巡检、自启收敛、故障自愈),可以直接看我们的服务口径:

上线前可以先拿这份免费清单自检一遍,9 项验收 + 3 条边界,不用留联系方式:下载《自动化与设备托管:上线前 9 项验收清单》→

看企业服务与报价 →

相关实录:多机 SSH 批量运维的 9 类故障分诊 · 无人值守 kiosk 的看门狗与自愈

常见问题

Q1:ADB 直读需要 root 吗?

读 inbox 不需要。Android 13 上 content query --uri content://sms/inbox 通过 adb shell 的 shell 权限通常可直接读。需要 root 的是改 appops 这类跨应用权限设置。

Q2:为什么不用转发 App 的「短信指令」远程控制?

它依赖手机能收到那条指令短信,一旦短信通道本身不可靠就形成循环依赖。我们只用它做最后手段的通知,不作为控制面。

Q3:转发 App 被杀后有没有办法自动发现?

有,而且必须做:定时 pidof 比「等一封测试短信」便宜得多。判据要写成「连续 N 次 pid 为空 → 告警并尝试拉起」,单次为空可能只是重启瞬间。

Q4:能不能两台机器同时读同一部手机?

ADB 同一时刻只服务一个 adb server 连接,另一台机器要读就得走网络 adb(需配对)或干脆让转发 App 发两个邮箱。后者的维护成本更低。

Q5:这套方案适合放到生产环境给客户用吗?

看客户能接受多少人工。如果「取不到码就转人工」是允许的,它够用且便宜;如果要求 100% 全自动、零人工,任何基于「一部手机」的方案都不该承诺,因为手机是世界上最不听话的服务器。