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.
@@ -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:
@@ -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
- self.api_base_url = "https://techsz.aoscdn.com/api/tasks/document/conversion"
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
- await self.logger.error(f"无效的页码位置值: {position}。有效值为: 1(左上), 2(上中), 3(右上), 4(左下), 5(下中), 6(右下)")
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
- await self.logger.error(f"无效的页码边距值: {margin}。有效值为: 10, 30, 60")
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
- await self.logger.error(f"起始页码必须是正整数,当前值为: {start_num}")
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
- await self.logger.error(f"不支持的输入文件格式: {self.file_handler.get_file_extension(file_path)}")
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
- await self.logger.error("去除水印功能仅支持PDF文件")
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
- await self.logger.error("添加页码功能仅支持PDF文件")
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
- await self.logger.error(f"不支持的输出格式: {format}")
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
- await self.logger.error(f"不支持的输出格式: {format}")
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=120.0) as client:
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:
@@ -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
- self.api_base_url = "https://techsz.aoscdn.com/api/tasks/document/pdfedit"
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(f"无效的图片质量: {image_quantity}。有效值范围为: 1-100")
213
- return EditResult(success=False, file_path=file_path, error_message=f"无效的图片质量: {image_quantity}。有效值范围为: 1-100")
217
+ # 验证图片质量范围
218
+ if not (1 <= image_quantity <= 100):
219
+ await self.logger.error("图片质量必须在1100之间")
220
+ return EditResult(success=False, file_path=file_path, error_message="图片质量必须在1100之间", 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: 设置的新密码(用于加密PDF)
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: 水印位置,可选值:"topleft","top","topright","left","center",
299
- "right","bottomleft","bottom","bottomright","diagonal","reverse-diagonal"
300
- opacity: 透明度,0.0-1.0,默认为0.5
301
- deg: 倾斜角度,字符串格式,如"-45",默认为"-45"
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 0.0 <= opacity <= 1.0:
318
- await self.logger.error(f"无效的透明度: {opacity}。有效值范围为: 0.0-1.0")
319
- return EditResult(success=False, file_path=file_path, error_message=f"无效的透明度: {opacity}。有效值范围为: 0.0-1.0")
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=120.0) as client:
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.is_url(file_path):
496
- # 对于URL或OSS路径,添加到inputs数组
497
- input_item = {"url": file_path}
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.is_oss_id(file_path):
502
- # 对于OSS路径,添加到inputs数组
503
- input_item = {"resource_id": file_path.split("oss_id://")[1]}
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)
@@ -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
- report_lines = [
51
- f"{operation_name}结果:共 {len(results)} 个文件" + (
52
- f",成功 {success_count} 个" if success_count > 0 else ""
53
- ) + (
54
- f",失败 {failed_count} 个" if failed_count > 0 else ""
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
- if failed_count > 0 and failed_action:
72
- report_lines.extend([f"[失败] {failed_action}的文件:", ""])
73
- for i, result in enumerate(results):
74
- if not result.success:
75
- report_lines.extend([
76
- f"[{i+1}] {result.file_path}",
77
- f"- 错误: {result.error_message}",
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
- return "\n".join(report_lines)
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和password参数
84
+ process_func: 处理单个文件的异步函数,接收file_path、passwordoriginal_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
- return await process_func(file_path, password)
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
- return [await process_func(file_path, password)]
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", "deg", "range", "layout",
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
- import mcp.server.stdio as stdio
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
- async with stdio.stdio_server() as (read_stream, write_stream):
881
- await app.run(
882
- read_stream,
883
- write_stream,
884
- app.create_initialization_options(
885
- notification_options=NotificationOptions()
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
- asyncio.run(main())
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.42
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,,