目前想封装一些wda操作为mcp,让llm直接操作手机,封装点击的时候碰到了坐标系统不一致的问题
factor 的实现逻辑
当前 touch_factor 的核心是:
python
screen = self.screen_size()
screenshot = self.screenshot_size()
factor = screen["height"] / screenshot["height"]
它解决的是:截图坐标系 和 WDA 点击坐标系 不一定一致的问题。
例如某些 iPhone / WDA 组合下:
text
截图尺寸 screenshot_size: 1170 x 2532
WDA 屏幕尺寸 screen_size: 390 x 844
这两个尺寸刚好是 3 倍关系:
text
factor = 844 / 2532 = 0.3333
如果模板匹配在截图上找到一个按钮中心:
text
screenshot 坐标: (585, 1266)
这个点在 WDA 坐标系里应该是:
text
wda 坐标: (585 * 0.3333, 1266 * 0.3333)
≈ (195, 422)
如果不乘 factor,直接把 (585, 1266) 传给 WDA,坐标就会超出 WDA 的逻辑屏幕范围,或者点击到错误位置。
所以 factor 本质是:
text
screenshot pixel coordinate -> WDA/device logical coordinate
也就是:
python
wda_x = int(screenshot_x * factor)
wda_y = int(screenshot_y * factor)
这里用 height 算 factor,是因为 Airtest iOS 也是按高度算,而且 iOS 截图和屏幕通常是等比例缩放,height 比 width 更稳定地反映竖屏/横屏后的实际比例。后续如果要更严谨,可以同时计算:
python
factor_x = screen["width"] / screenshot["width"]
factor_y = screen["height"] / screenshot["height"]
但为了和 Airtest 行为一致,目前使用单一 touch_factor = screen_height / screenshot_height 是合理的。
为什么 x/y < 1 可以按 percent 坐标直接传给 WDA
这是 Airtest iOS 的坐标约定。
Airtest 的 iOS touch 逻辑大致可以理解为:
python
if x < 1 and y < 1:
# 百分比坐标,直接交给 wda.click
pos = (x, y)
else:
# 截图像素坐标,需要乘 touch_factor
pos = (int(x * factor), int(y * factor))
原因是 iOS 的 facebook-wda / Airtest 封装里对小于 1 的坐标有特殊语义:
text
0 <= x < 1, 0 <= y < 1 表示屏幕比例坐标
例如:
python
touch((0.5, 0.5))
意思不是点击物理像素 (0.5, 0.5),而是点击屏幕中心:
text
x = 50% width
y = 50% height
所以这种坐标不需要乘 touch_factor。因为它不是截图像素坐标,而是比例坐标,本身和截图分辨率无关。
举例:
text
screen_size: 390 x 844
screenshot_size: 1170 x 2532
factor: 0.3333
如果传:
python
touch((0.5, 0.5))
语义是屏幕中心。WDA/Airtest 的 percent 处理会把它理解成:
text
WDA 实际点击点 ≈ (390 * 0.5, 844 * 0.5)
≈ (195, 422)
如果错误地给 percent 坐标也乘 factor:
python
0.5 * 0.3333 = 0.1666
那就变成点击大约 16.6% 的位置,明显错了。
所以判断逻辑是:
text
x/y < 1:
这是比例坐标,直接给 WDA/Airtest 封装处理。
x/y >= 1:
这是截图像素坐标,需要乘 factor 转成 WDA 坐标。
注意这里当前代码里是:
python
if not (raw_x < 1 and raw_y < 1):
# screenshot pixel
else:
# percent
也就是说只有当两个坐标都 < 1 时才认为是 percent;只要有一个坐标 >= 1,就整体按截图像素坐标处理。这和 Airtest 的语义保持一致。
方案 A 的具体改法
我建议后续实现时这样改:
- 在
WDAClient中新增统一坐标转换方法,例如:
python
def coordinate_scale_info(self) -> Dict[str, Any]:
screen = self.screen_size()
screenshot = self.screenshot_size()
screenshot_height = float(screenshot.get("height") or 0)
if screenshot_height <= 0:
raise WDAError(f"Invalid screenshot size for coordinate scale: {screenshot}")
return {
"factor": float(screen["height"]) / screenshot_height,
"screen_size": screen,
"screenshot_size": screenshot,
}
- 新增 Airtest 坐标归一化:
python
def _normalize_airtest_coordinates(self, x, y):
raw_x = float(x)
raw_y = float(y)
if raw_x < 1 and raw_y < 1:
return raw_x, raw_y, {
"coordinate_source": "percent",
"touch_factor": None,
"screen_size": None,
"screenshot_size": None,
}
scale = self.coordinate_scale_info()
factor = scale["factor"]
return int(raw_x * factor), int(raw_y * factor), {
"coordinate_source": "screenshot_pixel",
"touch_factor": factor,
"screen_size": scale["screen_size"],
"screenshot_size": scale["screenshot_size"],
}
-
让
airtest_touch()调用这个方法,避免重复 factor 逻辑。 -
修改
_normalize_coordinates()的auto语义:
text
auto:
x/y < 1 -> relative/percent
否则 -> screenshot_pixel + factor
也可以直接把 auto 作为 Airtest 坐标语义。
-
修改
swipe()显式坐标路径:- start 和 end 都走同一套 Airtest 坐标转换。
- 方向滑动,即只传
direction的情况,仍然基于window_size()生成 WDA 坐标即可,因为它本来不是截图坐标,是程序内部生成的逻辑屏幕坐标。
-
修改
ios_swipe()返回值:- 返回实际用于 WDA drag 的坐标
- 返回 factor / screen_size / screenshot_size
- 方便后续调试
例如:
json
{
"swiped": true,
"input": {
"start": [585, 1500],
"end": [585, 500],
"coordinate_mode": "auto"
},
"wda_start": [195, 500],
"wda_end": [195, 166],
"coordinate_source": "screenshot_pixel",
"touch_factor": 0.3333,
"screen_size": {"width": 390, "height": 844},
"screenshot_size": {"width": 1170, "height": 2532}
}
Template 后续应该怎么接
后续封装 Template 时,不建议让 Template 直接返回"点击坐标"这种模糊概念,而是明确返回:
json
{
"matched": true,
"confidence": 0.92,
"screenshot_position": {"x": 585, "y": 1266},
"wda_position": {"x": 195, "y": 422},
"touch_factor": 0.3333
}
这样:
- 模板匹配结果保留截图坐标,方便 debug 和可视化标注;
- 真正 touch/swipe 时使用统一转换后的 WDA 坐标;
- 所有视觉能力都和
ios_touch/ios_swipe保持一致。