Skip to content

Commit b322ae5

Browse files
authored
Merge pull request #430 from Integration-Automation/dev
Release: cross-app OS lane (shell_open, idle_keepawake, file_assoc)
2 parents ea0345a + 1e0eef8 commit b322ae5

21 files changed

Lines changed: 1461 additions & 197 deletions

File tree

WHATS_NEW.md

Lines changed: 229 additions & 197 deletions
Large diffs are not rendered by default.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
Open Files / URLs with the Default App
2+
======================================
3+
4+
The framework could launch a literal executable (``start_exe`` / ``shell_process``),
5+
but not the single most common "hand off to another app" RPA step: open
6+
``report.pdf`` with whatever app is registered for it, ``print`` a document, or
7+
open a URL in the default browser. ``shell_open`` adds that, routed per-OS to
8+
``os.startfile`` / ``open`` / ``xdg-open`` / ``webbrowser``.
9+
10+
* :func:`plan_open` — pure planner: classify the target (URL vs file path),
11+
validate it (URL scheme allow-list; ``realpath`` for files) and return the
12+
dispatch descriptor,
13+
* :func:`open_path` — run the plan through an injectable ``opener`` sink (the real
14+
OS call by default).
15+
16+
Pure stdlib; the dispatch logic is unit-testable without launching anything via
17+
the injectable ``opener``. Imports no ``PySide6``.
18+
19+
Headless API
20+
------------
21+
22+
.. code-block:: python
23+
24+
from je_auto_control import open_path, plan_open
25+
26+
open_path("report.pdf") # default PDF viewer
27+
open_path("invoice.pdf", verb="print") # print it
28+
open_path("https://example.com") # default browser
29+
30+
plan_open("https://example.com")
31+
# {"kind": "url", "scheme": "https", "target": "...", "backend": "webbrowser",
32+
# "verb": "open"}
33+
plan_open("report.pdf")
34+
# {"kind": "file", "target": "<realpath>", "backend": "startfile", ...}
35+
36+
A ``scheme://`` target (or ``mailto:`` / ``tel:``) is opened as a URL — only the
37+
allow-listed schemes (``http`` / ``https`` / ``ftp`` / ``file`` / ``mailto`` /
38+
``tel``) are accepted, anything else raises ``ValueError``. Everything else is a
39+
file path (a Windows drive like ``C:\\…`` is correctly treated as a path, not a
40+
scheme) and is ``realpath``-resolved. ``verb`` (``open`` / ``print`` / ``edit``)
41+
applies to files on Windows.
42+
43+
Executor commands
44+
-----------------
45+
46+
``AC_open_path`` (``target`` / ``verb`` → ``{opened}``) and ``AC_plan_open``
47+
(``target`` / ``verb`` → the plan). They are exposed as the matching ``ac_*`` MCP
48+
tools (``open_path`` side-effect-only, ``plan_open`` read-only) and as Script
49+
Builder commands under **Shell**.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
Idle Detection + Keep the Machine Awake
2+
=======================================
3+
4+
Long unattended automation runs get derailed two ways: the screensaver / power
5+
policy sleeps the box mid-run, or the run should hold while a human is actively
6+
using the machine. The framework had neither signal. ``idle_keepawake`` adds
7+
both, behind injectable seams so all logic is testable without touching the OS.
8+
9+
* :func:`idle_seconds` / :func:`is_idle` — seconds since the last user keyboard /
10+
mouse input (``GetLastInputInfo`` on Windows), through an injectable ``probe``.
11+
* :func:`plan_keep_awake` — pure planner describing which wake flags a request
12+
maps to.
13+
* :func:`keep_awake` — scoped context manager that keeps the machine awake for
14+
the duration of a ``with`` block, restoring the prior state on exit.
15+
* :func:`keep_awake_on` / :func:`allow_sleep` — a process-global on / off pair
16+
for JSON action flows.
17+
18+
All three keep-awake entry points apply the plan through an injectable ``driver``
19+
(``SetThreadExecutionState`` on Windows, ``caffeinate`` on macOS,
20+
``systemd-inhibit`` on Linux by default). Imports no ``PySide6``.
21+
22+
Headless API
23+
------------
24+
25+
.. code-block:: python
26+
27+
from je_auto_control import (
28+
idle_seconds, is_idle, keep_awake, keep_awake_on, allow_sleep,
29+
)
30+
31+
idle_seconds() # e.g. 3.4 — seconds since last input
32+
is_idle(300) # True once nobody has touched the machine for 5 min
33+
34+
# Scoped: keep awake only while a long step runs
35+
with keep_awake():
36+
run_long_batch()
37+
38+
# Flow-style: on at the start, off at the end
39+
keep_awake_on(display=True, system=True)
40+
try:
41+
run_long_batch()
42+
finally:
43+
allow_sleep()
44+
45+
:func:`is_idle` is the gate for "only run when the user has stepped away";
46+
:func:`keep_awake` / :func:`keep_awake_on` stop the display and system sleeping
47+
so an overnight run is not interrupted. ``display=False`` keeps the system awake
48+
but lets the screen blank (battery-friendly for headless boxes).
49+
50+
Executor commands
51+
-----------------
52+
53+
``AC_idle_seconds`` (→ ``{idle_seconds}``), ``AC_is_idle`` (``threshold`` →
54+
``{idle, idle_seconds}``), ``AC_plan_keep_awake`` (``display`` / ``system`` → the
55+
plan), ``AC_keep_awake_on`` (``display`` / ``system`` → the active plan) and
56+
``AC_allow_sleep`` (→ ``{released}``). They are exposed as the matching ``ac_*``
57+
MCP tools (reads read-only, keep-awake on/off side-effect-only) and as Script
58+
Builder commands under **Shell**. The :func:`keep_awake` context manager is the
59+
Python-API surface for scoped use.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Resolve the App Registered for a File Type
2+
==========================================
3+
4+
:func:`open_path` (``shell_open``) opens a file with whatever app is registered
5+
for it; ``file_assoc`` answers the inverse, read-only question — *which* app is
6+
that? Given ``report.pdf`` (or a bare ``.pdf`` / ``pdf``) it returns the
7+
registered executable, the friendly app name, the open command line and the MIME
8+
content type, via the Windows ``AssocQueryStringW`` shell API.
9+
10+
* :func:`normalize_ext` — pure helper turning a path / ``.ext`` / bare ``ext``
11+
into a lowercased ``.ext``,
12+
* :func:`file_association` — run the lookup through an injectable ``resolver``
13+
seam (the real shell API by default).
14+
15+
The assembly logic is unit-testable without Windows via the injectable
16+
``resolver``. Imports no ``PySide6``.
17+
18+
Headless API
19+
------------
20+
21+
.. code-block:: python
22+
23+
from je_auto_control import file_association, normalize_ext
24+
25+
normalize_ext("report.PDF") # ".pdf"
26+
normalize_ext("archive.tar.gz") # ".gz"
27+
28+
file_association("report.pdf")
29+
# {"ext": ".pdf", "command": "...AcroRd32.exe \"%1\"",
30+
# "exe": "...AcroRd32.exe", "friendly": "Adobe Acrobat",
31+
# "content_type": "application/pdf"}
32+
33+
The app fields are ``None`` when nothing is registered for the type. This is the
34+
natural companion to :func:`open_path`: ``file_association`` tells you *what*
35+
would open a file (assert "PDFs open in Acrobat, not the browser"), and
36+
``open_path`` actually opens it. The live lookup uses the Windows shell API; on
37+
other platforms pass your own ``resolver``.
38+
39+
Executor commands
40+
-----------------
41+
42+
``AC_normalize_ext`` (``target`` → ``{ext}``, pure) and ``AC_file_association``
43+
(``target`` → the association dict). They are exposed as the matching ``ac_*``
44+
MCP tools (both read-only) and as Script Builder commands under **Shell**.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
以預設程式開啟檔案 / URL
2+
========================
3+
4+
框架原本能啟動字面執行檔(``start_exe`` / ``shell_process``),卻無法做最常見的「交接給另一個應用程式」
5+
RPA 步驟:用註冊的應用程式開啟 ``report.pdf``、``print`` 一份文件,或在預設瀏覽器開啟 URL。
6+
``shell_open`` 補上這點,依作業系統路由到 ``os.startfile`` / ``open`` / ``xdg-open`` /
7+
``webbrowser``。
8+
9+
* :func:`plan_open` ——純 planner:分類目標(URL 或檔案路徑)、驗證(URL scheme 白名單;檔案用
10+
``realpath``)並回傳分派描述子,
11+
* :func:`open_path` ——透過可注入的 ``opener`` 接縫執行計畫(預設為真正的 OS 呼叫)。
12+
13+
純標準庫;透過可注入的 ``opener``,分派邏輯可在不真正開啟任何東西的情況下單元測試。不匯入
14+
``PySide6``。
15+
16+
無頭 API
17+
--------
18+
19+
.. code-block:: python
20+
21+
from je_auto_control import open_path, plan_open
22+
23+
open_path("report.pdf") # 預設 PDF 檢視器
24+
open_path("invoice.pdf", verb="print") # 列印
25+
open_path("https://example.com") # 預設瀏覽器
26+
27+
plan_open("https://example.com")
28+
# {"kind": "url", "scheme": "https", "target": "...", "backend": "webbrowser",
29+
# "verb": "open"}
30+
plan_open("report.pdf")
31+
# {"kind": "file", "target": "<realpath>", "backend": "startfile", ...}
32+
33+
``scheme://`` 目標(或 ``mailto:`` / ``tel:``)會以 URL 開啟——只接受白名單 scheme
34+
(``http`` / ``https`` / ``ftp`` / ``file`` / ``mailto`` / ``tel``),其他則拋出 ``ValueError``。
35+
其餘皆視為檔案路徑(Windows 磁碟代號如 ``C:\\…`` 會正確視為路徑而非 scheme)並以 ``realpath``
36+
解析。``verb``(``open`` / ``print`` / ``edit``)在 Windows 上套用於檔案。
37+
38+
執行器指令
39+
----------
40+
41+
``AC_open_path``(``target`` / ``verb`` → ``{opened}``)與 ``AC_plan_open``(``target`` /
42+
``verb`` → 計畫)。皆以對應的 ``ac_*`` MCP 工具(``open_path`` 為僅副作用、``plan_open`` 為唯讀)
43+
及 Script Builder 指令(位於 **Shell** 分類下)形式提供。
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
閒置偵測 + 保持機器清醒
2+
=======================
3+
4+
長時間無人值守的自動化執行常因兩種情況中斷:螢幕保護 / 電源原則在執行中途讓機器睡眠,或是當有人正在
5+
使用機器時執行應該暫停。框架原本兩種訊號都沒有。``idle_keepawake`` 補上這兩者,並以可注入接縫實作,
6+
所有邏輯都能在不碰作業系統的情況下測試。
7+
8+
* :func:`idle_seconds` / :func:`is_idle` ——距離使用者上次鍵盤 / 滑鼠輸入的秒數(Windows 上用
9+
``GetLastInputInfo``),透過可注入的 ``probe`` 取得。
10+
* :func:`plan_keep_awake` ——純 planner,描述請求對應到哪些清醒旗標。
11+
* :func:`keep_awake` ——具範圍的 context manager,在 ``with`` 區塊期間保持機器清醒,離開時還原先前狀態。
12+
* :func:`keep_awake_on` / :func:`allow_sleep` ——供 JSON 動作流程使用的行程全域開 / 關配對。
13+
14+
三個 keep-awake 入口皆透過可注入的 ``driver`` 套用計畫(預設 Windows 用
15+
``SetThreadExecutionState``、macOS 用 ``caffeinate``、Linux 用 ``systemd-inhibit``)。不匯入
16+
``PySide6``。
17+
18+
無頭 API
19+
--------
20+
21+
.. code-block:: python
22+
23+
from je_auto_control import (
24+
idle_seconds, is_idle, keep_awake, keep_awake_on, allow_sleep,
25+
)
26+
27+
idle_seconds() # 例如 3.4 ——距離上次輸入的秒數
28+
is_idle(300) # 沒人碰機器滿 5 分鐘後回傳 True
29+
30+
# 具範圍:只在長步驟執行時保持清醒
31+
with keep_awake():
32+
run_long_batch()
33+
34+
# 流程式:開始時開、結束時關
35+
keep_awake_on(display=True, system=True)
36+
try:
37+
run_long_batch()
38+
finally:
39+
allow_sleep()
40+
41+
:func:`is_idle` 是「只在使用者離開時才執行」的判斷閘;:func:`keep_awake` /
42+
:func:`keep_awake_on` 阻止螢幕與系統睡眠,讓整夜執行不被打斷。``display=False`` 會保持系統清醒但允許
43+
螢幕變黑(對無頭機器較省電)。
44+
45+
執行器指令
46+
----------
47+
48+
``AC_idle_seconds``(→ ``{idle_seconds}``)、``AC_is_idle``(``threshold`` →
49+
``{idle, idle_seconds}``)、``AC_plan_keep_awake``(``display`` / ``system`` → 計畫)、
50+
``AC_keep_awake_on``(``display`` / ``system`` → 生效中的計畫)與 ``AC_allow_sleep``
51+
(→ ``{released}``)。皆以對應的 ``ac_*`` MCP 工具(讀取為唯讀、keep-awake 開 / 關為僅副作用)
52+
及 Script Builder 指令(位於 **Shell** 分類下)形式提供。:func:`keep_awake` context manager
53+
則是具範圍使用的 Python API 介面。
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
解析檔案類型已註冊的應用程式
2+
============================
3+
4+
:func:`open_path`(``shell_open``)用註冊的應用程式開啟檔案;``file_assoc`` 回答相反的唯讀問題——
5+
那個應用程式是「哪一個」?給定 ``report.pdf``(或裸的 ``.pdf`` / ``pdf``),它會透過 Windows
6+
``AssocQueryStringW`` shell API 回傳已註冊的執行檔、友善應用程式名稱、開啟命令列與 MIME 內容類型。
7+
8+
* :func:`normalize_ext` ——純輔助函式,把路徑 / ``.ext`` / 裸 ``ext`` 轉成小寫的 ``.ext``,
9+
* :func:`file_association` ——透過可注入的 ``resolver`` 接縫執行查詢(預設為真正的 shell API)。
10+
11+
組裝邏輯可透過可注入的 ``resolver`` 在非 Windows 上單元測試。不匯入 ``PySide6``。
12+
13+
無頭 API
14+
--------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import file_association, normalize_ext
19+
20+
normalize_ext("report.PDF") # ".pdf"
21+
normalize_ext("archive.tar.gz") # ".gz"
22+
23+
file_association("report.pdf")
24+
# {"ext": ".pdf", "command": "...AcroRd32.exe \"%1\"",
25+
# "exe": "...AcroRd32.exe", "friendly": "Adobe Acrobat",
26+
# "content_type": "application/pdf"}
27+
28+
當該類型未註冊任何應用程式時,應用程式欄位為 ``None``。這是 :func:`open_path` 的自然搭檔:
29+
``file_association`` 告訴你「什麼」會開啟檔案(可斷言「PDF 用 Acrobat 開,不是瀏覽器」),而
30+
``open_path`` 實際開啟它。即時查詢使用 Windows shell API;其他平台請傳入自己的 ``resolver``。
31+
32+
執行器指令
33+
----------
34+
35+
``AC_normalize_ext``(``target`` → ``{ext}``,純)與 ``AC_file_association``
36+
(``target`` → 關聯 dict)。皆以對應的 ``ac_*`` MCP 工具(皆唯讀)及 Script Builder 指令
37+
(位於 **Shell** 分類下)形式提供。

