fix(cua): report readable argument errors and clarify coordinate targets

This commit is contained in:
creatixchu committed 2026-10-14 13:32:36 +08:00
1 parent 7bfefd3118
commit b01b889632
10 files changed
+55 -9

No files matched your search

+1 -1
View File
@@ -184,7 +184,7 @@ App and Window methods:
Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. For click and hover, coordinate targets must be [x, y], for example hover([650, 400], {relative:true}). A bare number such as hover(650) is always an AX element index. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Hover first uses advertised AX alternate UI without moving the system cursor, preserving revealed descendants until the pointer target leaves their region. When unavailable, it moves the visible system cursor without pressing a button; that route requires the target point to be unobscured in the foreground app. FOREGROUND_REQUIRED requires explicit activation or raising and a fresh observation before retrying. Returning, AX reads and screenshots do not restore the previous point. Wait for the application's hover delay, observe, then hover into the revealed controls before clicking. Use successive hover calls for intermediate points. The result reports hoverRoute and systemPointerMoved. Other input or application changes can interrupt the interaction; app objects do not reserve applications between calls.
+1 -1
View File
@@ -184,7 +184,7 @@ App and Window methods:
Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. For click and hover, coordinate targets must be [x, y], for example hover([650, 400], {relative:true}). A bare number such as hover(650) is always an AX element index. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Hover first uses advertised AX alternate UI without moving the system cursor, preserving revealed descendants until the pointer target leaves their region. When unavailable, it moves the visible system cursor without pressing a button; that route requires the target point to be unobscured in the foreground app. FOREGROUND_REQUIRED requires explicit activation or raising and a fresh observation before retrying. Returning, AX reads and screenshots do not restore the previous point. Wait for the application's hover delay, observe, then hover into the revealed controls before clicking. Use successive hover calls for intermediate points. The result reports hoverRoute and systemPointerMoved. Other input or application changes can interrupt the interaction; app objects do not reserve applications between calls.
@@ -37,7 +37,7 @@ App and Window methods:
Raw operation tools use { method, params }, including params: {} for parameterless methods. getApp selects without activating the app. Keep applications in the background for observation and routine input. Do not activate or raise an app during initialization, before each action, or merely because another app is foreground. Prefer advertised AX actions. If a window is hidden or minimized, try app.show() or window.show() and read fresh state. show requests unhide and unminimize without activation, raising or switching Spaces; it chooses the explicit, focused, main or sole AX window. A null windowId means no window was selected for restoration. foregroundChanged compares the front app and top window before and after the request; null means unavailable, and delayed focus changes remain possible. If restoration is unsupported or the window remains unusable, including another Space or Stage Manager, explain the needed foreground step, use app.activate() or an advertised raise action, and read fresh state. Completing the task takes priority when foreground access is necessary. Avoid repeated activation when the user switches away; continue background-capable steps and report what requires foreground access. Applications may still change focus in response to input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Await operations sequentially. After input, read fresh state to verify the effect. A successful action means dispatched, not verified completion. Cancellation may leave partial effects; never retry uncertain input without observing. STALE_APP requires getApp again; STALE_SNAPSHOT requires getState again. Absolute coordinate input requires a fresh screenshot of that window. Relative input can use a successful AX observation or screenshot. SCREENSHOT_REQUIRED means capture a fresh screenshot before using pixel coordinates. For click and hover, coordinate targets must be [x, y], for example hover([650, 400], {relative:true}). A bare number such as hover(650) is always an AX element index. The relative option applies only to coordinate targets, not AX indices. Window resizing since the latest observation rejects coordinate input; movement alone preserves window-local coordinates. WINDOW_CHANGED requires fresh state before any retry; use an advertised AX action or explicitly restore the window before screenshot or coordinate input, then observe again. PERMISSION_DENIED requires the user to grant macOS Accessibility or Screen Recording access in System Settings. After granting access, observe fresh state before continuing; do not automatically replay prior input.
Hover first uses advertised AX alternate UI without moving the system cursor, preserving revealed descendants until the pointer target leaves their region. When unavailable, it moves the visible system cursor without pressing a button; that route requires the target point to be unobscured in the foreground app. FOREGROUND_REQUIRED requires explicit activation or raising and a fresh observation before retrying. Returning, AX reads and screenshots do not restore the previous point. Wait for the application's hover delay, observe, then hover into the revealed controls before clicking. Use successive hover calls for intermediate points. The result reports hoverRoute and systemPointerMoved. Other input or application changes can interrupt the interaction; app objects do not reserve applications between calls.
@@ -78,6 +78,9 @@ it.skipIf(process.platform === 'win32' || !existsSync(bridge) || !existsSync(boo
expect(first.value).toEqual({ mimeType: 'image/png', pixelWidth: 1, pixelHeight: 1, windowId: 7, byteLength: 90 })
expect(images).toBe(1)
expect(JSON.stringify(first)).not.toContain('iVBORw0KGgo')
const invalid = await run('await cua.findApps("");', 'invalid')
expect(invalid.error?.message).toBe('findApps query: Expected a non-empty search string')
expect(calls.filter(call => call.phase === 'invalid')).toEqual([])
const second = await run('await win.getAXState({emit:false}); await win.click(0); await win.getAXState({emit:false});', 'second')
expect(second.error).toBeUndefined()
expect(second.value).toContain('Count: 2')
+2 -2
View File
@@ -15,8 +15,8 @@
en: d152484eb41ac6b4
zh: 1db86285abf6c499
/deepseek-ai-dsh-cua/use-this-package:
en: 4672d9e873336c60
zh: 034fa7a7528a1d8b
en: 52cacbe2d2aeb2bc
zh: b3c65814d9a17052
/deepseek-ai-dsh-cua/understand-the-implementation:
en: effcee57d8854c32
zh: d02c6b76e7249051
+1 -1
View File
@@ -27,7 +27,7 @@ Inspect applications, retain window objects across PTC calls, and send actions t
Mount the [macOS provider](../cua-macos/README.md) to enable native operations. External PTC hosts import `connectCua` from `@deepseek-ai/dsh-cua/bridge` and supply the current `tools.cua` binding. Source compositions and native preparation belong to the provider.
Use App methods for a single-window task. `getApp` reads initial accessibility state; `getWindows` and `getWindow` select an explicit window when needed. Re-reading one app/window scope replaces its element indices without invalidating another window's snapshot. Keyboard input follows the application's focus.
Use App methods for a single-window task. `getApp` reads initial accessibility state; `getWindows` and `getWindow` select an explicit window when needed. Re-reading one app/window scope replaces its element indices without invalidating another window's snapshot. Keyboard input follows the application's focus. Invalid request arguments reject before dispatch with a readable method, parameter and requirement.
Observation methods emit and return their result. Use `void await app.getAXState()` for one visible copy, or use `emit:false` and print the returned text once. Screenshots return `CuaScreenshot` with `bytes`, `mimeType`, `pixelWidth`, `pixelHeight`, and `windowId`. The image sink receives the same object. Use its MIME type when selecting a file extension or declaring an image; the format can be PNG, JPEG, WebP, or GIF. Each screenshot enters the attachment pipeline once; `emit:false` only suppresses the object sink.
+1 -1
View File
@@ -27,7 +27,7 @@ kind: "package-library"
挂载 [macOS 提供方](../cua-macos/README.zh.md)以启用原生操作。外部 PTC 宿主从 `@deepseek-ai/dsh-cua/bridge` 导入 `connectCua`,并提供当前 `tools.cua` 绑定。源码组合与原生组件准备由提供方负责。
单窗口任务直接使用 App 方法。`getApp` 读取初始辅助功能状态;需要指定窗口时使用 `getWindows` 和 `getWindow`。重新读取一个应用或窗口范围会替换其中的元素编号,不使其他窗口的快照失效。键盘输入跟随应用焦点。
单窗口任务直接使用 App 方法。`getApp` 读取初始辅助功能状态;需要指定窗口时使用 `getWindows` 和 `getWindow`。重新读取一个应用或窗口范围会替换其中的元素编号,不使其他窗口的快照失效。键盘输入跟随应用焦点。请求参数校验失败时,在分派前返回包含方法、参数和要求的可读消息。
观察方法会输出并返回结果。使用 `void await app.getAXState()` 只展示一份内容,或使用 `emit:false` 后打印一次返回文本。截图返回包含 `bytes`、`mimeType`、`pixelWidth`、`pixelHeight` 和 `windowId` 的 `CuaScreenshot`,图片输出回调接收同一个对象。选择文件后缀或声明图片时使用其中的 MIME 类型;格式可能是 PNG、JPEG、WebP 或 GIF。每张截图只进入一次附件处理链;`emit:false` 只抑制对象层的输出。
+23 -1
View File
@@ -1,10 +1,28 @@
/** Object facade over logged native operations; its caller owns evaluator persistence. */
import { Buffer } from 'node:buffer'
import type { ZodIssue } from 'zod'
import type { JsonValue } from '@deepseek-ai/dsh-util-values'
import { ActionResult, AppResult, AppsResult, CapabilitiesResult, FoundAppsResult, HoverResult, Request, ScreenshotResult, ShowResult, StateResult, WindowsResult } from './protocol.ts'
import type { CuaApi, CuaApp, CuaAppHandle, CuaDispatch, CuaObservationOptions, CuaObservationSink, CuaOperation, CuaScreenshot, CuaSurface, CuaWindow } from './types.ts'
export type * from './types.ts'
function requestIssueMessage(issues: readonly ZodIssue[], parent: readonly PropertyKey[] = []): string {
return [...new Set(issues.map((issue) => {
const path = [...parent, ...issue.path]
if (issue.code === 'invalid_union') {
const fewest = Math.min(...issue.errors.map(branch => branch.length))
return [...new Set(issue.errors.filter(branch => branch.length === fewest)
.map(branch => requestIssueMessage(branch, path)))].join(' or ')
}
// The facade supplies the method and wrapper; failures belong to params.
const fields = path.slice(1)
if (issue.code === 'unrecognized_keys') {
return `${issue.keys.map(key => [...fields, key].join('.')).join(', ')}: not accepted for these arguments`
}
return `${fields.join('.')}: ${issue.message}`
}))].join('; ')
}
function keyChord(chord: string): { key: string; modifiers: string[] } {
const parts = chord.split('+')
let key = parts.pop()
@@ -33,7 +51,11 @@ function keyChord(chord: string): { key: string; modifiers: string[] } {
* @returns Objects independent of variable storage and evaluator lifecycle.
*/
export function createCuaApi(dispatch: CuaDispatch, sink?: CuaObservationSink): CuaApi {
const call = (method: CuaOperation['method'], params: Record<string, JsonValue>) => dispatch(Request.parse({ method, params }))
const call = (method: CuaOperation['method'], params: Record<string, JsonValue>) => {
const request = Request.safeParse({ method, params })
if (!request.success) throw new Error(`${method} ${requestIssueMessage(request.error.issues)}`)
return dispatch(request.data)
}
const action = async (method: CuaOperation['method'], params: Record<string, JsonValue>) => ActionResult.parse(await call(method, params))
async function emit(value: object, options?: CuaObservationOptions): Promise<void> {
if (options?.emit !== false) await sink?.text(JSON.stringify(value))
+2 -1
View File
@@ -29,7 +29,8 @@ const imageBase64 = z.string().min(1).refine(value => Buffer.from(value, 'base64
/** Model-submittable operation parameters; Host resource fields and private continuations are separate. */
export const Request = z.discriminatedUnion('method', [
operation('getCapabilities', {}), operation('listApps', {}), operation('findApps', { query: z.string().min(1).regex(/\S/) }),
operation('getCapabilities', {}), operation('listApps', {}),
operation('findApps', { query: z.string().min(1, 'Expected a non-empty search string').regex(/\S/, 'Expected a non-empty search string') }),
operation('getApp', { selector: z.string().min(1) }),
operation('activate', app),
operation('show', { ...app, windowId: id.optional() }),
@@ -94,6 +94,12 @@ describe('application discovery', () => {
await expect(f.api.findApps(' ')).rejects.toThrow()
})
it.each(['', ' '])('names the invalid search parameter without exposing schema internals: %j', async (query) => {
const f = fixture()
await expect(f.api.findApps(query)).rejects.toThrow('findApps query: Expected a non-empty search string')
expect(f.dispatch).not.toHaveBeenCalled()
})
it('rejects malformed discovery and capability results', async () => {
const f = fixture()
f.dispatch.mockResolvedValueOnce({ platform: 'macos' })
@@ -467,6 +473,20 @@ describe('input actions', () => {
['extra plus delimiter', app => app.pressKey('cmd+++')],
['unknown modifier', app => app.pressKey('unknown+a')],
]
it.each<[string, (app: CuaApp) => Promise<unknown>]>([
['getState windowId: Too small: expected number to be >0', app => app.getWindow(0)],
['selectText text: Too small: expected string to have >=1 characters', app => app.selectText(0, '')],
['hover x: Invalid input: expected number, received NaN', app => app.hover([NaN, 0])],
['hover x: Too big: expected number to be <=1000', app => app.hover([1001, 0], { relative: true })],
['click relative: not accepted for these arguments', app => app.click(650, { relative: true })],
])('reports a readable validation failure: %s', async (message, run) => {
const f = fixture()
const app = await f.api.getApp('Editor')
f.dispatch.mockClear()
await expect(run(app)).rejects.toThrow(message)
expect(f.dispatch).not.toHaveBeenCalled()
})
it.each(invalidActions)('rejects %s without invoking native input', async (_label, run) => {
const f = fixture()
const app = await f.api.getApp('Editor')