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.

qrpa/fun_web.py ADDED
@@ -0,0 +1,148 @@
1
+ import json
2
+ from typing import Optional, Union
3
+ from playwright.sync_api import Page
4
+
5
+ from .fun_base import log, send_exception
6
+ from .time_utils import get_current_datetime
7
+
8
+
9
+ def fetch(page: Page, url: str, params: Optional[Union[dict, list, str]] = None, headers: Optional[dict] = None, config:
10
+ Optional[dict] = None) -> dict:
11
+ """
12
+ 发送 HTTP POST 请求,支持自定义 headers。
13
+
14
+ :param page: Playwright 的 Page 对象
15
+ :param url: 请求地址
16
+ :param params: 请求参数(dict、list、str 或 None)
17
+ :param headers: 自定义 headers 字典
18
+ :return: 服务器返回的 JSON 响应(dict)
19
+ """
20
+ if params is not None and not isinstance(params, (dict, list, str)):
21
+ raise ValueError("params 参数必须是 dict、list、str 或 None")
22
+ if headers is not None and not isinstance(headers, dict):
23
+ raise ValueError("headers 参数必须是 dict 或 None")
24
+
25
+ try:
26
+ response = page.evaluate("""
27
+ async ({ url, params, extraHeaders }) => {
28
+ try {
29
+ const defaultHeaders = {
30
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
31
+ 'x-requested-with': 'XMLHttpRequest',
32
+ };
33
+
34
+ const headers = Object.assign({}, defaultHeaders, extraHeaders || {});
35
+ const options = {
36
+ method: 'POST',
37
+ credentials: 'include',
38
+ headers: headers
39
+ };
40
+
41
+ if (params !== null) {
42
+ if (typeof params === 'string') {
43
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
44
+ options.body = params;
45
+ } else {
46
+ options.headers['Content-Type'] = 'application/json';
47
+ options.body = JSON.stringify(params);
48
+ }
49
+ }
50
+
51
+ const response = await fetch(url, options);
52
+ if (!response.ok) {
53
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
54
+ }
55
+ return await response.json();
56
+ } catch (error) {
57
+ return { "error": "fetch_failed", "message": error.message };
58
+ }
59
+ }
60
+ """, {"url": url, "params": params, "extraHeaders": headers})
61
+
62
+ return response
63
+ except Exception as e:
64
+ raise send_exception()
65
+ # return {"error": "fetch error", "message": str(e)}
66
+
67
+
68
+ def fetch_via_iframe(page: Page, target_domain: str, url: str, params: Optional[Union[dict, list, str]] = None, config:
69
+ Optional[dict] = None) -> dict:
70
+ """
71
+ 方案 2:在 iframe 内部执行 fetch 请求,绕过 CORS 限制
72
+
73
+ :param page: Playwright 的 Page 对象
74
+ :param url: 目标请求的 URL
75
+ :param target_domain: 目标 iframe 所在的域名(用于匹配 iframe)
76
+ :param params: 请求参数(dict、list、str 或 None)
77
+ :return: 服务器返回的 JSON 响应(dict)
78
+ """
79
+ if params is not None and not isinstance(params, (dict, list, str)):
80
+ raise ValueError("params 参数必须是 dict、list、str 或 None")
81
+ response = None
82
+ try:
83
+ # 获取所有 iframe,查找目标域名的 iframe
84
+ frames = page.frames
85
+ target_frame = None
86
+ for frame in frames:
87
+ if target_domain in frame.url:
88
+ target_frame = frame
89
+ break
90
+
91
+ if not target_frame:
92
+ return {"error": "iframe_not_found", "message": f"未找到包含 {target_domain} 的 iframe"}
93
+
94
+ response = target_frame.evaluate("""
95
+ async ({ url, params }) => {
96
+ try {
97
+ const options = {
98
+ method: 'POST',
99
+ credentials: 'include',
100
+ headers: {
101
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
102
+ 'x-requested-with': 'XMLHttpRequest',
103
+ }
104
+ };
105
+
106
+ if (params !== null) {
107
+ if (typeof params === 'string') {
108
+ options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
109
+ options.body = params;
110
+ } else {
111
+ options.headers['Content-Type'] = 'application/json';
112
+ options.body = JSON.stringify(params);
113
+ }
114
+ }
115
+
116
+ const response = await fetch(url, options);
117
+ if (!response.ok) {
118
+ throw new Error(`HTTP ${response.status} - ${response.statusText}`);
119
+ }
120
+ return await response.json();
121
+ } catch (error) {
122
+ return { "error": "iframe_fetch_failed", "message": error.message };
123
+ }
124
+ }
125
+ """, {"url": url, "params": params})
126
+
127
+ return response
128
+ except Exception as e:
129
+ raise send_exception()
130
+ # return {"error": "iframe_exception", "message": str(e)}
131
+
132
+
133
+ # 找到一个页面里面所有的iframe
134
+ def find_all_iframe(page: Page):
135
+ frames = page.frames
136
+ for frame in frames:
137
+ log("找到 iframe:", frame.url)
138
+ return [frame.url for frame in frames]
139
+
140
+
141
+ # 全屏幕截图
142
+ def full_screen_shot(web_page: Page, config):
143
+ # 设置页面的视口大小为一个较大的值,确保截图高清
144
+ web_page.set_viewport_size({"width": 1920, "height": 1080})
145
+ # 截取全页面的高清截图
146
+ full_screenshot_image_path = f'{config.auto_dir}/screenshot/{get_current_datetime()}.png'
147
+ web_page.screenshot(path=full_screenshot_image_path, full_page=True)
148
+ return full_screenshot_image_path
qrpa/fun_win.py CHANGED
@@ -1,7 +1,58 @@
1
1
  import os
