feat: patrol delete
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +1,11 @@
|
||||
import json
|
||||
import logging
|
||||
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
|
||||
@@ -17,8 +17,6 @@ except ImportError:
|
||||
winreg = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
STATUS_OK = "0"
|
||||
STATUS_LOGIN_FAILED = "-10003"
|
||||
|
||||
@@ -76,7 +74,7 @@ class UserInfo(TypedDict):
|
||||
|
||||
def kill_process(version: Literal["v5", "v6"]):
|
||||
"""结束指定版本的紫鸟客户端进程."""
|
||||
logger.info("准备杀紫鸟客户端进程,version=%s", version)
|
||||
logger.info("准备杀紫鸟客户端进程,version={}", version)
|
||||
driver = ZiniaoDriver({})
|
||||
driver.kill_process(version)
|
||||
|
||||
@@ -171,7 +169,7 @@ class ZiniaoDriver:
|
||||
(winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE"),
|
||||
):
|
||||
try:
|
||||
logger.info("正在从注册表读取紫鸟客户端路径:%s\\%s", root_name, key_path)
|
||||
logger.info("正在从注册表读取紫鸟客户端路径:{}\\{}", root_name, key_path)
|
||||
key = winreg.OpenKey(root_key, key_path)
|
||||
command, _ = winreg.QueryValueEx(key, "")
|
||||
winreg.CloseKey(key)
|
||||
@@ -180,12 +178,12 @@ class ZiniaoDriver:
|
||||
exe_path = command[0 : command.find(sub) + len(sub) + 1]
|
||||
else:
|
||||
exe_path = command[0]
|
||||
logger.info("已获取紫鸟客户端路径:%s", exe_path)
|
||||
logger.info("已获取紫鸟客户端路径:{}", exe_path)
|
||||
return exe_path
|
||||
except FileNotFoundError:
|
||||
logger.warning("注册表中未找到紫鸟客户端路径:%s\\%s", root_name, key_path)
|
||||
logger.warning("注册表中未找到紫鸟客户端路径:{}\\{}", root_name, key_path)
|
||||
|
||||
logger.error("未能获取紫鸟客户端路径,protocol_name=%s", protocol_name)
|
||||
logger.error("未能获取紫鸟客户端路径,protocol_name={}", protocol_name)
|
||||
return None
|
||||
|
||||
def update_core(self):
|
||||
@@ -199,13 +197,13 @@ class ZiniaoDriver:
|
||||
logger.info("开始更新紫鸟浏览器内核")
|
||||
while True:
|
||||
result = self._post_client(payload)
|
||||
logger.info("更新内核返回:%s", result)
|
||||
logger.info("更新内核返回:{}", result)
|
||||
if self._handle_update_core_result(result):
|
||||
return
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
logger.info("等待更新内核完成:%s", self._result_text(result))
|
||||
logger.info("等待更新内核完成:{}", self._result_text(result))
|
||||
time.sleep(UPDATE_CORE_RETRY_DELAY)
|
||||
|
||||
def _handle_update_core_result(self, result: Optional[Dict[str, Any]]) -> bool:
|
||||
@@ -232,14 +230,35 @@ class ZiniaoDriver:
|
||||
if version == "v5":
|
||||
process_name = "SuperBrowser.exe"
|
||||
logger.info("准备结束紫鸟 v5 starter.exe")
|
||||
os.system("taskkill /f /t /im starter.exe")
|
||||
self._kill_process_by_name("starter.exe")
|
||||
else:
|
||||
process_name = "ziniao.exe"
|
||||
|
||||
logger.info("准备结束紫鸟客户端进程:%s", process_name)
|
||||
os.system("taskkill /f /t /im " + process_name)
|
||||
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]]]:
|
||||
"""获取紫鸟浏览器店铺列表.
|
||||
|
||||
@@ -249,18 +268,18 @@ class ZiniaoDriver:
|
||||
payload = self._build_payload("getBrowserList")
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始获取紫鸟浏览器列表,requestId=%s", request_id)
|
||||
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("获取紫鸟浏览器列表成功,数量=%s", len(browser_list or []))
|
||||
logger.info("获取紫鸟浏览器列表成功,数量={}", len(browser_list or []))
|
||||
return browser_list
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("获取紫鸟浏览器列表登录失败:%s", self._result_text(result))
|
||||
logger.error("获取紫鸟浏览器列表登录失败:{}", self._result_text(result))
|
||||
return None
|
||||
|
||||
logger.error("获取紫鸟浏览器列表失败:%s", self._result_text(result))
|
||||
logger.error("获取紫鸟浏览器列表失败:{}", self._result_text(result))
|
||||
return None
|
||||
|
||||
def open_store(
|
||||
@@ -295,17 +314,17 @@ class ZiniaoDriver:
|
||||
)
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始打开紫鸟店铺,store_info=%s,requestId=%s", store_info, request_id)
|
||||
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=%s", result.get("debuggingPort"))
|
||||
logger.info("紫鸟店铺打开成功,debuggingPort={}", result.get("debuggingPort"))
|
||||
return result
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("打开紫鸟店铺登录失败:%s", self._result_text(result))
|
||||
logger.error("打开紫鸟店铺登录失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"[open_store]登录失败 {self._result_text(result)}")
|
||||
|
||||
logger.error("打开紫鸟店铺失败:%s", self._result_text(result))
|
||||
logger.error("打开紫鸟店铺失败:{}", self._result_text(result))
|
||||
raise RuntimeError(f"[open_store]失败 {self._result_text(result)} ")
|
||||
|
||||
def _build_start_browser_payload(
|
||||
@@ -352,18 +371,18 @@ class ZiniaoDriver:
|
||||
Returns:
|
||||
DrissionPage Chromium 实例.
|
||||
"""
|
||||
logger.info("开始连接 DrissionPage 浏览器,port=%s", port)
|
||||
logger.info("开始连接 DrissionPage 浏览器,port={}", port)
|
||||
browser = Chromium(port)
|
||||
logger.info("DrissionPage 浏览器连接完成,port=%s", port)
|
||||
logger.info("DrissionPage 浏览器连接完成,port={}", port)
|
||||
return browser
|
||||
|
||||
def start_client(self):
|
||||
"""启动紫鸟客户端并更新浏览器内核."""
|
||||
if self._is_client_ready():
|
||||
logger.info("端口 %s 已启动,跳过启动紫鸟客户端", self.socket_port)
|
||||
logger.info("端口 {} 已启动,跳过启动紫鸟客户端", self.socket_port)
|
||||
return
|
||||
|
||||
logger.info("端口 %s 未启动,开始启动紫鸟客户端", self.socket_port)
|
||||
logger.info("端口 {} 未启动,开始启动紫鸟客户端", self.socket_port)
|
||||
self.kill_process("v6")
|
||||
time.sleep(CLIENT_RESTART_DELAY)
|
||||
client_path = self.get_zinaio_exe("superbrowserv6")
|
||||
@@ -377,21 +396,21 @@ class ZiniaoDriver:
|
||||
"--ipc_type=http",
|
||||
"--port=" + str(self.socket_port),
|
||||
]
|
||||
logger.info("紫鸟客户端启动命令:%s", " ".join(cmd))
|
||||
logger.info("紫鸟客户端启动命令:{}", " ".join(cmd))
|
||||
|
||||
for retry_count in range(CLIENT_START_RETRIES):
|
||||
logger.info("第 %s/%s 次尝试启动紫鸟客户端", retry_count + 1, 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("紫鸟客户端启动成功,第 %s 次尝试", retry_count + 1)
|
||||
logger.info("紫鸟客户端启动成功,第 {} 次尝试", retry_count + 1)
|
||||
time.sleep(CLIENT_POST_START_DELAY)
|
||||
self.update_core()
|
||||
return
|
||||
|
||||
logger.warning("第 %s 次尝试启动失败,10秒内未检测到客户端启动", retry_count + 1)
|
||||
logger.warning("第 {} 次尝试启动失败,10秒内未检测到客户端启动", retry_count + 1)
|
||||
|
||||
logger.error("紫鸟客户端启动失败,已重试 %s 次", CLIENT_START_RETRIES)
|
||||
logger.error("紫鸟客户端启动失败,已重试 {} 次", CLIENT_START_RETRIES)
|
||||
raise RuntimeError(f"客户端启动失败:重试 {CLIENT_START_RETRIES} 次后仍未成功启动")
|
||||
|
||||
def open_shop(self, shop_name: str):
|
||||
@@ -403,12 +422,12 @@ class ZiniaoDriver:
|
||||
Returns:
|
||||
成功时返回浏览器实例.店铺不存在时返回错误信息.
|
||||
"""
|
||||
logger.info("开始打开店铺:%s", shop_name)
|
||||
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("店铺不存在:%s", shop_name)
|
||||
logger.warning("店铺不存在:{}", shop_name)
|
||||
return "店铺不存在"
|
||||
|
||||
ret_json = self.open_store(self.store_id)
|
||||
@@ -419,16 +438,16 @@ class ZiniaoDriver:
|
||||
self.browser = self.get_browser(ret_json.get("debuggingPort"))
|
||||
ip_check_url = ret_json.get("ipDetectionPage")
|
||||
if not ip_check_url:
|
||||
logger.error("ip检测页地址为空,准备关闭店铺:%s", shop_name)
|
||||
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检测通过,打开店铺平台主页:%s", shop_name)
|
||||
logger.info("IP检测通过,打开店铺平台主页:{}", shop_name)
|
||||
self.open_launcher_page(ret_json.get("launcherPage"), self.browser)
|
||||
return self.browser
|
||||
|
||||
logger.error("IP检测不通过,停止打开店铺:%s", shop_name)
|
||||
logger.error("IP检测不通过,停止打开店铺:{}", shop_name)
|
||||
raise RuntimeError("IP检测不通过,可能是因为网络环境变化导致的,为了店铺安全不打开店铺")
|
||||
|
||||
def close_store(self, browser_oauth=None):
|
||||
@@ -452,17 +471,17 @@ class ZiniaoDriver:
|
||||
)
|
||||
request_id = payload["requestId"]
|
||||
|
||||
logger.info("开始关闭紫鸟店铺,browserOauth=%s,requestId=%s", browser_oauth, request_id)
|
||||
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=%s", browser_oauth)
|
||||
logger.info("紫鸟店铺关闭成功,browserOauth={}", browser_oauth)
|
||||
return result
|
||||
if status_code == STATUS_LOGIN_FAILED:
|
||||
logger.error("关闭紫鸟店铺登录失败:%s", 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)}")
|
||||
|
||||
logger.error("关闭紫鸟店铺失败:%s", 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):
|
||||
@@ -475,7 +494,7 @@ class ZiniaoDriver:
|
||||
if browser is None:
|
||||
browser = self.browser
|
||||
|
||||
logger.info("打开店铺平台启动页:%s", launcher_page)
|
||||
logger.info("打开店铺平台启动页:{}", launcher_page)
|
||||
tab = browser.new_tab(url=launcher_page)
|
||||
self.tab = tab
|
||||
return tab
|
||||
@@ -491,7 +510,7 @@ class ZiniaoDriver:
|
||||
IP 检测通过时返回 True,否则返回 False.
|
||||
"""
|
||||
try:
|
||||
logger.info("开始打开IP检测页:%s", ip_check_url)
|
||||
logger.info("开始打开IP检测页:{}", ip_check_url)
|
||||
tab = browser.latest_tab
|
||||
tab.get(ip_check_url)
|
||||
success_button = tab.ele(
|
||||
@@ -512,7 +531,7 @@ class ZiniaoDriver:
|
||||
class AmamzonBase(ZiniaoDriver):
|
||||
"""亚马逊页面操作基类."""
|
||||
|
||||
def SwitchingCountries(self, country_name: str):
|
||||
def switch_to_country(self, country_name: str):
|
||||
"""切换亚马逊账号国家并验证结果.
|
||||
|
||||
流程包括读取当前国家,打开国家下拉框,选择目标国家并等待页面加载.
|
||||
@@ -534,7 +553,7 @@ class AmamzonBase(ZiniaoDriver):
|
||||
logger.warning("无法获取当前国家信息")
|
||||
return False
|
||||
if current_country == country_name:
|
||||
logger.info("当前已经是目标国家 %s,无需切换", country_name)
|
||||
logger.info("当前已经是目标国家 {},无需切换", country_name)
|
||||
return True
|
||||
|
||||
if not self._open_country_dropdown():
|
||||
@@ -557,7 +576,7 @@ class AmamzonBase(ZiniaoDriver):
|
||||
return None
|
||||
|
||||
current_country = current_country_ele.text.strip()
|
||||
logger.info("当前国家:%s", current_country)
|
||||
logger.info("当前国家:{}", current_country)
|
||||
return current_country
|
||||
|
||||
def _open_country_dropdown(self) -> bool:
|
||||
@@ -579,10 +598,10 @@ class AmamzonBase(ZiniaoDriver):
|
||||
return True
|
||||
|
||||
def _select_country(self, country_name: str) -> bool:
|
||||
logger.info("正在切换到国家:%s", country_name)
|
||||
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("找不到目标国家:%s", country_name)
|
||||
logger.warning("找不到目标国家:{}", country_name)
|
||||
return False
|
||||
target_country.click()
|
||||
return True
|
||||
@@ -601,10 +620,10 @@ class AmamzonBase(ZiniaoDriver):
|
||||
logger.warning("无法验证国家切换结果")
|
||||
return False
|
||||
if new_country == country_name:
|
||||
logger.info("国家切换成功:%s", new_country)
|
||||
logger.info("国家切换成功:{}", new_country)
|
||||
return True
|
||||
|
||||
logger.warning("国家切换失败,当前国家:%s,目标国家:%s", new_country, country_name)
|
||||
logger.warning("国家切换失败,当前国家:{},目标国家:{}", new_country, country_name)
|
||||
return False
|
||||
|
||||
def need_login(self):
|
||||
@@ -657,7 +676,7 @@ class AmamzonBase(ZiniaoDriver):
|
||||
|
||||
def _send_otp_if_present(self) -> bool:
|
||||
send_code = self.tab.eles(OTP_SEND_XPATH, timeout=OTP_SEND_TIMEOUT)
|
||||
logger.info("发送一次性密码元素数量:%s", len(send_code))
|
||||
logger.info("发送一次性密码元素数量:{}", len(send_code))
|
||||
if len(send_code) == 0:
|
||||
return False
|
||||
|
||||
@@ -678,7 +697,7 @@ class AmamzonBase(ZiniaoDriver):
|
||||
|
||||
def _find_otp_input(self):
|
||||
otp_code_input = self.tab.eles(OTP_INPUT_XPATH, timeout=OTP_INPUT_TIMEOUT)
|
||||
logger.info("验证码输入框数量:%s", len(otp_code_input))
|
||||
logger.info("验证码输入框数量:{}", len(otp_code_input))
|
||||
if len(otp_code_input) == 0:
|
||||
return None
|
||||
|
||||
@@ -706,7 +725,7 @@ class AmamzonBase(ZiniaoDriver):
|
||||
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("验证码输入错误:%s", error_mes[0].text)
|
||||
logger.warning("验证码输入错误:{}", error_mes[0].text)
|
||||
self.tab.refresh()
|
||||
return False
|
||||
|
||||
@@ -718,3 +737,4 @@ class AmamzonBase(ZiniaoDriver):
|
||||
if len(submit_btn) > 0:
|
||||
logger.info("点击二次登录提交按钮")
|
||||
submit_btn[0].click()
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ 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.approve import ApproveTask
|
||||
from amazon.match_action import MatchTak
|
||||
from amazon.price_match import PriceTask
|
||||
from amazon.asin_status import StatusTask
|
||||
from amazon.product import ProductTask
|
||||
|
||||
|
||||
from amazon.tool import get_shop_info,show_notification
|
||||
@@ -53,12 +54,13 @@ class TaskMonitor:
|
||||
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" : "状态查询"
|
||||
}
|
||||
task_type_info = {
|
||||
"product-risk-resolve-run" : "产品风险审批",
|
||||
"shop-match-run" : "匹配价格",
|
||||
"price-track-run" : "跟价",
|
||||
"query-asin-run" : "状态查询",
|
||||
"patrol-delete-run" : "巡店删除"
|
||||
}
|
||||
try:
|
||||
while self.running:
|
||||
try:
|
||||
@@ -123,10 +125,11 @@ class TaskMonitor:
|
||||
try:
|
||||
TASK_INFO = {
|
||||
"产品风险审批" : ApproveTask,
|
||||
"匹配价格" : MatchTak,
|
||||
"跟价" : PriceTask,
|
||||
"状态查询" : StatusTask
|
||||
}
|
||||
"匹配价格" : MatchTak,
|
||||
"跟价" : PriceTask,
|
||||
"状态查询" : StatusTask,
|
||||
"巡店删除" : ProductTask
|
||||
}
|
||||
self.log(f"线程 {id(task_data)} 开始处理产品风险审批任务...")
|
||||
# 创建ApproveTask实例并处理任务
|
||||
TASK_CLS = TASK_INFO[TASK_TYPE] # 根据任务类型选择处理类,默认为ApproveTask
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function rowOf(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text: textOf(el),
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2500),
|
||||
};
|
||||
}
|
||||
|
||||
const candidates = allElements()
|
||||
.map((el, index) => rowOf(el, index))
|
||||
.filter((row) => row.visible && /补全草稿|快速查看|quick view|draft/i.test(`${row.tag} ${row.text} ${Object.values(row.attrs).join(' ')}`))
|
||||
.slice(0, 120);
|
||||
|
||||
return candidates;
|
||||
@@ -0,0 +1,73 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function attrsText(attrs) {
|
||||
return Object.entries(attrs).map(([key, value]) => `${key}=${value}`).join(' ');
|
||||
}
|
||||
|
||||
const pattern = /快速查看|quick|draft|草稿|提交|submitted|missing|inventory|myinventory/i;
|
||||
const elements = allElements()
|
||||
.map((el, index) => {
|
||||
const attrs = attrsOf(el);
|
||||
const text = textOf(el);
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text: text.slice(0, 500),
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1000),
|
||||
haystack: `${el.tagName} ${text} ${attrsText(attrs)}`,
|
||||
};
|
||||
})
|
||||
.filter((row) => pattern.test(row.haystack))
|
||||
.map(({ haystack, ...row }) => row)
|
||||
.slice(0, 80);
|
||||
|
||||
const resources = performance
|
||||
.getEntriesByType('resource')
|
||||
.map((entry) => entry.name)
|
||||
.filter((url) => pattern.test(url))
|
||||
.slice(-120);
|
||||
|
||||
return {
|
||||
location: window.location.href,
|
||||
readyState: document.readyState,
|
||||
capturedResponses: (window.__crawlerCompleteDraftResponses || []).slice(-10),
|
||||
elements,
|
||||
resources,
|
||||
};
|
||||
121
app/amazon/scripts/product/complete_draft_quick_view_tags.js
Normal file
121
app/amazon/scripts/product/complete_draft_quick_view_tags.js
Normal file
@@ -0,0 +1,121 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function hasCountText(text) {
|
||||
return /[((]\s*[\d,]+\s*[))]/.test(text);
|
||||
}
|
||||
|
||||
function insideListingRow(el) {
|
||||
return Boolean(el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]'));
|
||||
}
|
||||
|
||||
function rowOf(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const text = textOf(el);
|
||||
const labelText = attrs.label || attrs.title || attrs['aria-label'] || '';
|
||||
const valueText = attrs.value || attrs.count || attrs['data-count'] || attrs['data-value'] || '';
|
||||
return {
|
||||
index,
|
||||
raw_text: text,
|
||||
label_text: labelText,
|
||||
quantity_text: valueText,
|
||||
attrs,
|
||||
visible: isVisible(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
function splitCountRow(el, index) {
|
||||
const countText = textOf(el);
|
||||
if (!/^\s*[\d,]+\s*$/.test(countText)) return null;
|
||||
|
||||
let parent = el.parentElement;
|
||||
for (let depth = 0; parent && depth < 4; depth += 1, parent = parent.parentElement) {
|
||||
const parentText = textOf(parent);
|
||||
const labelText = parentText
|
||||
.replace(new RegExp(`(^|\\s)${countText.replace(/,/g, '\\,')}(\\s|$)`), ' ')
|
||||
.replace(/[((]\s*[\d,]+\s*[))]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (labelText && labelText !== parentText && !/^\s*[\d,]+\s*$/.test(labelText)) {
|
||||
return {
|
||||
index,
|
||||
raw_text: '',
|
||||
label_text: labelText,
|
||||
quantity_text: countText,
|
||||
attrs: attrsOf(parent),
|
||||
visible: isVisible(parent),
|
||||
outerHTML: (parent.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const regionRows = allElements().filter((el) => isVisible(el) && !insideListingRow(el));
|
||||
|
||||
const byText = new Map();
|
||||
regionRows
|
||||
.forEach((el, index) => {
|
||||
const row = rowOf(el, index);
|
||||
const combined = `${row.raw_text} ${row.label_text} ${row.quantity_text}`;
|
||||
if (row.raw_text.length > 200) return;
|
||||
if (/[((]\s*[\d,]+\s*[))]/.test(combined)) {
|
||||
const key = combined;
|
||||
if (!byText.has(key)) byText.set(key, row);
|
||||
return;
|
||||
}
|
||||
|
||||
const splitRow = splitCountRow(el, index);
|
||||
if (splitRow) {
|
||||
const key = `${splitRow.label_text} ${splitRow.quantity_text}`;
|
||||
if (!byText.has(key)) byText.set(key, splitRow);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
region: {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
},
|
||||
rows: Array.from(byText.values()).filter((row) => {
|
||||
const combined = `${row.raw_text} ${row.label_text}`.toLowerCase();
|
||||
return !/所有商品|启售商品|改善商品信息/.test(combined);
|
||||
}),
|
||||
candidates: Array.from(byText.values()).slice(0, 80),
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
function textOf(node) {
|
||||
return ((node && (node.innerText || node.textContent || '')) || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function normalize(text) {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseStatus(rawText) {
|
||||
const text = normalize(rawText);
|
||||
if (!text) return '';
|
||||
const match = text.match(/^(.*?)\s*[((]\s*[\d,]+\s*[))]\s*$/);
|
||||
return normalize(match ? match[1] : text);
|
||||
}
|
||||
|
||||
function collectStatusTargets() {
|
||||
const pattern = /(全部|在搜索结果中禁止显示|配送问题|缺少报价|已停售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|不可售|在售)\s*[((]?\s*[\d,]*\s*[))]?/g;
|
||||
const hits = [];
|
||||
|
||||
for (const el of allElements()) {
|
||||
if (!isVisible(el)) continue;
|
||||
const text = normalize(textOf(el));
|
||||
if (!text) continue;
|
||||
const match = text.match(pattern);
|
||||
if (!match) continue;
|
||||
const parsed = parseStatus(match[0]);
|
||||
hits.push({
|
||||
el,
|
||||
text,
|
||||
parsed,
|
||||
tag: el.tagName,
|
||||
clickable: Boolean(el.onclick) || el.matches('button,[role="button"],kat-option,kat-label,a,[tabindex]'),
|
||||
});
|
||||
}
|
||||
|
||||
return hits;
|
||||
}
|
||||
|
||||
const targetStatus = normalize((__crawlerPayload || {}).status);
|
||||
const candidates = collectStatusTargets()
|
||||
.filter((item) => item.parsed === targetStatus || item.text.includes(targetStatus))
|
||||
.map((item) => ({
|
||||
...item,
|
||||
score:
|
||||
(item.parsed === targetStatus ? 300 : 0) +
|
||||
(item.clickable ? 120 : 0) +
|
||||
(/^[^ ]+\s*[((]\s*[\d,]+\s*[))]$/.test(item.text) ? 80 : 0) +
|
||||
(/button|option|label/i.test(item.tag) ? 30 : 0),
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
if (!candidates.length) {
|
||||
return { ok: false, reason: 'visible status target not found', targetStatus };
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const chain = [candidate.el];
|
||||
let node = candidate.el.parentElement;
|
||||
for (let depth = 0; node && depth < 5; depth += 1, node = node.parentElement) {
|
||||
chain.push(node);
|
||||
}
|
||||
|
||||
for (const el of chain) {
|
||||
try {
|
||||
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
el.click();
|
||||
return {
|
||||
ok: true,
|
||||
targetStatus,
|
||||
clicked: {
|
||||
tag: el.tagName,
|
||||
text: normalize(textOf(el)).slice(0, 300),
|
||||
},
|
||||
matched: {
|
||||
tag: candidate.tag,
|
||||
text: candidate.text.slice(0, 300),
|
||||
parsed: candidate.parsed,
|
||||
score: candidate.score,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
// try parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'visible status target click failed',
|
||||
targetStatus,
|
||||
candidates: candidates.slice(0, 10).map((item) => ({
|
||||
tag: item.tag,
|
||||
text: item.text.slice(0, 300),
|
||||
parsed: item.parsed,
|
||||
score: item.score,
|
||||
clickable: item.clickable,
|
||||
})),
|
||||
};
|
||||
95
app/amazon/scripts/product/listing_status_dropdown_debug.js
Normal file
95
app/amazon/scripts/product/listing_status_dropdown_debug.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const keywords = ['ListingStatus', 'listing status', '商品状态', '商品狀態', 'status', 'filter', '库存'];
|
||||
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function nearestLabel(el) {
|
||||
const parts = [];
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 6; depth += 1, node = node.parentElement) {
|
||||
const text = textOf(node);
|
||||
if (text) parts.push(text.slice(0, 400));
|
||||
}
|
||||
const id = el.getAttribute('id');
|
||||
if (id) {
|
||||
document.querySelectorAll(`label[for="${CSS.escape(id)}"]`).forEach((label) => {
|
||||
const text = textOf(label);
|
||||
if (text) parts.unshift(text);
|
||||
});
|
||||
}
|
||||
return [...new Set(parts)].join(' | ');
|
||||
}
|
||||
|
||||
function hasKeyword(text) {
|
||||
return keywords.some((keyword) => text.toLowerCase().includes(keyword.toLowerCase()));
|
||||
}
|
||||
|
||||
function candidateRow(el, index) {
|
||||
const attrs = attrsOf(el);
|
||||
const attrText = Object.entries(attrs).map(([k, v]) => `${k}=${v}`).join(' ');
|
||||
const label = nearestLabel(el);
|
||||
const text = textOf(el);
|
||||
const haystack = `${el.tagName} ${attrText} ${text} ${label}`;
|
||||
let score = 0;
|
||||
if (/商品状态|商品狀態|ListingStatus|listing status/i.test(haystack)) score += 100;
|
||||
if (/status/i.test(haystack)) score += 25;
|
||||
if (/filter|库存/i.test(haystack)) score += 10;
|
||||
if (isVisible(el)) score += 5;
|
||||
if (el.closest('[data-sku], tr, [role="row"]')) score -= 50;
|
||||
return {
|
||||
index,
|
||||
score,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
visible: isVisible(el),
|
||||
text,
|
||||
label,
|
||||
attrs,
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
const rows = allElements();
|
||||
const candidates = [];
|
||||
rows.forEach((el, index) => {
|
||||
const tag = (el.tagName || '').toLowerCase();
|
||||
const haystack = `${tag} ${textOf(el)} ${Object.values(attrsOf(el)).join(' ')} ${nearestLabel(el)}`;
|
||||
if (tag === 'kat-dropdown' || tag === 'kat-option' || hasKeyword(haystack)) {
|
||||
candidates.push(candidateRow(el, index));
|
||||
}
|
||||
});
|
||||
|
||||
return candidates.sort((a, b) => b.score - a.score).slice(0, 80);
|
||||
225
app/amazon/scripts/product/listing_status_dropdown_open.js
Normal file
225
app/amazon/scripts/product/listing_status_dropdown_open.js
Normal file
@@ -0,0 +1,225 @@
|
||||
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);
|
||||
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 statusHitCount(text) {
|
||||
const matches = normalize(text).match(
|
||||
/(全部\s*[((]\s*[\d,]+\s*[))]|在售\s*[((]\s*[\d,]+\s*[))]|不可售\s*[((]\s*[\d,]+\s*[))]|详情页面已删除\s*[((]\s*[\d,]+\s*[))]|订单页面已删除\s*[((]\s*[\d,]+\s*[))]|定价问题\s*[((]\s*[\d,]+\s*[))]|需要批准\s*[((]\s*[\d,]+\s*[))]|在搜索结果中禁止显示\s*[((]\s*[\d,]+\s*[))])/g
|
||||
);
|
||||
return matches ? matches.length : 0;
|
||||
}
|
||||
|
||||
function scoreDropdown(el) {
|
||||
const text = textOf(el);
|
||||
const label = nearestLabel(el);
|
||||
const haystack = `${el.tagName} ${attrsText(el)} ${text} ${label}`;
|
||||
const statusHits = statusHitCount(`${text} ${label}`);
|
||||
let score = 0;
|
||||
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}`)) score += 300;
|
||||
if (statusHits >= 2) score += statusHits * 180;
|
||||
if (/SKUS?|ASIN|FNSKU|UPC\/EAN|TITLE_KEYWORD|商品名称\/关键字/i.test(`${text} ${label}`)) score -= 300;
|
||||
if (isVisible(el)) score += 10;
|
||||
if (el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]')) score -= 80;
|
||||
return { score, text, label, haystack, statusHits };
|
||||
}
|
||||
|
||||
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];
|
||||
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,
|
||||
text: row.text,
|
||||
label: row.label,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
@@ -0,0 +1,78 @@
|
||||
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 insideListingRow(el) {
|
||||
return Boolean(el.closest('[data-sku], tr, [role="row"], [data-testid*="row"]'));
|
||||
}
|
||||
|
||||
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,
|
||||
attrs,
|
||||
visible: isVisible(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
const markedDropdown = document.querySelector('[data-crawler-listing-status-dropdown="true"]');
|
||||
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, [role="option"]')));
|
||||
}
|
||||
}
|
||||
|
||||
const allOptions = allElements().filter((el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const role = el.getAttribute('role');
|
||||
if (tag !== 'kat-option' && role !== 'option') return false;
|
||||
if (insideListingRow(el)) return false;
|
||||
return isVisible(el);
|
||||
});
|
||||
|
||||
const byNode = new Map();
|
||||
scoped.concat(allOptions).forEach((el, index) => {
|
||||
if (!byNode.has(el)) byNode.set(el, optionRow(el, index));
|
||||
});
|
||||
|
||||
return Array.from(byNode.values()).filter((row) => row.raw_text);
|
||||
91
app/amazon/scripts/product/listing_status_section.js
Normal file
91
app/amazon/scripts/product/listing_status_section.js
Normal file
@@ -0,0 +1,91 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function extractStatuses(text) {
|
||||
const pattern = /(全部|在搜索结果中禁止显示|配送问题|缺少报价|已停售|详情页面已删除|订单页面已删除|商品信息草稿|缺少的信息|定价问题|需要批准|不可售|在售)\s*(?:[((]\s*([\d,]+)\s*[))])?/g;
|
||||
const rows = [];
|
||||
let match;
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
rows.push({
|
||||
status: match[1],
|
||||
quantity: match[2] || '',
|
||||
raw_text: match[0].trim(),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseDropdown(dropdown, index) {
|
||||
const nodes = Array.from(dropdown.querySelectorAll('kat-option, kat-label, [class*="parentKatOptionStyle"]'));
|
||||
const seen = new Set();
|
||||
const rows = nodes
|
||||
.flatMap((node) => {
|
||||
const rawText = textOf(node);
|
||||
return extractStatuses(rawText).map((parsed) => ({
|
||||
...parsed,
|
||||
value: node.getAttribute('value') || '',
|
||||
}));
|
||||
})
|
||||
.filter((row) => {
|
||||
const key = `${row.status}::${row.quantity}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
|
||||
const quantityCount = rows.filter((row) => row.quantity).length;
|
||||
const rawText = textOf(dropdown);
|
||||
const className = String(dropdown.className || '');
|
||||
const isStatusDropdown = /statusDropdown/i.test(className);
|
||||
const score =
|
||||
rows.length * 100 +
|
||||
quantityCount * 50 +
|
||||
(isStatusDropdown ? 500 : 0) +
|
||||
(/在搜索结果中禁止显示|需要批准|详情页面已删除/.test(rawText) ? 200 : 0);
|
||||
|
||||
return {
|
||||
index,
|
||||
tag: (dropdown.tagName || '').toLowerCase(),
|
||||
raw_text: rawText,
|
||||
rows,
|
||||
rowCount: rows.length,
|
||||
quantityCount,
|
||||
score,
|
||||
className,
|
||||
outerHTML: (dropdown.outerHTML || '').slice(0, 1200),
|
||||
};
|
||||
}
|
||||
|
||||
const ranked = allElements()
|
||||
.map((el, index) => ({ el, index }))
|
||||
.filter(({ el }) => (el.tagName || '').toLowerCase() === 'kat-dropdown')
|
||||
.map(({ el, index }) => parseDropdown(el, index))
|
||||
.filter((row) => row.rowCount >= 3)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
const best = ranked[0];
|
||||
return {
|
||||
rows: best ? best.rows : [],
|
||||
selected: best || null,
|
||||
candidates: ranked.slice(0, 10),
|
||||
};
|
||||
79
app/amazon/scripts/product/listing_status_select_option.js
Normal file
79
app/amazon/scripts/product/listing_status_select_option.js
Normal file
@@ -0,0 +1,79 @@
|
||||
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 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),
|
||||
};
|
||||
}
|
||||
|
||||
const targetStatus = normalize((__crawlerPayload || {}).status);
|
||||
const dropdown = document.querySelector('[data-crawler-listing-status-dropdown="true"]');
|
||||
if (!dropdown) {
|
||||
return { ok: false, reason: 'marked dropdown missing', targetStatus };
|
||||
}
|
||||
|
||||
const roots = [dropdown];
|
||||
if (dropdown.shadowRoot) roots.push(dropdown.shadowRoot);
|
||||
|
||||
const options = [];
|
||||
for (const root of roots) {
|
||||
for (const el of Array.from(root.querySelectorAll('kat-option, [role="option"]'))) {
|
||||
if (!isVisible(el)) continue;
|
||||
options.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
const metas = options.map((el, index) => ({ el, ...optionMeta(el, index) }));
|
||||
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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
matched.el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
matched.el.click();
|
||||
return {
|
||||
ok: true,
|
||||
targetStatus,
|
||||
selected: {
|
||||
index: matched.index,
|
||||
rawText: matched.rawText,
|
||||
parsedStatus: matched.parsedStatus,
|
||||
value: matched.value,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function snapshot(el, index) {
|
||||
return {
|
||||
index,
|
||||
tag: el.tagName.toLowerCase(),
|
||||
text: textOf(el),
|
||||
attrs: attrsOf(el),
|
||||
outerHTML: (el.outerHTML || '').slice(0, 2000),
|
||||
};
|
||||
}
|
||||
|
||||
const KEYWORD_RE = /推荐报价|featured offer|buy box|商城|marketplace|欧洲|英国|德国|法国|西班牙|意大利|2\s*天前|30\s*天前|2\s*days?\s*ago|30\s*days?\s*ago/i;
|
||||
const visible = allElements().filter((el) => isVisible(el));
|
||||
|
||||
const keywordMatches = visible
|
||||
.map((el, index) => ({ el, index, text: textOf(el) }))
|
||||
.filter((row) => KEYWORD_RE.test(`${row.el.tagName} ${row.text} ${Object.values(attrsOf(row.el)).join(' ')}`))
|
||||
.slice(0, 120)
|
||||
.map(({ el, index }) => snapshot(el, index));
|
||||
|
||||
const buyBoxCard = visible.find((el) => (el.getAttribute('data-key') || '') === 'KPI_CARD_BUYBOX')
|
||||
|| visible.find((el) => (el.id || '') === 'KPI_CARD_BUYBOX');
|
||||
|
||||
const buyBoxNeighborhood = [];
|
||||
if (buyBoxCard) {
|
||||
let node = buyBoxCard;
|
||||
for (let depth = 0; node && depth < 6; depth += 1, node = node.parentElement) {
|
||||
buyBoxNeighborhood.push({
|
||||
depth,
|
||||
tag: node.tagName.toLowerCase(),
|
||||
text: textOf(node),
|
||||
attrs: attrsOf(node),
|
||||
outerHTML: (node.outerHTML || '').slice(0, 2500),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const buyBoxLeafMatches = [];
|
||||
if (buyBoxCard) {
|
||||
const scoped = [];
|
||||
walkRoots(buyBoxCard, scoped, new Set());
|
||||
scoped
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ el, index, text: textOf(el) }))
|
||||
.filter((row) => row.text && row.text.length <= 200)
|
||||
.filter((row) => /推荐报价|商城|marketplace|欧洲|英国|德国|法国|西班牙|意大利|\d+(?:\.\d+)?\s*%|2\s*天前|30\s*天前|2\s*days?\s*ago|30\s*days?\s*ago/i.test(row.text))
|
||||
.slice(0, 120)
|
||||
.forEach(({ el, index }) => buyBoxLeafMatches.push(snapshot(el, index)));
|
||||
}
|
||||
|
||||
return {
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
buyBoxFound: !!buyBoxCard,
|
||||
buyBoxNeighborhood,
|
||||
buyBoxLeafMatches,
|
||||
keywordMatches,
|
||||
};
|
||||
132
app/amazon/scripts/product/recommended_offer_percentage_open.js
Normal file
132
app/amazon/scripts/product/recommended_offer_percentage_open.js
Normal file
@@ -0,0 +1,132 @@
|
||||
function textOf(node) {
|
||||
return (node && (node.innerText || node.textContent || '') || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function attrsOf(el) {
|
||||
const attrs = {};
|
||||
if (!el || !el.attributes) return attrs;
|
||||
for (const attr of el.attributes) attrs[attr.name] = attr.value;
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function attrsText(el) {
|
||||
return Object.entries(attrsOf(el)).map(([key, value]) => `${key}=${value}`).join(' ');
|
||||
}
|
||||
|
||||
function isVisible(el) {
|
||||
if (!el || !el.getBoundingClientRect) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function walkRoots(root, rows, seen) {
|
||||
if (!root || seen.has(root)) return;
|
||||
seen.add(root);
|
||||
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||
let node = walker.currentNode;
|
||||
while (node) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.tagName) rows.push(node);
|
||||
if (node.shadowRoot) walkRoots(node.shadowRoot, rows, seen);
|
||||
node = walker.nextNode();
|
||||
}
|
||||
}
|
||||
|
||||
function allElements() {
|
||||
const rows = [];
|
||||
walkRoots(document, rows, new Set());
|
||||
return rows;
|
||||
}
|
||||
|
||||
function clickableFor(el) {
|
||||
if (el && el.querySelector) {
|
||||
const preferred = el.querySelector(
|
||||
'casino-button[data-actionname="expand"], [data-actionname="expand"], casino-button[aria-label="chevron-down"], [aria-label="chevron-down"]'
|
||||
);
|
||||
if (preferred) return preferred;
|
||||
}
|
||||
|
||||
let node = el;
|
||||
for (let depth = 0; node && depth < 7; depth += 1, node = node.parentElement) {
|
||||
if (node.querySelector) {
|
||||
const preferred = node.querySelector(
|
||||
'casino-button[data-actionname="expand"], [data-actionname="expand"], casino-button[aria-label="chevron-down"], [aria-label="chevron-down"]'
|
||||
);
|
||||
if (preferred) return preferred;
|
||||
}
|
||||
const tag = node.tagName.toLowerCase();
|
||||
const role = node.getAttribute('role');
|
||||
if (tag === 'button' || tag === 'a' || tag === 'kat-button' || role === 'button' || node.onclick) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function scoreEntry(el) {
|
||||
const text = textOf(el);
|
||||
const haystack = `${el.tagName} ${attrsText(el)} ${text}`;
|
||||
let score = 0;
|
||||
if (/推荐报价百分比|推荐报价.*百分比|featured offer percentage|featured offer.*%|featured offer/i.test(haystack)) {
|
||||
score += 120;
|
||||
}
|
||||
if (/buy box|购物车|报价/i.test(haystack)) score += 20;
|
||||
if (/%/.test(haystack)) score += 15;
|
||||
if (/button|link|href|click|card|metric/i.test(haystack)) score += 10;
|
||||
if (isVisible(el)) score += 10;
|
||||
if (text.length > 800) score -= 30;
|
||||
return { score, text, haystack };
|
||||
}
|
||||
|
||||
const ranked = allElements()
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ el, index, ...scoreEntry(el) }))
|
||||
.filter((row) => row.score > 0)
|
||||
.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
return a.text.length - b.text.length;
|
||||
});
|
||||
|
||||
if (!ranked.length) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'recommended offer percentage entry not found',
|
||||
url: window.location.href,
|
||||
title: document.title,
|
||||
candidates: allElements()
|
||||
.filter((el) => isVisible(el))
|
||||
.map((el, index) => ({ index, tag: el.tagName.toLowerCase(), text: textOf(el), attrs: attrsOf(el) }))
|
||||
.filter((row) => /推荐报价|featured offer|buy box|百分比|percentage/i.test(`${row.tag} ${row.text} ${Object.values(row.attrs).join(' ')}`))
|
||||
.slice(0, 80),
|
||||
};
|
||||
}
|
||||
|
||||
const selected = ranked[0];
|
||||
const clickable = clickableFor(selected.el);
|
||||
clickable.setAttribute('data-crawler-recommended-offer-entry', 'true');
|
||||
clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
|
||||
['pointerdown', 'mousedown', 'mouseup', 'click'].forEach((eventName) => {
|
||||
clickable.dispatchEvent(new MouseEvent(eventName, { bubbles: true, cancelable: true, composed: true }));
|
||||
});
|
||||
if (typeof clickable.click === 'function') clickable.click();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
selected: {
|
||||
index: selected.index,
|
||||
score: selected.score,
|
||||
text: selected.text,
|
||||
attrs: attrsText(selected.el),
|
||||
clickableTag: clickable.tagName.toLowerCase(),
|
||||
clickableText: textOf(clickable),
|
||||
outerHTML: (selected.el.outerHTML || '').slice(0, 2000),
|
||||
},
|
||||
candidates: ranked.slice(0, 12).map((row) => ({
|
||||
index: row.index,
|
||||
score: row.score,
|
||||
text: row.text,
|
||||
attrs: attrsText(row.el),
|
||||
outerHTML: (row.el.outerHTML || '').slice(0, 1000),
|
||||
})),
|
||||
};
|
||||
279
app/amazon/scripts/product/recommended_offer_percentage_rows.js
Normal file
279
app/amazon/scripts/product/recommended_offer_percentage_rows.js
Normal file
@@ -0,0 +1,279 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
scoped
|
||||
.filter((el) => isVisible(el))
|
||||
.forEach((el, index) => {
|
||||
const row = rowOf(el, index);
|
||||
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;
|
||||
if (row.raw_text.length > 800 && !hasDayLabels && !hasCountries) return;
|
||||
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),
|
||||
})),
|
||||
};
|
||||
Reference in New Issue
Block a user