From 2bacbf6a33d92dfa7568dad5d96f1ddbea8aadf2 Mon Sep 17 00:00:00 2001 From: ljq Date: Tue, 27 Oct 2026 15:22:26 +0800 Subject: [PATCH] Fix macOS key modifiers and clarify computer-use SOP --- memory/ljqCtrl_sop.md | 17 ++++++++--------- memory/macljqCtrl.py | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/memory/ljqCtrl_sop.md b/memory/ljqCtrl_sop.md index 698302c0..00eaa1b3 100644 --- a/memory/ljqCtrl_sop.md +++ b/memory/ljqCtrl_sop.md @@ -15,7 +15,7 @@ ## 1. 环境载入 import ljqCtrl -> **macOS**: 改 `import macljqCtrl as ljqCtrl`(API 镜像,Quartz/screencapture 实现)。依赖 `pyobjc-framework-Quartz`/`-Cocoa`,首次用前 `macljqCtrl.check_permissions()` 自检辅助功能/录屏授权。 +> **macOS**: 用 `import macljqCtrl as ljqCtrl`(Quartz 键鼠与进程内截图)。依赖 `pyobjc-framework-Quartz`/`-Cocoa`;首次用前 `ljqCtrl.check_permissions()` 检查辅助功能/录屏授权。 ## 2. 核心:High-DPI 物理坐标换算 `ljqCtrl` 的 `Click/MoveTo` 接口接收的是**物理像素坐标**。 @@ -33,22 +33,21 @@ ljqCtrl.Click(ox + (bbox[0]+bbox[2])//2, oy + (bbox[1]+bbox[3])//2) ``` 禁止全屏ImageGrab(必须针对窗口),所有逻辑坐标都要转物理。 -**macOS (`macljqCtrl`)**:窗口/区域截图使用进程内 `CGWindowListCreateImage`,无需临时文件。实测 Python 已获录屏授权时,`screencapture` 子进程仍可能失败,原生调用可用。授权对象可用 `NSBundle.mainBundle().bundlePath()` 定位。`GrabScreen(bbox)` 图内点用 `CropToScreen(bbox, px, py)` 转屏幕物理坐标;保留主屏 dpi_scale 约定,混合DPI多屏未验收。 +**macOS (`macljqCtrl`)**:截图用进程内 Quartz;Python 已授权不代表 `screencapture` 子进程可用。`NSBundle.mainBundle().bundlePath()` 定位当前执行应用。区域图内点用 `CropToScreen(bbox, px, py)` 转屏幕物理坐标;dpi_scale 基于主屏,混合DPI多屏未验收。 ## 4. 避坑指南 - **⚠️ 一律使用物理坐标**:传给 ljqCtrl.Click/SetCursorPos 的坐标必须是物理坐标(=截图像素坐标)。禁止传入逻辑坐标。 - **物理验证**:模拟操作前必须确保窗口已通过 `activate()` 置于前台。 - **坐标对齐**: 物理坐标 = 截图坐标;ljqCtrl 自动处理 DPI 换算,禁止手动重复计算。 - **⚠️ 窗口坐标转换陷阱**:使用 `win32gui.GetWindowRect(hwnd)` 获取的矩形包含标题栏和边框,而截图内容是客户区。点击截图内元素时,必须用 `win32gui.ClientToScreen(hwnd, (0, 0))` 获取客户区原点的屏幕坐标,再加上截图内坐标。禁止直接用 GetWindowRect 左上角 + 截图坐标。**同理禁止 `DwmGetWindowAttribute(hwnd, 9, ...)` 取窗口矩形替代 ClientToScreen,它也包含标题栏/阴影。** -- **Click 局部像素变化为0不等于失败**:macOS计算器乘号、词典搜索框实测点击生效但局部diff为0,AXClick回退也可能误报False。先看控件值/焦点/窗口截图;未生效再查窗口原点、DPI、客户区或裁剪原点(`CropToScreen`),禁止盲目重复点击。 -- **macOS键盘修饰标志**:实测普通键继承Command标志而失效;临时将keydown/up均显式`CGEventSetFlags(e, flags)`(含flags=0)后,数字8和回车恢复。仅非零时设置flags不够;正式键盘工具修复须另行确认。 +- **点击验收**:局部diff=0或AXClick=False未必失败;看控件值/焦点/截图,勿盲重试。 - **⚠️ win32 DPI 坐标陷阱**:未调用 `SetProcessDPIAware()` 时,`GetWindowRect/ClientToScreen/GetClientRect` 等拿到的窗口/客户区坐标通常是**逻辑坐标**,必须进行换算! -- **文本输入**:ljqCtrl 无 TypeText/SendKeys。向输入框键入文本:先点击/三击选中字段,再 `pyperclip.copy('文本'); ljqCtrl.Press('ctrl+v')`。 +- **文本输入**:先聚焦输入框。Windows 无 TypeText/SendKeys,用 `pyperclip.copy('文本'); ljqCtrl.Press('ctrl+v')`;macOS 用 `ljqCtrl.TypeText('文本')`。 -## 5. macOS:OCR/vision 认不准图标时,用辅助功能 API 枚举真实控件(强烈推荐) -> **两条通路**:①`macljqCtrl.py` 已封装原生 pyobjc AX API(首选,免 shell):`AXElements(pid或bundle_id或app名)` 枚举控件树(带 role/desc/title/id/value/**enabled**/物理坐标),`AXFind(...,enabled_only=)` 过滤,`AXClick(node)` = AXPress 优先失败回退物理坐标 Click。②无 pyobjc 时回退下述 osascript 方案。 -图标类按钮(···更多 / 铅笔编辑 / 关闭等)靠 OCR/vision 极易误判误点。优先走 GUI 优先链的「UIA」层:用 `osascript` 的 System Events 递归 `entire contents` 枚举进程**所有窗口**的真实控件,拿到 `AXRole + description(标识符) + position`,直接 `perform action "AXPress"` 点中。 +## 5. macOS:优先 AX 控件定位,不可用再走图像识别 +> **首选封装**:`AXElements(pid或bundle_id或app名)` 返回控件树及物理坐标;`AXFind(...,enabled_only=True)` 过滤可用控件;`AXClick(node)` 优先 AXPress,失败回退坐标点击。 +AX 封装不可用时,可用 `osascript` / System Events 的 `entire contents` 枚举**所有窗口**,按 role/description 定位后执行 `AXPress`。 - **关键坑**:弹窗/详情卡常是**独立子窗口**,`front window` 只返回主窗(如红绿灯按钮)。必须 `every window` 遍历 + `entire contents`,否则找不到目标控件。 - 控件常自带语义化 `description`/`identifier`(如 `xxx_button_more`),按 description 精确匹配比坐标稳定,枚举一次记下目标标识即可复用。 -- **坐标换算**:AX 返回的是**逻辑坐标**,截图/Click 用**物理坐标**,retina 屏 ×2(逻辑(537,121)↔物理(1074,242)实测吻合)。AX `AXPress` 直接作用元素免换算;若 AX 偶发 NOTFOUND(时序波动),用换算后物理坐标 `Click` 兜底。 +- **坐标换算**:原生 AX 返回逻辑坐标,须除以 `dpi_scale`;`AXElements/AXFind` 已返回物理坐标,勿重复缩放。`AXPress` 无需坐标;失效时重新定位,再用物理坐标 `Click`。 - **失焦陷阱**:点击坐标若落在窗口边界外,会点到背后别的 app 导致目标失焦。osascript `tell application "" to activate` 比 ljqCtrl 的 ActivateApp 更可靠,激活后用 `frontmost` 确认。 \ No newline at end of file diff --git a/memory/macljqCtrl.py b/memory/macljqCtrl.py index 372a38f6..4dfaecd4 100644 --- a/memory/macljqCtrl.py +++ b/memory/macljqCtrl.py @@ -157,10 +157,10 @@ _MODS = { def _key_tap(keycode, flags=0): down = Quartz.CGEventCreateKeyboardEvent(None, keycode, True) - if flags: Quartz.CGEventSetFlags(down, flags) + Quartz.CGEventSetFlags(down, flags) _post(down) up = Quartz.CGEventCreateKeyboardEvent(None, keycode, False) - if flags: Quartz.CGEventSetFlags(up, flags) + Quartz.CGEventSetFlags(up, flags) _post(up) def Press(cmd, staytime=0):