2
2
  import win32com.client
3
+ import winreg
3
4
 
4
- from .fun_base import log
5
+ import requests, subprocess, time
6
+ from contextlib import contextmanager
7
+
8
+ from .fun_base import log, create_file_path
9
+
10
+ default_chrome_user_data = 'D:\chrome_user_data'
11
+
12
+ def set_chrome_system_path():
13
+ path = os.path.dirname(find_software_install_path('chrome'))
14
+ add_to_system_path(path)
15
+
16
+ def add_to_system_path(path: str, scope: str = "user"):
17
+ """
18
+ 将指定路径添加到系统环境变量 Path 中
19
+ :param path: 要添加的路径(应为绝对路径)
20
+ :param scope: 'user' 表示用户变量,'system' 表示系统变量(需要管理员权限)
21
+ """
22
+ if not os.path.isabs(path):
23
+ raise ValueError("必须提供绝对路径")
24
+
25
+ path = os.path.normpath(path)
26
+
27
+ if scope == "user":
28
+ root = winreg.HKEY_CURRENT_USER
29
+ subkey = r"Environment"
30
+ elif scope == "system":
31
+ root = winreg.HKEY_LOCAL_MACHINE
32
+ subkey = r"SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
33
+ else:
34
+ raise ValueError("scope 参数必须是 'user' 或 'system'")
35
+
36
+ try:
37
+ with winreg.OpenKey(root, subkey, 0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
38
+ current_path, _ = winreg.QueryValueEx(key, "Path")
39
+ paths = current_path.split(";")
40
+
41
+ if path in paths:
42
+ print("路径已存在于 Path 中,无需添加: ", path)
43
+ return False
44
+
45
+ new_path = current_path + ";" + path
46
+ winreg.SetValueEx(key, "Path", 0, winreg.REG_EXPAND_SZ, new_path)
47
+ print("✅ 路径已成功添加到Path中: ", new_path)
48
+ return True
49
+
50
+ except PermissionError:
51
+ print("❌ 权限不足,系统变量修改需要管理员权限")
52
+ return False
53
+ except Exception as e:
54
+ print(f"❌ 添加失败: {e}")
55
+ return False
5
56
 
6
57
  def find_software_install_path(app_keyword: str):
7
58
  """从开始菜单或桌面查找指定软件的安装路径"""
@@ -29,3 +80,119 @@ def find_software_install_path(app_keyword: str):
29
80
 
30
81
  log(f'未能查找到{str}安装位置')
31
82
  return None
83
+
84
+ def init_chrome_env(account_list):
85
+ target = find_software_install_path('chrome')
86
+ for account in account_list:
87
+ store_key, port, *rest = account
88
+ user_data = rest[0] if rest and rest[0] else fr'{default_chrome_user_data}\{port}'
89
+ create_file_path(user_data)
90
+ args = fr'--remote-debugging-port={port} --user-data-dir="{user_data}"'
91
+ shortcut_name = f'{port}_{store_key}.lnk'
92
+ create_shortcut_on_desktop(target_path=target, arguments=args, shortcut_name=shortcut_name)
93
+
94
+ def create_shortcut_on_desktop(target_path, arguments='', shortcut_name='MyShortcut.lnk', icon_path=None):
95
+ """
96
+ 在桌面上创建快捷方式,若已存在指向相同 target + arguments 的快捷方式,则跳过创建。
97
+ """
98
+ # 获取当前用户桌面路径
99
+ desktop_path = os.path.join(os.environ['USERPROFILE'], 'Desktop')
100
+
101
+ shell = win32com.client.Dispatch('WScript.Shell')
102
+
103
+ # 检查是否已有相同目标的快捷方式
104
+ for file in os.listdir(desktop_path):
105
+ if file.lower().endswith('.lnk'):
106
+ shortcut_file = os.path.join(desktop_path, file)
107
+ shortcut = shell.CreateShortCut(shortcut_file)
108
+ if (os.path.normpath(shortcut.Targetpath) == os.path.normpath(target_path)
109
+ and shortcut.Arguments.strip() == arguments.strip()):
110
+ log("已存在指向该 target + args 的快捷方式,跳过创建")
111
+ return
112
+
113
+ # 创建新的快捷方式
114
+ shortcut_path = os.path.join(desktop_path, shortcut_name)
115
+ shortcut = shell.CreateShortCut(shortcut_path)
116
+ shortcut.Targetpath = target_path
117
+ shortcut.Arguments = arguments
118
+ shortcut.WorkingDirectory = os.path.dirname(target_path)
119
+ if icon_path:
120
+ shortcut.IconLocation = icon_path
121
+ shortcut.save()
122
+ log(f"已创建快捷方式:{shortcut_path}")
123
+
124
+ def check_chrome_dev(port=3000):
125
+ try:
126
+ url = f"http://127.0.0.1:{port}/json"
127
+ response = requests.get(url, timeout=5) # 设置超时,避免长时间等待
128
+ if response.status_code == 200:
129
+ try:
130
+ data = response.json()
131
+ if data:
132
+ # print("接口返回了数据:", data)
133
+ print("接口返回了数据:")
134
+ return True
135
+ else:
136
+ print("接口返回了空数据")
137
+ return False
138
+ except ValueError:
139
+ print("返回的不是有效的 JSON")
140
+ return False
141
+ else:
142
+ print(f"接口返回了错误状态码: {response.status_code}")
143
+ return False
144
+ except requests.RequestException as e:
145
+ print(f"请求接口时发生错误: {e}")
146
+ return False
147
+
148
+ def is_chrome_running():
149
+ try:
150
+ output = subprocess.check_output('tasklist', shell=True, text=True)
151
+ return 'chrome.exe' in output.lower()
152
+ except subprocess.CalledProcessError:
153
+ return False
154
+
155
+ @contextmanager
156
+ def get_chrome_page_v3(p, port=3000, user_data=None):
157
+ browser = context = page = None
158
+ is_custom_chrome_opened = False # 标记是否是程序自己开的浏览器
159
+
160
+ try:
161
+ if not check_chrome_dev(port):
162
+ set_chrome_system_path()
163
+ chrome_path = r'"chrome.exe"'
164
+ debugging_port = fr"--remote-debugging-port={port}"
165
+ if user_data is not None:
166
+ chrome_user_data = fr'--user-data-dir="{user_data}"'
167
+ else:
168
+ chrome_user_data = fr'--user-data-dir="{create_file_path(default_chrome_user_data)}\{port}"'
169
+
170
+ disable_webrtc = "--disable-features=WebRTC"
171
+ disable_webrtc_hw_encoder = "--disable-features=WebRTC-HW-ENCODER"
172
+ disable_webrtc_alt = "--disable-webrtc"
173
+ start_maximized = "--start-maximized"
174
+
175
+ command = f"{chrome_path} {debugging_port} {chrome_user_data} {disable_webrtc} {disable_webrtc_hw_encoder} {disable_webrtc_alt}"
176
+ subprocess.Popen(command, shell=True)
177
+ is_custom_chrome_opened = True
178
+ time.sleep(1)
179
+
180
+ browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
181
+ context = browser.contexts[0] if browser.contexts else browser.new_context()
182
+ page = context.pages[0] if context.pages else context.new_page()
183
+
184
+ yield browser, context, page
185
+
186
+ except Exception as e:
187
+ # 向上抛出错误,否则主函数感知不到错误
188
+ raise
189
+
190
+ finally:
191
+ for obj in [("page", page), ("context", context), ("browser", browser)]:
192
+ name, target = obj
193
+ try:
194
+ if target and is_custom_chrome_opened:
195
+ log(f'关闭: {name}')
196
+ target.close()
197
+ except Exception:
198
+ pass # 你可以在这里加日志记录关闭失败
qrpa/shein_ziniao.py CHANGED
@@ -24,6 +24,7 @@ from .fun_win import find_software_install_path
24
24
  from .fun_base import log, hostname, send_exception
25
25
  from .fun_file import check_progress_json_ex, get_progress_json_ex, done_progress_json_ex, write_dict_to_file_ex
26
26
 
27
+
27
28
  class ZiniaoClient:
28
29
  """紫鸟客户端管理类"""
29
30
 
@@ -52,7 +53,7 @@ class ZiniaoClient:
52
53
  def _get_user_info(self) -> Dict[str, str]:
53
54
  """获取用户登录信息"""
54
55
  return {
55
- "company" : self.config.ziniao.company,
56
+ "company": self.config.ziniao.company,
56
57
  "username": self.config.ziniao.username,
57
58
  "password": self.config.ziniao.password
58
59
  }
@@ -89,7 +90,7 @@ class ZiniaoClient:
89
90
  def update_core(self):
90
91
  """下载所有内核,打开店铺前调用,需客户端版本5.285.7以上"""
91
92
  data = {
92
- "action" : "updateCore",
93
+ "action": "updateCore",
93
94
  "requestId": str(uuid.uuid4()),
94
95
  }
95
96
  data.update(self.user_info)
@@ -145,6 +146,7 @@ class ZiniaoClient:
145
146
  print('@@ get_exit...' + json.dumps(data))
146
147
  self.send_http(data)
147
148
 
149
+
148
150
  class ZiniaoBrowser:
149
151
  """紫鸟浏览器操作类"""
150
152
 
@@ -153,22 +155,22 @@ class ZiniaoBrowser:
153
155
  self.config = config
154
156
 
155
157
  def open_store(self, store_info: str, isWebDriverReadOnlyMode: int = 0,
156
- isprivacy: int = 0, isHeadless: int = 0,
157
- cookieTypeSave: int = 0, jsInfo: str = "") -> Dict[str, Any]:
158
+ isprivacy: int = 0, isHeadless: int = 0,
159
+ cookieTypeSave: int = 0, jsInfo: str = "") -> Dict[str, Any]:
158
160
  """打开店铺"""
