ucu-mcp 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/src/mcp/tools/app-tools.js +7 -0
- package/dist/src/mcp/tools/input-tools.js +16 -10
- package/dist/src/mcp/tools/keyboard-tools.js +8 -4
- package/dist/src/platform/base.d.ts +17 -6
- package/dist/src/platform/macos/base.d.ts +2 -1
- package/dist/src/platform/macos/base.js +2 -1
- package/dist/src/platform/macos/element.d.ts +2 -0
- package/dist/src/platform/macos/element.js +54 -66
- package/dist/src/platform/macos/input.d.ts +7 -6
- package/dist/src/platform/macos/input.js +31 -7
- package/dist/src/platform/macos/window.d.ts +2 -0
- package/dist/src/platform/macos/window.js +28 -1
- package/dist/src/utils/input.d.ts +14 -8
- package/dist/src/utils/input.js +102 -43
- package/native/skylight/main.swift +402 -0
- package/native/skylight/skylight-helper +0 -0
- package/package.json +2 -2
- package/skills/ucu-mcp/SKILL.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-06-17
|
|
9
|
+
|
|
10
|
+
对标 Codex 原生 computer use:输入注入走 per-process 路径,不移动全局光标、不抢前台。仅 Mac 端(与 Codex mac 版一致,Win 版只前台)。
|
|
11
|
+
|
|
12
|
+
### Added — 核心后台能力
|
|
13
|
+
|
|
14
|
+
- **skylight-helper(native/skylight/main.swift)**:第 4 个 Swift native helper。dlopen SkyLight.framework,`SLEventPostToPid`(私有 SPI)按 PID 投递鼠标事件——绕开全局 HID 流,光标不动。完整 cua-driver 配方:NSEvent bridge 构建(绕开 Chromium renderer 过滤)+ field 40 pid latch + `CGEventSetWindowLocation` + focus-without-raise(`SLPSPostEventRecordTo` 248-byte event record,不调 `SLPSSetFrontProcessWithOptions`)+ (-1,-1) primer 欺骗 Chromium user-activation gate。键盘走公开 `CGEventPostToPid`。frontmost/canvas app 自动 fallback HID-tap。
|
|
15
|
+
- **pid/windowNumber 透传链**:`WindowInfo`/`AppTarget` 加 `windowNumber`(真实 CGWindowID,window.ts 原本丢弃了)。`macos/input.ts` 读 `activeTarget` 透传到 `utils/input.ts`。`utils/input.ts` 函数加 `target` 参数,pid>0 走 skylight-helper,否则 fallback cgevent-helper(HID-tap)。
|
|
16
|
+
- **AX 保活(方向4b 根因修复)**:skylight-helper 的 `keepAlive` 命令写 `AXManualAccessibility`/`AXEnhancedUserInterface` + 订阅 remote-aware AX observer(`_AXObserverAddNotificationAndCheckRemote`,符号缺失时 fallback 公开 API)。focusApp 成功后调用,保活目标 AX 树(被遮挡的 Electron/Tauri 不再冻结 → AXPress 不再静默失败)。
|
|
17
|
+
- **tray per-process**:`MenuBarExtraItem` 加 `pid` 字段,findMenuBarExtra 捕获 SystemUIServer pid。focusApp 建 tray target 时用真实 host pid(非 0),tray 坐标点击也走 per-process。
|
|
18
|
+
- **dispatch 信号透出**:所有输入工具返回 `result.dispatch`(`"per-pid" | "hid-tap"`)。hid-tap 时附 warning。doctor 检测 skylight-helper 可用性。
|
|
19
|
+
- **DispatchMethod 类型**(base.ts):Platform 接口 mouse/keyboard 方法返回 `Promise<DispatchMethod | void>`。
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **element.ts 坐标 fallback 迁移**:`JXA_COORDINATE_CLICK`(JXA HID-tap)重命名为 `JXA_BOUNDS_CENTER`(只算中心坐标)。clickElement/clickMenuBarExtra 的坐标点击退出 JXA,改在 TS 层调 `this.click()`(走 per-process)。
|
|
24
|
+
- SKILL.md:加 "Dispatch method (v0.6.0+)" 节 + Operating Rules 加 "focus_app before input"。
|
|
25
|
+
|
|
26
|
+
### 真机验证(2026-06-17)
|
|
27
|
+
|
|
28
|
+
- ✅ skylight-helper ping 返回 `{"ok":true,"skylight":true}`(SPI 全加载)。
|
|
29
|
+
- ✅ 对后台 Finder pid click(100,100) 返回 `method:per-pid`,光标不跳到 (100,100)(HID-tap 会直接跳过去)——核心承诺验证。
|
|
30
|
+
- ✅ keepAlive 对 Finder pid 返回 `method:ax-keepalive`。
|
|
31
|
+
- ✅ 310 tests passed(4 native helpers 全编译)。
|
|
32
|
+
|
|
33
|
+
### 已知限制
|
|
34
|
+
|
|
35
|
+
- Canvas/GPU app(Blender/Unity/游戏)的 event loop 只接受 HID-tap → 自动 fallback,返回 `dispatch:"hid-tap"` + warning。
|
|
36
|
+
- `_AXObserverAddNotificationAndCheckRemote` 符号在本机未通过 dlsym 解析(remote:false),但 AXManualAccessibility/AXEnhancedUserInterface 属性写入生效(覆盖非 Chromium 场景)。
|
|
37
|
+
|
|
8
38
|
## [0.5.2] - 2026-06-17
|
|
9
39
|
|
|
10
40
|
Agent Skill 重写为 CLI agent 优先(Claude Code / Codex / OpenCode 都是 stdio CLI 环境)。
|
|
@@ -122,10 +122,12 @@ export function registerAppTools(registerTool) {
|
|
|
122
122
|
const cgevent = resolveHelperPath(["native", "cgevent", "cgevent-helper"]);
|
|
123
123
|
const ocr = resolveHelperPath(["native", "ocr", "ocr-helper"]);
|
|
124
124
|
const windowlist = resolveHelperPath(["native", "windowlist", "windowlist-helper"]);
|
|
125
|
+
const skylight = resolveHelperPath(["native", "skylight", "skylight-helper"]);
|
|
125
126
|
nativeHelpers = {
|
|
126
127
|
cgevent: { ok: cgevent.path !== null, path: cgevent.path, tried: cgevent.tried },
|
|
127
128
|
ocr: { ok: ocr.path !== null, path: ocr.path, tried: ocr.tried },
|
|
128
129
|
windowlist: { ok: windowlist.path !== null, path: windowlist.path, tried: windowlist.tried },
|
|
130
|
+
skylight: { ok: skylight.path !== null, path: skylight.path, tried: skylight.tried },
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
let readiness = "ready";
|
|
@@ -153,6 +155,11 @@ export function registerAppTools(registerTool) {
|
|
|
153
155
|
readiness = readiness === "ready" ? "degraded" : readiness;
|
|
154
156
|
issues.push("Native windowlist helper not found (window enumeration will fall back to slow JXA). Run `npm run build` to compile it.");
|
|
155
157
|
}
|
|
158
|
+
if (!nativeHelpers.skylight.ok) {
|
|
159
|
+
// skylight missing → per-process posting unavailable; input falls back to
|
|
160
|
+
// HID-tap (moves cursor). Note as info, not degraded, since HID-tap still works.
|
|
161
|
+
issues.push("Native skylight helper not found (per-process event posting unavailable — clicks/typing will move the cursor via HID-tap). Run `npm run build` to compile it.");
|
|
162
|
+
}
|
|
156
163
|
}
|
|
157
164
|
const electronHint = "If the target app is Electron (e.g. CC Switch, VS Code, Discord), list_windows may return [] even with Accessibility granted to your terminal. Grant Accessibility to the Electron app itself in System Settings > Privacy & Security > Accessibility, and restart the app. Pixel-level workaround: use screenshot + ocr to locate UI elements by text, then click(x, y) at the detected bounding box coordinates. Alternatively, modify the app\'s config file or database directly.";
|
|
158
165
|
const clients = {};
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getPlatform, resolvePoint, getSafetyContext, withSafety, actionResponse, captureAfterFields, } from "./helpers.js";
|
|
3
|
+
/** Warning text emitted when an input action had to fall back to HID-tap (moves the cursor). */
|
|
4
|
+
const HID_TAP_WARNING = "Event dispatched via global HID tap (moved the cursor / may disturb foreground). This happens for frontmost or canvas/GPU apps where per-process posting is filtered.";
|
|
5
|
+
function dispatchWarnings(dispatch) {
|
|
6
|
+
// undefined means JXA fallback (HID-tap) or no target — warn in both cases.
|
|
7
|
+
return dispatch === "per-pid" ? [] : [HID_TAP_WARNING];
|
|
8
|
+
}
|
|
3
9
|
export function registerInputTools(registerTool) {
|
|
4
10
|
registerTool("click", "Click at screen coordinates", {
|
|
5
11
|
x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
|
|
@@ -9,8 +15,8 @@ export function registerInputTools(registerTool) {
|
|
|
9
15
|
}, async (params) => {
|
|
10
16
|
const pt = await resolvePoint(params.x, params.y, params.windowId);
|
|
11
17
|
const safetyCtx = await getSafetyContext(params.windowId);
|
|
12
|
-
await withSafety({ action: "click", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
|
|
13
|
-
return actionResponse("click", { clicked: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
18
|
+
const dispatch = await withSafety({ action: "click", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button) });
|
|
19
|
+
return actionResponse("click", { clicked: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
|
|
14
20
|
});
|
|
15
21
|
registerTool("double_click", "Double-click at screen coordinates", {
|
|
16
22
|
x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
|
|
@@ -20,8 +26,8 @@ export function registerInputTools(registerTool) {
|
|
|
20
26
|
}, async (params) => {
|
|
21
27
|
const pt = await resolvePoint(params.x, params.y, params.windowId);
|
|
22
28
|
const safetyCtx = await getSafetyContext(params.windowId);
|
|
23
|
-
await withSafety({ action: "double_click", params: { x: pt.x, y: pt.y, doubleClick: true, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
|
|
24
|
-
return actionResponse("double_click", { doubleClicked: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
29
|
+
const dispatch = await withSafety({ action: "double_click", params: { x: pt.x, y: pt.y, doubleClick: true, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().click(pt.x, pt.y, params.button, true) });
|
|
30
|
+
return actionResponse("double_click", { doubleClicked: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
|
|
25
31
|
});
|
|
26
32
|
registerTool("scroll", "Scroll at coordinates", {
|
|
27
33
|
x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
|
|
@@ -32,8 +38,8 @@ export function registerInputTools(registerTool) {
|
|
|
32
38
|
const pt = await resolvePoint(params.x, params.y, params.windowId);
|
|
33
39
|
const deltaX = params.deltaX ?? 0;
|
|
34
40
|
const safetyCtx = await getSafetyContext(params.windowId);
|
|
35
|
-
await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
|
|
36
|
-
return actionResponse("scroll", { scrolled: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
41
|
+
const dispatch = await withSafety({ action: "scroll", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().scroll(pt.x, pt.y, deltaX, params.deltaY) });
|
|
42
|
+
return actionResponse("scroll", { scrolled: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
|
|
37
43
|
});
|
|
38
44
|
registerTool("drag", "Drag from one point to another", {
|
|
39
45
|
startX: z.number().describe("Start X"), startY: z.number().describe("Start Y"),
|
|
@@ -46,8 +52,8 @@ export function registerInputTools(registerTool) {
|
|
|
46
52
|
const start = await resolvePoint(params.startX, params.startY, params.windowId);
|
|
47
53
|
const end = await resolvePoint(params.endX, params.endY, params.windowId);
|
|
48
54
|
const safetyCtx = await getSafetyContext(params.windowId);
|
|
49
|
-
await withSafety({ action: "drag", params: { startX: start.x, startY: start.y, endX: end.x, endY: end.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().drag(start.x, start.y, end.x, end.y, params.button, params.duration) });
|
|
50
|
-
return actionResponse("drag", { dragged: true, startX: start.x, startY: start.y, endX: end.x, endY: end.y }, { startX: start.x, startY: start.y, endX: end.x, endY: end.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
55
|
+
const dispatch = await withSafety({ action: "drag", params: { startX: start.x, startY: start.y, endX: end.x, endY: end.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().drag(start.x, start.y, end.x, end.y, params.button, params.duration) });
|
|
56
|
+
return actionResponse("drag", { dragged: true, startX: start.x, startY: start.y, endX: end.x, endY: end.y, dispatch: dispatch ?? "hid-tap" }, { startX: start.x, startY: start.y, endX: end.x, endY: end.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
|
|
51
57
|
});
|
|
52
58
|
registerTool("move", "Move cursor to coordinates", {
|
|
53
59
|
x: z.number().describe("X coordinate"), y: z.number().describe("Y coordinate"),
|
|
@@ -56,8 +62,8 @@ export function registerInputTools(registerTool) {
|
|
|
56
62
|
}, async (params) => {
|
|
57
63
|
const pt = await resolvePoint(params.x, params.y, params.windowId);
|
|
58
64
|
const safetyCtx = await getSafetyContext(params.windowId);
|
|
59
|
-
await withSafety({ action: "move", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
|
|
60
|
-
return actionResponse("move", { moved: true, x: pt.x, y: pt.y }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
65
|
+
const dispatch = await withSafety({ action: "move", params: { x: pt.x, y: pt.y, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().move(pt.x, pt.y) });
|
|
66
|
+
return actionResponse("move", { moved: true, x: pt.x, y: pt.y, dispatch: dispatch ?? "hid-tap" }, { x: pt.x, y: pt.y, windowId: params.windowId }, params.captureAfter, params.captureFormat, params.captureMaxWidth, dispatchWarnings(dispatch));
|
|
61
67
|
});
|
|
62
68
|
registerTool("get_cursor_position", "Get current cursor position", {}, async () => {
|
|
63
69
|
const pos = await withSafety({ action: "get_cursor_position", params: {}, execute: () => Promise.resolve(getPlatform().getCursorPosition()) });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { UnsupportedParameterError } from "../../util/errors.js";
|
|
3
3
|
import { getPlatform, getSafetyContext, withSafety, actionResponse, captureAfterFields, } from "./helpers.js";
|
|
4
|
+
function keyDispatchWarnings(dispatch) {
|
|
5
|
+
// undefined means JXA fallback (HID-tap) or no target — warn in both cases.
|
|
6
|
+
return dispatch === "per-pid" ? [] : ["Key event dispatched via global HID tap (no target pid available; may affect foreground). Use focus_app first to enable per-process posting."];
|
|
7
|
+
}
|
|
4
8
|
export function registerKeyboardTools(registerTool) {
|
|
5
9
|
registerTool("type_text", "Type text at the current cursor position", {
|
|
6
10
|
text: z.string().describe("Text to type"), delay: z.number().optional().describe("Delay between keystrokes in ms"),
|
|
@@ -10,8 +14,8 @@ export function registerKeyboardTools(registerTool) {
|
|
|
10
14
|
if (params.windowId)
|
|
11
15
|
throw new UnsupportedParameterError("windowId-targeted keyboard typing is not implemented");
|
|
12
16
|
const safetyCtx = await getSafetyContext();
|
|
13
|
-
await withSafety({ action: "type_text", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
|
|
14
|
-
return actionResponse("type_text", { typed: true, charCount: params.text.length }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
17
|
+
const dispatch = await withSafety({ action: "type_text", params: { text: params.text, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().type(params.text, params.delay) });
|
|
18
|
+
return actionResponse("type_text", { typed: true, charCount: params.text.length, dispatch: dispatch ?? "hid-tap" }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth, keyDispatchWarnings(dispatch));
|
|
15
19
|
});
|
|
16
20
|
registerTool("press_key", "Press a keyboard shortcut", {
|
|
17
21
|
keys: z.array(z.string()).optional().describe("Keys to press simultaneously — special keys (enter/escape/tab/f1-f12/arrows...), single letters a-z, or single digits 0-9"),
|
|
@@ -29,7 +33,7 @@ export function registerKeyboardTools(registerTool) {
|
|
|
29
33
|
if (keys.length === 0)
|
|
30
34
|
throw new UnsupportedParameterError("press_key requires at least one key");
|
|
31
35
|
const safetyCtx = await getSafetyContext();
|
|
32
|
-
await withSafety({ action: "press_key", params: { keys, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
|
|
33
|
-
return actionResponse("press_key", { pressed: true, keys }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth);
|
|
36
|
+
const dispatch = await withSafety({ action: "press_key", params: { keys, ...safetyCtx }, requiresAccessibility: true, execute: () => getPlatform().key(keys) });
|
|
37
|
+
return actionResponse("press_key", { pressed: true, keys, dispatch: dispatch ?? "hid-tap" }, {}, params.captureAfter, params.captureFormat, params.captureMaxWidth, keyDispatchWarnings(dispatch));
|
|
34
38
|
});
|
|
35
39
|
}
|
|
@@ -31,6 +31,8 @@ export interface WindowInfo {
|
|
|
31
31
|
};
|
|
32
32
|
isMinimized: boolean;
|
|
33
33
|
isOnScreen: boolean;
|
|
34
|
+
/** Real CGWindowID (kCGWindowNumber) — needed for per-process event posting (focus-without-raise). Undefined for JXA-fallback enumeration. */
|
|
35
|
+
windowNumber?: number;
|
|
34
36
|
}
|
|
35
37
|
export interface AppInfo {
|
|
36
38
|
name: string;
|
|
@@ -45,6 +47,8 @@ export interface AppTarget {
|
|
|
45
47
|
windowId?: string;
|
|
46
48
|
title?: string;
|
|
47
49
|
capturedAt: string;
|
|
50
|
+
/** Real CGWindowID for per-process event posting (focus-without-raise). Absent for tray targets. */
|
|
51
|
+
windowNumber?: number;
|
|
48
52
|
}
|
|
49
53
|
export interface BrowserContext {
|
|
50
54
|
appName: string;
|
|
@@ -162,6 +166,13 @@ export interface ScreenDescription {
|
|
|
162
166
|
message: string;
|
|
163
167
|
}>;
|
|
164
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* How an input event was dispatched. "per-pid" = posted to the target process
|
|
171
|
+
* via SLEventPostToPid/CGEventPostToPid (no global cursor move, no foreground
|
|
172
|
+
* theft — Codex-style background operation). "hid-tap" = posted to the global
|
|
173
|
+
* HID event tap (moves the cursor; fallback when no pid or skylight unavailable).
|
|
174
|
+
*/
|
|
175
|
+
export type DispatchMethod = "per-pid" | "hid-tap";
|
|
165
176
|
export interface Platform {
|
|
166
177
|
screenshot(display?: number, region?: ScreenRegion, options?: ScreenshotOptions): Promise<Buffer>;
|
|
167
178
|
screenshotWindow?(windowId: string, options?: ScreenshotOptions): Promise<Buffer>;
|
|
@@ -171,14 +182,14 @@ export interface Platform {
|
|
|
171
182
|
getActiveBrowserContext?(app?: string): Promise<BrowserContext | undefined>;
|
|
172
183
|
listWindows(includeMinimized?: boolean): Promise<WindowInfo[]>;
|
|
173
184
|
getWindowState(windowId?: string, depth?: number, includeBounds?: boolean): Promise<WindowState>;
|
|
174
|
-
click(x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<void>;
|
|
175
|
-
move(x: number, y: number): Promise<void>;
|
|
176
|
-
drag(startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<void>;
|
|
177
|
-
scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<void>;
|
|
185
|
+
click(x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<DispatchMethod | void>;
|
|
186
|
+
move(x: number, y: number): Promise<DispatchMethod | void>;
|
|
187
|
+
drag(startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<DispatchMethod | void>;
|
|
188
|
+
scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<DispatchMethod | void>;
|
|
178
189
|
getCursorPosition(): CursorPosition;
|
|
179
190
|
ocr(display?: number, region?: ScreenRegion): Promise<OcrResult>;
|
|
180
|
-
type(text: string, delay?: number): Promise<void>;
|
|
181
|
-
key(keys: string[]): Promise<void>;
|
|
191
|
+
type(text: string, delay?: number): Promise<DispatchMethod | void>;
|
|
192
|
+
key(keys: string[]): Promise<DispatchMethod | void>;
|
|
182
193
|
findElement(options: FindElementOptions): Promise<FindElementResponse>;
|
|
183
194
|
clickElement(elementId: string, app?: string): Promise<ClickResult>;
|
|
184
195
|
typeInElement(elementId: string, text: string, app?: string, clearFirst?: boolean): Promise<void>;
|
|
@@ -2,7 +2,7 @@ import type { Platform, WindowInfo, AppTarget } from "../base.js";
|
|
|
2
2
|
import type { CachedElementDescriptor, MacOSPlatformOptions } from "./helpers.js";
|
|
3
3
|
import { saveFocus, restoreFocus } from "./focus.js";
|
|
4
4
|
import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
|
|
5
|
-
import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
|
|
5
|
+
import { listApps, focusApp, getActiveBrowserContext, listWindows, keepTargetAxAlive } from "./window.js";
|
|
6
6
|
import { getWindowState, findElement } from "./ax-tree.js";
|
|
7
7
|
import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
|
|
8
8
|
import { clickElement, typeInElement, setElementValue, findMenuBarExtra, clickMenuBarExtra } from "./element.js";
|
|
@@ -39,6 +39,7 @@ export declare class MacOSPlatform implements Platform {
|
|
|
39
39
|
listApps: typeof listApps;
|
|
40
40
|
focusApp: typeof focusApp;
|
|
41
41
|
getActiveBrowserContext: typeof getActiveBrowserContext;
|
|
42
|
+
keepTargetAxAlive: typeof keepTargetAxAlive;
|
|
42
43
|
listWindows: typeof listWindows;
|
|
43
44
|
getWindowState: typeof getWindowState;
|
|
44
45
|
findElement: typeof findElement;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TargetStaleError } from "../../util/errors.js";
|
|
2
2
|
import { saveFocus, restoreFocus } from "./focus.js";
|
|
3
3
|
import { screenshot, screenshotWindow, getScreenSize, isScreenLocked, ocr } from "./screen.js";
|
|
4
|
-
import { listApps, focusApp, getActiveBrowserContext, listWindows } from "./window.js";
|
|
4
|
+
import { listApps, focusApp, getActiveBrowserContext, listWindows, keepTargetAxAlive } from "./window.js";
|
|
5
5
|
import { getWindowState, findElement } from "./ax-tree.js";
|
|
6
6
|
import { click, move, drag, scroll, getCursorPosition, type as typeMethod, key } from "./input.js";
|
|
7
7
|
import { clickElement, typeInElement, setElementValue, findMenuBarExtra, clickMenuBarExtra } from "./element.js";
|
|
@@ -78,6 +78,7 @@ export class MacOSPlatform {
|
|
|
78
78
|
listApps = listApps;
|
|
79
79
|
focusApp = focusApp;
|
|
80
80
|
getActiveBrowserContext = getActiveBrowserContext;
|
|
81
|
+
keepTargetAxAlive = keepTargetAxAlive;
|
|
81
82
|
listWindows = listWindows;
|
|
82
83
|
getWindowState = getWindowState;
|
|
83
84
|
findElement = findElement;
|
|
@@ -14,6 +14,8 @@ export interface MenuBarExtraItem {
|
|
|
14
14
|
height: number;
|
|
15
15
|
/** Which process hosts this status item. "self" = app's own menu bar; "systemuiserver" = third-party tray hosted by SystemUIServer. */
|
|
16
16
|
host: "self" | "systemuiserver";
|
|
17
|
+
/** The hosting process pid — app's own pid for host:"self", SystemUIServer's pid for host:"systemuiserver". Enables per-process event posting. */
|
|
18
|
+
pid: number;
|
|
17
19
|
}
|
|
18
20
|
export interface MenuBarExtraSelector {
|
|
19
21
|
description?: string;
|
|
@@ -44,23 +44,16 @@ const JXA_STATE_SIGNATURE = `
|
|
|
44
44
|
function spinMs(ms) { var t = Date.now(); while (Date.now() - t < ms) {} }
|
|
45
45
|
`;
|
|
46
46
|
/**
|
|
47
|
-
* Shared JXA snippet:
|
|
48
|
-
*
|
|
47
|
+
* Shared JXA snippet: compute the element's bounds center (for coordinate fallback).
|
|
48
|
+
* Does NOT post any event — the actual click is performed by the TS input layer
|
|
49
|
+
* (this.click) so it routes through per-process posting (skylight-helper) when a
|
|
50
|
+
* pid is available, instead of the JXA HID-tap that would move the global cursor.
|
|
49
51
|
*/
|
|
50
|
-
const
|
|
51
|
-
function
|
|
52
|
+
const JXA_BOUNDS_CENTER = `
|
|
53
|
+
function boundsCenter(elem) {
|
|
52
54
|
var pos = elem.position();
|
|
53
55
|
var sz = elem.size();
|
|
54
|
-
|
|
55
|
-
var cy = pos[1] + sz[1] / 2;
|
|
56
|
-
ObjC.import('CoreGraphics');
|
|
57
|
-
var src = $.CGEventSourceCreate($.kCGEventSourceStateHIDSystemState);
|
|
58
|
-
var pt = $.CGPointMake(cx, cy);
|
|
59
|
-
var down = $.CGEventCreateMouseEvent(src, $.kCGEventLeftMouseDown, pt, $.kCGMouseButtonLeft);
|
|
60
|
-
$.CGEventPost($.kCGHIDEventTap, down);
|
|
61
|
-
var up = $.CGEventCreateMouseEvent(src, $.kCGEventLeftMouseUp, pt, $.kCGMouseButtonLeft);
|
|
62
|
-
$.CGEventPost($.kCGHIDEventTap, up);
|
|
63
|
-
return {x: cx, y: cy};
|
|
56
|
+
return {x: pos[0] + sz[0] / 2, y: pos[1] + sz[1] / 2};
|
|
64
57
|
}
|
|
65
58
|
`;
|
|
66
59
|
export async function clickElement(elementId, app) {
|
|
@@ -75,7 +68,7 @@ export async function clickElement(elementId, app) {
|
|
|
75
68
|
var _result = null;
|
|
76
69
|
${jxaElementActionHelpers()}
|
|
77
70
|
${JXA_STATE_SIGNATURE}
|
|
78
|
-
${
|
|
71
|
+
${JXA_BOUNDS_CENTER}
|
|
79
72
|
var elemPath = ${elementIdLiteral};
|
|
80
73
|
var appName = ${appLiteral};
|
|
81
74
|
// 容忍大小写/空格/连字符/下划线变体(cc-switch vs CC Switch vs cc_switch)
|
|
@@ -95,15 +88,11 @@ export async function clickElement(elementId, app) {
|
|
|
95
88
|
if (!elem) {
|
|
96
89
|
_result = {success: false, error: "Element not found: " + elemPath};
|
|
97
90
|
} else if (preferCoord) {
|
|
98
|
-
// 启发式命中(Tauri 等已知静默吞 AXPress
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
_result = {success: true, method: "coordinate", verified: false};
|
|
102
|
-
} catch(e) {
|
|
103
|
-
_result = {success: false, error: "Could not click element (coordinate): " + String(e.message || e)};
|
|
104
|
-
}
|
|
91
|
+
// 启发式命中(Tauri 等已知静默吞 AXPress 的应用):请求坐标点击(TS 层执行 per-process)
|
|
92
|
+
var c = boundsCenter(elem);
|
|
93
|
+
_result = {success: true, needCoordinateClick: true, cx: c.x, cy: c.y};
|
|
105
94
|
} else {
|
|
106
|
-
// AXPress + verify
|
|
95
|
+
// AXPress + verify:采样前后状态签名,无变化则请求坐标点击
|
|
107
96
|
var sigBefore = stateSignature(elem);
|
|
108
97
|
var axpressThrew = false;
|
|
109
98
|
try {
|
|
@@ -112,57 +101,50 @@ export async function clickElement(elementId, app) {
|
|
|
112
101
|
axpressThrew = true;
|
|
113
102
|
}
|
|
114
103
|
if (axpressThrew) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
coordinateClick(elem);
|
|
118
|
-
_result = {success: true, method: "coordinate", verified: false};
|
|
119
|
-
} catch(e2) {
|
|
120
|
-
_result = {success: false, error: "Could not click element: " + String(e2.message || e2)};
|
|
121
|
-
}
|
|
104
|
+
var c1 = boundsCenter(elem);
|
|
105
|
+
_result = {success: true, needCoordinateClick: true, cx: c1.x, cy: c1.y};
|
|
122
106
|
} else {
|
|
123
|
-
// AXPress 未抛异常:spin 等待异步生效,再采样签名判断是否真的生效
|
|
124
107
|
spinMs(80);
|
|
125
108
|
var sigAfter = stateSignature(elem);
|
|
126
109
|
if (sigBefore !== '' && sigAfter !== sigBefore) {
|
|
127
|
-
// 状态变化 → AXPress 生效
|
|
128
110
|
_result = {success: true, method: "axpress", verified: true};
|
|
129
111
|
} else if (sigBefore === '' && sigAfter === '') {
|
|
130
|
-
// 无可观测状态(纯按钮无 value/focused/selected):无法 verify,
|
|
131
|
-
// 保守认为 AXPress 可能成功(不降级坐标,避免对正常按钮误伤)
|
|
132
112
|
_result = {success: true, method: "axpress", verified: false};
|
|
133
113
|
} else {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
coordinateClick(elem);
|
|
137
|
-
_result = {success: true, method: "coordinate", verified: false};
|
|
138
|
-
} catch(e3) {
|
|
139
|
-
_result = {success: false, error: "Could not click element (coordinate fallback): " + String(e3.message || e3)};
|
|
140
|
-
}
|
|
114
|
+
var c2 = boundsCenter(elem);
|
|
115
|
+
_result = {success: true, needCoordinateClick: true, cx: c2.x, cy: c2.y};
|
|
141
116
|
}
|
|
142
117
|
}
|
|
143
118
|
}
|
|
144
119
|
JSON.stringify(_result);
|
|
145
120
|
`;
|
|
121
|
+
let result;
|
|
146
122
|
try {
|
|
147
123
|
const out = execFileSync("osascript", [
|
|
148
124
|
"-l", "JavaScript",
|
|
149
125
|
"-e", jxaScript,
|
|
150
126
|
], { encoding: "utf-8", timeout: 15000 }).trim();
|
|
151
|
-
|
|
152
|
-
if (!result.success) {
|
|
153
|
-
throw result.error
|
|
154
|
-
? new Error(result.error)
|
|
155
|
-
: new ElementNotFoundError(elementId);
|
|
156
|
-
}
|
|
157
|
-
return {
|
|
158
|
-
method: result.method ?? "axpress",
|
|
159
|
-
verified: Boolean(result.verified),
|
|
160
|
-
};
|
|
127
|
+
result = JSON.parse(out);
|
|
161
128
|
}
|
|
162
129
|
catch (error) {
|
|
163
130
|
rethrowElementActionError(error, "click_element", elementId);
|
|
164
131
|
return { method: "axpress", verified: false }; // unreachable — rethrow throws
|
|
165
132
|
}
|
|
133
|
+
if (!result.success) {
|
|
134
|
+
// Route through rethrowElementActionError so "element not found" / accessibility
|
|
135
|
+
// errors are converted to the proper UcuError subclasses (ElementNotFoundError etc.).
|
|
136
|
+
rethrowElementActionError(new Error(result.error || "click_element failed"), "click_element", elementId);
|
|
137
|
+
}
|
|
138
|
+
// JXA requested a coordinate click → perform it via the input layer (per-process
|
|
139
|
+
// when a pid is available, so the global cursor does not move).
|
|
140
|
+
if (result.needCoordinateClick && typeof result.cx === "number" && typeof result.cy === "number") {
|
|
141
|
+
await this.click(Math.round(result.cx), Math.round(result.cy), "left", false);
|
|
142
|
+
return { method: "coordinate", verified: false };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
method: result.method ?? "axpress",
|
|
146
|
+
verified: Boolean(result.verified),
|
|
147
|
+
};
|
|
166
148
|
}
|
|
167
149
|
export async function typeInElement(elementId, text, app, clearFirst) {
|
|
168
150
|
const textLiteral = JSON.stringify(text);
|
|
@@ -324,7 +306,7 @@ export async function findMenuBarExtra(app) {
|
|
|
324
306
|
return itemNorm === appNorm || itemNorm.indexOf(appNorm) !== -1 || appNorm.indexOf(itemNorm) !== -1;
|
|
325
307
|
};
|
|
326
308
|
|
|
327
|
-
var _readItem = function(item, mb, i, host) {
|
|
309
|
+
var _readItem = function(item, mb, i, host, hostPid) {
|
|
328
310
|
var desc = '', nm = '';
|
|
329
311
|
try { desc = item.description(); } catch(e) {}
|
|
330
312
|
try { nm = item.name(); } catch(e) {}
|
|
@@ -332,7 +314,7 @@ export async function findMenuBarExtra(app) {
|
|
|
332
314
|
try { pos = item.position(); } catch(e) {}
|
|
333
315
|
try { sz = item.size(); } catch(e) {}
|
|
334
316
|
if (sz[0] === 0 && sz[1] === 0) return null;
|
|
335
|
-
return {menuBar: mb, index: i, name: nm, description: desc, x: pos[0], y: pos[1], width: sz[0], height: sz[1], host: host};
|
|
317
|
+
return {menuBar: mb, index: i, name: nm, description: desc, x: pos[0], y: pos[1], width: sz[0], height: sz[1], host: host, pid: hostPid || 0};
|
|
336
318
|
};
|
|
337
319
|
|
|
338
320
|
try {
|
|
@@ -346,7 +328,9 @@ export async function findMenuBarExtra(app) {
|
|
|
346
328
|
|
|
347
329
|
var items = [];
|
|
348
330
|
|
|
349
|
-
// 阶段 1:app 自身 menuBarItems(host:"self")
|
|
331
|
+
// 阶段 1:app 自身 menuBarItems(host:"self",用 app 自身 pid)
|
|
332
|
+
var appPid = 0;
|
|
333
|
+
if (p) { try { appPid = p.unixId(); } catch(e) {} }
|
|
350
334
|
if (p) {
|
|
351
335
|
try {
|
|
352
336
|
var menuBars = p.menuBars();
|
|
@@ -354,7 +338,7 @@ export async function findMenuBarExtra(app) {
|
|
|
354
338
|
var mbItems;
|
|
355
339
|
try { mbItems = menuBars[mb].menuBarItems(); } catch(e) { continue; }
|
|
356
340
|
for (var i = 0; i < mbItems.length; i++) {
|
|
357
|
-
var rec = _readItem(mbItems[i], mb, i, "self");
|
|
341
|
+
var rec = _readItem(mbItems[i], mb, i, "self", appPid);
|
|
358
342
|
if (rec) items.push(rec);
|
|
359
343
|
}
|
|
360
344
|
}
|
|
@@ -372,6 +356,8 @@ export async function findMenuBarExtra(app) {
|
|
|
372
356
|
if (!hasNonApple) {
|
|
373
357
|
try {
|
|
374
358
|
var suiProcs = se.processes.byName("SystemUIServer");
|
|
359
|
+
var suiPid = 0;
|
|
360
|
+
if (suiProcs) { try { suiPid = suiProcs.unixId(); } catch(e) {} }
|
|
375
361
|
if (suiProcs) {
|
|
376
362
|
var suiBars = suiProcs.menuBars();
|
|
377
363
|
for (var smb = 0; smb < suiBars.length; smb++) {
|
|
@@ -384,7 +370,7 @@ export async function findMenuBarExtra(app) {
|
|
|
384
370
|
try { sNm = sItem.name(); } catch(e) {}
|
|
385
371
|
// 按 description/name 匹配目标 app(status item 的 description 通常是 app 名)
|
|
386
372
|
if (_matchApp(_norm(sDesc)) || _matchApp(_norm(sNm))) {
|
|
387
|
-
var sRec = _readItem(sItem, smb, si, "systemuiserver");
|
|
373
|
+
var sRec = _readItem(sItem, smb, si, "systemuiserver", suiPid);
|
|
388
374
|
if (sRec) {
|
|
389
375
|
// 保留匹配信号,供 click 二次定位
|
|
390
376
|
sRec.name = sNm; sRec.description = sDesc;
|
|
@@ -499,7 +485,7 @@ export async function clickMenuBarExtra(app, selector = {}) {
|
|
|
499
485
|
var tgtName = ${tgtNameLiteral};
|
|
500
486
|
var tgtDesc = ${tgtDescLiteral};
|
|
501
487
|
${JXA_STATE_SIGNATURE}
|
|
502
|
-
${
|
|
488
|
+
${JXA_BOUNDS_CENTER}
|
|
503
489
|
// 容忍大小写/空格/连字符/下划线变体(cc-switch vs CC Switch vs cc_switch)
|
|
504
490
|
var _norm = function(s) { return String(s||'').toLowerCase().split(' ').join('').split('-').join('').split('_').join(''); };
|
|
505
491
|
var appNorm = _norm(appName);
|
|
@@ -521,9 +507,9 @@ export async function clickMenuBarExtra(app, selector = {}) {
|
|
|
521
507
|
${resolveItemBlock}
|
|
522
508
|
if (!_result && item) {
|
|
523
509
|
if (preferCoord) {
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
_result = {success: true,
|
|
510
|
+
// 启发式命中:请求坐标点击(TS 层执行 per-process)
|
|
511
|
+
var c0 = boundsCenter(item);
|
|
512
|
+
_result = {success: true, needCoordinateClick: true, cx: c0.x, cy: c0.y};
|
|
527
513
|
} else {
|
|
528
514
|
// AXPress + verify
|
|
529
515
|
var sigBefore = stateSignature(item);
|
|
@@ -534,20 +520,18 @@ export async function clickMenuBarExtra(app, selector = {}) {
|
|
|
534
520
|
axpressThrew = true;
|
|
535
521
|
}
|
|
536
522
|
if (axpressThrew) {
|
|
537
|
-
|
|
538
|
-
_result = {success: true,
|
|
523
|
+
var c1 = boundsCenter(item);
|
|
524
|
+
_result = {success: true, needCoordinateClick: true, cx: c1.x, cy: c1.y};
|
|
539
525
|
} else {
|
|
540
526
|
spinMs(80);
|
|
541
527
|
var sigAfter = stateSignature(item);
|
|
542
528
|
if (sigBefore !== '' && sigAfter !== sigBefore) {
|
|
543
529
|
_result = {success: true, method: "axpress", verified: true};
|
|
544
530
|
} else if (sigBefore === '' && sigAfter === '') {
|
|
545
|
-
// 托盘 status item 通常无可观测状态(无 value/selected);AXPress 开菜单的效果
|
|
546
|
-
// 难以从 item 本身验证。保守认为成功(菜单是否打开由后续 find_element 判断)。
|
|
547
531
|
_result = {success: true, method: "axpress", verified: false};
|
|
548
532
|
} else {
|
|
549
|
-
|
|
550
|
-
_result = {success: true,
|
|
533
|
+
var c2 = boundsCenter(item);
|
|
534
|
+
_result = {success: true, needCoordinateClick: true, cx: c2.x, cy: c2.y};
|
|
551
535
|
}
|
|
552
536
|
}
|
|
553
537
|
}
|
|
@@ -569,6 +553,10 @@ export async function clickMenuBarExtra(app, selector = {}) {
|
|
|
569
553
|
if (!result.success) {
|
|
570
554
|
throw new Error(`click_menu_bar_extra failed in ${app}: ${result.error}`);
|
|
571
555
|
}
|
|
556
|
+
if (result.needCoordinateClick && typeof result.cx === "number" && typeof result.cy === "number") {
|
|
557
|
+
await this.click(Math.round(result.cx), Math.round(result.cy), "left", false);
|
|
558
|
+
return { method: "coordinate", verified: false };
|
|
559
|
+
}
|
|
572
560
|
return {
|
|
573
561
|
method: result.method ?? "axpress",
|
|
574
562
|
verified: Boolean(result.verified),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { MacOSPlatform } from "./base.js";
|
|
2
2
|
import type { CursorPosition } from "../base.js";
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
3
|
+
import type { DispatchMethod } from "../base.js";
|
|
4
|
+
export declare function click(this: MacOSPlatform, x: number, y: number, button?: "left" | "right" | "middle", doubleClick?: boolean): Promise<DispatchMethod | void>;
|
|
5
|
+
export declare function move(this: MacOSPlatform, x: number, y: number): Promise<DispatchMethod | void>;
|
|
6
|
+
export declare function drag(this: MacOSPlatform, startX: number, startY: number, endX: number, endY: number, button?: "left" | "right" | "middle", duration?: number): Promise<DispatchMethod | void>;
|
|
7
|
+
export declare function scroll(this: MacOSPlatform, x: number, y: number, deltaX: number, deltaY: number): Promise<DispatchMethod | void>;
|
|
7
8
|
export declare function getCursorPosition(this: MacOSPlatform): CursorPosition;
|
|
8
|
-
export declare function type(this: MacOSPlatform, text: string, delay?: number): Promise<void>;
|
|
9
|
-
export declare function key(this: MacOSPlatform, keys: string[]): Promise<void>;
|
|
9
|
+
export declare function type(this: MacOSPlatform, text: string, delay?: number): Promise<DispatchMethod | void>;
|
|
10
|
+
export declare function key(this: MacOSPlatform, keys: string[]): Promise<DispatchMethod | void>;
|
|
@@ -2,13 +2,37 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { click as inputClick, doubleClick as inputDoubleClick, move as inputMove, drag as inputDrag, scroll as inputScroll, typeText, pressShortcut } from "../../utils/input.js";
|
|
3
3
|
import { PlatformError } from "../../util/errors.js";
|
|
4
4
|
import { rethrowInputError, errorMessage } from "./helpers.js";
|
|
5
|
+
/**
|
|
6
|
+
* Process names that must NEVER receive per-process input injection (security).
|
|
7
|
+
* Per-process posting bypasses the frontmost/focus requirement, so without this
|
|
8
|
+
* denylist an attacker could deliver keystrokes/clicks directly into a password
|
|
9
|
+
* manager or auth dialog. These fall back to HID-tap (which at least requires
|
|
10
|
+
* the app to be frontmost / visible to the user).
|
|
11
|
+
*/
|
|
12
|
+
const SENSITIVE_PROCESS_HINTS = [
|
|
13
|
+
"keychainaccess", "1password", "lastpass", "bitwarden", "securityagent",
|
|
14
|
+
"authd", "loginwindow", "applekeychain", "dashlane",
|
|
15
|
+
];
|
|
16
|
+
/** Resolve the per-process event target from the active focus_app target (pid + windowNumber). */
|
|
17
|
+
function targetOf() {
|
|
18
|
+
const t = this.activeTarget;
|
|
19
|
+
if (!t || !t.pid || t.pid <= 0)
|
|
20
|
+
return undefined;
|
|
21
|
+
// Security: never route per-process events into sensitive apps (password managers / auth).
|
|
22
|
+
const name = (t.appName || "").toLowerCase();
|
|
23
|
+
if (SENSITIVE_PROCESS_HINTS.some((h) => name.includes(h))) {
|
|
24
|
+
return undefined; // forces HID-tap fallback (requires frontmost)
|
|
25
|
+
}
|
|
26
|
+
return { pid: t.pid, windowNumber: t.windowNumber };
|
|
27
|
+
}
|
|
5
28
|
export async function click(x, y, button, doubleClick) {
|
|
6
29
|
try {
|
|
30
|
+
const target = targetOf.call(this);
|
|
7
31
|
if (doubleClick) {
|
|
8
|
-
await inputDoubleClick(x, y, button);
|
|
32
|
+
return await inputDoubleClick(x, y, button, process.platform, target);
|
|
9
33
|
}
|
|
10
34
|
else {
|
|
11
|
-
await inputClick(x, y, button);
|
|
35
|
+
return await inputClick(x, y, button, process.platform, target);
|
|
12
36
|
}
|
|
13
37
|
}
|
|
14
38
|
catch (error) {
|
|
@@ -17,7 +41,7 @@ export async function click(x, y, button, doubleClick) {
|
|
|
17
41
|
}
|
|
18
42
|
export async function move(x, y) {
|
|
19
43
|
try {
|
|
20
|
-
await inputMove(x, y);
|
|
44
|
+
return await inputMove(x, y, process.platform, targetOf.call(this));
|
|
21
45
|
}
|
|
22
46
|
catch (error) {
|
|
23
47
|
rethrowInputError(error, "move");
|
|
@@ -25,7 +49,7 @@ export async function move(x, y) {
|
|
|
25
49
|
}
|
|
26
50
|
export async function drag(startX, startY, endX, endY, button, duration) {
|
|
27
51
|
try {
|
|
28
|
-
await inputDrag(startX, startY, endX, endY, button, duration);
|
|
52
|
+
return await inputDrag(startX, startY, endX, endY, button, duration, process.platform, targetOf.call(this));
|
|
29
53
|
}
|
|
30
54
|
catch (error) {
|
|
31
55
|
rethrowInputError(error, "drag");
|
|
@@ -33,7 +57,7 @@ export async function drag(startX, startY, endX, endY, button, duration) {
|
|
|
33
57
|
}
|
|
34
58
|
export async function scroll(x, y, deltaX, deltaY) {
|
|
35
59
|
try {
|
|
36
|
-
await inputScroll(x, y, deltaX, deltaY);
|
|
60
|
+
return await inputScroll(x, y, deltaX, deltaY, process.platform, targetOf.call(this));
|
|
37
61
|
}
|
|
38
62
|
catch (error) {
|
|
39
63
|
rethrowInputError(error, "scroll");
|
|
@@ -55,8 +79,8 @@ export function getCursorPosition() {
|
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
81
|
export async function type(text, delay) {
|
|
58
|
-
await typeText(text, delay);
|
|
82
|
+
return await typeText(text, delay, process.platform, targetOf.call(this));
|
|
59
83
|
}
|
|
60
84
|
export async function key(keys) {
|
|
61
|
-
await pressShortcut(keys);
|
|
85
|
+
return await pressShortcut(keys, process.platform, targetOf.call(this));
|
|
62
86
|
}
|
|
@@ -2,5 +2,7 @@ import type { MacOSPlatform } from "./base.js";
|
|
|
2
2
|
import type { AppInfo, AppTarget, WindowInfo, BrowserContext } from "../base.js";
|
|
3
3
|
export declare function listApps(this: MacOSPlatform): Promise<AppInfo[]>;
|
|
4
4
|
export declare function focusApp(this: MacOSPlatform, app: string): Promise<AppTarget>;
|
|
5
|
+
/** Call the skylight-helper keepAlive command for a pid (best-effort, non-throwing). */
|
|
6
|
+
export declare function keepTargetAxAlive(this: MacOSPlatform, pid: number): Promise<void>;
|
|
5
7
|
export declare function getActiveBrowserContext(this: MacOSPlatform, app?: string): Promise<BrowserContext | undefined>;
|
|
6
8
|
export declare function listWindows(this: MacOSPlatform, _includeMinimized?: boolean): Promise<WindowInfo[]>;
|