qrpa 1.0.5__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/__init__.py +6 -0
- qrpa/fun_base.py +41 -0
- qrpa/fun_file.py +173 -0
- qrpa/fun_win.py +31 -0
- qrpa/shein_ziniao.py +441 -0
- qrpa/time_utils.py +845 -0
- qrpa/time_utils_example.py +243 -0
- {qrpa-1.0.5.dist-info → qrpa-1.0.8.dist-info}/METADATA +3 -1
- qrpa-1.0.8.dist-info/RECORD +13 -0
- qrpa-1.0.5.dist-info/RECORD +0 -7
- {qrpa-1.0.5.dist-info → qrpa-1.0.8.dist-info}/WHEEL +0 -0
- {qrpa-1.0.5.dist-info → qrpa-1.0.8.dist-info}/top_level.txt +0 -0
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
|