ascript 1.3.27__py3-none-win_amd64.whl
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.
- ascript/__init__.py +8 -0
- ascript/hooks/__init__.py +0 -0
- ascript/hooks/hook-ascript.py +2 -0
- ascript/windows/__init__.py +29 -0
- ascript/windows/daos/__init__.py +0 -0
- ascript/windows/daos/screen.py +143 -0
- ascript/windows/hardware/__init__.py +5 -0
- ascript/windows/hardware/keyboard.py +279 -0
- ascript/windows/hardware/logitech_driver.py +179 -0
- ascript/windows/hardware/mouse.py +308 -0
- ascript/windows/license/__init__.py +16 -0
- ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
- ascript/windows/media/__init__.py +0 -0
- ascript/windows/resources/__init__.py +0 -0
- ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/templates/__init__.py +0 -0
- ascript/windows/resources/templates/__init__py.win.template +51 -0
- ascript/windows/resources/templates/build.as.win.template +14 -0
- ascript/windows/resources/templates/build.py +71 -0
- ascript/windows/resources/templates/logo_windows.ico +0 -0
- ascript/windows/resources/templates/windows/main.py +50 -0
- ascript/windows/resources/templates/windows/pyproject.toml +44 -0
- ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
- ascript/windows/screen/__init__.py +13 -0
- ascript/windows/screen/as_cv.py +382 -0
- ascript/windows/screen/canny.py +128 -0
- ascript/windows/screen/code_scanner.py +72 -0
- ascript/windows/screen/color_algorithms.py +277 -0
- ascript/windows/screen/find_colors.py +251 -0
- ascript/windows/screen/find_images.py +217 -0
- ascript/windows/screen/ocr/__init__.py +4 -0
- ascript/windows/screen/ocr/ocr.py +223 -0
- ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
- ascript/windows/screen/ocrx.py +169 -0
- ascript/windows/screen/screen_core.py +139 -0
- ascript/windows/store/__init__.py +21 -0
- ascript/windows/system/__init__.py +8 -0
- ascript/windows/system/device.py +355 -0
- ascript/windows/system/easy_print.py +115 -0
- ascript/windows/system/env.py +492 -0
- ascript/windows/system/exception/__init__.py +1 -0
- ascript/windows/system/key_value.py +145 -0
- ascript/windows/system/pip.py +175 -0
- ascript/windows/system/r.py +165 -0
- ascript/windows/system/runtime.py +62 -0
- ascript/windows/tools/__init__.py +27 -0
- ascript/windows/tools/__main__.py +21 -0
- ascript/windows/tools/action.json +33 -0
- ascript/windows/tools/action.py +168 -0
- ascript/windows/tools/colors_tool.py +391 -0
- ascript/windows/tools/config.py +26 -0
- ascript/windows/tools/dumptool.py +18 -0
- ascript/windows/tools/eel_api.py +495 -0
- ascript/windows/tools/gpplug/__init__.py +0 -0
- ascript/windows/tools/gpplug/plug.py +70 -0
- ascript/windows/tools/mouse_catch.py +168 -0
- ascript/windows/tools/plug_manager.py +38 -0
- ascript/windows/tools/tool_api.py +906 -0
- ascript/windows/tools/tool_window.py +56 -0
- ascript/windows/tools/uia_worker.py +365 -0
- ascript/windows/tools/web/app_store_window.html +481 -0
- ascript/windows/tools/web/colors.html +532 -0
- ascript/windows/tools/web/css/screen.css +1342 -0
- ascript/windows/tools/web/css/viewtree.css +357 -0
- ascript/windows/tools/web/flot_control.html +195 -0
- ascript/windows/tools/web/img/ico/favicon.png +0 -0
- ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
- ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
- ascript/windows/tools/web/img/svg/find_images.svg +1 -0
- ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
- ascript/windows/tools/web/img/svg/img_default.svg +1 -0
- ascript/windows/tools/web/img/svg/ocr.svg +1 -0
- ascript/windows/tools/web/js/screen/colors.js +466 -0
- ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
- ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
- ascript/windows/tools/web/js/screen/img_view.js +558 -0
- ascript/windows/tools/web/js/screen/plug.js +246 -0
- ascript/windows/tools/web/js/screen/screen.js +690 -0
- ascript/windows/tools/web/js/screen/screen_api.js +778 -0
- ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
- ascript/windows/tools/web/js/utils/map.js +73 -0
- ascript/windows/tools/web/js/utils/upload.js +157 -0
- ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
- ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
- ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
- ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
- ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
- ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
- ascript/windows/tools/web/libs/jquery/core.js +399 -0
- ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
- ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
- ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
- ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
- ascript/windows/tools/web/license_window.html +255 -0
- ascript/windows/tools/web/plugs/a.html +653 -0
- ascript/windows/tools/web/plugs/demo.html +52 -0
- ascript/windows/tools/web/plugs/find_colors.html +629 -0
- ascript/windows/tools/web/plugs/find_images.html +699 -0
- ascript/windows/tools/web/plugs/rapidocr.html +954 -0
- ascript/windows/tools/web/vtree.html +496 -0
- ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
- ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
- ascript/windows/tools/webview/index.html +13 -0
- ascript/windows/ui/__init__.py +6 -0
- ascript/windows/ui/app_store_window.py +272 -0
- ascript/windows/ui/draw_window.py +231 -0
- ascript/windows/ui/flot_test.py +297 -0
- ascript/windows/ui/flot_window.py +488 -0
- ascript/windows/ui/license_window.py +111 -0
- ascript/windows/ui/web_window.py +466 -0
- ascript/windows/utils/__init__.py +0 -0
- ascript/windows/utils/node_utils.py +20 -0
- ascript/windows/utils/screen_utils.py +280 -0
- ascript/windows/window/Selector.py +1015 -0
- ascript/windows/window/Window.py +747 -0
- ascript/windows/window/__init__.py +5 -0
- ascript/windows/window/launcher.py +213 -0
- ascript/windows/window/uia_runtime.py +26 -0
- ascript-1.3.27.dist-info/METADATA +41 -0
- ascript-1.3.27.dist-info/RECORD +177 -0
- ascript-1.3.27.dist-info/WHEEL +5 -0
- ascript-1.3.27.dist-info/entry_points.txt +2 -0
- ascript-1.3.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from ..system import R
|
|
2
|
+
from ..ui.web_window import WebWindow
|
|
3
|
+
from .tool_api import ToolApi
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
TOOL_TITLES = {
|
|
7
|
+
"inspector": "AScript 控件检索",
|
|
8
|
+
"colors": "AScript 图色工具",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SDKToolWindow(WebWindow):
|
|
13
|
+
def __init__(self, tool: str, hwnd=None):
|
|
14
|
+
if tool not in TOOL_TITLES:
|
|
15
|
+
raise ValueError(f"未知工具: {tool}")
|
|
16
|
+
html_path = R.internal("windows", "tools", "webview", "index.html")
|
|
17
|
+
super().__init__(html_path)
|
|
18
|
+
self.tool = tool
|
|
19
|
+
self.api = ToolApi(tool, hwnd)
|
|
20
|
+
for name in (
|
|
21
|
+
"get_context", "get_windows", "inspect_window", "capture", "load_image", "get_pixel_color", "test_selector",
|
|
22
|
+
"test_selector_spec",
|
|
23
|
+
"find_colors", "compare_colors", "count_colors", "find_blocks", "find_images",
|
|
24
|
+
"list_image_templates", "add_image_template", "import_image_template",
|
|
25
|
+
"rename_image_template", "delete_image_template", "clear_image_templates",
|
|
26
|
+
"save_image_templates", "list_project_images", "add_project_image_template",
|
|
27
|
+
"run_ocr", "run_ocrx", "run_canny", "scan_codes",
|
|
28
|
+
"save_capture", "open_capture_folder",
|
|
29
|
+
"list_capture_cache", "load_cached_capture", "delete_capture_cache", "clear_capture_cache",
|
|
30
|
+
):
|
|
31
|
+
self.expose(getattr(self.api, name))
|
|
32
|
+
|
|
33
|
+
def show(self, **kwargs):
|
|
34
|
+
defaults = {
|
|
35
|
+
"title": TOOL_TITLES[self.tool],
|
|
36
|
+
"width": 1180,
|
|
37
|
+
"height": 760,
|
|
38
|
+
"min_size": (820, 560),
|
|
39
|
+
"background_color": "#171a1c",
|
|
40
|
+
"bring_to_front": True,
|
|
41
|
+
"exclude_from_capture": self.tool == "colors",
|
|
42
|
+
}
|
|
43
|
+
defaults.update(kwargs)
|
|
44
|
+
return super().show(**defaults)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def start_tool(tool: str, hwnd=None, **kwargs):
|
|
48
|
+
return SDKToolWindow(tool, hwnd=hwnd).show(**kwargs)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def start_inspector(hwnd=None, **kwargs):
|
|
52
|
+
return start_tool("inspector", hwnd=hwnd, **kwargs)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def start_colors(hwnd=None, **kwargs):
|
|
56
|
+
return start_tool("colors", hwnd=hwnd, **kwargs)
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Crash- and hang-isolated UI Automation worker for the inspector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import ast
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RESULT_PREFIX = "ASCRIPT_UIA_RESULT="
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _count_nodes(node):
|
|
15
|
+
return 1 + sum(_count_nodes(child) for child in node.get("children", []))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _tag_source_and_offset(
|
|
19
|
+
node, source_hwnd: int, offset_x: int = 0, offset_y: int = 0,
|
|
20
|
+
source_class_name: str = "",
|
|
21
|
+
):
|
|
22
|
+
"""Keep enough origin information for overlays and generated selectors."""
|
|
23
|
+
node["source_hwnd"] = int(source_hwnd)
|
|
24
|
+
node["source_class_name"] = source_class_name
|
|
25
|
+
rect = node.get("rect")
|
|
26
|
+
if rect:
|
|
27
|
+
rect["left"] += offset_x
|
|
28
|
+
rect["right"] += offset_x
|
|
29
|
+
rect["top"] += offset_y
|
|
30
|
+
rect["bottom"] += offset_y
|
|
31
|
+
for child in node.get("children", []):
|
|
32
|
+
_tag_source_and_offset(child, source_hwnd, offset_x, offset_y, source_class_name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _renderer_documents(target, data, depth: int):
|
|
36
|
+
"""Read Chromium/Electron web content from its renderer child HWND.
|
|
37
|
+
|
|
38
|
+
Chromium does not expose the active document below the top-level browser
|
|
39
|
+
HWND in Raw View. ElementFromHandle on Chrome_RenderWidgetHostHWND does.
|
|
40
|
+
"""
|
|
41
|
+
import win32gui
|
|
42
|
+
from ..window import Selector, Window
|
|
43
|
+
|
|
44
|
+
handles = []
|
|
45
|
+
win32gui.EnumChildWindows(target.hwnd, lambda child, output: output.append(child), handles)
|
|
46
|
+
renderer_handles = []
|
|
47
|
+
for child in handles:
|
|
48
|
+
try:
|
|
49
|
+
if win32gui.GetClassName(child) == "Chrome_RenderWidgetHostHWND" and win32gui.IsWindowVisible(child):
|
|
50
|
+
renderer_handles.append(child)
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
if not renderer_handles:
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
main_client = target.client_rect
|
|
57
|
+
existing_documents = set()
|
|
58
|
+
|
|
59
|
+
def collect_documents(node):
|
|
60
|
+
if node.get("type") == "DocumentControl":
|
|
61
|
+
rect = node.get("rect") or {}
|
|
62
|
+
existing_documents.add((
|
|
63
|
+
node.get("name") or "",
|
|
64
|
+
rect.get("left"), rect.get("top"), rect.get("right"), rect.get("bottom"),
|
|
65
|
+
))
|
|
66
|
+
for child in node.get("children", []):
|
|
67
|
+
collect_documents(child)
|
|
68
|
+
|
|
69
|
+
collect_documents(data)
|
|
70
|
+
|
|
71
|
+
def remove_document(node, key):
|
|
72
|
+
children = node.get("children", [])
|
|
73
|
+
for index, child in enumerate(children):
|
|
74
|
+
rect = child.get("rect") or {}
|
|
75
|
+
child_key = (
|
|
76
|
+
child.get("name") or "",
|
|
77
|
+
rect.get("left"), rect.get("top"), rect.get("right"), rect.get("bottom"),
|
|
78
|
+
)
|
|
79
|
+
if child.get("type") == "DocumentControl" and child_key == key:
|
|
80
|
+
children.pop(index)
|
|
81
|
+
node["child_count"] = len(children)
|
|
82
|
+
return True
|
|
83
|
+
if remove_document(child, key):
|
|
84
|
+
return True
|
|
85
|
+
return False
|
|
86
|
+
added = 0
|
|
87
|
+
for child_hwnd in renderer_handles:
|
|
88
|
+
try:
|
|
89
|
+
renderer_window = Window(child_hwnd)
|
|
90
|
+
renderer_root = Selector(renderer_window, depth=depth).get_uielement_tree()
|
|
91
|
+
if not renderer_root:
|
|
92
|
+
continue
|
|
93
|
+
renderer_data = renderer_root.to_dict()
|
|
94
|
+
documents = [child for child in renderer_data.get("children", [])
|
|
95
|
+
if child.get("type") == "DocumentControl"]
|
|
96
|
+
if not documents:
|
|
97
|
+
continue
|
|
98
|
+
renderer_client = renderer_window.client_rect
|
|
99
|
+
offset_x = renderer_client.left - main_client.left
|
|
100
|
+
offset_y = renderer_client.top - main_client.top
|
|
101
|
+
for document in documents:
|
|
102
|
+
_tag_source_and_offset(
|
|
103
|
+
document, child_hwnd, offset_x, offset_y,
|
|
104
|
+
win32gui.GetClassName(child_hwnd),
|
|
105
|
+
)
|
|
106
|
+
rect = document.get("rect") or {}
|
|
107
|
+
key = (
|
|
108
|
+
document.get("name") or "",
|
|
109
|
+
rect.get("left"), rect.get("top"), rect.get("right"), rect.get("bottom"),
|
|
110
|
+
)
|
|
111
|
+
if key in existing_documents:
|
|
112
|
+
remove_document(data, key)
|
|
113
|
+
existing_documents.add(key)
|
|
114
|
+
document["depth"] = 1
|
|
115
|
+
data.setdefault("children", []).append(document)
|
|
116
|
+
added += _count_nodes(document)
|
|
117
|
+
except Exception:
|
|
118
|
+
continue
|
|
119
|
+
data["child_count"] = len(data.get("children", []))
|
|
120
|
+
return added
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def inspect_window(hwnd: int, depth: int):
|
|
124
|
+
from ..window import Selector, Window
|
|
125
|
+
import uiautomation as auto
|
|
126
|
+
|
|
127
|
+
with auto.UIAutomationInitializerInThread():
|
|
128
|
+
target = Window(hwnd)
|
|
129
|
+
selector = Selector(target, depth=depth if depth > 0 else 30)
|
|
130
|
+
root = selector.get_uielement_tree()
|
|
131
|
+
if not root:
|
|
132
|
+
return {"status": "empty", "message": "目标窗口未暴露 UIA 控件"}
|
|
133
|
+
data = root.to_dict()
|
|
134
|
+
_tag_source_and_offset(data, target.hwnd, source_class_name=target.class_name)
|
|
135
|
+
renderer_count = _renderer_documents(target, data, depth)
|
|
136
|
+
count = _count_nodes(data)
|
|
137
|
+
if count <= 1:
|
|
138
|
+
return {"status": "empty", "message": "目标窗口只暴露了 UIA 根节点"}
|
|
139
|
+
return {
|
|
140
|
+
"status": "success",
|
|
141
|
+
"backend": "UI Automation Raw View" + (" + Chromium Renderer" if renderer_count else ""),
|
|
142
|
+
"node_count": count,
|
|
143
|
+
"message": f"已从浏览器渲染进程补充 {renderer_count} 个网页控件" if renderer_count else "",
|
|
144
|
+
"data": data,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
FILTER_METHODS = {
|
|
149
|
+
"name", "type", "class_name", "res_id", "value", "description", "handle",
|
|
150
|
+
"enabled", "visible", "clickable", "password", "child_count", "depth",
|
|
151
|
+
"checkable", "checked", "editable", "focusable", "focused", "selected", "node_depth",
|
|
152
|
+
}
|
|
153
|
+
RELATION_METHODS = {"child", "parent", "brother"}
|
|
154
|
+
ACTION_METHODS = {"click", "input"}
|
|
155
|
+
TERMINAL_METHODS = {"find", "find_first", "find_all"}
|
|
156
|
+
WINDOW_FIND_ARGUMENTS = {"title_re", "process_name", "class_name", "visible_only", "timeout"}
|
|
157
|
+
CHILD_FIND_ARGUMENTS = {"title_re", "class_name", "visible_only", "timeout"}
|
|
158
|
+
SELECTOR_APP_ARGUMENTS = {"title_re", "class_name", "content", "timeout", "depth"}
|
|
159
|
+
SELECTOR_INIT_ARGUMENTS = {
|
|
160
|
+
"app", "title_re", "class_name", "content", "timeout", "depth",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _literal(node):
|
|
165
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (str, int, float, bool, type(None))):
|
|
166
|
+
return node.value
|
|
167
|
+
raise ValueError("Selector 参数只允许字符串、数字、布尔值或 None")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _evaluate_legacy_selector(expression: str):
|
|
171
|
+
"""Evaluate the old generated syntax without executing arbitrary Python."""
|
|
172
|
+
from ..window import Selector, Window
|
|
173
|
+
|
|
174
|
+
def evaluate(node):
|
|
175
|
+
if not isinstance(node, ast.Call):
|
|
176
|
+
raise ValueError("只支持 Selector 链式调用")
|
|
177
|
+
if isinstance(node.func, ast.Name) and node.func.id == "Window":
|
|
178
|
+
if len(node.args) != 1 or node.keywords:
|
|
179
|
+
raise ValueError("Window 只接受一个 HWND")
|
|
180
|
+
return Window(int(_literal(node.args[0])))
|
|
181
|
+
if isinstance(node.func, ast.Name) and node.func.id == "Selector":
|
|
182
|
+
if node.args:
|
|
183
|
+
raise ValueError("Selector 请使用关键字参数")
|
|
184
|
+
if any(item.arg is None for item in node.keywords):
|
|
185
|
+
raise ValueError("Selector 不支持展开关键字参数")
|
|
186
|
+
names = {item.arg for item in node.keywords}
|
|
187
|
+
allowed = SELECTOR_INIT_ARGUMENTS | {"window"}
|
|
188
|
+
if names - allowed or len(names) != len(node.keywords):
|
|
189
|
+
raise ValueError("Selector 包含不支持或重复的参数")
|
|
190
|
+
if "window" in names and len(names) != 1:
|
|
191
|
+
raise ValueError("window 不能与应用定位参数同时使用")
|
|
192
|
+
values = {
|
|
193
|
+
item.arg: evaluate(item.value) if item.arg == "window" else _literal(item.value)
|
|
194
|
+
for item in node.keywords
|
|
195
|
+
}
|
|
196
|
+
return Selector(**values)
|
|
197
|
+
if not isinstance(node.func, ast.Attribute):
|
|
198
|
+
raise ValueError("非法 Selector 表达式")
|
|
199
|
+
if isinstance(node.func.value, ast.Name) and node.func.value.id == "Selector" and node.func.attr == "app":
|
|
200
|
+
if len(node.args) > 1:
|
|
201
|
+
raise ValueError("Selector.app 最多接受一个位置参数")
|
|
202
|
+
kwargs = {item.arg: _literal(item.value) for item in node.keywords if item.arg}
|
|
203
|
+
if len(kwargs) != len(node.keywords) or set(kwargs) - SELECTOR_APP_ARGUMENTS:
|
|
204
|
+
raise ValueError("Selector.app 包含不支持的参数")
|
|
205
|
+
process_name = _literal(node.args[0]) if node.args else None
|
|
206
|
+
return Selector.app(process_name, **kwargs)
|
|
207
|
+
if isinstance(node.func.value, ast.Name) and node.func.value.id == "Window" and node.func.attr == "find":
|
|
208
|
+
if node.args:
|
|
209
|
+
raise ValueError("Window.find 请使用关键字参数")
|
|
210
|
+
kwargs = {item.arg: _literal(item.value) for item in node.keywords if item.arg}
|
|
211
|
+
if len(kwargs) != len(node.keywords) or set(kwargs) - WINDOW_FIND_ARGUMENTS:
|
|
212
|
+
raise ValueError("Window.find 包含不支持的参数")
|
|
213
|
+
window = Window.find(**kwargs)
|
|
214
|
+
if window is None:
|
|
215
|
+
raise ValueError("未找到目标窗口")
|
|
216
|
+
return window
|
|
217
|
+
target = evaluate(node.func.value)
|
|
218
|
+
method = node.func.attr
|
|
219
|
+
if isinstance(target, Window) and method == "find_child":
|
|
220
|
+
if node.args:
|
|
221
|
+
raise ValueError("find_child 请使用关键字参数")
|
|
222
|
+
kwargs = {item.arg: _literal(item.value) for item in node.keywords if item.arg}
|
|
223
|
+
if len(kwargs) != len(node.keywords) or set(kwargs) - CHILD_FIND_ARGUMENTS:
|
|
224
|
+
raise ValueError("find_child 包含不支持的参数")
|
|
225
|
+
child = target.find_child(**kwargs)
|
|
226
|
+
if child is None:
|
|
227
|
+
raise ValueError("未找到目标内容窗口")
|
|
228
|
+
return child
|
|
229
|
+
if method not in FILTER_METHODS | RELATION_METHODS | ACTION_METHODS | TERMINAL_METHODS:
|
|
230
|
+
raise ValueError(f"不支持的方法: {method}")
|
|
231
|
+
if node.keywords:
|
|
232
|
+
raise ValueError("Selector 表达式暂不支持关键字参数")
|
|
233
|
+
args = [_literal(item) for item in node.args]
|
|
234
|
+
return getattr(target, method)(*args)
|
|
235
|
+
|
|
236
|
+
parsed = ast.parse(expression.strip(), mode="eval")
|
|
237
|
+
return evaluate(parsed.body)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _serialize_results(values, source_window, display_window):
|
|
241
|
+
data = [item.to_dict() for item in values]
|
|
242
|
+
offset_x = offset_y = 0
|
|
243
|
+
if source_window.hwnd != display_window.hwnd:
|
|
244
|
+
source_rect = source_window.client_rect
|
|
245
|
+
display_rect = display_window.client_rect
|
|
246
|
+
offset_x = source_rect.left - display_rect.left
|
|
247
|
+
offset_y = source_rect.top - display_rect.top
|
|
248
|
+
for item in data:
|
|
249
|
+
_tag_source_and_offset(
|
|
250
|
+
item, source_window.hwnd, offset_x, offset_y,
|
|
251
|
+
source_window.class_name,
|
|
252
|
+
)
|
|
253
|
+
return data
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_window_locator(locator):
|
|
257
|
+
from ..window import Window
|
|
258
|
+
|
|
259
|
+
if isinstance(locator, int) and not isinstance(locator, bool):
|
|
260
|
+
window = Window(locator)
|
|
261
|
+
return window, window
|
|
262
|
+
if not isinstance(locator, dict):
|
|
263
|
+
raise ValueError("window 必须是窗口定位对象")
|
|
264
|
+
allowed = WINDOW_FIND_ARGUMENTS | {"child_class_name"}
|
|
265
|
+
unknown = set(locator) - allowed
|
|
266
|
+
if unknown:
|
|
267
|
+
raise ValueError(f"不支持的窗口定位属性: {', '.join(sorted(unknown))}")
|
|
268
|
+
find_args = {key: value for key, value in locator.items() if key in WINDOW_FIND_ARGUMENTS}
|
|
269
|
+
if not any(find_args.get(key) for key in ("title_re", "process_name", "class_name")):
|
|
270
|
+
raise ValueError("窗口定位至少需要 title_re、process_name 或 class_name")
|
|
271
|
+
find_args.setdefault("timeout", 10)
|
|
272
|
+
root_window = Window.find(**find_args)
|
|
273
|
+
if root_window is None:
|
|
274
|
+
raise ValueError("未找到目标窗口,请检查进程名、窗口类名或标题")
|
|
275
|
+
source_window = root_window
|
|
276
|
+
child_class_name = locator.get("child_class_name")
|
|
277
|
+
if child_class_name:
|
|
278
|
+
source_window = root_window.find_child(
|
|
279
|
+
class_name=str(child_class_name), visible_only=True,
|
|
280
|
+
timeout=float(find_args.get("timeout") or 0),
|
|
281
|
+
)
|
|
282
|
+
if source_window is None:
|
|
283
|
+
raise ValueError(f"未找到内容窗口: {child_class_name}")
|
|
284
|
+
return root_window, source_window
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_selector_spec(spec):
|
|
288
|
+
from ..window import Selector
|
|
289
|
+
import uiautomation as auto
|
|
290
|
+
|
|
291
|
+
if not isinstance(spec, dict):
|
|
292
|
+
raise ValueError("Selector spec 必须是对象")
|
|
293
|
+
display_window, source_window = _resolve_window_locator(spec.get("window"))
|
|
294
|
+
steps = spec.get("steps")
|
|
295
|
+
if not isinstance(steps, list) or not steps:
|
|
296
|
+
raise ValueError("Selector 至少需要一个步骤")
|
|
297
|
+
mode = spec.get("mode", "first")
|
|
298
|
+
if mode not in ("first", "all"):
|
|
299
|
+
raise ValueError("mode 只允许 first 或 all")
|
|
300
|
+
|
|
301
|
+
with auto.UIAutomationInitializerInThread():
|
|
302
|
+
selector = Selector(source_window, depth=max(1, min(int(spec.get("depth") or 30), 50)))
|
|
303
|
+
for index, step in enumerate(steps):
|
|
304
|
+
if not isinstance(step, dict):
|
|
305
|
+
raise ValueError(f"第 {index + 1} 个步骤格式错误")
|
|
306
|
+
method = step.get("method")
|
|
307
|
+
filters = step.get("filters", {})
|
|
308
|
+
if not isinstance(filters, dict):
|
|
309
|
+
raise ValueError(f"第 {index + 1} 个步骤 filters 必须是对象")
|
|
310
|
+
unknown = set(filters) - FILTER_METHODS
|
|
311
|
+
if unknown:
|
|
312
|
+
raise ValueError(f"不支持的属性: {', '.join(sorted(unknown))}")
|
|
313
|
+
if method in RELATION_METHODS:
|
|
314
|
+
getattr(selector, method)(step.get("param"))
|
|
315
|
+
elif method == "find":
|
|
316
|
+
if index and selector._current_filters:
|
|
317
|
+
selector._flush_to_step()
|
|
318
|
+
elif method == "action":
|
|
319
|
+
action = step.get("action")
|
|
320
|
+
if action not in ACTION_METHODS:
|
|
321
|
+
raise ValueError(f"不支持的动作: {action}")
|
|
322
|
+
getattr(selector, action)(step.get("param") if action == "input" else bool(step.get("param", False)))
|
|
323
|
+
else:
|
|
324
|
+
raise ValueError(f"不支持的步骤: {method}")
|
|
325
|
+
for name, value in filters.items():
|
|
326
|
+
getattr(selector, name)(value)
|
|
327
|
+
|
|
328
|
+
result = selector.find_all() if mode == "all" else selector.find()
|
|
329
|
+
values = [] if result is None else result if isinstance(result, list) else [result]
|
|
330
|
+
return {"status": "success", "data": _serialize_results(values, source_window, display_window)}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_selector(expression: str):
|
|
334
|
+
import uiautomation as auto
|
|
335
|
+
|
|
336
|
+
with auto.UIAutomationInitializerInThread():
|
|
337
|
+
result = _evaluate_legacy_selector(expression)
|
|
338
|
+
if hasattr(result, "find"):
|
|
339
|
+
result = result.find()
|
|
340
|
+
values = [] if result is None else result if isinstance(result, list) else [result]
|
|
341
|
+
source_window = values[0].window if values else None
|
|
342
|
+
data = _serialize_results(values, source_window, source_window) if source_window else []
|
|
343
|
+
return {"status": "success", "data": data}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def main():
|
|
347
|
+
parser = argparse.ArgumentParser()
|
|
348
|
+
parser.add_argument("action", choices=("inspect", "test", "test-spec"))
|
|
349
|
+
parser.add_argument("--hwnd", type=int)
|
|
350
|
+
parser.add_argument("--depth", type=int, default=0)
|
|
351
|
+
args = parser.parse_args()
|
|
352
|
+
try:
|
|
353
|
+
if args.action == "inspect":
|
|
354
|
+
result = inspect_window(args.hwnd, args.depth)
|
|
355
|
+
elif args.action == "test":
|
|
356
|
+
result = test_selector(sys.stdin.read())
|
|
357
|
+
else:
|
|
358
|
+
result = test_selector_spec(json.loads(sys.stdin.read()))
|
|
359
|
+
except Exception as exc:
|
|
360
|
+
result = {"status": "error", "message": str(exc), "data": []}
|
|
361
|
+
print(RESULT_PREFIX + json.dumps(result, ensure_ascii=False, separators=(",", ":")), flush=True)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
main()
|