qrpa 1.0.25__py3-none-any.whl → 1.0.27__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.

Potentially problematic release.


This version of qrpa might be problematic. Click here for more details.

qrpa/feishu_bot_app.py ADDED
@@ -0,0 +1,268 @@
1
+ # pip install lark-oapi -U
2
+ import json
3
+ import os
4
+ import uuid
5
+ from typing import Optional
6
+
7
+ import lark_oapi as lark
8
+ from lark_oapi.api.im.v1 import *
9
+
10
+
11
+ class FeishuBot:
12
+ """飞书机器人类,封装所有机器人相关功能"""
13
+
14
+ def __init__(self, config):
15
+ """
16
+ 初始化飞书机器人
17
+
18
+ Args:
19
+ config: 配置对象,包含应用ID、应用密钥和群组信息
20
+ """
21
+ self.config = config
22
+ self._client = None
23
+
24
+ @property
25
+ def client(self):
26
+ """获取飞书客户端,使用懒加载模式"""
27
+ if self._client is None:
28
+ self._client = lark.Client.builder() \
29
+ .app_id(self.config.feishu_bot_app_id) \
30
+ .app_secret(self.config.feishu_bot_app_secret) \
31
+ .log_level(lark.LogLevel.INFO) \
32
+ .build()
33
+ return self._client
34
+
35
+ def _get_chat_id(self, chat_alias: str) -> Optional[str]:
36
+ """
37
+ 根据群组别名获取群组ID
38
+
39
+ Args:
40
+ chat_alias: 群组别名
41
+
42
+ Returns:
43
+ 群组ID,如果别名不存在则返回None
44
+ """
45
+ return self.config.dict_feishu_group.get(chat_alias)
46
+
47
+ def _handle_response_error(self, response, operation_name: str):
48
+ """
49
+ 处理API响应错误
50
+
51
+ Args:
52
+ response: API响应对象
53
+ operation_name: 操作名称,用于错误日志
54
+ """
55
+ if not response.success():
56
+ lark.logger.error(
57
+ f"{operation_name} failed, code: {response.code}, "
58
+ f"msg: {response.msg}, log_id: {response.get_log_id()}, "
59
+ f"resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
60
+ )
61
+ return True
62
+ return False
63
+
64
+ def send_text(self, content: str, chat_alias: str = 'test') -> bool:
65
+ """
66
+ 发送文本消息
67
+
68
+ Args:
69
+ content: 文本内容
70
+ chat_alias: 群组别名,默认为'test'
71
+
72
+ Returns:
73
+ 发送是否成功
74
+ """
75
+ chat_id = self._get_chat_id(chat_alias)
76
+ if not chat_id:
77
+ lark.logger.error(f"未找到群组别名 '{chat_alias}' 对应的群组ID")
78
+ return False
79
+
80
+ message_content = {"text": content}
81
+
82
+ # 构造请求对象
83
+ request: CreateMessageRequest = CreateMessageRequest.builder() \
84
+ .receive_id_type("chat_id") \
85
+ .request_body(CreateMessageRequestBody.builder()
86
+ .receive_id(chat_id)
87
+ .msg_type("text")
88
+ .content(json.dumps(message_content))
89
+ .uuid(str(uuid.uuid4()))
90
+ .build()) \
91
+ .build()
92
+
93
+ # 发起请求
94
+ response: CreateMessageResponse = self.client.im.v1.message.create(request)
95
+
96
+ # 处理失败返回
97
+ if self._handle_response_error(response, "send_text"):
98
+ return False
99
+
100
+ # 处理业务结果
101
+ lark.logger.info(lark.JSON.marshal(response.data, indent=4))
102
+ return True
103
+
104
+ def send_image(self, file_path: str, chat_alias: str = 'test') -> bool:
105
+ """
106
+ 发送图片消息
107
+
108
+ Args:
109
+ file_path: 图片文件路径
110
+ chat_alias: 群组别名,默认为'test'
111
+
112
+ Returns:
113
+ 发送是否成功
114
+ """
115
+ # 先上传图片获取image_key
116
+ image_key = self.upload_image(file_path)
117
+ if not image_key:
118
+ return False
119
+
120
+ chat_id = self._get_chat_id(chat_alias)
121
+ if not chat_id:
122
+ lark.logger.error(f"未找到群组别名 '{chat_alias}' 对应的群组ID")
123
+ return False
124
+
125
+ message_content = {"image_key": image_key}
126
+
127
+ # 构造请求对象
128
+ request: CreateMessageRequest = CreateMessageRequest.builder() \
129
+ .receive_id_type("chat_id") \
130
+ .request_body(CreateMessageRequestBody.builder()
131
+ .receive_id(chat_id)
132
+ .msg_type("image")
133
+ .content(json.dumps(message_content))
134
+ .uuid(str(uuid.uuid4()))
135
+ .build()) \
136
+ .build()
137
+
138
+ # 发起请求
139
+ response: CreateMessageResponse = self.client.im.v1.message.create(request)
140
+
141
+ # 处理失败返回
142
+ if self._handle_response_error(response, "send_image"):
143
+ return False
144
+
145
+ # 处理业务结果
146
+ lark.logger.info(lark.JSON.marshal(response.data, indent=4))
147
+ return True
148
+
149
+ def send_excel(self, file_path: str, chat_alias: str = 'test') -> bool:
150
+ """
151
+ 发送Excel文件
152
+
153
+ Args:
154
+ file_path: Excel文件路径
155
+ chat_alias: 群组别名,默认为'test'
156
+
157
+ Returns:
158
+ 发送是否成功
159
+ """
160
+ # 先上传文件获取file_key
161
+ file_key = self.upload_excel(file_path)
162
+ if not file_key:
163
+ return False
164
+
165
+ chat_id = self._get_chat_id(chat_alias)
166
+ if not chat_id:
167
+ lark.logger.error(f"未找到群组别名 '{chat_alias}' 对应的群组ID")
168
+ return False
169
+
170
+ message_content = {"file_key": file_key}
171
+
172
+ # 构造请求对象
173
+ request: CreateMessageRequest = CreateMessageRequest.builder() \
174
+ .receive_id_type("chat_id") \
175
+ .request_body(CreateMessageRequestBody.builder()
176
+ .receive_id(chat_id)
177
+ .msg_type("file")
178
+ .content(json.dumps(message_content))
179
+ .uuid(str(uuid.uuid4()))
180
+ .build()) \
181
+ .build()
182
+
183
+ # 发起请求
184
+ response: CreateMessageResponse = self.client.im.v1.message.create(request)
185
+
186
+ # 处理失败返回
187
+ if self._handle_response_error(response, "send_excel"):
188
+ return False
189
+
190
+ # 处理业务结果
191
+ lark.logger.info(lark.JSON.marshal(response.data, indent=4))
192
+ return True
193
+
194
+ def upload_excel(self, file_path: str) -> Optional[str]:
195
+ """
196
+ 上传Excel文件
197
+
198
+ Args:
199
+ file_path: 文件路径
200
+
201
+ Returns:
202
+ 文件key,上传失败返回None
203
+ """
204
+ if not os.path.exists(file_path):
205
+ lark.logger.error(f"文件不存在: {file_path}")
206
+ return None
207
+
208
+ try:
209
+ with open(file_path, "rb") as file:
210
+ file_name = os.path.basename(file_path)
211
+ request: CreateFileRequest = CreateFileRequest.builder() \
212
+ .request_body(CreateFileRequestBody.builder()
213
+ .file_type("xls")
214
+ .file_name(file_name)
215
+ .file(file)
216
+ .build()) \
217
+ .build()
218
+
219
+ # 发起请求
220
+ response: CreateFileResponse = self.client.im.v1.file.create(request)
221
+
222
+ # 处理失败返回
223
+ if self._handle_response_error(response, "upload_excel"):
224
+ return None
225
+
226
+ # 处理业务结果
227
+ lark.logger.info(lark.JSON.marshal(response.data, indent=4))
228
+ return response.data.file_key
229
+ except Exception as e:
230
+ lark.logger.error(f"上传Excel文件时发生错误: {e}")
231
+ return None
232
+
233
+ def upload_image(self, file_path: str) -> Optional[str]:
234
+ """
235
+ 上传图片文件
236
+
237
+ Args:
238
+ file_path: 图片文件路径
239
+
240
+ Returns:
241
+ 图片key,上传失败返回None
242
+ """
243
+ if not os.path.exists(file_path):
244
+ lark.logger.error(f"文件不存在: {file_path}")
245
+ return None
246
+
247
+ try:
248
+ with open(file_path, "rb") as file:
249
+ request: CreateImageRequest = CreateImageRequest.builder() \
250
+ .request_body(CreateImageRequestBody.builder()
251
+ .image_type("message")
252
+ .image(file)
253
+ .build()) \
254
+ .build()
255
+
256
+ # 发起请求
257
+ response: CreateImageResponse = self.client.im.v1.image.create(request)
258
+
259
+ # 处理失败返回
260
+ if self._handle_response_error(response, "upload_image"):
261
+ return None
262
+
263
+ # 处理业务结果
264
+ lark.logger.info(lark.JSON.marshal(response.data, indent=4))
265
+ return response.data.image_key
266
+ except Exception as e:
267
+ lark.logger.error(f"上传图片文件时发生错误: {e}")
268
+ return None
qrpa/fun_excel.py CHANGED
@@ -1860,6 +1860,117 @@ def format_to_text_v2(sheet, columns=None):
1860
1860
  log(f'设置[{col_name}] 文本格式')