159
161
  request_id = str(uuid.uuid4())
160
162
  data = {
161
- "action" : "startBrowser",
162
- "isWaitPluginUpdate" : 0,
163
- "isHeadless" : isHeadless,
164
- "requestId" : request_id,
163
+ "action": "startBrowser",
164
+ "isWaitPluginUpdate": 0,
165
+ "isHeadless": isHeadless,
166
+ "requestId": request_id,
165
167
  "isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
166
- "cookieTypeLoad" : 0,
167
- "cookieTypeSave" : cookieTypeSave,
168
- "runMode" : "1",
169
- "isLoadUserPlugin" : False,
170
- "pluginIdType" : 1,
171
- "privacyMode" : isprivacy
168
+ "cookieTypeLoad": 0,
169
+ "cookieTypeSave": cookieTypeSave,
170
+ "runMode": "1",
171
+ "isLoadUserPlugin": False,
172
+ "pluginIdType": 1,
173
+ "privacyMode": isprivacy
172
174
  }
173
175
  data.update(self.client.user_info)
174
176
 
@@ -194,9 +196,9 @@ class ZiniaoBrowser:
194
196
  """关闭店铺"""
195
197
  request_id = str(uuid.uuid4())
196
198
  data = {
197
- "action" : "stopBrowser",
198
- "requestId" : request_id,
199
- "duplicate" : 0,
199
+ "action": "stopBrowser",
200
+ "requestId": request_id,
201
+ "duplicate": 0,
200
202
  "browserOauth": browser_oauth
201
203
  }
