
1. Android 安装密码自动输入与安装结果校验的完整链路做 Android 自动化测试的同学大概率都遇到过这个场景CI 流水线跑起来几十台设备同时装包结果卡在「请输入安装密码」的弹窗上或者装完了根本不知道到底成没成功。人工盯着屏幕点确定显然不现实尤其是批量设备场景一个人根本看不过来。这个问题的本质是两条链路要打通一条是安装密码的自动输入另一条是App 安装结果的可靠校验。前者依赖 UI 自动化框架去识别弹窗、填入密码、点击确认后者依赖 ADB 命令的返回值和设备端的状态轮询。两条链路如果各自为战就会出现「密码填了但没点确定」「点了确定但安装其实失败了」这类假成功。我试过把这两条链路拆开单独做结果就是 CI 报告里一堆误判。后来把它们串成一个闭环ADB 负责推包和安装uiautomator2 负责监听弹窗并输入密码安装结果通过pm install的 stdout 关键字加设备端pm list packages双重确认。这样跑下来批量设备的安装成功率判定才靠谱。适合谁看正在搭 Android CI 流水线、需要批量设备装包校验、或者被安装密码弹窗卡住的测试开发同学。下面我会给出可直接复制的 ADB 输入脚本、安装结果轮询判定逻辑以及通过统一 Key/API 通道接入自动化服务的配置示例。核心检索词就是 Android 自动化安装密码输入与 App 安装成功检测全文围绕这条链路展开。先说清楚整体架构。设备通过 USB 或网络连接到执行机执行机上的 Python 脚本做三件事解析参数拿到设备序列号和 APK 路径、启动一个线程执行pm install、主线程用 uiautomator2 连接设备并循环监听安装弹窗。安装线程结束后再校验安装结果。密码不是硬编码的而是根据设备 ID 从数据库或配置中心查询这样不同设备可以用不同密码。为什么要用pm install而不是adb install因为adb install的返回信息在部分定制 ROM 上不够稳定而pm install配合-r参数可以强制覆盖安装并且 stdout 里会明确输出Success或Failure方便程序判定。推包到/data/local/tmp再本地安装也比直接adb install更容易拿到设备端的真实反馈。2. TaoToken 统一 Key 接入自动化服务的前置准备在讲具体配置之前先解决一个容易被忽略的问题自动化脚本里经常需要调用大模型做日志分析、失败归因或者测试报告生成如果每个脚本都散落着不同的 API Key管理起来非常痛苦。TaoToken 的作用就是把这些调用统一到一个 Key 和一个 API 通道上脚本里只认一个 Base URL 和一个 Key换模型只改 Model ID。你需要先拿到自己的 Key。打开 https://taotoken.net/api-keys 创建注意这个 Key 只在创建时完整显示一次复制后存到环境变量里别写死在代码里。然后确认你的调用地址是 https://taotoken.net/api这个地址不带任何多余参数直接作为 OpenAI 兼容接口的 base_url 使用。模型 ID 怎么选如果你只是做日志摘要和失败归因选一个响应快的通用模型即可如果要做代码级的测试脚本生成选 coding 能力强的模型。具体可用模型列表在 https://taotoken.net/doc 里能查到也可以在 https://taotoken.net/models 直接对话测试效果。长期跑 CI 的话建议了解一下 Coding Plan按量或包月看你的调用频率地址是 https://taotoken.net/coding-plan。这里要强调一个配置原则Base URL、Key、Model ID 这三件套必须成组出现缺一个都跑不通。很多同学报 401 就是因为只填了 Key 没改 Base URL或者 Base URL 末尾多了斜杠导致路径拼接错误。下面第三节我会给出完整的可复制配置片段。环境准备方面执行机上需要装好 Python 3.8、ADB 工具、以及 uiautomator2。安装命令是pip install --pre uiautomator2和pip install -U weditorweditor 可以用来可视化查看设备上的控件方便你确认弹窗里「请输入」这个文本到底匹配哪个元素。数据库查询密码的部分用 pymysql 就够了。还有一点设备连接稳定性直接决定自动化能不能跑完。建议在脚本开头加一个设备在线检测用adb devices过滤出目标序列号如果设备掉线就直接退出并标记失败不要让它卡在连接阶段。这个检测逻辑我会放在第四节的成功结果验证里一起讲。3. 可复制的 ADB 输入脚本与统一 Key 配置片段先给统一 Key 的配置片段。你可以把它写成一个config.toml放在项目根目录脚本启动时读取。这样 CI 环境里只需要注入环境变量不用改代码。# config.toml [taotoken] base_url https://taotoken.net/api api_key ${TAOTOKEN_API_KEY} model_id your-model-id [device] adb_path adb install_timeout 120 poll_interval 2 remote_tmp /data/local/tmp [database] host 192.168.1.1 port 3306 user root db auto_build charset utf8如果你用的是 JSON 配置等价写法如下注意路径和字段名保持一致{ taotoken: { base_url: https://taotoken.net/api, api_key: 从环境变量注入, model_id: your-model-id }, device: { install_timeout: 120, poll_interval: 2, remote_tmp: /data/local/tmp } }接下来是 ADB 输入脚本的核心部分。先封装 ADB 命令头再实现推包和安装。注意pm install -r的-r表示覆盖安装批量场景下必须加否则已存在的包会直接失败。import subprocess import os import time import argparse import pymysql.cursors import pymysql import sys from threading import Thread def get_serial_and_apk(): parser argparse.ArgumentParser() parser.add_argument(--serial, typestr) parser.add_argument(--upload_filename, typestr) parser.add_argument(--replace_install, typeint) args parser.parse_args() serial args.serial upload_file args.upload_filename replace_install args.replace_install apk_path /Users/data/upload_apk/ upload_file return serial, apk_path, replace_install, upload_file def select_install_pwd(serial): connect pymysql.Connect( host192.168.1.1, port3306, userroot, passwd123456, dbauto_build, charsetutf8 ) cursor connect.cursor(cursorpymysql.cursors.DictCursor) try: sql fSELECT install_passwd FROM device_info WHERE id {serial} cursor.execute(sql) results cursor.fetchall() for row in results: return row[install_passwd] except Exception as e: print(e) connect.rollback() return None finally: cursor.close() connect.close() def cmd(adb_head, cmd_str, remove_shellFalse): head adb_head if remove_shell: head adb_head.replace(shell, ) full head cmd_str print(full) r os.popen(full) return r.read() def push_file(adb_head, file_path, remote_path): cmd(adb_head, push file_path remote_path, remove_shellTrue) def install(adb_head, apk_path, apk_name, remote_tmp): push_file(adb_head, apk_path, remote_tmp) remote_path remote_tmp / apk_name install_cmd adb_head pm install -r remote_path process subprocess.Popen( install_cmd, shellTrue, stdoutsubprocess.PIPE, stderrsubprocess.PIPE ) process.wait(120) for line in process.stdout: line_str line.decode(utf-8) print(line_str) if Success in line_str: print(安装包已成功安装) return True if Failure in line_str: print(line_str) print(请检查当前安装的 apk 版本是否过低) return False return False弹窗监听部分用 uiautomator2 的文本匹配。这里有个坑driver(text确定).exists如果不加括号永远返回真值必须写成.exists()。另外「继续安装」「无视风险安装」这些按钮在不同 ROM 上文案不一样建议用textContains做模糊匹配。import uiautomator2 as u2 def listen_alert(driver, serial): time.sleep(1) if driver(textContains请输入).exists(): pwd select_install_pwd(serial) if pwd: driver(textContains请输入).set_text(pwd) time.sleep(2) if driver(text确定).exists(): driver(text确定).click() if driver(text继续安装).exists(): driver(text继续安装).click() if driver(text无视风险安装).exists(): driver(text无视风险安装).click() if driver(text安装).exists(): driver(text安装).click()主流程把安装线程和弹窗监听串起来安装线程存活期间持续监听结束后做结果校验。if __name__ __main__: serial, apk_path, replace_install, upload_file get_serial_and_apk() adb_head adb -s serial shell if replace_install 3: p1 Thread(targetinstall, args(adb_head, apk_path, upload_file, /data/local/tmp)) p1.start() try: driver u2.connect_usb(serial) except RuntimeError: print(检测到设备断开连接) sys.exit(1) while p1.is_alive(): listen_alert(driver, serial) p1.join() try: driver.service(uiautomator).stop() except Exception as e: print(未能停止 uiautomator2 程序, e) else: print(使用原有的 apk 进行构建)4. 验证请求与安装成功结果判定脚本跑起来之后怎么确认真的成功了不能只看pm install打印了 Success 就完事因为有些 ROM 会先返回 Success 再弹密码框密码错了安装其实回滚了。所以要做双重校验第一层看安装命令的 stdout第二层用pm list packages确认包真的在设备上。先看第一层。安装线程里已经判断了Success和Failure但建议把结果写到一个共享变量里主线程 join 之后读取。改造一下 install 函数返回布尔值并存入队列或全局变量。install_result {success: False} def install_with_result(adb_head, apk_path, apk_name, remote_tmp): result install(adb_head, apk_path, apk_name, remote_tmp) install_result[success] result第二层校验用包名去查设备。注意pm list packages返回的是package:com.xxx.yyy格式需要做字符串包含判断。包名从 APK 文件名或 aapt 解析得到这里假设你已经有了包名变量。def verify_installed(adb_head, package_name, retries5, interval2): for i in range(retries): output cmd(adb_head, pm list packages package_name) if package_name in output: print(f校验通过{package_name} 已安装) return True time.sleep(interval) print(f校验失败{package_name} 未找到) return False把两层校验合起来主流程末尾加一段if install_result[success]: package_name com.example.app if verify_installed(adb_head, package_name): print(安装结果成功) else: print(安装结果命令成功但包未找到判定失败) sys.exit(1) else: print(安装结果失败) sys.exit(1)如果你在脚本里集成了 TaoToken 做失败日志归因调用方式如下。用 OpenAI 兼容的 SDKbase_url 指向 https://taotoken.net/apiKey 从环境变量读。from openai import OpenAI import os client OpenAI( base_urlhttps://taotoken.net/api, api_keyos.environ[TAOTOKEN_API_KEY] ) def analyze_failure(log_text): resp client.chat.completions.create( modelyour-model-id, messages[ {role: system, content: 你是 Android 安装失败分析助手输出简洁原因。}, {role: user, content: log_text} ] ) return resp.choices[0].message.content跑通之后你会看到类似输出Success、安装包已成功安装、校验通过com.example.app 已安装、安装结果成功。如果中间任何一步失败脚本以非零码退出CI 就能正确标记这次构建失败。想先验证模型通道是否通可以去 https://taotoken.net/models 直接对话测试确认 Key 和 Model ID 没问题再写进脚本。5. 本篇常见报错排查401 Unauthorized最常见的原因是 Key 没读到或者 Base URL 写错。检查环境变量TAOTOKEN_API_KEY是否真的注入到 CI 环境里Python 里用os.environ.get读一下打印长度确认非空。Base URL 必须是https://taotoken.net/api不要加/v1也不要加末尾斜杠否则路径会拼成/api/v1/chat/completions之外的错误地址。三件套 Base URL、Key、Model ID 缺一不可Model ID 写错也会报模型不存在。local proxy failed这个报错通常出现在网络层说明请求根本没发出去。先确认执行机能正常访问外网再检查是不是系统里配了什么全局代理导致请求被拦截。把代理环境变量清掉用curl https://taotoken.net/api测一下连通性。如果 CI 容器网络受限联系运维放行域名。reading choices 报错 / choices 为空说明请求发出去了但返回结构不对。常见原因是 Model ID 填了一个不存在的模型或者请求体里 messages 格式不对。检查resp.choices是否为空打印完整resp看返回内容。另外确认 SDK 版本老版本 OpenAI SDK 对 base_url 的处理有差异升级到较新版本。OAuth 相关报错如果你用的是 Claude Code 这类工具接入报 OAuth 错误一般是认证方式没配对。这类工具需要走 Anthropic 兼容通道配置时 Base URL 和 Key 的填法跟 OpenAI 兼容接口不同具体看 https://taotoken.net/doc 里的接入说明。别把 OpenAI 的 Key 填到 Anthropic 通道里反之亦然。uiautomator2 连接失败 RuntimeError设备掉线或者 adb 服务没起来。先adb devices确认设备在线再adb kill-server adb start-server重启服务。如果设备是网络连接确认 IP 和端口没变。脚本里已经做了 try 捕获掉线直接退出避免卡死。弹窗没被识别不同 ROM 的安装密码弹窗文案不一样有的叫「请输入密码」有的叫「输入安装密码」。用 weditor 连上设备手动触发一次安装看弹窗里控件的真实文本然后调整textContains的关键词。另外注意弹窗可能有延迟time.sleep(1)不够就加到 2 秒。pm install 返回 Failure 但没细节加上-r参数重试如果还失败用adb logcat抓安装阶段的日志搜索PackageManager关键字。常见原因是签名冲突、版本号低于已安装版本、或者存储空间不足。脚本里对 Failure 直接退出并提示检查版本就是为了避免在 CI 里反复重试浪费时间。6. 把统一 Key 通道接进你的自动化流水线到这里安装密码自动输入和安装结果校验的链路已经完整了。最后说下怎么把 TaoToken 统一 Key 通道真正接进你的 CI 流水线而不是只跑通一个 demo。第一步在 CI 的凭据管理里配置TAOTOKEN_API_KEY不要写在 Jenkinsfile 或 GitLab CI 的明文里。第二步把config.toml里的api_key写成环境变量占位符脚本启动时替换。第三步所有需要调用模型的环节——失败日志归因、测试报告摘要、脚本生成——都走同一个 base_url 和 Key换模型只改 Model ID 一个字段。如果你要长期跑批量设备建议把 Coding Plan 纳入考虑地址是 https://taotoken.net/coding-plan按调用量选择合适的档位比每次临时申请 Key 省事。接入文档在 https://taotoken.net/doc里面有各语言 SDK 的完整示例。需要管理多个项目的 Key 时控制台在 https://taotoken.net/console。实际落地时有个小技巧把安装结果和模型归因结果一起写进测试报告失败时自动附上归因文本这样排查的人不用再去翻 logcat。归因调用失败不影响主流程用 try 包起来超时就跳过保证安装校验本身不被模型调用拖慢。整套跑下来批量设备的安装成功率判定从「靠人看」变成「脚本判」CI 报告里的误判明显减少。你可以先把第三节的脚本复制到本地用一台设备跑通再扩展到多设备。遇到弹窗文案不匹配就用 weditor 调遇到 Key 报错就回头检查三件套。链路通了之后剩下的就是按你的业务加设备、加包、加校验规则。