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

@@ -0,0 +1,45 @@
1
+ import os
2
+ import time
3
+ import threading
4
+ from collections import deque
5
+ from datetime import datetime
6
+ import traceback
7
+
8
+ class RateLimitedSender:
9
+ def __init__(self, sender_func, interval=60):
10
+ """
11
+ :param sender_func: 实际的发送函数,参数是字符串消息
12
+ :param interval: 最短发送间隔(秒)
13
+ """
14
+ self.sender_func = sender_func
15
+ self.interval = interval
16
+ self.queue = deque()
17
+ self.lock = threading.Lock()
18
+ self.last_send_time = 0
19
+
20
+ self.thread = threading.Thread(target=self._worker, daemon=True)
21
+ self.thread.start()
22
+
23
+ def send(self, message):
24
+ """添加消息到队列(非阻塞)"""
25
+ with self.lock:
26
+ self.queue.append(message)
27
+
28
+ def _flush(self):
29
+ """立即发送队列消息(内部调用)"""
30
+ if not self.queue:
31
+ return
32
+ batch_message = "\n---\n".join(self.queue)
33
+ self.queue.clear()
34
+ try:
35
+ self.sender_func(batch_message)
36
+ except Exception as e:
37
+ print(f"[RateLimitedSender] 发送失败: {e}")
38
+ self.last_send_time = time.time()
39
+
40
+ def _worker(self):
41
+ while True:
42
+ with self.lock:
43
+ if self.queue and (time.time() - self.last_send_time >= self.interval):
44
+ self._flush()
45
+ time.sleep(1)
qrpa/__init__.py CHANGED
@@ -3,9 +3,12 @@ from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_
3
3
 
4
4
  from .shein_ziniao import ZiniaoRunner
5
5
 
6
- from .fun_base import log
6
+ from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename
7
7
 
8
8
  from .time_utils import TimeUtils
9
9
 
10
10
  from .fun_file import read_dict_from_file, read_dict_from_file_ex, write_dict_to_file, write_dict_to_file_ex
11
11
  from .fun_file import get_progress_json_ex, check_progress_json_ex, done_progress_json_ex
12
+
13
+ from .fun_web import fetch, fetch_via_iframe, find_all_iframe, full_screen_shot
14
+ from .fun_win import *
qrpa/fun_base.py CHANGED
@@ -2,11 +2,15 @@ import inspect
2
2
  import os
3
3
  import traceback
4
4
  import socket
5
+ import hashlib
6
+ import shutil
5
7
 
6
8
  from datetime import datetime
7
9
 
8
10
  from .wxwork import WxWorkBot
9
11
 
12
+ from .RateLimitedSender import RateLimitedSender
13
+
10
14
  from typing import TypedDict
11
15
 
12
16
  # 定义一个 TypedDict 来提供配置结构的类型提示
@@ -20,7 +24,6 @@ class Config(TypedDict):
20
24
  wxwork_bot_exception: str
21
25
  ziniao: ZiNiao
22
26
  auto_dir: str
23
- shein_store_alias: str
24
27
 
25
28
  def log(*args, **kwargs):
26
29
  """封装 print 函数,使其行为与原 print 一致,并写入日志文件"""
@@ -33,9 +36,72 @@ def log(*args, **kwargs):
33
36
  def hostname():
34
37
  return socket.gethostname()
35
38
 
36
- def send_exception(config: Config, msg=None):
37
- error_msg = f'【{hostname()}】{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n{msg}\n'
39
+ # ================= WxWorkBot 限频异常发送 =================
40
+ def send_exception(msg=None):
41
+ """
42
+ 发送异常到 WxWorkBot,限制发送频率,支持异步批量
43
+ """
44
+ # 首次调用时初始化限频发送器
45
+ if not hasattr(send_exception, "_wx_sender"):
46
+ def wxwork_bot_send(message):
47
+ bot_id = os.getenv('wxwork_bot_exception', 'ee5a048a-1b9e-41e4-9382-aa0ee447898e')
48
+ WxWorkBot(bot_id).send_text(message)
49
+
50
+ send_exception._wx_sender = RateLimitedSender(
51
+ sender_func=wxwork_bot_send,
52
+ interval=30, # 10 秒发一次
53
+ )
54
+
55
+ # 构造异常消息
56
+ error_msg = f'【{hostname()}】{datetime.now():%Y-%m-%d %H:%M:%S}\n{msg}\n'
38
57
  error_msg += f'{traceback.format_exc()}'
39
- WxWorkBot(config['wxwork_bot_exception']).send_text(error_msg)
40
58
  print(error_msg)
59
+
60
+ # 异步发送
61
+ send_exception._wx_sender.send(error_msg)
41
62
  return error_msg
63
+
64
+ def get_safe_value(data, key, default=0):
65
+ value = data.get(key)
66
+ return default if value is None else value
67
+
68
+ def md5_string(s):
69
+ # 需要先将字符串编码为 bytes
70
+ return hashlib.md5(s.encode('utf-8')).hexdigest()
71
+
72
+ # 将windows文件名不支持的字符替换成下划线
73
+ def sanitize_filename(filename):
74
+ # Windows 文件名非法字符
75
+ illegal_chars = r'\/:*?"<>|'
76
+ for char in illegal_chars:
77
+ filename = filename.replace(char, '_')
78
+
79
+ # 去除首尾空格和点
80
+ filename = filename.strip(' .')
81
+
82
+ # 替换连续多个下划线为单个
83
+ filename = '_'.join(filter(None, filename.split('_')))
84
+
85
+ return filename
86
+
87
+ def add_https(url):
88
+ if url and url.startswith('//'):
89
+ return 'https:' + url
90
+ return url
91
+
92
+ def create_file_path(file_path):
93
+ dir_name = os.path.dirname(file_path)
94
+ if dir_name and not os.path.exists(dir_name):
95
+ os.makedirs(dir_name, exist_ok=True) # 递归创建目录
96
+ return file_path
97
+
98
+ def copy_file(source, destination):
99
+ try:
100
+ shutil.copy2(source, destination)
101
+ print(f"文件已复制到 {destination}")
102
+ except FileNotFoundError:
103
+ print(f"错误:源文件 '{source}' 不存在")
104
+ except PermissionError:
105
+ print(f"错误:没有权限复制到 '{destination}'")
106
+ except Exception as e:
107
+ print(f"错误:发生未知错误 - {e}")