1861
1861
  sheet.range(f'{col_name}:{col_name}').number_format = '@'
1862
1862
 
1863
+ def format_to_text_v2_safe(sheet, columns=None, data_rows=None):
1864
+ """
1865
+ 更安全的文本格式化函数,避免COM异常
1866
+
1867
+ Args:
1868
+ sheet: Excel工作表对象
1869
+ columns: 要格式化的列名列表
1870
+ data_rows: 数据行数,用于限制格式化范围
1871
+ """
1872
+ if columns is None or len(columns) == 0:
1873
+ return
1874
+
1875
+ # 确保columns是列表
1876
+ if not isinstance(columns, list):
1877
+ columns = [columns]
1878
+
1879
+ for col_name in columns:
1880
+ try:
1881
+ if isinstance(col_name, int):
1882
+ col_name = xw.utils.col_name(col_name)
1883
+
1884
+ log(f'安全设置[{col_name}] 文本格式')
1885
+
1886
+ # 如果指定了数据行数,只格式化有数据的范围
1887
+ if data_rows and data_rows > 0:
1888
+ # 格式化从第1行到数据行数的范围
1889
+ range_str = f'{col_name}1:{col_name}{data_rows}'
1890
+ sheet.range(range_str).number_format = '@'
1891
+ else:
1892
+ # 检查列是否有数据,如果没有则跳过
1893
+ try:
1894
+ # 先检查第一个单元格是否存在
1895
+ test_range = sheet.range(f'{col_name}1')
1896
+ if test_range.value is not None or sheet.used_range.last_cell.column >= column_name_to_index(col_name) + 1:
1897
+ sheet.range(f'{col_name}:{col_name}').number_format = '@'
1898
+ else:
1899
+ log(f'列 {col_name} 没有数据,跳过格式化')
1900
+ except:
1901
+ log(f'列 {col_name} 格式化失败,跳过')
1902
+
1903
+ except Exception as e:
1904
+ log(f'设置列 {col_name} 文本格式失败: {e},继续处理其他列')
1905
+
1906
+ def pre_format_columns_safe(sheet, columns, data_rows):
1907
+ """
1908
+ 预格式化函数:在写入数据前安全地设置列格式
1909
+
1910
+ Args:
1911
+ sheet: Excel工作表对象
1912
+ columns: 要格式化的列名列表
1913
+ data_rows: 预期数据行数
1914
+ """
1915
+ if not columns or not isinstance(columns, list):
1916
+ return
1917
+
1918
+ for col_name in columns:
1919
+ try:
1920
+ if isinstance(col_name, int):
1921
+ col_name = xw.utils.col_name(col_name)
1922
+
1923
+ log(f'预格式化列 [{col_name}] 为文本格式')
1924
+
1925
+ # 方法1:先创建最小范围,避免整列操作
1926
+ try:
1927
+ # 创建足够大的范围来覆盖预期数据
1928
+ range_str = f'{col_name}1:{col_name}{max(data_rows, 1000)}'
1929
+ sheet.range(range_str).number_format = '@'
1930
+ log(f'预格式化成功: {range_str}')
1931
+ except Exception as e1:
1932
+ log(f'预格式化方法1失败: {e1}')
1933
+
1934
+ # 方法2:逐行设置格式,更安全但稍慢
1935
+ try:
1936
+ for row in range(1, data_rows + 1):
1937
+ cell = sheet.range(f'{col_name}{row}')
1938
+ cell.number_format = '@'
1939
+ log(f'逐行预格式化成功: {col_name}')
1940
+ except Exception as e2:
1941
+ log(f'逐行预格式化也失败: {e2}')
1942
+
1943
+ except Exception as e:
1944
+ log(f'预格式化列 {col_name} 失败: {e},继续处理其他列')
1945
+
1946
+ def post_format_columns_safe(sheet, columns, data_rows):
1947
+ """
1948
+ 后格式化函数:在写入数据后确认列格式
1949
+
1950
+ Args:
1951
+ sheet: Excel工作表对象
1952
+ columns: 要格式化的列名列表
1953
+ data_rows: 实际数据行数
1954
+ """
1955
+ if not columns or not isinstance(columns, list):
1956
+ return
1957
+
1958
+ for col_name in columns:
1959
+ try:
1960
+ if isinstance(col_name, int):
1961
+ col_name = xw.utils.col_name(col_name)
1962
+
1963
+ log(f'后格式化列 [{col_name}] 为文本格式')
1964
+
1965
+ # 只对实际有数据的行进行格式化
1966
+ if data_rows > 0:
1967
+ range_str = f'{col_name}1:{col_name}{data_rows}'
1968
+ sheet.range(range_str).number_format = '@'
1969
+ log(f'后格式化成功: {range_str}')
1970
+
1971
+ except Exception as e:
1972
+ log(f'后格式化列 {col_name} 失败: {e},继续处理其他列')
1973
+
1863
1974
  def format_to_text(sheet, columns=None):
