pixelarraylib 1.0.0__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.
arraylib/aliyun/oss.py ADDED
@@ -0,0 +1,649 @@
1
+ import os
2
+ import json
3
+ import traceback
4
+ import oss2
5
+ import hmac
6
+ import base64
7
+ import aiohttp
8
+ import asyncio
9
+ import xml.etree.ElementTree as ET
10
+ from hashlib import sha1
11
+ from datetime import datetime
12
+ from urllib.parse import quote, urlencode
13
+ from datetime import datetime
14
+ from arraylib.system.common import size_unit_convert, percentage
15
+ from arraylib.monitor.feishu import Feishu
16
+ import aiofiles
17
+ from concurrent.futures import ThreadPoolExecutor
18
+
19
+
20
+ feishu_alert = Feishu("devtoolkit服务报警")
21
+
22
+
23
+ class OSSUtils:
24
+ def __init__(
25
+ self,
26
+ access_key_id,
27
+ access_key_secret,
28
+ region_id,
29
+ bucket_name,
30
+ use_vpc=False,
31
+ ):
32
+ self.bucket_name = bucket_name
33
+ if use_vpc:
34
+ self.endpoint = f"https://oss-{region_id}-internal.aliyuncs.com"
35
+ else:
36
+ self.endpoint = f"https://oss-{region_id}.aliyuncs.com"
37
+ self.client = self.get_oss_client(
38
+ access_key_id, access_key_secret, self.endpoint, bucket_name
39
+ )
40
+
41
+ def get_oss_client(
42
+ self, access_key_id, access_key_secret, endpoint, bucket_name, retry=3
43
+ ):
44
+ """
45
+ description:
46
+ 获取OSS客户端
47
+ parameters:
48
+ access_key_id(str): 阿里云access_key_id
49
+ access_key_secret(str): 阿里云access_key_secret
50
+ endpoint(str): 阿里云endpoint
51
+ bucket_name(str): 阿里云bucket_name
52
+ return:
53
+ bucket(oss2.Bucket): OSS客户端
54
+ """
55
+ for _ in range(retry):
56
+ try:
57
+ return oss2.Bucket(
58
+ oss2.Auth(access_key_id, access_key_secret), endpoint, bucket_name
59
+ )
60
+ except Exception as e:
61
+ print(e)
62
+ return None
63
+
64
+ def list_objects(self, prefix, batch_size=100):
65
+ """
66
+ description:
67
+ 列出OSS中的对象,返回生成器
68
+ parameters:
69
+ prefix(str): 前缀
70
+ batch_size(int): 每批返回的对象数量
71
+ return:
72
+ generator: 对象列表生成器
73
+ """
74
+ next_marker = ""
75
+ while True:
76
+ object_list = self.client.list_objects(
77
+ prefix=prefix, marker=next_marker, max_keys=batch_size
78
+ )
79
+ for obj in object_list.object_list:
80
+ if obj.size > 0:
81
+ yield obj
82
+ if not object_list.is_truncated:
83
+ break
84
+ next_marker = object_list.next_marker
85
+
86
+ def is_object(self, key):
87
+ """
88
+ description:
89
+ 判断某个对象是否在OSS中存在
90
+ parameters:
91
+ key(str): 对象的key
92
+ return:
93
+ flag(bool): 是否存在
94
+ """
95
+ try:
96
+ return self.client.object_exists(key)
97
+ except Exception as e:
98
+ print(e)
99
+ return False
100
+
101
+ def get_size(self, prefix, unit="B"):
102
+ """
103
+ description:
104
+ 获取OSS中对象或前缀下所有对象的大小
105
+ parameters:
106
+ prefix(str): 前缀
107
+ unit(str): 单位,默认B
108
+ return:
109
+ size(int): 对象或前缀下所有对象的大小
110
+ count(int): 对象或前缀下所有对象的个数
111
+ """
112
+ object_list = self.list_objects(prefix)
113
+ total_size = 0
114
+ object_count = 0
115
+ for obj in object_list:
116
+ total_size += self.client.get_object_meta(obj.key).content_length
117
+ object_count += 1
118
+
119
+ return (
120
+ size_unit_convert(total_size, input_unit="B", output_unit=unit),
121
+ object_count,
122
+ )
123
+
124
+ def delete(self, prefix):
125
+ """
126
+ description:
127
+ 删除OSS中的对象或前缀
128
+ parameters:
129
+ prefix(str): 对象或前缀的key
130
+ return:
131
+ flag(bool): 是否删除成功
132
+ """
133
+ try:
134
+ for obj in self.list_objects(prefix):
135
+ self.client.delete_object(obj.key)
136
+ return True
137
+ except Exception as e:
138
+ feishu_alert.send(traceback.format_exc())
139
+ return False
140
+
141
+ def ls(self, prefix):
142
+ """
143
+ description:
144
+ 列出当前目录下的所有文件夹和文件的key
145
+ parameters:
146
+ prefix(str): 前缀
147
+ return:
148
+ key_list(list): 文件夹和文件的key列表
149
+ """
150
+ if not prefix.endswith("/"):
151
+ prefix += "/"
152
+
153
+ key_list = []
154
+ next_marker = ""
155
+
156
+ while True:
157
+ result = self.client.list_objects(
158
+ prefix=prefix, marker=next_marker, max_keys=1000, delimiter="/"
159
+ )
160
+
161
+ key_list.extend(
162
+ [{"type": "directory", "key": p} for p in result.prefix_list]
163
+ )
164
+ key_list.extend(
165
+ [{"type": "file", "key": obj.key} for obj in result.object_list]
166
+ )
167
+
168
+ if not result.is_truncated:
169
+ break
170
+ next_marker = result.next_marker
171
+
172
+ return [key for key in key_list if key["key"] != prefix]
173
+
174
+ def get_last_modified(self, prefix):
175
+ """
176
+ description:
177
+ 获取OSS中对象或前缀下所有对象的最后修改时间
178
+ parameters:
179
+ prefix(str): 前缀
180
+ return:
181
+ last_modified(str): 最后修改时间
182
+ """
183
+ if self.is_object(prefix):
184
+ obj_key = prefix
185
+ else:
186
+ obj_key = max(
187
+ self.list_objects(prefix),
188
+ key=lambda x: self.client.get_object_meta(x.key).last_modified,
189
+ ).key
190
+
191
+ return datetime.fromtimestamp(
192
+ self.client.get_object_meta(obj_key).last_modified
193
+ ).strftime("%Y-%m-%d %H:%M:%S")
194
+
195
+ def path_exists(self, prefix):
196
+ """
197
+ description:
198
+ 检查OSS中对象或前缀是否存在
199
+ parameters:
200
+ prefix(str): 前缀
201
+ return:
202
+ flag(bool): 是否存在
203
+ """
204
+ try:
205
+ for obj in self.list_objects(prefix):
206
+ return True
207
+ return False
208
+ except Exception as e:
209
+ feishu_alert.send(traceback.format_exc())
210
+ return False
211
+
212
+ def download_object(self, prefix, dir_path):
213
+ """
214
+ description:
215
+ 下载OSS中的对象
216
+ parameters:
217
+ prefix(str): 前缀
218
+ local_path(str): 本地文件路径
219
+ return:
220
+ flag(bool): 是否下载成功
221
+ """
222
+ if not dir_path:
223
+ dir_path = "."
224
+ if not self.is_object(prefix):
225
+ return False
226
+ file_name = os.path.basename(prefix)
227
+ os.makedirs(dir_path, exist_ok=True)
228
+ download_path = os.path.join(dir_path, file_name)
229
+ print("download_path", download_path)
230
+ try:
231
+ self.client.get_object_to_file(prefix, download_path)
232
+ return True
233
+ except Exception as e:
234
+ feishu_alert.send(f"oss download_object error: {traceback.format_exc()}")
235
+ return False
236
+
237
+ def upload_object(self, prefix, local_path):
238
+ """
239
+ description:
240
+ 上传对象到OSS
241
+ parameters:
242
+ prefix(str): 前缀
243
+ local_path(str): 本地文件路径
244
+ return:
245
+ flag(bool): 是否上传成功
246
+ """
247
+ if not os.path.exists(local_path):
248
+ print(f"文件 {local_path} 不存在,请检查!")
249
+ return False
250
+
251
+ file_name = os.path.basename(local_path)
252
+ try:
253
+ self.client.put_object_from_file(
254
+ os.path.join(prefix, file_name), local_path
255
+ )
256
+ return True
257
+ except Exception as e:
258
+ feishu_alert.send(traceback.format_exc())
259
+ return False
260
+
261
+ def generate_presigned_url(self, prefix, expires_in=60 * 60 * 24):
262
+ """
263
+ description:
264
+ 使用OSS生成预签名URL
265
+ parameters:
266
+ prefix(str): 前缀
267
+ expires_in(int): 过期时间,默认24小时
268
+ return:
269
+ url(str): 预签名URL
270
+ """
271
+ return self.client.sign_url("GET", prefix, expires_in)
272
+
273
+
274
+ class OSSObject:
275
+ """OSS 对象表示类"""
276
+
277
+ def __init__(self, key, size, last_modified, etag=None):
278
+ self.key = key
279
+ self.size = size
280
+ self.last_modified = last_modified
281
+ self.etag = etag
282
+
283
+
284
+ class OSSUtilsAsync:
285
+ def __init__(
286
+ self, access_key_id, access_key_secret, region_id, bucket_name, use_vpc=False
287
+ ):
288
+ self.access_key_id = access_key_id
289
+ self.access_key_secret = access_key_secret
290
+ self.bucket_name = bucket_name
291
+ if use_vpc:
292
+ self.endpoint = (
293
+ f"https://{self.bucket_name}.oss-{region_id}-internal.aliyuncs.com"
294
+ )
295
+ else:
296
+ self.endpoint = f"https://{self.bucket_name}.oss-{region_id}.aliyuncs.com"
297
+ self.base_url = self.endpoint
298
+ self.session = None
299
+ self.thread_pool = ThreadPoolExecutor(max_workers=4)
300
+
301
+ # 创建oss2的Auth对象用于签名
302
+ self.auth = oss2.Auth(access_key_id, access_key_secret)
303
+
304
+ # 创建oss2的Bucket对象用于同步操作
305
+ correct_endpoint = self.endpoint.replace(f"{self.bucket_name}.", "")
306
+ self.oss_bucket = oss2.Bucket(self.auth, correct_endpoint, self.bucket_name)
307
+
308
+ async def _ensure_session(self):
309
+ """确保 aiohttp 会话已创建"""
310
+ if self.session is None:
311
+ self.session = aiohttp.ClientSession()
312
+ return self.session
313
+
314
+ async def close(self):
315
+ """关闭 aiohttp 会话和线程池"""
316
+ if self.session is not None:
317
+ await self.session.close()
318
+ self.session = None
319
+ if self.thread_pool is not None:
320
+ self.thread_pool.shutdown(wait=True)
321
+ self.thread_pool = None
322
+
323
+ async def _run_in_thread(self, func, *args, **kwargs):
324
+ """在线程池中运行同步函数"""
325
+ loop = asyncio.get_event_loop()
326
+ return await loop.run_in_executor(self.thread_pool, func, *args, **kwargs)
327
+
328
+ def _sign_request(self, method, url, headers=None, params=None):
329
+ """使用oss2库生成OSS签名"""
330
+ if headers is None:
331
+ headers = {}
332
+ if params is None:
333
+ params = {}
334
+
335
+ # 创建oss2的Request对象
336
+ req = oss2.http.Request(method, url, params=params, headers=headers)
337
+
338
+ # 使用oss2的签名功能
339
+ self.auth._sign_request(req, self.bucket_name, "")
340
+
341
+ # 过滤掉None键和None值
342
+ result = {}
343
+ for k, v in req.headers.items():
344
+ if k is not None and v is not None:
345
+ result[str(k)] = str(v)
346
+ return result
347
+
348
+ def _parse_list_objects_xml(self, xml_text):
349
+ """解析 OSS ListObjects 响应的 XML"""
350
+ try:
351
+ root = ET.fromstring(xml_text)
352
+ objects = []
353
+
354
+ # 查找所有 Contents 元素(不使用命名空间)
355
+ for contents in root.findall(".//Contents"):
356
+ key_elem = contents.find("Key")
357
+ size_elem = contents.find("Size")
358
+ last_modified_elem = contents.find("LastModified")
359
+ etag_elem = contents.find("ETag")
360
+
361
+ if key_elem is not None and size_elem is not None:
362
+ # URL解码key
363
+ from urllib.parse import unquote
364
+
365
+ key = unquote(key_elem.text) if key_elem.text else ""
366
+ size = int(size_elem.text) if size_elem.text else 0
367
+ last_modified = (
368
+ last_modified_elem.text
369
+ if last_modified_elem is not None
370
+ else None
371
+ )
372
+ etag = etag_elem.text if etag_elem is not None else None
373
+
374
+ # 只返回大小大于0的对象(排除目录)
375
+ if size > 0:
376
+ objects.append(OSSObject(key, size, last_modified, etag))
377
+
378
+ return objects
379
+ except ET.ParseError as e:
380
+ print(f"XML 解析错误: {e}")
381
+ return []
382
+
383
+ def _parse_ls_xml(self, xml_text, prefix):
384
+ """解析 OSS ListObjects 响应的 XML (用于 ls 方法)"""
385
+ try:
386
+ root = ET.fromstring(xml_text)
387
+ key_list = []
388
+
389
+ # 查找所有 CommonPrefixes 元素 (目录) - 不使用命名空间
390
+ for prefix_elem in root.findall(".//CommonPrefixes"):
391
+ prefix_text = prefix_elem.find("Prefix")
392
+ if prefix_text is not None and prefix_text.text != prefix:
393
+ # URL解码key,与同步版本保持一致
394
+ from urllib.parse import unquote
395
+
396
+ decoded_key = unquote(prefix_text.text)
397
+ key_list.append({"type": "directory", "key": decoded_key})
398
+
399
+ # 查找所有 Contents 元素 (文件) - 不使用命名空间
400
+ for contents in root.findall(".//Contents"):
401
+ key_elem = contents.find("Key")
402
+ if key_elem is not None and key_elem.text != prefix:
403
+ # URL解码key,与同步版本保持一致
404
+ from urllib.parse import unquote
405
+
406
+ decoded_key = unquote(key_elem.text)
407
+ key_list.append({"type": "file", "key": decoded_key})
408
+
409
+ return key_list
410
+ except ET.ParseError as e:
411
+ print(f"XML 解析错误: {e}")
412
+ return []
413
+
414
+ async def list_objects(self, prefix="", max_keys=100):
415
+ """列出对象,返回所有匹配的对象"""
416
+ session = await self._ensure_session()
417
+ all_objects = []
418
+ next_marker = ""
419
+
420
+ while True:
421
+ params = {
422
+ "prefix": prefix,
423
+ "max-keys": str(max_keys),
424
+ "delimiter": "",
425
+ "marker": next_marker,
426
+ "encoding-type": "url",
427
+ }
428
+
429
+ # 构建URL参数
430
+ param_str = "&".join(f"{k}={v}" for k, v in params.items())
431
+ url = f"{self.base_url}/?{param_str}"
432
+ headers = self._sign_request("GET", url, params=params)
433
+ async with session.get(url, headers=headers) as resp:
434
+ if resp.status != 200:
435
+ break
436
+
437
+ text = await resp.text()
438
+ objects = self._parse_list_objects_xml(text)
439
+ all_objects.extend(objects)
440
+
441
+ # 检查是否还有更多对象
442
+ root = ET.fromstring(text)
443
+ is_truncated = root.find(".//IsTruncated")
444
+ if is_truncated is None or is_truncated.text.lower() != "true":
445
+ break
446
+
447
+ # 获取下一页的marker
448
+ next_marker_elem = root.find(".//NextMarker")
449
+ if next_marker_elem is None:
450
+ break
451
+ next_marker = next_marker_elem.text
452
+
453
+ return all_objects
454
+
455
+ async def is_object(self, key):
456
+ """判断对象是否存在"""
457
+ try:
458
+ # 使用list_objects来检查对象是否存在,避免签名问题
459
+ object_list = await self.list_objects(key)
460
+ # 检查是否有完全匹配的对象
461
+ for obj in object_list:
462
+ if obj.key == key:
463
+ return True
464
+ return False
465
+ except Exception as e:
466
+ print(f"is_object error: {e}")
467
+ return False
468
+
469
+ async def upload_object(self, prefix, local_path):
470
+ """上传对象"""
471
+ if not os.path.exists(local_path):
472
+ return False
473
+
474
+ file_name = os.path.basename(local_path)
475
+ key = f"{prefix}/{file_name}" if prefix else file_name
476
+
477
+ # 使用线程池运行oss2的同步操作
478
+ try:
479
+ await self._run_in_thread(self.oss_bucket.put_object_from_file, key, local_path)
480
+ return True
481
+ except Exception as e:
482
+ return False
483
+
484
+ async def download_object(self, key, dir_path="."):
485
+ """下载对象"""
486
+ if dir_path and dir_path != "":
487
+ os.makedirs(dir_path, exist_ok=True)
488
+ file_name = os.path.basename(key)
489
+ local_path = os.path.join(dir_path, file_name)
490
+
491
+ # 使用线程池运行oss2的同步操作
492
+ try:
493
+ await self._run_in_thread(self.oss_bucket.get_object_to_file, key, local_path)
494
+ return True
495
+ except Exception as e:
496
+ return False
497
+
498
+ async def delete(self, prefix):
499
+ """删除OSS中的对象或前缀"""
500
+ try:
501
+ object_list = await self.list_objects(prefix)
502
+
503
+ # 使用线程池批量删除对象
504
+ delete_tasks = []
505
+ for obj in object_list:
506
+ task = self._run_in_thread(self.oss_bucket.delete_object, obj.key)
507
+ delete_tasks.append(task)
508
+
509
+ # 等待所有删除操作完成
510
+ results = await asyncio.gather(*delete_tasks, return_exceptions=True)
511
+
512
+ # 检查是否有任何删除失败
513
+ for result in results:
514
+ if isinstance(result, Exception):
515
+ return False
516
+
517
+ return True
518
+ except Exception as e:
519
+ print(f"删除对象时出错: {e}")
520
+ return False
521
+
522
+ async def get_size(self, prefix, unit="B"):
523
+ """获取OSS中对象或前缀下所有对象的大小"""
524
+ object_list = await self.list_objects(prefix)
525
+ total_size = 0
526
+ object_count = 0
527
+
528
+ # 使用线程池获取每个对象的准确大小
529
+ size_tasks = []
530
+ for obj in object_list:
531
+ task = self._run_in_thread(self.oss_bucket.get_object_meta, obj.key)
532
+ size_tasks.append(task)
533
+
534
+ # 等待所有获取元数据操作完成
535
+ results = await asyncio.gather(*size_tasks, return_exceptions=True)
536
+
537
+ for result in results:
538
+ if not isinstance(result, Exception):
539
+ total_size += result.content_length
540
+ object_count += 1
541
+
542
+ return (
543
+ size_unit_convert(total_size, input_unit="B", output_unit=unit),
544
+ object_count,
545
+ )
546
+
547
+ async def ls(self, prefix):
548
+ """列出当前目录下的所有文件夹和文件的key"""
549
+ if not prefix.endswith("/"):
550
+ prefix += "/"
551
+
552
+ session = await self._ensure_session()
553
+ key_list = []
554
+ next_marker = ""
555
+
556
+ while True:
557
+ params = {
558
+ "prefix": prefix,
559
+ "delimiter": "/",
560
+ "max-keys": "1000",
561
+ "marker": next_marker,
562
+ "encoding-type": "url",
563
+ }
564
+
565
+ url = f"{self.base_url}"
566
+ headers = self._sign_request("GET", "", params=params)
567
+
568
+ async with session.get(url, headers=headers, params=params) as resp:
569
+ if resp.status != 200:
570
+ break
571
+
572
+ text = await resp.text()
573
+ batch_key_list = self._parse_ls_xml(text, prefix)
574
+ key_list.extend(batch_key_list)
575
+
576
+ # 检查是否还有更多结果
577
+ root = ET.fromstring(text)
578
+ is_truncated = root.find(".//IsTruncated")
579
+ if is_truncated is None or is_truncated.text.lower() != "true":
580
+ break
581
+
582
+ # 获取下一页的marker
583
+ next_marker_elem = root.find(".//NextMarker")
584
+ if next_marker_elem is None:
585
+ break
586
+ next_marker = next_marker_elem.text
587
+
588
+ return key_list
589
+
590
+ async def get_last_modified(self, prefix):
591
+ """获取OSS中对象或前缀下所有对象的最后修改时间,格式化为%Y-%m-%d %H:%M:%S"""
592
+ from datetime import datetime
593
+
594
+ def format_time(timestamp):
595
+ if not timestamp:
596
+ return ""
597
+ try:
598
+ # 将时间戳转换为datetime对象
599
+ dt = datetime.fromtimestamp(timestamp)
600
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
601
+ except Exception:
602
+ return ""
603
+
604
+ if await self.is_object(prefix):
605
+ # 如果是单个对象,直接获取其最后修改时间
606
+ try:
607
+ meta = await self._run_in_thread(self.oss_bucket.get_object_meta, prefix)
608
+ return format_time(meta.last_modified)
609
+ except Exception:
610
+ return ""
611
+ else:
612
+ # 如果是前缀,找到最后修改的对象 - 与同步版本保持一致
613
+ object_list = await self.list_objects(prefix)
614
+ if object_list:
615
+ # 使用线程池获取每个对象的最后修改时间
616
+ meta_tasks = []
617
+ for obj in object_list:
618
+ task = self._run_in_thread(self.oss_bucket.get_object_meta, obj.key)
619
+ meta_tasks.append(task)
620
+
621
+ # 等待所有获取元数据操作完成
622
+ results = await asyncio.gather(*meta_tasks, return_exceptions=True)
623
+
624
+ # 找到最后修改的对象
625
+ latest_time = 0
626
+ latest_meta = None
627
+ for result in results:
628
+ if not isinstance(result, Exception) and result.last_modified > latest_time:
629
+ latest_time = result.last_modified
630
+ latest_meta = result
631
+
632
+ if latest_meta:
633
+ return format_time(latest_meta.last_modified)
634
+
635
+ return ""
636
+
637
+ async def path_exists(self, prefix):
638
+ """检查OSS中对象或前缀是否存在"""
639
+ try:
640
+ object_list = await self.list_objects(prefix)
641
+ return len(object_list) > 0
642
+ except Exception as e:
643
+ print(f"检查路径存在性时出错: {e}")
644
+ return False
645
+
646
+ async def generate_presigned_url(self, key, expires_in=3600):
647
+ """生成预签名URL"""
648
+ # 使用线程池运行oss2的同步操作
649
+ return await self._run_in_thread(self.oss_bucket.sign_url, "GET", key, expires_in)
arraylib/aliyun/sms.py ADDED
@@ -0,0 +1,59 @@
1
+ import random
2
+ import traceback
3
+ from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
4
+ from alibabacloud_tea_openapi import models as open_api_models
5
+ from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
6
+ from alibabacloud_tea_util import models as util_models
7
+ from arraylib.monitor.feishu import Feishu
8
+ feishu_alert = Feishu("devtoolkit服务报警")
9
+
10
+ class SMSUtils:
11
+ def __init__(self, access_key_id, access_key_secret):
12
+ self.sms_cilent = Dysmsapi20170525Client(
13
+ open_api_models.Config(
14
+ type="access_key",
15
+ access_key_id=access_key_id,
16
+ access_key_secret=access_key_secret,
17
+ endpoint="dysmsapi.aliyuncs.com",
18
+ )
19
+ )
20
+
21
+ def generate_verification_code(self, length=6):
22
+ """
23
+ description:
24
+ 生成数字验证码
25
+ parameters:
26
+ length(int): 验证码长度
27
+ return:
28
+ str: 验证码
29
+ """
30
+ return "".join(str(random.randint(0, 9)) for _ in range(length))
31
+
32
+ def send_verification_code(self, phone_numbers, verification_code):
33
+ """
34
+ description:
35
+ 发送验证码给指定手机号
36
+ param:
37
+ phone_numbers: 手机号
38
+ verification_code: 验证码(6位数字)
39
+ return:
40
+ flag(bool): 是否发送成功
41
+ """
42
+ send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
43
+ sign_name="北京矩阵像素科技有限公司",
44
+ template_code="SMS_318235798",
45
+ phone_numbers=phone_numbers,
46
+ template_param=f'{{"code":"{verification_code}"}}',
47
+ )
48
+ runtime = util_models.RuntimeOptions()
49
+ try:
50
+ response = self.sms_cilent.send_sms_with_options(send_sms_request, runtime)
51
+ flag = bool(
52
+ response and response.status_code == 200 and response.body.code == "OK"
53
+ )
54
+ if not flag:
55
+ feishu_alert.send(f"短信发送失败: {response}")
56
+ return flag
57
+ except Exception as e:
58
+ feishu_alert.send("短信发送失败: " + traceback.format_exc())
59
+ return False