Skip to content

Commit 8ecbf98

Browse files
committed
Fail the run command on failed actions and fix hotkey key codes
run exited 0 whenever the file loaded; the executor now counts the failures it records and run exits 1 on any. A recording that could not start wrote [] over the output file, and a second recording leaked the first input hook. Punctuation hotkeys registered unrelated virtual keys, combos Windows cannot take were accepted and retried every tick, and a history or executor error ended the hotkey listener. The legacy entry point reports a missing file or directory without a traceback.
1 parent 360d2a8 commit 8ecbf98

17 files changed

Lines changed: 347 additions & 46 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's
2828

2929
### Changed
3030

31+
- **`je_auto_control run` exits 1 when any action failed.** It used to exit
32+
0 whenever the file loaded. Migration: a pipeline that relied on the old
33+
status can ignore it (`|| true`).
34+
3135
- **Chat-ops `/screenshot` takes a file name, not a path.** The PNG is
3236
written into the router context's `screenshot_dir` (default: a
3337
`je_auto_control_chatops` folder in the temp directory); anyone in the
@@ -146,6 +150,17 @@ it shipped into a `## [x.y.z] - date` section of their own; the tag's
146150

147151
### Fixed
148152

153+
- **Recording and hotkeys.** A recording that cannot start no longer
154+
replaces the output file with `[]`; starting a second recording stops the
155+
first input hook instead of leaking it. Hotkeys on punctuation keys
156+
(`ctrl+.`, `ctrl+[`) register those keys rather than Delete or the Windows
157+
key; a combo Windows cannot register is refused by `bind` and no longer
158+
retried 20 times a second; and an error in the run history or an injected
159+
executor no longer ends the hotkey listener.
160+
- **Legacy `python -m je_auto_control`.** A missing or invalid action file
161+
is reported as a log line instead of a traceback (the exit status was
162+
already 1), and a `-d` path that is not a directory is an error.
163+
149164
- **JSON-file stores.** The flaky-test quarantine, the remote-desktop trust
150165
list and known hosts, and the RBAC user store are replaced atomically
151166
(readers never see a partial file) and load empty instead of raising when

‎README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ je_auto_control version
196196
```
197197

198198
`--var name=value` is parsed as JSON when possible (`count=10` becomes an int),
199-
otherwise kept as a string. The legacy `python -m je_auto_control -e file.json`
200-
entry point still works.
199+
otherwise kept as a string. `run` exits 1 when any action failed (the run
200+
still goes on to the end), so a CI step fails with it. The legacy
201+
`python -m je_auto_control -e file.json` entry point still works.
201202

202203
---
203204

‎README/README_zh-CN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ je_auto_control version
183183
```
184184

185185
`--var name=value` 会尽量以 JSON 解析(`count=10` 会变成整数),否则视为字符串。
186-
旧版 `python -m je_auto_control -e file.json` 入口仍然可用。
186+
`run` 只要有任何动作失败就以 1 退出(仍会跑完整份脚本),CI 步骤会随之失败。旧版 `python -m je_auto_control -e file.json` 入口仍然可用。
187187

188188
---
189189

‎README/README_zh-TW.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ je_auto_control version
183183
```
184184

185185
`--var name=value` 會盡量以 JSON 解析(`count=10` 會變成整數),否則視為字串。
186-
舊版 `python -m je_auto_control -e file.json` 進入點仍然可用。
186+
`run` 只要有任何動作失敗就以 1 結束(仍會跑完整份腳本),CI 步驟會跟著失敗。舊版 `python -m je_auto_control -e file.json` 進入點仍然可用。
187187

188188
---
189189

‎architecture.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ entry points → execution core (`utils/executor/`) → headless capabilities (`
4444
| Python facade | `import je_auto_control` | Broad historical surface (`__all__`). |
4545
| Stable API | `je_auto_control.api` → `api/core.py` | `execute_action`, `execute_action_with_vars`, `generate_code`, `run_diagnostics`, `create_failure_bundle`, `failure_bundle_on_error`, `FailureBundleOptions`. |
4646
| Main CLI | `je_auto_control` → `je_auto_control.cli:main` | Subcommands `run` (`--var`, `--dry-run`), `validate` / `lint`, `fmt`, `list-commands`, `record`, `codegen`, `failure-bundle`, `list-jobs`, `start-server`, `start-rest`, `version`. |
47-
| Legacy CLI | `python -m je_auto_control` (`__main__.py`) | `-e/--execute_file FILE`, `-d/--execute_dir DIR`, `-c/--create_project PATH`, `--execute_str JSON`. `--execute_str` also accepts a double-encoded JSON string. |
47+
| Legacy CLI | `python -m je_auto_control` (`__main__.py`) | `-e/--execute_file FILE`, `-d/--execute_dir DIR`, `-c/--create_project PATH`, `--execute_str JSON`. `--execute_str` also accepts a double-encoded JSON string. Any error exits 1 with a log line rather than a traceback, and a `-d` path that is not a directory is an error. |
4848
| MCP server | `je_auto_control_mcp` → `utils/mcp_server/__main__.py:main` | stdio; `start_mcp_stdio_server()`; HTTP transport via the `AC_start_mcp_http_server` command. |
4949
| REST API | `je_auto_control start-rest`, `python -m je_auto_control.utils.rest_api`, `start_rest_api_server()` | Default `127.0.0.1:9939`, bearer token + rate limit. |
5050
| TCP server | `je_auto_control start-server`, `start_autocontrol_socket_server()` | `utils/socket_server/auto_control_socket_server.py`, default `127.0.0.1:9938`, JSON action lists. |