202
204
  data.update(self.client.user_info)
@@ -215,7 +217,7 @@ class ZiniaoBrowser:
215
217
  """获取浏览器列表"""
216
218
  request_id = str(uuid.uuid4())
217
219
  data = {
218
- "action" : "getBrowserList",
220
+ "action": "getBrowserList",
219
221
  "requestId": request_id
220
222
  }
221
223
  data.update(self.client.user_info)
@@ -265,6 +267,7 @@ class ZiniaoBrowser:
265
267
  # 标记完成
266
268
  done_progress_json_ex(self.config, task_key, store_name)
267
269
 
270
+
268
271
  class ZiniaoTaskManager:
269
272
  """紫鸟任务管理类"""
270
273
 
@@ -273,10 +276,10 @@ class ZiniaoTaskManager:
273
276
  self.config = config
274
277
 
275
278
  def run_single_store_task(self, browser_info: Dict[str, Any],
276
- run_func: Callable, task_key: str,
277
- just_store_username: Optional[List[str]] = None,
278
- is_skip_store: Optional[Callable] = None
279
- ):
279
+ run_func: Callable, task_key: str,
280
+ just_store_username: Optional[List[str]] = None,
281
+ is_skip_store: Optional[Callable] = None
282
+ ):
280
283
  """运行单个店铺的任务"""
