@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 +26 -0
- package/README.md +4 -10
- package/package.json +2 -1
- package/src/officer.py +1 -1
- package/src/server.py +68 -126
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.
|
|
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
package/src/server.py
CHANGED
|
@@ -13,12 +13,19 @@ import sys
|
|
|
13
13
|
import os
|
|
14
14
|
from io import StringIO
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
verify_license
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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'] =
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
154
|
+
doc = app.Documents.Open(open_path)
|
|
133
155
|
elif app_name == 'PowerPoint':
|
|
134
|
-
doc = app.Presentations.Open(
|
|
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)
|
|
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
|
-
|
|
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(
|
|
175
|
+
app = Officer.Application(app_name_check)
|
|
148
176
|
if app is None:
|
|
149
177
|
continue
|
|
150
|
-
if
|
|
178
|
+
if app_name_check == 'Excel':
|
|
151
179
|
doc = app.ActiveWorkbook
|
|
152
|
-
elif
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|