‎architecture_explore.md‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ iOS(WebDriverAgent)。核心能力是滑鼠/鍵盤控制、影像辨識、
2020
| 指標 | 數值 |
2121
| --- | ---: |
2222
| Python 模組總數(含周邊子專案) | 1,045 |
23-
| 程式碼總行數 | 142,940 |
23+
| 程式碼總行數 | 143,045 |
2424
| `je_auto_control/utils/` 子套件數 | 310 |
2525
| `AC_*` 動作指令數(`known_commands()` 實測) | 773 |
2626
| 套件門面 `__all__` 公開名稱數 | 1,239 |
@@ -155,8 +155,8 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
155155
| 模組 | 行數 | 職責 |
156156
| --- | ---: | --- |
157157
| `je_auto_control/__init__.py` | 1,970 | **套件門面**。集中匯入並再匯出 1,200 個公開名稱,以功能區塊註解分段(callback/exception/executor/a11y/vision/clipboard…)。 |
158-
| `je_auto_control/__main__.py` | 74 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 |
159-
| `je_auto_control/cli.py` | 323 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 |
158+
| `je_auto_control/__main__.py` | 83 | 舊版 argparse 進入點:`-e` 執行單檔、`-d` 執行整個目錄、`--execute_str` 執行 JSON 字串、`-c` 建立專案。 |
159+
| `je_auto_control/cli.py` | 335 | **主 CLI**(`je_auto_control` console script)。子命令:`run`(含 `--var`/`--dry-run`)、`validate`/`lint`、`list-commands`、`fmt`、`record`、`codegen`、`failure-bundle`、`list-jobs`、`start-server`、`start-rest`、`version`。所有子命令延遲匯入,確保不碰 Qt。 |
160160
| `je_auto_control/api/__init__.py` | 22 | 版本化整合進入點。 |
161161
| `je_auto_control/api/core.py` | 19 | **穩定無頭 API 門面**:只暴露 `execute_action`、`execute_action_with_vars`、`generate_code`、`run_diagnostics`、`create_failure_bundle`、`failure_bundle_on_error`、`FailureBundleOptions`。mypy 型別契約以此為起點,現已擴到整包(見「設定基線」)。 |
162162
| `je_auto_control/utils/deprecation.py` | 35 | 公開 API 的一致性棄用警告。 |
@@ -179,7 +179,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
179179
| `wrapper/auto_control_keyboard.py` | 364 | 鍵盤 API:鍵表查詢、按下/放開/敲擊、`write` 字串、`hotkey` 組合鍵、按鍵狀態偵測。**`type_keyboard` 與 `hotkey` 的放開走 `finally`**(見下)。 |
180180
| `wrapper/auto_control_screen.py` | 111 | 螢幕 API:`screen_size`、`screenshot`(可指定區域)、`get_pixel`。 |
181181
| `wrapper/auto_control_image.py` | 83 | 影像 API:`locate_all_image`、`locate_image_center`、`locate_and_click`。 |
182-
| `wrapper/auto_control_record.py` | 116 | 錄製 API:`record`/`stop_record`/`record_to_json`(支援 stop event 與逾時)。 |
182+
| `wrapper/auto_control_record.py` | 124 | 錄製 API:`record`/`stop_record`/`record_to_json`(支援 stop event 與逾時)。 |
183183
| `wrapper/auto_control_window.py` | 278 | 視窗管理門面:列舉、尋找、聚焦、等待、關閉、顯示狀態、幾何、所屬行程 PID、依行程列舉/最小化視窗、不搶焦點的投遞式輸入(目前僅 Windows 實作)。 |
184184
| `wrapper/window_backends/` | 988 | 視窗管理的平台縫(`base` / `windows_backend` / `x11_backend` / `macos_backend` / `null_backend`)。放在 `wrapper/` 而不是 `utils/`,因為它必須 import `windows/`、`linux_with_x11/`、`osx/`,而 `utils/` 在分層上在那三者之上。 |
185185

