273 Commits

Author SHA1 Message Date
super
bca1335dc6 后端优化,增加新需求 2026-07-21 19:53:47 +08:00
super
ba48bbc6af 更新ASIN库新需求 2026-07-20 20:12:38 +08:00
super
cbde22ec79 视频任务管理更新 2026-07-19 22:23:26 +08:00
super
69784b8d32 添加新需求 2026-07-16 13:12:46 +08:00
super
8543dad514 修复后端BUG 2026-07-14 22:11:19 +08:00
super
8a9126e626 提交昨晚更新改动 2026-07-14 12:50:17 +08:00
super
d8ca8de537 完成视频部分的前后端 2026-07-11 19:42:42 +08:00
super
aab4ce942c 更新带货视频工作流接口和页面 2026-07-09 00:27:49 +08:00
super
0ecf5cd45a 后端优化修复问题 2026-06-18 18:53:16 +08:00
super
021b0c618b 修复图片下载问题 2026-06-16 22:19:55 +08:00
super
f44110fae5 新增数字人版本管理 2026-06-14 19:00:55 +08:00
super
ac19637ad6 处理修复BUG 2026-06-14 16:48:20 +08:00
super
8672668c33 处理下载为公共模块 2026-06-12 13:40:29 +08:00
super
17236265c6 修复删除asinBUG 2026-06-11 20:37:05 +08:00
super
c9efdf8b0e 修复菜单消失 2026-06-10 17:52:58 +08:00
super
c3d43aa26a 更新新需求-采集 2026-06-09 21:07:50 +08:00
super
b0462df3c1 更新新模块内容 更新新需求 2026-06-09 21:07:28 +08:00
super
967eedcad7 提交更新 2026-06-08 18:31:55 +08:00
super
98e0e54309 删除不必要模块 2026-06-08 11:51:08 +08:00
super
eeb27e21ed 删除本地APP项目 2026-06-08 11:49:46 +08:00
super
c85e5b278f 新增内容,新增拉起软件层 2026-06-08 09:19:15 +08:00
super
8ab647c419 提交更改 2026-06-05 21:25:03 +08:00
super
95ccb00d25 fix: reload similar asin chunks after coze flush 2026-06-05 21:18:26 +08:00
super
ee9cf5f8c6 fix brand result submission lock wait 2026-06-05 19:30:20 +08:00
super
e98a1a1207 feat: update task routing and coze handling 2026-06-03 17:14:50 +08:00
super
ea37d82d73 合并分支 2026-05-31 09:28:48 +08:00
super
9835831415 更新地址 更新后端 2026-05-29 22:53:44 +08:00
super
080625567b 同步 Gitee 应用代码更新 2026-05-29 19:47:23 +08:00
super
266c0f17c1 完成这个地址替换 2026-05-29 19:35:55 +08:00
super
225d13fb6e 提交优化更新 2026-05-29 16:49:18 +08:00
super
2ed1250604 增加公共下载进度、增加接收SKU、密钥分别存放 2026-05-28 16:41:20 +08:00
super
ca4a2cd07a python端登录完成 2026-05-27 12:41:59 +08:00
super
73ac9187a6 提交货源流程优化 2026-05-25 22:16:50 +08:00
super
7503e3fa8b 提交货源图片和相似ASIN优化 2026-05-24 21:59:21 +08:00
super
532438faba 提交最新的优化结果 2026-05-24 12:42:53 +08:00
super
a1376b51b0 优化图片尺寸 2026-05-24 02:31:58 +08:00
super
1e087c1aae 提交登录修改 2026-05-23 18:32:45 +08:00
super
2e2de02476 Merge gitee/master into local master (sync from old repo)
# Conflicts:
#	app/web_source/admin.html
#	app/web_source/brand-旧.html
#	app/web_source/home.html
#	app/web_source/index.html
#	app/web_source/login.html
#	backend/web_source/login.html
#	web_source/templates_backup/login.html
2026-05-23 12:00:12 +08:00
super
aeb4e1710c chore: 从 gitee 旧仓库恢复资源和软件更新包 2026-05-23 11:56:36 +08:00
super
b4b80cb572 更新这个货源 2026-05-22 09:41:31 +08:00
super
70e902e7ea 更新这个货源 2026-05-22 09:41:31 +08:00
super
14cebf22c3 提交一些更改 2026-05-20 09:05:28 +08:00
super
73d25fcbcb 提交一些更改 2026-05-20 09:05:28 +08:00
super
01ecde45c5 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-15 16:06:15 +08:00
super
a2ebfc0b81 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-15 16:06:15 +08:00
super
1dc2cac19a 修改完善这个专利部分 2026-05-15 16:06:12 +08:00
super
14db1e0cb1 修改完善这个专利部分 2026-05-15 16:06:12 +08:00
铭坤
01f4dbb83a modified: amazon/similar_asin.py 2026-05-15 11:33:02 +08:00
铭坤
0d63d6d75a modified: amazon/similar_asin.py 2026-05-15 11:33:02 +08:00
铭坤
fe692fcd1d modified: amazon/chrome_base.py
modified:   amazon/detail_spider.py
	modified:   amazon/match_action.py
	modified:   amazon/similar_asin.py
	deleted:    amazon/user_data/chrome_data/BrowserMetrics-spare.pma
	deleted:    amazon/user_data/chrome_data/BrowserMetrics/BrowserMetrics-69FAEF07-558.pma
	deleted:    amazon/user_data/chrome_data/Crashpad/metadata
	deleted:    amazon/user_data/chrome_data/Crashpad/settings.dat
2026-05-13 16:51:29 +08:00
铭坤
79b177b345 modified: amazon/chrome_base.py
modified:   amazon/detail_spider.py
	modified:   amazon/match_action.py
	modified:   amazon/similar_asin.py
	deleted:    amazon/user_data/chrome_data/BrowserMetrics-spare.pma
	deleted:    amazon/user_data/chrome_data/BrowserMetrics/BrowserMetrics-69FAEF07-558.pma
	deleted:    amazon/user_data/chrome_data/Crashpad/metadata
	deleted:    amazon/user_data/chrome_data/Crashpad/settings.dat
2026-05-13 16:51:29 +08:00
super
18b0c2d211 软件前端更新 2026-05-12 23:25:12 +08:00
super
e4e01c1686 软件前端更新 2026-05-12 23:25:12 +08:00
super
648e7d2f14 后台管理接口修复 后端BUG修复 2026-05-12 21:03:42 +08:00
super
0d78d63437 后台管理接口修复 后端BUG修复 2026-05-12 21:03:42 +08:00
super
289b1c67ce 提交货源采集更新 2026-05-10 01:00:08 +08:00
super
5c990e651e 提交货源采集更新 2026-05-10 01:00:08 +08:00
super
52095eb992 更新处理相关内容 2026-05-09 00:20:52 +08:00
super
a4e4745921 更新处理相关内容 2026-05-09 00:20:52 +08:00
super
a2d0bd3c1c Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-07 16:22:42 +08:00
super
b9275bc174 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-07 16:22:42 +08:00
super
683a3934fc 提交打包 2026-05-07 16:21:35 +08:00
super
e5d0b2d9ab 提交打包 2026-05-07 16:21:35 +08:00
铭坤
b0514c3e9a modified: amazon/price_match.py
modified:   config.py
2026-05-07 11:14:15 +08:00
铭坤
d618a5abf0 modified: amazon/price_match.py
modified:   config.py
2026-05-07 11:14:15 +08:00
铭坤
8be12ca9bc new file: requirements.txt 2026-05-07 11:00:44 +08:00
铭坤
5ca46b0a57 new file: requirements.txt 2026-05-07 11:00:44 +08:00
super
afeeb8327c 更新 2026-05-06 23:00:57 +08:00
super
a7d8f8be6c 同步更新 2026-05-06 23:00:39 +08:00
super
4d2da94692 同步更新 2026-05-06 23:00:39 +08:00
super
8cd1a4e2b1 更新外观模块 2026-05-06 22:41:26 +08:00
super
b746812e23 更新外观模块 2026-05-06 22:41:26 +08:00
铭坤
3fbbf87c10 modified: .env
modified:   amazon/approve.py
	modified:   amazon/chrome_base.py
	modified:   amazon/detail_spider.py
	modified:   amazon/main.py
	modified:   amazon/price_match.py
	modified:   amazon/similar_asin.py
	modified:   amazon/tool.py
	new file:   amazon/user_data/chrome_data/BrowserMetrics-spare.pma
	new file:   amazon/user_data/chrome_data/BrowserMetrics/BrowserMetrics-69FAEF07-558.pma
2026-05-06 22:30:44 +08:00
铭坤
b0e97f21a4 modified: .env
modified:   amazon/approve.py
	modified:   amazon/chrome_base.py
	modified:   amazon/detail_spider.py
	modified:   amazon/main.py
	modified:   amazon/price_match.py
	modified:   amazon/similar_asin.py
	modified:   amazon/tool.py
	new file:   amazon/user_data/chrome_data/BrowserMetrics-spare.pma
	new file:   amazon/user_data/chrome_data/BrowserMetrics/BrowserMetrics-69FAEF07-558.pma
2026-05-06 22:30:44 +08:00
super
a00ff1804c 提交更新 2026-05-06 00:01:49 +08:00
super
c3810fdb7b 提交更新 2026-05-06 00:01:49 +08:00
super
203937335d 处理一些合并冲突 2026-05-05 14:11:35 +08:00
super
b16c63225f 处理一些合并冲突 2026-05-05 14:11:35 +08:00
铭坤
710b953120 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin
modified:   amazon/detail_spider.py
	modified:   amazon/main.py
2026-05-04 19:17:11 +08:00
铭坤
45a45dc0fe Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin
modified:   amazon/detail_spider.py
	modified:   amazon/main.py
2026-05-04 19:17:11 +08:00
铭坤
3ca569ed1e new file: amazon/amazon_base.py
modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	new file:   amazon/chrome_base.py
	modified:   amazon/del_brand.py
	modified:   amazon/detail_spider.py
	new file:   "amazon/detail_spider_\345\244\207\344\273\275.py"
	modified:   amazon/main.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	new file:   amazon/similar_asin.py
	modified:   app.py
	new file:   assets/appearance-patent-Br1dtmol.css
	new file:   assets/appearance-patent-D1DgeYhe.css
	new file:   assets/delete-brand-BfMLVSQU.css
	new file:   assets/patrol-delete-BAzbYtgc.css
	new file:   assets/price-track-Hozgt_em.css
	new file:   assets/product-risk-CbX7bwZi.css
	new file:   assets/query-asin-B4RsOiza.css
	new file:   assets/shop-match-B2HWAgBY.css
	modified:   main.py
2026-05-04 19:15:50 +08:00
铭坤
c315bb350c new file: amazon/amazon_base.py
modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	new file:   amazon/chrome_base.py
	modified:   amazon/del_brand.py
	modified:   amazon/detail_spider.py
	new file:   "amazon/detail_spider_\345\244\207\344\273\275.py"
	modified:   amazon/main.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	new file:   amazon/similar_asin.py
	modified:   app.py
	new file:   assets/appearance-patent-Br1dtmol.css
	new file:   assets/appearance-patent-D1DgeYhe.css
	new file:   assets/delete-brand-BfMLVSQU.css
	new file:   assets/patrol-delete-BAzbYtgc.css
	new file:   assets/price-track-Hozgt_em.css
	new file:   assets/product-risk-CbX7bwZi.css
	new file:   assets/query-asin-B4RsOiza.css
	new file:   assets/shop-match-B2HWAgBY.css
	modified:   main.py
2026-05-04 19:15:50 +08:00
super
111f30d150 异常记录检测修改 2026-05-04 19:04:55 +08:00
super
6d398b66bc 异常记录检测修改 2026-05-04 19:04:55 +08:00
super
f46c231323 稳定组成这个文件优化 2026-05-02 16:40:13 +08:00
super
934919c699 稳定组成这个文件优化 2026-05-02 16:40:13 +08:00
super
12d84a22e4 更新 2026-05-02 09:31:14 +08:00
super
9d4810eace Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-02 09:29:43 +08:00
super
dc457aff9e Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-05-02 09:29:43 +08:00
super
3ce66a59ee 提交更新 2026-05-02 09:28:26 +08:00
super
578847d596 提交更新 2026-05-02 09:28:26 +08:00
koko
147b324658 chore: serve bundled assets from app directory 2026-05-02 02:12:14 +08:00
koko
6416c14f20 chore: serve bundled assets from app directory 2026-05-02 02:12:14 +08:00
koko
e47c87f3a3 chore: remove stale web assets 2026-05-02 02:07:13 +08:00
koko
add6e6aeb5 chore: remove stale web assets 2026-05-02 02:07:13 +08:00
koko
1ecce13814 fix: update patrol delete progress handling 2026-05-02 02:03:49 +08:00
koko
7968cef8c3 fix: update patrol delete progress handling 2026-05-02 02:03:49 +08:00
koko
7c2b154c20 Merge patrol delete tests and remove pycache artifacts 2026-05-01 22:46:11 +08:00
koko
34148b5d8a Merge patrol delete tests and remove pycache artifacts 2026-05-01 22:46:11 +08:00
koko
7ba916f00c Merge branch 'dev/koko'
# Conflicts:
#	app/amazon/patrol_delete.py
2026-05-01 22:41:09 +08:00
koko
79e408333d Merge branch 'dev/koko'
# Conflicts:
#	app/amazon/patrol_delete.py
2026-05-01 22:41:09 +08:00
koko
af4f4a9ed4 Fix patrol delete condition handling 2026-05-01 22:36:37 +08:00
koko
74a7cd22b0 Fix patrol delete condition handling 2026-05-01 22:36:37 +08:00
super
aece8123c3 完善多店铺开启多窗口、外观进度条等 2026-05-01 21:09:54 +08:00
super
e1cca219b8 完善多店铺开启多窗口、外观进度条等 2026-05-01 21:09:54 +08:00
super
70658041a5 更新处理这个外观部分 2026-05-01 00:09:14 +08:00
super
4419c5bacd 更新处理这个外观部分 2026-05-01 00:09:14 +08:00
铭坤
668226a99d modified: amazon/__pycache__/price_match.cpython-39.pyc
modified:   amazon/price_match.py
	modified:   assets/appearance-patent.js
	modified:   assets/delete-brand.js
	modified:   assets/patrol-delete.js
	modified:   assets/price-track.js
	modified:   assets/product-risk.js
	modified:   assets/query-asin.js
	modified:   assets/shop-match.js
	modified:   new_web_source/delete-brand.html
2026-04-30 21:57:37 +08:00
铭坤
4eec0dd5a4 modified: amazon/__pycache__/price_match.cpython-39.pyc
modified:   amazon/price_match.py
	modified:   assets/appearance-patent.js
	modified:   assets/delete-brand.js
	modified:   assets/patrol-delete.js
	modified:   assets/price-track.js
	modified:   assets/product-risk.js
	modified:   assets/query-asin.js
	modified:   assets/shop-match.js
	modified:   new_web_source/delete-brand.html
2026-04-30 21:57:37 +08:00
koko
b315d11c28 Merge origin master into local master 2026-04-30 21:46:00 +08:00
koko
8e60f50616 Merge origin master into local master 2026-04-30 21:46:00 +08:00
koko
72faf0433a Harden patrol delete logging 2026-04-30 21:43:15 +08:00
koko
f85e3be40e Harden patrol delete logging 2026-04-30 21:43:15 +08:00
koko
3616d64735 Merge patrol delete changes into master 2026-04-30 21:40:35 +08:00
koko
7f9116f12a Merge patrol delete changes into master 2026-04-30 21:40:35 +08:00
super
01bae23df5 修复商品风险多店铺问题 2026-04-30 21:31:11 +08:00
super
11f0d2c745 修复商品风险多店铺问题 2026-04-30 21:31:11 +08:00
super
b073bbe97d Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-30 11:38:13 +08:00
super
955a6439a0 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-30 11:38:13 +08:00
super
c6ecfb5b77 优化后台业务 2026-04-30 11:36:58 +08:00
super
172917ac41 优化后台业务 2026-04-30 11:36:58 +08:00
铭坤
ad2bfcd2f5 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin
app/amazon/__pycache__/approve.cpython-39.pyc
	app/amazon/__pycache__/asin_status.cpython-39.pyc
	app/amazon/__pycache__/del_brand.cpython-39.pyc
	app/amazon/__pycache__/match_action.cpython-39.pyc
	app/amazon/__pycache__/price_match.cpython-39.pyc
2026-04-30 11:00:02 +08:00
铭坤
a5f6898d06 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin
app/amazon/__pycache__/approve.cpython-39.pyc
	app/amazon/__pycache__/asin_status.cpython-39.pyc
	app/amazon/__pycache__/del_brand.cpython-39.pyc
	app/amazon/__pycache__/match_action.cpython-39.pyc
	app/amazon/__pycache__/price_match.cpython-39.pyc
2026-04-30 11:00:02 +08:00
铭坤
83bd5839d0 modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/asin_status.cpython-39.pyc
	modified:   amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	modified:   amazon/del_brand.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	modified:   main.py
2026-04-30 10:52:52 +08:00
铭坤
83bc90ecf2 modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/asin_status.cpython-39.pyc
	modified:   amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	modified:   amazon/del_brand.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	modified:   main.py
2026-04-30 10:52:52 +08:00
koko
6ea3a960e7 Remove ts script 2026-04-28 22:41:56 +08:00
koko
cbfc29e953 Remove ts script 2026-04-28 22:41:56 +08:00
koko
fd2013b448 chore: update env 2026-04-28 22:40:39 +08:00
koko
b66465aa6a chore: update env 2026-04-28 22:40:39 +08:00
koko
a684fde472 Remove README from master 2026-04-28 22:39:38 +08:00
koko
fbd1a4dbea Remove README from master 2026-04-28 22:39:38 +08:00
koko
824fa7b76c Remove tracked .rtk config 2026-04-28 22:38:17 +08:00
koko
be8a8492a9 Remove tracked .rtk config 2026-04-28 22:38:17 +08:00
koko
b429721070 Stop tracking remaining Python bytecode on master
Master still tracked a small set of generated __pycache__ artifacts after merging dev/koko. The ignore rules already match dev/koko, so this commit only removes the remaining bytecode files from the Git index while leaving local files on disk.

