@tanskong/office-assistant 1.0.7 → 1.0.8

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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # 版本历史
2
+
3
+ ## 1.0.8 - 2026-05-31
4
+
5
+ ### 修复
6
+ - 修复 Word 自动化完全失效问题(app.Selection null 检查)
7
+ - 修复 PowerPoint 无幻灯片时崩溃问题(doc.Slides.Count 检查)
8
+ - 修复 save_file 遍历逻辑错误,添加 app_name 参数支持指定保存目标
9
+ - 修复中文路径不兼容问题,使用 win32api.GetShortPathName 转换
10
+ - 为 run_python 添加 finally 保护,确保 stdout 正确恢复
11
+ - 修复未知扩展名默认用 Excel 打开的问题
12
+ - 为 screenshot_element 添加返回值校验
13
+ - 为全局初始化添加异常捕获
14
+ - 为所有 UIA 工具添加模块可用性检查
15
+
16
+ ### 优化
17
+ - 精简 UIA 工具,删除不适合日常办公的工具:
18
+ - screenshot_element
19
+ - find_ui_element
20
+ - get_ui_tree
21
+ - click_ui_element
22
+ - 保留核心 UIA 工具:
23
+ - screenshot(截图)
24
+ - get_window_list(窗口列表)
25
+ - activate_window(激活窗口)
26
+ - 更新 README,反映最新功能状态
package/README.md CHANGED
@@ -7,8 +7,9 @@
7
7
  - **RunPython 主力工具**:自由执行 Python 代码控制 Office 应用程序
8
8
  - **智能工作区**:桌面文件夹安全处理文件
9
9
  - **全 Office 套件支持**:Excel、Word、PowerPoint、Outlook、Visio、WPS 等
10
- - **UI 自动化引擎**:基于 Windows UI Automation 框架的精准截图与控件操作
10
+ - **UI 自动化引擎**:基于 Windows UI Automation 框架的精准截图与窗口管理
11
11
  - **许可证保护**:机器绑定的许可证激活机制
12
+ - **中文路径支持**:完美解决中文文件路径问题
12
13
 
13
14
  ## 安装
14
15
 
@@ -48,18 +49,14 @@ npx @tanskong/office-assistant
48
49
  | `available_apps` | 列出已安装的 Office 应用程序 |
49
50
  | `launch_app` | 启动 Office 应用程序 |
50
51
  | `quit_app` | 退出 Office 应用程序 |
51
- | `open_file` | 打开 Office 文件 |
52
- | `save_file` | 保存当前文档 |
52
+ | `open_file` | 打开 Office 文件(支持中文路径) |
53
+ | `save_file` | 保存当前文档(可指定应用) |
53
54
 
54
55
  ### UIA 自动化工具
55
56
 
56
57
  | 工具 | 说明 |
57
58
  |------|------|
58
59
  | `screenshot` | 精准截图(全屏/活动窗口/指定窗口) |
59
- | `screenshot_element` | 截取指定 UI 元素的截图 |
60
- | `find_ui_element` | 查找 UI 元素并返回属性信息 |
61
- | `get_ui_tree` | 获取窗口的 UI 树结构 |
62
- | `click_ui_element` | 点击指定 UI 元素 |
63
60
  | `get_window_list` | 列出所有顶层窗口 |
64
61
  | `activate_window` | 激活指定窗口 |
65
62
 
@@ -81,9 +78,6 @@ screenshot('active')
81
78
 
82
79
  # 截取指定窗口
83
80
  screenshot('Excel')
