@tanskong/office-assistant 1.0.5 → 1.0.7

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 CHANGED
@@ -1,57 +1,91 @@
1
- # Office Assistant
1
+ # Office Assistant - 智能办公助手
2
2
 
3
- AI-powered Office automation via MCP protocol.
3
+ 基于 MCP 协议的 AI 驱动 Office 自动化工具。
4
4
 
5
- ## Features
5
+ ## 功能特性
6
6
 
7
- - **RunPython**: Execute Python code to control Office applications freely
8
- - **Smart Workspace**: Desktop folder for safe file processing
9
- - **Full Office Suite**: Excel, Word, PowerPoint, Outlook, Visio, WPS, and more
10
- - **License Protection**: Machine-bound license activation
7
+ - **RunPython 主力工具**:自由执行 Python 代码控制 Office 应用程序
8
+ - **智能工作区**:桌面文件夹安全处理文件
9
+ - **全 Office 套件支持**:ExcelWordPowerPointOutlookVisioWPS
10
+ - **UI 自动化引擎**:基于 Windows UI Automation 框架的精准截图与控件操作
11
+ - **许可证保护**:机器绑定的许可证激活机制
11
12
 
12
- ## Installation
13
+ ## 安装
13
14
 
14
15
  ```bash
15
- npx @yourname/office-assistant
16
+ npx @tanskong/office-assistant
16
17
  ```
17
18
 
18
- ## Activation
19
+ ## 激活
19
20
 
20
- 1. Set environment variable:
21
+ 1. 设置环境变量:
21
22
  ```bash
22
23
  set OFFICE_ASSISTANT_LICENSE=OFFICE-ASSISTANT-XXXX-XXXX-XXXX-XXXX
23
24
  ```
24
25
 
25
- 2. Run activation:
26
+ 2. 运行激活:
26
27
  ```bash
27
- npx @yourname/office-assistant activate
28
+ npx @tanskong/office-assistant activate
28
29
  ```
29
30
 
30
- 3. Start the server:
31
+ 3. 启动服务:
31
32
  ```bash
32
- npx @yourname/office-assistant
33
+ npx @tanskong/office-assistant
33
34
  ```
34
35
 
35
- ## Disclaimer
36
+ ## 免责声明
36
37
 
37
- Please place files in `Desktop/智能办公区/数据` before processing.
38
- We are not responsible for data loss when operating on original file locations.
38
+ 请将需要处理的文件复制到 `桌面/智能办公区/数据` 文件夹后再进行操作。
39
+ 直接在原文件位置操作导致的数据丢失,本软件概不负责。
39
40
 
40
- ## Tools
41
+ ## 工具列表
41
42
 
42
- | Tool | Description |
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
- ## License
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
- Commercial license required. See activation instructions above.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanskong/office-assistant",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Office Assistant - AI-powered Office automation via MCP protocol with license protection",
5
5
  "main": "bin/office-assistant.js",
6
6
  "bin": {
package/requirements.txt CHANGED
@@ -2,3 +2,4 @@ fastmcp>=2.3.3
2
2
  pywin32>=310
3
3
  Pillow>=10.0.0
4
4
  psutil>=5.9.0
5
+ uiautomation>=2.0.29
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):
@@ -27,10 +29,11 @@ class TheOfficer:
27
29
 
28
30
  # 使用用户目录,避免硬编码 D 盘
29
31
  self._default_folder = os.path.join(os.path.expanduser("~"), "OfficeMCP_Data")
30
- self.Version = "1.0.5"
32
+ self.Version = "1.0.7"
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',
@@ -226,30 +229,38 @@ class TheOfficer:
226
229
  return "Demo succeeded"
227
230
 
228
231
  def DemonstrateExcel(self) -> bool:
229
- excel = self.Excel
230
- if excel is None:
232
+ try:
233
+ excel = self.Application("Excel", asNewInstance=True)
234
+ if excel is None:
235
+ return False
236
+ book = excel.Workbooks.Add()
237
+ sheet = excel.ActiveSheet
238
+ excel.Visible = True
239
+ sheet.Cells(1, 1).Value = "Hello, World From Office Assistant!"
240
+ sheet.Cells(1, 1).Font.Size = 20
241
+ sheet.Cells(1, 1).Font.Bold = True
242
+ sheet.Cells(2, 1).Value = "This is a demonstration of the Office Assistant server."
243
+ sheet.Cells(3, 1).Value = "You can use run_python tool to control all Microsoft Office Applications."
244
+ return True
245
+ except Exception as e:
246
+ print(f"DemonstrateExcel error: {e}")
231
247
  return False
232
- book = excel.Workbooks.Add()
233
- sheet = excel.ActiveSheet
234
- excel.Visible = True
235
- sheet.Cells(1, 1).Value = "Hello, World From Office Assistant!"
236
- sheet.Cells(1, 1).Font.Size = 20
237
- sheet.Cells(1, 1).Font.Bold = True
238
- sheet.Cells(2, 1).Value = "This is a demonstration of the Office Assistant server."
239
- sheet.Cells(3, 1).Value = "You can use run_python tool to control all Microsoft Office Applications."
240
- return True
241
248
 
242
249
  def DemonstratePowerPoint(self) -> bool:
243
- ppt = self.Application("PowerPoint")
244
- if ppt is None:
250
+ try:
251
+ ppt = self.Application("PowerPoint", asNewInstance=True)
252
+ if ppt is None:
253
+ return False
254
+ ppt.Visible = True
255
+ presentation = ppt.Presentations.Add()
256
+ slide = presentation.Slides.Add(1, 12)
257
+ shape = slide.Shapes.AddTextbox(1, 100, 20, 800, 100)
258
+ shape.TextFrame.TextRange.Text = "Hello, World From Office Assistant!"
259
+ shape.TextFrame.TextRange.Font.Bold = True
260
+ return True
261
+ except Exception as e:
262
+ print(f"DemonstratePowerPoint error: {e}")
245
263
  return False
246
- ppt.Visible = True
247
- presentation = ppt.Presentations.Add()
248
- slide = presentation.Slides.Add(1, 12)
249
- shape = slide.Shapes.AddTextbox(1, 100, 20, 800, 100)
250
- shape.TextFrame.TextRange.Text = "Hello, World From Office Assistant!"
251
- shape.TextFrame.TextRange.Font.Bold = True
252
- return True
253
264
 
254
265
  def FilePath(self, file_name: str = None, subfolder: str = None) -> str:
255
266
  # 优先使用智能办公区路径
@@ -265,7 +276,26 @@ class TheOfficer:
265
276
  os.makedirs(folder, exist_ok=True)
266
277
  return os.path.join(folder, file_name)
267
278
 
268
- def ScreenShot(self, save_path: str = None) -> str:
279
+ def ScreenShot(self, save_path: str = None, target: str = None) -> str:
280
+ """
281
+ 截取屏幕截图。
282
+
283
+ Args:
284
+ save_path: 保存路径,None 则自动生成
285
+ target: 截图目标,None=全屏, 'active'=活动窗口, 其他=指定窗口名称
286
+
287
+ Returns:
288
+ 截图文件路径
289
+ """
290
+ if self.UIA.available:
291
+ if target is None:
292
+ return self.UIA.screenshot_desktop(save_path)
293
+ elif target == 'active':
294
+ return self.UIA.screenshot_window(None, save_path)
295
+ else:
296
+ return self.UIA.screenshot_window(target, save_path)
297
+
298
+ # 降级到原有 GDI 截图(当 UIA 不可用时)
269
299
  import win32gui
270
300
  import win32ui
271
301
  import win32con
package/src/server.py CHANGED
@@ -67,21 +67,29 @@ def available_apps() -> list:
67
67
 
68
68
 
69
69
  @mcp.tool()