1864
1975
  if columns is None:
1865
1976
  return
@@ -2469,12 +2580,25 @@ def batch_excel_operations(excel_path, operations):
2469
2580
  data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
2470
2581
  # 清空工作表
2471
2582
  sheet.clear()
2472
- # 格式化文本列
2473
- if format_to_text_colunm:
2474
- format_to_text_v2(sheet, format_to_text_colunm)
2583
+
2584
+ # 先设置文本格式,再写入数据(确保格式生效)
2585
+ if format_to_text_colunm and format_to_text_colunm[0]:
2586
+ try:
2587
+ # 使用安全的预格式化方式
2588
+ pre_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
2589
+ except Exception as e:
2590
+ log(f"预格式化失败: {e},继续执行")
2591
+
2475
2592
  # 写入数据
2476
2593
  sheet.range('A1').value = data
2477
2594
  log(f"批量操作:写入数据到 {sheet_name}")
2595
+
2596
+ # 写入后再次确认格式(双重保险)
2597
+ if format_to_text_colunm and format_to_text_colunm[0]:
2598
+ try:
2599
+ post_format_columns_safe(sheet, format_to_text_colunm[0], len(data))
2600
+ except Exception as e:
2601
+ log(f"后格式化失败: {e}")
2478
2602
 
2479
2603
  elif operation_type == 'format':
2480
2604
  format_func, format_args = args[0], args[1:] if len(args) > 1 else ()