Compare commits
342 Commits
855f1affd9
...
origin-syn
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
080625567b | ||
|
|
266c0f17c1 | ||
|
|
225d13fb6e | ||
|
|
2ed1250604 | ||
|
|
ca4a2cd07a | ||
|
|
73ac9187a6 | ||
|
|
7503e3fa8b | ||
|
|
532438faba | ||
|
|
a1376b51b0 | ||
|
|
1e087c1aae | ||
|
|
2e2de02476 | ||
|
|
aeb4e1710c | ||
|
|
b4b80cb572 | ||
|
|
70e902e7ea | ||
|
|
14cebf22c3 | ||
|
|
73d25fcbcb | ||
|
|
01ecde45c5 | ||
|
|
a2ebfc0b81 | ||
|
|
1dc2cac19a | ||
|
|
14db1e0cb1 | ||
|
|
01f4dbb83a | ||
|
|
0d63d6d75a | ||
|
|
fe692fcd1d | ||
|
|
79b177b345 | ||
|
|
18b0c2d211 | ||
|
|
e4e01c1686 | ||
|
|
648e7d2f14 | ||
|
|
0d78d63437 | ||
|
|
289b1c67ce | ||
|
|
5c990e651e | ||
|
|
52095eb992 | ||
|
|
a4e4745921 | ||
|
|
a2d0bd3c1c | ||
|
|
b9275bc174 | ||
|
|
683a3934fc | ||
|
|
e5d0b2d9ab | ||
|
|
b0514c3e9a | ||
|
|
d618a5abf0 | ||
|
|
8be12ca9bc | ||
|
|
5ca46b0a57 | ||
|
|
afeeb8327c | ||
|
|
a7d8f8be6c | ||
|
|
4d2da94692 | ||
|
|
8cd1a4e2b1 | ||
|
|
b746812e23 | ||
|
|
3fbbf87c10 | ||
|
|
b0e97f21a4 | ||
|
|
a00ff1804c | ||
|
|
c3810fdb7b | ||
|
|
203937335d | ||
|
|
b16c63225f | ||
|
|
710b953120 | ||
|
|
45a45dc0fe | ||
|
|
3ca569ed1e | ||
|
|
c315bb350c | ||
|
|
111f30d150 | ||
|
|
6d398b66bc | ||
|
|
f46c231323 | ||
|
|
934919c699 | ||
|
|
12d84a22e4 | ||
|
|
9d4810eace | ||
|
|
dc457aff9e | ||
|
|
3ce66a59ee | ||
|
|
578847d596 | ||
|
|
147b324658 | ||
|
|
6416c14f20 | ||
|
|
e47c87f3a3 | ||
|
|
add6e6aeb5 | ||
|
|
1ecce13814 | ||
|
|
7968cef8c3 | ||
|
|
7c2b154c20 | ||
|
|
34148b5d8a | ||
|
|
7ba916f00c | ||
|
|
79e408333d | ||
|
|
af4f4a9ed4 | ||
|
|
74a7cd22b0 | ||
|
|
aece8123c3 | ||
|
|
e1cca219b8 | ||
|
|
70658041a5 | ||
|
|
4419c5bacd | ||
|
|
668226a99d | ||
|
|
4eec0dd5a4 | ||
|
|
b315d11c28 | ||
|
|
8e60f50616 | ||
|
|
72faf0433a | ||
|
|
f85e3be40e | ||
|
|
3616d64735 | ||
|
|
7f9116f12a | ||
|
|
01bae23df5 | ||
|
|
11f0d2c745 | ||
|
|
b073bbe97d | ||
|
|
955a6439a0 | ||
|
|
c6ecfb5b77 | ||
|
|
172917ac41 | ||
|
|
ad2bfcd2f5 | ||
|
|
a5f6898d06 | ||
|
|
83bd5839d0 | ||
|
|
83bc90ecf2 | ||
|
|
6ea3a960e7 | ||
|
|
cbfc29e953 | ||
|
|
fd2013b448 | ||
|
|
b66465aa6a | ||
|
|
a684fde472 | ||
|
|
fbd1a4dbea | ||
|
|
824fa7b76c | ||
|
|
be8a8492a9 | ||
|
|
b429721070 | ||
|
|
b352eea21d | ||
|
|
73d1dff21d | ||
|
|
51bf0aaa8c | ||
|
|
d7d1021ec3 | ||
|
|
28a752265c | ||
|
|
735234f7bc | ||
|
|
1d9d138423 | ||
|
|
8c5edc6ad6 | ||
|
|
e09ce6d13b | ||
|
|
ee6fb5d33a | ||
|
|
46e55fa623 | ||
|
|
cd2b437be5 | ||
|
|
a92d1af4f2 | ||
|
|
a504e7a13b | ||
|
|
1be28f10e9 | ||
|
|
325687d532 | ||
|
|
9c16c9c589 | ||
|
|
9760d1171c | ||
|
|
8711b205cf | ||
|
|
d42ed57119 | ||
|
|
e316adca1f | ||
|
|
18208f691c | ||
|
|
0457fbfac0 | ||
|
|
c06a4d0e06 | ||
|
|
58df800685 | ||
|
|
f21934e55b | ||
|
|
7c198f7d6f | ||
|
|
515135c120 | ||
|
|
ec4d1b2567 | ||
|
|
42bdd59c71 | ||
|
|
4ff4d8cfdc | ||
|
|
8be85fd332 | ||
|
|
e4bf104ae2 | ||
|
|
b96b136fa3 | ||
|
|
5105bf7049 | ||
|
|
02ddfc7a76 | ||
|
|
08997e9e20 | ||
|
|
8defef904f | ||
|
|
9146d17625 | ||
|
|
13c3a04a48 | ||
|
|
1aca3d6152 | ||
|
|
60a6494260 | ||
|
|
052bb31aea | ||
|
|
995e2ee65a | ||
|
|
225b525ba1 | ||
|
|
7f83bceba1 | ||
|
|
e1543416e3 | ||
|
|
1eb622d33f | ||
|
|
0b57aea5f6 | ||
|
|
5d8ee8fe79 | ||
|
|
a04c2f9a19 | ||
|
|
6f1b8e46f5 | ||
|
|
c352c34501 | ||
|
|
f9b2d2da25 | ||
|
|
5c9671df44 | ||
|
|
26a920a1cc | ||
|
|
8a8d3c5cd9 | ||
|
|
e695fd5184 | ||
|
|
d272afae1c | ||
|
|
0caf62c3d2 | ||
|
|
03e697f5d3 | ||
|
|
326f78705d | ||
|
|
c502afb588 | ||
|
|
afa8e44909 | ||
|
|
3fada5d198 | ||
|
|
72fdb1841c | ||
|
|
c0fdea6570 | ||
|
|
34bc980eea | ||
|
|
0391cb223f | ||
|
|
2dab555ca5 | ||
|
|
b25111e4b4 | ||
|
|
d537924aa9 | ||
|
|
2f2db4986e | ||
|
|
dc6ecc1edf | ||
|
|
6f970b3783 | ||
|
|
95d5c82474 | ||
|
|
87507708ce | ||
|
|
f6e83352d7 | ||
|
|
c6ae7ca170 | ||
|
|
46f46039ea | ||
|
|
6894f9cc57 | ||
|
|
8c4a6cd12a | ||
|
|
524d8763ce | ||
|
|
236b73755c | ||
|
|
1e845a1510 | ||
|
|
0341838d19 | ||
|
|
b04b05e4da | ||
|
|
d691e53886 | ||
|
|
ea35273597 | ||
|
|
72c8167472 | ||
|
|
dabb278170 | ||
|
|
9b1138c83e | ||
|
|
7b12cebb45 | ||
|
|
dc5e23892f | ||
|
|
9d75d673f8 | ||
|
|
4f8cbc3e38 | ||
|
|
8d97f5318c | ||
|
|
3ffbb2b004 | ||
|
|
34ed15a9bd | ||
|
|
110f407ae2 | ||
|
|
4b6295dd44 | ||
|
|
8db9f3a408 | ||
|
|
01116d5607 | ||
|
|
2ae720ae09 | ||
|
|
07904a29aa | ||
|
|
f87c37528b | ||
|
|
8d683a791d | ||
|
|
71b09a89de | ||
|
|
ef0e0df0ac | ||
|
|
d5764f3b50 | ||
|
|
ac07416352 | ||
|
|
4e5ed76cb6 | ||
|
|
025ca6d4fd | ||
|
|
ecc8b6ce6c | ||
| 951a353881 | |||
| 9ad6e037dc | |||
|
|
eb04caccf1 | ||
|
|
4a58b366b6 | ||
|
|
cd84def61d | ||
|
|
bbc833f2c9 | ||
|
|
d8098b0378 | ||
|
|
325c2f7c82 | ||
|
|
62d30ec190 | ||
|
|
728eea4b83 | ||
|
|
d0d3c6ee67 | ||
|
|
e53eaf738e | ||
|
|
21b6b5b270 | ||
|
|
ad2bb0a6ea | ||
|
|
365050b890 | ||
|
|
7589b4e418 | ||
|
|
73392a9b83 | ||
|
|
b400b494ad | ||
|
|
c47c03fde4 | ||
|
|
9a59dac6d0 | ||
|
|
86fd9475e4 | ||
|
|
03a85d5523 | ||
|
|
2d0d1d3461 | ||
|
|
33186911f3 | ||
|
|
ccaec4a984 | ||
|
|
144d03965e | ||
|
|
169ba7edb6 | ||
|
|
e1cd176ab2 | ||
|
|
0327d1cc51 | ||
|
|
2e30821e0f | ||
|
|
e02ddab599 | ||
|
|
7151f6b5e0 | ||
|
|
336338cff5 | ||
|
|
3149d80456 | ||
|
|
acff31e652 | ||
|
|
62315cee6c | ||
|
|
ad304a6880 | ||
|
|
2f67b376ee | ||
|
|
cb34788e53 | ||
|
|
489e7e8269 | ||
|
|
13b0ffb5d8 | ||
|
|
c9947c4ac8 | ||
|
|
1b02160b20 | ||
|
|
afffb7aad2 | ||
|
|
d8d5b56c13 | ||
|
|
9720453ac4 | ||
|
|
486e690204 | ||
|
|
1ff6d5220e | ||
|
|
380dd9f206 | ||
|
|
46d91fd8ca | ||
|
|
fba85ca365 | ||
|
|
a24db2f73c | ||
|
|
99236c9feb | ||
|
|
ca9537f862 | ||
|
|
0cad577d7c | ||
|
|
94ae7e68ff | ||
|
|
274d294b2e | ||
|
|
94e1930538 | ||
|
|
f63ae84853 | ||
|
|
5bbe5a3077 | ||
|
|
4b50542779 | ||
|
|
8cead01270 | ||
|
|
20bd41008c | ||
|
|
99fc14b9f9 | ||
|
|
8b490d3dd4 | ||
|
|
d3671206e1 | ||
|
|
99e94a5e57 | ||
|
|
0a8202788b | ||
|
|
949a5b42ce | ||
|
|
1fdef3c67f | ||
|
|
5a294e5c85 | ||
|
|
a0624221de | ||
|
|
63043c56d4 | ||
|
|
6ed033fff2 | ||
|
|
785d7563f9 | ||
|
|
b6dd1d7931 | ||
|
|
bf802c1019 | ||
|
|
d3dd4898a4 | ||
|
|
1c1acbbe7d | ||
|
|
eeeed6d7b1 | ||
|
|
9cbede6e09 | ||
|
|
b8f88ca06e | ||
|
|
23e0705a99 | ||
|
|
a07b9d7fb2 | ||
|
|
efbff69a91 | ||
|
|
e962990ca0 | ||
|
|
e4a5f5acc0 | ||
|
|
5c7f1187c0 | ||
|
|
0aad7b3faf | ||
|
|
28d410f38f | ||
|
|
fe8c6b94ae | ||
|
|
e09f6d4a80 | ||
|
|
085e8f2f84 | ||
|
|
2952f3bc95 | ||
|
|
5dd7a92cf5 | ||
|
|
ea212c8931 | ||
|
|
14269d9513 | ||
|
|
8106a8ab7e | ||
|
|
bbca159684 | ||
|
|
6434ec3e00 | ||
|
|
8cfaab49fb | ||
|
|
2cd0fcff93 | ||
|
|
7ffe5279a3 | ||
|
|
12318f8770 | ||
|
|
e8bec339a7 | ||
|
|
e9684a81aa | ||
|
|
903c591181 | ||
|
|
923dfd2495 | ||
|
|
0d83d56a05 | ||
|
|
ef16abfbb9 | ||
|
|
cfbd5d6e84 | ||
|
|
497d65d41d | ||
|
|
c2e45128ee | ||
|
|
c80e1889ef | ||
|
|
7f9a6bd9cb | ||
|
|
32f4705491 | ||
|
|
bb06bc8f5a | ||
|
|
d39d310544 | ||
|
|
7efb86ed3a | ||
|
|
6791869e0c | ||
|
|
8a64a6cd9b |
111
.gitignore
vendored
111
.gitignore
vendored
@@ -1,41 +1,112 @@
|
||||
# frontend-vue dependencies
|
||||
# ===== frontend-vue =====
|
||||
frontend-vue/node_modules/
|
||||
|
||||
# frontend-vue build output
|
||||
frontend-vue/dist/
|
||||
frontend-vue/new_web_source
|
||||
|
||||
# generated type declarations
|
||||
frontend-vue/auto-imports.d.ts
|
||||
frontend-vue/components.d.ts
|
||||
|
||||
# logs
|
||||
frontend-vue/npm-debug.log*
|
||||
frontend-vue/yarn-debug.log*
|
||||
frontend-vue/yarn-error.log*
|
||||
frontend-vue/pnpm-debug.log*
|
||||
|
||||
# backend-java build output
|
||||
# ===== backend-java =====
|
||||
backend-java/target/
|
||||
|
||||
# backend-java runtime data
|
||||
backend-java/data/
|
||||
backend-java/*.log
|
||||
backend-java/logs/
|
||||
|
||||
# backend-java local config
|
||||
backend-java/src/main/resources/application-local.yml
|
||||
|
||||
# backend-java IDE files
|
||||
backend-java/.idea/
|
||||
backend-java/*.iml
|
||||
|
||||
# desktop app
|
||||
# ===== backend / app =====
|
||||
__pycache__/
|
||||
backend/tmp/
|
||||
backend/__pycache__
|
||||
app/__pycache__
|
||||
app/new_web_source
|
||||
app/user_data/
|
||||
|
||||
# ===== logs =====
|
||||
logs/
|
||||
aiimage-backend.log*
|
||||
|
||||
# ===== Python =====
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.pyo
|
||||
.Python
|
||||
*.egg-info/
|
||||
*.egg
|
||||
MANIFEST
|
||||
.installed.cfg
|
||||
|
||||
# Build / packaging
|
||||
app.zip
|
||||
build/
|
||||
dist/
|
||||
develop-eggs/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.python-version
|
||||
|
||||
# Testing
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
.pyre/
|
||||
.pytype/
|
||||
|
||||
# Jupyter
|
||||
.ipynb_checkpoints
|
||||
*.ipynb_checkpoints/
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# ===== Database =====
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# ===== Misc =====
|
||||
desktop/
|
||||
ERP-Demo/
|
||||
xlsx/
|
||||
app/__pycache__
|
||||
backend/__pycache__
|
||||
app/assets/
|
||||
app/new_web_source
|
||||
app/user_data/
|
||||
.omx/
|
||||
.codex
|
||||
.rtk
|
||||
OPS_REDIS_MYSQL_OPTIMIZATION_NOTES.md
|
||||
架构.md
|
||||
*ts.%
|
||||
.omc
|
||||
|
||||
7818
2026_05_29.log
Normal file
7818
2026_05_29.log
Normal file
File diff suppressed because one or more lines are too long
18
app/.env
18
app/.env
@@ -1,15 +1,23 @@
|
||||
base_url=http://8.136.19.173:15124
|
||||
base_url=http://47.110.241.161:15124
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=8.136.19.173
|
||||
mysql_host=47.110.241.161
|
||||
mysql_user=aiimage
|
||||
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=3&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_mode=2
|
||||
|
||||
zn_company=rongchuang123
|
||||
zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
|
||||
|
||||
client_name=ShuFuAI
|
||||
|
||||
# java_api_base=http://api.aishufu.top:18080/
|
||||
java_api_base=http://api.aishufu.top:18080/
|
||||
# java_api_base=http://api.aishufu.top:18080/
|
||||
|
||||
# 与 Java 后端共享的 JWT 签名密钥,必须与 backend-java 的 AIIMAGE_JWT_SECRET 完全一致
|
||||
AIIMAGE_JWT_SECRET=please-change-this-secret-please-rotate-at-least-32-bytes
|
||||
|
||||
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
|
||||
|
||||
|
||||
15
app/.env copy
Normal file
15
app/.env copy
Normal file
@@ -0,0 +1,15 @@
|
||||
base_url=http://47.111.163.154:15124
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=47.111.163.154
|
||||
mysql_user=aiimage
|
||||
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_mode=2
|
||||
|
||||
client_name=ShuFuAI
|
||||
|
||||
|
||||
java_api_base=http://api.aishufu.top:18080/
|
||||
# java_api_base=http://api.aishufu.top:18080/
|
||||
|
||||
|
||||
23
app/.env.example
Normal file
23
app/.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=47.111.163.154
|
||||
mysql_user=aiimage
|
||||
|
||||
proxy_url=https://api.jikip.com/ip-get?num=1&minute=1&format=json&area=all&protocol=1&mode=2&key=t24g6gi44ubufd8
|
||||
proxy_mode=2
|
||||
|
||||
zn_company=rongchuang123
|
||||
zn_username=%E8%87%AA%E5%8A%A8%E5%8C%96_Robot
|
||||
|
||||
client_name=ShuFuAI
|
||||
|
||||
|
||||
java_api_base=http://api.aishufu.top:18080/
|
||||
# java_api_base=http://api.aishufu.top:18080/
|
||||
# java_api_base=http://api.aishufu.top:18080/
|
||||
|
||||
# 与 Java 后端共享的 JWT 签名密钥,必须与 backend-java 的 AIIMAGE_JWT_SECRET 完全一致
|
||||
AIIMAGE_JWT_SECRET=please-change-this-secret-please-rotate-at-least-32-bytes
|
||||
# JWT cookie 名称,默认 aiimage_token;改动需与 Java 端 aiimage.auth.cookie-name 保持一致
|
||||
# AIIMAGE_AUTH_COOKIE_NAME=aiimage_token
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -14,7 +14,6 @@ def upload_file(file_content: bytes, key: str):
|
||||
cfg.credentials_provider = credentials_provider
|
||||
cfg.region = region
|
||||
cfg.endpoint = endpoint
|
||||
cfg.retry_max_attempts = 3
|
||||
client = oss.Client(cfg)
|
||||
|
||||
result = client.put_object(
|
||||
|
||||
847
app/amazon/amazon_base.py
Normal file
847
app/amazon/amazon_base.py
Normal file
@@ -0,0 +1,847 @@
|
||||
import traceback
|
||||
import winreg
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from DrissionPage import Chromium,ChromiumOptions
|
||||
from DrissionPage.common import By
|
||||
from DrissionPage._pages.chromium_tab import ChromiumTab
|
||||
from typing import Literal,Dict,Any,TypeVar,Type
|
||||
|
||||
|
||||
from amazon.tool import get_shop_info,show_notification
|
||||
from config import runing_shop
|
||||
|
||||
# 定义类型变量,用于泛型类型注解
|
||||
T = TypeVar('T', bound='AmamzonBase')
|
||||
|
||||
|
||||
def kill_process(version: Literal["v5", "v6"]):
|
||||
"""杀紫鸟客户端进程(独立函数版本)"""
|
||||
driver = ZiniaoDriver({})
|
||||
driver.kill_process(version)
|
||||
|
||||
|
||||
class ZiniaoDriver:
|
||||
"""紫鸟浏览器自动化驱动类"""
|
||||
|
||||
def __init__(self, user_info: dict, socket_port: int = 19890):
|
||||
"""
|
||||
初始化紫鸟浏览器驱动
|
||||
|
||||
Args:
|
||||
user_info: 用户信息字典,包含 company, username, password
|
||||
socket_port: 客户端通信端口,默认 19890
|
||||
"""
|
||||
self.user_info = user_info
|
||||
self.socket_port = socket_port
|
||||
self.client_path = None
|
||||
self.browser = None
|
||||
self.tab: ChromiumTab = None
|
||||
self.store_id = None
|
||||
|
||||
self.url = None #用于记录当前链接,实现断点续传
|
||||
|
||||
|
||||
|
||||
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
|
||||
"""
|
||||
获取紫鸟安装目录
|
||||
|
||||
Args:
|
||||
protocol_name: 协议名称,默认 "superbrowser"
|
||||
|
||||
Returns:
|
||||
exe_path: 紫鸟浏览器可执行文件路径
|
||||
"""
|
||||
try:
|
||||
key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command"
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
if isinstance(command, str):
|
||||
sub = "ziniao.exe"
|
||||
exe_path = command[0: command.find(sub) + len(sub) + 1]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
if isinstance(command, str):
|
||||
sub = "ziniao.exe"
|
||||
exe_path = command[0: command.find(sub) + len(sub) + 1]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def update_core(self):
|
||||
"""
|
||||
下载所有内核,打开店铺前调用,需客户端版本5.285.7以上
|
||||
因为http有超时时间,所以这个action适合循环调用,直到返回成功
|
||||
"""
|
||||
data = {
|
||||
"action": "updateCore",
|
||||
"requestId": str(uuid.uuid4()),
|
||||
}
|
||||
data.update(self.user_info)
|
||||
while True:
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
result = response.json()
|
||||
print(result)
|
||||
if result is None:
|
||||
print("等待客户端启动...")
|
||||
time.sleep(2)
|
||||
continue
|
||||
if result.get("statusCode") is None or result.get("statusCode") == -10003:
|
||||
print("当前版本不支持此接口,请升级客户端")
|
||||
return
|
||||
elif result.get("statusCode") == 0:
|
||||
print("更新内核完成")
|
||||
return
|
||||
else:
|
||||
print(f"等待更新内核: {json.dumps(result)}")
|
||||
time.sleep(2)
|
||||
|
||||
def kill_process(self, version: Literal["v5", "v6"]):
|
||||
"""
|
||||
杀紫鸟客户端进程
|
||||
|
||||
Args:
|
||||
version: 客户端版本
|
||||
"""
|
||||
if version == "v5":
|
||||
process_name = 'SuperBrowser.exe'
|
||||
os.system('taskkill /f /t /im ' + "starter.exe")
|
||||
else:
|
||||
process_name = 'ziniao.exe'
|
||||
os.system('taskkill /f /t /im ' + process_name)
|
||||
time.sleep(3)
|
||||
|
||||
def get_browser_list(self) -> list:
|
||||
"""
|
||||
获取浏览器列表
|
||||
|
||||
Returns:
|
||||
list: 浏览器列表
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "getBrowserList",
|
||||
"requestId": request_id
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
print(r)
|
||||
return r.get("browserList")
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
print(f"【get_browser_list】登录失败 {json.dumps(r, ensure_ascii=False)}")
|
||||
# exit()
|
||||
else:
|
||||
print(f"【get_browser_list】失败 {json.dumps(r, ensure_ascii=False)} ")
|
||||
# exit()
|
||||
|
||||
def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0,
|
||||
isHeadless=0, cookieTypeSave=0, jsInfo=""):
|
||||
"""
|
||||
打开店铺
|
||||
|
||||
Args:
|
||||
store_info: 店铺信息(browserId 或 browserOauth)
|
||||
isWebDriverReadOnlyMode: 是否只读模式,默认 0
|
||||
isprivacy: 隐私模式,默认 0
|
||||
isHeadless: 无头模式,默认 0
|
||||
cookieTypeSave: cookie保存类型,默认 0
|
||||
jsInfo: 注入的JS信息,默认 ""
|
||||
|
||||
Returns:
|
||||
dict: 返回结果
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "startBrowser",
|
||||
"isWaitPluginUpdate": 0,
|
||||
"isHeadless": isHeadless,
|
||||
"requestId": request_id,
|
||||
"isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
|
||||
"cookieTypeLoad": 0,
|
||||
"cookieTypeSave": cookieTypeSave,
|
||||
"runMode": "1",
|
||||
"isLoadUserPlugin": True,
|
||||
"pluginIdType": 1,
|
||||
"privacyMode": isprivacy
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
if store_info.isdigit():
|
||||
data["browserId"] = store_info
|
||||
else:
|
||||
data["browserOauth"] = store_info
|
||||
|
||||
if len(str(jsInfo)) > 2:
|
||||
data["injectJsInfo"] = json.dumps(jsInfo)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
return r
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
raise RuntimeError(f"【open_store】登录失败 {json.dumps(r, ensure_ascii=False)}")
|
||||
# exit()
|
||||
else:
|
||||
raise RuntimeError(f"【open_store】失败 {json.dumps(r, ensure_ascii=False)} ")
|
||||
# exit()
|
||||
|
||||
def get_browser(self, port) -> Chromium:
|
||||
"""
|
||||
获取浏览器实例
|
||||
|
||||
Args:
|
||||
port: 调试端口
|
||||
|
||||
Returns:
|
||||
Chromium: DrissionPage浏览器实例
|
||||
"""
|
||||
co = ChromiumOptions()
|
||||
# 设置不加载图片、静音
|
||||
co.set_local_port(port)
|
||||
co.no_imgs(True).mute(True)
|
||||
browser = Chromium(co)
|
||||
return browser
|
||||
|
||||
def start_client(self):
|
||||
"""启动紫鸟客户端"""
|
||||
|
||||
# 检查端口是否已经启动
|
||||
try:
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.get(url, timeout=2)
|
||||
print(f"端口 {self.socket_port} 已经启动,跳过启动客户端操作")
|
||||
return
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||
# 端口未启动,继续执行启动操作
|
||||
print(f"端口 {self.socket_port} 未启动,开始启动客户端")
|
||||
|
||||
self.kill_process('v6')
|
||||
time.sleep(5)
|
||||
self.client_path = self.get_zinaio_exe("superbrowserv6").strip('"')
|
||||
print(self.client_path)
|
||||
cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http',
|
||||
'--port=' + str(self.socket_port)]
|
||||
print(" ".join(cmd))
|
||||
|
||||
# 最大重试次数
|
||||
max_retries = 3
|
||||
|
||||
for retry_count in range(max_retries):
|
||||
print(f"第 {retry_count + 1} 次尝试启动客户端...")
|
||||
|
||||
# 启动进程
|
||||
subprocess.Popen(cmd)
|
||||
|
||||
# 循环检测10秒,每0.5秒检测一次
|
||||
start_check_time = time.time()
|
||||
client_started = False
|
||||
|
||||
while time.time() - start_check_time < 10:
|
||||
try:
|
||||
response = requests.get(url, timeout=2)
|
||||
print(f"客户端启动成功!(第 {retry_count + 1} 次尝试)")
|
||||
client_started = True
|
||||
break
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||
# 端口还未启动,继续等待
|
||||
time.sleep(0.5)
|
||||
|
||||
if client_started:
|
||||
time.sleep(5) # 等待客户端完全启动
|
||||
# 更新内核
|
||||
self.update_core()
|
||||
return
|
||||
else:
|
||||
print(f"第 {retry_count + 1} 次尝试启动失败,10秒内未检测到客户端启动")
|
||||
|
||||
# 超过最大重试次数,抛出异常
|
||||
raise RuntimeError(f"客户端启动失败:重试 {max_retries} 次后仍未成功启动")
|
||||
|
||||
def open_shop(self, shop_name: str):
|
||||
"""
|
||||
打开指定店铺
|
||||
|
||||
Args:
|
||||
shop_name: 店铺名称
|
||||
|
||||
Returns:
|
||||
Chromium or str: 成功返回浏览器实例,失败返回错误信息
|
||||
"""
|
||||
print("=============打开指定店铺================")
|
||||
# 启动客户端
|
||||
self.start_client()
|
||||
|
||||
# 获取店铺列表
|
||||
shop_ls = self.get_browser_list()
|
||||
self.store_id = None
|
||||
print(shop_ls)
|
||||
for shop in shop_ls:
|
||||
if shop.get("browserName") == shop_name:
|
||||
self.store_id = shop.get('browserOauth')
|
||||
break
|
||||
|
||||
if not self.store_id:
|
||||
print("店铺不存在")
|
||||
return "店铺不存在"
|
||||
|
||||
# 打开店铺
|
||||
ret_json = self.open_store(self.store_id)
|
||||
print(ret_json)
|
||||
self.store_id = ret_json.get("browserOauth")
|
||||
if self.store_id is None:
|
||||
self.store_id = ret_json.get("browserId")
|
||||
|
||||
# 获取drissionpage浏览器会话
|
||||
self.browser = self.get_browser(ret_json.get('debuggingPort'))
|
||||
|
||||
ip_check_url = ret_json.get("ipDetectionPage")
|
||||
if not ip_check_url:
|
||||
print("ip检测页地址为空,请升级紫鸟浏览器到最新版")
|
||||
print(f"=====关闭店铺:{shop_name}=====")
|
||||
self.close_store(self.store_id)
|
||||
# exit()
|
||||
raise RuntimeError("没有IP检测地址,为了店铺安全不打开店铺")
|
||||
ip_usable = self.open_ip_check(self.browser, ip_check_url)
|
||||
if ip_usable:
|
||||
print("ip检测通过,打开店铺平台主页")
|
||||
self.open_launcher_page(ret_json.get("launcherPage"), self.browser)
|
||||
else:
|
||||
print("IP检测不通过")
|
||||
raise RuntimeError("IP检测不通过,可能是因为网络环境变化导致的,为了店铺安全不打开店铺")
|
||||
return self.browser
|
||||
|
||||
def close_store(self, browser_oauth=None):
|
||||
"""
|
||||
关闭店铺
|
||||
|
||||
Args:
|
||||
browser_oauth: 店铺OAuth标识,如果不提供则使用当前打开的店铺
|
||||
|
||||
Returns:
|
||||
dict: 返回结果
|
||||
"""
|
||||
if browser_oauth is None:
|
||||
browser_oauth = self.store_id
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "stopBrowser",
|
||||
"requestId": request_id,
|
||||
"duplicate": 0,
|
||||
"browserOauth": browser_oauth
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
return r
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
raise RuntimeError(f"【close_store】登录失败 {json.dumps(r, ensure_ascii=False)}")
|
||||
# exit()
|
||||
else:
|
||||
raise RuntimeError(f"【close_store】失败: {json.dumps(r, ensure_ascii=False)} ")
|
||||
# exit()
|
||||
|
||||
def open_launcher_page(self, launcher_page: str, browser: Chromium = None):
|
||||
"""
|
||||
打开启动页面
|
||||
|
||||
Args:
|
||||
launcher_page: 要打开的页面URL
|
||||
browser: 浏览器实例,如果不提供则使用当前浏览器实例
|
||||
"""
|
||||
if browser is None:
|
||||
browser = self.browser
|
||||
|
||||
tab = browser.new_tab(url=launcher_page)
|
||||
self.tab = tab
|
||||
return tab
|
||||
|
||||
def open_ip_check(self, browser: Chromium, ip_check_url: str):
|
||||
"""
|
||||
打开ip检测页检测ip是否正常
|
||||
:param browser: drissionpage浏览器会话
|
||||
:param ip_check_url ip检测页地址
|
||||
:return 检测结果
|
||||
"""
|
||||
try:
|
||||
tab = browser.latest_tab
|
||||
tab.get(ip_check_url)
|
||||
success_button = tab.ele((By.XPATH, '//button[contains(@class, "styles_btn--success")]'),
|
||||
timeout=60) # 等待查找元素60秒
|
||||
if success_button:
|
||||
print("ip检测成功")
|
||||
return True
|
||||
else:
|
||||
print("ip检测超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
print("ip检测异常:" + traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
class AmamzonBase(ZiniaoDriver):
|
||||
"""亚马逊操作基类,包含一些通用方法"""
|
||||
mark_name = "操作基类"
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""日志输出
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别
|
||||
"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# if level == "ERROR":
|
||||
# show_notification(message, "error")
|
||||
print(f"[{timestamp}] [{self.mark_name}] [{level}] {message}")
|
||||
except Exception as e:
|
||||
print(f"输出出错,{e}")
|
||||
|
||||
|
||||
def SwitchingCountries(self, country_name: str):
|
||||
"""
|
||||
切换国家
|
||||
操作:
|
||||
1、//div[@class="dropdown-account-switcher-header-label"]/span[last()] 获取此元素文本,判断当前国家,如果与目标国家相同则不操作,否则执行下一步
|
||||
2、点击 //div[@class="dropdown-account-switcher-header-label"] 打开下拉框
|
||||
3、点击 //div[@class="dropdown-account-switcher-list-item"] 第一个展开国家列表
|
||||
4、点击 //div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="国家名"] 切换到目标国家
|
||||
5、等待页面加载完成,判断国家是否切换成功,成功则返回True,否则返回False
|
||||
|
||||
Args:
|
||||
country_name: 目标国家名称
|
||||
|
||||
Returns:
|
||||
bool: 切换成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
print("=============切换国家================")
|
||||
if self.browser is None:
|
||||
print("浏览器实例不存在,请先打开店铺")
|
||||
return False
|
||||
|
||||
# 获取当前标签页
|
||||
tab = self.tab
|
||||
tab.wait.doc_loaded(timeout=120, raise_err=False)
|
||||
|
||||
# 步骤1:获取当前国家名称
|
||||
self.log(f"正在检查当前国家...")
|
||||
current_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
|
||||
timeout=20)
|
||||
if current_country_ele:
|
||||
current_country = current_country_ele.text.strip()
|
||||
self.log(f"当前国家:{current_country}")
|
||||
|
||||
# 判断是否与目标国家相同
|
||||
if current_country == country_name:
|
||||
self.log(f"当前已经是目标国家 {country_name},无需切换")
|
||||
return True
|
||||
else:
|
||||
self.log("无法获取当前国家信息")
|
||||
return False
|
||||
|
||||
# 步骤2:点击打开下拉框
|
||||
self.log(f"正在打开国家切换下拉框...")
|
||||
dropdown_header = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]', timeout=10)
|
||||
if not dropdown_header:
|
||||
self.log("找不到国家切换下拉框")
|
||||
return False
|
||||
dropdown_header.click()
|
||||
time.sleep(1) # 等待下拉框展开
|
||||
|
||||
# 步骤3:点击第一个展开国家列表
|
||||
self.log(f"正在展开国家列表...")
|
||||
first_item = tab.ele('xpath://div[@class="dropdown-account-switcher-list-item"]', timeout=10)
|
||||
if not first_item:
|
||||
self.log("找不到国家列表项")
|
||||
return False
|
||||
first_item.click()
|
||||
time.sleep(1) # 等待国家列表展开
|
||||
|
||||
# 步骤4:点击目标国家
|
||||
self.log(f"正在切换到国家:{country_name}")
|
||||
target_country_xpath = f'//div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="{country_name}"]'
|
||||
target_country = tab.ele(f'xpath:{target_country_xpath}', timeout=10)
|
||||
if not target_country:
|
||||
self.log(f"找不到目标国家:{country_name}")
|
||||
return False
|
||||
target_country.click()
|
||||
|
||||
# 步骤5:等待页面加载完成并验证切换结果
|
||||
self.log(f"等待页面加载...")
|
||||
# time.sleep(3) # 等待页面加载
|
||||
self.tab.wait.doc_loaded()
|
||||
|
||||
# 再次检查当前国家
|
||||
new_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
|
||||
timeout=10)
|
||||
if new_country_ele:
|
||||
new_country = new_country_ele.text.strip()
|
||||
if new_country == country_name:
|
||||
self.log(f"国家切换成功:{new_country}")
|
||||
return True
|
||||
else:
|
||||
self.log(f"国家切换失败,当前国家:{new_country},目标国家:{country_name}")
|
||||
return False
|
||||
else:
|
||||
self.log("无法验证切换结果")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"切换国家时发生异常:{traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def need_login(self):
|
||||
"""
|
||||
判断是否需要登录,部分国家可能需要登录后才能切换国家
|
||||
处理流程:
|
||||
|
||||
"""
|
||||
time.sleep(3) # 等待页面可能的登录元素加载,避免跳转等等
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
need_login_ele = self.tab.eles('xpath://h1[@class="a-spacing-small"]|//span[contains(text(),"登录")]', timeout=5)
|
||||
if len(need_login_ele) > 0:
|
||||
self.log("检测到需要登录元素")
|
||||
return True
|
||||
return False
|
||||
|
||||
def login(self, password, username=""):
|
||||
try:
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
for _ in range(4):
|
||||
switch_account = self.tab.eles(
|
||||
'xpath://div[@data-test-id="switchableAccounts"]//div[@class="a-fixed-left-grid"]', timeout=5)
|
||||
if len(switch_account) > 0:
|
||||
switch_account[0].click()
|
||||
time.sleep(1)
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
|
||||
pwd_input = self.tab.eles('xpath://input[@type="password"]', timeout=5)
|
||||
if len(pwd_input) > 0:
|
||||
pwd_input[0].input(password, clear=True)
|
||||
submit_btn = self.tab.eles('xpath://input[@id="signInSubmit"]', timeout=5)
|
||||
if len(submit_btn) > 0:
|
||||
submit_btn[0].click()
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
|
||||
send_code = self.tab.eles('xpath://span[@id="auth-send-code" and contains(string(.),"发送一次性密码")]',
|
||||
timeout=5)
|
||||
self.log(f"发送一次性密码:{len(send_code)}")
|
||||
if len(send_code) > 0:
|
||||
self.log("检测到 发送一次性密码")
|
||||
send_code[0].click()
|
||||
time.sleep(1)
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
else:
|
||||
for j in range(5):
|
||||
opt_code_input = self.tab.eles('xpath://input[@name="otpCode"]', timeout=30)
|
||||
self.log(f"验证码输入框:{len(opt_code_input)}")
|
||||
if len(opt_code_input) > 0:
|
||||
opt_code_input[0].wait.displayed(timeout=10, raise_err=False)
|
||||
for _ in range(30):
|
||||
if opt_code_input[0].value is not None and opt_code_input[0].value.strip() != "":
|
||||
print("检测到验证码输入完成")
|
||||
submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]', timeout=10)
|
||||
if len(submit_btn) > 0:
|
||||
submit_btn[0].click()
|
||||
self.tab.wait.doc_loaded(timeout=20, raise_err=False)
|
||||
# return True
|
||||
error_mes = self.tab.eles('xpath://div[@id="auth-error-message-box"]',
|
||||
timeout=10)
|
||||
if len(error_mes) > 0:
|
||||
self.log("验证码输入错误")
|
||||
self.log(f"验证码输入错误提示:{error_mes[0].text}")
|
||||
self.tab.refresh()
|
||||
else:
|
||||
self.log("登入成功")
|
||||
return True
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
break
|
||||
|
||||
submit_btn = self.tab.eles('xpath://input[@id="auth-signin-button"]', timeout=10)
|
||||
if len(submit_btn) > 0:
|
||||
submit_btn[0].click()
|
||||
except Exception as e:
|
||||
self.log(f"登录过程中发生异常:{traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def SwitchPage(self):
|
||||
"""
|
||||
切换至 管理所有库存页面
|
||||
1、等待 //navigation-favorites-bar[@class="hydrated"] 出现
|
||||
"""
|
||||
navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]')
|
||||
navigation.wait.displayed(raise_err=False)
|
||||
page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr(
|
||||
'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]')
|
||||
page_btn.wait.displayed(raise_err=False)
|
||||
page_btn.click(timeout=5)
|
||||
|
||||
self.tab.wait.doc_loaded()
|
||||
# 等待搜索框出现
|
||||
search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group')
|
||||
search_region.wait.displayed(raise_err=False, timeout=60)
|
||||
|
||||
def search(self,filter_type="ApprovalRequired"):
|
||||
sku_ls = []
|
||||
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown')
|
||||
drop_down.wait.displayed(raise_err=False)
|
||||
drop_down.wait.enabled(raise_err=False)
|
||||
time.sleep(0.6)
|
||||
drop_down.click()
|
||||
# //kat-option[@value="SearchSuppressed"]
|
||||
xp = f'xpath://kat-option[@value="{filter_type}"]'
|
||||
self.log(f"正在寻找筛选条件 {filter_type},xpath: {xp}")
|
||||
approval_required = self.tab.eles(xp,timeout=5)
|
||||
if len(approval_required) == 0:
|
||||
self.log(f"没有需要{filter_type}选项】没有需要{filter_type}的商品了")
|
||||
return sku_ls # "没有需要审批的商品了"
|
||||
else:
|
||||
approval_required = approval_required[0]
|
||||
approval_required.wait.displayed(raise_err=False)
|
||||
approval_required.click()
|
||||
|
||||
approval_required_text = approval_required.text
|
||||
self.log(f"已选择筛选条件: {approval_required_text}")
|
||||
|
||||
count = re.findall(r'\d+', approval_required_text)
|
||||
if count:
|
||||
count = int(count[0])
|
||||
self.log(f"待审批的商品数量: {count}")
|
||||
if count <= 0:
|
||||
self.log(f"没有需要{filter_type}的商品了")
|
||||
return sku_ls #"没有需要审批的商品了"
|
||||
for _ in range(3):
|
||||
# 等待加载完成
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]")
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3)
|
||||
if len(sku_ls) > 0:
|
||||
break
|
||||
approval_required.click()
|
||||
return sku_ls
|
||||
|
||||
|
||||
class TaskBase:
|
||||
task_name = "任务基类"
|
||||
|
||||
country_info = {
|
||||
"DE": "德国",
|
||||
"FR": "法国",
|
||||
"ES": "西班牙",
|
||||
"IT": "意大利",
|
||||
"UK": "英国"
|
||||
}
|
||||
|
||||
def __init__(self, user_info: dict = None):
|
||||
"""初始化审批任务处理器
|
||||
|
||||
Args:
|
||||
user_info: 用户信息字典,包含 company, username, password
|
||||
"""
|
||||
self.user_info = user_info or {}
|
||||
self.running = True
|
||||
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""日志输出
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别
|
||||
"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# if level == "ERROR":
|
||||
# show_notification(message, "error")
|
||||
print(f"[{timestamp}] [{self.task_name}] [{level}] {message}")
|
||||
except Exception as e:
|
||||
print(f"输出出错,{e}")
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
pass
|
||||
|
||||
|
||||
def open_shop(self, cls: Type[T], max_retries: int, company_name: str, shop_name: str, iskill: bool = False) -> T:
|
||||
error_info = ""
|
||||
driver = None
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
if iskill:
|
||||
self.log("重试前先杀掉浏览器进程...")
|
||||
kill_process("v6")
|
||||
kill_process("v5")
|
||||
time.sleep(2)
|
||||
|
||||
# 组装用户信息并创建驱动
|
||||
user_info = {
|
||||
**self.user_info,
|
||||
"company": company_name
|
||||
}
|
||||
driver = cls(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
|
||||
if browser and browser != "店铺不存在":
|
||||
self.log(f"成功打开店铺 {shop_name}")
|
||||
else:
|
||||
self.log(f"打开店铺失败: {browser}", "WARNING")
|
||||
driver = None
|
||||
continue
|
||||
|
||||
# 判断是否需要登录
|
||||
need_login = driver.need_login()
|
||||
print("【是否需要登录】:", need_login)
|
||||
if need_login:
|
||||
self.log(f"店铺 {shop_name} 需要登录,正在登录...")
|
||||
# 获取店铺凭证
|
||||
response = get_shop_info(shop_name)
|
||||
print("【获取店铺凭证返回】:", response.text)
|
||||
shop_data = response.json()
|
||||
if not shop_data:
|
||||
mes = f"获取店铺凭证失败,响应数据: {shop_data.get('message', '未知错误')}"
|
||||
self.log(mes, "ERROR")
|
||||
# show_notification(mes, "ERROR")
|
||||
continue
|
||||
|
||||
password = shop_data["data"]["password"]
|
||||
|
||||
login_success = driver.login(password)
|
||||
if login_success:
|
||||
self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...")
|
||||
browser = driver.open_shop(shop_name)
|
||||
if browser and browser != "店铺不存在":
|
||||
self.log(f"成功打开店铺 {shop_name} 登录后")
|
||||
break
|
||||
else:
|
||||
self.log(f"登录后打开店铺失败: {browser}", "WARNING")
|
||||
driver = None
|
||||
else:
|
||||
self.log(f"店铺 {shop_name} 登录失败", "WARNING")
|
||||
driver = None
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO")
|
||||
driver = None
|
||||
error_info = str(e)
|
||||
time.sleep(10)
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(3)
|
||||
|
||||
# 检查是否成功打开
|
||||
if not driver or not browser or browser == "店铺不存在":
|
||||
error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}"
|
||||
self.log(error_msg, "ERROR")
|
||||
# 从执行列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
return driver
|
||||
return driver
|
||||
|
||||
|
||||
def process_shop(self, shop_data: Dict[str, Any], task_id: int):
|
||||
pass
|
||||
|
||||
def action_init(self, driver, country_name, shop_name, risk_listing_filter=None):
|
||||
"""切换到指定国际 -> 页面 -》 筛选好"""
|
||||
max_retries = 5
|
||||
switch_success = False
|
||||
switch_success_pg = False
|
||||
search_success = False
|
||||
sku_ls = []
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
if retry > 2:
|
||||
# 刷新不行就重新打开店铺
|
||||
self.log("重试前重新打开店铺...")
|
||||
try:
|
||||
driver.close_store()
|
||||
time.sleep(3)
|
||||
driver.open_shop(shop_name)
|
||||
except Exception as e:
|
||||
self.log(f"关闭重新打开店铺: {str(e)}", "WARNING")
|
||||
# 如果不是第一次尝试,先刷新页面
|
||||
if retry > 0:
|
||||
self.log("重试前刷新页面...")
|
||||
try:
|
||||
driver.tab.refresh()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
self.log(f"刷新页面失败: {str(e)}", "WARNING")
|
||||
|
||||
switch_success = driver.SwitchingCountries(country_name)
|
||||
if switch_success:
|
||||
self.log(f"成功切换到国家 {country_name}")
|
||||
|
||||
# 切换到库存管理页面
|
||||
driver.SwitchPage()
|
||||
self.log(f"已切换到库存管理页面")
|
||||
time.sleep(3)
|
||||
switch_success_pg = True
|
||||
|
||||
if risk_listing_filter is None:
|
||||
self.log(f"risk_listing_filter 为空,不筛选, {risk_listing_filter}")
|
||||
search_success = True
|
||||
break
|
||||
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
search_success = True
|
||||
self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...")
|
||||
break
|
||||
else:
|
||||
self.log(f"切换到国家 {country_name} 失败", "WARNING")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
return switch_success, switch_success_pg, search_success, sku_ls
|
||||
999
app/amazon/approve - 副本.py
Normal file
999
app/amazon/approve - 副本.py
Normal file
@@ -0,0 +1,999 @@
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
from DrissionPage._pages.chromium_tab import ChromiumTab
|
||||
|
||||
from config import runing_task, runing_shop
|
||||
from datetime import datetime
|
||||
from amazon.del_brand import AmamzonBase, kill_process
|
||||
|
||||
# 导入 webview 用于前端通知
|
||||
try:
|
||||
import webview
|
||||
except ImportError:
|
||||
webview = None
|
||||
|
||||
class AmzoneApprove(AmamzonBase):
|
||||
|
||||
def SwitchPage(self):
|
||||
"""
|
||||
切换至 管理所有库存页面
|
||||
1、等待 //navigation-favorites-bar[@class="hydrated"] 出现
|
||||
"""
|
||||
navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]')
|
||||
navigation.wait.displayed(raise_err=False)
|
||||
page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr(
|
||||
'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]')
|
||||
page_btn.wait.displayed(raise_err=False)
|
||||
page_btn.click(timeout=5)
|
||||
|
||||
self.tab.wait.doc_loaded()
|
||||
# 等待搜索框出现
|
||||
search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group')
|
||||
search_region.wait.displayed(raise_err=False)
|
||||
|
||||
|
||||
def search(self,filter_type="ApprovalRequired"):
|
||||
sku_ls = []
|
||||
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown')
|
||||
drop_down.wait.displayed(raise_err=False)
|
||||
drop_down.wait.enabled(raise_err=False)
|
||||
time.sleep(0.6)
|
||||
drop_down.click()
|
||||
# //kat-option[@value="SearchSuppressed"]
|
||||
xp = f'xpath://kat-option[@value="{filter_type}"]'
|
||||
print(f"正在寻找筛选条件 {filter_type},xpath: {xp}")
|
||||
approval_required = self.tab.eles(xp,timeout=5)
|
||||
if len(approval_required) == 0:
|
||||
print(f"【没有需要{filter_type}选项】没有需要{filter_type}的商品了")
|
||||
return sku_ls # "没有需要审批的商品了"
|
||||
else:
|
||||
approval_required = approval_required[0]
|
||||
approval_required.wait.displayed(raise_err=False)
|
||||
approval_required.click()
|
||||
|
||||
approval_required_text = approval_required.text
|
||||
print(f"已选择筛选条件: {approval_required_text}")
|
||||
|
||||
count = re.findall(r'\d+', approval_required_text)
|
||||
if count:
|
||||
count = int(count[0])
|
||||
print(f"待审批的商品数量: {count}")
|
||||
if count <= 0:
|
||||
print(f"没有需要{filter_type}的商品了")
|
||||
return sku_ls #"没有需要审批的商品了"
|
||||
asin = "B0C7KT6ZZN"
|
||||
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
|
||||
'xpath://span[@class="container"]//input[@part="input"]')
|
||||
search_input.input(asin, clear=True)
|
||||
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
|
||||
search_btn.click()
|
||||
|
||||
for _ in range(3):
|
||||
# 等待加载完成
|
||||
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
|
||||
load_ele.wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
|
||||
'xpath://span[@class="container"]//input[@part="input"]')
|
||||
search_input.input(asin, clear=True)
|
||||
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
|
||||
search_btn.click()
|
||||
|
||||
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
|
||||
load_ele.wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3)
|
||||
if len(sku_ls) > 0:
|
||||
break
|
||||
approval_required.click()
|
||||
|
||||
return sku_ls
|
||||
|
||||
def wait_loaded(self):
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.ele(
|
||||
'xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[contains(@class,"Loader-module__loader")]|//kat-panel[@data-testid="kat-panel-ActionPanelContent"]/div[@data-f1-component]//div[contains(@class,"==")]/div[contains(@class,"==")]/span',
|
||||
timeout=3)
|
||||
load_ele.wait.deleted(timeout=5, raise_err=False)
|
||||
except Exception as e:
|
||||
print("等待加载中消失出错", e)
|
||||
|
||||
def clear_tab(self):
|
||||
all_tab = self.browser.get_tabs(title="亚马逊")
|
||||
close_tab = []
|
||||
for tab in all_tab:
|
||||
tab_id = tab if isinstance(tab,str) else tab.tab_id
|
||||
if self.tab.tab_id == tab:
|
||||
continue
|
||||
close_tab.append(tab)
|
||||
print("需要关闭的标签页",close_tab)
|
||||
print("当前操作的tab_id",self.tab.tab_id)
|
||||
self.browser.close_tabs(close_tab)
|
||||
|
||||
def handle_repair_product(self,tab:ChromiumTab):
|
||||
def wait_loaded():
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = tab.ele(
|
||||
'xpath://div[@class="contentWrapper"]//div[contains(@class,"==")]/span[not(node())]',
|
||||
timeout=3)
|
||||
load_ele.wait.deleted(timeout=5, raise_err=False)
|
||||
except Exception as e:
|
||||
print("等待加载中消失出错", e)
|
||||
|
||||
tab.wait.doc_loaded(timeout=30)
|
||||
submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
|
||||
if len(submit_btn_ls) == 0:
|
||||
print("未找到提交按钮,可能页面未加载完成或者页面结构发生变化")
|
||||
return
|
||||
# todo 遍历
|
||||
for index in range(len(submit_btn_ls)):
|
||||
if index > len(submit_btn_ls)-1:
|
||||
print("提交按钮数量发生变化,停止处理")
|
||||
break
|
||||
|
||||
submit_btn = submit_btn_ls[index]
|
||||
|
||||
# 关闭按钮
|
||||
close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5)
|
||||
if len(close_x_btn) > 0:
|
||||
close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
|
||||
close_x_btn[0].click()
|
||||
|
||||
submit_btn.wait.enabled(timeout=5, raise_err=False)
|
||||
submit_btn.click()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
need_input = tab.eles('xpath://div[@class="contentWrapper"]//kat-input[@data-testid="kat-input-dew:ump_epr_resgitration_number_title"]',timeout=5)
|
||||
if len(need_input) > 0:
|
||||
print("需要输入注册号,不符合操作要求,跳过...")
|
||||
continue
|
||||
|
||||
target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]')
|
||||
if len(target_title) > 0:
|
||||
not_start = tab.eles('xpath://div[@class="contentWrapper"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]')
|
||||
if len(not_start) == 0:
|
||||
print("安全证明-不是未开始状态,不需要操作")
|
||||
continue
|
||||
|
||||
not_start.wait.displayed(timeout=5, raise_err=False)
|
||||
not_start.click()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
check = tab.ele('xpath://div[@class="contentWrapper"]//kat-checkbox[@name="value"]')
|
||||
check.wait.displayed(timeout=5, raise_err=False)
|
||||
check.click()
|
||||
|
||||
save_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]')
|
||||
save_btn.wait.enabled(timeout=5, raise_err=False)
|
||||
save_btn.click()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10)
|
||||
close_btn.wait.displayed(timeout=5, raise_err=False)
|
||||
close_btn.click()
|
||||
|
||||
save_btn_ls = tab.eles('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]',timeout=5)
|
||||
if len(save_btn_ls) > 0:
|
||||
option_selection_ls = tab.eles('xpath://div[@class="contentWrapper"]//div[@role="option"]',timeout=5)
|
||||
for option in option_selection_ls:
|
||||
option.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
save_btn_ls[0].wait.enabled(timeout=5, raise_err=False)
|
||||
save_btn_ls[0].click()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10)
|
||||
close_btn.wait.displayed(timeout=5, raise_err=False)
|
||||
close_btn.wait.enabled(timeout=5, raise_err=False)
|
||||
close_btn.click()
|
||||
|
||||
submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
|
||||
|
||||
print("修复商品信息处理完成!")
|
||||
tab.close()
|
||||
|
||||
def run_page_action(self):
|
||||
print("开始执行")
|
||||
num = 0
|
||||
retry_num = 0
|
||||
already_asin = set()
|
||||
|
||||
while retry_num < 3: # 最多重试3次
|
||||
# if num > 3: #测试
|
||||
# return
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 获取当前页码
|
||||
try:
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) > 0:
|
||||
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
|
||||
|
||||
# 总页数
|
||||
total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
|
||||
if len(total_page) > 0:
|
||||
total_page = total_page[-1].text
|
||||
else:
|
||||
total_page = 0
|
||||
print(f"当前页码: {current_page} / 总页数: {total_page}")
|
||||
except Exception as e:
|
||||
print("获取页码失败", e)
|
||||
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
||||
print(f"获取到 {len(sku_ls)}")
|
||||
# for sku_ele in sku_ls[0:2]:
|
||||
for sku_ele in sku_ls:
|
||||
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')
|
||||
|
||||
asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]').text
|
||||
print(f"ASIN {asin} 存在问题,正在点击解决...")
|
||||
if asin in already_asin:
|
||||
print(f"{asin} 已经处理过了,跳过")
|
||||
continue
|
||||
solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]')
|
||||
if len(solve_problem) == 0:
|
||||
print(f"{asin},没有处理入口,不处理")
|
||||
yield (asin,"没有处理入口,不处理")
|
||||
already_asin.add(asin)
|
||||
continue
|
||||
|
||||
solve_problem[0].click()
|
||||
|
||||
action_panel = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]')
|
||||
action_panel.wait.displayed(timeout=5, raise_err=False)
|
||||
|
||||
self.wait_loaded()
|
||||
|
||||
try:
|
||||
# 如果存在“请求批准”按钮,则直接返回跳过
|
||||
request_approval_btn = self.tab.eles('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="section-header" and contains(string(.),"移除")]',timeout=5)
|
||||
if len(request_approval_btn) > 0:
|
||||
print(f"ASIN {asin} 存在请求批准按钮,不需要处理,跳过...")
|
||||
panel_close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
|
||||
timeout=5).sr('xpath:.//button[@class="close"]')
|
||||
panel_close_btn.click()
|
||||
|
||||
action_panel.wait.deleted(timeout=3, raise_err=False)
|
||||
yield (asin,"请求批准")
|
||||
|
||||
# 无需采取任何操作
|
||||
not_operate = self.tab.eles('xpath://kat-alert[contains(@description,"如果您之前已提交更改,则这些更改当前正在处理中") and not(@dismissed)]',timeout=3)
|
||||
if len(not_operate) > 0:
|
||||
print(f"ASIN {asin} 无需采取任何操作,跳过...")
|
||||
yield (asin,"无需操作")
|
||||
already_asin.add(asin)
|
||||
continue
|
||||
|
||||
# 解决商品信息违规问题 按钮
|
||||
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]',timeout=5)
|
||||
# if len(kat_box_ls) == 0:
|
||||
# print(f"ASIN {asin} 的操作面板中未找到选项,跳过...")
|
||||
# continue
|
||||
print(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.")
|
||||
# 解决商品信息违规问题 按钮,有多少个都要处理
|
||||
for i in range(len(kat_box_ls)):
|
||||
print(f"开始处理第{i}个问题")
|
||||
if len(kat_box_ls) <= i:
|
||||
print(f"ASIN {asin} 的操作面板中选项数量发生变化,停止处理...")
|
||||
break
|
||||
kat_box_ls[i].click()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
|
||||
# TODO 增加 “出现警告和安全信息时候下滑” 的情况
|
||||
safe_handle_ls = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]',
|
||||
timeout=5)
|
||||
if len(safe_handle_ls) > 0:
|
||||
print("存在安全证明,正在处理...")
|
||||
safe_handle_ls[0].click()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
check_box = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-checkbox')
|
||||
check_box.click()
|
||||
else:
|
||||
problem_selection_ls = self.tab.eles(
|
||||
'xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="registry-list"]//div[@data-testid="registry"]',
|
||||
timeout=10)
|
||||
print("待选择数量",len(problem_selection_ls))
|
||||
for problem_selection in problem_selection_ls:
|
||||
problem_selection.click()
|
||||
print("点击选择完成")
|
||||
time.sleep(0.5)
|
||||
|
||||
# 点击保存
|
||||
confirm_btn = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//kat-button[@variant="primary"]')
|
||||
confirm_btn.wait.displayed(timeout=3, raise_err=False)
|
||||
confirm_btn.click()
|
||||
|
||||
self.wait_loaded()
|
||||
|
||||
# 等待关闭按钮出现
|
||||
try:
|
||||
close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-button[@label="关闭"]',
|
||||
timeout=10)
|
||||
close_btn.click()
|
||||
except Exception as e:
|
||||
print("点击关闭按钮失败",e)
|
||||
|
||||
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]', timeout=5)
|
||||
|
||||
# 修复商品信息
|
||||
repair_product = action_panel.eles('xpath:.//kat-button[@class="action-button"]',timeout=5)
|
||||
if len(repair_product) > 0:
|
||||
print(f"ASIN {asin} 存在修复商品信息按钮,正在点击...")
|
||||
self.clear_tab()
|
||||
repair_product[0].click()
|
||||
time.sleep(1)
|
||||
# 获取最新的tab
|
||||
new_tab = self.browser.latest_tab
|
||||
if isinstance(new_tab, str):
|
||||
new_tab = self.browser.get_tab(id_or_num=new_tab)
|
||||
|
||||
self.handle_repair_product(new_tab)
|
||||
except Exception as e:
|
||||
print("【asin】:",asin,"处理失败",traceback.format_exc())
|
||||
yield (asin,"处理失败")
|
||||
already_asin.add(asin)
|
||||
continue
|
||||
|
||||
yield (asin,"处理完成")
|
||||
already_asin.add(asin)
|
||||
|
||||
try:
|
||||
# 全部操作完成,关闭
|
||||
panel_close_btn= self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
|
||||
timeout=5).sr('xpath:.//button[@class="close"]')
|
||||
panel_close_btn.click()
|
||||
|
||||
action_panel.wait.deleted(timeout=3, raise_err=False)
|
||||
|
||||
except Exception as e:
|
||||
print("关闭操作面板失败", e)
|
||||
|
||||
# 判断是否存在需要翻页的情况
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) == 0:
|
||||
break
|
||||
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
|
||||
class_str = next_page_btn.attr('class')
|
||||
if "end" in class_str:
|
||||
break
|
||||
next_page_btn.click()
|
||||
num += 1
|
||||
print(f"【程序计算】正在翻页,已翻 {num} 页...")
|
||||
|
||||
already_asin = set()
|
||||
|
||||
except Exception as e:
|
||||
print("处理审批操作异常", e)
|
||||
traceback.print_exc()
|
||||
retry_num += 1
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False,timeout=120)
|
||||
|
||||
|
||||
|
||||
|
||||
class ApproveTask:
|
||||
"""审批任务处理类:负责处理产品风险审批任务"""
|
||||
|
||||
country_info = {
|
||||
"DE": "德国",
|
||||
"FR": "法国",
|
||||
"ES": "西班牙",
|
||||
"IT": "意大利",
|
||||
"UK": "英国"
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def show_notification(message: str, message_type: str = "error"):
|
||||
"""显示 pywebview 顶层通知(5秒后自动消失)
|
||||
|
||||
Args:
|
||||
message: 通知消息
|
||||
message_type: 消息类型 (success/warning/error/info)
|
||||
"""
|
||||
if webview and webview.windows:
|
||||
try:
|
||||
# 转义单引号,防止 JavaScript 语法错误
|
||||
safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
|
||||
|
||||
# 设置通知样式颜色
|
||||
color_map = {
|
||||
'success': '#67C23A',
|
||||
'warning': '#E6A23C',
|
||||
'error': '#F56C6C',
|
||||
'info': '#909399'
|
||||
}
|
||||
bg_color = color_map.get(message_type, '#F56C6C')
|
||||
|
||||
# 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知
|
||||
js_code = f"""
|
||||
(function() {{
|
||||
// 移除已存在的通知
|
||||
var existingNotif = document.getElementById('pywebview-notification');
|
||||
if (existingNotif) {{
|
||||
existingNotif.remove();
|
||||
}}
|
||||
|
||||
// 创建通知容器
|
||||
var notif = document.createElement('div');
|
||||
notif.id = 'pywebview-notification';
|
||||
notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' +
|
||||
'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' +
|
||||
'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' +
|
||||
'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;';
|
||||
|
||||
// 添加消息内容
|
||||
var msgSpan = document.createElement('span');
|
||||
msgSpan.textContent = '{safe_message}';
|
||||
notif.appendChild(msgSpan);
|
||||
|
||||
// 添加关闭按钮
|
||||
var closeBtn = document.createElement('span');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;';
|
||||
closeBtn.onclick = function() {{ notif.remove(); }};
|
||||
notif.appendChild(closeBtn);
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(notif);
|
||||
|
||||
// 5秒后自动消失
|
||||
setTimeout(function() {{
|
||||
if (notif && notif.parentNode) {{
|
||||
notif.style.transition = 'opacity 0.3s';
|
||||
notif.style.opacity = '0';
|
||||
setTimeout(function() {{ notif.remove(); }}, 300);
|
||||
}}
|
||||
}}, 5000);
|
||||
}})();
|
||||
"""
|
||||
# 在第一个窗口中执行 JavaScript
|
||||
webview.windows[0].evaluate_js(js_code)
|
||||
except Exception as e:
|
||||
print(f"显示通知失败: {str(e)}")
|
||||
|
||||
"""
|
||||
处理流程:
|
||||
1、解析任务json,提取对应的companyName 组组装user_info实例化 AmzoneApprove
|
||||
2、打开对应的店铺、遍历所有的国家站点处理
|
||||
3、切换至指定国家,切换至 管理所有库存页面
|
||||
4、调用search方法,查询出是否存在需要审批的商品,如果没有则结束;如果有则进入处理流程
|
||||
5、进入处理流程后,调用run_page_action方法(注意是通过yield 返回数据的)
|
||||
|
||||
任务json示例:
|
||||
{
|
||||
"type": "product-risk-resolve-run",
|
||||
"ts": 1775395996876,
|
||||
"data": {
|
||||
"taskId": 925,
|
||||
"items": [
|
||||
{
|
||||
"matched": true,
|
||||
"platform": "亚马逊",
|
||||
"shopName": "魏振峰",
|
||||
"shopId": "27543917795757",
|
||||
"companyName": "rongchuang123",
|
||||
"openStoreUrl": null,
|
||||
"matchedUserId": 17543915345493,
|
||||
"matchStatus": "MATCHED",
|
||||
"matchMessage": null
|
||||
}
|
||||
],
|
||||
"country_codes": [
|
||||
"UK",
|
||||
"DE",
|
||||
"FR",
|
||||
"ES"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, user_info: dict = None):
|
||||
"""初始化审批任务处理器
|
||||
|
||||
Args:
|
||||
user_info: 用户信息字典,包含 company, username, password
|
||||
"""
|
||||
self.user_info = user_info or {}
|
||||
self.running = True
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""日志输出
|
||||
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别
|
||||
"""
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if level == "ERROR":
|
||||
self.show_notification(message, "error")
|
||||
print(f"[{timestamp}] [ApproveTask] [{level}] {message}")
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""处理审批任务主入口
|
||||
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])
|
||||
country_codes = data.get("country_codes", [])
|
||||
risk_listing_filter = data.get("risk_listing_filter", "")
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
if not items:
|
||||
self.log("店铺列表为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
if not country_codes:
|
||||
self.log("国家列表为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家")
|
||||
|
||||
from config import runing_task
|
||||
runing_task[task_id] = {
|
||||
"status": "running",
|
||||
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_shops": len(items),
|
||||
"processed_shops": 0,
|
||||
"total_countries": len(country_codes) * len(items),
|
||||
"processed_countries": 0,
|
||||
"total_asins": 0,
|
||||
"processed_asins": 0,
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"stop_requested": False
|
||||
}
|
||||
|
||||
# 遍历处理每个店铺
|
||||
for idx, shop_item in enumerate(items, 1):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
return
|
||||
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
self.show_notification(f"开始处理店铺: {shop_name}", "info")
|
||||
|
||||
try:
|
||||
self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
from config import runing_task
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
|
||||
"""处理单个店铺
|
||||
|
||||
Args:
|
||||
shop_item: 店铺信息
|
||||
country_codes: 国家代码列表
|
||||
task_id: 任务ID
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
company_name = shop_item.get("companyName", "")
|
||||
|
||||
if not company_name:
|
||||
self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_shop"] = shop_name
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"店铺 {shop_name} 已添加到执行列表,账号: {company_name},开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
max_retries = 3
|
||||
|
||||
error_info = ""
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
# 如果不是第一次尝试,先杀进程
|
||||
# if retry > 0:
|
||||
# self.log("重试前先杀掉浏览器进程...")
|
||||
# kill_process("v6")
|
||||
# kill_process("v5")
|
||||
# time.sleep(2)
|
||||
|
||||
# 组装用户信息并创建驱动
|
||||
user_info = {
|
||||
**self.user_info,
|
||||
"company": company_name
|
||||
}
|
||||
driver = AmzoneApprove(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
|
||||
if browser and browser != "店铺不存在":
|
||||
self.log(f"成功打开店铺 {shop_name}")
|
||||
break
|
||||
else:
|
||||
self.log(f"打开店铺失败: {browser}", "WARNING")
|
||||
driver = None
|
||||
|
||||
# 判断是否需要登录
|
||||
need_login = driver.need_login()
|
||||
if need_login:
|
||||
self.log(f"店铺 {shop_name} 需要登录,正在登录...")
|
||||
password = ""
|
||||
|
||||
login_success = driver.login(password)
|
||||
if login_success:
|
||||
self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...")
|
||||
browser = driver.open_shop(shop_name)
|
||||
if browser and browser != "店铺不存在":
|
||||
self.log(f"成功打开店铺 {shop_name} 登录后")
|
||||
break
|
||||
else:
|
||||
self.log(f"登录后打开店铺失败: {browser}", "WARNING")
|
||||
driver = None
|
||||
else:
|
||||
self.log(f"店铺 {shop_name} 登录失败", "WARNING")
|
||||
driver = None
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO")
|
||||
driver = None
|
||||
error_info = str(e)
|
||||
time.sleep(10)
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(3)
|
||||
|
||||
# 检查是否成功打开
|
||||
if not driver or not browser or browser == "店铺不存在":
|
||||
error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}"
|
||||
self.log(error_msg, "ERROR")
|
||||
# 从执行列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
return
|
||||
|
||||
try:
|
||||
# 处理每个国家
|
||||
for country_code in country_codes:
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
|
||||
break
|
||||
|
||||
try:
|
||||
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
# 最后回传,标记完成
|
||||
try:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
finally:
|
||||
# 关闭店铺
|
||||
try:
|
||||
if driver:
|
||||
self.log(f"关闭店铺 {shop_name}")
|
||||
driver.close_store()
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
self.log(f"关闭店铺失败: {str(e)}", "WARNING")
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver: AmzoneApprove, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str):
|
||||
"""处理单个国家的审批任务
|
||||
|
||||
Args:
|
||||
driver: AmzoneApprove驱动实例
|
||||
country_code: 国家代码(如 UK, DE, FR 等)
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
from config import runing_task
|
||||
|
||||
# 转换国家代码为中文名称
|
||||
country_name = self.country_info.get(country_code, country_code)
|
||||
info_mes = f"开始处理国家: {country_name} ({country_code})"
|
||||
self.log(info_mes)
|
||||
self.show_notification(info_mes, "info")
|
||||
|
||||
# 更新当前处理的国家
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_country"] = country_name
|
||||
|
||||
# 切换国家,最多重试3次
|
||||
max_retries = 3
|
||||
switch_success = False
|
||||
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
# 如果不是第一次尝试,先刷新页面
|
||||
if retry > 0:
|
||||
self.log("重试前刷新页面...")
|
||||
try:
|
||||
driver.tab.refresh()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
self.log(f"刷新页面失败: {str(e)}", "WARNING")
|
||||
|
||||
switch_success = driver.SwitchingCountries(country_name)
|
||||
if switch_success:
|
||||
self.log(f"成功切换到国家 {country_name}")
|
||||
break
|
||||
else:
|
||||
self.log(f"切换到国家 {country_name} 失败", "WARNING")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
# 如果切换失败,直接返回
|
||||
if not switch_success:
|
||||
error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家"
|
||||
self.log(error_message, "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
# 切换到库存管理页面
|
||||
try:
|
||||
driver.SwitchPage()
|
||||
self.log(f"已切换到库存管理页面")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"切换页面失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
# 搜索需要审批的商品,最多重试3次
|
||||
sku_ls = []
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试搜索需要审批的商品 (第 {retry + 1}/{max_retries} 次)")
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"搜索商品异常: {str(e)}", "ERROR")
|
||||
if retry < max_retries - 1:
|
||||
try:
|
||||
driver.tab.refresh()
|
||||
time.sleep(3)
|
||||
except Exception as refresh_error:
|
||||
self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING")
|
||||
|
||||
# 如果没有需要审批的商品,直接返回
|
||||
if len(sku_ls) == 0:
|
||||
self.log(f"国家 {country_name} 没有需要审批的商品")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
self.log(f"国家 {country_name} 有 {len(sku_ls)} 个需要审批的商品,开始处理...")
|
||||
|
||||
# 处理所有需要审批的商品(通过yield获取结果)
|
||||
try:
|
||||
for asin, status in driver.run_page_action():
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理ASIN", "WARNING")
|
||||
break
|
||||
|
||||
self.log(f"ASIN {asin} 处理结果: {status}")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_asin"] = asin
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
|
||||
if status == "处理完成":
|
||||
runing_task[task_id]["success_count"] += 1
|
||||
elif status == "请求批准":
|
||||
# 请求批准的商品也算作成功(因为不需要处理)
|
||||
runing_task[task_id]["success_count"] += 1
|
||||
else:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
# 回传结果到API
|
||||
try:
|
||||
self.post_result(task_id, shop_name, country_code, asin, status)
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
self.log(f"处理审批商品异常: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 更新已处理国家数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
|
||||
self.log(f"国家 {country_name} 处理完成")
|
||||
|
||||
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
country_code: 国家代码
|
||||
asin: ASIN
|
||||
status: 处理状态
|
||||
"""
|
||||
import requests
|
||||
from config import DELETE_BRAND_API_BASE
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result"
|
||||
if status == "请求批准":
|
||||
country_data = {
|
||||
"status": "",
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": "",
|
||||
"done": is_done,
|
||||
"removeAsin": asin,
|
||||
"removeStatus": status
|
||||
}
|
||||
else:
|
||||
country_data = {
|
||||
"status": status,
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": asin,
|
||||
"done": is_done,
|
||||
"removeAsin": "",
|
||||
"removeStatus": ""
|
||||
}
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"error": "",
|
||||
"countries": {
|
||||
country_code : [ country_data ]
|
||||
},
|
||||
"shopName": shop_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
print("=====================================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
self.log(f"结果回传成功: {asin} - {status}")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
user_info = {
|
||||
"company": "rongchuang123",
|
||||
"username": "自动化_Robot",
|
||||
"password": "#20zsg25"
|
||||
}
|
||||
shop_name = "魏振峰"
|
||||
country = "西班牙"
|
||||
kill_process('v6')
|
||||
driver = AmzoneApprove(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
sw_suc = driver.SwitchingCountries(country)
|
||||
driver.SwitchPage()
|
||||
risk_listing_filter = "Active"
|
||||
for _ in range(3):
|
||||
try:
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
driver.tab.refresh()
|
||||
if len(sku_ls) > 0:
|
||||
print("有数据,开始操作")
|
||||
for asin, status in driver.run_page_action():
|
||||
print(f"ASIN {asin} 的处理结果: {status}")
|
||||
print("已完成操作")
|
||||
|
||||
# for i in range(0):
|
||||
# print(i)
|
||||
957
app/amazon/approve.py
Normal file
957
app/amazon/approve.py
Normal file
@@ -0,0 +1,957 @@
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
import requests
|
||||
|
||||
from DrissionPage._pages.chromium_tab import ChromiumTab
|
||||
|
||||
from config import runing_task, runing_shop,DELETE_BRAND_API_BASE
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from amazon.amazon_base import AmamzonBase,kill_process,TaskBase
|
||||
from amazon.tool import show_notification
|
||||
|
||||
class AmzoneApprove(AmamzonBase):
|
||||
mark_name = "产品风险审批"
|
||||
|
||||
def __init__(self, user_info: dict, socket_port: int = 19890):
|
||||
super().__init__(user_info,socket_port)
|
||||
self.already_asin = set()
|
||||
|
||||
def reset_already_asin(self):
|
||||
self.already_asin = set()
|
||||
self.log("already_asin 已重置")
|
||||
|
||||
def wait_loaded(self):
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.eles(
|
||||
'xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[contains(@class,"Loader-module__loader")]|//kat-panel[@data-testid="kat-panel-ActionPanelContent"]/div[@data-f1-component]//div[contains(@class,"==")]/div[contains(@class,"==")]/span',
|
||||
timeout=3)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=5, raise_err=False)
|
||||
except Exception as e:
|
||||
print("等待加载中消失出错", e)
|
||||
|
||||
def clear_tab(self):
|
||||
for i in ["亚马逊","您的账户是否符合条件?"]:
|
||||
all_tab = self.browser.get_tabs(title=i)
|
||||
close_tab = []
|
||||
for tab in all_tab:
|
||||
tab_id = tab if isinstance(tab,str) else tab.tab_id
|
||||
if self.tab.tab_id == tab_id:
|
||||
continue
|
||||
close_tab.append(tab)
|
||||
print("需要关闭的标签页",close_tab)
|
||||
print("当前操作的tab_id",self.tab.tab_id)
|
||||
self.browser.close_tabs(close_tab)
|
||||
|
||||
def handle_repair_product(self,tab:ChromiumTab,headle_type=""):
|
||||
def wait_loaded(wait_type="contentWrapper"):
|
||||
# 等待加载完成
|
||||
print("------------【修复商品信息】等待加载完成-----------------")
|
||||
for _ in range(3):
|
||||
try:
|
||||
load_ele = tab.eles(
|
||||
'xpath://div[@class="contentWrapper"]//div[contains(@class,"==")]/span[not(node())]|//kat-spinner[@id="ahd-app-page-spinner"]',
|
||||
timeout=10)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=20, raise_err=False)
|
||||
if wait_type == "contentWrapper":
|
||||
contentWrapper = tab.eles('xpath://div[@class="contentWrapper"]//h4',timeout=20)
|
||||
if len(contentWrapper) > 0:
|
||||
contentWrapper[0].wait.displayed(timeout=20, raise_err=True)
|
||||
break
|
||||
else:
|
||||
time.sleep(2)
|
||||
break
|
||||
if wait_type == "content":
|
||||
contentWrapper = tab.eles('xpath://div[@class="contentWrapper"]',timeout=20)
|
||||
if len(contentWrapper) > 0:
|
||||
contentWrapper[0].wait.displayed(timeout=20, raise_err=True)
|
||||
break
|
||||
else:
|
||||
time.sleep(2)
|
||||
break
|
||||
if wait_type == "section":
|
||||
section = tab.eles('xpath://section[@class="ahd-product-policy__section"]',timeout=20)
|
||||
if len(section) > 0:
|
||||
section[0].wait.displayed(timeout=20, raise_err=True)
|
||||
break
|
||||
except Exception as e:
|
||||
print("等待加载中消失出错", e)
|
||||
print("-----------------------------")
|
||||
|
||||
def close_contentWrapper():
|
||||
# 关闭按钮
|
||||
close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5)
|
||||
contentWrapper = tab.eles('xpath://div[@class="flyoutPanelContent"]/div[@class="contentWrapper"]',timeout=5)
|
||||
if len(close_x_btn) > 0 and len(contentWrapper)>0 :
|
||||
if len(contentWrapper[0].children()) > 0:
|
||||
close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
|
||||
close_x_btn[0].click()
|
||||
|
||||
tab.wait.doc_loaded(timeout=60,raise_err=False)
|
||||
|
||||
wait_loaded(wait_type="section")
|
||||
# product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy"]')
|
||||
product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy" and @class="ahd-product-policy-table-row-wrapper"]|//div[@class="ahd-product-policy-table-row-wrapper-nested" and @data-testid="ahd-desktop-product-policy"]')
|
||||
if len(product_policy) == 0:
|
||||
print("未找到提交处理区域,可能页面未加载完成或者页面结构发生变化")
|
||||
time.sleep(5)
|
||||
# product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy"]')
|
||||
product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy" and @class="ahd-product-policy-table-row-wrapper"]|//div[@class="ahd-product-policy-table-row-wrapper-nested" and @data-testid="ahd-desktop-product-policy"]')
|
||||
|
||||
print("等待5秒后再次查找提交处理区域:",len(product_policy))
|
||||
|
||||
# submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
|
||||
# if len(submit_btn_ls) == 0:
|
||||
# return
|
||||
now_index = 0
|
||||
submit_index = 0
|
||||
for _ in range(3):
|
||||
try:
|
||||
# 先全部点开
|
||||
chevron_right_ls = tab.eles('xpath://kat-icon[@name="chevron-right"]',timeout=10)
|
||||
for ele in chevron_right_ls:
|
||||
ele.click()
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
print("【修复商品信息】全部点开失败",e)
|
||||
try:
|
||||
# todo 遍历
|
||||
print("【商品修复产品卡片】数量",len(product_policy))
|
||||
print("【商品修复产品卡片】列表",product_policy)
|
||||
for index in range(now_index,len(product_policy)):
|
||||
product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy" and @class="ahd-product-policy-table-row-wrapper"]|//div[@class="ahd-product-policy-table-row-wrapper-nested" and @data-testid="ahd-desktop-product-policy"]')
|
||||
now_index = index
|
||||
if index > len(product_policy)-1:
|
||||
print("提交按钮区域发生变化,停止处理")
|
||||
break
|
||||
message_link = product_policy[index].eles('xpath:.//a[@data-testid="ahd-reason-message-link"]')
|
||||
message = message_link[0].text
|
||||
|
||||
if headle_type in ['查看冻结原因']:
|
||||
need_heanle = any(i in message for i in ["制造商详细联系信息","警告和安全信息","负责人详细联系信息"])
|
||||
else:
|
||||
need_heanle = True
|
||||
|
||||
if not need_heanle:
|
||||
print(f"【{headle_type}】【{message}】不需要处理,跳过")
|
||||
continue
|
||||
|
||||
spread_btn = product_policy[index].eles('xpath:.//kat-icon[@name="chevron-right"]',timeout=5)
|
||||
print(f"{index},是否存在展开按钮",len(spread_btn))
|
||||
if len(spread_btn) > 0:
|
||||
spread_btn[0].click()
|
||||
time.sleep(0.5)
|
||||
|
||||
submit_btn_ls = product_policy[index].eles('xpath:.//kat-button[@label="提交"]',timeout=5)
|
||||
# try:
|
||||
# submit_btn = product_policy[index].ele('xpath:.//kat-button[@label="提交"]',timeout=5)
|
||||
# except Exception as e:
|
||||
# print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") ,"没有查找到提交按钮,跳过!")
|
||||
# close_contentWrapper()
|
||||
# continue
|
||||
# print("提交按钮数量:",len(submit_btn))
|
||||
print("提交按钮列表:",submit_btn_ls)
|
||||
if len(submit_btn_ls) > 0:
|
||||
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"),"查找到提交按钮")
|
||||
# submit_btn = submit_btn[0]
|
||||
submit_btn = tab.eles('xpath://kat-button[@label="提交"]',timeout=10)[submit_index]
|
||||
submit_index += 1
|
||||
else:
|
||||
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S") ,"没有查找到提交按钮,跳过!")
|
||||
close_contentWrapper()
|
||||
continue
|
||||
# time.sleep(10)
|
||||
# 关闭按钮
|
||||
close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5)
|
||||
contentWrapper = tab.eles('xpath://div[@class="flyoutPanelContent"]/div[@class="contentWrapper"]',timeout=5)
|
||||
if len(close_x_btn) > 0 and len(contentWrapper)>0 :
|
||||
if len(contentWrapper[0].children()) > 0:
|
||||
close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
|
||||
close_x_btn[0].click()
|
||||
|
||||
print("开始操作",submit_btn)
|
||||
print("操作的ID 为:",submit_btn.attr('id'))
|
||||
submit_btn.wait.enabled(timeout=5, raise_err=False)
|
||||
submit_btn.click()
|
||||
|
||||
# 先直接等待contentWrapper出现,不要进行复杂的元素查找
|
||||
try:
|
||||
# 使用简单的等待,避免触发页面事件
|
||||
for check_count in range(10):
|
||||
contentWrapper_exists = tab.eles('xpath://div[@class="contentWrapper"]', timeout=2)
|
||||
if len(contentWrapper_exists) > 0:
|
||||
print(f"【弹窗已出现】第{check_count+1}次检查")
|
||||
time.sleep(2) # 再等待2秒让内容完全加载
|
||||
break
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"等待弹窗出现异常: {e}")
|
||||
|
||||
# wait_loaded(wait_type="content")
|
||||
|
||||
for j in range(4):
|
||||
if j == 0:
|
||||
timeout = 5
|
||||
else:
|
||||
timeout = 10
|
||||
contentWrapper = tab.eles('xpath://div[@class="contentWrapper"]//h4',timeout=timeout)
|
||||
print("标题的数量",len(contentWrapper))
|
||||
if len(contentWrapper) > 0:
|
||||
contentWrapper[0].wait.displayed(timeout=20, raise_err=True)
|
||||
break
|
||||
else:
|
||||
# 关闭按钮
|
||||
close_contentWrapper()
|
||||
time.sleep(2)
|
||||
submit_btn.click()
|
||||
|
||||
|
||||
need_input = tab.eles('xpath://div[@class="contentWrapper"]//kat-input[@data-testid="kat-input-dew:ump_epr_resgitration_number_title"]',timeout=5)
|
||||
if len(need_input) > 0:
|
||||
print("需要输入注册号,不符合操作要求,跳过...")
|
||||
close_contentWrapper()
|
||||
continue
|
||||
|
||||
target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]',timeout=10)
|
||||
print(f"序号{index}【警告和安全信息】数量 ->",len(target_title))
|
||||
if len(target_title) > 0:
|
||||
is_Continue = False
|
||||
for j in range(10):
|
||||
not_start = tab.eles('xpath://div[@class="contentWrapper"]//div[@aria-label="安全证明"]//kat-label',timeout=2)
|
||||
if len(not_start) > 0:
|
||||
status_lebel = not_start[0].attr("text")
|
||||
print(f"序号{index}【警告和安全信息】状态 ->",status_lebel)
|
||||
if status_lebel and str(status_lebel).strip() != "" and status_lebel != "未开始":
|
||||
print("安全证明-不是未开始状态,不需要操作")
|
||||
close_contentWrapper()
|
||||
continue
|
||||
if status_lebel == "未开始":
|
||||
is_Continue = True
|
||||
break
|
||||
else:
|
||||
time.sleep(1)
|
||||
# not_start = tab.eles('xpath://div[@class="contentWrapper"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]',timeout=10)
|
||||
# if len(not_start) == 0:
|
||||
# print("安全证明-不是未开始状态,不需要操作")
|
||||
# close_contentWrapper()
|
||||
# continue
|
||||
print(f"序号{index}【警告和安全信息】是否需要继续 ->",is_Continue)
|
||||
if not is_Continue:
|
||||
continue
|
||||
not_start = not_start[0]
|
||||
not_start.wait.displayed(timeout=5, raise_err=False)
|
||||
not_start.click()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
check = tab.ele('xpath://div[@class="contentWrapper"]//kat-checkbox[@name="value"]',timeout=10)
|
||||
check.wait.displayed(timeout=5, raise_err=False)
|
||||
check.click()
|
||||
|
||||
save_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]',timeout=10)
|
||||
save_btn.wait.enabled(timeout=5, raise_err=False)
|
||||
save_btn.click()
|
||||
|
||||
wait_loaded()
|
||||
|
||||
close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10)
|
||||
close_btn.wait.displayed(timeout=5, raise_err=False)
|
||||
close_btn.click()
|
||||
print(f"序号{index}【警告和安全信息】操作完成")
|
||||
continue
|
||||
|
||||
save_btn_ls = tab.eles('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]',timeout=10)
|
||||
print(f"序号{index}【选择保存】是否存在->",len(save_btn_ls))
|
||||
if len(save_btn_ls) > 0:
|
||||
option_selection_ls = tab.eles('xpath://div[@class="contentWrapper"]//div[@role="option"]',timeout=5)
|
||||
for option in option_selection_ls:
|
||||
option.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
save_btn_ls[0].wait.enabled(timeout=5, raise_err=False)
|
||||
save_btn_ls[0].click()
|
||||
|
||||
wait_loaded()
|
||||
close_contentWrapper()
|
||||
print(f"序号{index}【选择保存】操作完成")
|
||||
continue
|
||||
|
||||
|
||||
submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
|
||||
if len(submit_btn_ls) > 0:
|
||||
submit_btn_ls[0].click()
|
||||
submit_btn_ls[0].wait.deleted(timeout=5, raise_err=False)
|
||||
wait_loaded()
|
||||
|
||||
close_contentWrapper()
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"【handle_repair_product】处理修复商品信息异常", traceback.format_exc())
|
||||
tab.refresh()
|
||||
tab.wait.doc_loaded(timeout=60,raise_err=False)
|
||||
time.sleep(2)
|
||||
wait_loaded(wait_type="section")
|
||||
# product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy"]')
|
||||
product_policy = tab.eles('xpath://div[@data-testid="ahd-desktop-product-policy" and @class="ahd-product-policy-table-row-wrapper"]|//div[@class="ahd-product-policy-table-row-wrapper-nested" and @data-testid="ahd-desktop-product-policy"]')
|
||||
|
||||
|
||||
print("修复商品信息处理完成!")
|
||||
tab.close()
|
||||
|
||||
def run_page_action(self):
|
||||
self.log(f"==================={self.mark_name}=======================")
|
||||
self.log("开始执行...")
|
||||
num = 0
|
||||
retry_num = 0
|
||||
max_retry_num = 5
|
||||
total_page = 0
|
||||
current_page = 0
|
||||
while retry_num < max_retry_num: # 最多重试3次
|
||||
# if num > 3: #测试
|
||||
# return
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
|
||||
time.sleep(0.5)
|
||||
# 获取当前页码
|
||||
try:
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) > 0:
|
||||
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
|
||||
current_page = int(current_page.strip())
|
||||
|
||||
# 总页数
|
||||
total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
|
||||
if len(total_page) > 0:
|
||||
total_page = int(total_page[-1].text.strip())
|
||||
|
||||
self.log(f"当前页码: {current_page} / 总页数: {total_page}")
|
||||
except Exception as e:
|
||||
self.log(f"获取页码失败:{e}")
|
||||
|
||||
# 保存当前URL,用于失败重试时访问
|
||||
try:
|
||||
self.url = self.tab.url
|
||||
self.log(f"获取当前的链接:{self.url}")
|
||||
except Exception as e:
|
||||
self.url = None
|
||||
self.log(f"获取当前的链接失败:{e}")
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
||||
self.log(f"获取到 {len(sku_ls)}")
|
||||
|
||||
if len(sku_ls) == 0:
|
||||
retry_num += 1
|
||||
print(f"没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}")
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False, timeout=120)
|
||||
if retry_num == max_retry_num:
|
||||
raise RuntimeError(f"与页面的连接已断开,没有获取到 ASIN 商品,重试{retry_num}/{max_retry_num}")
|
||||
continue
|
||||
# for sku_ele in sku_ls[0:2]:
|
||||
for sku_ele in sku_ls:
|
||||
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')
|
||||
asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]').text
|
||||
self.log(f"*************** {asin} *****************")
|
||||
self.log(f"ASIN {asin} 存在问题,正在点击解决...")
|
||||
try:
|
||||
if asin in self.already_asin:
|
||||
self.log(f"{asin} 已经处理过了,跳过")
|
||||
continue
|
||||
xp_ls = [f'.//kat-link[@label="{i}"]' for i in ["解决商品信息问题","修复被禁止显示的商品","解决商品移除风险","查看冻结原因"]]
|
||||
xp = "|".join(xp_ls)
|
||||
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]')
|
||||
solve_problem = sku_ele.eles(f'xpath:{xp}')
|
||||
if len(solve_problem) == 0:
|
||||
# //a[text0=”添加缺失的商品详情”]
|
||||
lack_info = sku_ele.eles('xpath:.//a[text()="添加缺失的商品详情"]')
|
||||
if len(lack_info) > 0:
|
||||
self.log(f"{asin} 需要添加缺失的商品详情,跳过...")
|
||||
yield (asin,"添加缺失的商品详情")
|
||||
continue
|
||||
|
||||
self.log(f"{asin},没有处理入口,不处理")
|
||||
yield (asin,"没有处理入口,不处理")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
|
||||
solve_problem[0].click()
|
||||
|
||||
label = solve_problem[0].attr('label')
|
||||
self.log(f"【当前类别】{label}")
|
||||
|
||||
action_panel = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]')
|
||||
action_panel.wait.displayed(timeout=5, raise_err=False)
|
||||
|
||||
self.wait_loaded()
|
||||
time.sleep(1)
|
||||
|
||||
# 如果存在“请求批准”按钮,则直接返回跳过
|
||||
request_approval_btn = self.tab.eles('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="section-header" and contains(string(.),"移除")]',timeout=5)
|
||||
if len(request_approval_btn) > 0:
|
||||
# 新增"查看冻结原因" 的情况下,需要点击修复商品详情
|
||||
if "查看冻结原因" in label:
|
||||
repair_produ_btn_xp = '//kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//kat-button[@variant="secondary" and @class="action-button" and contains(string(.),"修复商品信息")]'
|
||||
repair_produ_btn = self.tab.eles(f'xpath:{repair_produ_btn_xp}',timeout=5)
|
||||
self.log(f"{asin}【查看冻结原因】修复商品信息数量,{len(repair_produ_btn)}")
|
||||
if len(repair_produ_btn) > 0:
|
||||
self.log(f"{asin}【查看冻结原因】修复商品信息数量,开始操作")
|
||||
self.clear_tab()
|
||||
repair_produ_btn[0].click()
|
||||
time.sleep(3)
|
||||
# 获取最新的tab
|
||||
new_tab = self.browser.latest_tab
|
||||
if isinstance(new_tab, str):
|
||||
new_tab = self.browser.get_tab(id_or_num=new_tab)
|
||||
|
||||
self.handle_repair_product(new_tab,headle_type="查看冻结原因")
|
||||
else:
|
||||
self.log(f"{i}【查看冻结原因】修复商品信息没有,不需要操作")
|
||||
else:
|
||||
self.log(f"ASIN {asin} 存在请求批准按钮,不需要处理,跳过...")
|
||||
panel_close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
|
||||
timeout=5).sr('xpath:.//button[@class="close"]')
|
||||
panel_close_btn.click()
|
||||
|
||||
action_panel.wait.deleted(timeout=3, raise_err=False)
|
||||
yield (asin,"请求批准")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
|
||||
# 无需采取任何操作
|
||||
not_operate = self.tab.eles('xpath://kat-alert[contains(@description,"如果您之前已提交更改,则这些更改当前正在处理中") and not(@dismissed)]',timeout=3)
|
||||
if len(not_operate) > 0:
|
||||
self.log(f"ASIN {asin} 无需采取任何操作,跳过...")
|
||||
yield (asin,"无需操作")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
|
||||
# 解决商品信息违规问题 按钮
|
||||
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]',timeout=5)
|
||||
# if len(kat_box_ls) == 0:
|
||||
# print(f"ASIN {asin} 的操作面板中未找到选项,跳过...")
|
||||
# continue
|
||||
self.log(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.")
|
||||
# 解决商品信息违规问题 按钮,有多少个都要处理
|
||||
for i in range(len(kat_box_ls)):
|
||||
self.log(f"开始处理第{i}个问题")
|
||||
if len(kat_box_ls) <= i:
|
||||
self.log(f"ASIN {asin} 的操作面板中选项数量发生变化,停止处理...")
|
||||
break
|
||||
kat_box_ls[i].click()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
|
||||
# TODO 增加 “出现警告和安全信息时候下滑” 的情况
|
||||
safe_handle_ls = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]',
|
||||
timeout=15)
|
||||
if len(safe_handle_ls) > 0:
|
||||
self.log("存在安全证明,正在处理...")
|
||||
safe_handle_ls[0].click()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
check_box = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-checkbox')
|
||||
check_box.click()
|
||||
else:
|
||||
problem_selection_ls = self.tab.eles(
|
||||
'xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="registry-list"]//div[@data-testid="registry"]',
|
||||
timeout=10)
|
||||
self.log(f"待选择数量,{len(problem_selection_ls)}")
|
||||
for problem_selection in problem_selection_ls:
|
||||
problem_selection.click()
|
||||
print("点击选择完成")
|
||||
time.sleep(0.5)
|
||||
|
||||
# 点击保存
|
||||
confirm_btn = self.tab.eles('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//kat-button[@variant="primary"]')
|
||||
if len(confirm_btn) > 0:
|
||||
confirm_btn[0].wait.displayed(timeout=3, raise_err=False)
|
||||
confirm_btn[0].click()
|
||||
|
||||
self.wait_loaded()
|
||||
|
||||
# 等待关闭按钮出现
|
||||
try:
|
||||
close_btn = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-button[@label="关闭"]',
|
||||
timeout=10)
|
||||
if len(close_btn)>0:
|
||||
close_btn[0].click()
|
||||
except Exception as e:
|
||||
self.log(f"点击关闭按钮失败,{e}")
|
||||
|
||||
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]', timeout=5)
|
||||
|
||||
# 修复商品信息
|
||||
repair_product = action_panel.eles('xpath:.//kat-button[@class="action-button" and not(@variant="secondary")]',timeout=5)
|
||||
if len(repair_product) > 0:
|
||||
self.log(f"ASIN {asin} 存在修复商品信息按钮,正在点击...")
|
||||
self.clear_tab()
|
||||
repair_product[0].click()
|
||||
time.sleep(3)
|
||||
# 获取最新的tab
|
||||
new_tab = self.browser.latest_tab
|
||||
if isinstance(new_tab, str):
|
||||
new_tab = self.browser.get_tab(id_or_num=new_tab)
|
||||
|
||||
self.handle_repair_product(new_tab)
|
||||
|
||||
yield (asin,"处理完成")
|
||||
self.already_asin.add(asin)
|
||||
self.log(f"*********************{asin} 处理完成*************************")
|
||||
|
||||
try:
|
||||
# 全部操作完成,关闭
|
||||
panel_close_btn= self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
|
||||
timeout=5).sr('xpath:.//button[@class="close"]')
|
||||
panel_close_btn.click()
|
||||
action_panel.wait.deleted(timeout=3, raise_err=False)
|
||||
self.clear_tab()
|
||||
except Exception as e:
|
||||
self.log(f"关闭操作面板失败,{e}")
|
||||
|
||||
except Exception as e:
|
||||
yield (asin, "处理失败")
|
||||
self.already_asin.add(asin)
|
||||
|
||||
self.log(f"{asin} 操作出错:{traceback.format_exc()}")
|
||||
|
||||
# 判断是否存在需要翻页的情况
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) == 0:
|
||||
break
|
||||
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
|
||||
class_str = next_page_btn.attr('class')
|
||||
if "end" in class_str:
|
||||
break
|
||||
next_page_btn.click()
|
||||
num += 1
|
||||
self.log(f"【程序计算】正在翻页,已翻 {num} 页...")
|
||||
|
||||
# already_asin = set()
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"处理审批操作异常,{e}")
|
||||
traceback.print_exc()
|
||||
retry_num += 1
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False,timeout=120)
|
||||
|
||||
#检查页数是否相等,不相等则继续
|
||||
self.log(f"开始检查页数,当前页数 {current_page} / {total_page}")
|
||||
if total_page != 0 and current_page!= 0 and current_page < total_page:
|
||||
self.log(f"检查到页数还未完成,重启浏览器继续")
|
||||
raise RuntimeError(f"与页面的连接已断开,检查到页数还未完成,重启浏览器继续")
|
||||
|
||||
|
||||
|
||||
class ApproveTask(TaskBase):
|
||||
"""审批任务处理类:负责处理产品风险审批任务"""
|
||||
task_name = "产品风险审批-TASK"
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""处理审批任务主入口
|
||||
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])
|
||||
country_codes = data.get("country_codes", [])
|
||||
risk_listing_filter = data.get("risk_listing_filter", "")
|
||||
# 测试
|
||||
# risk_listing_filter = "DetailPageRemoved" #data.get("risk_listing_filter", "")
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
if not items:
|
||||
self.log("店铺列表为空,跳过", "WARNING")
|
||||
return
|
||||
if not country_codes:
|
||||
self.log("国家列表为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家")
|
||||
|
||||
runing_task[task_id] = {
|
||||
"status": "running",
|
||||
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_shops": len(items),
|
||||
"processed_shops": 0,
|
||||
"total_countries": len(country_codes) * len(items),
|
||||
"processed_countries": 0,
|
||||
"total_asins": 0,
|
||||
"processed_asins": 0,
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"stop_requested": False
|
||||
}
|
||||
|
||||
# 遍历处理每个店铺
|
||||
for idx, shop_item in enumerate(items, 1):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
return
|
||||
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
show_notification(f"开始处理店铺: {shop_name}", "info")
|
||||
|
||||
try:
|
||||
self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
|
||||
"""处理单个店铺
|
||||
|
||||
Args:
|
||||
shop_item: 店铺信息
|
||||
country_codes: 国家代码列表
|
||||
task_id: 任务ID
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
company_name = shop_item.get("companyName", "")
|
||||
|
||||
if not company_name:
|
||||
self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_shop"] = shop_name
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"店铺 {shop_name} 已添加到执行列表,账号: {company_name},开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
max_retries = 3
|
||||
iskill = False
|
||||
|
||||
# 处理每个国家
|
||||
for country_code in country_codes:
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "ERROR")
|
||||
driver.close_store()
|
||||
break
|
||||
|
||||
# 打开店铺
|
||||
driver = self.open_shop(cls=AmzoneApprove,max_retries=max_retries, company_name=company_name,
|
||||
shop_name=shop_name, iskill=iskill)
|
||||
|
||||
if driver is None:
|
||||
self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR")
|
||||
for country_code in country_codes:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
return
|
||||
|
||||
current_url = None
|
||||
max_retries = 200 #只要没有完成,一直重试
|
||||
for _ in range(max_retries):
|
||||
try:
|
||||
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,target_url=current_url)
|
||||
driver.reset_already_asin()
|
||||
driver.close_store()
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
if "与页面的连接已断开" in str(e):
|
||||
iskill = True
|
||||
current_url = driver.url
|
||||
|
||||
# 更新已处理国家数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
|
||||
self.log(f"{task_id}任务处理完成")
|
||||
# 最后回传,标记完成
|
||||
try:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
driver.close_store() #完成退出店铺
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver: AmzoneApprove, country_code: str, task_id: int,
|
||||
shop_name: str, risk_listing_filter: str,target_url:str=None):
|
||||
"""
|
||||
处理单个国家的审批任务
|
||||
"""
|
||||
|
||||
# 转换国家代码为中文名称
|
||||
country_name = self.country_info.get(country_code, country_code)
|
||||
info_mes = f"开始处理国家: {country_name} ({country_code})"
|
||||
self.log(info_mes)
|
||||
show_notification(info_mes, "info")
|
||||
|
||||
# 更新当前处理的国家
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_country"] = country_name
|
||||
|
||||
# 切换国家,最多重试3次
|
||||
max_retries = 5
|
||||
switch_success, switch_success_pg, search_success,sku_ls = self.action_init(driver, country_name, shop_name,
|
||||
risk_listing_filter)
|
||||
# 如果切换失败,直接返回
|
||||
if not switch_success or not switch_success_pg or not search_success:
|
||||
error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家"
|
||||
self.log(error_message, "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
# # 如果没有需要审批的商品,直接返回
|
||||
if len(sku_ls) == 0:
|
||||
self.log(f"国家 {country_name} 没有需要审批的商品")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
self.log(f"国家 {country_name} 有 {len(sku_ls)} 个需要审批的商品,开始处理...")
|
||||
|
||||
if target_url is not None: #从失败的链接继续
|
||||
self.log(f"开始访问:{target_url}")
|
||||
driver.tab.get(url=target_url)
|
||||
driver.tab.wait.doc_loaded(timeout=60,raise_err=False)
|
||||
|
||||
result = []
|
||||
# 处理所有需要审批的商品(通过yield获取结果)
|
||||
for asin, status in driver.run_page_action():
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理ASIN", "WARNING")
|
||||
break
|
||||
|
||||
self.log(f"ASIN {asin} 处理结果: {status}")
|
||||
|
||||
if status in ["请求批准", "添加缺失的商品详情"]:
|
||||
country_data = {
|
||||
"status": "",
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": "",
|
||||
"done": False,
|
||||
"removeAsin": asin,
|
||||
"removeStatus": status
|
||||
}
|
||||
else:
|
||||
country_data = {
|
||||
"status": status,
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": asin,
|
||||
"done": False,
|
||||
"removeAsin": "",
|
||||
"removeStatus": ""
|
||||
}
|
||||
result.append(country_data)
|
||||
|
||||
if len(result) > 10:
|
||||
self.post_result_batch(task_id, shop_name, country_code, result)
|
||||
result = []
|
||||
|
||||
if len(result) > 0:
|
||||
self.post_result_batch(task_id, shop_name, country_code, result)
|
||||
|
||||
self.log(f"国家 {country_name} 处理完成")
|
||||
|
||||
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
country_code: 国家代码
|
||||
asin: ASIN
|
||||
status: 处理状态
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result"
|
||||
if status in ["请求批准","添加缺失的商品详情"]:
|
||||
country_data = {
|
||||
"status": "",
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": "",
|
||||
"done": is_done,
|
||||
"removeAsin": asin,
|
||||
"removeStatus": status
|
||||
}
|
||||
else:
|
||||
country_data = {
|
||||
"status": status,
|
||||
"shopName": shop_name,
|
||||
"productAsinSku": asin,
|
||||
"done": is_done,
|
||||
"removeAsin": "",
|
||||
"removeStatus": ""
|
||||
}
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"error": "",
|
||||
"countries": {
|
||||
country_code : [ country_data ]
|
||||
},
|
||||
"shopName": shop_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log("=====================================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
self.log(f"结果回传成功: {asin} - {status}")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
|
||||
def post_result_batch(self, task_id: int, shop_name: str, country_code: str, country_data_ls:list):
|
||||
"""
|
||||
批量回传处理结果到API
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result"
|
||||
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"error": "",
|
||||
"countries": {
|
||||
country_code: country_data_ls
|
||||
},
|
||||
"shopName": shop_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log("=====================================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
|
||||
if response.status_code == 200:
|
||||
self.log(f"结果回传成功")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
user_info = {
|
||||
"company": "rongchuang123",
|
||||
"username": "自动化_Robot",
|
||||
"password": "#20zsg25"
|
||||
}
|
||||
shop_name = "郭亚芳"
|
||||
country = "德国"
|
||||
kill_process('v6')
|
||||
driver = AmzoneApprove(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
sw_suc = driver.SwitchingCountries(country)
|
||||
driver.SwitchPage()
|
||||
risk_listing_filter = "DetailPageRemoved"
|
||||
for _ in range(3):
|
||||
try:
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
driver.tab.refresh()
|
||||
if len(sku_ls) > 0:
|
||||
print("有数据,开始操作")
|
||||
for asin, status in driver.run_page_action():
|
||||
print(f"ASIN {asin} 的处理结果: {status}")
|
||||
print("已完成操作")
|
||||
|
||||
# for i in range(0):
|
||||
# print(i)
|
||||
562
app/amazon/asin_status.py
Normal file
562
app/amazon/asin_status.py
Normal file
@@ -0,0 +1,562 @@
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
from amazon.amazon_base import AmamzonBase, kill_process,TaskBase
|
||||
from amazon.tool import show_notification
|
||||
|
||||
from config import runing_task, runing_shop, DELETE_BRAND_API_BASE
|
||||
|
||||
|
||||
class AmzonePStatus(AmamzonBase):
|
||||
mark_name = "状态查询"
|
||||
|
||||
def search_asin(self, asin):
|
||||
search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group')
|
||||
search_region.wait.displayed(raise_err=False)
|
||||
time.sleep(0.6)
|
||||
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
|
||||
'xpath://span[@class="container"]//input[@part="input"]')
|
||||
search_input.input(asin, clear=True)
|
||||
sku_ls = []
|
||||
for _ in range(3):
|
||||
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
|
||||
search_btn.click()
|
||||
|
||||
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
|
||||
# load_ele.wait.hidden(timeout=3, raise_err=False)
|
||||
load_ele.wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]", timeout=3)
|
||||
if len(sku_ls) > 0:
|
||||
break
|
||||
return sku_ls
|
||||
|
||||
def search_asin_action(self, asin: str):
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]", timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
sku_ls = self.search_asin(asin=asin)
|
||||
print(f"【{self.mark_name}】{asin} 搜索到 {len(sku_ls)} 个SKU")
|
||||
|
||||
def clear_tab(self):
|
||||
all_tab = self.browser.get_tabs()
|
||||
close_tab = []
|
||||
for tab in all_tab:
|
||||
tab_id = tab if isinstance(tab, str) else tab.tab_id
|
||||
if self.tab.tab_id == tab_id:
|
||||
continue
|
||||
close_tab.append(tab)
|
||||
print("需要关闭的标签页", close_tab)
|
||||
print("当前操作的tab_id", self.tab.tab_id)
|
||||
self.browser.close_tabs(close_tab)
|
||||
|
||||
def run_page_action(self, appoint_asin: str = None):
|
||||
self.log(f"==================={self.mark_name}=======================")
|
||||
self.log("开始执行...")
|
||||
num = 0
|
||||
retry_num = 0
|
||||
already_asin = set()
|
||||
get_page_faild = 0
|
||||
|
||||
while retry_num < 3: # 最多重试3次
|
||||
try:
|
||||
print(self.mark_name, "指定asin操作", appoint_asin)
|
||||
self.search_asin_action(asin=appoint_asin)
|
||||
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]", timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 获取当前页码
|
||||
try:
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination', timeout=5)
|
||||
if len(page_pamel) > 0:
|
||||
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
|
||||
|
||||
# 总页数
|
||||
total_page = page_pamel[0].sr.eles(
|
||||
'xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
|
||||
if len(total_page) > 0:
|
||||
total_page = total_page[-1].text
|
||||
else:
|
||||
total_page = 0
|
||||
print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}")
|
||||
get_page_faild = 0
|
||||
except Exception as e:
|
||||
print("【{self.mark_name}】", "获取页码失败", e)
|
||||
get_page_faild += 1
|
||||
if get_page_faild > 2:
|
||||
show_notification(f"【{self.mark_name}】获取页码失败超3次停止任务!")
|
||||
break
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]", timeout=10)
|
||||
print(f"【{self.mark_name}】获取到 {len(sku_ls)}")
|
||||
# for sku_ele in sku_ls[0:2]:
|
||||
if len(sku_ls) == 0:
|
||||
yield (appoint_asin,"查询不到")
|
||||
break
|
||||
|
||||
for sku_ele in sku_ls:
|
||||
asin = sku_ele.ele(
|
||||
'xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]',
|
||||
timeout=3).text
|
||||
print(f"【{self.mark_name}】ASIN {asin} 找到....")
|
||||
if asin in already_asin:
|
||||
print(f"【{self.mark_name}】{asin} 已经处理过了,跳过")
|
||||
continue
|
||||
|
||||
# 状态
|
||||
status_ele = sku_ele.eles('xpath:.//div[contains(@class,"Status-module__container")]//kat-label')
|
||||
if len(status_ele) > 0:
|
||||
statu = status_ele[0].attr("emphasis")
|
||||
else:
|
||||
statu = "未获取到状态"
|
||||
|
||||
already_asin.add(asin)
|
||||
yield (asin,statu)
|
||||
print(f"【{self.mark_name}】",(asin,statu))
|
||||
|
||||
# 判断是否存在需要翻页的情况
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination', timeout=5)
|
||||
if len(page_pamel) == 0:
|
||||
break
|
||||
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
|
||||
class_str = next_page_btn.attr('class')
|
||||
if "end" in class_str:
|
||||
break
|
||||
next_page_btn.click()
|
||||
num += 1
|
||||
print(f"【{self.mark_name}】【程序计算】正在翻页,已翻 {num} 页...")
|
||||
already_asin = set()
|
||||
|
||||
except Exception as e:
|
||||
print(f"【{self.mark_name}】处理跟价操作异常", e)
|
||||
traceback.print_exc()
|
||||
retry_num += 1
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False, timeout=120)
|
||||
|
||||
|
||||
class StatusTask(TaskBase):
|
||||
task_name = "状态查询-TASK"
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""处理审批任务主入口
|
||||
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])[0]
|
||||
shop_name = items.get("shop_name")
|
||||
queryAsins = items.get("queryAsins", [])
|
||||
|
||||
user_id = data.get("user_id")
|
||||
stage_index = data.get("stage_index")
|
||||
final_stage = bool(data.get("final_stage", True))
|
||||
|
||||
# 用于测试
|
||||
limit = data.get("limit", None)
|
||||
|
||||
if not task_id or not queryAsins:
|
||||
self.log("任务ID / queryAsins列表 为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理审批任务 {task_id},共 1 个店铺,{len(queryAsins)} 个国家")
|
||||
|
||||
runing_task[task_id] = {
|
||||
"status": "running",
|
||||
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_shops": 1,
|
||||
"processed_shops": 0,
|
||||
"total_countries": len(queryAsins),
|
||||
"processed_countries": 0,
|
||||
"total_asins": 0,
|
||||
"processed_asins": 0,
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"stop_requested": False
|
||||
}
|
||||
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
return
|
||||
|
||||
self.log(f"开始处理店铺: {shop_name}")
|
||||
show_notification(f"开始处理店铺: {shop_name}", "info")
|
||||
|
||||
try:
|
||||
self.process_shop(items, queryAsins, task_id, user_id, stage_index,
|
||||
final_stage, limit=limit)
|
||||
|
||||
# self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def process_shop(self, shop_item: dict, country_codes: list, task_id: int,
|
||||
user_id=None, stage_index=None, final_stage: bool = True, limit: str = None):
|
||||
"""处理单个店铺
|
||||
|
||||
Args:
|
||||
shop_item: 店铺信息
|
||||
country_codes: 国家代码列表
|
||||
task_id: 任务ID
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
company_name = shop_item.get("companyName", "")
|
||||
|
||||
if not company_name:
|
||||
self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_shop"] = shop_name
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"店铺 {shop_name} 已添加到执行列表,账号: {company_name},开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
max_retries = 3
|
||||
iskill = False
|
||||
# 处理每个国家
|
||||
for value in country_codes:
|
||||
country_code = value.get("country")
|
||||
all_asin = value.get("asins")
|
||||
# 打开店铺
|
||||
driver = self.open_shop(cls=AmzonePStatus,max_retries=max_retries, company_name=company_name,
|
||||
shop_name=shop_name, iskill=iskill)
|
||||
if driver is None:
|
||||
self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR")
|
||||
for country_code in country_codes:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
return
|
||||
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "ERROR")
|
||||
driver.close_store()
|
||||
break
|
||||
try:
|
||||
self.process_country(driver, country_code, task_id, shop_name,all_asin=all_asin )
|
||||
except Exception as e:
|
||||
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
if "与页面的连接已断开" in str(e):
|
||||
iskill = True
|
||||
|
||||
# 更新已处理国家数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
|
||||
# 关闭店铺
|
||||
driver.close_store()
|
||||
|
||||
self.log(f"{task_id}任务处理完成")
|
||||
|
||||
# 最后回传,标记完成
|
||||
try:
|
||||
# self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
if final_stage:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
else:
|
||||
self.post_stage_finished(task_id, user_id, stage_index)
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver: AmzonePStatus, country_code: str, task_id: int, shop_name: str,
|
||||
all_asin:list,limit: str = None):
|
||||
"""处理单个国家的审批任务
|
||||
|
||||
Args:
|
||||
driver: AmzoneApprove驱动实例
|
||||
country_code: 国家代码(如 UK, DE, FR 等)
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
|
||||
# 转换国家代码为中文名称
|
||||
country_name = self.country_info.get(country_code, country_code)
|
||||
info_mes = f"开始处理国家: {country_name} ({country_code})"
|
||||
self.log(info_mes)
|
||||
show_notification(info_mes, "info")
|
||||
|
||||
# 更新当前处理的国家
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_country"] = country_name
|
||||
|
||||
# 切换国家,最多重试3次
|
||||
max_retries = 3
|
||||
switch_success = False
|
||||
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
if retry > 1:
|
||||
# 刷新不行就重新打开店铺
|
||||
self.log("重试前重新打开店铺...")
|
||||
try:
|
||||
driver.close_store()
|
||||
time.sleep(3)
|
||||
driver.open_shop(shop_name)
|
||||
except Exception as e:
|
||||
self.log(f"关闭重新打开店铺: {str(e)}", "WARNING")
|
||||
# 如果不是第一次尝试,先刷新页面
|
||||
if retry > 0:
|
||||
self.log("重试前刷新页面...")
|
||||
try:
|
||||
driver.tab.refresh()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
self.log(f"刷新页面失败: {str(e)}", "WARNING")
|
||||
|
||||
switch_success = driver.SwitchingCountries(country_name)
|
||||
if switch_success:
|
||||
self.log(f"成功切换到国家 {country_name}")
|
||||
break
|
||||
else:
|
||||
self.log(f"切换到国家 {country_name} 失败", "WARNING")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
# 如果切换失败,直接返回
|
||||
if not switch_success:
|
||||
error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家"
|
||||
self.log(error_message, "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
# 切换到库存管理页面
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
driver.SwitchPage()
|
||||
self.log(f"已切换到库存管理页面")
|
||||
except Exception as e:
|
||||
self.log(f"切换页面失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
if retry >= max_retries - 1:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
self.log(f"切换页面失败重试退出", "ERROR")
|
||||
return
|
||||
|
||||
# 处理所有需要审批的商品(通过yield获取结果)
|
||||
|
||||
# 指定 asin
|
||||
for ap_asin in all_asin:
|
||||
print(f"【{driver.mark_name}】开始处理 ->",ap_asin)
|
||||
for asin, status in driver.run_page_action(
|
||||
appoint_asin=ap_asin
|
||||
):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理ASIN", "WARNING")
|
||||
break
|
||||
|
||||
self.log(f"ASIN {asin} 处理结果: {status}")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_asin"] = asin
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
# 回传结果到API
|
||||
try:
|
||||
self.post_result(task_id, shop_name, country_code, asin, status)
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
self.log(f"国家 {country_name} 处理完成")
|
||||
|
||||
def post_stage_finished(self, task_id: int, user_id, stage_index):
|
||||
# import requests
|
||||
# from config import DELETE_BRAND_API_BASE
|
||||
|
||||
if user_id in (None, "", 0):
|
||||
raise ValueError("user_id is required for stage completion callback")
|
||||
if stage_index is None:
|
||||
raise ValueError("stage_index is required for stage completion callback")
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/stage-finished"
|
||||
payload = {"stage_index": stage_index}
|
||||
params = {"user_id": user_id}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"Attempting stage completion callback ({retry + 1}/{max_retries})")
|
||||
response = requests.post(
|
||||
url,
|
||||
params=params,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False,
|
||||
)
|
||||
self.log(f"Stage completion callback response: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"Stage completion callback succeeded: task={task_id}, stage={stage_index}")
|
||||
return
|
||||
self.log(f"Stage completion callback failed, status={response.status_code}", "WARNING")
|
||||
except Exception as e:
|
||||
self.log(f"Stage completion callback exception: {str(e)}", "ERROR")
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
raise RuntimeError(f"Stage completion callback failed after retries: task={task_id}, stage={stage_index}")
|
||||
|
||||
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: dict,
|
||||
is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
country_code: 国家代码
|
||||
asin: ASIN
|
||||
status: 处理状态
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/query-asin/tasks/{task_id}/result"
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"shopName": shop_name,
|
||||
"countryResults": [
|
||||
{
|
||||
"country": country_code,
|
||||
"items": [
|
||||
{
|
||||
"asin": asin,
|
||||
"status": status
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"shopDone": is_done,
|
||||
"submissionId": f"{int(time.time())}"
|
||||
}
|
||||
]
|
||||
}
|
||||
if is_done:
|
||||
payload["shops"][0]["success"] = is_done
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
print("================【跟价】=====================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"结果回传成功: {asin} - {status}")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 使用示例
|
||||
user_info = {
|
||||
"company": "rongchuang123",
|
||||
"username": "自动化_Robot",
|
||||
"password": "#20zsg25"
|
||||
}
|
||||
shop_name = "魏振峰"
|
||||
country = "德国"
|
||||
kill_process('v6')
|
||||
driver = AmzonePStatus(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
sw_suc = driver.SwitchingCountries(country)
|
||||
driver.SwitchPage()
|
||||
risk_listing_filter = "Active"
|
||||
_shopMallName = "WEIZHENFENG168"
|
||||
ap_asin = "B0F1N18XFW"
|
||||
skip_asin = []
|
||||
for _ in range(3):
|
||||
try:
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
driver.tab.refresh()
|
||||
if len(sku_ls) > 0:
|
||||
print("有数据,开始操作")
|
||||
for asin, status in driver.run_page_action(
|
||||
current_shop_name=_shopMallName,
|
||||
appoint_asin=ap_asin, skip_asin=skip_asin
|
||||
):
|
||||
print(f"ASIN {asin} 的处理结果: {status}")
|
||||
print("已完成操作")
|
||||
0
app/amazon/backend_api.py
Normal file
0
app/amazon/backend_api.py
Normal file
808
app/amazon/base.py
Normal file
808
app/amazon/base.py
Normal file
@@ -0,0 +1,808 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Literal, Optional, TypedDict
|
||||
|
||||
from loguru import logger
|
||||
import requests
|
||||
from DrissionPage import Chromium
|
||||
from DrissionPage._pages.chromium_tab import ChromiumTab
|
||||
from DrissionPage.common import By
|
||||
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
winreg = None
|
||||
|
||||
|
||||
STATUS_OK = "0"
|
||||
STATUS_LOGIN_FAILED = "-10003"
|
||||
|
||||
DEFAULT_SOCKET_PORT = 20000
|
||||
CLIENT_API_TIMEOUT = 120
|
||||
PORT_CHECK_TIMEOUT = 2
|
||||
UPDATE_CORE_RETRY_DELAY = 2
|
||||
CLIENT_RESTART_DELAY = 5
|
||||
CLIENT_START_RETRIES = 3
|
||||
CLIENT_READY_TIMEOUT = 10
|
||||
CLIENT_READY_INTERVAL = 0.5
|
||||
CLIENT_POST_START_DELAY = 5
|
||||
PROCESS_KILL_DELAY = 3
|
||||
ZINIAO_WEBDRIVER_LOG_TAIL_LINES = 80
|
||||
|
||||
DOC_LOAD_TIMEOUT = 30
|
||||
COUNTRY_INITIAL_LOAD_TIMEOUT = 120
|
||||
COUNTRY_LOOKUP_TIMEOUT = 20
|
||||
COUNTRY_CLICK_TIMEOUT = 10
|
||||
COUNTRY_DROPDOWN_DELAY = 1
|
||||
COUNTRY_DROPDOWN_READY_TIMEOUT = 20
|
||||
COUNTRY_DROPDOWN_RETRY_INTERVAL = 0.5
|
||||
IP_CHECK_TIMEOUT = 60
|
||||
|
||||
LOGIN_ATTEMPTS = 4
|
||||
PASSWORD_INPUT_TIMEOUT = 5
|
||||
PASSWORD_SUBMIT_TIMEOUT = 5
|
||||
OTP_SEND_TIMEOUT = 5
|
||||
OTP_INPUT_LOOKUP_ATTEMPTS = 5
|
||||
OTP_INPUT_TIMEOUT = 30
|
||||
OTP_INPUT_DISPLAY_TIMEOUT = 10
|
||||
OTP_CODE_WAIT_SECONDS = 30
|
||||
OTP_SUBMIT_TIMEOUT = 10
|
||||
OTP_RESULT_LOAD_TIMEOUT = 20
|
||||
OTP_ERROR_TIMEOUT = 10
|
||||
OTP_SEND_DELAY = 1
|
||||
LOGIN_FINAL_SUBMIT_TIMEOUT = 10
|
||||
NEED_LOGIN_DELAY = 3
|
||||
NEED_LOGIN_TIMEOUT = 5
|
||||
|
||||
COUNTRY_LABEL_XPATH = 'xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]'
|
||||
COUNTRY_DROPDOWN_XPATH = 'xpath://div[@class="dropdown-account-switcher-header-label"]'
|
||||
COUNTRY_LIST_ITEM_XPATH = 'xpath://div[@class="dropdown-account-switcher-list-item"]'
|
||||
NEED_LOGIN_XPATH = 'xpath://h1[@class="a-spacing-small"]|//span[contains(text(),"登录")]'
|
||||
PASSWORD_INPUT_XPATH = 'xpath://input[@type="password"]'
|
||||
PASSWORD_SUBMIT_XPATH = 'xpath://input[@id="signInSubmit"]'
|
||||
OTP_SEND_XPATH = 'xpath://span[@id="auth-send-code" and contains(string(.),"发送一次性密码")]'
|
||||
OTP_INPUT_XPATH = 'xpath://input[@name="otpCode"]'
|
||||
OTP_SUBMIT_XPATH = 'xpath://input[@id="auth-signin-button"]'
|
||||
OTP_ERROR_XPATH = 'xpath://div[@id="auth-error-message-box"]'
|
||||
|
||||
|
||||
class UserInfo(TypedDict):
|
||||
username: str
|
||||
password: str
|
||||
company: str
|
||||
|
||||
|
||||
def kill_process(version: Literal["v5", "v6"]):
|
||||
"""结束指定版本的紫鸟客户端进程."""
|
||||
logger.info("准备杀紫鸟客户端进程,version={}", version)
|
||||
driver = ZiniaoDriver({})
|
||||
driver.kill_process(version)
|
||||
|
||||
|
||||
class ZiniaoDriver:
|
||||
"""封装紫鸟客户端启动,店铺浏览器生命周期和客户端 API 调用."""
|
||||
|
||||
def __init__(self, user_info: UserInfo, socket_port: int = DEFAULT_SOCKET_PORT):
|
||||
"""初始化紫鸟浏览器驱动.
|
||||
|
||||
Args:
|
||||
user_info: 紫鸟账号信息,包含 company, username, password.
|
||||
socket_port: 客户端 HTTP 通信端口.
|
||||
"""
|
||||
self.user_info = user_info
|
||||
self.socket_port = socket_port
|
||||
self.client_path = None
|
||||
self.browser = None
|
||||
self.tab: Optional[ChromiumTab] = None
|
||||
self.store_id = None
|
||||
|
||||
@property
|
||||
def client_url(self) -> str:
|
||||
return f"http://127.0.0.1:{self.socket_port}"
|
||||
|
||||
def _build_payload(self, action: str, extra: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
payload = {
|
||||
"action": action,
|
||||
"requestId": str(uuid.uuid4()),
|
||||
}
|
||||
if extra:
|
||||
payload.update(extra)
|
||||
payload.update(self.user_info)
|
||||
return payload
|
||||
|
||||
def _post_client(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
response = requests.post(
|
||||
self.client_url,
|
||||
json.dumps(payload).encode("utf-8"),
|
||||
timeout=CLIENT_API_TIMEOUT,
|
||||
)
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def _status_code(result: Optional[Dict[str, Any]]) -> Optional[str]:
|
||||
if result is None:
|
||||
return None
|
||||
return str(result.get("statusCode"))
|
||||
|
||||
@staticmethod
|
||||
def _result_text(result: Dict[str, Any]) -> str:
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@staticmethod
|
||||
def _find_store_oauth(shop_list: Optional[List[Dict[str, Any]]], shop_name: str):
|
||||
for shop in shop_list or []:
|
||||
if shop.get("browserName") == shop_name:
|
||||
return shop.get("browserOauth")
|
||||
return None
|
||||
|
||||
def _is_client_ready(self) -> bool:
|
||||
try:
|
||||
requests.get(self.client_url, timeout=PORT_CHECK_TIMEOUT)
|
||||
return True
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||
return False
|
||||
|
||||
def _wait_until_client_ready(self, timeout: float) -> bool:
|
||||
start_check_time = time.time()
|
||||
while time.time() - start_check_time < timeout:
|
||||
if self._is_client_ready():
|
||||
return True
|
||||
time.sleep(CLIENT_READY_INTERVAL)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _redact_ziniao_log_line(line: str) -> str:
|
||||
return re.sub(r'("password"\s*:\s*)"[^"]*"', r'\1"***"', line)
|
||||
|
||||
@staticmethod
|
||||
def _ziniao_webdriver_log_path() -> Optional[str]:
|
||||
appdata = os.getenv("APPDATA")
|
||||
if not appdata:
|
||||
return None
|
||||
filename = f"webdriver.{time.strftime('%Y%m%d')}.log"
|
||||
return os.path.join(
|
||||
appdata,
|
||||
"ziniaobrowser",
|
||||
"instances",
|
||||
"userdata1",
|
||||
"logs",
|
||||
"client",
|
||||
filename,
|
||||
)
|
||||
|
||||
def _log_ziniao_webdriver_tail(self) -> None:
|
||||
log_path = self._ziniao_webdriver_log_path()
|
||||
if not log_path:
|
||||
logger.warning("无法读取紫鸟 webdriver 日志: APPDATA 环境变量为空")
|
||||
return
|
||||
if not os.path.exists(log_path):
|
||||
logger.warning("紫鸟 webdriver 日志不存在:{}", log_path)
|
||||
return
|
||||
|
||||
try:
|
||||
with open(log_path, "r", encoding="utf-8", errors="replace") as log_file:
|
||||
lines = log_file.readlines()
|
||||
except OSError as exc:
|
||||
logger.warning("读取紫鸟 webdriver 日志失败:{} | {}", log_path, exc)
|
||||
return
|
||||
|
||||
tail_lines = lines[-ZINIAO_WEBDRIVER_LOG_TAIL_LINES:]
|
||||
tail_text = "".join(self._redact_ziniao_log_line(line) for line in tail_lines).strip()
|
||||
if tail_text:
|
||||
logger.error("紫鸟 webdriver 日志尾部({}):\n{}", log_path, tail_text)
|
||||
else:
|
||||
logger.warning("紫鸟 webdriver 日志为空:{}", log_path)
|
||||
|
||||
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
|
||||
"""从 Windows 注册表读取紫鸟客户端可执行文件路径.
|
||||
|
||||
Args:
|
||||
protocol_name: 注册表协议名称.
|
||||
|
||||
Returns:
|
||||
紫鸟浏览器可执行文件路径. 未找到时返回 None.
|
||||
"""
|
||||
if winreg is None:
|
||||
logger.error("当前系统不支持 winreg,无法从注册表获取紫鸟客户端路径")
|
||||
return None
|
||||
|
||||
key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command"
|
||||
for root_key, root_name in (
|
||||
(winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER"),
|
||||
(winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE"),
|
||||
):
|
||||
try:
|
||||
logger.info("正在从注册表读取紫鸟客户端路径:{}\\{}", root_name, key_path)
|
||||
key = winreg.OpenKey(root_key, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
if isinstance(command, str):
|
||||
sub = "ziniao.exe"
|
||||
exe_path = command[0 : command.find(sub) + len(sub) + 1]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
logger.info("已获取紫鸟客户端路径:{}", exe_path)
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
logger.warning("注册表中未找到紫鸟客户端路径:{}\\{}", root_name, key_path)
|
||||
|
||||
logger.error("未能获取紫鸟客户端路径,protocol_name={}", protocol_name)
|
||||
return None
|
||||
|
||||
def update_core(self):
|
||||
"""更新紫鸟浏览器内核.
|
||||
|
||||
打开店铺前调用,要求客户端版本 5.285.7 以上. 接口可能因 HTTP 超时
|
||||
返回未完成状态,因此会循环调用直到成功或判定客户端不支持.
|
||||
"""
|
||||
payload = self._build_payload("updateCore")
|
||||
|
||||
logger.info("开始更新紫鸟浏览器内核")
|
||||
while True:
|
||||
result = self._post_client(payload)
|
||||
logger.info("更新内核返回:{}", result)
|
||||
if self._handle_update_core_result(result):
|
||||
return
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
logger.info("等待更新内核完成:{}", self._result_text(result))
|
||||
time.sleep(UPDATE_CORE_RETRY_DELAY)
|
||||
|
||||
def _handle_update_core_result(self, result: Optional[Dict[str, Any]]) -> bool:
|
||||
if result is None:
|
||||
logger.info("等待客户端启动后继续更新内核")
|
||||
time.sleep(UPDATE_CORE_RETRY_DELAY)
|
||||
return False
|
||||
|
||||
status_code = self._status_code(result)
|
||||
if status_code is None or status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("当前紫鸟客户端版本不支持更新内核接口,请升级客户端")
|
||||
return True
|
||||
if status_code == STATUS_OK:
|
||||
logger.info("紫鸟浏览器内核更新完成")
|
||||
return True
|
||||
return False
|
||||
|
||||
def kill_process(self, version: Literal["v5", "v6"]):
|
||||
"""结束指定版本的紫鸟客户端进程.
|
||||
|
||||
Args:
|
||||
version: 客户端主版本.
|
||||
"""
|
||||
if version == "v5":
|
||||
process_name = "SuperBrowser.exe"
|
||||
logger.info("准备结束紫鸟 v5 starter.exe")
|
||||
self._kill_process_by_name("starter.exe")
|
||||
else:
|
||||
process_name = "ziniao.exe"
|
||||
|
||||
logger.info("准备结束紫鸟客户端进程:{}", process_name)
|
||||
self._kill_process_by_name(process_name)
|
||||
time.sleep(PROCESS_KILL_DELAY)
|
||||
|
||||
def _kill_process_by_name(self, process_name: str) -> None:
|
||||
result = subprocess.run(
|
||||
["taskkill", "/f", "/t", "/im", process_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
check=False,
|
||||
)
|
||||
output = (result.stdout or result.stderr or "").strip()
|
||||
if result.returncode == 0:
|
||||
if output:
|
||||
logger.info("结束进程返回:{}", output)
|
||||
return
|
||||
|
||||
if "没有找到进程" in output or "not found" in output.lower():
|
||||
logger.info("进程未运行,跳过结束:{}", process_name)
|
||||
return
|
||||
|
||||
logger.warning("结束进程失败:{} | {}", process_name, output or f"returncode={result.returncode}")
|
||||
|
||||
def get_browser_list(self) -> Optional[list[Dict[str, Any]]]:
|
||||
"""获取紫鸟浏览器店铺列表.
|
||||
|
||||
Returns:
|
||||
浏览器店铺列表.登录失败或请求失败时返回 None.
|
||||
"""
|
||||
payload = self._build_payload("getBrowserList")
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始获取紫鸟浏览器列表,requestId={}", request_id)
|
||||
result = self._post_client(payload)
|
||||
status_code = self._status_code(result)
|
||||
if status_code == STATUS_OK:
|
||||
browser_list = result.get("browserList")
|
||||
logger.info("获取紫鸟浏览器列表成功,数量={}", len(browser_list or []))
|
||||
return browser_list
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("获取紫鸟浏览器列表登录失败:{}", self._result_text(result))
|
||||
return None
|
||||
|
||||
logger.error("获取紫鸟浏览器列表失败:{}", self._result_text(result))
|
||||
return None
|
||||
|
||||
def open_store(
|
||||
self,
|
||||
store_info,
|
||||
isWebDriverReadOnlyMode=0,
|
||||
isprivacy=0,
|
||||
isHeadless=0,
|
||||
cookieTypeSave=0,
|
||||
jsInfo="",
|
||||
):
|
||||
"""启动紫鸟店铺浏览器.
|
||||
|
||||
Args:
|
||||
store_info: 店铺 browserId 或 browserOauth.
|
||||
isWebDriverReadOnlyMode: WebDriver 只读模式开关.
|
||||
isprivacy: 隐私模式开关.
|
||||
isHeadless: 无头模式开关.
|
||||
cookieTypeSave: Cookie 保存类型.
|
||||
jsInfo: 注入的 JS 信息.
|
||||
|
||||
Returns:
|
||||
紫鸟客户端返回的启动结果.
|
||||
"""
|
||||
payload = self._build_start_browser_payload(
|
||||
store_info=store_info,
|
||||
is_web_driver_read_only_mode=isWebDriverReadOnlyMode,
|
||||
is_privacy=isprivacy,
|
||||
is_headless=isHeadless,
|
||||
cookie_type_save=cookieTypeSave,
|
||||
js_info=jsInfo,
|
||||
)
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始打开紫鸟店铺,store_info={},requestId={}", store_info, request_id)
|
||||
result = self._post_client(payload)
|
||||
status_code = self._status_code(result)
|
||||
if status_code == STATUS_OK:
|
||||
logger.info("紫鸟店铺打开成功,debuggingPort={}", result.get("debuggingPort"))
|
||||
return result
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("打开紫鸟店铺登录失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"[open_store]登录失败 {self._result_text(result)}")
|
||||
|
||||
logger.error("打开紫鸟店铺失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"[open_store]失败 {self._result_text(result)} ")
|
||||
|
||||
def _build_start_browser_payload(
|
||||
self,
|
||||
*,
|
||||
store_info,
|
||||
is_web_driver_read_only_mode=0,
|
||||
is_privacy=0,
|
||||
is_headless=0,
|
||||
cookie_type_save=0,
|
||||
js_info="",
|
||||
) -> Dict[str, Any]:
|
||||
payload = self._build_payload(
|
||||
"startBrowser",
|
||||
{
|
||||
"isWaitPluginUpdate": 0,
|
||||
"isHeadless": is_headless,
|
||||
"isWebDriverReadOnlyMode": is_web_driver_read_only_mode,
|
||||
"cookieTypeLoad": 0,
|
||||
"cookieTypeSave": cookie_type_save,
|
||||
"runMode": "1",
|
||||
"isLoadUserPlugin": True,
|
||||
"pluginIdType": 1,
|
||||
"privacyMode": is_privacy,
|
||||
},
|
||||
)
|
||||
|
||||
if store_info.isdigit():
|
||||
payload["browserId"] = store_info
|
||||
else:
|
||||
payload["browserOauth"] = store_info
|
||||
|
||||
if len(str(js_info)) > 2:
|
||||
payload["injectJsInfo"] = json.dumps(js_info)
|
||||
|
||||
return payload
|
||||
|
||||
def get_browser(self, port) -> Chromium:
|
||||
"""连接 DrissionPage 浏览器实例.
|
||||
|
||||
Args:
|
||||
port: Chromium 调试端口.
|
||||
|
||||
Returns:
|
||||
DrissionPage Chromium 实例.
|
||||
"""
|
||||
logger.info("开始连接 DrissionPage 浏览器,port={}", port)
|
||||
browser = Chromium(port)
|
||||
logger.info("DrissionPage 浏览器连接完成,port={}", port)
|
||||
return browser
|
||||
|
||||
def start_client(self):
|
||||
"""启动紫鸟客户端并更新浏览器内核."""
|
||||
if self._is_client_ready():
|
||||
logger.info("端口 {} 已启动,跳过启动紫鸟客户端", self.socket_port)
|
||||
return
|
||||
|
||||
logger.info("端口 {} 未启动,开始启动紫鸟客户端", self.socket_port)
|
||||
self.kill_process("v6")
|
||||
time.sleep(CLIENT_RESTART_DELAY)
|
||||
client_path = self.get_zinaio_exe("superbrowserv6")
|
||||
if not client_path:
|
||||
raise RuntimeError("未找到紫鸟客户端路径,无法启动客户端")
|
||||
|
||||
self.client_path = client_path.strip('"')
|
||||
cmd = [
|
||||
self.client_path,
|
||||
"--run_type=web_driver",
|
||||
"--ipc_type=http",
|
||||
"--port=" + str(self.socket_port),
|
||||
]
|
||||
logger.info("紫鸟客户端启动命令:{}", " ".join(cmd))
|
||||
|
||||
for retry_count in range(CLIENT_START_RETRIES):
|
||||
logger.info("第 {}/{} 次尝试启动紫鸟客户端", retry_count + 1, CLIENT_START_RETRIES)
|
||||
process = subprocess.Popen(cmd)
|
||||
|
||||
if self._wait_until_client_ready(CLIENT_READY_TIMEOUT):
|
||||
logger.info("紫鸟客户端启动成功,第 {} 次尝试", retry_count + 1)
|
||||
time.sleep(CLIENT_POST_START_DELAY)
|
||||
self.update_core()
|
||||
return
|
||||
|
||||
logger.warning("第 {} 次尝试启动失败,10秒内未检测到客户端启动", retry_count + 1)
|
||||
if process.poll() is not None:
|
||||
logger.warning("紫鸟客户端进程已退出,returncode={}", process.returncode)
|
||||
self._log_ziniao_webdriver_tail()
|
||||
|
||||
logger.error("紫鸟客户端启动失败,已重试 {} 次", CLIENT_START_RETRIES)
|
||||
raise RuntimeError(f"客户端启动失败:重试 {CLIENT_START_RETRIES} 次后仍未成功启动")
|
||||
|
||||
def open_shop(self, shop_name: str):
|
||||
"""打开指定店铺并完成 IP 检测.
|
||||
|
||||
Args:
|
||||
shop_name: 店铺名称.
|
||||
|
||||
Returns:
|
||||
成功时返回浏览器实例.店铺不存在时返回错误信息.
|
||||
"""
|
||||
logger.info("开始打开店铺:{}", shop_name)
|
||||
self.start_client()
|
||||
|
||||
self.store_id = self._find_store_oauth(self.get_browser_list(), shop_name)
|
||||
if not self.store_id:
|
||||
logger.warning("店铺不存在:{}", shop_name)
|
||||
return "店铺不存在"
|
||||
|
||||
ret_json = self.open_store(self.store_id)
|
||||
self.store_id = ret_json.get("browserOauth")
|
||||
if self.store_id is None:
|
||||
self.store_id = ret_json.get("browserId")
|
||||
|
||||
self.browser = self.get_browser(ret_json.get("debuggingPort"))
|
||||
ip_check_url = ret_json.get("ipDetectionPage")
|
||||
if not ip_check_url:
|
||||
logger.error("ip检测页地址为空,准备关闭店铺:{}", shop_name)
|
||||
self.close_store(self.store_id)
|
||||
raise RuntimeError("没有IP检测地址,为了店铺安全不打开店铺")
|
||||
|
||||
if self.open_ip_check(self.browser, ip_check_url):
|
||||
logger.info("IP检测通过,打开店铺平台主页:{}", shop_name)
|
||||
self.open_launcher_page(ret_json.get("launcherPage"), self.browser)
|
||||
return self.browser
|
||||
|
||||
logger.error("IP检测不通过,停止打开店铺:{}", shop_name)
|
||||
raise RuntimeError("IP检测不通过,可能是因为网络环境变化导致的,为了店铺安全不打开店铺")
|
||||
|
||||
def close_store(self, browser_oauth=None):
|
||||
"""关闭紫鸟店铺浏览器.
|
||||
|
||||
Args:
|
||||
browser_oauth: 店铺 OAuth 标识. 未提供时使用当前打开的店铺.
|
||||
|
||||
Returns:
|
||||
紫鸟客户端返回的关闭结果.
|
||||
"""
|
||||
if browser_oauth is None:
|
||||
browser_oauth = self.store_id
|
||||
|
||||
payload = self._build_payload(
|
||||
"stopBrowser",
|
||||
{
|
||||
"duplicate": 0,
|
||||
"browserOauth": browser_oauth,
|
||||
},
|
||||
)
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始关闭紫鸟店铺,browserOauth={},requestId={}", browser_oauth, request_id)
|
||||
result = self._post_client(payload)
|
||||
status_code = self._status_code(result)
|
||||
if status_code == STATUS_OK:
|
||||
logger.info("紫鸟店铺关闭成功,browserOauth={}", browser_oauth)
|
||||
return result
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("关闭紫鸟店铺登录失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"【close_store】登录失败 {self._result_text(result)}")
|
||||
|
||||
logger.error("关闭紫鸟店铺失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"[close_store]失败: {self._result_text(result)} ")
|
||||
|
||||
def open_launcher_page(self, launcher_page: str, browser: Chromium = None):
|
||||
"""打开店铺平台启动页.
|
||||
|
||||
Args:
|
||||
launcher_page: 要打开的启动页 URL.
|
||||
browser: 浏览器实例. 未提供时使用当前浏览器实例.
|
||||
"""
|
||||
if browser is None:
|
||||
browser = self.browser
|
||||
|
||||
logger.info("打开店铺平台启动页:{}", launcher_page)
|
||||
tab = browser.new_tab(url=launcher_page)
|
||||
self.tab = tab
|
||||
return tab
|
||||
|
||||
def open_ip_check(self, browser: Chromium, ip_check_url: str):
|
||||
"""打开 IP 检测页并判断网络环境是否通过.
|
||||
|
||||
Args:
|
||||
browser: DrissionPage 浏览器会话.
|
||||
ip_check_url: IP 检测页地址.
|
||||
|
||||
Returns:
|
||||
IP 检测通过时返回 True,否则返回 False.
|
||||
"""
|
||||
try:
|
||||
logger.info("开始打开IP检测页:{}", ip_check_url)
|
||||
tab = browser.latest_tab
|
||||
tab.get(ip_check_url)
|
||||
success_button = tab.ele(
|
||||
(By.XPATH, '//button[contains(@class, "styles_btn--success")]'),
|
||||
timeout=IP_CHECK_TIMEOUT,
|
||||
)
|
||||
if success_button:
|
||||
logger.info("IP检测成功")
|
||||
return True
|
||||
|
||||
logger.warning("IP检测超时或未找到成功按钮")
|
||||
return False
|
||||
except Exception:
|
||||
logger.exception("IP检测异常")
|
||||
return False
|
||||
|
||||
|
||||
class AmamzonBase(ZiniaoDriver):
|
||||
"""亚马逊页面操作基类."""
|
||||
|
||||
def switch_to_country(self, country_name: str):
|
||||
"""切换亚马逊账号国家并验证结果.
|
||||
|
||||
流程包括读取当前国家,打开国家下拉框,选择目标国家并等待页面加载.
|
||||
|
||||
Args:
|
||||
country_name: 目标国家名称.
|
||||
|
||||
Returns:
|
||||
切换成功返回 True,否则返回 False.
|
||||
"""
|
||||
try:
|
||||
if self.browser is None:
|
||||
logger.warning("浏览器实例不存在,请先打开店铺")
|
||||
return False
|
||||
|
||||
self.tab.wait.doc_loaded(timeout=COUNTRY_INITIAL_LOAD_TIMEOUT, raise_err=False)
|
||||
current_country = self._get_current_country(timeout=COUNTRY_LOOKUP_TIMEOUT)
|
||||
if current_country is None:
|
||||
logger.warning("无法获取当前国家信息")
|
||||
return False
|
||||
if current_country == country_name:
|
||||
logger.info("当前已经是目标国家 {},无需切换", country_name)
|
||||
return True
|
||||
|
||||
if not self._open_country_dropdown():
|
||||
return False
|
||||
if not self._select_country(country_name):
|
||||
return False
|
||||
|
||||
logger.info("等待国家切换后页面加载")
|
||||
self.tab.wait.doc_loaded()
|
||||
return self._verify_country(country_name)
|
||||
|
||||
except Exception:
|
||||
logger.exception("切换国家时发生异常")
|
||||
return False
|
||||
|
||||
def _get_current_country(self, *, timeout: int) -> Optional[str]:
|
||||
logger.info("正在检查当前国家")
|
||||
current_country_ele = self.tab.ele(COUNTRY_LABEL_XPATH, timeout=timeout)
|
||||
if not current_country_ele:
|
||||
return None
|
||||
|
||||
current_country = current_country_ele.text.strip()
|
||||
logger.info("当前国家:{}", current_country)
|
||||
return current_country
|
||||
|
||||
def _open_country_dropdown(self) -> bool:
|
||||
logger.info("正在打开国家切换下拉框")
|
||||
dropdown_header = self.tab.ele(COUNTRY_DROPDOWN_XPATH, timeout=COUNTRY_CLICK_TIMEOUT)
|
||||
if not dropdown_header:
|
||||
logger.warning("找不到国家切换下拉框")
|
||||
return False
|
||||
|
||||
logger.info("正在展开国家列表")
|
||||
first_item = self._wait_country_list_item(dropdown_header)
|
||||
if first_item is None:
|
||||
logger.warning("找不到国家列表项")
|
||||
return False
|
||||
|
||||
first_item.click()
|
||||
time.sleep(COUNTRY_DROPDOWN_DELAY)
|
||||
return True
|
||||
|
||||
def _wait_country_list_item(self, dropdown_header):
|
||||
deadline = time.time() + COUNTRY_DROPDOWN_READY_TIMEOUT
|
||||
attempt = 0
|
||||
while time.time() < deadline:
|
||||
attempt += 1
|
||||
try:
|
||||
dropdown_header.click()
|
||||
except Exception as exc:
|
||||
logger.info("点击国家切换下拉框失败, attempt={}, error={}", attempt, exc)
|
||||
|
||||
time.sleep(COUNTRY_DROPDOWN_DELAY)
|
||||
first_item = self.tab.ele(COUNTRY_LIST_ITEM_XPATH, timeout=COUNTRY_DROPDOWN_RETRY_INTERVAL)
|
||||
if first_item:
|
||||
return first_item
|
||||
|
||||
self.tab.wait.doc_loaded(timeout=COUNTRY_CLICK_TIMEOUT, raise_err=False)
|
||||
time.sleep(COUNTRY_DROPDOWN_RETRY_INTERVAL)
|
||||
return None
|
||||
|
||||
def _select_country(self, country_name: str) -> bool:
|
||||
logger.info("正在切换到国家:{}", country_name)
|
||||
target_country = self.tab.ele(self._country_option_xpath(country_name), timeout=COUNTRY_CLICK_TIMEOUT)
|
||||
if not target_country:
|
||||
logger.warning("找不到目标国家:{}", country_name)
|
||||
return False
|
||||
target_country.click()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _country_option_xpath(country_name: str) -> str:
|
||||
return (
|
||||
"xpath://div[@class="
|
||||
'"dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" '
|
||||
f'and @title="{country_name}"]'
|
||||
)
|
||||
|
||||
def _verify_country(self, country_name: str) -> bool:
|
||||
new_country = self._get_current_country(timeout=COUNTRY_CLICK_TIMEOUT)
|
||||
if new_country is None:
|
||||
logger.warning("无法验证国家切换结果")
|
||||
return False
|
||||
if new_country == country_name:
|
||||
logger.info("国家切换成功:{}", new_country)
|
||||
return True
|
||||
|
||||
logger.warning("国家切换失败,当前国家:{},目标国家:{}", new_country, country_name)
|
||||
return False
|
||||
|
||||
def need_login(self):
|
||||
"""判断当前页面是否需要登录."""
|
||||
time.sleep(NEED_LOGIN_DELAY)
|
||||
self.tab.wait.doc_loaded(timeout=DOC_LOAD_TIMEOUT, raise_err=False)
|
||||
need_login_ele = self.tab.eles(NEED_LOGIN_XPATH, timeout=NEED_LOGIN_TIMEOUT)
|
||||
if len(need_login_ele) > 0:
|
||||
logger.info("检测到需要登录元素")
|
||||
return True
|
||||
logger.info("未检测到登录元素")
|
||||
return False
|
||||
|
||||
def login(self, password, username=""):
|
||||
"""登录当前页面,必要时等待并提交一次性验证码.
|
||||
|
||||
Args:
|
||||
password: 登录密码.
|
||||
username: 兼容保留参数,当前未使用.
|
||||
|
||||
Returns:
|
||||
登录成功返回 True,失败返回 False.
|
||||
"""
|
||||
try:
|
||||
self.tab.wait.doc_loaded(timeout=DOC_LOAD_TIMEOUT, raise_err=False)
|
||||
for _ in range(LOGIN_ATTEMPTS):
|
||||
self._submit_password_if_present(password)
|
||||
self.tab.wait.doc_loaded(timeout=DOC_LOAD_TIMEOUT, raise_err=False)
|
||||
|
||||
if self._send_otp_if_present():
|
||||
continue
|
||||
if self._submit_ready_otp():
|
||||
return True
|
||||
|
||||
self._click_final_login_submit_if_present()
|
||||
except Exception:
|
||||
logger.exception("登录过程中发生异常")
|
||||
return False
|
||||
|
||||
def _submit_password_if_present(self, password) -> None:
|
||||
pwd_input = self.tab.eles(PASSWORD_INPUT_XPATH, timeout=PASSWORD_INPUT_TIMEOUT)
|
||||
if len(pwd_input) > 0:
|
||||
logger.info("检测到密码输入框,准备输入密码")
|
||||
pwd_input[0].input(password, clear=True)
|
||||
|
||||
submit_btn = self.tab.eles(PASSWORD_SUBMIT_XPATH, timeout=PASSWORD_SUBMIT_TIMEOUT)
|
||||
if len(submit_btn) > 0:
|
||||
logger.info("点击登录提交按钮")
|
||||
submit_btn[0].click()
|
||||
|
||||
def _send_otp_if_present(self) -> bool:
|
||||
send_code = self.tab.eles(OTP_SEND_XPATH, timeout=OTP_SEND_TIMEOUT)
|
||||
logger.info("发送一次性密码元素数量:{}", len(send_code))
|
||||
if len(send_code) == 0:
|
||||
return False
|
||||
|
||||
logger.info("检测到发送一次性密码按钮,准备点击")
|
||||
send_code[0].click()
|
||||
time.sleep(OTP_SEND_DELAY)
|
||||
self.tab.wait.doc_loaded(timeout=DOC_LOAD_TIMEOUT, raise_err=False)
|
||||
return True
|
||||
|
||||
def _submit_ready_otp(self) -> bool:
|
||||
for _ in range(OTP_INPUT_LOOKUP_ATTEMPTS):
|
||||
otp_input = self._find_otp_input()
|
||||
if otp_input is None:
|
||||
return False
|
||||
if self._wait_for_otp_value(otp_input) and self._submit_otp_if_ready():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _find_otp_input(self):
|
||||
otp_code_input = self.tab.eles(OTP_INPUT_XPATH, timeout=OTP_INPUT_TIMEOUT)
|
||||
logger.info("验证码输入框数量:{}", len(otp_code_input))
|
||||
if len(otp_code_input) == 0:
|
||||
return None
|
||||
|
||||
otp_code_input[0].wait.displayed(timeout=OTP_INPUT_DISPLAY_TIMEOUT, raise_err=False)
|
||||
return otp_code_input[0]
|
||||
|
||||
@staticmethod
|
||||
def _has_otp_value(otp_input) -> bool:
|
||||
return otp_input.value is not None and otp_input.value.strip() != ""
|
||||
|
||||
def _wait_for_otp_value(self, otp_input) -> bool:
|
||||
for _ in range(OTP_CODE_WAIT_SECONDS):
|
||||
if self._has_otp_value(otp_input):
|
||||
logger.info("检测到验证码输入完成")
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def _submit_otp_if_ready(self) -> bool:
|
||||
submit_btn = self.tab.eles(OTP_SUBMIT_XPATH, timeout=OTP_SUBMIT_TIMEOUT)
|
||||
if len(submit_btn) == 0:
|
||||
return False
|
||||
|
||||
submit_btn[0].click()
|
||||
self.tab.wait.doc_loaded(timeout=OTP_RESULT_LOAD_TIMEOUT, raise_err=False)
|
||||
error_mes = self.tab.eles(OTP_ERROR_XPATH, timeout=OTP_ERROR_TIMEOUT)
|
||||
if len(error_mes) > 0:
|
||||
logger.warning("验证码输入错误:{}", error_mes[0].text)
|
||||
self.tab.refresh()
|
||||
return False
|
||||
|
||||
logger.info("登录成功")
|
||||
return True
|
||||
|
||||
def _click_final_login_submit_if_present(self) -> None:
|
||||
submit_btn = self.tab.eles(OTP_SUBMIT_XPATH, timeout=LOGIN_FINAL_SUBMIT_TIMEOUT)
|
||||
if len(submit_btn) > 0:
|
||||
logger.info("点击二次登录提交按钮")
|
||||
submit_btn[0].click()
|
||||
|
||||
206
app/amazon/chrome_base.py
Normal file
206
app/amazon/chrome_base.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from DrissionPage import Chromium, ChromiumOptions
|
||||
from collections import defaultdict
|
||||
|
||||
from config import base_dir,debug
|
||||
from amazon.tool import get_shop_info,show_notification
|
||||
|
||||
|
||||
|
||||
class ChromeAmzoneBase:
|
||||
mark_name = "谷歌浏览器基类"
|
||||
|
||||
country_info = {
|
||||
"英国": {
|
||||
"url": "https://www.amazon.co.uk/dp/B0CJ8SNXXV",
|
||||
"zip_code": "SW1A 1AA",
|
||||
"mark": "SW1A 1"
|
||||
},
|
||||
"德国": {
|
||||
"url": "https://www.amazon.de/dp/B0CC8CW9G2?th=1",
|
||||
"zip_code": "10115"
|
||||
},
|
||||
"法国": {
|
||||
"url": "https://www.amazon.fr/dp/B0FRG1MJ8H?th=1",
|
||||
"zip_code": "75001"
|
||||
},
|
||||
"西班牙": {
|
||||
"url": "https://www.amazon.es/dp/B08ZXVNYNN",
|
||||
"zip_code": "28001"
|
||||
},
|
||||
"意大利": {
|
||||
"url": "https://www.amazon.it/dp/B0D1P17T2Q",
|
||||
"zip_code": "20121"
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
杀死当前谷歌浏览器进程,并使用 drissionpage 启动谷歌浏览器,使用系统安装的浏览器默认用户文件夹
|
||||
"""
|
||||
print("正在关闭现有的chromium浏览器进程...")
|
||||
if not debug:
|
||||
os.system('taskkill /f /t /im chrome.exe')
|
||||
time.sleep(2)
|
||||
|
||||
print("正在启动chromium浏览器...")
|
||||
# 配置浏览器选项
|
||||
co = ChromiumOptions()
|
||||
co.no_imgs(True)
|
||||
user_data_path = os.path.join(base_dir, "user_data", "chrome_data")
|
||||
if not os.path.exists(user_data_path):
|
||||
os.makedirs(user_data_path, exist_ok=True)
|
||||
co.set_user_data_path(user_data_path)
|
||||
co.set_local_port(port=19897)
|
||||
self.browser = Chromium(co)
|
||||
self.tab = self.browser.latest_tab
|
||||
print("Chrome浏览器启动成功")
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""日志输出
|
||||
Args:
|
||||
message: 日志消息
|
||||
level: 日志级别
|
||||
"""
|
||||
try:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
# if level == "ERROR":
|
||||
# show_notification(message, "error")
|
||||
print(f"[{timestamp}] [{self.mark_name}] [{level}] {message}")
|
||||
except Exception as e:
|
||||
print(f"输出出错,{e}")
|
||||
|
||||
def close_init_popup(self):
|
||||
"""
|
||||
关闭所有的初始化弹窗
|
||||
"""
|
||||
self.tab.wait.doc_loaded(timeout=60, raise_err=True)
|
||||
continue_shipping = self.tab.eles('xpath://button[@alt="Continue shopping"]',timeout=3)
|
||||
if len(continue_shipping) > 0:
|
||||
continue_shipping[0].click()
|
||||
time.sleep(0.5)
|
||||
self.tab.wait.doc_loaded(timeout=60, raise_err=True)
|
||||
footbar = self.tab.eles('xpath://footer[@class="el-dialog__footer"]', timeout=5)
|
||||
if len(footbar) > 0:
|
||||
do_not_remind = footbar[0].eles('xpath:.//input[@class="el-checkbox__original"]')
|
||||
if len(do_not_remind) > 0:
|
||||
do_not_remind[0].check()
|
||||
resume_immediately = footbar[0].eles('xpath:.//button')
|
||||
if len(resume_immediately) > 0:
|
||||
resume_immediately[0].click()
|
||||
|
||||
self.tab.wait.doc_loaded(timeout=60, raise_err=True)
|
||||
accept_btn = self.tab.eles('xpath://input[@id="sp-cc-accept"]', timeout=5)
|
||||
if len(accept_btn) > 0:
|
||||
accept_btn[0].click()
|
||||
|
||||
def _set_zip_code(self, zip_code, mark=None):
|
||||
"""
|
||||
设置邮编
|
||||
|
||||
Args:
|
||||
zip_code: 目标邮编
|
||||
"""
|
||||
try:
|
||||
# 检查当前邮编
|
||||
zip_display = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10)
|
||||
|
||||
if zip_display:
|
||||
current_text = zip_display.text
|
||||
# print(f"当前地址信息: {current_text}")
|
||||
# 先检查标识
|
||||
if mark is not None and mark in current_text:
|
||||
self.log(f"邮编检测到标识: {mark},无需修改")
|
||||
return True
|
||||
# 检查是否已经包含目标邮编
|
||||
if zip_code in current_text:
|
||||
self.log(f"邮编已经设置为: {zip_code},无需修改")
|
||||
return True
|
||||
|
||||
# 需要设置邮编
|
||||
self.log(f"正在设置邮编为: {zip_code}")
|
||||
|
||||
# 点击地址选择按钮
|
||||
location_link = self.tab.ele('xpath://a[@id="nav-global-location-popover-link"]', timeout=10)
|
||||
if not location_link:
|
||||
self.log("找不到地址设置按钮")
|
||||
return False
|
||||
|
||||
location_link.click()
|
||||
time.sleep(1)
|
||||
|
||||
# 等待邮编输入框出现
|
||||
zip_input = self.tab.ele('xpath://input[@id="GLUXZipUpdateInput"]', timeout=10)
|
||||
if not zip_input:
|
||||
print("找不到邮编输入框")
|
||||
return False
|
||||
|
||||
# 输入邮编
|
||||
zip_input.input(zip_code, clear=True)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 点击提交按钮
|
||||
submit_btn = self.tab.ele('xpath://input[@aria-labelledby="GLUXZipUpdate-announce"]', timeout=10)
|
||||
if not submit_btn:
|
||||
print("找不到提交按钮")
|
||||
return False
|
||||
|
||||
submit_btn.click()
|
||||
|
||||
continue_btn = self.tab.eles('xpath://div[@class="a-popover-footer"]//input[@id="GLUXConfirmClose"]',
|
||||
timeout=10)
|
||||
if len(continue_btn) > 0:
|
||||
continue_btn[0].click()
|
||||
|
||||
# 等待提交按钮消失(表示请求已发送)
|
||||
print("等待邮编更新...")
|
||||
time.sleep(2)
|
||||
|
||||
# 等待页面加载完成
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
time.sleep(2)
|
||||
|
||||
# 验证邮编是否设置成功
|
||||
zip_display_after = self.tab.ele('xpath://div[@id="glow-ingress-block"]', timeout=10)
|
||||
if zip_display_after:
|
||||
updated_text = zip_display_after.text
|
||||
# print(f"更新后的地址信息: {updated_text}")
|
||||
|
||||
if zip_code in updated_text:
|
||||
print(f"邮编设置成功: {zip_code}")
|
||||
return True
|
||||
else:
|
||||
# print(f"邮编设置可能失败,当前显示: {updated_text}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"设置邮编时出错: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""关闭浏览器"""
|
||||
try:
|
||||
if self.browser:
|
||||
self.browser.quit()
|
||||
print("浏览器已关闭")
|
||||
except Exception as e:
|
||||
print(f"关闭浏览器时出错: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,432 +6,18 @@ import uuid
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from typing import Literal
|
||||
from DrissionPage import Chromium
|
||||
from DrissionPage.common import By
|
||||
from amazon.amazon_base import AmamzonBase,kill_process,TaskBase
|
||||
from amazon.tool import show_notification,get_shop_info
|
||||
|
||||
|
||||
def kill_process(version: Literal["v5", "v6"]):
|
||||
"""杀紫鸟客户端进程(独立函数版本)"""
|
||||
driver = ZiniaoDriver({})
|
||||
driver.kill_process(version)
|
||||
from config import runing_task,runing_shop,DELETE_BRAND_API_BASE
|
||||
|
||||
|
||||
class ZiniaoDriver:
|
||||
"""紫鸟浏览器自动化驱动类"""
|
||||
|
||||
def __init__(self, user_info: dict, socket_port: int = 19890):
|
||||
"""
|
||||
初始化紫鸟浏览器驱动
|
||||
|
||||
Args:
|
||||
user_info: 用户信息字典,包含 company, username, password
|
||||
socket_port: 客户端通信端口,默认 19890
|
||||
"""
|
||||
self.user_info = user_info
|
||||
self.socket_port = socket_port
|
||||
self.client_path = None
|
||||
self.browser = None
|
||||
self.tab = None
|
||||
self.store_id = None
|
||||
|
||||
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
|
||||
"""
|
||||
获取紫鸟安装目录
|
||||
|
||||
Args:
|
||||
protocol_name: 协议名称,默认 "superbrowser"
|
||||
|
||||
Returns:
|
||||
exe_path: 紫鸟浏览器可执行文件路径
|
||||
"""
|
||||
try:
|
||||
key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command"
|
||||
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
if isinstance(command, str):
|
||||
exe_path = command.split()[0]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
try:
|
||||
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
if isinstance(command, str):
|
||||
exe_path = command.split()[0]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
def update_core(self):
|
||||
"""
|
||||
下载所有内核,打开店铺前调用,需客户端版本5.285.7以上
|
||||
因为http有超时时间,所以这个action适合循环调用,直到返回成功
|
||||
"""
|
||||
data = {
|
||||
"action": "updateCore",
|
||||
"requestId": str(uuid.uuid4()),
|
||||
}
|
||||
data.update(self.user_info)
|
||||
while True:
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
result = response.json()
|
||||
print(result)
|
||||
if result is None:
|
||||
print("等待客户端启动...")
|
||||
time.sleep(2)
|
||||
continue
|
||||
if result.get("statusCode") is None or result.get("statusCode") == -10003:
|
||||
print("当前版本不支持此接口,请升级客户端")
|
||||
return
|
||||
elif result.get("statusCode") == 0:
|
||||
print("更新内核完成")
|
||||
return
|
||||
else:
|
||||
print(f"等待更新内核: {json.dumps(result)}")
|
||||
time.sleep(2)
|
||||
|
||||
def kill_process(self, version: Literal["v5", "v6"]):
|
||||
"""
|
||||
杀紫鸟客户端进程
|
||||
|
||||
Args:
|
||||
version: 客户端版本
|
||||
"""
|
||||
if version == "v5":
|
||||
process_name = 'SuperBrowser.exe'
|
||||
else:
|
||||
process_name = 'ziniao.exe'
|
||||
os.system('taskkill /f /t /im ' + process_name)
|
||||
time.sleep(3)
|
||||
|
||||
def get_browser_list(self) -> list:
|
||||
"""
|
||||
获取浏览器列表
|
||||
|
||||
Returns:
|
||||
list: 浏览器列表
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "getBrowserList",
|
||||
"requestId": request_id
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
print(r)
|
||||
return r.get("browserList")
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
print(f"login Err {json.dumps(r, ensure_ascii=False)}")
|
||||
exit()
|
||||
else:
|
||||
print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
|
||||
exit()
|
||||
|
||||
def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0,
|
||||
isHeadless=0, cookieTypeSave=0, jsInfo=""):
|
||||
"""
|
||||
打开店铺
|
||||
|
||||
Args:
|
||||
store_info: 店铺信息(browserId 或 browserOauth)
|
||||
isWebDriverReadOnlyMode: 是否只读模式,默认 0
|
||||
isprivacy: 隐私模式,默认 0
|
||||
isHeadless: 无头模式,默认 0
|
||||
cookieTypeSave: cookie保存类型,默认 0
|
||||
jsInfo: 注入的JS信息,默认 ""
|
||||
|
||||
Returns:
|
||||
dict: 返回结果
|
||||
"""
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "startBrowser",
|
||||
"isWaitPluginUpdate": 0,
|
||||
"isHeadless": isHeadless,
|
||||
"requestId": request_id,
|
||||
"isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
|
||||
"cookieTypeLoad": 0,
|
||||
"cookieTypeSave": cookieTypeSave,
|
||||
"runMode": "1",
|
||||
"isLoadUserPlugin": False,
|
||||
"pluginIdType": 1,
|
||||
"privacyMode": isprivacy
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
if store_info.isdigit():
|
||||
data["browserId"] = store_info
|
||||
else:
|
||||
data["browserOauth"] = store_info
|
||||
|
||||
if len(str(jsInfo)) > 2:
|
||||
data["injectJsInfo"] = json.dumps(jsInfo)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
return r
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
print(f"login Err {json.dumps(r, ensure_ascii=False)}")
|
||||
exit()
|
||||
else:
|
||||
print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
|
||||
exit()
|
||||
|
||||
def get_browser(self, port) -> Chromium:
|
||||
"""
|
||||
获取浏览器实例
|
||||
|
||||
Args:
|
||||
port: 调试端口
|
||||
|
||||
Returns:
|
||||
Chromium: DrissionPage浏览器实例
|
||||
"""
|
||||
browser = Chromium(port)
|
||||
return browser
|
||||
|
||||
def start_client(self):
|
||||
"""启动紫鸟客户端"""
|
||||
self.client_path = self.get_zinaio_exe("superbrowser").strip('"')
|
||||
print(self.client_path)
|
||||
cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http',
|
||||
'--port=' + str(self.socket_port)]
|
||||
print(" ".join(cmd))
|
||||
subprocess.Popen(cmd)
|
||||
time.sleep(5)
|
||||
|
||||
def open_shop(self, shop_name: str):
|
||||
"""
|
||||
打开指定店铺
|
||||
|
||||
Args:
|
||||
shop_name: 店铺名称
|
||||
|
||||
Returns:
|
||||
Chromium or str: 成功返回浏览器实例,失败返回错误信息
|
||||
"""
|
||||
# 启动客户端
|
||||
self.start_client()
|
||||
|
||||
# 更新内核
|
||||
self.update_core()
|
||||
|
||||
# 获取店铺列表
|
||||
shop_ls = self.get_browser_list()
|
||||
self.store_id = None
|
||||
# print(shop_ls)
|
||||
for shop in shop_ls:
|
||||
if shop.get("browserName") == shop_name:
|
||||
self.store_id = shop.get('browserOauth')
|
||||
break
|
||||
|
||||
if not self.store_id:
|
||||
print("店铺不存在")
|
||||
return "店铺不存在"
|
||||
|
||||
# 打开店铺
|
||||
ret_json = self.open_store(self.store_id)
|
||||
print(ret_json)
|
||||
self.store_id = ret_json.get("browserOauth")
|
||||
if self.store_id is None:
|
||||
self.store_id = ret_json.get("browserId")
|
||||
|
||||
# 获取drissionpage浏览器会话
|
||||
self.browser = self.get_browser(ret_json.get('debuggingPort'))
|
||||
|
||||
ip_check_url = ret_json.get("ipDetectionPage")
|
||||
if not ip_check_url:
|
||||
print("ip检测页地址为空,请升级紫鸟浏览器到最新版")
|
||||
print(f"=====关闭店铺:{shop_name}=====")
|
||||
self.close_store(self.store_id)
|
||||
exit()
|
||||
ip_usable = self.open_ip_check(self.browser, ip_check_url)
|
||||
if ip_usable:
|
||||
print("ip检测通过,打开店铺平台主页")
|
||||
self.open_launcher_page(ret_json.get("launcherPage"), self.browser)
|
||||
else:
|
||||
print("IP检测不通过")
|
||||
return self.browser
|
||||
|
||||
def close_store(self, browser_oauth=None):
|
||||
"""
|
||||
关闭店铺
|
||||
|
||||
Args:
|
||||
browser_oauth: 店铺OAuth标识,如果不提供则使用当前打开的店铺
|
||||
|
||||
Returns:
|
||||
dict: 返回结果
|
||||
"""
|
||||
if browser_oauth is None:
|
||||
browser_oauth = self.store_id
|
||||
|
||||
request_id = str(uuid.uuid4())
|
||||
data = {
|
||||
"action": "stopBrowser",
|
||||
"requestId": request_id,
|
||||
"duplicate": 0,
|
||||
"browserOauth": browser_oauth
|
||||
}
|
||||
data.update(self.user_info)
|
||||
|
||||
url = f'http://127.0.0.1:{self.socket_port}'
|
||||
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
|
||||
r = response.json()
|
||||
if str(r.get("statusCode")) == "0":
|
||||
return r
|
||||
elif str(r.get("statusCode")) == "-10003":
|
||||
print(f"login Err {json.dumps(r, ensure_ascii=False)}")
|
||||
exit()
|
||||
else:
|
||||
print(f"Fail {json.dumps(r, ensure_ascii=False)} ")
|
||||
exit()
|
||||
|
||||
def open_launcher_page(self, launcher_page: str, browser: Chromium = None):
|
||||
"""
|
||||
打开启动页面
|
||||
|
||||
Args:
|
||||
launcher_page: 要打开的页面URL
|
||||
browser: 浏览器实例,如果不提供则使用当前浏览器实例
|
||||
"""
|
||||
if browser is None:
|
||||
browser = self.browser
|
||||
|
||||
tab = browser.new_tab(url=launcher_page)
|
||||
self.tab = tab
|
||||
return tab
|
||||
|
||||
def open_ip_check(self, browser: Chromium, ip_check_url: str):
|
||||
"""
|
||||
打开ip检测页检测ip是否正常
|
||||
:param browser: drissionpage浏览器会话
|
||||
:param ip_check_url ip检测页地址
|
||||
:return 检测结果
|
||||
"""
|
||||
try:
|
||||
tab = browser.latest_tab
|
||||
tab.get(ip_check_url)
|
||||
success_button = tab.ele((By.XPATH, '//button[contains(@class, "styles_btn--success")]'),
|
||||
timeout=60) # 等待查找元素60秒
|
||||
if success_button:
|
||||
print("ip检测成功")
|
||||
return True
|
||||
else:
|
||||
print("ip检测超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
print("ip检测异常:" + traceback.format_exc())
|
||||
return False
|
||||
|
||||
|
||||
class AmazoneDriver(ZiniaoDriver):
|
||||
class AmazoneDriver(AmamzonBase):
|
||||
"""亚马逊专用驱动类,继承自ZiniaoDriver"""
|
||||
|
||||
def SwitchingCountries(self, country_name: str):
|
||||
"""
|
||||
切换国家
|
||||
操作:
|
||||
1、//div[@class="dropdown-account-switcher-header-label"]/span[last()] 获取此元素文本,判断当前国家,如果与目标国家相同则不操作,否则执行下一步
|
||||
2、点击 //div[@class="dropdown-account-switcher-header-label"] 打开下拉框
|
||||
3、点击 //div[@class="dropdown-account-switcher-list-item"] 第一个展开国家列表
|
||||
4、点击 //div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="国家名"] 切换到目标国家
|
||||
5、等待页面加载完成,判断国家是否切换成功,成功则返回True,否则返回False
|
||||
|
||||
Args:
|
||||
country_name: 目标国家名称
|
||||
|
||||
Returns:
|
||||
bool: 切换成功返回True,失败返回False
|
||||
"""
|
||||
try:
|
||||
if self.browser is None:
|
||||
print("浏览器实例不存在,请先打开店铺")
|
||||
return False
|
||||
|
||||
# 获取当前标签页
|
||||
tab = self.tab
|
||||
|
||||
# 步骤1:获取当前国家名称
|
||||
print(f"正在检查当前国家...")
|
||||
current_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
|
||||
timeout=10)
|
||||
if current_country_ele:
|
||||
current_country = current_country_ele.text.strip()
|
||||
print(f"当前国家:{current_country}")
|
||||
|
||||
# 判断是否与目标国家相同
|
||||
if current_country == country_name:
|
||||
print(f"当前已经是目标国家 {country_name},无需切换")
|
||||
return True
|
||||
else:
|
||||
print("无法获取当前国家信息")
|
||||
return False
|
||||
|
||||
# 步骤2:点击打开下拉框
|
||||
print(f"正在打开国家切换下拉框...")
|
||||
dropdown_header = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]', timeout=10)
|
||||
if not dropdown_header:
|
||||
print("找不到国家切换下拉框")
|
||||
return False
|
||||
dropdown_header.click()
|
||||
time.sleep(1) # 等待下拉框展开
|
||||
|
||||
# 步骤3:点击第一个展开国家列表
|
||||
print(f"正在展开国家列表...")
|
||||
first_item = tab.ele('xpath://div[@class="dropdown-account-switcher-list-item"]', timeout=10)
|
||||
if not first_item:
|
||||
print("找不到国家列表项")
|
||||
return False
|
||||
first_item.click()
|
||||
time.sleep(1) # 等待国家列表展开
|
||||
|
||||
# 步骤4:点击目标国家
|
||||
print(f"正在切换到国家:{country_name}")
|
||||
target_country_xpath = f'//div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="{country_name}"]'
|
||||
target_country = tab.ele(f'xpath:{target_country_xpath}', timeout=10)
|
||||
if not target_country:
|
||||
print(f"找不到目标国家:{country_name}")
|
||||
return False
|
||||
target_country.click()
|
||||
|
||||
# 步骤5:等待页面加载完成并验证切换结果
|
||||
print(f"等待页面加载...")
|
||||
# time.sleep(3) # 等待页面加载
|
||||
self.tab.wait.doc_loaded()
|
||||
|
||||
# 再次检查当前国家
|
||||
new_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
|
||||
timeout=10)
|
||||
if new_country_ele:
|
||||
new_country = new_country_ele.text.strip()
|
||||
if new_country == country_name:
|
||||
print(f"国家切换成功:{new_country}")
|
||||
return True
|
||||
else:
|
||||
print(f"国家切换失败,当前国家:{new_country},目标国家:{country_name}")
|
||||
return False
|
||||
else:
|
||||
print("无法验证切换结果")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"切换国家时发生异常:{traceback.format_exc()}")
|
||||
return False
|
||||
mark_name = "删除品牌"
|
||||
|
||||
def SwitchPage(self):
|
||||
"""
|
||||
@@ -456,16 +42,20 @@ class AmazoneDriver(ZiniaoDriver):
|
||||
time.sleep(0.6)
|
||||
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
|
||||
'xpath://span[@class="container"]//input[@part="input"]')
|
||||
search_input.input(asin)
|
||||
|
||||
search_input.input(asin,clear=True)
|
||||
sku_ls = []
|
||||
for _ in range(3):
|
||||
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
|
||||
search_btn.click()
|
||||
|
||||
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
|
||||
load_ele.wait.hidden(timeout=20)
|
||||
time.sleep(1)
|
||||
sku_ls = self.tab.eles("//div[@data-sku]")
|
||||
# load_ele.wait.hidden(timeout=3, raise_err=False)
|
||||
load_ele.wait.deleted(timeout=3, raise_err=False)
|
||||
time.sleep(0.5)
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3)
|
||||
if len(sku_ls) > 0:
|
||||
break
|
||||
return sku_ls
|
||||
|
||||
def del_action(self, sku_ele):
|
||||
@@ -473,7 +63,7 @@ class AmazoneDriver(ZiniaoDriver):
|
||||
dropdown.click()
|
||||
time.sleep(1)
|
||||
|
||||
del_btn = dropdown.ele("xpath:.//button[@role='menuitem' and text()='删除商品信息']")
|
||||
del_btn = dropdown.sr("xpath:.//button[@role='menuitem' and @data-action='DeleteListing']")
|
||||
del_btn.wait.displayed(raise_err=False)
|
||||
del_btn.click()
|
||||
|
||||
@@ -487,6 +77,448 @@ class AmazoneDriver(ZiniaoDriver):
|
||||
return suc
|
||||
|
||||
|
||||
class DelbramdTask(TaskBase):
|
||||
task_name = "删除品牌-TASK"
|
||||
|
||||
def update_task_status(self, task_id: int, **kwargs):
|
||||
"""更新任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
**kwargs: 要更新的字段
|
||||
"""
|
||||
if task_id not in runing_task:
|
||||
runing_task[task_id] = {}
|
||||
runing_task[task_id].update(kwargs)
|
||||
|
||||
def process_task(self,task_data):
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
# 初始化任务状态
|
||||
self.update_task_status(
|
||||
task_id,
|
||||
status="running",
|
||||
start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
total_shops=len(items),
|
||||
processed_shops=0,
|
||||
total_asins=sum(shop.get("totalRows", 0) for shop in items),
|
||||
processed_asins=0,
|
||||
success_count=0,
|
||||
failed_count=0,
|
||||
stop_requested=False # 暂停请求标志
|
||||
)
|
||||
|
||||
self.log(f"开始处理任务 {task_id},共 {len(items)} 个店铺")
|
||||
|
||||
# 遍历处理每个店铺
|
||||
for idx, shop_data in enumerate(items, 1):
|
||||
shop_name = shop_data.get("shopName", "未知店铺")
|
||||
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
|
||||
try:
|
||||
self.process_shop(shop_data, task_id)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 检查任务最终状态
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.update_task_status(task_id, status="stopped")
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
self.update_task_status(task_id, status="completed")
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
self.update_task_status(task_id, status="failed", error=str(e))
|
||||
|
||||
def process_shop(self, shop_data, task_id):
|
||||
"""处理单个店铺(包含重试逻辑)
|
||||
Args:
|
||||
shop_data: 店铺数据
|
||||
task_id: 任务ID
|
||||
"""
|
||||
shop_name = shop_data.get("shopName", "未知店铺")
|
||||
result_id = shop_data.get("resultId")
|
||||
countries = shop_data.get("countries", [])
|
||||
company_name = shop_data.get("companyName", "未知公司")
|
||||
|
||||
if not countries:
|
||||
self.log(f"店铺 {shop_name} 没有国家数据,跳过", "WARNING")
|
||||
return
|
||||
|
||||
# 更新当前处理的店铺
|
||||
self.update_task_status(task_id, current_shop=shop_name)
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"账号:{company_name},店铺 {shop_name} 已添加到执行列表,开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
max_retries = 5
|
||||
iskill = False
|
||||
|
||||
|
||||
# 处理每个国家
|
||||
chunk_index = 1
|
||||
for country_data in countries:
|
||||
self.log(f"开始处理国家:{country_data}")
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
|
||||
break # 跳出循环,进入finally关闭店铺
|
||||
|
||||
# 打开店铺
|
||||
driver = self.open_shop(AmazoneDriver,max_retries=max_retries, company_name=company_name,
|
||||
shop_name=shop_name, iskill=iskill)
|
||||
|
||||
# 检查是否成功打开
|
||||
if not driver:
|
||||
self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
chunk_index = self.process_country(driver, country_data, task_id, result_id, shop_data, chunk_index)
|
||||
except Exception as e:
|
||||
country_name = country_data.get("country", "未知")
|
||||
self.log(f"处理国家 {country_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
if "与页面的连接已断开" in str(e):
|
||||
iskill = True
|
||||
|
||||
try:
|
||||
driver.close_store()
|
||||
except Exception as e:
|
||||
iskill = True
|
||||
self.log(f"关闭国家失败,{e}")
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver: AmazoneDriver, country_data, task_id: int,
|
||||
result_id: int, shop_data, chunk_index: int):
|
||||
"""处理单个国家的所有ASIN
|
||||
|
||||
Args:
|
||||
driver: 亚马逊驱动实例
|
||||
country_data: 国家数据
|
||||
task_id: 任务ID
|
||||
result_id: 结果ID
|
||||
shop_data: 店铺数据
|
||||
"""
|
||||
country = country_data.get("country", "未知")
|
||||
items = country_data.get("items", [])
|
||||
shop_name = shop_data.get("shopName", "未知店铺")
|
||||
|
||||
|
||||
self.log(f"开始处理国家: {country},共 {len(items)} 个ASIN")
|
||||
self.update_task_status(task_id, current_country=country)
|
||||
|
||||
max_retries = 5
|
||||
switch_success, switch_success_pg, search_success, sku_ls = self.action_init(driver, country, shop_name,
|
||||
None)
|
||||
# 如果切换失败,直接返回
|
||||
if not switch_success or not switch_success_pg or not search_success:
|
||||
error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country} 失败,已重试 {max_retries} 次,跳过该国家"
|
||||
self.log(error_message, "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return chunk_index
|
||||
|
||||
# 处理每个ASIN
|
||||
file_key = shop_data.get("fileKey", "")
|
||||
source_filename = shop_data.get("sourceFilename", "")
|
||||
total_rows = shop_data.get("totalRows", 0)
|
||||
|
||||
for idx, asin_item in enumerate(items, 1):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
return # 返回到上层,会触发店铺关闭
|
||||
|
||||
try:
|
||||
self.log(f"[{idx}/{len(items)}] 处理ASIN: {asin_item.get('asin', '')}")
|
||||
self.process_asin(driver, asin_item, country, task_id, result_id,
|
||||
file_key, source_filename, total_rows, chunk_index)
|
||||
except Exception as e:
|
||||
asin = asin_item.get("asin", "未知")
|
||||
self.log(f"处理ASIN {asin} 失败: {str(e)}", "ERROR")
|
||||
# 继续处理下一个ASIN
|
||||
chunk_index += 1
|
||||
|
||||
return chunk_index
|
||||
|
||||
def process_asin(self, driver: AmazoneDriver, asin_item,
|
||||
country: str, task_id: int, result_id: int, file_key: str,
|
||||
source_filename: str, total_rows: int, chunk_index: int):
|
||||
"""处理单个ASIN并回传结果
|
||||
|
||||
Args:
|
||||
driver: 亚马逊驱动实例
|
||||
asin_item: ASIN数据项
|
||||
country: 国家名称
|
||||
task_id: 任务ID
|
||||
result_id: 结果ID
|
||||
file_key: 文件KEY
|
||||
source_filename: 源文件名
|
||||
total_rows: 总行数
|
||||
chunk_index: 当前处理索引
|
||||
"""
|
||||
asin = asin_item.get("asin", "")
|
||||
row_index = asin_item.get("rowIndex", 0)
|
||||
|
||||
# 更新当前处理的ASIN
|
||||
self.update_task_status(task_id, current_asin=asin)
|
||||
|
||||
status = "失败"
|
||||
max_retries = 3 # 最多重试3次
|
||||
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"处理ASIN {asin} (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
# 如果不是第一次尝试,先刷新页面
|
||||
if retry > 0:
|
||||
self.log("重试前刷新页面...")
|
||||
try:
|
||||
driver.tab.refresh()
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
self.log(f"刷新页面失败: {str(e)}", "WARNING")
|
||||
|
||||
# 搜索ASIN
|
||||
sku_ls = driver.search(asin=asin)
|
||||
self.log(f"搜索到 {len(sku_ls)} 个SKU")
|
||||
|
||||
if len(sku_ls) == 0:
|
||||
status = "查询不到"
|
||||
self.log(f"ASIN {asin} 未找到商品", "WARNING")
|
||||
break # 查询不到商品,无需重试
|
||||
else:
|
||||
# 删除所有找到的SKU,如果任何一个失败则重新开始整个流程
|
||||
success_count = 0
|
||||
all_success = True # 标记是否所有SKU都删除成功
|
||||
total_sku_count = len(sku_ls)
|
||||
|
||||
for sku in sku_ls:
|
||||
try:
|
||||
suc = driver.del_action(sku)
|
||||
if suc:
|
||||
success_count += 1
|
||||
self.log(f"SKU 删除成功 ({success_count}/{total_sku_count})")
|
||||
else:
|
||||
self.log(f"SKU 删除失败", "WARNING")
|
||||
all_success = False
|
||||
break # 任何一个失败,退出循环,准备重试整个流程
|
||||
except Exception as e:
|
||||
self.log(f"删除SKU异常: {str(e)}", "ERROR")
|
||||
all_success = False
|
||||
break # 发生异常,退出循环,准备重试整个流程
|
||||
|
||||
# 判断删除结果
|
||||
if all_success and success_count == total_sku_count:
|
||||
status = "成功"
|
||||
self.log(f"ASIN {asin} 所有SKU删除成功 ({success_count}/{total_sku_count})")
|
||||
# 更新成功计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["success_count"] += 1
|
||||
break # 全部成功,跳出重试循环
|
||||
|
||||
else:
|
||||
# 有失败的SKU
|
||||
if retry < max_retries - 1:
|
||||
self.log(f"有SKU删除失败,准备重试整个流程... ({retry + 1}/{max_retries})")
|
||||
time.sleep(2)
|
||||
continue # 继续下一次重试
|
||||
else:
|
||||
# 所有重试都用完了
|
||||
if success_count > 0:
|
||||
status = "部分成功"
|
||||
self.log(f"ASIN {asin} 部分SKU删除成功 ({success_count}/{total_sku_count})", "WARNING")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["success_count"] += 1
|
||||
else:
|
||||
status = "失败"
|
||||
self.log(f"ASIN {asin} 所有SKU删除失败", "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
except Exception as e:
|
||||
status = "删除异常"
|
||||
self.log(f"处理ASIN {asin} 异常: {str(e)}", "ERROR")
|
||||
|
||||
# 如果还有重试机会,继续重试
|
||||
if retry < max_retries - 1:
|
||||
self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})")
|
||||
time.sleep(2)
|
||||
else:
|
||||
# 所有重试都失败了,更新失败计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
# 更新已处理ASIN计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
|
||||
# 回传结果到API
|
||||
try:
|
||||
payload = {
|
||||
"submissionId": "",
|
||||
"files": [{
|
||||
"fileKey": file_key,
|
||||
"sourceFilename": source_filename,
|
||||
"chunkIndex": chunk_index,
|
||||
"chunkTotal": total_rows,
|
||||
"processedRows": chunk_index,
|
||||
"totalRows": total_rows,
|
||||
"currentCountry": country,
|
||||
"currentAsin": asin,
|
||||
"countries": [{
|
||||
"country": country,
|
||||
"items": [{
|
||||
"asin": asin,
|
||||
"status": status
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
self.post_result(task_id, payload)
|
||||
self.log(f"ASIN {asin} 结果已回传,状态: {status}")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
def _report_all_asins_failed(self, country: str, items, task_id, shop_data, chunk_index):
|
||||
"""将国家下所有ASIN标记为失败并回传
|
||||
|
||||
Args:
|
||||
country: 国家名称
|
||||
items: ASIN列表
|
||||
task_id: 任务ID
|
||||
shop_data: 店铺数据
|
||||
"""
|
||||
self.log(f"开始回传国家 {country} 下的 {len(items)} 个ASIN失败状态")
|
||||
|
||||
file_key = shop_data.get("fileKey", "")
|
||||
source_filename = shop_data.get("sourceFilename", "")
|
||||
total_rows = shop_data.get("totalRows", 0)
|
||||
|
||||
for asin_item in items:
|
||||
asin = asin_item.get("asin", "")
|
||||
|
||||
try:
|
||||
# 更新失败计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
|
||||
# 回传失败状态
|
||||
payload = {
|
||||
"submissionId": "",
|
||||
"files": [{
|
||||
"fileKey": file_key,
|
||||
"sourceFilename": source_filename,
|
||||
"chunkIndex": chunk_index,
|
||||
"chunkTotal": total_rows,
|
||||
"processedRows": chunk_index,
|
||||
"totalRows": total_rows,
|
||||
"currentCountry": country,
|
||||
"currentAsin": asin,
|
||||
"countries": [{
|
||||
"country": country,
|
||||
"items": [{
|
||||
"asin": asin,
|
||||
"status": "失败"
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
}
|
||||
|
||||
self.post_result(task_id, payload)
|
||||
chunk_index += 1
|
||||
self.log(f"ASIN {asin} 失败状态已回传")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"回传ASIN {asin} 失败状态时出错: {str(e)}", "ERROR")
|
||||
|
||||
return chunk_index
|
||||
|
||||
def post_result(self, task_id: int, payload):
|
||||
"""回传结果到API(带重试机制)
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
payload: 结果数据
|
||||
"""
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/delete-brand/tasks/{task_id}/result"
|
||||
max_retries = 3 # 最多重试3次
|
||||
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False # 忽略SSL证书验证
|
||||
)
|
||||
print("【结果提交】:", payload)
|
||||
print("【结果提交返回】:", response.text)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.log(f"结果回传成功: {url}")
|
||||
return # 成功后直接返回,不再重试
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
# 如果还有重试机会,继续重试
|
||||
if retry < max_retries - 1:
|
||||
self.log(f"准备重试... ({retry + 1}/{max_retries})")
|
||||
time.sleep(2) # 等待2秒后重试
|
||||
else:
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
# 如果还有重试机会,继续重试
|
||||
if retry < max_retries - 1:
|
||||
self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})")
|
||||
time.sleep(2) # 等待2秒后重试
|
||||
else:
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
|
||||
def update_task_status(self, task_id: int, **kwargs):
|
||||
"""更新任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
**kwargs: 要更新的字段
|
||||
"""
|
||||
if task_id not in runing_task:
|
||||
runing_task[task_id] = {}
|
||||
|
||||
runing_task[task_id].update(kwargs)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 使用示例
|
||||
user_info = {
|
||||
|
||||
8271
app/amazon/detail_spider.py
Normal file
8271
app/amazon/detail_spider.py
Normal file
File diff suppressed because it is too large
Load Diff
8384
app/amazon/detail_spider_备份.py
Normal file
8384
app/amazon/detail_spider_备份.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,19 @@
|
||||
import time
|
||||
import traceback
|
||||
import requests
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_BASE, ZN_COMPANY, ZN_USERNAME, ZN_PASSWORD
|
||||
from amazon.del_brand import AmazoneDriver, kill_process
|
||||
from amazon.del_brand import AmazoneDriver, DelbramdTask
|
||||
from amazon.approve import ApproveTask
|
||||
from amazon.match_action import MatchTak
|
||||
from amazon.price_match import PriceTask
|
||||
from amazon.asin_status import StatusTask
|
||||
from amazon.patrol_delete import PatrolDeleteTask
|
||||
from amazon.detail_spider import SpiderTask
|
||||
from amazon.similar_asin import SimilarAsinTask
|
||||
from amazon.amazon_base import kill_process
|
||||
|
||||
|
||||
class TaskMonitor:
|
||||
@@ -20,6 +29,15 @@ class TaskMonitor:
|
||||
}
|
||||
|
||||
self.chunk_index = 1 # 当前处理的分块索引
|
||||
self.max_workers = 5 # 线程池最大线程数
|
||||
self.executor = None # 线程池执行器
|
||||
self.serial_task_locks = {
|
||||
"product-risk-resolve-run": threading.Lock(),
|
||||
}
|
||||
|
||||
# 在提交新任务前杀掉旧进程(确保环境干净)
|
||||
kill_process("v6")
|
||||
kill_process("v5")
|
||||
|
||||
def log(self, message: str, level: str = "INFO"):
|
||||
"""日志输出
|
||||
@@ -32,371 +50,107 @@ class TaskMonitor:
|
||||
print(f"[{timestamp}] [{level}] {message}")
|
||||
|
||||
def start(self):
|
||||
"""启动任务监控(阻塞式循环)"""
|
||||
self.log("任务监控器启动,开始监听队列...")
|
||||
"""启动任务监控(使用线程池处理任务)"""
|
||||
self.log(f"任务监控器启动,开始监听队列... (线程池大小: {self.max_workers})")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
# 阻塞式获取任务,避免CPU空转
|
||||
task_data = JSON_TASK_QUEUE.get(block=True, timeout=1)
|
||||
|
||||
# 检查任务类型
|
||||
task_type = task_data.get("type", "")
|
||||
if task_type != "delete-brand-run":
|
||||
self.log(f"未知任务类型: {task_type},跳过", "WARNING")
|
||||
continue
|
||||
|
||||
# 处理任务
|
||||
self.log(f"接收到删除品牌任务,开始处理...")
|
||||
self.process_task(task_data)
|
||||
|
||||
except Exception as e:
|
||||
if "Empty" not in str(e): # 忽略队列为空的超时异常
|
||||
self.log(f"任务处理异常: {traceback.format_exc()}", "ERROR")
|
||||
time.sleep(0.1) # 短暂等待避免异常循环
|
||||
|
||||
def process_task(self, task_data: Dict[str, Any]):
|
||||
"""处理单个任务
|
||||
# 创建线程池
|
||||
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
|
||||
futures = [] # 保存所有提交的任务Future对象
|
||||
|
||||
task_type_info = {
|
||||
"delete-brand-run" : "删除品牌",
|
||||
"product-risk-resolve-run" : "产品风险审批",
|
||||
"shop-match-run" : "匹配价格",
|
||||
"price-track-run" : "跟价",
|
||||
"query-asin-run" : "状态查询",
|
||||
"patrol-delete-run" : "巡店删除",
|
||||
"appearance-patent-run" : "亚马逊采集",
|
||||
"similar-asin-run" : "相似ASIN"
|
||||
}
|
||||
try:
|
||||
while self.running:
|
||||
try:
|
||||
# 超时后继续循环检查self.running状态,避免卡死
|
||||
task_data = JSON_TASK_QUEUE.get(block=True, timeout=30)
|
||||
|
||||
# 检查任务类型
|
||||
task_type = task_data.get("type", "")
|
||||
|
||||
if task_type in task_type_info:
|
||||
# 提交产品风险审批任务到线程池
|
||||
self.log(f"接收到任务,提交到线程池处理...")
|
||||
future = self.executor.submit(self._process_approve_task_wrapper, task_data, task_type_info[task_type])
|
||||
futures.append(future)
|
||||
|
||||
else:
|
||||
self.log(f"未知任务类型: {task_type},跳过", "WARNING")
|
||||
continue
|
||||
|
||||
# 清理已完成的future对象,避免内存累积
|
||||
futures = [f for f in futures if not f.done()]
|
||||
|
||||
except Exception as e:
|
||||
# 静默处理队列为空的超时,只记录真正的异常
|
||||
if str(e) and "Empty" not in str(e):
|
||||
self.log(f"任务监控异常: {str(e)}", "ERROR")
|
||||
# 队列为空时不需要额外等待,直接继续循环
|
||||
|
||||
finally:
|
||||
# 关闭监控时,等待所有任务完成
|
||||
self.log("正在关闭任务监控器,等待所有任务完成...")
|
||||
if self.executor:
|
||||
self.executor.shutdown(wait=True)
|
||||
self.log("所有任务已完成,监控器已关闭")
|
||||
|
||||
def _process_task_wrapper(self, task_data: Dict[str, Any]):
|
||||
"""任务处理包装器(用于线程池调用)
|
||||
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
# 初始化任务状态
|
||||
self.update_task_status(
|
||||
task_id,
|
||||
status="running",
|
||||
start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
total_shops=len(items),
|
||||
processed_shops=0,
|
||||
total_asins=sum(shop.get("totalRows", 0) for shop in items),
|
||||
processed_asins=0,
|
||||
success_count=0,
|
||||
failed_count=0,
|
||||
stop_requested=False # 暂停请求标志
|
||||
)
|
||||
|
||||
self.log(f"开始处理任务 {task_id},共 {len(items)} 个店铺")
|
||||
|
||||
# 遍历处理每个店铺
|
||||
for idx, shop_data in enumerate(items, 1):
|
||||
shop_name = shop_data.get("shopName", "未知店铺")
|
||||
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
|
||||
try:
|
||||
self.process_shop(shop_data, task_id)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 检查任务最终状态
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.update_task_status(task_id, status="stopped")
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
self.update_task_status(task_id, status="completed")
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
self.log(f"线程 {id(task_data)} 开始处理任务...")
|
||||
self.process_task(task_data)
|
||||
self.log(f"线程 {id(task_data)} 任务处理完成")
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
self.update_task_status(task_id, status="failed", error=str(e))
|
||||
|
||||
def process_shop(self, shop_data: Dict[str, Any], task_id: int):
|
||||
"""处理单个店铺(包含重试逻辑)
|
||||
self.log(f"线程 {id(task_data)} 删除品牌任务处理异常: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str):
|
||||
task_type = task_data.get("type", "")
|
||||
task_lock = self.serial_task_locks.get(task_type)
|
||||
if task_lock is not None:
|
||||
self.log(f"线程 {id(task_data)} 等待 {task_type} 串行锁...")
|
||||
with task_lock:
|
||||
self.log(f"线程 {id(task_data)} 获取 {task_type} 串行锁,开始处理...")
|
||||
return self._process_approve_task_unlocked(task_data, TASK_TYPE)
|
||||
return self._process_approve_task_unlocked(task_data, TASK_TYPE)
|
||||
|
||||
def _process_approve_task_unlocked(self, task_data: Dict[str, Any],TASK_TYPE:str):
|
||||
"""审批任务处理包装器(用于线程池调用)
|
||||
|
||||
Args:
|
||||
shop_data: 店铺数据
|
||||
task_id: 任务ID
|
||||
task_data: 任务数据
|
||||
"""
|
||||
shop_name = shop_data.get("shopName", "未知店铺")
|
||||
result_id = shop_data.get("resultId")
|
||||
countries = shop_data.get("countries", [])
|
||||
|
||||
if not countries:
|
||||
self.log(f"店铺 {shop_name} 没有国家数据,跳过", "WARNING")
|
||||
return
|
||||
|
||||
# 更新当前处理的店铺
|
||||
self.update_task_status(task_id, current_shop=shop_name)
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"店铺 {shop_name} 已添加到执行列表,开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
browser = None
|
||||
max_retries = 3
|
||||
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)")
|
||||
|
||||
# 如果不是第一次尝试,先杀进程
|
||||
if retry > 0:
|
||||
self.log("重试前先杀掉浏览器进程...")
|
||||
kill_process("v6")
|
||||
time.sleep(2)
|
||||
|
||||
# 创建驱动并打开店铺
|
||||
driver = AmazoneDriver(self.user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
|
||||
if browser and browser != "店铺不存在":
|
||||
self.log(f"成功打开店铺 {shop_name}")
|
||||
break
|
||||
else:
|
||||
self.log(f"打开店铺失败: {browser}", "WARNING")
|
||||
driver = None
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"打开店铺异常: {str(e)}", "ERROR")
|
||||
driver = None
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(3)
|
||||
|
||||
# 检查是否成功打开
|
||||
if not driver or not browser or browser == "店铺不存在":
|
||||
self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR")
|
||||
return
|
||||
|
||||
try:
|
||||
# 处理每个国家
|
||||
for country_data in countries:
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
|
||||
break # 跳出循环,进入finally关闭店铺
|
||||
|
||||
try:
|
||||
self.process_country(driver, country_data, task_id, result_id, shop_data)
|
||||
except Exception as e:
|
||||
country_name = country_data.get("country", "未知")
|
||||
self.log(f"处理国家 {country_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
finally:
|
||||
# 关闭店铺
|
||||
try:
|
||||
if driver:
|
||||
self.log(f"关闭店铺 {shop_name}")
|
||||
driver.close_store()
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
self.log(f"关闭店铺失败: {str(e)}", "WARNING")
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver: AmazoneDriver, country_data: Dict[str, Any],
|
||||
task_id: int, result_id: int, shop_data: Dict[str, Any]):
|
||||
"""处理单个国家的所有ASIN
|
||||
|
||||
Args:
|
||||
driver: 亚马逊驱动实例
|
||||
country_data: 国家数据
|
||||
task_id: 任务ID
|
||||
result_id: 结果ID
|
||||
shop_data: 店铺数据
|
||||
"""
|
||||
country = country_data.get("country", "未知")
|
||||
items = country_data.get("items", [])
|
||||
|
||||
self.log(f"开始处理国家: {country},共 {len(items)} 个ASIN")
|
||||
self.update_task_status(task_id, current_country=country)
|
||||
|
||||
# 切换国家
|
||||
try:
|
||||
switch_success = driver.SwitchingCountries(country)
|
||||
if not switch_success:
|
||||
self.log(f"切换到国家 {country} 失败,跳过该国家", "ERROR")
|
||||
return
|
||||
except Exception as e:
|
||||
self.log(f"切换国家 {country} 异常: {str(e)}", "ERROR")
|
||||
return
|
||||
|
||||
# 切换到库存管理页面
|
||||
try:
|
||||
driver.SwitchPage()
|
||||
self.log(f"已切换到库存管理页面")
|
||||
except Exception as e:
|
||||
self.log(f"切换页面失败: {str(e)}", "ERROR")
|
||||
return
|
||||
|
||||
# 处理每个ASIN
|
||||
file_key = shop_data.get("fileKey", "")
|
||||
source_filename = shop_data.get("sourceFilename", "")
|
||||
total_rows = shop_data.get("totalRows", 0)
|
||||
|
||||
for idx, asin_item in enumerate(items, 1):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
return # 返回到上层,会触发店铺关闭
|
||||
|
||||
try:
|
||||
self.log(f"[{idx}/{len(items)}] 处理ASIN: {asin_item.get('asin', '')}")
|
||||
self.process_asin(driver, asin_item, country, task_id, result_id,
|
||||
file_key, source_filename, total_rows, idx)
|
||||
except Exception as e:
|
||||
asin = asin_item.get("asin", "未知")
|
||||
self.log(f"处理ASIN {asin} 失败: {str(e)}", "ERROR")
|
||||
# 继续处理下一个ASIN
|
||||
|
||||
def process_asin(self, driver: AmazoneDriver, asin_item: Dict[str, Any],
|
||||
country: str, task_id: int, result_id: int, file_key: str,
|
||||
source_filename: str, total_rows: int):
|
||||
"""处理单个ASIN并回传结果
|
||||
|
||||
Args:
|
||||
driver: 亚马逊驱动实例
|
||||
asin_item: ASIN数据项
|
||||
country: 国家名称
|
||||
task_id: 任务ID
|
||||
result_id: 结果ID
|
||||
file_key: 文件KEY
|
||||
source_filename: 源文件名
|
||||
total_rows: 总行数
|
||||
chunk_index: 当前处理索引
|
||||
"""
|
||||
asin = asin_item.get("asin", "")
|
||||
row_index = asin_item.get("rowIndex", 0)
|
||||
|
||||
# 更新当前处理的ASIN
|
||||
self.update_task_status(task_id, current_asin=asin)
|
||||
|
||||
status = "失败"
|
||||
|
||||
try:
|
||||
# 搜索ASIN
|
||||
sku_ls = driver.search(asin=asin)
|
||||
self.log(f"搜索到 {len(sku_ls)} 个SKU")
|
||||
|
||||
if len(sku_ls) == 0:
|
||||
status = "查询不到"
|
||||
self.log(f"ASIN {asin} 未找到商品", "WARNING")
|
||||
else:
|
||||
# 删除所有找到的SKU
|
||||
success_count = 0
|
||||
for sku in sku_ls:
|
||||
try:
|
||||
suc = driver.del_action(sku)
|
||||
if suc:
|
||||
success_count += 1
|
||||
self.log(f"SKU 删除成功")
|
||||
else:
|
||||
self.log(f"SKU 删除失败", "WARNING")
|
||||
except Exception as e:
|
||||
self.log(f"删除SKU异常: {str(e)}", "ERROR")
|
||||
|
||||
if success_count > 0:
|
||||
status = "成功"
|
||||
self.log(f"ASIN {asin} 删除成功 ({success_count}/{len(sku_ls)})")
|
||||
# 更新成功计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["success_count"] += 1
|
||||
else:
|
||||
status = "失败"
|
||||
self.log(f"ASIN {asin} 所有SKU删除失败", "ERROR")
|
||||
# 更新失败计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
except Exception as e:
|
||||
status = "删除异常"
|
||||
self.log(f"处理ASIN {asin} 异常: {str(e)}", "ERROR")
|
||||
# 更新失败计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
# 更新已处理ASIN计数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
|
||||
# 回传结果到API
|
||||
try:
|
||||
payload = {
|
||||
"submissionId": "",
|
||||
"files": [{
|
||||
"fileKey": file_key,
|
||||
"sourceFilename": source_filename,
|
||||
"chunkIndex": self.chunk_index,
|
||||
"chunkTotal": total_rows,
|
||||
"processedRows": self.chunk_index,
|
||||
"totalRows": total_rows,
|
||||
"currentCountry": country,
|
||||
"currentAsin": asin,
|
||||
"countries": [{
|
||||
"country": country,
|
||||
"items": [{
|
||||
"asin": asin,
|
||||
"status": status
|
||||
}]
|
||||
}]
|
||||
}]
|
||||
TASK_INFO = {
|
||||
"删除品牌": DelbramdTask ,
|
||||
"产品风险审批" : ApproveTask,
|
||||
"匹配价格" : MatchTak,
|
||||
"跟价" : PriceTask,
|
||||
"状态查询" : StatusTask,
|
||||
"巡店删除" : PatrolDeleteTask,
|
||||
"亚马逊采集" : SpiderTask,
|
||||
"相似ASIN" : SimilarAsinTask
|
||||
}
|
||||
|
||||
self.post_result(task_id, payload)
|
||||
self.log(f"ASIN {asin} 结果已回传,状态: {status}")
|
||||
|
||||
self.log(f"线程 {id(task_data)} 开始处理产品风险审批任务...")
|
||||
# 创建ApproveTask实例并处理任务
|
||||
TASK_CLS = TASK_INFO[TASK_TYPE] # 根据任务类型选择处理类,默认为ApproveTask
|
||||
approve_task = TASK_CLS(user_info=self.user_info)
|
||||
approve_task.process_task(task_data)
|
||||
self.log(f"线程 {id(task_data)} {TASK_TYPE} 任务处理完成")
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
def post_result(self, task_id: int, payload: Dict[str, Any]):
|
||||
"""回传结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
payload: 结果数据
|
||||
"""
|
||||
url = f"{DELETE_BRAND_API_BASE}/newApi/api/delete-brand/tasks/{task_id}/result"
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False # 忽略SSL证书验证
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
self.log(f"结果回传成功: {url}")
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
|
||||
def update_task_status(self, task_id: int, **kwargs):
|
||||
"""更新任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
**kwargs: 要更新的字段
|
||||
"""
|
||||
if task_id not in runing_task:
|
||||
runing_task[task_id] = {}
|
||||
|
||||
runing_task[task_id].update(kwargs)
|
||||
|
||||
self.log(f"线程 {id(task_data)} {TASK_TYPE} 任务处理异常: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
def stop(self):
|
||||
"""停止监控"""
|
||||
self.running = False
|
||||
@@ -404,7 +158,7 @@ class TaskMonitor:
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数:启动任务监控"""
|
||||
"""主函数:启动任务监控"""
|
||||
# 创建并启动任务监控器
|
||||
monitor = TaskMonitor()
|
||||
|
||||
@@ -413,7 +167,7 @@ def main():
|
||||
except KeyboardInterrupt:
|
||||
monitor.log("接收到中断信号,正在停止...")
|
||||
monitor.stop()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
monitor.log(f"监控器异常退出: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
|
||||
|
||||
591
app/amazon/match_action.py
Normal file
591
app/amazon/match_action.py
Normal file
@@ -0,0 +1,591 @@
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from config import runing_task, runing_shop,DELETE_BRAND_API_BASE
|
||||
from datetime import datetime
|
||||
from amazon.amazon_base import AmamzonBase, kill_process,TaskBase
|
||||
from amazon.tool import show_notification
|
||||
import requests
|
||||
|
||||
|
||||
class AmzoneMatchAction(AmamzonBase):
|
||||
mark_name = "匹配价格"
|
||||
|
||||
def __init__(self, user_info: dict, socket_port: int = 19890):
|
||||
super().__init__(user_info,socket_port)
|
||||
self.already_asin = set()
|
||||
|
||||
def reset_already_asin(self):
|
||||
self.already_asin = set()
|
||||
self.log("already_asin 已重置")
|
||||
|
||||
def wait_loaded(self):
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.ele(
|
||||
'xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[contains(@class,"Loader-module__loader")]|//kat-panel[@data-testid="kat-panel-ActionPanelContent"]/div[@data-f1-component]//div[contains(@class,"==")]/div[contains(@class,"==")]/span',
|
||||
timeout=3)
|
||||
load_ele.wait.deleted(timeout=5, raise_err=False)
|
||||
except Exception as e:
|
||||
print(f"【{self.mark_name}】等待加载中消失出错", e)
|
||||
|
||||
def run_page_action(self,skip_asin=[]):
|
||||
self.log(f"==================={self.mark_name}=======================")
|
||||
self.log("开始执行...")
|
||||
num = 0
|
||||
retry_num = 0
|
||||
|
||||
max_retry_num = 5
|
||||
total_page = 0
|
||||
current_page = 0
|
||||
|
||||
while retry_num < max_retry_num:
|
||||
# if num > 3: #测试
|
||||
# return
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=3, raise_err=False)
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# 获取当前页码
|
||||
try:
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) > 0:
|
||||
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
|
||||
current_page = int(current_page.strip())
|
||||
# 测试
|
||||
# if current_page > 1:
|
||||
# break
|
||||
# 总页数
|
||||
total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
|
||||
if len(total_page) > 0:
|
||||
total_page = int(total_page[-1].text.strip())
|
||||
self.log(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}")
|
||||
except Exception as e:
|
||||
self.log(f"【{self.mark_name}】获取页码失败", e)
|
||||
|
||||
# 保存当前URL,用于失败重试时访问
|
||||
try:
|
||||
self.url = self.tab.url
|
||||
self.log(f"获取当前的链接:{self.url}")
|
||||
except Exception as e:
|
||||
self.url = None
|
||||
self.log(f"获取当前的链接失败:{e}")
|
||||
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
||||
self.log(f"【{self.mark_name}】获取到 {len(sku_ls)}")
|
||||
if len(sku_ls) == 0:
|
||||
retry_num += 1
|
||||
self.log(f"没有获取到SKU列表,重试{retry_num}/{max_retry_num}")
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False, timeout=120)
|
||||
if retry_num == max_retry_num:
|
||||
raise RuntimeError(f"与页面的连接已断开,没有获取到SKU列表,重试{retry_num}/{max_retry_num}")
|
||||
continue
|
||||
|
||||
# for sku_ele in sku_ls[0:2]:
|
||||
for sku_ele in sku_ls:
|
||||
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')
|
||||
|
||||
asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]',timeout=3).text
|
||||
self.log(f"ASIN {asin} 找到....")
|
||||
if asin in skip_asin:
|
||||
self.log(f"ASIN {asin} 跳过....")
|
||||
yield (asin,"有最低价跳过")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
|
||||
if asin in self.already_asin:
|
||||
self.log(f"【{self.mark_name}】{asin} 已经处理过了,跳过")
|
||||
continue
|
||||
|
||||
price_match = sku_ele.eles('xpath:.//div[@data-test-id="FeaturedOfferPrice"]//a[text()="匹配"]',timeout=2)
|
||||
if len(price_match) == 0:
|
||||
self.log(f"【{self.mark_name}】{asin},没有推荐价格匹配按钮")
|
||||
yield (asin,"无需处理")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
try:
|
||||
price_match[0].click()
|
||||
|
||||
save_all_btn = self.tab.ele('xpath://kat-button[@label="保存所有"]',timeout=10)
|
||||
save_all_btn.wait.displayed(timeout=10, raise_err=False)
|
||||
save_all_btn.click()
|
||||
|
||||
save_all_btn.wait.deleted(timeout=10, raise_err=False)
|
||||
|
||||
yield (asin,"处理完成")
|
||||
self.already_asin.add(asin)
|
||||
except Exception as e:
|
||||
print(f"{asin} 处理失败", e)
|
||||
yield (asin,"处理失败")
|
||||
self.already_asin.add(asin)
|
||||
continue
|
||||
|
||||
|
||||
# 判断是否存在需要翻页的情况
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
if len(page_pamel) == 0:
|
||||
break
|
||||
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
|
||||
class_str = next_page_btn.attr('class')
|
||||
if "end" in class_str:
|
||||
break
|
||||
next_page_btn.click()
|
||||
num += 1
|
||||
self.log(f"【{self.mark_name}】【程序计算】正在翻页,已翻 {num} 页...")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"【{self.mark_name}】处理匹配操作异常", e)
|
||||
traceback.print_exc()
|
||||
retry_num += 1
|
||||
self.tab.refresh()
|
||||
self.tab.wait.doc_loaded(raise_err=False,timeout=120)
|
||||
|
||||
#检查页数是否相等,不相等则继续
|
||||
self.log(f"开始检查页数,当前页数 {current_page} / {total_page}")
|
||||
if total_page != 0 and current_page!= 0 and current_page < total_page:
|
||||
self.log(f"检查到页数还未完成,重启浏览器继续")
|
||||
raise RuntimeError(f"与页面的连接已断开,检查到页数还未完成,重启浏览器继续")
|
||||
|
||||
|
||||
class MatchTak(TaskBase):
|
||||
task_name = "匹配价格-TASK"
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""处理审批任务主入口
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
items = data.get("items", [])
|
||||
country_codes = data.get("country_codes", [])
|
||||
risk_listing_filter = data.get("risk_listing_filter", "All")
|
||||
user_id = data.get("user_id")
|
||||
stage_index = data.get("stage_index")
|
||||
final_stage = bool(data.get("final_stage", True))
|
||||
|
||||
# 用于测试
|
||||
limit = data.get("limit",None)
|
||||
|
||||
if not task_id or not items or not country_codes:
|
||||
self.log(f"任务ID为空{task_id}/店铺列表为空{items}/国家列表为空{country_codes},跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家")
|
||||
|
||||
runing_task[task_id] = {
|
||||
"status": "running",
|
||||
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_shops": len(items),
|
||||
"processed_shops": 0,
|
||||
"total_countries": len(country_codes) * len(items),
|
||||
"processed_countries": 0,
|
||||
"total_asins": 0,
|
||||
"processed_asins": 0,
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"stop_requested": False
|
||||
}
|
||||
|
||||
# 遍历处理每个店铺
|
||||
for idx, shop_item in enumerate(items, 1):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
return
|
||||
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
|
||||
show_notification(f"开始处理店铺: {shop_name}", "info")
|
||||
|
||||
try:
|
||||
self.process_shop(shop_item, country_codes, task_id, risk_listing_filter, user_id, stage_index, final_stage,limit=limit)
|
||||
|
||||
# self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str,
|
||||
user_id=None, stage_index=None, final_stage: bool = True,limit:str=None):
|
||||
"""处理单个店铺
|
||||
|
||||
Args:
|
||||
shop_item: 店铺信息
|
||||
country_codes: 国家代码列表
|
||||
task_id: 任务ID
|
||||
risk_listing_filter: 风险商品筛选条件
|
||||
"""
|
||||
shop_name = shop_item.get("shopName", "未知店铺")
|
||||
company_name = shop_item.get("companyName", "")
|
||||
|
||||
skip_asins_by_country = shop_item.get("skipAsinsByCountry",{})
|
||||
skipAsinDetailsByCountry = shop_item.get("skipAsinDetailsByCountry",{})
|
||||
|
||||
if not company_name:
|
||||
self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_shop"] = shop_name
|
||||
|
||||
# 将店铺添加到正在执行中的店铺列表
|
||||
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
runing_shop[shop_name] = start_time
|
||||
self.log(f"店铺 {shop_name} 已添加到执行列表,账号: {company_name},开始时间: {start_time}")
|
||||
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
max_retries = 3
|
||||
iskill = False
|
||||
|
||||
# 处理每个国家
|
||||
for country_code in country_codes:
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
|
||||
break
|
||||
|
||||
skip_asin = skip_asins_by_country.get(country_code,[])
|
||||
if skipAsinDetailsByCountry :
|
||||
skipAsinDetails = {i.get("asin"):i.get("minimumPrice") for i in skipAsinDetailsByCountry.get(country_code)}
|
||||
else:
|
||||
skipAsinDetails = {}
|
||||
# 打开店铺
|
||||
driver = self.open_shop(cls=AmzoneMatchAction,max_retries=max_retries, company_name=company_name,
|
||||
shop_name=shop_name, iskill=iskill)
|
||||
if driver is None:
|
||||
self.log(f"任务 {task_id} 启动店铺失败,结束任务", "ERROR")
|
||||
for country_code in country_codes:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
return
|
||||
|
||||
current_url = None
|
||||
max_retries = 200
|
||||
for _ in range(max_retries):
|
||||
try:
|
||||
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,limit,
|
||||
target_url=current_url,skip_asin=skip_asin,skipAsinDetails=skipAsinDetails)
|
||||
driver.reset_already_asin()
|
||||
driver.close_store()
|
||||
break
|
||||
except Exception as e:
|
||||
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
if "与页面的连接已断开" in str(e):
|
||||
iskill = True
|
||||
current_url = driver.url
|
||||
|
||||
# 更新已处理国家数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
|
||||
self.log(f"{task_id}任务处理完成")
|
||||
# 最后回传,标记完成
|
||||
try:
|
||||
# self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
if final_stage:
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
else:
|
||||
self.post_stage_finished(task_id, user_id, stage_index)
|
||||
except Exception as e:
|
||||
self.log(f"回传结果失败: {str(e)}", "ERROR")
|
||||
|
||||
|
||||
# 从正在执行中的店铺列表中移除
|
||||
if shop_name in runing_shop:
|
||||
del runing_shop[shop_name]
|
||||
self.log(f"店铺 {shop_name} 已从执行列表中移除")
|
||||
|
||||
def process_country(self, driver, country_code, task_id, shop_name, risk_listing_filter,limit=None,target_url=None,
|
||||
skip_asin=[],skipAsinDetails={}):
|
||||
"""处理单个国家的审批任务
|
||||
"""
|
||||
|
||||
# 转换国家代码为中文名称
|
||||
country_name = self.country_info.get(country_code, country_code)
|
||||
info_mes = f"开始处理国家: {country_name} ({country_code})"
|
||||
self.log(info_mes)
|
||||
show_notification(info_mes, "info")
|
||||
|
||||
# 更新当前处理的国家
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_country"] = country_name
|
||||
|
||||
|
||||
max_retries = 5
|
||||
switch_success, switch_success_pg, search_success, sku_ls = self.action_init(driver, country_name, shop_name,
|
||||
risk_listing_filter)
|
||||
# 如果切换失败,直接返回
|
||||
if not switch_success or not switch_success_pg or not search_success:
|
||||
error_message = f"切换到国家({switch_success})/库存页面({switch_success_pg})/搜索筛选到指定选项({search_success}) {country_name} 失败,已重试 {max_retries} 次,跳过该国家"
|
||||
self.log(error_message, "ERROR")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
# # 如果没有需要审批的商品,直接返回
|
||||
if len(sku_ls) == 0:
|
||||
self.log(f"国家 {country_name} 没有需要审批的商品")
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_countries"] += 1
|
||||
return
|
||||
|
||||
self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...")
|
||||
|
||||
if target_url is not None: # 从失败的链接继续
|
||||
self.log(f"开始访问:{target_url}")
|
||||
driver.tab.get(url=target_url)
|
||||
driver.tab.wait.doc_loaded(timeout=60, raise_err=False)
|
||||
|
||||
result = []
|
||||
# 处理所有需要审批的商品(通过yield获取结果)
|
||||
for asin, status in driver.run_page_action(skip_asin):
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理ASIN", "WARNING")
|
||||
break
|
||||
|
||||
self.log(f"ASIN {asin} 处理结果: {status}")
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["current_asin"] = asin
|
||||
runing_task[task_id]["processed_asins"] += 1
|
||||
runing_task[task_id]["failed_count"] += 1
|
||||
|
||||
minimumPrice = ""
|
||||
if "有最低价跳过" in status:
|
||||
minimumPrice = skipAsinDetails.get(asin)
|
||||
result.append({
|
||||
"asin": asin,
|
||||
"status": status,
|
||||
"minimumPrice": minimumPrice,
|
||||
"done": False
|
||||
})
|
||||
|
||||
if len(result) > 10:
|
||||
self.post_result_batch(task_id, shop_name, country_code,result)
|
||||
result = []
|
||||
|
||||
if len(result) > 0:
|
||||
self.post_result_batch(task_id, shop_name, country_code, result)
|
||||
|
||||
self.log(f"国家 {country_name} 处理完成")
|
||||
|
||||
def post_stage_finished(self, task_id: int, user_id, stage_index):
|
||||
|
||||
if user_id in (None, "", 0):
|
||||
raise ValueError("user_id is required for stage completion callback")
|
||||
if stage_index is None:
|
||||
raise ValueError("stage_index is required for stage completion callback")
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/stage-finished"
|
||||
payload = {"stage_index": stage_index}
|
||||
params = {"user_id": user_id}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
self.log(f"Attempting stage completion callback ({retry + 1}/{max_retries})")
|
||||
response = requests.post(
|
||||
url,
|
||||
params=params,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False,
|
||||
)
|
||||
self.log(f"Stage completion callback response: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"Stage completion callback succeeded: task={task_id}, stage={stage_index}")
|
||||
return
|
||||
self.log(f"Stage completion callback failed, status={response.status_code}", "WARNING")
|
||||
except Exception as e:
|
||||
self.log(f"Stage completion callback exception: {str(e)}", "ERROR")
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
raise RuntimeError(f"Stage completion callback failed after retries: task={task_id}, stage={stage_index}")
|
||||
|
||||
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
country_code: 国家代码
|
||||
asin: ASIN
|
||||
status: 处理状态
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/result"
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"error": "",
|
||||
"countries": {
|
||||
country_code: [
|
||||
{
|
||||
"asin": asin,
|
||||
"status": status,
|
||||
"done": is_done
|
||||
}
|
||||
]
|
||||
},
|
||||
"shopName": shop_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
print("================【匹配】=====================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"结果回传成功: {asin} - {status}")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
|
||||
|
||||
def post_result_batch(self, task_id: int, shop_name: str, country_code: str, country_data:list,
|
||||
is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
shop_name: 店铺名称
|
||||
country_code: 国家代码
|
||||
asin: ASIN
|
||||
status: 处理状态
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/result"
|
||||
|
||||
payload = {
|
||||
"shops": [
|
||||
{
|
||||
"error": "",
|
||||
"countries": {
|
||||
country_code: country_data
|
||||
},
|
||||
"shopName": shop_name
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
print("================【匹配】=====================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=30,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"结果回传成功")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
user_info = {
|
||||
"company": "rongchuang123",
|
||||
"username": "自动化_Robot",
|
||||
"password": "#20zsg25"
|
||||
}
|
||||
shop_name = "魏振峰"
|
||||
country = "德国"
|
||||
kill_process('v6')
|
||||
driver = AmzoneMatchAction(user_info)
|
||||
browser = driver.open_shop(shop_name)
|
||||
sw_suc = driver.SwitchingCountries(country)
|
||||
driver.SwitchPage()
|
||||
risk_listing_filter = "All"
|
||||
for _ in range(3):
|
||||
try:
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
break
|
||||
except Exception as e:
|
||||
print(e)
|
||||
driver.tab.refresh()
|
||||
if len(sku_ls) > 0:
|
||||
print("有数据,开始操作")
|
||||
for asin, status in driver.run_page_action():
|
||||
print(f"ASIN {asin} 的处理结果: {status}")
|
||||
print("已完成操作")
|
||||
2515
app/amazon/patrol_delete.py
Normal file
2515
app/amazon/patrol_delete.py
Normal file
File diff suppressed because it is too large
Load Diff
1311
app/amazon/price_match.py
Normal file
1311
app/amazon/price_match.py
Normal file
File diff suppressed because one or more lines are too long
1270
app/amazon/price_match_旧.py
Normal file
1270
app/amazon/price_match_旧.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function clickElement(el) {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
if (el.focus) el.focus();
|
||||
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
function summarize(el) {
|
||||
return {
|
||||
tag: el.tagName,
|
||||
text: textOf(el).slice(0, 500),
|
||||
attrs: Array.from(el.attributes || []).reduce((acc, attr) => {
|
||||
acc[attr.name] = attr.value;
|
||||
return acc;
|
||||
}, {}),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||
};
|
||||
}
|
||||
|
||||
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||
|
||||
const selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||
if (!selectedRows.length) return { ok: false, reason: 'no selected complete draft rows' };
|
||||
|
||||
const bulkDropdown = view.querySelector('#bulk-dropdown');
|
||||
if (!bulkDropdown) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'bulk dropdown not found',
|
||||
selectedCount: selectedRows.length,
|
||||
};
|
||||
}
|
||||
|
||||
const toggle = bulkDropdown.shadowRoot && bulkDropdown.shadowRoot.querySelector('button[part="dropdown-button-toggle-button"], button');
|
||||
clickElement(toggle || bulkDropdown);
|
||||
|
||||
function findBulkDeleteAction() {
|
||||
const all = [];
|
||||
const seen = new Set();
|
||||
function walk(root) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) all.push(node);
|
||||
if (node.shadowRoot) walk(node.shadowRoot);
|
||||
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
|
||||
for (const assigned of node.assignedElements({ flatten: true })) walk(assigned);
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
walk(document);
|
||||
const exactAction = all.find((el) => {
|
||||
const action = String(el.getAttribute && el.getAttribute('data-action') || '');
|
||||
const text = textOf(el);
|
||||
return isVisible(el) && action === 'DELETE_DRAFTS' && /删除商品信息草稿/.test(text);
|
||||
});
|
||||
if (exactAction) return exactAction;
|
||||
|
||||
return all.find((el) => {
|
||||
const text = textOf(el);
|
||||
const attrs = Array.from(el.attributes || []).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
const haystack = `${text} ${attrs}`;
|
||||
if (!isVisible(el) || !/删除商品信息草稿/.test(haystack)) return false;
|
||||
if (/^(HTML|BODY|KAT-DATA-GRID)$/i.test(el.tagName || '') && text.length > 80) return false;
|
||||
if (/^DIV$/i.test(el.tagName || '') && text !== '删除商品信息草稿') return false;
|
||||
if ((el.tagName || '').toLowerCase() === 'kat-icon') return false;
|
||||
return /^(BUTTON|KAT-BUTTON|KAT-OPTION|KAT-DROPDOWN-OPTION|LI|A|SPAN|DIV)$/i.test(el.tagName || '');
|
||||
});
|
||||
}
|
||||
|
||||
const deleteAction = findBulkDeleteAction();
|
||||
|
||||
if (!deleteAction) {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
function walkCandidates(root) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName && isVisible(node)) {
|
||||
const text = textOf(node);
|
||||
const attrs = Array.from(node.attributes || []).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
if (/删除|草稿|选择组操作|menu|option/i.test(`${text} ${attrs}`)) candidates.push(summarize(node));
|
||||
}
|
||||
if (node.shadowRoot) walkCandidates(node.shadowRoot);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
walkCandidates(document);
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'bulk delete draft action not found',
|
||||
menuOpened: true,
|
||||
selectedCount: selectedRows.length,
|
||||
bulkDropdown: summarize(bulkDropdown),
|
||||
candidates: candidates.slice(0, 50),
|
||||
};
|
||||
}
|
||||
|
||||
clickElement(deleteAction);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
selectedCount: selectedRows.length,
|
||||
bulkDropdown: summarize(bulkDropdown),
|
||||
clicked: summarize(deleteAction),
|
||||
rows: selectedRows.slice(0, 10).map(summarize),
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function rowOf(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text: textOf(el),
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2500),
|
||||
};
|
||||
}
|
||||
|
||||
const candidates = allElements()
|
||||
.map((el, index) => rowOf(el, index))
|
||||
.filter((row) => row.visible && /补全草稿|快速查看|quick view|draft/i.test(`${row.tag} ${row.text} ${Object.values(row.attrs).join(' ')}`))
|
||||
.slice(0, 120);
|
||||
|
||||
return candidates;
|
||||
@@ -0,0 +1,73 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function attrsText(attrs) {
|
||||
return Object.entries(attrs).map(([key, value]) => `${key}=${value}`).join(' ');
|
||||
}
|
||||
|
||||
const pattern = /快速查看|quick|draft|草稿|提交|submitted|missing|inventory|myinventory/i;
|
||||
const elements = allElements()
|
||||
.map((el, index) => {
|
||||
const attrs = attrsOf(el);
|
||||
const text = textOf(el);
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text: text.slice(0, 500),
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||
haystack: `${el.tagName} ${text} ${attrsText(attrs)}`,
|
||||
};
|
||||
})
|
||||
.filter((row) => pattern.test(row.haystack))
|
||||
.map(({ haystack, ...row }) => row)
|
||||
.slice(0, 80);
|
||||
|
||||
const resources = performance
|
||||
.getEntriesByType('resource')
|
||||
.map((entry) => entry.name)
|
||||
.filter((url) => pattern.test(url))
|
||||
.slice(-120);
|
||||
|
||||
return {
|
||||
location: window.location.href,
|
||||
readyState: document.readyState,
|
||||
capturedResponses: (window.__crawlerCompleteDraftResponses || []).slice(-10),
|
||||
elements,
|
||||
resources,
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function hasCountText(text) {
|
||||
return /[((]\s*[\d,]+\s*[))]/.test(text);
|
||||
}
|
||||
|
||||
function insideListingRow(el) {
|
||||
return Boolean(el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]'));
|
||||
}
|
||||
|
||||
function rowOf(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const text = textOf(el);
|
||||
const labelText = attrs.label || attrs.title || attrs['aria-label'] || '';
|
||||
const valueText = attrs.value || attrs.count || attrs['data-count'] || attrs['data-value'] || '';
|
||||
return {
|
||||
index,
|
||||
raw_text: text,
|
||||
label_text: labelText,
|
||||
quantity_text: valueText,
|
||||
attrs,
|
||||
visible: isVisible(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
function splitCountRow(el, index) {
|
||||
const countText = textOf(el);
|
||||
if (!/^\s*[\d,]+\s*$/.test(countText)) return null;
|
||||
|
||||
let parent = el.parentElement;
|
||||
for (let depth = 0; parent && depth < 4; depth += 1, parent = parent.parentElement) {
|
||||
const parentText = textOf(parent);
|
||||
const labelText = parentText
|
||||
.replace(new RegExp(`(^|\\s)${countText.replace(/,/g, '\\,')}(\\s|$)`), ' ')
|
||||
.replace(/[((]\s*[\d,]+\s*[))]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (labelText && labelText !== parentText && !/^\s*[\d,]+\s*$/.test(labelText)) {
|
||||
return {
|
||||
index,
|
||||
raw_text: '',
|
||||
label_text: labelText,
|
||||
quantity_text: countText,
|
||||
attrs: attrsOf(parent),
|
||||
visible: isVisible(parent),
|
||||
outerHTML: (parent.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const regionRows = allElements().filter((el) => isVisible(el) && !insideListingRow(el));
|
||||
|
||||
const byText = new Map();
|
||||
regionRows
|
||||
.forEach((el, index) => {
|
||||
const row = rowOf(el, index);
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.quantity_text}`;
|
||||
if (row.raw_text.length > 200) return;
|
||||
if (/[((]\s*[\d,]+\s*[))]/.test(combined)) {
|
||||
const key = combined;
|
||||
if (!byText.has(key)) byText.set(key, row);
|
||||
return;
|
||||
}
|
||||
|
||||
const splitRow = splitCountRow(el, index);
|
||||
if (splitRow) {
|
||||
const key = `${splitRow.label_text} ${splitRow.quantity_text}`;
|
||||
if (!byText.has(key)) byText.set(key, splitRow);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
region: {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
},
|
||||
rows: Array.from(byText.values()).filter((row) => {
|
||||
const combined = `${row.raw_text} ${row.label_text}`.toLowerCase();
|
||||
return !/所有商品|启售商品|改善商品信息/.test(combined);
|
||||
}),
|
||||
candidates: Array.from(byText.values()).slice(0, 80),
|
||||
};
|
||||
100
app/amazon/scripts/patrol_delete/complete_draft_select_all.js
Normal file
100
app/amazon/scripts/patrol_delete/complete_draft_select_all.js
Normal file
@@ -0,0 +1,100 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function clickElement(el) {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
if (el.focus) el.focus();
|
||||
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
function summarizeRow(row) {
|
||||
return {
|
||||
rowId: row.getAttribute('row-id') || '',
|
||||
text: textOf(row).slice(0, 500),
|
||||
selected: row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected'),
|
||||
};
|
||||
}
|
||||
|
||||
const payload = arguments[0] || {};
|
||||
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||
|
||||
const rows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||
.filter((row) => isVisible(row));
|
||||
const rowCount = rows.length;
|
||||
const selectedBefore = rows.filter((row) => row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')).length;
|
||||
const maxSelectCountRaw = Number(payload.maxSelectCount || 0);
|
||||
const maxSelectCount = Number.isFinite(maxSelectCountRaw) && maxSelectCountRaw > 0 ? Math.floor(maxSelectCountRaw) : 0;
|
||||
|
||||
if (payload.probeOnly) {
|
||||
return {
|
||||
ok: true,
|
||||
probeOnly: true,
|
||||
rowCount,
|
||||
selectedCount: selectedBefore,
|
||||
rows: rows.slice(0, 10).map(summarizeRow),
|
||||
};
|
||||
}
|
||||
|
||||
if (rowCount <= 0) {
|
||||
return { ok: false, reason: 'no complete draft rows', rowCount, selectedCount: selectedBefore };
|
||||
}
|
||||
|
||||
const headerCheckbox = view.querySelector('.ag-header-select-all input[type="checkbox"]');
|
||||
if (!headerCheckbox) {
|
||||
return { ok: false, reason: 'complete draft select-all checkbox not found', rowCount, selectedCount: selectedBefore };
|
||||
}
|
||||
|
||||
if (maxSelectCount > 0 && maxSelectCount < rowCount) {
|
||||
if (headerCheckbox.checked) {
|
||||
clickElement(headerCheckbox);
|
||||
}
|
||||
for (const row of rows) {
|
||||
if (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')) {
|
||||
const checkbox = row.querySelector('input[type="checkbox"]');
|
||||
if (checkbox && checkbox.checked) clickElement(checkbox);
|
||||
}
|
||||
}
|
||||
for (const row of rows.slice(0, maxSelectCount)) {
|
||||
const checkbox = row.querySelector('input[type="checkbox"]');
|
||||
if (checkbox && !checkbox.checked) {
|
||||
clickElement(checkbox);
|
||||
} else if (!checkbox) {
|
||||
clickElement(row);
|
||||
}
|
||||
}
|
||||
} else if (!headerCheckbox.checked || selectedBefore < rowCount) {
|
||||
clickElement(headerCheckbox);
|
||||
}
|
||||
|
||||
let selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||
|
||||
if (selectedRows.length <= 0) {
|
||||
const firstRowCheckbox = rows[0] && rows[0].querySelector('input[type="checkbox"]');
|
||||
if (firstRowCheckbox && !firstRowCheckbox.checked) {
|
||||
clickElement(firstRowCheckbox);
|
||||
}
|
||||
}
|
||||
|
||||
selectedRows = Array.from(view.querySelectorAll('.ag-center-cols-container [role="row"]'))
|
||||
.filter((row) => isVisible(row) && (row.getAttribute('aria-selected') === 'true' || row.classList.contains('ag-row-selected')));
|
||||
|
||||
return {
|
||||
ok: selectedRows.length > 0,
|
||||
rowCount,
|
||||
selectedCount: selectedRows.length,
|
||||
headerChecked: headerCheckbox.checked,
|
||||
rows: selectedRows.slice(0, 10).map(summarizeRow),
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function clickElement(el) {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
if (el.focus) el.focus();
|
||||
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
function summarize(el) {
|
||||
return {
|
||||
tag: el.tagName,
|
||||
text: textOf(el),
|
||||
className: el.className || '',
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||
};
|
||||
}
|
||||
|
||||
const targetTag = String((arguments[0] && arguments[0].tag) || '').replace(/\s+/g, ' ').trim();
|
||||
if (!targetTag) return { ok: false, reason: 'target tag empty' };
|
||||
|
||||
const view = document.querySelector('[data-testid="complete-drafts-view"]');
|
||||
if (!view) return { ok: false, reason: 'complete drafts view not found' };
|
||||
|
||||
const chips = Array.from(view.querySelectorAll('.JanusChip-module__janusChipContainer--gRjdp'));
|
||||
const chip = chips.find((el) => {
|
||||
const text = textOf(el);
|
||||
return text === targetTag || text.startsWith(`${targetTag} (`) || text.startsWith(`${targetTag}(`);
|
||||
});
|
||||
|
||||
if (!chip) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'complete draft chip not found',
|
||||
tag: targetTag,
|
||||
chips: chips.map(summarize),
|
||||
};
|
||||
}
|
||||
|
||||
if (!/active/.test(String(chip.className || ''))) {
|
||||
clickElement(chip);
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
tag: targetTag,
|
||||
clicked: summarize(chip),
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
if (!el || !el.attributes) return '';
|
||||
return Array.from(el.attributes).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
|
||||
for (const assigned of node.assignedElements({ flatten: true })) {
|
||||
walkRoots(assigned, rows, seen);
|
||||
}
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements(root) {
|
||||
const rows = [];
|
||||
walkRoots(root || document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function clickElement(el) {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
if (el.focus) el.focus();
|
||||
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
function clickTargetFor(el) {
|
||||
if (el && el.shadowRoot) {
|
||||
const button = el.shadowRoot.querySelector('button:not([disabled]), [role="button"]:not([disabled])');
|
||||
if (button) return button;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
const modal = document.querySelector('kat-modal[data-testid="action-modal"]') ||
|
||||
allElements(document).find((el) => /删除|Delete/i.test(textOf(el)) && (el.tagName || '').toLowerCase() === 'kat-modal');
|
||||
|
||||
if (!modal) return { ok: false, reason: 'delete modal not found' };
|
||||
|
||||
const buttons = allElements(modal).filter((el) => {
|
||||
const haystack = `${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`;
|
||||
return isVisible(el) && /kat-button|button/i.test(el.tagName || '') && /variant=primary|删除|确认|Delete|Confirm/i.test(haystack);
|
||||
});
|
||||
const button = buttons[0];
|
||||
|
||||
if (!button) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'confirm button not found',
|
||||
modalText: textOf(modal),
|
||||
modalHTML: (modal.outerHTML || '').slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
const target = clickTargetFor(button);
|
||||
clickElement(target);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
clicked: {
|
||||
tag: button.tagName,
|
||||
text: textOf(button),
|
||||
attrs: attrsText(button),
|
||||
targetTag: target.tagName,
|
||||
},
|
||||
};
|
||||
46
app/amazon/scripts/patrol_delete/delete_success_messages.js
Normal file
46
app/amazon/scripts/patrol_delete/delete_success_messages.js
Normal file
@@ -0,0 +1,46 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
if (!el || !el.attributes) return '';
|
||||
return Array.from(el.attributes).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
|
||||
for (const assigned of node.assignedElements({ flatten: true })) {
|
||||
walkRoots(assigned, rows, seen);
|
||||
}
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
|
||||
const seen = new Set();
|
||||
return rows
|
||||
.filter((el) => {
|
||||
const haystack = `${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`;
|
||||
return isVisible(el) && /kat-alert|alert|toast|notification|已删除|已经删除|15\s*分钟|success/i.test(haystack);
|
||||
})
|
||||
.map((el) => textOf(el) || attrsText(el))
|
||||
.filter((text) => text && !seen.has(text) && seen.add(text))
|
||||
.slice(0, 30);
|
||||
187
app/amazon/scripts/patrol_delete/inventory_delete_selected.js
Normal file
187
app/amazon/scripts/patrol_delete/inventory_delete_selected.js
Normal file
@@ -0,0 +1,187 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
if (!el || !el.attributes) return '';
|
||||
return Array.from(el.attributes).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
if ((node.tagName || '').toLowerCase() === 'slot' && node.assignedElements) {
|
||||
for (const assigned of node.assignedElements({ flatten: true })) {
|
||||
walkRoots(assigned, rows, seen);
|
||||
}
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements(root) {
|
||||
const rows = [];
|
||||
walkRoots(root || document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function closestDeep(el, selector) {
|
||||
let node = el;
|
||||
while (node) {
|
||||
if (node.closest) {
|
||||
const found = node.closest(selector);
|
||||
if (found) return found;
|
||||
}
|
||||
const root = node.getRootNode && node.getRootNode();
|
||||
node = root && root.host ? root.host : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function insideInventoryRow(el) {
|
||||
return Boolean(closestDeep(el, 'div[data-sku]'));
|
||||
}
|
||||
|
||||
function isDeleteAction(el) {
|
||||
const haystack = `${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`;
|
||||
return /data-action=DeleteListing|action=DeleteListing|删除商品信息|删除商品和报价|DeleteListing|delete\s+listing/i.test(haystack);
|
||||
}
|
||||
|
||||
function clickElement(el) {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
if (el.focus) el.focus();
|
||||
for (const eventName of ['pointerdown', 'mousedown', 'pointerup', 'mouseup']) {
|
||||
el.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, view: window }));
|
||||
}
|
||||
el.click();
|
||||
}
|
||||
|
||||
function hostDropdownOf(el) {
|
||||
const host = closestDeep(el, 'kat-dropdown-button');
|
||||
return host || ((el.tagName || '').toLowerCase() === 'kat-dropdown-button' ? el : null);
|
||||
}
|
||||
|
||||
function dropdownToggleOf(dropdown, fallback) {
|
||||
if (!dropdown) return fallback;
|
||||
if (fallback && textOf(fallback)) return fallback;
|
||||
return fallback || dropdown;
|
||||
}
|
||||
|
||||
function openBulkActionMenu(candidate) {
|
||||
const dropdown = hostDropdownOf(candidate);
|
||||
const toggle = dropdownToggleOf(dropdown, candidate);
|
||||
clickElement(toggle);
|
||||
return { dropdown, toggle };
|
||||
}
|
||||
|
||||
function summarize(el) {
|
||||
return {
|
||||
tag: el.tagName,
|
||||
text: textOf(el),
|
||||
attrs: attrsText(el),
|
||||
visible: isVisible(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||
};
|
||||
}
|
||||
|
||||
function findVisibleBulkDeleteAction() {
|
||||
return allElements(document).find((el) => isDeleteAction(el) && isVisible(el) && !insideInventoryRow(el));
|
||||
}
|
||||
|
||||
function findDeleteActionAfterMenuOpen(candidate) {
|
||||
const dropdown = hostDropdownOf(candidate) || candidate;
|
||||
const exactMenuButton = allElements(dropdown).find((child) => {
|
||||
const tag = (child.tagName || '').toLowerCase();
|
||||
const role = child.getAttribute && child.getAttribute('role');
|
||||
return tag === 'button' && role === 'menuitem' && isDeleteAction(child);
|
||||
});
|
||||
if (exactMenuButton) return { mode: 'bulk-action-menuitem-delete', el: exactMenuButton };
|
||||
|
||||
const nestedDeleteAction = allElements(dropdown).find((child) => isDeleteAction(child) && isVisible(child));
|
||||
if (nestedDeleteAction) return { mode: 'bulk-action-nested-delete', el: nestedDeleteAction };
|
||||
|
||||
const deleteAction = findVisibleBulkDeleteAction();
|
||||
if (deleteAction) return { mode: 'bulk-action-visible-delete', el: deleteAction };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isBulkActionCandidate(el) {
|
||||
const tag = (el.tagName || '').toLowerCase();
|
||||
if (!/^(button|kat-button|kat-dropdown-button|kat-select|kat-popover|kat-menu-button)$/.test(tag)) return false;
|
||||
if (!isVisible(el) || insideInventoryRow(el)) return false;
|
||||
const haystack = `${tag} ${attrsText(el)} ${textOf(el)}`;
|
||||
return /DeleteListing|删除商品信息|删除|操作|更多|Action|Actions|selected|选定|已选择|批量/i.test(haystack);
|
||||
}
|
||||
|
||||
const directDelete = findVisibleBulkDeleteAction();
|
||||
if (directDelete) {
|
||||
clickElement(directDelete);
|
||||
return { ok: true, mode: 'direct-delete-button', clicked: summarize(directDelete) };
|
||||
}
|
||||
|
||||
const rows = allElements(document);
|
||||
const candidates = rows.filter(isBulkActionCandidate);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const openResult = openBulkActionMenu(candidate);
|
||||
const deleteAction = findDeleteActionAfterMenuOpen(candidate);
|
||||
if (deleteAction) {
|
||||
clickElement(deleteAction.el);
|
||||
return {
|
||||
ok: true,
|
||||
mode: deleteAction.mode,
|
||||
candidate: summarize(candidate),
|
||||
opened: {
|
||||
dropdown: openResult.dropdown ? summarize(openResult.dropdown) : null,
|
||||
toggle: openResult.toggle ? summarize(openResult.toggle) : null,
|
||||
},
|
||||
clicked: summarize(deleteAction.el),
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'bulk action menu opened; delete action not visible yet',
|
||||
menuOpened: true,
|
||||
candidate: summarize(candidate),
|
||||
opened: {
|
||||
dropdown: openResult.dropdown ? summarize(openResult.dropdown) : null,
|
||||
toggle: openResult.toggle ? summarize(openResult.toggle) : null,
|
||||
},
|
||||
deleteLikeElements: allElements(document)
|
||||
.filter((el) => /DeleteListing|删除商品信息|删除商品和报价|delete\s+listing/i.test(`${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`) && !insideInventoryRow(el))
|
||||
.slice(0, 20)
|
||||
.map(summarize),
|
||||
};
|
||||
} catch (error) {
|
||||
// Try the next non-row bulk action control.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'bulk delete action not found',
|
||||
candidates: candidates.slice(0, 20).map(summarize),
|
||||
deleteLikeElements: rows
|
||||
.filter((el) => /DeleteListing|删除商品信息|删除商品和报价|delete\s+listing/i.test(`${el.tagName || ''} ${attrsText(el)} ${textOf(el)}`) && !insideInventoryRow(el))
|
||||
.slice(0, 20)
|
||||
.map(summarize),
|
||||
visibleButtons: rows
|
||||
.filter((el) => /^(button|kat-button|kat-dropdown-button)$/i.test(el.tagName || '') && isVisible(el))
|
||||
.slice(0, 20)
|
||||
.map(summarize),
|
||||
};
|
||||
71
app/amazon/scripts/patrol_delete/inventory_select_all.js
Normal file
71
app/amazon/scripts/patrol_delete/inventory_select_all.js
Normal file
@@ -0,0 +1,71 @@
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function insideInventoryRow(el) {
|
||||
return Boolean(el && el.closest && el.closest('div[data-sku]'));
|
||||
}
|
||||
|
||||
function clickTargetFor(checkbox) {
|
||||
if (!checkbox) return null;
|
||||
if (checkbox.shadowRoot) {
|
||||
const shadowTarget = checkbox.shadowRoot.querySelector('[role="checkbox"], .checkbox');
|
||||
if (shadowTarget) return shadowTarget;
|
||||
}
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
const rowCount = document.querySelectorAll('div[data-sku]').length;
|
||||
if (rowCount <= 0) {
|
||||
return { ok: false, reason: 'no inventory rows after status filter', rowCount };
|
||||
}
|
||||
|
||||
const checkboxes = allElements()
|
||||
.filter((el) => (el.tagName || '').toLowerCase() === 'kat-checkbox' && isVisible(el))
|
||||
.sort((a, b) => {
|
||||
const rectA = a.getBoundingClientRect();
|
||||
const rectB = b.getBoundingClientRect();
|
||||
return rectA.top - rectB.top || rectA.left - rectB.left;
|
||||
});
|
||||
|
||||
const headerCheckbox = checkboxes.find((el) => !insideInventoryRow(el)) || checkboxes[0];
|
||||
if (!headerCheckbox) {
|
||||
return { ok: false, reason: 'select-all checkbox not found', rowCount, checkboxCount: checkboxes.length };
|
||||
}
|
||||
|
||||
const target = clickTargetFor(headerCheckbox);
|
||||
target.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
target.click();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
rowCount,
|
||||
checkboxCount: checkboxes.length,
|
||||
clicked: {
|
||||
tag: headerCheckbox.tagName,
|
||||
insideInventoryRow: insideInventoryRow(headerCheckbox),
|
||||
outerHTML: (headerCheckbox.outerHTML || '').slice(0, 1000),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseStatus(rawText) {
|
||||
const text = normalize(rawText);
|
||||
if (!text) return '';
|
||||
const match = text.match(/^(.*?)\s*[((]\s*[\d,]+\s*[))]\s*$/);
|
||||
return normalize(match ? match[1] : text);
|
||||
}
|
||||
|
||||
function collectStatusTargets() {
|
||||
const pattern = /(全部|在搜索结果中禁止显示|配送问题|缺少报价|已停售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|不可售|在售)\s*[((]?\s*[\d,]*\s*[))]?/g;
|
||||
const hits = [];
|
||||
|
||||
for (const el of allElements()) {
|
||||
if (!isVisible(el)) continue;
|
||||
const text = normalize(textOf(el));
|
||||
if (!text) continue;
|
||||
const match = text.match(pattern);
|
||||
if (!match) continue;
|
||||
const parsed = parseStatus(match[0]);
|
||||
hits.push({
|
||||
el,
|
||||
text,
|
||||
parsed,
|
||||
tag: el.tagName,
|
||||
clickable: Boolean(el.onclick) || el.matches('button,[role="button"],kat-option,kat-label,a,[tabindex]'),
|
||||
});
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
const targetStatus = normalize((__crawlerPayload || {}).status);
|
||||
const candidates = collectStatusTargets()
|
||||
.filter((item) => item.parsed === targetStatus || item.text.includes(targetStatus))
|
||||
.map((item) => ({
|
||||
...item,
|
||||
score:
|
||||
(item.parsed === targetStatus ? 300 : 0) +
|
||||
(item.clickable ? 120 : 0) +
|
||||
(/^[^ ]+\s*[((]\s*[\d,]+\s*[))]$/.test(item.text) ? 80 : 0) +
|
||||
(/button|option|label/i.test(item.tag) ? 30 : 0),
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (!candidates.length) {
|
||||
return { ok: false, reason: 'visible status target not found', targetStatus };
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const chain = [candidate.el];
|
||||
let node = candidate.el.parentElement;
|
||||
for (let depth = 0; node && depth < 5; depth += 1, node = node.parentElement) {
|
||||
chain.push(node);
|
||||
}
|
||||
|
||||
for (const el of chain) {
|
||||
try {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
el.click();
|
||||
return {
|
||||
ok: true,
|
||||
targetStatus,
|
||||
clicked: {
|
||||
tag: el.tagName,
|
||||
text: normalize(textOf(el)).slice(0, 300),
|
||||
},
|
||||
matched: {
|
||||
tag: candidate.tag,
|
||||
text: candidate.text.slice(0, 300),
|
||||
parsed: candidate.parsed,
|
||||
score: candidate.score,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// try parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'visible status target click failed',
|
||||
targetStatus,
|
||||
candidates: candidates.slice(0, 10).map((item) => ({
|
||||
tag: item.tag,
|
||||
text: item.text.slice(0, 300),
|
||||
parsed: item.parsed,
|
||||
score: item.score,
|
||||
clickable: item.clickable,
|
||||
})),
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }));
|
||||
document.body.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
||||
return true;
|
||||
@@ -0,0 +1,95 @@
|
||||
const keywords = ['ListingStatus', 'listing status', '商品状态', '商品狀態', 'status', 'filter', '库存'];
|
||||
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function nearestLabel(el) {
|
||||
const parts = [];
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 6; depth += 1, node = node.parentElement) {
|
||||
const text = textOf(node);
|
||||
if (text) parts.push(text.slice(0, 400));
|
||||
}
|
||||
const id = el.getAttribute('id');
|
||||
if (id) {
|
||||
document.querySelectorAll(`label[for="${CSS.escape(id)}"]`).forEach((label) => {
|
||||
const text = textOf(label);
|
||||
if (text) parts.unshift(text);
|
||||
});
|
||||
}
|
||||
return [...new Set(parts)].join(' | ');
|
||||
}
|
||||
|
||||
function hasKeyword(text) {
|
||||
return keywords.some((keyword) => text.toLowerCase().includes(keyword.toLowerCase()));
|
||||
}
|
||||
|
||||
function candidateRow(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const attrText = Object.entries(attrs).map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
const label = nearestLabel(el);
|
||||
const text = textOf(el);
|
||||
const haystack = `${el.tagName} ${attrText} ${text} ${label}`;
|
||||
let score = 0;
|
||||
if (/商品状态|商品狀態|ListingStatus|listing status/i.test(haystack)) score += 100;
|
||||
if (/status/i.test(haystack)) score += 25;
|
||||
if (/filter|库存/i.test(haystack)) score += 10;
|
||||
if (isVisible(el)) score += 5;
|
||||
if (el.closest('[data-sku], tr, [role="row"]')) score -= 50;
|
||||
return {
|
||||
index,
|
||||
score,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text,
|
||||
label,
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
const rows = allElements();
|
||||
const candidates = [];
|
||||
rows.forEach((el, index) => {
|
||||
const tag = (el.tagName || '').toLowerCase();
|
||||
const haystack = `${tag} ${textOf(el)} ${Object.values(attrsOf(el)).join(' ')} ${nearestLabel(el)}`;
|
||||
if (tag === 'kat-dropdown' || tag === 'kat-option' || hasKeyword(haystack)) {
|
||||
candidates.push(candidateRow(el, index));
|
||||
}
|
||||
});
|
||||
|
||||
return candidates.sort((a, b) => b.score - a.score).slice(0, 80);
|
||||
295
app/amazon/scripts/patrol_delete/listing_status_dropdown_open.js
Normal file
295
app/amazon/scripts/patrol_delete/listing_status_dropdown_open.js
Normal file
@@ -0,0 +1,295 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
if (!el || !el.attributes) return '';
|
||||
return Array.from(el.attributes).map((attr) => `${attr.name}=${attr.value}`).join(' ');
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isDisabled(el) {
|
||||
if (!el) return false;
|
||||
return (
|
||||
el.hasAttribute('disabled') ||
|
||||
el.disabled === true ||
|
||||
el.getAttribute('aria-disabled') === 'true' ||
|
||||
/\bdisabled\b/i.test(el.getAttribute('class') || '')
|
||||
);
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function nearestLabel(el) {
|
||||
const parts = [];
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 7; depth += 1, node = node.parentElement) {
|
||||
const text = textOf(node);
|
||||
if (text) parts.push(text.slice(0, 500));
|
||||
}
|
||||
const id = el.getAttribute('id');
|
||||
if (id) {
|
||||
document.querySelectorAll(`label[for="${CSS.escape(id)}"]`).forEach((label) => {
|
||||
const text = textOf(label);
|
||||
if (text) parts.unshift(text);
|
||||
});
|
||||
}
|
||||
return [...new Set(parts)].join(' | ');
|
||||
}
|
||||
|
||||
function optionTextOf(dropdown) {
|
||||
const roots = [dropdown];
|
||||
if (dropdown.shadowRoot) roots.push(dropdown.shadowRoot);
|
||||
const texts = [];
|
||||
for (const root of roots) {
|
||||
for (const option of Array.from(root.querySelectorAll('kat-option, kat-label, [role="option"], [class*="parentKatOptionStyle"]'))) {
|
||||
texts.push(`${textOf(option)} ${attrsText(option)}`);
|
||||
}
|
||||
}
|
||||
return normalize(texts.join(' '));
|
||||
}
|
||||
|
||||
function isSearchFieldDropdown(text) {
|
||||
return /(^|\s)(SKUS?|ASIN|FNSKU|UPC\/EAN|TITLE_KEYWORD|GTIN)(\s|$)|商品名称\/关键字|data-value=(SKU|ASIN|FNSKU|TITLE_KEYWORD|GTIN)|value=(SKU|ASIN|FNSKU|TITLE_KEYWORD|GTIN)/i.test(text);
|
||||
}
|
||||
|
||||
function statusHitCount(text) {
|
||||
const matches = normalize(text).match(
|
||||
/(全部|在售|不可售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|配送问题|缺少报价|已停售|在搜索结果中禁止显示)\s*(?:[((]\s*[\d,]+\s*[))])?/g
|
||||
);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
function scoreDropdown(el) {
|
||||
const text = textOf(el);
|
||||
const label = nearestLabel(el);
|
||||
const attrs = attrsText(el);
|
||||
const optionText = optionTextOf(el);
|
||||
const haystack = `${el.tagName} ${attrs} ${text} ${label} ${optionText}`;
|
||||
const statusHits = statusHitCount(`${text} ${label} ${optionText}`);
|
||||
const isStatusDropdown = /statusDropdown|商品状态|商品狀態|ListingStatus|listing status/i.test(haystack);
|
||||
let score = 0;
|
||||
if (isSearchFieldDropdown(haystack)) score -= 1000;
|
||||
if (isStatusDropdown) score += 500;
|
||||
if (/商品状态|商品狀態|ListingStatus|listing status/i.test(haystack)) score += 120;
|
||||
if (/\bstatus\b|状态/i.test(haystack)) score += 25;
|
||||
if (/filter|筛选|筛選|库存/i.test(haystack)) score += 10;
|
||||
if (/全部\s*[((]\s*[\d,]+\s*[))]/.test(`${text} ${label} ${optionText}`)) score += 300;
|
||||
if (statusHits >= 2) score += statusHits * 180;
|
||||
if (!isStatusDropdown && statusHits === 0) score -= 200;
|
||||
if (isVisible(el)) score += 10;
|
||||
if (el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]')) score -= 80;
|
||||
if (isDisabled(el)) score -= 20;
|
||||
return { score, text, label, haystack, statusHits, disabled: isDisabled(el) };
|
||||
}
|
||||
|
||||
function candidateTriggers(dropdown) {
|
||||
const selectors = [
|
||||
'kat-dropdown-button',
|
||||
'kat-button',
|
||||
'button',
|
||||
'[role="button"]',
|
||||
'[aria-haspopup]',
|
||||
'[part="trigger"]',
|
||||
'[part="button"]',
|
||||
'.header',
|
||||
'.trigger',
|
||||
'.button',
|
||||
'span',
|
||||
'div',
|
||||
];
|
||||
|
||||
const seen = new Set();
|
||||
const triggers = [];
|
||||
const roots = [dropdown];
|
||||
if (dropdown.shadowRoot) roots.push(dropdown.shadowRoot);
|
||||
|
||||
for (const root of roots) {
|
||||
for (const selector of selectors) {
|
||||
for (const el of Array.from(root.querySelectorAll(selector))) {
|
||||
if (!el || seen.has(el)) continue;
|
||||
seen.add(el);
|
||||
const text = textOf(el);
|
||||
const attrs = attrsText(el);
|
||||
const score =
|
||||
(isVisible(el) ? 20 : 0) +
|
||||
(/全部\s*[((]\s*[\d,]+\s*[))]/.test(text) ? 200 : 0) +
|
||||
(/商品状态|商品狀態|ListingStatus|listing status/i.test(`${text} ${attrs}`) ? 120 : 0) +
|
||||
(/button|trigger|toggle|dropdown|expand/i.test(`${el.tagName} ${attrs}`) ? 40 : 0);
|
||||
triggers.push({ el, text, attrs, score });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return triggers
|
||||
.filter((item) => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
function clickTrigger(dropdown) {
|
||||
const triggers = candidateTriggers(dropdown);
|
||||
for (const trigger of triggers) {
|
||||
try {
|
||||
trigger.el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
trigger.el.click();
|
||||
return {
|
||||
ok: true,
|
||||
trigger: {
|
||||
tag: trigger.el.tagName,
|
||||
text: trigger.text,
|
||||
attrs: trigger.attrs,
|
||||
score: trigger.score,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// continue
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
dropdown.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
dropdown.click();
|
||||
return {
|
||||
ok: true,
|
||||
trigger: {
|
||||
tag: dropdown.tagName,
|
||||
text: textOf(dropdown),
|
||||
attrs: attrsText(dropdown),
|
||||
score: -1,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: String(error),
|
||||
tried: triggers.slice(0, 10).map((item) => ({
|
||||
tag: item.el.tagName,
|
||||
text: item.text,
|
||||
attrs: item.attrs,
|
||||
score: item.score,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const dropdowns = allElements().filter((el) => el.tagName.toLowerCase() === 'kat-dropdown');
|
||||
const ranked = dropdowns
|
||||
.map((el, index) => ({ el, index, ...scoreDropdown(el) }))
|
||||
.filter((row) => row.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (!ranked.length) {
|
||||
return { ok: false, reason: 'listing status kat-dropdown not found', candidates: [] };
|
||||
}
|
||||
|
||||
const selected = ranked[0];
|
||||
if (selected.disabled) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'listing status dropdown disabled/loading',
|
||||
selected: {
|
||||
index: selected.index,
|
||||
score: selected.score,
|
||||
statusHits: selected.statusHits,
|
||||
text: selected.text,
|
||||
label: selected.label,
|
||||
attrs: attrsOf(selected.el),
|
||||
outerHTML: (selected.el.outerHTML || '').slice(0, 2000),
|
||||
},
|
||||
candidates: ranked.slice(0, 10).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
statusHits: row.statusHits,
|
||||
disabled: row.disabled,
|
||||
text: row.text,
|
||||
label: row.label,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
selected.el.setAttribute('data-crawler-listing-status-dropdown', 'true');
|
||||
const clickResult = clickTrigger(selected.el);
|
||||
|
||||
if (!clickResult.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'listing status dropdown trigger click failed',
|
||||
selected: {
|
||||
index: selected.index,
|
||||
score: selected.score,
|
||||
text: selected.text,
|
||||
label: selected.label,
|
||||
attrs: attrsText(selected.el),
|
||||
outerHTML: (selected.el.outerHTML || '').slice(0, 2000),
|
||||
},
|
||||
clickResult,
|
||||
candidates: ranked.slice(0, 10).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
text: row.text,
|
||||
label: row.label,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
selected: {
|
||||
index: selected.index,
|
||||
score: selected.score,
|
||||
statusHits: selected.statusHits,
|
||||
text: selected.text,
|
||||
label: selected.label,
|
||||
attrs: attrsText(selected.el),
|
||||
outerHTML: (selected.el.outerHTML || '').slice(0, 2000),
|
||||
},
|
||||
trigger: clickResult.trigger,
|
||||
candidates: ranked.slice(0, 10).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
statusHits: row.statusHits,
|
||||
disabled: row.disabled,
|
||||
text: row.text,
|
||||
label: row.label,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function findMarkedDropdown() {
|
||||
return allElements().find((el) => el.getAttribute('data-crawler-listing-status-dropdown') === 'true') || null;
|
||||
}
|
||||
|
||||
function insideListingRow(el) {
|
||||
return Boolean(el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]'));
|
||||
}
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isOptionLike(el) {
|
||||
if (!el || !el.tagName) return false;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const role = el.getAttribute('role');
|
||||
const className = el.getAttribute('class') || '';
|
||||
return (
|
||||
tag === 'kat-option' ||
|
||||
tag === 'kat-label' ||
|
||||
role === 'option' ||
|
||||
/parentKatOptionStyle/i.test(className)
|
||||
);
|
||||
}
|
||||
|
||||
function parseStatus(rawText) {
|
||||
const text = normalize(rawText);
|
||||
if (!text) return '';
|
||||
const match = text.match(/^(.*?)\s*[((]\s*[\d,]+\s*[))]\s*$/);
|
||||
return normalize(match ? match[1] : text);
|
||||
}
|
||||
|
||||
function isListingStatusOption(row) {
|
||||
const text = normalize(`${row.raw_text} ${row.value || ''}`);
|
||||
const parsed = parseStatus(row.raw_text);
|
||||
const haystack = normalize(`${text} ${parsed}`);
|
||||
if (/^(所有|SKUS?|ASIN|FNSKU|UPC\/EAN|商品名称\/关键字)$|^(ALL|SKU|ASIN|FNSKU|TITLE_KEYWORD|GTIN)$/i.test(haystack)) {
|
||||
return false;
|
||||
}
|
||||
return /^(全部|在售|不可售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|配送问题|缺少报价|已停售|在搜索结果中禁止显示)$/.test(parsed);
|
||||
}
|
||||
|
||||
function optionRow(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const rawText = textOf(el) || attrs.label || attrs.text || attrs.value || '';
|
||||
return {
|
||||
index,
|
||||
raw_text: rawText,
|
||||
value: attrs.value || attrs['data-value'] || attrs.name || null,
|
||||
tag: (el.tagName || '').toLowerCase(),
|
||||
attrs,
|
||||
visible: isVisible(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
const markedDropdown = findMarkedDropdown();
|
||||
let scoped = [];
|
||||
if (markedDropdown) {
|
||||
const roots = [markedDropdown];
|
||||
if (markedDropdown.shadowRoot) roots.push(markedDropdown.shadowRoot);
|
||||
for (const root of roots) {
|
||||
scoped = scoped.concat(Array.from(root.querySelectorAll('kat-option, kat-label, [role="option"], [class*="parentKatOptionStyle"]')));
|
||||
}
|
||||
}
|
||||
|
||||
const allOptions = allElements().filter((el) => {
|
||||
if (!isOptionLike(el)) return false;
|
||||
if (insideListingRow(el)) return false;
|
||||
return isVisible(el);
|
||||
});
|
||||
|
||||
const byNode = new Map();
|
||||
const byStatus = new Map();
|
||||
scoped.concat(allOptions).forEach((el, index) => {
|
||||
if (!byNode.has(el)) {
|
||||
const row = optionRow(el, index);
|
||||
if (isListingStatusOption(row)) {
|
||||
byNode.set(el, row);
|
||||
const parsed = parseStatus(row.raw_text);
|
||||
const hasValue = Boolean(row.value);
|
||||
const hasQuantity = /[((]\s*[\d,]+\s*[))]/.test(row.raw_text);
|
||||
const quality = (hasValue ? 2 : 0) + (hasQuantity ? 1 : 0);
|
||||
const existing = byStatus.get(parsed);
|
||||
if (!existing || quality > existing.quality) {
|
||||
byStatus.set(parsed, { row, quality });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(byStatus.values()).map((item) => item.row).filter((row) => row.raw_text);
|
||||
91
app/amazon/scripts/patrol_delete/listing_status_section.js
Normal file
91
app/amazon/scripts/patrol_delete/listing_status_section.js
Normal file
@@ -0,0 +1,91 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function extractStatuses(text) {
|
||||
const pattern = /(全部|在搜索结果中禁止显示|配送问题|缺少报价|已停售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|不可售|在售)\s*(?:[((]\s*([\d,]+)\s*[))])?/g;
|
||||
const rows = [];
|
||||
let match;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
rows.push({
|
||||
status: match[1],
|
||||
quantity: match[2] || '',
|
||||
raw_text: match[0].trim(),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseDropdown(dropdown, index) {
|
||||
const nodes = Array.from(dropdown.querySelectorAll('kat-option, kat-label, [class*="parentKatOptionStyle"]'));
|
||||
const seen = new Set();
|
||||
const rows = nodes
|
||||
.flatMap((node) => {
|
||||
const rawText = textOf(node);
|
||||
return extractStatuses(rawText).map((parsed) => ({
|
||||
...parsed,
|
||||
value: node.getAttribute('value') || '',
|
||||
}));
|
||||
})
|
||||
.filter((row) => {
|
||||
const key = `${row.status}::${row.quantity}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
const quantityCount = rows.filter((row) => row.quantity).length;
|
||||
const rawText = textOf(dropdown);
|
||||
const className = String(dropdown.className || '');
|
||||
const isStatusDropdown = /statusDropdown/i.test(className);
|
||||
const score =
|
||||
rows.length * 100 +
|
||||
quantityCount * 50 +
|
||||
(isStatusDropdown ? 500 : 0) +
|
||||
(/在搜索结果中禁止显示|需要批准|详情页面已删除/.test(rawText) ? 200 : 0);
|
||||
|
||||
return {
|
||||
index,
|
||||
tag: (dropdown.tagName || '').toLowerCase(),
|
||||
raw_text: rawText,
|
||||
rows,
|
||||
rowCount: rows.length,
|
||||
quantityCount,
|
||||
score,
|
||||
className,
|
||||
outerHTML: (dropdown.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
const ranked = allElements()
|
||||
.map((el, index) => ({ el, index }))
|
||||
.filter(({ el }) => (el.tagName || '').toLowerCase() === 'kat-dropdown')
|
||||
.map(({ el, index }) => parseDropdown(el, index))
|
||||
.filter((row) => row.rowCount >= 3)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const best = ranked[0];
|
||||
return {
|
||||
rows: best ? best.rows : [],
|
||||
selected: best || null,
|
||||
candidates: ranked.slice(0, 10),
|
||||
};
|
||||
148
app/amazon/scripts/patrol_delete/listing_status_select_option.js
Normal file
148
app/amazon/scripts/patrol_delete/listing_status_select_option.js
Normal file
@@ -0,0 +1,148 @@
|
||||
const __crawlerPayload = arguments[0] || {};
|
||||
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function findMarkedDropdown() {
|
||||
return allElements().find((el) => el.getAttribute('data-crawler-listing-status-dropdown') === 'true') || null;
|
||||
}
|
||||
|
||||
function insideListingRow(el) {
|
||||
return Boolean(el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]'));
|
||||
}
|
||||
|
||||
function isOptionLike(el) {
|
||||
if (!el || !el.tagName) return false;
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const role = el.getAttribute('role');
|
||||
const className = el.getAttribute('class') || '';
|
||||
return (
|
||||
tag === 'kat-option' ||
|
||||
tag === 'kat-label' ||
|
||||
role === 'option' ||
|
||||
/parentKatOptionStyle/i.test(className)
|
||||
);
|
||||
}
|
||||
|
||||
function isSearchFieldOption(meta) {
|
||||
return /^(所有|SKUS?|ASIN|FNSKU|UPC\/EAN|商品名称\/关键字)$|^(ALL|SKU|ASIN|FNSKU|TITLE_KEYWORD|GTIN)$/i.test(
|
||||
`${meta.rawText} ${meta.parsedStatus} ${meta.value}`
|
||||
);
|
||||
}
|
||||
|
||||
function isListingStatusOption(meta) {
|
||||
if (isSearchFieldOption(meta)) return false;
|
||||
return /^(全部|在售|不可售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|配送问题|缺少报价|已停售|在搜索结果中禁止显示)$/.test(
|
||||
meta.parsedStatus
|
||||
);
|
||||
}
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function parseStatus(rawText) {
|
||||
const text = normalize(rawText);
|
||||
if (!text) return '';
|
||||
const match = text.match(/^(.*?)\s*[((]\s*[\d,]+\s*[))]\s*$/);
|
||||
return normalize(match ? match[1] : text);
|
||||
}
|
||||
|
||||
function optionMeta(el, index) {
|
||||
const rawText = textOf(el) || el.getAttribute('label') || el.getAttribute('text') || el.getAttribute('value') || '';
|
||||
return {
|
||||
index,
|
||||
rawText: normalize(rawText),
|
||||
parsedStatus: parseStatus(rawText),
|
||||
value: el.getAttribute('value') || el.getAttribute('data-value') || '',
|
||||
visible: isVisible(el),
|
||||
};
|
||||
}
|
||||
|
||||
function clickTargetFor(el) {
|
||||
if (!el) return el;
|
||||
return el.closest('[class*="parentKatOptionStyle"], kat-option, [role="option"]') || el;
|
||||
}
|
||||
|
||||
const targetStatus = normalize((__crawlerPayload || {}).status);
|
||||
const dropdown = findMarkedDropdown();
|
||||
const options = [];
|
||||
|
||||
if (dropdown) {
|
||||
const roots = [dropdown];
|
||||
if (dropdown.shadowRoot) roots.push(dropdown.shadowRoot);
|
||||
for (const root of roots) {
|
||||
for (const el of Array.from(root.querySelectorAll('kat-option, kat-label, [role="option"], [class*="parentKatOptionStyle"]'))) {
|
||||
options.push(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.length) {
|
||||
for (const el of allElements()) {
|
||||
if (!isOptionLike(el)) continue;
|
||||
if (!isVisible(el) || insideListingRow(el)) continue;
|
||||
options.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
const metas = options
|
||||
.map((el, index) => ({ el, ...optionMeta(el, index) }))
|
||||
.filter((item) => isListingStatusOption(item));
|
||||
const matched = metas.find((item) => item.parsedStatus === targetStatus || item.rawText === targetStatus);
|
||||
if (!matched) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'status option not found',
|
||||
targetStatus,
|
||||
options: metas.map(({ index, rawText, parsedStatus, value, visible }) => ({
|
||||
index,
|
||||
rawText,
|
||||
parsedStatus,
|
||||
value,
|
||||
visible,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const clickTarget = clickTargetFor(matched.el);
|
||||
clickTarget.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
clickTarget.click();
|
||||
return {
|
||||
ok: true,
|
||||
targetStatus,
|
||||
selected: {
|
||||
index: matched.index,
|
||||
rawText: matched.rawText,
|
||||
parsedStatus: matched.parsedStatus,
|
||||
value: matched.value,
|
||||
clickedTag: clickTarget.tagName,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function snapshot(el, index) {
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
text: textOf(el),
|
||||
attrs: attrsOf(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
const KEYWORD_RE = /推荐报价|featured offer|buy box|商城|marketplace|欧洲|英国|德国|法国|西班牙|意大利|2\s*天前|30\s*天前|2\s*days?\s*ago|30\s*days?\s*ago/i;
|
||||
const visible = allElements().filter((el) => isVisible(el));
|
||||
|
||||
const keywordMatches = visible
|
||||
.map((el, index) => ({ el, index, text: textOf(el) }))
|
||||
.filter((row) => KEYWORD_RE.test(`${row.el.tagName} ${row.text} ${Object.values(attrsOf(row.el)).join(' ')}`))
|
||||
.slice(0, 120)
|
||||
.map(({ el, index }) => snapshot(el, index));
|
||||
|
||||
const buyBoxCard = visible.find((el) => (el.getAttribute('data-key') || '') === 'KPI_CARD_BUYBOX')
|
||||
|| visible.find((el) => (el.id || '') === 'KPI_CARD_BUYBOX');
|
||||
|
||||
const buyBoxNeighborhood = [];
|
||||
if (buyBoxCard) {
|
||||
let node = buyBoxCard;
|
||||
for (let depth = 0; node && depth < 6; depth += 1, node = node.parentElement) {
|
||||
buyBoxNeighborhood.push({
|
||||
depth,
|
||||
tag: node.tagName.toLowerCase(),
|
||||
text: textOf(node),
|
||||
attrs: attrsOf(node),
|
||||
outerHTML: (node.outerHTML || '').slice(0, 2500),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const buyBoxLeafMatches = [];
|
||||
if (buyBoxCard) {
|
||||
const scoped = [];
|
||||
walkRoots(buyBoxCard, scoped, new Set());
|
||||
scoped
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ el, index, text: textOf(el) }))
|
||||
.filter((row) => row.text && row.text.length <= 200)
|
||||
.filter((row) => /推荐报价|商城|marketplace|欧洲|英国|德国|法国|西班牙|意大利|\d+(?:\.\d+)?\s*%|2\s*天前|30\s*天前|2\s*days?\s*ago|30\s*days?\s*ago/i.test(row.text))
|
||||
.slice(0, 120)
|
||||
.forEach(({ el, index }) => buyBoxLeafMatches.push(snapshot(el, index)));
|
||||
}
|
||||
|
||||
return {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
buyBoxFound: !!buyBoxCard,
|
||||
buyBoxNeighborhood,
|
||||
buyBoxLeafMatches,
|
||||
keywordMatches,
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
return Object.entries(attrsOf(el)).map(([key, value]) => `${key}=${value}`).join(' ');
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function clickableFor(el) {
|
||||
if (el && el.querySelector) {
|
||||
const preferred = el.querySelector(
|
||||
'casino-button[data-actionname="expand"], [data-actionname="expand"], casino-button[aria-label="chevron-down"], [aria-label="chevron-down"]'
|
||||
);
|
||||
if (preferred) return preferred;
|
||||
}
|
||||
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 7; depth += 1, node = node.parentElement) {
|
||||
if (node.querySelector) {
|
||||
const preferred = node.querySelector(
|
||||
'casino-button[data-actionname="expand"], [data-actionname="expand"], casino-button[aria-label="chevron-down"], [aria-label="chevron-down"]'
|
||||
);
|
||||
if (preferred) return preferred;
|
||||
}
|
||||
const tag = node.tagName.toLowerCase();
|
||||
const role = node.getAttribute('role');
|
||||
if (tag === 'button' || tag === 'a' || tag === 'kat-button' || role === 'button' || node.onclick) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function scoreEntry(el) {
|
||||
const text = textOf(el);
|
||||
const haystack = `${el.tagName} ${attrsText(el)} ${text}`;
|
||||
let score = 0;
|
||||
if (/推荐报价百分比|推荐报价.*百分比|featured offer percentage|featured offer.*%|featured offer/i.test(haystack)) {
|
||||
score += 120;
|
||||
}
|
||||
if (/buy box|购物车|报价/i.test(haystack)) score += 20;
|
||||
if (/%/.test(haystack)) score += 15;
|
||||
if (/button|link|href|click|card|metric/i.test(haystack)) score += 10;
|
||||
if (isVisible(el)) score += 10;
|
||||
if (text.length > 800) score -= 30;
|
||||
return { score, text, haystack };
|
||||
}
|
||||
|
||||
const ranked = allElements()
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ el, index, ...scoreEntry(el) }))
|
||||
.filter((row) => row.score > 0)
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return a.text.length - b.text.length;
|
||||
});
|
||||
|
||||
if (!ranked.length) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'recommended offer percentage entry not found',
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
candidates: allElements()
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ index, tag: el.tagName.toLowerCase(), text: textOf(el), attrs: attrsOf(el) }))
|
||||
.filter((row) => /推荐报价|featured offer|buy box|百分比|percentage/i.test(`${row.tag} ${row.text} ${Object.values(row.attrs).join(' ')}`))
|
||||
.slice(0, 80),
|
||||
};
|
||||
}
|
||||
|
||||
const selected = ranked[0];
|
||||
const clickable = clickableFor(selected.el);
|
||||
clickable.setAttribute('data-crawler-recommended-offer-entry', 'true');
|
||||
clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
['pointerdown', 'mousedown', 'mouseup', 'click'].forEach((eventName) => {
|
||||
clickable.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, composed: true }));
|
||||
});
|
||||
if (typeof clickable.click === 'function') clickable.click();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
selected: {
|
||||
index: selected.index,
|
||||
score: selected.score,
|
||||
text: selected.text,
|
||||
attrs: attrsText(selected.el),
|
||||
clickableTag: clickable.tagName.toLowerCase(),
|
||||
clickableText: textOf(clickable),
|
||||
outerHTML: (selected.el.outerHTML || '').slice(0, 2000),
|
||||
},
|
||||
candidates: ranked.slice(0, 12).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
text: row.text,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function rowOf(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
raw_text: textOf(el),
|
||||
label_text: attrs.label || attrs.title || attrs['aria-label'] || '',
|
||||
percentage_text: attrs.value || attrs['data-value'] || '',
|
||||
attrs,
|
||||
visible: isVisible(el),
|
||||
rect_left: rect.left,
|
||||
rect_top: rect.top,
|
||||
rect_width: rect.width,
|
||||
rect_height: rect.height,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
const COUNTRIES = [
|
||||
{ country: '欧洲', aliases: ['欧洲', 'Europe'] },
|
||||
{ country: '英国', aliases: ['英国', 'UK', 'United Kingdom'] },
|
||||
{ country: '德国', aliases: ['德国', 'Germany'] },
|
||||
{ country: '法国', aliases: ['法国', 'France'] },
|
||||
{ country: '西班牙', aliases: ['西班牙', 'Spain'] },
|
||||
{ country: '意大利', aliases: ['意大利', 'Italy'] },
|
||||
];
|
||||
|
||||
function percentagesOf(text) {
|
||||
return (text.match(/\d+(?:\.\d+)?\s*%/g) || []).map((item) => item.replace(/\s+/g, ''));
|
||||
}
|
||||
|
||||
function parentOrHost(el) {
|
||||
if (!el) return null;
|
||||
if (el.parentElement) return el.parentElement;
|
||||
const root = el.getRootNode && el.getRootNode();
|
||||
return root && root.host ? root.host : null;
|
||||
}
|
||||
|
||||
function countryOfText(text) {
|
||||
const normalized = (text || '').trim().toLowerCase();
|
||||
return COUNTRIES.find(({ aliases }) => aliases.some((alias) => normalized === alias.toLowerCase())) || null;
|
||||
}
|
||||
|
||||
function countCountries(text) {
|
||||
return COUNTRIES.filter(({ aliases }) => aliases.some((alias) => new RegExp(`(^|\\s)${alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`, 'i').test(text))).length;
|
||||
}
|
||||
|
||||
function bestCountryRowText(el) {
|
||||
const visited = new Set();
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 7; depth += 1, node = parentOrHost(node)) {
|
||||
if (visited.has(node)) break;
|
||||
visited.add(node);
|
||||
const text = textOf(node);
|
||||
if (!text || text.length > 500) continue;
|
||||
const percentages = percentagesOf(text);
|
||||
if (percentages.length >= 2 && countCountries(text) <= 1) return text;
|
||||
}
|
||||
|
||||
const parent = parentOrHost(el);
|
||||
if (!parent || !parent.children) return '';
|
||||
|
||||
const children = Array.from(parent.children).filter((child) => isVisible(child) && textOf(child));
|
||||
const index = children.indexOf(el);
|
||||
if (index < 0) return '';
|
||||
|
||||
const nearby = children.slice(Math.max(0, index - 1), Math.min(children.length, index + 5)).map((child) => textOf(child));
|
||||
const joined = nearby.join(' ');
|
||||
return percentagesOf(joined).length >= 2 ? joined : '';
|
||||
}
|
||||
|
||||
function directCountryRows(elements) {
|
||||
const results = new Map();
|
||||
elements.forEach((el) => {
|
||||
const match = countryOfText(textOf(el));
|
||||
if (!match || results.has(match.country)) return;
|
||||
const text = bestCountryRowText(el);
|
||||
const percentages = percentagesOf(text);
|
||||
if (percentages.length < 2) return;
|
||||
results.set(match.country, {
|
||||
country: match.country,
|
||||
ratio2DaysAgo: percentages[0],
|
||||
ratio30DaysAgo: percentages[1],
|
||||
raw_text: text,
|
||||
source: 'rendered-country-row',
|
||||
});
|
||||
});
|
||||
return Array.from(results.values());
|
||||
}
|
||||
|
||||
function expandedCardOf(elements) {
|
||||
return elements.find((el) => el.tagName && el.tagName.toLowerCase() === 'casino-card' && /\bcasino-expanded-card\b/.test(el.className || ''));
|
||||
}
|
||||
|
||||
function directRowsFromExpandedCard(expandedCard) {
|
||||
if (!expandedCard) return [];
|
||||
|
||||
const scoped = [];
|
||||
walkRoots(expandedCard, scoped, new Set());
|
||||
|
||||
const countries = [];
|
||||
const percentages = [];
|
||||
const seen = new Set();
|
||||
|
||||
scoped
|
||||
.filter((el) => isVisible(el))
|
||||
.forEach((el) => {
|
||||
const text = textOf(el);
|
||||
const title = (el.getAttribute && el.getAttribute('title')) || '';
|
||||
const aria = (el.getAttribute && (el.getAttribute('aria-label') || el.getAttribute('arialabel'))) || '';
|
||||
const candidate = (title || aria || text || '').replace(/\s+/g, ' ').trim();
|
||||
if (!candidate || candidate.length > 20) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const area = rect.width * rect.height;
|
||||
if (area <= 0 || area > 20000) return;
|
||||
|
||||
const key = `${candidate}|${Math.round(rect.left)}|${Math.round(rect.top)}|${Math.round(rect.width)}|${Math.round(rect.height)}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
|
||||
const countryMatch = countryOfText(candidate);
|
||||
if (countryMatch) {
|
||||
countries.push({
|
||||
country: countryMatch.country,
|
||||
x: rect.left,
|
||||
y: rect.top + rect.height / 2,
|
||||
h: rect.height,
|
||||
area,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^\d+(?:\.\d+)?\s*%$/.test(candidate)) {
|
||||
percentages.push({
|
||||
value: candidate.replace(/\s+/g, ''),
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
area,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const dedupedCountries = [];
|
||||
countries
|
||||
.sort((a, b) => a.y - b.y || a.x - b.x || a.area - b.area)
|
||||
.forEach((item) => {
|
||||
const exists = dedupedCountries.some((row) => row.country === item.country && Math.abs(row.y - item.y) < 8);
|
||||
if (!exists) dedupedCountries.push(item);
|
||||
});
|
||||
|
||||
const results = dedupedCountries.map((row) => {
|
||||
const sameRow = percentages
|
||||
.filter((item) => item.x > row.x + 20 && Math.abs(item.y - row.y) <= Math.max(14, row.h))
|
||||
.sort((a, b) => a.x - b.x || a.y - b.y || a.area - b.area);
|
||||
|
||||
const unique = [];
|
||||
sameRow.forEach((item) => {
|
||||
if (!unique.some((existing) => existing.value === item.value && Math.abs(existing.x - item.x) < 8)) {
|
||||
unique.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
country: row.country,
|
||||
ratio2DaysAgo: unique[0] ? unique[0].value : '',
|
||||
ratio30DaysAgo: unique[1] ? unique[1].value : '',
|
||||
source: 'expanded-card-geometry',
|
||||
};
|
||||
});
|
||||
|
||||
return results.filter((item) => item.ratio2DaysAgo || item.ratio30DaysAgo);
|
||||
}
|
||||
|
||||
function scoreRegion(el) {
|
||||
const text = textOf(el);
|
||||
if (!/%/.test(text)) return 0;
|
||||
|
||||
let score = 0;
|
||||
if (/推荐报价百分比|推荐报价|featured offer percentage|featured offer|buy box/i.test(text)) score += 80;
|
||||
if (/2\s*天前|30\s*天前|2\s*日前|30\s*日前|2\s*days?\s*ago|30\s*days?\s*ago/i.test(text)) score += 80;
|
||||
if (/欧洲|英国|德国|法国|西班牙|意大利|Europe|UK|United Kingdom|Germany|France|Spain|Italy/i.test(text)) score += 40;
|
||||
if (/%/.test(text)) score += 20;
|
||||
if (isVisible(el)) score += 10;
|
||||
if (text.length > 2500) score -= 40;
|
||||
return score;
|
||||
}
|
||||
|
||||
function isRelevantRatioRow(row) {
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.percentage_text}`;
|
||||
const hasDayLabels = /2\s*天前|30\s*天前|2\s*日前|30\s*日前|2\s*days?\s*ago|30\s*days?\s*ago/i.test(combined);
|
||||
const hasCountries = /欧洲|英国|德国|法国|西班牙|意大利|Europe|UK|United Kingdom|Germany|France|Spain|Italy/i.test(combined);
|
||||
if (!/%|2\s*天前|30\s*天前|2\s*日前|30\s*日前|2\s*days?\s*ago|30\s*days?\s*ago/i.test(combined) && !hasCountries) return false;
|
||||
if (row.raw_text.length > 800 && !hasDayLabels && !hasCountries) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function collectRelevantRows(sourceElements) {
|
||||
const byText = new Map();
|
||||
const scoped = [];
|
||||
sourceElements.forEach((root) => walkRoots(root, scoped, new Set()));
|
||||
scoped
|
||||
.filter((el) => isVisible(el))
|
||||
.forEach((el, index) => {
|
||||
const row = rowOf(el, index);
|
||||
if (!isRelevantRatioRow(row)) return;
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.percentage_text}`;
|
||||
const key = `${combined}|${Math.round(row.rect_left)}|${Math.round(row.rect_top)}|${Math.round(row.rect_width)}|${Math.round(row.rect_height)}`;
|
||||
if (!byText.has(key)) byText.set(key, row);
|
||||
});
|
||||
return Array.from(byText.values());
|
||||
}
|
||||
|
||||
function scrollableElements(elements) {
|
||||
return elements.filter((el) => {
|
||||
if (!el || !el.getBoundingClientRect || !isVisible(el)) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
if (!/(auto|scroll)/i.test(`${style.overflowY} ${style.overflow}`)) return false;
|
||||
return el.scrollHeight > el.clientHeight + 20;
|
||||
});
|
||||
}
|
||||
|
||||
function collectRowsAcrossScroll(sourceElements, visibleElements) {
|
||||
const targets = scrollableElements(visibleElements);
|
||||
if (document.scrollingElement) targets.push(document.scrollingElement);
|
||||
|
||||
const byKey = new Map();
|
||||
const originals = targets.map((target) => ({ target, top: target.scrollTop }));
|
||||
for (const target of targets) {
|
||||
const maxTop = Math.max(0, target.scrollHeight - target.clientHeight);
|
||||
const positions = [...new Set([0, Math.floor(maxTop / 2), maxTop])];
|
||||
for (const position of positions) {
|
||||
target.scrollTop = position;
|
||||
collectRelevantRows(sourceElements).forEach((row) => {
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.percentage_text}`;
|
||||
const key = `${combined}|${Math.round(row.rect_left)}|${Math.round(row.rect_top)}|${Math.round(row.rect_width)}|${Math.round(row.rect_height)}`;
|
||||
if (!byKey.has(key)) byKey.set(key, row);
|
||||
});
|
||||
}
|
||||
}
|
||||
originals.forEach(({ target, top }) => {
|
||||
target.scrollTop = top;
|
||||
});
|
||||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
const visible = allElements().filter((el) => isVisible(el));
|
||||
const expandedCard = expandedCardOf(visible);
|
||||
const expandedDirectRows = directRowsFromExpandedCard(expandedCard);
|
||||
const directRows = expandedDirectRows.length ? expandedDirectRows : directCountryRows(visible);
|
||||
const regions = visible
|
||||
.map((el, index) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { el, index, score: scoreRegion(el), text: textOf(el), area: rect.width * rect.height };
|
||||
})
|
||||
.filter((row) => row.score > 0)
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
if (a.text.length !== b.text.length) return a.text.length - b.text.length;
|
||||
return a.area - b.area;
|
||||
});
|
||||
|
||||
const sourceElements = expandedCard ? [expandedCard] : (regions.length ? [regions[0].el] : visible);
|
||||
const scoped = [];
|
||||
sourceElements.forEach((root) => walkRoots(root, scoped, new Set()));
|
||||
|
||||
const byText = new Map();
|
||||
collectRelevantRows(sourceElements)
|
||||
.concat(collectRowsAcrossScroll(sourceElements, visible))
|
||||
.forEach((row) => {
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.percentage_text}`;
|
||||
const key = `${combined}|${Math.round(row.rect_left)}|${Math.round(row.rect_top)}|${Math.round(row.rect_width)}|${Math.round(row.rect_height)}`;
|
||||
if (!byText.has(key)) byText.set(key, row);
|
||||
});
|
||||
|
||||
return {
|
||||
directRows,
|
||||
region: regions.length
|
||||
? {
|
||||
score: regions[0].score,
|
||||
text: regions[0].text.slice(0, 2000),
|
||||
outerHTML: (regions[0].el.outerHTML || '').slice(0, 3000),
|
||||
}
|
||||
: {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
},
|
||||
rows: Array.from(byText.values()),
|
||||
candidates: regions.slice(0, 20).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
text: row.text.slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
941
app/amazon/similar_asin.py
Normal file
941
app/amazon/similar_asin.py
Normal file
@@ -0,0 +1,941 @@
|
||||
# encoding utf-8
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import base64
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import os
|
||||
from curl_cffi import requests as requests_frp
|
||||
|
||||
from config import base_dir
|
||||
|
||||
|
||||
from amazon.tool import show_notification,get_shop_info,remove_special_characters,split_currency_values
|
||||
from amazon.amazon_base import TaskBase
|
||||
|
||||
from amazon.chrome_base import ChromeAmzoneBase
|
||||
|
||||
from config import runing_task, runing_shop,base_dir,DELETE_BRAND_API_BASE
|
||||
|
||||
try:
|
||||
from config import proxy_url as CONFIG_PROXY_URL, proxy_mode as CONFIG_PROXY_MODE
|
||||
except ImportError:
|
||||
CONFIG_PROXY_URL = None
|
||||
CONFIG_PROXY_MODE = 1
|
||||
|
||||
# Forbidden 后 3 分钟内统一使用代理:记录代理生效截止时间与当前代理
|
||||
_FORBIDDEN_PROXY_UNTIL_Similar = 0.0
|
||||
_FORBIDDEN_PROXY_DICT_Similar = None
|
||||
_FORBIDDEN_PROXY_MINUTES_Similar = 0.5
|
||||
|
||||
|
||||
class ChromeAmzone(ChromeAmzoneBase):
|
||||
mark_name = "亚马逊相似ASIN"
|
||||
|
||||
def wait_load_complete(self):
|
||||
try:
|
||||
for _ in range(3):
|
||||
content_area = self.tab.eles('xpath://div[@class="ap-sbi-aside__main"]', timeout=20)
|
||||
if len(content_area) > 0:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"等待加载完成失败,{e},等待5秒")
|
||||
time.sleep(5)
|
||||
|
||||
def encode_custom(self,input_):
|
||||
if not input_ or not isinstance(input_, str):
|
||||
return ""
|
||||
|
||||
transformed = ""
|
||||
|
||||
# 第一个字符:字符编码 + 字符串长度
|
||||
transformed += chr((ord(input_[0]) + len(input_)) & 0xFFFF)
|
||||
|
||||
# 后续字符:当前字符编码 + 前一个字符编码
|
||||
for i in range(1, len(input_)):
|
||||
current_code = ord(input_[i])
|
||||
prev_code = ord(input_[i - 1])
|
||||
transformed += chr((current_code + prev_code) & 0xFFFF)
|
||||
|
||||
# 对应 JavaScript 的 encodeURIComponent
|
||||
encoded = quote(transformed, safe="-_.!~*'()")
|
||||
|
||||
# 对应 replace(/[!'()*]/g, ...)
|
||||
for ch in ["!", "'", "(", ")", "*"]:
|
||||
encoded = encoded.replace(ch, "%" + format(ord(ch), "x"))
|
||||
return encoded
|
||||
|
||||
def image_to_base64(self,image_source: str) -> str:
|
||||
"""
|
||||
将图片链接(URL 或本地文件路径)转换为 Base64 编码的字符串。
|
||||
"""
|
||||
if image_source.startswith(('http://', 'https://')):
|
||||
response = requests.get(image_source, timeout=10,headers={
|
||||
"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0 18444"
|
||||
})
|
||||
response.raise_for_status() # 非 2xx 状态码将抛出异常
|
||||
image_data = response.content
|
||||
else:
|
||||
path = Path(image_source)
|
||||
if not path.is_file():
|
||||
raise ValueError(f"本地文件不存在: {image_source}")
|
||||
with open(path, 'rb') as f:
|
||||
image_data = f.read()
|
||||
base64_str = base64.b64encode(image_data).decode('utf-8')
|
||||
return base64_str
|
||||
|
||||
def get_aliprice_data(self,page=1,title="",domain="",category="",imageBase64=""):
|
||||
"""
|
||||
"fishkeeper Quick Aquarium Siphon Pump Gravel Cleaner - 256GPH Adjustable Powerful Fish Tank Vacuum Gravel Cleaning Kit for Aquarium Water Changer, Sand Cleaner, Dirt Removal : Amazon.co.uk: Pet Supplies"
|
||||
标题 :
|
||||
|
||||
"""
|
||||
global _FORBIDDEN_PROXY_UNTIL_Similar, _FORBIDDEN_PROXY_DICT_Similar
|
||||
|
||||
headers = {
|
||||
"accept": "application/json, text/plain, */*",
|
||||
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"browser": "chrome",
|
||||
"cache-control": "no-cache",
|
||||
"channel": "chrome_offline",
|
||||
"content-type": "application/json;charset=UTF-8",
|
||||
"ext-id": "10100",
|
||||
"ext_id": "10100",
|
||||
"origin": "chrome-extension://ephkdklmdkaakeleplfpahjphaokcllh",
|
||||
"platform": "1688",
|
||||
"pragma": "no-cache",
|
||||
"priority": "u=1, i",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "none",
|
||||
"sec-fetch-storage-access": "active",
|
||||
"version": "3.7.4",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
|
||||
}
|
||||
cookie = {
|
||||
"e-info": "[{\"e-name\":\"1688\",\"adid\":\"100\",\"version\":\"3.7.4\",\"ext_id\":\"10100\"}]",
|
||||
"language": "chinese",
|
||||
"province_code": "Guangdong",
|
||||
"_ct": "czg",
|
||||
"_e_ct": "czg",
|
||||
"is_reto": "1",
|
||||
"crossborder": "1",
|
||||
"agent": "0",
|
||||
"first_view": "1",
|
||||
"currency": "USD",
|
||||
"is_coo": "1",
|
||||
"plugin": "chrome",
|
||||
"plugin_ext": "100,129",
|
||||
"m-info": "[{\"platform\":\"1688\",\"version\":\"3.7.4\",\"browser\":\"chrome\",\"m\":\"uni\",\"t\":1777902599578}]"
|
||||
}
|
||||
url = "https://api.aliprice.com/index.php/chrome/items/imageAnalysis"
|
||||
itemTitle = f"{title} : {domain}: {category}"
|
||||
|
||||
params = {"page": page, "size": 20, "website": "1688_lite2", "language": "zh-CN", "currency": "USD", "from": "",
|
||||
"itemTitle": itemTitle, "domain": domain }
|
||||
params['sign'] = self.encode_custom(json.dumps(params, ensure_ascii=False))
|
||||
data = {
|
||||
"imageBase64" : imageBase64
|
||||
}
|
||||
|
||||
proxies = None
|
||||
|
||||
# 若 3 分钟内曾出现 Forbidden,则直接使用当时保存的代理
|
||||
now = time.time()
|
||||
if now < _FORBIDDEN_PROXY_UNTIL_Similar and _FORBIDDEN_PROXY_DICT_Similar:
|
||||
proxies = _FORBIDDEN_PROXY_DICT_Similar
|
||||
print("处于 Forbidden 代理窗口内,直接使用代理", proxies)
|
||||
|
||||
# 发送第一次请求
|
||||
try:
|
||||
response = requests_frp.post(url, headers=headers, params=params, json=data, impersonate="chrome101",
|
||||
cookies=cookie, proxies=proxies,verify=False)
|
||||
response.encoding = "utf-8"
|
||||
|
||||
# 检查响应状态码和数据有效性
|
||||
need_retry = False
|
||||
if response.status_code != 200:
|
||||
print(f"请求失败,状态码: {response.status_code}")
|
||||
need_retry = True
|
||||
else:
|
||||
try:
|
||||
result_data = response.json()
|
||||
# 检查是否获取到有效数据
|
||||
if not result_data or not isinstance(result_data, dict):
|
||||
print("返回数据为空或格式不正确")
|
||||
need_retry = True
|
||||
elif "Forbidden" in response.text or "Too Many Requests" in response.text:
|
||||
print("返回Forbidden或Too Many Requests")
|
||||
need_retry = True
|
||||
except Exception as e:
|
||||
print(f"解析响应JSON失败: {e}")
|
||||
need_retry = True
|
||||
|
||||
# 如果需要重试且配置了代理URL
|
||||
# need_retry = True
|
||||
if need_retry and CONFIG_PROXY_URL and not proxies:
|
||||
try:
|
||||
proxy_resp = requests.get(CONFIG_PROXY_URL, timeout=10)
|
||||
print("代理请求结果->:", proxy_resp.text)
|
||||
|
||||
# 模式 2:账号密码代理,接口返回 JSON
|
||||
if CONFIG_PROXY_MODE == 2:
|
||||
resp_json = proxy_resp.json()
|
||||
proxy_list = resp_json.get("data", {}).get("list") or []
|
||||
first_item = proxy_list[0] if proxy_list else None
|
||||
if first_item:
|
||||
ip = first_item.get("ip")
|
||||
port = first_item.get("port")
|
||||
account = first_item.get("account")
|
||||
password = first_item.get("password")
|
||||
if ip and port and account and password:
|
||||
auth_proxy = f"{account}:{password}@{ip}:{port}"
|
||||
print("获取到账号密码代理:", auth_proxy)
|
||||
proxies = {
|
||||
"http": f"http://{auth_proxy}",
|
||||
"https": f"http://{auth_proxy}",
|
||||
}
|
||||
# 默认模式 1:普通 IP:port 文本
|
||||
if CONFIG_PROXY_MODE != 2:
|
||||
proxy_ip = (proxy_resp.text or "").strip()
|
||||
if proxy_ip:
|
||||
proxies = {
|
||||
"http": f"http://{proxy_ip}",
|
||||
"https": f"https://{proxy_ip}",
|
||||
}
|
||||
|
||||
if proxies:
|
||||
print("使用代理重试请求")
|
||||
response = requests_frp.post(url, headers=headers, params=params, json=data,
|
||||
impersonate="chrome101", cookies=cookie, proxies=proxies)
|
||||
response.encoding = "utf-8"
|
||||
print("代理重试结果状态码:", response.status_code)
|
||||
|
||||
# 记录 3 分钟内都使用该代理
|
||||
_FORBIDDEN_PROXY_UNTIL_Similar = now + _FORBIDDEN_PROXY_MINUTES_Similar * 60
|
||||
_FORBIDDEN_PROXY_DICT_Similar = proxies
|
||||
|
||||
except Exception as e:
|
||||
print("获取代理或重试失败:", e)
|
||||
|
||||
# 返回最终结果
|
||||
result_data = response.json()
|
||||
return result_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"get_aliprice_data 请求异常: {e}")
|
||||
return {}
|
||||
|
||||
def _scrape_data(self):
|
||||
"""
|
||||
抓取商品数据
|
||||
|
||||
Returns:
|
||||
dict: 抓取到的数据
|
||||
"""
|
||||
data = {
|
||||
'image_url': "",
|
||||
'title': "",
|
||||
'category' : '',
|
||||
'sku' : '',
|
||||
'success' : True
|
||||
}
|
||||
|
||||
try:
|
||||
# 等待页面加载
|
||||
# time.sleep(3)
|
||||
title_ele = self.tab.ele('xpath://h1[@id="title"]',timeout=30)
|
||||
title = title_ele.text
|
||||
data["title"] = title
|
||||
imge_ele = self.tab.ele('xpath://div[@id="imgTagWrapperId"]//img',timeout=20)
|
||||
|
||||
image_url = ""
|
||||
min_image_url = ""
|
||||
data_a_dynamic_image = imge_ele.attr("data-a-dynamic-image")
|
||||
if data_a_dynamic_image:
|
||||
dynamic_image_json = json.loads(data_a_dynamic_image)
|
||||
self.log(f"图片信息:{dynamic_image_json}")
|
||||
max_area = 0
|
||||
min_area = 0
|
||||
|
||||
for url, (width, height) in dynamic_image_json.items():
|
||||
area = width * height
|
||||
if area > max_area:
|
||||
max_area = area
|
||||
image_url = url
|
||||
if min_area == 0:
|
||||
min_area = area
|
||||
min_image_url = url
|
||||
if area < min_area:
|
||||
min_area = area
|
||||
min_image_url = url
|
||||
|
||||
if not image_url:
|
||||
image_url = imge_ele.attr("src")
|
||||
|
||||
if not min_image_url:
|
||||
min_image_url = imge_ele.attr("src")
|
||||
|
||||
|
||||
data["image_url"] = image_url
|
||||
data["min_image_url"] = min_image_url
|
||||
|
||||
# sku
|
||||
sku_ele_ls = self.tab.eles('xpath://ul[@class="a-unordered-list a-vertical a-spacing-mini"]',timeout=20)
|
||||
if len(sku_ele_ls) > 0:
|
||||
data["sku"] = sku_ele_ls[0].text
|
||||
category = self.tab.eles('xpath://div[@id="wayfinding-breadcrumbs_feature_div"]//span[@class="a-list-item"]//a[@class="a-link-normal a-color-tertiary"]',timeout=20)
|
||||
if len(category) > 0:
|
||||
data["category"] = category[0].text
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
print(f"抓取数据时出错: {traceback.format_exc()}")
|
||||
data["success"] = False
|
||||
return data
|
||||
|
||||
def run(self, country, asin,total_page=2):
|
||||
"""
|
||||
运行亚马逊详情采集任务
|
||||
Args:
|
||||
country: 国家名称(如:英国、德国、法国、西班牙、意大利)
|
||||
asin: 亚马逊商品ASIN码
|
||||
Returns:
|
||||
dict: 包含采集到的数据
|
||||
"""
|
||||
return_data = {}
|
||||
try:
|
||||
# 验证国家是否支持
|
||||
if country not in self.country_info:
|
||||
error_msg = f"不支持的国家: {country},支持的国家有: {list(self.country_info.keys())}"
|
||||
print(error_msg)
|
||||
# show_notification(error_msg, "error")
|
||||
return None
|
||||
|
||||
# 获取国家配置
|
||||
country_config = self.country_info[country]
|
||||
zip_code = country_config["zip_code"]
|
||||
mark = country_config.get("mark")
|
||||
|
||||
# 1. 根据国家和ASIN拼接链接
|
||||
base_url = country_config["url"]
|
||||
# 提取域名部分
|
||||
domain = base_url.split("/dp/")[0]
|
||||
# 拼接新的URL
|
||||
product_url = f"{domain}/dp/{asin}"
|
||||
self.log(f"正在访问: {product_url}")
|
||||
|
||||
# 打开链接
|
||||
self.tab.get(product_url)
|
||||
time.sleep(3) # 等待页面初步加载
|
||||
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
|
||||
|
||||
self.close_init_popup()
|
||||
# 2. 切换国家/设置邮编
|
||||
self.log(f"正在检查并设置邮编: {zip_code},标识: {mark}")
|
||||
self._set_zip_code(zip_code, mark)
|
||||
|
||||
# self.tab.wait.doc_loaded(timeout=5, raise_err=False)
|
||||
|
||||
# 3. 抓取数据
|
||||
self.log("正在抓取商品数据...")
|
||||
data = self._scrape_data()
|
||||
|
||||
return_data.update(data)
|
||||
|
||||
# 判断是否采集标题出错
|
||||
new_size = "220,220"
|
||||
pattern = r"\._(?:[A-Z]+)?(\d+)_\."
|
||||
# replacement = f"._{new_size}_.jpg"
|
||||
# 使用正则替换
|
||||
# image_new_url = re.sub(pattern, lambda m: replacement, data["image_url"])
|
||||
image_new_url = data["min_image_url"]
|
||||
|
||||
print(image_new_url)
|
||||
|
||||
iamge_base64 = self.image_to_base64(image_new_url)
|
||||
|
||||
similar_data = []
|
||||
# 4、请求获取插件的数据
|
||||
if image_new_url:
|
||||
for page_num in range(total_page):
|
||||
resp_data = self.get_aliprice_data(
|
||||
page=page_num+1,
|
||||
title = data["title"],
|
||||
domain=domain,
|
||||
category=data["category"],
|
||||
imageBase64 = iamge_base64
|
||||
)
|
||||
self.log(f"Aliprice 扩展数据获取:{str(resp_data)[0:200]}")
|
||||
if len(resp_data.get("data",[])) > 0:
|
||||
for i in resp_data.get("data"):
|
||||
similar_data.append(i)
|
||||
|
||||
data["similar_data"] = similar_data
|
||||
|
||||
# 添加基本信息
|
||||
data['country'] = country
|
||||
data['asin'] = asin
|
||||
data['url'] = product_url
|
||||
data['timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
print(f"数据抓取完成: {json.dumps(return_data)}")
|
||||
return_data.update(data)
|
||||
|
||||
return return_data
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"运行出错: {traceback.format_exc()}"
|
||||
print(error_msg)
|
||||
# show_notification(f"采集失败: {str(e)}", "error")
|
||||
raise RuntimeError(f"{traceback.format_exc()}")
|
||||
return return_data
|
||||
|
||||
|
||||
class SimilarAsinTask(TaskBase):
|
||||
mark_name = "相似ASIN采集"
|
||||
|
||||
@staticmethod
|
||||
def group_by_id_prefix(data):
|
||||
"""
|
||||
根据每条数据的 id 字段进行分组:
|
||||
- 如果 id 为 "2_1",则取 "_" 前面的 "2" 作为分组 key
|
||||
- 如果 id 为 "1",则分组 key 就是 "1"
|
||||
- 相同 key 的数据归为同一组
|
||||
|
||||
:param data: 原始列表数据
|
||||
:return: 二维列表,按 id 前缀分组
|
||||
"""
|
||||
grouped = defaultdict(list)
|
||||
|
||||
for item in data:
|
||||
item_id = str(item.get("id", ""))
|
||||
group_key = item_id.split("_")[0]
|
||||
grouped[group_key].append(item)
|
||||
|
||||
return list(grouped.values())
|
||||
|
||||
@staticmethod
|
||||
def normalize_groups(data):
|
||||
groups = data.get("groups")
|
||||
if isinstance(groups, list) and len(groups) > 0:
|
||||
return groups
|
||||
|
||||
rows = data.get("rows") or data.get("items") or []
|
||||
if not isinstance(rows, list) or len(rows) == 0:
|
||||
return []
|
||||
|
||||
normalized = []
|
||||
for items in SimilarAsinTask.group_by_id_prefix(rows):
|
||||
first = items[0] if items else {}
|
||||
item_id = str(first.get("id") or first.get("displayId") or "")
|
||||
base_id = item_id.split("_")[0] if item_id else ""
|
||||
normalized.append({
|
||||
"sourceFileKey": first.get("sourceFileKey", ""),
|
||||
"sourceFilename": first.get("sourceFilename", ""),
|
||||
"groupKey": first.get("groupKey") or base_id,
|
||||
"baseId": first.get("baseId") or base_id,
|
||||
"displayId": first.get("displayId") or item_id,
|
||||
"items": items,
|
||||
})
|
||||
return normalized
|
||||
|
||||
def fetch_parsed_payload(self, task_id, user_id=1):
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/similar-asin/tasks/{task_id}/parsed-payload"
|
||||
response = requests.get(url, params={"user_id": user_id}, timeout=300, verify=False)
|
||||
response.raise_for_status()
|
||||
payload = response.json() if response.text else {}
|
||||
if isinstance(payload, dict) and payload.get("success"):
|
||||
return payload.get("data") or {}
|
||||
raise RuntimeError(f"获取货源查询解析载荷失败: {payload}")
|
||||
|
||||
def _merge_parsed_payload(self, data, parsed_payload):
|
||||
payload_rows = parsed_payload.get("allItems") or parsed_payload.get("items") or []
|
||||
merged = {
|
||||
**data,
|
||||
"groups": parsed_payload.get("groups") or [],
|
||||
"rows": payload_rows,
|
||||
}
|
||||
if parsed_payload.get("aiPrompt"):
|
||||
merged["prompt"] = parsed_payload.get("aiPrompt")
|
||||
if parsed_payload.get("apiKey") and not merged.get("api_key"):
|
||||
merged["api_key"] = parsed_payload.get("apiKey")
|
||||
return merged
|
||||
|
||||
def process_task(self, task_data: dict):
|
||||
"""处理审批任务主入口
|
||||
|
||||
Args:
|
||||
task_data: 任务数据
|
||||
"""
|
||||
try:
|
||||
data = task_data.get("data", {})
|
||||
task_id = data.get("taskId")
|
||||
parsed_payload = self.fetch_parsed_payload(task_id, data.get("user_id") or data.get("userId") or 1)
|
||||
data = self._merge_parsed_payload(data, parsed_payload)
|
||||
groups = self.normalize_groups(data)
|
||||
print(groups)
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理爬取任务 {task_id},{len(groups)} 个任务")
|
||||
|
||||
if not groups:
|
||||
self.log("appearance-patent groups/rows is empty, skip", "WARNING")
|
||||
return
|
||||
|
||||
runing_task[task_id] = {
|
||||
"status": "running",
|
||||
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"total_shops": 1,
|
||||
"processed_shops": 0,
|
||||
"total_countries": len(groups) ,
|
||||
"processed_countries": 0,
|
||||
"total_asins": 0,
|
||||
"processed_asins": 0,
|
||||
"success_count": 0,
|
||||
"failed_count": 0,
|
||||
"stop_requested": False
|
||||
}
|
||||
|
||||
# 检查是否收到暂停请求
|
||||
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
|
||||
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
return
|
||||
max_retry = 3
|
||||
show_notification(f"开始爬取数据", "info")
|
||||
chrome = ChromeAmzone()
|
||||
try:
|
||||
# 数据整理
|
||||
# new_items = self.group_by_id_prefix(items)
|
||||
result = []
|
||||
for gp_index,gp in enumerate(groups):
|
||||
items = gp.get("items", [])
|
||||
group_item = []
|
||||
for index,value in enumerate(items):
|
||||
print(value)
|
||||
return_data = {
|
||||
'image_url': "",
|
||||
'title': "",
|
||||
'category' : '',
|
||||
'sku' : '',
|
||||
'success' : False,
|
||||
'similar_data' : []
|
||||
}
|
||||
asin = value.get("asin")
|
||||
country = value.get("country")
|
||||
for _ in range(max_retry):
|
||||
try:
|
||||
return_data = chrome.run(country, asin,total_page=1) or {}
|
||||
self.log(f"抓取结果->{return_data}")
|
||||
break
|
||||
except Exception as e:
|
||||
# if "与页面的连接已断开" in str(e):
|
||||
chrome = ChromeAmzone()
|
||||
self.log(f"{asin}抓取数据报错,{e}")
|
||||
# if not isinstance(return_data, dict):
|
||||
# return_data = {}
|
||||
# if return_data.get("image_url"):
|
||||
# break
|
||||
|
||||
group_item.append({
|
||||
"sourceFileKey": value.get("sourceFileKey",""),
|
||||
"sourceFilename": value.get("sourceFilename",""),
|
||||
"rowToken": value.get("rowToken",""),
|
||||
"groupKey": value.get("groupKey",""),
|
||||
"id": value.get("values",{}).get("id",""),
|
||||
"asin": value.get("asin",""),
|
||||
"sku" : return_data.get("sku",""),
|
||||
"country": value.get("country",""),
|
||||
"url": return_data.get("image_url",""),
|
||||
"title": return_data.get("title",""),
|
||||
"done": False,
|
||||
"urls" : [i.get("ori_picture") for i in return_data.get("similar_data",[])[:16]]
|
||||
})
|
||||
# task_id: int, chunkIndex:int,chunkTotal: int, country_code: str, asin: str, status: dict,error:str="",
|
||||
# item_data:dict={},
|
||||
|
||||
|
||||
res = {
|
||||
"sourceFileKey": gp.get("sourceFileKey"),
|
||||
"sourceFilename": gp.get("sourceFilename"),
|
||||
"groupKey": gp.get("groupKey"),
|
||||
"baseId": gp.get("baseId"),
|
||||
"displayId": gp.get("displayId"),
|
||||
"items": group_item
|
||||
}
|
||||
|
||||
# print("================")
|
||||
result.append(res)
|
||||
# print("================")
|
||||
is_done = gp_index == len(groups)-1
|
||||
if len(result) > 5 or is_done:
|
||||
self.post_result(task_id=task_id,chunkIndex=gp_index+1,chunkTotal=len(groups),
|
||||
asin=asin,item_data=result,is_done=is_done)
|
||||
result = []
|
||||
|
||||
|
||||
if len(result) > 0:
|
||||
is_done = True
|
||||
self.post_result(task_id=task_id, chunkIndex=len(groups), chunkTotal=len(groups),
|
||||
asin=asin, item_data=result, is_done=is_done)
|
||||
|
||||
|
||||
# 更新已处理店铺数
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["processed_shops"] += 1
|
||||
except Exception as e:
|
||||
self.log(f"处理店铺 {task_id} 失败: {str(e)}", "ERROR")
|
||||
self.log(traceback.format_exc(), "ERROR")
|
||||
|
||||
try:
|
||||
chrome.close()
|
||||
except Exception as e:
|
||||
print("退出浏览器出错", e)
|
||||
|
||||
# 更新任务状态
|
||||
if task_id in runing_task:
|
||||
if runing_task[task_id].get("stop_requested", False):
|
||||
runing_task[task_id]["status"] = "stopped"
|
||||
self.log(f"任务 {task_id} 已被暂停!")
|
||||
else:
|
||||
runing_task[task_id]["status"] = "completed"
|
||||
|
||||
self.log(f"任务 {task_id} 处理完成!")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
|
||||
if task_id:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
def post_result(self, task_id: int, chunkIndex:int,chunkTotal: int, asin: str, error:str="",
|
||||
item_data:dict={},
|
||||
is_done: bool = False):
|
||||
"""回传处理结果到API
|
||||
"""
|
||||
|
||||
url = f"{DELETE_BRAND_API_BASE}/api/similar-asin/tasks/{task_id}/result"
|
||||
|
||||
payload ={
|
||||
"submissionId": f"{task_id}",
|
||||
"chunkIndex": chunkIndex,
|
||||
"chunkTotal": chunkTotal,
|
||||
"error": error,
|
||||
"groups": item_data,
|
||||
"done": is_done
|
||||
}
|
||||
|
||||
max_retries = 3
|
||||
for retry in range(max_retries):
|
||||
try:
|
||||
request_timeout = 300 if is_done else 30
|
||||
print("================【详情采集】=====================")
|
||||
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
|
||||
self.log(f"回传URL: {url}")
|
||||
self.log(f"回传数据: {payload}")
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=request_timeout,
|
||||
verify=False
|
||||
)
|
||||
self.log(f"回传结果: {response.text}")
|
||||
data = response.json() if response.text else {}
|
||||
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
|
||||
self.log(f"结果回传成功: {asin} - {item_data}")
|
||||
return
|
||||
else:
|
||||
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
|
||||
print("=====================================")
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"调用API异常: {str(e)}", "ERROR")
|
||||
print("=====================================")
|
||||
|
||||
# 如果还有重试机会,等待后继续
|
||||
if retry < max_retries - 1:
|
||||
time.sleep(2)
|
||||
|
||||
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
|
||||
# raise RuntimeError("已达到最大重试次数,结果回传最终失败")
|
||||
|
||||
if __name__ == '__main__':
|
||||
spide = ChromeAmzone()
|
||||
|
||||
resp = spide.run(
|
||||
country="德国", asin="B0CJ8SNXXV", total_page=2
|
||||
)
|
||||
print(resp)
|
||||
# task_data = {
|
||||
# "type": "similar-asin-run",
|
||||
# "ts": 1778038688824,
|
||||
# "data": {
|
||||
# "taskId": 6850,
|
||||
# "api_key": "sk-EMcDFg36zCeWUtCbRzKUbrRZyNeC6M4KBhY6fAVcNP7GG4xI",
|
||||
# "groups": [
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "groupKey": "similar-asin:6850",
|
||||
# "baseId": "",
|
||||
# "displayId": "1",
|
||||
# "items": [
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::2",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::1@2",
|
||||
# "id": "1",
|
||||
# "asin": "B0D792ND9V",
|
||||
# "country": "英国",
|
||||
# "price": "12.29",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::3",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_1",
|
||||
# "asin": "B0D14L34S7",
|
||||
# "country": "英国",
|
||||
# "price": "14.88",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::4",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_2",
|
||||
# "asin": "B0D14N8FJY",
|
||||
# "country": "英国",
|
||||
# "price": "15.36",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::5",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_3",
|
||||
# "asin": "B0D14NBL5D",
|
||||
# "country": "英国",
|
||||
# "price": "14.87",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::6",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_4",
|
||||
# "asin": "B0D14M5X4J",
|
||||
# "country": "英国",
|
||||
# "price": "14.41",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::7",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7",
|
||||
# "id": "3_1",
|
||||
# "asin": "B0CWQFZMGY",
|
||||
# "country": "英国",
|
||||
# "price": "17.05",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::8",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7",
|
||||
# "id": "3_2",
|
||||
# "asin": "B0CX8D68L3",
|
||||
# "country": "英国",
|
||||
# "price": "16.46",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::9",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::4@9",
|
||||
# "id": "4_1",
|
||||
# "asin": "B0CX87XXWJ",
|
||||
# "country": "英国",
|
||||
# "price": "17.54",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::10",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::5@10",
|
||||
# "id": "5_1",
|
||||
# "asin": "B0CR3PMR8L",
|
||||
# "country": "英国",
|
||||
# "price": "16.74",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ],
|
||||
# "rows": [
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::2",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::1@2",
|
||||
# "id": "1",
|
||||
# "asin": "B0D792ND9V",
|
||||
# "country": "英国",
|
||||
# "price": "12.29",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::3",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_1",
|
||||
# "asin": "B0D14L34S7",
|
||||
# "country": "英国",
|
||||
# "price": "14.88",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::4",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_2",
|
||||
# "asin": "B0D14N8FJY",
|
||||
# "country": "英国",
|
||||
# "price": "15.36",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::5",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_3",
|
||||
# "asin": "B0D14NBL5D",
|
||||
# "country": "英国",
|
||||
# "price": "14.87",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::6",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::2@3",
|
||||
# "id": "2_4",
|
||||
# "asin": "B0D14M5X4J",
|
||||
# "country": "英国",
|
||||
# "price": "14.41",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::7",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7",
|
||||
# "id": "3_1",
|
||||
# "asin": "B0CWQFZMGY",
|
||||
# "country": "英国",
|
||||
# "price": "17.05",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::8",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::3@7",
|
||||
# "id": "3_2",
|
||||
# "asin": "B0CX8D68L3",
|
||||
# "country": "英国",
|
||||
# "price": "16.46",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::9",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::4@9",
|
||||
# "id": "4_1",
|
||||
# "asin": "B0CX87XXWJ",
|
||||
# "country": "英国",
|
||||
# "price": "17.54",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# },
|
||||
# {
|
||||
# "sourceFileKey": "cdfa437df62b40a0bde5682a05c1f6a3",
|
||||
# "sourceFilename": "17(1) - 副本1.xlsx",
|
||||
# "rowToken": "cdfa437df62b40a0bde5682a05c1f6a3::row::10",
|
||||
# "groupKey": "cdfa437df62b40a0bde5682a05c1f6a3::5@10",
|
||||
# "id": "5_1",
|
||||
# "asin": "B0CR3PMR8L",
|
||||
# "country": "英国",
|
||||
# "price": "16.74",
|
||||
# "url": "",
|
||||
# "title": "",
|
||||
# "target_urls": []
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# }
|
||||
# sim_asin = SimilarAsinTask({})
|
||||
# sim_asin.process_task(task_data)
|
||||
|
||||
|
||||
|
||||
161
app/amazon/tool.py
Normal file
161
app/amazon/tool.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# 导入 webview 用于前端通知
|
||||
try:
|
||||
import webview
|
||||
except ImportError:
|
||||
webview = None
|
||||
|
||||
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import re
|
||||
|
||||
|
||||
def show_notification(message: str, message_type: str = "error"):
|
||||
"""显示 pywebview 顶层通知(5秒后自动消失)
|
||||
|
||||
Args:
|
||||
message: 通知消息
|
||||
message_type: 消息类型 (success/warning/error/info)
|
||||
"""
|
||||
if webview and webview.windows:
|
||||
try:
|
||||
# 转义单引号,防止 JavaScript 语法错误
|
||||
safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
|
||||
|
||||
# 设置通知样式颜色
|
||||
color_map = {
|
||||
'success': '#67C23A',
|
||||
'warning': '#E6A23C',
|
||||
'error': '#F56C6C',
|
||||
'info': '#909399'
|
||||
}
|
||||
bg_color = color_map.get(message_type, '#F56C6C')
|
||||
|
||||
# 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知
|
||||
js_code = f"""
|
||||
(function() {{
|
||||
// 移除已存在的通知
|
||||
var existingNotif = document.getElementById('pywebview-notification');
|
||||
if (existingNotif) {{
|
||||
existingNotif.remove();
|
||||
}}
|
||||
|
||||
// 创建通知容器
|
||||
var notif = document.createElement('div');
|
||||
notif.id = 'pywebview-notification';
|
||||
notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' +
|
||||
'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' +
|
||||
'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' +
|
||||
'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;';
|
||||
|
||||
// 添加消息内容
|
||||
var msgSpan = document.createElement('span');
|
||||
msgSpan.textContent = '{safe_message}';
|
||||
notif.appendChild(msgSpan);
|
||||
|
||||
// 添加关闭按钮
|
||||
var closeBtn = document.createElement('span');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;';
|
||||
closeBtn.onclick = function() {{ notif.remove(); }};
|
||||
notif.appendChild(closeBtn);
|
||||
|
||||
// 添加到页面
|
||||
document.body.appendChild(notif);
|
||||
|
||||
// 5秒后自动消失
|
||||
setTimeout(function() {{
|
||||
if (notif && notif.parentNode) {{
|
||||
notif.style.transition = 'opacity 0.3s';
|
||||
notif.style.opacity = '0';
|
||||
setTimeout(function() {{ notif.remove(); }}, 300);
|
||||
}}
|
||||
}}, 5000);
|
||||
}})();
|
||||
"""
|
||||
# 在第一个窗口中执行 JavaScript
|
||||
webview.windows[0].evaluate_js(js_code)
|
||||
except Exception as e:
|
||||
print(f"显示通知失败: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
def get_shop_info(shop_name: str, base_url: str = "http://8.136.19.173:18080") -> requests.Response:
|
||||
"""
|
||||
调用商铺凭证接口
|
||||
:param shop_name: 商铺名称,将自动进行 URL 编码
|
||||
:param base_url: API 基础地址,默认从 curl 中提取
|
||||
:return: requests.Response 对象
|
||||
"""
|
||||
url = f"{base_url}/api/admin/shop-manages/credential"
|
||||
params = {"shopName": shop_name}
|
||||
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": "http://8.136.19.173:18080/doc.html",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
||||
"X-Internal-Token": "59c6691199917a2095827b5188502029",
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
verify=False,
|
||||
timeout=30
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
def remove_special_characters(text: str) -> str:
|
||||
"""
|
||||
去除字符串中的特殊字符,只保留数字、小数点和负号。
|
||||
例如:'£24.55' -> '24.55'
|
||||
"""
|
||||
# 匹配所有允许的字符:数字、小数点、负号
|
||||
# 注意:负号必须位于开头才合法,但这里只做字符保留,不做格式校验
|
||||
cleaned = re.sub(r'[^0-9.-]', '', text)
|
||||
return cleaned
|
||||
|
||||
|
||||
def split_currency_values(currency_str: str) -> tuple[float, float]:
|
||||
"""
|
||||
将包含两个货币值的字符串拆分成两个浮点数。
|
||||
参数:
|
||||
currency_str (str): 格式如 "€22.64 + €0.00" 的字符串,中间以 '+' 分隔,
|
||||
每部分可包含任意货币符号或前缀/后缀。
|
||||
返回:
|
||||
tuple[float, float]: 两个数值,顺序与字符串中的出现顺序一致。
|
||||
异常:
|
||||
ValueError: 如果字符串不包含正好两个部分,或者任一部分中无法提取到数值。
|
||||
"""
|
||||
# 按第一个 '+' 分割,最多分为两部分
|
||||
parts = currency_str.split('+', 1)
|
||||
if len(parts) != 2:
|
||||
raise ValueError("字符串必须包含两个由 '+' 分隔的部分")
|
||||
|
||||
# 匹配整数或浮点数(可选负号)
|
||||
number_pattern = r'-?\d+(?:\.\d+)?'
|
||||
values = []
|
||||
|
||||
for part in parts:
|
||||
# 去除首尾空格
|
||||
part = part.strip()
|
||||
match = re.search(number_pattern, part)
|
||||
if not match:
|
||||
raise ValueError(f"无法从 '{part}' 中提取数值")
|
||||
values.append(float(match.group()))
|
||||
|
||||
return tuple(values)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cu_str = "€58.44 + €0.00 匹配"
|
||||
res = split_currency_values(cu_str)
|
||||
print(sum(res))
|
||||
58
app/app.py
58
app/app.py
@@ -3,8 +3,9 @@
|
||||
按功能拆分为蓝图:认证(auth)、主页面(main)、管理员(admin)、图片(image)、品牌(brand)
|
||||
"""
|
||||
import os
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
@@ -17,6 +18,50 @@ from blueprints.image import image_bp
|
||||
from blueprints.brand import brand_bp
|
||||
from blueprints.communication import communication_bp
|
||||
|
||||
from config import cache_path, debug
|
||||
|
||||
|
||||
def setup_flask_logging(app):
|
||||
"""配置Flask访问日志,输出到独立的日志文件"""
|
||||
|
||||
api_cache_path = os.path.join(cache_path, "api_logs")
|
||||
os.makedirs(api_cache_path, exist_ok=True)
|
||||
|
||||
# 只在非debug模式下配置文件日志
|
||||
if not debug:
|
||||
# 获取当前日期作为日志文件名的一部分
|
||||
today = datetime.now().strftime("%Y_%m_%d")
|
||||
log_file = os.path.join(api_cache_path, f'api_{today}.log')
|
||||
|
||||
# 创建文件处理器,使用RotatingFileHandler避免日志文件过大
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
backupCount=5,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
# 设置日志格式
|
||||
formatter = logging.Formatter(
|
||||
'[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# 配置Flask应用日志
|
||||
app.logger.addHandler(file_handler)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
# 配置Werkzeug日志(HTTP访问日志)
|
||||
werkzeug_logger = logging.getLogger('werkzeug')
|
||||
werkzeug_logger.addHandler(file_handler)
|
||||
werkzeug_logger.setLevel(logging.INFO)
|
||||
|
||||
print(f"API 日志已配置,输出到: {log_file}")
|
||||
else:
|
||||
print("Debug模式下,API 日志输出到控制台")
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR)
|
||||
@@ -27,12 +72,6 @@ def create_app():
|
||||
supports_credentials=True,
|
||||
resources={r"/api/*": {"origins": cors_origins}, r"/login": {"origins": cors_origins}},
|
||||
)
|
||||
# 生产环境必须通过环境变量注入固定 SECRET_KEY;否则服务重启会使旧会话失效。
|
||||
app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-me')
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax')
|
||||
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('SESSION_COOKIE_SECURE', '0') == '1'
|
||||
|
||||
# 注册蓝图(不设 url_prefix,保持原有 URL 路径不变,前端无需改动)
|
||||
app.register_blueprint(auth_bp)
|
||||
@@ -41,6 +80,9 @@ def create_app():
|
||||
app.register_blueprint(image_bp)
|
||||
app.register_blueprint(brand_bp)
|
||||
app.register_blueprint(communication_bp)
|
||||
|
||||
# 配置Flask访问日志
|
||||
setup_flask_logging(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染
|
||||
公共模块:数据库连接、初始化、JWT 鉴权装饰器、模板渲染
|
||||
供各蓝图复用
|
||||
"""
|
||||
import os
|
||||
@@ -8,13 +8,19 @@ from datetime import timedelta
|
||||
from functools import wraps
|
||||
|
||||
import pymysql
|
||||
from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string
|
||||
from flask import request, redirect, url_for, jsonify, render_template, render_template_string, g
|
||||
|
||||
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
||||
from jwt_util import parse_token, get_token_from_request
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
||||
ASSETS_DIR = os.path.join(BASE_DIR, 'assets')
|
||||
WEB_SOURCE_DIR = os.path.join(BASE_DIR, 'web_source')
|
||||
TEMPLATE_FALLBACK_DIRS = (
|
||||
WEB_SOURCE_DIR,
|
||||
os.path.join(WEB_SOURCE_DIR, 'templates_backup'),
|
||||
)
|
||||
|
||||
|
||||
def get_db():
|
||||
@@ -30,8 +36,12 @@ def get_db():
|
||||
|
||||
def _render_html(template_name: str, **context):
|
||||
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
||||
path = os.path.join(BASE_DIR, "web_source", template_name)
|
||||
if not os.path.isfile(path):
|
||||
path = next(
|
||||
(candidate for candidate in (os.path.join(base_path, template_name) for base_path in TEMPLATE_FALLBACK_DIRS)
|
||||
if os.path.isfile(candidate)),
|
||||
None,
|
||||
)
|
||||
if path is None:
|
||||
return render_template(template_name, **context)
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
@@ -45,8 +55,12 @@ def _render_html(template_name: str, **context):
|
||||
|
||||
def _render_html_new(template_name: str, **context):
|
||||
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
||||
path = os.path.join(BASE_DIR, "web_source", template_name)
|
||||
if not os.path.isfile(path):
|
||||
path = next(
|
||||
(candidate for candidate in (os.path.join(base_path, template_name) for base_path in TEMPLATE_FALLBACK_DIRS)
|
||||
if os.path.isfile(candidate)),
|
||||
None,
|
||||
)
|
||||
if path is None:
|
||||
return render_template(template_name, **context)
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
@@ -59,9 +73,36 @@ def _render_html_new(template_name: str, **context):
|
||||
|
||||
|
||||
|
||||
def _resolve_jwt_user():
|
||||
"""从 Authorization/cookie 解析 JWT,命中后写入 flask.g 缓存。无效返回 None。"""
|
||||
cached = getattr(g, "_aiimage_user", None)
|
||||
if cached is not None:
|
||||
return cached or None
|
||||
token = get_token_from_request()
|
||||
payload = parse_token(token) if token else None
|
||||
if not payload:
|
||||
g._aiimage_user = False
|
||||
return None
|
||||
g._aiimage_user = payload
|
||||
g.aiimage_user_id = payload.get("user_id")
|
||||
g.aiimage_username = payload.get("username") or ""
|
||||
g.aiimage_device_id = payload.get("device_id") or ""
|
||||
return payload
|
||||
|
||||
|
||||
def current_user_id():
|
||||
user = _resolve_jwt_user()
|
||||
return user.get("user_id") if user else None
|
||||
|
||||
|
||||
def current_username():
|
||||
user = _resolve_jwt_user()
|
||||
return user.get("username") if user else ""
|
||||
|
||||
|
||||
def _is_session_user_valid():
|
||||
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
|
||||
uid = session.get('user_id')
|
||||
"""兼容旧调用:判断当前 JWT 用户是否仍存在于数据库。"""
|
||||
uid = current_user_id()
|
||||
if not uid:
|
||||
return False
|
||||
try:
|
||||
@@ -70,23 +111,22 @@ def _is_session_user_valid():
|
||||
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
session.clear()
|
||||
return False
|
||||
return True
|
||||
return bool(row)
|
||||
except Exception:
|
||||
session.clear()
|
||||
return False
|
||||
|
||||
|
||||
def _get_current_admin_role():
|
||||
"""获取当前登录用户的管理角色:super_admin / admin / None(非管理员)"""
|
||||
uid = current_user_id()
|
||||
if not uid:
|
||||
return None, None
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
||||
(session['user_id'],)
|
||||
(uid,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
@@ -97,13 +137,19 @@ def _get_current_admin_role():
|
||||
return None, None
|
||||
|
||||
|
||||
def _unauthorized_response():
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' \
|
||||
or request.path.startswith('/api/') \
|
||||
or 'application/json' in (request.headers.get('Accept') or ''):
|
||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('user_id') or not _is_session_user_valid():
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
if not current_user_id():
|
||||
return _unauthorized_response()
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
@@ -111,22 +157,24 @@ def login_required(f):
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('user_id'):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
uid = current_user_id()
|
||||
if not uid:
|
||||
return _unauthorized_response()
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],))
|
||||
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (uid,))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row or not row.get('is_admin'):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' \
|
||||
or request.path.startswith('/api/') \
|
||||
or 'application/json' in (request.headers.get('Accept') or ''):
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
return redirect(url_for('main.home'))
|
||||
except Exception as e:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' \
|
||||
or request.path.startswith('/api/'):
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
return redirect(url_for('main.home'))
|
||||
return f(*args, **kwargs)
|
||||
|
||||
1
app/assets/appearance-patent-BoTBnU7J.css
Normal file
1
app/assets/appearance-patent-BoTBnU7J.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/appearance-patent.js
Normal file
1
app/assets/appearance-patent.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/brand-COze15GJ.js
Normal file
1
app/assets/brand-COze15GJ.js
Normal file
@@ -0,0 +1 @@
|
||||
import{bN as r}from"./java-modules-B8c-YG5x.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/categorized-timers-JPA-olTr.js
Normal file
1
app/assets/categorized-timers-JPA-olTr.js
Normal file
@@ -0,0 +1 @@
|
||||
const r=new Map;function c(n){let t=r.get(n);return t||(t=new Map,r.set(n,t)),t}function u(n,t){const e=r.get(n);e&&(e.delete(t),e.size||r.delete(n))}function l(n,t,e){const i=window.setTimeout(()=>{u(n,i),t()},e);return c(n).set(i,{id:i,kind:"timeout",category:n}),i}function a(n,t,e){const i=window.setInterval(t,e);return c(n).set(i,{id:i,kind:"interval",category:n}),i}function f(n,t){if(t==null)return;const i=r.get(n)?.get(t);i?.kind==="interval"?window.clearInterval(t):window.clearTimeout(t),i?.cancel?.(),u(n,t)}function s(n){const t=r.get(n);if(t){for(const e of t.values())e.kind==="interval"?window.clearInterval(e.id):window.clearTimeout(e.id),e.cancel?.();r.delete(n)}}function d(n,t){return new Promise(e=>{const i=window.setTimeout(()=>{u(n,i),e()},t);c(n).set(i,{id:i,kind:"timeout",category:n,cancel:e})})}function m(n){const t=`${n}:`;for(const e of Array.from(r.keys()))(e===n||e.startsWith(t))&&s(e)}function w(n){const t=e=>`${n}:${e}`;return{setTimeout(e,i,o){return l(t(e),i,o)},setInterval(e,i,o){return a(t(e),i,o)},clearTimer(e,i){f(t(e),i)},clearCategory(e){s(t(e))},clearScope(){m(n)},sleep(e,i){return d(t(e),i)}}}export{w as c};
|
||||
1
app/assets/collect-data-DPGAQszU.css
Normal file
1
app/assets/collect-data-DPGAQszU.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/collect-data.js
Normal file
1
app/assets/collect-data.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-6oxZNMye.css
Normal file
1
app/assets/convert-6oxZNMye.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-DpfPYQDv.css
Normal file
1
app/assets/dedupe-DpfPYQDv.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-DZ3x5hOX.css
Normal file
1
app/assets/delete-brand-DZ3x5hOX.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/el-input-Cxc-k42P.css
Normal file
1
app/assets/el-input-Cxc-k42P.css
Normal file
File diff suppressed because one or more lines are too long
55
app/assets/java-modules-B8c-YG5x.js
Normal file
55
app/assets/java-modules-B8c-YG5x.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/java-modules-l5anrOZ2.css
Normal file
1
app/assets/java-modules-l5anrOZ2.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/listingFilters-BpGOU_pJ.js
Normal file
1
app/assets/listingFilters-BpGOU_pJ.js
Normal file
@@ -0,0 +1 @@
|
||||
const e=[{value:"SearchSuppressed",label:"在搜索结果中禁止显示"},{value:"ApprovalRequired",label:"需要批准"},{value:"Active",label:"在售"},{value:"DetailPageRemoved",label:"详情页面已删除"}];export{e as L};
|
||||
1
app/assets/listingFilters-CK58rX4v.css
Normal file
1
app/assets/listingFilters-CK58rX4v.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/patrol-delete-Cqhg2qXm.css
Normal file
1
app/assets/patrol-delete-Cqhg2qXm.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/patrol-delete.js
Normal file
1
app/assets/patrol-delete.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/price-track-BsA0AIfw.css
Normal file
1
app/assets/price-track-BsA0AIfw.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/price-track.js
Normal file
1
app/assets/price-track.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/product-risk-BQIMXTON.css
Normal file
1
app/assets/product-risk-BQIMXTON.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/product-risk.js
Normal file
1
app/assets/product-risk.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/query-asin-CECO5lUB.css
Normal file
1
app/assets/query-asin-CECO5lUB.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/query-asin.js
Normal file
1
app/assets/query-asin.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/shop-match-DCW4m_j8.css
Normal file
1
app/assets/shop-match-DCW4m_j8.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/shop-match.js
Normal file
1
app/assets/shop-match.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/similar-asin-Bl4LsbT8.css
Normal file
1
app/assets/similar-asin-Bl4LsbT8.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/similar-asin.js
Normal file
1
app/assets/similar-asin.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/split-CR1PtDQE.css
Normal file
1
app/assets/split-CR1PtDQE.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
app/assets/zh-cn-CscLo6na.js
Normal file
1
app/assets/zh-cn-CscLo6na.js
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user