Skip to content

Commit a3ed5f8

Browse files
authored
Merge pull request #353 from Integration-Automation/feat/locator-chain-batch
Add composable / filtered candidate locators (chained locator)
2 parents 4c9a2b5 + 3d392f1 commit a3ed5f8

15 files changed

Lines changed: 366 additions & 1 deletion

File tree

README/WHATS_NEW_zh-CN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 可串接 / 可过滤的候选定位器
4+
5+
用链式调用细化已定位的元素:`.within(panel).filter(has_text="Delete").nth(1)`。完整参考:[`docs/source/Zh/doc/new_features/v143_features_doc.rst`](../docs/source/Zh/doc/new_features/v143_features_doc.rst)
6+
7+
- **`from_boxes` / `Candidates`**(`AC_locate_chain`):`anchor_locator` 是单一关系、`grid_locator` 是单元格——两者都不支援对候选集合做可组合细化(Selenium-4 / Playwright 的链式定位惯用法)。本功能是对来自*任何*来源(模板 / OCR / a11y / `fuse_elements`)的框做纯后置过滤:`within`(区域裁切)、`filter`(`has_text` / `near` / 面积 / predicate)、`sort_reading``nth` / `first` / `last``resolve()` / `center()`。每个方法返回新的 `Candidates`(不变动)→ 完全无头可测。执行器命令套用 JSON `ops` 列表。
8+
39
## 本次更新 (2026-06-23) — 重试式数值断言(expect.poll)
410

511
重试*任意*值直到符合,不只限内建检查。完整参考:[`docs/source/Zh/doc/new_features/v142_features_doc.rst`](../docs/source/Zh/doc/new_features/v142_features_doc.rst)

README/WHATS_NEW_zh-TW.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 本次更新 — AutoControl
22

3+
## 本次更新 (2026-06-23) — 可串接 / 可過濾的候選定位器
4+
5+
用鏈式呼叫細化已定位的元素:`.within(panel).filter(has_text="Delete").nth(1)`。完整參考:[`docs/source/Zh/doc/new_features/v143_features_doc.rst`](../docs/source/Zh/doc/new_features/v143_features_doc.rst)
6+
7+
- **`from_boxes` / `Candidates`**(`AC_locate_chain`):`anchor_locator` 是單一關係、`grid_locator` 是儲存格——兩者都不支援對候選集合做可組合細化(Selenium-4 / Playwright 的串接定位慣用法)。本功能是對來自*任何*來源(模板 / OCR / a11y / `fuse_elements`)的框做純後置過濾:`within`(區域裁切)、`filter`(`has_text` / `near` / 面積 / predicate)、`sort_reading``nth` / `first` / `last``resolve()` / `center()`。每個方法回傳新的 `Candidates`(不變動)→ 完全無頭可測。執行器命令套用 JSON `ops` 清單。
8+
39
## 本次更新 (2026-06-23) — 重試式數值斷言(expect.poll)
410

511
重試*任意*值直到符合,不只限內建檢查。完整參考:[`docs/source/Zh/doc/new_features/v142_features_doc.rst`](../docs/source/Zh/doc/new_features/v142_features_doc.rst)

WHATS_NEW.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# What's New — AutoControl
22

3+
## What's new (2026-06-23) — Composable / Filtered Candidate Locators
4+
5+
Refine located elements with a chain: `.within(panel).filter(has_text="Delete").nth(1)`. Full reference: [`docs/source/Eng/doc/new_features/v143_features_doc.rst`](docs/source/Eng/doc/new_features/v143_features_doc.rst).
6+
7+
- **`from_boxes` / `Candidates`** (`AC_locate_chain`): `anchor_locator` is a single relation and `grid_locator` is cells — neither supports composable refinement of a candidate set (the Selenium-4 / Playwright chained-locator idiom). This is a pure post-filter over boxes from *any* source (template / OCR / a11y / `fuse_elements`): `within` (region clip), `filter` (`has_text` / `near` / area / predicate), `sort_reading`, `nth` / `first` / `last`, `resolve()` / `center()`. Every method returns a new `Candidates` (no mutation) → fully headless-testable. The executor command applies a JSON `ops` list.
8+
39
## What's new (2026-06-23) — Retrying Value Assertions (expect.poll)
410

