@tanskong/office-assistant 1.0.6 → 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.6",
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.5"
32
+ self.Version = "1.0.8"
33
33
  self.ComObjects = {}
34
34
  self._printable = False
35
35
  self.Data = OfficerData()
@@ -229,30 +229,38 @@ class TheOfficer:
229
229
  return "Demo succeeded"
230
230
 
231
231
  def DemonstrateExcel(self) -> bool:
232
- excel = self.Excel
233
- 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}")
234
247
  return False
235
- book = excel.Workbooks.Add()
236
- sheet = excel.ActiveSheet
237
- excel.Visible = True
238
- sheet.Cells(1, 1).Value = "Hello, World From Office Assistant!"
239
- sheet.Cells(1, 1).Font.Size = 20
240
- sheet.Cells(1, 1).Font.Bold = True
241
- sheet.Cells(2, 1).Value = "This is a demonstration of the Office Assistant server."
242
- sheet.Cells(3, 1).Value = "You can use run_python tool to control all Microsoft Office Applications."
243
- return True
244
248
 
245
249
  def DemonstratePowerPoint(self) -> bool:
246
- ppt = self.Application("PowerPoint")
247
- 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}")
248
263
  return False
249
- ppt.Visible = True
250
- presentation = ppt.Presentations.Add()
251
- slide = presentation.Slides.Add(1, 12)
252
- shape = slide.Shapes.AddTextbox(1, 100, 20, 800, 100)
253
- shape.TextFrame.TextRange.Text = "Hello, World From Office Assistant!"
254
- shape.TextFrame.TextRange.Font.Bold = True
255
- return True
256
264
 
257
265
  def FilePath(self, file_name: str = None, subfolder: str = None) -> str:
258
266
  # 优先使用智能办公区路径
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
 
@@ -67,21 +80,29 @@ def available_apps() -> list:
67
80
 
68
81
 
69
82
  @mcp.tool()
70
- def launch_app(app_name: str = "Excel", visible: bool = True) -> bool:
83
+ def launch_app(app_name: str = "Excel", visible: bool = True) -> dict:
71
84
  """Launch a Microsoft Office application."""
72
85
  try:
73
86
  app = Officer.Application(app_name)
74
- app.Visible = visible
75
- return True
87
+ if app is None:
88
+ return {"success": False, "error": f"Failed to start {app_name}"}
89
+ try:
90
+ app.Visible = visible
91
+ except Exception:
92
+ pass
93
+ return {"success": True, "app": app_name, "name": app.Name}
76
94
  except Exception as e:
77
- print(f"Launch failed: {e}")
78
- return False
95
+ return {"success": False, "error": str(e)}
79
96
 
80
97
 
81
98
  @mcp.tool()
82
- def quit_app(app_name: str = "Excel", force: bool = False) -> bool:
99
+ def quit_app(app_name: str = "Excel", force: bool = False) -> dict:
83
100
  """Quit a Microsoft Office application."""
84
- return Officer.Quit(app_name, force)
101
+ try:
102
+ result = Officer.Quit(app_name, force)
103
+ return {"success": result}
104
+ except Exception as e:
105
+ return {"success": False, "error": str(e)}
85
106
 
86
107
 
87
108
  # ========== File Operation Tools ==========
@@ -92,54 +113,71 @@ def open_file(file_path: str) -> dict:
92
113
  Open an Office file.
93
114
 
94
115
  Supported formats:
95
- - Excel: .xlsx, .xls, .csv
96
- - Word: .docx, .doc
97
- - PowerPoint: .pptx, .ppt
116
+ - Excel: .xlsx, .xls, .csv
117
+ - Word: .docx, .doc
118
+ - PowerPoint: .pptx, .ppt
98
119
 
99
120
  Tip: Place files in "Desktop/智能办公区/数据" for safe processing.
