chore: 引入 mobile-mcp 项目级 MCP 并沉淀 ADB×MCP 工具选型纪要

mobile-mcp 固定 1.0.7 版本;verification.md 新增 2026-10-02 工具选型纪要
(ADB/ui_probe.sh 与 Mobile MCP 的分工规则、实测踩坑、本机环境更新)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-10-02 12:09:11 +08:00
parent dda0ff873a
commit 871fa4c5ce
2 changed files with 270 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
# 端到端验证手册
目标:在**真实接口**下把手机端走通,并确认平板端没退化。只看编译通过是不够的——
本次适配里"筛选条件被静默清空"和"后端不认新参数"两个问题,都只有跑真机才暴露。
## 环境
```bash
/opt/homebrew/share/android-commandlinetools/emulator/emulator -list-avds # 本机现仅 AeroLogic_Phone(手机)
adb devices -l
adb -s <serial> shell wm size && adb -s <serial> shell wm density
```
判定形态:手机 720×1440@292dpi → `PHONE(sw≈394dp)`;平板 1280×800@160dpi → `TABLET(sw=800dp)`(本机平板 AVD 待建,见文末工具选型纪要)。
`ui_probe.sh` 封装了常用操作,比手写 adb 省事且更可靠:
```bash
P=.claude/skills/phone-adapt/scripts/ui_probe.sh
$P <serial> form # 打印形态识别结果
$P <serial> texts # 当前界面所有可见文字(做断言用)
$P <serial> bounds "已交接" # 元素 bounds + 中心点
$P <serial> tap-text "确认" # 按文字点击(自动取中心点)
$P <serial> wait-text "待交接" # 等页面就绪
$P <serial> wait-gone "请稍候……" # 等加载弹窗消失
$P <serial> shot /tmp/a.png # 截图
$P <serial> req pageQuery # 抓请求/响应体
$P <serial> crash # 崩溃次数 + 栈
```
## 构建安装
```bash
./gradlew assembleDebug 2>&1 | grep -E "^(BUILD|FAILURE)|error:"
adb -s <serial> install -r -d app/build/outputs/apk/debug/app-debug.apk
```
注意 gradle 输出里大量 `e: 注: ARouter::Compiler …` 是注解处理器的**提示**被误标成 `e:`,
不是错误;只看 `BUILD SUCCESSFUL` / `error:` 就行。
## 进入目标页面的两条路
### 正规路径(优先):登录 → 首页菜单
```bash
adb -s <serial> shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1
```
测试环境账号在记忆文件 `aerologic-test-account.md`(本机,admin / 用户已授权保存;旧机器文件名为 `test-env-login.md`),可自主登录。
填表用 `input text`,**`#` 不需要转义**——新版 adb 的 input text 参数不经远端 shell 解析,
写 `\#` 反而会把 `\` 也输进去(实测多出一位)。
⚠️ **「记住密码」回填哈希坑(实测踩过)**:输入用户名后 App 会调 `queryUserByName`
把本地存储的 **bcrypt 哈希**回填进密码框,盖掉/混入你输入的明文 → 登录 200 但校验失败、
静默停在登录页(密码框点数对不上是判断信号:明文 9 位 vs 回填后 10 位显示)。
正确顺序:点密码框 → 等回填(sleep 1)→ `input keycombination 113 29`(CTRL+A)+
`KEYCODE_DEL` 清空 → 再输明文 → `KEYCODE_BACK` 收键盘 → 点登录。
键盘不收起时「登录」按钮可能点不到(tap 被吞),收起后再点。
底部有「首页 / 国内 / 国际 / 我的」Tab,国际业务页找对应入口
(底部 Tab 的「国际」与顶部筛选的「国际」同名,`tap-text "国际" 2` 指定第 2 处)。
**首页菜单是按权限过滤的**:当前账号角色没有该权限时,入口根本不显示(本次 ADMIN 登录后
国际业务只有「板箱过磅」「进港理货」,没有「出库交接」)。这时走下面的直启。
### 直启(权限缺失时的替代):冷启动 + am start
`app/src/debug/AndroidManifest.xml` 里把调试页临时 `exported="true"`,就能直接拉起。
**必须冷启动**(先 force-stop),否则会撞上既有的 LoadingModel 崩溃:
```bash
adb -s <serial> shell am force-stop com.lukouguoji.aerologic
sleep 2
adb -s <serial> shell am start -n com.lukouguoji.aerologic/<完整类名>
```
登录态存在 SharedPreference 里,冷启动直启同样带 token,接口能正常返回真实数据。
> 先经首页再 `am start` 会崩在 `LoadingModel.showLoading` → XPopup `popupInfo is null`,
> 这是既有缺陷(未改动的页面也复现),不是本次改动引入的。
## 点击前先等就绪
加载弹窗("请稍候……")会盖住整屏并**吞掉 `input tap`**,表现为"点了没反应"。
本次验证就因此白跑了几轮。所以:
```bash
$P <serial> wait-text "待交接" # 等目标元素出现
$P <serial> tap-text "已交接"
$P <serial> texts | grep -c "全选" # 用文字变化确认点击确实生效
```
点完**一定要断言效果**(用 `texts` 或截图),别假设点中了。
## 逐项走查清单
每一步做完都跑一次 `crash`(必须为 0):
- [ ] 列表加载:卡片字段、各种状态形态(如待处理 / 已完成两种卡片)
- [ ] Tab 切换:指示条位置、角标数字、底部条按设计显隐
- [ ] 筛选弹层:打开、各控件取值、重置、确认后重新查询
- [ ] 下拉字典:真实取到数据(不是空列表)
- [ ] 多选:单选、全选联动、"已选 N 项"计数
- [ ] 详情页/二级页:字段绑定、条件显示的区块
- [ ] 扫码入口能拉起
## 核对请求参数是否真的生效
新增的查询字段要确认两件事:客户端**发出去了**,服务端**认**。
```bash
$P <serial> req pageQuery
```
OkHttp 日志是多行 pretty JSON。看到请求体里有你的字段(如 `"handoverState": "0"`)只说明客户端对了。
**服务端是否生效要用行为验证**:切到另一个 Tab,看返回数据是否真的变了。
⚠️ 行为验证失败时**先怀疑参数名,再怀疑后端**:出库交接第一版传 `hoState`,两个 Tab 返回同一批数据,
一度误判为「后端没做过滤」;实际是参数名错了(api-doc 里是 `handoverState`),后端对不认识的参数
**静默忽略**。正确顺序:查 `references/api-doc.md` 核对参数名 → 用 token 直调后端做 A/B 对比
(同条件只改一个参数看 total 变化)→ 仍不生效才定性为后端缺陷并记入
`documents/后端接口对接问题清单.xlsx`。
## 无数据时怎么核对 UI
服务端当天可能没数据(列表空)。放宽条件(清空日期)再查;仍为空时,临时注入样例 Bean:
```kotlin
// 临时:覆盖各种形态各一条,验证后删除
binding.rv.postDelayed({
binding.rv.commonAdapter()?.refresh(listOf(/* 待处理 / 危险品 / 已完成 各一条 */))
}, 2500)
```
截图核对完**必须删除**,然后重新构建 + grep 确认无残留:
```bash
grep -rn "injectMockForVisualCheck\|TODO 临时" module_base/src module_gjc/src | wc -l # 应为 0
```
## 屏幕方向验证
新增/修改 Activity 后,确认手机端不会「先横屏再转竖屏」:
```bash
# 冷启动时连续采样:应全程 mRotation=0
adb -s <phone> shell am force-stop com.lukouguoji.aerologic; sleep 2
adb -s <phone> shell monkey -p com.lukouguoji.aerologic -c android.intent.category.LAUNCHER 1
for i in $(seq 1 10); do adb -s <phone> shell dumpsys window | grep -m1 -oE "mRotation=[0-9]+"; sleep 0.4; done
```
平板则应首屏就是横屏(截图宽 > 高)。若手机出现横屏帧,检查该 Activity 的 Manifest
是否写成了别的值——**必须是 `android:screenOrientation="unspecified"`**(写死 `userLandscape`
或用 `@integer/...` 资源引用都会出问题,详见 SKILL.md 阶段 4)。
## Pad 端回归(必做)
```bash
adb -s <tablet> install -r -d app/build/outputs/apk/debug/app-debug.apk
adb -s <tablet> shell am start -n com.lukouguoji.aerologic/<同一个类名>
$P <tablet> shot /tmp/tablet.png
$P <tablet> form # 应为 TABLET(sw=800dp…)
```
对比改造前后:搜索条数量与顺序、日期默认值、按钮(平板可能比手机多按钮)、底部统计文案。
改过 `module_base` 里的公共基类或 Pad 系组件时,这一步不能省,且要多看几个页面。
## 定位"值被神秘修改"类问题
在 setter 里打带堆栈的日志,一次就能看到调用来源:
```kotlin
android.util.Log.d("DBG", "value '$field' -> '$v'", Throwable("trace"))
```
```bash
adb -s <serial> logcat -d -s DBG | grep -A14 "'旧值' -> ''" | grep "at com.lukouguoji\|at android"
```
本次靠这招定位到 `TextView.onRestoreInstanceState → EditText.setText → doOnTextChanged`,
即复合自定义 View 的 id 串档(见 `components.md`)。**排完删日志。**
---
## 工具选型纪要:ADB(ui_probe.sh) × Mobile MCP 分工(2026-10-02)
背景:`mobile-mcp`(`@mobilenext/mobile-mcp`)已装入项目级 `.mcp.json`,与本手册的
ADB/ui_probe.sh 流程并存。以下基于 2026-10-02 在 `AeroLogic_Phone` 模拟器上的实测对比
(登录流程实测到“填表”一步,因内网不通放弃后续),给出后期开发的工具选择规则。
### 能力对照(只列有差异的维度)
| 维度 | ADB + ui_probe.sh | Mobile MCP |
|------|-------------------|------------|
| 元素定位 | `tap-text` 按文字点,**每次点击前重新 dump**,天然抗布局变化;同名多处可指定序号 | 按 ref 点,键盘弹出/页面导航后 **ref 失效**,必须重新 list(实测踩过) |
| 元素读取 | `texts` 只出可见文字,token 便宜,配 grep 断言(`texts \| grep -c "全选"`) | 完整控件树(text/id/类型/focused/disabled/checked/坐标),信息全但 **token 重**(登录页 54 行) |
| 文本输入 | `input text` + `keycombination`(CTRL+A/DEL 清空——登录 bcrypt 回填坑的标准解法) | `type_keys` 只能输入,**无清空原语**(实测需回落 adb) |
| 请求核对 | `req <关键字>`(OkHttp 多行 pretty JSON 按关键字截取,为本项目定制) | `get_device_logs` 可过滤 tag/进程,多行报文截取不如 `req` 顺手 |
| 崩溃检查 | `crash` | `list_crashes` / `get_crash`(等价) |
| 前台断言 | `dumpsys activity`(手写 grep) | `get_foreground_app` 一次调用 |
| 录屏 | `adb shell screenrecord`(手拉文件) | `start/stop_screen_recording` 直接回传路径,更省事 |
| 批量操作 | 脚本串联,每条一次往返 | `mobile_batch_commands` 一次往返多步(**页面稳定时**才高效) |
| iOS / 云真机 | 无 | 有(本项目 Android-only,暂无用武之地) |
| 依赖 | 零依赖(本机 adb 即可) | npx 每会话冷启动、Node 20+、`@latest` 有版本漂移风险(建议固定 `@1.0.7`) |
### 选型规则(后期开发按此执行)
**默认走 ADB + ui_probe.sh**。本手册全部流程不变:构建安装、冷启动/直启、登录、
wait-text 就绪等待、tap-text 交互、texts 断言、req 抓包、crash 归零、方向采样、Pad 回归、
样例 Bean 注入。理由:为本项目的坑定制(bcrypt 回填、Loading 吞点击、参数名 A/B)、
token 成本低、零外部依赖,且 tap-text 每次重新 dump,比 ref 更抗布局变化。
**以下四种场景切 Mobile MCP**:
1. **控件级语义断言**——要看 disabled/checked/focused 状态、控件 id、输入框变换后的值
(如“密码框 9 个点”“角色 Spinner 是否禁用”“用户名框自动大写成 ADMIN”)。元素树比
截图肉眼判读可靠,比 `texts` 信息全。实测:登录页一次拿到 `inputPass text="•••••••••"`、
角色 Spinner `disabled`,这类断言 `texts` 做不到(密码框无明文文字)
2. **页面稳定时的多步连续输入**——list 一次拿 ref,`batch_commands` 一次往返完成
tap→type→tap→type;比逐条 adb 省 round-trip。前提:中间不弹键盘收不起/不导航
(弹了就见下方回落规则)
3. **录屏取证**——复现“先横屏闪 1 秒”类**时序 bug**,mp4 归档进问题清单,比连拍截图省力
4. **前台应用/安装卸载等状态核查**——`get_foreground_app`、`install/uninstall/launch_app`
一步到位,省得记 dumpsys/am 语法
**回落规则(两者随时可混用,操作同一设备无状态冲突)**:
- mobile-mcp ref 失效(键盘弹出/导航后点击落空)→ 重新 `list_elements`,或直接换
`ui_probe.sh tap-text`(按文字,永远新鲜)
- 需要清空输入框(登录 bcrypt 回填)→ 只能 adb:`input keycombination 113 29` + DEL
- 多行请求/响应体核对 → `req`,不用 mobile-mcp 的日志过滤硬凑
### 实测踩坑(Mobile MCP,2026-10-02)
- ⚠️ **键盘弹出会平移布局**:之前 list 出的 ref/坐标失效。batch 里连续 click+type 会把
两段文本打进同一个框(用户名框变成 `ADMINAA123456#`)。规避:每步输入完先
`KEYCODE_BACK` 收键盘再点下一个目标;或点完先确认焦点再 type
- 无“清空字段”原语;`type_keys` 输入即所得,不会帮你选中文本
- 元素树 token 开销大,断言优先 `texts | grep`,确实要看状态/id 才上 list_elements
### 本机环境更新(2026-10-02)
- SDK 根目录:`/opt/homebrew/share/android-commandlinetools`(brew 安装,emulator 包已装)
- 手机 AVD:`AeroLogic_Phone`——720×1440、5.51 寸 292dpi(sw≈394dp,落手机桶 360~411dp)、
Android 12 / API 31(与 targetSdk 一致)、arm64 纯 64 位(本项目无 abiFilters,唯一含
.so 的 fimage.aar 自带 arm64-v8a,兼容)、无边框(`showDeviceFrame=no`)
- ⚠️ **旧文档里的 `Aerologic_Phone_5.5_API31`(300dpi/sw=384dp)、`Medium_Phone_API_36.1`、
`Aerologic_Tablet` 均为旧机器 AVD,本机不存在**。平板回归前需先建平板 AVD
(1280×800@160dpi,`avdmanager create avd` 后照本文「Pad 端回归」执行)
- 启动:`/opt/homebrew/share/android-commandlinetools/emulator/emulator -avd AeroLogic_Phone`
(Quick Boot 快照已存在,二次启动数秒就绪)
- 模拟器 NAT 出网,**内网服务不可达**(本次登录实测“服务器不通”而放弃)——需要真实接口
联调时先确认宿主机网络可达内网;不可达时按上文「无数据时怎么核对 UI」注入样例 Bean
- `.mcp.json` 已加 `mobile-mcp` 条目(npx stdio),**建议把 `@latest` 固定为 `@1.0.7`**;
该文件 git 跟踪,改动需提交后队友才可用
### 推荐配合模式(单页验证的完整动线)
adb 骨架 + mobile-mcp 点睛,一条龙:
1. **adb**:构建安装 → 冷启动/直启(`am start`)→ 按登录节填表(含 bcrypt 清空顺序)
2. **ui_probe**:`wait-text` 就绪 → `tap-text` 交互 → `texts | grep` 断言 → `req` 抓包 → `crash` 归零
3. `texts` 断言不够(要看状态/id/变换值)或要连续多步输入 → **mobile-mcp**:
`list_elements` → `batch_commands`(tap/type)
4. 时序类 bug(闪屏/动画/异步错位)→ **mobile-mcp 录屏**取证,问题清单贴 mp4 + 截图
5. 全程崩溃检查两家等价(`crash` / `list_crashes`),任选