511
Retry *any* value until it matches, not just the built-in checks. Full reference: [`docs/source/Eng/doc/new_features/v142_features_doc.rst`](docs/source/Eng/doc/new_features/v142_features_doc.rst).
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Composable / Filtered Candidate Locators
2+
=========================================
3+
4+
``anchor_locator`` resolves a single anchor→target relation and ``grid_locator``
5+
addresses grid cells; neither supports *composable refinement* of a candidate set —
6+
``.within(panel).filter(has_text="Delete").nth(2)`` — the Selenium-4 / Playwright
7+
chained-and-filtered locator idiom. Today refining means re-querying a backend. This is
8+
a pure post-filter over boxes from *any* source (template match, OCR, the a11y tree,
9+
:doc:`v138_features_doc`).
10+
11+
A ``Candidates`` wraps a list of ``{x, y, width, height, …}`` boxes; every method
12+
returns a *new* ``Candidates`` so chains are side-effect-free and fully unit-testable.
13+
Pure-stdlib, imports no ``PySide6``.
14+
15+
Headless API
16+
------------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import from_boxes
21+
22+
target = (from_boxes(boxes) # boxes from any locator
23+
.within((0, 0, 1920, 120)) # only the toolbar
24+
.filter(has_text="Delete") # only delete buttons
25+
.sort_reading() # left-to-right
26+
.nth(1)) # the second one
27+
if target.center():
28+
click(*target.center())
29+
30+
``within(region)`` keeps boxes whose centre is inside the rectangle; ``filter`` keeps
31+
boxes matching every supplied criterion (``has_text`` substring, ``near`` ``(x, y,
32+
max_dist)`` proximity, ``min_area`` / ``max_area``, or an arbitrary ``predicate``);
33+
``sort_reading`` orders them; ``nth`` / ``first`` / ``last`` select; ``resolve()``
34+
returns the surviving list and ``center()`` the first box's centre. Chains never mutate
35+
the original set.
36+
37+
Executor command
38+
----------------
39+
40+
``AC_locate_chain`` applies a JSON list of ``ops`` to a ``boxes`` array in order —
41+
``{op:"within",region:[…]}``, ``{op:"filter",has_text:…}``, ``{op:"reading"}``,
42+
``{op:"nth",index:…}``, ``{op:"first"}``, ``{op:"last"}`` — returning
43+
``{count, boxes, center}``. It is exposed as the MCP tool ``ac_locate_chain`` and as a
44+
Script Builder command under **Image**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ Comprehensive guides for all AutoControl features.
165165
doc/new_features/v140_features_doc
166166
doc/new_features/v141_features_doc
167167
doc/new_features/v142_features_doc
168+
doc/new_features/v143_features_doc
168169
doc/ocr_backends/ocr_backends_doc
169170
doc/observability/observability_doc
170171
doc/operations_layer/operations_layer_doc
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
可串接 / 可過濾的候選定位器
2+
============================
3+
4+
``anchor_locator`` 解析單一錨點→目標關係,``grid_locator`` 定址網格儲存格;兩者都不支援對候選集合做*可組合的細化*
5+
——``.within(panel).filter(has_text="Delete").nth(2)``——這是 Selenium-4 / Playwright 的串接並過濾定位慣用法。
6+
目前細化得重新查詢後端。本功能是對來自*任何*來源(模板比對、OCR、a11y 樹、:doc:`v138_features_doc`)的框做純
7+
後置過濾。
8+
9+
``Candidates`` 包裝一串 ``{x, y, width, height, …}`` 框;每個方法都回傳*新的* ``Candidates``,因此鏈式呼叫無副作用
10+
且完全可單元測試。純標準函式庫,不匯入 ``PySide6``。
11+
12+
無頭 API
13+
--------
14+
15+
.. code-block:: python
16+
17+
from je_auto_control import from_boxes
18+
19+
target = (from_boxes(boxes) # 來自任何定位器的框
20+
.within((0, 0, 1920, 120)) # 只取工具列
21+
.filter(has_text="Delete") # 只取刪除按鈕
22+
.sort_reading() # 由左到右
23+
.nth(1)) # 第二個
24+
if target.center():
25+
click(*target.center())
26+
27+
``within(region)`` 保留中心落在矩形內的框;``filter`` 保留符合所有條件的框(``has_text`` 子字串、``near``
28+
``(x, y, max_dist)`` 鄰近、``min_area`` / ``max_area``,或任意 ``predicate``);``sort_reading`` 排序;
29+
``nth`` / ``first`` / ``last`` 選取;``resolve()`` 回傳存活清單、``center()`` 回傳第一個框的中心。鏈式呼叫永不
30+
變動原集合。
31+
32+
執行器命令
33+
----------
34+
35+
``AC_locate_chain`` 對 ``boxes`` 陣列依序套用一串 JSON ``ops``——``{op:"within",region:[…]}``、
36+
``{op:"filter",has_text:…}``、``{op:"reading"}``、``{op:"nth",index:…}``、``{op:"first"}``、``{op:"last"}``——
37+
回傳 ``{count, boxes, center}``。它以 MCP 工具 ``ac_locate_chain`` 以及 Script Builder 中 **Image** 分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ AutoControl 所有功能的完整使用指南。
165165
doc/new_features/v140_features_doc
166166
doc/new_features/v141_features_doc
167167
doc/new_features/v142_features_doc
168+
doc/new_features/v143_features_doc
168169
doc/ocr_backends/ocr_backends_doc
169170
doc/observability/observability_doc
170171
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@
333333
PollResult, assert_poll, expect_poll, to_be_greater_than, to_be_stable,
334334
to_be_truthy, to_contain, to_equal, to_match_regex,
335335
)
336+
# Composable / filtered candidate locators (chained-locator idiom)
337+
from je_auto_control.utils.locator_chain import Candidates, from_boxes
336338
# CI workflow annotations (GitHub Actions)
337339
from je_auto_control.utils.ci_annotations import (
338340
emit_annotations, format_annotation,
@@ -1183,6 +1185,8 @@ def start_autocontrol_gui(*args, **kwargs):
11831185
"to_match_regex",
11841186
"to_be_truthy",
11851187
"to_be_stable",
1188+
"Candidates",
1189+
"from_boxes",
11861190
"emit_annotations", "format_annotation",
11871191
"ClipboardHistory", "default_clipboard_history",
11881192
"analyze_heal_log", "heal_stats", "scan_secrets",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,16 @@ def _add_image_specs(specs: List[CommandSpec]) -> None:
492492
),
493493
description="Order element boxes top-to-bottom, left-to-right (+ index).",
494494
))
495+
specs.append(CommandSpec(
496+
"AC_locate_chain", "Image", "Locate Chain (refine boxes)",
497+
fields=(
498+
FieldSpec("boxes", FieldType.STRING,
499+
placeholder='[{"x":..,"y":..,"width":..,"height":..}]'),
500+
FieldSpec("ops", FieldType.STRING,
501+
placeholder='[{"op":"filter","has_text":"OK"},{"op":"first"}]'),
502+
),
503+
description="Refine candidate boxes (within / filter / reading / nth / …).",
504+
))
495505