Constraint: dev/koko .gitignore already contains recursive __pycache__ ignore coverage

Rejected: Delete local cache directories | repository tracking cleanup only requires --cached removal

Confidence: high

Scope-risk: narrow

Tested: git ls-files shows no remaining paths containing __pycache__/

Not-tested: Application runtime; generated bytecode tracking only
2026-04-28 22:33:27 +08:00
koko
73d1dff21d Merge dev/koko into master preserving master conflict behavior
Conflict resolution kept master-side changes for non-patrol-delete files as requested. No app/amazon/patrol_delete.py conflict was present. Existing dev/koko assets were retained so /assets resources required by new_web_source pages resolve in the Flask app.

Constraint: User requested authentication-requiring git operations be reported as commands only

Rejected: Rework non-conflicting dev/koko changes during merge | scope was conflict resolution, not feature review

Confidence: high

Scope-risk: moderate

Tested: uv run python -c Flask test client returned 200 for /new_web_source/patrol-delete.html, /assets/patrol-delete.js, /assets/pywebview-C66x_2Dh.js

Tested: uv run python -m py_compile amazon\\patrol_delete.py amazon\\detail_spider.py amazon\\price_match.py main.py

Tested: uv run --group dev pytest tests\\test_patrol_delete.py tests\\test_amazon_base.py

Not-tested: authenticated Git push/pull; Java backend build
2026-04-28 22:31:38 +08:00
koko
d7d1021ec3 fix: brand html 2026-04-28 22:14:41 +08:00
koko
735234f7bc fix: update frontend 2026-04-28 21:42:30 +08:00
铭坤
e09ce6d13b modified: amazon/__pycache__/detail_spider.cpython-39.pyc
modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/detail_spider.py
	modified:   amazon/price_match.py
	modified:   blueprints/__pycache__/admin.cpython-39.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	modified:   blueprints/__pycache__/main.cpython-39.pyc
	modified:   brand_spider/__pycache__/main.cpython-39.pyc
	modified:   main.py
	modified:   web_source/brand.html
2026-04-28 21:11:48 +08:00
koko
46e55fa623 Stop tracking generated Python bytecode
Generated __pycache__ files were already ignored in some paths but remained tracked in the repository, which caused merge noise and binary conflicts. This removes the tracked bytecode from the index and adds an explicit recursive ignore rule so future Python runs do not reintroduce them.

Constraint: Existing repository history already tracked pycache files across app, backend, and source_code

Rejected: Delete local pycache directories from disk | only repository tracking needed to be removed

Confidence: high

Scope-risk: narrow

Directive: Do not force-add __pycache__ or *.pyc files unless there is a documented runtime packaging reason

Tested: git ls-files shows no remaining paths containing __pycache__/

Not-tested: Application runtime; change only affects tracked generated artifacts and ignore rules
2026-04-28 20:57:53 +08:00
koko
a92d1af4f2 Merge master into dev/koko after patrol delete fixes
The merge keeps dev/koko patrol-delete asset fallback behavior while accepting the latest master feature work. Conflicts were limited to ignore rules, local environment/config artifacts, tracked pycache binaries, and the Flask main blueprint.

Constraint: master and dev/koko both edited app/blueprints/main.py around brand and asset serving

Rejected: Drop dev/koko asset fallback logic | patrol delete and rebuilt Vite assets still need hashed asset resolution across app/new_web_source locations

Confidence: medium

Scope-risk: broad

Directive: app/.env remains a tracked local-config file in this repository; do not print or normalize secrets during conflict handling

Tested: uv run python -m py_compile blueprints\\main.py amazon\\base.py amazon\\main.py

Tested: uv run --group dev pytest tests\\test_amazon_base.py tests\\test_patrol_delete.py

Not-tested: Full backend-java/frontend-vue test suites
2026-04-28 20:55:27 +08:00
koko
1be28f10e9 Stabilize patrol delete automation against delayed Amazon UI
The patrol-delete flow now waits for country and listing-status controls before acting, uses the dedicated PatrolDeleteTask entry point, and includes the browser helper scripts required by the bulk-delete path. Tests cover delayed dropdown readiness, payload rejection/retry behavior, complete-draft normalization, filtered bulk deletion, and asset fallback lookup.

Constraint: Amazon listing UI exposes status controls through dynamic KAT components and delayed option rendering

Rejected: Keep row-by-row delete flow | bulk selection is the current implemented path and is covered by the added helper scripts

Confidence: high

Scope-risk: moderate

Directive: Do not remove the patrol_delete JS helper files without checking app/amazon/patrol_delete.py script loading

Tested: uv run --group dev pytest tests\\test_amazon_base.py tests\\test_patrol_delete.py

Tested: uv run python -m py_compile amazon\\base.py amazon\\main.py blueprints\\main.py

Not-tested: Real Amazon Seller Central browser session
2026-04-28 20:46:50 +08:00
super
9c16c9c589 提交跟新 2026-04-28 17:16:04 +08:00
super
8711b205cf 提交暂存 2026-04-28 16:57:05 +08:00
super
e316adca1f 更新内容 2026-04-28 16:56:53 +08:00
super
0457fbfac0 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-28 15:33:42 +08:00
super
58df800685 合并错误信息 2026-04-28 15:25:34 +08:00
铭坤
7c198f7d6f Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-28 15:23:55 +08:00
铭坤
ec4d1b2567 modified: .env
modified:   amazon/__pycache__/approve.cpython-39.pyc
	modified:   amazon/__pycache__/detail_spider.cpython-39.pyc
	modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/detail_spider.py
	modified:   amazon/price_match.py
	modified:   assets/convert.js
	modified:   assets/dedupe.js
	modified:   assets/delete-brand.js
	modified:   assets/split.js
	modified:   blueprints/__pycache__/admin.cpython-39.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	modified:   blueprints/__pycache__/main.cpython-39.pyc
	modified:   blueprints/main.py
	modified:   brand_spider/__pycache__/main.cpython-39.pyc
	modified:   new_web_source/convert.html
	modified:   new_web_source/dedupe.html
	modified:   new_web_source/delete-brand.html
	modified:   new_web_source/split.html
	modified:   tool/__pycache__/devices.cpython-39.pyc
2026-04-28 15:21:57 +08:00
koko
4ff4d8cfdc Expose patrol delete progress in logs
Patrol delete can spend a long time switching countries, reading listing states, and confirming deletes, so the flow now emits concise progress logs around each high-value step. The logs avoid full DOM dumps outside existing diagnostics and focus on task, shop, country, status, deletion counts, and callback boundaries.

Constraint: Existing patrol delete runs are browser-driven and need observable progress without changing deletion behavior
Rejected: Log every DOM probe | too noisy for routine task monitoring
Confidence: high
Scope-risk: narrow
Tested: uv run --group dev pytest tests\\test_patrol_delete.py
Tested: uv run --group dev ruff check amazon\\patrol_delete.py
Tested: uv run python -m py_compile amazon\\patrol_delete.py
Not-tested: Live Seller Central browser run
2026-04-28 01:23:58 +08:00
super
b96b136fa3 增加rufts 2026-04-27 18:45:33 +08:00
koko
02ddfc7a76 Rename patrol delete tests
Keep the patrol deletion test module aligned with the renamed patrol_delete implementation and update test function names so future searches no longer point at the old product module name.

Constraint: This is a naming-only follow-up to the patrol_delete module rename

Confidence: high

Scope-risk: narrow

Tested: uv run pytest tests/test_patrol_delete.py
2026-04-27 17:59:46 +08:00
koko
8defef904f Align patrol delete module naming
Rename the patrol deletion implementation and browser helper scripts away from the generic product name so the filesystem matches the queued patrol-delete task domain. Update imports, script loading, and tests to use the patrol_delete module path.

Constraint: Existing task class remains ProductTask to avoid broad API churn beyond the requested file and script naming

Rejected: Rename ProductTask class now | would expand the change into a larger API update across tests and task dispatch

Confidence: high

Scope-risk: narrow

Directive: Keep patrol-delete script paths under app/amazon/scripts/patrol_delete when adding new helper scripts

Tested: uv run pytest tests/test_product.py

Not-tested: Remote push before this commit due previous SSH publickey failure
2026-04-27 17:58:40 +08:00
koko
13c3a04a48 Bring Amazon collection tasks into dev/koko
Merge origin/master into dev/koko while preserving the patrol delete task added on dev/koko. The task dispatcher now recognizes both patrol deletion and appearance patent collection, and the master-side SpiderTask implementation is included.

Constraint: origin fetch and push over SSH are blocked in this environment by Gitee publickey authentication

Rejected: Overwrite dev/koko with master | would drop the patrol delete branch work

Confidence: medium

Scope-risk: moderate

Directive: Do not remove either patrol-delete-run or appearance-patent-run without checking queued task producers

Tested: python -m py_compile app/amazon/approve.py app/amazon/asin_status.py app/amazon/detail_spider.py app/amazon/main.py app/amazon/match_action.py app/amazon/price_match.py

Not-tested: Remote push, blocked by SSH publickey authentication
2026-04-27 17:36:30 +08:00
super
60a6494260 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-27 09:20:04 +08:00
super
995e2ee65a 后端架构更新 2026-04-27 09:18:10 +08:00
koko
7f83bceba1 feat: patrol delete 2026-04-27 00:23:44 +08:00
铭坤
1eb622d33f modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/asin_status.cpython-39.pyc
	new file:   amazon/__pycache__/detail_spider.cpython-39.pyc
	modified:   amazon/__pycache__/main.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	new file:   amazon/detail_spider.py
	modified:   amazon/main.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	modified:   assets/convert.js
	modified:   assets/dedupe.js
	modified:   assets/delete-brand.js
	modified:   assets/split.js
	modified:   new_web_source/convert.html
	modified:   new_web_source/dedupe.html
	modified:   new_web_source/delete-brand.html
	modified:   new_web_source/split.html
	modified:   web_source/brand.html
2026-04-27 00:21:37 +08:00
koko
5d8ee8fe79 feat: update amazon driver 2026-04-25 17:59:12 +08:00
koko
6f1b8e46f5 feat: update amazon driver 2026-04-25 17:58:28 +08:00
koko
f9b2d2da25 feat: update driver BaseClass 2026-04-25 17:14:23 +08:00
koko
26a920a1cc Merge branch 'master' into dev/koko 2026-04-25 15:47:02 +08:00
铭坤
e695fd5184 modified: amazon/__pycache__/approve.cpython-39.pyc
new file:   amazon/__pycache__/asin_status.cpython-39.pyc
	modified:   amazon/__pycache__/main.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	modified:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/asin_status.py
	modified:   amazon/main.py
	modified:   amazon/match_action.py
	modified:   amazon/price_match.py
	modified:   web_source/brand.html
	modified:   web_source/templates_backup/brand.html
2026-04-24 23:00:59 +08:00
super
0caf62c3d2 后台查询Asin文档更新 2026-04-24 15:14:14 +08:00
super
326f78705d 完成查询asin开发 2026-04-23 20:48:57 +08:00
koko
afa8e44909 Merge branch 'master' into dev/koko 2026-04-23 11:49:50 +00:00
super
72fdb1841c Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-23 15:25:49 +08:00
super
34bc980eea 完成后端架构重构等 2026-04-23 15:25:41 +08:00
koko
2dab555ca5 Merge remote-tracking branch 'origin/master' into dev/koko 2026-04-23 03:16:15 +00:00
铭坤
d537924aa9 modified: app/amazon/__pycache__/approve.cpython-39.pyc
modified:   app/amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   app/amazon/__pycache__/match_action.cpython-39.pyc
	modified:   app/amazon/__pycache__/price_match.cpython-39.pyc
	modified:   app/amazon/approve.py
	modified:   app/amazon/del_brand.py
	modified:   app/amazon/match_action.py
	modified:   app/amazon/price_match.py
	modified:   app/assets/convert.js
	modified:   app/assets/dedupe.js
	modified:   app/assets/delete-brand.js
	modified:   app/assets/split.js
	modified:   app/new_web_source/convert.html
	modified:   app/new_web_source/dedupe.html
	modified:   app/new_web_source/delete-brand.html
	modified:   app/new_web_source/split.html
2026-04-23 10:41:07 +08:00
koko
dc6ecc1edf Clarify the current automation example in onboarding docs
Update the README so new contributors start from the latest patrol-delete automation flow instead of treating delete-brand as the primary example. Keep the older delete-brand path as contrast for the file-driven workflow.

Constraint: Recent repository changes made patrol-delete the newest automation module, so the onboarding example needed to match current development reality
Rejected: Leave delete-brand as the primary example | would mislead readers about the latest automation entry point
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Revisit this section when a newer automation module replaces patrol-delete as the primary example
Tested: README diff review
Not-tested: lint, typecheck, unit/integration tests not run (documentation-only change)
2026-04-22 08:33:02 +00:00
koko
f6e83352d7 Document repo structure and ignore local OMX state
Add a root README that explains the active runtime layers and onboarding path, and keep local OMX session state out of version control so developer tooling does not interfere with pulls.

Constraint: Local OMX state is developer-specific and should not block branch sync or appear in shared history
Rejected: Keep onboarding notes untracked | easy to lose and hard to share with the team
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep the README aligned with the active app/backend-java/frontend-vue execution path; revisit if the runtime architecture changes
Tested: git diff review; git status after staging
Not-tested: lint, typecheck, unit/integration tests not run (docs and ignore rules only)
2026-04-22 07:16:04 +00:00
super
46f46039ea Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-22 01:09:51 +08:00
super
8c4a6cd12a 处理后台跳转错误 2026-04-22 01:09:47 +08:00
super
236b73755c 处理后台管理系统、修复BUG、处理权限 2026-04-22 01:09:28 +08:00
铭坤
b04b05e4da modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/main.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	new file:   amazon/__pycache__/price_match.cpython-39.pyc
	modified:   amazon/__pycache__/tool.cpython-39.pyc
	modified:   amazon/approve.py
	new file:   amazon/asin_status.py
	modified:   amazon/main.py
	modified:   amazon/price_match.py
	new file:   "amazon/price_match_\346\227\247.py"
	modified:   amazon/tool.py
	modified:   assets/convert.js
	modified:   assets/dedupe.js
	modified:   assets/delete-brand.js
	modified:   assets/split.js
	modified:   new_web_source/convert.html
	modified:   new_web_source/dedupe.html
	modified:   new_web_source/delete-brand.html
	modified:   new_web_source/split.html
	deleted:    web_source/admin.html
	deleted:    "web_source/brand - \345\211\257\346\234\254.html"
	deleted:    "web_source/brand-\346\227\247.html"
	deleted:    web_source/brand.html
	deleted:    web_source/home.html
	deleted:    web_source/index.html
	deleted:    web_source/login.html
2026-04-22 00:51:41 +08:00
super
d691e53886 改造后端服务,处理后台管理新增组(部门概念) 2026-04-22 00:32:58 +08:00
super
dabb278170 更新跟价相关内容修改 2026-04-20 00:41:49 +08:00
super
7b12cebb45 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-19 15:47:37 +08:00
super
9d75d673f8 提交改造app权限 2026-04-19 15:47:20 +08:00
铭坤
8d97f5318c modified: ali_oss.py
modified:   config.py
	modified:   main.py
	new file:   winsrc.bat
2026-04-19 15:38:09 +08:00
super
110f407ae2 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-19 15:06:13 +08:00
super
8db9f3a408 改造后台权限和APP权限 2026-04-19 15:06:09 +08:00
supernijia
2ae720ae09 删除文件 desktop 2026-04-18 16:23:13 +00:00
铭坤
f87c37528b modified: amazon/match_action.py
modified:   amazon/price_match.py
	modified:   config.py
2026-04-19 00:18:28 +08:00
铭坤
71b09a89de Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-19 00:18:02 +08:00
super
d5764f3b50 完成匹配、跟价、权限部分 2026-04-17 12:52:16 +08:00
铭坤
4e5ed76cb6 modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   amazon/__pycache__/match_action.cpython-39.pyc
	modified:   amazon/approve.py
	modified:   amazon/del_brand.py
	modified:   amazon/match_action.py
	new file:   amazon/price_match.py
	modified:   main.py
2026-04-17 10:04:24 +08:00
super
ecc8b6ce6c 后台管理跳过asin、后台相关数据做数据隔离 2026-04-14 10:04:50 +08:00
9ad6e037dc 提交任务更新 2026-04-13 20:07:00 +08:00
super
4a58b366b6 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-13 13:02:45 +08:00
super
bbc833f2c9 提交工作更新 2026-04-13 13:02:41 +08:00
铭坤
325c2f7c82 modified: app/amazon/__pycache__/del_brand.cpython-39.pyc
modified:   app/amazon/__pycache__/match_action.cpython-39.pyc
	modified:   app/amazon/del_brand.py
	modified:   app/amazon/match_action.py
	modified:   app/assets/delete-brand.js
	modified:   app/new_web_source/delete-brand.html
	new file:   "app/web_source/brand - \345\211\257\346\234\254.html"
2026-04-12 20:52:26 +08:00
super
728eea4b83 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-12 17:15:54 +08:00
super
e53eaf738e 完成菜单权限添加和软件菜单改造 2026-04-12 17:15:15 +08:00
铭坤
ad2bb0a6ea modified: app/amazon/__pycache__/approve.cpython-39.pyc
modified:   app/amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   app/amazon/__pycache__/main.cpython-39.pyc
	modified:   app/amazon/__pycache__/match_action.cpython-39.pyc
	modified:   app/amazon/__pycache__/tool.cpython-39.pyc
	modified:   app/amazon/approve.py
	modified:   app/amazon/del_brand.py
	modified:   app/amazon/main.py
	modified:   app/amazon/match_action.py
	modified:   app/amazon/tool.py
	modified:   app/assets/delete-brand.js
	modified:   app/new_web_source/delete-brand.html