70
- def launch_app(app_name: str = "Excel", visible: bool = True) -> bool:
70
+ def launch_app(app_name: str = "Excel", visible: bool = True) -> dict:
71
71
  """Launch a Microsoft Office application."""
72
72
  try:
73
73
  app = Officer.Application(app_name)
74
- app.Visible = visible
75
- return True
74
+ if app is None:
75
+ return {"success": False, "error": f"Failed to start {app_name}"}
76
+ try:
77
+ app.Visible = visible
78
+ except Exception:
79
+ pass
80
+ return {"success": True, "app": app_name, "name": app.Name}
76
81
  except Exception as e:
77
- print(f"Launch failed: {e}")
78
- return False
82
+ return {"success": False, "error": str(e)}
79
83
 
80
84
 
81
85
  @mcp.tool()
82
- def quit_app(app_name: str = "Excel", force: bool = False) -> bool:
86
+ def quit_app(app_name: str = "Excel", force: bool = False) -> dict:
83
87
  """Quit a Microsoft Office application."""
84
- return Officer.Quit(app_name, force)
88
+ try:
89
+ result = Officer.Quit(app_name, force)
90
+ return {"success": result}
91
+ except Exception as e:
92
+ return {"success": False, "error": str(e)}
85
93
 
86
94
 
87
95
  # ========== File Operation Tools ==========
@@ -137,6 +145,8 @@ def save_file(file_path: str = None) -> dict:
137
145
  for app_name in ['Excel', 'Word', 'PowerPoint']:
138
146
  try:
139
147
  app = Officer.Application(app_name)
148
+ if app is None:
149
+ continue
140
150
  if app_name == 'Excel':
141
151
  doc = app.ActiveWorkbook
142
152
  elif app_name == 'Word':
@@ -150,7 +160,7 @@ def save_file(file_path: str = None) -> dict:
150
160
  else:
151
161
  doc.Save()
152
162
  return {"success": True, "file": doc.FullName}
153
- except:
163
+ except Exception:
154
164
  continue
155
165
  return {"success": False, "error": "No active document found"}
156
166
 
@@ -199,24 +209,154 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
199
209
  return {"success": False, "error": str(e)}
200
210
 
201
211
 
202
- # ========== System Tools ==========
212
+ # ========== UIA Tools ==========
213
+
214
+ @mcp.tool()
215
+ def screenshot(target: str = None, save_name: str = None) -> dict:
216
+ """
217
+ Capture a screenshot with UIA precision.
218
+
219
+ Args:
220
+ target: Screenshot target. None=full desktop, 'active'=active window, other=window name
221
+ save_name: Filename to save (auto-generated if None)
222
+
223
+ Examples:
224
+ screenshot() # Full desktop
225
+ screenshot('active') # Current active window
226
+ screenshot('Excel') # Window with name containing 'Excel'
227
+ """
228
+ try:
229
+ path = Officer.ScreenShot(save_name, target)
230
+ return {"success": True, "path": path}
231
+ except Exception as e:
232
+ return {"success": False, "error": str(e)}
233
+
203
234
 
204
235
  @mcp.tool()
205
- def screenshot(save_name: str = None) -> dict:
206
- """Capture a screenshot and save to the export folder."""
236
+ def screenshot_element(element_name: str, control_type: str = None,
237
+ parent_name: str = None, save_name: str = None) -> dict:
238
+ """
239
+ Capture screenshot of a specific UI element.
240
+
241
+ Args:
242
+ element_name: Name of the UI element
243
+ control_type: Control type, e.g., 'Button', 'Edit', 'Document'
244
+ parent_name: Parent window name to limit search scope
245
+ save_name: Filename to save
246
+
247
+ Examples:
248
+ screenshot_element('Calculate', 'Button', 'Calculator')
249
+ screenshot_element('Sheet1', 'TabItem', 'Excel')
250
+ """
207
251
  try:
208
- path = Officer.ScreenShot(save_name)
252
+ path = Officer.UIA.screenshot_element(element_name, control_type, parent_name, save_name)
209
253
  return {"success": True, "path": path}