496506

497507
def _add_ocr_specs(specs: List[CommandSpec]) -> None:

je_auto_control/utils/executor/action_executor.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3625,6 +3625,42 @@ def getter():
36253625
"waited_s": result.waited_s}
36263626

36273627

3628+
def _apply_locate_op(candidates, op: Dict[str, Any]):
3629+
"""Apply one locate-chain op spec to a Candidates set."""
3630+
name = op.get("op")
3631+
if name == "within":
3632+
return candidates.within(op["region"])
3633+
if name == "filter":
3634+
return candidates.filter(has_text=op.get("has_text"), near=op.get("near"),
3635+
min_area=op.get("min_area"),
3636+
max_area=op.get("max_area"))
3637+
if name == "reading":
3638+
return candidates.sort_reading(row_tol=int(op.get("row_tol", 12)))
3639+
if name == "nth":
3640+
return candidates.nth(int(op["index"]))
3641+
if name == "first":
3642+
return candidates.first()
3643+
if name == "last":
3644+
return candidates.last()
3645+
raise AutoControlActionException(f"unknown locate-chain op: {name!r}")
3646+
3647+
3648+
def _locate_chain(boxes: Any, ops: Any = None) -> Dict[str, Any]:
3649+
"""Adapter: apply a chain of refinement ops to a set of element boxes."""
3650+
import json
3651+
from je_auto_control.utils.locator_chain import from_boxes
3652+
if isinstance(boxes, str):
3653+
boxes = json.loads(boxes)
3654+
if isinstance(ops, str):
3655+
ops = json.loads(ops) if ops.strip() else []
3656+
candidates = from_boxes(list(boxes))
3657+
for op in ops or ():
3658+
candidates = _apply_locate_op(candidates, op)
3659+
resolved = candidates.resolve()
3660+
return {"count": len(resolved), "boxes": resolved,
3661+
"center": candidates.center()}
3662+
3663+
36283664
def _with_modifiers(modifiers: Any, actions: Any) -> Dict[str, Any]:
36293665
"""Adapter: run nested actions while modifier keys are held down."""
36303666
import json
@@ -5365,6 +5401,7 @@ def __init__(self):
53655401
"AC_find_grid": _find_grid,
53665402
"AC_find_separators": _find_separators,
53675403
"AC_expect_poll": _expect_poll,
5404+
"AC_locate_chain": _locate_chain,
53685405
"AC_tile_rect": _tile_rect,
53695406
"AC_grid_rects": _grid_rects,
53705407
"AC_cascade_rects": _cascade_rects,

0 commit comments

Comments
 (0)