2026-04-11 23:00:55 +08:00
super
7589b4e418 提交bug修复 后台权限管理 2026-04-11 22:25:14 +08:00
super
b400b494ad 修复BUG,完善匹配店铺 2026-04-11 00:57:34 +08:00
super
9a59dac6d0 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-10 19:35:08 +08:00
super
03a85d5523 匹配回收,bug修复 2026-04-10 19:33:58 +08:00
铭坤
33186911f3 modified: app/amazon/__pycache__/approve.cpython-39.pyc
modified:   app/amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   app/amazon/__pycache__/main.cpython-39.pyc
	new file:   app/amazon/__pycache__/match_action.cpython-39.pyc
	new file:   app/amazon/__pycache__/tool.cpython-39.pyc
	new file:   "app/amazon/approve - \345\211\257\346\234\254.py"
	modified:   app/amazon/approve.py
	modified:   app/amazon/del_brand.py
	modified:   app/amazon/main.py
	new file:   app/amazon/match_action.py
	new file:   app/amazon/tool.py
	modified:   app/main.py
	modified:   app/web_source/brand.html
2026-04-10 16:51:52 +08:00
super
144d03965e 完善后台管理店铺搜索 2026-04-08 21:08:52 +08:00
super
e1cd176ab2 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-08 16:14:30 +08:00
super
2e30821e0f 修复接口内容缺失 2026-04-08 16:14:26 +08:00
铭坤
7151f6b5e0 modified: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   amazon/__pycache__/main.cpython-39.pyc
	modified:   amazon/approve.py
	new file:   amazon/backend_api.py
	modified:   amazon/del_brand.py
	modified:   amazon/main.py
	modified:   blueprints/main.py
	modified:   main.py
2026-04-08 16:03:48 +08:00
super
3149d80456 完善后台接口字段 2026-04-07 17:57:03 +08:00
super
62315cee6c Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-07 14:50:09 +08:00
super
2f67b376ee 完成店铺增删改查 2026-04-07 14:50:06 +08:00
铭坤
489e7e8269 new file: amazon/__pycache__/approve.cpython-39.pyc
modified:   amazon/__pycache__/del_brand.cpython-39.pyc
	modified:   amazon/__pycache__/main.cpython-39.pyc
	new file:   amazon/approve.py
	modified:   amazon/del_brand.py
	modified:   amazon/main.py
	modified:   main.py
2026-04-07 14:09:42 +08:00
super
1b02160b20 更改下载方法名 2026-04-06 22:26:44 +08:00
super
d8d5b56c13 更改下载类型处理 2026-04-06 16:58:58 +08:00
super
486e690204 更新商品风险处理 2026-04-05 22:04:21 +08:00
super
380dd9f206 新增删除增加公司名 2026-04-04 00:10:08 +08:00
super
fba85ca365 提交接口进度完善 2026-04-03 00:53:52 +08:00
super
99236c9feb Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-02 23:29:19 +08:00
super
0cad577d7c 测试多任务暂存 2026-04-02 23:29:15 +08:00
铭坤
274d294b2e modified: app/amazon/__pycache__/del_brand.cpython-39.pyc
modified:   app/amazon/__pycache__/main.cpython-39.pyc
	modified:   app/amazon/main.py
	modified:   app/main.py
2026-04-02 23:03:13 +08:00
super
f63ae84853 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-02 20:37:38 +08:00
super
4b50542779 修改多文件 2026-04-02 20:37:35 +08:00
铭坤
20bd41008c modified: app/amazon/del_brand.py
backend/blueprints/get_resource.py
2026-04-02 20:35:50 +08:00
铭坤
8b490d3dd4 modified: backend/app.py
backend/blueprints/get_resource.py
2026-04-02 20:35:12 +08:00
super
99e94a5e57 完善后端轮询记忆紫鸟店铺逻辑 2026-04-02 12:26:48 +08:00
super
949a5b42ce Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-02 01:15:54 +08:00
super
1fdef3c67f 紫鸟查询店铺更新 2026-04-02 01:15:50 +08:00
铭坤
a0624221de new file: app/amazon/__pycache__/del_brand.cpython-39.pyc
new file:   app/amazon/__pycache__/main.cpython-39.pyc
	modified:   app/amazon/del_brand.py
	modified:   app/amazon/main.py
	modified:   app/blueprints/__pycache__/main.cpython-39.pyc
	modified:   app/blueprints/main.py
	modified:   app/config.py
	modified:   app/main.py
	new file:   "app/web_source/brand-\346\227\247.html"
	modified:   app/web_source/brand.html
2026-04-02 00:03:16 +08:00
super
63043c56d4 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-04-01 21:20:04 +08:00
super
6ed033fff2 更新部分内容 2026-04-01 21:20:01 +08:00
铭坤
785d7563f9 modified: app/ali_oss.py
new file:   app/amazon/del_brand.py
	new file:   app/amazon/main.py
	modified:   app/blueprints/__pycache__/brand.cpython-39.pyc
	modified:   app/blueprints/__pycache__/communication.cpython-39.pyc
	modified:   app/blueprints/__pycache__/main.cpython-39.pyc
	modified:   app/blueprints/brand.py
	new file:   "app/blueprints/brand_\345\244\207\344\273\275.py"
	modified:   app/blueprints/communication.py
	modified:   app/blueprints/main.py
	modified:   app/config.py
	modified:   app/main.py
2026-04-01 11:53:12 +08:00
super
b6dd1d7931 删除模块 取消打开紫鸟 2026-03-31 23:12:46 +08:00
super
bf802c1019 处理这个python打开紫鸟 2026-03-30 23:15:08 +08:00
super
d3dd4898a4 处理掉一些BUG 2026-03-30 22:47:19 +08:00
super
1c1acbbe7d 品牌多文件chunk修复 2026-03-30 21:06:10 +08:00
super
eeeed6d7b1 暂存 2026-03-30 20:54:05 +08:00
super
9cbede6e09 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-30 20:48:02 +08:00
super
b8f88ca06e 暂存 2026-03-30 20:39:13 +08:00
铭坤
23e0705a99 modified: app/.env
modified:   app/ali_oss.py
	modified:   app/app.py
	new file:   app/blueprints/__pycache__/communication.cpython-39.pyc
	modified:   app/blueprints/communication.py
	modified:   app/brand_spider/__pycache__/main.cpython-39.pyc
	modified:   app/config.py
	modified:   app/main.py
	modified:   app/static/bg.jpg
	modified:   app/tool/__pycache__/devices.cpython-39.pyc
	modified:   app/web_source/brand.html
	modified:   app/web_source/templates_backup/brand.html
2026-03-30 19:44:04 +08:00
super
a07b9d7fb2 完善删除品牌asin 2026-03-29 19:03:24 +08:00
super
efbff69a91 完成删除品牌 2026-03-29 13:41:03 +08:00
super
e962990ca0 完成品牌删除模块 2026-03-29 00:22:45 +08:00
super
e4a5f5acc0 完成前后端紫鸟部分开发 2026-03-28 15:42:18 +08:00
super
5c7f1187c0 提交暂存 2026-03-27 22:00:01 +08:00
super
0aad7b3faf 提交过滤 2026-03-27 21:58:43 +08:00
铭坤
28d410f38f Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-27 21:56:53 +08:00
铭坤
fe8c6b94ae modified: assets/convert.js
modified:   assets/dedupe.js
	modified:   assets/split.js
	modified:   blueprints/__pycache__/brand.cpython-311.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	new file:   blueprints/communication.py
	modified:   config.py
	modified:   main.py
	modified:   new_web_source/convert.html
	modified:   new_web_source/dedupe.html
	modified:   new_web_source/split.html
2026-03-27 21:56:19 +08:00
super
e09f6d4a80 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-27 21:51:34 +08:00
super
085e8f2f84 品牌 2026-03-27 21:48:13 +08:00
铭坤
2952f3bc95 new file: assets/_plugin-vue_export-helper-Dh4yMZWw.js
new file:   assets/_plugin-vue_export-helper-xvHHTGU_.css
	new file:   assets/convert-1dkFPbKa.css
	new file:   assets/convert-6b0yq-M0.css
	new file:   assets/convert-BxkpMHO9.css
	new file:   assets/convert-D4Yv2oRy.css
	new file:   assets/convert-DwbQug2y.css
	modified:   assets/convert.js
	new file:   assets/dedupe-B0eqpVpj.css
	new file:   assets/dedupe-BHE_BCAJ.css
	new file:   assets/dedupe-Day_nGxq.css
	new file:   assets/dedupe-DzFvTfoj.css
	new file:   assets/dedupe-rhGu1E8E.css
	modified:   assets/dedupe.js
	new file:   assets/el-alert-Ct0RpqUD.css
	new file:   assets/pywebview-B6ZNXusn.css
	new file:   assets/pywebview-BRrok0pS.js
	new file:   assets/pywebview-BnpJyyGZ.js
	new file:   assets/pywebview-CGwF8TuM.js
	new file:   assets/pywebview-Chz98E9c.css
	new file:   assets/pywebview-D-PKWjUg.js
	new file:   assets/pywebview-DVgP2-kW.js
	new file:   assets/pywebview-IqgMfeBe.css
	new file:   assets/pywebview-Rm5Hhqpf.js
	new file:   assets/pywebview-leZCedEt.js
	new file:   assets/split-BsJfbBqn.css
	new file:   assets/split-DAa_Usoh.css
	new file:   assets/split-DidKJ84l.css
	new file:   assets/split-R_8V5xgh.css
	new file:   assets/split-_O6NHqDj.css
	modified:   assets/split.js
	modified:   blueprints/__pycache__/brand.cpython-311.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	modified:   blueprints/brand.py
	modified:   config.py
	modified:   main.py
	modified:   new_web_source/convert.html
	modified:   new_web_source/dedupe.html
	modified:   new_web_source/split.html
2026-03-27 21:35:28 +08:00
super
5dd7a92cf5 删除品牌 2026-03-27 21:32:00 +08:00
super
ea212c8931 提交品牌删除更新 2026-03-26 17:39:20 +08:00
super
14269d9513 提交品牌删除更新 2026-03-26 17:39:01 +08:00
super
8106a8ab7e 更新后端新增内容 2026-03-26 16:42:25 +08:00
铭坤
bbca159684 modified: blueprints/__pycache__/__init__.cpython-39.pyc
modified:   blueprints/__pycache__/admin.cpython-39.pyc
	modified:   blueprints/__pycache__/auth.cpython-39.pyc
	modified:   blueprints/__pycache__/brand.cpython-311.pyc
	modified:   blueprints/__pycache__/brand.cpython-39.pyc
	modified:   blueprints/brand.py
	new file:   brand_spider/__pycache__/main.cpython-311.pyc
	modified:   brand_spider/__pycache__/main.cpython-39.pyc
	new file:   brand_spider/__pycache__/web_dec.cpython-311.pyc
	modified:   brand_spider/main.py
	modified:   config.py
	modified:   web_source/admin.html
	modified:   web_source/brand.html
	modified:   web_source/home.html
	modified:   web_source/index.html
	modified:   web_source/login.html
	new file:   web_source/templates_backup/admin.html
	new file:   web_source/templates_backup/brand.html
	new file:   web_source/templates_backup/home.html
	new file:   web_source/templates_backup/index.html
	new file:   web_source/templates_backup/login.html
2026-03-26 16:39:39 +08:00
super
6434ec3e00 增加紫鸟相关接口 2026-03-25 21:33:49 +08:00
super
8cfaab49fb 完善打包部分的压缩包 2026-03-25 09:52:04 +08:00
super
2cd0fcff93 完成品牌部分改造 2026-03-24 22:22:37 +08:00
super
7ffe5279a3 增加用户访问数据总表 2026-03-22 21:27:41 +08:00
super
12318f8770 数据去重完善 2026-03-22 20:58:36 +08:00
super
e8bec339a7 总表更新 2026-03-22 17:51:34 +08:00
super
e9684a81aa 修复tab切换问题 2026-03-22 13:39:32 +08:00
super
903c591181 完成后台管理开发 2026-03-22 13:33:37 +08:00
super
923dfd2495 提交所有内容 2026-03-22 12:45:58 +08:00
super
0d83d56a05 暂替 2026-03-22 11:37:23 +08:00
super
ef16abfbb9 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-22 11:37:03 +08:00
super
cfbd5d6e84 暂提更新 2026-03-22 11:35:45 +08:00
super
497d65d41d 更新新增三个模块 2026-03-22 11:20:09 +08:00
铭坤
c2e45128ee new file: app/assets/_plugin-vue_export-helper-Dh4yMZWw.js
new file:   app/assets/_plugin-vue_export-helper-xvHHTGU_.css
	new file:   app/assets/convert-1dkFPbKa.css
	new file:   app/assets/convert-BxkpMHO9.css
	new file:   app/assets/convert-DwbQug2y.css
	modified:   app/assets/convert.js
	new file:   app/assets/dedupe-B0eqpVpj.css
	new file:   app/assets/dedupe-DzFvTfoj.css
	new file:   app/assets/dedupe-rhGu1E8E.css
	modified:   app/assets/dedupe.js
	new file:   app/assets/el-alert-Ct0RpqUD.css
	new file:   app/assets/pywebview-B6ZNXusn.css
	new file:   app/assets/pywebview-BnpJyyGZ.js
	new file:   app/assets/pywebview-CGwF8TuM.js
	new file:   app/assets/pywebview-D-PKWjUg.js
	new file:   app/assets/pywebview-IqgMfeBe.css
	new file:   app/assets/split-DAa_Usoh.css
	new file:   app/assets/split-DidKJ84l.css
	new file:   app/assets/split-R_8V5xgh.css
	modified:   app/assets/split.js
	modified:   app/blueprints/__pycache__/main.cpython-39.pyc
	modified:   app/blueprints/main.py
	modified:   app/main.py
	modified:   app/new_web_source/convert.html
	modified:   app/new_web_source/dedupe.html
	modified:   app/new_web_source/split.html
2026-03-22 10:29:12 +08:00
super
c80e1889ef 增加log输出、跳转tab顺序 2026-03-21 20:34:49 +08:00
super
7f9a6bd9cb 环境搭建配置 2026-03-21 10:00:16 +08:00
铭坤
32f4705491 Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-20 23:16:13 +08:00
铭坤
bb06bc8f5a new file: app/blueprints/__init__.py
new file:   app/blueprints/__pycache__/__init__.cpython-311.pyc
	new file:   app/blueprints/__pycache__/__init__.cpython-39.pyc
	new file:   app/blueprints/__pycache__/admin.cpython-311.pyc
	new file:   app/blueprints/__pycache__/admin.cpython-39.pyc
	new file:   app/blueprints/__pycache__/auth.cpython-311.pyc
	new file:   app/blueprints/__pycache__/auth.cpython-39.pyc
	new file:   app/blueprints/__pycache__/brand.cpython-311.pyc
	new file:   app/blueprints/__pycache__/brand.cpython-39.pyc
	new file:   app/blueprints/__pycache__/image.cpython-311.pyc
	new file:   app/blueprints/__pycache__/image.cpython-39.pyc
	new file:   app/blueprints/__pycache__/main.cpython-311.pyc
	new file:   app/blueprints/__pycache__/main.cpython-39.pyc
	new file:   app/blueprints/admin.py
	new file:   app/blueprints/auth.py
	new file:   app/blueprints/brand.py
	new file:   app/blueprints/image.py
	new file:   app/blueprints/main.py
	new file:   app/brand_spider/__pycache__/main.cpython-39.pyc
	new file:   app/brand_spider/__pycache__/web_dec.cpython-39.pyc
	new file:   app/brand_spider/main.py
	new file:   app/brand_spider/web_dec.py
	new file:   app/static/bg.jpg
	new file:   "app/static/\345\223\201\347\211\214\346\226\207\346\241\243\346\240\274\345\274\217_\346\250\241\346\235\277.xlsx"
	new file:   "app/static/\346\250\241\346\235\2772-\344\273\245\346\226\207\344\273\266\345\244\271\346\226\271\345\274\217\344\270\212\344\274\240.zip"
	new file:   app/tool/.device_id
	new file:   app/tool/__pycache__/devices.cpython-311.pyc
	new file:   app/tool/__pycache__/devices.cpython-39.pyc
	new file:   app/tool/devices.py
2026-03-20 23:04:19 +08:00
super
d39d310544 新增接口文档 2026-03-20 19:31:02 +08:00
super
7efb86ed3a 打包项目 2026-03-20 18:13:47 +08:00
super
6791869e0c Merge branch 'master' of https://gitee.com/TeaCodeNice/crawler-plugin 2026-03-20 14:26:41 +08:00
铭坤
8a64a6cd9b new file: ali_oss.py
new file:   app.py
	new file:   app/ali_oss.py
	new file:   app/app.py
	new file:   app/app_common.py
	new file:   app/config.py
	new file:   app/coze.py
	new file:   app/generate_api.py
	new file:   app/html_crypto.py
	new file:   app/main.py
	new file:   app/web_source/admin.html
	new file:   app/web_source/brand.html
	new file:   app/web_source/home.html
	new file:   app/web_source/index.html
	new file:   app/web_source/login.html
	new file:   app_common.py
	new file:   blueprints/__init__.py
	new file:   blueprints/__pycache__/__init__.cpython-311.pyc
	new file:   blueprints/__pycache__/__init__.cpython-39.pyc
	new file:   blueprints/__pycache__/admin.cpython-311.pyc
	new file:   blueprints/__pycache__/admin.cpython-39.pyc
	new file:   blueprints/__pycache__/auth.cpython-311.pyc
	new file:   blueprints/__pycache__/auth.cpython-39.pyc
	new file:   blueprints/__pycache__/brand.cpython-311.pyc
	new file:   blueprints/__pycache__/brand.cpython-39.pyc
	new file:   blueprints/__pycache__/image.cpython-311.pyc
	new file:   blueprints/__pycache__/image.cpython-39.pyc
	new file:   blueprints/__pycache__/main.cpython-311.pyc
	new file:   blueprints/__pycache__/main.cpython-39.pyc
	new file:   blueprints/admin.py
	new file:   blueprints/auth.py
	new file:   blueprints/brand.py
	new file:   blueprints/image.py
	new file:   blueprints/main.py
	new file:   brand_spider/__pycache__/main.cpython-39.pyc
	new file:   brand_spider/__pycache__/web_dec.cpython-39.pyc
	new file:   brand_spider/main.py
	new file:   brand_spider/web_dec.py
	new file:   config.py
	new file:   coze.py
	new file:   generate_api.py
	new file:   html_crypto.py
	new file:   main.py
	new file:   static/bg.jpg
	new file:   "static/\345\223\201\347\211\214\346\226\207\346\241\243\346\240\274\345\274\217_\346\250\241\346\235\277.xlsx"
	new file:   "static/\346\250\241\346\235\2772-\344\273\245\346\226\207\344\273\266\345\244\271\346\226\271\345\274\217\344\270\212\344\274\240.zip"
	new file:   tool/.device_id
	new file:   tool/__pycache__/devices.cpython-311.pyc
	new file:   tool/__pycache__/devices.cpython-39.pyc
	new file:   tool/devices.py