@@ -270,7 +270,7 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
270270

271271
### 5.4.1 執行引擎與腳本資產
272272

273-
> 24 個套件、約 13,288 行。
273+
> 24 個套件、約 13,319 行。
274274
275275
| 模組 | 行數 | 職責 |
276276
| --- | ---: | --- |
@@ -281,9 +281,9 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
281281
| `utils/dag/` | 478 | 跨主機 DAG 編排器(圖模型 + runner) |
282282
| `utils/decision_table/` | 103 | DMN 風格決策表:規則 + 命中策略,把分支外部化 |
283283
| `utils/deterministic/` | 98 | 決定性執行控制:固定亂數種子 + 凍結時鐘 |
284-
| `utils/executor/` | 9,211 | **核心**。`Executor` 指令分派表(773 個 `AC_*`)、參數插值、乾跑、逐步 callback;`flow_control` 提供 34 個區塊指令(迴圈/分支/try/巨集/變數) |
284+
| `utils/executor/` | 9,238 | **核心**。`Executor` 指令分派表(773 個 `AC_*`)、參數插值、乾跑、逐步 callback;`flow_control` 提供 34 個區塊指令(迴圈/分支/try/巨集/變數) |
285285
| `utils/flow_debugger/` | 142 | action list 的單步除錯器與追蹤器 |
286-
| `utils/input_macro/` | 355 | 定時輸入事件:錄製結果的整形(`timeline`/`InputRecorder`,Windows 與 macOS 共用)、重播與宣告式輸入序列 DSL |
286+
| `utils/input_macro/` | 359 | 定時輸入事件:錄製結果的整形(`timeline`/`InputRecorder`,Windows 與 macOS 共用)、重播與宣告式輸入序列 DSL |
287287
| `utils/json/` | 94 | action JSON 檔讀寫與正規化格式化(`fmt --check` 的後端) |
288288
| `utils/json_store/` | 78 | JSON 字典檔持久化的共用小工具(內部管線) |
289289
| `utils/loop_guard/` | 140 | 機械式卡死迴圈偵測(agent loop 用) |
@@ -322,11 +322,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
322322

323323
### 5.4.3 排程、觸發與背景監看
324324

325-
> 11 個套件、約 3,663 行。
325+
> 11 個套件、約 3,708 行。
326326
327327
| 模組 | 行數 | 職責 |
328328
| --- | ---: | --- |
329-
| `utils/hotkey/` | 738 | 全域熱鍵守護行程,把 OS 層熱鍵綁到 action 檔(Win/macOS/X11 三後端) |
329+
| `utils/hotkey/` | 783 | 全域熱鍵守護行程,把 OS 層熱鍵綁到 action 檔(Win/macOS/X11 三後端) |
330330
| `utils/idle_keepawake/` | 212 | 偵測使用者閒置時間並在無人值守執行期間阻止系統睡眠 |
331331
| `utils/lock_session/` | 163 | 鎖定工作站、等待解鎖並分類鎖定狀態轉換 |
332332
| `utils/observer/` | 222 | 反應式畫面觀察者,在出現/消失/變化時觸發 |
@@ -694,11 +694,11 @@ socket server 有 8 MiB 讀取上限與 30 秒 handler timeout。
694694

695695
上表以子套件為單位;以下把行數最大的幾個子系統展開到檔案層。
696696

697-
#### `utils/executor/`(9,211 行)— 執行核心
697+
#### `utils/executor/`(9,238 行)— 執行核心
698698

