lightpdf-aipdf-mcp 0.1.42__py3-none-any.whl → 0.1.44__py3-none-any.whl
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.
- lightpdf_aipdf_mcp/common.py +3 -2
- lightpdf_aipdf_mcp/converter.py +39 -24
- lightpdf_aipdf_mcp/editor.py +85 -79
- lightpdf_aipdf_mcp/server.py +186 -96
- {lightpdf_aipdf_mcp-0.1.42.dist-info → lightpdf_aipdf_mcp-0.1.44.dist-info}/METADATA +3 -2
- lightpdf_aipdf_mcp-0.1.44.dist-info/RECORD +9 -0
- lightpdf_aipdf_mcp-0.1.42.dist-info/RECORD +0 -9
- {lightpdf_aipdf_mcp-0.1.42.dist-info → lightpdf_aipdf_mcp-0.1.44.dist-info}/WHEEL +0 -0
- {lightpdf_aipdf_mcp-0.1.42.dist-info → lightpdf_aipdf_mcp-0.1.44.dist-info}/entry_points.txt +0 -0
lightpdf_aipdf_mcp/common.py
CHANGED
@@ -5,7 +5,7 @@ import os
|
|
5
5
|
import time
|
6
6
|
from dataclasses import dataclass
|
7
7
|
from typing import List, Optional, Dict, Any, Tuple
|
8
|
-
|
8
|
+
import urllib.parse
|
9
9
|
import httpx
|
10
10
|
|
11
11
|
@dataclass
|
@@ -15,6 +15,7 @@ class BaseResult:
|
|
15
15
|
file_path: str
|
16
16
|
error_message: Optional[str] = None
|
17
17
|
download_url: Optional[str] = None
|
18
|
+
original_name: Optional[str] = None
|
18
19
|
|
19
20
|
class Logger:
|
20
21
|
"""日志记录器类"""
|
@@ -57,7 +58,7 @@ class FileHandler:
|
|
57
58
|
@staticmethod
|
58
59
|
def is_url(path: str) -> bool:
|
59
60
|
"""检查路径是否为URL"""
|
60
|
-
return path.startswith(("http://", "https://"))
|
61
|
+
return path.startswith(("http://", "https://", "oss://"))
|
61
62
|
|
62
63
|
@staticmethod
|
63
64
|
def is_oss_id(path: str) -> bool:
|
lightpdf_aipdf_mcp/converter.py
CHANGED
@@ -103,9 +103,10 @@ class Converter(BaseApiClient):
|
|
103
103
|
"""PDF文档转换器"""
|
104
104
|
def __init__(self, logger: Logger, file_handler: FileHandler):
|
105
105
|
super().__init__(logger, file_handler)
|
106
|
-
|
106
|
+
api_endpoint = os.environ.get("API_ENDPOINT", "devaw.aoscdn.com/tech")
|
107
|
+
self.api_base_url = f"https://{api_endpoint}/tasks/document/conversion"
|
107
108
|
|
108
|
-
async def add_page_numbers(self, file_path: str, start_num: int = 1, position: str = "5", margin: int = 30, password: str = None) -> ConversionResult:
|
109
|
+
async def add_page_numbers(self, file_path: str, start_num: int = 1, position: str = "5", margin: int = 30, password: str = None, original_name: Optional[str] = None) -> ConversionResult:
|
109
110
|
"""为PDF文档添加页码
|
110
111
|
|
111
112
|
Args:
|
@@ -114,6 +115,7 @@ class Converter(BaseApiClient):
|
|
114
115
|
position: 页码显示位置,字符串类型,可选值1-6(左上/上中/右上/左下/下中/右下),默认为5(下中)
|
115
116
|
margin: 页码显示的边距,整数类型,可选值10/30/60,默认为30
|
116
117
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
118
|
+
original_name: 原始文件名(可选)
|
117
119
|
|
118
120
|
Returns:
|
119
121
|
ConversionResult: 转换结果
|
@@ -122,6 +124,7 @@ class Converter(BaseApiClient):
|
|
122
124
|
input_format = self.file_handler.get_input_format(file_path)
|
123
125
|
if input_format != InputFormat.PDF:
|
124
126
|
await self.logger.error(f"添加页码功能仅支持PDF文件,当前文件格式为: {self.file_handler.get_file_extension(file_path)}")
|
127
|
+
return ConversionResult(success=False, file_path=file_path, error_message="添加页码功能仅支持PDF文件", original_name=original_name)
|
125
128
|
|
126
129
|
# 验证参数
|
127
130
|
valid_positions = {"1", "2", "3", "4", "5", "6"}
|
@@ -129,15 +132,21 @@ class Converter(BaseApiClient):
|
|
129
132
|
|
130
133
|
# 验证position参数
|
131
134
|
if position not in valid_positions:
|
132
|
-
|
135
|
+
error_msg = f"无效的页码位置值: {position}。有效值为: 1(左上), 2(上中), 3(右上), 4(左下), 5(下中), 6(右下)"
|
136
|
+
await self.logger.error(error_msg)
|
137
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
133
138
|
|
134
139
|
# 验证margin参数
|
135
140
|
if margin not in valid_margins:
|
136
|
-
|
141
|
+
error_msg = f"无效的页码边距值: {margin}。有效值为: 10, 30, 60"
|
142
|
+
await self.logger.error(error_msg)
|
143
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
137
144
|
|
138
145
|
# 验证start_num是否为正整数
|
139
146
|
if not isinstance(start_num, int) or start_num < 1:
|
140
|
-
|
147
|
+
error_msg = f"起始页码必须是正整数,当前值为: {start_num}"
|
148
|
+
await self.logger.error(error_msg)
|
149
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
141
150
|
|
142
151
|
# 构建API参数
|
143
152
|
extra_params = {
|
@@ -150,9 +159,9 @@ class Converter(BaseApiClient):
|
|
150
159
|
await self.logger.log("info", f"正在为PDF添加页码(起始页码: {start_num}, 位置: {position}, 边距: {margin})...")
|
151
160
|
|
152
161
|
# 调用convert_file方法处理API请求
|
153
|
-
return await self.convert_file(file_path, "number-pdf", extra_params, password)
|
162
|
+
return await self.convert_file(file_path, "number-pdf", extra_params, password, original_name)
|
154
163
|
|
155
|
-
async def convert_file(self, file_path: str, format: str, extra_params: dict = None, password: str = None) -> ConversionResult:
|
164
|
+
async def convert_file(self, file_path: str, format: str, extra_params: dict = None, password: str = None, original_name: Optional[str] = None) -> ConversionResult:
|
156
165
|
"""转换单个文件
|
157
166
|
|
158
167
|
Args:
|
@@ -160,12 +169,14 @@ class Converter(BaseApiClient):
|
|
160
169
|
format: 目标格式
|
161
170
|
extra_params: 额外的API参数,例如去除水印
|
162
171
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
172
|
+
original_name: 原始文件名(可选)
|
163
173
|
|
164
174
|
Returns:
|
165
175
|
ConversionResult: 转换结果
|
166
176
|
"""
|
167
177
|
if not self.api_key:
|
168
178
|
await self.logger.error("未找到API_KEY。请在客户端配置API_KEY环境变量。")
|
179
|
+
return ConversionResult(success=False, file_path=file_path, error_message="未找到API_KEY", original_name=original_name)
|
169
180
|
|
170
181
|
# 特殊格式:doc-repair用于去除水印,number-pdf用于添加页码,输出均为PDF
|
171
182
|
is_special_operation = format in ["doc-repair", "number-pdf"]
|
@@ -180,38 +191,40 @@ class Converter(BaseApiClient):
|
|
180
191
|
# 验证输入文件格式
|
181
192
|
input_format = self.file_handler.get_input_format(file_path)
|
182
193
|
if not input_format and not is_special_operation:
|
183
|
-
|
194
|
+
error_msg = f"不支持的输入文件格式: {self.file_handler.get_file_extension(file_path)}"
|
195
|
+
await self.logger.error(error_msg)
|
196
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
184
197
|
|
185
198
|
# 如果是去除水印操作,检查是否PDF文件
|
186
199
|
if format == "doc-repair" and input_format != InputFormat.PDF:
|
187
|
-
|
200
|
+
error_msg = "去除水印功能仅支持PDF文件"
|
201
|
+
await self.logger.error(error_msg)
|
202
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
188
203
|
|
189
204
|
# 如果是添加页码操作,检查是否PDF文件
|
190
205
|
if format == "number-pdf" and input_format != InputFormat.PDF:
|
191
|
-
|
206
|
+
error_msg = "添加页码功能仅支持PDF文件"
|
207
|
+
await self.logger.error(error_msg)
|
208
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
192
209
|
|
193
210
|
# 验证输出格式(除去特殊操作外)
|
194
211
|
if not is_special_operation:
|
195
212
|
try:
|
196
213
|
output_format = OutputFormat(format)
|
197
214
|
except ValueError:
|
198
|
-
|
215
|
+
error_msg = f"不支持的输出格式: {format}"
|
216
|
+
await self.logger.error(error_msg)
|
217
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
199
218
|
|
200
|
-
# 验证格式转换是否支持
|
201
|
-
if input_format: # 确保input_format有效
|
202
|
-
available_formats = self.file_handler.get_available_output_formats(input_format)
|
203
|
-
if output_format not in available_formats:
|
204
|
-
await self.logger.error(
|
205
|
-
f"不支持从 {input_format.value} 格式转换为 {output_format.value} 格式。"
|
206
|
-
f"支持的输出格式: {', '.join(f.value for f in available_formats)}"
|
207
|
-
)
|
208
219
|
else:
|
209
220
|
# 远程路径的情况,设置必要的变量以便后续使用
|
210
221
|
if not is_special_operation:
|
211
222
|
try:
|
212
223
|
output_format = OutputFormat(format)
|
213
224
|
except ValueError:
|
214
|
-
|
225
|
+
error_msg = f"不支持的输出格式: {format}"
|
226
|
+
await self.logger.error(error_msg)
|
227
|
+
return ConversionResult(success=False, file_path=file_path, error_message=error_msg, original_name=original_name)
|
215
228
|
else:
|
216
229
|
output_format = OutputFormat.PDF
|
217
230
|
|
@@ -221,7 +234,7 @@ class Converter(BaseApiClient):
|
|
221
234
|
# 验证文件
|
222
235
|
exists = await self.file_handler.validate_file_exists(file_path)
|
223
236
|
if not exists:
|
224
|
-
return ConversionResult(success=False, file_path=file_path, error_message="文件不存在")
|
237
|
+
return ConversionResult(success=False, file_path=file_path, error_message="文件不存在", original_name=original_name)
|
225
238
|
|
226
239
|
# 操作描述
|
227
240
|
if format == "doc-repair":
|
@@ -239,7 +252,7 @@ class Converter(BaseApiClient):
|
|
239
252
|
await self.logger.log("info", f"正在{operation_desc}...")
|
240
253
|
|
241
254
|
import httpx
|
242
|
-
async with httpx.AsyncClient(timeout=
|
255
|
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
243
256
|
try:
|
244
257
|
# 初始化extra_params(如果为None)
|
245
258
|
if extra_params is None:
|
@@ -262,7 +275,8 @@ class Converter(BaseApiClient):
|
|
262
275
|
success=True,
|
263
276
|
file_path=file_path,
|
264
277
|
error_message=None,
|
265
|
-
download_url=download_url
|
278
|
+
download_url=download_url,
|
279
|
+
original_name=original_name
|
266
280
|
)
|
267
281
|
|
268
282
|
except Exception as e:
|
@@ -270,7 +284,8 @@ class Converter(BaseApiClient):
|
|
270
284
|
success=False,
|
271
285
|
file_path=file_path,
|
272
286
|
error_message=str(e),
|
273
|
-
download_url=None
|
287
|
+
download_url=None,
|
288
|
+
original_name=original_name
|
274
289
|
)
|
275
290
|
|
276
291
|
async def _create_task(self, client: httpx.AsyncClient, file_path: str, format: str, extra_params: dict = None) -> str:
|
lightpdf_aipdf_mcp/editor.py
CHANGED
@@ -29,7 +29,8 @@ class Editor(BaseApiClient):
|
|
29
29
|
"""PDF文档编辑器"""
|
30
30
|
def __init__(self, logger: Logger, file_handler: FileHandler):
|
31
31
|
super().__init__(logger, file_handler)
|
32
|
-
|
32
|
+
api_endpoint = os.environ.get("API_ENDPOINT", "devaw.aoscdn.com/tech")
|
33
|
+
self.api_base_url = f"https://{api_endpoint}/tasks/document/pdfedit"
|
33
34
|
|
34
35
|
async def _validate_pdf_file(self, file_path: str) -> bool:
|
35
36
|
"""验证文件是否为PDF格式
|
@@ -63,7 +64,7 @@ class Editor(BaseApiClient):
|
|
63
64
|
log_msg += "..."
|
64
65
|
await self.logger.log("info", log_msg)
|
65
66
|
|
66
|
-
async def split_pdf(self, file_path: str, pages: str = "", password: Optional[str] = None, split_type: str = "page", merge_all: int = 1) -> EditResult:
|
67
|
+
async def split_pdf(self, file_path: str, pages: str = "", password: Optional[str] = None, split_type: str = "page", merge_all: int = 1, original_name: Optional[str] = None) -> EditResult:
|
67
68
|
"""拆分PDF文件
|
68
69
|
|
69
70
|
Args:
|
@@ -72,19 +73,20 @@ class Editor(BaseApiClient):
|
|
72
73
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
73
74
|
split_type: 拆分类型,可选值: "every"=每页拆分为一个文件, "page"=指定页面规则拆分,默认为"page"
|
74
75
|
merge_all: 是否合并拆分后的文件,仅在split_type="page"时有效,0=不合并,1=合并,默认为1
|
76
|
+
original_name: 原始文件名(可选)
|
75
77
|
|
76
78
|
Returns:
|
77
79
|
EditResult: 拆分结果
|
78
80
|
"""
|
79
81
|
# 验证输入文件是否为PDF
|
80
82
|
if not await self._validate_pdf_file(file_path):
|
81
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
83
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
82
84
|
|
83
85
|
# 验证拆分类型
|
84
86
|
valid_split_types = {"every", "page"}
|
85
87
|
if split_type not in valid_split_types:
|
86
88
|
await self.logger.error(f"无效的拆分类型: {split_type}。有效值为: every, page")
|
87
|
-
return EditResult(success=False, file_path=file_path, error_message=f"无效的拆分类型: {split_type}。有效值为: every, page")
|
89
|
+
return EditResult(success=False, file_path=file_path, error_message=f"无效的拆分类型: {split_type}。有效值为: every, page", original_name=original_name)
|
88
90
|
|
89
91
|
# 构建API参数
|
90
92
|
extra_params = {
|
@@ -103,30 +105,31 @@ class Editor(BaseApiClient):
|
|
103
105
|
await self._log_operation("拆分PDF文件", operation_details)
|
104
106
|
|
105
107
|
# 调用edit_pdf方法处理API请求
|
106
|
-
return await self.edit_pdf(file_path, EditType.SPLIT, extra_params, password)
|
108
|
+
return await self.edit_pdf(file_path, EditType.SPLIT, extra_params, password, original_name)
|
107
109
|
|
108
|
-
async def merge_pdfs(self, file_paths: List[str], password: Optional[str] = None) -> EditResult:
|
110
|
+
async def merge_pdfs(self, file_paths: List[str], password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
109
111
|
"""合并多个PDF文件
|
110
112
|
|
111
113
|
Args:
|
112
114
|
file_paths: 要合并的PDF文件路径列表
|
113
115
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
116
|
+
original_name: 原始文件名(可选)
|
114
117
|
|
115
118
|
Returns:
|
116
119
|
EditResult: 合并结果
|
117
120
|
"""
|
118
121
|
if len(file_paths) < 2:
|
119
122
|
await self.logger.error("合并PDF至少需要两个文件")
|
120
|
-
return EditResult(success=False, file_path=','.join(file_paths), error_message="合并PDF至少需要两个文件")
|
123
|
+
return EditResult(success=False, file_path=','.join(file_paths), error_message="合并PDF至少需要两个文件", original_name=original_name)
|
121
124
|
|
122
125
|
# 验证所有文件是否都是PDF并且存在
|
123
126
|
for pdf_file in file_paths:
|
124
127
|
if not await self._validate_pdf_file(pdf_file):
|
125
|
-
return EditResult(success=False, file_path=pdf_file, error_message="非PDF文件")
|
128
|
+
return EditResult(success=False, file_path=pdf_file, error_message="非PDF文件", original_name=original_name)
|
126
129
|
|
127
130
|
exists = await self.file_handler.validate_file_exists(pdf_file)
|
128
131
|
if not exists:
|
129
|
-
return EditResult(success=False, file_path=pdf_file, error_message="文件不存在")
|
132
|
+
return EditResult(success=False, file_path=pdf_file, error_message="文件不存在", original_name=original_name)
|
130
133
|
|
131
134
|
# 记录操作描述
|
132
135
|
await self._log_operation("合并PDF文件", f"{len(file_paths)} 个文件")
|
@@ -147,7 +150,8 @@ class Editor(BaseApiClient):
|
|
147
150
|
success=True,
|
148
151
|
file_path=file_paths[0], # 使用第一个文件路径作为参考
|
149
152
|
error_message=None,
|
150
|
-
download_url=download_url
|
153
|
+
download_url=download_url,
|
154
|
+
original_name=original_name
|
151
155
|
)
|
152
156
|
|
153
157
|
except Exception as e:
|
@@ -155,10 +159,11 @@ class Editor(BaseApiClient):
|
|
155
159
|
success=False,
|
156
160
|
file_path=file_paths[0],
|
157
161
|
error_message=str(e),
|
158
|
-
download_url=None
|
162
|
+
download_url=None,
|
163
|
+
original_name=original_name
|
159
164
|
)
|
160
165
|
|
161
|
-
async def rotate_pdf(self, file_path: str, angle: int, pages: str = "", password: Optional[str] = None) -> EditResult:
|
166
|
+
async def rotate_pdf(self, file_path: str, angle: int, pages: str = "", password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
162
167
|
"""旋转PDF文件的页面
|
163
168
|
|
164
169
|
Args:
|
@@ -166,19 +171,20 @@ class Editor(BaseApiClient):
|
|
166
171
|
angle: 旋转角度,可选值为90、180、270
|
167
172
|
pages: 指定要旋转的页面范围,例如 "1,3,5-7" 或 "" 表示所有页面
|
168
173
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
174
|
+
original_name: 原始文件名(可选)
|
169
175
|
|
170
176
|
Returns:
|
171
177
|
EditResult: 旋转结果
|
172
178
|
"""
|
173
179
|
# 验证输入文件是否为PDF
|
174
180
|
if not await self._validate_pdf_file(file_path):
|
175
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
181
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
176
182
|
|
177
183
|
# 验证旋转角度
|
178
184
|
valid_angles = {90, 180, 270}
|
179
185
|
if angle not in valid_angles:
|
180
186
|
await self.logger.error("无效的旋转角度。角度必须是: 90, 180, 270")
|
181
|
-
return EditResult(success=False, file_path=file_path, error_message="无效的旋转角度。角度必须是: 90, 180, 270")
|
187
|
+
return EditResult(success=False, file_path=file_path, error_message="无效的旋转角度。角度必须是: 90, 180, 270", original_name=original_name)
|
182
188
|
|
183
189
|
# 构建API参数
|
184
190
|
extra_params = {
|
@@ -190,27 +196,28 @@ class Editor(BaseApiClient):
|
|
190
196
|
await self._log_operation("旋转PDF文件", f"参数: {angle_desc}")
|
191
197
|
|
192
198
|
# 调用edit_pdf方法处理API请求
|
193
|
-
return await self.edit_pdf(file_path, EditType.ROTATE, extra_params, password)
|
199
|
+
return await self.edit_pdf(file_path, EditType.ROTATE, extra_params, password, original_name)
|
194
200
|
|
195
|
-
async def compress_pdf(self, file_path: str, image_quantity: int = 60, password: Optional[str] = None) -> EditResult:
|
201
|
+
async def compress_pdf(self, file_path: str, image_quantity: int = 60, password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
196
202
|
"""压缩PDF文件
|
197
203
|
|
198
204
|
Args:
|
199
205
|
file_path: 要压缩的PDF文件路径
|
200
206
|
image_quantity: 图片质量,范围1-100,默认为60
|
201
207
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
208
|
+
original_name: 原始文件名(可选)
|
202
209
|
|
203
210
|
Returns:
|
204
211
|
EditResult: 压缩结果
|
205
212
|
"""
|
206
213
|
# 验证输入文件是否为PDF
|
207
214
|
if not await self._validate_pdf_file(file_path):
|
208
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
215
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
209
216
|
|
210
|
-
#
|
211
|
-
if not 1 <= image_quantity <= 100:
|
212
|
-
await self.logger.error(
|
213
|
-
return EditResult(success=False, file_path=file_path, error_message=
|
217
|
+
# 验证图片质量范围
|
218
|
+
if not (1 <= image_quantity <= 100):
|
219
|
+
await self.logger.error("图片质量必须在1到100之间")
|
220
|
+
return EditResult(success=False, file_path=file_path, error_message="图片质量必须在1到100之间", original_name=original_name)
|
214
221
|
|
215
222
|
# 构建API参数
|
216
223
|
extra_params = {
|
@@ -221,60 +228,65 @@ class Editor(BaseApiClient):
|
|
221
228
|
await self._log_operation("压缩PDF文件", f"图片质量: {image_quantity}")
|
222
229
|
|
223
230
|
# 调用edit_pdf方法处理API请求
|
224
|
-
return await self.edit_pdf(file_path, EditType.COMPRESS, extra_params, password)
|
231
|
+
return await self.edit_pdf(file_path, EditType.COMPRESS, extra_params, password, original_name)
|
225
232
|
|
226
|
-
async def encrypt_pdf(self, file_path: str, password: str, original_password: Optional[str] = None) -> EditResult:
|
233
|
+
async def encrypt_pdf(self, file_path: str, password: str, original_password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
227
234
|
"""加密PDF文件
|
228
235
|
|
229
236
|
Args:
|
230
237
|
file_path: 要加密的PDF文件路径
|
231
|
-
password:
|
232
|
-
original_password:
|
233
|
-
|
234
|
-
注意:
|
235
|
-
根据API文档,加密操作需要通过password参数指定要设置的新密码。
|
236
|
-
如果文档已经受密码保护,则使用original_password参数提供原密码进行解锁。
|
238
|
+
password: 要设置的新密码
|
239
|
+
original_password: 原始密码,如果文件已经加密,则需要提供(可选)
|
240
|
+
original_name: 原始文件名(可选)
|
237
241
|
|
238
242
|
Returns:
|
239
243
|
EditResult: 加密结果
|
240
244
|
"""
|
241
245
|
# 验证输入文件是否为PDF
|
242
246
|
if not await self._validate_pdf_file(file_path):
|
243
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
247
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
248
|
+
|
249
|
+
# 验证新密码
|
250
|
+
if not password:
|
251
|
+
await self.logger.error("加密PDF文件需要提供新密码")
|
252
|
+
return EditResult(success=False, file_path=file_path, error_message="加密PDF文件需要提供新密码", original_name=original_name)
|
244
253
|
|
245
254
|
# 构建API参数
|
246
255
|
extra_params = {
|
247
|
-
"password": password #
|
256
|
+
"password": password # 新密码
|
248
257
|
}
|
249
258
|
|
250
259
|
# 记录操作描述
|
251
260
|
await self._log_operation("加密PDF文件")
|
252
261
|
|
253
262
|
# 调用edit_pdf方法处理API请求
|
254
|
-
return await self.edit_pdf(file_path, EditType.ENCRYPT, extra_params, original_password)
|
263
|
+
return await self.edit_pdf(file_path, EditType.ENCRYPT, extra_params, original_password, original_name)
|
255
264
|
|
256
|
-
async def decrypt_pdf(self, file_path: str, password: Optional[str] = None) -> EditResult:
|
265
|
+
async def decrypt_pdf(self, file_path: str, password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
257
266
|
"""解密PDF文件
|
258
267
|
|
259
268
|
Args:
|
260
269
|
file_path: 要解密的PDF文件路径
|
261
|
-
password:
|
262
|
-
|
263
|
-
注意:
|
264
|
-
该方法调用API的unlock功能,需要提供正确的PDF密码才能成功解密。
|
270
|
+
password: 文档密码,用于解锁已加密的文档
|
271
|
+
original_name: 原始文件名(可选)
|
265
272
|
|
266
273
|
Returns:
|
267
274
|
EditResult: 解密结果
|
268
275
|
"""
|
269
276
|
# 验证输入文件是否为PDF
|
270
277
|
if not await self._validate_pdf_file(file_path):
|
271
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
278
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
279
|
+
|
280
|
+
# 验证密码
|
281
|
+
if not password:
|
282
|
+
await self.logger.error("解密PDF文件需要提供密码")
|
283
|
+
return EditResult(success=False, file_path=file_path, error_message="解密PDF文件需要提供密码", original_name=original_name)
|
272
284
|
|
273
285
|
# 记录操作描述
|
274
286
|
await self._log_operation("解密PDF文件")
|
275
287
|
|
276
288
|
# 调用edit_pdf方法处理API请求
|
277
|
-
return await self.edit_pdf(file_path, EditType.DECRYPT, {}, password)
|
289
|
+
return await self.edit_pdf(file_path, EditType.DECRYPT, {}, password, original_name)
|
278
290
|
|
279
291
|
async def add_watermark(
|
280
292
|
self,
|
@@ -282,56 +294,47 @@ class Editor(BaseApiClient):
|
|
282
294
|
text: str,
|
283
295
|
position: str, # 必需参数:位置,如"top", "center", "diagonal"等
|
284
296
|
opacity: float = 0.5,
|
285
|
-
deg: str = "-45", # 直接使用字符串格式的角度
|
286
297
|
range: str = "", # 与API保持一致,使用range而非pages
|
287
298
|
layout: Optional[str] = None, # 可选参数: "on"/"under"
|
288
299
|
font_family: Optional[str] = None,
|
289
300
|
font_size: Optional[int] = None,
|
290
301
|
font_color: Optional[str] = None,
|
291
|
-
password: Optional[str] = None
|
302
|
+
password: Optional[str] = None,
|
303
|
+
original_name: Optional[str] = None
|
292
304
|
) -> EditResult:
|
293
|
-
"""为PDF
|
305
|
+
"""为PDF文件添加文本水印
|
294
306
|
|
295
307
|
Args:
|
296
308
|
file_path: 要添加水印的PDF文件路径
|
297
309
|
text: 水印文本内容
|
298
|
-
position:
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
range: 指定页面范围,例如 "1,3,5-7" 或 "" 表示所有页面
|
303
|
-
layout: 布局位置,可选值:"on"(在内容上)/"under"(在内容下)
|
310
|
+
position: 水印位置,如"top", "center", "diagonal"等
|
311
|
+
opacity: 透明度,0.0-1.0,默认0.5
|
312
|
+
range: 页面范围,例如 "1,3,5-7" 或空字符串表示所有页面
|
313
|
+
layout: 布局方式:"on"=在内容上,"under"=在内容下,默认"on"
|
304
314
|
font_family: 字体
|
305
315
|
font_size: 字体大小
|
306
|
-
font_color: 字体颜色,如"#ff0000"
|
316
|
+
font_color: 字体颜色,如 "#ff0000"
|
307
317
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
318
|
+
original_name: 原始文件名(可选)
|
308
319
|
|
309
320
|
Returns:
|
310
321
|
EditResult: 添加水印结果
|
311
322
|
"""
|
312
323
|
# 验证输入文件是否为PDF
|
313
324
|
if not await self._validate_pdf_file(file_path):
|
314
|
-
return EditResult(success=False, file_path=file_path, error_message="非PDF文件")
|
325
|
+
return EditResult(success=False, file_path=file_path, error_message="非PDF文件", original_name=original_name)
|
315
326
|
|
316
|
-
#
|
317
|
-
if not
|
318
|
-
await self.logger.error(
|
319
|
-
return EditResult(success=False, file_path=file_path, error_message=
|
320
|
-
|
321
|
-
# 验证position参数
|
322
|
-
valid_positions = {"topleft", "top", "topright", "left", "center", "right",
|
323
|
-
"bottomleft", "bottom", "bottomright", "diagonal", "reverse-diagonal"}
|
324
|
-
if position not in valid_positions:
|
325
|
-
await self.logger.error(f"无效的位置: {position}")
|
326
|
-
return EditResult(success=False, file_path=file_path, error_message=f"无效的位置: {position}")
|
327
|
+
# 验证必需参数
|
328
|
+
if not text:
|
329
|
+
await self.logger.error("水印文本不能为空")
|
330
|
+
return EditResult(success=False, file_path=file_path, error_message="水印文本不能为空", original_name=original_name)
|
327
331
|
|
328
332
|
# 构建API参数
|
329
333
|
extra_params = {
|
330
|
-
"edit_type": "text",
|
334
|
+
"edit_type": "text",
|
331
335
|
"text": text,
|
332
336
|
"position": position,
|
333
337
|
"opacity": opacity,
|
334
|
-
"deg": deg,
|
335
338
|
"range": range
|
336
339
|
}
|
337
340
|
|
@@ -349,9 +352,9 @@ class Editor(BaseApiClient):
|
|
349
352
|
await self._log_operation("为PDF添加水印", f"文本: {text}, 位置: {position}, 透明度: {opacity}")
|
350
353
|
|
351
354
|
# 调用edit_pdf方法处理API请求
|
352
|
-
return await self.edit_pdf(file_path, EditType.ADD_WATERMARK, extra_params, password)
|
355
|
+
return await self.edit_pdf(file_path, EditType.ADD_WATERMARK, extra_params, password, original_name)
|
353
356
|
|
354
|
-
async def edit_pdf(self, file_path: str, edit_type: EditType, extra_params: Dict[str, Any] = None, password: Optional[str] = None) -> EditResult:
|
357
|
+
async def edit_pdf(self, file_path: str, edit_type: EditType, extra_params: Dict[str, Any] = None, password: Optional[str] = None, original_name: Optional[str] = None) -> EditResult:
|
355
358
|
"""编辑PDF文件
|
356
359
|
|
357
360
|
Args:
|
@@ -359,6 +362,7 @@ class Editor(BaseApiClient):
|
|
359
362
|
edit_type: 编辑操作类型
|
360
363
|
extra_params: 额外的API参数
|
361
364
|
password: 文档密码,如果文档受密码保护,则需要提供(可选)
|
365
|
+
original_name: 原始文件名(可选)
|
362
366
|
|
363
367
|
注意:
|
364
368
|
1. 对于加密操作(protect),需要在extra_params中提供新密码
|
@@ -370,14 +374,14 @@ class Editor(BaseApiClient):
|
|
370
374
|
"""
|
371
375
|
if not self.api_key:
|
372
376
|
await self.logger.error("未找到API_KEY。请在客户端配置API_KEY环境变量。")
|
373
|
-
return EditResult(success=False, file_path=file_path, error_message="未找到API_KEY。请在客户端配置API_KEY环境变量。")
|
377
|
+
return EditResult(success=False, file_path=file_path, error_message="未找到API_KEY。请在客户端配置API_KEY环境变量。", original_name=original_name)
|
374
378
|
|
375
379
|
# 验证文件
|
376
380
|
exists = await self.file_handler.validate_file_exists(file_path)
|
377
381
|
if not exists:
|
378
|
-
return EditResult(success=False, file_path=file_path, error_message="文件不存在")
|
382
|
+
return EditResult(success=False, file_path=file_path, error_message="文件不存在", original_name=original_name)
|
379
383
|
|
380
|
-
async with httpx.AsyncClient(timeout=
|
384
|
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
381
385
|
try:
|
382
386
|
# 初始化extra_params(如果为None)
|
383
387
|
if extra_params is None:
|
@@ -400,7 +404,8 @@ class Editor(BaseApiClient):
|
|
400
404
|
success=True,
|
401
405
|
file_path=file_path,
|
402
406
|
error_message=None,
|
403
|
-
download_url=download_url
|
407
|
+
download_url=download_url,
|
408
|
+
original_name=original_name
|
404
409
|
)
|
405
410
|
|
406
411
|
except Exception as e:
|
@@ -408,7 +413,8 @@ class Editor(BaseApiClient):
|
|
408
413
|
success=False,
|
409
414
|
file_path=file_path,
|
410
415
|
error_message=str(e),
|
411
|
-
download_url=None
|
416
|
+
download_url=None,
|
417
|
+
original_name=original_name
|
412
418
|
)
|
413
419
|
|
414
420
|
async def _create_task(self, client: httpx.AsyncClient, file_path: str, edit_type: EditType, extra_params: Dict[str, Any] = None) -> str:
|
@@ -434,10 +440,10 @@ class Editor(BaseApiClient):
|
|
434
440
|
|
435
441
|
# 检查是否为OSS路径
|
436
442
|
if self.file_handler.is_oss_id(file_path):
|
437
|
-
# OSS路径处理方式,与URL类似,但提取resource_id
|
438
|
-
data["resource_id"] = file_path.split("oss_id://")[1]
|
439
443
|
# 使用JSON方式时添加Content-Type
|
440
444
|
headers["Content-Type"] = "application/json"
|
445
|
+
# OSS路径处理方式,与URL类似,但提取resource_id
|
446
|
+
data["resource_id"] = file_path.split("oss_id://")[1]
|
441
447
|
response = await client.post(
|
442
448
|
self.api_base_url,
|
443
449
|
json=data,
|
@@ -445,9 +451,9 @@ class Editor(BaseApiClient):
|
|
445
451
|
)
|
446
452
|
# 检查是否为URL路径
|
447
453
|
elif self.file_handler.is_url(file_path):
|
448
|
-
data["url"] = file_path
|
449
454
|
# 使用JSON方式时添加Content-Type
|
450
455
|
headers["Content-Type"] = "application/json"
|
456
|
+
data["url"] = file_path
|
451
457
|
response = await client.post(
|
452
458
|
self.api_base_url,
|
453
459
|
json=data,
|
@@ -492,15 +498,15 @@ class Editor(BaseApiClient):
|
|
492
498
|
|
493
499
|
for i, file_path in enumerate(file_paths):
|
494
500
|
# 检查是否为URL或OSS路径
|
495
|
-
if self.file_handler.
|
496
|
-
# 对于
|
497
|
-
input_item = {"
|
501
|
+
if self.file_handler.is_oss_id(file_path):
|
502
|
+
# 对于OSS路径,添加到inputs数组
|
503
|
+
input_item = {"resource_id": file_path.split("oss_id://")[1]}
|
498
504
|
if password:
|
499
505
|
input_item["password"] = password
|
500
506
|
url_inputs.append(input_item)
|
501
|
-
elif self.file_handler.
|
502
|
-
# 对于OSS路径,添加到inputs数组
|
503
|
-
input_item = {"
|
507
|
+
elif self.file_handler.is_url(file_path):
|
508
|
+
# 对于URL或OSS路径,添加到inputs数组
|
509
|
+
input_item = {"url": file_path}
|
504
510
|
if password:
|
505
511
|
input_item["password"] = password
|
506
512
|
url_inputs.append(input_item)
|
lightpdf_aipdf_mcp/server.py
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
import asyncio
|
4
4
|
import os
|
5
5
|
import sys
|
6
|
+
import argparse
|
7
|
+
import json
|
6
8
|
from typing import List, Dict, Any, Callable, TypeVar, Optional, Union
|
7
9
|
|
8
10
|
# 第三方库导入
|
@@ -18,80 +20,68 @@ from .common import BaseResult, Logger, FileHandler
|
|
18
20
|
from .converter import Converter, ConversionResult
|
19
21
|
from .editor import Editor, EditResult
|
20
22
|
|
21
|
-
# 加载环境变量
|
22
|
-
load_dotenv()
|
23
|
-
|
24
23
|
# 类型定义
|
25
24
|
T = TypeVar('T', bound=BaseResult)
|
26
25
|
ProcessFunc = Callable[[str], Any]
|
27
26
|
|
28
27
|
def generate_result_report(
|
29
28
|
results: List[BaseResult],
|
30
|
-
operation_name: str
|
31
|
-
success_action: Optional[str] = None,
|
32
|
-
failed_action: Optional[str] = None
|
29
|
+
operation_name: str
|
33
30
|
) -> str:
|
34
31
|
"""生成通用结果报告
|
35
32
|
|
36
33
|
Args:
|
37
34
|
results: 结果列表
|
38
35
|
operation_name: 操作名称
|
39
|
-
success_action: 成功动作描述
|
40
|
-
failed_action: 失败动作描述
|
41
36
|
|
42
37
|
Returns:
|
43
|
-
str:
|
38
|
+
str: JSON格式的报告文本
|
44
39
|
"""
|
45
40
|
# 统计结果
|
46
41
|
success_count = sum(1 for r in results if r.success)
|
47
42
|
failed_count = len(results) - success_count
|
48
43
|
|
49
|
-
#
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
""
|
57
|
-
|
58
|
-
|
59
|
-
# 添加成功的文件信息
|
60
|
-
if success_count > 0 and success_action:
|
61
|
-
report_lines.extend([f"[成功] {success_action}的文件:", ""])
|
62
|
-
for i, result in enumerate(results):
|
63
|
-
if result.success:
|
64
|
-
report_lines.extend([
|
65
|
-
f"[{i+1}] {result.file_path}",
|
66
|
-
f"- 在线下载地址: {result.download_url}",
|
67
|
-
""
|
68
|
-
])
|
44
|
+
# 构建结果JSON对象
|
45
|
+
report_obj = {
|
46
|
+
"operation": operation_name,
|
47
|
+
"total": len(results),
|
48
|
+
"success_count": success_count,
|
49
|
+
"failed_count": failed_count,
|
50
|
+
"success_files": [],
|
51
|
+
"failed_files": []
|
52
|
+
}
|
69
53
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
54
|
+
for result in results:
|
55
|
+
if result.success:
|
56
|
+
# 添加成功的文件信息
|
57
|
+
report_obj["success_files"].append({
|
58
|
+
"file_path": result.file_path,
|
59
|
+
"original_name": result.original_name,
|
60
|
+
"download_url": result.download_url
|
61
|
+
})
|
62
|
+
else:
|
63
|
+
# 添加失败的文件信息
|
64
|
+
report_obj["failed_files"].append({
|
65
|
+
"file_path": result.file_path,
|
66
|
+
"original_name": result.original_name,
|
67
|
+
"error_message": result.error_message
|
68
|
+
})
|
80
69
|
|
81
|
-
|
70
|
+
# 返回JSON字符串
|
71
|
+
return json.dumps(report_obj, ensure_ascii=False)
|
82
72
|
|
83
73
|
async def process_batch_files(
|
84
74
|
file_objects: List[Dict[str, str]],
|
85
75
|
logger: Logger,
|
86
|
-
process_func: Callable[[str, Optional[str]], T],
|
76
|
+
process_func: Callable[[str, Optional[str], Optional[str]], T],
|
87
77
|
operation_desc: Optional[str] = None
|
88
78
|
) -> List[T]:
|
89
79
|
"""通用批处理文件函数
|
90
80
|
|
91
81
|
Args:
|
92
|
-
file_objects: 文件对象列表,每个对象包含path和可选的password
|
82
|
+
file_objects: 文件对象列表,每个对象包含path和可选的password及name
|
93
83
|
logger: 日志记录器
|
94
|
-
process_func: 处理单个文件的异步函数,接收file_path和
|
84
|
+
process_func: 处理单个文件的异步函数,接收file_path、password和original_name参数
|
95
85
|
operation_desc: 操作描述,用于日志记录
|
96
86
|
|
97
87
|
Returns:
|
@@ -107,7 +97,8 @@ async def process_batch_files(
|
|
107
97
|
async with semaphore:
|
108
98
|
file_path = file_obj["path"]
|
109
99
|
password = file_obj.get("password")
|
110
|
-
|
100
|
+
original_name = file_obj.get("name")
|
101
|
+
return await process_func(file_path, password, original_name)
|
111
102
|
|
112
103
|
# 创建任务列表
|
113
104
|
tasks = [process_with_semaphore(file_obj) for file_obj in file_objects]
|
@@ -116,14 +107,16 @@ async def process_batch_files(
|
|
116
107
|
# 单文件处理
|
117
108
|
file_path = file_objects[0]["path"]
|
118
109
|
password = file_objects[0].get("password")
|
119
|
-
|
110
|
+
original_name = file_objects[0].get("name")
|
111
|
+
return [await process_func(file_path, password, original_name)]
|
120
112
|
|
121
113
|
async def process_conversion_file(
|
122
114
|
file_path: str,
|
123
115
|
format: str,
|
124
116
|
converter: Converter,
|
125
117
|
extra_params: Optional[Dict[str, Any]] = None,
|
126
|
-
password: Optional[str] = None
|
118
|
+
password: Optional[str] = None,
|
119
|
+
original_name: Optional[str] = None
|
127
120
|
) -> ConversionResult:
|
128
121
|
"""处理单个文件转换"""
|
129
122
|
is_page_numbering = format == "number-pdf"
|
@@ -135,47 +128,51 @@ async def process_conversion_file(
|
|
135
128
|
extra_params.get("start_num", 1),
|
136
129
|
extra_params.get("position", "5"),
|
137
130
|
extra_params.get("margin", 30),
|
138
|
-
password
|
131
|
+
password,
|
132
|
+
original_name
|
139
133
|
)
|
140
134
|
else:
|
141
135
|
# 对于其他操作,使用convert_file方法
|
142
|
-
return await converter.convert_file(file_path, format, extra_params, password)
|
136
|
+
return await converter.convert_file(file_path, format, extra_params, password, original_name)
|
143
137
|
|
144
138
|
async def process_edit_file(
|
145
139
|
file_path: str,
|
146
140
|
edit_type: str,
|
147
141
|
editor: Editor,
|
148
142
|
extra_params: Dict[str, Any] = None,
|
149
|
-
password: Optional[str] = None
|
143
|
+
password: Optional[str] = None,
|
144
|
+
original_name: Optional[str] = None
|
150
145
|
) -> EditResult:
|
151
146
|
"""处理单个文件编辑"""
|
152
147
|
if edit_type == "decrypt":
|
153
|
-
return await editor.decrypt_pdf(file_path, password)
|
148
|
+
return await editor.decrypt_pdf(file_path, password, original_name)
|
154
149
|
elif edit_type == "add_watermark":
|
155
150
|
return await editor.add_watermark(
|
156
151
|
file_path=file_path,
|
157
152
|
text=extra_params.get("text", "水印"),
|
158
153
|
position=extra_params.get("position", "center"),
|
159
154
|
opacity=extra_params.get("opacity", 0.5),
|
160
|
-
deg=extra_params.get("deg", "0"),
|
161
155
|
range=extra_params.get("range", ""),
|
162
156
|
layout=extra_params.get("layout", "on"),
|
163
157
|
font_family=extra_params.get("font_family"),
|
164
158
|
font_size=extra_params.get("font_size"),
|
165
159
|
font_color=extra_params.get("font_color"),
|
166
|
-
password=password
|
160
|
+
password=password,
|
161
|
+
original_name=original_name
|
167
162
|
)
|
168
163
|
elif edit_type == "encrypt":
|
169
164
|
return await editor.encrypt_pdf(
|
170
165
|
file_path=file_path,
|
171
166
|
password=extra_params.get("password", ""),
|
172
|
-
original_password=password
|
167
|
+
original_password=password,
|
168
|
+
original_name=original_name
|
173
169
|
)
|
174
170
|
elif edit_type == "compress":
|
175
171
|
return await editor.compress_pdf(
|
176
172
|
file_path=file_path,
|
177
173
|
image_quantity=extra_params.get("image_quantity", 60),
|
178
|
-
password=password
|
174
|
+
password=password,
|
175
|
+
original_name=original_name
|
179
176
|
)
|
180
177
|
elif edit_type == "split":
|
181
178
|
return await editor.split_pdf(
|
@@ -183,27 +180,31 @@ async def process_edit_file(
|
|
183
180
|
pages=extra_params.get("pages", ""),
|
184
181
|
password=password,
|
185
182
|
split_type=extra_params.get("split_type", "page"),
|
186
|
-
merge_all=extra_params.get("merge_all", 1)
|
183
|
+
merge_all=extra_params.get("merge_all", 1),
|
184
|
+
original_name=original_name
|
187
185
|
)
|
188
186
|
elif edit_type == "merge":
|
189
187
|
# 对于合并操作,我们需要特殊处理,因为它需要处理多个文件
|
190
188
|
return EditResult(
|
191
189
|
success=False,
|
192
190
|
file_path=file_path,
|
193
|
-
error_message="合并操作需要使用特殊处理流程"
|
191
|
+
error_message="合并操作需要使用特殊处理流程",
|
192
|
+
original_name=original_name
|
194
193
|
)
|
195
194
|
elif edit_type == "rotate":
|
196
195
|
return await editor.rotate_pdf(
|
197
196
|
file_path=file_path,
|
198
197
|
angle=extra_params.get("angle", 90),
|
199
198
|
pages=extra_params.get("pages", ""),
|
200
|
-
password=password
|
199
|
+
password=password,
|
200
|
+
original_name=original_name
|
201
201
|
)
|
202
202
|
else:
|
203
203
|
return EditResult(
|
204
204
|
success=False,
|
205
205
|
file_path=file_path,
|
206
|
-
error_message=f"不支持的编辑类型: {edit_type}"
|
206
|
+
error_message=f"不支持的编辑类型: {edit_type}",
|
207
|
+
original_name=original_name
|
207
208
|
)
|
208
209
|
|
209
210
|
async def process_tool_call(
|
@@ -246,8 +247,8 @@ async def process_tool_call(
|
|
246
247
|
results = await process_batch_files(
|
247
248
|
file_objects,
|
248
249
|
logger,
|
249
|
-
lambda file_path, password: process_edit_file(
|
250
|
-
file_path, edit_type, editor, extra_params, password
|
250
|
+
lambda file_path, password, original_name: process_edit_file(
|
251
|
+
file_path, edit_type, editor, extra_params, password, original_name
|
251
252
|
),
|
252
253
|
operation_desc
|
253
254
|
)
|
@@ -255,9 +256,7 @@ async def process_tool_call(
|
|
255
256
|
# 生成报告
|
256
257
|
report_msg = generate_result_report(
|
257
258
|
results,
|
258
|
-
operation_desc
|
259
|
-
f"{edit_map.get(edit_type, edit_type)}成功",
|
260
|
-
f"{edit_map.get(edit_type, edit_type)}失败"
|
259
|
+
operation_desc
|
261
260
|
)
|
262
261
|
else:
|
263
262
|
# 转换操作
|
@@ -271,25 +270,19 @@ async def process_tool_call(
|
|
271
270
|
if is_watermark_removal:
|
272
271
|
operation_desc = "去除水印"
|
273
272
|
task_type = "水印去除"
|
274
|
-
success_action = "去除水印成功"
|
275
|
-
failed_action = "水印去除失败"
|
276
273
|
elif is_page_numbering:
|
277
274
|
operation_desc = "添加页码"
|
278
275
|
task_type = "添加页码"
|
279
|
-
success_action = "添加页码成功"
|
280
|
-
failed_action = "添加页码失败"
|
281
276
|
else:
|
282
277
|
operation_desc = f"转换为 {format} 格式"
|
283
278
|
task_type = "转换"
|
284
|
-
success_action = "转换成功"
|
285
|
-
failed_action = "转换失败"
|
286
279
|
|
287
280
|
# 处理文件
|
288
281
|
results = await process_batch_files(
|
289
282
|
file_objects,
|
290
283
|
logger,
|
291
|
-
lambda file_path, password: process_conversion_file(
|
292
|
-
file_path, format, converter, extra_params, password
|
284
|
+
lambda file_path, password, original_name: process_conversion_file(
|
285
|
+
file_path, format, converter, extra_params, password, original_name
|
293
286
|
),
|
294
287
|
operation_desc
|
295
288
|
)
|
@@ -297,9 +290,7 @@ async def process_tool_call(
|
|
297
290
|
# 生成报告
|
298
291
|
report_msg = generate_result_report(
|
299
292
|
results,
|
300
|
-
task_type
|
301
|
-
success_action,
|
302
|
-
failed_action
|
293
|
+
task_type
|
303
294
|
)
|
304
295
|
|
305
296
|
# 如果全部失败,记录错误
|
@@ -336,6 +327,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
336
327
|
"password": {
|
337
328
|
"type": "string",
|
338
329
|
"description": "文档密码,如果文档受密码保护,则需要提供此参数"
|
330
|
+
},
|
331
|
+
"name": {
|
332
|
+
"type": "string",
|
333
|
+
"description": "文件的原始文件名"
|
339
334
|
}
|
340
335
|
},
|
341
336
|
"required": ["path"]
|
@@ -369,6 +364,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
369
364
|
"password": {
|
370
365
|
"type": "string",
|
371
366
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
367
|
+
},
|
368
|
+
"name": {
|
369
|
+
"type": "string",
|
370
|
+
"description": "文件的原始文件名"
|
372
371
|
}
|
373
372
|
},
|
374
373
|
"required": ["path"]
|
@@ -415,6 +414,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
415
414
|
"password": {
|
416
415
|
"type": "string",
|
417
416
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
417
|
+
},
|
418
|
+
"name": {
|
419
|
+
"type": "string",
|
420
|
+
"description": "文件的原始文件名"
|
418
421
|
}
|
419
422
|
},
|
420
423
|
"required": ["path"]
|
@@ -443,6 +446,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
443
446
|
"password": {
|
444
447
|
"type": "string",
|
445
448
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
449
|
+
},
|
450
|
+
"name": {
|
451
|
+
"type": "string",
|
452
|
+
"description": "文件的原始文件名"
|
446
453
|
}
|
447
454
|
},
|
448
455
|
"required": ["path"]
|
@@ -455,7 +462,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
455
462
|
},
|
456
463
|
"position": {
|
457
464
|
"type": "string",
|
458
|
-
"description": "
|
465
|
+
"description": "水印位置: 左上(topleft), 上中(top), 右上(topright), 左(left), 中(center), 右(right), 左下(bottomleft), 下(bottom), 右下(bottomright), 对角线(diagonal,-45度), 反对角线(reverse-diagonal,45度)",
|
459
466
|
"enum": ["topleft", "top", "topright", "left", "center", "right",
|
460
467
|
"bottomleft", "bottom", "bottomright", "diagonal", "reverse-diagonal"],
|
461
468
|
"default": "center"
|
@@ -467,11 +474,6 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
467
474
|
"minimum": 0.0,
|
468
475
|
"maximum": 1.0
|
469
476
|
},
|
470
|
-
"deg": {
|
471
|
-
"type": "string",
|
472
|
-
"description": "倾斜角度,字符串格式",
|
473
|
-
"default": "0"
|
474
|
-
},
|
475
477
|
"range": {
|
476
478
|
"type": "string",
|
477
479
|
"description": "页面范围,例如 '1,3,5-7' 或 ''(空字符串或不设置)表示所有页面"
|
@@ -516,6 +518,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
516
518
|
"password": {
|
517
519
|
"type": "string",
|
518
520
|
"description": "PDF文档的密码,用于解锁文档,如果文档受密码保护,则需要提供此参数"
|
521
|
+
},
|
522
|
+
"name": {
|
523
|
+
"type": "string",
|
524
|
+
"description": "文件的原始文件名"
|
519
525
|
}
|
520
526
|
},
|
521
527
|
"required": ["path", "password"]
|
@@ -544,6 +550,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
544
550
|
"password": {
|
545
551
|
"type": "string",
|
546
552
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
553
|
+
},
|
554
|
+
"name": {
|
555
|
+
"type": "string",
|
556
|
+
"description": "文件的原始文件名"
|
547
557
|
}
|
548
558
|
},
|
549
559
|
"required": ["path"]
|
@@ -576,6 +586,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
576
586
|
"password": {
|
577
587
|
"type": "string",
|
578
588
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
589
|
+
},
|
590
|
+
"name": {
|
591
|
+
"type": "string",
|
592
|
+
"description": "文件的原始文件名"
|
579
593
|
}
|
580
594
|
},
|
581
595
|
"required": ["path"]
|
@@ -611,6 +625,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
611
625
|
"password": {
|
612
626
|
"type": "string",
|
613
627
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
628
|
+
},
|
629
|
+
"name": {
|
630
|
+
"type": "string",
|
631
|
+
"description": "文件的原始文件名"
|
614
632
|
}
|
615
633
|
},
|
616
634
|
"required": ["path"]
|
@@ -655,6 +673,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
655
673
|
"password": {
|
656
674
|
"type": "string",
|
657
675
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
676
|
+
},
|
677
|
+
"name": {
|
678
|
+
"type": "string",
|
679
|
+
"description": "文件的原始文件名"
|
658
680
|
}
|
659
681
|
},
|
660
682
|
"required": ["path"]
|
@@ -683,6 +705,10 @@ async def handle_list_tools() -> list[types.Tool]:
|
|
683
705
|
"password": {
|
684
706
|
"type": "string",
|
685
707
|
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
708
|
+
},
|
709
|
+
"name": {
|
710
|
+
"type": "string",
|
711
|
+
"description": "文件的原始文件名"
|
686
712
|
}
|
687
713
|
},
|
688
714
|
"required": ["path"]
|
@@ -738,7 +764,7 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[types.Text
|
|
738
764
|
"add_watermark": {
|
739
765
|
"edit_type": "add_watermark", # 编辑类型
|
740
766
|
"is_edit_operation": True, # 标记为编辑操作
|
741
|
-
"param_keys": ["text", "position", "opacity", "
|
767
|
+
"param_keys": ["text", "position", "opacity", "range", "layout",
|
742
768
|
"font_family", "font_size", "font_color"] # 需要从arguments获取的参数
|
743
769
|
},
|
744
770
|
"protect_pdf": {
|
@@ -773,7 +799,6 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[types.Text
|
|
773
799
|
"position_watermark": "center", # 水印的位置默认值
|
774
800
|
"margin": 30,
|
775
801
|
"opacity": 0.5,
|
776
|
-
"deg": "0", # 更新为"0"以匹配handle_list_tools中的默认值
|
777
802
|
"range": "",
|
778
803
|
"layout": "on", # 添加layout默认值
|
779
804
|
"image_quantity": 60,
|
@@ -832,23 +857,33 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[types.Text
|
|
832
857
|
file_handler = FileHandler(logger)
|
833
858
|
editor = Editor(logger, file_handler)
|
834
859
|
|
835
|
-
#
|
860
|
+
# 提取文件路径、密码和原始名称
|
836
861
|
file_paths = [file_obj["path"] for file_obj in file_objects]
|
837
862
|
passwords = [file_obj.get("password") for file_obj in file_objects]
|
863
|
+
original_names = [file_obj.get("name") for file_obj in file_objects]
|
838
864
|
|
839
865
|
# 由于merge_pdfs方法只接受一个密码参数,如果文件密码不同,可能需要特殊处理
|
840
866
|
# 此处简化处理,使用第一个非空密码
|
841
867
|
password = next((p for p in passwords if p), None)
|
842
868
|
|
869
|
+
# 合并文件名用于结果文件
|
870
|
+
merged_name = None
|
871
|
+
if any(original_names):
|
872
|
+
# 如果有原始文件名,则合并它们(最多使用前两个文件名)
|
873
|
+
valid_names = [name for name in original_names if name]
|
874
|
+
if valid_names:
|
875
|
+
if len(valid_names) == 1:
|
876
|
+
merged_name = valid_names[0]
|
877
|
+
else:
|
878
|
+
merged_name = f"{valid_names[0]}_{valid_names[1]}_等"
|
879
|
+
|
843
880
|
# 直接调用merge_pdfs方法
|
844
|
-
result = await editor.merge_pdfs(file_paths, password)
|
881
|
+
result = await editor.merge_pdfs(file_paths, password, merged_name)
|
845
882
|
|
846
883
|
# 构建结果报告
|
847
884
|
report_msg = generate_result_report(
|
848
885
|
[result],
|
849
|
-
"PDF合并"
|
850
|
-
"合并成功",
|
851
|
-
"合并失败"
|
886
|
+
"PDF合并"
|
852
887
|
)
|
853
888
|
|
854
889
|
# 如果失败,记录错误
|
@@ -867,6 +902,9 @@ async def handle_call_tool(name: str, arguments: dict | None) -> list[types.Text
|
|
867
902
|
|
868
903
|
async def main():
|
869
904
|
"""应用主入口"""
|
905
|
+
# 加载环境变量
|
906
|
+
load_dotenv()
|
907
|
+
|
870
908
|
# 打印版本号
|
871
909
|
try:
|
872
910
|
import importlib.metadata
|
@@ -875,19 +913,71 @@ async def main():
|
|
875
913
|
except Exception as e:
|
876
914
|
print("LightPDF AI-PDF MCP Server (版本信息获取失败)", file=sys.stderr)
|
877
915
|
|
878
|
-
|
916
|
+
# 解析命令行参数
|
917
|
+
parser = argparse.ArgumentParser(description="LightPDF AI-PDF MCP Server")
|
918
|
+
parser.add_argument("-p", "--port", type=int, default=0, help="指定SSE服务器的端口号,如果提供则使用SSE模式,否则使用stdio模式")
|
919
|
+
args = parser.parse_args()
|
920
|
+
|
921
|
+
initialization_options = app.create_initialization_options(
|
922
|
+
notification_options=NotificationOptions()
|
923
|
+
)
|
879
924
|
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
925
|
+
if args.port:
|
926
|
+
from mcp.server.sse import SseServerTransport
|
927
|
+
from starlette.applications import Starlette
|
928
|
+
from starlette.routing import Mount, Route
|
929
|
+
import uvicorn
|
930
|
+
|
931
|
+
# 使用SSE服务器
|
932
|
+
print(f"启动SSE服务器,端口号:{args.port}", file=sys.stderr)
|
933
|
+
|
934
|
+
# 创建SSE传输
|
935
|
+
transport = SseServerTransport("/messages/")
|
936
|
+
|
937
|
+
# 定义SSE连接处理函数
|
938
|
+
async def handle_sse(request):
|
939
|
+
async with transport.connect_sse(
|
940
|
+
request.scope, request.receive, request._send
|
941
|
+
) as streams:
|
942
|
+
await app.run(
|
943
|
+
streams[0], streams[1], initialization_options
|
944
|
+
)
|
945
|
+
|
946
|
+
# 创建Starlette应用
|
947
|
+
sse_app = Starlette(routes=[
|
948
|
+
Route("/sse/", endpoint=handle_sse),
|
949
|
+
Mount("/messages/", app=transport.handle_post_message),
|
950
|
+
])
|
951
|
+
|
952
|
+
# 使用异步方式启动服务器
|
953
|
+
server = uvicorn.Server(uvicorn.Config(
|
954
|
+
app=sse_app,
|
955
|
+
host="0.0.0.0",
|
956
|
+
port=args.port,
|
957
|
+
log_level="warning"
|
958
|
+
))
|
959
|
+
await server.serve()
|
960
|
+
else:
|
961
|
+
import mcp.server.stdio as stdio
|
962
|
+
|
963
|
+
# 使用stdio服务器
|
964
|
+
print("启动stdio服务器", file=sys.stderr)
|
965
|
+
async with stdio.stdio_server() as (read_stream, write_stream):
|
966
|
+
await app.run(
|
967
|
+
read_stream,
|
968
|
+
write_stream,
|
969
|
+
initialization_options
|
886
970
|
)
|
887
|
-
)
|
888
971
|
|
889
972
|
def cli_main():
|
890
|
-
|
973
|
+
try:
|
974
|
+
asyncio.run(main())
|
975
|
+
except KeyboardInterrupt:
|
976
|
+
print("服务器被用户中断", file=sys.stderr)
|
977
|
+
sys.exit(0)
|
978
|
+
except Exception as e:
|
979
|
+
print(f"服务器发生错误: {e}", file=sys.stderr)
|
980
|
+
sys.exit(1)
|
891
981
|
|
892
982
|
if __name__ == "__main__":
|
893
983
|
cli_main()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: lightpdf-aipdf-mcp
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.44
|
4
4
|
Summary: MCP Server for LightPDF AI-PDF
|
5
5
|
Author: LightPDF Team
|
6
6
|
License: Proprietary
|
@@ -8,9 +8,11 @@ Requires-Python: >=3.8
|
|
8
8
|
Requires-Dist: httpx
|
9
9
|
Requires-Dist: httpx-sse
|
10
10
|
Requires-Dist: mcp
|
11
|
+
Requires-Dist: mcp[cli]
|
11
12
|
Requires-Dist: pydantic
|
12
13
|
Requires-Dist: pydantic-settings
|
13
14
|
Requires-Dist: python-dotenv
|
15
|
+
Requires-Dist: starlette
|
14
16
|
Requires-Dist: uvicorn
|
15
17
|
Description-Content-Type: text/markdown
|
16
18
|
|
@@ -205,7 +207,6 @@ pip install lightpdf-aipdf-mcp
|
|
205
207
|
- `text`: 水印文本内容
|
206
208
|
- `position`: 水印位置,可选值包括"center"、"topleft"等
|
207
209
|
- `opacity`: 透明度(0.0-1.0)
|
208
|
-
- `deg`: 旋转角度
|
209
210
|
- `range`: 页面范围,如 "1,3,5-7"
|
210
211
|
- `layout`: 水印显示位置,"on"(在内容上)或"under"(在内容下)
|
211
212
|
- 可选的字体设置: `font_family`、`font_size`、`font_color`
|
@@ -0,0 +1,9 @@
|
|
1
|
+
lightpdf_aipdf_mcp/__init__.py,sha256=PPnAgpvJLYLVOTxnHDmJAulFnHJD6wuTwS6tRGjqq6s,141
|
2
|
+
lightpdf_aipdf_mcp/common.py,sha256=-7LU6gm-As_F8Ly68ssy15Vc9Zt_eNSnvDLEtVZDwlI,6633
|
3
|
+
lightpdf_aipdf_mcp/converter.py,sha256=gsYBLqE6EnMCpCYHaYjcdBD5mhXVqiTRvl1yLSqA01w,14773
|
4
|
+
lightpdf_aipdf_mcp/editor.py,sha256=KySdMurM8AZHwqSU1PpkSCCesQ4t2h-D8Mm12wf90CA,23539
|
5
|
+
lightpdf_aipdf_mcp/server.py,sha256=dd3lAg3DhFYPdgigpcicY_SiMRnwqXkxjbagj5ijSYY,39974
|
6
|
+
lightpdf_aipdf_mcp-0.1.44.dist-info/METADATA,sha256=Q8EvtD9EOAMKV1xNC-9hrQ97lSVVsXrHb1i2Ri4Kar0,7906
|
7
|
+
lightpdf_aipdf_mcp-0.1.44.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
8
|
+
lightpdf_aipdf_mcp-0.1.44.dist-info/entry_points.txt,sha256=X7TGUe52N4sYH-tYt0YUGApeJgw-efQlZA6uAZmlmr4,63
|
9
|
+
lightpdf_aipdf_mcp-0.1.44.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
lightpdf_aipdf_mcp/__init__.py,sha256=PPnAgpvJLYLVOTxnHDmJAulFnHJD6wuTwS6tRGjqq6s,141
|
2
|
-
lightpdf_aipdf_mcp/common.py,sha256=JbKSAknOVvtDIQ7QG4fxj5H1FtCMRMF8PEJw10vyN_k,6564
|
3
|
-
lightpdf_aipdf_mcp/converter.py,sha256=MxjO1yqMqfOQ9B696IyjRbtB3oZpemtD7x8arz52zoc,13312
|
4
|
-
lightpdf_aipdf_mcp/editor.py,sha256=4hU_GogU1sgZbrrJFhz8106qG18T2OixXNTQHsJTtvE,22802
|
5
|
-
lightpdf_aipdf_mcp/server.py,sha256=jXAcfLWhef5E3cle28bytDTTuMiwTMYAglbAFVF6Wlw,35737
|
6
|
-
lightpdf_aipdf_mcp-0.1.42.dist-info/METADATA,sha256=WXRb8FSntMMTz-erOUdm3fLFIbOwFIp-1bEURQqJQzI,7879
|
7
|
-
lightpdf_aipdf_mcp-0.1.42.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
8
|
-
lightpdf_aipdf_mcp-0.1.42.dist-info/entry_points.txt,sha256=X7TGUe52N4sYH-tYt0YUGApeJgw-efQlZA6uAZmlmr4,63
|
9
|
-
lightpdf_aipdf_mcp-0.1.42.dist-info/RECORD,,
|
File without changes
|
{lightpdf_aipdf_mcp-0.1.42.dist-info → lightpdf_aipdf_mcp-0.1.44.dist-info}/entry_points.txt
RENAMED
File without changes
|