2026-03-20 11:23:57 +08:00
841 changed files with 58821 additions and 69344 deletions

4
.gitignore vendored
View File

@@ -19,10 +19,10 @@ backend-java/.idea/
backend-java/*.iml
# ===== backend / app =====
__pycache__/
backend/tmp/
backend/__pycache__
app/__pycache__
app/assets/
app/new_web_source
app/user_data/
@@ -43,6 +43,7 @@ MANIFEST
.installed.cfg
# Build / packaging
app.zip
build/
dist/
develop-eggs/
@@ -108,3 +109,4 @@ xlsx/
OPS_REDIS_MYSQL_OPTIMIZATION_NOTES.md
架构.md
*ts.%
.omc

409
README.md
View File

@@ -1,409 +0,0 @@
# 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`,则更适合当作你理解新增自动化功能的第一条例子。

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,15 +0,0 @@
base_url=http://8.136.19.173:15124
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
client_name=ShuFuAI
java_api_base=http://127.0.0.1:18080
# java_api_base=http://8.136.19.173:18080

View File

@@ -1,18 +0,0 @@
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

View File

@@ -1,13 +0,0 @@
# 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"

View File

@@ -1,72 +0,0 @@
import argparse
import base64
import re
import time
import alibabacloud_oss_v2 as oss
import requests
from config import region, endpoint, bucket, file_url_pre, bucket_path
def upload_file(file_content: bytes, key: str):
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
cfg = oss.config.load_default()
cfg.credentials_provider = credentials_provider
cfg.region = region
cfg.endpoint = endpoint
client = oss.Client(cfg)
result = client.put_object(
oss.PutObjectRequest(
bucket=bucket, # 存储空间名称
key=key, # 对象名称
body=file_content # 读取文件内容
)
)
# print(result)
return file_url_pre + key
def upload_data_url(data_url: str, prefix: str = "history", key_hint: str = "") -> str:
"""
将 base64 data URL 上传到 OSS返回图片链接
data_url: data:image/png;base64,xxxx 或 data:image/jpeg;base64,xxxx
prefix: OSS key 前缀
key_hint: 可选后缀避免重名,如 "_0", "_1"
"""
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
if not match:
raise ValueError('无效的 data URL 格式')
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
file_content = base64.b64decode(match.group(2))
ts = int(time.time() * 1000)
key = f"{bucket_path}{prefix}/{ts}{key_hint}.{ext}"
return upload_file(file_content, key)
def upload_data_urls(data_urls: list, prefix: str = "history") -> list:
"""批量上传 base64 图片到 OSS返回图片链接列表"""
urls = []
ts = int(time.time() * 1000)
for i, data_url in enumerate(data_urls or []):
if not data_url or not isinstance(data_url, str):
continue
if not data_url.startswith("http"):
match = re.match(r'data:image/(\w+);base64,(.+)', data_url)
if not match:
continue
ext = 'png' if match.group(1).lower() in ('png', 'webp') else 'jpg'
file_content = base64.b64decode(match.group(2))
else:
file_content = requests.get(data_url).content
ext = "png"
key = f"{bucket_path}{prefix}/{ts}_{i}.{ext}"
urls.append(upload_file(file_content, key))
return urls
# 脚本入口当文件被直接运行时调用main函数
if __name__ == "__main__":
with open("测试图片数据/IMG_2685.JPG", "rb") as f:
file_content = f.read()
upload_file(file_content,key=bucket_path+"test.png")

View File

@@ -1,999 +0,0 @@
import time
import re
import traceback
from DrissionPage._pages.chromium_tab import ChromiumTab
from config import runing_task, runing_shop
from datetime import datetime
from amazon.del_brand import AmamzonBase, kill_process
# 导入 webview 用于前端通知
try:
import webview
except ImportError:
webview = None
class AmzoneApprove(AmamzonBase):
def SwitchPage(self):
"""
切换至 管理所有库存页面
1、等待 //navigation-favorites-bar[@class="hydrated"] 出现
"""
navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]')
navigation.wait.displayed(raise_err=False)
page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr(
'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]')
page_btn.wait.displayed(raise_err=False)
page_btn.click(timeout=5)
self.tab.wait.doc_loaded()
# 等待搜索框出现
search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group')
search_region.wait.displayed(raise_err=False)
def search(self,filter_type="ApprovalRequired"):
sku_ls = []
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
if len(load_ele) > 0:
load_ele[0].wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown')
drop_down.wait.displayed(raise_err=False)
drop_down.wait.enabled(raise_err=False)
time.sleep(0.6)
drop_down.click()
# //kat-option[@value="SearchSuppressed"]
xp = f'xpath://kat-option[@value="{filter_type}"]'
print(f"正在寻找筛选条件 {filter_type}xpath: {xp}")
approval_required = self.tab.eles(xp,timeout=5)
if len(approval_required) == 0:
print(f"【没有需要{filter_type}选项】没有需要{filter_type}的商品了")
return sku_ls # "没有需要审批的商品了"
else:
approval_required = approval_required[0]
approval_required.wait.displayed(raise_err=False)
approval_required.click()
approval_required_text = approval_required.text
print(f"已选择筛选条件: {approval_required_text}")
count = re.findall(r'\d+', approval_required_text)
if count:
count = int(count[0])
print(f"待审批的商品数量: {count}")
if count <= 0:
print(f"没有需要{filter_type}的商品了")
return sku_ls #"没有需要审批的商品了"
asin = "B0C7KT6ZZN"
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
'xpath://span[@class="container"]//input[@part="input"]')
search_input.input(asin, clear=True)
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
search_btn.click()
for _ in range(3):
# 等待加载完成
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
load_ele.wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
search_input = self.tab.ele("xpath://kat-input[contains(@class,'SearchBox-module__searchInput')]").sr(
'xpath://span[@class="container"]//input[@part="input"]')
search_input.input(asin, clear=True)
search_btn = self.tab.ele("xpath://kat-icon[@name='search']")
search_btn.click()
load_ele = self.tab.ele("xpath://div[contains(@class,'Loader-module__loader')]")
load_ele.wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3)
if len(sku_ls) > 0:
break
approval_required.click()
return sku_ls
def wait_loaded(self):
# 等待加载完成
try:
load_ele = self.tab.ele(
'xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[contains(@class,"Loader-module__loader")]|//kat-panel[@data-testid="kat-panel-ActionPanelContent"]/div[@data-f1-component]//div[contains(@class,"==")]/div[contains(@class,"==")]/span',
timeout=3)
load_ele.wait.deleted(timeout=5, raise_err=False)
except Exception as e:
print("等待加载中消失出错", e)
def clear_tab(self):
all_tab = self.browser.get_tabs(title="亚马逊")
close_tab = []
for tab in all_tab:
tab_id = tab if isinstance(tab,str) else tab.tab_id
if self.tab.tab_id == tab:
continue
close_tab.append(tab)
print("需要关闭的标签页",close_tab)
print("当前操作的tab_id",self.tab.tab_id)
self.browser.close_tabs(close_tab)
def handle_repair_product(self,tab:ChromiumTab):
def wait_loaded():
# 等待加载完成
try:
load_ele = tab.ele(
'xpath://div[@class="contentWrapper"]//div[contains(@class,"==")]/span[not(node())]',
timeout=3)
load_ele.wait.deleted(timeout=5, raise_err=False)
except Exception as e:
print("等待加载中消失出错", e)
tab.wait.doc_loaded(timeout=30)
submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
if len(submit_btn_ls) == 0:
print("未找到提交按钮,可能页面未加载完成或者页面结构发生变化")
return
# todo 遍历
for index in range(len(submit_btn_ls)):
if index > len(submit_btn_ls)-1:
print("提交按钮数量发生变化,停止处理")
break
submit_btn = submit_btn_ls[index]
# 关闭按钮
close_x_btn = tab.eles('xpath://div[@class="flyoutPanelContent"]/span',timeout=5)
if len(close_x_btn) > 0:
close_x_btn[0].wait.displayed(timeout=5, raise_err=False)
close_x_btn[0].click()
submit_btn.wait.enabled(timeout=5, raise_err=False)
submit_btn.click()
time.sleep(0.5)
self.wait_loaded()
wait_loaded()
need_input = tab.eles('xpath://div[@class="contentWrapper"]//kat-input[@data-testid="kat-input-dew:ump_epr_resgitration_number_title"]',timeout=5)
if len(need_input) > 0:
print("需要输入注册号,不符合操作要求,跳过...")
continue
target_title = tab.eles('xpath://div[@class="contentWrapper"]//section//h4[text()="警告和安全信息"]')
if len(target_title) > 0:
not_start = tab.eles('xpath://div[@class="contentWrapper"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]')
if len(not_start) == 0:
print("安全证明-不是未开始状态,不需要操作")
continue
not_start.wait.displayed(timeout=5, raise_err=False)
not_start.click()
wait_loaded()
check = tab.ele('xpath://div[@class="contentWrapper"]//kat-checkbox[@name="value"]')
check.wait.displayed(timeout=5, raise_err=False)
check.click()
save_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]')
save_btn.wait.enabled(timeout=5, raise_err=False)
save_btn.click()
wait_loaded()
close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10)
close_btn.wait.displayed(timeout=5, raise_err=False)
close_btn.click()
save_btn_ls = tab.eles('xpath://div[@class="contentWrapper"]//kat-button[@variant="primary"]',timeout=5)
if len(save_btn_ls) > 0:
option_selection_ls = tab.eles('xpath://div[@class="contentWrapper"]//div[@role="option"]',timeout=5)
for option in option_selection_ls:
option.click()
time.sleep(0.5)
save_btn_ls[0].wait.enabled(timeout=5, raise_err=False)
save_btn_ls[0].click()
wait_loaded()
close_btn = tab.ele('xpath://div[@class="contentWrapper"]//kat-button[@label="关闭"]',timeout=10)
close_btn.wait.displayed(timeout=5, raise_err=False)
close_btn.wait.enabled(timeout=5, raise_err=False)
close_btn.click()
submit_btn_ls = tab.eles('xpath://div[@id="ahd-product-policies-table"]/div//div[@data-testid="nextStepsMetricWrapper"]//kat-button[@label="提交"]',timeout=10)
print("修复商品信息处理完成!")
tab.close()
def run_page_action(self):
print("开始执行")
num = 0
retry_num = 0
already_asin = set()
while retry_num < 3: # 最多重试3次
# if num > 3: #测试
# return
# 等待加载完成
try:
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
if len(load_ele) > 0:
load_ele[0].wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
# 获取当前页码
try:
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
if len(page_pamel) > 0:
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
# 总页数
total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
if len(total_page) > 0:
total_page = total_page[-1].text
else:
total_page = 0
print(f"当前页码: {current_page} / 总页数: {total_page}")
except Exception as e:
print("获取页码失败", e)
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=10)
print(f"获取到 {len(sku_ls)}")
# for sku_ele in sku_ls[0:2]:
for sku_ele in sku_ls:
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')
asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]').text
print(f"ASIN {asin} 存在问题,正在点击解决...")
if asin in already_asin:
print(f"{asin} 已经处理过了,跳过")
continue
solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]|.//kat-link[@label="修复被禁止显示的商品"]|.//kat-link[@label="解决商品移除风险"]')
if len(solve_problem) == 0:
print(f"{asin},没有处理入口,不处理")
yield (asin,"没有处理入口,不处理")
already_asin.add(asin)
continue
solve_problem[0].click()
action_panel = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]')
action_panel.wait.displayed(timeout=5, raise_err=False)
self.wait_loaded()
try:
# 如果存在“请求批准”按钮,则直接返回跳过
request_approval_btn = self.tab.eles('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="section-header" and contains(string(.),"移除")]',timeout=5)
if len(request_approval_btn) > 0:
print(f"ASIN {asin} 存在请求批准按钮,不需要处理,跳过...")
panel_close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
timeout=5).sr('xpath:.//button[@class="close"]')
panel_close_btn.click()
action_panel.wait.deleted(timeout=3, raise_err=False)
yield (asin,"请求批准")
# 无需采取任何操作
not_operate = self.tab.eles('xpath://kat-alert[contains(@description,"如果您之前已提交更改,则这些更改当前正在处理中") and not(@dismissed)]',timeout=3)
if len(not_operate) > 0:
print(f"ASIN {asin} 无需采取任何操作,跳过...")
yield (asin,"无需操作")
already_asin.add(asin)
continue
# 解决商品信息违规问题 按钮
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]',timeout=5)
# if len(kat_box_ls) == 0:
# print(f"ASIN {asin} 的操作面板中未找到选项,跳过...")
# continue
print(f"ASIN {asin}需要处理的有:{len(kat_box_ls)}个问题.")
# 解决商品信息违规问题 按钮,有多少个都要处理
for i in range(len(kat_box_ls)):
print(f"开始处理第{i}个问题")
if len(kat_box_ls) <= i:
print(f"ASIN {asin} 的操作面板中选项数量发生变化,停止处理...")
break
kat_box_ls[i].click()
time.sleep(0.5)
self.wait_loaded()
# TODO 增加 “出现警告和安全信息时候下滑” 的情况
safe_handle_ls = self.tab.eles('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[@aria-label="安全证明"]//kat-label[@text="未开始"]',
timeout=5)
if len(safe_handle_ls) > 0:
print("存在安全证明,正在处理...")
safe_handle_ls[0].click()
time.sleep(0.5)
self.wait_loaded()
check_box = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-checkbox')
check_box.click()
else:
problem_selection_ls = self.tab.eles(
'xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//div[@data-testid="registry-list"]//div[@data-testid="registry"]',
timeout=10)
print("待选择数量",len(problem_selection_ls))
for problem_selection in problem_selection_ls:
problem_selection.click()
print("点击选择完成")
time.sleep(0.5)
# 点击保存
confirm_btn = self.tab.ele('xpath://kat-panel-wrapper[@data-testid="kat-panel-wrapper-ActionPanelContent"]//kat-button[@variant="primary"]')
confirm_btn.wait.displayed(timeout=3, raise_err=False)
confirm_btn.click()
self.wait_loaded()
# 等待关闭按钮出现
try:
close_btn = self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//kat-button[@label="关闭"]',
timeout=10)
close_btn.click()
except Exception as e:
print("点击关闭按钮失败",e)
kat_box_ls = action_panel.eles('xpath:.//kat-box[@variant="white"]', timeout=5)
# 修复商品信息
repair_product = action_panel.eles('xpath:.//kat-button[@class="action-button"]',timeout=5)
if len(repair_product) > 0:
print(f"ASIN {asin} 存在修复商品信息按钮,正在点击...")
self.clear_tab()
repair_product[0].click()
time.sleep(1)
# 获取最新的tab
new_tab = self.browser.latest_tab
if isinstance(new_tab, str):
new_tab = self.browser.get_tab(id_or_num=new_tab)
self.handle_repair_product(new_tab)
except Exception as e:
print("【asin】",asin,"处理失败",traceback.format_exc())
yield (asin,"处理失败")
already_asin.add(asin)
continue
yield (asin,"处理完成")
already_asin.add(asin)
try:
# 全部操作完成,关闭
panel_close_btn= self.tab.ele('xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]',
timeout=5).sr('xpath:.//button[@class="close"]')
panel_close_btn.click()
action_panel.wait.deleted(timeout=3, raise_err=False)
except Exception as e:
print("关闭操作面板失败", e)
# 判断是否存在需要翻页的情况
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
if len(page_pamel) == 0:
break
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
class_str = next_page_btn.attr('class')
if "end" in class_str:
break
next_page_btn.click()
num += 1
print(f"【程序计算】正在翻页,已翻 {num} 页...")
already_asin = set()
except Exception as e:
print("处理审批操作异常", e)
traceback.print_exc()
retry_num += 1
self.tab.refresh()
self.tab.wait.doc_loaded(raise_err=False,timeout=120)
class ApproveTask:
"""审批任务处理类:负责处理产品风险审批任务"""
country_info = {
"DE": "德国",
"FR": "法国",
"ES": "西班牙",
"IT": "意大利",
"UK": "英国"
}
@staticmethod
def show_notification(message: str, message_type: str = "error"):
"""显示 pywebview 顶层通知5秒后自动消失
Args:
message: 通知消息
message_type: 消息类型 (success/warning/error/info)
"""
if webview and webview.windows:
try:
# 转义单引号,防止 JavaScript 语法错误
safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
# 设置通知样式颜色
color_map = {
'success': '#67C23A',
'warning': '#E6A23C',
'error': '#F56C6C',
'info': '#909399'
}
bg_color = color_map.get(message_type, '#F56C6C')
# 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知
js_code = f"""
(function() {{
// 移除已存在的通知
var existingNotif = document.getElementById('pywebview-notification');
if (existingNotif) {{
existingNotif.remove();
}}
// 创建通知容器
var notif = document.createElement('div');
notif.id = 'pywebview-notification';
notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' +
'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' +
'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' +
'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;';
// 添加消息内容
var msgSpan = document.createElement('span');
msgSpan.textContent = '{safe_message}';
notif.appendChild(msgSpan);
// 添加关闭按钮
var closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;';
closeBtn.onclick = function() {{ notif.remove(); }};
notif.appendChild(closeBtn);
// 添加到页面
document.body.appendChild(notif);
// 5秒后自动消失
setTimeout(function() {{
if (notif && notif.parentNode) {{
notif.style.transition = 'opacity 0.3s';
notif.style.opacity = '0';
setTimeout(function() {{ notif.remove(); }}, 300);
}}
}}, 5000);
}})();
"""
# 在第一个窗口中执行 JavaScript
webview.windows[0].evaluate_js(js_code)
except Exception as e:
print(f"显示通知失败: {str(e)}")
"""
处理流程:
1、解析任务json,提取对应的companyName 组组装user_info实例化 AmzoneApprove
2、打开对应的店铺、遍历所有的国家站点处理
3、切换至指定国家切换至 管理所有库存页面
4、调用search方法查询出是否存在需要审批的商品如果没有则结束如果有则进入处理流程
5、进入处理流程后调用run_page_action方法(注意是通过yield 返回数据的)
任务json示例
{
"type": "product-risk-resolve-run",
"ts": 1775395996876,
"data": {
"taskId": 925,
"items": [
{
"matched": true,
"platform": "亚马逊",
"shopName": "魏振峰",
"shopId": "27543917795757",
"companyName": "rongchuang123",
"openStoreUrl": null,
"matchedUserId": 17543915345493,
"matchStatus": "MATCHED",
"matchMessage": null
}
],
"country_codes": [
"UK",
"DE",
"FR",
"ES"
]
}
}
"""
def __init__(self, user_info: dict = None):
"""初始化审批任务处理器
Args:
user_info: 用户信息字典,包含 company, username, password
"""
self.user_info = user_info or {}
self.running = True
def log(self, message: str, level: str = "INFO"):
"""日志输出
Args:
message: 日志消息
level: 日志级别
"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if level == "ERROR":
self.show_notification(message, "error")
print(f"[{timestamp}] [ApproveTask] [{level}] {message}")
def process_task(self, task_data: dict):
"""处理审批任务主入口
Args:
task_data: 任务数据
"""
try:
data = task_data.get("data", {})
task_id = data.get("taskId")
items = data.get("items", [])
country_codes = data.get("country_codes", [])
risk_listing_filter = data.get("risk_listing_filter", "")
if not task_id:
self.log("任务ID为空跳过", "WARNING")
return
if not items:
self.log("店铺列表为空,跳过", "WARNING")
return
if not country_codes:
self.log("国家列表为空,跳过", "WARNING")
return
self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家")
from config import runing_task
runing_task[task_id] = {
"status": "running",
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_shops": len(items),
"processed_shops": 0,
"total_countries": len(country_codes) * len(items),
"processed_countries": 0,
"total_asins": 0,
"processed_asins": 0,
"success_count": 0,
"failed_count": 0,
"stop_requested": False
}
# 遍历处理每个店铺
for idx, shop_item in enumerate(items, 1):
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
runing_task[task_id]["status"] = "stopped"
return
shop_name = shop_item.get("shopName", "未知店铺")
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
self.show_notification(f"开始处理店铺: {shop_name}", "info")
try:
self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
# 更新已处理店铺数
if task_id in runing_task:
runing_task[task_id]["processed_shops"] += 1
except Exception as e:
import traceback
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 更新任务状态
if task_id in runing_task:
if runing_task[task_id].get("stop_requested", False):
runing_task[task_id]["status"] = "stopped"
self.log(f"任务 {task_id} 已被暂停!")
else:
runing_task[task_id]["status"] = "completed"
self.log(f"任务 {task_id} 处理完成!")
except Exception as e:
import traceback
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
if task_id:
from config import runing_task
if task_id in runing_task:
runing_task[task_id]["status"] = "failed"
runing_task[task_id]["error"] = str(e)
def process_shop(self, shop_item: dict, country_codes: list, task_id: int, risk_listing_filter: str):
"""处理单个店铺
Args:
shop_item: 店铺信息
country_codes: 国家代码列表
task_id: 任务ID
risk_listing_filter: 风险商品筛选条件
"""
shop_name = shop_item.get("shopName", "未知店铺")
company_name = shop_item.get("companyName", "")
if not company_name:
self.log(f"店铺 {shop_name} 的公司名称为空,跳过", "WARNING")
return
if task_id in runing_task:
runing_task[task_id]["current_shop"] = shop_name
# 将店铺添加到正在执行中的店铺列表
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
runing_shop[shop_name] = start_time
self.log(f"店铺 {shop_name} 已添加到执行列表,账号: {company_name},开始时间: {start_time}")
# 店铺打开重试最多3次
driver = None
max_retries = 3
error_info = ""
for retry in range(max_retries):
try:
self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)")
# 如果不是第一次尝试,先杀进程
# if retry > 0:
# self.log("重试前先杀掉浏览器进程...")
# kill_process("v6")
# kill_process("v5")
# time.sleep(2)
# 组装用户信息并创建驱动
user_info = {
**self.user_info,
"company": company_name
}
driver = AmzoneApprove(user_info)
browser = driver.open_shop(shop_name)
if browser and browser != "店铺不存在":
self.log(f"成功打开店铺 {shop_name}")
break
else:
self.log(f"打开店铺失败: {browser}", "WARNING")
driver = None
# 判断是否需要登录
need_login = driver.need_login()
if need_login:
self.log(f"店铺 {shop_name} 需要登录,正在登录...")
password = ""
login_success = driver.login(password)
if login_success:
self.log(f"店铺 {shop_name} 登录成功,正在重新打开店铺...")
browser = driver.open_shop(shop_name)
if browser and browser != "店铺不存在":
self.log(f"成功打开店铺 {shop_name} 登录后")
break
else:
self.log(f"登录后打开店铺失败: {browser}", "WARNING")
driver = None
else:
self.log(f"店铺 {shop_name} 登录失败", "WARNING")
driver = None
except Exception as e:
import traceback
self.log(f"打开店铺异常: {traceback.format_exc()}", "INFO")
driver = None
error_info = str(e)
time.sleep(10)
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(3)
# 检查是否成功打开
if not driver or not browser or browser == "店铺不存在":
error_msg = f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺,{error_info}"
self.log(error_msg, "ERROR")
# 从执行列表中移除
if shop_name in runing_shop:
del runing_shop[shop_name]
return
try:
# 处理每个国家
for country_code in country_codes:
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
break
try:
self.process_country(driver, country_code, task_id, shop_name,risk_listing_filter)
except Exception as e:
import traceback
self.log(f"处理国家 {country_code} 失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 最后回传,标记完成
try:
self.post_result(task_id, shop_name, country_code, "", "", is_done=True)
except Exception as e:
self.log(f"回传结果失败: {str(e)}", "ERROR")
finally:
# 关闭店铺
try:
if driver:
self.log(f"关闭店铺 {shop_name}")
driver.close_store()
time.sleep(2)
except Exception as e:
self.log(f"关闭店铺失败: {str(e)}", "WARNING")
# 从正在执行中的店铺列表中移除
if shop_name in runing_shop:
del runing_shop[shop_name]
self.log(f"店铺 {shop_name} 已从执行列表中移除")
def process_country(self, driver: AmzoneApprove, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str):
"""处理单个国家的审批任务
Args:
driver: AmzoneApprove驱动实例
country_code: 国家代码(如 UK, DE, FR 等)
task_id: 任务ID
shop_name: 店铺名称
risk_listing_filter: 风险商品筛选条件
"""
from config import runing_task
# 转换国家代码为中文名称
country_name = self.country_info.get(country_code, country_code)
info_mes = f"开始处理国家: {country_name} ({country_code})"
self.log(info_mes)
self.show_notification(info_mes, "info")
# 更新当前处理的国家
if task_id in runing_task:
runing_task[task_id]["current_country"] = country_name
# 切换国家最多重试3次
max_retries = 3
switch_success = False
for retry in range(max_retries):
try:
self.log(f"尝试切换到国家 {country_name} (第 {retry + 1}/{max_retries} 次)")
# 如果不是第一次尝试,先刷新页面
if retry > 0:
self.log("重试前刷新页面...")
try:
driver.tab.refresh()
time.sleep(3)
except Exception as e:
self.log(f"刷新页面失败: {str(e)}", "WARNING")
switch_success = driver.SwitchingCountries(country_name)
if switch_success:
self.log(f"成功切换到国家 {country_name}")
break
else:
self.log(f"切换到国家 {country_name} 失败", "WARNING")
except Exception as e:
import traceback
self.log(f"切换国家 {country_name} 异常: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(2)
# 如果切换失败,直接返回
if not switch_success:
error_message = f"切换到国家 {country_name} 失败,已重试 {max_retries} 次,跳过该国家"
self.log(error_message, "ERROR")
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
return
# 切换到库存管理页面
try:
driver.SwitchPage()
self.log(f"已切换到库存管理页面")
except Exception as e:
import traceback
self.log(f"切换页面失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
return
# 搜索需要审批的商品最多重试3次
sku_ls = []
for retry in range(max_retries):
try:
self.log(f"尝试搜索需要审批的商品 (第 {retry + 1}/{max_retries} 次)")
sku_ls = driver.search(filter_type=risk_listing_filter)
break
except Exception as e:
self.log(f"搜索商品异常: {str(e)}", "ERROR")
if retry < max_retries - 1:
try:
driver.tab.refresh()
time.sleep(3)
except Exception as refresh_error:
self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING")
# 如果没有需要审批的商品,直接返回
if len(sku_ls) == 0:
self.log(f"国家 {country_name} 没有需要审批的商品")
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
return
self.log(f"国家 {country_name}{len(sku_ls)} 个需要审批的商品,开始处理...")
# 处理所有需要审批的商品通过yield获取结果
try:
for asin, status in driver.run_page_action():
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求停止处理ASIN", "WARNING")
break
self.log(f"ASIN {asin} 处理结果: {status}")
# 更新任务状态
if task_id in runing_task:
runing_task[task_id]["current_asin"] = asin
runing_task[task_id]["processed_asins"] += 1
if status == "处理完成":
runing_task[task_id]["success_count"] += 1
elif status == "请求批准":
# 请求批准的商品也算作成功(因为不需要处理)
runing_task[task_id]["success_count"] += 1
else:
runing_task[task_id]["failed_count"] += 1
# 回传结果到API
try:
self.post_result(task_id, shop_name, country_code, asin, status)
except Exception as e:
self.log(f"回传结果失败: {str(e)}", "ERROR")
except Exception as e:
import traceback
self.log(f"处理审批商品异常: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 更新已处理国家数
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
self.log(f"国家 {country_name} 处理完成")
def post_result(self, task_id: int, shop_name: str, country_code: str, asin: str, status: str,is_done: bool = False):
"""回传处理结果到API
Args:
task_id: 任务ID
shop_name: 店铺名称
country_code: 国家代码
asin: ASIN
status: 处理状态
"""
import requests
from config import DELETE_BRAND_API_BASE
url = f"{DELETE_BRAND_API_BASE}/api/product-risk-resolve/tasks/{task_id}/result"
if status == "请求批准":
country_data = {
"status": "",
"shopName": shop_name,
"productAsinSku": "",
"done": is_done,
"removeAsin": asin,
"removeStatus": status
}
else:
country_data = {
"status": status,
"shopName": shop_name,
"productAsinSku": asin,
"done": is_done,
"removeAsin": "",
"removeStatus": ""
}
payload = {
"shops": [
{
"error": "",
"countries": {
country_code : [ country_data ]
},
"shopName": shop_name
}
]
}
max_retries = 3
for retry in range(max_retries):
try:
print("=====================================")
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
self.log(f"回传URL: {url}")
self.log(f"回传数据: {payload}")
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
verify=False
)
self.log(f"回传结果: {response.text}")
if response.status_code == 200:
self.log(f"结果回传成功: {asin} - {status}")
return
else:
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
print("=====================================")
except Exception as e:
self.log(f"调用API异常: {str(e)}", "ERROR")
print("=====================================")
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(2)
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
if __name__ == '__main__':
user_info = {
"company": "rongchuang123",
"username": "自动化_Robot",
"password": "#20zsg25"
}
shop_name = "魏振峰"
country = "西班牙"
kill_process('v6')
driver = AmzoneApprove(user_info)
browser = driver.open_shop(shop_name)
sw_suc = driver.SwitchingCountries(country)
driver.SwitchPage()
risk_listing_filter = "Active"
for _ in range(3):
try:
sku_ls = driver.search(filter_type=risk_listing_filter)
break
except Exception as e:
print(e)
driver.tab.refresh()
if len(sku_ls) > 0:
print("有数据,开始操作")
for asin, status in driver.run_page_action():
print(f"ASIN {asin} 的处理结果: {status}")
print("已完成操作")
# for i in range(0):
# print(i)

File diff suppressed because it is too large Load Diff

View File

@@ -1,714 +0,0 @@
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("已完成操作")

View File

@@ -1,760 +0,0 @@
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()

View File

@@ -1,646 +0,0 @@
import traceback
import winreg
import subprocess
import time
import uuid
import requests
import json
import os
from typing import Literal
from DrissionPage import Chromium
from DrissionPage.common import By
from DrissionPage._pages.chromium_tab import ChromiumTab
def kill_process(version: Literal["v5", "v6"]):
"""杀紫鸟客户端进程(独立函数版本)"""
driver = ZiniaoDriver({})
driver.kill_process(version)
class ZiniaoDriver:
"""紫鸟浏览器自动化驱动类"""
def __init__(self, user_info: dict, socket_port: int = 19890):
"""
初始化紫鸟浏览器驱动
Args:
user_info: 用户信息字典,包含 company, username, password
socket_port: 客户端通信端口,默认 19890
"""
self.user_info = user_info
self.socket_port = socket_port
self.client_path = None
self.browser = None
self.tab: ChromiumTab = None
self.store_id = None
def get_zinaio_exe(self, protocol_name: str = "superbrowser"):
"""
获取紫鸟安装目录
Args:
protocol_name: 协议名称,默认 "superbrowser"
Returns:
exe_path: 紫鸟浏览器可执行文件路径
"""
try:
key_path = rf"SOFTWARE\Classes\{protocol_name}\shell\open\command"
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path)
command, _ = winreg.QueryValueEx(key, "")
winreg.CloseKey(key)
if isinstance(command, str):
sub = "ziniao.exe"
exe_path = command[0: command.find(sub)+len(sub)+1]
else:
exe_path = command[0]
return exe_path
except FileNotFoundError:
try:
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path)
command, _ = winreg.QueryValueEx(key, "")
winreg.CloseKey(key)
if isinstance(command, str):
sub = "ziniao.exe"
exe_path = command[0: command.find(sub)+len(sub)+1]
else:
exe_path = command[0]
return exe_path
except FileNotFoundError:
return None
def update_core(self):
"""
下载所有内核打开店铺前调用需客户端版本5.285.7以上
因为http有超时时间所以这个action适合循环调用直到返回成功
"""
data = {
"action": "updateCore",
"requestId": str(uuid.uuid4()),
}
data.update(self.user_info)
while True:
url = f'http://127.0.0.1:{self.socket_port}'
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
result = response.json()
print(result)
if result is None:
print("等待客户端启动...")
time.sleep(2)
continue
if result.get("statusCode") is None or result.get("statusCode") == -10003:
print("当前版本不支持此接口,请升级客户端")
return
elif result.get("statusCode") == 0:
print("更新内核完成")
return
else:
print(f"等待更新内核: {json.dumps(result)}")
time.sleep(2)
def kill_process(self, version: Literal["v5", "v6"]):
"""
杀紫鸟客户端进程
Args:
version: 客户端版本
"""
if version == "v5":
process_name = 'SuperBrowser.exe'
os.system('taskkill /f /t /im ' + "starter.exe")
else:
process_name = 'ziniao.exe'
os.system('taskkill /f /t /im ' + process_name)
time.sleep(3)
def get_browser_list(self) -> list:
"""
获取浏览器列表
Returns:
list: 浏览器列表
"""
request_id = str(uuid.uuid4())
data = {
"action": "getBrowserList",
"requestId": request_id
}
data.update(self.user_info)
url = f'http://127.0.0.1:{self.socket_port}'
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
r = response.json()
if str(r.get("statusCode")) == "0":
print(r)
return r.get("browserList")
elif str(r.get("statusCode")) == "-10003":
print(f"【get_browser_list】登录失败 {json.dumps(r, ensure_ascii=False)}")
# exit()
else:
print(f"【get_browser_list】失败 {json.dumps(r, ensure_ascii=False)} ")
# exit()
def open_store(self, store_info, isWebDriverReadOnlyMode=0, isprivacy=0,
isHeadless=0, cookieTypeSave=0, jsInfo=""):
"""
打开店铺
Args:
store_info: 店铺信息browserId 或 browserOauth
isWebDriverReadOnlyMode: 是否只读模式,默认 0
isprivacy: 隐私模式,默认 0
isHeadless: 无头模式,默认 0
cookieTypeSave: cookie保存类型默认 0
jsInfo: 注入的JS信息默认 ""
Returns:
dict: 返回结果
"""
request_id = str(uuid.uuid4())
data = {
"action": "startBrowser",
"isWaitPluginUpdate": 0,
"isHeadless": isHeadless,
"requestId": request_id,
"isWebDriverReadOnlyMode": isWebDriverReadOnlyMode,
"cookieTypeLoad": 0,
"cookieTypeSave": cookieTypeSave,
"runMode": "1",
"isLoadUserPlugin": True,
"pluginIdType": 1,
"privacyMode": isprivacy
}
data.update(self.user_info)
if store_info.isdigit():
data["browserId"] = store_info
else:
data["browserOauth"] = store_info
if len(str(jsInfo)) > 2:
data["injectJsInfo"] = json.dumps(jsInfo)
url = f'http://127.0.0.1:{self.socket_port}'
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
r = response.json()
if str(r.get("statusCode")) == "0":
return r
elif str(r.get("statusCode")) == "-10003":
raise RuntimeError(f"【open_store】登录失败 {json.dumps(r, ensure_ascii=False)}")
# exit()
else:
raise RuntimeError(f"【open_store】失败 {json.dumps(r, ensure_ascii=False)} ")
# exit()
def get_browser(self, port) -> Chromium:
"""
获取浏览器实例
Args:
port: 调试端口
Returns:
Chromium: DrissionPage浏览器实例
"""
browser = Chromium(port)
return browser
def start_client(self):
"""启动紫鸟客户端"""
# 检查端口是否已经启动
try:
url = f'http://127.0.0.1:{self.socket_port}'
response = requests.get(url, timeout=2)
print(f"端口 {self.socket_port} 已经启动,跳过启动客户端操作")
return
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
# 端口未启动,继续执行启动操作
print(f"端口 {self.socket_port} 未启动,开始启动客户端")
self.kill_process('v6')
time.sleep(5)
self.client_path = self.get_zinaio_exe("superbrowserv6").strip('"')
print(self.client_path)
cmd = [self.client_path, '--run_type=web_driver', '--ipc_type=http',
'--port=' + str(self.socket_port)]
print(" ".join(cmd))
# 最大重试次数
max_retries = 3
for retry_count in range(max_retries):
print(f"{retry_count + 1} 次尝试启动客户端...")
# 启动进程
subprocess.Popen(cmd)
# 循环检测10秒每0.5秒检测一次
start_check_time = time.time()
client_started = False
while time.time() - start_check_time < 10:
try:
response = requests.get(url, timeout=2)
print(f"客户端启动成功!(第 {retry_count + 1} 次尝试)")
client_started = True
break
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
# 端口还未启动,继续等待
time.sleep(0.5)
if client_started:
time.sleep(5) # 等待客户端完全启动
# 更新内核
self.update_core()
return
else:
print(f"{retry_count + 1} 次尝试启动失败10秒内未检测到客户端启动")
# 超过最大重试次数,抛出异常
raise RuntimeError(f"客户端启动失败:重试 {max_retries} 次后仍未成功启动")
def open_shop(self, shop_name: str):
"""
打开指定店铺
Args:
shop_name: 店铺名称
Returns:
Chromium or str: 成功返回浏览器实例,失败返回错误信息
"""
# 启动客户端
self.start_client()
# 获取店铺列表
shop_ls = self.get_browser_list()
self.store_id = None
print(shop_ls)
for shop in shop_ls:
if shop.get("browserName") == shop_name:
self.store_id = shop.get('browserOauth')
break
if not self.store_id:
print("店铺不存在")
return "店铺不存在"
# 打开店铺
ret_json = self.open_store(self.store_id)
print(ret_json)
self.store_id = ret_json.get("browserOauth")
if self.store_id is None:
self.store_id = ret_json.get("browserId")
# 获取drissionpage浏览器会话
self.browser = self.get_browser(ret_json.get('debuggingPort'))
ip_check_url = ret_json.get("ipDetectionPage")
if not ip_check_url:
print("ip检测页地址为空请升级紫鸟浏览器到最新版")
print(f"=====关闭店铺:{shop_name}=====")
self.close_store(self.store_id)
# exit()
raise RuntimeError("没有IP检测地址为了店铺安全不打开店铺")
ip_usable = self.open_ip_check(self.browser, ip_check_url)
if ip_usable:
print("ip检测通过打开店铺平台主页")
self.open_launcher_page(ret_json.get("launcherPage"), self.browser)
else:
print("IP检测不通过")
raise RuntimeError("IP检测不通过可能是因为网络环境变化导致的为了店铺安全不打开店铺")
return self.browser
def close_store(self, browser_oauth=None):
"""
关闭店铺
Args:
browser_oauth: 店铺OAuth标识如果不提供则使用当前打开的店铺
Returns:
dict: 返回结果
"""
if browser_oauth is None:
browser_oauth = self.store_id
request_id = str(uuid.uuid4())
data = {
"action": "stopBrowser",
"requestId": request_id,
"duplicate": 0,
"browserOauth": browser_oauth
}
data.update(self.user_info)
url = f'http://127.0.0.1:{self.socket_port}'
response = requests.post(url, json.dumps(data).encode('utf-8'), timeout=120)
r = response.json()
if str(r.get("statusCode")) == "0":
return r
elif str(r.get("statusCode")) == "-10003":
raise RuntimeError(f"【close_store】登录失败 {json.dumps(r, ensure_ascii=False)}")
# exit()
else:
raise RuntimeError(f"【close_store】失败 {json.dumps(r, ensure_ascii=False)} ")
# exit()
def open_launcher_page(self, launcher_page: str, browser: Chromium = None):
"""
打开启动页面
Args:
launcher_page: 要打开的页面URL
browser: 浏览器实例,如果不提供则使用当前浏览器实例
"""
if browser is None:
browser = self.browser
tab = browser.new_tab(url=launcher_page)
self.tab = tab
return tab
def open_ip_check(self, browser: Chromium, ip_check_url: str):
"""
打开ip检测页检测ip是否正常
:param browser: drissionpage浏览器会话
:param ip_check_url ip检测页地址
:return 检测结果
"""
try:
tab = browser.latest_tab
tab.get(ip_check_url)
success_button = tab.ele((By.XPATH, '//button[contains(@class, "styles_btn--success")]'),
timeout=60) # 等待查找元素60秒
if success_button:
print("ip检测成功")
return True
else:
print("ip检测超时")
return False
except Exception as e:
print("ip检测异常:" + traceback.format_exc())
return False
class AmamzonBase(ZiniaoDriver):
"""亚马逊操作基类,包含一些通用方法"""
def SwitchingCountries(self, country_name: str):
"""
切换国家
操作:
1、//div[@class="dropdown-account-switcher-header-label"]/span[last()] 获取此元素文本,判断当前国家,如果与目标国家相同则不操作,否则执行下一步
2、点击 //div[@class="dropdown-account-switcher-header-label"] 打开下拉框
3、点击 //div[@class="dropdown-account-switcher-list-item"] 第一个展开国家列表
4、点击 //div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="国家名"] 切换到目标国家
5、等待页面加载完成判断国家是否切换成功成功则返回True否则返回False
Args:
country_name: 目标国家名称
Returns:
bool: 切换成功返回True失败返回False
"""
try:
if self.browser is None:
print("浏览器实例不存在,请先打开店铺")
return False
# 获取当前标签页
tab = self.tab
tab.wait.doc_loaded(timeout=120,raise_err=False)
# 步骤1获取当前国家名称
print(f"正在检查当前国家...")
current_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
timeout=20)
if current_country_ele:
current_country = current_country_ele.text.strip()
print(f"当前国家:{current_country}")
# 判断是否与目标国家相同
if current_country == country_name:
print(f"当前已经是目标国家 {country_name},无需切换")
return True
else:
print("无法获取当前国家信息")
return False
# 步骤2点击打开下拉框
print(f"正在打开国家切换下拉框...")
dropdown_header = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]', timeout=10)
if not dropdown_header:
print("找不到国家切换下拉框")
return False
dropdown_header.click()
time.sleep(1) # 等待下拉框展开
# 步骤3点击第一个展开国家列表
print(f"正在展开国家列表...")
first_item = tab.ele('xpath://div[@class="dropdown-account-switcher-list-item"]', timeout=10)
if not first_item:
print("找不到国家列表项")
return False
first_item.click()
time.sleep(1) # 等待国家列表展开
# 步骤4点击目标国家
print(f"正在切换到国家:{country_name}")
target_country_xpath = f'//div[@class="dropdown-account-switcher-list-item dropdown-account-switcher-list-item-indented" and @title="{country_name}"]'
target_country = tab.ele(f'xpath:{target_country_xpath}', timeout=10)
if not target_country:
print(f"找不到目标国家:{country_name}")
return False
target_country.click()
# 步骤5等待页面加载完成并验证切换结果
print(f"等待页面加载...")
# time.sleep(3) # 等待页面加载
self.tab.wait.doc_loaded()
# 再次检查当前国家
new_country_ele = tab.ele('xpath://div[@class="dropdown-account-switcher-header-label"]/span[last()]',
timeout=10)
if new_country_ele:
new_country = new_country_ele.text.strip()
if new_country == country_name:
print(f"国家切换成功:{new_country}")
return True
else:
print(f"国家切换失败,当前国家:{new_country},目标国家:{country_name}")
return False
else:
print("无法验证切换结果")
return False
except Exception as e:
print(f"切换国家时发生异常:{traceback.format_exc()}")
return False
def need_login(self):
"""
判断是否需要登录,部分国家可能需要登录后才能切换国家
处理流程:
"""
time.sleep(3) # 等待页面可能的登录元素加载,避免跳转等等
self.tab.wait.doc_loaded(timeout=30, raise_err=False)
need_login_ele = self.tab.eles('xpath://h1[@class="a-spacing-small"]|//span[contains(text(),"登录")]',timeout=5)
if len(need_login_ele) >0:
print("检测到需要登录元素")
return True
return False
def login(self,password,username=""):
try:
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)
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
class AmazoneDriver(AmamzonBase):
"""亚马逊专用驱动类继承自ZiniaoDriver"""
def SwitchPage(self):
"""
切换至 管理所有库存页面
1、等待 //navigation-favorites-bar[@class="hydrated"] 出现
"""
navigation = self.tab.ele('xpath://navigation-favorites-bar[@class="hydrated"]')
navigation.wait.displayed(raise_err=False)
page_btn = navigation.sr('xpath://internal-fav-bar-links[@data-internal="navigation"]').sr(
'xpath://a[@data-page-id="ezdpc-gui-inventory-mons"]')
page_btn.wait.displayed(raise_err=False)
page_btn.click(timeout=5)
self.tab.wait.doc_loaded()
# 等待搜索框出现
search_region = self.tab.ele('xpath://div[@id="searchBoxContainer"]//kat-input-group')
search_region.wait.displayed(raise_err=False)
def search(self, 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 del_action(self, sku_ele):
dropdown = sku_ele.ele('xpath:.//kat-dropdown-button[@variant="secondary"]')
dropdown.click()
time.sleep(1)
del_btn = dropdown.sr("xpath:.//button[@role='menuitem' and @data-action='DeleteListing']")
del_btn.wait.displayed(raise_err=False)
del_btn.click()
confirm_btn = self.tab.ele('xpath://kat-modal[@data-testid="action-modal"]//kat-button[@variant="primary"]')
confirm_btn.wait.displayed(raise_err=False)
confirm_btn.wait.enabled()
confirm_btn.click()
suc_alert = self.tab.ele("xpath://kat-alert[@variant='success']")
suc = suc_alert.wait.displayed(raise_err=False, timeout=5)
return suc
if __name__ == '__main__':
# 使用示例
user_info = {
"company": "rongchuang123",
"username": "自动化_Robot",
"password": "#20zsg25"
}
shop_name = "郭亚芳"
kill_process("v6")
# 创建驱动实例
driver = AmazoneDriver(user_info)
# 打开店铺并获取浏览器实例
browser = driver.open_shop(shop_name)
country = "西班牙"
asin = "Voanos"
sw_suc = driver.SwitchingCountries(country)
driver.SwitchPage()
sku_ls = driver.search(asin=asin)
print("查询结果有:", len(sku_ls))
for sku in sku_ls:
suc = driver.del_action(sku)
print(sku, "删除结果", suc)

File diff suppressed because it is too large Load Diff

View File

@@ -1,696 +0,0 @@
import time
import traceback
import requests
from datetime import datetime
from typing import Dict, Any, List
from concurrent.futures import ThreadPoolExecutor, as_completed
from config import JSON_TASK_QUEUE, runing_task, runing_shop, DELETE_BRAND_API_BASE, ZN_COMPANY, ZN_USERNAME, ZN_PASSWORD
from amazon.del_brand import AmazoneDriver, kill_process
from amazon.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
class TaskMonitor:
"""任务监控器:负责监控队列并执行品牌删除任务"""
def __init__(self):
"""初始化任务监控器"""
self.running = True
self.user_info = {
"company": ZN_COMPANY,
"username": ZN_USERNAME,
"password": ZN_PASSWORD
}
self.chunk_index = 1 # 当前处理的分块索引
self.max_workers = 5 # 线程池最大线程数
self.executor = None # 线程池执行器
# 在提交新任务前杀掉旧进程(确保环境干净)
kill_process("v6")
kill_process("v5")
def log(self, message: str, level: str = "INFO"):
"""日志输出
Args:
message: 日志消息
level: 日志级别
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] [{level}] {message}")
def start(self):
"""启动任务监控(使用线程池处理任务)"""
self.log(f"任务监控器启动,开始监听队列... (线程池大小: {self.max_workers})")
# 创建线程池
self.executor = ThreadPoolExecutor(max_workers=self.max_workers)
futures = [] # 保存所有提交的任务Future对象
task_type_info = {
"product-risk-resolve-run" : "产品风险审批",
"shop-match-run" : "匹配价格",
"price-track-run" : "跟价",
"query-asin-run" : "状态查询",
"patrol-delete-run" : "巡店删除",
"appearance-patent-run" : "亚马逊采集",
}
try:
while self.running:
try:
# 使用较长超时时间等待任务,减少空等待异常
# 超时后继续循环检查self.running状态避免卡死
task_data = JSON_TASK_QUEUE.get(block=True, timeout=30)
# 检查任务类型
task_type = task_data.get("type", "")
if task_type == "delete-brand-run":
# 提交删除品牌任务到线程池
self.log(f"接收到【删除品牌】任务,提交到线程池处理...")
future = self.executor.submit(self._process_task_wrapper, task_data)
futures.append(future)
elif task_type in task_type_info:
# 提交产品风险审批任务到线程池
self.log(f"接收到任务,提交到线程池处理...")
future = self.executor.submit(self._process_approve_task_wrapper, task_data, task_type_info[task_type])
futures.append(future)
else:
self.log(f"未知任务类型: {task_type},跳过", "WARNING")
continue
# 清理已完成的future对象避免内存累积
futures = [f for f in futures if not f.done()]
except Exception as e:
# 静默处理队列为空的超时,只记录真正的异常
if str(e) and "Empty" not in str(e):
self.log(f"任务监控异常: {str(e)}", "ERROR")
# 队列为空时不需要额外等待,直接继续循环
finally:
# 关闭监控时,等待所有任务完成
self.log("正在关闭任务监控器,等待所有任务完成...")
if self.executor:
self.executor.shutdown(wait=True)
self.log("所有任务已完成,监控器已关闭")
def _process_task_wrapper(self, task_data: Dict[str, Any]):
"""任务处理包装器(用于线程池调用)
Args:
task_data: 任务数据
"""
try:
self.log(f"线程 {id(task_data)} 开始处理删除品牌任务...")
self.process_task(task_data)
self.log(f"线程 {id(task_data)} 删除品牌任务处理完成")
except Exception as e:
self.log(f"线程 {id(task_data)} 删除品牌任务处理异常: {traceback.format_exc()}", "ERROR")
def _process_approve_task_wrapper(self, task_data: Dict[str, Any],TASK_TYPE:str):
"""审批任务处理包装器(用于线程池调用)
Args:
task_data: 任务数据
"""
try:
TASK_INFO = {
"产品风险审批" : ApproveTask,
"匹配价格" : MatchTak,
"跟价" : PriceTask,
"状态查询" : StatusTask,
"巡店删除" : PatrolDeleteTask,
"亚马逊采集" : SpiderTask,
}
self.log(f"线程 {id(task_data)} 开始处理产品风险审批任务...")
# 创建ApproveTask实例并处理任务
TASK_CLS = TASK_INFO[TASK_TYPE] # 根据任务类型选择处理类默认为ApproveTask
approve_task = TASK_CLS(user_info=self.user_info)
approve_task.process_task(task_data)
self.log(f"线程 {id(task_data)} 产品风险审批任务处理完成")
except Exception as e:
self.log(f"线程 {id(task_data)} 产品风险审批任务处理异常: {traceback.format_exc()}", "ERROR")
def process_task(self, task_data: Dict[str, Any]):
"""处理单个任务
Args:
task_data: 任务数据
"""
try:
data = task_data.get("data", {})
task_id = data.get("taskId")
items = data.get("items", [])
if not task_id:
self.log("任务ID为空跳过", "WARNING")
return
# 初始化任务状态
self.update_task_status(
task_id,
status="running",
start_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
total_shops=len(items),
processed_shops=0,
total_asins=sum(shop.get("totalRows", 0) for shop in items),
processed_asins=0,
success_count=0,
failed_count=0,
stop_requested=False # 暂停请求标志
)
self.log(f"开始处理任务 {task_id},共 {len(items)} 个店铺")
# 遍历处理每个店铺
for idx, shop_data in enumerate(items, 1):
shop_name = shop_data.get("shopName", "未知店铺")
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
try:
self.process_shop(shop_data, task_id)
# 更新已处理店铺数
if task_id in runing_task:
runing_task[task_id]["processed_shops"] += 1
except Exception as e:
self.log(f"处理店铺 {shop_name} 失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
# 检查任务最终状态
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.update_task_status(task_id, status="stopped")
self.log(f"任务 {task_id} 已被暂停!")
else:
self.update_task_status(task_id, status="completed")
self.log(f"任务 {task_id} 处理完成!")
except Exception as e:
self.log(f"任务处理失败: {traceback.format_exc()}", "ERROR")
if task_id:
self.update_task_status(task_id, status="failed", error=str(e))
def process_shop(self, shop_data: Dict[str, Any], task_id: int):
"""处理单个店铺(包含重试逻辑)
Args:
shop_data: 店铺数据
task_id: 任务ID
"""
shop_name = shop_data.get("shopName", "未知店铺")
result_id = shop_data.get("resultId")
countries = shop_data.get("countries", [])
company_name = shop_data.get("companyName", "未知公司")
if not countries:
self.log(f"店铺 {shop_name} 没有国家数据,跳过", "WARNING")
return
# 更新当前处理的店铺
self.update_task_status(task_id, current_shop=shop_name)
# 将店铺添加到正在执行中的店铺列表
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
runing_shop[shop_name] = start_time
self.log(f"账号:{company_name},店铺 {shop_name} 已添加到执行列表,开始时间: {start_time}")
# 店铺打开重试最多3次
driver = None
browser = None
max_retries = 3
for retry in range(max_retries):
try:
self.log(f"尝试打开店铺 {shop_name} (第 {retry + 1}/{max_retries} 次)")
# 如果不是第一次尝试,先杀进程
# if retry > 0:
# self.log("重试前先杀掉浏览器进程...")
# kill_process("v6")
# kill_process("v5")
# time.sleep(2)
# 创建驱动并打开店铺
user_info = {
**self.user_info,
"company": company_name
}
driver = AmazoneDriver(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
# 判断是否需要登录
driver.tab.wait.doc_loaded(timeout=30, raise_err=False) # 等待页面加载,避免过早判断登录状态
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
except Exception as e:
self.log(f"打开店铺异常: {str(e)}", "ERROR")
driver = None
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(3)
# 检查是否成功打开
if not driver or not browser or browser == "店铺不存在":
self.log(f"店铺 {shop_name} 打开失败,已重试 {max_retries} 次,跳过该店铺", "ERROR")
return
try:
# 处理每个国家
chunk_index =1
for country_data in countries:
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
break # 跳出循环进入finally关闭店铺
try:
chunk_index = self.process_country(driver, country_data, task_id, result_id, shop_data,chunk_index)
except Exception as e:
country_name = country_data.get("country", "未知")
self.log(f"处理国家 {country_name} 失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
finally:
# 关闭店铺
try:
if driver:
self.log(f"关闭店铺 {shop_name}")
driver.close_store()
time.sleep(2)
except Exception as e:
self.log(f"关闭店铺失败: {str(e)}", "WARNING")
# 从正在执行中的店铺列表中移除
if shop_name in runing_shop:
del runing_shop[shop_name]
self.log(f"店铺 {shop_name} 已从执行列表中移除")
def process_country(self, driver: AmazoneDriver, country_data: Dict[str, Any],
task_id: int, result_id: int, shop_data: Dict[str, Any],chunk_index:int):
"""处理单个国家的所有ASIN
Args:
driver: 亚马逊驱动实例
country_data: 国家数据
task_id: 任务ID
result_id: 结果ID
shop_data: 店铺数据
"""
country = country_data.get("country", "未知")
items = country_data.get("items", [])
self.log(f"开始处理国家: {country},共 {len(items)} 个ASIN")
self.update_task_status(task_id, current_country=country)
# 切换国家最多重试3次
max_retries = 3
switch_success = False
for retry in range(max_retries):
try:
self.log(f"尝试切换到国家 {country} (第 {retry + 1}/{max_retries} 次)")
# 如果不是第一次尝试,先刷新页面
if retry > 0:
self.log("重试前刷新页面...")
try:
driver.tab.refresh()
time.sleep(3)
except Exception as e:
self.log(f"刷新页面失败: {str(e)}", "WARNING")
switch_success = driver.SwitchingCountries(country)
if switch_success:
self.log(f"成功切换到国家 {country}")
break
else:
self.log(f"切换到国家 {country} 失败", "WARNING")
except Exception as e:
self.log(f"切换国家 {country} 异常: {str(e)}", "ERROR")
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(2)
# 如果切换失败回传该国家所有ASIN为失败状态
if not switch_success:
self.log(f"切换到国家 {country} 失败,已重试 {max_retries}将所有ASIN标记为失败", "ERROR")
chunk_index = self._report_all_asins_failed(country, items, task_id, shop_data,chunk_index)
return chunk_index
# 切换到库存管理页面
try:
driver.SwitchPage()
self.log(f"已切换到库存管理页面")
except Exception as e:
self.log(f"切换页面失败: {str(e)}", "ERROR")
# 切换页面失败也回传所有ASIN为失败
chunk_index = self._report_all_asins_failed(country, items, task_id, shop_data,chunk_index)
return chunk_index
# 处理每个ASIN
file_key = shop_data.get("fileKey", "")
source_filename = shop_data.get("sourceFilename", "")
total_rows = shop_data.get("totalRows", 0)
for idx, asin_item in enumerate(items, 1):
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
return # 返回到上层,会触发店铺关闭
try:
self.log(f"[{idx}/{len(items)}] 处理ASIN: {asin_item.get('asin', '')}")
self.process_asin(driver, asin_item, country, task_id, result_id,
file_key, source_filename, total_rows,chunk_index)
except Exception as e:
asin = asin_item.get("asin", "未知")
self.log(f"处理ASIN {asin} 失败: {str(e)}", "ERROR")
# 继续处理下一个ASIN
chunk_index += 1
return chunk_index
def process_asin(self, driver: AmazoneDriver, asin_item: Dict[str, Any],
country: str, task_id: int, result_id: int, file_key: str,
source_filename: str, total_rows: int,chunk_index:int):
"""处理单个ASIN并回传结果
Args:
driver: 亚马逊驱动实例
asin_item: ASIN数据项
country: 国家名称
task_id: 任务ID
result_id: 结果ID
file_key: 文件KEY
source_filename: 源文件名
total_rows: 总行数
chunk_index: 当前处理索引
"""
asin = asin_item.get("asin", "")
row_index = asin_item.get("rowIndex", 0)
# 更新当前处理的ASIN
self.update_task_status(task_id, current_asin=asin)
status = "失败"
max_retries = 3 # 最多重试3次
for retry in range(max_retries):
try:
self.log(f"处理ASIN {asin} (第 {retry + 1}/{max_retries} 次)")
# 如果不是第一次尝试,先刷新页面
if retry > 0:
self.log("重试前刷新页面...")
try:
driver.tab.refresh()
time.sleep(3)
except Exception as e:
self.log(f"刷新页面失败: {str(e)}", "WARNING")
# 搜索ASIN
sku_ls = driver.search(asin=asin)
self.log(f"搜索到 {len(sku_ls)} 个SKU")
if len(sku_ls) == 0:
status = "查询不到"
self.log(f"ASIN {asin} 未找到商品", "WARNING")
break # 查询不到商品,无需重试
else:
# 删除所有找到的SKU如果任何一个失败则重新开始整个流程
success_count = 0
all_success = True # 标记是否所有SKU都删除成功
total_sku_count = len(sku_ls)
for sku in sku_ls:
try:
suc = driver.del_action(sku)
if suc:
success_count += 1
self.log(f"SKU 删除成功 ({success_count}/{total_sku_count})")
else:
self.log(f"SKU 删除失败", "WARNING")
all_success = False
break # 任何一个失败,退出循环,准备重试整个流程
except Exception as e:
self.log(f"删除SKU异常: {str(e)}", "ERROR")
all_success = False
break # 发生异常,退出循环,准备重试整个流程
# 判断删除结果
if all_success and success_count == total_sku_count:
status = "成功"
self.log(f"ASIN {asin} 所有SKU删除成功 ({success_count}/{total_sku_count})")
# 更新成功计数
if task_id in runing_task:
runing_task[task_id]["success_count"] += 1
break # 全部成功,跳出重试循环
else:
# 有失败的SKU
if retry < max_retries - 1:
self.log(f"有SKU删除失败准备重试整个流程... ({retry + 1}/{max_retries})")
time.sleep(2)
continue # 继续下一次重试
else:
# 所有重试都用完了
if success_count > 0:
status = "部分成功"
self.log(f"ASIN {asin} 部分SKU删除成功 ({success_count}/{total_sku_count})", "WARNING")
if task_id in runing_task:
runing_task[task_id]["success_count"] += 1
else:
status = "失败"
self.log(f"ASIN {asin} 所有SKU删除失败", "ERROR")
if task_id in runing_task:
runing_task[task_id]["failed_count"] += 1
except Exception as e:
status = "删除异常"
self.log(f"处理ASIN {asin} 异常: {str(e)}", "ERROR")
# 如果还有重试机会,继续重试
if retry < max_retries - 1:
self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})")
time.sleep(2)
else:
# 所有重试都失败了,更新失败计数
if task_id in runing_task:
runing_task[task_id]["failed_count"] += 1
# 更新已处理ASIN计数
if task_id in runing_task:
runing_task[task_id]["processed_asins"] += 1
# 回传结果到API
try:
payload = {
"submissionId": "",
"files": [{
"fileKey": file_key,
"sourceFilename": source_filename,
"chunkIndex": chunk_index,
"chunkTotal": total_rows,
"processedRows": chunk_index,
"totalRows": total_rows,
"currentCountry": country,
"currentAsin": asin,
"countries": [{
"country": country,
"items": [{
"asin": asin,
"status": status
}]
}]
}]
}
self.post_result(task_id, payload)
self.log(f"ASIN {asin} 结果已回传,状态: {status}")
except Exception as e:
self.log(f"回传结果失败: {str(e)}", "ERROR")
def _report_all_asins_failed(self, country: str, items: List[Dict[str, Any]],
task_id: int, shop_data: Dict[str, Any],chunk_index:int):
"""将国家下所有ASIN标记为失败并回传
Args:
country: 国家名称
items: ASIN列表
task_id: 任务ID
shop_data: 店铺数据
"""
self.log(f"开始回传国家 {country} 下的 {len(items)} 个ASIN失败状态")
file_key = shop_data.get("fileKey", "")
source_filename = shop_data.get("sourceFilename", "")
total_rows = shop_data.get("totalRows", 0)
for asin_item in items:
asin = asin_item.get("asin", "")
try:
# 更新失败计数
if task_id in runing_task:
runing_task[task_id]["failed_count"] += 1
runing_task[task_id]["processed_asins"] += 1
# 回传失败状态
payload = {
"submissionId": "",
"files": [{
"fileKey": file_key,
"sourceFilename": source_filename,
"chunkIndex": chunk_index,
"chunkTotal": total_rows,
"processedRows": chunk_index,
"totalRows": total_rows,
"currentCountry": country,
"currentAsin": asin,
"countries": [{
"country": country,
"items": [{
"asin": asin,
"status": "失败"
}]
}]
}]
}
self.post_result(task_id, payload)
chunk_index += 1
self.log(f"ASIN {asin} 失败状态已回传")
except Exception as e:
self.log(f"回传ASIN {asin} 失败状态时出错: {str(e)}", "ERROR")
return chunk_index
def post_result(self, task_id: int, payload: Dict[str, Any]):
"""回传结果到API带重试机制
Args:
task_id: 任务ID
payload: 结果数据
"""
url = f"{DELETE_BRAND_API_BASE}/api/delete-brand/tasks/{task_id}/result"
max_retries = 3 # 最多重试3次
for retry in range(max_retries):
try:
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
verify=False # 忽略SSL证书验证
)
print("【结果提交】:",payload)
print("【结果提交返回】:",response.text)
if response.status_code == 200:
self.log(f"结果回传成功: {url}")
return # 成功后直接返回,不再重试
else:
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
# 如果还有重试机会,继续重试
if retry < max_retries - 1:
self.log(f"准备重试... ({retry + 1}/{max_retries})")
time.sleep(2) # 等待2秒后重试
else:
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
except Exception as e:
self.log(f"调用API异常: {str(e)}", "ERROR")
# 如果还有重试机会,继续重试
if retry < max_retries - 1:
self.log(f"发生异常,准备重试... ({retry + 1}/{max_retries})")
time.sleep(2) # 等待2秒后重试
else:
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
def update_task_status(self, task_id: int, **kwargs):
"""更新任务状态
Args:
task_id: 任务ID
**kwargs: 要更新的字段
"""
if task_id not in runing_task:
runing_task[task_id] = {}
runing_task[task_id].update(kwargs)
def stop(self):
"""停止监控"""
self.running = False
self.log("任务监控器已停止")
def main():
"""主函数:启动任务监控"""
# 创建并启动任务监控器
monitor = TaskMonitor()
try:
monitor.start()
except KeyboardInterrupt:
monitor.log("接收到中断信号,正在停止...")
monitor.stop()
except Exception:
monitor.log(f"监控器异常退出: {traceback.format_exc()}", "ERROR")
if __name__ == "__main__":
main()

View File

@@ -1,738 +0,0 @@
import time
import re
import traceback
from DrissionPage._pages.chromium_tab import ChromiumTab
from config import runing_task, runing_shop
from datetime import datetime
from amazon.del_brand import AmamzonBase, kill_process
from amazon.tool import show_notification,get_shop_info
class AmzoneMatchAction(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)
def search(self,filter_type="ApprovalRequired"):
sku_ls = []
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
if len(load_ele) > 0:
load_ele[0].wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
drop_down = self.tab.ele('xpath://div[contains(@class,"VolusListingStatusDropDown-module__verticalContainer")]//kat-dropdown')
drop_down.wait.displayed(raise_err=False)
drop_down.wait.enabled(raise_err=False)
time.sleep(0.6)
drop_down.click()
# //kat-option[@value="SearchSuppressed"]
xp = f'xpath://kat-option[@value="{filter_type}"]'
print(f"{self.mark_name}】正在寻找筛选条件 {filter_type}xpath: {xp}")
approval_required = self.tab.eles(xp,timeout=5)
if len(approval_required) == 0:
print(f"{self.mark_name}】没有需要{filter_type}选项】没有需要{filter_type}的商品了")
return sku_ls # "没有需要审批的商品了"
else:
approval_required = approval_required[0]
approval_required.wait.displayed(raise_err=False)
approval_required.click()
approval_required_text = approval_required.text
print(f"{self.mark_name}】已选择筛选条件: {approval_required_text}")
count = re.findall(r'\d+', approval_required_text)
if count:
count = int(count[0])
print(f"{self.mark_name}】待审批的商品数量: {count}")
if count <= 0:
print(f"{self.mark_name}】没有需要{filter_type}的商品了")
return sku_ls #"没有需要审批的商品了"
for _ in range(3):
# 等待加载完成
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]")
if len(load_ele) > 0:
load_ele[0].wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
sku_ls = self.tab.eles("xpath://div[@data-sku]",timeout=3)
if len(sku_ls) > 0:
break
approval_required.click()
return sku_ls
def wait_loaded(self):
# 等待加载完成
try:
load_ele = self.tab.ele(
'xpath://kat-panel[@data-testid="kat-panel-ActionPanelContent"]//div[contains(@class,"Loader-module__loader")]|//kat-panel[@data-testid="kat-panel-ActionPanelContent"]/div[@data-f1-component]//div[contains(@class,"==")]/div[contains(@class,"==")]/span',
timeout=3)
load_ele.wait.deleted(timeout=5, raise_err=False)
except Exception as e:
print(f"{self.mark_name}】等待加载中消失出错", e)
def run_page_action(self):
print(f"{self.mark_name}】开始执行")
num = 0
retry_num = 0
already_asin = set()
get_page_faild = 0
while retry_num < 3: # 最多重试3次
# if num > 3: #测试
# return
# 等待加载完成
try:
load_ele = self.tab.eles("xpath://div[contains(@class,'Loader-module__loader')]",timeout=5)
if len(load_ele) > 0:
load_ele[0].wait.deleted(timeout=3, raise_err=False)
time.sleep(0.5)
# 获取当前页码
try:
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
if len(page_pamel) > 0:
current_page = page_pamel[0].sr('xpath:.//ul[@class="pages"]//li[@aria-current="true"]').text
# 总页数
total_page = page_pamel[0].sr.eles('xpath:.//ul[@class="pages"]//span[@class="page__inner"][last()]')
if len(total_page) > 0:
total_page = total_page[-1].text
else:
total_page = 0
print(f"{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)}")
# for sku_ele in sku_ls[0:2]:
for sku_ele in sku_ls:
# solve_problem = sku_ele.eles('xpath:.//kat-link[@label="解决商品信息问题"]')
asin = sku_ele.ele('xpath:.//div[contains(@class,"JanusSplitBox-module__container")]//div[contains(@class,"JanusSplitBox-module__panel--") and contains(string(.),"ASIN")]/..//div[last()]',timeout=3).text
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=2)
if len(price_match) == 0:
print(f"{self.mark_name}{asin},没有推荐价格匹配按钮")
yield (asin,"无需处理")
already_asin.add(asin)
continue
try:
price_match[0].click()
save_all_btn = self.tab.ele('xpath://kat-button[@label="保存所有"]',timeout=10)
save_all_btn.wait.displayed(timeout=10, raise_err=False)
save_all_btn.click()
save_all_btn.wait.deleted(timeout=10, raise_err=False)
yield (asin,"处理完成")
already_asin.add(asin)
except Exception as e:
print(f"{asin} 处理失败", e)
yield (asin,"处理失败")
already_asin.add(asin)
continue
# 判断是否存在需要翻页的情况
page_pamel = self.tab.eles('xpath://kat-pagination',timeout=5)
if len(page_pamel) == 0:
break
next_page_btn = page_pamel[0].sr('xpath:.//span[@part="pagination-nav-right"]')
class_str = next_page_btn.attr('class')
if "end" in class_str:
break
next_page_btn.click()
num += 1
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 MatchTak:
country_info = {
"DE": "德国",
"FR": "法国",
"ES": "西班牙",
"IT": "意大利",
"UK": "英国"
}
def __init__(self, user_info: dict = None):
"""初始化审批任务处理器
Args:
user_info: 用户信息字典,包含 company, username, password
"""
self.user_info = user_info or {}
self.running = True
def log(self, message: str, level: str = "INFO"):
"""日志输出
Args:
message: 日志消息
level: 日志级别
"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if level == "ERROR":
show_notification(message, "error")
print(f"[{timestamp}] [MatchTak] [{level}] {message}")
def process_task(self, task_data: dict):
"""处理审批任务主入口
Args:
task_data: 任务数据
"""
try:
data = task_data.get("data", {})
task_id = data.get("taskId")
items = data.get("items", [])
country_codes = data.get("country_codes", [])
risk_listing_filter = data.get("risk_listing_filter", "All")
user_id = data.get("user_id")
stage_index = data.get("stage_index")
final_stage = bool(data.get("final_stage", True))
# 用于测试
limit = data.get("limit",None)
if not task_id:
self.log("任务ID为空跳过", "WARNING")
return
if not items:
self.log("店铺列表为空,跳过", "WARNING")
return
if not country_codes:
self.log("国家列表为空,跳过", "WARNING")
return
self.log(f"开始处理审批任务 {task_id},共 {len(items)} 个店铺,{len(country_codes)} 个国家")
from config import runing_task
runing_task[task_id] = {
"status": "running",
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_shops": len(items),
"processed_shops": 0,
"total_countries": len(country_codes) * len(items),
"processed_countries": 0,
"total_asins": 0,
"processed_asins": 0,
"success_count": 0,
"failed_count": 0,
"stop_requested": False
}
# 遍历处理每个店铺
for idx, shop_item in enumerate(items, 1):
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理", "WARNING")
runing_task[task_id]["status"] = "stopped"
return
shop_name = shop_item.get("shopName", "未知店铺")
self.log(f"[{idx}/{len(items)}] 开始处理店铺: {shop_name}")
show_notification(f"开始处理店铺: {shop_name}", "info")
try:
self.process_shop(shop_item, country_codes, task_id, risk_listing_filter, user_id, stage_index, final_stage,limit=limit)
# self.process_shop(shop_item, country_codes, task_id,risk_listing_filter)
# 更新已处理店铺数
if task_id in runing_task:
runing_task[task_id]["processed_shops"] += 1
except Exception as e:
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 = 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,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次
driver = None
max_retries = 3
iskill = False
# 处理每个国家
for country_code in country_codes:
# 检查是否收到暂停请求
if task_id in runing_task and runing_task[task_id].get("stop_requested", False):
self.log(f"检测到任务 {task_id} 的暂停请求,停止处理国家", "WARNING")
break
# 打开店铺
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.process_country(driver, country_code, task_id, shop_name,risk_listing_filter,limit)
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
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: AmzoneMatchAction, country_code: str, task_id: int, shop_name: str, risk_listing_filter: str,limit:str=None):
"""处理单个国家的审批任务
Args:
driver: AmzoneApprove驱动实例
country_code: 国家代码(如 UK, DE, FR 等)
task_id: 任务ID
shop_name: 店铺名称
risk_listing_filter: 风险商品筛选条件
"""
from config import runing_task
# 转换国家代码为中文名称
country_name = self.country_info.get(country_code, country_code)
info_mes = f"开始处理国家: {country_name} ({country_code})"
self.log(info_mes)
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
# 切换到库存管理页面
try:
driver.SwitchPage()
self.log(f"已切换到库存管理页面")
except Exception as e:
import traceback
self.log(f"切换页面失败: {str(e)}", "ERROR")
self.log(traceback.format_exc(), "ERROR")
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
return
# 搜索需要审批的商品最多重试3次
sku_ls = []
for retry in range(max_retries):
try:
self.log(f"尝试搜索匹配操作商品 (第 {retry + 1}/{max_retries} 次)")
sku_ls = driver.search(filter_type=risk_listing_filter)
break
except Exception as e:
self.log(f"搜索商品异常: {str(e)}", "ERROR")
if retry < max_retries - 1:
try:
driver.tab.refresh()
time.sleep(3)
except Exception as refresh_error:
self.log(f"刷新页面失败: {str(refresh_error)}", "WARNING")
# 如果没有需要审批的商品,直接返回
if len(sku_ls) == 0:
self.log(f"国家 {country_name} 没有搜索出的商品")
if task_id in runing_task:
runing_task[task_id]["processed_countries"] += 1
return
self.log(f"国家 {country_name} 搜索出 {len(sku_ls)} 商品,开始处理...")
# 处理所有需要审批的商品通过yield获取结果
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")
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: str,is_done: bool = False):
"""回传处理结果到API
Args:
task_id: 任务ID
shop_name: 店铺名称
country_code: 国家代码
asin: ASIN
status: 处理状态
"""
import requests
from config import DELETE_BRAND_API_BASE
url = f"{DELETE_BRAND_API_BASE}/api/shop-match/tasks/{task_id}/result"
payload = {
"shops": [
{
"error": "",
"countries": {
country_code: [
{
"asin": asin,
"status": status,
"done": is_done
}
]
},
"shopName": shop_name
}
]
}
max_retries = 3
for retry in range(max_retries):
try:
print("================【匹配】=====================")
self.log(f"尝试回传结果 (第 {retry + 1}/{max_retries} 次)")
self.log(f"回传URL: {url}")
self.log(f"回传数据: {payload}")
response = requests.post(
url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=30,
verify=False
)
self.log(f"回传结果: {response.text}")
data = response.json() if response.text else {}
if response.status_code == 200 and isinstance(data, dict) and data.get("success"):
self.log(f"结果回传成功: {asin} - {status}")
return
else:
self.log(f"结果回传失败,状态码: {response.status_code}", "WARNING")
print("=====================================")
except Exception as e:
self.log(f"调用API异常: {str(e)}", "ERROR")
print("=====================================")
# 如果还有重试机会,等待后继续
if retry < max_retries - 1:
time.sleep(2)
self.log(f"已达到最大重试次数,结果回传最终失败", "ERROR")
raise RuntimeError("已达到最大重试次数,结果回传最终失败")
if __name__ == "__main__":
user_info = {
"company": "rongchuang123",
"username": "自动化_Robot",
"password": "#20zsg25"
}
shop_name = "魏振峰"
country = "德国"
kill_process('v6')
driver = AmzoneMatchAction(user_info)
browser = driver.open_shop(shop_name)
sw_suc = driver.SwitchingCountries(country)
driver.SwitchPage()
risk_listing_filter = "All"
for _ in range(3):
try:
sku_ls = driver.search(filter_type=risk_listing_filter)
break
except Exception as e:
print(e)
driver.tab.refresh()
if len(sku_ls) > 0:
print("有数据,开始操作")
for asin, status in driver.run_page_action():
print(f"ASIN {asin} 的处理结果: {status}")
print("已完成操作")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +0,0 @@
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;

View File

@@ -1,73 +0,0 @@
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,
};

View File

@@ -1,121 +0,0 @@
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),
};

View File

@@ -1,89 +0,0 @@
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,
},
};

View File

@@ -1,46 +0,0 @@
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);

View File

@@ -1,187 +0,0 @@
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),
};

View File

@@ -1,71 +0,0 @@
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),
},
};

View File

@@ -1,124 +0,0 @@
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,
})),
};

View File

@@ -1,3 +0,0 @@
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;

View File

@@ -1,95 +0,0 @@
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);

View File

@@ -1,295 +0,0 @@
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),
})),
};

View File

@@ -1,129 +0,0 @@
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);

View File

@@ -1,91 +0,0 @@
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),
};

View File

@@ -1,148 +0,0 @@
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,
},
};

View File

@@ -1,94 +0,0 @@
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,
};

View File

@@ -1,132 +0,0 @@
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),
})),
};

View File

@@ -1,332 +0,0 @@
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),
})),
};

View File

@@ -1,162 +0,0 @@
# 导入 webview 用于前端通知
try:
import webview
except ImportError:
webview = None
import requests
from urllib.parse import quote
import re
def show_notification(message: str, message_type: str = "error"):
"""显示 pywebview 顶层通知5秒后自动消失
Args:
message: 通知消息
message_type: 消息类型 (success/warning/error/info)
"""
if webview and webview.windows:
try:
# 转义单引号,防止 JavaScript 语法错误
safe_message = message.replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
# 设置通知样式颜色
color_map = {
'success': '#67C23A',
'warning': '#E6A23C',
'error': '#F56C6C',
'info': '#909399'
}
bg_color = color_map.get(message_type, '#F56C6C')
# 构建前端通知的 JavaScript 代码 - 创建原生 HTML 通知
js_code = f"""
(function() {{
// 移除已存在的通知
var existingNotif = document.getElementById('pywebview-notification');
if (existingNotif) {{
existingNotif.remove();
}}
// 创建通知容器
var notif = document.createElement('div');
notif.id = 'pywebview-notification';
notif.style.cssText = 'position: fixed; top: 20px; left: 50%; transform: translateX(-50%); ' +
'background: {bg_color}; color: white; padding: 12px 20px; border-radius: 4px; ' +
'box-shadow: 0 2px 12px rgba(0,0,0,0.3); z-index: 99999; font-size: 14px; ' +
'max-width: 600px; word-wrap: break-word; display: flex; align-items: center; gap: 10px;';
// 添加消息内容
var msgSpan = document.createElement('span');
msgSpan.textContent = '{safe_message}';
notif.appendChild(msgSpan);
// 添加关闭按钮
var closeBtn = document.createElement('span');
closeBtn.innerHTML = '×';
closeBtn.style.cssText = 'cursor: pointer; font-size: 18px; font-weight: bold; margin-left: 10px;';
closeBtn.onclick = function() {{ notif.remove(); }};
notif.appendChild(closeBtn);
// 添加到页面
document.body.appendChild(notif);
// 5秒后自动消失
setTimeout(function() {{
if (notif && notif.parentNode) {{
notif.style.transition = 'opacity 0.3s';
notif.style.opacity = '0';
setTimeout(function() {{ notif.remove(); }}, 300);
}}
}}, 5000);
}})();
"""
# 在第一个窗口中执行 JavaScript
webview.windows[0].evaluate_js(js_code)
except Exception as e:
print(f"显示通知失败: {str(e)}")
def get_shop_info(shop_name: str, base_url: str = "http://8.136.19.173:18080") -> requests.Response:
"""
调用商铺凭证接口
:param shop_name: 商铺名称,将自动进行 URL 编码
:param base_url: API 基础地址,默认从 curl 中提取
:return: requests.Response 对象
"""
url = f"{base_url}/api/admin/shop-manages/credential"
params = {"shopName": shop_name}
headers = {
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Referer": "http://8.136.19.173:18080/doc.html",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
"X-Internal-Token": "59c6691199917a2095827b5188502029",
}
response = requests.get(
url,
params=params,
headers=headers,
verify=False,
timeout=30
)
return response
def remove_special_characters(text: str) -> str:
"""
去除字符串中的特殊字符,只保留数字、小数点和负号。
例如:'£24.55' -> '24.55'
"""
# 匹配所有允许的字符:数字、小数点、负号
# 注意:负号必须位于开头才合法,但这里只做字符保留,不做格式校验
cleaned = re.sub(r'[^0-9.-]', '', text)
return cleaned
def split_currency_values(currency_str: str) -> tuple[float, float]:
"""
将包含两个货币值的字符串拆分成两个浮点数。
参数:
currency_str (str): 格式如 "€22.64 + €0.00" 的字符串,中间以 '+' 分隔,
每部分可包含任意货币符号或前缀/后缀。
返回:
tuple[float, float]: 两个数值,顺序与字符串中的出现顺序一致。
异常:
ValueError: 如果字符串不包含正好两个部分,或者任一部分中无法提取到数值。
"""
# 按第一个 '+' 分割,最多分为两部分
parts = currency_str.split('+', 1)
if len(parts) != 2:
raise ValueError("字符串必须包含两个由 '+' 分隔的部分")
# 匹配整数或浮点数(可选负号)
number_pattern = r'-?\d+(?:\.\d+)?'
values = []
for part in parts:
# 去除首尾空格
part = part.strip()
match = re.search(number_pattern, part)
if not match:
raise ValueError(f"无法从 '{part}' 中提取数值")
values.append(float(match.group()))
return tuple(values)
if __name__ == '__main__':
cu_str = "€58.44 + €0.00 匹配"
res = split_currency_values(cu_str)
print(sum(res))

View File

@@ -1,55 +0,0 @@
"""
卖相AI - Flask 后端
按功能拆分为蓝图:认证(auth)、主页面(main)、管理员(admin)、图片(image)、品牌(brand)
"""
import os
import secrets
from datetime import timedelta
from flask import Flask
from flask_cors import CORS
from app_common import init_db, BASE_DIR
from blueprints.auth import auth_bp
from blueprints.main import main_bp
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', '*')
cors_origins = frontend_origin if frontend_origin != '*' else '*'
CORS(
app,
supports_credentials=True,
resources={r"/api/*": {"origins": cors_origins}, r"/login": {"origins": cors_origins}},
)
# 生产环境必须通过环境变量注入固定 SECRET_KEY否则服务重启会使旧会话失效。
app.secret_key = os.environ.get('SECRET_KEY', 'dev-secret-key-change-me')
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SAMESITE'] = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax')
app.config['SESSION_COOKIE_SECURE'] = os.environ.get('SESSION_COOKIE_SECURE', '0') == '1'
# 注册蓝图(不设 url_prefix保持原有 URL 路径不变,前端无需改动)
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(image_bp)
app.register_blueprint(brand_bp)
app.register_blueprint(communication_bp)
return app
app = create_app()
def run_app(host='127.0.0.1', port=5123):
# init_db()
app.run(host=host, port=port, threaded=True, use_reloader=False)
if __name__ == '__main__':
run_app()

View File

@@ -1,291 +0,0 @@
"""
公共模块:数据库连接、初始化、会话校验、装饰器、模板渲染
供各蓝图复用
"""
import os
import secrets
from datetime import timedelta
from functools import wraps
import pymysql
from flask import request, redirect, url_for, session, jsonify, render_template, render_template_string
from 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')
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():
return pymysql.connect(
host=mysql_host,
user=mysql_user,
password=mysql_password,
database=mysql_database,
charset='utf8mb4',
cursorclass=pymysql.cursors.DictCursor
)
def _render_html(template_name: str, **context):
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
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:
from html_crypto import decrypt
content = decrypt(raw).decode("utf-8")
except Exception:
content = raw.decode("utf-8", errors="replace")
return render_template_string(content, **context)
def _render_html_new(template_name: str, **context):
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
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:
from html_crypto import decrypt
content = decrypt(raw).decode("utf-8")
except Exception:
content = raw.decode("utf-8", errors="replace")
return render_template_string(content, **context)
def _is_session_user_valid():
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
uid = session.get('user_id')
if not uid:
return False
try:
conn = get_db()
with conn.cursor() as cur:
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
row = cur.fetchone()
conn.close()
if not row:
session.clear()
return False
return True
except Exception:
session.clear()
return False
def _get_current_admin_role():
"""获取当前登录用户的管理角色super_admin / admin / None非管理员"""
try:
conn = get_db()
with conn.cursor() as cur:
cur.execute(
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
(session['user_id'],)
)
row = cur.fetchone()
conn.close()
if not row or not row.get('is_admin'):
return None, None
return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row
except Exception:
return None, None
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not session.get('user_id') or not _is_session_user_valid():
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'error': '未登录'}), 401
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated
def admin_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not session.get('user_id'):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'error': '未登录'}), 401
return redirect(url_for('auth.login'))
try:
conn = get_db()
with conn.cursor() as cur:
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],))
row = cur.fetchone()
conn.close()
if not row or not row.get('is_admin'):
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
return redirect(url_for('main.home'))
except Exception as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'success': False, 'error': str(e)}), 500
return redirect(url_for('main.home'))
return f(*args, **kwargs)
return decorated
def init_db():
"""初始化数据库表,若不存在则创建"""
from werkzeug.security import generate_password_hash
conn = pymysql.connect(
host=mysql_host,
user=mysql_user,
password=mysql_password,
charset='utf8mb4'
)
try:
with conn.cursor() as cur:
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4")
cur.execute(f"USE `{mysql_database}`")
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL,
is_admin TINYINT(1) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS image_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
panel_type VARCHAR(64) DEFAULT '',
original_urls JSON,
params JSON,
result_urls JSON,
long_image_url VARCHAR(1024) DEFAULT NULL,
INDEX idx_user_created (user_id, created_at DESC)
)
""")
try:
cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL")
except Exception:
pass
try:
cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL")
except Exception:
pass
try:
cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'")
except Exception:
pass
try:
cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL")
except Exception:
pass
cur.execute("""
CREATE TABLE IF NOT EXISTS brand_crawl_tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
file_paths JSON NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行',
result_paths JSON NULL,
error_message TEXT NULL,
progress_current INT DEFAULT 0,
progress_total INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_status (user_id, status)
)
""")
try:
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_current INT DEFAULT 0")
except Exception:
pass
try:
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN progress_total INT DEFAULT 0")
except Exception:
pass
try:
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN `desc` VARCHAR(500) NULL COMMENT '上传文件名描述'")
except Exception:
pass
try:
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN strategy VARCHAR(20) DEFAULT 'Terms' COMMENT '品牌匹配方式: Terms/Simple'")
except Exception:
pass
try:
cur.execute("ALTER TABLE brand_crawl_tasks ADD COLUMN task_type TINYINT DEFAULT 1 COMMENT '1=立即执行,2=添加任务执行'")
except Exception:
pass
cur.execute("""
CREATE TABLE IF NOT EXISTS columns (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(128) NOT NULL COMMENT '栏目名',
column_key VARCHAR(64) NOT NULL COMMENT '栏目标识',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_column_key (column_key)
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS user_column_permission (
user_id INT NOT NULL,
column_id INT NOT NULL,
PRIMARY KEY (user_id, column_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
)
""")
try:
cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)")
cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1")
row = cur.fetchone()
if row and row.get('mid'):
mid = row['mid']
cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,))
cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,))
cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,))
except Exception:
pass
conn.commit()
finally:
conn.close()
_create_initial_admin()
def _create_initial_admin():
"""若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)"""
from werkzeug.security import generate_password_hash
try:
conn = get_db()
with conn.cursor() as cur:
cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1")
if cur.fetchone():
conn.close()
return
admin_user = os.environ.get('ADMIN_USER', 'admin')
admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123')
pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256')
cur.execute(
"INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')",
(admin_user, pwd_hash)
)
conn.commit()
conn.close()
except Exception:
pass

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
.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))}}

View File

@@ -1 +0,0 @@
.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))}}

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

View File

@@ -1 +0,0 @@
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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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