699699
| 檔案 | 行數 | 職責 |
700700
| --- | ---: | --- |
701-
| `action_executor.py` | 8,163 | `Executor` 類別與 `event_dict` 分派表(773 個指令),另含數百個把 utils 能力接成指令的 adapter 函式;全域單例 `executor` 與 `add_command_to_executor()` 擴充點。 |
701+
| `action_executor.py` | 8,190 | `Executor` 類別與 `event_dict` 分派表(773 個指令),另含數百個把 utils 能力接成指令的 adapter 函式;全域單例 `executor` 與 `add_command_to_executor()` 擴充點。 |
702702
| `flow_control.py` | 573 | 真正的流程控制:`AC_loop`/`AC_for_each`/`AC_while_*`/`AC_if_*`/`AC_try`/`AC_retry`/`AC_parallel`/`AC_define_macro`/`AC_call_macro`/變數指令(`AC_set_var`/`AC_get_var`/`AC_inc_var`)。`LoopBreak`/`LoopContinue` 以例外實作。34 個區塊指令的分派表 `BLOCK_COMMANDS` 也在這裡,含下一列匯入的資料來源指令。 |
703703
| `flow_data_commands.py` | 268 | `AC_*_to_var` 資料來源與轉換指令:shell、時鐘、亂數、PDF、TOTP、SQL、檔案、HTTP、OCR,加上 `AC_assert_var`/`AC_assert_db`/`AC_assert_duration`/`AC_transform_var`。都不執行巢狀 action list,所以沒有迴圈/分支語意。 |
704704
| `action_schema.py` | 128 | action list 的結構驗證:形狀、參數型別、未知指令拒絕。單一走訪同時支援兩種消費方式:`validate_actions()` 遇到第一個問題就拋、`unknown_command_names()` 收齊全部不認得的名字(REST `/execute` 用它回 400)。 |
@@ -1062,11 +1062,11 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
10621062
| `gui/` | 91 | 26,794 |
10631063
| `utils/mcp_server/` | 31 | 17,538 |
10641064
| `utils/remote_desktop/` | 56 | 12,216 |
1065-
| `utils/executor/` | 7 | 9,211 |
1065+
| `utils/executor/` | 7 | 9,238 |
10661066
| `utils/usb/` | 17 | 4,422 |
1067-
| `je_auto_control/`(頂層 3 檔) | 3 | 2,367 |
1067+
| `je_auto_control/`(頂層 3 檔) | 3 | 2,388 |
10681068
| `utils/accessibility/` | 13 | 2,842 |
1069-
| `wrapper/` | 19 | 3,551 |
1069+
| `wrapper/` | 19 | 3,559 |
10701070
| `windows/` | 23 | 1,951 |
10711071
| `utils/rest_api/` | 8 | 1,758 |
10721072
| `utils/agent/` | 8 | 1,252 |
@@ -1078,7 +1078,7 @@ socket 預設綁 `127.0.0.1`;資源一律用 `with`。
10781078
| `utils/assertion/` | 3 | 863 |
10791079
| `osx/` | 17 | 919 |
10801080
| `autocontrol-lsp/` | 8 | 744 |
1081-
| `utils/hotkey/` | 7 | 738 |
1082-
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 674 | 48,386 |
1083-
| **總計** | **1,039** | **142,875** |
1081+
| `utils/hotkey/` | 7 | 783 |
1082+
| 其餘模組(約 286 個 `utils/` 子套件 + `android/`/`ios/`/周邊小工具) | 674 | 48,390 |
1083+
| **總計** | **1,039** | **142,980** |
10841084

