qrpa 1.0.2__py3-none-any.whl → 1.0.8__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_file.py ADDED
@@ -0,0 +1,173 @@
1
+ import os
2
+ import threading
3
+ import json
4
+
5
+ file_lock = threading.Lock() # 线程锁
6
+
7
+ from datetime import date, datetime, timedelta, timezone
8
+
9
+ from .fun_base import log
10
+
11
+ def read_dict_from_file(file_path, cache_interval=3600 * 24 * 365 * 10):
12
+ """
13
+ 从文件中读取字典。
14
+ 如果文件的修改时间未超过一个小时,则返回字典;否则返回 None。
15
+
16
+ :param file_path: 文件路径
17
+ :return: 字典或 None
18
+ """
19
+ with file_lock: # 使用锁保护文件操作
20
+ # 检查文件是否存在
21
+ if not os.path.exists(file_path):
22
+ return {}
23
+
24
+ # 获取文件的最后修改时间
25
+ modification_time = os.path.getmtime(file_path)
26
+ modification_time = datetime.fromtimestamp(modification_time)
27
+
28
+ # 获取当前时间
29
+ current_time = datetime.now()
30
+
31
+ interval = current_time - modification_time
32
+ log(f'缓存文件 {file_path} 缓存时长 {timedelta(seconds=int(cache_interval))} 已过时长 {interval}')
33
+
34
+ # 判断文件的修改时间是否超过一个小时
35
+ if interval <= timedelta(seconds=int(cache_interval)):
36
+ # 如果未超过一个小时,则读取文件内容
37
+ with open(file_path, "r", encoding='utf-8') as file:
38
+ return json.load(file)
39
+ else:
40
+ # 如果超过一个小时,则返回 None
41
+ return {}
42
+
43
+ def write_dict_to_file(file_path, data):
44
+ """
45
+ 将字典写入文件。
46
+
47
+ :param file_path: 文件路径
48
+ :param data: 要写入的字典
49
+ """
50
+ with file_lock: # 使用锁保护文件操作
51
+ # 确保目标文件夹存在
52
+ dir_name = os.path.dirname(file_path)
53
+ if dir_name and not os.path.exists(dir_name):
54
+ os.makedirs(dir_name, exist_ok=True) # 递归创建目录
55
+
56
+ with open(file_path, 'w', encoding='utf-8') as f:
57
+ # 使用 json.dump() 并设置 ensure_ascii=False
58
+ json.dump(data, f, ensure_ascii=False, indent=4)
59
+
60
+ def read_dict_from_file_ex(file_path, key, cache_interval=3600 * 24 * 365 * 10, default='dict'):
61
+ """
62
+ 从 JSON 文件中读取指定键的值。
63
+
64
+ :param file_path: JSON 文件路径
65
+ :param key: 要读取的键
66
+ :param default: 如果文件不存在、解析失败或键不存在时返回的默认值
67
+ :return: 对应键的值,或 default
68
+ """
69
+ with file_lock: # 使用锁保护文件操作
70
+ if not os.path.exists(file_path):
71
+ return {} if default == 'dict' else []
72
+
73
+ # 获取文件的最后修改时间
74
+ modification_time = os.path.getmtime(file_path)
75
+ modification_time = datetime.fromtimestamp(modification_time)
76
+
77
+ # 获取当前时间
78
+ current_time = datetime.now()
79
+
80
+ interval = current_time - modification_time
81
+ log(f'缓存文件 {file_path} 缓存时长 {timedelta(seconds=cache_interval)} 已过时长 {interval}')
82
+
83
+ # 判断文件的修改时间是否超过一个小时
84
+ if interval <= timedelta(seconds=cache_interval):
85
+ # 如果未超过一个小时,则读取文件内容
86
+ with open(file_path, 'r', encoding='utf-8') as f:
87
+ data = json.load(f)
88
+ return data.get(key, {})
89
+ else:
90
+ # 如果超过一个小时,则返回 None
91
+ return {} if default == 'dict' else []
92
+
93
+ def write_dict_to_file_ex(file_path, data, update_keys=None):
94
+ """
95
+ 将字典写入文件,可选择性地只更新指定键。
96
+
97
+ :param file_path: 文件路径
98
+ :param data: 要写入的字典数据
99
+ :param update_keys: 可选,需要更新的键列表。如果为None,则替换整个文件内容
100
+ """
101
+ with file_lock: # 使用锁保护文件操作
102
+ # 确保目标文件夹存在
103
+ dir_name = os.path.dirname(file_path)
104
+ if dir_name and not os.path.exists(dir_name):
105
+ os.makedirs(dir_name, exist_ok=True) # 递归创建目录
106
+
107
+ # 如果指定了update_keys,先读取现有数据然后合并
108
+ if update_keys is not None:
109
+ try:
110
+ with open(file_path, 'r', encoding='utf-8') as f:
111
+ existing_data = json.load(f)
112
+ except (FileNotFoundError, json.JSONDecodeError):
113
+ existing_data = {}
114
+
115
+ # 只更新指定的键
116
+ for key in update_keys:
117
+ if key in data:
118
+ existing_data[key] = data[key]
119
+ data = existing_data
120
+
121
+ with open(file_path, 'w', encoding='utf-8') as f:
122
+ json.dump(data, f, ensure_ascii=False, indent=4)
123
+
124
+ ######################################################################################################
125
+ def getTaskStoreKey(key_id, store_name):
126
+ return f'{key_id}_{store_name}'
127
+
128
+ def generate_progress_file(config, key_id):
129
+ return f'{config.auto_dir}/progress/progress_{key_id}.json'
130
+
131
+ def get_progress_index_ex(config, task_key, store_name):
132
+ task_store_key = getTaskStoreKey(task_key, store_name)
133
+ progress_file = generate_progress_file(config, task_key)
134
+ dict = read_dict_from_file(progress_file)
135
+ if len(dict) > 0:
136
+ count = 0
137
+ for key, value in dict.items():
138
+ if key == task_store_key:
139
+ return count
140
+ count += 1
141
+ return len(dict)
142
+
143
+ def get_progress_json_ex(config, task_key, store_name):
144
+ task_store_key = getTaskStoreKey(task_key, store_name)
145
+ progress_file = generate_progress_file(config, task_key)
146
+ dict = read_dict_from_file_ex(progress_file, task_store_key)
147
+ if len(dict) > 0:
148
+ return dict[0] == 1
149
+ else:
150
+ length = get_progress_index_ex(config, task_key, store_name)
151
+ write_dict_to_file_ex(progress_file, {task_store_key: [0, length + 1, datetime.now().strftime('%Y-%m-%d %H:%M:%S')]}, [task_store_key])
152
+ return False
153
+
154
+ def done_progress_json_ex(config, task_key, store_name):
155
+ task_store_key = getTaskStoreKey(task_key, store_name)
156
+ progress_file = generate_progress_file(config, task_key)
157
+ length = get_progress_index_ex(config, task_key, store_name)
158
+ write_dict_to_file_ex(progress_file, {task_store_key: [1, length + 1, datetime.now().strftime('%Y-%m-%d %H:%M:%S')]}, [task_store_key])
159
+
160
+ def check_progress_json_ex(config, task_key, just_store_username=None):
161
+ progress_file = generate_progress_file(config, task_key)
162
+ dict = read_dict_from_file(progress_file)
163
+ if len(dict) > 0:
164
+ for task_store_key, data_list in dict.items():
165
+ if just_store_username and len(just_store_username) > 0:
166
+ if all([store_username not in task_store_key for store_username in just_store_username]):
167
+ continue
168
+ if 'run_' not in task_store_key and int(data_list[0]) == 0:
169
+ log(task_store_key, just_store_username)
170
+ return False
171
+ else:
172
+ log(f"进度文件不存在或为空: {progress_file}")
173
+ return True
qrpa/fun_win.py ADDED
@@ -0,0 +1,31 @@
1
+ import os
2
+ import win32com.client
3
+
4
+ from .fun_base import log
5
+
6
+ def find_software_install_path(app_keyword: str):
7
+ """从开始菜单或桌面查找指定软件的安装路径"""
8
+ possible_dirs = [
9
+ os.environ.get('PROGRAMDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
10
+ os.environ.get('APPDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
11
+ os.environ.get('USERPROFILE', '') + r'\Desktop',
12
+ os.environ.get('PUBLIC', '') + r'\Desktop'
13
+ ]
14
+
15
+ shell = win32com.client.Dispatch("WScript.Shell")
16
+
17
+ for base_dir in possible_dirs:
18
+ for root, _, files in os.walk(base_dir):
19
+ for file in files:
20
+ if file.lower().endswith('.lnk') and app_keyword.lower() in file.lower():
21
+ lnk_path = os.path.join(root, file)
22
+ try:
23
+ shortcut = shell.CreateShortcut(lnk_path)
24
+ target_path = shortcut.Targetpath
25
+ if os.path.exists(target_path):
26
+ return target_path
27
+ except Exception as e:
28
+ continue
29
+
30
+ log(f'未能查找到{str}安装位置')
31
+ return None
qrpa/shein_ziniao.py ADDED
@@ -0,0 +1,441 @@
1
+ """
2
+ # 适用环境python3
3
+ # 紫鸟浏览器自动化操作 - 面向对象重构版本
4
+ """
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import time
9
+ import traceback
10
+ import uuid
11
+ import json
12
+ from concurrent.futures import ThreadPoolExecutor
13
+ from typing import Literal, Optional, Callable, List, Dict, Any
14
+
15
+ import requests
16
+ import subprocess
17
+ from playwright import sync_api
18
+ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
19
+
20
+ # 使用 partial 或 lambda 固定部分参数
21
+ from functools import partial
22
+
23
+ from .fun_win import find_software_install_path
24
+ from .fun_base import log, hostname, send_exception
25
+ from .fun_file import check_progress_json_ex, get_progress_json_ex, done_progress_json_ex, write_dict_to_file_ex
26
+
27
+ class ZiniaoClient:
28
+ """紫鸟客户端管理类"""
29
+
30
+ def __init__(self, config):
31
+ self.config = config
32
+ self.version = "v5"
33
+ self.socket_port = 16851
34
+ self.is_windows = platform.system() == 'Windows'
35
+ self.is_mac = platform.system() == 'Darwin'
36
+ self.client_path = self._get_client_path()
37
+ self.user_info = self._get_user_info()
38
+
39
+ if not self.is_windows and not self.is_mac:
40
+ raise RuntimeError("webdriver/cdp只支持windows和mac操作系统")
41
+
42
+ def _get_client_path(self) -> str:
43
+ """获取客户端路径"""
44
+ if self.is_windows:
45
+ ziniao = find_software_install_path('SuperBrowser')
46
+ if ziniao is None:
47
+ raise RuntimeError('未找到SuperBrowser安装路径')
48
+ return ziniao
49
+ else:
50
+ return 'ziniao'
51
+
52
+ def _get_user_info(self) -> Dict[str, str]:
53
+ """获取用户登录信息"""
54
+ return {
55
+ "company" : self.config.ziniao.company,
56
+ "username": self.config.ziniao.username,
57
+ "password": self.config.ziniao.password
58
+ }
59
+
60
+ def kill_process(self):
61
+ """杀紫鸟客户端进程"""
62
+ if self.version == "v5":
63
+ process_name = 'SuperBrowser.exe'
64
+ else:
65
+ process_name = 'ziniao.exe'
66
+
67
+ if self.is_windows:
68
+ os.system('taskkill /f /t /im ' + process_name)
69
+ elif self.is_mac:
70
+ os.system('killall ziniao')
71
+ time.sleep(3)
72
+
73
+ def start_browser(self):
74
+ """启动客户端"""
75
+ try:
76
+ if self.is_windows:
77
+ cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http', '--port=' + str(self.socket_port)]
78
+ elif self.is_mac:
79
+ cmd = ['open', '-a', self.client_path, '--args', '--run_type=web_driver', '--ipc_type=http',
80
+ '--port=' + str(self.socket_port)]
81
+ else:
82
+ raise RuntimeError("不支持的操作系统")
83
+
84
+ subprocess.Popen(cmd)
85
+ time.sleep(5)
86
+ except Exception:
87
+ raise RuntimeError('start browser process failed')
88
+
89
+ def update_core(self):
90
+ """下载所有内核,打开店铺前调用,需客户端版本5.285.7以上"""
91
+ data = {
92
+ "action" : "updateCore",
93
+ "requestId": str(uuid.uuid4()),
94
+ }
95
+ data.update(self.user_info)
96
+
97
+ while True:
98
+ result = self.send_http(data)
99
+ print(result)
100
+ if result is None:
101
+ print("等待客户端启动...")
102
+ time.sleep(2)
103
+ continue
104
+ if result.get("statusCode") is None or result.get("statusCode") == -10003:
105
+ print("当前版本不支持此接口,请升级客户端")
106
+ return
107
+ elif result.get("statusCode") == 0:
108
+ print("更新内核完成")
109
+ return
110
+ else:
111
+ print(f"等待更新内核: {json.dumps(result)}")
112
+ time.sleep(2)
113
+
114
+ def send_http(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
115
+ """HTTP通讯方式"""
116
+ try:
117
+ url = f'http://127.0.0.1:{self.socket_port}'
118
+ response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
119
+ return json.loads(response.text)
120
+ except Exception as err:
121
+ print(err)
122
+ return None
123
+
124
+ def delete_all_cache(self):
125
+ """删除所有店铺缓存"""
126
+ if not self.is_windows:
127
+ return
128
+ local_appdata = os.getenv('LOCALAPPDATA')
129
+ cache_path = os.path.join(local_appdata, 'SuperBrowser')
130
+ if os.path.exists(cache_path):
131
+ shutil.rmtree(cache_path)
132
+
133
+ def delete_all_cache_with_path(self, path: str):
134
+ """删除指定路径的店铺缓存"""
135
+ if not self.is_windows:
136
+ return
137
+ cache_path = os.path.join(path, 'SuperBrowser')
138
+ if os.path.exists(cache_path):
139
+ shutil.rmtree(cache_path)
140
+
141
+ def exit(self):
142
+ """关闭客户端"""
143
+ data = {"action": "exit", "requestId": str(uuid.uuid4())}
144
+ data.update(self.user_info)
145
+ print('@@ get_exit...' + json.dumps(data))
146
+ self.send_http(data)
147
+
148
+ class ZiniaoBrowser:
149
+ """紫鸟浏览器操作类"""
150
+
151
+ def __init__(self, client: ZiniaoClient, config):
152
+ self.client = client
153
+ self.config = config
154
+
155
+ 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
+ """打开店铺"""
159
+ request_id = str(uuid.uuid4())
160
+ data = {
161
+ "action" : "startBrowser",
162
+ "isWaitPluginUpdate" : 0,
163
+ "isHeadless" : isHeadless,
164
+ "requestId" : request_id,
165
+ "isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
166
+ "cookieTypeLoad" : 0,
167
+ "cookieTypeSave" : cookieTypeSave,
168
+ "runMode" : "1",
169
+ "isLoadUserPlugin" : False,
170
+ "pluginIdType" : 1,
171
+ "privacyMode" : isprivacy
172
+ }
173
+ data.update(self.client.user_info)
174
+
175
+ if store_info.isdigit():
176
+ data["browserId"] = store_info
177
+ else:
178
+ data["browserOauth"] = store_info
179
+
180
+ if len(str(jsInfo)) > 2:
181
+ data["injectJsInfo"] = json.dumps(jsInfo)
182
+
183
+ r = self.client.send_http(data)
184
+ if str(r.get("statusCode")) == "0":
185
+ return r
186
+ elif str(r.get("statusCode")) == "-10003":
187
+ print(f"login Err {json.dumps(r, ensure_ascii=False)}")
188
+ raise RuntimeError("登录错误")
189
+ else:
190
+ print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
191
+ raise RuntimeError("打开店铺失败")
192
+
193
+ def close_store(self, browser_oauth: str):
194
+ """关闭店铺"""
195
+ request_id = str(uuid.uuid4())
196
+ data = {
197
+ "action" : "stopBrowser",
198
+ "requestId" : request_id,
199
+ "duplicate" : 0,
200
+ "browserOauth": browser_oauth
201
+ }
202
+ data.update(self.client.user_info)
203
+
204
+ r = self.client.send_http(data)
205
+ if str(r.get("statusCode")) == "0":
206
+ return r
207
+ elif str(r.get("statusCode")) == "-10003":
208
+ print(f"login Err {json.dumps(r, ensure_ascii=False)}")
209
+ raise RuntimeError("登录错误")
210
+ else:
211
+ print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
212
+ raise RuntimeError("关闭店铺失败")
213
+
214
+ def get_browser_list(self) -> List[Dict[str, Any]]:
215
+ """获取浏览器列表"""
216
+ request_id = str(uuid.uuid4())
217
+ data = {
218
+ "action" : "getBrowserList",
219
+ "requestId": request_id
220
+ }
221
+ data.update(self.client.user_info)
222
+
223
+ r = self.client.send_http(data)
224
+ if str(r.get("statusCode")) == "0":
225
+ print(r)
226
+ return r.get("browserList", [])
227
+ elif str(r.get("statusCode")) == "-10003":
228
+ print(f"login Err {json.dumps(r, ensure_ascii=False)}")
229
+ raise RuntimeError("登录错误")
230
+ else:
231
+ print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
232
+ raise RuntimeError("获取浏览器列表失败")
233
+
234
+ def get_browser_context(self, playwright, port: int):
235
+ """获取playwright浏览器会话"""
236
+ browser = playwright.chromium.connect_over_cdp(f"http://127.0.0.1:{port}")
237
+ context = browser.contexts[0]
238
+ return context
239
+
240
+ def open_ip_check(self, browser_context, ip_check_url: str) -> bool:
241
+ """打开ip检测页检测ip是否正常"""
242
+ try:
243
+ page = browser_context.pages[0]
244
+ page.goto(ip_check_url)
245
+ success_button = page.locator('//button[contains(@class, "styles_btn--success")]')
246
+ success_button.wait_for(timeout=60000) # 等待查找元素60秒
247
+ print("ip检测成功")
248
+ return True
249
+ except PlaywrightTimeoutError:
250
+ print("ip检测超时")
251
+ return False
252
+ except Exception as e:
253
+ print("ip检测异常:" + traceback.format_exc())
254
+ return False
255
+
256
+ def open_launcher_page(self, browser_context, launcher_page: str, store_username: str, store_name: str, run_func: Callable, task_key: str):
257
+ """打开启动页面并执行业务逻辑"""
258
+ page = browser_context.pages[0]
259
+ page.goto(launcher_page)
260
+ page.wait_for_load_state('load')
261
+
262
+ # 业务逻辑
263
+ run_func(page, store_username, store_name, task_key)
264
+
265
+ # 标记完成
266
+ done_progress_json_ex(self.config, task_key, store_name)
267
+
268
+ class ZiniaoTaskManager:
269
+ """紫鸟任务管理类"""
270
+
271
+ def __init__(self, browser: ZiniaoBrowser, config):
272
+ self.browser = browser
273
+ self.config = config
274
+
275
+ 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
+ ):
280
+ """运行单个店铺的任务"""
281
+ retry_count = 0
282
+ while True:
283
+ try:
284
+ retry_count += 1
285
+
286
+ store_id = browser_info.get('browserOauth')
287
+ store_name = browser_info.get("browserName")
288
+ store_username = browser_info.get("store_username")
289
+
290
+ # 记录店铺账号与店铺别名对应关系
291
+ write_dict_to_file_ex(self.config.shein_store_alias, {store_username: store_name}, [store_username])
292
+
293
+ if is_skip_store and is_skip_store(store_username, store_name):
294
+ return
295
+
296
+ if just_store_username is not None:
297
+ if store_username not in just_store_username:
298
+ log(f'=================================跳过 just_store_username: {store_name},{store_username}, {just_store_username}======================================')
299
+ return
300
+ else:
301
+ log(f'---------------------------------命中 just_store_username: {store_name},{store_username}, {just_store_username}-------------------------------------')
302
+
303
+ if get_progress_json_ex(self.config, task_key, store_name):
304
+ log(f'=================================跳过 进度已完成: {task_key},{store_name},{store_username}=================================')
305
+ return
306
+
307
+ # 打开店铺
308
+ print(f"=====打开店铺:{store_name}=====")
309
+ ret_json = self.browser.open_store(store_id)
310
+ print(ret_json)
311
+ store_id = ret_json.get("browserOauth") or ret_json.get("browserId")
312
+
313
+ # 获取playwright浏览器会话
314
+ with sync_api.sync_playwright() as playwright:
315
+ try:
316
+ browser_context = self.browser.get_browser_context(playwright, ret_json.get('debuggingPort'))
317
+ if browser_context is None:
318
+ print(f"=====关闭店铺:{store_name}=====")
319
+ self.browser.close_store(store_id)
320
+ return
321
+
322
+ # 获取ip检测页地址
323
+ ip_check_url = ret_json.get("ipDetectionPage")
324
+ if not ip_check_url:
325
+ print("ip检测页地址为空,请升级紫鸟浏览器到最新版")
326
+ print(f"=====关闭店铺:{store_name}=====")
327
+ self.browser.close_store(store_id)
328
+ raise RuntimeError("ip检测页地址为空")
329
+
330
+ ip_usable = self.browser.open_ip_check(browser_context, ip_check_url)
331
+ if ip_usable:
332
+ print("ip检测通过,打开店铺平台主页")
333
+ self.browser.open_launcher_page(browser_context, ret_json.get("launcherPage"), store_username, store_name, run_func, task_key)
334
+ else:
335
+ print("ip检测不通过,请检查")
336
+ except:
337
+ print("脚本运行异常:" + traceback.format_exc())
338
+ raise
339
+ finally:
340
+ print(f"=====关闭店铺:{store_name}=====")
341
+ self.browser.close_store(store_id)
342
+ break
343
+ except:
344
+ send_exception(self.config, f'第{retry_count}次运行失败,准备重新打开店铺')
345
+ if retry_count > 5:
346
+ break
347
+
348
+ 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
+ """循环运行所有店铺的任务"""
354
+ for browser_info in browser_list:
355
+ self.run_single_store_task(browser_info, run_func, task_key, just_store_username, is_skip_store)
356
+
357
+ 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
+ ):
363
+ """使用线程池控制最大并发线程数运行任务"""
364
+ with ThreadPoolExecutor(max_workers=max_threads) as executor:
365
+ task = partial(self.run_single_store_task,
366
+ run_func=run_func, task_key=task_key,
367
+ just_store_username=just_store_username,
368
+ is_skip_store=is_skip_store)
369
+ log(f'店铺总数: {len(browser_list)}')
370
+ executor.map(task, browser_list)
371
+
372
+ class ZiniaoRunner:
373
+ """紫鸟主运行器类"""
374
+
375
+ def __init__(self, config):
376
+ self.config = config
377
+ self.client = ZiniaoClient(config)
378
+ self.browser = ZiniaoBrowser(self.client, config)
379
+ self.task_manager = ZiniaoTaskManager(self.browser, config)
380
+
381
+ 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
+ ):
389
+ """主执行入口"""
390
+ # 前置执行 if run_prepare:
391
+ run_prepare()
392
+
393
+ # 终止紫鸟客户端已启动的进程
394
+ self.client.kill_process()
395
+
396
+ print("=====启动客户端=====")
397
+ self.client.start_browser()
398
+ print("=====更新内核=====")
399
+ self.client.update_core()
400
+
401
+ # 获取店铺列表
402
+ print("=====获取店铺列表=====")
403
+ browser_list = self.browser.get_browser_list()
404
+ if not browser_list:
405
+ print("browser list is empty")
406
+ raise RuntimeError("店铺列表为空")
407
+
408
+ # 多线程并发执行任务
409
+ max_threads = 5 if (hostname().lower() == 'krrpa') else 3
410
+ log(f'当前启用线程数: {max_threads}')
411
+ self.task_manager.run_with_thread_pool(browser_list, max_threads, run, task_key, just_store_username, is_skip_store)
412
+
413
+ # 任务重试逻辑
414
+ try_times = 0
415
+ while not check_progress_json_ex(self.config, task_key, just_store_username):
416
+ try_times += 1
417
+ send_exception(self.config, f'检测到任务未全部完成,再次执行: {try_times}')
418
+ self.task_manager.run_with_thread_pool(browser_list, max_threads, run, task_key, just_store_username, is_skip_store)
419
+ if try_times >= 4:
420
+ send_exception(self.config, f'检测到任务未全部完成,再次执行: {try_times}')
421
+ break
422
+
423
+ # 数据汇总
424
+ if not get_progress_json_ex(self.config, task_key, 'run_summary'):
425
+ if run_summary:
426
+ run_summary()
427
+ done_progress_json_ex(self.config, task_key, 'run_summary')
428
+ log('run_summary 完成')
429
+
430
+ # 发送通知
431
+ if not get_progress_json_ex(self.config, task_key, 'run_notify'):
432
+ if run_notify:
433
+ run_notify()
434
+ done_progress_json_ex(self.config, task_key, 'run_notify')
435
+ log('run_notify 完成')
436
+
437
+ # 关闭客户端
438
+ self.client.exit()
439
+
440
+ if __name__ == "__main__":
441
+ pass