210
- except ImportError as e:
211
- return {"success": False, "error": "缺少 Pillow 模块,请运行: pip install Pillow", "detail": str(e)}
212
254
  except Exception as e:
213
255
  return {"success": False, "error": str(e)}
214
256
 
215
257
 
216
258
  @mcp.tool()
217
- def download(url: str, save_name: str = None) -> str:
259
+ def find_ui_element(name: str, control_type: str = None,
260
+ parent_name: str = None) -> dict:
261
+ """
262
+ Find a UI element and return its properties.
263
+
264
+ Args:
265
+ name: Element name
266
+ control_type: Control type filter
267
+ parent_name: Parent window name
268
+
269
+ Returns:
270
+ Element properties including name, type, rect, automation_id
271
+ """
272
+ try:
273
+ element = Officer.UIA.find_element(name, control_type, parent_name)
274
+ if element is None:
275
+ return {"success": False, "error": f"Element not found: {name}"}
276
+ return {
277
+ "success": True,
278
+ "name": element.Name,
279
+ "type": element.ControlTypeName,
280
+ "automation_id": element.AutomationId,
281
+ "class_name": element.ClassName,
282
+ "rect": element.BoundingRectangle,
283
+ "enabled": element.IsEnabled,
284
+ "visible": not element.IsOffscreen
285
+ }
286
+ except Exception as e:
287
+ return {"success": False, "error": str(e)}
288
+
289
+
290
+ @mcp.tool()
291
+ def get_ui_tree(window_name: str = None, max_depth: int = 3) -> dict:
292
+ """
293
+ Get the UI automation tree of a window.
294
+
295
+ Args:
296
+ window_name: Window name, None for active window
297
+ max_depth: Maximum traversal depth
298
+
299
+ Returns:
300
+ Hierarchical tree structure of UI elements
301
+ """
302
+ try:
303
+ tree = Officer.UIA.get_element_tree(window_name, max_depth)
304
+ return {"success": True, "tree": tree}
305
+ except Exception as e:
306
+ return {"success": False, "error": str(e)}
307
+
308
+
309
+ @mcp.tool()
310
+ def click_ui_element(name: str, control_type: str = None,
311
+ parent_name: str = None) -> dict:
312
+ """
313
+ Click a UI element.
314
+
315
+ Args:
316
+ name: Element name
317
+ control_type: Control type
318
+ parent_name: Parent window name
319
+ """
320
+ try:
321
+ result = Officer.UIA.click_element(name, control_type, parent_name)
322
+ return {"success": result}
323
+ except Exception as e:
324
+ return {"success": False, "error": str(e)}
325
+
326
+
327
+ @mcp.tool()
328
+ def get_window_list() -> dict:
329
+ """List all top-level windows."""
330
+ try:
331
+ windows = Officer.UIA.get_window_list()
332
+ return {"success": True, "windows": windows}
333
+ except Exception as e:
334
+ return {"success": False, "error": str(e)}
335
+
336
+
337
+ @mcp.tool()
338
+ def activate_window(window_name: str) -> dict:
339
+ """Activate a window by name."""
340
+ try:
341
+ result = Officer.UIA.activate_window(window_name)
342
+ return {"success": result}
343
+ except Exception as e:
344
+ return {"success": False, "error": str(e)}
345
+
346
+
347
+ # ========== System Tools ==========
348
+
349
+ @mcp.tool()
350
+ def download(url: str, save_name: str = None) -> dict:
218
351
  """Download a file from URL to the download folder."""
219
- return Officer.DownloadURL(url, save_name)
352
+ try:
353
+ result = Officer.DownloadURL(url, save_name)
354
+ if result:
355
+ return {"success": True, "file_path": result}
356
+ else:
357
+ return {"success": False, "error": "Download failed or returned empty path"}
358
+ except Exception as e:
359
+ return {"success": False, "error": str(e)}
220
360
 
221
361
 
222
362
  @mcp.tool()
@@ -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()