281
284
  retry_count = 0
282
285
  while True:
@@ -288,7 +291,8 @@ class ZiniaoTaskManager:
288
291
  store_username = browser_info.get("store_username")
289
292
 
290
293
  # 记录店铺账号与店铺别名对应关系
291
- write_dict_to_file_ex(self.config.shein_store_alias, {store_username: store_name}, [store_username])
294
+ cache_file = f'{self.config.auto_dir}/shein_store_alias.json'
295
+ write_dict_to_file_ex(cache_file, {store_username: store_name}, [store_username])
292
296
 
293
297
  if is_skip_store and is_skip_store(store_username, store_name):
294
298
  return
@@ -341,25 +345,25 @@ class ZiniaoTaskManager:
341
345
  self.browser.close_store(store_id)
342
346
  break
343
347
  except:
344
- send_exception(self.config, f'第{retry_count}次运行失败,准备重新打开店铺')
348
+ send_exception(f'第{retry_count}次运行失败,准备重新打开店铺')
345
349
  if retry_count > 5:
346
350
  break
347
351
 
348
352
  def run_all_stores_task(self, browser_list: List[Dict[str, Any]],
349
- run_func: Callable, task_key: str,
350
- just_store_username: Optional[List[str]] = None,
351
- is_skip_store: Optional[Callable] = None
352
- ):
353
+ run_func: Callable, task_key: str,
354
+ just_store_username: Optional[List[str]] = None,
355
+ is_skip_store: Optional[Callable] = None
356
+ ):
353
357
  """循环运行所有店铺的任务"""