‎docs/updates/2026-09.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,3 +366,15 @@ Index and query commands: [README.md](README.md). New entries go at the end.
366366
- **Tests**: `test_json_store_audit.py` (new, 11). It does not import on the previous commit (`append_json_line` is new).
367367
- **Files**: `je_auto_control/utils/json_store/json_store.py`, `je_auto_control/utils/{quarantine/store,remote_desktop/trust_list,remote_desktop/fingerprint,rbac/users,ab_locator/store,cost_telemetry/store,self_healing/heal_log}.py`, `test/unit_test/headless/test_json_store_audit.py`, `Progress.md`, `CHANGELOG.md`, `architecture_explore.md` (figures).
368368
- **Open items**: the cross-process lock in `Progress.md`.
369+
370+
## U-20260923-27 · 2026-09-23 · CLI / hotkey / recording audit: exit codes, lost recordings, wrong virtual keys · #incident #cli #hotkey
371+
372+
- **What**: An audit of `cli.py`, the legacy `__main__.py`, the recording wrapper and recorder base, and `utils/hotkey`, with the platform input stubbed.
373+
- **CLI**: (1) `run` returned 0 even when actions failed — the executor records a failure and goes on — so CI could not tell a failed run from a clean one. The executor now counts, per thread, the failures it records (`reset_recorded_failures` / `recorded_failures` in `action_executor.py`; failures that propagate, e.g. inside `AC_try`, are left to whoever catches them), and `run` exits 1 with "N action(s) failed". (2) `failure-bundle --context '[1]'` ended in a `TypeError` traceback; a non-object context is an error message. (3) A recording whose hook could not start still wrote `[]` over the output file and reported "Recorded 0 action(s)" with status 0. `record()` returns whether it started and `record_to_json` raises instead of writing; a negative `--duration` is refused.
374+
- **Legacy entry point** (a cross-project contract; flags and the success exit code are unchanged): the `except` did not include `AutoControlException`, so a missing or invalid action file ended in a raw traceback — exit status 1 already, so only the output changes. A `-d` path that is not a directory ran nothing and exited 0; it is an error now. `architecture.md` §6 says both.
375+
- **Recording**: `InputRecorder._start` replaced a running hook without stopping it, leaving the first low-level hook and its message pump installed for the process's life (`AC_record` twice, or `record_to_json` after a stray `record()`); it stops the old one.
376+
- **Hotkeys**: (1) `_key_to_vk` used `ord()` for any single character, but a virtual-key code equals the code point only for A-Z and 0-9: `ctrl+.` registered VK_DELETE, `ctrl+[` VK_LWIN, `ctrl+,` VK_SNAPSHOT. The US-layout `VK_OEM_*` codes are mapped and other characters refused. (2) `bind` accepted combos `RegisterHotKey` cannot take (`ctrl+foo`); on Windows it validates with `parse_combo`. (3) The Windows backend retried a failed registration on every 50 ms tick, logging an error 20 times a second while another program owned the combo; a failed binding is retried only when its combo changes. (4) `_fire_binding` recorded the run in history outside its `try`, so a history failure ended the listener thread, and it caught a fixed tuple around an injected executor, so a `KeyError` did the same; history failures are logged and the executor call is a documented broad catch. `fired` is counted under the lock.
377+
- **Checked and fine**: `run`, `validate`, `fmt` and `codegen` report bad files cleanly; the callback executor keeps trigger and callback errors apart; recorded actions validate and replay; timeline gaps are clamped; the recording editor's operations bound-check and do not mutate their input.
378+
- **Tests**: `test_cli_hotkey_audit.py` (new, 21; 18 fail on the previous commit, the others guard a clean run and the A-Z / 0-9 mapping). `test_cli.py`'s record stub returns `True`, as `record()` now does.
379+
- **Files**: `je_auto_control/{cli,__main__}.py`, `je_auto_control/utils/executor/action_executor.py`, `je_auto_control/wrapper/auto_control_record.py`, `je_auto_control/utils/input_macro/recorder_base.py`, `je_auto_control/utils/hotkey/{hotkey_daemon,backends/windows_backend}.py`, `test/unit_test/headless/{test_cli_hotkey_audit,test_cli}.py`, `README.md`, `README/README_zh-{TW,CN}.md`, `architecture.md`, `CHANGELOG.md`, `architecture_explore.md` (figures).
380+
- **Open items**: none.

‎docs/updates/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here
5858

5959
| ID | Date | Title | Tags | Batch |
6060
|---|---|---|---|---|
61+
| U-20260923-27 | 2026-09-23 | CLI / hotkey / recording audit: exit codes, lost recordings, wrong virtual keys | #incident #cli #hotkey | [2026-09](2026-09.md) |
6162
| U-20260923-26 | 2026-09-23 | JSON store audit: in-place rewrites, constructor crashes, lost counts and log lines | #incident #stores | [2026-09](2026-09.md) |
6263
| U-20260923-25 | 2026-09-23 | SQLite store audit: stuck work items, dedupe race, leaked connections, audit chain | #incident #stores | [2026-09](2026-09.md) |
6364
| U-20260923-24 | 2026-09-23 | Outbound audit: credentials across redirects, egress bypass, chat-ops screenshot path | #incident #security #chatops | [2026-09](2026-09.md) |
@@ -131,7 +132,7 @@ In the same commit: delete the item from `Progress.md`, add a `#done` entry here
131132

132133
| File | Period | Entries |
133134
|---|---|---:|
134-
| [2026-09.md](2026-09.md) | 2026-09 | 42 |
135+
| [2026-09.md](2026-09.md) | 2026-09 | 43 |
135136
| [2026-08-f.md](2026-08-f.md) | 2026-08 | 1 |
136137
| [2026-08-e.md](2026-08-e.md) | 2026-08 | 2 |
137138
| [2026-08-d.md](2026-08-d.md) | 2026-08 | 2 |

0 commit comments

Comments
 (0)