@tanskong/office-assistant 1.0.4 → 1.0.6
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.
- package/README.md +67 -33
- package/package.json +1 -1
- package/requirements.txt +1 -0
- package/src/__pycache__/officer.cpython-310.pyc +0 -0
- package/src/__pycache__/uia_tools.cpython-310.pyc +0 -0
- package/src/officer.py +23 -1
- package/src/server.py +138 -7
- package/src/uia_tools.py +366 -0
package/README.md
CHANGED
|
@@ -1,57 +1,91 @@
|
|
|
1
|
-
# Office Assistant
|
|
1
|
+
# Office Assistant - 智能办公助手
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
基于 MCP 协议的 AI 驱动 Office 自动化工具。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 功能特性
|
|
6
6
|
|
|
7
|
-
- **RunPython
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
- **
|
|
7
|
+
- **RunPython 主力工具**:自由执行 Python 代码控制 Office 应用程序
|
|
8
|
+
- **智能工作区**:桌面文件夹安全处理文件
|
|
9
|
+
- **全 Office 套件支持**:Excel、Word、PowerPoint、Outlook、Visio、WPS 等
|
|
10
|
+
- **UI 自动化引擎**:基于 Windows UI Automation 框架的精准截图与控件操作
|
|
11
|
+
- **许可证保护**:机器绑定的许可证激活机制
|
|
11
12
|
|
|
12
|
-
##
|
|
13
|
+
## 安装
|
|
13
14
|
|
|
14
15
|
```bash
|
|
15
|
-
npx @
|
|
16
|
+
npx @tanskong/office-assistant
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
##
|
|
19
|
+
## 激活
|
|
19
20
|
|
|
20
|
-
1.
|
|
21
|
+
1. 设置环境变量:
|
|
21
22
|
```bash
|
|
22
23
|
set OFFICE_ASSISTANT_LICENSE=OFFICE-ASSISTANT-XXXX-XXXX-XXXX-XXXX
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
2.
|
|
26
|
+
2. 运行激活:
|
|
26
27
|
```bash
|
|
27
|
-
npx @
|
|
28
|
+
npx @tanskong/office-assistant activate
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
3.
|
|
31
|
+
3. 启动服务:
|
|
31
32
|
```bash
|
|
32
|
-
npx @
|
|
33
|
+
npx @tanskong/office-assistant
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
##
|
|
36
|
+
## 免责声明
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
请将需要处理的文件复制到 `桌面/智能办公区/数据` 文件夹后再进行操作。
|
|
39
|
+
直接在原文件位置操作导致的数据丢失,本软件概不负责。
|
|
39
40
|
|
|
40
|
-
##
|
|
41
|
+
## 工具列表
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|------|-------------|
|
|
44
|
-
| `run_python` | Execute Python code (PRIMARY TOOL) |
|
|
45
|
-
| `available_apps` | List installed Office applications |
|
|
46
|
-
| `launch_app` | Launch an Office application |
|
|
47
|
-
| `quit_app` | Quit an Office application |
|
|
48
|
-
| `open_file` | Open an Office file |
|
|
49
|
-
| `save_file` | Save current document |
|
|
50
|
-
| `screenshot` | Capture screenshot |
|
|
51
|
-
| `download` | Download file from URL |
|
|
52
|
-
| `speak` | Text-to-speech |
|
|
53
|
-
| `demonstrate` | Run demonstration |
|
|
43
|
+
### Office 控制工具
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
| 工具 | 说明 |
|
|
46
|
+
|------|------|
|
|
47
|
+
| `run_python` | 执行 Python 代码(主力工具) |
|
|
48
|
+
| `available_apps` | 列出已安装的 Office 应用程序 |
|
|
49
|
+
| `launch_app` | 启动 Office 应用程序 |
|
|
50
|
+
| `quit_app` | 退出 Office 应用程序 |
|
|
51
|
+
| `open_file` | 打开 Office 文件 |
|
|
52
|
+
| `save_file` | 保存当前文档 |
|
|
56
53
|
|
|
57
|
-
|
|
54
|
+
### UIA 自动化工具
|
|
55
|
+
|
|
56
|
+
| 工具 | 说明 |
|
|
57
|
+
|------|------|
|
|
58
|
+
| `screenshot` | 精准截图(全屏/活动窗口/指定窗口) |
|
|
59
|
+
| `screenshot_element` | 截取指定 UI 元素的截图 |
|
|
60
|
+
| `find_ui_element` | 查找 UI 元素并返回属性信息 |
|
|
61
|
+
| `get_ui_tree` | 获取窗口的 UI 树结构 |
|
|
62
|
+
| `click_ui_element` | 点击指定 UI 元素 |
|
|
63
|
+
| `get_window_list` | 列出所有顶层窗口 |
|
|
64
|
+
| `activate_window` | 激活指定窗口 |
|
|
65
|
+
|
|
66
|
+
### 系统工具
|
|
67
|
+
|
|
68
|
+
| 工具 | 说明 |
|
|
69
|
+
|------|------|
|
|
70
|
+
| `download` | 从 URL 下载文件 |
|
|
71
|
+
| `demonstrate` | 运行功能演示 |
|
|
72
|
+
|
|
73
|
+
## 截图功能示例
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# 截取全屏
|
|
77
|
+
screenshot()
|
|
78
|
+
|
|
79
|
+
# 截取活动窗口
|
|
80
|
+
screenshot('active')
|
|
81
|
+
|
|
82
|
+
# 截取指定窗口
|
|
83
|
+
screenshot('Excel')
|
|
84
|
+
|
|
85
|
+
# 截取 UI 元素
|
|
86
|
+
screenshot_element('Calculate', 'Button', 'Calculator')
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 许可证
|
|
90
|
+
|
|
91
|
+
需要商业许可证。请参阅上方的激活说明。
|
package/package.json
CHANGED
package/requirements.txt
CHANGED
|
Binary file
|
|
Binary file
|
package/src/officer.py
CHANGED
|
@@ -6,6 +6,8 @@ import win32com.client
|
|
|
6
6
|
import pythoncom
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
+
from uia_tools import UIATools
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class TheOfficer:
|
|
11
13
|
def __init__(self):
|
|
@@ -31,6 +33,7 @@ class TheOfficer:
|
|
|
31
33
|
self.ComObjects = {}
|
|
32
34
|
self._printable = False
|
|
33
35
|
self.Data = OfficerData()
|
|
36
|
+
self.UIA = UIATools()
|
|
34
37
|
|
|
35
38
|
self.MicrosoftApplications = [
|
|
36
39
|
'Word', 'Excel', 'PowerPoint',
|
|
@@ -265,7 +268,26 @@ class TheOfficer:
|
|
|
265
268
|
os.makedirs(folder, exist_ok=True)
|
|
266
269
|
return os.path.join(folder, file_name)
|
|
267
270
|
|
|
268
|
-
def ScreenShot(self, save_path: str = None) -> str:
|
|
271
|
+
def ScreenShot(self, save_path: str = None, target: str = None) -> str:
|
|
272
|
+
"""
|
|
273
|
+
截取屏幕截图。
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
save_path: 保存路径,None 则自动生成
|
|
277
|
+
target: 截图目标,None=全屏, 'active'=活动窗口, 其他=指定窗口名称
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
截图文件路径
|
|
281
|
+
"""
|
|
282
|
+
if self.UIA.available:
|
|
283
|
+
if target is None:
|
|
284
|
+
return self.UIA.screenshot_desktop(save_path)
|
|
285
|
+
elif target == 'active':
|
|
286
|
+
return self.UIA.screenshot_window(None, save_path)
|
|
287
|
+
else:
|
|
288
|
+
return self.UIA.screenshot_window(target, save_path)
|
|
289
|
+
|
|
290
|
+
# 降级到原有 GDI 截图(当 UIA 不可用时)
|
|
269
291
|
import win32gui
|
|
270
292
|
import win32ui
|
|
271
293
|
import win32con
|
package/src/server.py
CHANGED
|
@@ -11,6 +11,7 @@ We are not responsible for data loss when operating on original file locations.
|
|
|
11
11
|
|
|
12
12
|
import sys
|
|
13
13
|
import os
|
|
14
|
+
from io import StringIO
|
|
14
15
|
|
|
15
16
|
from license_manager import verify_license
|
|
16
17
|
verify_license()
|
|
@@ -184,27 +185,157 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
|
|
|
184
185
|
doc = _get_active_doc(app, app_name)
|
|
185
186
|
globals_dict = _build_globals(app, doc, app_name)
|
|
186
187
|
|
|
188
|
+
# Capture stdout to return print output
|
|
189
|
+
old_stdout = sys.stdout
|
|
190
|
+
sys.stdout = buffer = StringIO()
|
|
191
|
+
|
|
187
192
|
try:
|
|
188
193
|
exec(code, globals_dict)
|
|
189
|
-
|
|
194
|
+
output = buffer.getvalue()
|
|
195
|
+
sys.stdout = old_stdout
|
|
196
|
+
return {"success": True, "output": output or "(无输出)"}
|
|
190
197
|
except Exception as e:
|
|
198
|
+
sys.stdout = old_stdout
|
|
191
199
|
return {"success": False, "error": str(e)}
|
|
192
200
|
|
|
193
201
|
|
|
194
|
-
# ==========
|
|
202
|
+
# ========== UIA Tools ==========
|
|
195
203
|
|
|
196
204
|
@mcp.tool()
|
|
197
|
-
def screenshot(save_name: str = None) -> dict:
|
|
198
|
-
"""
|
|
205
|
+
def screenshot(target: str = None, save_name: str = None) -> dict:
|
|
206
|
+
"""
|
|
207
|
+
Capture a screenshot with UIA precision.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
target: Screenshot target. None=full desktop, 'active'=active window, other=window name
|
|
211
|
+
save_name: Filename to save (auto-generated if None)
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
screenshot() # Full desktop
|
|
215
|
+
screenshot('active') # Current active window
|
|
216
|
+
screenshot('Excel') # Window with name containing 'Excel'
|
|
217
|
+
"""
|
|
218
|
+
try:
|
|
219
|
+
path = Officer.ScreenShot(save_name, target)
|
|
220
|
+
return {"success": True, "path": path}
|
|
221
|
+
except Exception as e:
|
|
222
|
+
return {"success": False, "error": str(e)}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@mcp.tool()
|
|
226
|
+
def screenshot_element(element_name: str, control_type: str = None,
|
|
227
|
+
parent_name: str = None, save_name: str = None) -> dict:
|
|
228
|
+
"""
|
|
229
|
+
Capture screenshot of a specific UI element.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
element_name: Name of the UI element
|
|
233
|
+
control_type: Control type, e.g., 'Button', 'Edit', 'Document'
|
|
234
|
+
parent_name: Parent window name to limit search scope
|
|
235
|
+
save_name: Filename to save
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
screenshot_element('Calculate', 'Button', 'Calculator')
|
|
239
|
+
screenshot_element('Sheet1', 'TabItem', 'Excel')
|
|
240
|
+
"""
|
|
199
241
|
try:
|
|
200
|
-
path = Officer.
|
|
242
|
+
path = Officer.UIA.screenshot_element(element_name, control_type, parent_name, save_name)
|
|
201
243
|
return {"success": True, "path": path}
|
|
202
|
-
except ImportError as e:
|
|
203
|
-
return {"success": False, "error": "缺少 Pillow 模块,请运行: pip install Pillow", "detail": str(e)}
|
|
204
244
|
except Exception as e:
|
|
205
245
|
return {"success": False, "error": str(e)}
|
|
206
246
|
|
|
207
247
|
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
def find_ui_element(name: str, control_type: str = None,
|
|
250
|
+
parent_name: str = None) -> dict:
|
|
251
|
+
"""
|
|
252
|
+
Find a UI element and return its properties.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
name: Element name
|
|
256
|
+
control_type: Control type filter
|
|
257
|
+
parent_name: Parent window name
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Element properties including name, type, rect, automation_id
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
element = Officer.UIA.find_element(name, control_type, parent_name)
|
|
264
|
+
if element is None:
|
|
265
|
+
return {"success": False, "error": f"Element not found: {name}"}
|
|
266
|
+
return {
|
|
267
|
+
"success": True,
|
|
268
|
+
"name": element.Name,
|
|
269
|
+
"type": element.ControlTypeName,
|
|
270
|
+
"automation_id": element.AutomationId,
|
|
271
|
+
"class_name": element.ClassName,
|
|
272
|
+
"rect": element.BoundingRectangle,
|
|
273
|
+
"enabled": element.IsEnabled,
|
|
274
|
+
"visible": not element.IsOffscreen
|
|
275
|
+
}
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return {"success": False, "error": str(e)}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
def get_ui_tree(window_name: str = None, max_depth: int = 3) -> dict:
|
|
282
|
+
"""
|
|
283
|
+
Get the UI automation tree of a window.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
window_name: Window name, None for active window
|
|
287
|
+
max_depth: Maximum traversal depth
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Hierarchical tree structure of UI elements
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
tree = Officer.UIA.get_element_tree(window_name, max_depth)
|
|
294
|
+
return {"success": True, "tree": tree}
|
|
295
|
+
except Exception as e:
|
|
296
|
+
return {"success": False, "error": str(e)}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@mcp.tool()
|
|
300
|
+
def click_ui_element(name: str, control_type: str = None,
|
|
301
|
+
parent_name: str = None) -> dict:
|
|
302
|
+
"""
|
|
303
|
+
Click a UI element.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
name: Element name
|
|
307
|
+
control_type: Control type
|
|
308
|
+
parent_name: Parent window name
|
|
309
|
+
"""
|
|
310
|
+
try:
|
|
311
|
+
result = Officer.UIA.click_element(name, control_type, parent_name)
|
|
312
|
+
return {"success": result}
|
|
313
|
+
except Exception as e:
|
|
314
|
+
return {"success": False, "error": str(e)}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool()
|
|
318
|
+
def get_window_list() -> dict:
|
|
319
|
+
"""List all top-level windows."""
|
|
320
|
+
try:
|
|
321
|
+
windows = Officer.UIA.get_window_list()
|
|
322
|
+
return {"success": True, "windows": windows}
|
|
323
|
+
except Exception as e:
|
|
324
|
+
return {"success": False, "error": str(e)}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@mcp.tool()
|
|
328
|
+
def activate_window(window_name: str) -> dict:
|
|
329
|
+
"""Activate a window by name."""
|
|
330
|
+
try:
|
|
331
|
+
result = Officer.UIA.activate_window(window_name)
|
|
332
|
+
return {"success": result}
|
|
333
|
+
except Exception as e:
|
|
334
|
+
return {"success": False, "error": str(e)}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# ========== System Tools ==========
|
|
338
|
+
|
|
208
339
|
@mcp.tool()
|
|
209
340
|
def download(url: str, save_name: str = None) -> str:
|
|
210
341
|
"""Download a file from URL to the download folder."""
|
package/src/uia_tools.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
UI Automation Tools for Office Assistant
|
|
4
|
+
========================================
|
|
5
|
+
基于 Windows UI Automation (UIA) 框架的自动化工具模块。
|
|
6
|
+
提供精准截图、UI 元素探测、控件操作等功能。
|
|
7
|
+
|
|
8
|
+
依赖: uiautomation >= 2.0
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import datetime
|
|
13
|
+
from typing import Optional, List, Dict, Any
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import uiautomation as auto
|
|
18
|
+
except ImportError:
|
|
19
|
+
auto = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UIATools:
|
|
23
|
+
"""UI Automation 工具封装类"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self._uia_available = auto is not None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def available(self) -> bool:
|
|
30
|
+
"""检查 UIA 是否可用"""
|
|
31
|
+
return self._uia_available
|
|
32
|
+
|
|
33
|
+
def _ensure_available(self):
|
|
34
|
+
if not self._uia_available:
|
|
35
|
+
raise ImportError("uiautomation 模块未安装,请运行: pip install uiautomation")
|
|
36
|
+
|
|
37
|
+
def _get_save_path(self, file_name: str = None, subfolder: str = "导出") -> str:
|
|
38
|
+
"""获取保存路径"""
|
|
39
|
+
workspace = Path.home() / "Desktop" / "智能办公区"
|
|
40
|
+
if not workspace.exists():
|
|
41
|
+
workspace = Path.home() / "OfficeMCP_Data"
|
|
42
|
+
folder = workspace / subfolder
|
|
43
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
if file_name is None:
|
|
45
|
+
now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
46
|
+
file_name = f"screenshot_{now}.png"
|
|
47
|
+
return str(folder / file_name)
|
|
48
|
+
|
|
49
|
+
# ========== 截图功能 ==========
|
|
50
|
+
|
|
51
|
+
def screenshot_window(self, window_name: str = None, save_path: str = None) -> str:
|
|
52
|
+
"""
|
|
53
|
+
截取指定窗口的截图。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
window_name: 窗口标题或名称,None 则截取活动窗口
|
|
57
|
+
save_path: 保存路径,None 则自动生成
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
截图文件的完整路径
|
|
61
|
+
"""
|
|
62
|
+
self._ensure_available()
|
|
63
|
+
|
|
64
|
+
if window_name:
|
|
65
|
+
window = auto.WindowControl(searchDepth=1, Name=window_name)
|
|
66
|
+
if not window.Exists():
|
|
67
|
+
raise ValueError(f"未找到窗口: {window_name}")
|
|
68
|
+
else:
|
|
69
|
+
window = auto.GetForegroundControl()
|
|
70
|
+
|
|
71
|
+
if save_path is None:
|
|
72
|
+
save_path = self._get_save_path()
|
|
73
|
+
else:
|
|
74
|
+
save_path = self._get_save_path(save_path)
|
|
75
|
+
|
|
76
|
+
window.CaptureToImage(save_path)
|
|
77
|
+
return save_path
|
|
78
|
+
|
|
79
|
+
def screenshot_element(self, element_name: str, control_type: str = None,
|
|
80
|
+
parent_name: str = None, save_path: str = None) -> str:
|
|
81
|
+
"""
|
|
82
|
+
截取指定 UI 元素的截图。
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
element_name: 元素名称 (Name 属性)
|
|
86
|
+
control_type: 控件类型,如 'Button', 'Edit', 'Document' 等
|
|
87
|
+
parent_name: 父窗口名称,用于限定搜索范围
|
|
88
|
+
save_path: 保存路径
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
截图文件的完整路径
|
|
92
|
+
"""
|
|
93
|
+
self._ensure_available()
|
|
94
|
+
|
|
95
|
+
element = self.find_element(element_name, control_type, parent_name)
|
|
96
|
+
if element is None:
|
|
97
|
+
raise ValueError(f"未找到元素: {element_name}")
|
|
98
|
+
|
|
99
|
+
if save_path is None:
|
|
100
|
+
save_path = self._get_save_path()
|
|
101
|
+
else:
|
|
102
|
+
save_path = self._get_save_path(save_path)
|
|
103
|
+
|
|
104
|
+
element.CaptureToImage(save_path)
|
|
105
|
+
return save_path
|
|
106
|
+
|
|
107
|
+
def screenshot_desktop(self, save_path: str = None) -> str:
|
|
108
|
+
"""
|
|
109
|
+
截取全屏截图(替代原有的 GDI 截图)。
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
save_path: 保存路径
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
截图文件的完整路径
|
|
116
|
+
"""
|
|
117
|
+
self._ensure_available()
|
|
118
|
+
|
|
119
|
+
if save_path is None:
|
|
120
|
+
save_path = self._get_save_path()
|
|
121
|
+
else:
|
|
122
|
+
save_path = self._get_save_path(save_path)
|
|
123
|
+
|
|
124
|
+
desktop = auto.GetRootControl()
|
|
125
|
+
desktop.CaptureToImage(save_path)
|
|
126
|
+
return save_path
|
|
127
|
+
|
|
128
|
+
# ========== 元素查找功能 ==========
|
|
129
|
+
|
|
130
|
+
def find_element(self, name: str, control_type: str = None,
|
|
131
|
+
parent_name: str = None, timeout: int = 3):
|
|
132
|
+
"""
|
|
133
|
+
查找 UI 元素。
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: 元素名称
|
|
137
|
+
control_type: 控件类型
|
|
138
|
+
parent_name: 父窗口名称
|
|
139
|
+
timeout: 超时时间(秒)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
找到的元素对象,未找到返回 None
|
|
143
|
+
"""
|
|
144
|
+
self._ensure_available()
|
|
145
|
+
|
|
146
|
+
if parent_name:
|
|
147
|
+
parent = auto.WindowControl(searchDepth=1, Name=parent_name)
|
|
148
|
+
if not parent.Exists(timeout):
|
|
149
|
+
return None
|
|
150
|
+
scope = parent
|
|
151
|
+
else:
|
|
152
|
+
scope = auto.GetRootControl()
|
|
153
|
+
|
|
154
|
+
kwargs = {"Name": name, "searchDepth": 10}
|
|
155
|
+
if control_type:
|
|
156
|
+
kwargs["ControlType"] = getattr(auto.ControlType, control_type, 0)
|
|
157
|
+
|
|
158
|
+
element = scope.FindFirst(**kwargs)
|
|
159
|
+
return element
|
|
160
|
+
|
|
161
|
+
def find_elements(self, name: str = None, control_type: str = None,
|
|
162
|
+
parent_name: str = None, timeout: int = 3) -> List[Any]:
|
|
163
|
+
"""
|
|
164
|
+
查找多个匹配的 UI 元素。
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: 元素名称(支持部分匹配)
|
|
168
|
+
control_type: 控件类型
|
|
169
|
+
parent_name: 父窗口名称
|
|
170
|
+
timeout: 超时时间
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
元素列表
|
|
174
|
+
"""
|
|
175
|
+
self._ensure_available()
|
|
176
|
+
|
|
177
|
+
if parent_name:
|
|
178
|
+
parent = auto.WindowControl(searchDepth=1, Name=parent_name)
|
|
179
|
+
if not parent.Exists(timeout):
|
|
180
|
+
return []
|
|
181
|
+
scope = parent
|
|
182
|
+
else:
|
|
183
|
+
scope = auto.GetRootControl()
|
|
184
|
+
|
|
185
|
+
kwargs = {"searchDepth": 10}
|
|
186
|
+
if name:
|
|
187
|
+
kwargs["Name"] = name
|
|
188
|
+
if control_type:
|
|
189
|
+
kwargs["ControlType"] = getattr(auto.ControlType, control_type, 0)
|
|
190
|
+
|
|
191
|
+
return scope.FindAll(**kwargs)
|
|
192
|
+
|
|
193
|
+
def get_element_tree(self, window_name: str = None, max_depth: int = 3) -> Dict:
|
|
194
|
+
"""
|
|
195
|
+
获取 UI 元素树结构。
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
window_name: 窗口名称,None 则获取当前活动窗口
|
|
199
|
+
max_depth: 最大遍历深度
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
树结构的字典表示
|
|
203
|
+
"""
|
|
204
|
+
self._ensure_available()
|
|
205
|
+
|
|
206
|
+
if window_name:
|
|
207
|
+
root = auto.WindowControl(searchDepth=1, Name=window_name)
|
|
208
|
+
else:
|
|
209
|
+
root = auto.GetForegroundControl()
|
|
210
|
+
|
|
211
|
+
if not root.Exists():
|
|
212
|
+
return {}
|
|
213
|
+
|
|
214
|
+
def build_tree(control, depth: int) -> Dict:
|
|
215
|
+
if depth > max_depth:
|
|
216
|
+
return {"name": control.Name, "type": control.ControlTypeName, "truncated": True}
|
|
217
|
+
|
|
218
|
+
children = []
|
|
219
|
+
for child in control.GetChildren():
|
|
220
|
+
children.append(build_tree(child, depth + 1))
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
"name": control.Name,
|
|
224
|
+
"type": control.ControlTypeName,
|
|
225
|
+
"automation_id": control.AutomationId,
|
|
226
|
+
"class_name": control.ClassName,
|
|
227
|
+
"rect": control.BoundingRectangle,
|
|
228
|
+
"children": children
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return build_tree(root, 1)
|
|
232
|
+
|
|
233
|
+
# ========== 元素操作功能 ==========
|
|
234
|
+
|
|
235
|
+
def click_element(self, name: str, control_type: str = None,
|
|
236
|
+
parent_name: str = None) -> bool:
|
|
237
|
+
"""
|
|
238
|
+
点击指定元素。
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
name: 元素名称
|
|
242
|
+
control_type: 控件类型
|
|
243
|
+
parent_name: 父窗口名称
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
是否成功
|
|
247
|
+
"""
|
|
248
|
+
self._ensure_available()
|
|
249
|
+
|
|
250
|
+
element = self.find_element(name, control_type, parent_name)
|
|
251
|
+
if element is None:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
element.Click()
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def send_keys_to_element(self, name: str, text: str,
|
|
258
|
+
control_type: str = "Edit",
|
|
259
|
+
parent_name: str = None) -> bool:
|
|
260
|
+
"""
|
|
261
|
+
向输入框发送文本。
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: 元素名称
|
|
265
|
+
text: 要发送的文本
|
|
266
|
+
control_type: 控件类型,默认 Edit
|
|
267
|
+
parent_name: 父窗口名称
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
是否成功
|
|
271
|
+
"""
|
|
272
|
+
self._ensure_available()
|
|
273
|
+
|
|
274
|
+
element = self.find_element(name, control_type, parent_name)
|
|
275
|
+
if element is None:
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
element.SendKeys(text)
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
def get_element_text(self, name: str, control_type: str = None,
|
|
282
|
+
parent_name: str = None) -> str:
|
|
283
|
+
"""
|
|
284
|
+
获取元素的文本内容。
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
name: 元素名称
|
|
288
|
+
control_type: 控件类型
|
|
289
|
+
parent_name: 父窗口名称
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
文本内容
|
|
293
|
+
"""
|
|
294
|
+
self._ensure_available()
|
|
295
|
+
|
|
296
|
+
element = self.find_element(name, control_type, parent_name)
|
|
297
|
+
if element is None:
|
|
298
|
+
return ""
|
|
299
|
+
|
|
300
|
+
return element.Name or ""
|
|
301
|
+
|
|
302
|
+
def wait_for_element(self, name: str, control_type: str = None,
|
|
303
|
+
parent_name: str = None, timeout: int = 10) -> bool:
|
|
304
|
+
"""
|
|
305
|
+
等待元素出现。
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
name: 元素名称
|
|
309
|
+
control_type: 控件类型
|
|
310
|
+
parent_name: 父窗口名称
|
|
311
|
+
timeout: 超时时间(秒)
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
是否找到
|
|
315
|
+
"""
|
|
316
|
+
self._ensure_available()
|
|
317
|
+
|
|
318
|
+
element = self.find_element(name, control_type, parent_name)
|
|
319
|
+
if element is None:
|
|
320
|
+
return False
|
|
321
|
+
return element.Exists(timeout)
|
|
322
|
+
|
|
323
|
+
# ========== 窗口操作 ==========
|
|
324
|
+
|
|
325
|
+
def get_window_list(self) -> List[Dict[str, str]]:
|
|
326
|
+
"""
|
|
327
|
+
获取所有顶层窗口列表。
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
窗口信息列表
|
|
331
|
+
"""
|
|
332
|
+
self._ensure_available()
|
|
333
|
+
|
|
334
|
+
windows = []
|
|
335
|
+
root = auto.GetRootControl()
|
|
336
|
+
for window in root.GetChildren():
|
|
337
|
+
if window.ControlType == auto.ControlType.WindowControl:
|
|
338
|
+
windows.append({
|
|
339
|
+
"name": window.Name,
|
|
340
|
+
"class": window.ClassName,
|
|
341
|
+
"handle": window.NativeWindowHandle,
|
|
342
|
+
"rect": window.BoundingRectangle
|
|
343
|
+
})
|
|
344
|
+
return windows
|
|
345
|
+
|
|
346
|
+
def activate_window(self, window_name: str) -> bool:
|
|
347
|
+
"""
|
|
348
|
+
激活指定窗口。
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
window_name: 窗口名称
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
是否成功
|
|
355
|
+
"""
|
|
356
|
+
self._ensure_available()
|
|
357
|
+
|
|
358
|
+
window = auto.WindowControl(searchDepth=1, Name=window_name)
|
|
359
|
+
if window.Exists():
|
|
360
|
+
window.SwitchToThisWindow()
|
|
361
|
+
return True
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# 全局实例
|
|
366
|
+
UIA = UIATools()
|