354
358
  for browser_info in browser_list:
355
359
  self.run_single_store_task(browser_info, run_func, task_key, just_store_username, is_skip_store)
356
360
 
357
361
  def run_with_thread_pool(self, browser_list: List[Dict[str, Any]],
358
- max_threads: int = 3, run_func: Callable = None,
359
- task_key: str = None,
360
- just_store_username: Optional[List[str]] = None,
361
- is_skip_store: Optional[Callable] = None
362
- ):
362
+ max_threads: int = 3, run_func: Callable = None,
363
+ task_key: str = None,
364
+ just_store_username: Optional[List[str]] = None,
365
+ is_skip_store: Optional[Callable] = None
366
+ ):
363
367
  """使用线程池控制最大并发线程数运行任务"""
364
368
  with ThreadPoolExecutor(max_workers=max_threads) as executor:
365
369
  task = partial(self.run_single_store_task,
@@ -369,23 +373,27 @@ class ZiniaoTaskManager:
369
373
  log(f'店铺总数: {len(browser_list)}')
370
374
  executor.map(task, browser_list)
371
375
 
376
+
372
377
  class ZiniaoRunner:
373
378
  """紫鸟主运行器类"""
374
379
 
375
380
  def __init__(self, config):
376
381
  self.config = config
382
+ os.environ['auto_dir'] = self.config.auto_dir
383
+ os.environ['wxwork_bot_exception'] = self.config.wxwork_bot_exception
384
+
377
385
  self.client = ZiniaoClient(config)
378
386
  self.browser = ZiniaoBrowser(self.client, config)
379
387
  self.task_manager = ZiniaoTaskManager(self.browser, config)
380
388
 
381
389
  def execute(self, run_prepare: Optional[Callable] = None,
382
- run: Optional[Callable] = None,
383
- run_summary: Optional[Callable] = None,
384
- run_notify: Optional[Callable] = None,
385
- task_key: Optional[str] = None,
386
- just_store_username: Optional[List[str]] = None,
387
- is_skip_store: Optional[Callable] = None,
388
- ):
390
+ run: Optional[Callable] = None,
391
+ run_summary: Optional[Callable] = None,
392
+ run_notify: Optional[Callable] = None,
393
+ task_key: Optional[str] = None,
394
+ just_store_username: Optional[List[str]] = None,
395
+ is_skip_store: Optional[Callable] = None,
396
+ ):
389
397
  """主执行入口"""
390
398
  # 前置执行 if run_prepare:
391
399
  run_prepare()
@@ -414,10 +422,10 @@ class ZiniaoRunner:
414
422
  try_times = 0
415
423
  while not check_progress_json_ex(self.config, task_key, just_store_username):
416
424
  try_times += 1
417
- send_exception(self.config, f'检测到任务未全部完成,再次执行: {try_times}')
425
+ send_exception(f'检测到任务未全部完成,再次执行: {try_times}')
418
426
  self.task_manager.run_with_thread_pool(browser_list, max_threads, run, task_key, just_store_username, is_skip_store)
419
427
  if try_times >= 4:
420
- send_exception(self.config, f'检测到任务未全部完成,再次执行: {try_times}')
428
+ send_exception(f'检测到任务未全部完成,再次执行: {try_times}')
421
429
  break
422
430
 
423
431
  # 数据汇总
@@ -437,5 +445,6 @@ class ZiniaoRunner:
437
445
  # 关闭客户端
438
446
  self.client.exit()
439
447
 
448
+
440
449
  if __name__ == "__main__":
441
450
  pass