je_auto_control/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@
9191
)
9292
# Reactive UIA event waits (focus-changed)
9393
from je_auto_control.utils.ax_events import wait_for_focus_change
94+
# Open a file with its default app / a URL in the default browser
95+
from je_auto_control.utils.shell_open import open_path, plan_open
96+
# Detect user-idle time and keep the machine awake during unattended runs
97+
from je_auto_control.utils.idle_keepawake import (
98+
allow_sleep, idle_seconds, is_idle, keep_awake, keep_awake_on,
99+
plan_keep_awake,
100+
)
101+
# Resolve which application is registered to open a given file type
102+
from je_auto_control.utils.file_assoc import file_association, normalize_ext
94103
# Rich clipboard formats — RTF + CSV/TSV codecs and Windows get / set
95104
from je_auto_control.utils.clipboard_rich_formats import (
96105
build_rtf, csv_to_rows, get_clipboard_csv, get_clipboard_rtf, rows_to_csv,
@@ -1700,6 +1709,10 @@ def start_autocontrol_gui(*args, **kwargs):
17001709
"legacy_info", "legacy_default_action",
17011710
"get_selection", "list_views", "set_view",
17021711
"wait_for_focus_change",
1712+
"plan_open", "open_path",
1713+
"idle_seconds", "is_idle", "plan_keep_awake",
1714+
"keep_awake", "keep_awake_on", "allow_sleep",
1715+
"normalize_ext", "file_association",
17031716
"build_rtf", "rtf_to_text", "rows_to_csv", "csv_to_rows",
17041717
"set_clipboard_rtf", "get_clipboard_rtf",
17051718
"set_clipboard_csv", "get_clipboard_csv",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4250,6 +4250,74 @@ def _add_work_queue_specs(specs: List[CommandSpec]) -> None:
42504250
"AC_shell_command", "Shell", "Shell Command",
42514251
fields=(FieldSpec("shell_command", FieldType.STRING),),
42524252
))
4253+
specs.append(CommandSpec(
4254+
"AC_open_path", "Shell", "Open File / URL (default app)",
4255+
fields=(
4256+
FieldSpec("target", FieldType.STRING,
4257+
placeholder="report.pdf or https://example.com"),
4258+
FieldSpec("verb", FieldType.STRING, optional=True, default="open",
4259+
placeholder="open / print / edit"),
4260+
),
4261+
description="Open a file with its default app, or a URL in the browser.",
4262+
))
4263+
specs.append(CommandSpec(
4264+
"AC_plan_open", "Shell", "Plan Open (classify)",
4265+
fields=(
4266+
FieldSpec("target", FieldType.STRING),
4267+
FieldSpec("verb", FieldType.STRING, optional=True, default="open"),
4268+
),
4269+
description="Classify how a file/URL would be opened (pure, no launch).",
4270+
))
4271+
specs.append(CommandSpec(
4272+
"AC_idle_seconds", "Shell", "Idle Seconds",
4273+
fields=(),
4274+
description="Seconds since the last user keyboard / mouse input.",
4275+
))
4276+
specs.append(CommandSpec(
4277+
"AC_is_idle", "Shell", "Is User Idle",
4278+
fields=(
4279+
FieldSpec("threshold", FieldType.FLOAT, default=300.0,
4280+
placeholder="idle seconds threshold"),
4281+
),
4282+
description="True if the user has been idle for >= threshold seconds.",
4283+
))
4284+
specs.append(CommandSpec(
4285+
"AC_plan_keep_awake", "Shell", "Plan Keep Awake",
4286+
fields=(
4287+
FieldSpec("display", FieldType.BOOL, optional=True, default=True),
4288+
FieldSpec("system", FieldType.BOOL, optional=True, default=True),
4289+
),
4290+
description="Describe a keep-awake request (pure, no OS call).",
4291+
))
4292+
specs.append(CommandSpec(
4293+
"AC_keep_awake_on", "Shell", "Keep Machine Awake",
4294+
fields=(
4295+
FieldSpec("display", FieldType.BOOL, optional=True, default=True),
4296+
FieldSpec("system", FieldType.BOOL, optional=True, default=True),
4297+
),
4298+
description="Keep the machine awake until Allow Sleep is run.",
4299+
))
4300+
specs.append(CommandSpec(
4301+
"AC_allow_sleep", "Shell", "Allow Machine to Sleep",
4302+
fields=(),
4303+
description="Release a previously-started keep-awake.",
4304+
))
4305+
specs.append(CommandSpec(
4306+
"AC_normalize_ext", "Shell", "Normalize Extension",
4307+
fields=(
4308+
FieldSpec("target", FieldType.STRING,
4309+
placeholder="report.pdf or .pdf or pdf"),
4310+
),
4311+
description="Lowercased file extension (with dot) of a path / ext.",
4312+
))
4313+
specs.append(CommandSpec(
4314+
"AC_file_association", "Shell", "File Association (default app)",
4315+
fields=(
4316+
FieldSpec("target", FieldType.STRING,
4317+
placeholder="report.pdf or .pdf"),
4318+
),
4319+
description="Which app is registered to open a file type (Windows).",
4320+
))
42534321
specs.append(CommandSpec(
42544322
"AC_take_golden", "Report", "Capture Golden Image",
42554323
fields=(FieldSpec("path", FieldType.FILE_PATH),),

0 commit comments

Comments
 (0)