100
- Note: For Chinese file paths, use run_python tool instead.
101
121
  """
102
122
  import os
103
123
 
104
124
  # 处理中文路径编码
105
- # 方法1: 使用绝对路径
125
+ # 方法1:使用绝对路径
106
126
  file_path = os.path.abspath(file_path)
107
127
 
108
- # 方法2: 确保路径存在
128
+ # 方法2:确保路径存在
109
129
  if not os.path.exists(file_path):
110
130
  return {"success": False, "error": f"File not found: {file_path}"}
111
131
 
112
132
  ext = file_path.split('.')[-1].lower()
113
- 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)
114
137
  app = Officer.Application(app_name)
115
138
 
116
139
  if app is None:
117
140
  return {"success": False, "error": f"Failed to start {app_name}"}
118
141
 
119
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
+
120
151
  if app_name == 'Excel':
121
- # 使用 Unicode 字符串传递路径
122
- doc = app.Workbooks.Open(file_path)
152
+ doc = app.Workbooks.Open(open_path)
123
153
  elif app_name == 'Word':
124
- doc = app.Documents.Open(file_path)
154
+ doc = app.Documents.Open(open_path)
125
155
  elif app_name == 'PowerPoint':
126
- doc = app.Presentations.Open(file_path)
156
+ doc = app.Presentations.Open(open_path)
127
157
 
128
158
  app.Visible = True
129
159
  return {"success": True, "app": app_name, "file": file_path}
130
160
  except Exception as e:
131
- return {"success": False, "error": str(e), "note": "For Chinese paths, use run_python tool"}
161
+ return {"success": False, "error": str(e)}
132
162
 
133
163
 
134
164
  @mcp.tool()
135
- def save_file(file_path: str = None) -> dict:
136
- """Save the current active document."""
137
- 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:
138
174
  try:
139
- app = Officer.Application(app_name)
140
- if app_name == 'Excel':
175
+ app = Officer.Application(app_name_check)
176
+ if app is None:
177
+ continue
178
+ if app_name_check == 'Excel':
141
179
  doc = app.ActiveWorkbook
142
- elif app_name == 'Word':
180
+ elif app_name_check == 'Word':
143
181
  doc = app.ActiveDocument
144
182
  else:
145
183
  doc = app.ActivePresentation
@@ -150,7 +188,7 @@ def save_file(file_path: str = None) -> dict:
150
188
  else:
151
189
  doc.Save()
152
190
  return {"success": True, "file": doc.FullName}
153
- except:
191
+ except Exception:
154
192
  continue
155
193
  return {"success": False, "error": "No active document found"}
156
194
 
@@ -163,12 +201,12 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
163
201
  Execute Python code to control Office applications (PRIMARY TOOL).
164
202
 
165
203
  Available global variables:
166
- - app: The Office application object
167
- - doc: The current active document
168
- - sheet: Active worksheet (Excel)
169
- - selection: Current selection (Word)
170
- - slide: Active slide (PowerPoint)
171
- - 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
172
210
 
173
211
  Excel example:
174
212
  sheet.Cells(1, 1).Value = "Hello"
@@ -192,11 +230,11 @@ def run_python(code: str, app_name: str = "Excel") -> dict:
192
230
  try:
193
231
  exec(code, globals_dict)
194
232
  output = buffer.getvalue()
195
- sys.stdout = old_stdout
196
233
  return {"success": True, "output": output or "(无输出)"}
197
234
  except Exception as e:
198
- sys.stdout = old_stdout
199
235
  return {"success": False, "error": str(e)}
236
+ finally:
237
+ sys.stdout = old_stdout
200
238
 
201
239
 
202
240
  # ========== UIA Tools ==========
@@ -222,102 +260,13 @@ def screenshot(target: str = None, save_name: str = None) -> dict:
222
260
  return {"success": False, "error": str(e)}
223
261
 
224
262
 
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
- """
241
- try:
242
- path = Officer.UIA.screenshot_element(element_name, control_type, parent_name, save_name)
243
- return {"success": True, "path": path}
244
- except Exception as e:
245
- return {"success": False, "error": str(e)}
246
-
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
263
  @mcp.tool()
318
264
  def get_window_list() -> dict:
319
265
  """List all top-level windows."""
320
266
  try:
267
+ if not hasattr(Officer, 'UIA') or not Officer.UIA.available:
268
+ return {"success": False, "error": "UIA automation module not available"}
269
+
321
270
  windows = Officer.UIA.get_window_list()
322
271
  return {"success": True, "windows": windows}
323
272
  except Exception as e:
@@ -328,6 +277,9 @@ def get_window_list() -> dict:
328
277
  def activate_window(window_name: str) -> dict:
329
278
  """Activate a window by name."""
330
279
  try:
280
+ if not hasattr(Officer, 'UIA') or not Officer.UIA.available:
281
+ return {"success": False, "error": "UIA automation module not available"}
282
+
331
283
  result = Officer.UIA.activate_window(window_name)
332
284
  return {"success": result}
333
285
  except Exception as e:
@@ -337,9 +289,16 @@ def activate_window(window_name: str) -> dict:
337
289
  # ========== System Tools ==========
338
290
 
339
291
  @mcp.tool()
340
- def download(url: str, save_name: str = None) -> str:
292
+ def download(url: str, save_name: str = None) -> dict:
341
293
  """Download a file from URL to the download folder."""
342
- return Officer.DownloadURL(url, save_name)
294
+ try:
295
+ result = Officer.DownloadURL(url, save_name)
296
+ if result:
297
+ return {"success": True, "file_path": result}
298
+ else:
299
+ return {"success": False, "error": "Download failed or returned empty path"}
300
+ except Exception as e:
301
+ return {"success": False, "error": str(e)}
343
302
 
344
303
 
345
304
  @mcp.tool()