84
-
85
- # 截取 UI 元素
86
- screenshot_element('Calculate', 'Button', 'Calculator')
87
81
  ```
88
82
 
89
83
  ## 许可证
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanskong/office-assistant",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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": {
@@ -39,6 +39,7 @@
39
39
  "bin/",
40
40
  "src/",
41
41
  "README.md",
42
+ "CHANGELOG.md",
42
43
  "requirements.txt"
43
44
  ],
44
45
  "repository": {
package/src/officer.py CHANGED
@@ -29,7 +29,7 @@ class TheOfficer:
29
29
 
30
30
  # 使用用户目录,避免硬编码 D 盘
31
31
  self._default_folder = os.path.join(os.path.expanduser("~"), "OfficeMCP_Data")
32
- self.Version = "1.0.7"
32
+ self.Version = "1.0.8"
33
33
  self.ComObjects = {}
34
34
  self._printable = False
35
35
  self.Data = OfficerData()
package/src/server.py CHANGED
@@ -13,12 +13,19 @@ import sys
13
13
  import os
14
14
  from io import StringIO
15
15
 
16
- from license_manager import verify_license
17
- verify_license()
18
-
19
- from fastmcp import FastMCP
20
- from officer import TheOfficer
21
- from utils import get_workspace, get_data_dir, get_export_dir, get_download_dir
16
+ try:
17
+ from license_manager import verify_license
18
+ verify_license()
19
+ except Exception as e:
20
+ print(f"[WARNING] License verification failed: {e}")
21
+
22
+ try:
23
+ from fastmcp import FastMCP
24
+ from officer import TheOfficer
25
+ from utils import get_workspace, get_data_dir, get_export_dir, get_download_dir
26
+ except Exception as e:
27
+ print(f"[ERROR] Failed to import dependencies: {e}")
28
+ sys.exit(1)
22
29
 
23
30
  mcp = FastMCP("Office Assistant")
24
31
  Officer = TheOfficer()
@@ -52,9 +59,15 @@ def _build_globals(app, doc, app_name):
52
59
  if app_name.lower() in ['excel', 'ket']:
53
60
  base['sheet'] = doc.ActiveSheet if doc else None
54
61
  elif app_name.lower() in ['word', 'kwps']:
55
- base['selection'] = app.Selection
62
+ base['selection'] = app.Selection if app else None
56
63
  elif app_name.lower() in ['powerpoint', 'kwpp']:
57
- base['slide'] = doc.Slides(doc.Slides.Count) if doc else None
64
+ base['slide'] = None
65
+ if doc and hasattr(doc, 'Slides'):
66
+ try:
67
+ if doc.Slides.Count > 0:
68
+ base['slide'] = doc.Slides(doc.Slides.Count)
69
+ except Exception:
70
+ pass
58
71
  return base
59
72
 
60
73
 
@@ -100,56 +113,71 @@ def open_file(file_path: str) -> dict:
100
113
  Open an Office file.
101
114
 
102
115
  Supported formats:
103
- - Excel: .xlsx, .xls, .csv
104
- - Word: .docx, .doc
105
- - PowerPoint: .pptx, .ppt
116
+ - Excel: .xlsx, .xls, .csv
117
+ - Word: .docx, .doc
118
+ - PowerPoint: .pptx, .ppt
106
119
 
107
120
  Tip: Place files in "Desktop/智能办公区/数据" for safe processing.
108
- Note: For Chinese file paths, use run_python tool instead.
109
121
  """
110
122
  import os
111
123
 
112
124
  # 处理中文路径编码
113
- # 方法1: 使用绝对路径
125
+ # 方法1:使用绝对路径
114
126
  file_path = os.path.abspath(file_path)
115
127
 
116
- # 方法2: 确保路径存在
128
+ # 方法2:确保路径存在
117
129
  if not os.path.exists(file_path):
118
130
  return {"success": False, "error": f"File not found: {file_path}"}
119
131
 
120
132
  ext = file_path.split('.')[-1].lower()
121
- app_name = APP_MAP.get(ext, 'Excel')
133
+ if ext not in APP_MAP:
134
+ return {"success": False, "error": f"Unsupported file format: .{ext}"}
135
+
136
+ app_name = APP_MAP.get(ext)
122
137
  app = Officer.Application(app_name)
123
138
 
124
139
  if app is None:
125
140
  return {"success": False, "error": f"Failed to start {app_name}"}
126
141
 
127
142
  try:
143
+ # 使用win32api.GetShortPathName解决中文路径问题
144
+ try:
145
+ import win32api
146
+ short_path = win32api.GetShortPathName(file_path)
147
+ open_path = short_path
148
+ except Exception:
149
+ open_path = file_path
150
+
128
151
  if app_name == 'Excel':
129
- # 使用 Unicode 字符串传递路径
130
- doc = app.Workbooks.Open(file_path)
152
+ doc = app.Workbooks.Open(open_path)
131
153
  elif app_name == 'Word':
132
- doc = app.Documents.Open(file_path)
154
+ doc = app.Documents.Open(open_path)
133
155
  elif app_name == 'PowerPoint':
134
- doc = app.Presentations.Open(file_path)
156
+ doc = app.Presentations.Open(open_path)
135
157
 
136
158
  app.Visible = True
137
159
  return {"success": True, "app": app_name, "file": file_path}
138
160
  except Exception as e:
139
- return {"success": False, "error": str(e), "note": "For Chinese paths, use run_python tool"}
161
+ return {"success": False, "error": str(e)}
140
162
 
141
163
 
142
164
  @mcp.tool()
143
- def save_file(file_path: str = None) -> dict:
144
- """Save the current active document."""
145
- for app_name in ['Excel', 'Word', 'PowerPoint']:
165
+ def save_file(file_path: str = None, app_name: str = None) -> dict:
166
+ """Save the current active document.
167
+
168
+ Args:
169
+ file_path: Optional file path to save as
170
+ app_name: Optional specific app to save (Excel, Word, PowerPoint)
171
+ """
172
+ apps_to_check = [app_name] if app_name else ['Excel', 'Word', 'PowerPoint']
173
+ for app_name_check in apps_to_check:
146
174
  try:
147
- app = Officer.Application(app_name)
175
+ app = Officer.Application(app_name_check)
148
176
  if app is None:
149
177
  continue
150
- if app_name == 'Excel':
178
+ if app_name_check == 'Excel':
151
179
  doc = app.ActiveWorkbook
152
- elif app_name == 'Word':
180
+ elif app_name_check == 'Word':
153
181
  doc = app.ActiveDocument
154
182
  else:
155
183
  doc = app.ActivePresentation
@@ -173,12 +201,12 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
173
201
  Execute Python code to control Office applications (PRIMARY TOOL).
174
202
 
175
203
  Available global variables:
176
- - app: The Office application object
177
- - doc: The current active document
178
- - sheet: Active worksheet (Excel)
179
- - selection: Current selection (Word)
180
- - slide: Active slide (PowerPoint)
181
- - Officer: TheOfficer instance for accessing other apps
204
+ - app: The Office application object
205
+ - doc: The current active document
206
+ - sheet: Active worksheet (Excel)
207
+ - selection: Current selection (Word)
208
+ - slide: Active slide (PowerPoint)
209
+ - Officer: TheOfficer instance for accessing other apps
182
210
 
183
211
  Excel example:
184
212
  sheet.Cells(1, 1).Value = "Hello"
@@ -202,11 +230,11 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
202
230
  try:
203
231
  exec(code, globals_dict)
204
232
  output = buffer.getvalue()
205
- sys.stdout = old_stdout
206
233
  return {"success": True, "output": output or "(无输出)"}
207
234
  except Exception as e:
208
- sys.stdout = old_stdout
209
235
  return {"success": False, "error": str(e)}
236
+ finally:
237
+ sys.stdout = old_stdout
210
238
 
211
239
 
212
240
  # ========== UIA Tools ==========
@@ -232,102 +260,13 @@ def screenshot(target: str = None, save_name: str = None) -> dict:
232
260
  return {"success": False, "error": str(e)}
233
261
 
234
262
 
235
- @mcp.tool()
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
- """
251
- try:
252
- path = Officer.UIA.screenshot_element(element_name, control_type, parent_name, save_name)
253
- return {"success": True, "path": path}
254
- except Exception as e:
255
- return {"success": False, "error": str(e)}
256
-
257
-
258
- @mcp.tool()
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
263
  @mcp.tool()
328
264
  def get_window_list() -> dict:
329
265
  """List all top-level windows."""
330
266
  try:
267
+ if not hasattr(Officer, 'UIA') or not Officer.UIA.available:
268
+ return {"success": False, "error": "UIA automation module not available"}
269
+
331
270
  windows = Officer.UIA.get_window_list()
332
271
  return {"success": True, "windows": windows}
333
272
  except Exception as e:
@@ -338,6 +277,9 @@ def get_window_list() -> dict:
338
277
  def activate_window(window_name: str) -> dict:
339
278
  """Activate a window by name."""
340
279
  try:
280
+ if not hasattr(Officer, 'UIA') or not Officer.UIA.available:
281
+ return {"success": False, "error": "UIA automation module not available"}
282
+
341
283
  result = Officer.UIA.activate_window(window_name)
342
284
  return {"success": result}
343
285
  except Exception as e: