Compare commits
58 Commits
ef0e0df0ac
...
b352eea21d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b352eea21d | ||
|
|
51bf0aaa8c | ||
|
|
28a752265c | ||
|
|
1d9d138423 | ||
|
|
8c5edc6ad6 | ||
|
|
ee6fb5d33a | ||
|
|
cd2b437be5 | ||
|
|
a504e7a13b | ||
|
|
325687d532 | ||
|
|
9760d1171c | ||
|
|
d42ed57119 | ||
|
|
18208f691c | ||
|
|
c06a4d0e06 | ||
|
|
f21934e55b | ||
|
|
515135c120 | ||
|
|
42bdd59c71 | ||
|
|
8be85fd332 | ||
|
|
e4bf104ae2 | ||
|
|
5105bf7049 | ||
|
|
08997e9e20 | ||
|
|
9146d17625 | ||
|
|
1aca3d6152 | ||
|
|
052bb31aea | ||
|
|
225b525ba1 | ||
|
|
e1543416e3 | ||
|
|
0b57aea5f6 | ||
|
|
a04c2f9a19 | ||
|
|
c352c34501 | ||
|
|
5c9671df44 | ||
|
|
8a8d3c5cd9 | ||
|
|
d272afae1c | ||
|
|
03e697f5d3 | ||
|
|
c502afb588 | ||
|
|
3fada5d198 | ||
|
|
c0fdea6570 | ||
|
|
0391cb223f | ||
|
|
b25111e4b4 | ||
|
|
2f2db4986e | ||
|
|
6f970b3783 | ||
|
|
95d5c82474 | ||
|
|
87507708ce | ||
|
|
c6ae7ca170 | ||
|
|
6894f9cc57 | ||
|
|
524d8763ce | ||
|
|
1e845a1510 | ||
|
|
0341838d19 | ||
|
|
ea35273597 | ||
|
|
72c8167472 | ||
|
|
9b1138c83e | ||
|
|
dc5e23892f | ||
|
|
4f8cbc3e38 | ||
|
|
3ffbb2b004 | ||
|
|
34ed15a9bd | ||
|
|
4b6295dd44 | ||
|
|
01116d5607 | ||
|
|
07904a29aa | ||
|
|
8d683a791d | ||
|
|
ac07416352 |
107
.gitignore
vendored
107
.gitignore
vendored
@@ -1,41 +1,110 @@
|
||||
# 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
|
||||
desktop/
|
||||
ERP-Demo/
|
||||
xlsx/
|
||||
app/__pycache__
|
||||
# ===== backend / app =====
|
||||
backend/tmp/
|
||||
backend/__pycache__
|
||||
app/__pycache__
|
||||
app/assets/
|
||||
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
|
||||
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/
|
||||
.omx/
|
||||
.codex
|
||||
.rtk
|
||||
OPS_REDIS_MYSQL_OPTIMIZATION_NOTES.md
|
||||
架构.md
|
||||
*ts.%
|
||||
|
||||
409
README.md
Normal file
409
README.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# crawler-plugin
|
||||
|
||||
## 这个仓库是什么
|
||||
|
||||
这个仓库不是一个“单体项目”,而是一个混合工作目录。当前更像是以下三层共同组成的一套系统:
|
||||
|
||||
- `app/`:Python 桌面宿主、Flask 页面承载层、`pywebview` 本地桥接层、自动化任务入口
|
||||
- `backend-java/`:Java 业务后端,负责任务模型、文件处理、进度缓存、结果组装、落库、下载
|
||||
- `frontend-vue/`:Vue 多页面前端,负责工具页交互和任务发起
|
||||
|
||||
如果你是第一次接手这个仓库,不要先把它理解成:
|
||||
|
||||
- 纯 Java 后端项目
|
||||
- 纯 Python 自动化项目
|
||||
- 纯前后端分离 Web 项目
|
||||
|
||||
它当前的真实形态更接近:
|
||||
|
||||
`Vue 页面 -> Python 桌面桥接/Flask -> Java 任务系统 -> Redis / DB / OSS`
|
||||
|
||||
同时,Python 自动化还会再去驱动本地紫鸟客户端和浏览器。
|
||||
|
||||
## 先看哪里
|
||||
|
||||
如果你的目标是理解当前主线,请按这个顺序读:
|
||||
|
||||
1. `app/`
|
||||
2. `backend-java/`
|
||||
3. `frontend-vue/`
|
||||
|
||||
原因很简单:
|
||||
|
||||
- `app/` 决定了桌面端怎么承载页面、怎么暴露本地能力、怎么把任务推给 Python 自动化
|
||||
- `backend-java/` 决定了任务怎么创建、状态怎么缓存、结果怎么回传、文件怎么生成和下载
|
||||
- `frontend-vue/` 决定了用户是怎么触发这些链路的
|
||||
|
||||
以下目录不要默认当成当前主线:
|
||||
|
||||
- `source_code/`
|
||||
- `backend/`
|
||||
|
||||
它们和 `app/` 有明显重叠,更像历史版本、迁移残留或中间态副本。阅读时要先带着“可能不是当前生效版本”的假设。
|
||||
|
||||
## 主线架构怎么分工
|
||||
|
||||
### Python 层:桌面壳 + 本地桥接 + 自动化执行
|
||||
|
||||
`app/` 不是单纯的业务后端。它更像一个桌面宿主,负责三件事:
|
||||
|
||||
- 承载 Flask 页面和登录态
|
||||
- 通过 `pywebview` 暴露本地文件选择、保存文件、上传文件、任务入队等能力
|
||||
- 启动和调度 Python 自动化任务
|
||||
|
||||
这一层解决的是“本地能力”和“桌面壳”的问题,不是最终任务结果的权威存储。
|
||||
|
||||
### Java 层:任务系统 + 结果系统
|
||||
|
||||
`backend-java/` 是当前业务核心后端,负责:
|
||||
|
||||
- 文件上传与临时文件管理
|
||||
- 任务创建、历史查询、批量轮询
|
||||
- Redis 进度缓存
|
||||
- 分片结果接收与聚合
|
||||
- 结果文件生成
|
||||
- OSS 上传
|
||||
- 下载接口输出
|
||||
|
||||
从模块名看,Java 端已经承接了大部分工具型任务,例如:
|
||||
|
||||
- `brand`
|
||||
- `dedupe`
|
||||
- `split`
|
||||
- `convert`
|
||||
- `deletebrand`
|
||||
- `productrisk`
|
||||
- `shopmatch`
|
||||
- `pricetrack`
|
||||
|
||||
### Vue 层:多页面工具前端
|
||||
|
||||
`frontend-vue/` 不是单页应用,而是多入口页面工程。它的作用主要是:
|
||||
|
||||
- 让用户选择文件或参数
|
||||
- 调用 Java API 创建任务或查询任务
|
||||
- 调用 `pywebview` 桥接拿本地能力
|
||||
- 在任务执行过程中轮询状态和下载结果
|
||||
|
||||
它构建后的产物输出到 `new_web_source/`,再由 Python/Flask 暴露出来供桌面端使用。
|
||||
|
||||
## 如何理解目录
|
||||
|
||||
### 当前最值得关注的目录
|
||||
|
||||
#### `app/`
|
||||
|
||||
这是当前阅读优先级最高的 Python 目录,重点看这些角色:
|
||||
|
||||
- `main.py`:创建 `pywebview` 窗口,暴露本地桥接 API
|
||||
- `app.py`:Flask 应用入口
|
||||
- `blueprints/`:页面、登录、品牌、通信等 HTTP 层
|
||||
- `amazon/`:删除品牌、商品风险、匹配、跟价等自动化任务实现
|
||||
|
||||
#### `backend-java/`
|
||||
|
||||
这是任务系统核心,重点关注:
|
||||
|
||||
- controller:对外 API
|
||||
- service:任务创建、进度、结果回传、组装
|
||||
- model / mapper:任务和结果模型
|
||||
- `application.yml`:后端依赖配置
|
||||
|
||||
#### `frontend-vue/`
|
||||
|
||||
这是当前工具页前端源码,重点关注:
|
||||
|
||||
- `src/shared/bridges/pywebview.ts`:前端如何调 Python 本地桥接
|
||||
- `src/shared/api/java-modules.ts`:前端如何调 Java API
|
||||
- `src/pages/brand/components/`:各个具体工具页
|
||||
|
||||
### 不要误判成源码主线的目录
|
||||
|
||||
#### `new_web_source/`
|
||||
|
||||
更像前端构建产物目录,不是首选手改源码位置。
|
||||
|
||||
#### `source_code/`
|
||||
|
||||
和 `app/` 结构高度相似,但从当前仓库关系看,更像旧线或中间迁移副本。
|
||||
|
||||
#### `backend/`
|
||||
|
||||
另一套 Python 后端实现,和当前主线职责不完全一致,更像旧后台线。
|
||||
|
||||
## 用“patrol-delete”看懂最近新增的自动化链路
|
||||
|
||||
从最近的提交看,`patrol-delete` 是当前最新落地的一条自动化模块。它同样同时经过:
|
||||
|
||||
- 前端
|
||||
- Python 桌面桥接
|
||||
- Python 自动化执行
|
||||
- Java 任务系统
|
||||
|
||||
但它和“删除品牌”有一个关键差别:
|
||||
|
||||
- `delete-brand` 更偏“本地 Excel 文件驱动”
|
||||
- `patrol-delete` 更偏“店铺输入 + 任务模板 + 串行自动化执行”
|
||||
|
||||
如果你想快速理解当前新增自动化功能的开发方式,优先看这条线更合适。
|
||||
|
||||
下面按职责来拆。
|
||||
|
||||
### 1. 用户先在前端输入店铺,并加入备选区
|
||||
|
||||
用户打开 `patrol-delete` 页面后,不是先选本地文件,而是:
|
||||
|
||||
- 输入店铺名
|
||||
- 将店铺加入备选区
|
||||
- 勾选要处理的店铺
|
||||
- 点击“匹配店铺”
|
||||
|
||||
这一阶段前端的核心职责是收集“要处理哪个店铺”。
|
||||
|
||||
### 2. 前端先让 Java 做店铺匹配
|
||||
|
||||
前端不会直接把“店铺名”交给 Python 执行,而是先调用 Java 的匹配接口。
|
||||
|
||||
Java 在这一阶段负责:
|
||||
|
||||
1. 根据店铺名查索引
|
||||
2. 判断是否能命中店铺
|
||||
3. 返回 `shopId`、平台、公司、匹配状态等信息
|
||||
|
||||
这里要注意:
|
||||
|
||||
- 这是 `frontend -> Java`
|
||||
- 目的是先把“可执行店铺”筛出来
|
||||
- Python 还没有开始实际自动化
|
||||
|
||||
### 3. 前端要求 Java 创建“patrol-delete 任务”
|
||||
|
||||
匹配通过后,前端调用 Java 的建任务接口。
|
||||
|
||||
Java 在这一阶段做的事情也不是“立刻去删商品”,而是:
|
||||
|
||||
1. 接收店铺与模板结构
|
||||
2. 创建 `taskId`
|
||||
3. 创建结果记录
|
||||
4. 返回当前任务项的初始数据给前端
|
||||
|
||||
这一阶段可以理解成:
|
||||
|
||||
- Java 负责建模和建任务
|
||||
- Python 还没有开始自动化执行
|
||||
|
||||
### 4. 前端把“可执行项”推入 Python 本地队列
|
||||
|
||||
`patrol-delete` 不是由 Java 主动调用 Python,而是前端把任务 payload 推给 Python。
|
||||
|
||||
这一步通过 `pywebview` 暴露的 `enqueue_json()` 完成,前端会构造一个类似下面的 payload:
|
||||
|
||||
- `type: patrol-delete-run`
|
||||
- `taskId`
|
||||
- `user_id`
|
||||
- `items`
|
||||
- `template_rows`
|
||||
- `country_sections`
|
||||
- `cart_ratios`
|
||||
|
||||
然后交给 Python。
|
||||
|
||||
重要区分:
|
||||
|
||||
- 这一步不是 Java 队列
|
||||
- 这是 Python 本地进程内队列
|
||||
- 当前仓库里对应的是 `JSON_TASK_QUEUE`
|
||||
|
||||
### 5. Python 的任务监听器开始消费队列
|
||||
|
||||
Python 侧有一个任务监听器持续盯着 `JSON_TASK_QUEUE`。
|
||||
|
||||
按 `patrol-delete` 这条链路的设计,Python 拿到任务后会按店铺串行执行,再在店铺内部做页面自动化。
|
||||
|
||||
这一层的职责重点是:
|
||||
|
||||
1. 根据 `taskId` 和店铺信息定位当前任务
|
||||
2. 串行执行当前店铺,避免多个店铺同时抢本地浏览器环境
|
||||
3. 按模板结构产出各国家的删除结果与购物车比例
|
||||
4. 在执行过程中持续回传 Java
|
||||
|
||||
同时,Python 会在本地维护当前任务状态,例如:
|
||||
|
||||
- 当前店铺
|
||||
- 当前步骤
|
||||
- 已完成/失败数量
|
||||
- 是否需要继续下一个店铺
|
||||
|
||||
这些状态主要服务于 Python 本地执行期,不是前端最终查询任务状态的权威来源。
|
||||
|
||||
### 6. Python 自动化驱动紫鸟客户端和浏览器
|
||||
|
||||
`patrol-delete` 的“真正执行动作”也发生在这里。
|
||||
|
||||
Python 不会直接操作普通浏览器,而是先和紫鸟客户端通信,再接管浏览器调试口。大致过程是:
|
||||
|
||||
1. 连接或启动本机紫鸟客户端
|
||||
2. 打开指定店铺
|
||||
3. 进入需要巡店/删除的目标页面
|
||||
4. 根据模板中的国家块、状态和数量条件读取页面数据
|
||||
5. 找到符合删除条件的商品
|
||||
6. 执行删除并整理结果
|
||||
|
||||
所以这一段实际上又分两层:
|
||||
|
||||
- `Python -> 紫鸟客户端`:本地 HTTP IPC
|
||||
- `Python -> 浏览器`:通过调试口/自动化驱动执行页面操作
|
||||
|
||||
### 7. Python 一边执行,一边把结果分片回传给 Java
|
||||
|
||||
这条链路的关键点在这里:
|
||||
|
||||
- Python 不是等整个任务做完再一次性提交结果
|
||||
- 而是每处理完一部分,就立刻回传给 Java
|
||||
|
||||
`patrol-delete` 这里是按“店铺结果分片”回传,更强调:
|
||||
|
||||
- `shopName`
|
||||
- `submissionId`
|
||||
- `chunkIndex`
|
||||
- `chunkTotal`
|
||||
- `shopDone`
|
||||
- `countrySections`
|
||||
- `cartRatios`
|
||||
- `error`
|
||||
|
||||
回传方式:
|
||||
|
||||
- `Python -> Java`
|
||||
- `Content-Type: application/json`
|
||||
- 接口:`/api/patrol-delete/tasks/{taskId}/result`
|
||||
|
||||
回传的数据里会包含:
|
||||
|
||||
- 当前店铺标识
|
||||
- `chunkIndex`
|
||||
- `chunkTotal`
|
||||
- 该店铺当前片段的国家结果
|
||||
- 购物车比例
|
||||
- 当前片段是否已结束
|
||||
- 失败时的错误信息
|
||||
|
||||
因此,`patrol-delete` 这条线里更核心的是一类传输:
|
||||
|
||||
1. 执行结果回传:`application/json`
|
||||
|
||||
它不依赖“先上传本地 Excel 文件”这个前置动作。
|
||||
|
||||
### 8. Java 负责接收分片、缓存进度、判断是否可以完结
|
||||
|
||||
Java 收到 Python 的结果分片后,不会简单地“收一条写一条最终结果”,而是会先做任务系统层面的处理:
|
||||
|
||||
1. 校验 `taskId`
|
||||
2. 按 `taskId + shopName` 聚合店铺结果
|
||||
3. 过滤重复分片
|
||||
4. 将结果分片缓存起来
|
||||
5. 更新实时进度
|
||||
6. 判断某个店铺是否已 `shopDone`
|
||||
7. 判断整个任务是否达到 finalize 条件
|
||||
|
||||
这一步说明 Java 才是“任务状态和最终结果”的权威系统。
|
||||
|
||||
### 9. Java 在分片收齐后生成最终结果
|
||||
|
||||
当 Java 发现所有需要的结果分片都已收齐时,会执行最终组装:
|
||||
|
||||
1. 合并分片
|
||||
2. 重建最终结果数据
|
||||
3. 生成结果 Excel
|
||||
4. 上传 OSS
|
||||
5. 更新任务状态
|
||||
6. 写入结果记录
|
||||
|
||||
如果这一步成功,任务会变成 `SUCCESS`;否则会进入 `FAILED`。
|
||||
|
||||
### 10. 前端轮询 Java,不轮询 Python
|
||||
|
||||
前端展示任务状态时,查询对象是 Java,而不是 Python 本地队列。
|
||||
|
||||
前端主要关心的是:
|
||||
|
||||
- 任务详情
|
||||
- 批量进度
|
||||
- 下载地址
|
||||
|
||||
也就是说:
|
||||
|
||||
- Python 负责执行
|
||||
- Java 负责对前端提供任务状态与结果
|
||||
|
||||
### 11. 前端最终通过 Java 下载结果,再交给 Python 保存到本地
|
||||
|
||||
任务成功后,前端拿到 Java 下载地址,然后再通过 `pywebview` 调 Python 的保存能力,把文件落回用户本地磁盘。
|
||||
|
||||
所以最后一步仍然是混合协作:
|
||||
|
||||
- 下载来源是 Java
|
||||
- 本地保存能力来自 Python 桌面桥接
|
||||
|
||||
## 这条例子可以类比到哪些模块
|
||||
|
||||
`patrol-delete` 不是孤例,它更像当前仓库最近新增自动化模块的代表模式。
|
||||
|
||||
同类模式至少还能看到这些任务类型:
|
||||
|
||||
- `patrol-delete-run`
|
||||
- `delete-brand-run`
|
||||
- `product-risk-resolve-run`
|
||||
- `shop-match-run`
|
||||
- `price-track`
|
||||
|
||||
它们的共性通常是:
|
||||
|
||||
1. 前端先建任务
|
||||
2. Python 负责本地自动化执行
|
||||
3. Java 负责任务状态、结果缓存和最终输出
|
||||
|
||||
其中:
|
||||
|
||||
- `patrol-delete` 更适合理解“店铺驱动 + 模板驱动”的新链路
|
||||
- `delete-brand` 更适合理解“本地文件上传 + 文件解析驱动”的旧链路
|
||||
|
||||
理解这两条线之后,再看商品风险、店铺匹配、跟价等模块会容易很多。
|
||||
|
||||
## 接手时最容易踩的坑
|
||||
|
||||
### 不要把 `enqueue_json()` 当成 Java 队列
|
||||
|
||||
它是前端调 Python 的本地桥接,进入的是 Python 进程内队列,不是 Java 消息系统。
|
||||
|
||||
### 不要把 `new_web_source/` 当成首选源码目录
|
||||
|
||||
它更像构建产物输出,首选还是看 `frontend-vue/`。
|
||||
|
||||
### 不要默认 `source_code/` 和 `backend/` 仍是当前主线
|
||||
|
||||
这两个目录有参考价值,但从当前结构看,不应先于 `app/`、`backend-java/`、`frontend-vue/` 阅读。
|
||||
|
||||
### 不要把 Python 当成最终任务状态来源
|
||||
|
||||
Python 负责执行期状态;前端面向用户看到的任务状态、历史、下载结果,当前主权在 Java。
|
||||
|
||||
## 当前文档的边界
|
||||
|
||||
这份 README 基于当前仓库的静态分析整理,有几个边界需要明确:
|
||||
|
||||
- 没有运行仓库程序做启动验证
|
||||
- 没有确认最终发布时到底启用哪一个入口
|
||||
- 对 `source_code/`、`backend/` 的判断是“更像旧线/残留”,不是运行态绝对结论
|
||||
|
||||
所以这份文档的目标不是提供启动手册,而是帮助接手开发者先建立正确的系统地图。
|
||||
|
||||
## 一句话结论
|
||||
|
||||
理解这个仓库最有效的方式不是按语言分开看,而是按职责看:
|
||||
|
||||
- Python 负责桌面壳、本地桥接、自动化执行
|
||||
- Java 负责任务系统、结果系统、下载系统
|
||||
- Vue 负责工具页交互和任务发起
|
||||
|
||||
其中,“删除品牌自动化”更适合帮助你理解旧的文件驱动链路;当前最新落地的 `patrol-delete`,则更适合当作你理解新增自动化功能的第一条例子。
|
||||
5
app/.env
5
app/.env
@@ -1,4 +1,3 @@
|
||||
base_url=http://8.136.19.173:15124
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=8.136.19.173
|
||||
mysql_user=aiimage
|
||||
@@ -13,7 +12,7 @@ client_name=ShuFuAI
|
||||
|
||||
|
||||
# java_api_base=http://47.111.163.154:18080
|
||||
java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
java_api_base=http://8.136.19.173:18080
|
||||
|
||||
|
||||
|
||||
18
app/.env.example
Normal file
18
app/.env.example
Normal file
@@ -0,0 +1,18 @@
|
||||
workflow_id=7608812635877900322
|
||||
mysql_host=8.136.19.173
|
||||
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://47.111.163.154:18080
|
||||
# java_api_base=http://127.0.0.1:18080
|
||||
# java_api_base=http://8.136.19.173:18080
|
||||
|
||||
|
||||
13
app/.rtk/filters.toml
Normal file
13
app/.rtk/filters.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Project-local RTK filters — commit this file with your repo.
|
||||
# Filters here override user-global and built-in filters.
|
||||
# Docs: https://github.com/rtk-ai/rtk#custom-filters
|
||||
schema_version = 1
|
||||
|
||||
# Example: suppress build noise from a custom tool
|
||||
# [filters.my-tool]
|
||||
# description = "Compact my-tool output"
|
||||
# match_command = "^my-tool\\s+build"
|
||||
# strip_ansi = true
|
||||
# strip_lines_matching = ["^\\s*$", "^Downloading", "^Installing"]
|
||||
# max_lines = 30
|
||||
# on_empty = "my-tool: ok"
|
||||
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(
|
||||
|
||||
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.
@@ -2,6 +2,7 @@ import time
|
||||
import re
|
||||
import traceback
|
||||
from DrissionPage._pages.chromium_tab import ChromiumTab
|
||||
import pyautogui
|
||||
|
||||
from config import runing_task, runing_shop
|
||||
from datetime import datetime
|
||||
@@ -86,10 +87,11 @@ class AmzoneApprove(AmamzonBase):
|
||||
def wait_loaded(self):
|
||||
# 等待加载完成
|
||||
try:
|
||||
load_ele = self.tab.ele(
|
||||
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)
|
||||
load_ele.wait.deleted(timeout=5, raise_err=False)
|
||||
if len(load_ele) > 0:
|
||||
load_ele[0].wait.deleted(timeout=5, raise_err=False)
|
||||
except Exception as e:
|
||||
print("等待加载中消失出错", e)
|
||||
|
||||
@@ -106,67 +108,317 @@ class AmzoneApprove(AmamzonBase):
|
||||
print("当前操作的tab_id",self.tab.tab_id)
|
||||
self.browser.close_tabs(close_tab)
|
||||
|
||||
def handle_repair_product(self,tab:ChromiumTab):
|
||||
def wait_loaded():
|
||||
# def handle_repair_product_old(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=60,raise_err=False)
|
||||
# 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
|
||||
# for _ in range(3):
|
||||
# try:
|
||||
# # 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)
|
||||
# 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)
|
||||
|
||||
# print("修复商品信息处理完成!")
|
||||
# tab.close()
|
||||
|
||||
def handle_repair_product(self,tab:ChromiumTab,headle_type=""):
|
||||
def wait_loaded(wait_type="contentWrapper"):
|
||||
# 等待加载完成
|
||||
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)
|
||||
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)
|
||||
# if len(close_x_btn) > 0:
|
||||
# close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
|
||||
# close_x_btn[0].click()
|
||||
# time.sleep(1)
|
||||
# close_x_btn[0].wait.deleted(timeout=5, raise_err=False)
|
||||
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)
|
||||
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("未找到提交按钮,可能页面未加载完成或者页面结构发生变化")
|
||||
|
||||
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 遍历
|
||||
for index in range(len(submit_btn_ls)):
|
||||
if index > len(submit_btn_ls)-1:
|
||||
print("提交按钮数量发生变化,停止处理")
|
||||
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
|
||||
|
||||
submit_btn = submit_btn_ls[index]
|
||||
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)
|
||||
if len(close_x_btn) > 0:
|
||||
close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
|
||||
close_x_btn[0].click()
|
||||
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()
|
||||
time.sleep(0.5)
|
||||
self.wait_loaded()
|
||||
|
||||
# 先直接等待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()
|
||||
|
||||
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("需要输入注册号,不符合操作要求,跳过...")
|
||||
close_contentWrapper()
|
||||
continue
|
||||
|
||||
target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]')
|
||||
target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]',timeout=10)
|
||||
print(f"序号{index}【警告和安全信息】数量 ->",len(target_title))
|
||||
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("安全证明-不是未开始状态,不需要操作")
|
||||
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"]')
|
||||
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"]')
|
||||
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()
|
||||
|
||||
@@ -175,8 +427,11 @@ class AmzoneApprove(AmamzonBase):
|
||||
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=5)
|
||||
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:
|
||||
@@ -187,30 +442,41 @@ class AmzoneApprove(AmamzonBase):
|
||||
save_btn_ls[0].click()
|
||||
|
||||
wait_loaded()
|
||||
close_contentWrapper()
|
||||
print(f"序号{index}【选择保存】操作完成")
|
||||
continue
|
||||
|
||||
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)
|
||||
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):
|
||||
print("开始执行")
|
||||
num = 0
|
||||
retry_num = 0
|
||||
already_asin = set()
|
||||
|
||||
while retry_num < 3: # 最多重试3次
|
||||
now_page = None
|
||||
max_retry_num = 5
|
||||
while retry_num < max_retry_num: # 最多重试3次
|
||||
# if num > 3: #测试
|
||||
# return
|
||||
# 等待加载完成
|
||||
@@ -218,8 +484,8 @@ class AmzoneApprove(AmamzonBase):
|
||||
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)
|
||||
|
||||
time.sleep(0.5)
|
||||
# 获取当前页码
|
||||
try:
|
||||
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
|
||||
@@ -235,20 +501,30 @@ class AmzoneApprove(AmamzonBase):
|
||||
print(f"当前页码: {current_page} / 总页数: {total_page}")
|
||||
except Exception as e:
|
||||
print("获取页码失败", e)
|
||||
|
||||
print("==========商品风险解决==============")
|
||||
|
||||
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
|
||||
print(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)
|
||||
continue
|
||||
# for sku_ele in sku_ls[0:2]:
|
||||
for sku_ele in sku_ls:
|
||||
for sku_ele in sku_ls[1:]:
|
||||
# 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} *****************")
|
||||
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="解决商品移除风险"]')
|
||||
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()="添加缺失的商品详情"]')
|
||||
@@ -264,6 +540,9 @@ class AmzoneApprove(AmamzonBase):
|
||||
|
||||
solve_problem[0].click()
|
||||
|
||||
label = solve_problem[0].attr('label')
|
||||
print("【当前类别】",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)
|
||||
|
||||
@@ -274,15 +553,34 @@ class AmzoneApprove(AmamzonBase):
|
||||
# 如果存在“请求批准”按钮,则直接返回跳过
|
||||
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()
|
||||
# 新增"查看冻结原因" 的情况下,需要点击修复商品详情
|
||||
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)
|
||||
print(f"{asin}【查看冻结原因】修复商品信息数量",len(repair_produ_btn))
|
||||
if len(repair_produ_btn) > 0:
|
||||
print(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)
|
||||
|
||||
action_panel.wait.deleted(timeout=3, raise_err=False)
|
||||
yield (asin,"请求批准")
|
||||
already_asin.add(asin)
|
||||
continue
|
||||
self.handle_repair_product(new_tab,headle_type="查看冻结原因")
|
||||
else:
|
||||
print(f"{i}【查看冻结原因】修复商品信息没有,不需要操作")
|
||||
else:
|
||||
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,"请求批准")
|
||||
already_asin.add(asin)
|
||||
continue
|
||||
|
||||
# 无需采取任何操作
|
||||
not_operate = self.tab.eles('xpath://kat-alert[contains(@description,"如果您之前已提交更改,则这些更改当前正在处理中") and not(@dismissed)]',timeout=3)
|
||||
@@ -310,7 +608,7 @@ class AmzoneApprove(AmamzonBase):
|
||||
|
||||
# TODO 增加 “出现警告和安全信息时候下滑” 的情况
|
||||
safe_handle_ls = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]',
|
||||
timeout=5)
|
||||
timeout=15)
|
||||
if len(safe_handle_ls) > 0:
|
||||
print("存在安全证明,正在处理...")
|
||||
safe_handle_ls[0].click()
|
||||
@@ -329,17 +627,19 @@ class AmzoneApprove(AmamzonBase):
|
||||
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()
|
||||
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.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-button[@label="关闭"]',
|
||||
close_btn = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-button[@label="关闭"]',
|
||||
timeout=10)
|
||||
close_btn.click()
|
||||
if len(close_btn)>0:
|
||||
close_btn[0].click()
|
||||
except Exception as e:
|
||||
print("点击关闭按钮失败",e)
|
||||
|
||||
@@ -351,7 +651,7 @@ class AmzoneApprove(AmamzonBase):
|
||||
print(f"ASIN {asin} 存在修复商品信息按钮,正在点击...")
|
||||
self.clear_tab()
|
||||
repair_product[0].click()
|
||||
time.sleep(1)
|
||||
time.sleep(3)
|
||||
# 获取最新的tab
|
||||
new_tab = self.browser.latest_tab
|
||||
if isinstance(new_tab, str):
|
||||
@@ -366,6 +666,7 @@ class AmzoneApprove(AmamzonBase):
|
||||
|
||||
yield (asin,"处理完成")
|
||||
already_asin.add(asin)
|
||||
print(f"*********************{asin} 处理完成*************************")
|
||||
|
||||
try:
|
||||
# 全部操作完成,关闭
|
||||
@@ -556,6 +857,8 @@ class ApproveTask:
|
||||
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")
|
||||
@@ -623,7 +926,90 @@ class ApproveTask:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
|
||||
def open_shop(self,max_retries,company_name,shop_name,iskill=False):
|
||||
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 = AmzoneApprove(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_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
|
||||
"""处理单个店铺
|
||||
|
||||
@@ -652,120 +1038,49 @@ class ApproveTask:
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
max_retries = 3
|
||||
|
||||
error_info = ""
|
||||
for retry in range(max_retries):
|
||||
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(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
|
||||
|
||||
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
|
||||
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
|
||||
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
self.log(f"{task_id}任务处理完成")
|
||||
# 最后回传,标记完成
|
||||
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} 已从执行列表中移除")
|
||||
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
|
||||
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):
|
||||
"""处理单个国家的审批任务
|
||||
@@ -796,7 +1111,16 @@ class ApproveTask:
|
||||
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("重试前刷新页面...")
|
||||
@@ -868,42 +1192,33 @@ class ApproveTask:
|
||||
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 in ["请求批准", "添加缺失的商品详情"]:
|
||||
# 请求批准的商品也算作成功(因为不需要处理)
|
||||
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
|
||||
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 in ["请求批准", "添加缺失的商品详情"]:
|
||||
# 请求批准的商品也算作成功(因为不需要处理)
|
||||
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")
|
||||
|
||||
|
||||
self.log(f"国家 {country_name} 处理完成")
|
||||
|
||||
@@ -993,14 +1308,14 @@ if __name__ == '__main__':
|
||||
"username": "自动化_Robot",
|
||||
"password": "#20zsg25"
|
||||
}
|
||||
shop_name = "魏振峰"
|
||||
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 = "ApprovalRequired"
|
||||
risk_listing_filter = "DetailPageRemoved"
|
||||
for _ in range(3):
|
||||
try:
|
||||
sku_ls = driver.search(filter_type=risk_listing_filter)
|
||||
|
||||
714
app/amazon/asin_status.py
Normal file
714
app/amazon/asin_status.py
Normal file
@@ -0,0 +1,714 @@
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
# sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
import time
|
||||
import re
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from DrissionPage import Chromium, ChromiumOptions
|
||||
import requests
|
||||
|
||||
from amazon.del_brand import AmamzonBase, kill_process
|
||||
from amazon.tool import show_notification, get_shop_info, remove_special_characters, split_currency_values
|
||||
|
||||
from config import runing_task, runing_shop, base_dir, DELETE_BRAND_API_BASE
|
||||
|
||||
|
||||
class AmzonePStatus(AmamzonBase):
|
||||
mark_name = "状态查询"
|
||||
|
||||
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_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):
|
||||
"""
|
||||
|
||||
miniprice_info :
|
||||
{
|
||||
"B0DSJGJBWV" : "19.81" # asin : 最低价
|
||||
}
|
||||
"""
|
||||
print(f"【{self.mark_name}】,开始执行")
|
||||
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:
|
||||
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"):
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if level == "ERROR":
|
||||
show_notification(message, "error")
|
||||
print(f"[{timestamp}] [PriceTask] [{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", [])[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:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
# if not items:
|
||||
# self.log("店铺列表为空,跳过", "WARNING")
|
||||
# return
|
||||
|
||||
if not queryAsins:
|
||||
self.log("queryAsins 列表为空,跳过", "WARNING")
|
||||
return
|
||||
|
||||
self.log(f"开始处理审批任务 {task_id},共 1 个店铺,{len(queryAsins)} 个国家")
|
||||
|
||||
from config import runing_task
|
||||
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:
|
||||
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 open_shop(self, max_retries, company_name, shop_name, iskill=False):
|
||||
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 = AmzonePStatus(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_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(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:
|
||||
import traceback
|
||||
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:
|
||||
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
|
||||
|
||||
# 切换到库存管理页面
|
||||
for retry in range(max_retries):
|
||||
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 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("已完成操作")
|
||||
760
app/amazon/base.py
Normal file
760
app/amazon/base.py
Normal file
@@ -0,0 +1,760 @@
|
||||
import json
|
||||
import os
|
||||
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 = 19890
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
@@ -169,7 +169,7 @@ class ZiniaoDriver:
|
||||
"cookieTypeLoad": 0,
|
||||
"cookieTypeSave": cookieTypeSave,
|
||||
"runMode": "1",
|
||||
"isLoadUserPlugin": False,
|
||||
"isLoadUserPlugin": True,
|
||||
"pluginIdType": 1,
|
||||
"privacyMode": isprivacy
|
||||
}
|
||||
@@ -314,6 +314,7 @@ class ZiniaoDriver:
|
||||
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):
|
||||
@@ -499,26 +500,55 @@ class AmamzonBase(ZiniaoDriver):
|
||||
|
||||
def login(self,password,username=""):
|
||||
try:
|
||||
pwd_input = self.tab.eles('xpath://input[@type="password"]',timeout=10)
|
||||
if len(pwd_input) > 0:
|
||||
pwd_input[0].input(password,clear=True)
|
||||
submit_btn = self.tab.eles('xpath://input[@id="signInSubmit"]',timeout=10)
|
||||
if len(submit_btn) > 0:
|
||||
submit_btn[0].click()
|
||||
self.tab.wait.doc_loaded()
|
||||
self.tab.wait.doc_loaded(timeout=30,raise_err=False)
|
||||
for _ in range(4):
|
||||
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)
|
||||
|
||||
opt_code_input = self.tab.ele('xpath://input[@name="otpCode"]',timeout=60)
|
||||
opt_code_input.wait.displayed(timeout=10,raise_err=False)
|
||||
for _ in range(30):
|
||||
if opt_code_input.value is not None and opt_code_input.value.strip() != "":
|
||||
print("检测到验证码输入完成")
|
||||
submit_btn = self.tab.ele('xpath://input[@id="auth-signin-button"]',timeout=10)
|
||||
submit_btn.click()
|
||||
self.tab.wait.doc_loaded()
|
||||
return True
|
||||
time.sleep(1)
|
||||
submit_btn = self.tab.ele('xpath://input[@id="auth-signin-button"]',timeout=10)
|
||||
submit_btn.click()
|
||||
send_code = self.tab.eles('xpath://span[@id="auth-send-code" and contains(string(.),"发送一次性密码")]',timeout=5)
|
||||
print("发送一次性密码:",len(send_code))
|
||||
if len(send_code) > 0:
|
||||
print("检测到 发送一次性密码")
|
||||
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)
|
||||
print("验证码输入框:",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:
|
||||
print("验证码输入错误")
|
||||
print("验证码输入错误提示:",error_mes[0].text)
|
||||
self.tab.refresh()
|
||||
else:
|
||||
print("登入成功")
|
||||
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:
|
||||
print("登录过程中发生异常:" + traceback.format_exc())
|
||||
return False
|
||||
|
||||
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
@@ -8,6 +8,10 @@ from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_B
|
||||
from amazon.del_brand import AmazoneDriver, kill_process
|
||||
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.tool import get_shop_info,show_notification
|
||||
@@ -53,7 +57,11 @@ class TaskMonitor:
|
||||
|
||||
task_type_info = {
|
||||
"product-risk-resolve-run" : "产品风险审批",
|
||||
"shop-match-run" : "匹配价格"
|
||||
"shop-match-run" : "匹配价格",
|
||||
"price-track-run" : "跟价",
|
||||
"query-asin-run" : "状态查询",
|
||||
"patrol-delete-run" : "巡店删除",
|
||||
"appearance-patent-run" : "亚马逊采集",
|
||||
}
|
||||
try:
|
||||
while self.running:
|
||||
@@ -119,7 +127,11 @@ class TaskMonitor:
|
||||
try:
|
||||
TASK_INFO = {
|
||||
"产品风险审批" : ApproveTask,
|
||||
"匹配价格" : MatchTak
|
||||
"匹配价格" : MatchTak,
|
||||
"跟价" : PriceTask,
|
||||
"状态查询" : StatusTask,
|
||||
"巡店删除" : PatrolDeleteTask,
|
||||
"亚马逊采集" : SpiderTask,
|
||||
}
|
||||
self.log(f"线程 {id(task_data)} 开始处理产品风险审批任务...")
|
||||
# 创建ApproveTask实例并处理任务
|
||||
@@ -129,8 +141,7 @@ class TaskMonitor:
|
||||
self.log(f"线程 {id(task_data)} 产品风险审批任务处理完成")
|
||||
except Exception as e:
|
||||
self.log(f"线程 {id(task_data)} 产品风险审批任务处理异常: {traceback.format_exc()}", "ERROR")
|
||||
|
||||
|
||||
|
||||
def process_task(self, task_data: Dict[str, Any]):
|
||||
"""处理单个任务
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@ class AmzoneMatchAction(AmamzonBase):
|
||||
num = 0
|
||||
retry_num = 0
|
||||
already_asin = set()
|
||||
get_page_faild = 0
|
||||
|
||||
while retry_num < 3: # 最多重试3次
|
||||
# if num > 3: #测试
|
||||
@@ -117,9 +118,13 @@ class AmzoneMatchAction(AmamzonBase):
|
||||
else:
|
||||
total_page = 0
|
||||
print(f"【{self.mark_name}】当前页码: {current_page} / 总页数: {total_page}")
|
||||
get_page_faild = 0
|
||||
except Exception as e:
|
||||
print(f"【{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)}")
|
||||
@@ -127,13 +132,13 @@ class AmzoneMatchAction(AmamzonBase):
|
||||
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=10).text
|
||||
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
|
||||
|
||||
price_match = sku_ele.eles('xpath:.//div[@data-test-id="FeaturedOfferPrice"]//a[text()="匹配"]',timeout=5)
|
||||
price_match = sku_ele.eles('xpath:.//div[@data-test-id="FeaturedOfferPrice"]//a[text()="匹配"]',timeout=2)
|
||||
if len(price_match) == 0:
|
||||
print(f"【{self.mark_name}】{asin},没有推荐价格匹配按钮")
|
||||
yield (asin,"无需处理")
|
||||
@@ -230,6 +235,9 @@ class MatchTak:
|
||||
stage_index = data.get("stage_index")
|
||||
final_stage = bool(data.get("final_stage", True))
|
||||
|
||||
# 用于测试
|
||||
limit = data.get("limit",None)
|
||||
|
||||
if not task_id:
|
||||
self.log("任务ID为空,跳过", "WARNING")
|
||||
return
|
||||
@@ -272,7 +280,7 @@ class MatchTak:
|
||||
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)
|
||||
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)
|
||||
# 更新已处理店铺数
|
||||
@@ -300,9 +308,92 @@ class MatchTak:
|
||||
if task_id in runing_task:
|
||||
runing_task[task_id]["status"] = "failed"
|
||||
runing_task[task_id]["error"] = str(e)
|
||||
|
||||
|
||||
def open_shop(self,max_retries,company_name,shop_name,iskill=False):
|
||||
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 = AmzoneMatchAction(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_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
|
||||
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):
|
||||
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:
|
||||
@@ -330,126 +421,54 @@ class MatchTak:
|
||||
# 店铺打开重试最多3次
|
||||
driver = None
|
||||
max_retries = 3
|
||||
|
||||
error_info = ""
|
||||
for retry in range(max_retries):
|
||||
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
|
||||
|
||||
# 打开店铺
|
||||
driver = self.open_shop(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
|
||||
|
||||
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 = AmzoneMatchAction(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
|
||||
|
||||
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,limit)
|
||||
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
|
||||
|
||||
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
|
||||
self.log(f"{task_id}任务处理完成")
|
||||
# 最后回传,标记完成
|
||||
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)
|
||||
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")
|
||||
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} 已从执行列表中移除")
|
||||
# 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: AmzoneMatchAction, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str):
|
||||
def process_country(self, driver: AmzoneMatchAction, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str,limit:str=None):
|
||||
"""处理单个国家的审批任务
|
||||
|
||||
Args:
|
||||
@@ -478,7 +497,16 @@ class MatchTak:
|
||||
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("重试前刷新页面...")
|
||||
@@ -550,38 +578,31 @@ class MatchTak:
|
||||
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
|
||||
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
|
||||
|
||||
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")
|
||||
|
||||
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_stage_finished(self, task_id: int, user_id, stage_index):
|
||||
import requests
|
||||
from config import DELETE_BRAND_API_BASE
|
||||
|
||||
1882
app/amazon/patrol_delete.py
Normal file
1882
app/amazon/patrol_delete.py
Normal file
File diff suppressed because it is too large
Load Diff
1449
app/amazon/price_match.py
Normal file
1449
app/amazon/price_match.py
Normal file
File diff suppressed because it is too large
Load Diff
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,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),
|
||||
};
|
||||
@@ -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),
|
||||
})),
|
||||
};
|
||||
@@ -7,6 +7,7 @@ except ImportError:
|
||||
|
||||
import requests
|
||||
from urllib.parse import quote
|
||||
import re
|
||||
|
||||
|
||||
def show_notification(message: str, message_type: str = "error"):
|
||||
@@ -112,5 +113,50 @@ def get_shop_info(shop_name: str, base_url: str = "http://8.136.19.173:18080") -
|
||||
|
||||
|
||||
|
||||
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))
|
||||
@@ -16,8 +16,6 @@ from blueprints.admin import admin_bp
|
||||
from blueprints.image import image_bp
|
||||
from blueprints.brand import brand_bp
|
||||
from blueprints.communication import communication_bp
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR)
|
||||
frontend_origin = os.environ.get('FRONTEND_ORIGIN', '*')
|
||||
|
||||
@@ -12,9 +12,14 @@ from flask import request, redirect, url_for, session, jsonify, render_template,
|
||||
|
||||
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
||||
|
||||
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')
|
||||
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,9 +35,13 @@ 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):
|
||||
return render_template(template_name, **context)
|
||||
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()
|
||||
try:
|
||||
@@ -45,9 +54,13 @@ 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):
|
||||
return render_template(template_name, **context)
|
||||
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()
|
||||
try:
|
||||
|
||||
55
app/assets/_plugin-vue_export-helper-Dh4yMZWw.js
Normal file
55
app/assets/_plugin-vue_export-helper-Dh4yMZWw.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/_plugin-vue_export-helper-xvHHTGU_.css
Normal file
1
app/assets/_plugin-vue_export-helper-xvHHTGU_.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/appearance-patent-BNTrbLJh.css
Normal file
1
app/assets/appearance-patent-BNTrbLJh.css
Normal file
@@ -0,0 +1 @@
|
||||
.module-page[data-v-e32c69fa]{min-height:100vh;background:#1a1a1a}.main-content[data-v-e32c69fa]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-e32c69fa]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-e32c69fa]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-e32c69fa],.subsection-title[data-v-e32c69fa]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-e32c69fa]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-e32c69fa],.loading-msg[data-v-e32c69fa],.files[data-v-e32c69fa],.muted[data-v-e32c69fa]{color:#888;font-size:12px;line-height:1.5}.link[data-v-e32c69fa]{color:#6ea8fe;text-decoration:none}.link[data-v-e32c69fa]:hover{color:#9fc5ff}.btns[data-v-e32c69fa],.run-row[data-v-e32c69fa]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-e32c69fa],.btn-run[data-v-e32c69fa],.btn-delete[data-v-e32c69fa]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-e32c69fa]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-e32c69fa]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-e32c69fa]{background:#27ae60}.btn-run[data-v-e32c69fa]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-e32c69fa]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-e32c69fa]{display:block;margin:4px 0}.prompt-card[data-v-e32c69fa]{margin-bottom:18px}.prompt-input[data-v-e32c69fa]{width:100%;box-sizing:border-box;resize:vertical;min-height:180px;padding:10px 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;line-height:1.6;outline:none}.prompt-input[data-v-e32c69fa]:focus{border-color:#3498db}.prompt-default-label[data-v-e32c69fa]{margin-top:10px;color:#8d8d8d;font-size:12px}.prompt-preview[data-v-e32c69fa]{margin-top:10px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#9ea7b3;font-size:12px;line-height:1.6;white-space:pre-wrap}.parse-card[data-v-e32c69fa],.queue-payload[data-v-e32c69fa]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-e32c69fa]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-e32c69fa]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-e32c69fa]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-e32c69fa]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-e32c69fa]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-e32c69fa]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-e32c69fa]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-e32c69fa]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-e32c69fa]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-e32c69fa]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-e32c69fa]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-e32c69fa]{list-style:none;margin:0;padding:12px}.task-item[data-v-e32c69fa]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-e32c69fa]{flex:1;min-width:0}.id[data-v-e32c69fa]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-e32c69fa]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-e32c69fa]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-e32c69fa]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-e32c69fa]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-e32c69fa]{background:#3498db2e;color:#3498db}.btn-delete[data-v-e32c69fa]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-e32c69fa]{flex-direction:column;height:auto}.left-panel[data-v-e32c69fa]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-e32c69fa]{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
1
app/assets/appearance-patent-BPCrG75P.css
Normal file
1
app/assets/appearance-patent-BPCrG75P.css
Normal file
@@ -0,0 +1 @@
|
||||
.module-page[data-v-d71413c4]{min-height:100vh;background:#1a1a1a}.main-content[data-v-d71413c4]{display:flex;height:calc(100vh - 56px);min-height:calc(100vh - 56px)}.left-panel[data-v-d71413c4]{width:400px;background:#1e1e1e;padding:20px;overflow-y:auto;border-right:1px solid #2a2a2a}.right-panel[data-v-d71413c4]{flex:1;min-width:0;background:#1a1a1a;display:flex;flex-direction:column}.section-title[data-v-d71413c4],.subsection-title[data-v-d71413c4]{font-size:13px;color:#bbb;margin-bottom:10px}.upload-zone[data-v-d71413c4]{border:1px dashed #3a3a3a;border-radius:10px;padding:18px;background:#252525;margin-bottom:18px}.hint[data-v-d71413c4],.loading-msg[data-v-d71413c4],.files[data-v-d71413c4],.muted[data-v-d71413c4]{color:#888;font-size:12px;line-height:1.5}.link[data-v-d71413c4]{color:#6ea8fe;text-decoration:none}.link[data-v-d71413c4]:hover{color:#9fc5ff}.btns[data-v-d71413c4],.run-row[data-v-d71413c4]{display:flex;gap:10px;flex-wrap:wrap}.opt-btn[data-v-d71413c4],.btn-run[data-v-d71413c4],.btn-delete[data-v-d71413c4],.download[data-v-d71413c4]{border:none;cursor:pointer;border-radius:7px}.opt-btn[data-v-d71413c4]{padding:8px 14px;color:#ccc;background:#2a2a2a;border:1px solid #3a3a3a}.btn-run[data-v-d71413c4]{padding:10px 18px;color:#fff;background:#3498db;font-weight:600}.btn-queue[data-v-d71413c4]{background:#27ae60}.btn-run[data-v-d71413c4]:disabled{opacity:.55;cursor:not-allowed}.selected-files[data-v-d71413c4]{margin-top:14px;color:#999;font-size:12px;word-break:break-all}.selected-files span[data-v-d71413c4]{display:block;margin:4px 0}.prompt-card[data-v-d71413c4]{margin-bottom:18px}.prompt-input[data-v-d71413c4]{width:100%;box-sizing:border-box;resize:vertical;min-height:180px;padding:10px 12px;border:1px solid #333;border-radius:8px;background:#202020;color:#d8d8d8;font-size:12px;line-height:1.6;outline:none}.prompt-input[data-v-d71413c4]:focus{border-color:#3498db}.prompt-default-label[data-v-d71413c4]{margin-top:10px;color:#8d8d8d;font-size:12px}.prompt-preview[data-v-d71413c4]{margin-top:10px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#9ea7b3;font-size:12px;line-height:1.6;white-space:pre-wrap}.parse-card[data-v-d71413c4],.queue-payload[data-v-d71413c4]{margin-top:14px;padding:12px;border:1px solid #2a2a2a;border-radius:8px;background:#202020;color:#b8c1cc;font-size:12px}.queue-payload[data-v-d71413c4]{max-height:220px;overflow:auto;color:#8fd3ff;white-space:pre-wrap}.panel-header[data-v-d71413c4]{padding:16px 20px;border-bottom:1px solid #2a2a2a;font-size:15px;font-weight:600;color:#ddd}.task-list-wrap[data-v-d71413c4]{flex:1;padding:16px 20px;overflow:auto}.clean-result-summary[data-v-d71413c4]{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.summary-card[data-v-d71413c4]{padding:14px 16px;border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e}.summary-card strong[data-v-d71413c4]{display:block;margin-top:8px;color:#eaf4ff;font-size:22px}.summary-label[data-v-d71413c4]{color:#8d8d8d;font-size:12px}.result-list-wrap[data-v-d71413c4]{border:1px solid #2a2a2a;border-radius:8px;background:#1e1e1e;min-height:180px;margin:0 0 16px}.result-list-header[data-v-d71413c4]{display:flex;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #2a2a2a;color:#ddd;font-size:14px}.empty-tasks[data-v-d71413c4]{color:#666;font-size:13px;padding:18px;text-align:center}.result-table[data-v-d71413c4]{--el-table-bg-color: #222;--el-table-tr-bg-color: #222;--el-table-header-bg-color: #2a2a2a;--el-table-text-color: #ccc;--el-table-border-color: #333}.task-list[data-v-d71413c4]{list-style:none;margin:0;padding:12px}.task-item[data-v-d71413c4]{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px;border:1px solid #2a2a2a;border-radius:8px;margin-bottom:8px;background:#222}.left[data-v-d71413c4]{flex:1;min-width:0}.id[data-v-d71413c4]{color:#e0e0e0;font-size:13px;font-weight:600}.task-right[data-v-d71413c4]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.status[data-v-d71413c4]{padding:4px 10px;border-radius:6px;font-size:12px}.status.success[data-v-d71413c4]{background:#2ecc712e;color:#2ecc71}.status.failed[data-v-d71413c4]{background:#e74c3c2e;color:#ff6b6b}.status.running[data-v-d71413c4]{background:#3498db2e;color:#3498db}.result-hint[data-v-d71413c4]{margin-top:6px;color:#e0b96d}.download[data-v-d71413c4]{padding:6px 10px;color:#d6ecff;background:#3498db2e}.btn-delete[data-v-d71413c4]{padding:6px 10px;color:#ff8f8f;background:#e74c3c1f}@media(max-width:1100px){.main-content[data-v-d71413c4]{flex-direction:column;height:auto}.left-panel[data-v-d71413c4]{width:100%;border-right:none;border-bottom:1px solid #2a2a2a}.clean-result-summary[data-v-d71413c4]{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
6
app/assets/appearance-patent.js
Normal file
6
app/assets/appearance-patent.js
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/brand-1nUVsWrm.js
Normal file
1
app/assets/brand-1nUVsWrm.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-V_UIajJm.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-5-W0Wonr.js
Normal file
1
app/assets/brand-5-W0Wonr.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aV as r}from"./pywebview-Bt854mYs.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-BATjYfEL.js
Normal file
1
app/assets/brand-BATjYfEL.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-DiP0HdY6.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-BZije8D7.js
Normal file
1
app/assets/brand-BZije8D7.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aZ as r}from"./pywebview-C66x_2Dh.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-C0Q5LZ0P.js
Normal file
1
app/assets/brand-C0Q5LZ0P.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-Cs1Kot1q.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-CFZZtTv8.js
Normal file
1
app/assets/brand-CFZZtTv8.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-BCPdlDdb.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-CKgrwtni.js
Normal file
1
app/assets/brand-CKgrwtni.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aO as r}from"./pywebview-DuyK2jB1.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-CRmRvyGD.js
Normal file
1
app/assets/brand-CRmRvyGD.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aO as r}from"./pywebview-9YBa--7x.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-CXHemZwJ.js
Normal file
1
app/assets/brand-CXHemZwJ.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aI as r}from"./pywebview-D4gpiFjY.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-DBpLCgAF.js
Normal file
1
app/assets/brand-DBpLCgAF.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aO as r}from"./pywebview-ClWy2SbE.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/brand-DRn3N4FA.js
Normal file
1
app/assets/brand-DRn3N4FA.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-D808cNhy.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-DZ5_PGnw.js
Normal file
1
app/assets/brand-DZ5_PGnw.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-Cq_E2BnJ.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-DysFlQbZ.js
Normal file
1
app/assets/brand-DysFlQbZ.js
Normal file
@@ -0,0 +1 @@
|
||||
import{Q as r}from"./pywebview-D7_PdvyM.js";const n="";function s(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{s as e};
|
||||
1
app/assets/brand-UU-ckLq6.js
Normal file
1
app/assets/brand-UU-ckLq6.js
Normal file
@@ -0,0 +1 @@
|
||||
import{aO as r}from"./pywebview-CeWJDVeG.js";const n="";function a(e){return r(`${n}/api/brand/expand-folder-recursive`,{folder:e})}export{a as e};
|
||||
1
app/assets/convert-1dkFPbKa.css
Normal file
1
app/assets/convert-1dkFPbKa.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-6b0yq-M0.css
Normal file
1
app/assets/convert-6b0yq-M0.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-7wWJ02Tw.css
Normal file
1
app/assets/convert-7wWJ02Tw.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-BKSNvX8i.css
Normal file
1
app/assets/convert-BKSNvX8i.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-BxkpMHO9.css
Normal file
1
app/assets/convert-BxkpMHO9.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-CQ22EBpm.css
Normal file
1
app/assets/convert-CQ22EBpm.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-Cfr3mUjs.css
Normal file
1
app/assets/convert-Cfr3mUjs.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-D4Yv2oRy.css
Normal file
1
app/assets/convert-D4Yv2oRy.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/convert-DwbQug2y.css
Normal file
1
app/assets/convert-DwbQug2y.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/dedupe-B0eqpVpj.css
Normal file
1
app/assets/dedupe-B0eqpVpj.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
1
app/assets/dedupe-BHE_BCAJ.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-BpNHwt51.css
Normal file
1
app/assets/dedupe-BpNHwt51.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-CS18ls2z.css
Normal file
1
app/assets/dedupe-CS18ls2z.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-DNlVfFj-.css
Normal file
1
app/assets/dedupe-DNlVfFj-.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-Day_nGxq.css
Normal file
1
app/assets/dedupe-Day_nGxq.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-DyzjKbsT.css
Normal file
1
app/assets/dedupe-DyzjKbsT.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-DzFvTfoj.css
Normal file
1
app/assets/dedupe-DzFvTfoj.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/dedupe-rhGu1E8E.css
Normal file
1
app/assets/dedupe-rhGu1E8E.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-BNvh4ru4.css
Normal file
1
app/assets/delete-brand-BNvh4ru4.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-BP3XWKAC.css
Normal file
1
app/assets/delete-brand-BP3XWKAC.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-BRIDBaoM.css
Normal file
1
app/assets/delete-brand-BRIDBaoM.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-BcZomrN3.css
Normal file
1
app/assets/delete-brand-BcZomrN3.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-BgPrpPsj.css
Normal file
1
app/assets/delete-brand-BgPrpPsj.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-ByeMdpEk.css
Normal file
1
app/assets/delete-brand-ByeMdpEk.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-C-Rj3NMH.css
Normal file
1
app/assets/delete-brand-C-Rj3NMH.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-CDZTH98q.css
Normal file
1
app/assets/delete-brand-CDZTH98q.css
Normal file
File diff suppressed because one or more lines are too long
1
app/assets/delete-brand-CS44Bk-y.css
Normal file
1
app/assets/delete-brand-CS44Bk-y.css
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user